xref: /unit/tools/setup-unit (revision 2498:dd4a36a92e4c)
1#!/usr/bin/env bash
2
3#####################################################################
4#
5# Copyright (C) NGINX, Inc.
6# Author:  NGINX Unit Team, F5 Inc.
7#
8#####################################################################
9
10
11if test -n ${BASH_VERSION} && test "${BASH_VERSINFO[0]}" -eq 3; then
12    >&2 cat <<__EOF__ ;
13Your version of bash(1) isn't supported by this script.  You're probably
14running on macOS.  We recommend that you either install a newer version
15of bash(1) or run this script with another shell, such as zsh(1):
16
17    $ ${SUDO_USER:+sudo }zsh $0 ...
18__EOF__
19    exit 1;
20fi;
21
22
23set -Eefuo pipefail;
24
25test -v BASH_VERSION \
26&& shopt -s lastpipe;
27
28test -v ZSH_VERSION \
29&& setopt sh_word_split;
30
31export LC_ALL=C
32
33dry_run='no';
34
35help_unit()
36{
37    cat <<__EOF__ ;
38SYNOPSIS
39        $0 [-h] COMMAND [ARGS]
40
41    Subcommands
42        ├── repo-config  [-hn] [PKG-MANAGER OS-NAME OS-VERSION]
43        └── welcome      [-hn]
44
45DESCRIPTION
46        This script simplifies installing and configuring an NGINX Unit server
47        for first-time users.
48
49        Run '$0 COMMAND -h' for more information on a command.
50
51COMMANDS
52        repo-config
53                Configure your package manager with the NGINX Unit repository
54                for later installation.
55
56        welcome
57                Create an initial configuration to serve a welcome web page
58                with NGINX Unit.
59
60OPTIONS
61        -h, --help
62                Print this help.
63
64        --help-more
65                Print help for more commands.  They are experimental.  Using
66                these isn't recommended, unless you know what you're doing.
67
68__EOF__
69}
70
71help_more_unit()
72{
73    cat <<__EOF__ ;
74SYNOPSIS
75        $0 [-h] COMMAND [ARGS]
76
77    Subcommands
78        ├── cmd          [-h]
79        ├── ctl          [-h] [-s SOCK] SUBCOMMAND [ARGS]
80        │   ├── edit     [-h] PATH
81        │   ├── http     [-h] [-c CURLOPT] METHOD PATH
82        │   └── insert   [-h] PATH INDEX
83        ├── freeport     [-h]
84        ├── json-ins     [-hn] JSON INDEX
85        ├── os-probe     [-h]
86        ├── ps           [-h] [-t TYPE]
87        ├── repo-config  [-hn] [PKG-MANAGER OS-NAME OS-VERSION]
88        ├── restart      [-hls]
89        ├── sock         [-h] SUBCOMMAND [ARGS]
90        │   ├── filter   [-chs]
91        │   └── find     [-h]
92        └── welcome      [-hn]
93
94DESCRIPTION
95        This script simplifies installing and configuring
96        an NGINX Unit server for first-time users.
97
98        Run '$0 COMMAND -h' for more information on a command.
99
100COMMANDS
101        cmd     Print the invocation line of unitd(8).
102
103        ctl     Control a running unitd(8) instance via its control API socket.
104
105        freeport
106                Print an available TCP port.
107
108        json-ins
109                Insert a JSON element read from standard input into a JSON
110                array read from a file at a given INDEX.
111
112        os-probe
113                Probe the OS and print details about its version.
114
115        ps      List unitd(8) processes.
116
117        repo-config
118                Configure your package manager with the NGINX Unit
119                repository for later installation.
120
121        sock    Print the control API socket address.
122
123        welcome
124                Create an initial configuration to serve a welcome web page
125                with NGINX Unit.
126
127OPTIONS
128        -h, --help
129                Print basic help (some commands are hidden).
130
131        --help-more
132                Print the hidden help with more commands.
133
134__EOF__
135}
136
137warn()
138{
139    >&2 echo "$(basename "$0"): error: $*";
140}
141
142err()
143{
144    >&2 echo "$(basename "$0"): error: $*";
145    exit 1;
146}
147
148dry_run_echo()
149{
150    if test "$dry_run" = "yes"; then
151        echo "$*";
152    fi;
153}
154
155dry_run_eval()
156{
157    if test "$dry_run" = "yes"; then
158        echo "    $*";
159    else
160        eval "$*";
161    fi;
162}
163
164
165help_unit_cmd()
166{
167    cat <<__EOF__ ;
168SYNOPSIS
169        $0 cmd [-h]
170
171DESCRIPTION
172        Print the invocation line of running unitd(8) instances.
173
174OPTIONS
175        -h, --help
176                Print this help.
177
178__EOF__
179}
180
181
182unit_cmd()
183{
184    while test $# -ge 1; do
185        case "$1" in
186        -h | --help)
187            help_unit_cmd;
188            exit 0;
189            ;;
190        -*)
191            err "cmd: $1: Unknown option.";
192            ;;
193        *)
194            err "cmd: $1: Unknown argument.";
195            ;;
196        esac;
197        shift;
198    done;
199
200    unit_ps -t m \
201    | sed 's/.*\[\(.*\)].*/\1/';
202}
203
204
205help_unit_ctl()
206{
207    cat <<__EOF__ ;
208SYNOPSIS
209        $0 ctl [-h] [-s SOCK] SUBCOMMAND [ARGS]
210
211    Subcommands
212        ├── edit    [-h] PATH
213        ├── http    [-h] [-c CURLOPT] METHOD PATH
214        └── insert  [-h] PATH INDEX
215
216DESCRIPTION
217        Control a running unitd(8) instance through its control API socket.
218
219        Run '$0 ctl SUBCOMMAND -h' for more information on a
220        subcommand.
221
222SUBCOMMANDS
223        edit    Edit the unitd(8) configuration with an editor.
224
225        http    Send an HTTP request to the control API socket.
226
227        insert  Insert an element at the specified index into an array in the
228                JSON configuration.
229
230OPTIONS
231        -h, --help
232                Print this help.
233
234        -s, --sock SOCK
235                Use SOCK as the control API socket address.  If not specified,
236                the script tries to find it.  This value is used by subcommands.
237
238                The socket can be a tcp(7) socket or a unix(7) socket; in
239                the case of a unix(7) socket, it can exist locally or on
240                a remote machine, accessed through ssh(1).  Accepted syntax
241                for SOCK:
242
243                    unix:/path/to/control.sock
244                    ssh://[user@]host[:port]/path/to/control.sock
245                    [http[s]://]host[:port]
246
247                The last form is less secure than the first two; have a look:
248                <https://unit.nginx.org/howto/security/#secure-socket-and-stat>
249
250ENVIRONMENT
251        Options take precedence over their equivalent environment variables;
252        if both are specified, the command-line option is used.
253
254        UNIT_CTL_SOCK
255                Equivalent to the option -s (--sock).
256
257__EOF__
258}
259
260
261unit_ctl()
262{
263
264    if test -v UNIT_CTL_SOCK; then
265        local sock="$UNIT_CTL_SOCK";
266    fi;
267
268    while test $# -ge 1; do
269        case "$1" in
270        -h | --help)
271            help_unit_ctl;
272            exit 0;
273            ;;
274        -s | --sock)
275            if ! test $# -ge 2; then
276                err "ctl: $1: Missing argument.";
277            fi;
278            local sock="$2";
279            shift;
280            ;;
281        -*)
282            err "ctl: $1: Unknown option.";
283            ;;
284        *)
285            break;
286            ;;
287        esac;
288        shift;
289    done;
290
291    if test ! $# -ge 1; then
292        err 'ctl: Missing subcommand.';
293    fi;
294
295    if ! test -v sock; then
296        local sock="$(unit_sock_find)";
297    fi;
298
299    if echo $sock | grep '^ssh://' >/dev/null; then
300        local remote="$(echo $sock | sed 's,\(ssh://[^/]*\).*,\1,')";
301        local sock="$(echo $sock | sed 's,ssh://[^/]*\(.*\),unix:\1,')";
302    fi;
303
304    case $1 in
305    edit)
306        shift;
307        unit_ctl_edit ${remote:+ ---r $remote} ---s "$sock" $@;
308        ;;
309    http)
310        shift;
311        unit_ctl_http ${remote:+ ---r $remote} ---s "$sock" $@;
312        ;;
313    insert)
314        shift;
315        unit_ctl_insert ${remote:+ ---r $remote} ---s "$sock" $@;
316        ;;
317    *)
318        err "ctl: $1: Unknown argument.";
319        ;;
320    esac;
321}
322
323
324help_unit_ctl_edit()
325{
326    cat <<__EOF__ ;
327SYNOPSIS
328        $0 ctl [CTL-OPTS] edit [-h] PATH
329
330DESCRIPTION
331        Edit the JSON configuration with an editor.  The current configuration
332        is downloaded into a temporary file, open with the editor, and then
333        sent back to the control API socket.
334
335        The following editors are tried in this order of preference: \$VISUAL,
336        \$EDITOR, editor(1), vi(1), vim(1), ed(1).
337
338
339OPTIONS
340        -h, --help
341                Print this help.
342
343ENVIRONMENT
344        VISUAL
345        EDITOR
346                See environ(7).
347
348SEE ALSO
349        $0 ctl http -h;
350
351        update-alternatives(1)
352
353__EOF__
354}
355
356
357unit_ctl_edit()
358{
359    while test $# -ge 1; do
360        case "$1" in
361        -h | --help)
362            help_unit_ctl_edit;
363            exit 0;
364            ;;
365        ---r | ----remote)
366            local remote="$2";
367            shift;
368            ;;
369        ---s | ----sock)
370            local sock="$2";
371            shift;
372            ;;
373        -*)
374            err "ctl: edit: $1: Unknown option.";
375            ;;
376        *)
377            break;
378            ;;
379        esac;
380        shift;
381    done;
382
383    if ! test $# -ge 1; then
384        err 'ctl: insert: PATH: Missing argument.';
385    fi;
386    local req_path="$1";
387
388    if test -v remote; then
389        local remote_sock="$(echo "$sock" | unit_sock_filter -s)";
390        local local_sock="$(mktemp -u -p /var/run/unit/)";
391        local ssh_ctrl="$(mktemp -u -p /var/run/unit/)";
392
393        mkdir -p /var/run/unit/;
394
395        ssh -fMNnT -S "$ssh_ctrl" \
396                   -o 'ExitOnForwardFailure yes' \
397                   -L "$local_sock:$remote_sock" "$remote";
398
399        sock="unix:$local_sock";
400    fi;
401
402    local tmp="$(mktemp ||:)";
403
404    unit_ctl_http ---s "$sock" -c --no-progress-meter GET "$req_path" \
405            </dev/null >"$tmp" \
406    ||:;
407
408    $(
409        ((test -v VISUAL && test -n "$VISUAL") && printf '%s\n' "$VISUAL") \
410        || ((test -v EDITOR && test -n "$EDITOR") && printf '%s\n' "$EDITOR") \
411        || command -v editor \
412        || command -v vi \
413        || command -v vim \
414        || echo ed;
415    ) "$tmp" \
416    ||:;
417
418    unit_ctl_http ---s "$sock" PUT "$req_path" <"$tmp" \
419    ||:;
420
421    if test -v remote; then
422        ssh -S "$ssh_ctrl" -O exit "$remote" 2>/dev/null;
423        unlink "$local_sock";
424    fi;
425}
426
427
428help_unit_ctl_http()
429{
430    cat <<__EOF__ ;
431SYNOPSIS
432        $0 ctl [CTL-OPTS] http [-h] [-c CURLOPT] METHOD PATH
433
434DESCRIPTION
435        Send an HTTP request to the unitd(8) control API socket.
436
437        The payload is read from standard input.
438
439OPTIONS
440        -c, --curl CURLOPT
441                Pass CURLOPT as an option to curl.  This script is implemented
442                in terms of curl(1), so it's useful to be able to tweak its
443                behavior.  The option can be cumulatively used multiple times
444                (the result is also appended to UNIT_CTL_HTTP_CURLOPTS).
445
446        -h, --help
447                Print this help.
448
449ENVIRONMENT
450        UNIT_CTL_HTTP_CURLOPTS
451                Equivalent to the option -c (--curl).
452
453EXAMPLES
454        $0 ctl http -c --no-progress-meter GET /config >tmp;
455
456SEE ALSO
457        <https://unit.nginx.org/controlapi/#api-manipulation>
458
459__EOF__
460}
461
462
463unit_ctl_http()
464{
465    local curl_options="${UNIT_CTL_HTTP_CURLOPTS:-}";
466
467    while test $# -ge 1; do
468        case "$1" in
469        -c | --curl)
470            if ! test $# -ge 2; then
471                err "ctl: http: $1: Missing argument.";
472            fi;
473            curl_options="$curl_options $2";
474            shift;
475            ;;
476        -h | --help)
477            help_unit_ctl_http;
478            exit 0;
479            ;;
480        ---r | ----remote)
481            local remote="$2";
482            shift;
483            ;;
484        ---s | ----sock)
485            local sock="$2";
486            shift;
487            ;;
488        -*)
489            err "ctl: http: $1: Unknown option.";
490            ;;
491        *)
492            break;
493            ;;
494        esac;
495        shift;
496    done;
497
498    if ! test $# -ge 1; then
499        err 'ctl: http: METHOD: Missing argument.';
500    fi;
501    local method="$1";
502
503    if ! test $# -ge 2; then
504        err 'ctl: http: PATH: Missing argument.';
505    fi;
506    local req_path="$2";
507
508    if test -v remote; then
509        local remote_sock="$(echo "$sock" | unit_sock_filter -s)";
510        local local_sock="$(mktemp -u -p /var/run/unit/)";
511        local ssh_ctrl="$(mktemp -u -p /var/run/unit/)";
512
513        mkdir -p /var/run/unit/;
514
515        ssh -fMNnT -S "$ssh_ctrl" \
516                   -o 'ExitOnForwardFailure yes' \
517                   -L "$local_sock:$remote_sock" "$remote";
518
519        sock="unix:$local_sock";
520    fi;
521
522    curl $curl_options -X $method -d@- \
523            $(echo "$sock" | unit_sock_filter -c)${req_path} \
524    ||:;
525
526    if test -v remote; then
527        ssh -S "$ssh_ctrl" -O exit "$remote" 2>/dev/null;
528        unlink "$local_sock";
529    fi;
530}
531
532
533help_unit_ctl_insert()
534{
535    cat <<__EOF__ ;
536SYNOPSIS
537        $0 ctl [CTL-OPTS] insert [-h] PATH INDEX
538
539DESCRIPTION
540        Insert an element at the specified position (INDEX) into the JSON array
541        located at PATH in unitd(8) control API.
542
543        The new element is read from standard input.
544
545OPTIONS
546        -h, --help
547                Print this help.
548
549SEE ALSO
550        $0 ctl http -h;
551
552__EOF__
553}
554
555
556unit_ctl_insert()
557{
558    while test $# -ge 1; do
559        case "$1" in
560        -h | --help)
561            help_unit_ctl_insert;
562            exit 0;
563            ;;
564        ---r | ----remote)
565            local remote="$2";
566            shift;
567            ;;
568        ---s | ----sock)
569            local sock="$2";
570            shift;
571            ;;
572        -*)
573            err "ctl: insert: $1: Unknown option.";
574            ;;
575        *)
576            break;
577            ;;
578        esac;
579        shift;
580    done;
581
582    if ! test $# -ge 1; then
583        err 'ctl: insert: PATH: Missing argument.';
584    fi;
585    local req_path="$1";
586
587    if ! test $# -ge 2; then
588        err 'ctl: insert: INDEX: Missing argument.';
589    fi;
590    local idx="$2";
591
592    if test -v remote; then
593        local remote_sock="$(echo "$sock" | unit_sock_filter -s)";
594        local local_sock="$(mktemp -u -p /var/run/unit/)";
595        local ssh_ctrl="$(mktemp -u -p /var/run/unit/)";
596
597        mkdir -p /var/run/unit/;
598
599        ssh -fMNnT -S "$ssh_ctrl" \
600                   -o 'ExitOnForwardFailure yes' \
601                   -L "$local_sock:$remote_sock" "$remote";
602
603        sock="unix:$local_sock";
604    fi;
605
606    local old="$(mktemp ||:)";
607
608    unit_ctl_http ---s "$sock" -c --no-progress-meter GET "$req_path" \
609            </dev/null >"$old" \
610    ||:;
611
612    unit_json_ins "$old" "$idx" \
613    | unit_ctl_http ---s "$sock" PUT "$req_path" \
614    ||:;
615
616    if test -v remote; then
617        ssh -S "$ssh_ctrl" -O exit "$remote" 2>/dev/null;
618        unlink "$local_sock";
619    fi;
620}
621
622
623help_unit_ctl_welcome()
624{
625    cat <<__EOF__ ;
626SYNOPSIS
627        $0 welcome [-hn]
628
629DESCRIPTION
630        This script tests an NGINX Unit installation by creating an initial
631        configuration and serving a welcome web page.  Recommended for
632        first-time users.
633
634OPTIONS
635        -h, --help
636                Print this help.
637
638        -n, --dry-run
639                Dry run.  Print the commands to be run instead of actually
640                running them.  Each command is preceded by a line explaining
641                what it does.
642
643__EOF__
644}
645
646
647unit_ctl_welcome()
648{
649    while test $# -ge 1; do
650        case "$1" in
651        -f | --force)
652            local force='yes';
653            ;;
654        -h | --help)
655            help_unit_ctl_welcome;
656            exit 0;
657            ;;
658        -n | --dry-run)
659            dry_run='yes';
660            ;;
661        -*)
662            err "welcome: $1: Unknown option.";
663            ;;
664        *)
665            err "welcome: $1: Unknown argument.";
666            ;;
667        esac;
668        shift;
669    done;
670
671    command -v curl >/dev/null \
672    || err 'welcome: curl(1) not found in PATH.  It must be installed to run this script.';
673
674    www='/srv/www/unit/index.html';
675    if test -e "$www" && ! test -v force || ! test -w /srv; then
676        www="$HOME/srv/www/unit/index.html";
677    fi;
678    if test -e "$www" && ! test -v force; then
679        www="$(mktemp)";
680        mv "$www" "$www.html";
681        www="$www.html"
682    fi;
683
684    unit_ps -t m \
685    | wc -l \
686    | read -r nprocs \
687    ||:
688
689    if test 0 -eq "$nprocs"; then
690        warn "welcome: NGINX Unit isn't running.";
691        warn 'For help with starting NGINX Unit, see:';
692        err  "  <https://unit.nginx.org/installation/#startup-and-shutdown>";
693    elif test 1 -ne "$nprocs"; then
694        err 'welcome: Only one NGINX Unit instance should be running.';
695    fi;
696
697    local sock="$(unit_sock_find)";
698    local curl_opt="$(unit_sock_find | unit_sock_filter -c)";
699
700    curl $curl_opt/ >/dev/null 2>&1 \
701    || err "welcome: Can't reach the control API socket.";
702
703    if ! test -v force; then
704        unit_cmd \
705        | read -r cmd;
706
707        # Check unitd is not configured already.
708        echo "$cmd" \
709        | if grep '\--statedir' >/dev/null; then
710            echo "$cmd" \
711            | sed 's/ --/\n--/g' \
712            | grep '\--statedir' \
713            | cut -d' ' -f2;
714        else
715            $cmd --help \
716            | sed -n '/\--statedir/,+1p' \
717            | grep 'default:' \
718            | sed 's/ *default: "\(.*\)"/\1/';
719        fi \
720        | sed 's,$,/conf.json,' \
721        | read -r conffile \
722        ||:;
723
724        if test -e $conffile; then
725            if ! unit_ctl_http ---s "$sock" 'GET' '/config' </dev/null 2>/dev/null | grep -q '^{}.\?$';  # The '.\?' is for the possible carriage return.
726            then
727                warn 'welcome: NGINX Unit is already configured.  To overwrite';
728                err  'its current configuration, run the script again with --force.';
729            fi;
730        fi;
731    fi;
732
733    (
734        unit_freeport \
735        || err "welcome: Can't find an available port.";
736    ) \
737    | read -r port;
738
739    dry_run_echo 'Create a file to serve:';
740    dry_run_eval "mkdir -p $(dirname $www);";
741    dry_run_eval "tee '$www' >/dev/null"' <<__EOF__;
742        <!DOCTYPE html>
743        <html>
744            <head>
745                <title>Welcome to NGINX Unit</title>
746                <style type="text/css">
747                    body { background: white; color: black; font-family: sans-serif; margin: 2em; line-height: 1.5; }
748                    h1,h2 { color: #00974d; }
749                    li { margin-bottom: 0.5em; }
750                    pre { background-color: beige; padding: 0.4em; }
751                    hr { margin-top: 2em; border: 1px solid #00974d; }
752                    .indent { margin-left: 1.5em; }
753                </style>
754            </head>
755            <body>
756                <h1>Welcome to NGINX Unit</h1>
757                <p>Congratulations! NGINX Unit is installed and running.</p>
758                <h3>Useful Links</h3>
759                <ul>
760                    <li><b><a href="https://unit.nginx.org/configuration/?referer=welcome">https://unit.nginx.org/configuration/</a></b><br>
761                        To get started with Unit, see the <em>Configuration</em> docs, starting with
762                        the <em>Quick Start</em> guide.</li>
763                    <li><b><a href="https://github.com/nginx/unit">https://github.com/nginx/unit</a></b><br>
764                        See our GitHub repo to browse the code, contribute, or seek help from the
765                        <a href="https://github.com/nginx/unit#community">community</a>.</li>
766                </ul>
767
768                <h2>Next steps</h2>
769
770                <h3>Check Current Configuration</h3>
771                <div class="indent">
772                <p>Unit'"'"'s control API is currently listening for configuration changes
773                   on the '"$(unit_sock_find | grep -q '^unix:' && echo '<a href="https://en.wikipedia.org/wiki/Unix_domain_socket">Unix socket</a>' || echo 'socket')"' at
774                   <b>'"$(unit_sock_find)"'</b><br>
775                   To see the current configuration:</p>
776                <pre>'"${SUDO_USER:+sudo }"'curl '"$curl_opt"'/config</pre>
777                </div>
778
779                <h3>Change Listener Port</h3>
780                <div class="indent">
781                <p>This page is served over a random TCP high port.  To choose the default HTTP port (80),
782                   replace the <b>"listeners"</b> object:</p>
783                <pre>echo '"'"'{"*:80": {"pass": "routes"}}'"'"' | '"${SUDO_USER:+sudo }"'curl -X PUT -d@- '"$curl_opt"'/config/listeners</pre>
784                Then remove the port number from the address bar and reload the page.
785                </div>
786
787                <hr>
788                <p><a href="https://unit.nginx.org/?referer=welcome">NGINX Unit &mdash; the universal web app server</a><br>
789                NGINX, Inc. &copy; 2023</p>
790            </body>
791        </html>
792__EOF__';
793    dry_run_echo;
794    dry_run_echo 'Give it appropriate permissions:';
795    dry_run_eval "chmod 644 '$www';";
796    dry_run_echo;
797
798    dry_run_echo 'Configure unitd:'
799    dry_run_eval "cat <<__EOF__ \\
800        | sed 's/8080/$port/' \\
801        | curl -X PUT -d@- $curl_opt/config;
802        {
803            \"listeners\": {
804                \"*:8080\": {
805                    \"pass\": \"routes\"
806                }
807            },
808            \"routes\": [{
809                \"action\": {
810                    \"share\": \"$www\"
811                }
812            }]
813        }
814__EOF__";
815
816    dry_run_echo;
817
818    echo;
819    echo 'You may want to try the following commands now:';
820    echo;
821    echo 'Check out current unitd configuration:';
822    echo "    ${SUDO_USER:+sudo} curl $curl_opt/config";
823    echo;
824    echo 'Browse the welcome page:';
825    echo "    curl http://localhost:$port/";
826}
827
828
829help_unit_freeport()
830{
831    cat <<__EOF__ ;
832SYNOPSIS
833        $0 freeport [-h]
834
835DESCRIPTION
836        Print an available TCP port.
837
838OPTIONS
839        -h, --help
840                Print this help.
841
842__EOF__
843}
844
845
846unit_freeport()
847{
848    while test $# -ge 1; do
849        case "$1" in
850        -h | --help)
851            help_unit_freeport;
852            exit 0;
853            ;;
854        -*)
855            err "freeport: $1: Unknown option.";
856            ;;
857        *)
858            err "freeport: $1: Unknown argument.";
859            ;;
860        esac;
861        shift;
862    done;
863
864    freeport="$(mktemp -t freeport-XXXXXX)";
865
866    cat <<__EOF__ \
867    | cc -x c -o $freeport -;
868        #include <netinet/in.h>
869        #include <stdio.h>
870        #include <stdlib.h>
871        #include <strings.h>
872        #include <sys/socket.h>
873        #include <unistd.h>
874
875
876        int32_t get_free_port(void);
877
878
879        int
880        main(void)
881        {
882            int32_t  port;
883
884            port = get_free_port();
885            if (port == -1)
886                exit(EXIT_FAILURE);
887
888            printf("%d\n", port);
889            exit(EXIT_SUCCESS);
890        }
891
892
893        int32_t
894        get_free_port(void)
895        {
896            int                 sfd;
897            int32_t             port;
898            socklen_t           len;
899            struct sockaddr_in  addr;
900
901            port = -1;
902
903            sfd = socket(PF_INET, SOCK_STREAM, 0);
904            if (sfd == -1) {
905                perror("socket()");
906                return -1;
907            }
908
909            bzero(&addr, sizeof(addr));
910            addr.sin_family = AF_INET;
911            addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
912            addr.sin_port = htons(0);  // random port
913
914            len = sizeof(addr);
915            if (bind(sfd, (struct sockaddr *) &addr, len)) {
916                perror("bind()");
917                goto fail;
918            }
919
920            if (getsockname(sfd, (struct sockaddr *) &addr, &len)) {
921                perror("getsockname()");
922                goto fail;
923            }
924
925            port = ntohs(addr.sin_port);
926
927        fail:
928            close(sfd);
929            return port;
930        }
931__EOF__
932
933    $freeport;
934}
935
936
937help_unit_json_ins()
938{
939cat <<__EOF__ ;
940SYNOPSIS
941        $0 json-ins [-hn] JSON INDEX
942
943ARGUMENTS
944        JSON    Path to a JSON file containing a top-level array.
945
946        INDEX   Position in the array to insert the element at.
947
948DESCRIPTION
949        Insert a JSON element read from standard input into a JSON array read
950        from a file at a given INDEX.
951
952        The resulting array is printed to standard output.
953
954OPTIONS
955        -h, --help
956                Print this help.
957
958        -n, --dry-run
959                Dry run.  Print the command to be run instead of actually
960                running it.
961
962__EOF__
963}
964
965
966unit_json_ins()
967{
968    while test $# -ge 1; do
969        case "$1" in
970        -h | --help)
971            help_unit_json_ins;
972            exit 0;
973            ;;
974        -n | --dry-run)
975            dry_run='yes';
976            ;;
977        -*)
978            err "json-ins: $1: Unknown option.";
979            ;;
980        *)
981            break;
982            ;;
983        esac;
984        shift;
985    done;
986
987    if ! test $# -ge 1; then
988        err 'json-ins: JSON: Missing argument.';
989    fi;
990    local arr=$1;
991
992    if ! test $# -ge 2; then
993        err 'json-ins: INDEX: Missing argument.';
994    fi;
995    local idx=$2;
996
997    dry_run_eval "(
998        jq '.[0:$idx]' <'$arr';
999        echo '[';
1000        jq .;
1001        echo ']';
1002        jq '.[$idx:]' <'$arr';
1003    ) \\
1004    | sed '/^\[]$/d' \\
1005    | sed '/^]$/{N;s/^]\n\[$/,/}' \\
1006    | jq .;"
1007}
1008
1009
1010help_unit_os_probe()
1011{
1012    cat <<__EOF__ ;
1013SYNOPSIS
1014        $0 os-probe [-h]
1015
1016DESCRIPTION
1017        This script probes the OS and prints three fields, delimited by ':';
1018        the first is the package manager, the second is the OS name, the third
1019        is the OS version.
1020
1021OPTIONS
1022        -h, --help
1023                Print this help.
1024
1025__EOF__
1026}
1027
1028
1029unit_os_probe()
1030{
1031    while test $# -ge 1; do
1032        case "$1" in
1033        -h | --help)
1034            help_unit_os_probe;
1035            exit 0;
1036            ;;
1037        -*)
1038            err "os-probe: $1: Unknown option.";
1039            ;;
1040        *)
1041            err "os-probe: $1: Unknown argument.";
1042            ;;
1043        esac;
1044        shift;
1045    done;
1046
1047    local os=$(uname | tr '[:upper:]' '[:lower:]')
1048
1049    if [ "$os" != 'linux' ] && [ "$os" != 'freebsd' ]; then
1050        err "os-probe: The OS isn't Linux or FreeBSD; can't proceed."
1051    fi
1052
1053    if [ "$os" = 'linux' ]; then
1054        if command -v apt-get >/dev/null; then
1055            local pkgMngr='apt';
1056        elif command -v dnf >/dev/null; then
1057            local pkgMngr='dnf';
1058        elif command -v yum >/dev/null; then
1059            local pkgMngr='yum';
1060        else
1061            local pkgMngr='';
1062        fi;
1063
1064        local osRelease='/etc/os-release';
1065
1066        if [ -f "$osRelease" ]; then
1067            # The value for the ID and VERSION_ID may or may not be in quotes
1068            local osName=$(grep "^ID=" "$osRelease" | sed s/\"//g | awk -F= '{ print $2 }' ||:)
1069            local osVersion=$(grep '^VERSION_ID=' "$osRelease" | sed s/\"//g | awk -F= '{ print $2 }' || lsb_release -cs)
1070        else
1071            err "os-probe: Unable to determine OS and version, or the OS isn't supported."
1072        fi
1073    else
1074        local pkgMngr='pkg';
1075        local osName=$os
1076        local osVersion=$(uname -rs | awk -F '[ -]' '{print $2}' ||:)
1077        if [ -z "$osVersion" ]; then
1078            err 'os-probe: Unable to get the FreeBSD version.'
1079        fi
1080    fi
1081
1082    osName=$(echo "$osName" | tr '[:upper:]' '[:lower:]')
1083    echo "$pkgMngr:$osName:$osVersion"
1084}
1085
1086
1087help_unit_ps()
1088{
1089    cat <<__EOF__ ;
1090SYNOPSIS
1091        $0 ps [-h] [-t TYPE]
1092
1093DESCRIPTION
1094        List unitd(8) processes.
1095
1096OPTIONS
1097        -h, --help
1098                Print this help.
1099
1100        -t, --type TYPE
1101                List only processes of type TYPE.  The available types are:
1102
1103                -  controller (c)
1104                -  main (m)
1105                -  router (r)
1106
1107__EOF__
1108}
1109
1110
1111unit_ps()
1112{
1113    while test $# -ge 1; do
1114        case "$1" in
1115        -h | --help)
1116            help_unit_ps;
1117            exit 0;
1118            ;;
1119        -t | --type)
1120            if ! test $# -ge 2; then
1121                err "ps: $1: Missing argument.";
1122            fi;
1123            local type=;
1124            case "$2" in
1125            c | controller)
1126                local type_c='c';
1127                ;;
1128            m | main)
1129                local type_m='m';
1130                ;;
1131            r | router)
1132                local type_r='r';
1133                ;;
1134            esac;
1135            shift;
1136            ;;
1137        -*)
1138            err "ps: $1: Unknown option.";
1139            ;;
1140        *)
1141            err "ps: $1: Unknown argument.";
1142            ;;
1143        esac;
1144        shift;
1145    done;
1146
1147    ps awwx \
1148    | if test -v type; then
1149        grep ${type_c:+-e 'unit: controller'} \
1150             ${type_m:+-e 'unit: main'} \
1151             ${type_r:+-e 'unit: router'};
1152    else
1153        grep 'unit: ';
1154    fi \
1155    | grep -v grep \
1156    ||:
1157}
1158
1159
1160help_unit_repo_config()
1161{
1162    cat <<__EOF__ ;
1163SYNOPSIS
1164        $0 repo-config [-hn] [PKG-MANAGER OS-NAME OS-VERSION]
1165
1166DESCRIPTION
1167        This script configures the NGINX Unit repository for the system
1168        package manager.
1169
1170        The script automatically detects the OS and proceeds accordingly.
1171        However, if this automatic selection fails, you may specify the
1172        package manager and the OS name and version.
1173
1174ARGUMENTS
1175        PKG-MANAGER
1176                Supported: 'apt', 'dnf', and 'yum'.
1177
1178        OS-NAME
1179                Supported: 'debian', 'ubuntu', 'fedora', 'rhel', and 'amzn2'.
1180
1181        OS-VERSION
1182                For most distributions, this should be a numeric value; for
1183                Debian derivatives, use the codename instead.
1184
1185OPTIONS
1186        -h, --help
1187                Print this help.
1188
1189        -n, --dry-run
1190                Dry run.  Print the commands to be run instead of actually
1191                running them.  Each command is preceded by a line explaining
1192                what it does.
1193
1194EXAMPLES
1195        $ $(basename "$0") repo-config apt debian bullseye;
1196        $ $(basename "$0") repo-config apt ubuntu jammy;
1197        $ $(basename "$0") repo-config dnf fedora 36;
1198        $ $(basename "$0") repo-config dnf rhel 9;
1199        $ $(basename "$0") repo-config yum amzn2 2;
1200
1201__EOF__
1202}
1203
1204
1205unit_repo_config()
1206{
1207    installAPT ()
1208    {
1209        local os_name="$2";
1210
1211        dry_run_echo "Install on $os_name";
1212        dry_run_echo;
1213        dry_run_eval 'curl --output /usr/share/keyrings/nginx-keyring.gpg https://unit.nginx.org/keys/nginx-keyring.gpg;';
1214        dry_run_echo;
1215        dry_run_eval 'apt-get install -y apt-transport-https lsb-release ca-certificates;';
1216
1217        if test $# -ge 3; then
1218            local os_version="$3";
1219        else
1220            local os_version='$(lsb_release -cs)';
1221        fi;
1222
1223        dry_run_echo;
1224        dry_run_eval "printf 'deb [signed-by=/usr/share/keyrings/nginx-keyring.gpg] https://packages.nginx.org/unit/$os_name/ %s unit\n' \"$os_version\" | tee /etc/apt/sources.list.d/unit.list;";
1225        dry_run_eval "printf 'deb-src [signed-by=/usr/share/keyrings/nginx-keyring.gpg] https://packages.nginx.org/unit/$os_name/ %s unit\n' \"$os_version\" | tee -a /etc/apt/sources.list.d/unit.list;";
1226        dry_run_echo;
1227        dry_run_eval 'apt-get update;';
1228    }
1229
1230    installYumDnf ()
1231    {
1232        local pkg_mngr="$1";
1233        local os_name="$2";
1234
1235        if test $# -ge 3; then
1236            local os_version="$3";
1237        else
1238            local os_version='\$releasever';
1239        fi;
1240
1241        dry_run_echo "Install on $os_name";
1242        dry_run_echo;
1243
1244        dry_run_eval "cat >/etc/yum.repos.d/unit.repo <<__EOF__
1245[unit]
1246name=unit repo
1247baseurl=https://packages.nginx.org/unit/$os_name/$os_version/\\\$basearch/
1248gpgcheck=0
1249enabled=1
1250__EOF__";
1251
1252        dry_run_echo;
1253        dry_run_eval "$pkg_mngr makecache;";
1254    }
1255
1256    while test $# -ge 1; do
1257        case "$1" in
1258        -h | --help)
1259            help_unit_repo_config;
1260            exit 0;
1261            ;;
1262        -n | --dry-run)
1263            dry_run='yes';
1264            ;;
1265        -*)
1266            err "repo-config: $1: Unknown option.";
1267            ;;
1268        *)
1269            break;
1270            ;;
1271        esac;
1272        shift;
1273    done;
1274
1275    if test $# -ge 1; then
1276        local pkg_mngr="$1";
1277
1278        if ! test $# -ge 2; then
1279            err "repo-config: OS-NAME: Missing argument.";
1280        fi;
1281        local os_name="$2";
1282
1283        if ! test $# -ge 3; then
1284            err "repo-config: OS-VERSION: Missing argument.";
1285        fi;
1286        local os_version="$3";
1287    fi;
1288
1289    command -v curl >/dev/null \
1290    || err 'repo-config: curl(1) not found in PATH.  It must be installed to run this script.';
1291
1292    echo 'This script sets up the NGINX Unit repository';
1293
1294    if ! test $# -ge 3; then
1295        local os_pkg_name_version=$(unit_os_probe || warn "On macOS, try 'brew install nginx/unit/unit'.")
1296        local pkg_mngr=$(echo "$os_pkg_name_version" | awk -F: '{print $1}')
1297        local os_name=$(echo "$os_pkg_name_version" | awk -F: '{print $2}')
1298        local os_version=$(echo "$os_pkg_name_version" | awk -F: '{print $3}')
1299    fi;
1300
1301    # Call the appropriate installation function
1302    case "$pkg_mngr" in
1303    apt)
1304        case "$os_name" in
1305        debian | ubuntu)
1306            installAPT "$pkg_mngr" "$os_name" ${3:+$os_version};
1307            ;;
1308        *)
1309            err "repo-config: $os_name: The OS isn't supported.";
1310            ;;
1311        esac
1312        ;;
1313    yum | dnf)
1314        case "$os_name" in
1315        rhel | amzn | fedora)
1316            installYumDnf "$pkg_mngr" "$os_name" "$os_version" ${3:+ovr};
1317            ;;
1318        *)
1319            err "repo-config: $os_name: The OS isn't supported.";
1320            ;;
1321        esac;
1322        ;;
1323    *)
1324        err "repo-config: $pkg_mngr: The package manager isn't supported.";
1325        ;;
1326    esac;
1327
1328    echo
1329    echo 'All done; the NGINX Unit repository is set up.';
1330    echo "Configured with '$pkg_mngr' on '$os_name' '$os_version'.";
1331    echo 'Further steps: <https://unit.nginx.org/installation/#official-packages>'
1332}
1333
1334
1335help_unit_restart()
1336{
1337    cat <<__EOF__ ;
1338SYNOPSIS
1339        $0 restart [-hls]
1340
1341DESCRIPTION
1342        Restart all running unitd(8) instances.
1343
1344OPTIONS
1345        -h, --help
1346                Print this help.
1347
1348        -l, --log
1349                Reset log file.
1350
1351        -s, --statedir
1352                Reset \$statedir.
1353
1354CAVEATS
1355        This command will ask for confirmation before removing
1356        directories; please review those prompts with care, as unknown
1357        bugs in the command may attempt to wipe your file system.
1358
1359__EOF__
1360}
1361
1362
1363unit_restart()
1364{
1365    while test $# -ge 1; do
1366        case "$1" in
1367        -h | --help)
1368            help_unit_restart;
1369            exit 0;
1370            ;;
1371        -l | --log)
1372            local log_flag='yes';
1373            ;;
1374        -s | --statedir)
1375            local state_flag='yes';
1376            ;;
1377        -*)
1378            err "restart: $1: Unknown option.";
1379            ;;
1380        *)
1381            err "restart: $1: Unknown argument.";
1382            ;;
1383        esac;
1384        shift;
1385    done;
1386
1387    local cmds="$(unit_cmd)";
1388
1389    pkill -e unitd;
1390
1391    printf '%s\n' "$cmds" \
1392    | while read -r cmd; do
1393        if test -v log_flag; then
1394            (
1395                echo "$cmd" \
1396                | grep '\--log' \
1397                | sed 's/.*--log \+\([^ ]\+\).*/\1/' \
1398                || eval $cmd --help \
1399                | grep -A1 '\--log FILE' \
1400                | grep 'default:' \
1401                | sed 's/.*"\(.*\)".*/\1/';
1402            ) \
1403            | xargs rm -f;
1404        fi;
1405
1406        if test -v state_flag; then
1407            (
1408                echo "$cmd" \
1409                | grep '\--statedir' \
1410                | sed 's/.*--statedir \+\([^ ]\+\).*/\1/' \
1411                || eval $cmd --help \
1412                | grep -A1 '\--statedir DIR' \
1413                | grep 'default:' \
1414                | sed 's/.*"\(.*\)".*/\1/';
1415            ) \
1416            | xargs -I {} find {} -mindepth 1 -maxdepth 1 \
1417            | xargs rm -rfi;
1418        fi;
1419
1420        eval $cmd;
1421    done;
1422}
1423
1424
1425help_unit_sock()
1426{
1427    cat <<__EOF__ ;
1428SYNOPSIS
1429        $0 sock [-h] SUBCOMMAND [ARGS]
1430
1431    Subcommands
1432        ├── filter  [-ch]
1433        └── find    [-h]
1434
1435DESCRIPTION
1436        Print the control API socket address of running unitd(8)
1437        instances.
1438
1439        Run '$0 sock SUBCOMMAND -h' for more information on a
1440        subcommand.
1441
1442SUBCOMMANDS
1443        filter  Filter the output of the 'find' subcommand and transform it
1444                to something suitable for running other commands, such as
1445                curl(1) or ssh(1).
1446
1447        find    Find and print the control API socket address of running
1448                unitd(8) instances.
1449
1450OPTIONS
1451        -h, --help
1452                Print this help.
1453
1454__EOF__
1455}
1456
1457
1458unit_sock()
1459{
1460    while test $# -ge 1; do
1461        case "$1" in
1462        -h | --help)
1463            help_unit_sock;
1464            exit 0;
1465            ;;
1466        -*)
1467            err "sock: $1: Unknown option.";
1468            ;;
1469        *)
1470            break;
1471            ;;
1472        esac;
1473        shift;
1474    done;
1475
1476    if ! test $# -ge 1; then
1477        err 'sock: Missing subcommand.';
1478    fi;
1479
1480    case $1 in
1481    filter)
1482        shift;
1483        unit_sock_filter $@;
1484        ;;
1485    find)
1486        shift;
1487        unit_sock_find $@;
1488        ;;
1489    *)
1490        err "sock: $1: Unknown subcommand.";
1491        ;;
1492    esac;
1493}
1494
1495
1496help_unit_sock_filter()
1497{
1498    cat <<__EOF__ ;
1499SYNOPSIS
1500        $0 sock filter [-chs]
1501
1502DESCRIPTION
1503        Filter the output of the 'sock find' command and transform it to
1504        something suitable for running other commands, such as
1505        curl(1) or ssh(1).
1506
1507OPTIONS
1508        -c, --curl
1509                Print an argument suitable for curl(1).
1510
1511        -h, --help
1512                Print this help.
1513
1514        -s, --ssh
1515                Print a socket address suitable for use in an ssh(1) tunnel.
1516
1517__EOF__
1518}
1519
1520
1521unit_sock_filter()
1522{
1523    while test $# -ge 1; do
1524        case "$1" in
1525        -c | --curl)
1526            if test -v ssh_flag; then
1527                err "sock: filter: $1: Missing argument.";
1528            fi;
1529            local curl_flag='yes';
1530            ;;
1531        -h | --help)
1532            help_unit_sock_filter;
1533            exit 0;
1534            ;;
1535        -s | --ssh)
1536            if test -v curl_flag; then
1537                err "sock: filter: $1: Missing argument.";
1538            fi;
1539            local ssh_flag='yes';
1540            ;;
1541        -*)
1542            err "sock: filter: $1: Unknown option.";
1543            ;;
1544        *)
1545            err "sock: filter: $1: Unknown argument.";
1546            ;;
1547        esac;
1548        shift;
1549    done;
1550
1551    while read -r control; do
1552
1553        if test -v curl_flag; then
1554            if echo "$control" | grep '^unix:' >/dev/null; then
1555                unix_socket="$(echo "$control" | sed 's/unix:/--unix-socket /')";
1556                host='http://localhost';
1557            else
1558                unix_socket='';
1559                host="$control";
1560            fi;
1561
1562            echo "$unix_socket $host";
1563
1564        elif test -v ssh_flag; then
1565            echo "$control" \
1566            | sed -E 's,^(unix:|http://|https://),,';
1567
1568        else
1569            echo "$control";
1570        fi;
1571    done;
1572}
1573
1574
1575help_unit_sock_find()
1576{
1577    cat <<__EOF__ ;
1578SYNOPSIS
1579        $0 sock find [-h]
1580
1581DESCRIPTION
1582        Find and print the control API socket address of running
1583        unitd(8) instances.
1584
1585OPTIONS
1586        -h, --help
1587                Print this help.
1588
1589__EOF__
1590}
1591
1592
1593unit_sock_find()
1594{
1595    while test $# -ge 1; do
1596        case "$1" in
1597        -h | --help)
1598            help_unit_sock_find;
1599            exit 0;
1600            ;;
1601        -*)
1602            err "sock: find: $1: Unknown option.";
1603            ;;
1604        *)
1605            err "sock: find: $1: Unknown argument.";
1606            ;;
1607        esac;
1608        shift;
1609    done;
1610
1611    unit_cmd \
1612    | while read -r cmd; do
1613        if echo "$cmd" | grep '\--control' >/dev/null; then
1614            echo "$cmd" \
1615            | sed 's/ --/\n--/g' \
1616            | grep '\--control' \
1617            | cut -d' ' -f2;
1618        else
1619            if ! command -v $cmd >/dev/null; then
1620                local cmd='unitd';
1621            fi;
1622            $cmd --help \
1623            | sed -n '/\--control/,+1p' \
1624            | grep 'default:' \
1625            | sed 's/ *default: "\(.*\)"/\1/';
1626        fi;
1627    done;
1628}
1629
1630
1631while test $# -ge 1; do
1632    case "$1" in
1633    -h | --help)
1634        help_unit;
1635        exit 0;
1636        ;;
1637    --help-more)
1638        help_more_unit;
1639        exit 0;
1640        ;;
1641    -*)
1642        err "$1: Unknown option.";
1643        ;;
1644    *)
1645        break;
1646        ;;
1647    esac;
1648    shift;
1649done;
1650
1651if ! test $# -ge 1; then
1652    err "Missing command.";
1653fi;
1654
1655case $1 in
1656cmd)
1657    shift;
1658    unit_cmd $@;
1659    ;;
1660ctl)
1661    shift;
1662    unit_ctl $@;
1663    ;;
1664freeport)
1665    shift;
1666    unit_freeport $@;
1667    ;;
1668json-ins)
1669    shift;
1670    unit_json_ins $@;
1671    ;;
1672os-probe)
1673    shift;
1674    unit_os_probe $@;
1675    ;;
1676ps)
1677    shift;
1678    unit_ps $@;
1679    ;;
1680repo-config)
1681    shift;
1682    unit_repo_config $@;
1683    ;;
1684restart)
1685    shift;
1686    unit_restart $@;
1687    ;;
1688sock)
1689    shift;
1690    unit_sock $@;
1691    ;;
1692welcome)
1693    shift;
1694    unit_ctl_welcome $@;
1695    ;;
1696*)
1697    err "$1: Unknown command.";
1698    ;;
1699esac;
1700