web_update.py 8.93 KB
Newer Older
1 2 3 4
# Creates an nginx configuration file so we serve HTTP/HTTPS on all
# domains for which a mail account has been set up.
########################################################################

5
import os.path, re, rtyaml
6 7

from mailconfig import get_mail_domains
8 9
from dns_update import get_custom_dns_config, get_dns_zones
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
10
from utils import shell, safe_domain_name, sort_domains
11

12
def get_web_domains(env, include_www_redirects=True, exclude_dns_elsewhere=True):
13
	# What domains should we serve HTTP(S) for?
14 15
	domains = set()

16
	# Serve web for all mail domains so that we might at least
Joshua Tauberer's avatar
Joshua Tauberer committed
17
	# provide auto-discover of email settings, and also a static website
18 19 20 21 22 23 24 25 26
	# if the user wants to make one.
	domains |= get_mail_domains(env)

	if include_www_redirects:
		# Add 'www.' subdomains that we want to provide default redirects
		# to the main domain for. We'll add 'www.' to any DNS zones, i.e.
		# the topmost of each domain we serve.
		domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env))
	 
27 28 29 30
	if exclude_dns_elsewhere:
		# ...Unless the domain has an A/AAAA record that maps it to a different
		# IP address than this box. Remove those domains from our list.
		domains -= get_domains_with_a_records(env)
31 32 33 34 35

	# Ensure the PRIMARY_HOSTNAME is in the list so we can serve webmail
	# as well as Z-Push for Exchange ActiveSync. This can't be removed
	# by a custom A/AAAA record and is never a 'www.' redirect.
	domains.add(env['PRIMARY_HOSTNAME'])
36

37
	# Sort the list so the nginx conf gets written in a stable order.
38
	domains = sort_domains(domains, env)
39 40

	return domains
41 42 43 44 45

def get_domains_with_a_records(env):
	domains = set()
	dns = get_custom_dns_config(env)
	for domain, rtype, value in dns:
46
		if rtype == "CNAME" or (rtype in ("A", "AAAA") and value not in ("local", env['PUBLIC_IP'])):
47 48 49
			domains.add(domain)
	return domains

50 51 52 53 54 55 56 57 58 59 60 61 62 63
def get_web_domains_with_root_overrides(env):
	# Load custom settings so we can tell what domains have a redirect or proxy set up on '/',
	# which means static hosting is not happening.
	root_overrides = { }
	nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
	if os.path.exists(nginx_conf_custom_fn):
		custom_settings = rtyaml.load(open(nginx_conf_custom_fn))
		for domain, settings in custom_settings.items():
			for type, value in [('redirect', settings.get('redirects', {}).get('/')),
				('proxy', settings.get('proxies', {}).get('/'))]:
				if value:
					root_overrides[domain] = (type, value)
	return root_overrides

64
def do_web_update(env):
65 66 67
	# Pre-load what SSL certificates we will use for each domain.
	ssl_certificates = get_ssl_certificates(env)

68
	# Build an nginx configuration file.
69 70
	nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read()

71
	# Load the templates.
72 73
	template0 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read()
	template1 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-alldomains.conf")).read()
74
	template2 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-primaryonly.conf")).read()
bizonix's avatar
bizonix committed
75
	template3 = "\trewrite ^(.*) https://$REDIRECT_DOMAIN$1 permanent;\n"
76 77

	# Add the PRIMARY_HOST configuration first so it becomes nginx's default server.
78
	nginx_conf += make_domain_config(env['PRIMARY_HOSTNAME'], [template0, template1, template2], ssl_certificates, env)
79 80

	# Add configuration all other web domains.
81
	has_root_proxy_or_redirect = get_web_domains_with_root_overrides(env)
82
	web_domains_not_redirect = get_web_domains(env, include_www_redirects=False)
83
	for domain in get_web_domains(env):
84 85 86 87 88 89 90 91 92
		if domain == env['PRIMARY_HOSTNAME']:
			# PRIMARY_HOSTNAME is handled above.
			continue
		if domain in web_domains_not_redirect:
			# This is a regular domain.
			if domain not in has_root_proxy_or_redirect:
				nginx_conf += make_domain_config(domain, [template0, template1], ssl_certificates, env)
			else:
				nginx_conf += make_domain_config(domain, [template0], ssl_certificates, env)
93
		else:
94 95
			# Add default 'www.' redirect.
			nginx_conf += make_domain_config(domain, [template0, template3], ssl_certificates, env)
96

97 98 99 100 101 102 103
	# Did the file change? If not, don't bother writing & restarting nginx.
	nginx_conf_fn = "/etc/nginx/conf.d/local.conf"
	if os.path.exists(nginx_conf_fn):
		with open(nginx_conf_fn) as f:
			if f.read() == nginx_conf:
				return ""

104
	# Save the file.
105
	with open(nginx_conf_fn, "w") as f:
106 107
		f.write(nginx_conf)

