Fixes on website apache backend

This commit is contained in:
Marc Aymerich 2015-03-10 21:51:10 +00:00
parent 44e8b29b43
commit 340a40262f
19 changed files with 341 additions and 142 deletions

View File

@ -7,7 +7,9 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceController from orchestra.apps.orchestration import ServiceController
from orchestra.apps.systemusers.backends import SystemUserBackend
from orchestra.apps.resources import ServiceMonitor from orchestra.apps.resources import ServiceMonitor
from orchestra.utils.humanize import unit_to_bytes
from . import settings from . import settings
from .models import Address from .models import Address
@ -20,6 +22,59 @@ from .models import Address
logger = logging.getLogger(__name__) 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): class PasswdVirtualUserBackend(ServiceController):
verbose_name = _("Mail virtual user (passwd-file)") verbose_name = _("Mail virtual user (passwd-file)")
model = 'mailboxes.Mailbox' model = 'mailboxes.Mailbox'
@ -29,8 +84,8 @@ class PasswdVirtualUserBackend(ServiceController):
def set_user(self, context): def set_user(self, context):
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""
if [[ $( grep "^%(username)s:" %(passwd_path)s ) ]]; then if [[ $( grep "^%(user)s:" %(passwd_path)s ) ]]; then
sed -i 's#^%(username)s:.*#%(passwd)s#' %(passwd_path)s sed -i 's#^%(user)s:.*#%(passwd)s#' %(passwd_path)s
else else
echo '%(passwd)s' >> %(passwd_path)s echo '%(passwd)s' >> %(passwd_path)s
fi""") % context fi""") % context
@ -40,14 +95,14 @@ class PasswdVirtualUserBackend(ServiceController):
def set_mailbox(self, context): def set_mailbox(self, context):
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""
if [[ ! $(grep "^%(username)s@%(mailbox_domain)s\s" %(virtual_mailbox_maps)s) ]]; then if [[ ! $(grep "^%(user)s@%(mailbox_domain)s\s" %(virtual_mailbox_maps)s) ]]; then
echo "%(username)s@%(mailbox_domain)s\tOK" >> %(virtual_mailbox_maps)s echo "%(user)s@%(mailbox_domain)s\tOK" >> %(virtual_mailbox_maps)s
UPDATED_VIRTUAL_MAILBOX_MAPS=1 UPDATED_VIRTUAL_MAILBOX_MAPS=1
fi""") % context fi""") % context
) )
def generate_filter(self, mailbox, 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 context['filtering_path'] = settings.MAILBOXES_SIEVE_PATH % context
filtering = mailbox.get_filtering() filtering = mailbox.get_filtering()
if filtering: if filtering:
@ -67,8 +122,8 @@ class PasswdVirtualUserBackend(ServiceController):
context = self.get_context(mailbox) context = self.get_context(mailbox)
self.append("{ sleep 2 && killall -u %(uid)s -s KILL; } &" % context) self.append("{ sleep 2 && killall -u %(uid)s -s KILL; } &" % context)
self.append("killall -u %(uid)s || true" % context) self.append("killall -u %(uid)s || true" % context)
self.append("sed -i '/^%(username)s:.*/d' %(passwd_path)s" % context) self.append("sed -i '/^%(user)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@%(mailbox_domain)s\s.*/d' %(virtual_mailbox_maps)s" % context)
self.append("UPDATED_VIRTUAL_MAILBOX_MAPS=1") self.append("UPDATED_VIRTUAL_MAILBOX_MAPS=1")
# TODO delete # TODO delete
context['deleted'] = context['home'].rstrip('/') + '.deleted' context['deleted'] = context['home'].rstrip('/') + '.deleted'
@ -99,7 +154,7 @@ class PasswdVirtualUserBackend(ServiceController):
def get_context(self, mailbox): def get_context(self, mailbox):
context = { context = {
'name': mailbox.name, 'name': mailbox.name,
'username': mailbox.name, 'user': mailbox.name,
'password': mailbox.password if mailbox.active else '*%s' % mailbox.password, 'password': mailbox.password if mailbox.active else '*%s' % mailbox.password,
'uid': 10000 + mailbox.pk, 'uid': 10000 + mailbox.pk,
'gid': 10000 + mailbox.pk, 'gid': 10000 + mailbox.pk,
@ -112,7 +167,7 @@ class PasswdVirtualUserBackend(ServiceController):
'mailbox_domain': settings.MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN, 'mailbox_domain': settings.MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN,
} }
context['extra_fields'] = self.get_extra_fields(mailbox, context) 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 return context

View File

@ -10,6 +10,7 @@ from .backends import ServiceBackend
from .models import Server, Route, BackendLog, BackendOperation from .models import Server, Route, BackendLog, BackendOperation
from .widgets import RouteBackendSelect from .widgets import RouteBackendSelect
STATE_COLORS = { STATE_COLORS = {
BackendLog.RECEIVED: 'darkorange', BackendLog.RECEIVED: 'darkorange',
BackendLog.TIMEOUT: 'red', BackendLog.TIMEOUT: 'red',

View File

@ -140,7 +140,7 @@ class ServiceBackend(plugins.Plugin):
return list(scripts.iteritems()) return list(scripts.iteritems())
def get_banner(self): 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 return "Generated by Orchestra at %s" % time
def execute(self, server, async=False): def execute(self, server, async=False):

View File

@ -19,28 +19,28 @@ class SystemUserBackend(ServiceController):
groups = ','.join(self.get_groups(user)) groups = ','.join(self.get_groups(user))
context['groups_arg'] = '--groups %s' % groups if groups else '' context['groups_arg'] = '--groups %s' % groups if groups else ''
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""
if [[ $( id %(username)s ) ]]; then if [[ $( id %(user)s ) ]]; then
usermod %(username)s --password '%(password)s' --shell %(shell)s %(groups_arg)s usermod %(user)s --password '%(password)s' --shell %(shell)s %(groups_arg)s
else 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 fi
mkdir -p %(home)s mkdir -p %(home)s
chmod 750 %(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: for member in settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS:
context['member'] = member 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: 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): def delete(self, user):
context = self.get_context(user) context = self.get_context(user)
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
{ sleep 2 && killall -u %(username)s -s KILL; } & { sleep 2 && killall -u %(user)s -s KILL; } &
killall -u %(username)s || true killall -u %(user)s || true
userdel %(username)s || true userdel %(user)s || true
groupdel %(username)s || true""") % context groupdel %(group)s || true""") % context
) )
self.delete_home(context, user) self.delete_home(context, user)
@ -51,8 +51,7 @@ class SystemUserBackend(ServiceController):
def delete_home(self, context, user): def delete_home(self, context, user):
if user.home.rstrip('/') == user.get_base_home().rstrip('/'): if user.home.rstrip('/') == user.get_base_home().rstrip('/'):
# TODO delete instead of this shit # TODO delete instead of this shit
context['deleted'] = context['home'].rstrip('/') + '.deleted' self.append("mv %(home)s %(home)s.deleted" % context)
self.append("mv %(home)s %(deleted)s" % context)
def get_groups(self, user): def get_groups(self, user):
if user.is_main: if user.is_main:
@ -62,10 +61,11 @@ class SystemUserBackend(ServiceController):
def get_context(self, user): def get_context(self, user):
context = { context = {
'object_id': user.pk, 'object_id': user.pk,
'username': user.username, 'user': user.username,
'group': user.username,
'password': user.password if user.active else '*%s' % user.password, 'password': user.password if user.active else '*%s' % user.password,
'shell': user.shell, '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() 'home': user.get_home()
} }
return context return context

