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

View file

@ -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',

View file

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

View file

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

View file

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

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

View file

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

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

View file

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

View file

@ -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('<a href="%s">%s</a>' % (url, url))
return '<br>'.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]

View file

@ -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 }}
<VirtualHost {{ ip }}:{{ site.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 %}\
@ -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]
</VirtualHost>"""
))
apache_conf = apache_conf.render(Context(context))
# apache_conf += self.get_protections(site)
</VirtualHost>
""")
).render(Context(context))
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("""\
{
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 !
<Directory %(app_path)s>
Alias %(location)s/ %(app_path)s/
ProxyPass %(location)s/ !
<Directory %(app_path)s/>
Options +ExecCGI
AddHandler fcgid-script .php
FcgidWrapper %(wrapper_path)s
</Directory>
""") % 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("""\
<LocationMatch %s>
SecRuleEngine Off
config.append("SecRuleRemoveById %i" % int(rule))
for modsecurity in directives.get('sec_rule_off', []):
config.append(textwrap.dedent("""\
<Location %s>
SecRuleEngine off
</LocationMatch>\
""") % modsecurity.value
if directives:
directives = '<IfModule mod_security2.c>\n%s\n</IfModule>' % 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(),
})

View file

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

View file

@ -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 = _("<tt>&lt;website path&gt; &lt;destination URL&gt;</tt>")
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 = _("<tt>&lt;website path&gt; &lt;target URL&gt;</tt>")
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 = _("<tt>user [group]</tt>, 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 = _("&lt;error code&gt; &lt;URL/path/message&gt;<br>"
"<tt>&nbsp;500 http://foo.example.com/cgi-bin/tester</tt><br>"
"<tt>&nbsp;404 /cgi-bin/bad_urls.pl</tt><br>"
@ -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 = _("<tt>On</tt> or <tt>Off</tt>, defaults to On")
regex = r'^(On|Off)$'
verbose_name = _("Modsecurity engine")
help_text = _("URL location for disabling modsecurity engine.")
regex = r'^[^ ]+$'
group = SiteDirective.SEC

View file

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

View file

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

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
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',
# '')

View file

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