108 109 110 111 112
	# Kick nginx. Since this might be called from the web admin
	# don't do a 'restart'. That would kill the connection before
	# the API returns its response. A 'reload' should be good
	# enough and doesn't break any open connections.
	shell('check_call', ["/usr/sbin/service", "nginx", "reload"])
113

114
	return "web updated\n"
115

116
def make_domain_config(domain, templates, ssl_certificates, env):
117
	# GET SOME VARIABLES
118

119 120
	# Where will its root directory be for static files?
	root = get_web_root(domain, env)
121

122
	# What private key and SSL certificate will we use for this domain?
123
	tls_cert = get_domain_ssl_files(domain, ssl_certificates, env)
124

125
	# ADDITIONAL DIRECTIVES.
126

127
	nginx_conf_extra = ""
128

129 130 131 132 133 134 135 136 137 138 139
	# Because the certificate may change, we should recognize this so we
	# can trigger an nginx update.
	def hashfile(filepath):
		import hashlib
		sha1 = hashlib.sha1()
		f = open(filepath, 'rb')
		try:
			sha1.update(f.read())
		finally:
			f.close()
		return sha1.hexdigest()
140
	nginx_conf_extra += "# ssl files sha1: %s / %s\n" % (hashfile(tls_cert["private-key"]), hashfile(tls_cert["certificate"]))
141

142
	# Add in any user customizations in YAML format.
143
	hsts = "yes"
144 145 146 147 148
	nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
	if os.path.exists(nginx_conf_custom_fn):
		yaml = rtyaml.load(open(nginx_conf_custom_fn))
		if domain in yaml:
			yaml = yaml[domain]
149 150

			# any proxy or redirect here?
151
			for path, url in yaml.get("proxies", {}).items():
152
				nginx_conf_extra += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url)
153
			for path, url in yaml.get("redirects", {}).items():
154
				nginx_conf_extra += "\trewrite %s %s permanent;\n" % (path, url)
155

156 157 158 159 160
			# override the HSTS directive type
			hsts = yaml.get("hsts", hsts)

	# Add the HSTS header.
	if hsts == "yes":
161
		nginx_conf_extra += "add_header Strict-Transport-Security max-age=15768000;\n"
162
	elif hsts == "preload":
163
		nginx_conf_extra += "add_header Strict-Transport-Security \"max-age=15768000; includeSubDomains; preload\";\n"
164

165 166 167
	# Add in any user customizations in the includes/ folder.
	nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf")
	if os.path.exists(nginx_conf_custom_include):
168 169 170 171 172 173 174 175
		nginx_conf_extra += "\tinclude %s;\n" % (nginx_conf_custom_include)
	# PUT IT ALL TOGETHER

	# Combine the pieces. Iteratively place each template into the "# ADDITIONAL DIRECTIVES HERE" placeholder
	# of the previous template.
	nginx_conf = "# ADDITIONAL DIRECTIVES HERE\n"
	for t in templates + [nginx_conf_extra]:
		nginx_conf = re.sub("[ \t]*# ADDITIONAL DIRECTIVES HERE *\n", t, nginx_conf)
176

177 178 179 180
	# Replace substitution strings in the template & return.
	nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT'])
	nginx_conf = nginx_conf.replace("$HOSTNAME", domain)
	nginx_conf = nginx_conf.replace("$ROOT", root)
181 182
	nginx_conf = nginx_conf.replace("$SSL_KEY", tls_cert["private-key"])
	nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", tls_cert["certificate"])
183
	nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain
184

185 186
	return nginx_conf

187
def get_web_root(domain, env, test_exists=True):
188 189 190
	# Try STORAGE_ROOT/web/domain_name if it exists, but fall back to STORAGE_ROOT/web/default.
	for test_domain in (domain, 'default'):
		root = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(test_domain))
191
		if os.path.exists(root) or not test_exists: break
192 193
	return root

194
def get_web_domains_info(env):
195 196
	www_redirects = set(get_web_domains(env)) - set(get_web_domains(env, include_www_redirects=False))
	has_root_proxy_or_redirect = set(get_web_domains_with_root_overrides(env))
197
	ssl_certificates = get_ssl_certificates(env)
198 199

	# for the SSL config panel, get cert status
200
	def check_cert(domain):
201 202 203
		tls_cert = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
		if tls_cert is None: return ("danger", "No Certificate Installed")
		cert_status, cert_status_details = check_certificate(domain, tls_cert["certificate"], tls_cert["private-key"])
204
		if cert_status == "OK":
205
			return ("success", "Signed & valid. " + cert_status_details)
206 207 208 209 210
		elif cert_status == "SELF-SIGNED":
			return ("warning", "Self-signed. Get a signed certificate to stop warnings.")
		else:
			return ("danger", "Certificate has a problem: " + cert_status)

211 212 213 214 215
	return [
		{
			"domain": domain,
			"root": get_web_root(domain, env),
			"custom_root": get_web_root(domain, env, test_exists=False),
216
			"ssl_certificate": check_cert(domain),
217
			"static_enabled": domain not in (www_redirects | has_root_proxy_or_redirect),
218 219
		}
		for domain in get_web_domains(env)
220
	]