View File

@ -122,6 +122,7 @@ class SystemUser(models.Model):
def get_base_home(self): def get_base_home(self):
context = { context = {
'user': self.username,
'username': self.username, 'username': self.username,
} }
return os.path.normpath(settings.SYSTEMUSERS_HOME % context) return os.path.normpath(settings.SYSTEMUSERS_HOME % context)

View File

@ -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') SYSTEMUSERS_FTP_LOG_PATH = getattr(settings, 'SYSTEMUSERS_FTP_LOG_PATH', '/var/log/vsftpd.log')

View File

@ -40,7 +40,8 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController):
def delete(self, webapp): def delete(self, webapp):
context = self.get_context(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) self.delete_webapp_dir(context)
def commit(self): def commit(self):
@ -75,7 +76,8 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController):
if value: if value:
cmd_options.append("%s %s" % (directive, value)) cmd_options.append("%s %s" % (directive, value))
if cmd_options: 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) return ' \\\n '.join(cmd_options)
def get_context(self, webapp): def get_context(self, webapp):

View File

@ -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)

View File

@ -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,
),
]

View File

@ -1,3 +1,4 @@
import os
import re import re
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -68,7 +69,7 @@ class WebApp(models.Model):
public_root = self.options.filter(name='public-root').first() public_root = self.options.filter(name='public-root').first()
if public_root: if public_root:
path = os.path.join(path, public_root.value) path = os.path.join(path, public_root.value)
return path.replace('//', '/') return os.path.normpath(path.replace('//', '/'))
def get_user(self): def get_user(self):
return self.account.main_systemuser return self.account.main_systemuser

