def mount(self, passwd=None): """ Mount partition. :param str passwd: If disk is encrypted, use this passphrase to unlock """ if self.mountpoint and os.path.ismount(self.mountpoint): raise errors.InvalidConfigError("Virtual disk already mounted") signals.emit("filesystems", "pre_mount", self) if not os.path.isdir(os.path.join("/media", self.id)): os.makedirs(os.path.join("/media", self.id)) mount_point = self.mountpoint or os.path.join("/media", self.id) luks_point = os.path.join("/dev/mapper", self.id) # Find a free loopback device and mount loop = losetup.find_unused_loop_device() loop.mount(str(self.path), offset=1048576) if self.crypt and passwd: # If it's an encrypted virtual disk, decrypt first then mount s = crypto.luks_open(loop.device, self.id, passwd) if s != 0: loop.unmount() excmsg = "Failed to decrypt {0} with errno {1}" raise errors.OperationFailedError( excmsg.format(self.id, str(s))) s = libc.mount(ctypes.c_char_p(b(luks_point)), ctypes.c_char_p(b(mount_point)), ctypes.c_char_p(b(self.fstype)), 0, ctypes.c_char_p(b"")) if s == -1: crypto.luks_close(self.id) loop.unmount() excmsg = "Failed to mount {0}: {1}" raise errors.OperationFailedError( excmsg.format(self.id, os.strerror(ctypes.get_errno()))) elif self.crypt and not passwd: excstr = "Must provide password to decrypt encrypted container" raise errors.InvalidConfigError(excstr) else: s = libc.mount(ctypes.c_char_p(b(loop.device)), ctypes.c_char_p(b(mount_point)), ctypes.c_char_p(b(self.fstype)), 0, ctypes.c_char_p(b"")) if s == -1: loop.unmount() excstr = "Failed to mount {0}: {1}" raise errors.OperationFailedError( excmsg.format(self.id, os.strerror(ctypes.get_errno()))) signals.emit("filesystems", "post_mount", self) self.mountpoint = mount_point
def create(id, data=True, nthread=NotificationThread()): """ Convenience function to create a backup. :param str id: ID of associated app (or website) to backup :param bool data: Backup app data also? :returns: Backup info :rtype: Backup """ controller = None if id == "arkOS": controller = arkOSBackupCfg("arkOS", "setting", version=arkos_version) return controller.backup() app = applications.get(id) if app and app.type != "website" and hasattr(app, "_backup"): controller = app._backup(app.id, app.icon, version=app.version) else: sites = websites.get() for x in sites: if x.id == id: controller = x.backup break if not controller: raise errors.InvalidConfigError("No backup controller found") return controller.backup(data=data, nthread=nthread)
def execute(self, cmd, commit=False, strf=True): self.manager.connect() conns.MariaDB.query('USE {0}'.format(self.id)) cur = conns.MariaDB.cursor() parse, s = [], "" for l in cmd.split('\n'): if not l.split() or re.match('--', l): continue elif not re.search('[^-;]+;', l): s = s + l elif re.search('^\s*USE\s*', l, re.IGNORECASE): raise errors.InvalidConfigError( 'Cannot switch databases during execution') else: s = s + l cur.execute(s) for x in cur.fetchall(): parse.append(x) s = "" if commit: conns.MariaDB.commit() if strf: status = "" for line in parse: line = [str(x) for x in line] status += ', '.join(line) + '\n' return status else: return parse
def restore(backup, data=True, nthread=NotificationThread()): """ Convenience function to restore a backup. :param Backup backup: Backup to restore :param bool data: Restore included data files as well? :returns: Backup info :rtype: Backup """ controller = None if backup["type"] == "site": sites = websites.get() for x in sites: if x.id == backup["pid"]: controller = x.backup break else: app = applications.get(backup["site_type"]) controller = app._backup(backup["pid"], backup["icon"], True) else: app = applications.get(backup["pid"]) controller = app._backup() if not controller: raise errors.InvalidConfigError("No backup controller found") b = controller.restore(backup, data, nthread) return b
def add_share(self): """Add a share.""" config = configparser.ConfigParser() config.read(["/etc/samba/smb.conf"]) if config.has_section(self.id): raise errors.InvalidConfigError( "Share already present with this name" ) if not os.path.exists(self.path): os.makedirs(self.path) config.set("global", "security", "user") config.set("global", "map to guest", "bad user") config.add_section(self.id) config.set(self.id, "comment", self.comment) config.set(self.id, "path", self.path) config.set(self.id, "browseable", "yes") config.set(self.id, "public", "yes" if not self.valid_users else "no") config.set( self.id, "guest ok", "yes" if not self.valid_users else "no" ) config.set(self.id, "read only", "yes" if self.readonly else "no") config.set(self.id, "writable", "no" if self.readonly else "yes") if self.valid_users: config.set(self.id, "valid users", " ".join(self.valid_users)) with open("/etc/samba/smb.conf", "w") as f: config.write(f) svc = services.get("smbd") if svc: svc.restart()
def remove(self): """Delete domain.""" if self.name in [x.domain for x in users.get()]: emsg = "A user is still using this domain" raise errors.InvalidConfigError(emsg) signals.emit("domains", "pre_remove", self) conns.LDAP.delete_s(self.ldap_id) signals.emit("domains", "post_remove", self)
def mount(self, passwd=None): """ Mount partition. :param str passwd: If disk is encrypted, use this passphrase to unlock """ if self.mountpoint and os.path.ismount(self.mountpoint): raise errors.InvalidConfigError("Disk partition already mounted") elif self.fstype == "Unknown": emsg = "Cannot mount a partition of unknown type" raise errors.InvalidConfigError(emsg) signals.emit("filesystems", "pre_mount", self) mount_point = self.mountpoint or os.path.join("/media", self.id) luks_point = os.path.join("/dev/mapper", self.id) if not os.path.isdir(mount_point): os.makedirs(mount_point) if self.crypt and passwd: # Decrypt the disk first if it's an encrypted disk s = crypto.luks_open(self.path, self.id, passwd) if s != 0: excmsg = "Failed to decrypt {0} with errno {1}" excmsg = excmsg.format(self.id, str(s)) raise errors.OperationFailedError(excmsg) s = libc.mount(ctypes.c_char_p(b(luks_point)), ctypes.c_char_p(b(mount_point)), ctypes.c_char_p(b(self.fstype)), 0, ctypes.c_char_p(b"")) if s == -1: crypto.luks_close(self.id) excmsg = "Failed to mount {0}: {1}" raise errors.OperationFailedError( excmsg.format(self.id, os.strerror(ctypes.get_errno()))) elif self.crypt and not passwd: emsg = "Must provide password to decrypt encrypted disk" raise errors.InvalidConfigError(emsg) else: s = libc.mount(ctypes.c_char_p(b(self.path)), ctypes.c_char_p(b(mount_point)), ctypes.c_char_p(b(self.fstype)), 0, ctypes.c_char_p(b"")) if s == -1: excmsg = "Failed to mount {0}: {1}" raise errors.OperationFailedError( excmsg.format(self.id, os.strerror(ctypes.get_errno()))) signals.emit("filesystems", "post_mount", self) self.mountpoint = mount_point
def _update(self, nthread): nthread.title = "Updating website" if self.version == self.app.version.rsplit("-", 1)[0]: raise errors.InvalidConfigError( "Website is already at the latest version") elif self.version in [None, "None"]: raise errors.InvalidConfigError( "Updates not supported for this website type") # Classify the source package type if not self.app.download_url: ending = "" elif self.app.download_url.endswith(".tar.gz"): ending = ".tar.gz" elif self.app.download_url.endswith(".tgz"): ending = ".tgz" elif self.app.download_url.endswith(".tar.bz2"): ending = ".tar.bz2" elif self.app.download_url.endswith(".zip"): ending = ".zip" elif self.app.download_url.endswith(".git"): ending = ".git" else: raise errors.InvalidConfigError( "Invalid source archive format in {0}".format(self.app.id)) # Download and extract the source package msg = "Downloading website source..." nthread.update(Notification("info", "Webs", msg)) if self.app.download_url and ending == ".git": pkg_path = self.download_url elif self.app.download_url: pkg_path = os.path.join("/tmp", self.id + ending) download(self.app.download_url, file=pkg_path, crit=True) # Call the site type's update hook msg = "Updating website..." nthread.update(Notification("info", "Webs", msg)) self.update_site(pkg_path, self.app.version) # Update stored version and remove temp source archive msg = "{0} updated successfully".format(self.id) nthread.complete(Notification("success", "Webs", msg)) self.version = self.app.version.rsplit("-", 1)[0] if pkg_path: os.unlink(pkg_path)
def validate(self, id='', user='', passwd=''): if id and re.search('\.|-|`|\\\\|\/|^test$|[ ]', id): raise errors.InvalidConfigError( 'Database name must not contain spaces, dots, dashes or other ' 'special characters') elif id and len(id) > 16: raise errors.InvalidConfigError( 'Database name must be shorter than 16 characters') if user and re.search('\.|-|`|\\\\|\/|^test$|[ ]', user): raise errors.InvalidConfigError( 'Database username must not contain spaces, dots, dashes or ' 'other special characters') elif user and len(user) > 16: raise errors.InvalidConfigError( 'Database username must be shorter than 16 characters') if passwd and len(passwd) < 8: raise errors.InvalidConfigError( 'Database password must be longer than 8 characters') if id: for x in self.get_dbs(): if x.id == id: raise errors.InvalidConfigError( 'You already have a database named {0} - please ' 'remove that one or choose a new name!'.format(id)) if user: for x in self.get_users(): if x.id == user: raise errors.InvalidConfigError( 'You already have a database user named {0} - please ' 'remove that one or choose a new name!'.format(user)) return True
def uninstall(self, force=False, nthread=NotificationThread()): """ Uninstall the arkOS application from the system. :param bool force: Uninstall the app even if others depend on it? :param NotificationThread nthread: notification thread to use """ signals.emit("apps", "pre_remove", self) msg = "Uninstalling application..." nthread.update(Notification("info", "Apps", msg)) exclude = ["openssl", "openssh", "nginx", "python2", "git", "nodejs", "npm"] # Make sure this app can be successfully removed, and if so also remove # any system-level packages that *only* this app requires for x in get(installed=True): for item in x.dependencies: if item["type"] == "app" and item["package"] == self.id \ and not force: exc_str = "{0} depends on this application" raise errors.InvalidConfigError(exc_str.format(x.name)) elif item["type"] == "system": exclude.append(item["package"]) # Stop any running services associated with this app for item in self.dependencies: if item["type"] == "system" and not item["package"] in exclude: if item.get("daemon"): try: services.get(item["daemon"]).stop() services.get(item["daemon"]).disable() except: pass pacman.remove([item["package"]], purge=config.get("apps", "purge")) # Remove the app's directory and cleanup the app object shutil.rmtree(os.path.join(config.get("apps", "app_dir"), self.id)) self.loadable = False self.installed = False # Regenerate the firewall and re-block the abandoned ports regen_fw = False for x in self.services: if x["ports"]: regen_fw = True if regen_fw: tracked_services.deregister(self.id) ports = [] for s in self.services: if s.get("default_policy", 0) and s["ports"]: ports.append(s["ports"]) if ports and config.get("general", "enable_upnp"): tracked_services.close_all_upnp(ports) smsg = "{0} uninstalled successfully".format(self.name) nthread.complete(Notification("success", "Apps", smsg)) signals.emit("apps", "post_remove", self)
def add_db(self): if re.search('\.|-|`|\\\\|\/|[ ]', self.id): raise errors.InvalidConfigError( 'Name must not contain spaces, dots, dashes or other ' 'special characters') self.manager.chkpath() status = shell("sqlite3 {0} \"ATTACH '{1}' AS {2};\"".format( self.path, self.path, self.id)) if status["code"] >= 1: raise errors.OperationFailedError(status["stderr"])
def add(self): """Add the domain to LDAP.""" try: ldif = conns.LDAP.search_s(self.ldap_id, ldap.SCOPE_SUBTREE, "(objectClass=*)", None) emsg = "This domain is already present here" raise errors.InvalidConfigError(emsg) except ldap.NO_SUCH_OBJECT: pass ldif = {"virtualdomain": [b(self.name)], "objectClass": [b"mailDomain", b"top"]} signals.emit("domains", "pre_add", self) conns.LDAP.add_s(self.ldap_id, ldap.modlist.addModlist(ldif)) signals.emit("domains", "post_add", self)
def update(self): """Update a group object in LDAP. Change params on the object first.""" try: ldif = conns.LDAP.search_s(self.ldap_id, ldap.SCOPE_SUBTREE, "(objectClass=*)", None) except ldap.NO_SUCH_OBJECT: raise errors.InvalidConfigError("This group does not exist") ldif = ldap.modlist.modifyModlist( ldif[0][1], {"memberUid": [b(u) for u in self.users]}, ignore_oldexistent=1) signals.emit("groups", "pre_update", self) conns.LDAP.modify_s(self.ldap_id, ldif) signals.emit("groups", "post_update", self)
def create(self, mount=False, will_crypt=False, nthread=NotificationThread()): """ Create virtual disk image. :param bool mount: Mount after creation? :param bool will_crypt: Will this disk be encrypted later? :param NotificationThread nthread: notification thread to use """ nthread.title = "Creating virtual disk" vdisk_dir = config.get("filesystems", "vdisk_dir") if not os.path.exists(vdisk_dir): os.mkdir(vdisk_dir) self.path = str(os.path.join(vdisk_dir, self.id + ".img")) if os.path.exists(self.path): raise errors.InvalidConfigError("This virtual disk already exists") # Create an empty file matching disk size signals.emit("filesystems", "pre_add", self) msg = "Creating virtual disk..." nthread.update(Notification("info", "Filesystems", msg)) with open(self.path, "wb") as f: written = 0 with open("/dev/zero", "rb") as zero: while self.size > written: written += 1024 f.write(zero.read(1024)) if not will_crypt: # Get a free loopback device and mount loop = losetup.find_unused_loop_device() loop.mount(str(self.path), offset=1048576) # Make a filesystem msg = "Writing filesystem..." nthread.update(Notification("info", "Filesystems", msg)) s = shell("mkfs.ext4 {0}".format(loop.device)) if s["code"] != 0: excmsg = "Failed to format loop device: {0}" raise errors.OperationFailedError(excmsg.format(s["stderr"])) loop.unmount() msg = "Virtual disk created successfully" nthread.complete(Notification("success", "Filesystems", msg)) signals.emit("filesystems", "post_add", self) if mount: self.mount()
def post(self): data = request.get_json()["user"] try: u = users.User(name=data["name"], first_name=data["first_name"], last_name=data["last_name"], domain=data["domain"], admin=data["admin"], sudo=data["sudo"]) u.add(data["passwd"]) except KeyError as e: raise errors.InvalidConfigError(str(e) if e else "Value not found") except errors.InvalidConfigError as e: return jsonify(errors={"msg": str(e)}), 422 return jsonify(user=u.serialized)
def add_share(self): """Add a share.""" with open("/etc/exports", "r") as f: data = f.readlines() if any([shlex.split(x)[0] == self.path for x in data]): raise errors.InvalidConfigError( "Share already present with this path" ) if not os.path.exists(self.path): os.makedirs(self.path) sstr = '"' + self.path + '" ' sstr += '*(' + ("ro" if self.readonly else "rw") + ',sync)' with open("/etc/exports", "w") as f: f.writelines(data) f.write(sstr + '\n') shell("exportfs -arv")
def enable(self): """Enable mounting of this partition on boot.""" if self.crypt: raise errors.InvalidConfigError( "Cannot enable encrypted virutal disks") f = FstabEntry() f.src = self.path f.dst = os.path.join("/media", self.id) f.uuid = get_partition_uuid_by_name(self.path) f.fs_type = "ext4" f.options = "defaults" f.dump_p = 0 f.fsck_p = 0 save_fstab_entry(f) if not os.path.exists(f.dst): os.makedirs(f.dst) self.enabled = True
def put(self, id): data = request.get_json()["user"] u = users.get(id) if not u: abort(404) try: u.first_name = data["first_name"] u.last_name = data["last_name"] u.domain = data["domain"] u.admin = data["admin"] u.sudo = data["sudo"] u.mail = [str(x) for x in data["mail_addresses"]] u.update(data.get("passwd")) except KeyError as e: raise errors.InvalidConfigError(str(e)) except errors.InvalidConfigError as e: return jsonify(errors={"msg": str(e)}), 422 return jsonify(user=u.serialized)
def __init__(self, level, comp, message, cls="notify", id=None, title=None): level = level.upper() if level not in self.LEVELS: raise errors.InvalidConfigError("Unrecognized log level specified") id = id or random_string(16) self.level = self.LEVELS[level] self.comp = comp self.message = message self.cls = cls self.id = id self.title = title self.message_id = id self.complete = True
def add(self, passwd): """ Add the user to LDAP. :param str passwd: user password to set """ try: ldif = conns.LDAP.search_s(self.ldap_id, ldap.SCOPE_BASE, "(objectClass=*)", None) msg = "A user named {0} already exists".format(self.name) raise errors.InvalidConfigError(msg) except ldap.NO_SUCH_OBJECT: pass # Create LDAP user with proper metadata ldif = { "objectClass": [b"mailAccount", b"inetOrgPerson", b"posixAccount"], "givenName": [b(self.first_name)], "sn": [b(self.last_name)] if self.last_name else [b"NONE"], "displayName": [b(self.full_name)], "cn": [b(self.full_name)], "uid": [b(self.name)], "mail": [b(self.name + "@" + self.domain)], "maildrop": [b(self.name)], "userPassword": [b(ldap_sha512_crypt.encrypt(passwd))], "gidNumber": [b"100"], "uidNumber": [b(str(self.uid))], "homeDirectory": [b("/home/" + self.name)], "loginShell": [b"/usr/bin/bash"] } ldif = ldap.modlist.addModlist(ldif) signals.emit("users", "pre_add", self) logger.debug("Roles", "Adding user: {0}".format(self.ldap_id)) conns.LDAP.add_s(self.ldap_id, ldif) modes = ["admin" if self.admin else "", "sudo" if self.sudo else ""] msg = "Setting user modes: {0}".format(", ".join(modes)) logger.debug("Roles", msg) self.update_adminsudo() self.update_samba(passwd) signals.emit("users", "post_add", {"user": self, "passwd": passwd})
def update(self, newpasswd=""): """ Update a user's object in LDAP. Change params on the object first. To change password, do so via the ``newpasswd`` param here. :param str newpasswd: new password to set """ try: ldif = conns.LDAP.search_s(self.ldap_id, ldap.SCOPE_SUBTREE, "(objectClass=*)", None) except ldap.NO_SUCH_OBJECT: raise errors.InvalidConfigError("Users", "This user does not exist") self.mail = list(set(self.mail)) for i, x in enumerate(self.mail): if not x.endswith(self.domain): self.mail[i] = x.split("@")[0] + "@" + self.domain ldif = ldif[0][1] attrs = { "givenName": [b(self.first_name)], "sn": [b(self.last_name)] if self.last_name else [b"NONE"], "displayName": [b(self.full_name)], "cn": [b(self.full_name)], "mail": [b(x) for x in self.mail] } if newpasswd: attrs["userPassword"] = [b(ldap_sha512_crypt.encrypt(newpasswd))] signals.emit("users", "pre_update", self) nldif = ldap.modlist.modifyModlist(ldif, attrs, ignore_oldexistent=1) conns.LDAP.modify_s(self.ldap_id, nldif) self.update_adminsudo() self.update_samba(newpasswd) signals.emit("users", "post_update", { "user": self, "passwd": newpasswd })
def add(self): """Add the group to LDAP.""" try: ldif = conns.LDAP.search_s(self.ldap_id, ldap.SCOPE_SUBTREE, "(objectClass=*)", None) emsg = "A group with this name already exists" raise errors.InvalidConfigError(emsg) except ldap.NO_SUCH_OBJECT: pass ldif = { "objectClass": [b"posixGroup", b"top"], "cn": [b(self.name)], "gidNumber": [b(str(self.gid))] } if self.users: ldif["memberUid"] = [b(u) for u in self.users] ldif = ldap.modlist.addModlist(ldif) signals.emit("groups", "pre_add", self) conns.LDAP.add_s(self.ldap_id, ldif) signals.emit("groups", "post_add", self)
def add_share(self): """Add a share.""" config = configparser.ConfigParser() config.read(["/etc/afp.conf"]) if config.has_section(self.id): raise errors.InvalidConfigError( "Share already present with this name") if not os.path.exists(self.path): os.makedirs(self.path) config.add_section(self.id) config.set(self.id, "path", self.path) config.set(self.id, "read only", "yes" if self.readonly else "no") if self.valid_users: config.set(self.id, "valid users", " ".join(self.valid_users)) config.set("Global", "ldap_server", "localhost") config.set("Global", "ldap_auth_method", "none") config.set("Global", "ldap_userbase", "ou=users,dc=arkos-servers,dc=org") with open("/etc/afp.conf", "w") as f: config.write(f) svc = services.get("netatalk") if svc and svc.state: svc.restart()
def _ssl_enable(self): # Get server-preferred ciphers if config.get("certificates", "ciphers"): ciphers = config.get("certificates", "ciphers") else: config.set("certificates", "ciphers", ciphers) config.save() block = nginx.loadf( os.path.join("/etc/nginx/sites-available/", self.id)) # If the site is on port 80, setup an HTTP redirect to new port 443 server = block.server listens = server.filter("Key", "listen") for listen in listens: httport = "80" sslport = "443" if listen.value.startswith("[::]"): # IPv6 httport = "[::]:80" sslport = "[::]:443" if listen.value == httport: listen.value = (sslport + " ssl http2") block.add( nginx.Server( nginx.Key("listen", httport), nginx.Key("server_name", self.domain), nginx.Location( "/", nginx.Key("return", "301 https://$host$request_uri")), nginx.Location("/.well-known/acme-challenge/", nginx.Key("root", self.path)))) for x in block.servers: if " ssl" in x.filter("Key", "listen")[0].value: server = x break else: listen.value = listen.value.split(" ssl")[0] + " ssl http2" # Clean up any pre-existing SSL directives that no longer apply to_remove = [x for x in server.keys if x.name.startswith("ssl_")] server.remove(*to_remove) # Add the necessary SSL directives to the serverblock and save server.add( nginx.Key("ssl_certificate", self.cert.cert_path), nginx.Key("ssl_certificate_key", self.cert.key_path), nginx.Key("ssl_protocols", "TLSv1 TLSv1.1 TLSv1.2"), nginx.Key("ssl_ciphers", ciphers), nginx.Key("ssl_session_timeout", "5m"), nginx.Key("ssl_prefer_server_ciphers", "on"), nginx.Key("ssl_dhparam", "/etc/arkos/ssl/dh_params.pem"), nginx.Key("ssl_session_cache", "shared:SSL:50m"), ) nginx.dumpf(block, os.path.join("/etc/nginx/sites-available/", self.id)) # Set the certificate name in the metadata file if not os.path.exists(os.path.join(self.path, ".arkos")): raise errors.InvalidConfigError("Could not find metadata file") meta = configparser.SafeConfigParser() meta.read(os.path.join(self.path, ".arkos")) meta.set("website", "ssl", self.cert.id) with open(os.path.join(self.path, ".arkos"), "w") as f: meta.write(f) # Call the website type's SSL enable hook self.enable_ssl(self.cert.cert_path, self.cert.key_path)
def _install(self, extra_vars, enable, nthread): nthread.title = "Installing website" msg = Notification("info", "Webs", "Preparing to install...") nthread.update(msg) # Make sure the chosen port is indeed open if not tracked_services.is_open_port(self.port, self.domain): cname = "({0})".format(self.app.id) raise errors.InvalidConfigError(cname, nthread)\ from tracked_services.PortConflictError(self.port, self.domain) # Set some metadata values specialmsg, dbpasswd = "", "" site_dir = config.get("websites", "site_dir") path = (self.path or os.path.join(site_dir, self.id)) self.path = path self.php = extra_vars.get("php") or self.php \ or self.app.uses_php or False self.version = self.app.version.rsplit("-", 1)[0] \ if self.app.website_updates else None # Classify the source package type if not self.app.download_url: ending = "" elif self.app.download_url.endswith(".tar.gz"): ending = ".tar.gz" elif self.app.download_url.endswith(".tgz"): ending = ".tgz" elif self.app.download_url.endswith(".tar.bz2"): ending = ".tar.bz2" elif self.app.download_url.endswith(".zip"): ending = ".zip" elif self.app.download_url.endswith(".git"): ending = ".git" else: raise errors.InvalidConfigError( "Invalid source archive format in {0}".format(self.app.id)) msg = "Running pre-installation..." uid, gid = users.get_system("http").uid, groups.get_system("http").gid nthread.update(Notification("info", "Webs", msg)) # Call website type's pre-install hook self.pre_install(extra_vars) # If needs DB and user didn't select an engine, choose one for them if len(self.app.database_engines) > 1 \ and extra_vars.get("dbengine", None): self.app.selected_dbengine = extra_vars.get("dbengine") if not getattr(self.app, "selected_dbengine", None)\ and self.app.database_engines: self.app.selected_dbengine = self.app.database_engines[0] # Create DB and/or DB user as necessary if getattr(self.app, "selected_dbengine", None): msg = "Creating database..." nthread.update(Notification("info", "Webs", msg)) mgr = databases.get_managers(self.app.selected_dbengine) if not mgr: estr = "No manager found for {0}" raise errors.InvalidConfigError( estr.format(self.app.selected_dbengine)) # Make sure DB daemon is running if it has one if not mgr.state: svc = services.get(mgr.meta.database_service) svc.restart() self.db = mgr.add_db(self.id) if hasattr(self.db, "path"): os.chmod(self.db.path, 0o660) os.chown(self.db.path, -1, gid) # If multiuser DB type, create user if mgr.meta.database_multiuser: dbpasswd = random_string(16) db_user = mgr.add_user(self.id, dbpasswd) db_user.chperm("grant", self.db) # Make sure the target directory exists, but is empty pkg_path = os.path.join("/tmp", self.id + ending) if os.path.isdir(self.path): shutil.rmtree(self.path) os.makedirs(self.path) # Download and extract the source repo / package msg = "Downloading website source..." nthread.update(Notification("info", "Webs", msg)) if self.app.download_url and ending == ".git": g = git.Repo.clone_from(self.app.download_url, self.path) if hasattr(self.app, "download_at_tag"): g = git.Git(self.path) g.checkout(self.app.download_git_tag) elif self.app.download_url: download(self.app.download_url, file=pkg_path, crit=True) # Format extraction command according to type msg = "Extracting source..." nthread.update(Notification("info", "Webs", msg)) if ending in [".tar.gz", ".tgz", ".tar.bz2"]: arch = tarfile.open(pkg_path, "r:gz") r = (x for x in arch.getnames() if re.match("^[^/]*$", x)) toplvl = next(r, None) if not toplvl: raise errors.OperationFailedError( "Malformed source archive") arch.extractall(site_dir) os.rename(os.path.join(site_dir, toplvl), self.path) else: arch = zipfile.ZipFile(pkg_path) r = (x for x in arch.namelist() if re.match("^[^/]*/$", x)) toplvl = next(r, None) if not toplvl: raise errors.OperationFailedError( "Malformed source archive") arch.extractall(site_dir) os.rename(os.path.join(site_dir, toplvl.rstrip("/")), self.path) os.remove(pkg_path) # Set proper starting permissions on source directory os.chmod(self.path, 0o755) os.chown(self.path, uid, gid) for r, d, f in os.walk(self.path): for x in d: os.chmod(os.path.join(r, x), 0o755) os.chown(os.path.join(r, x), uid, gid) for x in f: os.chmod(os.path.join(r, x), 0o644) os.chown(os.path.join(r, x), uid, gid) # If there is a custom path for the data directory, set it up if getattr(self.app, "website_datapaths", None) \ and extra_vars.get("datadir"): self.data_path = extra_vars["datadir"] if not os.path.exists(self.data_path): os.makedirs(self.data_path) os.chmod(self.data_path, 0o755) os.chown(self.data_path, uid, gid) elif hasattr(self, "website_default_data_subdir"): self.data_path = os.path.join(self.path, self.website_default_data_subdir) else: self.data_path = self.path # Create the nginx serverblock addtoblock = self.addtoblock or [] if extra_vars.get("addtoblock"): addtoblock += nginx.loads(extra_vars.get("addtoblock"), False) default_index = "index." + ("php" if self.php else "html") if hasattr(self.app, "website_root"): webroot = os.path.join(self.path, self.app.website_root) else: webroot = self.path block = nginx.Conf() server = nginx.Server( nginx.Key("listen", str(self.port)), nginx.Key("listen", "[::]:" + str(self.port)), nginx.Key("server_name", self.domain), nginx.Key("root", webroot), nginx.Key( "index", getattr(self.app, "website_index", None) or default_index), nginx.Location("/.well-known/acme-challenge/", nginx.Key("root", self.path))) if addtoblock: server.add(*[x for x in addtoblock]) block.add(server) nginx.dumpf(block, os.path.join("/etc/nginx/sites-available", self.id)) challenge_dir = os.path.join(self.path, ".well-known/acme-challenge/") if not os.path.exists(challenge_dir): os.makedirs(challenge_dir) # Create arkOS metadata file meta = configparser.SafeConfigParser() meta.add_section("website") meta.set("website", "id", self.id) meta.set("website", "app", self.app.id) meta.set("website", "ssl", self.cert.id if getattr(self, "cert", None) else "None") meta.set("website", "version", self.version or "None") if getattr(self.app, "website_datapaths", None) \ and self.data_path: meta.set("website", "data_path", self.data_path) meta.set("website", "dbengine", "") meta.set("website", "dbengine", getattr(self.app, "selected_dbengine", "")) with open(os.path.join(self.path, ".arkos"), "w") as f: meta.write(f) # Call site type's post-installation hook msg = "Running post-installation. This may take a few minutes..." nthread.update(Notification("info", "Webs", msg)) specialmsg = self.post_install(extra_vars, dbpasswd) # Cleanup and reload daemons msg = "Finishing..." nthread.update(Notification("info", "Webs", msg)) self.installed = True storage.websites[self.id] = self if self.port == 80: cleanup_acme_dummy(self.domain) signals.emit("websites", "site_installed", self) if enable: self.nginx_enable() if enable and self.php: php.open_basedir("add", "/srv/http/") php_reload() msg = "{0} site installed successfully".format(self.app.name) nthread.complete(Notification("success", "Webs", msg)) if specialmsg: return specialmsg
def _request_acme_certificate(domain, webroot, nthread): nthread.title = "Requesting ACME certificate" signals.emit("certificates", "pre_add", id) domains = [domain] uid = users.get_system("http").uid gid = groups.get_system("ssl-cert").gid if webroot: webroot = os.path.join(webroot, ".well-known", "acme-challenge") acme_dir = config.get("certificates", "acme_dir") cert_dir = os.path.join(acme_dir, "certs", domain) cert_path = os.path.join(cert_dir, "cert.pem") key_path = os.path.join(cert_dir, "privkey.pem") if not os.path.exists(cert_dir): os.makedirs(cert_dir) if not webroot: sites = websites.get() for x in sites: if x.port in [80, 443] and x.domain == domain: webroot = x.add_acme_challenge() break else: webroot = websites.create_acme_dummy(domain) smsg = "Requesting certificate from Let's Encrypt CA..." nthread.update(Notification("info", "Certificates", smsg)) agree_to_tos = None has_written_files = False while True: try: leclient.issue_certificate( domains, acme_dir, acme_server=config.get("certificates", "acme_server"), certificate_file=cert_path, private_key_file=key_path, agree_to_tos_url=agree_to_tos) break except leclient.NeedToAgreeToTOS as e: agree_to_tos = e.url continue except leclient.NeedToTakeAction as e: if not has_written_files: if not os.path.exists(webroot): os.makedirs(webroot) os.chown(webroot, uid, gid) for x in e.actions: fn = os.path.join(webroot, x.file_name) with open(fn, 'w') as f: f.write(x.contents) os.chown(fn, uid, gid) has_written_files = True continue else: raise errors.InvalidConfigError( "Requesting a certificate failed - it doesn't appear your " "requested domain's DNS is pointing to your server, or " "there was a port problem. Please check these things and " "try again.") except leclient.WaitABit as e: while e.until_when > datetime.datetime.now(): until = e.until_when - datetime.datetime.now() until_secs = int(round(until.total_seconds())) + 1 if until_secs > 300: raise errors.InvalidConfigError( "Requesting a certificate failed - LE rate limiting " "detected, for a period of more than five minutes. " "Please try again later." ) nthread.update( Notification( "warning", "Certificates", "LE rate limiting detected." " Will reattempt in {0} seconds".format(until_secs)) ) time.sleep(until_secs) continue except leclient.InvalidDomainName: raise errors.InvalidConfigError( "Requesting a certificate failed - invalid domain name" ) except leclient.RateLimited: raise errors.InvalidConfigError( "Requesting a certificate failed - LE is refusing to issue " "more certificates to you for this domain. Please choose " "another domain or try again another time." ) os.chown(cert_path, -1, gid) os.chown(key_path, -1, gid) os.chmod(cert_path, 0o750) os.chmod(key_path, 0o750) with open(cert_path, "rb") as f: cert = x509.load_pem_x509_certificate(f.read(), default_backend()) with open(key_path, "rb") as f: key = serialization.load_pem_private_key( f.read(), password=None, backend=default_backend() ) sha1 = binascii.hexlify(cert.fingerprint(hashes.SHA1())).decode() md5 = binascii.hexlify(cert.fingerprint(hashes.MD5())).decode() sha1 = ":".join([sha1[i:i+2].upper() for i in range(0, len(sha1), 2)]) md5 = ":".join([md5[i:i+2].upper() for i in range(0, len(md5), 2)]) if isinstance(key.public_key(), rsa.RSAPublicKey): ktype = "RSA" elif isinstance(key.public_key(), dsa.DSAPublicKey): ktype = "DSA" elif isinstance(key.public_key(), ec.EllipticCurvePublicKey): ktype = "EC" else: ktype = "Unknown" ksize = key.key_size c = Certificate(domain, domain, cert_path, key_path, ktype, ksize, [], cert.not_valid_after, sha1, md5, is_acme=True) storage.certificates[c.id] = c with open("/etc/cron.d/arkos-acme-renew", "a") as f: f.write("0 4 * * * systemctl reload nginx\n") fln = ("30 3 * * * free_tls_certificate {0} {1} {2} {3} {4} " ">> /var/log/acme-renew.log\n") f.write(fln.format( " ".join(domains), key_path, cert_path, webroot.split("/.well-known/acme-challenge")[0], acme_dir )) signals.emit("certificates", "post_add", c) msg = "Certificate issued successfully" nthread.complete(Notification("success", "Certificates", msg)) return c
def update_samba(self, passwd=""): """Update Samba values in LDAP.""" try: domain = conns.LDAP.search_s(self.rootdn, ldap.SCOPE_SUBTREE, "objectClass=sambaDomain", None) except ldap.NO_SUCH_OBJECT: domain = None if not domain: hostname = sysconfig.get_hostname().upper() sambaSID = "S-1-5-21-0-0-0" dldif = { "objectClass": [b"sambaDomain"], "sambaDomainName": [b(hostname)], "sambaSID": [b(sambaSID)], "sambaAlgorithmicRidBase": [b"1000"], "sambaNextUserRid": [b"1000"], "sambaMinPwdLength": [b"5"], "sambaPwdHistoryLength": [b"0"], "sambaLogonToChgPwd": [b"0"], "sambaMaxPwdAge": [b"-1"], "sambaMinPwdAge": [b"0"], "sambaLockoutDuration": [b"30"], "sambaLockoutObservationWindow": [b"30"], "sambaLockoutThreshold": [b"0"], "sambaForceLogoff": [b"-1"], "sambaRefuseMachinePwdChange": [b"0"], "sambaNextRid": [b(str(get_next_uid()))] } dldif = ldap.modlist.addModlist(dldif) try: conns.LDAP.add_s( "sambaDomainName={0},{1}".format(hostname, self.rootdn), dldif) except ldap.ALREADY_EXISTS: pass else: sambaSID = domain[0][1]["sambaSID"][0].decode() attrs = {"sambaNextRid": [b(str(get_next_uid()))]} dldif = ldap.modlist.modifyModlist(domain[0][1], attrs, ignore_oldexistent=1) conns.LDAP.modify_s(domain[0][0], dldif) if passwd: try: uldif = conns.LDAP.search_s(self.ldap_id, ldap.SCOPE_SUBTREE, "(objectClass=*)", None) except ldap.NO_SUCH_OBJECT: raise errors.InvalidConfigError("Users", "This user does not exist") uldif = uldif[0][1] attrs = { "objectClass": [ b"mailAccount", b"inetOrgPerson", b"posixAccount", b"sambaSamAccount" ], "sambaSID": [b("{0}-{1}".format(sambaSID, self.uid))], "sambaAcctFlags": [b"[UX ]"], "sambaNTPassword": [b(nthash.encrypt(passwd).upper())] } nldif = ldap.modlist.modifyModlist(uldif, attrs, ignore_oldexistent=1) conns.LDAP.modify_s(self.ldap_id, nldif)
def _install(self, extra_vars, enable, nthread): # Set metadata values site_dir = config.get("websites", "site_dir") path = (self.path or os.path.join(site_dir, self.id)) self.path = path if os.path.isdir(self.path): shutil.rmtree(self.path) os.makedirs(self.path) # If extra data is passed in, set up the serverblock accordingly uwsgi_block = [ nginx.Location( extra_vars.get("lregex", "/"), nginx.Key("{0}_pass".format(extra_vars.get("type")), extra_vars.get("pass", "")), nginx.Key("include", "{0}_params".format(extra_vars.get("type")))) ] default_block = [ nginx.Location(extra_vars.get("lregex", "/"), nginx.Key("proxy_pass", extra_vars.get("pass", "")), nginx.Key("proxy_redirect", "off"), nginx.Key("proxy_buffering", "off"), nginx.Key("proxy_set_header", "Host $host")) ] if extra_vars: if not extra_vars.get("type") or not extra_vars.get("pass"): raise errors.InvalidConfigError( "Must enter ReverseProxy type and location to pass to") elif extra_vars.get("type") in ["fastcgi", "uwsgi"]: self.block = uwsgi_block else: self.block = default_block if extra_vars.get("xrip"): self.block[0].add( nginx.Key("proxy_set_header", "X-Real-IP $remote_addr")) if extra_vars.get("xff") == "1": xff_key = "X-Forwarded-For $proxy_add_x_forwarded_for" self.block[0].add(nginx.Key("proxy_set_header", xff_key)) # Create the nginx serverblock and arkOS metadata files block = nginx.Conf() server = nginx.Server( nginx.Key("listen", self.port), nginx.Key("listen", "[::]:" + str(self.port)), nginx.Key("server_name", self.domain), nginx.Key("root", self.base_path or self.path), nginx.Location("/.well-known/acme-challenge/", nginx.Key("root", self.path))) server.add(*[x for x in self.block]) block.add(server) nginx.dumpf(block, os.path.join("/etc/nginx/sites-available", self.id)) challenge_dir = os.path.join(self.path, ".well-known/acme-challenge/") if not os.path.exists(challenge_dir): os.makedirs(challenge_dir) meta = configparser.SafeConfigParser() ssl = self.cert.id if getattr(self, "cert", None) else "None" meta.add_section("website") meta.set("website", "id", self.id) meta.set("website", "app", self.app.id if self.app else "None") meta.set("website", "version", "None") meta.set("website", "ssl", ssl) with open(os.path.join(self.path, ".arkos"), "w") as f: meta.write(f) # Track port and reload daemon self.installed = True storage.websites[self.id] = self signals.emit("websites", "site_installed", self) self.nginx_enable()