diff --git a/orchestra/apps/mailboxes/backends.py b/orchestra/apps/mailboxes/backends.py index e466da16..483c4c93 100644 --- a/orchestra/apps/mailboxes/backends.py +++ b/orchestra/apps/mailboxes/backends.py @@ -7,7 +7,9 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from orchestra.apps.orchestration import ServiceController +from orchestra.apps.systemusers.backends import SystemUserBackend from orchestra.apps.resources import ServiceMonitor +from orchestra.utils.humanize import unit_to_bytes from . import settings from .models import Address @@ -20,6 +22,59 @@ from .models import Address logger = logging.getLogger(__name__) +class MailSystemUserBackend(ServiceController): + verbose_name = _("Mail system users") + model = 'mailboxes.Mailbox' + + def save(self, mailbox): + context = self.get_context(mailbox) + self.append(textwrap.dedent(""" + if [[ $( id %(user)s ) ]]; then + usermod %(user)s --password '%(password)s' --shell %(initial_shell)s + else + useradd %(user)s --home %(home)s --password '%(password)s' + fi + mkdir -p %(home)s + chmod 751 %(home)s + chown %(user)s:%(group)s %(home)s""") % context + ) + if hasattr(mailbox, 'resources') and hasattr(mailbox.resources, 'disk'): + self.set_quota(mailbox, context) + + def set_quota(self, mailbox, context): + context['quota'] = mailbox.resources.disk.allocated * unit_to_bytes(mailbox.resources.disk.unit) + self.append(textwrap.dedent(""" + mkdir -p %(home)s/Maildir + chown %(user)s:%(group)s %(home)s/Maildir + if [[ ! -f %(home)s/Maildir/maildirsize ]]; then + echo "%(quota)iS" > %(home)s/Maildir/maildirsize + chown %(user)s:%(group)s %(home)s/Maildir/maildirsize + else + sed -i '1s/.*/%(quota)iS/' %(home)s/Maildir/maildirsize + fi""") % context + ) + + def delete(self, mailbox): + context = self.get_context(mailbox) + self.append(textwrap.dedent(""" + { sleep 2 && killall -u %(user)s -s KILL; } & + killall -u %(user)s || true + userdel %(user)s || true + groupdel %(user)s || true""") % context + ) + self.append('mv %(home)s %(home)s.deleted' % context) + + def get_context(self, mailbox): + context = { + 'user': mailbox.name, + 'group': mailbox.name, + 'password': mailbox.password if mailbox.active else '*%s' % mailbox.password, + 'home': mailbox.get_home(), + 'initial_shell': '/dev/null', + } + return context + + class PasswdVirtualUserBackend(ServiceController): verbose_name = _("Mail virtual user (passwd-file)") model = 'mailboxes.Mailbox' @@ -29,8 +84,8 @@ class PasswdVirtualUserBackend(ServiceController): def set_user(self, context): self.append(textwrap.dedent(""" - if [[ $( grep "^%(username)s:" %(passwd_path)s ) ]]; then - sed -i 's#^%(username)s:.*#%(passwd)s#' %(passwd_path)s + if [[ $( grep "^%(user)s:" %(passwd_path)s ) ]]; then + sed -i 's#^%(user)s:.*#%(passwd)s#' %(passwd_path)s else echo '%(passwd)s' >> %(passwd_path)s fi""") % context @@ -40,14 +95,14 @@ class PasswdVirtualUserBackend(ServiceController): def set_mailbox(self, context): self.append(textwrap.dedent(""" - if [[ ! $(grep "^%(username)s@%(mailbox_domain)s\s" %(virtual_mailbox_maps)s) ]]; then - echo "%(username)s@%(mailbox_domain)s\tOK" >> %(virtual_mailbox_maps)s + if [[ ! $(grep "^%(user)s@%(mailbox_domain)s\s" %(virtual_mailbox_maps)s) ]]; then + echo "%(user)s@%(mailbox_domain)s\tOK" >> %(virtual_mailbox_maps)s UPDATED_VIRTUAL_MAILBOX_MAPS=1 fi""") % context ) def generate_filter(self, mailbox, context): - self.append("doveadm mailbox create -u %(username)s Spam" % context) + self.append("doveadm mailbox create -u %(user)s Spam" % context) context['filtering_path'] = settings.MAILBOXES_SIEVE_PATH % context filtering = mailbox.get_filtering() if filtering: @@ -67,8 +122,8 @@ class PasswdVirtualUserBackend(ServiceController): context = self.get_context(mailbox) self.append("{ sleep 2 && killall -u %(uid)s -s KILL; } &" % context) self.append("killall -u %(uid)s || true" % context) - self.append("sed -i '/^%(username)s:.*/d' %(passwd_path)s" % context) - self.append("sed -i '/^%(username)s@%(mailbox_domain)s\s.*/d' %(virtual_mailbox_maps)s" % context) + self.append("sed -i '/^%(user)s:.*/d' %(passwd_path)s" % context) + self.append("sed -i '/^%(user)s@%(mailbox_domain)s\s.*/d' %(virtual_mailbox_maps)s" % context) self.append("UPDATED_VIRTUAL_MAILBOX_MAPS=1") # TODO delete context['deleted'] = context['home'].rstrip('/') + '.deleted' @@ -99,7 +154,7 @@ class PasswdVirtualUserBackend(ServiceController): def get_context(self, mailbox): context = { 'name': mailbox.name, - 'username': mailbox.name, + 'user': mailbox.name, 'password': mailbox.password if mailbox.active else '*%s' % mailbox.password, 'uid': 10000 + mailbox.pk, 'gid': 10000 + mailbox.pk, @@ -112,7 +167,7 @@ class PasswdVirtualUserBackend(ServiceController): 'mailbox_domain': settings.MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN, } context['extra_fields'] = self.get_extra_fields(mailbox, context) - context['passwd'] = '{username}:{password}:{uid}:{gid}::{home}::{extra_fields}'.format(**context) + context['passwd'] = '{user}:{password}:{uid}:{gid}::{home}::{extra_fields}'.format(**context) return context diff --git a/orchestra/apps/orchestration/admin.py b/orchestra/apps/orchestration/admin.py index 94b4a14e..a74277f5 100644 --- a/orchestra/apps/orchestration/admin.py +++ b/orchestra/apps/orchestration/admin.py @@ -10,6 +10,7 @@ from .backends import ServiceBackend from .models import Server, Route, BackendLog, BackendOperation from .widgets import RouteBackendSelect + STATE_COLORS = { BackendLog.RECEIVED: 'darkorange', BackendLog.TIMEOUT: 'red', diff --git a/orchestra/apps/orchestration/backends.py b/orchestra/apps/orchestration/backends.py index 904cc846..cd0d09f8 100644 --- a/orchestra/apps/orchestration/backends.py +++ b/orchestra/apps/orchestration/backends.py @@ -140,7 +140,7 @@ class ServiceBackend(plugins.Plugin): return list(scripts.iteritems()) def get_banner(self): - time = timezone.now().strftime("%h %d, %Y %I:%M:%S") + time = timezone.now().strftime("%h %d, %Y %I:%M:%S %Z") return "Generated by Orchestra at %s" % time def execute(self, server, async=False): diff --git a/orchestra/apps/systemusers/backends.py b/orchestra/apps/systemusers/backends.py index eb7d5349..adcf4e01 100644 --- a/orchestra/apps/systemusers/backends.py +++ b/orchestra/apps/systemusers/backends.py @@ -19,28 +19,28 @@ class SystemUserBackend(ServiceController): groups = ','.join(self.get_groups(user)) context['groups_arg'] = '--groups %s' % groups if groups else '' self.append(textwrap.dedent(""" - if [[ $( id %(username)s ) ]]; then - usermod %(username)s --password '%(password)s' --shell %(shell)s %(groups_arg)s + if [[ $( id %(user)s ) ]]; then + usermod %(user)s --password '%(password)s' --shell %(shell)s %(groups_arg)s else - useradd %(username)s --home %(home)s --password '%(password)s' --shell %(shell)s %(groups_arg)s + useradd %(user)s --home %(home)s --password '%(password)s' --shell %(shell)s %(groups_arg)s fi mkdir -p %(home)s chmod 750 %(home)s - chown %(username)s:%(username)s %(home)s""") % context + chown %(user)s:%(user)s %(home)s""") % context ) for member in settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS: context['member'] = member - self.append('usermod -a -G %(username)s %(member)s' % context) + self.append('usermod -a -G %(user)s %(member)s' % context) if not user.is_main: - self.append('usermod -a -G %(username)s %(mainusername)s' % context) + self.append('usermod -a -G %(user)s %(mainuser)s' % context) def delete(self, user): context = self.get_context(user) self.append(textwrap.dedent("""\ - { sleep 2 && killall -u %(username)s -s KILL; } & - killall -u %(username)s || true - userdel %(username)s || true - groupdel %(username)s || true""") % context + { sleep 2 && killall -u %(user)s -s KILL; } & + killall -u %(user)s || true + userdel %(user)s || true + groupdel %(group)s || true""") % context ) self.delete_home(context, user) @@ -51,8 +51,7 @@ class SystemUserBackend(ServiceController): def delete_home(self, context, user): if user.home.rstrip('/') == user.get_base_home().rstrip('/'): # TODO delete instead of this shit - context['deleted'] = context['home'].rstrip('/') + '.deleted' - self.append("mv %(home)s %(deleted)s" % context) + self.append("mv %(home)s %(home)s.deleted" % context) def get_groups(self, user): if user.is_main: @@ -62,10 +61,11 @@ class SystemUserBackend(ServiceController): def get_context(self, user): context = { 'object_id': user.pk, - 'username': user.username, + 'user': user.username, + 'group': user.username, 'password': user.password if user.active else '*%s' % user.password, 'shell': user.shell, - 'mainusername': user.username if user.is_main else user.account.username, + 'mainuser': user.username if user.is_main else user.account.username, 'home': user.get_home() } return context diff --git a/orchestra/apps/systemusers/models.py b/orchestra/apps/systemusers/models.py index 687651a2..df2d905b 100644 --- a/orchestra/apps/systemusers/models.py +++ b/orchestra/apps/systemusers/models.py @@ -122,6 +122,7 @@ class SystemUser(models.Model): def get_base_home(self): context = { + 'user': self.username, 'username': self.username, } return os.path.normpath(settings.SYSTEMUSERS_HOME % context) diff --git a/orchestra/apps/systemusers/settings.py b/orchestra/apps/systemusers/settings.py index 1e18c3c0..83e0e2c3 100644 --- a/orchestra/apps/systemusers/settings.py +++ b/orchestra/apps/systemusers/settings.py @@ -20,7 +20,7 @@ SYSTEMUSERS_DISABLED_SHELLS = getattr(settings, 'SYSTEMUSERS_DISABLED_SHELLS', ( )) -SYSTEMUSERS_HOME = getattr(settings, 'SYSTEMUSERS_HOME', '/home/./%(username)s') +SYSTEMUSERS_HOME = getattr(settings, 'SYSTEMUSERS_HOME', '/home/./%(user)s') SYSTEMUSERS_FTP_LOG_PATH = getattr(settings, 'SYSTEMUSERS_FTP_LOG_PATH', '/var/log/vsftpd.log') diff --git a/orchestra/apps/webapps/backends/phpfcgid.py b/orchestra/apps/webapps/backends/phpfcgid.py index 604d0e4d..9745e127 100644 --- a/orchestra/apps/webapps/backends/phpfcgid.py +++ b/orchestra/apps/webapps/backends/phpfcgid.py @@ -40,7 +40,8 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController): def delete(self, webapp): context = self.get_context(webapp) - self.append("rm '%(wrapper_path)s'" % context) + self.append("rm -f '%(wrapper_path)s'" % context) + self.append("rm -f '%(cmd_options_path)s'" % context) self.delete_webapp_dir(context) def commit(self): @@ -75,7 +76,8 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController): if value: cmd_options.append("%s %s" % (directive, value)) if cmd_options: - cmd_options.insert(0, 'FcgidCmdOptions %(wrapper_path)s' % context) + head = '# %(banner)s\nFcgidCmdOptions %(wrapper_path)s' % context + cmd_options.insert(0, head) return ' \\\n '.join(cmd_options) def get_context(self, webapp): diff --git a/orchestra/apps/webapps/backends/webalizer.py b/orchestra/apps/webapps/backends/webalizer.py new file mode 100644 index 00000000..70e5b20d --- /dev/null +++ b/orchestra/apps/webapps/backends/webalizer.py @@ -0,0 +1,19 @@ +from django.utils.translation import ugettext_lazy as _ + +from orchestra.apps.orchestration import ServiceController + +from . import WebAppServiceMixin + + +class WebalizerAppBackend(WebAppServiceMixin, ServiceController): + """ Needed for cleaning up webalizer main folder when webapp deleteion withou related contents """ + verbose_name = _("Webalizer App") + default_route_match = "webapp.type == 'webalizer'" + + def save(self, webapp): + context = self.get_context(webapp) + self.create_webapp_dir(context) + + def delete(self, webapp): + context = self.get_context(webapp) + self.delete_webapp_dir(context) diff --git a/orchestra/apps/webapps/migrations/0003_auto_20150310_2103.py b/orchestra/apps/webapps/migrations/0003_auto_20150310_2103.py new file mode 100644 index 00000000..348d3793 --- /dev/null +++ b/orchestra/apps/webapps/migrations/0003_auto_20150310_2103.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapps', '0002_webapp_data'), + ] + + operations = [ + migrations.AlterField( + model_name='webapp', + name='type', + field=models.CharField(max_length=32, verbose_name='type', choices=[(b'dokuwiki-mu', b'DokuWiki (SaaS)'), (b'drupal-mu', b'Drupdal (SaaS)'), (b'php4-fcgid', b'PHP 4 FCGID'), (b'php5.2-fcgid', b'PHP 5.2 FCGID'), (b'php5.3-fcgid', b'PHP 5.3 FCGID'), (b'php5.4-fpm', b'PHP 5.4 FPM'), (b'static', b'Static'), (b'symbolic-link', b'Symbolic link'), (b'webalizer', b'Webalizer'), (b'wordpress', b'WordPress'), (b'wordpress-mu', b'WordPress (SaaS)')]), + preserve_default=True, + ), + migrations.AlterField( + model_name='webappoption', + name='name', + field=models.CharField(max_length=128, verbose_name='name', choices=[(None, b'-------'), (b'FileSystem', [(b'public-root', 'Public root')]), (b'Process', [(b'timeout', 'Process timeout'), (b'processes', 'Number of processes')]), (b'PHP', [(b'enabled_functions', 'Enabled functions'), (b'allow_url_include', 'Allow URL include'), (b'allow_url_fopen', 'Allow URL fopen'), (b'auto_append_file', 'Auto append file'), (b'auto_prepend_file', 'Auto prepend file'), (b'date.timezone', 'date.timezone'), (b'default_socket_timeout', 'Default socket timeout'), (b'display_errors', 'Display errors'), (b'extension', 'Extension'), (b'magic_quotes_gpc', 'Magic quotes GPC'), (b'magic_quotes_runtime', 'Magic quotes runtime'), (b'magic_quotes_sybase', 'Magic quotes sybase'), (b'max_execution_time', 'Max execution time'), (b'max_input_time', 'Max input time'), (b'max_input_vars', 'Max input vars'), (b'memory_limit', 'Memory limit'), (b'mysql.connect_timeout', 'Mysql connect timeout'), (b'output_buffering', 'Output buffering'), (b'register_globals', 'Register globals'), (b'post_max_size', 'zend_extension'), (b'sendmail_path', 'sendmail_path'), (b'session.bug_compat_warn', 'session.bug_compat_warn'), (b'session.auto_start', 'session.auto_start'), (b'safe_mode', 'Safe mode'), (b'suhosin.post.max_vars', 'Suhosin POST max vars'), (b'suhosin.get.max_vars', 'Suhosin GET max vars'), (b'suhosin.request.max_vars', 'Suhosin request max vars'), (b'suhosin.session.encrypt', 'suhosin.session.encrypt'), (b'suhosin.simulation', 'Suhosin simulation'), (b'suhosin.executor.include.whitelist', 'suhosin.executor.include.whitelist'), (b'upload_max_filesize', 'upload_max_filesize'), (b'post_max_size', 'zend_extension')])]), + preserve_default=True, + ), + ] diff --git a/orchestra/apps/webapps/models.py b/orchestra/apps/webapps/models.py index 2eb7bec1..6dc6410f 100644 --- a/orchestra/apps/webapps/models.py +++ b/orchestra/apps/webapps/models.py @@ -1,3 +1,4 @@ +import os import re from django.core.exceptions import ValidationError @@ -68,7 +69,7 @@ class WebApp(models.Model): public_root = self.options.filter(name='public-root').first() if public_root: path = os.path.join(path, public_root.value) - return path.replace('//', '/') + return os.path.normpath(path.replace('//', '/')) def get_user(self): return self.account.main_systemuser diff --git a/orchestra/apps/webapps/types.py b/orchestra/apps/webapps/types.py index d36c1443..3a472177 100644 --- a/orchestra/apps/webapps/types.py +++ b/orchestra/apps/webapps/types.py @@ -167,7 +167,8 @@ class PHPAppType(AppType): init_vars['dissabled_functions'] = ','.join(disabled_functions) if settings.WEBAPPS_PHP_ERROR_LOG_PATH and 'error_log' not in init_vars: context = self.get_context(webapp) - init_vars['error_log'] = settings.WEBAPPS_PHP_ERROR_LOG_PATH % context + error_log_path = os.path.normpath(settings.WEBAPPS_PHP_ERROR_LOG_PATH % context) + init_vars['error_log'] = error_log_path return init_vars @@ -192,7 +193,7 @@ class PHP53App(PHPAppType): def get_directive(self, webapp): context = self.get_directive_context(webapp) - wrapper_path = settings.WEBAPPS_FCGID_PATH % context + wrapper_path = os.path.normpath(settings.WEBAPPS_FCGID_PATH % context) return ('fcgid', webapp.get_path(), wrapper_path) @@ -236,7 +237,9 @@ class WebalizerApp(AppType): option_groups = () def get_directive(self, webapp): - return ('static', os.path.join(webapp.get_path(), '%(site_name)s/')) + webalizer_path = os.path.join(webapp.get_path(), '%(site_name)s') + webalizer_path = os.path.normpath(webalizer_path) + return ('static', webalizer_path) class WordPressMuApp(PHPAppType): diff --git a/orchestra/apps/websites/admin.py b/orchestra/apps/websites/admin.py index 7529da91..3cc0f82c 100644 --- a/orchestra/apps/websites/admin.py +++ b/orchestra/apps/websites/admin.py @@ -59,14 +59,14 @@ class ContentInline(AccountAdminMixin, admin.TabularInline): class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): list_display = ('name', 'display_domains', 'display_webapps', 'account_link') - list_filter = ('port', 'is_active') + list_filter = ('protocol', 'is_active',) change_readonly_fields = ('name',) inlines = [ContentInline, DirectiveInline] filter_horizontal = ['domains'] fieldsets = ( (None, { 'classes': ('extrapretty',), - 'fields': ('account_link', 'name', 'port', 'domains', 'is_active'), + 'fields': ('account_link', 'name', 'protocol', 'domains', 'is_active'), }), ) form = WebsiteAdminForm @@ -77,7 +77,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): def display_domains(self, website): domains = [] for domain in website.domains.all(): - url = '%s://%s' % (website.protocol, domain) + url = '%s://%s' % (website.get_protocol(), domain) domains.append('%s' % (url, url)) return '
'.join(domains) display_domains.short_description = _("domains") @@ -102,9 +102,12 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): """ formfield = super(WebsiteAdmin, self).formfield_for_dbfield(db_field, **kwargs) if db_field.name == 'domains': - qset = Q() - for port, __ in settings.WEBSITES_PORT_CHOICES: - qset = qset & Q(websites__port=port) + qset = Q( + Q(websites__protocol=Website.HTTPS_ONLY) | + Q(websites__protocol=Website.HTTP_AND_HTTPS) | Q( + Q(websites__protocol=Website.HTTP) & Q(websites__protocol=Website.HTTPS) + ) + ) args = resolve(kwargs['request'].path).args if args: object_id = args[0] diff --git a/orchestra/apps/websites/backends/apache.py b/orchestra/apps/websites/backends/apache.py index 2d66fe05..d4fce726 100644 --- a/orchestra/apps/websites/backends/apache.py +++ b/orchestra/apps/websites/backends/apache.py @@ -9,27 +9,31 @@ from orchestra.apps.orchestration import ServiceController from orchestra.apps.resources import ServiceMonitor from .. import settings +from ..utils import normurlpath class Apache2Backend(ServiceController): + HTTP_PORT = 80 + HTTPS_PORT = 443 + model = 'websites.Website' related_models = ( ('websites.Content', 'website'), ) verbose_name = _("Apache 2") - def save(self, site): - context = self.get_context(site) + def render_virtual_host(self, site, context, ssl=False): + context['port'] = self.HTTPS_PORT if ssl else self.HTTP_PORT extra_conf = self.get_content_directives(site) - if site.protocol is 'https': - extra_conf += self.get_ssl(site) - extra_conf += self.get_security(site) - extra_conf += self.get_redirect(site) + directives = site.get_directives() + if ssl: + extra_conf += self.get_ssl(directives) + extra_conf += self.get_security(directives) + extra_conf += self.get_redirects(directives) + extra_conf += self.get_proxies(directives) context['extra_conf'] = extra_conf - - apache_conf = Template(textwrap.dedent("""\ - # {{ banner }} - + return Template(textwrap.dedent("""\ + ServerName {{ site.domains.all|first }}\ {% if site.domains.all|slice:"1:" %} ServerAlias {{ site.domains.all|slice:"1:"|join:' ' }}{% endif %}\ @@ -41,12 +45,38 @@ class Apache2Backend(ServiceController): {% for line in extra_conf.splitlines %} {{ line | safe }}{% endfor %} #IncludeOptional /etc/apache2/extra-vhos[t]/{{ site_unique_name }}.con[f] - """ - )) - apache_conf = apache_conf.render(Context(context)) -# apache_conf += self.get_protections(site) + + """) + ).render(Context(context)) + + def render_redirect_https(self, context): + context['port'] = self.HTTP_PORT + return Template(textwrap.dedent(""" + + ServerName {{ site.domains.all|first }}\ + {% if site.domains.all|slice:"1:" %} + ServerAlias {{ site.domains.all|slice:"1:"|join:' ' }}{% endif %}\ + {% if access_log %} + CustomLog {{ access_log }} common{% endif %}\ + {% if error_log %} + ErrorLog {{ error_log }}{% endif %} + RewriteEngine On + RewriteCond %{HTTPS} off + RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} + + """) + ).render(Context(context)) + + def save(self, site): + context = self.get_context(site) + apache_conf = '# %(banner)s\n' % context + if site.protocol in (site.HTTP, site.HTTP_AND_HTTPS): + apache_conf += self.render_virtual_host(site, context, ssl=False) + if site.protocol in (site.HTTP_AND_HTTPS, site.HTTPS_ONLY, site.HTTPS): + apache_conf += self.render_virtual_host(site, context, ssl=True) + if site.protocol == site.HTTPS_ONLY: + apache_conf += self.render_redirect_https(context) context['apache_conf'] = apache_conf - self.append(textwrap.dedent("""\ { echo -e '%(apache_conf)s' | diff -N -I'^\s*#' %(sites_available)s - @@ -78,7 +108,7 @@ class Apache2Backend(ServiceController): def get_static_directives(self, content, app_path): context = self.get_content_context(content) context['app_path'] = app_path % context - return "Alias %(location)s %(app_path)s\n" % context + return "Alias %(location)s/ %(app_path)s/\n" % context def get_fpm_directives(self, content, socket_type, socket, app_path): if socket_type == 'unix': @@ -95,8 +125,8 @@ class Apache2Backend(ServiceController): 'socket': socket, }) return textwrap.dedent("""\ - ProxyPassMatch ^%(location)s(.*\.php(/.*)?)$ {target} - Alias %(location)s %(app_path)s/ + ProxyPassMatch ^%(location)s/(.*\.php(/.*)?)$ {target} + Alias %(location)s/ %(app_path)s/ """.format(target=target) % context ) @@ -107,52 +137,61 @@ class Apache2Backend(ServiceController): 'wrapper_path': wrapper_path, }) return textwrap.dedent("""\ - Alias %(location)s %(app_path)s - ProxyPass %(location)s ! - + Alias %(location)s/ %(app_path)s/ + ProxyPass %(location)s/ ! + Options +ExecCGI AddHandler fcgid-script .php FcgidWrapper %(wrapper_path)s """) % context - def get_ssl(self, site): - cert = settings.WEBSITES_DEFAULT_HTTPS_CERT - custom_cert = site.options.filter(name='ssl') - if custom_cert: - cert = tuple(custom_cert[0].value.split()) - # TODO separate directtives? - directives = textwrap.dedent("""\ - SSLEngine on - SSLCertificateFile %s - SSLCertificateKeyFile %s\ - """) % cert - return directives - - def get_security(self, site): - directives = '' - for rules in site.directives.filter(name='sec_rule_remove'): + def get_ssl(self, directives): + config = [] + ca = directives.get('ssl_ca') + if ca: + config.append("SSLCACertificateFile %s" % ca[0]) + cert = directives.get('ssl_cert') + if cert: + config.append("SSLCertificateFile %" % cert[0]) + key = directives.get('ssl_key') + if key: + config.append("SSLCertificateKeyFile %s" % key[0]) + return '\n'.join(config) + + def get_security(self, directives): + config = [] + for rules in directives.get('sec_rule_remove', []): for rule in rules.value.split(): - directives += "SecRuleRemoveById %i\n" % int(rule) - for modsecurity in site.directives.filter(name='sec_rule_off'): - directives += textwrap.dedent("""\ - - SecRuleEngine Off + config.append("SecRuleRemoveById %i" % int(rule)) + for modsecurity in directives.get('sec_rule_off', []): + config.append(textwrap.dedent("""\ + + SecRuleEngine off \ - """) % modsecurity.value - if directives: - directives = '\n%s\n' % directives - return directives + """) % modsecurity + ) + return '\n'.join(config) - def get_redirect(self, site): - directives = '' - for redirect in site.directives.filter(name='redirect'): - if re.match(r'^.*[\^\*\$\?\)]+.*$', redirect.value): - directives += "RedirectMatch %s" % redirect.value + def get_redirects(self, directives): + config = [] + for redirect in directives.get('redirect', []): + source, target = redirect.split() + if re.match(r'^.*[\^\*\$\?\)]+.*$', redirect): + config.append("RedirectMatch %s %s" % (source, target)) else: - directives += "Redirect %s" % redirect.value - return directives + config.append("Redirect %s %s" % (source, target)) + return '\n'.join(config) + def get_proxies(self, directives): + config = [] + for proxy in directives.get('proxy', []): + source, target = redirect.split() + source = normurlpath(source) + config.append('ProxyPass %s %s' % (source, target)) + config.append('ProxyPassReverse %s %s' % (source, target)) + return '\n'.join(directives) + # def get_protections(self, site): # protections = '' # context = self.get_context(site) @@ -192,15 +231,15 @@ class Apache2Backend(ServiceController): ) def get_username(self, site): - option = site.directives.filter(name='user_group').first() + option = site.get_directives().get('user_group') if option: - return option.value.split()[0] + return option[0] return site.account.username def get_groupname(self, site): - option = site.directives.filter(name='user_group').first() - if option and ' ' in option.value: - user, group = option.value.split() + option = site.get_directives().get('user_group') + if option and ' ' in option: + user, group = option.split() return group return site.account.username @@ -227,7 +266,7 @@ class Apache2Backend(ServiceController): context = self.get_context(content.website) context.update({ 'type': content.webapp.type, - 'location': content.path, + 'location': normurlpath(content.path), 'app_name': content.webapp.name, 'app_path': content.webapp.get_path(), }) diff --git a/orchestra/apps/websites/backends/webalizer.py b/orchestra/apps/websites/backends/webalizer.py index db9ca4d2..440fe898 100644 --- a/orchestra/apps/websites/backends/webalizer.py +++ b/orchestra/apps/websites/backends/webalizer.py @@ -9,7 +9,7 @@ from .. import settings class WebalizerBackend(ServiceController): - verbose_name = _("Webalizer") + verbose_name = _("Webalizer Content") model = 'websites.Content' def save(self, content): @@ -27,7 +27,7 @@ class WebalizerBackend(ServiceController): context = self.get_context(content) delete_webapp = type(content.webapp).objects.filter(pk=content.webapp.pk).exists() if delete_webapp: - self.append("mv %(webapp_path)s %(webapp_path)s.deleted" % context) + self.append("rm -f %(webapp_path)s" % context) if delete_webapp or not content.webapp.content_set.filter(website=content.website).exists(): self.append("rm -fr %(webalizer_path)s" % context) self.append("rm -f %(webalizer_conf_path)s" % context) diff --git a/orchestra/apps/websites/directives.py b/orchestra/apps/websites/directives.py index a665c26c..7187b1b0 100644 --- a/orchestra/apps/websites/directives.py +++ b/orchestra/apps/websites/directives.py @@ -1,3 +1,5 @@ +import re + from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ @@ -60,7 +62,7 @@ class SiteDirective(Plugin): class Redirect(SiteDirective): name = 'redirect' - verbose_name=_("Redirection") + verbose_name = _("Redirection") help_text = _("<website path> <destination URL>") regex = r'^[^ ]+\s[^ ]+$' group = SiteDirective.HTTPD @@ -68,7 +70,7 @@ class Redirect(SiteDirective): class Proxy(SiteDirective): name = 'proxy' - verbose_name=_("Proxy") + verbose_name = _("Proxy") help_text = _("<website path> <target URL>") regex = r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$' group = SiteDirective.HTTPD @@ -76,7 +78,7 @@ class Proxy(SiteDirective): class UserGroup(SiteDirective): name = 'user_group' - verbose_name=_("SuexecUserGroup") + verbose_name = _("SuexecUserGroup") help_text = _("user [group], username and optional groupname.") regex = r'^[\w/_]+(\s[\w/_]+)*$' group = SiteDirective.HTTPD @@ -101,7 +103,7 @@ class UserGroup(SiteDirective): class ErrorDocument(SiteDirective): name = 'error_document' - verbose_name=_("ErrorDocumentRoot") + verbose_name = _("ErrorDocumentRoot") help_text = _("<error code> <URL/path/message>
" " 500 http://foo.example.com/cgi-bin/tester
" " 404 /cgi-bin/bad_urls.pl
" @@ -113,7 +115,7 @@ class ErrorDocument(SiteDirective): class SSLCA(SiteDirective): name = 'ssl_ca' - verbose_name=_("SSL CA") + verbose_name = _("SSL CA") help_text = _("Filesystem path of the CA certificate file.") regex = r'^[^ ]+$' group = SiteDirective.SSL @@ -121,7 +123,7 @@ class SSLCA(SiteDirective): class SSLCert(SiteDirective): name = 'ssl_cert' - verbose_name=_("SSL cert") + verbose_name = _("SSL cert") help_text = _("Filesystem path of the certificate file.") regex = r'^[^ ]+$' group = SiteDirective.SSL @@ -129,7 +131,7 @@ class SSLCert(SiteDirective): class SSLKey(SiteDirective): name = 'ssl_key' - verbose_name=_("SSL key") + verbose_name = _("SSL key") help_text = _("Filesystem path of the key file.") regex = r'^[^ ]+$' group = SiteDirective.SSL @@ -137,7 +139,7 @@ class SSLKey(SiteDirective): class SecRuleRemove(SiteDirective): name = 'sec_rule_remove' - verbose_name=_("SecRuleRemoveById") + verbose_name = _("SecRuleRemoveById") help_text = _("Space separated ModSecurity rule IDs.") regex = r'^[0-9\s]+$' group = SiteDirective.SEC @@ -145,7 +147,7 @@ class SecRuleRemove(SiteDirective): class SecEngine(SiteDirective): name = 'sec_engine' - verbose_name=_("Modsecurity engine") - help_text = _("On or Off, defaults to On") - regex = r'^(On|Off)$' + verbose_name = _("Modsecurity engine") + help_text = _("URL location for disabling modsecurity engine.") + regex = r'^[^ ]+$' group = SiteDirective.SEC diff --git a/orchestra/apps/websites/forms.py b/orchestra/apps/websites/forms.py index dd82cc42..ea1335e3 100644 --- a/orchestra/apps/websites/forms.py +++ b/orchestra/apps/websites/forms.py @@ -1,20 +1,43 @@ from django import forms from django.core.exceptions import ValidationError +from django.db.models import Q + +from .models import Website class WebsiteAdminForm(forms.ModelForm): def clean(self): - """ Prevent multiples domains on the same port """ + """ Prevent multiples domains on the same protocol """ domains = self.cleaned_data.get('domains') - port = self.cleaned_data.get('port') + if not domains: + return self.cleaned_data + protocol = self.cleaned_data.get('protocol') existing = [] for domain in domains.all(): - if domain.websites.filter(port=port).exclude(pk=self.instance.pk).exists(): + if protocol == Website.HTTP: + qset = Q( + Q(protocol=Website.HTTP) | + Q(protocol=Website.HTTP_AND_HTTPS) | + Q(protocol=Website.HTTPS_ONLY) + ) + elif protocol == Website.HTTPS: + qset = Q( + Q(protocol=Website.HTTPS) | + Q(protocol=Website.HTTP_AND_HTTPS) | + Q(protocol=Website.HTTPS_ONLY) + ) + elif protocol in (Website.HTTP_AND_HTTPS, Website.HTTPS_ONLY): + qset = Q() + else: + raise ValidationError({ + 'protocol': _("Unknown protocol %s") % protocol + }) + if domain.websites.filter(qset).exclude(pk=self.instance.pk).exists(): existing.append(domain.name) if existing: - context = (', '.join(existing), port) + context = (', '.join(existing), protocol) raise ValidationError({ - 'domains': 'A website is already defined for "%s" on port %s' % context + 'domains': 'A website is already defined for "%s" on protocol %s' % context }) return self.cleaned_data diff --git a/orchestra/apps/websites/models.py b/orchestra/apps/websites/models.py index bd9c025f..3505635c 100644 --- a/orchestra/apps/websites/models.py +++ b/orchestra/apps/websites/models.py @@ -1,3 +1,4 @@ +import os import re from django.core.exceptions import ValidationError @@ -10,18 +11,26 @@ from orchestra.utils.functional import cached from . import settings from .directives import SiteDirective +from .utils import normurlpath class Website(models.Model): """ Models a web site, also known as virtual host """ + HTTP = 'http' + HTTPS = 'https' + HTTP_AND_HTTPS = 'http/https' + HTTPS_ONLY = 'https-only' + name = models.CharField(_("name"), max_length=128, validators=[validators.validate_name]) account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), related_name='websites') - # TODO protocol - port = models.PositiveIntegerField(_("port"), - choices=settings.WEBSITES_PORT_CHOICES, - default=settings.WEBSITES_DEFAULT_PORT) + protocol = models.CharField(_("protocol"), max_length=16, + choices=settings.WEBSITES_PROTOCOL_CHOICES, + default=settings.WEBSITES_DEFAULT_PROTOCOL) +# port = models.PositiveIntegerField(_("port"), +# choices=settings.WEBSITES_PORT_CHOICES, +# default=settings.WEBSITES_DEFAULT_PORT) domains = models.ManyToManyField(settings.WEBSITES_DOMAIN_MODEL, related_name='websites', verbose_name=_("domains")) contents = models.ManyToManyField('webapps.WebApp', through='websites.Content') @@ -39,28 +48,29 @@ class Website(models.Model): 'id': self.id, 'pk': self.pk, 'account': self.account.username, - 'port': self.port, + 'protocol': self.protocol, 'name': self.name, } - @property - def protocol(self): - if self.port == 80: - return 'http' - if self.port == 443: - return 'https' - raise TypeError('No protocol for port "%s"' % self.port) + def get_protocol(self): + if self.protocol in (self.HTTP, self.HTTP_AND_HTTPS): + return self.HTTP + return self.HTTPS @cached def get_directives(self): - return { - opt.name: opt.value for opt in self.directives.all() - } + directives = {} + for opt in self.directives.all(): + try: + directives[opt.name].append(opt.value) + except KeyError: + directives[opt.name] = [opt.value] + return directives def get_absolute_url(self): domain = self.domains.first() if domain: - return '%s://%s' % (self.protocol, domain) + return '%s://%s' % (self.get_protocol(), domain) def get_www_log_context(self): return { @@ -74,12 +84,12 @@ class Website(models.Model): def get_www_access_log_path(self): context = self.get_www_log_context() path = settings.WEBSITES_WEBSITE_WWW_ACCESS_LOG_PATH % context - return path.replace('//', '/') + return os.path.normpath(path.replace('//', '/')) def get_www_error_log_path(self): context = self.get_www_log_context() path = settings.WEBSITES_WEBSITE_WWW_ERROR_LOG_PATH % context - return path.replace('//', '/') + return os.path.normpath(path.replace('//', '/')) class Directive(models.Model): @@ -122,15 +132,12 @@ class Content(models.Model): return self.path def clean(self): - if not self.path.startswith('/'): - self.path = '/' + self.path - if not self.path.endswith('/'): - self.path = self.path + '/' + self.path = normurlpath(self.path) def get_absolute_url(self): domain = self.website.domains.first() if domain: - return '%s://%s%s' % (self.website.protocol, domain, self.path) + return '%s://%s%s' % (self.website.get_protocol(), domain, self.path) services.register(Website) diff --git a/orchestra/apps/websites/settings.py b/orchestra/apps/websites/settings.py index 38689ba2..bacabeb1 100644 --- a/orchestra/apps/websites/settings.py +++ b/orchestra/apps/websites/settings.py @@ -7,19 +7,21 @@ WEBSITES_UNIQUE_NAME_FORMAT = getattr(settings, 'WEBSITES_UNIQUE_NAME_FORMAT', # TODO 'http', 'https', 'https-only', 'http and https' and rename to PROTOCOL -WEBSITES_PORT_CHOICES = getattr(settings, 'WEBSITES_PORT_CHOICES', ( - (80, 'HTTP'), - (443, 'HTTPS'), -)) +#WEBSITES_PORT_CHOICES = getattr(settings, 'WEBSITES_PORT_CHOICES', ( +# (80, 'HTTP'), +# (443, 'HTTPS'), +#)) WEBSITES_PROTOCOL_CHOICES = getattr(settings, 'WEBSITES_PROTOCOL_CHOICES', ( ('http', "HTTP"), ('https', "HTTPS"), - ('http-https', _("HTTP and HTTPS")), + ('http/https', _("HTTP and HTTPS")), ('https-only', _("HTTPS only")), )) +WEBSITES_DEFAULT_PROTOCOL = getattr(settings, 'WEBSITES_DEFAULT_PROTOCOL', 'http') + WEBSITES_DEFAULT_PORT = getattr(settings, 'WEBSITES_DEFAULT_PORT', 80) @@ -60,3 +62,13 @@ WEBSITES_WEBSITE_WWW_ERROR_LOG_PATH = getattr(settings, 'WEBSITES_WEBSITE_WWW_ER WEBSITES_TRAFFIC_IGNORE_HOSTS = getattr(settings, 'WEBSITES_TRAFFIC_IGNORE_HOSTS', ('127.0.0.1',)) + + +#WEBSITES_DEFAULT_SSl_CA = getattr(settings, 'WEBSITES_DEFAULT_SSl_CA', +# '') + +#WEBSITES_DEFAULT_SSl_CERT = getattr(settings, 'WEBSITES_DEFAULT_SSl_CERT', +# '') + +#WEBSITES_DEFAULT_SSl_KEY = getattr(settings, 'WEBSITES_DEFAULT_SSl_KEY', +# '') diff --git a/orchestra/apps/websites/utils.py b/orchestra/apps/websites/utils.py new file mode 100644 index 00000000..33f8dfee --- /dev/null +++ b/orchestra/apps/websites/utils.py @@ -0,0 +1,5 @@ +def normurlpath(path): + if not path.startswith('/'): + path = '/' + path + path = path.rstrip('/') + return path.replace('//', '/')