View File

@ -167,7 +167,8 @@ class PHPAppType(AppType):
init_vars['dissabled_functions'] = ','.join(disabled_functions) init_vars['dissabled_functions'] = ','.join(disabled_functions)
if settings.WEBAPPS_PHP_ERROR_LOG_PATH and 'error_log' not in init_vars: if settings.WEBAPPS_PHP_ERROR_LOG_PATH and 'error_log' not in init_vars:
context = self.get_context(webapp) 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 return init_vars
@ -192,7 +193,7 @@ class PHP53App(PHPAppType):
def get_directive(self, webapp): def get_directive(self, webapp):
context = self.get_directive_context(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) return ('fcgid', webapp.get_path(), wrapper_path)
@ -236,7 +237,9 @@ class WebalizerApp(AppType):
option_groups = () option_groups = ()
def get_directive(self, webapp): 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): class WordPressMuApp(PHPAppType):

View File

@ -59,14 +59,14 @@ class ContentInline(AccountAdminMixin, admin.TabularInline):
class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = ('name', 'display_domains', 'display_webapps', 'account_link') list_display = ('name', 'display_domains', 'display_webapps', 'account_link')
list_filter = ('port', 'is_active') list_filter = ('protocol', 'is_active',)
change_readonly_fields = ('name',) change_readonly_fields = ('name',)
inlines = [ContentInline, DirectiveInline] inlines = [ContentInline, DirectiveInline]
filter_horizontal = ['domains'] filter_horizontal = ['domains']
fieldsets = ( fieldsets = (
(None, { (None, {
'classes': ('extrapretty',), 'classes': ('extrapretty',),
'fields': ('account_link', 'name', 'port', 'domains', 'is_active'), 'fields': ('account_link', 'name', 'protocol', 'domains', 'is_active'),
}), }),
) )
form = WebsiteAdminForm form = WebsiteAdminForm
@ -77,7 +77,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
def display_domains(self, website): def display_domains(self, website):
domains = [] domains = []
for domain in website.domains.all(): for domain in website.domains.all():
url = '%s://%s' % (website.protocol, domain) url = '%s://%s' % (website.get_protocol(), domain)
domains.append('<a href="%s">%s</a>' % (url, url)) domains.append('<a href="%s">%s</a>' % (url, url))
return '<br>'.join(domains) return '<br>'.join(domains)
display_domains.short_description = _("domains") display_domains.short_description = _("domains")
@ -102,9 +102,12 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
""" """
formfield = super(WebsiteAdmin, self).formfield_for_dbfield(db_field, **kwargs) formfield = super(WebsiteAdmin, self).formfield_for_dbfield(db_field, **kwargs)
if db_field.name == 'domains': if db_field.name == 'domains':
qset = Q() qset = Q(
for port, __ in settings.WEBSITES_PORT_CHOICES: Q(websites__protocol=Website.HTTPS_ONLY) |
qset = qset & Q(websites__port=port) Q(websites__protocol=Website.HTTP_AND_HTTPS) | Q(
Q(websites__protocol=Website.HTTP) & Q(websites__protocol=Website.HTTPS)
)
)
args = resolve(kwargs['request'].path).args args = resolve(kwargs['request'].path).args
if args: if args:
object_id = args[0] object_id = args[0]

