├── .github
└── workflows
│ └── codeql-analysis.yml
├── .gitignore
├── .semver.yaml
├── LICENSE.md
├── Nginx
├── Dockerfile
├── acme.conf
├── build.sh
├── nginx.conf
├── publish.sh
├── ssl-create.sh
└── ssl-renew.sh
├── README.md
├── go.mod
├── go.sum
├── highlevel.go
├── lowlevel.go
├── midlevel.go
├── nginx.txt
├── ngman.go
├── setup.sh
└── types.go
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ "main" ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ "main" ]
20 | schedule:
21 | - cron: '42 19 * * 0'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'go' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v3
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v2
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 |
52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
53 | # queries: security-extended,security-and-quality
54 |
55 |
56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
57 | # If this step fails, then you should remove it and run the build manually (see below)
58 | - name: Autobuild
59 | uses: github/codeql-action/autobuild@v2
60 |
61 | # ℹ️ Command-line programs to run using the OS shell.
62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
63 |
64 | # If the Autobuild fails above, remove it and uncomment the following three lines.
65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
66 |
67 | # - run: |
68 | # echo "Run, Build Application using script"
69 | # ./location_of_script_within_repo/buildscript.sh
70 |
71 | - name: Perform CodeQL Analysis
72 | uses: github/codeql-action/analyze@v2
73 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea/
3 | release/
--------------------------------------------------------------------------------
/.semver.yaml:
--------------------------------------------------------------------------------
1 | alpha: 0
2 | beta: 0
3 | rc: 0
4 | release: v1.1.6
5 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 memmaker
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | # idea
2 | # create a nginx container that includes lego & ngman
3 | # it should expose ngman as an interface
4 | # and use lego in the background to create SSL certs
5 | FROM alpine:3.16
6 |
7 | ARG USERNAME=nginx
8 | ARG USER_UID=1000
9 |
10 | ARG NGMAN_VERSION="v1.1.6"
11 | ARG HOME=/home/$USERNAME
12 |
13 | COPY ssl-* /usr/local/bin/
14 |
15 | RUN adduser -u "$USER_UID" -D -g 'nginx user' -h /home/"$USERNAME"/ "$USERNAME" && \
16 | apk update && apk add --no-cache nginx curl && \
17 | mkdir -p /var/www /home/"$USERNAME"/default "$HOME" && \
18 | echo "
Default page
" > /home/"$USERNAME"/default/index.html && \
19 | curl -sL "https://github.com/memmaker/ngman/releases/download/${NGMAN_VERSION}/ngman_${NGMAN_VERSION}_linux_amd64.tgz" | tar xzO > /usr/local/bin/ngman && \
20 | curl -sL https://github.com/go-acme/lego/releases/download/v4.8.0/lego_v4.8.0_linux_amd64.tar.gz | tar xzO lego > /usr/local/bin/lego && \
21 | chown -R $USERNAME:0 "$HOME" /var/www /etc/nginx/ /usr/local/bin/ && \
22 | chmod g+rwxs "$HOME" /var/www /etc/nginx/ /usr/local/bin/ && \
23 | chmod +x /usr/local/bin/ngman /usr/local/bin/lego && \
24 | printf "\numask 0002\n" >> /etc/profile
25 |
26 |
27 | USER $USERNAME
28 |
29 | COPY *.conf /etc/nginx/
30 |
31 | RUN mkdir -p "$HOME"/.ngman
32 | VOLUME /ssl /var/www /etc/nginx/conf.d "$HOME"/.ngman /etc/nginx/dhparam.pem
33 |
34 | EXPOSE 1080 10443
35 |
36 | CMD ["nginx", "-g", "daemon off;", "-c", "/etc/nginx/nginx.conf"]
--------------------------------------------------------------------------------
/Nginx/acme.conf:
--------------------------------------------------------------------------------
1 |
2 | location ^~ /.well-known/acme-challenge {
3 | default_type "text/plain";
4 | root /var/www;
5 | allow all;
6 | break;
7 | }
8 |
9 | location = /.well-known/acme-challenge/ {
10 | return 404;
11 | }
--------------------------------------------------------------------------------
/Nginx/build.sh:
--------------------------------------------------------------------------------
1 | #podman build -f ./Dockerfile -t ghcr.io/memmaker/nginx:latest .
2 | podman build --cap-add all -f ./Dockerfile -t ghcr.io/memmaker/nginx:latest .
3 |
--------------------------------------------------------------------------------
/Nginx/nginx.conf:
--------------------------------------------------------------------------------
1 |
2 | #user nginx;
3 | worker_processes auto;
4 |
5 | error_log /dev/stdout notice;
6 | pid /home/nginx/nginx.pid;
7 |
8 |
9 | events {
10 | worker_connections 1024;
11 | }
12 |
13 |
14 | http {
15 | include /etc/nginx/mime.types;
16 | default_type application/octet-stream;
17 |
18 | log_format main '$remote_addr - $remote_user [$time_local] "$request" '
19 | '$status $body_bytes_sent "$http_referer" '
20 | '"$http_user_agent" "$http_x_forwarded_for"';
21 |
22 | access_log /dev/stdout main;
23 |
24 | sendfile on;
25 | #tcp_nopush on;
26 |
27 | keepalive_timeout 65;
28 |
29 | #gzip on;
30 |
31 | include /etc/nginx/conf.d/*.conf;
32 |
33 | client_max_body_size 200M;
34 |
35 | server {
36 | listen 1080 default_server;
37 | listen [::]:1080 default_server;
38 | server_name localhost;
39 | root /home/nginx/default;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Nginx/publish.sh:
--------------------------------------------------------------------------------
1 | podman push ghcr.io/memmaker/nginx:latest
--------------------------------------------------------------------------------
/Nginx/ssl-create.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | WEBROOT="/var/www"
4 |
5 | # User has to set these values for the container env
6 |
7 | #DNS_PROVIDER="cloudflare"
8 | #CLOUDFLARE_EMAIL="you@example.com"
9 | #CLOUDFLARE_API_KEY="yourprivatecloudflareapikey"
10 |
11 | create_ssl_cert_http () {
12 | DOMAIN="$1"
13 | # lego -s https://acme-staging-v02.api.letsencrypt.org/directory \
14 | lego -m "$ACMEMAIL" -a -d "$DOMAIN" --path /ssl --http --http.webroot "$WEBROOT" run
15 | }
16 |
17 | create_ssl_cert_dns () {
18 | DOMAIN="$1"
19 | # lego -s https://acme-staging-v02.api.letsencrypt.org/directory \
20 | lego -m "$ACMEMAIL" -a -d "$DOMAIN" --path /ssl --dns "$DNS_PROVIDER" run
21 | }
22 |
23 | if [ -z "$DNS_PROVIDER" ]; then
24 | create_ssl_cert_http "$1"
25 | else
26 | create_ssl_cert_dns "$1"
27 | fi
--------------------------------------------------------------------------------
/Nginx/ssl-renew.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | renew_ssl_cert_http () {
4 | DOMAIN="$1"
5 | # lego -s https://acme-staging-v02.api.letsencrypt.org/directory \
6 | lego -m "$ACMEMAIL" -a -d "$DOMAIN" --path /ssl --http renew
7 | }
8 |
9 | renew_ssl_cert_dns () {
10 | DOMAIN="$1"
11 | # lego -s https://acme-staging-v02.api.letsencrypt.org/directory \
12 | lego -m "$ACMEMAIL" -a -d "$DOMAIN" --path /ssl --dns renew
13 | }
14 |
15 | for keyfile in /ssl/certificates/*.key
16 | do
17 | replaced=$(echo "$keyfile" | tr '_' '*')
18 | if [ -z "$DNS_PROVIDER" ]; then
19 | renew_ssl_cert_http "${replaced%.*}" >> /tmp/cert.log
20 | else
21 | renew_ssl_cert_dns "${replaced%.*}" >> /tmp/cert.log
22 | fi
23 | done
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ngman
2 |
3 | A lightweight abstraction layer around [nginx](https://www.nginx.com/) and [lego](https://github.com/go-acme/lego)
4 |
5 | **Homepage / Demo:** https://textzentrisch.de/pages/ngman/
6 |
7 | ## Features
8 |
9 | * Launch a new website with a single command
10 | * Supports static locations
11 | * Supports reverse proxy locations
12 | * Simplified declarative or imperative configuration
13 | * Automatic SSL certificate generation and renewal
14 |
15 | It basically aims at making [nginx](https://www.nginx.com/) as easy to configure as [Caddy](https://caddyserver.com/).
16 | At least regarding the specific use-cases of static site hosting and reverse proxying.
17 |
18 | ## Requirements
19 |
20 | 1. A linux (Ubuntu 22.04) based web-server with root shell access
21 | 2. A domain name pointing to the ip address of the web-server
22 |
23 | **NOTE:** Currently the setup.sh script uses **apt** to install **podman**.
24 | It *should* also work correctly if you just pre-install podman via your package manager of choice and then run
25 | **setup.sh** script.
26 |
27 | In combination with [podman](https://podman.io/) and a pre-configured nginx container, you can do some pretty cool stuff.
28 | These examples use a container that has been built from the [ngman/Nginx subdirectory](https://github.com/memmaker/ngman/tree/main/Nginx).
29 |
30 | The container can be found here: https://github.com/memmaker/ngman/pkgs/container/nginx
31 |
32 | ## Self-hosted HTTPS static content in three steps
33 |
34 | 1. Setup a Web Server
35 | curl -sL https://raw.githubusercontent.com/memmaker/ngman/main/setup.sh | sh -s
36 |
37 | 2. Add a site with the respective domain
38 | ngman add-site
39 |
40 | 3. Publish your content
41 | echo "It Works" > /var/www//index.html
42 |
43 | You can now visit https://<your-domain>/ in the browser and will see "It Works".
44 |
45 | ## Self-hosted HTTPS reverse proxy in three steps
46 |
47 | 1. Setup a Web Server
48 | curl -sL https://raw.githubusercontent.com/memmaker/ngman/main/setup.sh | sh -s
49 |
50 | 2. Startup your service container
51 | podman run --name webserver --network podnet -dt docker.io/library/httpd:alpine
52 |
53 | 3. Add your service to ngman
54 | ngman add-proxy webserver:80
55 |
56 | You can now visit https://<your-domain>/ in the browser and will see "It Works".
57 |
58 | ## Adding new sites locations
59 |
60 | You can add additional virtual hosts to your web server by using the respective command:
61 |
62 | ngman add-site
63 | or
64 | ngman add-location /static /var/www//static
65 | or
66 | ngman add-proxy webserver:80
67 |
68 | These will
69 |
70 | * update your site's configuration,
71 | * write a new nginx configuration file,
72 | * make sure that the SSL certificate is available
73 | * and reload the nginx container.
74 |
75 | Alternatively you can also edit the configuration file directly:
76 |
77 | ngman edit
78 |
79 | ## What does setup.sh do?
80 |
81 | 1. Installs [podman](https://podman.io/)
82 | 2. Generate DH parameters for HTTPS
83 | 3. Set up permissions for the shared folders
84 | 4. Set up a container network with DNS support
85 | 5. Start a pre-configured nginx container that includes [lego](https://github.com/go-acme/lego)
86 | 6. Set up a cronjob for automatic SSL certificate renewal
87 |
88 | ## Concepts of ngman
89 |
90 | A site is uniquely identified by the domain name.
91 |
92 | This tool supports two types of locations: static & proxy.
93 |
94 | A site can have multiple static and proxy locations.
95 |
96 | The configuration and state of this tool is kept under its config directory.
97 | By default, that is **"~/.ngman/"** and the tool will create it on first start.
98 | I am using [TOML](https://github.com/toml-lang/toml) as the configuration format.
99 |
100 | The config directory also needs to contain **"nginx.txt"**, the file with all the partial templates.
101 | You can easily adapt that file to your needs to adjust the nginx configurations created.
102 |
103 | For every site that it manages the tool will create a file in the **~/.ngman/sites/** directory.
104 |
105 | You can always re-create all the nginx config files by running **ngman write-all**.
106 |
107 | ## Global settings (config.toml)
108 |
109 | Example config.toml file in a production environment:
110 |
111 | CertificateRootPath = '/ssl/certificates'
112 | SiteStorageDirectory = '/root/.ngman/sites'
113 | NginxSiteConfigDirectory = '/etc/nginx/sites-enabled'
114 | TemplateFile = '/root/.ngman/nginx.txt'
115 | PostRunCommand = 'service nginx reload'
116 | GenerateCertCommand = 'create_ssl_cert'
117 |
118 | ### CertificateRootPath
119 |
120 | The path to the directory where the SSL certificates are stored.
121 | The files are expected to conform to the following naming scheme:
122 |
123 | .key
124 | .crt
125 |
126 | ### SiteStorageDirectory
127 |
128 | The path to the directory where the TOML site configuration files are stored.
129 | Must be writable by the user executing the tool.
130 |
131 | ### NginxSiteConfigDirectory
132 |
133 | This is the main output directory for the nginx config files.
134 |
135 | ### TemplateFile
136 |
137 | The path to the file that contains the nginx config templates.
138 |
139 | The template language used is [Go's text/template](https://golang.org/pkg/text/template/).
140 |
141 | ### PostRunCommand
142 |
143 | ngman will try to execute this command after it has made any changes to nginx configuration files.
144 |
145 | ### GenerateCertCommand
146 |
147 | ngman will try to execute this command when it needs to generate a new SSL certificate.
148 | It will pass the respective domain name as the first argument.
149 |
150 |
151 | ## Usage
152 |
153 | ngman list
154 | ngman create
155 | ngman add-static
156 | ngman add-proxy
157 | ngman edit
158 | ngman delete
159 | ngman write-all
160 |
161 | ## Advanced Usage
162 |
163 | ### Enable automated startup on reboot
164 |
165 | This little script will create a systemd unit file, enabling automatic startup of the nginx container on reboot.
166 |
167 | mkdir -p "$HOME"/.config/systemd/user
168 |
169 | if [ ! -f "$HOME"/.config/systemd/user/nginx.service ]; then
170 | echo "Creating systemd service for nginx"
171 | podman generate systemd --restart-policy=always -t 1 ngx > "$HOME"/.config/systemd/user/nginx.service
172 | systemctl --user enable nginx.service
173 | sudo loginctl enable-linger
174 | fi
175 |
176 |
177 | ### PHP-FPM Support
178 |
179 | You can add the key UsePHP to a site config to enable PHP-FPM support.
180 |
181 | Example:
182 |
183 | Domain = 'example.org'
184 | RootPath = '/var/www/example'
185 | UsePHP = true # <-- this is the important part
186 |
187 | ngman will then insert the template called **"php-fpm-support"** from the **"nginx.txt"** file into the nginx configuration of that site.
188 |
189 | ### Misc. Options
190 |
191 | The TOML files for the site configuration also allow adding an array of
192 | miscellaneous options to the nginx config file.
193 |
194 | Every string to be found in the array called **"MiscOptions"** in a site configuration
195 | will be inserted as a single line into the nginx config file.
196 |
197 | Example:
198 |
199 | Domain = 'example.com'
200 | RootPath = '/var/www/example.com'
201 | MiscOptions = [
202 | 'gzip on',
203 | 'gzip_disable "msie6"',
204 | 'gzip_vary on',
205 | 'gzip_proxied any'
206 | ]
207 |
208 | **Note:** The semicolon is appended automatically in the config template.
209 |
210 | ### Chunks
211 |
212 | When creating the nginx configuration files, ngman also looks into
213 | a directory called chunks for files named like the domain name.
214 |
215 | config.SiteStorageDirectory + "/chunks/" + domain
216 | eg. example.org -> /root/.ngman/sites/chunks/example.org
217 |
218 | These configuration chunks are inserted into the nginx site configuration file as is.
219 | This mechanism can be used for further customizations of individual sites.
220 |
221 | ### Wildcard certificate support
222 |
223 | ngman will assume that any subdomain will require a wildcard certificate.
224 |
225 | So if you add a site with a domain like **"example.org"** a normal LetsEncrypt certificate will be generated.
226 |
227 | However, if you add a site with a domain like **"sub.example.org"** a wildcard certificate will be generated and used.
228 |
229 | Subsequent sites with a domain like **"foo.example.org"** will then also use the same wildcard certificate.
230 |
231 | NOTE: In order to use wildcard support, you will have to provide the file **"~/.ngman/dnsprovider.env"**.
232 | This file should contain the credentials for your DNS provider.
233 |
234 | See [lego's documentation](https://go-acme.github.io/lego/dns/) for more information.
235 |
236 | Example:
237 |
238 | root@dallas:~/.ngman# cat dnsprovider.env
239 | DNS_PROVIDER=dode
240 | DODE_TOKEN=12345678901234677
241 |
242 | For your convenience, I have provided the script that I use for setting this up:
243 |
244 | mkdir -m 2774 "$HOME"/.ngman
245 | printf "DNS_PROVIDER=dode\nDODE_TOKEN=12345678901234677" > "$HOME"/.ngman/dnsprovider.env
246 | curl -sL https://raw.githubusercontent.com/memmaker/ngman/main/setup.sh | sh -s "$EMAIL"
247 |
248 | You would have to replace the information of the DNS provider with your own.
249 |
250 | ## Standalone usage of ngman
251 |
252 | **NOTE: ALL THE FOLLOWING IS ALREADY INCLUDED AND AUTOMATED IN THE [setup.sh file](https://github.com/memmaker/ngman/blob/main/setup.sh) and [corresponding nginx container](https://github.com/memmaker/ngman/pkgs/container/nginx)**
253 |
254 | I suggest using [lego](https://github.com/go-acme/lego) in combination with [podman](https://podman.io/) for certificate generation.
255 | You can then do something like this
256 |
257 | create_ssl_cert () {
258 | podman run \
259 | --env [YOUR-DOMAIN-API-TOKEN] \
260 | -v /ssl:/lego \
261 | goacme/lego \
262 | --accept-tos \
263 | --path /lego \
264 | --email [YOUR-EMAIL] \
265 | --dns dode \
266 | --domains "$@" \
267 | run
268 | }
269 |
270 | Which will create a command as expected by **ngman**, where you just have to provide a domain name as argument.
271 |
272 | For certificate renewal, I suggest something like this
273 |
274 | for keyfile in $(sudo ls /ssl/certificates/ | grep key)
275 | do
276 | replaced="${keyfile/_/*}"
277 | renew_ssl_cert "${replaced%.*}" >> /tmp/cert.log
278 | done
279 |
280 | Where renew_ssl_cert is the same as create_ssl_cert, but with the **run** command replaced by **renew**.
281 |
282 |
283 | ### Installation
284 |
285 | ARCH=darwin_amd64;
286 | mkdir ~/.ngman > /dev/null 2>&1;
287 | pushd;
288 | cd ~/.ngman && \
289 | wget https://github.com/memmaker/ngman/releases/latest/download/nginx.txt && \
290 | wget https://github.com/memmaker/ngman/releases/latest/download/ngman_${ARCH}.zip && \
291 | unzip ngman_${ARCH}.zip && rm ngman_${ARCH}.zip && \
292 | mv ngman_${ARCH} /usr/local/bin/ngman && popd
293 |
294 | ### Uninstall
295 |
296 | rm -rf ~/.ngman && rm /usr/local/bin/ngman
297 |
298 |
299 | ### Setting DNS resolver for containers
300 |
301 | ngman will set the DNS resolver for all reverse proxy locations, if it detects the
302 | **NGMAN_PROXY_RESOLVER** environment variable.
303 |
304 | Example:
305 |
306 | export NGMAN_PROXY_RESOLVER=10.89.1.1
307 |
308 | NOTE: This will be applied on write, so the environment variable must be set then.
309 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module nginxManager
2 |
3 | go 1.19
4 |
5 | require github.com/pelletier/go-toml/v2 v2.0.5
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
5 | github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
9 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
10 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
11 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
12 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
15 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
16 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
17 |
--------------------------------------------------------------------------------
/highlevel.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "os/exec"
8 | "path"
9 | "strings"
10 | )
11 |
12 | func addProxy(domain string, endpoint string, uriLocation string, headers map[string]string) bool {
13 | ensureSiteExists(domain)
14 | endpoint = strings.Replace(endpoint, "http://", "", 1)
15 | endpoint = strings.Replace(endpoint, "https://", "", 1)
16 | var site SiteInfo
17 | newLocation := ReverseProxyLocation{
18 | URLLocation: uriLocation,
19 | Endpoint: endpoint,
20 | Headers: headers,
21 | }
22 | site = getSiteByDomain(domain)
23 | site.ProxyLocations = append(site.ProxyLocations, newLocation)
24 | updateSite(site)
25 | return true
26 | }
27 |
28 | func addStaticSite(domain string, rootPath string, uriLocation string) bool {
29 | ensureSiteExists(domain)
30 |
31 | if !dirExists(rootPath) {
32 | ensureDirExists(rootPath)
33 | content := "It's working! (" + domain + ")
"
34 | try(writeFile(path.Join(rootPath, "index.html"), []byte(content)))
35 | }
36 | var site SiteInfo
37 | // count the number of dots in the domain name
38 | newLocation := StaticLocation{
39 | URLLocation: uriLocation,
40 | RootPath: rootPath,
41 | }
42 | site = getSiteByDomain(domain)
43 | site.StaticLocations = append(site.StaticLocations, newLocation)
44 | updateSite(site)
45 | return true
46 | }
47 |
48 | func ensureSiteExists(domain string) {
49 | if !siteExists(domain) {
50 | rootPath := path.Join(config.WebRootPath, domain)
51 | fmt.Println("No WebRoot specified, using '" + rootPath + "'")
52 | createSite(domain, rootPath)
53 | }
54 | }
55 |
56 | func isSubDomain(domain string) bool {
57 | dots := strings.Count(domain, ".")
58 | return dots > 1
59 | }
60 |
61 | func editSite(domain string) {
62 | // call the editor
63 | editor := os.Getenv("EDITOR")
64 | if editor == "" {
65 | editor = "nano"
66 | }
67 | cmd := exec.Command(editor, path.Join(config.SiteStorageDirectory, domain+".toml"))
68 | cmd.Stdin = os.Stdin
69 | cmd.Stdout = os.Stdout
70 | cmd.Stderr = os.Stderr
71 | err := cmd.Run()
72 | if err != nil {
73 | log.Fatal(err)
74 | }
75 |
76 | site := getSiteByDomain(domain)
77 | writeNginxConfig(site)
78 | fmt.Println("Site updated: " + domain)
79 | }
80 |
81 | func listAll() {
82 | sites := getAllSites()
83 | for _, site := range sites {
84 | if site.RootPath != "" {
85 | fmt.Println(site.Domain + " -> " + site.RootPath)
86 | } else {
87 | fmt.Println(site.Domain)
88 | }
89 | // print all static locations
90 | for _, location := range site.StaticLocations {
91 | fmt.Println(" " + location.URLLocation + " -> " + location.RootPath)
92 | }
93 | // print all proxy locations
94 | for _, location := range site.ProxyLocations {
95 | fmt.Println(" " + location.URLLocation + " -> " + location.Endpoint)
96 | }
97 | }
98 | }
99 |
100 | func writeAll() {
101 | sites := getAllSites()
102 | for _, site := range sites {
103 | writeNginxConfig(site)
104 | }
105 | }
106 |
107 | func deleteSite(domain string) {
108 | try(os.Remove(path.Join(config.SiteStorageDirectory, domain+".toml")))
109 | try(os.Remove(path.Join(config.NginxSiteConfigDirectory, domain+".conf")))
110 | path.Join()
111 | }
112 |
113 | func createSite(domain string, rootPath string) {
114 | if !siteExists(domain) {
115 | initSite(domain, rootPath)
116 | } else {
117 | fmt.Println("Site already exists: " + domain)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/lowlevel.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "log"
7 | "os"
8 | "strings"
9 | )
10 |
11 | func removeRedundantEmptyLines(content string) string {
12 | return strings.Replace(content, "\n\n", "\n", -1)
13 | }
14 |
15 | func must[V any](value V, err error) V {
16 | if err != nil {
17 | log.Fatal(err)
18 | }
19 | return value
20 | }
21 | func try(err error) {
22 | if err != nil {
23 | fmt.Println("Warning: " + err.Error())
24 | }
25 | }
26 |
27 | func dirExists(path string) bool {
28 | _, err := os.Stat(path)
29 | return !os.IsNotExist(err)
30 | }
31 |
32 | func fileExists(filename string) bool {
33 | _, err := os.Stat(filename)
34 | return !os.IsNotExist(err)
35 | }
36 |
37 | func ensureDirExists(dir string) {
38 | _, err := os.Stat(dir)
39 | if os.IsNotExist(err) {
40 | fmt.Println("Directory '" + dir + "' does not exist, creating it")
41 | try(os.Mkdir(dir, 02774))
42 | }
43 | }
44 |
45 | func readFile(filename string) string {
46 | data, err := os.ReadFile(filename)
47 | if err != nil {
48 | return ""
49 | }
50 | return string(data)
51 | }
52 |
53 | func readLines(filename string) []string {
54 | scanner := bufio.NewScanner(must(os.Open(filename)))
55 | var lines []string
56 | for scanner.Scan() {
57 | lines = append(lines, scanner.Text())
58 | }
59 | return lines
60 | }
61 |
62 | func writeFile(filename string, content []byte) error {
63 | return os.WriteFile(filename, content, 00774)
64 | }
65 |
--------------------------------------------------------------------------------
/midlevel.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "github.com/pelletier/go-toml/v2"
8 | "io/fs"
9 | "log"
10 | "os"
11 | "os/exec"
12 | "path"
13 | "strings"
14 | "text/template"
15 | )
16 |
17 | func getAllSites() []SiteInfo {
18 | var sites []SiteInfo
19 | files, err := os.ReadDir(config.SiteStorageDirectory)
20 | if err != nil {
21 | return sites
22 | }
23 | for _, file := range files {
24 | if file.IsDir() {
25 | continue
26 | }
27 | siteFilename := file.Name()
28 | var domainOfSite string
29 | if strings.HasSuffix(siteFilename, ".toml") {
30 | domainOfSite = strings.TrimSuffix(siteFilename, ".toml")
31 | sites = append(sites, getSiteByDomain(domainOfSite))
32 | }
33 | }
34 | return sites
35 | }
36 |
37 | func getChunk(domain string) string {
38 | chunkString := readFile(path.Join(config.SiteStorageDirectory, "chunks", domain))
39 | return chunkString
40 | }
41 |
42 | func getResolver() string {
43 | return os.Getenv("NGMAN_PROXY_RESOLVER")
44 | }
45 |
46 | func chunkExists(domain string) bool {
47 | _, err := os.Stat(path.Join(config.SiteStorageDirectory, "chunks", domain))
48 | return !errors.Is(err, fs.ErrNotExist)
49 | }
50 |
51 | func getSiteByDomain(domain string) SiteInfo {
52 | tomlString := readFile(path.Join(config.SiteStorageDirectory, domain+".toml"))
53 | var siteinfo SiteInfo
54 | try(toml.Unmarshal([]byte(tomlString), &siteinfo))
55 | return siteinfo
56 | }
57 |
58 | func siteExists(domain string) bool {
59 | _, err := os.Stat(path.Join(config.SiteStorageDirectory, domain+".toml"))
60 | return !os.IsNotExist(err)
61 | }
62 |
63 | func initSite(domain string, rootPath string) {
64 | useWildcard := isSubDomain(domain)
65 | ensureDirExists(rootPath)
66 | site := SiteInfo{
67 | Domain: domain,
68 | RootPath: rootPath,
69 | }
70 | writeHTTPOnlyNginxConfig(site)
71 | tryPostRunCommand()
72 | if !certExists(domain, useWildcard) {
73 | fmt.Println("No certificate found for " + domain + ". Generating one...")
74 | tryGenerateCertificate(domain, rootPath, useWildcard)
75 | }
76 | if certExists(domain, useWildcard) {
77 | updateSite(site)
78 | tryPostRunCommand()
79 | }
80 | }
81 |
82 | func certExists(domain string, useWildcard bool) bool {
83 | var certFileName string
84 | if useWildcard {
85 | certFileName = path.Join(config.CertificateRootPath, getWildcardName(domain)+".crt")
86 | } else {
87 | certFileName = path.Join(config.CertificateRootPath, domain+".crt")
88 | }
89 | exists := fileExists(certFileName)
90 | if exists {
91 | fmt.Println("Using certificate " + certFileName)
92 | }
93 | return exists
94 | }
95 |
96 | func tryGenerateCertificate(domain string, rootPath string, wildcard bool) {
97 | if config.GenerateCertCommand == "" {
98 | fmt.Println("No certificate generation command specified. Skipping...")
99 | return
100 | }
101 | if wildcard {
102 | wildcardName := getWildcardName(domain)
103 | wildcardName = strings.Replace(wildcardName, "_", "*", 1)
104 | domain = wildcardName
105 | }
106 | commandLine := config.GenerateCertCommand + " " + domain + " " + rootPath
107 | arguments := []string{"--login", "-c", commandLine}
108 | fmt.Println("Running certificate generation command: sh " + strings.Join(arguments, " "))
109 | cmd := exec.Command("sh", arguments...)
110 | cmd.Stdout = os.Stdout
111 | cmd.Stderr = os.Stderr
112 | try(cmd.Run())
113 | }
114 |
115 | func tryPostRunCommand() {
116 | if config.PostRunCommand != "" {
117 | cmd := exec.Command("sh", "--login", "-c", config.PostRunCommand)
118 | // attach stdout and stderr to the current process
119 | cmd.Stdout = os.Stdout
120 | cmd.Stderr = os.Stderr
121 | try(cmd.Run())
122 | }
123 | }
124 |
125 | func updateSite(siteinfo SiteInfo) {
126 | writeSiteInfo(siteinfo)
127 | writeNginxConfig(siteinfo)
128 | }
129 |
130 | func writeSiteInfo(siteinfo SiteInfo) {
131 | // marshal the siteinfo to toml
132 | tomlString := must(toml.Marshal(siteinfo))
133 | try(writeFile(path.Join(config.SiteStorageDirectory, siteinfo.Domain+".toml"), tomlString))
134 | }
135 | func writeNginxConfig(site SiteInfo) {
136 | context := RenderContext{
137 | Site: site,
138 | Config: config,
139 | SSLEnabled: true,
140 | }
141 | renderNginxConfig(site, context)
142 | }
143 |
144 | func writeHTTPOnlyNginxConfig(site SiteInfo) {
145 | context := RenderContext{
146 | Site: site,
147 | Config: config,
148 | SSLEnabled: false,
149 | }
150 | renderNginxConfig(site, context)
151 | }
152 |
153 | func renderNginxConfig(site SiteInfo, context RenderContext) {
154 | output := renderTemplate(context)
155 | renderedString := removeRedundantEmptyLines(string(output))
156 | try(writeFile(path.Join(config.NginxSiteConfigDirectory, site.Domain+".conf"), []byte(renderedString)))
157 | }
158 |
159 | func loadConfig() {
160 | configDir := getConfigDir()
161 | ensureDirExists(configDir)
162 | configFilename := path.Join(configDir, "config.toml")
163 | if !fileExists(configFilename) {
164 | config = GlobalConfig{
165 | SiteStorageDirectory: path.Join(configDir, "sites"),
166 | TemplateFile: path.Join(configDir, "nginx.txt"),
167 | NginxSiteConfigDirectory: path.Join(configDir, "sites-enabled"),
168 | PostRunCommand: "",
169 | GenerateCertCommand: "",
170 | CertificateRootPath: "/ssl/certificates",
171 | WebRootPath: "/var/www",
172 | }
173 |
174 | tomlString := must(toml.Marshal(config))
175 | try(writeFile(configFilename, tomlString))
176 | } else {
177 | tomlString := readFile(configFilename)
178 | try(toml.Unmarshal([]byte(tomlString), &config))
179 | }
180 | }
181 |
182 | func getConfigDir() string {
183 | homeDir := must(os.UserHomeDir())
184 | configDir := path.Join(homeDir, ".ngman")
185 | return configDir
186 | }
187 |
188 | // sub.domain.com -> _.domain.com
189 | func getWildcardName(domain string) string {
190 | domainParts := strings.Split(domain, ".")
191 | return "_." + strings.Join(domainParts[len(domainParts)-2:], ".")
192 | }
193 |
194 | func renderTemplate(data RenderContext) []byte {
195 | var buffer bytes.Buffer
196 | err := rootTemplate.ExecuteTemplate(&buffer, "nginx", data)
197 | if err != nil {
198 | log.Fatal(err)
199 | }
200 | return buffer.Bytes()
201 | }
202 |
203 | func loadTemplates() {
204 | rootTemplate = template.Must(template.New("nginx").Funcs(template.FuncMap{"getWildcardName": getWildcardName, "getChunk": getChunk, "chunkExists": chunkExists, "getResolver": getResolver}).ParseFiles(config.TemplateFile))
205 | }
206 |
--------------------------------------------------------------------------------
/nginx.txt:
--------------------------------------------------------------------------------
1 | {{define "static"}}
2 | location {{ .URLLocation }} {
3 | root {{ .RootPath }};
4 | expires 30d;
5 | }
6 | {{ end }}
7 |
8 | {{define "proxy"}}
9 | location {{ .URLLocation }} {
10 | {{- $resolver := getResolver -}}
11 | {{- if $resolver }}
12 | resolver {{ $resolver }} ipv6=off;
13 | {{- end }}
14 | set $proxy_server_location {{ .Endpoint }};
15 | proxy_pass http://$proxy_server_location;
16 | {{ range $key, $value := .Headers }}proxy_set_header {{ $key }} {{ $value }};
17 | {{ end }}
18 | proxy_pass_header Authorization;
19 | proxy_http_version 1.1;
20 | proxy_set_header X-Real-IP $remote_addr;
21 | proxy_set_header X-Forwarded-For $remote_addr;
22 | proxy_set_header Host $host;
23 | proxy_set_header "Connection" "";
24 | }
25 | {{ end }}
26 |
27 | {{ define "php-fpm-support" }}
28 | location ~ ^(.+\.php)(.*)$ {
29 | try_files $fastcgi_script_name =404;
30 | include /etc/nginx/fastcgi_params;
31 | fastcgi_split_path_info ^(.+\.php)(.*)$;
32 | fastcgi_pass unix:/var/run/php/php-fpm.sock;
33 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
34 | fastcgi_param PATH_INFO $fastcgi_path_info;
35 | }
36 | {{ end }}
37 | {{ define "nginx" }}
38 | {{ if .SSLEnabled }}
39 | server {
40 | index index.html;
41 | server_name {{ .Site.Domain }};
42 | {{ if .Site.RootPath }}
43 | root {{ .Site.RootPath }};
44 | {{ end }}
45 |
46 | charset utf-8;
47 |
48 | {{ range $option := .Site.MiscOptions }}
49 | {{ $option }};
50 | {{ end }}
51 |
52 | location ~ /(\.ht) {
53 | deny all;
54 | return 404;
55 | }
56 |
57 | listen [::]:10443 ssl http2;
58 | listen 10443 ssl http2;
59 |
60 | ### SSL Certificates ###
61 | {{ if .Site.UseWildcardCert }}
62 | ssl_certificate_key {{ .Config.CertificateRootPath }}/{{ getWildcardName .Site.Domain }}.key;
63 | ssl_certificate {{ .Config.CertificateRootPath }}/{{ getWildcardName .Site.Domain }}.crt;
64 | {{ else }}
65 | ssl_certificate_key {{ .Config.CertificateRootPath }}/{{ .Site.Domain }}.key;
66 | ssl_certificate {{ .Config.CertificateRootPath }}/{{ .Site.Domain }}.crt;
67 | {{ end }}
68 |
69 | ### SSL CIPHERS BLOCK ###
70 | ssl_protocols TLSv1.3;# Requires nginx >= 1.13.0 else use TLSv1.2
71 | ssl_prefer_server_ciphers on;
72 | ssl_dhparam /etc/nginx/dhparam.pem; # openssl dhparam -out /etc/nginx/dhparam.pem 4096
73 | ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
74 | ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
75 | ssl_session_timeout 10m;
76 | ssl_session_cache shared:SSL:10m;
77 | ssl_session_tickets off; # Requires nginx >= 1.5.9
78 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
79 | add_header X-Frame-Options DENY;
80 | add_header X-Content-Type-Options nosniff;
81 | add_header X-XSS-Protection "1; mode=block";
82 |
83 | {{ if chunkExists .Site.Domain }}
84 | {{ getChunk .Site.Domain }}
85 | {{ end }}
86 |
87 | {{ range $index, $location := .Site.StaticLocations }}
88 | {{ template "static" $location }}
89 | {{ end }}
90 |
91 | {{ range $index, $location := .Site.ProxyLocations }}
92 | {{ template "proxy" $location }}
93 | {{ end }}
94 |
95 | {{ if .Site.UsePHP }}
96 | {{ template "php-fpm-support" . }}
97 | {{ end }}
98 | }
99 | {{ end }}
100 | server {
101 | listen 1080;
102 | listen [::]:1080;
103 | server_name {{ .Site.Domain }};
104 |
105 | location / {
106 | include /etc/nginx/acme[.]conf;
107 |
108 | if ($host = {{ .Site.Domain }}) {
109 | return 301 https://$host$request_uri;
110 | }
111 | return 404;
112 | }
113 | }
114 | {{ end }}
--------------------------------------------------------------------------------
/ngman.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "path"
8 | "text/template"
9 | )
10 |
11 | var config GlobalConfig
12 |
13 | var rootTemplate *template.Template
14 |
15 | func main() {
16 | loadConfig()
17 | if config.WebRootPath == "" {
18 | printCheckConfig()
19 | log.Fatal("Web root path is not set.")
20 | }
21 | if config.SiteStorageDirectory == "" {
22 | printCheckConfig()
23 | log.Fatal("Site storage directory is not set.")
24 | }
25 | if config.NginxSiteConfigDirectory == "" {
26 | printCheckConfig()
27 | log.Fatal("Nginx site config directory is not set.")
28 | }
29 | if !fileExists(config.TemplateFile) {
30 | printCheckConfig()
31 | log.Fatal("Template file '" + config.TemplateFile + "' does not exist")
32 | }
33 | ensureDirExists(config.WebRootPath)
34 | ensureDirExists(config.SiteStorageDirectory)
35 | ensureDirExists(config.NginxSiteConfigDirectory)
36 |
37 | if !dirExists(config.WebRootPath) || !dirExists(config.SiteStorageDirectory) || !dirExists(config.NginxSiteConfigDirectory) {
38 | printCheckConfig()
39 | log.Fatal("Could not ensure that all directories exist, check access rights.")
40 | }
41 |
42 | loadTemplates()
43 |
44 | // get command line arguments
45 | args := os.Args[1:]
46 | if len(args) == 0 {
47 | printUsage()
48 | return
49 | }
50 | if args[0] == "add-site" && len(args) > 1 {
51 | var rootPath string
52 | if len(args) == 3 {
53 | rootPath = args[2]
54 | } else {
55 | rootPath = path.Join(config.WebRootPath, args[1])
56 | fmt.Println("No WebRoot specified, using '" + rootPath + "'")
57 | }
58 | createSite(args[1], rootPath)
59 | return
60 | }
61 | if args[0] == "add-static" && len(args) == 4 {
62 | addStaticSite(args[1], args[2], args[3])
63 | tryPostRunCommand()
64 | return
65 | }
66 | if args[0] == "add-proxy" && len(args) > 2 {
67 | var uriLocation string
68 | if len(args) == 4 {
69 | uriLocation = args[3]
70 | } else {
71 | uriLocation = "/"
72 | fmt.Println("No URI location specified, using '" + uriLocation + "'")
73 | }
74 | addProxy(args[1], args[2], uriLocation, nil)
75 | tryPostRunCommand()
76 | return
77 | }
78 | if args[0] == "delete" && len(args) == 2 {
79 | deleteSite(args[1])
80 | tryPostRunCommand()
81 | return
82 | }
83 | if args[0] == "edit" && len(args) == 2 {
84 | editSite(args[1])
85 | tryPostRunCommand()
86 | return
87 | }
88 | if args[0] == "list" {
89 | listAll()
90 | return
91 | }
92 | if args[0] == "write-all" {
93 | writeAll()
94 | tryPostRunCommand()
95 | return
96 | }
97 | printUsage()
98 | }
99 |
100 | func printCheckConfig() {
101 | configFilename := path.Join(getConfigDir(), "config.toml")
102 | fmt.Println("Please check the config file at " + configFilename)
103 | }
104 |
105 | func printUsage() {
106 | fmt.Println("Usage: ngman list")
107 | fmt.Println("Usage: ngman add-site []")
108 | fmt.Println("Usage: ngman add-static ")
109 | fmt.Println("Usage: ngman add-proxy []")
110 | fmt.Println("Usage: ngman edit ")
111 | fmt.Println("Usage: ngman delete ")
112 | fmt.Println("Usage: ngman write-all")
113 | }
114 |
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | EMAIL="$1"
4 |
5 | DH_PARAM_BITS=2048
6 |
7 | # Check for needed arguments
8 | if [ -z "$EMAIL" ]; then
9 | echo "Please provide an email address as first argument"
10 | exit 1
11 | fi
12 |
13 | newcron () {
14 | crontab -l > /tmp/crontab_temp 2> /dev/null
15 | if grep -Fxq "$*" /tmp/crontab_temp; then
16 | echo "Cronjob already exists, skipping"
17 | else
18 | echo "Adding cronjob $*"
19 | echo "$*" >> /tmp/crontab_temp && \
20 | crontab /tmp/crontab_temp && \
21 | rm /tmp/crontab_temp
22 | fi
23 | }
24 |
25 | WEBROOT="$HOME/www"
26 | CERTROOT="$HOME/ssl"
27 |
28 | mkdir -p "$WEBROOT" "$CERTROOT" "$HOME"/.ngman "$HOME"/keys "$HOME"/bin "$HOME"/nginx-conf
29 |
30 | if ! command -v podman 1> /dev/null 2> /dev/null
31 | then
32 | echo "podman not found, installing it"
33 | sudo apt-get update 1> /dev/null 2> /dev/null && sudo apt-get install -y podman > /dev/null
34 | fi
35 |
36 | if [ ! -f /etc/sysctl.d/99-rootless.conf ]; then
37 | echo "Setting up sysctl for rootless http services"
38 | echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/99-rootless.conf
39 | sudo sysctl --system
40 | fi
41 |
42 | if [ ! -f "$HOME"/.ngman/nginx.txt ]; then
43 | curl -sL "https://raw.githubusercontent.com/memmaker/ngman/main/nginx.txt" > "$HOME"/.ngman/nginx.txt
44 | fi
45 |
46 | if [ ! -f "$HOME"/.ngman/config.toml ]; then
47 | printf "CertificateRootPath = '/ssl/certificates'\nSiteStorageDirectory = '/home/nginx/.ngman/sites'\nNginxSiteConfigDirectory = '/etc/nginx/conf.d'\nTemplateFile = '/home/nginx/.ngman/nginx.txt'\nPostRunCommand = 'nginx -s reload'\nWebRootPath = '/var/www'\nGenerateCertCommand = 'ssl-create.sh'" > "$HOME"/.ngman/config.toml
48 | fi
49 |
50 | if [ ! -f "$HOME"/.ngman/dnsprovider.env ] || [ ! -s "$HOME"/.ngman/dnsprovider.env ]; then
51 | echo "Could not find dnsprovider.env, please create it and add your dns provider credentials"
52 | echo "ACME DNS Challenge is currently disabled, no wildcard certificate support."
53 | touch "$HOME"/.ngman/dnsprovider.env
54 | else
55 | echo "Found dnsprovider.env, enabling ACME DNS Challenge and wildcard support"
56 | fi
57 |
58 | if [ ! -f "$HOME"/keys/dhparam.pem ]; then
59 | echo "Generating dhparam.pem for nginx https (this may take a while)"
60 | openssl dhparam -out "$HOME"/keys/dhparam.pem "$DH_PARAM_BITS"
61 | fi
62 |
63 | podman unshare chown 1000:0 -R "$HOME"/.ngman "$HOME"/nginx-conf "$HOME"/ssl "$HOME"/www
64 | podman unshare chmod g+rwxs -R "$HOME"/.ngman "$HOME"/nginx-conf "$HOME"/ssl "$HOME"/www
65 |
66 | if ! podman network exists podnet; then
67 | echo "Creating podman network podnet"
68 | podman network create podnet > /dev/null
69 | fi
70 |
71 | if podman image exists ghcr.io/memmaker/nginx:latest; then
72 | echo "Pulling latest image"
73 | podman pull ghcr.io/memmaker/nginx:latest
74 | fi
75 |
76 | if podman container exists ngx; then
77 | echo "Removing existing container ngx"
78 | podman rm -f ngx > /dev/null
79 | fi
80 |
81 | PROXY_RESOLVER=$(podman network inspect podnet | jq -r '.[0].plugins[0].ipam.ranges[0][0].gateway')
82 |
83 | echo "Starting container ngx"
84 | podman run \
85 | -d \
86 | -e ACMEMAIL="$EMAIL" \
87 | -e NGMAN_PROXY_RESOLVER="$PROXY_RESOLVER" \
88 | --env-file="$HOME"/.ngman/dnsprovider.env \
89 | --name ngx \
90 | -p 80:1080 \
91 | -p 443:10443 \
92 | -v "$HOME"/.ngman:/home/nginx/.ngman \
93 | -v "$HOME"/keys/dhparam.pem:/etc/nginx/dhparam.pem \
94 | -v "$HOME"/nginx-conf:/etc/nginx/conf.d/ \
95 | -v "$CERTROOT":/ssl \
96 | -v "$WEBROOT":/var/www \
97 | --network podnet \
98 | ghcr.io/memmaker/nginx:latest
99 |
100 | RENEWCMD="podman exec ngx ssl-renew.sh"
101 | newcron "0 4 1 */2 * ${RENEWCMD} >/dev/null 2>&1"
102 |
103 | # shellcheck disable=SC2028
104 | printf '#!/bin/zsh\npodman exec -it ngx sh --login -c "ngman $*"' > "$HOME"/bin/ngman
105 | chmod +x "$HOME"/bin/ngman
106 |
--------------------------------------------------------------------------------
/types.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type GlobalConfig struct {
4 | CertificateRootPath string
5 | SiteStorageDirectory string
6 | WebRootPath string
7 | NginxSiteConfigDirectory string
8 | TemplateFile string
9 | PostRunCommand string
10 | GenerateCertCommand string
11 | }
12 |
13 | type SiteInfo struct {
14 | Domain string
15 | UsePHP bool
16 | StaticLocations []StaticLocation
17 | ProxyLocations []ReverseProxyLocation
18 | MiscOptions []string
19 | RootPath string
20 | }
21 |
22 | func (s SiteInfo) UseWildcardCert() bool {
23 | return isSubDomain(s.Domain)
24 | }
25 |
26 | type ReverseProxyLocation struct {
27 | URLLocation string
28 | Endpoint string
29 | Headers map[string]string
30 | }
31 |
32 | type StaticLocation struct {
33 | URLLocation string
34 | RootPath string
35 | }
36 |
37 | type RenderContext struct {
38 | Site SiteInfo
39 | Config GlobalConfig
40 | SSLEnabled bool
41 | }
42 |
--------------------------------------------------------------------------------