Repository navigation
Expand file tree
/
Copy pathhttps-gateway
More file actions
executable file
Β·531 lines (430 loc) Β· 12.2 KB
/
Copy pathhttps-gateway
File metadata and controls
executable file
Β·531 lines (430 loc) Β· 12.2 KB
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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
#!/bin/sh
# https-gateway β HTTPS reverse proxy manager for OpenWrt 25.x
# Generates nginx virtual hosts, syncs ACME certificates, manages local DNS.
# Compatible with busybox ash (no bash required).
set -e
. /lib/functions.sh
CONF="https_gateway"
NGINX_CONF="/etc/nginx/conf.d/https-gateway.conf"
ACME_CONF="/etc/config/acme"
CERT_STATUS_DIR="/var/run/https-gateway"
LOCK_FILE="/var/lock/https-gateway.lock"
# --- Helpers ---
log() {
logger -t "https-gateway" "$1"
echo "https-gateway: $1"
}
die() {
log "ERROR: $1"
exit 1
}
validate_domain() {
echo "$1" | grep -qE '^\*?\.?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$' || return 1
}
validate_location() {
echo "$1" | grep -qE '^/[a-zA-Z0-9_./-]*$' || return 1
}
validate_upstream() {
echo "$1" | grep -qE '^https?://[][a-zA-Z0-9.:-]+(/[^ ]*)?$' || return 1
}
validate_size() {
echo "$1" | grep -qE '^[0-9]+[kKmMgG]?$' || return 1
}
get_cert_path() {
local domain="$1"
local base="/etc/ssl/acme"
if [ -f "${base}/${domain}.fullchain.crt" ]; then
echo "${base}/${domain}"
return 0
fi
# Wildcard: try without asterisk prefix
local stripped="${domain#\*.}"
if [ -f "${base}/*.${stripped}.fullchain.crt" ]; then
echo "${base}/*.${stripped}"
return 0
fi
if [ -f "${base}/_wildcard.${stripped}.fullchain.crt" ]; then
echo "${base}/_wildcard.${stripped}"
return 0
fi
# Fallback: use _wildcard. prefix for wildcard domains (avoids * in filenames)
case "$domain" in
\*.*)
echo "${base}/_wildcard.${stripped}"
;;
*)
echo "${base}/${domain}"
;;
esac
return 1
}
# --- ACME Sync ---
sync_acme() {
log "Syncing ACME configuration..."
local email dns_provider dns_wait use_staging
email=$(uci -q get ${CONF}.global.email)
dns_provider=$(uci -q get ${CONF}.global.dns_provider)
dns_wait=$(uci -q get ${CONF}.global.dns_wait)
use_staging=$(uci -q get ${CONF}.global.use_staging)
[ -z "$email" ] && die "Email not configured in ${CONF}.global.email"
[ -z "$dns_provider" ] && die "DNS provider not configured"
# Clear existing ACME cert sections
while uci -q delete acme.@cert[-1] 2>/dev/null; do :; done
# Set global ACME settings
uci -q batch <<-EOF
set acme.@acme[0].account_email='${email}'
set acme.@acme[0].debug='0'
EOF
# Create cert sections from our certificate configs
config_load "$CONF"
config_foreach _sync_one_cert certificate "$dns_provider" "$dns_wait" "$use_staging"
uci commit acme
log "ACME configuration synced"
}
_sync_one_cert() {
local section="$1"
local dns_provider="$2"
local dns_wait="$3"
local use_staging="$4"
local enabled domain key_type
config_get enabled "$section" enabled '1'
config_get domain "$section" domain ''
config_get key_type "$section" key_type 'ec256'
[ "$enabled" = "1" ] || return 0
[ -n "$domain" ] || return 0
validate_domain "$domain" || {
log "WARNING: Invalid domain '$domain' in certificate '$section', skipping"
return 0
}
uci add acme cert >/dev/null
uci set acme.@cert[-1].enabled='1'
uci set acme.@cert[-1].use_staging="${use_staging:-0}"
uci set acme.@cert[-1].validation_method='dns'
uci set acme.@cert[-1].dns="$dns_provider"
uci set acme.@cert[-1].key_type="$key_type"
uci set acme.@cert[-1].dns_wait="${dns_wait:-180}"
uci set acme.@cert[-1].update_nginx='1'
# Add domain(s)
uci add_list acme.@cert[-1].domains="$domain"
# Wildcard certs require the base domain too
case "$domain" in
\*.*)
local base="${domain#\*.}"
uci add_list acme.@cert[-1].domains="$base"
;;
esac
# Add credentials from global config
for cred in $(uci -q get ${CONF}.global.credentials 2>/dev/null); do
uci add_list acme.@cert[-1].credentials="$cred"
done
}
# --- nginx Config Generation ---
generate_nginx_conf() {
log "Generating nginx configuration..."
local listen_ip
listen_ip=$(uci -q get ${CONF}.global.listen_ip || echo '0.0.0.0')
local tmp_conf="${NGINX_CONF}.tmp"
mkdir -p "$(dirname "$NGINX_CONF")"
cat > "$tmp_conf" <<'HEADER'
# === Auto-generated by https-gateway β DO NOT EDIT ===
# Managed by /usr/sbin/https-gateway
# Changes will be overwritten on next reload
HEADER
# HTTP -> HTTPS redirect
cat >> "$tmp_conf" <<EOF
server {
listen ${listen_ip}:80 default_server;
listen [::]:80 default_server;
server_name _;
return 301 https://\$host\$request_uri;
}
EOF
# Collect unique domains from proxy rules
local domains_seen=""
config_load "$CONF"
config_foreach _collect_domain proxy
# Generate server blocks per unique domain
for domain in $domains_seen; do
_generate_server_block "$domain" "$listen_ip" >> "$tmp_conf"
done
# Validate with nginx -t
mv "$tmp_conf" "$NGINX_CONF"
if nginx -t 2>&1; then
log "nginx configuration valid"
else
log "WARNING: nginx config test failed β certificates may not be issued yet"
fi
}
_collect_domain() {
local section="$1"
local enabled domain
config_get enabled "$section" enabled '1'
config_get domain "$section" domain ''
[ "$enabled" = "1" ] || return 0
[ -n "$domain" ] || return 0
case " $domains_seen " in
*" $domain "*) ;;
*) domains_seen="$domains_seen $domain" ;;
esac
}
_generate_server_block() {
local domain="$1"
local listen_ip="$2"
# Find which certificate covers this domain
local cert_path=""
local cert_section=""
config_foreach _find_cert_for_domain certificate "$domain"
if [ -z "$cert_path" ]; then
cert_path="/etc/ssl/acme/${domain}"
fi
local cert_file="${cert_path}.fullchain.crt"
local key_file="${cert_path}.key"
cat <<EOF
server {
listen ${listen_ip}:443 ssl;
listen [::]:443 ssl;
server_name ${domain};
ssl_certificate ${cert_file};
ssl_certificate_key ${key_file};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL_${domain%%.*}:10m;
ssl_session_timeout 10m;
EOF
# Add all location blocks for this domain
config_foreach _generate_location proxy "$domain"
echo "}"
echo ""
}
_find_cert_for_domain() {
local section="$1"
local target_domain="$2"
local cert_domain
config_get cert_domain "$section" domain ''
# Direct match
if [ "$cert_domain" = "$target_domain" ]; then
cert_path=$(get_cert_path "$cert_domain") || true
cert_section="$section"
return 0
fi
# Wildcard match: *.example.com covers sub.example.com
case "$cert_domain" in
\*.*)
local wildcard_base="${cert_domain#\*.}"
local target_base="${target_domain#*.}"
if [ "$wildcard_base" = "$target_base" ] || [ "$wildcard_base" = "$target_domain" ]; then
cert_path=$(get_cert_path "$cert_domain") || true
cert_section="$section"
return 0
fi
;;
esac
}
_generate_location() {
local section="$1"
local target_domain="$2"
local enabled domain location upstream websocket max_body_size
config_get enabled "$section" enabled '1'
config_get domain "$section" domain ''
config_get location "$section" location ''
config_get upstream "$section" upstream ''
config_get websocket "$section" websocket '0'
config_get max_body_size "$section" max_body_size ''
[ "$enabled" = "1" ] || return 0
[ "$domain" = "$target_domain" ] || return 0
[ -n "$location" ] || return 0
[ -n "$upstream" ] || return 0
validate_location "$location" || {
log "WARNING: Invalid location '$location' in proxy '$section', skipping"
return 0
}
validate_upstream "$upstream" || {
log "WARNING: Invalid upstream '$upstream' in proxy '$section', skipping"
return 0
}
if [ -n "$max_body_size" ] && ! validate_size "$max_body_size"; then
log "WARNING: Invalid max_body_size '$max_body_size' in proxy '$section', ignoring"
max_body_size=""
fi
cat <<EOF
location ${location} {
proxy_pass ${upstream};
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
EOF
[ -n "$max_body_size" ] && echo " client_max_body_size ${max_body_size};"
if [ "$websocket" = "1" ]; then
cat <<EOF
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
proxy_buffering off;
EOF
else
cat <<EOF
proxy_read_timeout 300;
EOF
fi
echo " }"
echo ""
}
# --- DNS Management ---
sync_dns() {
local auto_dns
auto_dns=$(uci -q get ${CONF}.global.auto_dns)
[ "$auto_dns" = "1" ] || return 0
log "Syncing local DNS entries..."
local router_ip
router_ip=$(uci -q get network.lan.ipaddr || echo '192.168.0.1')
# Collect domains from proxy rules (skip wildcards)
local managed_domains=""
config_load "$CONF"
config_foreach _collect_dns_domain proxy
# Remove old dhcp domain entries that match our domains
local i=0
while true; do
local name
name=$(uci -q get dhcp.@domain[$i].name) || break
case " $managed_domains " in
*" $name "*)
uci delete "dhcp.@domain[$i]"
;;
*)
i=$((i + 1))
;;
esac
done
# Add fresh entries
for domain in $managed_domains; do
uci add dhcp domain >/dev/null
uci set dhcp.@domain[-1].name="$domain"
uci set dhcp.@domain[-1].ip="$router_ip"
done
uci commit dhcp
/etc/init.d/dnsmasq reload 2>/dev/null || true
log "DNS entries synced for: $managed_domains"
}
_collect_dns_domain() {
local section="$1"
local enabled domain
config_get enabled "$section" enabled '1'
config_get domain "$section" domain ''
[ "$enabled" = "1" ] || return 0
[ -n "$domain" ] || return 0
# Skip wildcard domains for DNS
case "$domain" in
\*.*) return 0 ;;
esac
case " $managed_domains " in
*" $domain "*) ;;
*) managed_domains="$managed_domains $domain" ;;
esac
}
# --- uhttpd Management ---
setup_uhttpd() {
log "Configuring uhttpd as backend..."
local cur_http
cur_http=$(uci -q get uhttpd.main.listen_http || echo '')
if ! echo "$cur_http" | grep -q '127.0.0.1:8080'; then
uci set uhttpd.main.listen_http='127.0.0.1:8080'
uci delete uhttpd.main.listen_https 2>/dev/null || true
uci set uhttpd.main.redirect_https='0'
uci commit uhttpd
/etc/init.d/uhttpd restart
log "uhttpd moved to 127.0.0.1:8080"
fi
}
# --- Status ---
cmd_status() {
. /usr/share/libubox/jshn.sh
json_init
# nginx status
local nginx_running=0
if pidof nginx >/dev/null 2>&1; then
nginx_running=1
fi
json_add_boolean "nginx_running" "$nginx_running"
# Cert statuses
json_add_array "certificates"
config_load "$CONF"
config_foreach _status_cert certificate
json_close_array
# Proxy count
local proxy_count=0
config_foreach _count_proxy proxy
json_add_int "proxy_count" "$proxy_count"
json_dump
}
_status_cert() {
local section="$1"
local domain key_type
config_get domain "$section" domain ''
config_get key_type "$section" key_type 'ec256'
[ -n "$domain" ] || return 0
json_add_object
json_add_string "name" "$section"
json_add_string "domain" "$domain"
json_add_string "key_type" "$key_type"
local cert_base
cert_base=$(get_cert_path "$domain" 2>/dev/null || echo "/etc/ssl/acme/${domain}")
local cert_file="${cert_base}.fullchain.crt"
if [ -f "$cert_file" ]; then
json_add_string "status" "valid"
local expiry
expiry=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | sed 's/notAfter=//')
json_add_string "expires" "$expiry"
else
json_add_string "status" "pending"
json_add_string "expires" ""
fi
json_close_object
}
_count_proxy() {
proxy_count=$((proxy_count + 1))
}
# --- Commands ---
cmd_apply() {
mkdir -p "$CERT_STATUS_DIR"
config_load "$CONF"
local enabled
config_get enabled global enabled '0'
[ "$enabled" = "1" ] || {
log "Service disabled, skipping"
return 0
}
setup_uhttpd
sync_acme
generate_nginx_conf
sync_dns
/etc/init.d/nginx reload 2>/dev/null || /etc/init.d/nginx start 2>/dev/null || true
log "Configuration applied successfully"
}
cmd_issue() {
local cert_name="${1:-}"
[ -n "$cert_name" ] || die "Usage: https-gateway issue <cert_section_name>"
log "Issuing certificate for section '$cert_name'..."
/etc/init.d/acme renew
}
cmd_test() {
if nginx -t 2>/dev/null; then
echo '{"valid":true,"message":"nginx configuration test passed"}'
else
local err
err=$(nginx -t 2>&1)
echo "{\"valid\":false,\"message\":\"$(echo "$err" | tr '"' "'" | tr '\n' ' ')\"}"
fi
}
# --- Main ---
case "${1:-}" in
apply) cmd_apply ;;
issue) cmd_issue "$2" ;;
test) cmd_test ;;
status) cmd_status ;;
*)
echo "Usage: https-gateway {apply|issue <cert>|test|status}"
exit 1
;;
esac