View File

@ -9,27 +9,31 @@ from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor from orchestra.apps.resources import ServiceMonitor
from .. import settings from .. import settings
from ..utils import normurlpath
class Apache2Backend(ServiceController): class Apache2Backend(ServiceController):
HTTP_PORT = 80
HTTPS_PORT = 443
model = 'websites.Website' model = 'websites.Website'
related_models = ( related_models = (
('websites.Content', 'website'), ('websites.Content', 'website'),
) )
verbose_name = _("Apache 2") verbose_name = _("Apache 2")
def save(self, site): def render_virtual_host(self, site, context, ssl=False):
context = self.get_context(site) context['port'] = self.HTTPS_PORT if ssl else self.HTTP_PORT
extra_conf = self.get_content_directives(site) extra_conf = self.get_content_directives(site)
if site.protocol is 'https': directives = site.get_directives()
extra_conf += self.get_ssl(site) if ssl:
extra_conf += self.get_security(site) extra_conf += self.get_ssl(directives)
extra_conf += self.get_redirect(site) extra_conf += self.get_security(directives)
extra_conf += self.get_redirects(directives)
extra_conf += self.get_proxies(directives)
context['extra_conf'] = extra_conf context['extra_conf'] = extra_conf
return Template(textwrap.dedent("""\
apache_conf = Template(textwrap.dedent("""\ <VirtualHost {{ ip }}:{{ port }}>
# {{ banner }}
<VirtualHost {{ ip }}:{{ site.port }}>
ServerName {{ site.domains.all|first }}\ ServerName {{ site.domains.all|first }}\
{% if site.domains.all|slice:"1:" %} {% if site.domains.all|slice:"1:" %}
ServerAlias {{ site.domains.all|slice:"1:"|join:' ' }}{% endif %}\ ServerAlias {{ site.domains.all|slice:"1:"|join:' ' }}{% endif %}\
@ -41,12 +45,38 @@ class Apache2Backend(ServiceController):
{% for line in extra_conf.splitlines %} {% for line in extra_conf.splitlines %}
{{ line | safe }}{% endfor %} {{ line | safe }}{% endfor %}
#IncludeOptional /etc/apache2/extra-vhos[t]/{{ site_unique_name }}.con[f] #IncludeOptional /etc/apache2/extra-vhos[t]/{{ site_unique_name }}.con[f]
</VirtualHost>""" </VirtualHost>
)) """)
apache_conf = apache_conf.render(Context(context)) ).render(Context(context))
# apache_conf += self.get_protections(site)
context['apache_conf'] = apache_conf
def render_redirect_https(self, context):
context['port'] = self.HTTP_PORT
return Template(textwrap.dedent("""
<VirtualHost {{ ip }}:{{ port }}>
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}
</VirtualHost>
""")
).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("""\ self.append(textwrap.dedent("""\
{ {
echo -e '%(apache_conf)s' | diff -N -I'^\s*#' %(sites_available)s - 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): def get_static_directives(self, content, app_path):
context = self.get_content_context(content) context = self.get_content_context(content)
context['app_path'] = app_path % context 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): def get_fpm_directives(self, content, socket_type, socket, app_path):
if socket_type == 'unix': if socket_type == 'unix':
@ -95,8 +125,8 @@ class Apache2Backend(ServiceController):
'socket': socket, 'socket': socket,
}) })
return textwrap.dedent("""\ return textwrap.dedent("""\
ProxyPassMatch ^%(location)s(.*\.php(/.*)?)$ {target} ProxyPassMatch ^%(location)s/(.*\.php(/.*)?)$ {target}
Alias %(location)s %(app_path)s/ Alias %(location)s/ %(app_path)s/
""".format(target=target) % context """.format(target=target) % context
) )
@ -107,51 +137,60 @@ class Apache2Backend(ServiceController):
'wrapper_path': wrapper_path, 'wrapper_path': wrapper_path,
}) })
return textwrap.dedent("""\ return textwrap.dedent("""\
Alias %(location)s %(app_path)s Alias %(location)s/ %(app_path)s/
ProxyPass %(location)s ! ProxyPass %(location)s/ !
<Directory %(app_path)s> <Directory %(app_path)s/>
Options +ExecCGI Options +ExecCGI
AddHandler fcgid-script .php AddHandler fcgid-script .php
FcgidWrapper %(wrapper_path)s FcgidWrapper %(wrapper_path)s
</Directory> </Directory>
""") % context """) % context
def get_ssl(self, site): def get_ssl(self, directives):
cert = settings.WEBSITES_DEFAULT_HTTPS_CERT config = []
custom_cert = site.options.filter(name='ssl') ca = directives.get('ssl_ca')
if custom_cert: if ca:
cert = tuple(custom_cert[0].value.split()) config.append("SSLCACertificateFile %s" % ca[0])
# TODO separate directtives? cert = directives.get('ssl_cert')
directives = textwrap.dedent("""\ if cert:
SSLEngine on config.append("SSLCertificateFile %" % cert[0])
SSLCertificateFile %s key = directives.get('ssl_key')
SSLCertificateKeyFile %s\ if key:
""") % cert config.append("SSLCertificateKeyFile %s" % key[0])
return directives return '\n'.join(config)
def get_security(self, site): def get_security(self, directives):
directives = '' config = []
for rules in site.directives.filter(name='sec_rule_remove'): for rules in directives.get('sec_rule_remove', []):
for rule in rules.value.split(): for rule in rules.value.split():
directives += "SecRuleRemoveById %i\n" % int(rule) config.append("SecRuleRemoveById %i" % int(rule))
for modsecurity in site.directives.filter(name='sec_rule_off'): for modsecurity in directives.get('sec_rule_off', []):
directives += textwrap.dedent("""\ config.append(textwrap.dedent("""\
<LocationMatch %s> <Location %s>
SecRuleEngine Off SecRuleEngine off
</LocationMatch>\ </LocationMatch>\
""") % modsecurity.value """) % modsecurity
if directives: )
directives = '<IfModule mod_security2.c>\n%s\n</IfModule>' % directives return '\n'.join(config)
return directives
def get_redirect(self, site): def get_redirects(self, directives):
directives = '' config = []
for redirect in site.directives.filter(name='redirect'): for redirect in directives.get('redirect', []):
if re.match(r'^.*[\^\*\$\?\)]+.*$', redirect.value): source, target = redirect.split()
directives += "RedirectMatch %s" % redirect.value if re.match(r'^.*[\^\*\$\?\)]+.*$', redirect):
config.append("RedirectMatch %s %s" % (source, target))
else: else:
directives += "Redirect %s" % redirect.value config.append("Redirect %s %s" % (source, target))
return directives 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): # def get_protections(self, site):
# protections = '' # protections = ''
@ -192,15 +231,15 @@ class Apache2Backend(ServiceController):
) )
def get_username(self, site): def get_username(self, site):
option = site.directives.filter(name='user_group').first() option = site.get_directives().get('user_group')
if option: if option:
return option.value.split()[0] return option[0]
return site.account.username return site.account.username
def get_groupname(self, site): def get_groupname(self, site):
option = site.directives.filter(name='user_group').first() option = site.get_directives().get('user_group')
if option and ' ' in option.value: if option and ' ' in option:
user, group = option.value.split() user, group = option.split()
return group return group
return site.account.username return site.account.username
@ -227,7 +266,7 @@ class Apache2Backend(ServiceController):
context = self.get_context(content.website) context = self.get_context(content.website)
context.update({ context.update({
'type': content.webapp.type, 'type': content.webapp.type,
'location': content.path, 'location': normurlpath(content.path),
'app_name': content.webapp.name, 'app_name': content.webapp.name,
'app_path': content.webapp.get_path(), 'app_path': content.webapp.get_path(),
}) })

