1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
|
# gnix
a simple stupid reverse proxy
## Features
- HTTP/1.1 and HTTP/2
- Simple to configure (see below)
- Somewhat fast (about 150k req/s on a rpi5 for simple routes)
- Composable modules
- Handles connection upgrades correctly by default (websocket, etc.)
- TLS support with automatic certificate selection
- Configuration hot-reloading
- Client authentification (http basic auth, form + cookie)
## Links
- [AUR Package](https://aur.archlinux.org/packages/gnix)
- [Matrix Room](https://matrix.to/#/#gnix:metamuffin.org) for questions,
announcements and general discussion around the software.
## Quick Start
Run the binary with the a path to the configuration as the first argument. The
configuration file is written in YAML and could look like this:
```yaml
http:
bind: "[::]:80"
https:
bind: "[::]:443"
cert_path: "/etc/letsencrypt/live" # Scans directory and selects a valid certificate based on hostname
handler: !hosts # !hosts routes requests for different hostnames.
# requests for `example.org` are forwarded to 127.0.0.1:8000.
"example.org": !proxy { backend: "127.0.0.1:8000" }
# requests for `mydomain.com` will access files from /srv/http.
"mydomain.com": !files
root: "/srv/http"
index: true
# requests for `panel.mydomain.com` will be logged, authentificated and passed on to `otherserver`.
"panel.mydomain.com": !access_log
file: "/var/log/gnix/panel"
next: !cookie_auth
users: "/etc/panel_creds"
expire: 86400
fail: !file { path: "/usr/share/gnix/login.html" }
next: !proxy { backend: "otherserver:80" }
```
## Reference
The configuration uses YAML formatting. When the configuration file is changed,
it will automatically be loaded and applied if valid. Configuration errors are
reported in stderr and are only fatal at startup.
- **section `http`**
- Optional section. Omit to disable unencrypted http.
- `bind`: Addresses to accept http requests on (string or list of strings).
- **section `https`**
- Optional section. Omit to disable https.
- `bind`: Addresses to accept https requests on (string or list of strings).
- `cert_path`: Path to a directory structure that certificates are loaded
from. The hierachy should contain directories containing corresponding
`fullchain.pem` and `privkey.pem` files. The correct certificate is selected
automatically by subject (`CN`). Pointing this directly at
`/etc/letsencrypt/live` is possible. (string or list of strings)
- `cert_fallback`: Path to a single directory containing a key-cert pair that
is used when no other certificate seems appropriate. This is useful for
testing locally with a self-signed subjectless certificate. (optional
string)
- **section `limits`**
- Note: Make sure you do not exceed the maximum file descriptor limit on your
platform.
- `max_incoming_connections` number of maximum incoming (downstream)
connections. excess connections are rejected. Default: 512
- `max_outgoing_connections` number of maximum outgoing (upstream)
connections. excess connections are rejected. Default: 256
- **section `handler`**
- A module to handle all requests. Usually an instance of `hosts` or `paths`.
- `watch_config`: boolean if to watch the configuration file for changes and
apply them accordingly. Default: true (Note: This will watch the entire parent
directory of the config since most editors first move the file. Currently any
change will trigger a reload. TODO)
### Modules
Modules handle requests. Some of them have arguments which are modules
themselves; in that case the request is passed on.
- **module `hosts`**
- Hands over the requests to different modules depending on the `host` header.
Special keys are `:fallback` which handles hosts that are not found in the
map and `:none` to handle requests without a host header.
- Takes a map from hostname (string) to handler (module)
- **module `paths`**
- Routes requests by matching the path against regexes. The first matching
regex determines the module to handle this request. If the regex has an
unnamed capture group, its value will become the downstream request URI.
- Takes a map from regex (string) to handler (module) OR a sequence of such
maps. The priority is assigned first by position in the sequence then
alphabetically in the map.
- **module `proxy`**
- Forwards the request as-is to some other server. the `x-real-ip` header is
added to the request. Connection upgrades are handled by direct forwarding
of network traffic.
- `backend`: socket address (string) to the backend server
- `set_real_ip`: Sets the `X-Real-IP` header. (boolean)
- `set_forwarded_for`: Sets the `X-Forwarded-For`, `X-Forwarded-Host`,
`X-Forwaded-Proto`, `X-Forwarded-Scheme` and `X-Forwarded-Port` headers.
(boolean)
- **module `files`**
- Provides a simple built-in fileserver. The server handles `accept-ranges`.
The `content-type` header is inferred from the file extension and falls back
to `application/octet-stream`. If a directory is requested `index.html` will
be served or else when indexing is enabled, `index.banner.html` will be
prepended to the response.
- `root`: root directory to be served (string)
- `index`: enables directory indexing (boolean)
- `localize`: sends a localized version if possible. for localized content the
ISO 639-1 language code is injected in front of the first file name
extension (boolean) **semi-experimental**
- **module `file`**
- Replies with static content.
- `path`: file path to the content. _This will be loaded into memory at
startup!_ (string)
- `content`: inline declaration of the content. (string)
- `type`: type of content reflected in `content-type` header. (string)
- **module `access_log`**
- Logs requests to a file.
- `file`: file path to log (string)
- `reject_on_fail`: rejects requests if log could not be written (boolean)
- `flush`: flushes log on every request (boolean)
- `next`: module for further handling of the request (module)
- **module `error`**
- Rejects every request with a custom error message.
- Takes an error message (string)
- **module `http_basic_auth`**
- Filters requests via HTTP Basic Authentification. Unauthorized clients will
be challenged.
- `realm`: describes what the user is logging into (most modern browsers dont
show this anymore -_-)
- `users`: list of valid logins (credentials)
- `next`: a module to handle this request on successfully authentificated.
(module)
- **module `cookie_auth`**
- Authentificates a client based on cookies. The cookies are set on login by a
POST request to `/_gnix_login` with form fields `password` and `username`
(optional, default: "user") in `x-www-form-urlencoded` format. In any case
the users submitting this request will be directed back to the page they
come from. For successful logins two cookies are set: `gnix_username`
containing the username and `gnix_auth` containing an opaque
authentification token. The `gnix_username` cookie is authentificated by
gnix and can therefore be used by applications. Alternatively a login may be
implemented by returning the `gnix-auth-success` header with a username as
the value from the `fail` handler, which is handled like a sucessful login
for that user. If `gnix-auth-no-redirect` is set the response of the `fail`
handler is passed instead of a redirect being sent. This method can be
useful for implementing custom login logic like OTP login or a CAPTCHA.
- `users`: list of valid logins (credentials)
- `expire`: seconds before logins expire; not setting this option keeps the
login valid forever on the server but cleared after the session on the
client (optional number)
- `secure`: makes the cookies accessable from secure contexts exclusively i.e.
HTTPS (boolean)
- `next`: a module to handle this request on successfully authentification.
(module)
- `fail`: a module to handle the request when a user is not authorized. This
could show an HTML form prompting the user to log in. An implementation of
such a form is provided with the distribution of this software, usually in
`/usr/share/gnix/login.html`. It can return the `gnix-auth-success` header,
see above. (module)
- **module `switch`**
- Decides between two possible routes based on a condition.
- `condition`:
- `!is_websocket_upgrade`: Checks if a websocket was requested.
- `!is_get`: Checks if this is a GET request
- `!is_post`: Checks if this is a POST request
- `!path_starts_with <prefix>`: Checks if the URI path starts with some
prefix
- `!path_is <path>`: Checks if the URI path is exactly what you specified
- `!has_header <name>`: Checks if the request includes a certain header.
- `!any [conditions]`: Checks if any of a set of conditions are satisfied.
- `!all [conditions]`: Checks if all conditions are satisfied.
- `case_true` Handler with matched requests (module)
- `case_false` Handler for all other requests (module)
- **module `headers`**
- Appends multiple headers to the response.
- `request`: A map of all header name-value pairs added before the request is
passed to `next`. (object string:string)
- `response`: A map headers added to responsed from `next`. (object
string:string)
- `next`: The handler to add the headers to. (module)
- **module `redirect`**
- Responds with a permanent redirect.
- Takes the location to redirect to. (string)
- **module `cgi`**
- Runs a CGI script on the request.
- `bin`: Path to the CGI binary (string)
- `user`: User that the script is executed as. Requires to run gnix as root.
(optional string)
- **module `cache`**
- Caches requests. **This is experimental! Don't use this.**
- Not documented yet
- **module `loadbalance`**
- Balances load across multiple modules (presumably proxied backends). The
current implementation selects the least busy handler for each request and
does not scale well to many modules. **This is experimental! Don't use
this.**
- Takes a set of handlers. (sequence of module)
- **module `fallback`**
- Runs a series of handlers until one returns a success, informational or
redirect response and the returns that. The last handler's response will
unconditionally be returned. Note that to run multiple handlers the request
body will first be completely received to memory before any handler is run.
There is currently no size limit for request bodies, so be careful using
this. **TODO request size limit**
- Takes a sequence of handlers.
- **module `debug`**
- Replies with information about the request to debug. Includes source
address, HTTP version, URI and headers.
#### Credentials config format
Login credentials for `cookie_auth` and `http_basic_auth` are supplied as either
an object mapping usernames to PHC strings or a file path pointing to a file
that contains that map in YALM format. Currently only `argon2id` is supported.
### Additional Notes
Internally gnix processes requests as they would be sent in HTTP/1.1. HTTP/2 is
translated on arrival.
Paths matching `/_gnix*` might be used internally in gnix for purposes like
OpenID callback or login action endpoints. I hope your application doesn't rely
on using them for itself.
## License
AGPL-3.0-only; see [COPYING](./COPYING)
|