#!/usr/bin/python3 # Dependencies: # pip3 install crossplane # https://github.com/nginxinc/crossplane import sys import crossplane import pprint import shlex from subprocess import run import certbot_config as config # Resolve 'include' directive (non-recursive) def resolve_include(response, directive): result = [] for index in directive['includes']: # Join parsed included file to result list result.extend(response['config'][index]['parsed']) return result # Resolve all 'include' directives (recursive) def resolve_includes(response, directives=None): # Assume resolve of first file in response if not specified if directives is None: directives = response['config'][0]['parsed'] # Rebuild directive tree with includes in-place result = [] for directive in directives: if 'includes' in directive: # Resolve include and recursively resolve includes in included file result.extend(resolve_includes(response, resolve_include(response, directive))) continue if 'block' in directive: # Recursively traverse block statement directive['block'] = resolve_includes(response, directive['block']) result.append(directive) return result # Search in directives using matching function def search_directives(directives, fn, recursive=False): result = [] for directive in directives: if fn(directive): # Append directive matched by search function to result list result.append(directive) elif recursive and 'block' in directive: # Recursively search block statement result.extend(search_directives(directive['block'], fn, True)) return result # Recursive search in directives, steered by matching functions def resolve_path(directives, fns): # Pick the first search function in the list and perform a flat search results = search_directives(directives, fns[0]) if len(results) > 0 and len(fns) > 1: # A match for the first path element was found, recurse subtree to find next match return resolve_path(results[0]['block'], fns[1:]) else: # No further matches were found return results # Get all HTTP server directives def get_servers(directives): return resolve_path(directives, [ lambda d: ('directive', 'http') in d.items(), lambda d: ('directive', 'server') in d.items()]) # Generate { server_name: root } dictionary of virtual hosts def get_vhosts(directives): servers = get_servers(directives) vhosts = [] for server in servers: # Retrieve server name directive server_name = search_directives(server['block'], lambda d: ('directive', 'server_name') in d.items()) if len(server_name) == 0: continue for name in server_name[0]['args']: # Apply server alias to vhosts list vhosts.append(name) return vhosts # Parse nginx.conf file with Crossplane response = crossplane.parse(config.nginx_config_file) if response['errors']: pprint.pprint(response['errors']) sys.exit(1) # Resolve all includes (combine into to one big config tree) directives = resolve_includes(response) # Retrieve all virtual hostnames vhosts = [ name for name in get_vhosts(directives) if not name in config.ignored_domains ] if config.primary_domain in vhosts: # Ensure the primary hostname comes first vhosts.remove(config.primary_domain) vhosts.insert(0, config.primary_domain) else: print('Primary domain is missing from configuration') sys.exit(1) print() print('Found virtual hosts:') for name in vhosts: print(' {}'.format(name)) print() # Build certbot command line cmd = config.certbot_command for name in vhosts: cmd.extend(['-d', name]) print('Running', ' '.join(cmd)) print() run(cmd)