View File

@ -9,7 +9,7 @@ from .. import settings
class WebalizerBackend(ServiceController): class WebalizerBackend(ServiceController):
verbose_name = _("Webalizer") verbose_name = _("Webalizer Content")
model = 'websites.Content' model = 'websites.Content'
def save(self, content): def save(self, content):
@ -27,7 +27,7 @@ class WebalizerBackend(ServiceController):
context = self.get_context(content) context = self.get_context(content)
delete_webapp = type(content.webapp).objects.filter(pk=content.webapp.pk).exists() delete_webapp = type(content.webapp).objects.filter(pk=content.webapp.pk).exists()
if delete_webapp: 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(): 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 -fr %(webalizer_path)s" % context)
self.append("rm -f %(webalizer_conf_path)s" % context) self.append("rm -f %(webalizer_conf_path)s" % context)

View File

@ -1,3 +1,5 @@
import re
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -146,6 +148,6 @@ class SecRuleRemove(SiteDirective):
class SecEngine(SiteDirective): class SecEngine(SiteDirective):
name = 'sec_engine' name = 'sec_engine'
verbose_name = _("Modsecurity engine") verbose_name = _("Modsecurity engine")
help_text = _("<tt>On</tt> or <tt>Off</tt>, defaults to On") help_text = _("URL location for disabling modsecurity engine.")
regex = r'^(On|Off)$' regex = r'^[^ ]+$'
group = SiteDirective.SEC group = SiteDirective.SEC

