feature: the name server's domains have wan.local_fqdn and dns.secondary as their name servers, and a site with dns router gets records of the router's public, internal or external address.
Release / release (push) Successful in 35s
Release / release (push) Successful in 35s
This commit is contained in:
1 parent
c2c33cfca0
commit
7f85a1a2e5
16 files changed
+497
-304
No files matched your search
@@ -820,13 +820,33 @@ def manage_webdav(ctx):
|
||||
run(['systemctl', 'enable', '--now', WEBDAV_UNIT], check=False)
|
||||
|
||||
|
||||
def site_addresses(cfg, ctx):
|
||||
"""The pairs of a host name and the router's address of dns_address of each enabled site with dns router: public,
|
||||
wan.public_address; internal, the internal interface's; external, the external interface's first. A site whose
|
||||
address is not known gets none, with a warning."""
|
||||
addresses = {'public': cfg['wan']['public_address'].strip(), 'internal': ctx['int_addr'],
|
||||
'external': next(iter(ctx['local_addr_list']), '')}
|
||||
pairs = []
|
||||
for site in ctx['sites']:
|
||||
if site['dns'] != 'router':
|
||||
continue
|
||||
address = addresses[site['dns_address']]
|
||||
if not address:
|
||||
warn(f'site {site["name"]}: the router has no {site["dns_address"]} address, so its host names get no '
|
||||
'records')
|
||||
continue
|
||||
pairs += [(host, address) for host in site['hosts']]
|
||||
return pairs
|
||||
|
||||
|
||||
def manage_dns(cfg, ctx):
|
||||
"""Run PowerDNS, the primary name server of the domains of vpnrouter_dns, on the addresses of dns.listen while DNS
|
||||
is enabled, and stop it when not; its packages are installed when they are missing.
|
||||
|
||||
The settings, vpnrouter_dns.PDNS_CONF, are written before the packages are installed, so the server their
|
||||
installation starts answers on those addresses only, beside systemd-resolved's stub on 127.0.0.53. The domains
|
||||
with site records get the host names of the enabled sites, and keep those they have while the sites cannot be read.
|
||||
installation starts answers on those addresses only, beside systemd-resolved's stub on 127.0.0.53. The host names
|
||||
of the enabled sites with dns router get records of the router's address they choose (site_addresses), and the
|
||||
domains keep those they have while the sites cannot be read.
|
||||
"""
|
||||
if not vpnrouter.dns_ready(ctx):
|
||||
if unit_is_active(vpnrouter_dns.UNIT):
|
||||
@@ -854,7 +874,7 @@ def manage_dns(cfg, ctx):
|
||||
os.chown(vpnrouter_dns.PDNS_CONF, 0, grp.getgrnam(vpnrouter_dns.GROUP).gr_gid)
|
||||
vpnrouter_dns.provision()
|
||||
if ctx['sites'] is not None:
|
||||
vpnrouter_dns.apply(cfg, [host for site in ctx['sites'] for host in site['hosts']])
|
||||
vpnrouter_dns.apply(cfg, site_addresses(cfg, ctx))
|
||||
except (OSError, KeyError, vpnrouter_dns.DatabaseError) as exc:
|
||||
warn(f'the name server cannot be set up: {exc}')
|
||||
return
|
||||
|
||||
@@ -55,7 +55,7 @@ OPENVPN_LDAP = pathlib.Path('/etc/openvpn/server/vpn-router-ldap.conf')
|
||||
SCHEMA = {
|
||||
'general': {'platform': 'none', 'mode': 'manual'},
|
||||
'interfaces': {'external': '', 'internal': ''},
|
||||
'wan': {'local_fqdn': ''},
|
||||
'wan': {'local_fqdn': '', 'public_address': ''},
|
||||
'local': {'cidrs': [], 'int_addr': '', 'int_gateway_ip': ''},
|
||||
'site_to_site': {},
|
||||
'p2s': {'enabled': False, 'address_pool': '', 'dns_listen': 'internal', 'dns_domains': []},
|
||||
@@ -70,7 +70,7 @@ SCHEMA = {
|
||||
'acme_staging': False},
|
||||
'oidc': {'enabled': False},
|
||||
'sites': {'enabled': False, 'upload_max_mb': 512, 'webdav': False, 'webdav_port': 7445},
|
||||
'dns': {'enabled': False, 'nameserver': '', 'addresses': [], 'listen': 'external', 'ttl': 300},
|
||||
'dns': {'enabled': False, 'secondary': '', 'listen': 'external', 'ttl': 300},
|
||||
'pki': {'root_name': 'VPN Router Root CA', 'root_days': 7300, 'ca_days': 3650, 'key': 'rsa-4096',
|
||||
'crl_days': 7, 'root_crl_days': 180, 'publish': ['http'], 'p2s_name': 'VPN Router P2S CA',
|
||||
's2s_name': 'VPN Router Site-to-Site CA', 'issuing_cas': {}, 'templates': {},
|
||||
@@ -144,10 +144,14 @@ WEBDAV_LOOPBACK_PORT = 7446
|
||||
#: The largest sites.upload_max_mb, 10 GiB.
|
||||
UPLOAD_MAX_MB = 10240
|
||||
|
||||
#: Settings that no longer exist, section -> options, left out when the file is read so that a file of an earlier
|
||||
#: version loads: the name server is wan.local_fqdn and a site chooses the address of its records.
|
||||
RETIRED = {'dns': ('nameserver', 'addresses')}
|
||||
|
||||
#: The shortest and the longest dns.ttl, in seconds.
|
||||
DNS_TTL_RANGE = (60, 86400)
|
||||
|
||||
#: A DNS name without a wildcard, in lowercase, of dns.nameserver and the domains of the name server.
|
||||
#: A DNS name without a wildcard, in lowercase, of the name servers and the domains of the name server.
|
||||
ZONE_NAME = re.compile(r'^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$')
|
||||
|
||||
#: A domain of p2s.dns_domains, one label or more, and the most domains.
|
||||
@@ -300,6 +304,9 @@ def read_settings(path=None):
|
||||
for section, options in data.items():
|
||||
if not isinstance(options, dict):
|
||||
raise ConfigError(f'cannot parse {path}: {section} is not an object')
|
||||
for section, options in RETIRED.items():
|
||||
for option in options:
|
||||
data.get(section, {}).pop(option, None)
|
||||
return data
|
||||
|
||||
|
||||
@@ -581,14 +588,17 @@ def validate(cfg):
|
||||
raise ConfigError('sites.webdav: WebDAV is served by Caddy at https://<wan.local_fqdn>:<sites.webdav_port>, '
|
||||
'which needs https.enabled and wan.local_fqdn')
|
||||
|
||||
_check_address(_get(cfg, 'wan', 'public_address'), 'wan.public_address')
|
||||
dns = cfg['dns']
|
||||
if dns['enabled']:
|
||||
if not ZONE_NAME.match(dns['nameserver'].strip()):
|
||||
raise ConfigError(f'dns.nameserver: {dns["nameserver"]!r} is not a DNS name in lowercase')
|
||||
if not _get_list(cfg, 'dns', 'addresses'):
|
||||
raise ConfigError('dns.enabled needs dns.addresses, the public addresses of the name server and the sites')
|
||||
for item in _get_list(cfg, 'dns', 'addresses'):
|
||||
_check_address(item, 'dns.addresses')
|
||||
if dns['enabled'] and not (ZONE_NAME.match(_get(cfg, 'wan', 'local_fqdn'))
|
||||
and not re.search(r'\.[0-9]+$', _get(cfg, 'wan', 'local_fqdn'))):
|
||||
raise ConfigError('dns.enabled needs wan.local_fqdn, a DNS name in lowercase: the primary name server of the '
|
||||
'domains')
|
||||
secondary = _get(cfg, 'dns', 'secondary')
|
||||
if secondary and not ZONE_NAME.match(secondary):
|
||||
raise ConfigError(f'dns.secondary: {secondary!r} is not a DNS name in lowercase')
|
||||
if secondary and secondary == _get(cfg, 'wan', 'local_fqdn'):
|
||||
raise ConfigError('dns.secondary: the secondary name server is not wan.local_fqdn, the primary')
|
||||
if dns['listen'] not in LISTEN_CHOICES:
|
||||
raise ConfigError(f'dns.listen: {dns["listen"]!r} is not one of {", ".join(LISTEN_CHOICES)}')
|
||||
if not DNS_TTL_RANGE[0] <= dns['ttl'] <= DNS_TTL_RANGE[1]:
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"""The router's authoritative name server, PowerDNS with its gpgsql backend, for vpn-router-setup and the web service.
|
||||
|
||||
The name server is the primary of its domains and answers on the addresses of the interfaces of dns.listen. The
|
||||
domains, their secondary name servers and the records an administrator added (manual records) are in the
|
||||
vpn-router-dns database, which provision() creates with its role and the password in DATABASE_SECRET, and fills with
|
||||
the schema of pdns-backend-pgsql, PDNS_SCHEMA; DNS_SQL defines the router's own tables and procedures, and the first
|
||||
connection of a process loads it.
|
||||
domains and the records an administrator added (manual records) are in the vpn-router-dns database, which
|
||||
provision() creates with its role and the password in DATABASE_SECRET, and fills with the schema of
|
||||
pdns-backend-pgsql, PDNS_SCHEMA; DNS_SQL defines the router's own tables and procedures, and the first connection of a
|
||||
process loads it.
|
||||
|
||||
apply() writes every domain as a primary zone: its SOA with dns.nameserver, its NS, dns.nameserver and the name
|
||||
servers of its secondaries, the NS of the domains directly below it, A and AAAA records of dns.addresses for its apex,
|
||||
for those name servers inside it that are the router, and, when the domain has site records, for every host name of an
|
||||
enabled site whose closest domain it is, and the manual records, but those of a name and type the records before them
|
||||
have. A secondary's address is notified of every change (ALSO-NOTIFY) and may transfer the zone (ALLOW-AXFR-FROM); the
|
||||
serial of the SOA grows only when the records change.
|
||||
apply() writes every domain as a primary zone: its SOA with wan.local_fqdn as the primary name server, its NS,
|
||||
wan.local_fqdn and dns.secondary, the NS of the domains directly below it, an A or AAAA record for every host name of
|
||||
an enabled site with dns router whose closest domain it is, with the router's address the site chooses, and the manual
|
||||
records, but those of a name and type the records before them have. wan.local_fqdn gets no record in the zones: its
|
||||
record is kept where its own domain is. The addresses dns.secondary resolves to are notified of every change
|
||||
(ALSO-NOTIFY) and may transfer the zones (ALLOW-AXFR-FROM); the serial of the SOA grows only when the records change.
|
||||
|
||||
Every failure of the database raises DatabaseError; a request that cannot be done raises Failure.
|
||||
"""
|
||||
@@ -20,6 +20,7 @@ import contextlib
|
||||
import ipaddress
|
||||
import pathlib
|
||||
import re
|
||||
import socket
|
||||
|
||||
import psycopg
|
||||
from psycopg.rows import dict_row
|
||||
@@ -44,9 +45,8 @@ GROUP = 'pdns'
|
||||
#: The types of the manual records.
|
||||
TYPES = ('A', 'AAAA', 'CNAME', 'TXT', 'MX', 'SRV', 'CAA')
|
||||
|
||||
#: The most domains, secondaries of a domain, and manual records of a domain.
|
||||
#: The most domains, and manual records of a domain.
|
||||
DOMAINS_MAX = 100
|
||||
SECONDARIES_MAX = 8
|
||||
MANUAL_MAX = 1000
|
||||
|
||||
#: The refresh, retry and expire times of the SOA, in seconds.
|
||||
@@ -73,8 +73,7 @@ class DatabaseError(Exception):
|
||||
|
||||
|
||||
class Failure(Exception):
|
||||
"""A request that cannot be done; status is 2 invalid, 3 no such domain, secondary or record, 4 the domain
|
||||
exists."""
|
||||
"""A request that cannot be done; status is 2 invalid, 3 no such domain or record, 4 the domain exists."""
|
||||
|
||||
def __init__(self, message, status=2):
|
||||
super().__init__(message)
|
||||
@@ -211,13 +210,10 @@ def domain(connection, name):
|
||||
|
||||
|
||||
def listing():
|
||||
"""Every domain with its secondaries, its manual records and the records the name server answers for it."""
|
||||
"""Every domain with its manual records and the records the name server answers for it."""
|
||||
with database() as connection:
|
||||
secondaries = connection.execute('SELECT * FROM dns_secondaries_list()').fetchall()
|
||||
manual = connection.execute('SELECT * FROM dns_records_list()').fetchall()
|
||||
return [{**row,
|
||||
'secondaries': [{key: item[key] for key in ('nameserver', 'address')}
|
||||
for item in secondaries if item['domain'] == row['name']],
|
||||
'manual': [{key: item[key] for key in ('name', 'type', 'content', 'ttl')}
|
||||
for item in manual if item['domain'] == row['name']],
|
||||
'records': connection.execute('SELECT * FROM dns_served(%s)', (row['name'],)).fetchall()}
|
||||
@@ -225,71 +221,52 @@ def listing():
|
||||
|
||||
|
||||
def domain_add(body):
|
||||
"""Add the domain {name, sites, ttl?}; the domain."""
|
||||
if not isinstance(body, dict) or not isinstance(body.get('sites', False), bool):
|
||||
raise Failure('a domain is an object of name, sites and ttl')
|
||||
name, sites, ttl = check_domain(body.get('name')), body.get('sites', False), check_ttl(body.get('ttl'))
|
||||
"""Add the domain {name, ttl?}; the domain."""
|
||||
if not isinstance(body, dict) or not set(body) <= {'name', 'ttl'}:
|
||||
raise Failure('a domain is an object of name and ttl')
|
||||
name, ttl = check_domain(body.get('name')), check_ttl(body.get('ttl'))
|
||||
with database() as connection:
|
||||
names = [row['name'] for row in connection.execute('SELECT * FROM dns_domains_list()').fetchall()]
|
||||
if name in names:
|
||||
raise Failure(f'the domain {name} exists', 4)
|
||||
if len(names) >= DOMAINS_MAX:
|
||||
raise Failure(f'the name server has {DOMAINS_MAX} domains')
|
||||
connection.execute('CALL dns_domain_put(%s, %s, %s)', (name, sites, ttl))
|
||||
return {'name': name, 'sites': sites, 'ttl': ttl}
|
||||
connection.execute('CALL dns_domain_put(%s, %s)', (name, ttl))
|
||||
return {'name': name, 'ttl': ttl}
|
||||
|
||||
|
||||
def domain_change(name, body):
|
||||
"""Change the sites and ttl of the domain; the domain."""
|
||||
if not isinstance(body, dict) or not set(body) <= {'sites', 'ttl'} or \
|
||||
not isinstance(body.get('sites', False), bool):
|
||||
raise Failure('a change of a domain is an object of sites and ttl')
|
||||
"""Change the ttl of the domain; the domain."""
|
||||
if not isinstance(body, dict) or not set(body) <= {'ttl'}:
|
||||
raise Failure('a change of a domain is an object of ttl')
|
||||
with database() as connection:
|
||||
row = domain(connection, name)
|
||||
sites, ttl = body.get('sites', row['sites']), check_ttl(body.get('ttl', row['ttl']))
|
||||
connection.execute('CALL dns_domain_put(%s, %s, %s)', (name, sites, ttl))
|
||||
return {'name': name, 'sites': sites, 'ttl': ttl}
|
||||
ttl = check_ttl(body.get('ttl', row['ttl']))
|
||||
connection.execute('CALL dns_domain_put(%s, %s)', (name, ttl))
|
||||
return {'name': name, 'ttl': ttl}
|
||||
|
||||
|
||||
def domain_remove(name):
|
||||
"""Remove the domain with its secondaries and manual records."""
|
||||
"""Remove the domain with its manual records."""
|
||||
with database() as connection:
|
||||
domain(connection, name)
|
||||
connection.execute('CALL dns_domain_remove(%s)', (name,))
|
||||
return {'name': name}
|
||||
|
||||
|
||||
def secondary_add(name, body):
|
||||
"""Add the secondary name server {nameserver, address} of the domain, or set its address; the secondary."""
|
||||
if not isinstance(body, dict):
|
||||
raise Failure('a secondary is an object of nameserver and address')
|
||||
nameserver = body.get('nameserver')
|
||||
if not isinstance(nameserver, str) or not vpnrouter.ZONE_NAME.match(nameserver.strip().lower().rstrip('.')):
|
||||
raise Failure(f'{nameserver!r} is not a name server name')
|
||||
nameserver = nameserver.strip().lower().rstrip('.')
|
||||
def secondary_addresses(cfg):
|
||||
"""The addresses dns.secondary resolves to, which are notified of the zones' changes and may transfer them; none,
|
||||
with a warning, while it is not set or does not resolve."""
|
||||
secondary = cfg['dns']['secondary'].strip()
|
||||
if not secondary:
|
||||
vpnrouter.warn('dns.secondary is not set: the domains have no secondary name server')
|
||||
return []
|
||||
try:
|
||||
address = str(ipaddress.ip_address(str(body.get('address', '')).strip()))
|
||||
except ValueError as exc:
|
||||
raise Failure(f'{body.get("address")!r} is not an IP address') from exc
|
||||
with database() as connection:
|
||||
domain(connection, name)
|
||||
others = [row for row in connection.execute('SELECT * FROM dns_secondaries_list()').fetchall()
|
||||
if row['domain'] == name and row['nameserver'] != nameserver]
|
||||
if len(others) >= SECONDARIES_MAX:
|
||||
raise Failure(f'the domain {name} has {SECONDARIES_MAX} secondaries')
|
||||
connection.execute('CALL dns_secondary_put(%s, %s, %s)', (name, nameserver, address))
|
||||
return {'domain': name, 'nameserver': nameserver, 'address': address}
|
||||
|
||||
|
||||
def secondary_remove(name, nameserver):
|
||||
"""Remove the secondary name server of the domain."""
|
||||
with database() as connection:
|
||||
domain(connection, name)
|
||||
if not any(row['domain'] == name and row['nameserver'] == nameserver
|
||||
for row in connection.execute('SELECT * FROM dns_secondaries_list()').fetchall()):
|
||||
raise Failure(f'no secondary {nameserver} of {name}', 3)
|
||||
connection.execute('CALL dns_secondary_remove(%s, %s)', (name, nameserver))
|
||||
return {'domain': name, 'nameserver': nameserver}
|
||||
found = socket.getaddrinfo(secondary, 53, proto=socket.IPPROTO_UDP)
|
||||
except OSError as exc:
|
||||
vpnrouter.warn(f'dns.secondary {secondary} does not resolve, so it may not transfer the domains: {exc}')
|
||||
return []
|
||||
return list(dict.fromkeys(str(ipaddress.ip_address(item[4][0])) for item in found))
|
||||
|
||||
|
||||
def add(cfg, name, record):
|
||||
@@ -332,25 +309,20 @@ def remove(name, record_name, kind, content):
|
||||
return {'name': full, 'type': kind, 'removed': len(found)}
|
||||
|
||||
|
||||
def records(cfg, row, zones, secondaries, hosts, manual_records):
|
||||
def records(cfg, row, zones, hosts, manual_records):
|
||||
"""The records of the domain row besides its SOA, as (name, type, content, ttl): the NS of the domain and of the
|
||||
domains directly below it, the A and AAAA records of dns.addresses for its apex, dns.nameserver when it is inside
|
||||
and, with site records, each host name whose closest domain it is, the glue of the secondaries inside it, and its
|
||||
manual records. secondaries maps a domain to its secondaries."""
|
||||
domains directly below it, wan.local_fqdn and dns.secondary, the A or AAAA record of each host name of hosts, pairs
|
||||
of a host name and an address, whose closest domain it is, and its manual records."""
|
||||
dns = cfg['dns']
|
||||
zone, nameserver, ttl = row['name'], dns['nameserver'].strip(), row['ttl'] or dns['ttl']
|
||||
addresses = [ipaddress.ip_address(item.strip()) for item in dns['addresses'] if item.strip()]
|
||||
zone, ttl = row['name'], row['ttl'] or dns['ttl']
|
||||
servers = [cfg['wan']['local_fqdn'].strip(), *([dns['secondary'].strip()] if dns['secondary'].strip() else [])]
|
||||
children = [name for name in zones
|
||||
if name != zone and closest([item for item in zones if item != name], name) == zone]
|
||||
found = [(name, 'NS', server, ttl) for name in [zone, *children]
|
||||
for server in [nameserver, *(item['nameserver'] for item in secondaries.get(name, []))]]
|
||||
for name in dict.fromkeys([zone, *([nameserver] if in_zone(zone, nameserver) else []),
|
||||
*(host for host in hosts if row['sites'] and closest(zones, host) == zone)]):
|
||||
found += [(name, 'A' if address.version == 4 else 'AAAA', str(address), ttl) for address in addresses]
|
||||
for item in (item for name in [zone, *children] for item in secondaries.get(name, [])):
|
||||
if in_zone(zone, item['nameserver']) and item['nameserver'] != nameserver:
|
||||
address = ipaddress.ip_address(item['address'])
|
||||
found.append((item['nameserver'], 'A' if address.version == 4 else 'AAAA', str(address), ttl))
|
||||
found = [(name, 'NS', server, ttl) for name in [zone, *children] for server in servers]
|
||||
for host, address in hosts:
|
||||
if closest(zones, host) == zone:
|
||||
address = ipaddress.ip_address(address)
|
||||
found.append((host, 'A' if address.version == 4 else 'AAAA', str(address), ttl))
|
||||
taken = {(name, kind) for name, kind, _, _ in found}
|
||||
found += [(item['name'], item['type'], item['content'], item['ttl']) for item in manual_records
|
||||
if (item['name'], item['type']) not in taken]
|
||||
@@ -358,7 +330,8 @@ def records(cfg, row, zones, secondaries, hosts, manual_records):
|
||||
|
||||
|
||||
def apply(cfg, hosts):
|
||||
"""Make the name server the primary of every domain with records(), and of none while dns.enabled is false."""
|
||||
"""Make the name server the primary of every domain with records(), hosts being the pairs of a host name and an
|
||||
address of the sites with dns router, and of none while dns.enabled is false."""
|
||||
with database() as connection:
|
||||
if not cfg['dns']['enabled']:
|
||||
connection.execute('CALL dns_retain(%s::text[])', ([],))
|
||||
@@ -366,20 +339,21 @@ def apply(cfg, hosts):
|
||||
dns = cfg['dns']
|
||||
domains = connection.execute('SELECT * FROM dns_domains_list()').fetchall()
|
||||
zones = [row['name'] for row in domains]
|
||||
secondaries, manual = {}, {}
|
||||
for item in connection.execute('SELECT * FROM dns_secondaries_list()').fetchall():
|
||||
secondaries.setdefault(item['domain'], []).append(item)
|
||||
for host, _ in hosts:
|
||||
if closest(zones, host) is None:
|
||||
vpnrouter.warn(f'{host} is in none of the domains, so the name server has no record of it')
|
||||
manual = {}
|
||||
for item in connection.execute('SELECT * FROM dns_records_list()').fetchall():
|
||||
manual.setdefault(item['domain'], []).append(item)
|
||||
transfers = secondary_addresses(cfg) if domains else []
|
||||
for row in domains:
|
||||
zone, ttl = row['name'], row['ttl'] or dns['ttl']
|
||||
rows = records(cfg, row, zones, secondaries, hosts, manual.get(zone, []))
|
||||
soa = f'{dns["nameserver"].strip()} hostmaster.{zone} {{serial}} {SOA_TIMES} {ttl}'
|
||||
rows = records(cfg, row, zones, hosts, manual.get(zone, []))
|
||||
soa = f'{cfg["wan"]["local_fqdn"].strip()} hostmaster.{zone} {{serial}} {SOA_TIMES} {ttl}'
|
||||
connection.execute('CALL dns_apply(%s, %s, %s, %s::text[], %s::text[], %s::text[], %s::integer[], '
|
||||
'%s::text[])',
|
||||
(zone, soa, ttl, [item[0] for item in rows], [item[1] for item in rows],
|
||||
[item[2] for item in rows], [item[3] for item in rows],
|
||||
[item['address'] for item in secondaries.get(zone, [])]))
|
||||
[item[2] for item in rows], [item[3] for item in rows], transfers))
|
||||
connection.execute('CALL dns_retain(%s::text[])', (zones,))
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,11 @@ Caddy answers on its port (hosts, the names TLS clients send as SNI; none answer
|
||||
firewall opens the port (listen), and TLS: none (plain HTTP), acme (a certificate of Let's Encrypt), ca (certificates
|
||||
Caddy issues with one of pki.issuing_cas) or file (a certificate and key given with the site). Caddy serves the files of
|
||||
the folder, index_file for a folder, and with spa index_file of the site for any path without a file. editors names
|
||||
the group whose members change the site's files besides the admins, through the web service and WebDAV.
|
||||
the group whose members change the site's files besides the admins, through the web service and WebDAV. dns says who
|
||||
serves the records of its host names: user, a DNS provider outside the router, or router, the router's name server,
|
||||
which gives each of them in the closest of its domains an A or AAAA record of the router's address of dns_address:
|
||||
public (wan.public_address), internal or external. A site may use wan.local_fqdn, on a port of its own; its record
|
||||
stays where the domain of wan.local_fqdn is.
|
||||
|
||||
The sites are served while sites.enabled is true; vpn-router-setup writes Caddy's configuration and opens the ports. A
|
||||
site is changed only through this module, which checks it (check), and its callers then restart vpn-router-setup.
|
||||
@@ -55,6 +59,11 @@ INDEX = re.compile(r'^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$')
|
||||
LISTEN = vpnrouter.LISTEN_CHOICES
|
||||
TLS = ('none', 'acme', 'ca', 'file')
|
||||
|
||||
#: Who serves the records of a site's host names: user, elsewhere, or router, the router's name server, with the
|
||||
#: router's address of DNS_ADDRESSES.
|
||||
DNS = ('user', 'router')
|
||||
DNS_ADDRESSES = ('public', 'internal', 'external')
|
||||
|
||||
#: The most host names of one site.
|
||||
HOSTS_MAX = 16
|
||||
|
||||
@@ -66,7 +75,8 @@ CHUNK = 65536
|
||||
|
||||
#: The settings of a site and their defaults; the type of a default is the required type.
|
||||
SETTINGS = {'enabled': True, 'hosts': [], 'port': 443, 'listen': 'external', 'tls': 'acme', 'ca': '', 'certificate': '',
|
||||
'private_key': '', 'index_file': 'index.html', 'spa': False, 'editors': ''}
|
||||
'private_key': '', 'index_file': 'index.html', 'spa': False, 'editors': '', 'dns': 'user',
|
||||
'dns_address': 'public'}
|
||||
|
||||
#: Whether this process has loaded SITES_SQL.
|
||||
_loaded = False
|
||||
@@ -215,8 +225,17 @@ def check(cfg, sites, site):
|
||||
for host in hosts:
|
||||
if not (vpnrouter_pki.DNS_NAME.match(host) or vpnrouter_pki.is_address(host)):
|
||||
raise Failure(f'hosts: {host!r} is not a host name in lowercase or an IP address', 2)
|
||||
if host == cfg['wan']['local_fqdn'].strip():
|
||||
raise Failure(f'hosts: {host} is wan.local_fqdn, the name of the router\'s own services', 2)
|
||||
if site['dns'] not in DNS:
|
||||
raise Failure(f'dns: {site["dns"]!r} is not one of {", ".join(DNS)}', 2)
|
||||
if site['dns_address'] not in DNS_ADDRESSES:
|
||||
raise Failure(f'dns_address: {site["dns_address"]!r} is not one of {", ".join(DNS_ADDRESSES)}', 2)
|
||||
if site['dns'] == 'router':
|
||||
local_fqdn = cfg['wan']['local_fqdn'].strip()
|
||||
if any(vpnrouter_pki.is_address(host) or host.startswith('*.') or host == local_fqdn for host in hosts):
|
||||
raise Failure('dns router takes host names only, without wildcards and other than wan.local_fqdn, whose '
|
||||
'record is kept where its domain is', 2)
|
||||
if site['dns_address'] == 'public' and not cfg['wan']['public_address'].strip():
|
||||
raise Failure('dns_address public needs wan.public_address', 2)
|
||||
port = site['port']
|
||||
if not 1 <= port <= 65535:
|
||||
raise Failure(f'port: {port} is not a port', 2)
|
||||
@@ -285,7 +304,7 @@ def save(cfg, name, values, create):
|
||||
if site['tls'] != 'ca':
|
||||
site['ca'] = ''
|
||||
check(cfg, rows, site)
|
||||
connection.execute('CALL site_put(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)',
|
||||
connection.execute(f'CALL site_put({", ".join(["%s"] * (len(SETTINGS) + 1))})',
|
||||
tuple(site[key] for key in ('name', *SETTINGS)))
|
||||
ensure_folders([name])
|
||||
return shown(get(connection, name))
|
||||
|
||||
@@ -55,7 +55,7 @@ Pages:
|
||||
and the files of each: list, download, upload, extract a zip archive, create a folder, rename and
|
||||
remove (see Handler.site_files()); a member of a site's editors group may use the site's files
|
||||
/dns the primary name server of the router's domains: its settings, and the domains: add, change and
|
||||
remove, each with its secondaries, the records it serves, and its manual records (see dns())
|
||||
remove, each with the records it serves and its manual records (see dns())
|
||||
/postgresql
|
||||
the databases of each PostgreSQL cluster on the router with their sizes, as JSON (see
|
||||
postgresql_databases()); /config has the clusters' settings
|
||||
|
||||
@@ -10,22 +10,20 @@ from .common import section_change, unit_state
|
||||
def dns(method, path, query, body):
|
||||
"""Answer a /dns request with vpnrouter_dns: the HTTP status and the JSON document.
|
||||
|
||||
GET /dns {"settings", "installed", "running", "domains", "problem"}: the dns section of the
|
||||
configuration, whether PowerDNS is installed and runs, the domains, each {"name", "sites",
|
||||
"ttl", "secondaries", "manual", "records"} with its secondaries {"nameserver", "address"},
|
||||
its manual records and the records the name server serves, and why the domains cannot be
|
||||
read, null when they can
|
||||
PUT /dns change any of the settings of the dns section
|
||||
POST /dns/domains add a domain: {"name", "sites"?, "ttl"?}; sites gives the host names of the enabled sites
|
||||
in it records; ttl is dns.ttl when null or absent; an existing name is 409
|
||||
GET /dns {"settings", "nameserver", "public_address", "installed", "running", "domains",
|
||||
"problem"}: the dns section of the configuration, wan.local_fqdn, the primary name server
|
||||
of the domains, wan.public_address, the address of the records of the sites with
|
||||
dns_address public, whether PowerDNS is installed and runs, the domains, each {"name",
|
||||
"ttl", "manual", "records"} with its manual records and the records the name server
|
||||
serves, and why the domains cannot be read, null when they can
|
||||
PUT /dns change any of the settings of the dns section, and with "public_address"
|
||||
wan.public_address; {"settings", "public_address"}
|
||||
POST /dns/domains add a domain: {"name", "ttl"?}; ttl is dns.ttl when null or absent; an existing name is
|
||||
409
|
||||
PUT /dns/domains/<domain>
|
||||
change "sites" or "ttl" of the domain
|
||||
change "ttl" of the domain
|
||||
DELETE /dns/domains/<domain>
|
||||
remove the domain with its secondaries and manual records
|
||||
POST /dns/domains/<domain>/secondaries
|
||||
add a secondary name server of the domain, or change its address: {"nameserver", "address"}
|
||||
DELETE /dns/domains/<domain>/secondaries/<nameserver>
|
||||
remove the secondary name server
|
||||
remove the domain with its manual records
|
||||
POST /dns/domains/<domain>/records
|
||||
add a manual record: {"name", "type", "content", "ttl"?}; name is relative to the domain,
|
||||
@ for the domain itself, or absolute with a final dot; ttl is the domain's without it
|
||||
@@ -47,22 +45,27 @@ def dns(method, path, query, body):
|
||||
domains = vpnrouter_dns.listing()
|
||||
except vpnrouter_dns.DatabaseError as exc:
|
||||
problem = str(exc)
|
||||
return 200, {'settings': cfg['dns'], 'installed': vpnrouter_dns.PDNS_SERVER.exists(),
|
||||
return 200, {'settings': cfg['dns'], 'nameserver': cfg['wan']['local_fqdn'].strip(),
|
||||
'public_address': cfg['wan']['public_address'].strip(),
|
||||
'installed': vpnrouter_dns.PDNS_SERVER.exists(),
|
||||
'running': unit_state(vpnrouter_dns.UNIT) == 'active', 'domains': domains,
|
||||
'problem': problem}
|
||||
if route == ('PUT', 'dns'):
|
||||
wan = cfg['wan']
|
||||
if 'public_address' in body:
|
||||
wan = section_change('wan', {'public_address': body.pop('public_address')})
|
||||
if isinstance(wan, tuple):
|
||||
return wan
|
||||
changed = section_change('dns', body)
|
||||
return changed if isinstance(changed, tuple) else (200, {'settings': changed})
|
||||
if isinstance(changed, tuple):
|
||||
return changed
|
||||
return 200, {'settings': changed, 'public_address': wan['public_address'].strip()}
|
||||
if route == ('POST', 'dns', 'domains') and len(parts) == 2:
|
||||
document, code = vpnrouter_dns.domain_add(body), 201
|
||||
elif route == ('PUT', 'dns', 'domains') and len(parts) == 3:
|
||||
document, code = vpnrouter_dns.domain_change(parts[2], body), 200
|
||||
elif route == ('DELETE', 'dns', 'domains') and len(parts) == 3:
|
||||
document, code = vpnrouter_dns.domain_remove(parts[2]), 200
|
||||
elif route == ('POST', 'dns', 'domains', 'secondaries') and len(parts) == 4:
|
||||
document, code = vpnrouter_dns.secondary_add(parts[2], body), 201
|
||||
elif route == ('DELETE', 'dns', 'domains', 'secondaries') and len(parts) == 5:
|
||||
document, code = vpnrouter_dns.secondary_remove(parts[2], parts[4]), 200
|
||||
elif route == ('POST', 'dns', 'domains', 'records') and len(parts) == 4:
|
||||
document, code = vpnrouter_dns.add(cfg, parts[2], body), 201
|
||||
elif route == ('DELETE', 'dns', 'domains', 'records') and len(parts) == 4:
|
||||
|
||||
@@ -25,7 +25,8 @@ def sites(method, path, query, body):
|
||||
settings but private_key, with private_key_set, modified and its folder
|
||||
PUT /sites change any of the settings of the sites section
|
||||
POST /sites add a site: {"name", and any of "enabled", "hosts", "port", "listen", "tls", "ca",
|
||||
"certificate", "private_key", "index_file", "spa", "editors"}; an existing name is 409
|
||||
"certificate", "private_key", "index_file", "spa", "editors", "dns",
|
||||
"dns_address"}; an existing name is 409
|
||||
GET /sites/<name> the site
|
||||
PUT /sites/<name> change any of the settings of POST but name; an empty or absent private_key keeps the key
|
||||
DELETE /sites/<name> remove the site, with content=true also its folder
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
A site has a name, which also names its folder in /var/lib/vpn-router/sites, host names, a port, the interfaces whose
|
||||
firewall opens it, TLS (none, acme, ca with --ca, or file with --certificate and --key), the file served for a folder,
|
||||
whether any path without a file is served that file of the site (--spa), and the group whose members change its files.
|
||||
whether any path without a file is served that file of the site (--spa), the group whose members change its files, and
|
||||
who serves the records of its host names (--dns user, elsewhere, or router, the router's name server, with the router's
|
||||
address of --dns-address).
|
||||
A change is applied by restarting vpn-router-setup without waiting for it. The answer is text for people on standard
|
||||
output, and a failure is a message on standard error. With --json the answer is JSON on standard output, and a failure
|
||||
is {"error": ...} there too. The exit status says what failed: 2 the request is invalid, 3 the site does not exist, 4
|
||||
@@ -17,7 +19,8 @@ the site exists, 1 anything else. -h or --help shows the commands and their opti
|
||||
|
||||
options: [--host H ...] [--no-hosts] [--port P] [--listen internal|external|both] [--tls none|acme|ca|file]
|
||||
[--ca NAME] [--certificate FILE] [--key FILE] [--index FILE] [--spa | --no-spa]
|
||||
[--enabled | --no-enabled] [--editors GROUP]
|
||||
[--enabled | --no-enabled] [--editors GROUP] [--dns user|router]
|
||||
[--dns-address public|internal|external]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -73,6 +76,9 @@ def parse(argv):
|
||||
'path without a file')
|
||||
sub.add_argument('--enabled', action=argparse.BooleanOptionalAction, help='serve the site')
|
||||
sub.add_argument('--editors', metavar='GROUP', help="the group whose members change the files; '' for none")
|
||||
sub.add_argument('--dns', choices=vpnrouter_sites.DNS, help='who serves the records of the host names')
|
||||
sub.add_argument('--dns-address', choices=vpnrouter_sites.DNS_ADDRESSES,
|
||||
help="the router's address in the records of dns router")
|
||||
if command == 'remove':
|
||||
sub.add_argument('--content', action='store_true', help='remove the site\'s folder too')
|
||||
return parser.parse_args(argv)
|
||||
@@ -82,7 +88,8 @@ def values_of(options):
|
||||
"""The settings the options give."""
|
||||
values = {key: getattr(options, option) for key, option in (('port', 'port'), ('listen', 'listen'), ('tls', 'tls'),
|
||||
('ca', 'ca'), ('index_file', 'index'), ('spa', 'spa'),
|
||||
('enabled', 'enabled'), ('editors', 'editors'))
|
||||
('enabled', 'enabled'), ('editors', 'editors'),
|
||||
('dns', 'dns'), ('dns_address', 'dns_address'))
|
||||
if getattr(options, option) is not None}
|
||||
if options.no_hosts:
|
||||
values['hosts'] = []
|
||||
@@ -120,9 +127,10 @@ def text_of(answer):
|
||||
sites = answer['sites'] if 'sites' in answer else [answer]
|
||||
if not sites:
|
||||
return 'no sites'
|
||||
head = ('SITE', 'ENABLED', 'HOSTS', 'PORT', 'LISTEN', 'TLS', 'EDITORS')
|
||||
head = ('SITE', 'ENABLED', 'HOSTS', 'PORT', 'LISTEN', 'TLS', 'EDITORS', 'DNS')
|
||||
rows = [(site['name'], 'yes' if site['enabled'] else 'no', ', '.join(site['hosts']) or '*', str(site['port']),
|
||||
site['listen'], site['tls'] + (f' {site["ca"]}' if site['tls'] == 'ca' else ''), site['editors'] or '-')
|
||||
site['listen'], site['tls'] + (f' {site["ca"]}' if site['tls'] == 'ca' else ''), site['editors'] or '-',
|
||||
site['dns'] + (f' {site["dns_address"]}' if site['dns'] == 'router' else ''))
|
||||
for site in sites]
|
||||
widths = [max(len(row[column]) for row in (head, *rows)) for column in range(len(head))]
|
||||
return '\n'.join(' '.join(cell.ljust(width) for cell, width in zip(row, widths)).rstrip() for row in (head, *rows))
|
||||
|
||||
@@ -64,6 +64,14 @@ _vpn_router_site()
|
||||
COMPREPLY=($(compgen -W "$(_vpn_router_site_groups)" -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--dns)
|
||||
COMPREPLY=($(compgen -W 'user router' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--dns-address)
|
||||
COMPREPLY=($(compgen -W 'public internal external' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--certificate | --key)
|
||||
_filedir
|
||||
return
|
||||
@@ -78,7 +86,7 @@ _vpn_router_site()
|
||||
local options='--help'
|
||||
case $command in
|
||||
add | update) options+=' --host --no-hosts --port --listen --tls --ca --certificate --key --index --spa
|
||||
--no-spa --enabled --no-enabled --editors' ;;
|
||||
--no-spa --enabled --no-enabled --editors --dns --dns-address' ;;
|
||||
remove) options+=' --content' ;;
|
||||
esac
|
||||
COMPREPLY=($(compgen -W "$options" -- "$cur"))
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"internal": "eth1"
|
||||
},
|
||||
"wan": {
|
||||
"local_fqdn": "router.example.com"
|
||||
"local_fqdn": "router.example.com",
|
||||
"public_address": ""
|
||||
},
|
||||
"local": {
|
||||
"cidrs": ["10.0.0.0/24"],
|
||||
@@ -89,8 +90,7 @@
|
||||
},
|
||||
"dns": {
|
||||
"enabled": false,
|
||||
"nameserver": "",
|
||||
"addresses": [],
|
||||
"secondary": "",
|
||||
"listen": "external",
|
||||
"ttl": 300
|
||||
},
|
||||
|
||||
@@ -6,21 +6,21 @@ SELECT pg_advisory_xact_lock(hashtext('vpn-router dns schema'));
|
||||
|
||||
DROP TABLE IF EXISTS dns_manual_records;
|
||||
|
||||
-- A domain the router serves as its primary name server; sites says whether the host names of the enabled sites in
|
||||
-- it get records, ttl is the TTL of its records, null for dns.ttl.
|
||||
-- The secondaries of each domain and the domain's switch of site records: the secondary is dns.secondary, the same
|
||||
-- for every domain, and each site says whether the router serves its records.
|
||||
DROP PROCEDURE IF EXISTS dns_secondary_put(text, text, inet);
|
||||
DROP PROCEDURE IF EXISTS dns_secondary_remove(text, text);
|
||||
DROP FUNCTION IF EXISTS dns_secondaries_list();
|
||||
DROP TABLE IF EXISTS dns_secondaries;
|
||||
DROP FUNCTION IF EXISTS dns_domains_list();
|
||||
DROP PROCEDURE IF EXISTS dns_domain_put(text, boolean, integer);
|
||||
|
||||
-- A domain the router serves as its primary name server; ttl is the TTL of its records, null for dns.ttl.
|
||||
CREATE TABLE IF NOT EXISTS dns_domains (
|
||||
name text PRIMARY KEY,
|
||||
sites boolean NOT NULL,
|
||||
ttl integer
|
||||
);
|
||||
|
||||
-- A secondary name server of a domain: its NS name and the address it transfers the domain from.
|
||||
CREATE TABLE IF NOT EXISTS dns_secondaries (
|
||||
domain text NOT NULL REFERENCES dns_domains (name) ON DELETE CASCADE,
|
||||
nameserver text NOT NULL,
|
||||
address inet NOT NULL,
|
||||
PRIMARY KEY (domain, nameserver)
|
||||
);
|
||||
ALTER TABLE dns_domains DROP COLUMN IF EXISTS sites;
|
||||
|
||||
-- A record an administrator added to a domain, besides those of the domain itself and of the sites.
|
||||
CREATE TABLE IF NOT EXISTS dns_records (
|
||||
@@ -39,43 +39,25 @@ CREATE TABLE IF NOT EXISTS dns_state (
|
||||
serial bigint NOT NULL
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION dns_domains_list()
|
||||
RETURNS TABLE (name text, sites boolean, ttl integer)
|
||||
CREATE FUNCTION dns_domains_list()
|
||||
RETURNS TABLE (name text, ttl integer)
|
||||
LANGUAGE sql STABLE AS $$
|
||||
SELECT name, sites, ttl FROM dns_domains ORDER BY name;
|
||||
SELECT name, ttl FROM dns_domains ORDER BY name;
|
||||
$$;
|
||||
|
||||
-- Add the domain, or set its sites and TTL.
|
||||
CREATE OR REPLACE PROCEDURE dns_domain_put(p_name text, p_sites boolean, p_ttl integer)
|
||||
-- Add the domain, or set its TTL.
|
||||
CREATE PROCEDURE dns_domain_put(p_name text, p_ttl integer)
|
||||
LANGUAGE sql AS $$
|
||||
INSERT INTO dns_domains VALUES (p_name, p_sites, p_ttl)
|
||||
ON CONFLICT (name) DO UPDATE SET sites = p_sites, ttl = p_ttl;
|
||||
INSERT INTO dns_domains (name, ttl) VALUES (p_name, p_ttl)
|
||||
ON CONFLICT (name) DO UPDATE SET ttl = p_ttl;
|
||||
$$;
|
||||
|
||||
-- Remove the domain with its secondaries and records.
|
||||
-- Remove the domain with its records.
|
||||
CREATE OR REPLACE PROCEDURE dns_domain_remove(p_name text)
|
||||
LANGUAGE sql AS $$
|
||||
DELETE FROM dns_domains WHERE name = p_name;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION dns_secondaries_list()
|
||||
RETURNS TABLE (domain text, nameserver text, address text)
|
||||
LANGUAGE sql STABLE AS $$
|
||||
SELECT domain, nameserver, host(address) FROM dns_secondaries ORDER BY domain, nameserver;
|
||||
$$;
|
||||
|
||||
-- Add the secondary of the domain, or set its address.
|
||||
CREATE OR REPLACE PROCEDURE dns_secondary_put(p_domain text, p_nameserver text, p_address inet)
|
||||
LANGUAGE sql AS $$
|
||||
INSERT INTO dns_secondaries VALUES (p_domain, p_nameserver, p_address)
|
||||
ON CONFLICT (domain, nameserver) DO UPDATE SET address = p_address;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE dns_secondary_remove(p_domain text, p_nameserver text)
|
||||
LANGUAGE sql AS $$
|
||||
DELETE FROM dns_secondaries WHERE domain = p_domain AND nameserver = p_nameserver;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION dns_records_list()
|
||||
RETURNS TABLE (domain text, name text, type text, content text, ttl integer)
|
||||
LANGUAGE sql STABLE AS $$
|
||||
@@ -97,7 +79,7 @@ LANGUAGE sql AS $$
|
||||
$$;
|
||||
|
||||
-- Serve the zone as its primary and give it the records, its SOA first, whose content has {serial} for the serial,
|
||||
-- and the addresses of its secondaries, which are notified of its changes and may transfer it. The serial grows, to
|
||||
-- and the addresses of its secondary, which are notified of its changes and may transfer it. The serial grows, to
|
||||
-- at least the current Unix time, only when the records differ from those last written.
|
||||
CREATE OR REPLACE PROCEDURE dns_apply(p_zone text, p_soa text, p_ttl integer, p_names text[], p_types text[],
|
||||
p_contents text[], p_ttls integer[], p_secondaries text[])
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
SELECT pg_advisory_xact_lock(hashtext('vpn-router sites schema'));
|
||||
|
||||
-- A static web site: its host names, port, interfaces and TLS, and the group whose members change its files.
|
||||
-- A static web site: its host names, port, interfaces and TLS, the group whose members change its files, and its
|
||||
-- DNS: user, the router serves no records of its host names, or router, its name server serves A or AAAA records of
|
||||
-- them with the router's public, internal or external address (dns_address).
|
||||
CREATE TABLE IF NOT EXISTS sites (
|
||||
name text PRIMARY KEY,
|
||||
enabled boolean NOT NULL,
|
||||
@@ -19,44 +21,60 @@ CREATE TABLE IF NOT EXISTS sites (
|
||||
editors text NOT NULL,
|
||||
modified timestamptz NOT NULL
|
||||
);
|
||||
ALTER TABLE sites ADD COLUMN IF NOT EXISTS dns text NOT NULL DEFAULT 'user' CHECK (dns IN ('user', 'router'));
|
||||
ALTER TABLE sites ADD COLUMN IF NOT EXISTS dns_address text NOT NULL DEFAULT 'public'
|
||||
CHECK (dns_address IN ('public', 'internal', 'external'));
|
||||
|
||||
-- The functions whose columns and the procedure whose arguments changed with dns and dns_address.
|
||||
DROP FUNCTION IF EXISTS sites_list();
|
||||
DROP FUNCTION IF EXISTS site_get(text);
|
||||
DROP FUNCTION IF EXISTS sites_served();
|
||||
DROP PROCEDURE IF EXISTS site_put(text, boolean, text[], integer, text, text, text, text, text, text, boolean, text);
|
||||
|
||||
-- Every site, by name; the private key is left out.
|
||||
CREATE OR REPLACE FUNCTION sites_list()
|
||||
CREATE FUNCTION sites_list()
|
||||
RETURNS TABLE (name text, enabled boolean, hosts text[], port integer, listen text, tls text, ca text,
|
||||
certificate text, index_file text, spa boolean, editors text, modified timestamptz)
|
||||
certificate text, index_file text, spa boolean, editors text, dns text, dns_address text,
|
||||
modified timestamptz)
|
||||
LANGUAGE sql STABLE AS $$
|
||||
SELECT name, enabled, hosts, port, listen, tls, ca, certificate, index_file, spa, editors, modified
|
||||
SELECT name, enabled, hosts, port, listen, tls, ca, certificate, index_file, spa, editors, dns, dns_address, modified
|
||||
FROM sites ORDER BY name;
|
||||
$$;
|
||||
|
||||
-- The site with its private key.
|
||||
CREATE OR REPLACE FUNCTION site_get(p_name text)
|
||||
CREATE FUNCTION site_get(p_name text)
|
||||
RETURNS TABLE (name text, enabled boolean, hosts text[], port integer, listen text, tls text, ca text,
|
||||
certificate text, private_key text, index_file text, spa boolean, editors text, modified timestamptz)
|
||||
certificate text, private_key text, index_file text, spa boolean, editors text, dns text,
|
||||
dns_address text, modified timestamptz)
|
||||
LANGUAGE sql STABLE AS $$
|
||||
SELECT name, enabled, hosts, port, listen, tls, ca, certificate, private_key, index_file, spa, editors, modified
|
||||
SELECT name, enabled, hosts, port, listen, tls, ca, certificate, private_key, index_file, spa, editors, dns,
|
||||
dns_address, modified
|
||||
FROM sites WHERE name = p_name;
|
||||
$$;
|
||||
|
||||
-- The enabled sites with their private keys, by name, for Caddy's configuration and certificate files.
|
||||
CREATE OR REPLACE FUNCTION sites_served()
|
||||
CREATE FUNCTION sites_served()
|
||||
RETURNS TABLE (name text, enabled boolean, hosts text[], port integer, listen text, tls text, ca text,
|
||||
certificate text, private_key text, index_file text, spa boolean, editors text, modified timestamptz)
|
||||
certificate text, private_key text, index_file text, spa boolean, editors text, dns text,
|
||||
dns_address text, modified timestamptz)
|
||||
LANGUAGE sql STABLE AS $$
|
||||
SELECT name, enabled, hosts, port, listen, tls, ca, certificate, private_key, index_file, spa, editors, modified
|
||||
SELECT name, enabled, hosts, port, listen, tls, ca, certificate, private_key, index_file, spa, editors, dns,
|
||||
dns_address, modified
|
||||
FROM sites WHERE enabled ORDER BY name;
|
||||
$$;
|
||||
|
||||
-- Add the site, or replace all its settings.
|
||||
CREATE OR REPLACE PROCEDURE site_put(p_name text, p_enabled boolean, p_hosts text[], p_port integer, p_listen text,
|
||||
p_tls text, p_ca text, p_certificate text, p_private_key text, p_index_file text,
|
||||
p_spa boolean, p_editors text)
|
||||
CREATE PROCEDURE site_put(p_name text, p_enabled boolean, p_hosts text[], p_port integer, p_listen text, p_tls text,
|
||||
p_ca text, p_certificate text, p_private_key text, p_index_file text, p_spa boolean,
|
||||
p_editors text, p_dns text, p_dns_address text)
|
||||
LANGUAGE sql AS $$
|
||||
INSERT INTO sites VALUES (p_name, p_enabled, p_hosts, p_port, p_listen, p_tls, p_ca, p_certificate, p_private_key,
|
||||
p_index_file, p_spa, p_editors, now())
|
||||
INSERT INTO sites (name, enabled, hosts, port, listen, tls, ca, certificate, private_key, index_file, spa, editors,
|
||||
dns, dns_address, modified)
|
||||
VALUES (p_name, p_enabled, p_hosts, p_port, p_listen, p_tls, p_ca, p_certificate, p_private_key, p_index_file,
|
||||
p_spa, p_editors, p_dns, p_dns_address, now())
|
||||
ON CONFLICT (name) DO UPDATE SET enabled = p_enabled, hosts = p_hosts, port = p_port, listen = p_listen,
|
||||
tls = p_tls, ca = p_ca, certificate = p_certificate, private_key = p_private_key, index_file = p_index_file,
|
||||
spa = p_spa, editors = p_editors, modified = now();
|
||||
spa = p_spa, editors = p_editors, dns = p_dns, dns_address = p_dns_address, modified = now();
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE PROCEDURE site_remove(p_name text)
|
||||
|
||||
Reference in new issue
Block a user