├── .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 | --------------------------------------------------------------------------------