diff --git a/roles/mediaimport/defaults/main.yml b/roles/mediaimport/defaults/main.yml index ee7c14b755c8520fb96c6092f07304c11af9e15f..6e90ebc7e208eaa66fa5e3e63fadedc352d99d9a 100644 --- a/roles/mediaimport/defaults/main.yml +++ b/roles/mediaimport/defaults/main.yml @@ -5,31 +5,11 @@ mediaimport_users: passwd: "{{ envsetup_mediaimport_password | d() }}" mediaimport_packages: - - clamav - - mysecureshell - openssh-server - - openssl - - pure-ftpd - - python3-unidecode + - mysecureshell # unreleased version that includes CallbackUpload, packaged by UbiCast + - pure-ftpd # must be installed before ubicast-mediaimport so that config can be tweaked by the postinst + - ssl-cert # for optionnal FTPS support (the mediaimport postinst will will use the snakeoil certificate for pure-ftpd) - ubicast-mediaimport - # required by ansible tasks - - python3-openssl - -mediaimport_pureftpd_config: - - key: AllowDotFiles - value: "no" - - key: CallUploadScript - value: "yes" - - key: ChrootEveryone - value: "yes" - - key: DontResolve - value: "yes" - - key: PAMAuthentication - value: "yes" - - key: TLS - value: "1" - -mediaimport_virus_scan_on_upload: false mediaimport_ms_api_key: "{{ envsetup_ms_api_key | d() }}" mediaimport_ms_server_name: "{{ envsetup_ms_server_name | d() }}" diff --git a/roles/mediaimport/files/mediaimport b/roles/mediaimport/files/mediaimport deleted file mode 100644 index 3294539e8c7c441ecca84e81478fdbbc02e7e7da..0000000000000000000000000000000000000000 --- a/roles/mediaimport/files/mediaimport +++ /dev/null @@ -1,6 +0,0 @@ -# purge mediaimport files that are older than 60 days -0 23 * * * root /usr/bin/find /home/ftp/storage/incoming/ -type f -mtime +60 -delete - -# purge empty folders -0 23 * * * root /usr/bin/find /home/ftp/storage -type d -empty -name thumbnails -delete -0 23 * * * root /usr/bin/find /home/ftp/storage -type d -empty -name "*20*-*" -delete diff --git a/roles/mediaimport/files/mediaimport.py b/roles/mediaimport/files/mediaimport.py deleted file mode 100644 index 0cb2b47a7db80ddc10a4f9b40c7ee0eab35cc445..0000000000000000000000000000000000000000 --- a/roles/mediaimport/files/mediaimport.py +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import crypt -import shutil -import subprocess - -BASE_DIR = "/home/ftp/storage" -INCOMING_DIR = BASE_DIR + "/incoming" -WATCH_DIR = BASE_DIR + "/watchfolder" - - -def main(): - commands = MediaImport() - - parser = argparse.ArgumentParser(prog="mediaimport", description=commands.__doc__) - subparsers = parser.add_subparsers(title="available commands", dest="command") - subparsers.required = True - - # add command and arguments - parser_add = subparsers.add_parser("add", help=commands.add_user.__doc__) - parser_add.add_argument( - "-u", - "--user", - help="username", - action="store", - type=commands._new_user, - required=True, - ) - parser_add.add_argument( - "-p", "--passwd", help="password", action="store", type=str, required=True - ) - parser_add.add_argument( - "-y", "--yes", action="store_true", help="do not prompt for confirmation" - ) - parser_add.set_defaults(func=commands.add_user) - - # delete command and arguments - parser_del = subparsers.add_parser("delete", help=commands.del_user.__doc__) - parser_del.add_argument( - "-u", - "--user", - help="username", - action="store", - type=commands._user, - required=True, - ) - parser_del.add_argument( - "-y", "--yes", action="store_true", help="do not prompt for confirmation" - ) - parser_del.set_defaults(func=commands.del_user) - - # list command and arguments - parser_list = subparsers.add_parser("list", help=commands.list_users.__doc__) - parser_list.set_defaults(func=commands.list_users) - - # parse and run - args = parser.parse_args() - args.func(args) - - -class MediaImport: - """Manage mediaimport users.""" - - def __init__(self): - self.users = self._get_users() - - def _get_users(self) -> list: - """Get mysecureshell users list.""" - - with open("/etc/passwd") as fh: - passwd = fh.readlines() - - return sorted( - [ - u.split(":")[0] - for u in passwd - if u.split(":")[-1].strip() == "/usr/bin/mysecureshell" - ] - ) - - def _confirm(self, message: str = None): - """Ask for confirmation.""" - - if message: - print(message) - choice = input("Do you want to continue [y/N]? ").lower() - - if choice not in ["y", "yes"]: - print("Exit.") - exit(0) - - def _new_user(self, value: str) -> str: - """Check that username does not exist.""" - - if value in self.users: - raise argparse.ArgumentTypeError(f"{value} already exists") - - return value - - def _user(self, value: str) -> str: - """Check that username exists.""" - - if value not in self.users: - raise argparse.ArgumentTypeError(f"{value} does not exists") - - return value - - def add_user(self, args: argparse.Namespace): - """add an user""" - - username = args.user - password = args.passwd - - if not args.yes: - self._confirm(f"MediaImport user '{username}' will be created.") - - # create user - subprocess.Popen( - [ - "useradd", - "-b", - INCOMING_DIR, - "-m", - "-p", - crypt.crypt(password), - "-s", - "/usr/bin/mysecureshell", - "-U", - username, - ], - stdout=subprocess.DEVNULL, - ) - print(f"User {username} created, adjust /etc/mediaserver/mediaimport.json and restart the service:\nsystemctl restart mediaimport") - - def del_user(self, args: argparse.Namespace): - """delete an user""" - - username = args.user - paths = [f"{INCOMING_DIR}/{username}", f"{WATCH_DIR}/{username}"] - - if not args.yes: - self._confirm(f"MediaImport user '{username}' data will be deleted.") - - # remove user - subprocess.Popen( - ["userdel", "-f", "-r", username], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - - # remove user's folders - for path in paths: - shutil.rmtree(path, ignore_errors=True) - print(f"User {username} deleted, adjust /etc/mediaserver/mediaimport.json and restart the service:\nsystemctl restart mediaimport") - - def list_users(self, args: argparse.Namespace): - """list existing users""" - - if len(self.users): - print("\n".join(self.users)) - - -if __name__ == "__main__": - main() diff --git a/roles/mediaimport/files/on-upload b/roles/mediaimport/files/on-upload deleted file mode 100755 index 770286dd32b194ba8c3e4c538fec4faced985db2..0000000000000000000000000000000000000000 Binary files a/roles/mediaimport/files/on-upload and /dev/null differ diff --git a/roles/mediaimport/files/on-upload.go b/roles/mediaimport/files/on-upload.go deleted file mode 100644 index b4d27ecf5724da51c3af4c2fe1dc470661ebe518..0000000000000000000000000000000000000000 --- a/roles/mediaimport/files/on-upload.go +++ /dev/null @@ -1,141 +0,0 @@ -package main - -import ( - "log" - "os" - "os/exec" - "path/filepath" - "strings" - "unicode" - - "github.com/jessevdk/go-flags" - "golang.org/x/text/transform" - "golang.org/x/text/unicode/norm" -) - -const ( - baseDir = "/home/ftp/storage" - incomingDir = baseDir + "/incoming" - watchDir = baseDir + "/watchfolder" - quarantineDir = baseDir + "/quarantine" -) - -func setPermissions(path string) error { - stat, err := os.Stat(path) - if err != nil { - return err - } - - switch mode := stat.Mode(); { - case mode.IsDir(): - if err := os.Chmod(path, 0755); err != nil { - return err - } - case mode.IsRegular(): - if err := os.Chmod(path, 0644); err != nil { - return err - } - } - - return nil -} - -func cleanName(filename string) string { - // normalize - isMn := func(r rune) bool { - return unicode.Is(unicode.Mn, r) - } - t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) - cleanedName, _, _ := transform.String(t, filename) - - // replace non allowed characters - allowedChars := strings.Split("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.", "") - for _, filenameChar := range strings.Split(cleanedName, "") { - flagged := false - for _, allowedChar := range allowedChars { - if filenameChar == allowedChar { - flagged = true - } - } - // if not in allowed list replace by underscore - if !flagged { - cleanedName = strings.Replace(cleanedName, filenameChar, "_", 1) - } - } - - return cleanedName -} - -func virusScan(path string) error { - // will move file into quarantine directory if infected - cmd := exec.Command( - "/usr/bin/clamscan", - "--quiet", - "--infected", - "--recursive", - "--move="+quarantineDir, - "--max-scantime=600000", // 10 minutes - "--max-filesize=4000M", - "--max-scansize=4000M", - "--max-files=200", - "--max-recursion=6", - "--max-dir-recursion=6", - path, - ) - err := cmd.Run() - - return err -} - -func main() { - var opts struct { - Scan bool `short:"s" long:"scan-virus" description:"Scan file for virus"` - Args struct { - SrcPaths []string `positional-arg-name:"path" required:"yes" description:"Paths of uploaded files"` - } `positional-args:"yes"` - } - - if _, err := flags.Parse(&opts); err != nil { - os.Exit(1) - } - - for _, srcPath := range opts.Args.SrcPaths { - // check that file is into incoming folder - if !strings.HasPrefix(srcPath, baseDir) { - log.Fatalln("file not in base dir (" + baseDir + "): " + srcPath) - } - - // ensure permissions are correct - if err := setPermissions(srcPath); err != nil { - log.Fatalln(err) - } - - // scan for virus if enabled - if opts.Scan { - if err := os.MkdirAll(quarantineDir, 0775); err != nil { - log.Fatalln(err) - } - if err := virusScan(srcPath); err != nil { - log.Fatalln(err) - } - } - - // cleanup and set destination path - srcDir, srcFile := filepath.Split(srcPath) - dstFile := cleanName(srcFile) - dstDir := strings.ReplaceAll(srcDir, incomingDir, watchDir) - dstPath := dstDir + dstFile - - // create destination directory - if err := os.MkdirAll(dstDir, 0775); err != nil { - log.Fatalln(err) - } - - // move file into watchfolder - if err := os.Rename(srcPath, dstPath); err != nil { - log.Fatalln(err) - } - - log.Println(srcPath + " moved to " + dstPath) - } -} diff --git a/roles/mediaimport/handlers/main.yml b/roles/mediaimport/handlers/main.yml index b87528a150512ae485d9f60c64083572a5953062..60e664ba04be31637362603611e5959415a35aff 100644 --- a/roles/mediaimport/handlers/main.yml +++ b/roles/mediaimport/handlers/main.yml @@ -29,8 +29,4 @@ name: fail2ban state: restarted -- name: sftp-verif - ansible.builtin.command: - cmd: timeout 30 sftp-verif - ... diff --git a/roles/mediaimport/tasks/main.yml b/roles/mediaimport/tasks/main.yml index 528c843d876390dd280672549eb5cb0929dda011..9b79be8bdb06d1bdb5cb6da08d00730f97a88225 100644 --- a/roles/mediaimport/tasks/main.yml +++ b/roles/mediaimport/tasks/main.yml @@ -8,38 +8,17 @@ ## USERS -- name: create ftp folders - loop: - - /home/ftp/storage/incoming - - /home/ftp/storage/watchfolder - ansible.builtin.file: - path: "{{ item }}" - state: directory - mode: '755' - -- name: deploy users management script - ansible.builtin.copy: - src: files/mediaimport.py - dest: /usr/local/bin/mediaimport - mode: '755' - - name: create users loop: "{{ mediaimport_users }}" when: - item.name | d(false) - item.passwd | d(false) no_log: true - ansible.builtin.command: mediaimport add --yes --user {{ item.name }} --passwd {{ item.passwd }} + ansible.builtin.command: /usr/bin/mediaimportctl.py add --yes --user {{ item.name }} --passwd {{ item.passwd }} args: creates: /home/ftp/storage/incoming/{{ item.name }} -- name: deploy on-upload script with setuid - ansible.builtin.copy: - src: files/on-upload - dest: /home/ftp/on-upload - mode: 04755 - -## MYSECURESHELL +## MEDIAIMPORT - name: enable password login for ssh notify: restart sshd @@ -48,97 +27,6 @@ regexp: "^PasswordAuthentication no" replace: "#PasswordAuthentication yes" -- name: set the setuid on mysecureshell - ansible.builtin.file: - path: /usr/bin/mysecureshell - mode: 04755 - -- name: configure mysecureshell - notify: - - restart mysecureshell - - sftp-verif - ansible.builtin.template: - src: sftp_config.j2 - dest: /etc/ssh/sftp_config - mode: '644' - -## PURE-FTPD - -- name: set pure-ftpd default config - notify: restart pure-ftpd - ansible.builtin.copy: - dest: /etc/default/pure-ftpd-common - mode: '644' - content: | - STANDALONE_OR_INETD=standalone - VIRTUALCHROOT=false - UPLOADSCRIPT="/home/ftp/on-upload{% if mediaimport_virus_scan_on_upload %} --scan-virus{% endif %}" - UPLOADUID=0 - UPLOADGID=0 - -- name: configure pure-ftpd - notify: restart pure-ftpd - loop: "{{ mediaimport_pureftpd_config }}" - ansible.builtin.copy: - dest: /etc/pure-ftpd/conf/{{ item.key }} - content: "{{ item.value }}" - mode: '644' - -## PURE-FTPD CERTIFICATES - -- name: create certificate directory - ansible.builtin.file: - path: /etc/ssl/{{ ansible_fqdn }} - state: directory - mode: '755' - -- name: generate an private key - register: mediaimport_privkey - community.crypto.openssl_privatekey: - path: /etc/ssl/{{ ansible_fqdn }}/key.pem - mode: '600' - -- name: generate an csr - when: mediaimport_privkey is changed # noqa no-handler - register: mediaimport_csr - openssl_csr: - path: /etc/ssl/{{ ansible_fqdn }}/csr.pem - privatekey_path: /etc/ssl/{{ ansible_fqdn }}/key.pem - common_name: "{{ ansible_fqdn }}" - mode: '600' - -- name: generate a self-signed certificate - when: mediaimport_csr is changed # noqa no-handler - register: mediaimport_cert - openssl_certificate: - path: /etc/ssl/{{ ansible_fqdn }}/cert.pem - privatekey_path: /etc/ssl/{{ ansible_fqdn }}/key.pem - csr_path: /etc/ssl/{{ ansible_fqdn }}/csr.pem - provider: selfsigned - mode: '600' - -- name: concatenate key and certificate - when: mediaimport_cert is changed # noqa no-handler - notify: restart pure-ftpd - ansible.builtin.shell: > - cat /etc/ssl/{{ ansible_fqdn }}/key.pem /etc/ssl/{{ ansible_fqdn }}/cert.pem > /etc/ssl/private/pure-ftpd.pem; - chmod 600 /etc/ssl/private/pure-ftpd.pem; - -- name: generate dhparams - notify: restart pure-ftpd - openssl_dhparam: - path: /etc/ssl/private/pure-ftpd-dhparams.pem - size: 1024 - mode: '600' - -## MEDIAIMPORT - -- name: setup cron job - ansible.builtin.copy: - src: files/mediaimport - dest: /etc/cron.d/mediaimport - mode: '644' - - name: configure mediaimport when: - mediaimport_ms_api_key | d(false) @@ -155,6 +43,11 @@ name: mediaimport enabled: true +- name: enable mediaimport-cleanup timer + ansible.builtin.systemd: + name: mediaimport-cleanup.timer + enabled: true + # FAIL2BAN - name: deploy fail2ban jail diff --git a/roles/mediaimport/templates/sftp_config.j2 b/roles/mediaimport/templates/sftp_config.j2 deleted file mode 100644 index b33a786d8fd2b3029536244801a3a1c076859b2d..0000000000000000000000000000000000000000 --- a/roles/mediaimport/templates/sftp_config.j2 +++ /dev/null @@ -1,26 +0,0 @@ -## MySecureShell Configuration File -# To get more informations on all possible options, please look at the doc: -# http://mysecureshell.readthedocs.org - -#Default rules for everybody -<Default> - GlobalDownload 50k - GlobalUpload 0 - Download 5k - Upload 0 - StayAtHome true - VirtualChroot true - LimitConnection 100 - LimitConnectionByUser 2 - LimitConnectionByIP 10 - Home /home/ftp/storage/incoming/$USER - CallbackUpload "/home/ftp/on-upload{% if mediaimport_virus_scan_on_upload %} --scan-virus{% endif %} /home/ftp/storage/incoming/$USER$LAST_FILE_PATH" - IdleTimeOut 5m - ResolveIP false - HideNoAccess true - DefaultRights 0640 0750 - ShowLinksAsLinks false - LogFile /var/log/sftp-server.log - LogLevel 6 - LogSyslog true -</Default>