diff --git a/Dockerfile b/Dockerfile
index 66f4bee..2f37032 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
# mesudip/python-nginx:alpine is merge of official python and nginx images.
FROM mesudip/python-nginx:alpine
-HEALTHCHECK --interval=10s --timeout=2s --start-period=10s --retries=3 CMD pgrep nginx >> /dev/null || exit 1
+HEALTHCHECK --interval=10s --timeout=2s --start-period=10s --retries=3 CMD pgrep nginx && pgrep python3 >> /dev/null || exit 1
VOLUME ["/etc/nginx/dhparam", "/tmp/acme-challenges/","/etc/nginx/conf.d","/etc/nginx/ssl"]
CMD ["sh","-e" ,"/docker-entrypoint.sh"]
COPY ./requirements.txt /requirements.txt
@@ -14,6 +14,8 @@ RUN apk --no-cache add openssl && \
ARG LETSENCRYPT_API="https://acme-v02.api.letsencrypt.org/directory"
ENV LETSENCRYPT_API=${LETSENCRYPT_API} \
CHALLENGE_DIR=/tmp/acme-challenges/ \
- DHPARAM_SIZE=2048
+ DHPARAM_SIZE=2048 \
+ CLIENT_MAX_BODY_SIZE=1m \
+ DEFAULT_HOST=true
WORKDIR /app
COPY . /app/
\ No newline at end of file
diff --git a/README.md b/README.md
index f870541..fc73be4 100644
--- a/README.md
+++ b/README.md
@@ -2,35 +2,70 @@ Nginx-Proxy
===================================================
Docker container for automatically creating nginx configuration based on active containers in docker host.
-## Basic setup of nginx-proxy
+- Easy server configuration with environment variables
+- Map multiple containers to different locations on same server
+- Automatic Let's Encrypt ssl certificate registration
+- Basic Authorization
+
+## Quick Setup
+### Setup nginx-proxy
```
+docker pull mesudip/nginx-proxy
docker network create frontend; # create a network for nginx proxy
docker run --network frontend \
- --name nginx \
+ --name nginx-proxy \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /etc/ssl:/etc/ssl \
+ -v /etc/ssl/dhparam:/etc/nginx/dhparam \
-p 80:80 \
-p 443:443 \
- mesudip/nginx-proxy
+ -d --restart always mesudip/nginx-proxy
```
-### Volumes
-Following directries can be made into volumes to persist configurations
-- `/etc/nginx/conf.d` nginx configuration directory. You can add your own server configurations here
-- `/etc/nginx/dhparam` the directory for storing DH parameter for ssl connections
-- `/etc/ssl` directory for storing ssl certificates, ssl private key and letsencrypt account key.
-
-## Configuring the container in detail
+### Setup your container
The only thing that matters is that the container shares at least one common network to the nginx container and `VIRTUAL_HOST`
-environment variable is set. If you have multiple exposed ports in the container, don't forget to
-mention the container port too.
+environment variable is set.
+
+Examples:
+- **WordPress**
```
docker run --network frontend \
- --name test-host \
- -e VIRTUAL_HOST="example.com" \
- nginx:alpine
+ --name wordpress-server \
+ -e VIRTUAL_HOST="wordpress.example.com" \
+ wordpress
+```
+ - **Docker Registry**
+ ```
+docker run --network frontend \
+ --name docker-registry \
+ -e VIRTUAL_HOST='registry.example.com/v2 -> /v2; client_max_body_size 2g' \
+ -e PROXY_BASIC_AUTH="registry.example.com -> user1:password,user2:password2,user3:password3"
+ registry:2
```
-### Using the environment `VIRTUAL_HOST`
+Details of Using nginx-proxy
+======================
+ - [Configure `nginx-proxy`](#configure-nginx-proxy)
+ - [Configure enrironment VIRTUAL_HOST in your containers](#configure-environment-virtual_host-in-your-containers)
+ - [WebSockets](#support-for-websocket)
+ - [Multiple hosts in same container](#multiple-virtual-hosts-on-same-container)
+ - [Redirection](#redirection)
+ - [Https and SSL](#ssl-support)
+ - [Basic Authorization](#basic-authorization)
+ - [Default Server](#default-server)
+
+## Configure `nginx-proxy`
+Following directries can be made into volumes to persist configurations
+- `/etc/nginx/conf.d` nginx configuration directory. You can add your own server configurations here
+- `/etc/nginx/dhparam` the directory for storing DH parameter for ssl connections
+- `/etc/ssl` directory for storing ssl certificates, ssl private key and Let's Encrypt account key.
+- `/var/log/nginx` directory nginx logs
+- `/tmp/acme-challenges` directory for storing challenge content when registering Let's Encrypt certificate
+
+Some of the default behaviour of `nginx-proxy` can be changed with environment variables.
+- `DHPARAM_SIZE` Default - `2048` : Set size of dhparam usd for ssl certificates
+- `CLIENT_MAX_BODY_SIZE` Default - `1m` : Set default max body size for all the servers.
+
+## Configure environment `VIRTUAL_HOST` in your containers
When you want a container's to be hosted on a domain set `VIRTUAL_HOST` environment variable to desired `server_name` entry.
For virtual host to work it requires
- nginx-proxy container to be on the same network as that of the container.
@@ -49,7 +84,22 @@ example.com/api | http://example.com/api |/ | exposed
example.com/api -> :8080/api | http://example.com/api | /api | 8080
https://example.com/api/v1:5001 -> :8080/api | https://example.com/api/v1:5001 | /api | 8080
wss://example.com/websocket | wss://example.com/websocket | / | exposed port
-
+
+With `VIRTUAL_HOST` you can inject nginx directives into location each configuration must be separed with a `;`
+. You can see the possible directives in nginx documentation.
+
+**Example :** `VIRTUAL_HOST=somesite.example.com -> :8080 ;proxy_read_timeout 900;client_max_body_size 2g;` will generate configuration as follows
+```nginx.conf
+server{
+ server_name somesite.example.com;
+ listen 80;
+ location /{
+ proxy_read_timeout 900;
+ client_max_body_size 2g;
+ proxy_pass http://127.2.3.4; // your container ip here
+ }
+}
+```
### Support for websocket
Exposing websocket requires the websocket endpoint to be explicitly configured via virtual host. The websocket endpoint can be `ws://` or `wss://`.
If you want to use both websocket and non-websocket endpoints you will have to use multiple hosts
@@ -67,10 +117,17 @@ To have multiple virtual hosts out of single container, you can use `VIRTUAL_HOS
ethereum/client-go \
--rpc --rpcaddr "0.0.0.0" --ws --wsaddr 0.0.0.0
```
+## Redirection
+ Let's say you want to serve a website on `example.uk`. You might want users visiting `www.example.uk`,`example.com`,`www.example.com`
+ to redirect to `example.uk`. You can simply use `PROXY_FULL_REDIRECT` environment variable.
+ ```
+ -e 'VIRTUAL_HOST=https://example.uk -> :7000' \
+ -e 'PROXY_FULL_REDIRECT=example.com,www.example.com,www.example.uk->example.uk'
+ ```
## SSL Support
Issuing of SSL certificate is done using acme-nginx library for Let's Encrypt. If a precheck determines that
-the domain we are trying to issue certificate is not owned by current machine, a self signed certificate is
+the domain we are trying to issue certificate is not owned by current machine, a self-signed certificate is
generated instead.
### Using SSL for exposing endpoint
@@ -85,7 +142,7 @@ If you already have a ssl certificate that you want to use, copy it under the `/
Wildcard certificates can be used. For example to use `*.example.com` wildcard, you should create files
`/etc/ssl/certs/*.example.com.crt` and `/etc/ssl/private/*.example.com.key` in the container's filesystem.
-**Note that `*.blah` or `*` is not a valid wildcard.**
+**Note that `*.com` or `*` is not a valid wildcard.** Wild card must have at least 2 dots.
`/etc/ssl/certs/*.example.com.crt` certificate will :
- be used for `host1.example.com`
@@ -93,7 +150,6 @@ Wildcard certificates can be used. For example to use `*.example.com` wildcard,
- not be used for `xyz.host1.example.com`
- not be used for `example.com`
-
***DHPARAM_SIZE :***
Default size of DH key used for https connection is `2048`bits. The key size can be changed by changing `DHPARAM_SIZE` environment variable
@@ -102,20 +158,27 @@ You can manually obtain Let's encrypt certificate using the nginx-proxy containe
Note that you must set ip in DNS entry to point the correct server.
To issue a certificate for a domain you can simply use this command.
-- `docker exec nginx getssl www.example.com`
+- `docker exec nginx-proxy getssl www.example.com`
Obtained certificate is saved on `/etc/ssl/certs/www.example.com` and private is saved on `/etc/ssl/private/www.example.com`
To issue certificates for multiple domain you can simply add more parameters to the above command
- - `docker exec nginx getssl www.example.com example.com ww.example.com`
+ - `docker exec nginx-proxy getssl www.example.com example.com ww.example.com`
All the domains are registered on the same certificate and the filename is set from the first parameter
passed to the command. so `/etc/ssl/certs/www.example.com` and `/etc/ssl/private/www.example.com` are generated
-Use `docker exec nginx getssl --help` for getting help with the command
+Use `docker exec nginx-proxy getssl --help` for getting help with the command
+
+## Basic Authorization
+Basic Auth can be enabled on the container with environment variable `PROXY_BASIC_AUTH`.
+- `PROXY_BASIC_AUTH=user1:password1,user2:password2,user3:password3` adds basic auth feature to your configured `VIRTUAL_HOST` server root.
+- `PROXY_BASIC_AUTH=example.com/api/v1/admin -> admin1:password1,admin2:password2` adds basic auth only to the location starting from `api/v1/admin`
+
+## Default Server
+When request comes for a server name that is not registered in `nginx-proxy`, It responds with 503 by default.
+If you want the requested to be passed to a container instead, when setting up the container you can add `PROXY_DEFAULT_SERVER=true` environment along with `VIRTUAL_HOST`.
-## Compatibility with jwilder/nginx-proxy
-This nginx-proxy supports `VIRTUAL_HOST` `LETSENCRYPT_HOST` AND `VIRTUAL_PORT` like in jwilder/nginx-proxy.
-But comma separated `VIRTUAL_HOST` is not supported. It's still missing a lot of other feature of jwilder/nginx-proxy
-hopefully they will be available in future versions.
+This much is sufficient for http connections, but for https connections, you might want to setup
+[wildcard certificates](#using-your-own-ssl-certificate) so that your users dont get invalid ssl certificate errors.
diff --git a/acme_nginx/AWSRoute53.py b/acme_nginx/AWSRoute53.py
deleted file mode 100644
index e8a39a2..0000000
--- a/acme_nginx/AWSRoute53.py
+++ /dev/null
@@ -1,91 +0,0 @@
-import boto3
-
-
-class AWSRoute53(object):
- def __init__(self):
- self.session = boto3.Session()
- self.client = self.session.client('route53')
-
- def determine_domain(self, domain):
- """
- Determine registered domain in API and return it's hosted zone id
- Params:
- domain, string, domain name that is be part of account's hosted zones
- Returns:
- zone_id, string, hosted zone id of matching domain
- """
- if not domain.endswith('.'):
- domain = domain + '.'
- # use paginator to iterate over all hosted zones
- paginator = self.client.get_paginator('list_hosted_zones')
- # https://github.com/boto/botocore/issues/1535 result_key_iters is undocumented
- for page in paginator.paginate().result_key_iters():
- for result in page:
- if result['Name'] in domain:
- return result['Id']
-
- def create_record(self, name, data, domain):
- """
- Create TXT DNS record
- Params:
- name, string, record name
- data, string, record data
- domain, string, dns domain
- Return:
- record_id, int, created record id
- """
- zone_id = self.determine_domain(domain)
- if not zone_id:
- raise Exception('Hosted zone for domain {0} not found'.format(domain))
- response = self.client.change_resource_record_sets(
- HostedZoneId=zone_id,
- ChangeBatch={
- 'Changes': [
- {
- 'Action': 'UPSERT',
- 'ResourceRecordSet': {
- 'Name': name,
- 'Type': 'TXT',
- 'TTL': 60,
- 'ResourceRecords': [
- {
- 'Value': '"{0}"'.format(data)
- }
- ]
- }
- }
- ]
- }
- )
- waiter = self.client.get_waiter('resource_record_sets_changed')
- waiter.wait(Id=response['ChangeInfo']['Id'])
- return {'name': name, 'data': data}
-
- def delete_record(self, record, domain):
- """
- Delete TXT DNS record
- Params:
- record, dict, record dict with name, data keys
- domain, string, dns domain
- """
- zone_id = self.determine_domain(domain)
- self.client.change_resource_record_sets(
- HostedZoneId=zone_id,
- ChangeBatch={
- 'Changes': [
- {
- 'Action': 'DELETE',
- 'ResourceRecordSet': {
- 'Name': record['name'],
- 'Type': 'TXT',
- 'TTL': 60,
- 'ResourceRecords': [
- {
- 'Value': '"{0}"'.format(record['data'])
- }
- ]
- }
- }
- ]
- }
- )
diff --git a/acme_nginx/Acme.py b/acme_nginx/Acme.py
index 0353126..70a40e8 100644
--- a/acme_nginx/Acme.py
+++ b/acme_nginx/Acme.py
@@ -220,6 +220,8 @@ def _send_signed_request(self, url, payload=None, directory=None):
except UnicodeDecodeError:
pass
return resp.getcode(), resp_data, resp.headers
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
return getattr(e, "code", None), \
getattr(e, "read", e.__str__)(), \
diff --git a/acme_nginx/AcmeV1.py b/acme_nginx/AcmeV1.py
index 0c2a50b..12c000a 100644
--- a/acme_nginx/AcmeV1.py
+++ b/acme_nginx/AcmeV1.py
@@ -21,6 +21,8 @@ def register_account(self):
try:
self.log.info('trying to create account key {0}'.format(self.account_key))
account_key = self.create_key(self.account_key)
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
self.log.error('creating key {0} {1}'.format(type(e).__name__, e))
sys.exit(1)
@@ -44,6 +46,8 @@ def get_certificate(self):
self.register_account()
try:
self.create_key(self.domain_key)
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
self.log.error('creating key {0} {1}'.format(type(e).__name__, e))
sys.exit(1)
@@ -66,6 +70,8 @@ def get_certificate(self):
self.log.info('adding nginx virtual host and completing challenge')
try:
self._write_challenge(token, thumbprint)
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
self.log.error('error adding virtual host {0} {1}'.format(type(e).__name__, e))
sys.exit(1)
@@ -97,6 +103,8 @@ def get_certificate(self):
chain_str = urlopen(self.chain).read()
if chain_str:
chain_str = chain_str.decode('utf8')
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
self.log.error('error getting chain: {0} {1}'.format(type(e).__name__, e))
sys.exit(1)
@@ -111,6 +119,8 @@ def get_certificate(self):
)))
)
fd.write(chain_str)
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
self.log.error('error writing cert: {0} {1}'.format(type(e).__name__, e))
sys.exit(1)
diff --git a/acme_nginx/AcmeV2.py b/acme_nginx/AcmeV2.py
index 0e46b47..9804fa7 100644
--- a/acme_nginx/AcmeV2.py
+++ b/acme_nginx/AcmeV2.py
@@ -2,13 +2,10 @@
import json
import re
import sys
-try:
- from urllib.request import urlopen, Request # Python 3
-except ImportError:
- from urllib2 import urlopen, Request # Python 2
+from urllib.request import urlopen, Request # Python 3
+
from .Acme import Acme
from .DigitalOcean import DigitalOcean
-from .AWSRoute53 import AWSRoute53
class AcmeV2(Acme):
@@ -22,6 +19,8 @@ def register_account(self):
try:
self.log.info('trying to create account key {0}'.format(self.account_key))
self.create_key(self.account_key)
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
self.log.error('creating key {0} {1}'.format(type(e).__name__, e))
sys.exit(1)
@@ -50,6 +49,8 @@ def register_account(self):
try:
self.log.info('trying to create domain key')
self.create_key(self.domain_key)
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
self.log.error('creating key {0} {1}'.format(type(e).__name__, e))
sys.exit(1)
@@ -73,6 +74,8 @@ def _sign_certificate(self, order, directory):
try:
with open(self.cert_path, 'w') as fd:
fd.write(certificate_pem)
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
self.log.error('error writing cert: {0} {1}'.format(type(e).__name__, e))
if not self.skip_nginx_reload:
@@ -110,6 +113,8 @@ def solve_http_challenge(self, directory):
self.log.info('adding nginx virtual host and completing challenge')
try:
self._write_challenge(token, thumbprint)
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
self.log.error('error adding virtual host {0} {1}'.format(type(e).__name__, e))
sys.exit(1)
@@ -160,6 +165,8 @@ def solve_dns_challenge(self, directory, client):
domain=domain,
name='_acme-challenge.{0}.'.format(domain.lstrip('*.').rstrip('.')),
data=txt_record)
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
self.log.error('error creating dns record')
self.log.error(e)
@@ -182,6 +189,8 @@ def solve_dns_challenge(self, directory, client):
if not self.debug:
self.log.info('delete dns record')
client.delete_record(domain=domain, record=record)
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
self.log.error('error deleting dns record')
self.log.error(e)
@@ -192,8 +201,6 @@ def get_certificate(self):
if self.dns_provider:
if self.dns_provider == 'digitalocean':
dns_client = DigitalOcean()
- elif self.dns_provider == 'route53':
- dns_client = AWSRoute53()
self.solve_dns_challenge(directory, dns_client)
else:
self.solve_http_challenge(directory)
diff --git a/acme_nginx/Readme.md b/acme_nginx/Readme.md
new file mode 100644
index 0000000..d4252e4
--- /dev/null
+++ b/acme_nginx/Readme.md
@@ -0,0 +1,4 @@
+Acme Interaction for Let's Encrypt certificate
+==============================================
+Contents copied from [kshcherban/acme-nginx](https://github.com/kshcherban/acme-nginx) and modified to work in alpine-linux and added failure recovery instead of exit(1)
+
\ No newline at end of file
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
index b71c233..c3cda5e 100755
--- a/docker-entrypoint.sh
+++ b/docker-entrypoint.sh
@@ -1,8 +1,7 @@
#!/usr/bin/env sh
mkdir -p /etc/nginx/dhparam
-if ! openssl dhparam -in /etc/nginx/dhparam/dhparam.pem > /dev/null 2>&1
-then
- echo "Generating new DH Parameters for SSL as it's missing"
- openssl dhparam -out /etc/nginx/dhparam/dhparam.pem ${DHPARAM_SIZE:-2048}
+
+if ! openssl dhparam -in /etc/nginx/dhparam/dhparam.pem >/dev/null 2>&1; then
+ openssl dhparam -out /etc/nginx/dhparam/dhparam.pem ${DHPARAM_SIZE:-2048}
fi
-python3 -u /app/main.py
\ No newline at end of file
+exec python3 -u main.py
diff --git a/easy_debug.sh b/easy_debug.sh
new file mode 100755
index 0000000..11c82e8
--- /dev/null
+++ b/easy_debug.sh
@@ -0,0 +1,26 @@
+#!/bin/sh
+#When working on the project locally, you want to debug things running inside docker container
+# like a normal python script. This helps achieve that by creating proper directory mapping in the container
+# and then starting the container with pydevd enabled.
+#
+if ! docker network inspect frontend >>/dev/null; then
+ docker network create frontend >>/dev/null
+fi
+IMAGE_NAME="mesudip/nginx-proxy:local-debug"
+echo "Started Docker build. This will take a while if you have changed requirements.txt"
+docker build -t "$IMAGE_NAME" --build-arg WORK_DIR="$(pwd)" . >>/dev/null
+docker rm --force mesudip-nginx-local-debug >/dev/null
+docker run -d \
+ -v /var/run/docker.sock:/var/run/docker.sock:ro \
+ -v /etc/ssl:/etc/ssl \
+ -v "$(pwd):$(pwd)" \
+ -v /etc/ssl/dhparam:/etc/nginx/dhparam \
+ -v /tmp/mesdip-nginx-conf:/etc/nginx/conf.d \
+ -e PYTHON_DEBUG_ENABLE=true -e PYTHON_DEBUG_PORT=5678 \
+ -p 80:80 -p 443:443 \
+ --entrypoint /bin/sh \
+ --name mesudip-nginx-local-debug \
+ "$IMAGE_NAME" -c "cd $(pwd) && ./docker-entrypoint.sh"
+docker network connect frontend mesudip-nginx-local-debug
+echo "Container started :) "
+docker logs -f mesudip-nginx-local-debug
diff --git a/main.py b/main.py
index 39e350f..03b1c5c 100644
--- a/main.py
+++ b/main.py
@@ -1,12 +1,31 @@
import os
import re
+import signal
import subprocess
import sys
+import traceback
import docker
-from nginx_proxy import WebServer as containers
+from nginx_proxy.WebServer import WebServer
+server = None
+
+
+# Handle exit signal to respond to stop command.
+def receiveSignal(signalNumber, frame):
+ global server
+ if signalNumber == 15:
+ print("\nShutdown Requested")
+ if server is not None:
+ server.cleanup()
+ server = None
+ sys.exit(0)
+
+
+signal.signal(signal.SIGTERM, receiveSignal)
+
+# Enable pydevd for debugging locally.
debug_config = {}
if "PYTHON_DEBUG_PORT" in os.environ:
if os.environ["PYTHON_DEBUG_PORT"].strip():
@@ -19,11 +38,12 @@
debug_config["host"] = re.findall("([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)+",
subprocess.run(["ip", "route"],
stdout=subprocess.PIPE).stdout.decode().split(
- "\n")[0])[0]
+ "\n")[0])[0]
if len(debug_config):
import pydevd
+ print("Starting nginx-proxy in debug mode. Trying to connect to debug server ", str(debug_config))
pydevd.settrace(stdoutToServer=True, stderrToServer=True, **debug_config)
# fix for https://trello.com/c/dMG5lcTZ
@@ -31,9 +51,10 @@
client = docker.from_env()
client.version()
except Exception as e:
- print("There was error connecting with the docker server \nHave you correctly mounted /var/run/docker.sock?\n"+str(e.args),file=sys.stderr)
+ print(
+ "There was error connecting with the docker server \nHave you correctly mounted /var/run/docker.sock?\n" + str(
+ e.args), file=sys.stderr)
sys.exit(1)
-hosts = containers.WebServer(client)
def eventLoop():
@@ -46,8 +67,12 @@ def eventLoop():
process_network_event(event["Action"], event)
elif eventType == "container":
process_container_event(event["Action"], event)
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
- print("Unexpected error :" + e.__class__.__name__ + str(e))
+ print("Unexpected error :" + e.__class__.__name__ + ' -> ' + str(e), file=sys.stderr)
+ traceback.print_exc(limit=10)
+
def process_service_event(action, event):
if action == "create":
@@ -57,10 +82,10 @@ def process_service_event(action, event):
def process_container_event(action, event):
if action == "start":
# print("container started", event["id"])
- hosts.update_container(event["id"])
+ server.update_container(event["id"])
elif action == "die":
# print("container died", event["id"])
- hosts.remove_container(event["id"])
+ server.remove_container(event["id"])
def process_network_event(action, event):
@@ -70,18 +95,21 @@ def process_network_event(action, event):
elif "container" in event["Actor"]["Attributes"]:
if action == "disconnect":
# print("network disconnect")
- hosts.disconnect(network=event["Actor"]["ID"], container=event["Actor"]["Attributes"]["container"],
- scope=event["scope"])
+ server.disconnect(network=event["Actor"]["ID"], container=event["Actor"]["Attributes"]["container"],
+ scope=event["scope"])
elif action == "connect":
# print("network connect")
- hosts.connect(network=event["Actor"]["ID"], container=event["Actor"]["Attributes"]["container"],
- scope=event["scope"])
+ server.connect(network=event["Actor"]["ID"], container=event["Actor"]["Attributes"]["container"],
+ scope=event["scope"])
elif action == "destroy":
# print("network destryed")
pass
try:
+ server = WebServer(client)
eventLoop()
except (KeyboardInterrupt, SystemExit):
- hosts.cleanup();
+ print("-------------------------------\nPerforming Graceful ShutDown !!")
+ if server is not None:
+ server.cleanup()
diff --git a/nginx/Config.py b/nginx/Config.py
index 30c7c16..6fc395b 100644
--- a/nginx/Config.py
+++ b/nginx/Config.py
@@ -1,13 +1,117 @@
-class Block:
+import re
+
+
+class ConfigNode():
def __init__(self):
- self.parameters=[]
- self.contents=[]
+ self._parent_block = None
+ self.offset_char = " "
+
+ def is_block(self):
+ return False
+
+ def is_direction(self):
+ return False
+
+
+class Block(ConfigNode):
+ def __init__(self, name, parameters, contents):
+ super().__init__()
+ self.name = name
+ self.parameters = parameters
+ self.contents = contents
+
+ def __del__(self):
+ if self._parent_block:
+ self._parent_block.delete(self)
+
+ def append(self, data):
+ data._parent_block = self
+ self.contents.append(data)
+
+ def is_block(self):
+ return True
+
+ def is_direction(self):
+ return False
+
+ def delete(self, content):
+ self.contents.remove(content)
+ def __str__(self, offset=-1, sep=" ", gen_block_name=False):
-class Direction:
- def __init__(self,name,value):
+ data = "".join([c.__str__(offset=offset + 1, sep=sep, gen_block_name=True) for c in self.contents])
+ if gen_block_name:
+ return '%(offset)s%(name)s %(param)s {\n%(data)s%(offset)s}\n' % {
+ 'offset': sep * offset,
+ 'name': self.name,
+ 'data': data,
+ 'param': " ".join(self.parameters) if type(self.parameters) is not str else self.parameters}
+ else:
+ return data
+ def __len__(self):
+ return len(self.contents)
+ def __getitem__(self, item):
+ if type(item) is str:
+ for x in self.contents:
+ if x.name == item:
+ yield x
+ if type(item) is tuple:
+ for x in self.contents:
+ if x.name==item[0]:
+ type(x is Block)
+
+
+ def __setitem__(self, key, value):
+ pass
+ def __delitem__(self, key):
+ pass
+ def __iter__(self):
+ pass
+ def __contains__(self, item):
+ pass
+
+ def __repr__(self):
+ return self.name + "{}"
+
+
+class Direction(ConfigNode):
+ def __init__(self, name, value):
+ super().__init__()
self.name = name
- if type(value) in (tuple,list):
- self.values=value
+ if type(value) in (tuple, list):
+ self.values = [x for x in value]
else:
- self.values=[value]
+ self.values = [value]
+
+ def __del__(self):
+ if self._parent_block:
+ self._parent_block.delete(self)
+
+ def __hash__(self):
+ return hash(self.name, self.values)
+
+ def is_block(self):
+ return False
+
+ def is_direction(self):
+ return True
+
+ def __str__(self, offset=0, __values=None, sep=" ", gen_block_name=False):
+
+ return sep * offset + self.name + " " + " ".join(self.values) + ";\n"
+ # if not __values:
+ # block = self.values
+ # else:
+ # block = __values
+ # if isinstance(block, tuple):
+ # if len(block) == 1 and type(block[0]) == str: # single param
+ # return sep * offset + '%s;\n' % (block[0])
+ # elif isinstance(block[1], str):
+ # return sep * offset + '%s %s;\n' % (block[0], block[1])
+ # else: # multiline
+ # return sep * offset + '%s %s;\n' % (block[0],
+ # self.__str__(block[1], offset + len(
+ # block[0]) + 1).rstrip())
+
+ def __repr__(self):
+ return "<" + self.name + ">"
diff --git a/nginx/ConfigParser.py b/nginx/ConfigParser.py
index adf8206..ab50102 100644
--- a/nginx/ConfigParser.py
+++ b/nginx/ConfigParser.py
@@ -21,10 +21,11 @@
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
======================================================================================================
'''
+from nginx.Config import *
class ConfigParser:
def __init__(self, offset_char=' '):
- self.i = 0 #char iterator for parsing
+ self.i = 0 # char iterator for parsing
self.length = 0
self.config = ''
self.data = []
@@ -42,152 +43,11 @@ def __delitem__(self, index):
def __call__(self):
return self.gen_config()
- def get_value(self, data):
- if isinstance(data, tuple):
- return data[1]
- elif isinstance(data, dict):
- return data['value']
- else:
- return data
-
- def get_name(self, data):
- if isinstance(data, tuple):
- return data[0]
- elif isinstance(data, dict):
- return data['name']
- else:
- return data
-
- def set(self, item_arr, value=None, param=None, name=None):
- if isinstance(item_arr, str):
- elem = item_arr
- parent = self.data
- elif isinstance(item_arr, list) and len(item_arr) == 1:
- elem = item_arr[0]
- parent = self.data
- else:
- elem = item_arr.pop()
- parent = self.get_value(self.get(item_arr))
-
- if parent is None:
- raise KeyError('No such block.')
-
- if isinstance(elem, str) and isinstance(value, str):
- #modifying text parameter
- for i, param in enumerate(parent):
- if isinstance(param, tuple):
- if param[0] == elem:
- if value is not None and name is not None:
- parent[i] = (name, value)
- return
- elif value is not None:
- parent[i] = (param[0], value)
- return
- elif name is not None:
- parent[i] = (name, param[1])
- return
- raise TypeError('Not expected value type')
-
- elif isinstance(elem, tuple):
- #modifying block
- if len(elem) == 1:
- elem = (elem[0], '')
- for i, block in enumerate(parent):
- if isinstance(block, dict):
- if elem == (block['name'], block['param']):
- if value is not None and isinstance(value, list):
- parent[i]['value'] = value
- return
- if param is not None and isinstance(param, str):
- parent[i]['param'] = param
- return
- if name is not None and isinstance(name, str):
- parent[i]['name'] = name
- return
- raise TypeError('Not expected value type')
- raise KeyError('No such parameter.')
-
- def get(self, item_arr, data=[]):
- if data == []:
- data = self.data
- if type(item_arr) in [str, tuple]:
- item = item_arr
- elif isinstance(item_arr, list):
- if len(item_arr) == 1:
- item = item_arr[0]
- else:
- element = item_arr.pop(0)
- if isinstance(element, tuple):#cannot be a string
- if len(element) == 1:
- element = (element[0], '')
- for i, data_elem in enumerate(data):
- if isinstance(data_elem, dict):
- if (data_elem['name'], data_elem['param']) == element:
- return self.get(item_arr, self.get_value(data[i]))
-
- if not 'item' in locals():
- raise KeyError('Error while getting parameter.')
- if isinstance(item, str):
- for i, elem in enumerate(data):
- if isinstance(elem, tuple):
- if elem[0] == item:
- return data[i]
- elif isinstance(item, tuple):
- if len(item) == 1:
- item = (item[0], '')
- for i, elem in enumerate(data):
- if isinstance(elem, dict):
- if (elem['name'], elem['param']) == item:
- return data[i]
- return None
-
- def append(self, item, root=[], position=None):
- if root == []:
- root = self.data
- elif root is None:
- raise AttributeError('Root element is None')
- if position:
- root.insert(position, item)
- else:
- root.append(item)
-
- def remove(self, item_arr, data=[]):
- if data == []:
- data = self.data
- if type(item_arr) in [str, tuple]:
- item = item_arr
- elif isinstance(item_arr, list):
- if len(item_arr) == 1:
- item = item_arr[0]
- else:
- elem = item_arr.pop(0)
- if type(elem) in [tuple,str]:
- self.remove(item_arr, self.get_value(self.get(elem, data)))
- return
-
- if isinstance(item, str):
- for i,elem in enumerate(data):
- if isinstance(elem, tuple):
- if elem[0] == item:
- del data[i]
- return
- elif isinstance(item, tuple):
- if len(item) == 1:
- item = (item[0], '')
- for i,elem in enumerate(data):
- if isinstance(elem, dict):
- if (elem['name'], elem['param']) == item:
- del data[i]
- return
- else:
- raise AttributeError("Unknown item type '%s' in item_arr" % item.__class__.__name__)
- raise KeyError('Unable to remove')
-
def load(self, config):
self.config = config
self.length = len(config) - 1
self.i = 0
- self.data = self.parse_block()
+ self.data = self.parse_block("","")
def loadf(self, filename):
with open(filename, 'r') as f:
@@ -199,13 +59,14 @@ def savef(self, filename):
conf = self.gen_config()
f.write(conf)
- def parse_block(self):
+ def parse_block(self,name,parameters):
data = []
param_name = None
param_value = None
buf = ''
+ block=Block(name,parameters,[])
while self.i < self.length:
- if self.config[self.i] == '\n': #multiline value
+ if self.config[self.i] == '\n': # multiline value
if buf and param_name:
if param_value is None:
param_value = []
@@ -223,43 +84,45 @@ def parse_block(self):
else:
param_value = buf.strip()
if param_name:
- data.append((param_name, param_value))
+ block.append(Direction(param_name,param_value))
else:
- data.append((param_value,))
+ block.append(Direction(param_value,[]))
param_name = None
param_value = None
buf = ''
elif self.config[self.i] == '{':
self.i += 1
- block = self.parse_block()
- data.append({'name':param_name, 'param':buf.strip(), 'value':block})
+ _block = self.parse_block(param_name,"")
+ _block.parameters=buf.strip()
+ block.append(_block)
param_name = None
param_value = None
buf = ''
elif self.config[self.i] == '}':
self.i += 1
- return data
- elif self.config[self.i] == '#': #skip comments
+ return block
+ elif self.config[self.i] == '#': # skip comments
while self.i < self.length and self.config[self.i] != '\n':
self.i += 1
else:
buf += self.config[self.i]
self.i += 1
- return data
+ return block
def gen_block(self, blocks, offset):
- subrez = '' # ready to return string
+ subrez = '' # ready to return string
block_name = None
block_param = ''
for i, block in enumerate(blocks):
if isinstance(block, tuple):
- if len(block) == 1 and type(block[0]) == str: #single param
+ if len(block) == 1 and type(block[0]) == str: # single param
subrez += self.off_char * offset + '%s;\n' % (block[0])
elif isinstance(block[1], str):
subrez += self.off_char * offset + '%s %s;\n' % (block[0], block[1])
- else: #multiline
+ else: # multiline
subrez += self.off_char * offset + '%s %s;\n' % (block[0],
- self.gen_block(block[1], offset + len(block[0]) + 1).rstrip())
+ self.gen_block(block[1], offset + len(
+ block[0]) + 1).rstrip())
elif isinstance(block, dict):
block_value = self.gen_block(block['value'], offset + 4)
@@ -270,10 +133,12 @@ def gen_block(self, blocks, offset):
if subrez != '':
subrez += '\n'
subrez += '%(offset)s%(name)s %(param)s{\n%(data)s%(offset)s}\n' % {
- 'offset':self.off_char * offset, 'name':block['name'], 'data':block_value,
- 'param':param}
+ 'offset': self.off_char * offset,
+ 'name': block['name'],
+ 'data': block_value,
+ 'param': param}
- elif isinstance(block, str): #multiline params
+ elif isinstance(block, str): # multiline params
if i == 0:
subrez += '%s\n' % block
else:
@@ -281,8 +146,8 @@ def gen_block(self, blocks, offset):
if block_name:
return '%(offset)s%(name)s %(param)s{\n%(data)s%(offset)s}\n' % {
- 'offset':self.off_char * offset, 'name':block_name, 'data':subrez,
- 'param':block_param}
+ 'offset': self.off_char * offset, 'name': block_name, 'data': subrez,
+ 'param': block_param}
else:
return subrez
diff --git a/nginx/Nginx.py b/nginx/Nginx.py
index 5e59ebe..c1433e5 100644
--- a/nginx/Nginx.py
+++ b/nginx/Nginx.py
@@ -2,14 +2,16 @@
import os
import pathlib
import random
-import re
import string
import subprocess
import sys
from os import path
+from typing import Union, Tuple
import requests
+from nginx import Url
+
class Nginx:
command_config_test = ["nginx", "-t"]
@@ -69,7 +71,7 @@ def pop_config(self):
file.write(self.config_stack.pop())
return self.reload()
- def forced_update(self, config_str):
+ def force_start(self, config_str):
"""
Simply reload the nginx with the configuration, don't check whether or not configuration is changed or not.
If change causes nginx to fail, revert to last working config.
@@ -95,44 +97,54 @@ def update_config(self, config_str) -> bool:
if config_str == self.last_working_config:
print("Configuration not changed, skipping nginx reload")
return False
- diff = str.join("\n", difflib.unified_diff(self.last_working_config.splitlines(),
- config_str.splitlines(),
- fromfile='Old Config',
- tofile='New Config',
- lineterm='\n'))
+
with open(self.config_file_path, "w") as file:
file.write(config_str)
- if not self.reload():
+ result, data = self.reload(return_error=True)
+ if not result:
+ diff = str.join("\n", difflib.unified_diff(self.last_working_config.splitlines(),
+ config_str.splitlines(),
+ fromfile='Old Config',
+ tofile='New Config',
+ lineterm='\n'))
print(diff, file=sys.stderr)
- print("ERROR: Above change made nginx to fail. Thus it's rolled back", file=sys.stderr)
-
+ if data is not None:
+ print(data, file=sys.stderr)
+ print("ERROR: New change made nginx to fail. Thus it's rolled back", file=sys.stderr)
with open(self.config_file_path, "w") as file:
file.write(self.last_working_config)
return False
else:
- print(diff)
+ print("Nginx Reloaded Successfully")
self.last_working_config = config_str
return True
- def reload(self) -> bool:
+ def reload(self, return_error=False) -> Union[bool, Tuple[bool, Union[str, None]]]:
"""
Reload nginx so that new configurations are applied.
:return: true if nginx reload was successful false otherwise
"""
- if self.config_test():
- reload_result = subprocess.run(Nginx.command_reload, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- if reload_result.returncode is not 0:
+ reload_result = subprocess.run(Nginx.command_reload, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ if reload_result.returncode is not 0:
+ if return_error:
+ return False, reload_result.stderr.decode("utf-8")
+ else:
print("Nginx reload failed with exit code ", file=sys.stderr)
print(reload_result.stderr.decode("utf-8"), file=sys.stderr)
- return False
- return True
- return False
+ result = False
+ else:
+ result = True
+
+ if return_error:
+ return result, None
+ else:
+ return result
def verify_domain(self, _domain: list or str):
domain = [_domain] if type(_domain) is str else _domain
## when not included, one invalid domain in a list of 100 will make all domains to be unverified due to nginx failing to start.
- domain = [x for x in domain if Nginx.is_valid_hostname(x)]
+ domain = [x for x in domain if Url.is_valid_hostname(x)]
success = []
while True:
r1 = "".join([random.choice(string.ascii_letters + string.digits) for _ in range(32)])
@@ -150,13 +162,17 @@ def verify_domain(self, _domain: list or str):
if response.content.decode("utf-8") == r2:
success.append(d)
continue
- print("[ERROR] " + url + "\n" + "Status Code :" + str(response.status_code), file=sys.stderr)
- if len(response.content) > 0:
- print(response.content.decode("utf-8"), file=sys.stderr)
+ print("[Error] [" + d + "] Not owned by this machine:" + "Status Code[" + str(
+ response.status_code) + "] -> " + url, file=sys.stderr)
continue
except requests.exceptions.RequestException as e:
- print("[ERROR] Domain is not owned by this machine :" + d, file=sys.stderr)
- print("Reason: " + str(e))
+ error=str(e)
+ if error.find("Name does not resolve") > -1:
+ print("[Error] [" + d + "] Domain Name could not be resolved", file=sys.stderr)
+ elif error.find("Connection refused") >-1:
+ print("[Error] [" + d + "] Connection Refused! The port is filtered or not open.", file=sys.stderr)
+ else:
+ print("[ERROR] Domain is not owned by this machine : Reason: " + str(e))
continue
os.remove(file)
break
@@ -164,24 +180,3 @@ def verify_domain(self, _domain: list or str):
return len(success) > 0
else:
return success
-
- @staticmethod
- def is_valid_hostname(hostname: str):
- """
- https://stackoverflow.com/a/33214423/2804342
- :return: True if for valid hostname False otherwise
- """
- if hostname[-1] == ".":
- # strip exactly one dot from the right, if present
- hostname = hostname[:-1]
- if len(hostname) > 253:
- return False
-
- labels = hostname.split(".")
-
- # the TLD must be not all-numeric
- if re.match(r"[0-9]+$", labels[-1]):
- return False
-
- allowed = re.compile(r"(?!-)[a-z0-9-]{1,63}(? 'Url':
+ # Tried parsing urls with urllib.parse.urlparse but it doesn't work quiet
+ # well when scheme( eg: "https://") is missing eg "example.com"
+ # it says that example.com is path not the hostname.
+ if default_scheme is None:
+ default_scheme = []
+ split_scheme = entry_string.strip().split("://", 1)
+ scheme, host_part = split_scheme if len(split_scheme) is 2 else (default_scheme, split_scheme[0])
+ host_entries = host_part.split("/", 1)
+ hostport, location = (host_entries[0], "/" + host_entries[1]) if len(host_entries) is 2 else (
+ host_entries[0], default_location)
+ hostport_entries = hostport.split(":", 1)
+ host, port = hostport_entries if len(hostport_entries) is 2 else (hostport_entries[0], default_port)
+ return Url(scheme, host if host else None, port, location)
+
+ @staticmethod
+ def is_valid_hostname(hostname: str) -> bool:
+ """
+ https://stackoverflow.com/a/33214423/2804342
+ :return: True if for valid hostname False otherwise
+ """
+ if hostname[-1] == ".":
+ # strip exactly one dot from the right, if present
+ hostname = hostname[:-1]
+ if len(hostname) > 253:
+ return False
+
+ labels = hostname.split(".")
+
+ # the TLD must be not all-numeric
+ if re.match(r"[0-9]+$", labels[-1]):
+ return False
+
+ allowed = re.compile(r"(?!-)[a-z0-9-]{1,63}(?")
- external, internal = host_list if len(host_list) is 2 else (host_list[0], "")
- external, internal = (split_url(external), split_url(internal))
- c = Container(None,
- scheme=internal["scheme"] if internal["scheme"] else "http",
- address=None,
- port=internal["port"] if internal["port"] else None,
- path=internal["location"] if internal["location"] else "/")
- h = Host.Host(
- external["host"] if external["host"] else None,
- # having https port on 80 will be detected later and used for redirection.
- external["port"] if external["port"] else "80",
- scheme=external["scheme"] if external["scheme"] else "http"
- )
-
- return (h,
- external["location"] if external["location"] else "/",
- c)
-
-
- @staticmethod
- def host_generator(container, service_id: str = None, known_networks: set = {}):
- """
- :param container:
- :param service_id:
- :param known_networks:
- :return: (Host,str,Container)
- """
- c = Container(container.id)
- network_settings = container.attrs["NetworkSettings"]
+ def get_env_map(container: DockerContainer):
# first we get the list of tuples each containing data in form (key, value)
env_list = [x.split("=", 1) for x in container.attrs['Config']['Env']]
# convert the environment list into map
- env_map = {x[0]: x[1].strip() for x in env_list if len(x) is 2 and x[1].strip()}
-
- # List all the environment variables with VIRTUAL_HOST and list them.
- virtual_hosts = [x[1] for x in env_map.items() if x[0].startswith("VIRTUAL_HOST")]
- if len(virtual_hosts) is 0:
- raise NoHostConiguration()
-
- # Instead of directly processing container details, check whether or not it's accessible through known networks.
- known_networks = set(known_networks)
- unknown = True
- for name, detail in network_settings["Networks"].items():
- c.add_network(detail["NetworkID"])
- # fix for https://trello.com/c/js37t4ld
- if detail["Aliases"] is not None:
- if detail["NetworkID"] in known_networks and unknown:
- alias = detail["Aliases"][len(detail["Aliases"]) - 1]
- ip_address = detail["IPAddress"]
- network = name
- if ip_address:
- break
- else:
- raise UnreachableNetwork()
-
- override_ssl = False
- override_port = None
- if len(virtual_hosts) is 1:
- if "LETSENCRYPT_HOST" in env_map:
- override_ssl = True
- if "VIRTUAL_PORT" in env_map:
- override_port=env_map["VIRTUAL_PORT"]
+ return {x[0]: x[1].strip() for x in env_list if len(x) is 2}
- for host_config in virtual_hosts:
- host, location, container_data = Container._parse_host_entry(host_config)
- container_data.address = ip_address
- container_data.id = container.id
- if override_port:
- container_data.port = override_port
- elif container_data.port is None:
- if len(network_settings["Ports"]) is 1:
- container_data.port = list(network_settings["Ports"].keys())[0].split("/")[0]
- else:
- container_data.port = "80"
- if override_ssl:
- if "ws" in host.scheme:
- host.scheme = {"wss", "https"}
- else:
- host.scheme = {"https", }
- yield (host, location, container_data)
class UnconfiguredContainer(Exception):
pass
diff --git a/nginx_proxy/Host.py b/nginx_proxy/Host.py
index 5d78122..a795962 100644
--- a/nginx_proxy/Host.py
+++ b/nginx_proxy/Host.py
@@ -1,3 +1,6 @@
+from typing import Set, Dict, Union, Any
+
+from nginx import Url
from nginx_proxy import Container
from nginx_proxy.Location import Location
@@ -9,33 +12,65 @@ class Host:
"""
- def __init__(self, hostname, port, scheme="http"):
- self.port = port
- self.hostname = hostname
- self.locations: dict[str:Location] = {} # the map of locations.and the container that serve the locations
- self.container_set: set = set()
- self.scheme = scheme
+ @staticmethod
+ def fromurl(url: Url):
+ return Host(url.hostname, url.port, url.scheme)
+
+ def __init__(self, hostname: str, port: int, scheme=None):
+ if scheme is None:
+ scheme = {'http', }
+ self.port: int = port
+ self.hostname: str = hostname
+ self.locations: Dict[str, Location] = {} # the map of locations.and the container that serve the locations
+ self.container_set: Set[str] = set()
+ self.scheme: set = scheme
+ self.secured: bool = 'https' in scheme or 'wss' in scheme
+ self.full_redirect: Union[Url, None] = None
+ self.extras: Dict[str, Any] = {}
- def set_external_parameters(self, host, port):
+ def set_external_parameters(self, host, port) -> None:
self.hostname = host
self.port = port
- def add_container(self, location: str, container: Container, websocket=False, http=True):
+ def update_extras(self, extras: Dict[str, Any]) -> None:
+ for x in extras:
+ self.update_extras_content(x, extras[x])
+
+ def update_extras_content(self, key: str, value: Any) -> None:
+ if key in self.extras:
+ data = self.extras[key]
+ if type(data) in (dict, set):
+ self.extras[key].update(value)
+ elif type(data) is list:
+ self.extras[key].extend(value)
+ else:
+ self.extras[key] = value
+ else:
+ self.extras[key] = value
+
+ def add_container(self, location: str, container: Container, websocket=False, http=True) -> None:
if location not in self.locations:
self.locations[location] = Location(location, is_websocket_backend=websocket, is_http_backend=http)
elif websocket:
- self.locations[location].websocket = self.locations[location].websocket or websocket
+ self.locations[location].websocket = websocket
self.locations[location].http = self.locations[location].http or http
self.locations[location].add(container)
self.container_set.add(container.id)
- def remove_container(self, container_id):
+ def update_with_host(self, host: 'Host') -> None:
+ for location in host.locations.values():
+ for container in location.containers:
+ self.add_container(location.name, container, location.websocket, location.http)
+ self.container_set.add(container.id)
+ self.locations[location.name].update_extras(location.extras)
+
+ def remove_container(self, container_id) -> None:
removed = False
deletions = []
if container_id in self.container_set:
for path, location in self.locations.items():
removed = location.remove(container_id) or removed
- if location.isEmpty():
+ if location.isempty():
deletions.append(path)
for path in deletions:
del self.locations[path]
@@ -43,14 +78,14 @@ def remove_container(self, container_id):
self.container_set.remove(container_id)
return removed
- def isEmpty(self):
+ def isempty(self) -> bool:
return len(self.container_set) == 0
- def isManaged(self):
+ def ismanaged(self) -> bool:
return False
- def is_redirect(self):
- return False
+ def isredirect(self) -> bool:
+ return self.full_redirect is not None
def __repr__(self):
return str({
@@ -58,3 +93,10 @@ def __repr__(self):
"locations": self.locations,
"server_name": self.hostname,
"port": self.port})
+
+ def __str__(self):
+ hostname= "%s:%s" % (
+ self.hostname if self.hostname else '?',
+ str(self.port) if self.port is not None else '?')
+
+
diff --git a/nginx_proxy/Location.py b/nginx_proxy/Location.py
index 74d268c..52f9810 100644
--- a/nginx_proxy/Location.py
+++ b/nginx_proxy/Location.py
@@ -1,3 +1,5 @@
+from typing import Dict, Any
+
from . import Container
@@ -11,11 +13,25 @@ def __init__(self, name, is_websocket_backend=False, is_http_backend=True):
self.websocket = is_websocket_backend
self.name = name
self.containers = set()
+ self.extras: Dict[str, Any] = {}
+
+ def update_extras(self, extras: Dict[str, Any]):
+ for x in extras:
+ if x in self.extras:
+ data = self.extras[x]
+ if type(data) in (dict, set):
+ self.extras[x].update(extras[x])
+ elif type(data) in list:
+ self.extras[x].extend(extras[x])
+ else:
+ self.extras[x] = extras[x]
+ else:
+ self.extras[x] = extras[x]
def add(self, container: Container):
self.containers.add(container)
- def isEmpty(self):
+ def isempty(self):
return len(self.containers) == 0
def remove(self, container: Container):
diff --git a/nginx_proxy/ProxyConfigData.py b/nginx_proxy/ProxyConfigData.py
new file mode 100644
index 0000000..8ee180e
--- /dev/null
+++ b/nginx_proxy/ProxyConfigData.py
@@ -0,0 +1,106 @@
+from typing import Dict, Set, Generator, Tuple, Union
+
+from nginx_proxy.Host import Host
+
+
+class ProxyConfigData:
+ """
+ All the configuration data that are obtained from the current state of container.
+ nginx configuration or any other reverse proxy configuration can be generated using the data available here.
+ """
+
+ def __init__(self):
+ # map the hostname -> port -> hostCofiguration
+ self.config_map: Dict[str, Dict[int, Host]] = {}
+ self.containers: Set[str] = set()
+ self._len = 0
+
+ def getHost(self, hostname: str, port: int = 80) -> Union[None, Host]:
+ if hostname in self.config_map:
+ if port in self.config_map[hostname]:
+ return self.config_map[hostname][port]
+ return None
+
+ def add_host(self, host: Host) -> None:
+ if host.hostname in self.config_map:
+ port_map = self.config_map[host.hostname]
+ if host.port in port_map:
+ existing_host: Host = port_map[host.port]
+ existing_host.secured = host.secured or existing_host.secured
+ existing_host.update_extras(host.extras)
+ for location in host.locations.values():
+ for container in location.containers:
+ existing_host.add_container(location.name, container, location.websocket, location.http)
+ self.containers.add(container.id)
+ existing_host.locations[location.name].update_extras(location.extras)
+ return
+ else:
+ self._len = self._len + 1
+ port_map[host.port] = host
+
+ else:
+ self._len = self._len + 1
+ self.config_map[host.hostname] = {host.port: host}
+
+ for location in host.locations.values():
+ for container in location.containers:
+ self.containers.add(container.id)
+
+ def remove_container(self, container_id: str) -> Tuple[bool, Set[Tuple[str, int]]]:
+ removed_domains = set()
+ result = False
+ if container_id in self.containers:
+ self.containers.remove(container_id)
+ for host in self.host_list():
+ if host.remove_container(container_id):
+ result = True
+ if host.isempty():
+ host.extras={}
+ removed_domains.add((host.hostname, host.port))
+ return result, removed_domains
+
+ def has_container(self, container_id):
+ return container_id in self.containers
+
+ def host_list(self) -> Generator[Host, None, None]:
+ for port_map in self.config_map.values():
+ for host in port_map.values():
+ yield host
+
+ def __len__(self):
+ return self._len
+
+ def print(self):
+
+ for host in self.host_list():
+ if host.port != 80:
+ url = "- " + ("https" if host.secured else "http") + "://" + host.hostname + ":" + str(host.port)
+ else:
+ url = "- " + ("https" if host.secured else "http") + "://" + host.hostname
+ if host.isredirect():
+ print(url)
+ print(" redirect : ", host.full_redirect)
+ else:
+ if len(host.extras):
+ print(url)
+ self.printextra(" ", host.extras)
+ for location in host.locations.values():
+ print(url + location.name)
+ print(" Type: ", "Websocket" if location.websocket else "Http")
+ if len(location.extras):
+ self.printextra(" ", location.extras)
+
+ @staticmethod
+ def printextra(gap, extra):
+ print(gap + "Extras:")
+ for x in extra:
+ if x is 'security' or type(x) in (set, list):
+ print(gap + " " + x + ":")
+ for s in extra[x]:
+ print(gap + " " + s)
+ elif type(x) is dict:
+ print(gap + " " + x + ":")
+ for s in extra[x]:
+ print(gap + " " + s + ":" + extra[x][s])
+ else:
+ print(gap + " " + x + " : " + str(extra[x]))
diff --git a/nginx_proxy/SSL.py b/nginx_proxy/SSL.py
index 741e030..2ea50ca 100644
--- a/nginx_proxy/SSL.py
+++ b/nginx_proxy/SSL.py
@@ -12,6 +12,7 @@
class SSL:
+
def __init__(self, ssl_path, nginx: Nginx):
self.ssl_path = ssl_path
self.nginx = nginx
@@ -32,9 +33,18 @@ def __init__(self, ssl_path, nginx: Nginx):
except FileExistsError as e:
pass
- def get_cert_file(self, domain):
+ def cert_file(self, domain):
return os.path.join(self.ssl_path, "certs", domain + ".crt")
+ def private_file(self, domain):
+ return os.path.join(self.ssl_path, "private", domain + ".key")
+
+ def selfsigned_cert_file(self, domain):
+ return os.path.join(self.ssl_path, "certs", domain + ".selfsigned.cert")
+
+ def selfsigned_private_file(self, domain):
+ return os.path.join(self.ssl_path, "private", domain + "selfsgned.key")
+
def self_sign(self, domain):
CERT_FILE = domain + ".selfsigned.crt"
KEY_FILE = domain + ".selfsigned.key"
@@ -64,7 +74,7 @@ def self_sign(self, domain):
def expiry_time(self, domain) -> datetime:
if self.cert_exists(domain):
- with open(self.get_cert_file(domain)) as file:
+ with open(self.cert_file(domain)) as file:
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, file.read())
return datetime.datetime.strptime(x509.get_notAfter().decode(), "%Y%m%d%H%M%SZ")
return datetime.datetime.now()
@@ -99,10 +109,10 @@ def reuse(self, domain1, domain2):
def register_certificate(self, domain, no_self_check=False, ignore_existing=False):
if type(domain) is str:
domain = [domain]
+ domain = [d for d in domain if
+ '.' in d] # when the domain doesn't have '.' it shouldn't be requested for letsencrypt certificate
verified_domain = domain if no_self_check else self.nginx.verify_domain(domain)
domain = verified_domain if ignore_existing else [x for x in verified_domain if not self.cert_exists(x)]
- domain = [d for d in domain if
- '.' in d] # when the domain doesn't have '.' it shouldn't be requested for letsencrypt certificate.
if len(domain):
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.DEBUG)
acme = AcmeV2(
@@ -145,7 +155,8 @@ def register_certificate_or_selfsign(self, domain, no_self_check=False, ignore_e
def register_certificate_self_sign(self, domain):
if type(domain) is str:
- domain = [domain]
- domain = [x for x in domain if not self.cert_exists_self_signed(x)]
- for d in domain:
- self.self_sign(d)
+ self.self_sign(domain)
+ else:
+ for d in domain:
+ if not self.cert_exists_self_signed(d):
+ self.self_sign(d)
diff --git a/nginx_proxy/WebServer.py b/nginx_proxy/WebServer.py
index 8dad038..d456232 100644
--- a/nginx_proxy/WebServer.py
+++ b/nginx_proxy/WebServer.py
@@ -1,96 +1,77 @@
import copy
-import datetime
import os
import re
import sys
-import threading
+import time
+from typing import List
import requests
from docker import DockerClient
+from docker.models.containers import Container as DockerContainer
from jinja2 import Template
+import nginx_proxy.post_processors as post_processors
+import nginx_proxy.pre_processors as pre_processors
from nginx.Nginx import Nginx
from nginx_proxy import Container
+from nginx_proxy import ProxyConfigData
from nginx_proxy.Host import Host
-from nginx_proxy.SSL import SSL
class WebServer():
def __init__(self, client: DockerClient, *args):
+ import socket
+ def wait_nginx():
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ result = sock.connect_ex(('127.0.0.1', 80))
+ while result != 0:
+ print("Waiting for nginx process to be ready")
+ time.sleep(1)
+ result = sock.connect_ex(('127.0.0.1', 80))
+ sock.close()
+ print("Nginx is alive")
+
+ self.config = self.loadconfig()
self.shouldExit = False
self.client = client
- challenge_dir = os.environ.get("CHALLENGE_DIR")
conf_file = "/etc/nginx/conf.d/default.conf"
- self.nginx = Nginx(conf_file, challenge_dir=challenge_dir) if challenge_dir else Nginx(conf_file)
- self.ssl = SSL("/etc/ssl", self.nginx)
- self.containers = set()
+ self.nginx = Nginx(conf_file, self.config['challenge_dir'])
+ self.config_data = ProxyConfigData()
self.services = set()
self.networks = {}
self.conf_file_name = "/etc/nginx/conf.d/default.conf"
self.host_file = "/etc/hosts"
- self.hosts = {}
- self.ssl_certificates = {}
- self.self_signed_certificates = set()
- self.next_ssl_expiry = None
- file = open("vhosts_template/default.conf.template")
+ file = open("vhosts_template/default.conf.jinja2")
self.template = Template(file.read())
file.close()
self.learn_yourself()
- self.rescan_all_container()
- self.rescan_time = None
- self.lock = threading.Condition()
- self.expiry_changed = threading.Event()
- self.certificate_expiry_thread = threading.Thread(target=self.check_certificate_expiry)
+ self.ssl_processor = post_processors.SslCertificateProcessor(self.nginx, self, start_ssl_thread=False)
+ self.basic_auth_processor = post_processors.BasicAuthProcessor()
+ self.redirect_processor = post_processors.RedirectProcessor()
if self.nginx.config_test():
- if not self.nginx.start():
+ if len(self.nginx.last_working_config) < 50:
+ print("Writing default config before reloading server.")
+ if not self.nginx.force_start(self.template.render(config=self.config)):
+ print("Nginx failed when reloaded with default config", file=sys.stderr)
+ print("Exiting .....", file=sys.stderr)
+ exit(1)
+ elif not self.nginx.start():
print("ERROR: Config test succeded but nginx failed to start", file=sys.stderr)
print("Exiting .....", file=sys.stderr)
- self.reload()
+ exit(1)
else:
- print("ERROR: Existing nginx configuration has error, trying to override with new configuration",
+ print("ERROR: Existing nginx configuration has error, trying to override with default configuration",
file=sys.stderr)
- if not self.reload(forced=True):
- print("ERROR: Existing nginx configuration has error", file=sys.stderr)
- print("ERROR: New generated configuration also has error", file=sys.stderr)
- print("Please check the configuration of your containers and restart this container", file=sys.stderr)
- print("EXITING .....", file=sys.stderr)
+ if not self.nginx.force_start(self.template.render(config=self.config)):
+ print("Nginx failed when reloaded with default config", file=sys.stderr)
+ print("Exiting .....", file=sys.stderr)
exit(1)
- self.certificate_expiry_thread.start()
+ wait_nginx()
- def check_certificate_expiry(self):
- self.lock.acquire()
- while True:
- if self.shouldExit:
- return
- if self.next_ssl_expiry is None:
- print("[SSL Refresh Thread] Looks like there no ssl certificates, Sleeping until there's one")
- self.lock.wait()
- else:
- now = datetime.datetime.now()
- remaining_days = (self.next_ssl_expiry - now).days
- remaining_days = 30 if remaining_days > 30 else remaining_days
-
- if remaining_days > 2:
- print(
- "[SSL Refresh Thread] All the certificates are up to date sleeping for" + str(
- remaining_days) + "days.")
- self.lock.wait((remaining_days - 2) * 3600 * 24)
- else:
- print("[SSL Refresh Thread] Looks like we need to refresh certificates that are about to expire")
- for x in self.ssl_certificates:
- print("Remaining days :", x, ":", (self.ssl_certificates[x] - now).days)
- x = [x for x in self.ssl_certificates if (self.ssl_certificates[x] - now).days < 6]
- acme_ssl_certificates = set(self.ssl.register_certificate_or_selfsign(x, ignore_existing=True))
- for host in x:
- if host not in acme_ssl_certificates:
- del self.ssl_certificates[host]
- if not self.ssl.cert_exists_wildcard(host):
- self.self_signed_certificates.add(host)
- else:
- self.ssl_certificates[host] = self.ssl.expiry_time(domain=host)
- self.next_ssl_expiry = min(self.ssl_certificates.values())
- self.reload(forced=True)
+ self.rescan_all_container()
+ self.reload()
+ self.ssl_processor.certificate_expiry_thread.start()
def learn_yourself(self):
"""
@@ -100,7 +81,7 @@ def learn_yourself(self):
"""
try:
file = open("/proc/self/cgroup")
- self.id = [l for l in file.read().split("\n") if l.find("cpu") != -1][0].split("/")[-1]
+ self.id = [l.strip() for l in file.readlines() if l.find("cpu") != -1][0].split("/")[-1]
if len(self.id) > 64:
slice = [x for x in re.split('[^a-fA-F0-9]', self.id) if len(x) is 64]
if len(slice) is 1:
@@ -112,6 +93,8 @@ def learn_yourself(self):
self.networks = [a for a in self.container.attrs["NetworkSettings"]["Networks"].keys()]
self.networks = {self.client.networks.get(a).id: a for a in self.networks}
file.close()
+ except (KeyboardInterrupt, SystemExit) as e:
+ raise e
except Exception as e:
print("[ERROR]Couldn't determine container ID of this container:", e.args,
"\n Is it running in docker environment?",
@@ -120,144 +103,67 @@ def learn_yourself(self):
network = self.client.networks.get("frontend")
self.networks[network.id] = "frontend"
- def _register_container(self, container):
+ def _register_container(self, container: DockerContainer):
"""
Find the details about container and register it and return True.
If it's not configured with desired settings or is not accessible, return False
@:returns True if the container is added to virtual hosts, false otherwise.
"""
- found = False
- try:
- for host, location, container in Container.Container.host_generator(container,
- known_networks=self.networks.keys()):
- websocket = "ws" in host.scheme or "wss" in host.scheme
- secured = 'https' in host.scheme or 'wss' in host.scheme
- http = 'http' in host.scheme or 'https' in host.scheme
- # it might return string if there's a error in processing
- if type(host) is not str:
- if (host.hostname, host.port) in self.hosts:
- existing_host: Host = self.hosts[(host.hostname, host.port)]
- existing_host.add_container(location, container, websocket=websocket, http=http)
- ## if any of the containers in for the virtualHost require https, the all others will be redirected to https.
- if secured:
- existing_host.secured = True
- host = existing_host
- else:
- host.secured = secured
- host.add_container(location, container, websocket=websocket, http=http)
- self.hosts[(host.hostname, host.port)] = host
-
- if host.secured:
-
- if host.hostname not in self.ssl_certificates:
- host.ssl_expiry = self.ssl.expiry_time(host.hostname)
- if (host.ssl_expiry - datetime.datetime.now()).days > 2:
- self.ssl_certificates[host.hostname] = host.ssl_expiry
- else:
- host.ssl_expiry = self.ssl_certificates[host.hostname]
-
- found = True
- self.containers.add(container.id)
-
- except Container.NoHostConiguration:
- print("Skip Container:", "No VIRTUAL_HOST configuration", "Id:" + container.id,
- "Name:" + container.attrs["Name"].replace("/", ""), sep="\t")
- except Container.UnreachableNetwork:
- print("Skip Container:", "UNREACHABLE Network ", "Id:" + container.id,
- "Name:" + container.attrs["Name"].replace("/", ""), sep="\t")
- return found
+ environments = Container.Container.get_env_map(container)
+ known_networks = set(self.networks.keys())
+ hosts = pre_processors.process_virtual_hosts(container, environments, known_networks)
+ if len(hosts):
+ pre_processors.process_default_server(container, environments, hosts)
+ pre_processors.process_basic_auth(container, environments, hosts.config_map)
+ pre_processors.process_redirection(container, environments, hosts.config_map)
+ hosts.print()
+ for h in hosts.host_list():
+ self.config_data.add_host(h)
+ return len(hosts) > 0
# removes container from the maintained list.
# this is called when a caontainer dies or leaves a known network
- def remove_container(self, container):
- if type(container) is Container:
- container = container.id
-
- if container in self.containers:
- removed = False
- deletions = []
- for host in self.hosts.values():
- if host.remove_container(container):
- removed = True
- if host.isEmpty():
- if host.scheme == "https":
- if host.hostname in self.self_signed_certificates:
- self.self_signed_certificates.remove(host.hostname)
- else:
- del self.ssl_certificates[host.hostname]
- deletions.append((host.hostname, host.port))
- if removed:
- for d in deletions:
- del self.hosts[d]
- self.containers.remove(container)
- return self.reload()
+ def remove_container(self, container_id: str):
+ deleted, deleted_domain = self.config_data.remove_container(container_id)
+ if deleted:
+ self.reload()
def reload(self, forced=False) -> bool:
- self.lock.acquire()
"""
Creates a new configuration based on current state and signals nginx to reload.
This is called whenever there's change in container or network state.
:return:
"""
- host_list = [copy.deepcopy(host) for host in self.hosts.values()]
-
- next_reload = None
- now = datetime.datetime.now()
- ssl_requests = set()
- for host in host_list:
- host.locations = list(host.locations.values())
+ self.redirect_processor.process_redirection(self.config_data)
+ hosts: List[Host] = []
+ has_default = False
+ for host_data in self.config_data.host_list():
+ host = copy.deepcopy(host_data)
host.upstreams = {}
- for i, location in enumerate(host.locations):
+ host.is_down = host_data.isempty()
+ if 'default_server' in host.extras:
+ if has_default:
+ del host.extras['default_server']
+ else:
+ has_default = True
+ for i, location in enumerate(host.locations.values()):
location.container = list(location.containers)[0]
if len(location.containers) > 1:
- location.upstream = host.hostname + "-" + str(host.port) + "-" + str(i + 1)
+ location.upstream = host_data.hostname + "-" + str(host.port) + "-" + str(i + 1)
host.upstreams[location.upstream] = location.containers
else:
location.upstream = False
host.upstreams = [{"id": x, "containers": y} for x, y in host.upstreams.items()]
- if host.secured:
- if int(host.port) in (80, 443):
- host.ssl_redirect = True
- host.port = 443
- host.ssl_host = True
- wildcard = self.ssl.wildcard_domain_name(host.hostname)
- if wildcard is not None:
- if self.ssl.cert_exists(wildcard):
- host.ssl_file = wildcard
- continue
- if host.hostname in self.ssl_certificates:
- host.ssl_file = host.hostname
- elif host.hostname in self.self_signed_certificates:
- host.ssl_file = host.hostname + ".selfsigned"
- else:
- ssl_requests.add(host.hostname)
+ hosts.append(host)
- if len(ssl_requests):
- registered = self.ssl.register_certificate_or_selfsign(list(ssl_requests))
- for host in host_list:
- if host.hostname in ssl_requests:
- if host.hostname not in registered:
- self.ssl.register_certificate_self_sign(host.hostname)
- host.ssl_file = host.hostname + ".selfsigned"
- self.self_signed_certificates.add(host.hostname)
- else:
- host.ssl_file = host.hostname
- self.ssl_certificates[host.hostname] = self.ssl.expiry_time(host.hostname)
- host.ssl_expiry = self.ssl_certificates[host.hostname]
- if self.next_ssl_expiry:
- if self.next_ssl_expiry > host.ssl_expiry:
- self.next_ssl_expiry = host.ssl_expiry
- self.lock.notify()
- else:
- self.lock.notify()
- self.next_ssl_expiry = host.ssl_expiry
-
- output = self.template.render(virtual_servers=host_list, challenge_dir=self.nginx.challenge_dir)
+ self.basic_auth_processor.process_basic_auth(hosts)
+ self.ssl_processor.process_ssl_certificates(hosts)
+ self.config['default_server'] = not has_default
+ output = self.template.render(virtual_servers=hosts, config=self.config)
if forced:
- response = self.nginx.forced_update(output)
+ response = self.nginx.force_start(output)
else:
response = self.nginx.update_config(output)
- self.lock.release()
return response
def disconnect(self, network, container, scope):
@@ -276,11 +182,10 @@ def connect(self, network, container, scope):
if network not in self.networks:
self.networks[network] = self.client.networks.get(network).name
self.rescan_and_reload()
- elif container not in self.containers and network in self.networks:
- if self.update_container(container):
- self.reload()
+ elif network in self.networks:
+ self.update_container(container)
- def update_container(self, container):
+ def update_container(self, container_id):
'''
Rescan the container to detect changes. And update nginx configuration if necessary.
This is usually called in one of the following conditions:
@@ -291,8 +196,10 @@ def update_container(self, container):
:return: true if container state change affected the nginx configuration else false
'''
try:
- if self._register_container(self.client.containers.get(container)):
- return True
+ if not self.config_data.has_container(container_id):
+ if self._register_container(self.client.containers.get(container_id)):
+ self.reload()
+ return True
except requests.exceptions.HTTPError as e:
pass
return False
@@ -316,7 +223,11 @@ def rescan_and_reload(self):
return self.reload()
def cleanup(self):
- self.lock.acquire()
- self.shouldExit = True
- self.lock.notify()
- self.lock.release()
+ self.ssl_processor.shutdown()
+
+ def loadconfig(self):
+ return {
+ 'client_max_body_size': os.getenv("CLIENT_MAX_BODY_SIZE", "1m"),
+ 'challenge_dir': os.getenv("CHALLENGE_DIR", "/tmp/acme-challenges"),
+ 'default_server': os.getenv("DEFAULT_HOST", "true") == "true"
+ }
diff --git a/nginx_proxy/__init__.py b/nginx_proxy/__init__.py
index e69de29..b6d8ee3 100644
--- a/nginx_proxy/__init__.py
+++ b/nginx_proxy/__init__.py
@@ -0,0 +1,2 @@
+from .Host import Host
+from .ProxyConfigData import ProxyConfigData
diff --git a/nginx_proxy/post_processors/__init__.py b/nginx_proxy/post_processors/__init__.py
new file mode 100644
index 0000000..a358ecc
--- /dev/null
+++ b/nginx_proxy/post_processors/__init__.py
@@ -0,0 +1,3 @@
+from .basic_auth_processor import BasicAuthProcessor
+from .redirect_processor import RedirectProcessor
+from .ssl_certificate_processor import SslCertificateProcessor
diff --git a/nginx_proxy/post_processors/basic_auth_processor.py b/nginx_proxy/post_processors/basic_auth_processor.py
new file mode 100644
index 0000000..9b9d229
--- /dev/null
+++ b/nginx_proxy/post_processors/basic_auth_processor.py
@@ -0,0 +1,47 @@
+import crypt
+import os
+from pathlib import Path
+from random import random
+from typing import List, Dict
+
+from nginx_proxy.Host import Host
+
+
+class BasicAuthProcessor():
+ def __init__(self, basic_auth_dir: str = "/etc/nginx/basic_auth"):
+ self.cache: Dict[str:hash] = {}
+ self.basic_auth_dir = basic_auth_dir
+ if not os.path.exists(basic_auth_dir):
+ Path(basic_auth_dir).mkdir(parents=True)
+ # self.certificate_expiry_thread = threading.Thread(target=self.check_certificate_expiry)
+ # self.certificate_expiry_thread.start()
+
+ @staticmethod
+ def salt():
+ """Returns a string of 2 randome letters"""
+ letters = 'abcdefghijklmnopqrstuvwxyz' \
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' \
+ '0123456789/.'
+ return random.choice(letters) + random.choice(letters)
+
+ def generate_htpasswd_file(self, folder, file, securities):
+ data = [user + ':' + crypt.crypt(securities[user]) for user in securities]
+ folder = os.path.join(self.basic_auth_dir, folder)
+ if not os.path.exists(folder):
+ os.mkdir(folder)
+
+ file = os.path.join(folder, file)
+ with open(file, "w") as openfile:
+ openfile.write('\n'.join(data))
+ return file
+
+ def process_basic_auth(self, hosts: List[Host]):
+ for host in hosts:
+ if 'security' in host.extras:
+ host.extras['security_file'] = self.generate_htpasswd_file(host.hostname, '_', host.extras['security'])
+
+ for location in host.locations.values():
+ if 'security' in location.extras:
+ location.extras['security_file'] = self.generate_htpasswd_file(host.hostname,
+ location.name.replace('/', '_'),
+ location.extras['security'])
diff --git a/nginx_proxy/post_processors/redirect_processor.py b/nginx_proxy/post_processors/redirect_processor.py
new file mode 100644
index 0000000..902d1b0
--- /dev/null
+++ b/nginx_proxy/post_processors/redirect_processor.py
@@ -0,0 +1,26 @@
+from nginx_proxy import ProxyConfigData
+
+
+class RedirectProcessor:
+ def __init__(self, ):
+ pass
+
+ def process_redirection(self, config: ProxyConfigData):
+ redirected_hosts = {}
+ for host in config.host_list():
+ if host.isredirect():
+ redirected_hosts[host.hostname] = host.full_redirect
+ target = config.getHost(host.full_redirect.hostname)
+ if target is not None:
+ if target.hostname == host.hostname:
+ host.full_redirect = None
+ if target.hostname in redirected_hosts:
+ continue
+ target.update_with_host(host)
+ host.container_set = set()
+ host.locations = {}
+ host.secured = host.secured or target.secured
+ target.secured = host.secured
+ target.update_extras(extras=host.extras)
+ host.extras = {}
+ host.is_redirect = host.isredirect()
diff --git a/nginx_proxy/post_processors/ssl_certificate_processor.py b/nginx_proxy/post_processors/ssl_certificate_processor.py
new file mode 100644
index 0000000..0330a5b
--- /dev/null
+++ b/nginx_proxy/post_processors/ssl_certificate_processor.py
@@ -0,0 +1,102 @@
+import threading
+from datetime import date, datetime
+from typing import List, Dict, Set, Union
+
+from nginx.Nginx import Nginx
+from nginx_proxy import WebServer
+from nginx_proxy.Host import Host
+from nginx_proxy.SSL import SSL
+
+
+class SslCertificateProcessor():
+ def __init__(self, nginx: Nginx, server: WebServer, start_ssl_thread=False):
+ self.cache: Dict[str:date] = {}
+ self.self_signed: Set[str] = set()
+ self.shutdown_requested: bool = False
+ self.lock: threading.Condition = threading.Condition()
+ self.nginx: Nginx = nginx
+ self.ssl: SSL = SSL("/etc/ssl", nginx)
+ self.server: WebServer = server
+ self.next_ssl_expiry: Union[datetime, None] = None
+ self.certificate_expiry_thread: threading.Thread = threading.Thread(target=self.update_ssl_certificates)
+ if start_ssl_thread:
+ self.certificate_expiry_thread.start()
+
+ def update_ssl_certificates(self):
+ self.lock.acquire()
+ while not self.shutdown_requested:
+ if self.next_ssl_expiry is None:
+ print("[SSL Refresh Thread] Looks like there no ssl certificates, Sleeping until there's one")
+ self.lock.wait()
+ else:
+ now = datetime.now()
+ remaining_days = (self.next_ssl_expiry - now).days
+
+ if remaining_days > 2:
+ print("[SSL Refresh Thread] SSL certificate status:")
+
+ max_size = max([len(x) for x in self.cache])
+ for host in self.cache:
+ print(' {host: <{width}} - {remain}'.format(host=host, width=max_size + 2,
+ remain=self.cache[host] - now))
+ sleep_time = (32 if remaining_days > 30 else remaining_days) - 2
+ print(
+ "[SSL Refresh Thread] All the certificates are up to date sleeping for " + str(
+ sleep_time) + " days.")
+ self.lock.wait(sleep_time * 3600 * 24 - 10)
+ else:
+ print(
+ "[SSL Refresh Thread] Looks like we need to refresh certificates that are about to expire")
+ for x in self.cache:
+ print("Remaining days :", x, ":", (self.cache[x] - now).days)
+ x = [x for x in self.cache if (self.cache[x] - now).days < 6]
+ self.server.reload()
+
+ def process_ssl_certificates(self, hosts: List[Host]):
+ ssl_requests: Set[Host] = set()
+ self.lock.acquire()
+ for host in hosts:
+ if host.secured:
+ if int(host.port) in (80, 443):
+ host.ssl_redirect = True
+ host.port = 443
+ if host.hostname in self.cache:
+ host.ssl_file = host.hostname
+ else:
+ wildcard = self.ssl.wildcard_domain_name(host.hostname)
+ if wildcard is not None:
+ if self.ssl.cert_exists(wildcard):
+ host.ssl_file = wildcard
+ continue
+ # find the ssl certificate if it exists
+ time = self.ssl.expiry_time(host.hostname)
+ if (time - datetime.now()).days > 2:
+ self.cache[host.hostname] = time
+ host.ssl_file = host.hostname
+ else:
+ ssl_requests.add(host)
+
+ if len(ssl_requests):
+ registered = self.ssl.register_certificate_or_selfsign([h.hostname for h in ssl_requests],
+ ignore_existing=False)
+ for host in ssl_requests:
+ if host.hostname not in registered:
+ host.ssl_file = host.hostname + ".selfsigned"
+ self.self_signed.add(host.hostname)
+ else:
+ host.ssl_file = host.hostname
+ self.cache[host.hostname] = self.ssl.expiry_time(host.hostname)
+ host.ssl_expiry = self.cache[host.hostname]
+ if len(self.cache):
+ expiry = min(self.cache.values())
+ if expiry != self.next_ssl_expiry:
+ self.next_ssl_expiry = expiry
+ self.lock.notify()
+ self.lock.release()
+
+ def shutdown(self):
+ self.lock.acquire()
+ self.shutdown_requested = True
+ self.lock.notify()
+ self.lock.release()
+ pass
diff --git a/nginx_proxy/pre_processors/__init__.py b/nginx_proxy/pre_processors/__init__.py
new file mode 100644
index 0000000..f06cfb7
--- /dev/null
+++ b/nginx_proxy/pre_processors/__init__.py
@@ -0,0 +1,4 @@
+from .basic_auth_processor import process_basic_auth as process_basic_auth
+from .default_server_processor import process_default_server as process_default_server
+from .redirect_processor import process_redirection
+from .virtual_host_processor import process_virtual_hosts as process_virtual_hosts
diff --git a/nginx_proxy/pre_processors/basic_auth_processor.py b/nginx_proxy/pre_processors/basic_auth_processor.py
new file mode 100644
index 0000000..834fff4
--- /dev/null
+++ b/nginx_proxy/pre_processors/basic_auth_processor.py
@@ -0,0 +1,63 @@
+# this one will get the list of environment variables and will process them.
+from typing import Dict, Tuple, List
+
+from nginx import Url
+from nginx_proxy.Host import Host
+
+htaccess_folder = "/etc/nginx/generated/htaccess"
+from docker.models.containers import Container
+
+def process_basic_auth(container: Container, environments: map, vhost_map: Dict[str, Dict[int, Host]]):
+ def get_auth_map(credentials: str) -> Dict[str, str]:
+ auth_map = {}
+ for credential in credentials.split(','):
+ username_password = credential.split(':')
+ if len(username_password) == 2:
+ u = username_password[0].strip()
+ p = username_password[1].strip()
+ if len(u) > 2 and len(p) > 2:
+ auth_map[u] = p
+ return auth_map
+
+ def update_security():
+ if basic_auth_host.location == '/':
+ host.update_extras_content('security', keys)
+ else:
+ for location in host.locations.values():
+ if location.name.startswith(basic_auth_host.location):
+ if 'security' in location.extras:
+ location.extras['security'].update(keys)
+ else:
+ location.extras['security'] = keys
+
+ auth_env = [e[1] for e in environments.items() if e[0].startswith("PROXY_BASIC_AUTH")]
+ if len(auth_env):
+ auth_list: List[Tuple[Url, Dict[str, str]]] = []
+ for auth_entry in auth_env:
+ host_list = auth_entry.split("->")
+ if len(host_list) is 2:
+ url = host_list[0]
+ keys = get_auth_map(host_list[1])
+ auth_list.append((Url.parse(url, default_location='/',default_port=80), keys))
+ elif len(host_list) is 1:
+ keys = get_auth_map(auth_entry)
+ if len(keys):
+ auth_list.append((Url.root, keys))
+
+ for basic_auth_host, keys in auth_list:
+ # if there is no hostname in auth, then there must be
+ if basic_auth_host.hostname is None:
+ if len(vhost_map) == 1:
+ port_map = list(vhost_map.values())[0]
+ if len(port_map) == 1:
+ host = list(port_map.values())[0]
+ update_security()
+ elif basic_auth_host.hostname in vhost_map:
+ port_map = vhost_map[basic_auth_host.hostname]
+ if basic_auth_host.port in port_map:
+ host = port_map[basic_auth_host.port]
+ update_security()
+ else:
+ print("Basic Auth for "+basic_auth_host.hostname+":"+str(basic_auth_host.port)+" in container with "+str(list(vhost_map.keys())))
+ else:
+ print("Unknown hostname : "+basic_auth_host.hostname+"+ in PROXY_BASIC_AUTH in container: " + container.name)
diff --git a/nginx_proxy/pre_processors/default_server_processor.py b/nginx_proxy/pre_processors/default_server_processor.py
new file mode 100644
index 0000000..abc722e
--- /dev/null
+++ b/nginx_proxy/pre_processors/default_server_processor.py
@@ -0,0 +1,29 @@
+# this one will get the list of environment variables and will process them.
+from typing import Dict
+
+from nginx import Url
+from nginx_proxy import ProxyConfigData
+from nginx_proxy.Host import Host
+
+htaccess_folder = "/etc/nginx/generated/htaccess"
+from docker.models.containers import Container
+
+
+def process_default_server(container: Container, environments: Dict[str, str], vhosts: ProxyConfigData):
+ if 'PROXY_DEFAULT_SERVER' in environments:
+ server: str = environments['PROXY_DEFAULT_SERVER']
+ url = Url.parse(server, default_port=80)
+ if url.hostname not in ("true","false","yes") and Url.is_valid_hostname(url.hostname):
+ host = vhosts.getHost(hostname=url.hostname)
+ if host is None:
+ host = Host.fromurl(url)
+ vhosts.add_host(host)
+ else:
+ if len(vhosts) is 1:
+ for host in vhosts.host_list():
+ pass
+ else:
+ print("DEFAULT_SERVER configured for ", container.name, "but has multiple hosts")
+ return
+
+ host.update_extras_content("default_server", "default_server")
diff --git a/nginx_proxy/pre_processors/redirect_processor.py b/nginx_proxy/pre_processors/redirect_processor.py
new file mode 100644
index 0000000..28ba8b2
--- /dev/null
+++ b/nginx_proxy/pre_processors/redirect_processor.py
@@ -0,0 +1,52 @@
+# this one will get the list of environment variables and will process them.
+# this one can call itself before each configuration and modify the dhparam parameter.
+import re
+from typing import Dict
+
+from nginx import Url
+from nginx_proxy import Container, Host
+
+
+def process_redirection(container: Container, environments: map, vhost_map: Dict[str, Dict[int, Host]]):
+ redirect_env = [e[1] for e in environments.items() if e[0].startswith("PROXY_FULL_REDIRECT")]
+ hosts = []
+ for port_map in vhost_map.values():
+ for vhosts in port_map.values():
+ hosts.append(vhosts)
+ single_host = len(hosts) == 1
+ if len(redirect_env):
+ for redirect_entry in redirect_env:
+ redirect_entry = re.sub(r"\s+", "", redirect_entry, flags=re.UNICODE)
+
+ split = redirect_entry.split("->")
+ if len(split) is 2:
+ _sources, target = split
+ sources = [Url.parse(source) for source in _sources.split(',')]
+ target = Url.parse(target, default_port=80)
+ if single_host:
+ if target.hostname is None:
+ target = single_host
+ elif target.hostname is None:
+ print("Unknown target to redirect with PROXY_FULL_REDIRECT" + redirect_entry)
+ continue
+ for source in sources:
+ if source.hostname is not None:
+ port = 80 if source.port is None else source.port
+ if source.hostname not in vhost_map:
+ host = Host(source.hostname, port)
+ host.full_redirect = target
+ vhost_map[source.hostname] = {port: host}
+ else:
+ if port in vhost_map[source.hostname]:
+ existing_host = vhost_map[source.hostname][port]
+ existing_host.full_redirect = target
+ else:
+ host = Host(source.hostname, port)
+ host.full_redirect = target
+ vhost_map[source.hostname][port] = host
+ if target.hostname not in vhost_map:
+ vhost_map[target.hostname] = {target.port: Host(target.hostname, target.port)}
+ elif target.port not in vhost_map[target.hostname]:
+ vhost_map[target.hostname][target.port] = Host(target.hostname, target.port)
+ else:
+ print("Invalid entry of PROXY_FULL_REDIRECT :" + redirect_entry)
diff --git a/nginx_proxy/pre_processors/vhost_template_processor.py b/nginx_proxy/pre_processors/vhost_template_processor.py
new file mode 100644
index 0000000..7cdfb63
--- /dev/null
+++ b/nginx_proxy/pre_processors/vhost_template_processor.py
@@ -0,0 +1,2 @@
+# this one will get all the sites and need to determine if it has a template that can be loaded.
+# TODO
diff --git a/nginx_proxy/pre_processors/virtual_host_processor.py b/nginx_proxy/pre_processors/virtual_host_processor.py
new file mode 100644
index 0000000..52ff923
--- /dev/null
+++ b/nginx_proxy/pre_processors/virtual_host_processor.py
@@ -0,0 +1,131 @@
+from docker.models.containers import Container as DockerContainer
+
+from nginx_proxy import Host, ProxyConfigData
+from nginx_proxy.Container import Container, NoHostConiguration, UnreachableNetwork
+from nginx_proxy.utils import split_url
+
+
+def process_virtual_hosts(container: DockerContainer, environments: map, known_networks: set) -> ProxyConfigData:
+ """
+
+ :param container:
+ :param environments: parsed container environment
+ :param known_networks: networks known to the nginx-proxy container
+ :return:
+ """
+ hosts = ProxyConfigData()
+ try:
+ for host, location, proxied_container, extras in host_generator(container, known_networks=known_networks):
+ websocket = "ws" in host.scheme or "wss" in host.scheme
+ secured = 'https' in host.scheme or 'wss' in host.scheme
+ http = 'http' in host.scheme or 'https' in host.scheme
+ # it might return string if there's a error in processing
+ if type(host) is not str:
+ host.add_container(location, proxied_container, websocket=websocket, http=http)
+ if len(extras):
+ host.locations[location].update_extras({'injected': extras})
+ hosts.add_host(host)
+ print("Valid configuration ", "Id:" + container.id[:12],
+ " " + container.attrs["Name"].replace("/", ""), sep="\t")
+ return hosts
+ except NoHostConiguration:
+ print("No VIRTUAL_HOST ", "Id:" + container.id[:12],
+ " " + container.attrs["Name"].replace("/", ""), sep="\t")
+ except UnreachableNetwork:
+ print("Unreachable Network ", "Id:" + container.id[:12],
+ " " + container.attrs["Name"].replace("/", ""), sep="\t")
+ return hosts
+
+
+def _parse_host_entry(entry_string: str):
+ """
+
+ :param entry_string:
+ :return: (dict,dict)
+ """
+ configs = entry_string.split(";", 1)
+ extras = set()
+ if len(configs) > 1:
+ entry_string = configs[0]
+ for x in configs[1].split(';'):
+ x = x.strip()
+ if x:
+ extras.add(x)
+ host_list = entry_string.strip().split("->")
+ external, internal = host_list if len(host_list) is 2 else (host_list[0], "")
+ external, internal = (split_url(external), split_url(internal))
+ c = Container(None,
+ scheme=internal["scheme"] if internal["scheme"] else "http",
+ address=None,
+ port=internal["port"] if internal["port"] else None,
+ path=internal["location"] if internal["location"] else "/")
+ h = Host(
+ external["host"] if external["host"] else None,
+ # having https port on 80 will be detected later and used for redirection.
+ int(external["port"]) if external["port"] else 80,
+ scheme=external["scheme"] if external["scheme"] else {"http"}
+ )
+ return (h,
+ external["location"] if external["location"] else "/",
+ c, extras)
+
+
+def host_generator(container: DockerContainer, service_id: str = None, known_networks: set = {}):
+ """
+ :param container:
+ :param service_id:
+ :param known_networks:
+ :return: (Host,str,Container,set)
+ """
+ c = Container(container.id)
+ network_settings = container.attrs["NetworkSettings"]
+ env_map = Container.get_env_map(container)
+
+ # List all the environment variables with VIRTUAL_HOST and list them.
+ virtual_hosts = [x[1] for x in env_map.items() if x[0].startswith("VIRTUAL_HOST")]
+ if len(virtual_hosts) is 0:
+ raise NoHostConiguration()
+
+ # Instead of directly processing container details, check whether or not it's accessible through known networks.
+ known_networks = set(known_networks)
+ unknown = True
+ for name, detail in network_settings["Networks"].items():
+ c.add_network(detail["NetworkID"])
+ # fix for https://trello.com/c/js37t4ld
+ if detail["Aliases"] is not None:
+ if detail["NetworkID"] in known_networks and unknown:
+ alias = detail["Aliases"][len(detail["Aliases"]) - 1]
+ ip_address = detail["IPAddress"]
+ network = name
+ if ip_address:
+ break
+ else:
+ raise UnreachableNetwork()
+
+ override_ssl = False
+ override_port = None
+ if len(virtual_hosts) is 1:
+ if "LETSENCRYPT_HOST" in env_map:
+ override_ssl = True
+ if "VIRTUAL_PORT" in env_map:
+ override_port = env_map["VIRTUAL_PORT"]
+
+ for host_config in virtual_hosts:
+ host, location, container_data, extras = _parse_host_entry(host_config)
+ container_data.address = ip_address
+ container_data.id = container.id
+ if override_port:
+ container_data.port = override_port
+ elif container_data.port is None:
+ if len(network_settings["Ports"]) is 1:
+ container_data.port = int(list(network_settings["Ports"].keys())[0].split("/")[0])
+ else:
+ container_data.port = 80
+ if override_ssl:
+ if "ws" in host.scheme:
+ host.scheme = {"wss", "https"}
+ host.secured = True
+ else:
+ host.scheme = {"https", }
+ host.secured = 'https' in host.scheme or host.port == 443
+ yield (host, location, container_data, extras)
diff --git a/nginx_proxy/utils/__init__.py b/nginx_proxy/utils/__init__.py
new file mode 100644
index 0000000..6e31328
--- /dev/null
+++ b/nginx_proxy/utils/__init__.py
@@ -0,0 +1,24 @@
+from typing import Union, Dict
+
+
+def split_url(entry_string: str, default_scheme=None, default_port=None, default_location=None) -> Dict[
+ str, Union[str, int]]:
+ # Tried parsing urls with urllib.parse.urlparse but it doesn't work quiet
+ # well when scheme( eg: "https://") is missing eg "example.com"
+ # it says that example.com is path not the hostname.
+ if default_scheme is None:
+ default_scheme = []
+ split_scheme = entry_string.strip().split("://", 1)
+ scheme, host_part = split_scheme if len(split_scheme) is 2 else (default_scheme, split_scheme[0])
+ host_entries = host_part.split("/", 1)
+ hostport, location = (host_entries[0], "/" + host_entries[1]) if len(host_entries) is 2 else (
+ host_entries[0], default_location)
+ hostport_entries = hostport.split(":", 1)
+ host, port = hostport_entries if len(hostport_entries) is 2 else (hostport_entries[0], default_port)
+
+ return {
+ "scheme": set([x for x in scheme.split("+") if x]) if scheme else default_scheme,
+ "host": host if host else None,
+ "port": port,
+ "location": location
+ }
diff --git a/nginx_proxy/vhosts.py b/nginx_proxy/vhosts.py
new file mode 100644
index 0000000..208e73a
--- /dev/null
+++ b/nginx_proxy/vhosts.py
@@ -0,0 +1,11 @@
+from nginx.ConfigParser import ConfigParser as Parser
+
+
+def get_vhost_as_template(vhost_file: str):
+ parser = Parser()
+ parser.loadf(vhost_file)
+ b=3
+ print(str(parser.data))
+ parser.loadf(vhost_file)
+
+
diff --git a/requirements.txt b/requirements.txt
index 8796a8e..351e659 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
-docker==4.1.0
-Jinja2==2.10.3
-acme-nginx==0.2.1
-pydevd==1.9.0
+docker==4.2.0
+Jinja2==2.11.1
+pyOpenSSL==19.1.0
+pycrypto==2.6.1
+pydevd==1.9.0
\ No newline at end of file
diff --git a/vhosts_template/default.conf.jinja2 b/vhosts_template/default.conf.jinja2
new file mode 100644
index 0000000..020ebc6
--- /dev/null
+++ b/vhosts_template/default.conf.jinja2
@@ -0,0 +1,96 @@
+map $http_upgrade $connection_upgrade {
+ default upgrade;
+ '' close;
+}
+
+proxy_cache off;
+proxy_request_buffering off;
+
+ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:!DSS';
+ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
+ssl_prefer_server_ciphers on;
+ssl_session_timeout 5m;
+ssl_session_cache shared:SSL:50m;
+ssl_session_tickets off;
+ssl_stapling on;
+ssl_stapling_verify on;
+add_header Strict-Transport-Security "max-age=31536000" always;
+access_log /var/log/nginx/access.log;
+client_max_body_size {{ config.client_max_body_size }};
+
+{% for server in virtual_servers%}{% for upstream in server.upstreams %}
+ upstream {{ upstream.id }} { {% for container in upstream.containers %}
+ server {{ container.address }}:{{ container.port }};{% endfor %}
+ }{% endfor %}
+{% if server.secured %}
+server{
+ server_name {{ server.hostname }};
+ listen {{ server.port }} ssl http2 {{ server.extras.default_server }} ;
+ ssl_certificate /etc/ssl/certs/{{ server.ssl_file }}.crt;
+ ssl_certificate_key /etc/ssl/private/{{ server.ssl_file }}.key;{% if server.is_redirect %}
+ return 301 https://{{ server.full_redirect.hostname }}$request_uri;{% elif server.is_down %}
+ return 503;{% else %} {% if server.extras.security %}
+ auth_basic "Basic Auth Enabled";
+ auth_basic_user_file {{ server.extras.security_file }};{% endif %} {% for location in server.locations.values() %}
+ location {{ location.name }} { {% for injection in location.extras.injected %}
+ {{ injection }};{% endfor %} {% if location.extras.security %}
+ auth_basic_user_file {{ location.extras.security_file }};{% endif %} {% if location.upstream %}
+ proxy_pass {{ location.container.scheme }}://{{ location.upstream }}{{location.container.path}};{% else %}
+ proxy_pass {{location.container.scheme }}://{{ location.container.address }}:{{ location.container.port }}{{ location.container.path }};{% endif %}{% if location.name != '/' %}
+ proxy_redirect $scheme://$http_host{{ location.container.path if location.container.path else '/' }} $scheme://$http_host{{location.name}};{% endif %} {% if location.websocket and location.http %}
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;{% elif location.websocket %}
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "Upgrade";
+ proxy_read_timeout 1h;
+ proxy_send_timeout 1h;{% endif %}
+ }{% endfor %} {% endif %}
+
+}{% else %}
+server{
+ listen {{ server.port }} {{ server.extras.default_server }};
+ server_name {{ server.hostname }};{% if server.is_redirect %}
+ return 301 {{ "https" if server.secured else "http" }}://{{ server.full_redirect.hostname }}$request_uri;{% else %}{% for location in server.locations.values() %}
+ location {{ location.name }} { {% if location.upstream %}{% for injection in location.extras.injected %}
+ {{ injection }};{% endfor %}
+ proxy_pass {{ location.container.scheme }}://{{ location.upstream }}{{location.container.path}};{% else %}
+ proxy_pass {{location.container.scheme }}://{{ location.container.address }}:{{ location.container.port }}{{ location.container.path }};{% endif %}{% if location.name != '/' %}
+ proxy_redirect $scheme://$http_host{{ location.container.path if location.container.path else '/' }} $scheme://$http_host{{location.name}};{% endif %} {% if location.websocket and location.http %}
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;{% elif location.websocket %}
+ proxy_http_version 1.1;
+ proxy_read_timeout 1h;
+ proxy_send_timeout 1h;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "Upgrade";{% endif %}
+ }{% endfor %} {%endif %}
+ location /.well-known/acme-challenge/ {
+ alias {{ config.challenge_dir }};
+ try_files $uri =404;
+ }
+ }{% endif %}{% if server.ssl_redirect %}
+server {
+ listen 80 {{ server.extras.default_server }};
+ server_name {{ server.hostname }};
+ location /.well-known/acme-challenge/ {
+ alias {{ config.challenge_dir }};
+ try_files $uri =404;
+ }
+ location /{ {% if server.is_redirect %}
+ return 301 https://{{ server.full_redirect.hostname }}$request_uri;{% else %}
+ return 301 https://$host$request_uri;{% endif %}
+ }
+}{% endif %}{% endfor %}
+{% if config.default_server %}
+server{
+ listen 80 default_server;
+ server_name _ ;
+ location /.well-known/acme-challenge/ {
+ alias {{ config.challenge_dir }};
+ try_files $uri =404;
+ }
+ location / {
+ return 503;
+ }
+}{% endif %}
\ No newline at end of file
diff --git a/vhosts_template/default.conf.template b/vhosts_template/default.conf.template
deleted file mode 100644
index 3a3d9ba..0000000
--- a/vhosts_template/default.conf.template
+++ /dev/null
@@ -1,83 +0,0 @@
-map $http_upgrade $connection_upgrade {
- default upgrade;
- '' close;
-}
-
-{% for server in virtual_servers%}{% for upstream in server.upstreams %}
- upstream {{ upstream.id }} { {% for container in upstream.containers %}
- server {{ container.address }}:{{ container.port }}; {% endfor %}
- }{% endfor %}
-{% if server.secured %}
-server{
- server_name {{ server.hostname }};
- listen {{ server.port }} ssl http2 ;
- access_log /var/log/nginx/access.log;
- ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
- ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:!DSS';
- ssl_prefer_server_ciphers on;
- ssl_session_timeout 5m;
- ssl_session_cache shared:SSL:50m;
- ssl_session_tickets off;
- ssl_certificate /etc/ssl/certs/{{ server.ssl_file }}.crt;
- ssl_certificate_key /etc/ssl/private/{{ server.ssl_file }}.key;
- ssl_stapling on;
- ssl_stapling_verify on;
- add_header Strict-Transport-Security "max-age=31536000" always;
- {% for location in server.locations %}
- location {{ location.name }} { {% if location.upstream %}
- proxy_pass {{ location.container.scheme }}://{{ location.upstream }}{{location.container.path}};{% else %}
- proxy_pass {{location.container.scheme }}://{{ location.container.address }}:{{ location.container.port }}{{ location.container.path }};{% endif %}{% if location.name != '/' %}
- proxy_redirect $scheme://$http_host{% if location.container.path!="/" %}{{ location.container.path }}{% endif%} $scheme://$http_host{{location.name}};{% endif %} {% if location.websocket and location.http %}
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;{% elif location.websocket %}
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "Upgrade";
- proxy_read_timeout 7d;
- proxy_send_timeout 7d;{% endif %}
- }{% endfor %}
-
-}{% else %}
-server{
- listen {{ server.port }};
- server_name {{ server.hostname }};{% for location in server.locations %}
- location {{ location.name }} { {% if location.upstream %}
- proxy_pass {{ location.container.scheme }}://{{ location.upstream }}{{location.container.path}};{% else %}
- proxy_pass {{location.container.scheme }}://{{ location.container.address }}:{{ location.container.port }}{{ location.container.path }};{% endif %}{% if location.name != '/' %}
- proxy_redirect $scheme://$http_host{% if location.container.path!="/" %}{{ location.container.path }}{% endif%} $scheme://$http_host{{location.name}};{% endif %} {% if location.websocket and location.http %}
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;{% elif location.websocket %}
- proxy_http_version 1.1;
- proxy_read_timeout 7d;
- proxy_send_timeout 7d;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "Upgrade";{% endif %}
- }{% endfor %}
- location /.well-known/acme-challenge/ {
- alias {{ challenge_dir }};
- try_files $uri =404;
- }
- }{% endif %}{% if server.ssl_redirect %}
-server {
- listen 80;
- server_name {{ server.hostname }};
- location /.well-known/acme-challenge/ {
- alias {{ challenge_dir }};
- try_files $uri =404;
- }
- location /{
- return 301 https://$host$request_uri;
- }
-}{% endif %}{% endfor %}
-
-server{
- listen 80 default_server;
- server_name _ ;
- location /.well-known/acme-challenge/ {
- alias {{ challenge_dir }};
- try_files $uri =404;
- }
- location / {
- return 503;
- }
-}