View File

@ -1,20 +1,43 @@
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Q
from .models import Website
class WebsiteAdminForm(forms.ModelForm): class WebsiteAdminForm(forms.ModelForm):
def clean(self): def clean(self):
""" Prevent multiples domains on the same port """ """ Prevent multiples domains on the same protocol """
domains = self.cleaned_data.get('domains') 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 = [] existing = []
for domain in domains.all(): 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) existing.append(domain.name)
if existing: if existing:
context = (', '.join(existing), port) context = (', '.join(existing), protocol)
raise ValidationError({ 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 return self.cleaned_data

View File

@ -1,3 +1,4 @@
import os
import re import re
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -10,18 +11,26 @@ from orchestra.utils.functional import cached
from . import settings from . import settings
from .directives import SiteDirective from .directives import SiteDirective
from .utils import normurlpath
class Website(models.Model): class Website(models.Model):
""" Models a web site, also known as virtual host """ """ 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, name = models.CharField(_("name"), max_length=128,
validators=[validators.validate_name]) validators=[validators.validate_name])
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='websites') related_name='websites')
# TODO protocol protocol = models.CharField(_("protocol"), max_length=16,
port = models.PositiveIntegerField(_("port"), choices=settings.WEBSITES_PROTOCOL_CHOICES,
choices=settings.WEBSITES_PORT_CHOICES, default=settings.WEBSITES_DEFAULT_PROTOCOL)
default=settings.WEBSITES_DEFAULT_PORT) # port = models.PositiveIntegerField(_("port"),
# choices=settings.WEBSITES_PORT_CHOICES,
# default=settings.WEBSITES_DEFAULT_PORT)
domains = models.ManyToManyField(settings.WEBSITES_DOMAIN_MODEL, domains = models.ManyToManyField(settings.WEBSITES_DOMAIN_MODEL,
related_name='websites', verbose_name=_("domains")) related_name='websites', verbose_name=_("domains"))
contents = models.ManyToManyField('webapps.WebApp', through='websites.Content') contents = models.ManyToManyField('webapps.WebApp', through='websites.Content')
@ -39,28 +48,29 @@ class Website(models.Model):
'id': self.id, 'id': self.id,
'pk': self.pk, 'pk': self.pk,
'account': self.account.username, 'account': self.account.username,
'port': self.port, 'protocol': self.protocol,
'name': self.name, 'name': self.name,
} }
@property def get_protocol(self):
def protocol(self): if self.protocol in (self.HTTP, self.HTTP_AND_HTTPS):
if self.port == 80: return self.HTTP
return 'http' return self.HTTPS
if self.port == 443:
return 'https'
raise TypeError('No protocol for port "%s"' % self.port)
@cached @cached
def get_directives(self): def get_directives(self):
return { directives = {}
opt.name: opt.value for opt in self.directives.all() 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): def get_absolute_url(self):
domain = self.domains.first() domain = self.domains.first()
if domain: if domain:
return '%s://%s' % (self.protocol, domain) return '%s://%s' % (self.get_protocol(), domain)
def get_www_log_context(self): def get_www_log_context(self):
return { return {
@ -74,12 +84,12 @@ class Website(models.Model):
def get_www_access_log_path(self): def get_www_access_log_path(self):
context = self.get_www_log_context() context = self.get_www_log_context()
path = settings.WEBSITES_WEBSITE_WWW_ACCESS_LOG_PATH % 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): def get_www_error_log_path(self):
context = self.get_www_log_context() context = self.get_www_log_context()
path = settings.WEBSITES_WEBSITE_WWW_ERROR_LOG_PATH % context path = settings.WEBSITES_WEBSITE_WWW_ERROR_LOG_PATH % context
return path.replace('//', '/') return os.path.normpath(path.replace('//', '/'))
class Directive(models.Model): class Directive(models.Model):
@ -122,15 +132,12 @@ class Content(models.Model):
return self.path return self.path
def clean(self): def clean(self):
if not self.path.startswith('/'): self.path = normurlpath(self.path)
self.path = '/' + self.path
if not self.path.endswith('/'):
self.path = self.path + '/'
def get_absolute_url(self): def get_absolute_url(self):
domain = self.website.domains.first() domain = self.website.domains.first()
if domain: 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) services.register(Website)

View File

@ -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 # TODO 'http', 'https', 'https-only', 'http and https' and rename to PROTOCOL
WEBSITES_PORT_CHOICES = getattr(settings, 'WEBSITES_PORT_CHOICES', ( #WEBSITES_PORT_CHOICES = getattr(settings, 'WEBSITES_PORT_CHOICES', (
(80, 'HTTP'), # (80, 'HTTP'),
(443, 'HTTPS'), # (443, 'HTTPS'),
)) #))
WEBSITES_PROTOCOL_CHOICES = getattr(settings, 'WEBSITES_PROTOCOL_CHOICES', ( WEBSITES_PROTOCOL_CHOICES = getattr(settings, 'WEBSITES_PROTOCOL_CHOICES', (
('http', "HTTP"), ('http', "HTTP"),
('https', "HTTPS"), ('https', "HTTPS"),
('http-https', _("HTTP and HTTPS")), ('http/https', _("HTTP and HTTPS")),
('https-only', _("HTTPS only")), ('https-only', _("HTTPS only")),
)) ))
WEBSITES_DEFAULT_PROTOCOL = getattr(settings, 'WEBSITES_DEFAULT_PROTOCOL', 'http')
WEBSITES_DEFAULT_PORT = getattr(settings, 'WEBSITES_DEFAULT_PORT', 80) 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', WEBSITES_TRAFFIC_IGNORE_HOSTS = getattr(settings, 'WEBSITES_TRAFFIC_IGNORE_HOSTS',
('127.0.0.1',)) ('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',
# '')

View File

@ -0,0 +1,5 @@
def normurlpath(path):
if not path.startswith('/'):
path = '/' + path
path = path.rstrip('/')
return path.replace('//', '/')