mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
301 lines
11 KiB
Python
301 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
""" License
|
|
|
|
Copyright (C) 2013 YunoHost
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program; if not, see http://www.gnu.org/licenses
|
|
|
|
"""
|
|
|
|
""" yunohost_domain.py
|
|
|
|
Manage domains
|
|
"""
|
|
import os
|
|
import sys
|
|
import datetime
|
|
import re
|
|
import shutil
|
|
import json
|
|
import yaml
|
|
from urllib import urlopen
|
|
|
|
from moulinette.core import MoulinetteError
|
|
|
|
|
|
def domain_list(auth, filter=None, limit=None, offset=None):
|
|
"""
|
|
List domains
|
|
|
|
Keyword argument:
|
|
filter -- LDAP filter used to search
|
|
offset -- Starting number for domain fetching
|
|
limit -- Maximum number of domain fetched
|
|
|
|
"""
|
|
result_list = []
|
|
|
|
# Set default arguments values
|
|
if offset is None:
|
|
offset = 0
|
|
if limit is None:
|
|
limit = 1000
|
|
if filter is None:
|
|
filter = 'virtualdomain=*'
|
|
|
|
result = auth.search('ou=domains,dc=yunohost,dc=org', filter, ['virtualdomain'])
|
|
|
|
if len(result) > offset and limit > 0:
|
|
for domain in result[offset:offset+limit]:
|
|
result_list.append(domain['virtualdomain'][0])
|
|
return { 'domains': result_list }
|
|
|
|
|
|
def domain_add(auth, domains, main=False, dyndns=False):
|
|
"""
|
|
Create a custom domain
|
|
|
|
Keyword argument:
|
|
domains -- Domain name to add
|
|
main -- Is the main domain
|
|
dyndns -- Subscribe to DynDNS
|
|
|
|
"""
|
|
attr_dict = { 'objectClass' : ['mailDomain', 'top'] }
|
|
ip = str(urlopen('http://ip.yunohost.org').read())
|
|
now = datetime.datetime.now()
|
|
timestamp = str(now.year) + str(now.month) + str(now.day)
|
|
result = []
|
|
|
|
if not isinstance(domains, list):
|
|
domains = [ domains ]
|
|
|
|
for domain in domains:
|
|
if domain in domain_list(auth)['domains']:
|
|
continue
|
|
|
|
# DynDNS domain
|
|
if dyndns:
|
|
if len(domain.split('.')) < 3:
|
|
raise MoulinetteError(22, _("Invalid domain '%s' for DynDNS" % domain))
|
|
import requests
|
|
from yunohost.dyndns import dyndns_subscribe
|
|
|
|
r = requests.get('http://dyndns.yunohost.org/domains')
|
|
dyndomains = json.loads(r.text)
|
|
dyndomain = '.'.join(domain.split('.')[1:])
|
|
if dyndomain in dyndomains:
|
|
if os.path.exists('/etc/cron.d/yunohost-dyndns'):
|
|
raise MoulinetteError(22, _("You already have a DynDNS domain"))
|
|
dyndns_subscribe(domain=domain)
|
|
else:
|
|
raise MoulinetteError(22, _("Unknown DynDNS domain '%s'" % dyndomain))
|
|
|
|
# Commands
|
|
ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA'
|
|
ssl_domain_path = '/etc/yunohost/certs/'+ domain
|
|
with open(ssl_dir +'/serial', 'r') as f:
|
|
serial = f.readline().rstrip()
|
|
try: os.listdir(ssl_domain_path)
|
|
except OSError: os.makedirs(ssl_domain_path)
|
|
|
|
command_list = [
|
|
'cp '+ ssl_dir +'/openssl.cnf '+ ssl_domain_path,
|
|
'sed -i "s/yunohost.org/' + domain + '/g" '+ ssl_domain_path +'/openssl.cnf',
|
|
'openssl req -new -config '+ ssl_domain_path +'/openssl.cnf -days 3650 -out '+ ssl_dir +'/certs/yunohost_csr.pem -keyout '+ ssl_dir +'/certs/yunohost_key.pem -nodes -batch',
|
|
'openssl ca -config '+ ssl_domain_path +'/openssl.cnf -days 3650 -in '+ ssl_dir +'/certs/yunohost_csr.pem -out '+ ssl_dir +'/certs/yunohost_crt.pem -batch',
|
|
'ln -s /etc/ssl/certs/ca-yunohost_crt.pem '+ ssl_domain_path +'/ca.pem',
|
|
'cp '+ ssl_dir +'/certs/yunohost_key.pem '+ ssl_domain_path +'/key.pem',
|
|
'cp '+ ssl_dir +'/newcerts/'+ serial +'.pem '+ ssl_domain_path +'/crt.pem',
|
|
'chmod 755 '+ ssl_domain_path,
|
|
'chmod 640 '+ ssl_domain_path +'/key.pem',
|
|
'chmod 640 '+ ssl_domain_path +'/crt.pem',
|
|
'chmod 600 '+ ssl_domain_path +'/openssl.cnf',
|
|
'chown root:metronome '+ ssl_domain_path +'/key.pem',
|
|
'chown root:metronome '+ ssl_domain_path +'/crt.pem'
|
|
]
|
|
|
|
for command in command_list:
|
|
if os.system(command) != 0:
|
|
raise MoulinetteError(17, _("An error occurred during certificate generation"))
|
|
|
|
try:
|
|
auth.validate_uniqueness({ 'virtualdomain': domain })
|
|
except MoulinetteError:
|
|
raise MoulinetteError(17, _("Domain already created"))
|
|
|
|
|
|
attr_dict['virtualdomain'] = domain
|
|
|
|
try:
|
|
with open('/var/lib/bind/'+ domain +'.zone') as f: pass
|
|
except IOError as e:
|
|
zone_lines = [
|
|
'$TTL 38400',
|
|
domain +'. IN SOA ns.'+ domain +'. root.'+ domain +'. '+ timestamp +' 10800 3600 604800 38400',
|
|
domain +'. IN NS ns.'+ domain +'.',
|
|
domain +'. IN A '+ ip,
|
|
domain +'. IN MX 5 '+ domain +'.',
|
|
domain +'. IN TXT "v=spf1 mx a -all"',
|
|
'ns.'+ domain +'. IN A '+ ip,
|
|
'_xmpp-client._tcp.'+ domain +'. IN SRV 0 5 5222 '+ domain +'.',
|
|
'_xmpp-server._tcp.'+ domain +'. IN SRV 0 5 5269 '+ domain +'.',
|
|
'_jabber._tcp.'+ domain +'. IN SRV 0 5 5269 '+ domain +'.',
|
|
]
|
|
if main:
|
|
zone_lines.extend([
|
|
'pubsub.'+ domain +'. IN A '+ ip,
|
|
'muc.'+ domain +'. IN A '+ ip,
|
|
'vjud.'+ domain +'. IN A '+ ip
|
|
])
|
|
with open('/var/lib/bind/' + domain + '.zone', 'w') as zone:
|
|
for line in zone_lines:
|
|
zone.write(line + '\n')
|
|
|
|
os.system('chown bind /var/lib/bind/' + domain + '.zone')
|
|
|
|
else:
|
|
raise MoulinetteError(17, _("Zone file already exists for ") + domain)
|
|
|
|
conf_lines = [
|
|
'zone "'+ domain +'" {',
|
|
' type master;',
|
|
' file "/var/lib/bind/'+ domain +'.zone";',
|
|
' allow-transfer {',
|
|
' 127.0.0.1;',
|
|
' localnets;',
|
|
' };',
|
|
'};'
|
|
]
|
|
with open('/etc/bind/named.conf.local', 'a') as conf:
|
|
for line in conf_lines:
|
|
conf.write(line + '\n')
|
|
|
|
os.system('service bind9 reload')
|
|
|
|
# XMPP
|
|
try:
|
|
with open('/etc/metronome/conf.d/'+ domain +'.cfg.lua') as f: pass
|
|
except IOError as e:
|
|
conf_lines = [
|
|
'VirtualHost "'+ domain +'"',
|
|
' ssl = {',
|
|
' key = "'+ ssl_domain_path +'/key.pem";',
|
|
' certificate = "'+ ssl_domain_path +'/crt.pem";',
|
|
' }',
|
|
' authentication = "ldap2"',
|
|
' ldap = {',
|
|
' hostname = "localhost",',
|
|
' user = {',
|
|
' basedn = "ou=users,dc=yunohost,dc=org",',
|
|
' filter = "(&(objectClass=posixAccount)(mail=*@'+ domain +'))",',
|
|
' usernamefield = "mail",',
|
|
' namefield = "cn",',
|
|
' },',
|
|
' }',
|
|
]
|
|
with open('/etc/metronome/conf.d/' + domain + '.cfg.lua', 'w') as conf:
|
|
for line in conf_lines:
|
|
conf.write(line + '\n')
|
|
|
|
os.system('mkdir -p /var/lib/metronome/'+ domain.replace('.', '%2e') +'/pep')
|
|
os.system('chown -R metronome: /var/lib/metronome/')
|
|
os.system('chown -R metronome: /etc/metronome/conf.d/')
|
|
os.system('service metronome restart')
|
|
|
|
|
|
# Nginx
|
|
os.system('cp /usr/share/yunohost/yunohost-config/nginx/template.conf /etc/nginx/conf.d/'+ domain +'.conf')
|
|
os.system('mkdir /etc/nginx/conf.d/'+ domain +'.d/')
|
|
os.system('sed -i s/yunohost.org/'+ domain +'/g /etc/nginx/conf.d/'+ domain +'.conf')
|
|
os.system('service nginx reload')
|
|
|
|
if auth.add('virtualdomain=' + domain + ',ou=domains', attr_dict):
|
|
result.append(domain)
|
|
continue
|
|
else:
|
|
raise MoulinetteError(169, _("An error occurred during domain creation"))
|
|
|
|
|
|
os.system('yunohost app ssowatconf > /dev/null 2>&1')
|
|
|
|
msignals.display(_("Domain(s) successfully created."), 'success')
|
|
return { 'domains': result }
|
|
|
|
|
|
def domain_remove(auth, domains):
|
|
"""
|
|
Delete domains
|
|
|
|
Keyword argument:
|
|
domains -- Domain(s) to delete
|
|
|
|
"""
|
|
result = []
|
|
domains_list = domain_list(auth)['domains']
|
|
|
|
if not isinstance(domains, list):
|
|
domains = [ domains ]
|
|
|
|
for domain in domains:
|
|
if domain not in domains_list:
|
|
raise MoulinetteError(22, _("Unknown domain '%s'") % domain)
|
|
|
|
# Check if apps are installed on the domain
|
|
for app in os.listdir('/etc/yunohost/apps/'):
|
|
with open('/etc/yunohost/apps/' + app +'/settings.yml') as f:
|
|
try:
|
|
app_domain = yaml.load(f)['domain']
|
|
except:
|
|
continue
|
|
else:
|
|
if app_domain == domain:
|
|
raise MoulinetteError(1, _("One or more apps are installed on this domain, please uninstall them before proceed to domain removal"))
|
|
|
|
if auth.remove('virtualdomain=' + domain + ',ou=domains'):
|
|
try:
|
|
shutil.rmtree('/etc/yunohost/certs/'+ domain)
|
|
os.remove('/var/lib/bind/'+ domain +'.zone')
|
|
shutil.rmtree('/var/lib/metronome/'+ domain.replace('.', '%2e'))
|
|
os.remove('/etc/metronome/conf.d/'+ domain +'.cfg.lua')
|
|
shutil.rmtree('/etc/nginx/conf.d/'+ domain +'.d')
|
|
os.remove('/etc/nginx/conf.d/'+ domain +'.conf')
|
|
except:
|
|
pass
|
|
with open('/etc/bind/named.conf.local', 'r') as conf:
|
|
conf_lines = conf.readlines()
|
|
with open('/etc/bind/named.conf.local', 'w') as conf:
|
|
in_block = False
|
|
for line in conf_lines:
|
|
if re.search(r'^zone "'+ domain, line):
|
|
in_block = True
|
|
if in_block:
|
|
if re.search(r'^};$', line):
|
|
in_block = False
|
|
else:
|
|
conf.write(line)
|
|
result.append(domain)
|
|
continue
|
|
else:
|
|
raise MoulinetteError(169, _("An error occurred during domain deletion"))
|
|
|
|
os.system('yunohost app ssowatconf > /dev/null 2>&1')
|
|
os.system('service nginx reload')
|
|
os.system('service bind9 reload')
|
|
os.system('service metronome restart')
|
|
|
|
msignals.display(_("Domain(s) successfully deleted."), 'success')
|
|
return { 'domains': result }
|
|
|