From eac796782bef6f2d1780ad8267a66ba93ef2ffdb Mon Sep 17 00:00:00 2001 From: nyxnor Date: Sat, 19 Feb 2022 01:49:54 +0000 Subject: [PATCH] massive changes to dissipate scripts - removed location, best to look on TPO site for that - scripts separated - functions on /usr/share - you can run your own onionjuggler-cli-plugin - Whonix WS and GW requirements are different - improved manual pages --- .shellcheckrc | 3 + auto-generated-man-pages/onionjuggler-cli.1 | 94 +- auto-generated-man-pages/onionjuggler-tui.1 | 16 - auto-generated-man-pages/onionjuggler.conf.5 | 13 +- configure.sh | 134 +- etc/onionjuggler/openbsd.conf | 2 +- .../{whonix.conf => whonix-gateway.conf} | 7 +- etc/onionjuggler/whonix-workstation.conf | 29 + man/onionjuggler-cli.1.md | 71 +- man/onionjuggler-tui.1.md | 14 - man/onionjuggler.conf.5.md | 9 +- usr/bin/onionjuggler-cli | 1228 ++--------------- usr/bin/onionjuggler-cli-auth-client | 131 ++ usr/bin/onionjuggler-cli-auth-server | 174 +++ usr/bin/onionjuggler-cli-backup | 119 ++ usr/bin/onionjuggler-cli-vanguards | 148 ++ usr/bin/onionjuggler-cli-web | 216 +++ usr/bin/onionjuggler-tui | 322 ++--- usr/share/onionjuggler/defaults.sh | 437 ++++++ 19 files changed, 1643 insertions(+), 1524 deletions(-) rename etc/onionjuggler/{whonix.conf => whonix-gateway.conf} (82%) create mode 100644 etc/onionjuggler/whonix-workstation.conf create mode 100755 usr/bin/onionjuggler-cli-auth-client create mode 100755 usr/bin/onionjuggler-cli-auth-server create mode 100755 usr/bin/onionjuggler-cli-backup create mode 100755 usr/bin/onionjuggler-cli-vanguards create mode 100755 usr/bin/onionjuggler-cli-web create mode 100644 usr/share/onionjuggler/defaults.sh diff --git a/.shellcheckrc b/.shellcheckrc index e2c59f8..cf4fbec 100644 --- a/.shellcheckrc +++ b/.shellcheckrc @@ -1,2 +1,5 @@ shell=sh source=/dev/null +## disabling 2154 is raw but the variables +## are sourced from a non constant source +disable=SC2154 diff --git a/auto-generated-man-pages/onionjuggler-cli.1 b/auto-generated-man-pages/onionjuggler-cli.1 index bef8b06..635fa45 100644 --- a/auto-generated-man-pages/onionjuggler-cli.1 +++ b/auto-generated-man-pages/onionjuggler-cli.1 @@ -8,31 +8,29 @@ onionjuggler-cli - Dinamically juggle with onion services with a POSIX compliant shell .SH SYNOPSIS .PP -\f[B]onionjuggler-cli\f[R] \f[B]command\f[R] -[\f[B]--option\f[R]<=\f[I]ARGUMENT\f[R]>] +\f[B]onionjuggler-cli\f[R] [\f[B]--option\f[R]<=\f[I]ARGUMENT\f[R]>] .PD 0 .P .PD -.PP \f[B]onionjuggler-cli [--getconf]\f[R] .PD 0 .P .PD -\f[B]onionjuggler-cli [--getopt]\f[R] +\f[B]onionjuggler-cli [--getopt]\f[R] [\f[B]--service\f[R] +<\f[I]SERVICE\f[R]>] .PD 0 .P .PD \f[B]onionjuggler-cli --activate\f[R] [\f[B]--service\f[R] -<\f[I]SERVICE\f[R]>] [\f[B]--version\f[R] <\f[I]VERSION\f[R]>] -[\f[B]--socket\f[R] <\f[I]tcp\f[R]>] [\f[B]--port\f[R] -<\f[I]VIRTPORT\f[R] [,\f[I]TARGET\f[R]] -[\f[I]VIRTPORT2\f[R]][,\f[I]TARGET2\f[R]]>] [\f[B]--gateway\f[R]] +<\f[I]SERVICE\f[R]>] [\f[B]--socket\f[R] <\f[I]tcp\f[R]>] +[\f[B]--port\f[R] <\f[I]VIRTPORT\f[R][,\f[I]TARGET\f[R]] +[\f[I]VIRTPORTn\f[R]][,\f[I]TARGETn\f[R]]>] [\f[B]--gateway\f[R]] .PD 0 .P .PD \f[B]onionjuggler-cli --activate\f[R] [\f[B]--service\f[R] <\f[I]SERVICE\f[R]>] [\f[B]--version\f[R] <\f[I]VERSION\f[R]>] -[\f[B]--socket\f[R] <\f[I]unix\f[R]> [\f[B]--port\f[R] +[\f[B]--socket\f[R] <\f[I]unix\f[R]>] [\f[B]--port\f[R] [\f[I]VIRTPORT\f[R] [\f[I]VIRTPORT2\f[R]]>] .PD 0 .P @@ -110,12 +108,6 @@ compliant shell .PD 0 .P .PD -\f[B]onionjuggler-cli --location\f[R] [\f[B]--service\f[R] -<\f[I]SERVICE\f[R]>] -[\f[B]--nginx\f[R]|\f[B]--apache2\f[R]|\f[B]--html\f[R]] -.PD 0 -.P -.PD \f[B]onionjuggler-cli --backup\f[R] [\f[B]--create\f[R]|\f[B]--integrate\f[R]] .PD 0 @@ -126,6 +118,11 @@ compliant shell .PD 0 .P .PD +\f[B]onionjuggler-cli\f[R] [\f[B]--plugin\f[R] +<\f[I]auth-server\f[R]|\f[I]auth-client\f[R]|\f[I]web\f[R]|\f[I]vanguards\f[R]|\f[I]backup\f[R]>] +.PD 0 +.P +.PD \f[B]onionjuggler-cli\f[R] [\f[B]-h\f[R]|\f[B]-help\f[R]|\f[B]--help\f[R]|\f[B]help\f[R]] .SH DESCRIPTION @@ -210,12 +207,13 @@ OR --service \[dq]ssh nextcloud\[dq] OR --service=ssh,nextcloud Example of onion services directory names. .SS ARGUMENTS .TP -\f[B]getconf\f[R] -Print configuration in the format key=val. -.PP -\f[B]getopt\f[R] +\f[B]--getconf\f[R] +Print configuration in the format \f[B]key\f[R]=\[dq]\f[I]val\f[R]\[dq]. .TP -\f[B]--activate\f[R] \f[B]--service\f[R] [\f[I]SERV\f[R]] \f[B]--version\f[R] \f[I]3\f[R] \f[B]--socket\f[R] \f[I]tcp\f[R] \f[B]--port\f[R] [\f[I]VIRTPORT\f[R],<\f[I]TARGET\f[R]>,<\f[I]VIRTPORT2\f[R]>,<\f[I]TARGET2\f[R]>] [\f[B]--gateway\f[R]] +\f[B]--getopt\f[R] +Print option parsing results. +.TP +\f[B]--activate\f[R] \f[B]--service\f[R] <\f[I]SERV\f[R]> \f[B]--version\f[R] \f[I]3\f[R] \f[B]--socket\f[R] \f[I]tcp\f[R] \f[B]--port\f[R] <\f[I]VIRTPORT\f[R],<\f[I]TARGET\f[R]>,<\f[I]VIRTPORT2\f[R]>,<\f[I]TARGET2\f[R]>> \f[B]--gateway\f[R] Enable an onion service using TCP socket (addr:port) as target. If the TARGET is only the port of it TARGET was not provided, will use the same port as VIRTPORT and bind to 127.0.0.1. @@ -250,7 +248,7 @@ onionjuggler-cli --activate --service ssh --socket tcp --port 22 --gateway .fi .RE .TP -\f[B]--activate\f[R] \f[B]--service\f[R] [\f[I]SERV\f[R]] \f[B]--version\f[R] \f[I]3\f[R] \f[B]--socket\f[R] \f[I]unix\f[R] \f[B]--port\f[R] [\f[I]VIRTPORT\f[R],<\f[I]VIRTPORT2\f[R]>] +\f[B]--activate\f[R] \f[B]--service\f[R] <\f[I]SERV\f[R]> \f[B]--version\f[R] \f[I]3\f[R] \f[B]--socket\f[R] \f[I]unix\f[R] \f[B]--port\f[R] <\f[I]VIRTPORT\f[R],<\f[I]VIRTPORT2\f[R]>> Enable an onion service using UNIX socket (unix:path) as target. The TARGET is handled automatically by the script. This method avoids leaking the onion service address to the local @@ -267,7 +265,7 @@ onionjuggler-cli --activate --service ssh --version 3--socket unix --port 22,80 .fi .RE .TP -\f[B]--deactivate\f[R] \f[B]--service\f[R] [\f[I]SERV1\f[R],\f[I]SERV2\f[R],\f[I]...\f[R]] <\f[I]--purge\f[R]> +\f[B]--deactivate\f[R] \f[B]--service\f[R] <\f[I]SERV1\f[R],\f[I]SERV2\f[R],\f[I]...\f[R]> <\f[I]--purge\f[R]> Disable an onion service by removing it configuration lines (HiddenService) from the torrc. Optionally purge its data directory, which will delete permanently the @@ -284,7 +282,7 @@ onionjuggler-cli --deactivate --service ssh,xmpp --purge .fi .RE .TP -\f[B]--info\f[R] \f[B]--service\f[R] [\f[I]\[at]all\f[R]|\f[I]SERV1\f[R],\f[I]SERV2\f[R],\f[I]...\f[R]] <\f[I]--quiet\f[R]> +\f[B]--info\f[R] \f[B]--service\f[R] <\f[I]\[at]all\f[R]|\f[I]SERV1\f[R],\f[I]SERV2\f[R],\f[I]...\f[R]> <\f[I]--quiet\f[R]> List onion service information: hostname (address) and in QR encoded format, clients names and quantity, status if service is active or inactive regarding the torrc lines (un)present and the HiddenServiceDir @@ -302,7 +300,7 @@ onionjuggler-cli --info --service \[at]all --quiet .fi .RE .TP -\f[B]--renew\f[R] \f[B]--service\f[R] [\f[I]\[at]all\f[R]|\f[I]SERV1\f[R],\f[I]SERV2\f[R],\f[I]...\f[R]] +\f[B]--renew\f[R] \f[B]--service\f[R] <\f[I]\[at]all\f[R]|\f[I]SERV1\f[R],\f[I]SERV2\f[R],\f[I]...\f[R]> Renew onion service hostname (.onion domain) and clients (inside HiddenServiceDir/authorized_clients/). The onion service keys (hs_ed25519_public_key and @@ -319,7 +317,7 @@ onionjuggler-cli --renew --service \[at]all .fi .RE .TP -\f[B]--auth-server --on\f[R] \f[B]--service\f[R] [\f[I]SERV\f[R]] \f[B]--client\f[R] [\f[I]CLIENT\f[R]] \f[B]--client-pub-key\f[R] <\f[I]CLIENT_PUB_KEY\f[R]> +\f[B]--auth-server --on\f[R] \f[B]--service\f[R] <\f[I]SERV\f[R]> \f[B]--client\f[R] <\f[I]CLIENT\f[R]> \f[B]--client-pub-key\f[R] <\f[I]CLIENT_PUB_KEY\f[R]> Authorize to your service a client. If the client public key is not provided, a new key pair of public and private keys will be generated, keys are sent to stdout and you should @@ -337,7 +335,7 @@ onionjuggler-cli --auth-server --on --service ssh --client alice --client-pub-ke .fi .RE .TP -\f[B]--auth-server --on\f[R] \f[B]--service\f[R] [\f[I]\[at]all\f[R]|\f[I]SERV1\f[R],\f[I]SERV2\f[R],\f[I]...\f[R]] \f[B]--client\f[R] [\f[I]CLIENT1\f[R],\f[I]CLIENT2\f[R],\f[I]...\f[R]] +\f[B]--auth-server --on\f[R] \f[B]--service\f[R] <\f[I]\[at]all\f[R]|\f[I]SERV1\f[R],\f[I]SERV2\f[R],\f[I]...\f[R]> \f[B]--client\f[R] <\f[I]CLIENT1\f[R],\f[I]CLIENT2\f[R],\f[I]...\f[R]> Authorize to your service a client. A key pair of public and private keys will be generated, keys are sent to stdout and you should send to the client. @@ -358,7 +356,7 @@ onionjuggler-cli --auth-server --on -service \[at]all --client \[at]all .fi .RE .TP -\f[B]--auth-server --off\f[R] \f[B]--service\f[R] [\f[I]\[at]all\f[R]|\f[I]SERV1\f[R],\f[I]SERV2\f[R],\f[I]...\f[R]] \f[B]--client\f[R] [\f[I]\[at]all\f[R]|\f[I]CLIENT1\f[R],\f[I]CLIENT2\f[R],\f[I]...\f[R]] +\f[B]--auth-server --off\f[R] \f[B]--service\f[R] <\f[I]\[at]all\f[R]|\f[I]SERV1\f[R],\f[I]SERV2\f[R],\f[I]...\f[R]> \f[B]--client\f[R] <\f[I]\[at]all\f[R]|\f[I]CLIENT1\f[R],\f[I]CLIENT2\f[R],\f[I]...\f[R]> Deauthorize from your service a client that is inside HiddenServiceDir/authorized_clients folder. File(s) modified: HiddenServiceDir/authorized_clients/ @@ -376,7 +374,7 @@ onionjuggler-cli --auth-server --off --service \[at]all --client \[at]all .fi .RE .TP -\f[B]--auth-server --list\f[R] \f[B]--service\f[R] [\f[I]\[at]all\f[R]|\f[I]SERV1\f[R],\f[I]SERV2\f[R],\f[I]...\f[R]] +\f[B]--auth-server --list\f[R] \f[B]--service\f[R] <\f[I]\[at]all\f[R]|\f[I]SERV1\f[R],\f[I]SERV2\f[R],\f[I]...\f[R]> List authorized clients and the respective public keys that are inside HiddenServiceDir/authorized_clients folder. File(s) modified: none @@ -391,7 +389,7 @@ onionjuggler-cli --auth-server --list --service \[at]all .fi .RE .TP -\f[B]--auth-client --on\f[R] \f[B]--onion\f[R] [\f[I]ONION\f[R]] \f[B]--client-priv-key\f[R] <\f[I]CLIENT_PRIV_KEY\f[R]> +\f[B]--auth-client --on\f[R] \f[B]--onion\f[R] <\f[I]ONION\f[R]> \f[B]--client-priv-key\f[R] <\f[I]CLIENT_PRIV_KEY\f[R]> Authenticate as a client to a remote onion serivce. If the client private keys is not provided, a new key pair of public and private keys will be generated, keys are sent to stdout and you should @@ -408,7 +406,7 @@ onionjuggler-cli --auth-client --on --onion fe4avn4qtxht5wighyii62n2nw72spfabzv6 .fi .RE .TP -\f[B]--auth-client --off\f[R] \f[B]--onion\f[R] [\f[I]ONION1\f[R],\f[I]ONION2\f[R],\f[I]...\f[R]] +\f[B]--auth-client --off\f[R] \f[B]--onion\f[R] <\f[I]ONION1\f[R],\f[I]ONION2\f[R],\f[I]...\f[R]> Deauthenticate from a remote onion serivce. Remove the $ONION.auth_private file from ClientOnionAuthDir. File(s) modified: ClientOnionAuthDir/. @@ -436,7 +434,7 @@ onionjuggler-cli --auth-client --list .fi .RE .TP -\f[B]--web --on\f[R] \f[B]--service\f[R] [\f[I]SERV\f[R]] \f[B]--folder\f[R] [\f[I]FOLDER\f[R]] +\f[B]--web --on\f[R] \f[B]--service\f[R] <\f[I]SERV\f[R]> \f[B]--folder\f[R] <\f[I]FOLDER\f[R]> Enable a website using a specific onion service by creating a configuration file inside the web server folder by default, the folder name is to be considered the wanted folder inside website_dir variable @@ -453,7 +451,7 @@ onionjuggler-cli --web on nextcloud nextcloud-local-site .fi .RE .TP -\f[B]--web --off\f[R] \f[B]--service\f[R] [\f[I]SERV\f[R]] +\f[B]--web --off\f[R] \f[B]--service\f[R] <\f[I]SERV\f[R]> Disable a website from a specific onion service by removing its configuration file from the webserver folder. File(s) modified: $webserver_conf @@ -479,24 +477,7 @@ onionjuggler-cli --web --list .fi .RE .TP -\f[B]--location\f[R] \f[B]--service\f[R] [\f[I]SERV\f[R]] [\f[I]--nginx\f[R]|\f[I]--apache2\f[R]|\f[I]--html\f[R]] -Guide to add onion location to your plainnet website when using the -webserver Nginx or Apache2 or an HTML header. -It does not modify any configuration by itself, the instructions to do -so are send to stdout. -File(s) modified: none. -.RS -.IP -.nf -\f[C] -onionjuggler-cli --location --service nextcloud --nginx -onionjuggler-cli --location --service nextcloud --apache2 -onionjuggler-cli --location --service nextcloud --html -\f[R] -.fi -.RE -.TP -\f[B]backup\f[R] [\f[I]--create\f[R]|\f[I]--integrate\f[R]] +\f[B]--backup\f[R] [\f[I]--create\f[R]|\f[I]--integrate\f[R]] Backup all of the torrc, DataDir/services and ClientOnionAuthDir either by creating a backup file or integrating to the system from a backup made before. @@ -561,9 +542,10 @@ Signal tor daemon to restart or reload after the CLI edits tor\[aq]s configuration files. (Default: reload) .TP -\f[B]-C\f[R], \f[B]--config\f[R] -Specify and alternative configuration file to override default -configuration. +\f[B]-G\f[R], \f[B]--plugin\f[R] <\f[I]PLUGIN\f[R]> +Run an onionjuggler plugin. +Requires the plugin to be installed with the name +\f[I]onionjuggler-cli-*\f[R]. .SH FILES .TP \f[B]/etc/onionjuggler/onionjuggler.conf\f[R] @@ -571,16 +553,12 @@ Default system configuration file. .TP \f[B]/etc/onionjuggler/conf.d/*.conf\f[R] Local configuration files that overrrite the default one. -.SH ENVIRONMENT -.TP -\f[B]ONIONJUGGLER_CONF\f[R] -The environmental variable will override all previous options. .SH EXIT VALUE .TP \f[B]0\f[R] Success .TP -\f[B]1\f[R] +\f[B]>0\f[R] Fail .SH BUGS .PP diff --git a/auto-generated-man-pages/onionjuggler-tui.1 b/auto-generated-man-pages/onionjuggler-tui.1 index 9b5c50a..d7ff78b 100644 --- a/auto-generated-man-pages/onionjuggler-tui.1 +++ b/auto-generated-man-pages/onionjuggler-tui.1 @@ -14,11 +14,6 @@ Dinamically juggle with onion services with a POSIX compliant shell .PD 0 .P .PD -\f[B]onionjuggler-tui\f[R] [\f[B]--config\f[R] -\f[I]ONIONJUGGLER_CONF\f[R]] -.PD 0 -.P -.PD \f[B]onionjuggler-tui\f[R] \f[B]--help\f[R] .SH DESCRIPTION .PP @@ -31,23 +26,12 @@ a terminal dialog box. .TP \f[B]-h\f[R], \f[B]--help\f[R] Display a short help message and exit. -.TP -\f[B]-C\f[R], \f[B]--config\f[R] \f[I]ONIONJUGGLER_CONF\f[R] -Specify and alternative configuration file to override default -configuration. .SH FILES .TP -\f[B]/usr/local/bin/onionjuggler-cli\f[R] -OnionJuggler TUI will call the CLI to execute the tasks after the dialog -options have been selected. -.TP \f[B]/etc/onionjuggler/dialogrc\f[R] Default dialog run commands file. .SH ENVIRONMENT .TP -\f[B]ONIONJUGGLER_CONF\f[R] -The environmental variable will override all previous options. -.TP \f[B]SUDO_EDITOR\f[R], \f[B]DOAS_EDITOR\f[R], \f[B]VISUAL\f[R], \f[B]EDITOR\f[R] Use environment variables in the above order to define the editor, in case any are empty, fallback to the next. diff --git a/auto-generated-man-pages/onionjuggler.conf.5 b/auto-generated-man-pages/onionjuggler.conf.5 index 3c9e014..0d091ee 100644 --- a/auto-generated-man-pages/onionjuggler.conf.5 +++ b/auto-generated-man-pages/onionjuggler.conf.5 @@ -11,12 +11,13 @@ onionjuggler.conf - Configuration file for OnionJuggler combination of POSIX compliant scripts helps the interaction with onion service configuration and files to speed up usage and avoid misconfiguration. -The system variables are defined by the environment variable -\f[I]ONIONJUGGLER_CONF\f[R], but if it is empty, will read +The system variables are defined by the default configuration file \f[I]/etc/onionjuggler/onionjuggler.conf\f[R]. The configuration file is then sourced be used by the program. It defines where the hidden services are located, the owner of the -DataDir folder, the ControlPort to be used. +DataDirectory older, the ControlPort to be used. +Variables defined inside _/etc/onionjuggler/conf.d/*.conf_ are parsed in +lexical order and overwrite the default configuration. .PP The configuration file is parsed by the shell and interpreted as variables. @@ -133,12 +134,6 @@ Useful for when the server is running on an external host related to the tor process. On Qubes-Whonix, you should set the Whonix Workstation Qubes IP address (Default: 127.0.0.1). -.SH ENVIRONMENT -.TP -\f[B]ONIONJUGGLER_CONF\f[R] -Use the environment variable to search for the configuration file, if -the variable is empty, use the default confiugration on -\f[I]/etc/onionjuggler/onionjuggler.conf\f[R]. .SH FILES .TP \f[B]/etc/onionjuggler/onionjuggler.conf\f[R] diff --git a/configure.sh b/configure.sh index 04db991..2dec91b 100755 --- a/configure.sh +++ b/configure.sh @@ -6,44 +6,28 @@ onionjuggler_repo="${ONIONJUGGLER_GIT_ORIGIN:-"https://github.com/nyxnor/onionjuggler.git"}" -me="${0##*/}" -## colors -nocolor="\033[0m" -#bold="\033[1m" -#nobold="\033[22m" -#underline="\033[4m" -#nounderline="\033[24m" -red="\033[31m" -green="\033[32m" -yellow="\033[33m" -blue="\033[34m" -magenta="\033[35m" -cyan="\033[36m" - -notice(){ printf %s"${me}: ${1}\n" 1>&2; } -error_msg(){ notice "${red}error: ${1}${nocolor}"; exit 1; } - topdir="$(git rev-parse --show-toplevel)" -check_repo(){ - if [ ! -f "${topdir}"/usr/bin/onionjuggler-cli ] || [ ! -f "${topdir}"/usr/bin/onionjuggler-tui ]; then - error_msg "This script must be run from inside the onionjuggler repository!" - fi -} +me="${0##*/}" +onionjuggler_defaults="/usr/share/onionjuggler/defaults.sh" +[ -e "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} does not exist"; exit 1; } +[ -f "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} is not a regular file"; exit 1; } +[ -r "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} can not be read"; exit 1; } +. "${onionjuggler_defaults}" usage(){ - printf "Configure the environment for OnionJuggler -\nUsage: configure.sh command [option ] + printf %s"Configure the environment for OnionJuggler +\nUsage: ${me} [--option ] \nOptions: -i, --install setup environment copying files to path -d, --uninstall [-P, --purge] remove onionjuggler scripts and manual pages from path -h, --help show this help message \nAdvanced options: - -i, --install [-b |-c |-m ] setup environment with specified paths - -C, --config specify alternative onionjuggler configuration file to be read -B, --bin-dir script directory that is on path (Default: /usr/local/bin) -F, --conf-dir configuration directory (Default: /etc) -M, --man-dir manual directory (Default: /usr/local/man/man1) + -i, --install [-b |-c |-m ] setup environment with specified paths + -G, --plugin if plugin is specified, only install selected plugins (e.g: vanguards,web) -k, --check run pre-defined shellcheck -m, --man build manual pages -r, --release prepare for commiting @@ -53,48 +37,26 @@ usage(){ } [ -z "${1}" ] && usage -## if option requires argument, check if it was provided, if yes, assign the arg to the opt -## $arg was already assigned, and if valid, will use it for the key value -## usage: get_arg key -get_arg(){ - ## if argument is empty or starts with '-', fail as it possibly is an option - case "${arg}" in ""|-*) error_msg "Option '${opt_orig}' requires an argument.";; esac - ## assign - value="${arg}" - ## Escaping quotes is needed because else it will fail if the argument is quoted - # shellcheck disable=SC2140 - eval "${1}"="\"${value}\"" - - ## shift positional argument two times, as this option demands argument, unless they are separated by equal sign '=' - ## shift_n default value was assigned when trimming hifens '--' from the options - ## if shift_n is equal to zero, '--option arg' - ## if shift_n is not equal to zero, '--option=arg' - [ -z "${shift_n}" ] && shift_n=2 + +check_repo(){ + if [ ! -f "${topdir}"/usr/bin/onionjuggler-cli ] || [ ! -f "${topdir}"/usr/bin/onionjuggler-tui ]; then + error_msg "This script must be run from inside the onionjuggler repository!" + fi } -## hacky getopts -## accepts long (--option) and short (-o) options -## accept argument assignment with space (--option arg | -o arg) or equal sign (--option=arg | -o=arg) + while :; do - ## '--option=value' should shift once and '--option value' should shift twice - ## but at this point it is not possible to be sure if option requires an argument - ## reset shift to zero, at the end, if it is still 0, it will be assigned to one - ## has to be zero here so we can check later if option argument is separated by space ' ' or equal sign '=' shift_n="" + # shellcheck disable=SC2034 opt_orig="${1}" ## save opt orig for error message to understand which opt failed - case "${opt_orig}" in - --) shift 1; break;; ## stop option parsing - --*=*) opt="${1%=*}"; opt="${opt#*--}"; arg="${1#*=}"; shift_n=1;; ## long option '--sleep=1' - -*=*) opt="${1%=*}"; opt="${opt#*-}"; arg="${1#*=}"; shift_n=1;; ## short option '-s=1' - --*) opt="${1#*--}"; arg="${2}";; ## long option '--sleep 1' - -*) opt="${1#*-}"; arg="${2}";; ## short option '-s 1' - "") break;; ## options ended - *) usage;; ## not an option - esac + # shellcheck disable=SC2034 + arg_possible="${2}" ## need to pass the second positional parameter because maybe it is an argument + clean_opt "${1}" || break + # shellcheck disable=SC2034 case "${opt}" in - c|clone|i|install|u|update|d|uninstall|r|release|k|check|m|man) command="${opt}"; shift;; - P|purge) action="${opt}"; shift;; - C|config|C=*|confg=*) get_arg ONIONJUGGLER_CONF;; + c|clone|i|install|u|update|d|uninstall|r|release|k|check|m|man) command="${opt}";; + G|plugin|G=*|plugin=*) get_arg plugin;; + P|purge) action="${opt}";; B|bin-dir|B=*|bin-dir=*) get_arg bin_dir;; F|conf-dir|F=*|confi-dir=*) get_arg conf_dir;; M|man-dir|M=*|man-dir=*) get_arg man_dir;; @@ -102,6 +64,8 @@ while :; do "") break;; *) error_msg "Invalid option: '${opt}'";; esac + shift "${shift_n:-1}" + [ -z "${1}" ] && break done @@ -124,7 +88,7 @@ check_dir(){ install_package(){ - install_pkg="" + install_pkg="" for package in "${@}"; do case "${package}" in python-stem|python3-stem|security/py-stem|py-stem|py3-stem|py37-stem|stem) @@ -156,7 +120,9 @@ make_shellcheck(){ command -v shellcheck >/dev/null || error_msg "Install shellcheck to review syntax" notice "${yellow}Checking shell syntax${nocolor}" ## Customize severity with -S [error|warning|info|style] - if ! shellcheck "${topdir}"/configure.sh "${topdir}"/etc/onionjuggler/*.conf "${topdir}"/etc/onionjuggler/conf.d/*.conf "${topdir}"/usr/bin/*; then + if ! shellcheck "${topdir}"/configure.sh "${topdir}"/etc/onionjuggler/*.conf \ + "${topdir}"/etc/onionjuggler/conf.d/*.conf "${topdir}"/usr/bin/* \ + "${topdir}"/usr/share/onionjuggler/*; then error_msg "Please fix the shellcheck warnings above before pushing!" fi } @@ -166,7 +132,7 @@ make_man(){ command -v pandoc >/dev/null || error_msg "Install pandoc to create manuals" notice "${magenta}Creating manual pages${nocolor}" for man in "${topdir}"/man/*; do - man="${man##*/}" + man="${man##*/}" pandoc -s -f markdown-smart -t man "${topdir}/man/${man}" -o "${topdir}/auto-generated-man-pages/${man%*.md}" done } @@ -192,10 +158,10 @@ get_os(){ case ${os} in Linux*) - if test -f /usr/share/anon-ws-base-files/workstation; then - error_msg "OnionJuggler is meant to be run on the Gateway, not Workstation" - elif test -f /usr/share/anon-gw-base-files/gateway; then - distro="Whonix" + if test -f /usr/share/anon-gw-base-files/gateway; then + distro="Whonix-Gateway" + elif test -f /usr/share/anon-ws-base-files/workstation; then + distro="Whonix-Workstation" elif command -v lsb_release >/dev/null; then distro=$(lsb_release -sd) elif [ -f /etc/os-release ]; then @@ -230,7 +196,6 @@ get_os(){ ## 1. source default configuration file first ## 2. source local (user made) configuration files to override the default values -## 3. source the ONIONJUGGLER_CONF specified by the cli argument and if it empty, use the environment variable get_vars(){ if [ ! -f /etc/onionjuggler/onionjuggler.conf ]; then get_os @@ -238,8 +203,9 @@ get_vars(){ Linux*) case "${distro}" in "Debian"*|*"buntu"*|"Armbian"*|"Rasp"*|"Linux Mint"*|"LinuxMint"*|"mint"*) . "${topdir}"/etc/onionjuggler/debian.conf;; - "Tails"*) . "${topdir}"/etc/onionjuggler/tails.conf;; - "Whonix"*) . "${topdir}"/etc/onionjuggler/whonix.conf;; + "Tails"*) . "${topdir}"/etc/onionjuggler/tails.conf;; + "Whonix-Gateway") . "${topdir}"/etc/onionjuggler/whonix-gateway.conf;; + "Whonix-Workstation") . "${topdir}"/etc/onionjuggler/whonix-workstation.conf;; "Arch"*|"Artix"*|"ArcoLinux"*) . "${topdir}"/etc/onionjuggler/arch.conf;; "Fedora"*|"CentOS"*|"rhel"*|"Redhat"*|"Red hat") . "${topdir}"/etc/onionjuggler/fedora.conf;; esac @@ -253,7 +219,6 @@ get_vars(){ [ -r /etc/onionjuggler/onionjuggler.conf ] && . /etc/onionjuggler/onionjuggler.conf fi for file in /etc/onionjuggler/conf.d/*.conf; do [ -f "${file}" ] && . "${file}"; done - [ -r "${ONIONJUGGLER_CONF}" ] && . "${ONIONJUGGLER_CONF}" ## if any of the configurations are empty, use default ones : "${su_cmd:="sudo"}" @@ -275,10 +240,10 @@ get_vars(){ range_variable dialog_box dialog whiptail } + ################### ###### MAIN ####### - case "${command}" in c|clone) @@ -288,7 +253,6 @@ case "${command}" in onionjuggler_dir="${onionjuggler_repo##*/}" if [ -d "${onionjuggler_dir}" ]; then cd "${onionjuggler_dir}" || error_msg "Couldn't change to directory ${onionjuggler_dir}" - "${0}" -i fi else error_msg "Can't clone when already in the repository." @@ -326,16 +290,30 @@ case "${command}" in man_extension="${man##*.}" cp "${man}" "${man_dir}/man${man_extension}" done - cp "${topdir}"/usr/bin/onionjuggler-cli "${topdir}"/usr/bin/onionjuggler-tui "${bin_dir}" + if [ -n "${plugin}" ]; then + cp "${topdir}"/usr/bin/onionjuggler-tui "${topdir}"/usr/bin/onionjuggler-cli "${bin_dir}" + for pg in $(printf '%s\n' "${plugin}" | tr "," " "); do + pg="${pg##*onionjuggler-cli-}" + if test -f "${topdir}/usr/bin/onionjuggler-cli-${pg}"; then + cp "${topdir}/usr/bin/onionjuggler-cli-${pg}" "${bin_dir}" + else + error_msg "Plugin '${pg}' does not exist and is not going to be installed" + fi + done + else + cp "${topdir}"/usr/bin/* "${bin_dir}" + fi + [ ! -d "${conf_dir}/onionjuggler" ] && mkdir -p "${conf_dir}/conf.d" cp "${topdir}"/etc/onionjuggler/dialogrc "${conf_dir}" - ## Source of distro names: neofetch -> https://github.com/dylanaraps/neofetch/blob/master/neofetch + ## Source of distro names: neofetch -> https://github.com/dylanaraps/neofetch case "${os}" in Linux*) case "${distro}" in "Debian"*|*"buntu"*|"Armbian"*|"Rasp"*|"Linux Mint"*|"LinuxMint"*|"mint"*) cp "${topdir}"/etc/onionjuggler/debian.conf "${conf_dir}/onionjuggler.conf";; "Tails"*) cp "${topdir}"/etc/onionjuggler/tails.conf "${conf_dir}/oionjuggler.conf";; - "Whonix"*) cp "${topdir}"/etc/onionjuggler/whonix.conf "${conf_dir}/onionjuggler.conf";; + "Whonix-Gateway") cp "${topdir}"/etc/onionjuggler/whonix-gateway.conf "${conf_dir}/onionjuggler.conf";; + "Whonix-Workstation") cp "${topdir}"/etc/onionjuggler/whonix-workstation.conf "${conf_dir}/onionjuggler.conf";; "Arch"*|"Artix"*|"ArcoLinux"*) cp "${topdir}"/etc/onionjuggler/arch.conf "${conf_dir}/onionjuggler.conf";; "Fedora"*|"CentOS"*|"rhel"*|"Redhat"*|"Red hat") cp "${topdir}"/etc/onionjuggler/fedora.conf "${conf_dir}/onionjuggler.conf";; esac diff --git a/etc/onionjuggler/openbsd.conf b/etc/onionjuggler/openbsd.conf index e5b6525..e453eb3 100644 --- a/etc/onionjuggler/openbsd.conf +++ b/etc/onionjuggler/openbsd.conf @@ -31,7 +31,7 @@ openssl_cmd="eopenssl30" webserver="openbsd-httpd" webserver_conf="/etc/httpd.conf" dialog_box="dialog" -requirements="tor grep sed tar eopenssl30 basez git py3-stem libqrencode ${dialog_box}" +requirements="tor grep sed tar ${openssl_cmd} basez git py3-stem libqrencode ${dialog_box}" ########## tor daemon ########## daemon_control="rcctl" diff --git a/etc/onionjuggler/whonix.conf b/etc/onionjuggler/whonix-gateway.conf similarity index 82% rename from etc/onionjuggler/whonix.conf rename to etc/onionjuggler/whonix-gateway.conf index 4023071..3171b99 100644 --- a/etc/onionjuggler/whonix.conf +++ b/etc/onionjuggler/whonix-gateway.conf @@ -1,4 +1,4 @@ -## Configuration file for OnionJuggler on Debian based systems +## Configuration file for OnionJuggler on Whonix-Gateway ## ## DO NOT EDIT THIS FILE!! ## @@ -21,12 +21,13 @@ # shellcheck disable=SC2034 ########## System ########## +onionjuggler_plugins="auth-server,auth-client" su_cmd="sudo" pkg_mngr_install="apt install -y" openssl_cmd="openssl" webserver="nginx" dialog_box="whiptail" -requirements="tor grep sed tar openssl basez git python3-stem qrencode ${dialog_box} ${webserver}" +requirements="tor grep sed tar openssl basez qrencode ${dialog_box}" ########## tor daemon ########## daemon_control="systemctl" @@ -34,6 +35,6 @@ tor_daemon="tor@default" tor_user="debian-tor" tor_conf_user_group="root:root" tor_conf_dir="/usr/local/etc/torrc.d" -tor_conf="${tor_conf_dir}/50_user.conf" +tor_conf="${tor_conf_dir}/45_onionjuggler.conf" tor_data_dir="/var/lib/tor" tor_data_dir_auth="${tor_data_dir}/authdir" diff --git a/etc/onionjuggler/whonix-workstation.conf b/etc/onionjuggler/whonix-workstation.conf new file mode 100644 index 0000000..4f99a60 --- /dev/null +++ b/etc/onionjuggler/whonix-workstation.conf @@ -0,0 +1,29 @@ +## Configuration file for OnionJuggler on Whonix-Workstation +## +## DO NOT EDIT THIS FILE!! +## +## This file defines the default service selection as shipped in a +## release. Upgrades of OnionJuggler will modify this file. +## +## To select the service options you desire, please override these +## options in the file /etc/onionjuggler/conf.d/*.conf +## +## DO NOT EDIT THIS FILE!! +## +## Lines that begin with "## " try to explain what's going on. Lines +## that begin with just "#" are disabled commands: you can enable them +## by removing the "#" symbol. +## +## If the variable is empty (var=""), will use the default option. +## Double quote to prevent globbing and word splitting. +## variable="value" +## +# shellcheck disable=SC2034 + +########## System ########## +onionjuggler_plugins="web" +su_cmd="sudo" +pkg_mngr_install="apt install -y" +webserver="nginx" +dialog_box="whiptail" +requirements="grep sed qrencode ${dialog_box} ${webserver}" diff --git a/man/onionjuggler-cli.1.md b/man/onionjuggler-cli.1.md index b748923..b6441c0 100644 --- a/man/onionjuggler-cli.1.md +++ b/man/onionjuggler-cli.1.md @@ -9,13 +9,11 @@ onionjuggler-cli - Dinamically juggle with onion services with a POSIX compliant # SYNOPSIS -**onionjuggler-cli** **command** [**--option**<=*ARGUMENT*>]\ - - +**onionjuggler-cli** [**--option**<=*ARGUMENT*>]\ **onionjuggler-cli [--getconf]**\ -**onionjuggler-cli [--getopt]**\ -**onionjuggler-cli --activate** [**--service** <*SERVICE*>] [**--version** <*VERSION*>] [**--socket** <*tcp*>] [**--port** <*VIRTPORT* [,*TARGET*] [*VIRTPORT2*][,*TARGET2*]>] [**--gateway**]\ -**onionjuggler-cli --activate** [**--service** <*SERVICE*>] [**--version** <*VERSION*>] [**--socket** <*unix*> [**--port** [*VIRTPORT* [*VIRTPORT2*]>]\ +**onionjuggler-cli [--getopt]** [**--service** <*SERVICE*>]\ +**onionjuggler-cli --activate** [**--service** <*SERVICE*>] [**--socket** <*tcp*>] [**--port** <*VIRTPORT*[,*TARGET*] [*VIRTPORTn*][,*TARGETn*]>] [**--gateway**]\ +**onionjuggler-cli --activate** [**--service** <*SERVICE*>] [**--version** <*VERSION*>] [**--socket** <*unix*>] [**--port** [*VIRTPORT* [*VIRTPORT2*]>]\ **onionjuggler-cli --deactivate** [**--service** <*SERV1*,*SERV2*,*...*>] [**--purge**]\ **onionjuggler-cli --list** [**--service** <*@all*|*SERV1*,*SERV2*,*...*>] [**--quiet**]\ **onionjuggler-cli --renew** [**--service** <*@all*|*SERV1*,*SERV2*,*...*>]\ @@ -29,9 +27,9 @@ onionjuggler-cli - Dinamically juggle with onion services with a POSIX compliant **onionjuggler-cli --web** [**--on**] [**--service** <*SERVICE*>] [**--folder** <*FOLDER*>]\ **onionjuggler-cli --web** [**--off**] [**--service** <*SERVICE*>]\ **onionjuggler-cli --web** [**--list**]\ -**onionjuggler-cli --location** [**--service** <*SERVICE*>] [**--nginx**|**--apache2**|**--html**]\ **onionjuggler-cli --backup** [**--create**|**--integrate**]\ **onionjuggler-cli --vanguards** [**--on**|**--list**|**--upgrade**|**--off**]\ +**onionjuggler-cli** [**--plugin** <*auth-server*|*auth-client*|*web*|*vanguards*|*backup*>]\ **onionjuggler-cli** [**-h**|**-help**|**--help**|**help**] @@ -108,15 +106,15 @@ The script tries its best to avoid inserting incorrect lines to torrc, that woul ## ARGUMENTS -**getconf** - -: Print configuration in the format key=val. +**--getconf** -**getopt** +: Print configuration in the format **key**="*val*". +**--getopt** +: Print option parsing results. -**--activate** **--service** [*SERV*] **--version** *3* **--socket** *tcp* **--port** [*VIRTPORT*,<*TARGET*>,<*VIRTPORT2*>,<*TARGET2*>] [**--gateway**] +**--activate** **--service** <*SERV*> **--version** *3* **--socket** *tcp* **--port** <*VIRTPORT*,<*TARGET*>,<*VIRTPORT2*>,<*TARGET2*>> **--gateway** : Enable an onion service using TCP socket (addr:port) as target. If the TARGET is only the port of it TARGET was not provided, will use the same port as VIRTPORT and bind to 127.0.0.1. TARGET examples: 127.0.0.1:80, 192.168.1.100:80. File(s) modified: torrc. ``` @@ -133,7 +131,7 @@ By default, services created on a Qubes-Whonix Gateway uses the Whonix Workstati onionjuggler-cli --activate --service ssh --socket tcp --port 22 --gateway ``` -**--activate** **--service** [*SERV*] **--version** *3* **--socket** *unix* **--port** [*VIRTPORT*,<*VIRTPORT2*>] +**--activate** **--service** <*SERV*> **--version** *3* **--socket** *unix* **--port** <*VIRTPORT*,<*VIRTPORT2*>> : Enable an onion service using UNIX socket (unix:path) as target. The TARGET is handled automatically by the script. This method avoids leaking the onion service address to the local network. File(s) modified: torrc. ``` @@ -142,7 +140,7 @@ onionjuggler-cli --activate --service ssh --socket unix --port 22 onionjuggler-cli --activate --service ssh --version 3--socket unix --port 22,80 ``` -**--deactivate** **--service** [*SERV1*,*SERV2*,*...*] <*--purge*> +**--deactivate** **--service** <*SERV1*,*SERV2*,*...*> <*--purge*> : Disable an onion service by removing it configuration lines (HiddenService) from the torrc. Optionally purge its data directory, which will delete permanently the onion service folder (HiddenServiceDir). File(s) modified: torrc and optionally HiddenServiceDir. ``` @@ -151,7 +149,7 @@ onionjuggler-cli --deactivate --service ssh,xmpp onionjuggler-cli --deactivate --service ssh,xmpp --purge ``` -**--info** **--service** [*@all*|*SERV1*,*SERV2*,*...*] <*--quiet*> +**--info** **--service** <*@all*|*SERV1*,*SERV2*,*...*> <*--quiet*> : List onion service information: hostname (address) and in QR encoded format, clients names and quantity, status if service is active or inactive regarding the torrc lines (un)present and the HiddenServiceDir presence, the torrc block. File(s) modified: none. ``` @@ -161,7 +159,7 @@ onionjuggler-cli --info --service @all onionjuggler-cli --info --service @all --quiet ``` -**--renew** **--service** [*@all*|*SERV1*,*SERV2*,*...*] +**--renew** **--service** <*@all*|*SERV1*,*SERV2*,*...*> : Renew onion service hostname (.onion domain) and clients (inside HiddenServiceDir/authorized_clients/). The onion service keys (hs_ed25519_public_key and hs_ed25519_private_key) will be removed to override the hostname file. File(s) modified: HiddenServiceDir. ``` @@ -170,7 +168,7 @@ onionjuggler-cli --renew --service ssh,xmpp onionjuggler-cli --renew --service @all ``` -**--auth-server --on** **--service** [*SERV*] **--client** [*CLIENT*] **--client-pub-key** <*CLIENT_PUB_KEY*> +**--auth-server --on** **--service** <*SERV*> **--client** <*CLIENT*> **--client-pub-key** <*CLIENT_PUB_KEY*> : Authorize to your service a client. If the client public key is not provided, a new key pair of public and private keys will be generated, keys are sent to stdout and you should send to the client. A $CLIENT.auth file will be created on HiddenServiceDir/authorized_clients folder. File(s) modified: HiddenServiceDir/authorized_clients/ ``` @@ -178,7 +176,7 @@ onionjuggler-cli --auth-server --on --service ssh --client alice onionjuggler-cli --auth-server --on --service ssh --client alice --client-pub-key ABVCL52QL6IRYIOLEAYUVTZY3AIOMDI3AIFBAALZ7HJOHIJFVBIQ ``` -**--auth-server --on** **--service** [*@all*|*SERV1*,*SERV2*,*...*] **--client** [*CLIENT1*,*CLIENT2*,*...*] +**--auth-server --on** **--service** <*@all*|*SERV1*,*SERV2*,*...*> **--client** <*CLIENT1*,*CLIENT2*,*...*> : Authorize to your service a client. A key pair of public and private keys will be generated, keys are sent to stdout and you should send to the client. A $CLIENT.auth file will be created on HiddenServiceDir/authorized_clients folder. File(s) modified: HiddenServiceDir/authorized_clients/ ``` @@ -190,7 +188,7 @@ onionjuggler-cli --auth-server --on -service @all --client alice,bob onionjuggler-cli --auth-server --on -service @all --client @all ``` -**--auth-server --off** **--service** [*@all*|*SERV1*,*SERV2*,*...*] **--client** [*@all*|*CLIENT1*,*CLIENT2*,*...*] +**--auth-server --off** **--service** <*@all*|*SERV1*,*SERV2*,*...*> **--client** <*@all*|*CLIENT1*,*CLIENT2*,*...*> : Deauthorize from your service a client that is inside HiddenServiceDir/authorized_clients folder. File(s) modified: HiddenServiceDir/authorized_clients/ ``` @@ -202,7 +200,7 @@ onionjuggler-cli --auth-server --off --service @all --client alice,bob onionjuggler-cli --auth-server --off --service @all --client @all ``` -**--auth-server --list** **--service** [*@all*|*SERV1*,*SERV2*,*...*] +**--auth-server --list** **--service** <*@all*|*SERV1*,*SERV2*,*...*> : List authorized clients and the respective public keys that are inside HiddenServiceDir/authorized_clients folder. File(s) modified: none ``` @@ -211,7 +209,7 @@ onionjuggler-cli --auth-server --list --service ssh,xmpp onionjuggler-cli --auth-server --list --service @all ``` -**--auth-client --on** **--onion** [*ONION*] **--client-priv-key** <*CLIENT_PRIV_KEY*> +**--auth-client --on** **--onion** <*ONION*> **--client-priv-key** <*CLIENT_PRIV_KEY*> : Authenticate as a client to a remote onion serivce. If the client private keys is not provided, a new key pair of public and private keys will be generated, keys are sent to stdout and you should send to the onion service operator. Add a $ONION.auth_private to ClientOnionAuthDir. File(s) modified: ClientOnionAuthDir. ``` @@ -219,7 +217,7 @@ onionjuggler-cli --auth-client --on --onion fe4avn4qtxht5wighyii62n2nw72spfabzv6 onionjuggler-cli --auth-client --on --onion fe4avn4qtxht5wighyii62n2nw72spfabzv6dyqilokzltet4b2r4wqd.onion --client-priv-key UBVCL52FL6IRYIOLEAYUVTZY3AIOMDI3AIFBAALZ7HJOHIJFVBIQ ``` -**--auth-client --off** **--onion** [*ONION1*,*ONION2*,*...*] +**--auth-client --off** **--onion** <*ONION1*,*ONION2*,*...*> : Deauthenticate from a remote onion serivce. Remove the $ONION.auth_private file from ClientOnionAuthDir. File(s) modified: ClientOnionAuthDir/. ``` @@ -234,14 +232,14 @@ onionjuggler-cli --auth-client --off --onion fe4avn4qtxht5wighyii62n2nw72spfabzv onionjuggler-cli --auth-client --list ``` -**--web --on** **--service** [*SERV*] **--folder** [*FOLDER*] +**--web --on** **--service** <*SERV*> **--folder** <*FOLDER*> : Enable a website using a specific onion service by creating a configuration file inside the web server folder by default, the folder name is to be considered the wanted folder inside website_dir variable defined on /etc/onionservice.conf. If the path starts with forward slash "/" or tilde and slash "~/", that path will be considered instead. File(s) modified: "${webserver_conf}". ``` onionjuggler-cli --web on nextcloud nextcloud-local-site ``` -**--web --off** **--service** [*SERV*] +**--web --off** **--service** <*SERV*> : Disable a website from a specific onion service by removing its configuration file from the webserver folder. File(s) modified: $webserver_conf ``` @@ -255,16 +253,7 @@ onionjuggler-cli --web --off --service nextcloud onionjuggler-cli --web --list ``` -**--location** **--service** [*SERV*] [*--nginx*|*--apache2*|*--html*] - -: Guide to add onion location to your plainnet website when using the webserver Nginx or Apache2 or an HTML header. It does not modify any configuration by itself, the instructions to do so are send to stdout. File(s) modified: none. -``` -onionjuggler-cli --location --service nextcloud --nginx -onionjuggler-cli --location --service nextcloud --apache2 -onionjuggler-cli --location --service nextcloud --html -``` - -**backup** [*--create*|*--integrate*] +**--backup** [*--create*|*--integrate*] : Backup all of the torrc, DataDir/services and ClientOnionAuthDir either by creating a backup file or integrating to the system from a backup made before. File(s) modified: torrc, DataDir/services, ClientOnionAuthDir. ``` @@ -297,8 +286,9 @@ onionjuggler-cli help **-R**, **--restart**, **-r**, **--reload** : Signal tor daemon to restart or reload after the CLI edits tor's configuration files. (Default: reload) -**-C**, **--config** -: Specify and alternative configuration file to override default configuration. +**-G**, **--plugin** <*PLUGIN*> + +: Run an onionjuggler plugin. Requires the plugin to be installed with the name *onionjuggler-cli-\**. # FILES @@ -312,19 +302,12 @@ onionjuggler-cli help : Local configuration files that overrrite the default one. -# ENVIRONMENT - -**ONIONJUGGLER_CONF** - -: The environmental variable will override all previous options. - - # EXIT VALUE **0** : Success -**1** +**>0** : Fail diff --git a/man/onionjuggler-tui.1.md b/man/onionjuggler-tui.1.md index 99fd6ef..57777d5 100644 --- a/man/onionjuggler-tui.1.md +++ b/man/onionjuggler-tui.1.md @@ -10,7 +10,6 @@ onionjuggler-tui - OnionJuggler Terminal User Interface, also known as the *onio # SYNOPSIS **onionjuggler-tui** **command** [**--option**<=*ARGUMENT*>]\ -**onionjuggler-tui** [**--config** *ONIONJUGGLER_CONF*]\ **onionjuggler-tui** **--help** # DESCRIPTION @@ -24,17 +23,8 @@ onionjuggler-tui - OnionJuggler Terminal User Interface, also known as the *onio : Display a short help message and exit. -**-C**, **--config** *ONIONJUGGLER_CONF* - -: Specify and alternative configuration file to override default configuration. - - # FILES -**/usr/local/bin/onionjuggler-cli** - -: OnionJuggler TUI will call the CLI to execute the tasks after the dialog options have been selected. - **/etc/onionjuggler/dialogrc** : Default dialog run commands file. @@ -42,10 +32,6 @@ onionjuggler-tui - OnionJuggler Terminal User Interface, also known as the *onio # ENVIRONMENT -**ONIONJUGGLER_CONF** - -: The environmental variable will override all previous options. - **SUDO_EDITOR**, **DOAS_EDITOR**, **VISUAL**, **EDITOR** : Use environment variables in the above order to define the editor, in case any are empty, fallback to the next. If every variable is empty, fallback to Vi(1). diff --git a/man/onionjuggler.conf.5.md b/man/onionjuggler.conf.5.md index 220876a..ab4839e 100644 --- a/man/onionjuggler.conf.5.md +++ b/man/onionjuggler.conf.5.md @@ -9,7 +9,8 @@ onionjuggler.conf - Configuration file for OnionJuggler # DESCRIPTION -**onionjuggler.conf** is the configuration for for OnionJuggler, a combination of POSIX compliant scripts helps the interaction with onion service configuration and files to speed up usage and avoid misconfiguration. The system variables are defined by the environment variable *ONIONJUGGLER_CONF*, but if it is empty, will read */etc/onionjuggler/onionjuggler.conf*. The configuration file is then sourced be used by the program. It defines where the hidden services are located, the owner of the DataDir folder, the ControlPort to be used. +**onionjuggler.conf** is the configuration for for OnionJuggler, a combination of POSIX compliant scripts helps the interaction with onion service configuration and files to speed up usage and avoid misconfiguration. The system variables are defined by the default configuration file */etc/onionjuggler/onionjuggler.conf*. The configuration file is then sourced be used by the program. It defines where the hidden services are located, the owner of the DataDirectory older, the ControlPort to be used. +Variables defined inside _/etc/onionjuggler/conf.d/*.conf_ are parsed in lexical order and overwrite the default configuration. The configuration file is parsed by the shell and interpreted as variables. When assigning a value to a variable, use double quotes to avoid word splitting: **variable**=*"value"*. @@ -106,12 +107,6 @@ Variables set to and empty string, either *var=* or *var=""*, will run with defa : Specify default HiddenServicePort target address. Useful for when the server is running on an external host related to the tor process. On Qubes-Whonix, you should set the Whonix Workstation Qubes IP address (Default: 127.0.0.1). -# ENVIRONMENT - -**ONIONJUGGLER_CONF** - -: Use the environment variable to search for the configuration file, if the variable is empty, use the default confiugration on */etc/onionjuggler/onionjuggler.conf*. - # FILES **/etc/onionjuggler/onionjuggler.conf** diff --git a/usr/bin/onionjuggler-cli b/usr/bin/onionjuggler-cli index 097d8d9..03845e5 100755 --- a/usr/bin/onionjuggler-cli +++ b/usr/bin/onionjuggler-cli @@ -5,26 +5,14 @@ ## Lines that begin with "## " try to explain what's going on. Lines ## that begin with just "#" are disabled commands. +## script name me="${0##*/}" -## colors -nocolor="\033[0m" -bold="\033[1m" -#nobold="\033[22m" -underline="\033[4m" -nounderline="\033[24m" -red="\033[31m" -green="\033[32m" -yellow="\033[33m" -blue="\033[34m" -magenta="\033[35m" -cyan="\033[36m" -get_intr="$(stty -a | sed -n '/.*intr = / {s///;s/;.*$//;p;}')" - -## display error message with instructions to use the script correctly. -#notice(){ printf %s"${me}: ${1}\n" 1>&2; } -notice(){ printf %s"${1}\n" 1>&2; } -error_msg(){ notice "${red}error: ${1}${nocolor}"; exit 1; } +onionjuggler_defaults="/usr/share/onionjuggler/defaults.sh" +[ -e "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} does not exist"; exit 1; } +[ -f "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} is not a regular file"; exit 1; } +[ -r "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} can not be read"; exit 1; } +. "${onionjuggler_defaults}" usage(){ printf %s"${magenta} @@ -58,7 +46,7 @@ _@v'':i''L,'''''_{'_'y''kJu@6jjuuuujjjN@- :jWkmycjl^:FxPXQ@@kjSEDQQe; -;vmdgN@@Q@@@QROsev^- ${nocolor} -Usage: ${me} command [--option ] +Usage: ${me} [--option ] \nComplete options: --activate [--service ] [--socket ] [--version <3>] [--port ] enable a service listening with tcp sockets @@ -80,7 +68,7 @@ Usage: ${me} command [--option ] --auth-client --list list your keys as a client --web --on [--service ] [--folder ] start serving a website for certain service and its folder --web --on [--service ] [--folder ] [--port ] [--no-check-service] - useful for workstations when the tor process is running on the gateway + useful for workstations when the tor process is running on the gateway --web --off [--service ] stop serving a website for certain service and its folder --web --list list enabled websites --location [--nginx|--apache2|--html] [--service ] onion-location guide, no execution @@ -122,26 +110,24 @@ Options: -f, --off deactivate website to an onion service -l, --list list active websites -w, --folder specify website files - --location guide to add onion-location to redirect users to your onion domain - -s, --service indicate service to see onion-location string - --nginx Nginx webserver header for onion-location - --apache2 Apache2 webserver header for onion-location - --html HTML header for onion-location + -p, --port use specified port. Useful for Workstations + --no-check-service don't check if service exists. Useful for Workstations --backup complete backup of onion services -M, --make make a backup -I, --integrate integrate backup to your system --vanguards manage Vanguards protection - -n, --on install Vanguards addon, if already installed, upgrade. + -n, --on install Vanguards addon, if already installed, upgrade -f, --off remove Vanguards -l, --list see Vanguards logs Advanced: - -C, --config specify an alternative onionjuggler.conf to be parsed - -R, --restart signal tor to restart when the daemon status is failed or inactive - -r, --reload signal tor to reload (default option) + -G, --plugin specify a plugin (onionjuggler-cli-*) to be parsed, example: web + -R, --restart signal tor to restart if the option ever signal tor + -r, --reload signal tor to reload (default option) if the option ever signal tor Option names: - main activate, deactivate, info, renew, auth-server, auth-client, web, location, vanguards + main activate, deactivate, info, renew, auth-server, auth-client, web, vanguards + plugin auth-server, auth-client, web, vanguards status --on, --off, --list action --purge, --quiet, --nginx, --apache2, --html, --make, --integrate signal --restart, --reload @@ -159,76 +145,29 @@ If your services are unreacheable, restart tor. exit 1 } -[ -z "${1}" ] && usage ######################## #### OPTION PARSING #### -## this getopts might seem complex, so check this template -## https://github.com/nyxnor/scripts/blob/master/getopts.sh - -## check if argument is within range -## usage: -## $ range_arg key "1-5" -## $ range_arg key "1" "2" "3" "4" "5" -## $ range_arg key "a-cA-C" -## $ range_arg key "a" "b" "c" "A" "B" "C" -range_arg(){ - list="${*}" - eval var='$'"${1}" - range="${list#"${1} "}" - if [ -n "${var:-}" ]; then - success=0 - for tests in ${range}; do - ## it needs to expand for ranges 'a-z' to be evaluated, and not considered as a value to be used - # shellcheck disable=SC2295 - [ "${var%%*[^${tests}]*}" ] && success=1 - done - ## if not withing range, fail and show the fixed range that can be used - [ ${success} -ne 1 ] && error_msg "Option '${opt_orig}' can not be '${var}'! It can only be: ${range}." - fi -} - -## if option requires argument, check if it was provided, if yes, assign the arg to the opt -## $arg was already assigned, and if valid, will use it for the key value -## usage: get_arg key -get_arg(){ - ## if argument is empty or starts with '-', fail as it possibly is an option - case "${arg}" in ""|-*) error_msg "Option '${opt_orig}' requires an argument.";; esac - ## assign - value="${arg}" - ## Escaping quotes is needed because else it will fail if the argument is quoted - # shellcheck disable=SC2140 - eval "${1}"="\"${value}\"" - - ## shift positional argument two times, as this option demands argument, unless they are separated by equal sign '=' - ## shift_n default value was assigned when trimming hifens '--' from the options - ## if shift_n is equal to zero, '--option arg' - ## if shift_n is not equal to zero, '--option=arg' - [ -z "${shift_n}" ] && shift_n=2 -} +## Save all options +args_full="${*}" ## hacky getopts ## accepts long (--option) and short (-o) options ## accept argument assignment with space (--option arg | -o arg) or equal sign (--option=arg | -o=arg) +[ -z "${1}" ] && usage while :; do - ## '--option=value' should shift once and '--option value' should shift twice - ## but at this point it is not possible to be sure if option requires an argument - ## reset shift to zero, at the end, if it is still 0, it will be assigned to one - ## has to be zero here so we can check later if option argument is separated by space ' ' or equal sign '=' + [ -z "${1}" ] && break ## options ended shift_n="" opt_orig="${1}" ## save opt orig for error message to understand which opt failed - case "${opt_orig}" in - --) shift 1; break;; ## stop option parsing - --*=*) opt="${1%=*}"; opt="${opt#*--}"; arg="${1#*=}"; shift_n=1;; ## long option '--sleep=1' - -*=*) opt="${1%=*}"; opt="${opt#*-}"; arg="${1#*=}"; shift_n=1;; ## short option '-s=1' - --*) opt="${1#*--}"; arg="${2}";; ## long option '--sleep 1' - -*) opt="${1#*-}"; arg="${2}";; ## short option '-s 1' - "") break;; ## options ended - *) usage;; ## not an option - esac + # shellcheck disable=SC2034 + arg_possible="${2}" ## need to pass the second positional parameter because maybe it is an argument + clean_opt "${1}" || break + # shellcheck disable=SC2034 case "${opt}" in - activate|deactivate|info|renew|auth-server|auth-client|web|location|backup|vanguards) main="${opt}";; + activate|deactivate|info|renew) main="${opt}";; + auth-server|auth-client|web|location|backup|vanguards) plugin="${opt}";; + G|plugin|G=*|plugin=*) get_arg plugin;; getopt|getopts|getconf) dev="${opt}";; on|off|list|n|f|l) status="${opt}";; R|restart|r|reload) signal="${opt}";; @@ -243,7 +182,6 @@ while :; do w|folder|w=*|folder=*) get_arg folder;; k|client-priv-key|k=*|client-priv-key=*) get_arg client_priv_key;; K|client-pub-key|K=*|client-pub-key=*) get_arg client_pub_key;; - C|config|C=*|config=*) get_arg ONIONJUGGLER_CONF;; no-check-service) no_check_service=1;; h|help) usage;; *) error_msg "Invalid option: '${opt_orig}'";; @@ -251,340 +189,9 @@ while :; do ## shift as many times as demanded ## if empty, shift at least once to pass to next option shift "${shift_n:-1}" + [ -z "${1}" ] && break done -################### -#### VARIABLES #### - -## 1. source default configuration file first -## 2. source local (user made) configuration files to override the default values -## 3. source the ONIONJUGGLER_CONF specified by the cli argument and if it empty, use the environment variable -[ ! -f /etc/onionjuggler/onionjuggler.conf ] && error_msg "Default configuration file not found: /etc/onionjuggler/onionjuggler.conf" -[ -r /etc/onionjuggler/onionjuggler.conf ] && . /etc/onionjuggler/onionjuggler.conf -for file in /etc/onionjuggler/conf.d/*.conf; do [ -f "${file}" ] && . "${file}"; done -[ -r "${ONIONJUGGLER_CONF}" ] && . "${ONIONJUGGLER_CONF}" - -## : ${var:="value"} -> initialize the variable (SC2154) and if empty or unset, use default values -## var=${var%*/} -> removes the trailing slash "/" at the end of directories variables - -## system -: "${su_cmd:="sudo"}" -: "${openssl_cmd:="openssl"}" -: "${webserver:="nginx"}" -: "${webserver_conf:="/etc/nginx/sites-enabled"}" -: "${website_dir:="/var/www"}"; website_dir="${website_dir%*/}" -: "${vanguards_commit:="10942de93f6578f8303f60014f34de2fca345545"}" - -## tor defaults -: "${daemon_control:="systemctl"}"; daemon_control="${daemon_control%*/}" -: "${tor_daemon:="tor@default"}" -: "${tor_user:="debian-tor"}" -: "${tor_conf_user_group:="root:root"}" -: "${tor_data_dir:="/var/lib/tor"}"; tor_data_dir="${tor_data_dir%*/}" -: "${tor_data_dir_services:="${tor_data_dir}/services"}"; tor_data_dir_services="${tor_data_dir_services%*/}" -: "${tor_data_dir_auth:="${tor_data_dir}/onion_auth"}"; tor_data_dir_auth="${tor_data_dir_auth%*/}" -: "${tor_conf_dir:="/etc/tor"}"; tor_conf_dir="${tor_conf_dir%*/}" -: "${tor_conf:="${tor_conf_dir}/torrc"}" -: "${tor_control_port:="9051"}" ## only the port, not the host -: "${tor_backup_dir:="${HOME}/.onionjuggler/backup"}"; tor_backup_dir="${tor_backup_dir%*/}" -: "${tor_hiddenserviceport_target_addr:="127.0.0.1"}" - -################### -#### FUNCTIONS #### - -## Elegantly modify files on a temporary directory. Test the configuration with another function. -## If correct, then save file back to its original location. This avoids running with an invalid -## configuration that can make a daemon fail to reload or even start -## Limitation is file name cannot start with a number. -## $ safe_edit tmp variable -## $ safe_edit tmp tor_conf -## modify the "${tor_conf_tmp}" -## use the daemon option to verify config -f "${tor_conf_tmp}" -## $ safe_edit save tor_conf -safe_edit(){ - [ -w "${TMPDIR:="/tmp"}" ] || export TMPDIR="~" - TMPDIR="${TMPDIR%*/}" - key="${2}" - eval file="$(printf '%s\n' '$'"${key}")" - case "${1}" in - tmp) - file_name_tmp="$(mktemp "${TMPDIR}/${file##*/}.XXXXXX")" - notice "Saving a copy of ${file} to ${file_name_tmp}" - chown "${tor_conf_user_group}" "${file_name_tmp}" - cp "${file}" "${file_name_tmp}" - ## assign variable_tmp - eval "${key}"_tmp="${file_name_tmp}" - # shellcheck disable=SC2064 - trap "printf %s\"Exiting script ${me}\nDeleting ${file_name_tmp}\n\"; rm -f ${file_name_tmp}" EXIT INT TERM - ;; - save) - ## get variable_tmp file - eval file_name_tmp='$'"${key}_tmp" - if cmp -s "${file_name_tmp}" "${file}"; then - notice "File ${file_name_tmp} do not differ from ${file}" - notice "Not writing back to original location.${nocolor}" - rm -f "${file_name_tmp}" - else - notice "Moving ${file_name_tmp} back to its original location ${file}" - mv "${file_name_tmp}" "${file}" - fi - ;; - esac -} - - -## Verify tor configuration of the temporary file and if variable is empty, use the main configuration, if wrong, exit. -verify_config_tor(){ - config="${tor_conf_tmp:-"${tor_conf}"}" - ## if User is set on the config, then run tor as root - "${su_cmd}" grep -q "^User" "${config}" && su_tor_cmd="${su_cmd}" - ## user may not be on this config, but on another, so run tor as its user if $su_tor_cmd is empty - : "${su_tor_cmd:="${su_cmd} -u ${tor_user}"}" - notice "Verifying tor configuration file ${config}" - ! ${su_tor_cmd} tor -f "${config}" --verify-config --hush && error_msg "aborting: configuration is invalid" - notice "${green}Configuration OK${nocolor}" - [ -n "${tor_conf_tmp}" ] && safe_edit save tor_conf -} - - -## TODO: vinculate with verify_config_tor() -## TODO: parse this with the modified file and without the original one -## get files tor will read -read_tor_files(){ - if test -f /lib/systemd/system/tor@default.service; then - tor_start_command="$(grep "ExecStart=" /lib/systemd/system/tor@default.service | sed "s/ExecStart=//")" - elif test -f /lib/systemd/system/tor.service; then - tor_start_command="$(grep "ExecStart=" /lib/systemd/system/tor.service | sed "s/ExecStart=//")" - fi - tor_verify_config_output="$(${tor_start_command:="tor"} --verify-config)" - tor_config_files="$(printf '%s\n' "${tor_verify_config_output}" | grep -E " Read configuration file [^ ]*| Including configuration file [^ ]*" | awk '{print $NF}' | sed "s/\"//;s/\".//;s/\/\//\//" | tr "\n" " ")" -} - - -## set correct permissions for tor directories and files -## find helps do the job because it can segreggate directories from files -set_owner_permission(){ - ## data - chown -R "${tor_user}:${tor_user}" "${tor_data_dir}" - find "${tor_data_dir}" -type d -exec chmod 700 {} \; - find "${tor_data_dir}" -type f -exec chmod 600 {} \; - ## conf - chown -R "${tor_conf_user_group}" "${tor_conf_dir}" - find "${tor_conf_dir}" -type d -exec chmod 755 {} \; - find "${tor_conf_dir}" -type f -exec chmod 644 {} \; - -} - - -# reloads tor by default or forces to restart if $1 is not empty -# shellcheck disable=SC2120 -signal_tor(){ - verify_config_tor - set_owner_permission - ## default signal is to reload, but if restart was specified, use it - : "${signal:="reload"}" - [ "${signal}" = "r" ] && signal="reload" - [ "${signal}" = "R" ] && signal="restart" - printf "\n" - notice "${signal}ing tor, please be patient." - notice "Process hanged? Press (${get_intr}) to abort and maintain previous configuration." - case "${daemon_control}" in - systemctl|sv|rcctl) "${daemon_control}" "${signal}" "${tor_daemon}";; - service) "${daemon_control}" "${tor_daemon}" "${signal}";; - /etc/rc.d) "${daemon_control}"/"${tor_daemon}" "${signal}";; - *) error_msg "daemon_control value not supported: ${daemon_control}" - esac - [ "${?}" -eq 1 ] && error_msg "Failed to ${signal} tor. Check logs first, correct the problem them restart tor." - notice "${green}${signal}ed tor succesfully!${nocolor}" - printf "\n" -} - - -## check if variable is integer -is_integer(){ printf %d "${1}" >/dev/null 2>&1 || error_msg "Not an integer: ${1}" ; } - - -## checks if the target is valid. -## Address range from 0.0.0.0 to 255.255.255.255. Port ranges from 0 to 65535 -## this is not perfect but it is better than nothing -is_addr_port(){ - addr_port="${1}" - port="${addr_port##*:}" - addr="${addr_port%%:*}" - - printf %d "${port}" >/dev/null 2>&1 || error_msg "'${port}' is not a valid port, not an integer" - { [ "${port}" -gt 0 ] && [ "${port}" -le 65535 ]; } || \ - error_msg "${port} is not a valid port, not within range: 0-65535" - - for quad in $(printf '%s\n' "${addr}" | tr "." " "); do - printf %d "${quad}" >/dev/null 2>&1 || error_msg "${addr} is not a valid address, ${quad} is not and integer" - { [ "${quad}" -ge 0 ] && [ "${quad}" -le 255 ]; } || \ - error_msg "${addr} is not a valid address, ${quad} is not within range: 0-255" - done -} - - -## returns 1 if is not empty -## no better way to do with posix utilities -check_folder_is_not_empty(){ - dir="${1}" - if [ -d "${dir}" ] && files=$(ls -qAH -- "${dir}") && [ -z "${files}" ]; then - return 1 - else - return 0 - fi -} - - -is_service_dir_empty(){ - check_folder_is_not_empty "${tor_data_dir_services}" || error_msg "Onion services directory is empty. Create a service first before running this command again." -} - - -## test if service exists to continue the script or output error logs. -## if the service exists, will save the hostname for when requested. -test_service_exists(){ - service="${1}" - onion_hostname=$(grep ".onion" "${tor_data_dir_services}"/"${service}"/hostname 2>/dev/null) - [ -z "${onion_hostname}" ] && error_msg "Service does not exist: ${service}" -} - - -## save the clients names that are inside the /authorized_clients/ in list format (CLIENT1,CLIENT2,...) -create_client_list(){ - service="${1}" - client_name_list="" - for client_listed in "${tor_data_dir_services}/${service}/authorized_clients"/*; do - client_listed="${client_listed##*/}" - [ "${client_listed}" = "*" ] && break - client_listed="${client_listed%*.auth}" - client_name_list="$(printf '%s\n%s\n' "${client_name_list}" "${client_listed}")" - done - [ -n "${client_name_list}" ] && client_name_list="$(printf '%s\n' "${client_name_list}" | tr "\n" "," | sed "s/\,$//" | sed "s/^,//")" - client_count="" - # shellcheck disable=SC2086 - [ -n "${client_name_list}" ] && client_count="$(IFS=','; set -f -- ${client_name_list}; printf %s"${#}")" -} - - -## save the service names that have a in list format (SERV1,SERV2,...) -create_service_list(){ - for hs in "${tor_data_dir_services}"/*; do - hs="${hs##*/}" - service_name_list="$(printf '%s\n' "${service_name_list}" "${hs}")" - done -} - -## loops the parameters -## $1 must be the function to loop -## $2 normally is service, but can be any other parameter (accepts list -> SERV1,SERV2,...) -## $3 normally is client, but can be any other (accepts list -> client1,client2...) -## $ loop_list function_name ssh,xmpp,web [alice,bob] -loop_list(){ - for item in $(printf %s"${2}" | tr "," " "); do - case "${3}" in - "") "${1}" "${item}";; - *) for subitem in $(printf %s"${3}" | tr "," " "); do "${1}" "${item}" "${subitem}"; done;; - esac - done -} - -## https://github.com/koalaman/shellcheck/wiki/SC3050 -escape_printf_percent() { printf "%s\n" "$(printf '%s' "${1}" | sed "s/\%/\%/g")"; } - - -## TODO: find a better way to handle commented lines and empty lines -## the problem is that the script only stop at the next HiddenServiceDir, -## but discard every line not starting with HiddenServiceDir -## https://github.com/nyxnor/onionjuggler/issues/51 -service_block(){ - process="${1}" - service="${2}" - file="${3:-"${tor_conf_tmp}"}" - i=0 - ## print the exact match HiddenServiceDir of the requested service that must end with the service name or with "/", also prit n lines below it - match="HiddenServiceDir ${tor_data_dir_services}/${service}" - hs_found="" - hs_lines_delete="" - while IFS="$(printf '\n')" read -r line; do - [ -z "${hs_found}" ] && printf '%s\n' "${line}" | grep -q -E "^${match}$|^${match}/$" && hs_found="1" - if [ "${hs_found}" = "1" ]; then - i=$((i+1)) - case "${line}" in - "HiddenServiceStatistics"*) :;; ## relays only - "HiddenService"*) - ## break on next HiddenService configuration - { [ ${i} -gt 1 ] && [ "${line%% *}" = "HiddenServiceDir" ]; } && break - case "${process}" in - print|printf) printf '%s\n' "${line}";; - delete) - ## delete only works if hs lines are consecutive, - ## meaning no blank lines or commented lines between the wanted hs - if [ -z "${hs_lines_delete}" ]; then - hs_lines_delete="$(printf '%s\n' "${line}")" - else - hs_lines_delete="$(printf '%s\n%s\n' "${hs_lines_delete}" "${line}")" - fi - ;; - esac - ;; - esac - fi - done < "${file}" - - if [ -n "${hs_lines_delete}" ]; then - ## sed is a stream line editor, so lets make the file a single line transforming new lines to carriage return - hs_lines_delete="$(printf '%s\n' "${hs_lines_delete}" | tr "\n" "\r")" - ## then convert the file also as done above, so sed can see the file and pattern on the same format - tr "\n" "\r" < "${file}" | sed "s|${hs_lines_delete}||" | tr "\r" "\n" | tee tmpfile >/dev/null - mv tmpfile "${file}" - fi -} - -## TODO: finish: https://github.com/nyxnor/onionjuggler/issues/32 -httpd_service_block(){ - process="${1}" - service="${2}" - file="${3:-"/etc/httpd.conf"}" - i=0 - test_service_exists "${service}" - grep -A 10 "server \"${onion_hostname}\"" "${file}" | while IFS= read -r line; do - case "${process}" in - print|printf) escape_printf_percent "${line}";; - delete) [ -n "${line}" ] && sed -i'' "s|${line}||" "${file}";; - esac - escape_printf_percent "${line}" | grep -q "^}" && break - done - cat_squeeze_blank "${file}" -} - -## http://sed.sourceforge.net/local/docs/emulating_unix.txt -## tac is not posix -tac(){ - sed '1!G;h;$!d' "${1}" -} - -## 'cat -s' is not posix -cat_squeeze_blank(){ - while :; do - case "${1}" in - "/"*|[[:alnum:]]*) files="${files} ${1}"; shift;; ## only consider path starting with "/" or alphanumeric - *) break;; ## made to break on pipes and everything else - esac - done - # shellcheck disable=SC2086 - sed '1s/^$//p;/./,/^$/!d' ${files} -} - -## error_msg self explanatory, tor breaks with special chars on the dir name -check_service_name(){ - [ "${service%%*[^a-zA-Z0-9_.-]*}" ] || { - error_msg "Service name \"${service}\" is invalid\nIt must only contain the characters that are: - - letters (a-z A-Z) - - numbers (0-9) - - punctuations limited to hifen (-), underscore (_), dot (.)" - } -} ########################### ########## MAIN ########### @@ -594,7 +201,7 @@ case "${dev}" in ## execute or modify nothing, just print the configuration values ## usefult to see if there is nothing messed up and if there is, can be checked before running the cli. getconf) - for key in ONIONJUGGLER_CONF su_cmd openssl_cmd webserver webserver_conf website_dir vanguards_commit \ + for key in su_cmd openssl_cmd webserver webserver_conf website_dir vanguards_commit \ tor_daemon tor_user tor_conf_dir tor_conf_user_group tor_conf tor_data_dir tor_data_dir_services tor_data_dir_auth \ tor_control_port tor_backup_dir tor_hiddenserviceport_target_addr; do eval val='$'"${key}" @@ -606,7 +213,7 @@ case "${dev}" in ## only print the options given on the command line, mostly for development purposes to check the argument ## but can be useful to see if the command is correct before running it. getopt|getopts) - for key in signal main service client onion status action version socket port folder \ + for key in signal main plugin service client onion status action version socket port folder \ client_priv_key client_pub_key gateway_service; do eval val='$'"${key}" [ -n "${val}" ] && printf '%s\n' "${key}=\"${val}\"" @@ -618,41 +225,24 @@ esac ## user option [ "$(id -u)" -ne 0 ] && error_msg "run as root" -case "${main}" in - - ## disable a service by removing service torrc's block. - ## it is raw, services variables should be separated by an empty line per service, else you might get other non-related configuration deleted. - ## purge is optional, it deletes the - ## will not check if folder or configuration exist, this is cleanup mode - ## will not use '@all'. Purge is dangerous, purging all service is even more dangerous. Always backup. - deactivate) - [ -z "${service}" ] && usage - delete_service(){ - service="${1}" - printf "\n" - ## remove service service data - case "${action:-}" in - purge|P) - notice "${red}Deleting HiddenServiceDir ${underline}${tor_data_dir_services}/${service}${nocolor}" - rm -rfv "${tor_data_dir_services:?}"/"${service:?}" - ;; - *) notice "${yellow}Keeping HiddenServiceDir ${underline}${tor_data_dir_services}/${service}${nocolor}";; - esac - ## remove service paragraph in torrc - notice "Deleting HiddenService configuration in ${underline}${tor_conf_tmp}${nounderline}" - service_block delete "${service}" "${tor_conf_tmp}" - ## substitute multiple sequential empty lines to a single one per sequence - cat_squeeze_blank "${tor_conf_tmp}" | tee "${tor_conf_tmp}".tmp >/dev/null && mv "${tor_conf_tmp}".tmp "${tor_conf_tmp}" - notice "Disabled service: ${bold}${service}${magenta}${nocolor}" - } - safe_edit tmp tor_conf - loop_list delete_service "${service}" - printf "\n" - signal_tor - notice "${green}done!${nocolor}" - ;; +## plugins +## requires script named "onionjuggler-cli-*" +if [ -n "${plugin}" ]; then + plugin="${plugin##*onionjuggler-cli-}" + script_plugin="onionjuggler-cli-${plugin}" + ## check if plugin is installed + has "${script_plugin}" || error_msg "${script_plugin} is not installed" + ## clean uncessary options to parse on plugin + args_plugin_clean="$(printf '%s\n' "${args_full}" | sed "s|--plugin [^ ]*||;s|-G [^ ]*||" | tr " " "\n" | sed "s|^--plugin=.*$||;s|^-G=.*$||;s|^--${plugin}$||" | tr "\n" " " | tr -s " ")" + # shellcheck disable=SC2086 + "${script_plugin}" ${args_plugin_clean} + ## exit with the same exit code from the plugin script + exit "${?}" +fi +## main options +case "${main}" in ## enable a service by configure its own torrc's block, consequentially the will be created. ## tcp-socket uses addr:port, which can be remote or localhost. It leaks onion address to the local network @@ -688,7 +278,7 @@ case "${main}" in notice "Service name = ${service}" notice "Service address = ${magenta}${onion_hostname}${nocolor}" notice "Virtual port = ${virtport}" - command -v qrencode >/dev/null && qrencode -m 2 -t ANSIUTF8 "${onion_hostname}" + has qrencode && qrencode -m 2 -t ANSIUTF8 "${onion_hostname}" } case "${socket}" in @@ -699,20 +289,19 @@ case "${main}" in ## Also, substitutes localhost:port for $target_ip_default:$port to make exact math for target always, as localhost and target_ip_default mean the same thing ## This measures avoid using the same local port for different services ## Sanity check - if test -f /usr/share/anon-gw-base-files/gateway; then ## Whonix Gateway - if [ "${gateway_service}" = "1" ]; then ## Service should use Gateway ip (127.0.0.1) - target_ip_default="127.0.0.1" - elif command -v qubesdb-read >/dev/null; then ## Qubes-Whonix - target_ip_default="${tor_hiddenserviceport_target_addr}" - else - target_ip_default=10.152.152.11 ## Non-Qubes-Whonix - fi - elif test -f /usr/share/anon-ws-base-files/workstation; then ## Whonix Workstation - error_msg "Create onion services on the Gateway, not on the Workstation" - else - target_ip_default="${tor_hiddenserviceport_target_addr:-127.0.0.1}" ## Common target - fi - + if test -f /usr/share/anon-gw-base-files/gateway; then ## Whonix Gateway + if [ "${gateway_service}" = "1" ]; then ## Service should use Gateway ip (127.0.0.1) + target_ip_default="127.0.0.1" + elif has qubesdb-read; then ## Qubes-Whonix + target_ip_default="${tor_hiddenserviceport_target_addr}" + else + target_ip_default="10.152.152.11" ## Non-Qubes-Whonix + fi + elif test -f /usr/share/anon-ws-base-files/workstation; then ## Whonix Workstation + error_msg "Create onion services on the Gateway, not on the Workstation" + else + target_ip_default="${tor_hiddenserviceport_target_addr:-127.0.0.1}" ## Common target + fi ## get ports port="$(printf %s"${port}" | tr " " "\n" | tr "," " " | tr -s " ")" @@ -736,7 +325,7 @@ EOF is_addr_port "${target}" ## Qubes-Whonix should have a different target for services aiming to the Workstation - if command -v qubesdb-read >/dev/null && [ "${target_addr}" = "127.0.0.1" ] && [ "${gateway_service}" != "1" ]; then + if has qubesdb-read && [ "${target_addr}" = "127.0.0.1" ] && [ "${gateway_service}" != "1" ]; then echo "1" | tee -a "${fail_log}" >/dev/null error_msg "Please set the Workstation Qube IP address using the option 'tor_hiddenserviceport_target_addr' on /etc/onionjuggler/conf.d/*.conf" fi @@ -773,7 +362,7 @@ EOF printf '%s\n' "${port}" | while IFS="$(printf '\n')" read -r port_line; do IFS=" " read -r virtport <<-EOF $(printf '%s\n' "${port_line}") - EOF +EOF [ -z "${virtport}" ] && break ## use a key="-onion" on the target to facilitate discovering it later and distinction if there is a plain net site target="${unix_path}-${virtport}-onion.sock" @@ -805,228 +394,68 @@ EOF ;; - ## manage client authorization server side (HiddenServiceDir/authorized_clients/) or client side (ClientOnionAuthDir) - auth-server|auth-client) - host="${main#*-}" - [ -z "${status:-}" ] && usage - case "${host}" in - - server) - [ -z "${service}" ] && usage - [ "${service}" != "@all" ] && check_service_name - is_service_dir_empty - case "${status}" in - - ## as the onion service operator, make your onion authenticated by generating a pair or public and private keys, - ## the client pub key is automatically saved inside /authorized_clients/alice.auth - ## the client private key is shown in the screen and the key file deleted - ## the onion service operator should send the private key for the desired client - n|on) - [ -z "${client}" ] && usage - #printf "\n# Generating keys to access onion service (Client Authorization) ...\n" - auth_server_add(){ - service="${1}" - client="${2}" - test_service_exists "${service}" - ## Generate pem and derive pub and priv keys - "${openssl_cmd}" genpkey -algorithm x25519 -out /tmp/k1.prv.pem - grep -v " PRIVATE KEY" /tmp/k1.prv.pem | base64pem -d | tail -c 32 | base32 | sed "s/=//g" > /tmp/k1.prv.key - "${openssl_cmd}" pkey -in /tmp/k1.prv.pem -pubout | grep -v " PUBLIC KEY" | base64pem -d | tail -c 32 | base32 | sed "s/=//g" > /tmp/k1.pub.key - ## save variables - client_pub_key=$(cat /tmp/k1.pub.key) - client_priv_key=$(cat /tmp/k1.prv.key) - onion_hostname_without_onion="${onion_hostname%.onion}" - client_priv_key_config="${onion_hostname%.onion}:descriptor:x25519:${client_priv_key}" - client_pub_key_config="descriptor:x25519:${client_pub_key}" - # Server side configuration - printf %s"${client_pub_key_config}\n" | tee "${tor_data_dir_services}"/"${service}"/authorized_clients/"${client}".auth >/dev/null - ## Client side configuration - printf "<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>\n" - printf %s"service=${bold}${service}${nocolor}\n" - printf %s"client=${bold}${client}${nocolor}\n" - printf %s"onion_hostname=${bold}${magenta}${onion_hostname}${nocolor}\n" - printf %s"client_pub_key=${bold}${client_pub_key}${nocolor}\n" - printf %s"client_pub_key_config=${bold}${client_pub_key_config}${nocolor}\n" - printf %s"client_priv_key=${bold}${client_priv_key}${nocolor}\n" - printf %s"client_priv_key_config=${bold}${client_priv_key_config}${nocolor}\n" - printf "<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>\n" - ## Delete pem and keys - rm -f /tmp/k1.pub.key /tmp/k1.prv.key /tmp/k1.prv.pem - } - [ "${service}" = "@all" ] && { create_service_list ; service="${service_name_list}" ; } - [ "${client}" = "@all" ] && error_msg "Client name cannot be @all, it is a restricted wildcard for referring to all clients, not a name per se." - if [ -n "${client_pub_key}" ]; then - test_service_exists "${service}" - client_pub_key_config="descriptor:x25519:${client_pub_key}" - printf %s"${client_pub_key_config}" | tee "${tor_data_dir_services}"/"${service}"/authorized_clients/"${client}".auth >/dev/null - notice "\nServer side authorization configured\n" - printf %s" client_pub_key_config=${client_pub_key_config}\n" - notice "\nAs you inserted the public key manually, we expect that the client already has the private key" - else - loop_list auth_server_add "${service}" "${client}" - fi - signal_tor - ;; - - ## as the onion service operator, after making your onion service authenticated, you can also remove a specific client authorization - ## if no clients are present, the service will be available to anyone that has the onion service address - f|off) - - auth_server_remove_clients(){ - service="${1}" - client="${2}" - #notice "Service: ${service}" - if [ "${client}" = "@all" ]; then - rm -fv "${tor_data_dir_services}"/"${service}"/authorized_clients/*.auth - else - rm -fv "${tor_data_dir_services}"/"${service}"/authorized_clients/"${client}".auth - fi - } - - [ -z "${client}" ] && usage - [ -z "${service}" ] && error_msg "service is missing" - [ "${service}" != "@all" ] && check_service_name - - if [ "${service}" = "@all" ]; then - notice "Removing client authorization for:" - notice "Service: @all" - create_service_list; service="${service_name_list}" - if [ "${client}" = "@all" ]; then - notice "Clients: @all.\nThe service is now accessible for anyone with the onion address.\n" - else - notice "If any client remains, the service will still be authenticated." - fi - else - notice "Removing client authorization for:" - notice "Service: ${service}" - if [ "${client}" = "@all" ]; then - notice "Clients: @all.\nThe service is now accessible for anyone with the onion address.\n" - else - notice "If any client remains, the service will still be authenticated." - fi - fi - loop_list auth_server_remove_clients "${service}" "${client}" - printf "\n" - signal_tor - ;; - - l|list) - auth_server_list(){ - service="${1}" - test_service_exists "${service}" - create_client_list "${service}" - notice "\nService: ${service}" - if [ -n "${client_count}" ]; then - [ -n "${client_name_list}" ] && printf %s"Clients: ${client_name_list} (${client_count})\n" - for auth in "${tor_data_dir_services}/${service}/authorized_clients"/*; do - auth="${auth##*/}" - notice "${auth}: $(grep "descriptor:x25519:" "${tor_data_dir_services}"/"${service}"/authorized_clients/"${auth}")${nocolor}" - done - else - notice "Clients: NONE (0)" - fi - } - notice "${blue}Authorized clients for Hidden Services${nocolor}" - [ "${service}" = "@all" ] && { create_service_list; service="${service_name_list}"; } - loop_list auth_server_list "${service}" - ;; - - *) - error_msg "Invalid argument: status=${status}" - esac - ;; - - - client) - case "${status}" in - - ## as the onion service client, add a key given by the onion service operator to authenticate yourself inside ClientOnionAuthDir - ## The suffix '.auth_private' should not be mentioned, it will be automatically inserted when mentioning the name of the file. - ## private key format must be: :descriptor:x25519: - ## use the onion hostname as the file name, this avoid overriding the file by mistake and it indicates outside of the file for which service it refers to (of course it is written inside also) - ## adding to Tor Browser automatically not supported yet - n|on) - [ -z "${onion}" ] && usage - ## removes protocol such as http(s)://, ssh:// and git:// from the front of the address and trailing / at the end of the onion to clean it and only show the hostname (address.onion) - onion="$(printf %s"${onion}\n" | sed "s|.*://||" | sed "s|/$||")" - onion_hostname_without_onion="${onion%.onion}" - [ "${onion_hostname_without_onion%%*[^a-z2-7]*}" ] || error_msg "Onion domain is invalid, it is not within base32 alphabet lower-case encoding [a-z][2-7]" - [ "${#onion_hostname_without_onion}" = "56" ] || error_msg "Onion domain is invalid, LENGTH=${#onion} is different than 56 characters (<56-char-base32>.onion)" - safe_edit tmp tor_conf - read_tor_files - # shellcheck disable=SC2086 - grep -q "ClientOnionAuthDir" ${tor_config_files} || { printf %s"\nClientOnionAuthDir ${tor_data_dir_auth}\n\n" | tee -a "${tor_conf_tmp}"; } - "${su_cmd}" -u "${tor_user}" mkdir -p "${tor_data_dir_auth}" - if [ -z "${client_priv_key}" ]; then - ## Generate pem and derive pub and priv keys - "${openssl_cmd}" genpkey -algorithm x25519 -out /tmp/k1.prv.pem - grep -v "PRIVATE KEY" /tmp/k1.prv.pem | base64pem -d | tail -c 32 | base32 | sed 's/=//g' > /tmp/k1.prv.key - "${openssl_cmd}" pkey -in /tmp/k1.prv.pem -pubout | grep -v "PUBLIC KEY" | base64pem -d | tail -c 32 | base32 | sed 's/=//g' > /tmp/k1.pub.key - ## save variables - client_pub_key=$(cat /tmp/k1.pub.key) - client_priv_key=$(cat /tmp/k1.prv.key) - client_priv_key_config="${onion_hostname_without_onion}:descriptor:x25519:${client_priv_key}" - client_pub_key_config="descriptor:x25519:${client_pub_key}" - ## Delete pem and keys - rm -f /tmp/k1.pub.key /tmp/k1.prv.key /tmp/k1.prv.pem - # Client side configuration - printf %s"${client_priv_key_config}\n" | tee "${tor_data_dir_auth}"/"${onion_hostname_without_onion}".auth_private >/dev/null - notice "${bold}Client side authorization configured${nocolor}" - notice "This is your private key, keep it safe, keep it hidden:" - notice "client_priv_key=${client_priv_key}" - notice "client_priv_key_config=${client_priv_key_config}" - notice "\n${bold}Now it depends on the service operator to authorize your client public key${nocolor}" - ## Server side configuration - notice "Send the public key and instructions to the onion service operator of ${onion}" - notice "client_pub_key=${client_pub_key}" - notice "client_pub_key_config=descriptor:x25519:${client_pub_key}" - else - client_priv_key_config="${onion_hostname_without_onion}:descriptor:x25519:${client_priv_key}" - printf %s"${client_priv_key_config}\n" | tee "${tor_data_dir_auth}"/"${onion_hostname_without_onion}".auth_private >/dev/null - notice "\n${bold}Client side authorization configured${nocolor}" - notice "As you inserted the private key manually, it ise expected that you have already sent/received the public key to/from the onion service operator" - notice "client_priv_key_config=${client_priv_key_config}" - fi - ;; - - ## as the onion service client, delete '.auth_private' files from ClientOnionAuthDir that are not valid or has no use anymore - f|off) - [ -z "${onion}" ] && usage - onion="$(printf %s"${onion}\n" | sed "s|.*://||" | sed "s|/.*$||")" - auth_client_remove(){ - onion="${1}" - notice "\n${red}Removing ${tor_data_dir_auth}/${onion}.auth_private${nocolor}" - rm -fv "${tor_data_dir_auth}"/"${onion}".auth_private - } - if ! check_folder_is_not_empty "${tor_data_dir_auth}"; then - loop_list auth_client_remove "${onion}" - else - error_msg "ClientOnionAuthDir is empty" - fi - ;; - - l|list) - if ! check_folder_is_not_empty "${tor_data_dir_auth}"; then - notice "ClientOnionAuthDir ${tor_data_dir_auth}" - for auth in "${tor_data_dir_auth}"/*; do - auth="${auth##*/}" - notice "\nFile name: ${bold}${auth}${nocolor}" - notice "Content: ${bold}$(grep "descriptor:x25519:" "${tor_data_dir_auth}"/"${auth}")${nocolor}" - done - printf "\n" - else - error_msg "ClientOnionAuthDir is empty" - fi - ;; + ## disable a service by removing service torrc's block. + ## it is raw, services variables should be separated by an empty line per service, else you might get other non-related configuration deleted. + ## purge is optional, it deletes the + ## will not check if folder or configuration exist, this is cleanup mode + ## will not use '@all'. Purge is dangerous, purging all service is even more dangerous. Always backup. + deactivate) + [ -z "${service}" ] && usage + delete_service(){ + service="${1}" + printf "\n" + ## remove service service data + case "${action:-}" in + purge|P) + notice "${red}Deleting HiddenServiceDir ${underline}${tor_data_dir_services}/${service}${nocolor}" + rm -rfv "${tor_data_dir_services:?}"/"${service:?}" + ;; + *) notice "${yellow}Keeping HiddenServiceDir ${underline}${tor_data_dir_services}/${service}${nocolor}";; + esac + ## remove service paragraph in torrc + notice "Deleting HiddenService configuration in ${underline}${tor_conf_tmp}${nounderline}" + service_block delete "${service}" "${tor_conf_tmp}" + ## substitute multiple sequential empty lines to a single one per sequence + cat_squeeze_blank "${tor_conf_tmp}" | tee "${tor_conf_tmp}".tmp >/dev/null && mv "${tor_conf_tmp}".tmp "${tor_conf_tmp}" + notice "Disabled service: ${bold}${service}${magenta}${nocolor}" + } + safe_edit tmp tor_conf + loop_list delete_service "${service}" + printf "\n" + signal_tor + notice "${green}done!${nocolor}" + ;; - *) - error_msg "Invalid argument: status=${status}" - esac - ;; - *) - error_msg "Invalid argument: host=${host}" - esac + ## show all the necessary information to access the service such as the hostname and the QR encoded hostname to scan for Tor Browser Mobile + ## show the clients names and quantity, as well as the service torrc's block + ## @all will read through all services folders and execute the commands + info) + is_service_dir_empty + [ -z "${service}" ] && usage + get_service_info(){ + service="${1}" + test_service_exists "${service}" + j=$((j+1)) + [ ${j} -eq 1 ] && printf "<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>\n" + ## save clients names that are inside /authorized_clients/ + create_client_list "${service}" + if [ "${action}" != "q" ] && [ "${action}" != "quiet" ]; then + has qrencode && qrencode -m 2 -t ANSIUTF8 "${onion_hostname}" + fi + notice "Address = ${bold}${magenta}${onion_hostname}${nocolor}" + notice "Service = ${bold}${service}${nocolor}" + [ -n "${client_name_list}" ] && notice "Clients = ${bold}${client_name_list} (${client_count})${nocolor}" + if grep -q -E "^HiddenServiceDir ${tor_data_dir_services}/${service}$|^HiddenServiceDir ${tor_data_dir_services}/${service}/$" "${tor_conf}"; then + notice "Status = ${bold}${green}active${nocolor}" && service_block print "${service}" "${tor_conf}" + else + notice "Status = ${bold}${yellow}inactive${nocolor}" + fi + printf "<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>\n" + } + j=0 + [ "${service}" = "@all" ] && { create_service_list; service="${service_name_list}"; } + loop_list get_service_info "${service}" ;; @@ -1072,418 +501,7 @@ EOF ;; - ## show all the necessary information to access the service such as the hostname and the QR encoded hostname to scan for Tor Browser Mobile - ## show the clients names and quantity, as well as the service torrc's block - ## @all will read through all services folders and execute the commands - info) - is_service_dir_empty - [ -z "${service}" ] && usage - get_service_info(){ - service="${1}" - test_service_exists "${service}" - j=$((j+1)) - [ ${j} -eq 1 ] && printf "<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>\n" - ## save clients names that are inside /authorized_clients/ - create_client_list "${service}" - if [ "${action}" != "q" ] && [ "${action}" != "quiet" ]; then - command -v qrencode >/dev/null && qrencode -m 2 -t ANSIUTF8 "${onion_hostname}" - fi - notice "Address = ${bold}${magenta}${onion_hostname}${nocolor}" - notice "Service = ${bold}${service}${nocolor}" - [ -n "${client_name_list}" ] && notice "Clients = ${bold}${client_name_list} (${client_count})${nocolor}" - if grep -q -E "^HiddenServiceDir ${tor_data_dir_services}/${service}$|^HiddenServiceDir ${tor_data_dir_services}/${service}/$" "${tor_conf}"; then - notice "Status = ${bold}${green}active${nocolor}" && service_block print "${service}" "${tor_conf}" - else - notice "Status = ${bold}${yellow}inactive${nocolor}" - fi - printf "<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>\n" - } - j=0 - [ "${service}" = "@all" ] && { create_service_list; service="${service_name_list}"; } - loop_list get_service_info "${service}" - ;; - - - ## start serving files with a webserver for a specific onion service and specific website folder - web) - [ -z "${status}" ] && usage - if [ "${webserver}" != "nginx" ] && [ "${webserver}" != "apache2" ] && [ "${webserver}" != "openbsd-httpd" ]; then - error_msg "webserver can be either 'nginx' or 'apache2', not '${webserver}'" - fi - - reload_webserver(){ - notice "\nReloading web server to apply new configuration" - case "${webserver}" in - nginx) nginx -t && nginx -s reload;; - apache2) apache2 -t && apache2 -k graceful;; - openbsd-httpd) - openbsd_httpd_test="$(httpd -n -f "${webserver_conf}" 2>&1)" - if [ "${openbsd_httpd_test}" = "no actions, nothing to do" ] || [ "${openbsd_httpd_test}" = "configuration OK" ]; then - rcctl reload httpd - fi - ;; - esac - [ "${?}" -eq 1 ] && error_msg "Failed to reload ${webserver}, you must restart it manually before running this script again." - } - - case "${status}" in - - n|on) - { [ -z "${service}" ] || [ -z "${folder}" ]; } && usage - - if [ "${no_check_service}" = "1" ]; then - is_service_dir_empty - test_service_exists "${service}" - port=$(service_block print "${service}" "${tor_conf}" | grep "HiddenServicePort" | tail -n 1) - only_ports=${port#* } - virtport=${only_ports% *} - target=${only_ports##* } - target_addr="${target%%:*}" - target_port="${target##*:}" - else - [ -z "${port}" ] && error_msg "Missing onion service target port that the webserver will listen to" - is_integer "${port}" || is_addr_port "${port}" - target="${port}" - target_addr="${target%%:*}" - target_port="${target##*:}" - ## happens when user specified only the port without addr - [ "${target_addr}" = "${target_port}" ] && target="127.0.0.1:${target_port}" - fi - - case "${webserver}" in - apache2|openbsd-httpd) printf %s"${target}" | grep -q "unix" && error_msg "Web server '${webserver}' does not accept listening on a unix domain socket." ;; - esac - notice "${cyan}Activating web server for the onion service: ${service}${nocolor}\n" - case "${webserver}" in - nginx|apache2) - [ ! -d "${webserver_conf}" ] && error_msg "webserver_conf=${webserver_conf} directory does not exist" - ## If $folder starts with '~/' or '/', user specified the path, if started with anything else expect a folder inside ${website_dir} - case "${folder}" in - ~/*|/*) :;; - *) folder="${website_dir}/${folder}";; - esac - [ ! -d "${folder}" ] && error_msg "Website folder '${folder}' does not exist." - ;; - openbsd-httpd) - ## TODO: website_dir is not being used here, the chroot dir is /var/www and the root expected is /htdocs - case "${folder}" in - "htdocs"*) folder="/${folder}";; - "/htdocs"*|/*) :;; - *) folder="/htdocs/${folder}";; - esac - [ ! -d "${folder}" ] && error_msg "Folder '${folder}' does not exist." - ;; - esac - - case "${webserver}" in - nginx) - printf %s" -server { - listen ${target}; - server_name ${onion_hostname:-"_"}; - - server_tokens off; - access_log /var/log/nginx/access_${service}.log; - error_log /var/log/nginx/error_${service}.log; - - root ${folder}; - index index.html index.htm index.php; -} -" | tee "${webserver_conf}/${service}-onion.conf" - ;; - apache2) - printf %s" - - ServerName ${onion_hostname:-"_"} - DocumentRoot ${folder} - ErrorLog /var/log/${webserver}/${service}.log - ServerTokens Prod - ServerSignature Off - -" | tee "${webserver_conf}/${service}-onion.conf" - ;; - openbsd-httpd) - printf %s" -server \"${onion_hostname}\" { - listen on ${target_addr} port ${target_port} - root \"${folder}\" -} -" | tee -a "${webserver_conf}" - ;; - esac - reload_webserver - #rm -f /tmp/"${service}"-onion.conf - [ -n "${onion_hostname}" ] && notice "\n# Address: ${magenta}${onion_hostname}:${virtport}${nocolor}" - ;; - - f|off) - [ -z "${service}" ] && usage - disable_site(){ - service="${1}" - notice "\nStopping website of the service: ${service}" - case "${webserver}" in - nginx|apache2) rm -fv "${webserver_conf}/${service}-onion.conf";; - openbsd-httpd) httpd_service_block delete "${service}" "${webserver_conf}";; - esac - } - loop_list disable_site "${service}" 0 - reload_webserver - ;; - - l|list) - notice "${bold}Web server: ${webserver}${nocolor}\n" - notice "${bold}# Enabled websites:${nocolor}" - case "${webserver}" in - nginx|apache2) - for site in "${webserver_conf}"/*; do - site="${site##*/}" - site="${site%*-onion.conf}" - sites_enabled="$(printf '%s\n%s\n' "${sites_enabled}" "${site}")" - done - ;; - openbsd-httpd) httpd_service_block print "${service}" "${webserver_conf}";; - esac - if [ -n "${sites_enabled}" ]; then - notice "\n${sites_enabled}" - else - error_msg "No website enabled" - fi - ;; - - *) - error_msg "Invalid argument: status=${status}" - esac - ;; - - - ## guide to add onion-location to redirect tor users when using your plainnet site to the onion service address - ## https://matt.traudt.xyz/posts/2021-05-03-website-setup/ - location) - is_service_dir_empty - { [ -z "${action}" ] || [ -z "${service}" ]; } && usage - test_service_exists "${service}" - - start_location(){ - printf "Onion-Location guided steps -\n* The below output is printing text, no file was modified by this script, therefore, user needs to manually configure. -* For web servers, include header line inside the plainnet ssl block (port 443). -* It assumes you know how to run a plainnet server, configuration is an example and should be adapted to your needs. -\n# Add to your \"%s${action}\" configuration:\n" -} - - finish_location(){ - printf "\nTest redirection -\n* Open the web site in Tor Browser and a purple pill will appear in the address bar; or -* Fetch the web site HTTP headers and look for onion-location entry and the onion service address: -\n\twget --server-response --spider your-website.tld\n" -} - - case "${action}" in - - nginx) - start_location - printf " -server {\n\tlisten 443 ssl http2;\n\tadd_header Onion-Location http://""%s${onion_hostname}""\$request_uri;\n} -\n# Reload web server:\n\n\tnginx -t && nginx -s reload\n" - finish_location - ;; - - apache2) - start_location - printf " -\n\tHeader set Onion-Location \"http://%s${onion_hostname}%%{REQUEST_URI}s\"\n -\n# Enable headers and rewrite modules:\n\n\ta2enmod headers rewrite -\n# Reload web server:\n\n\tapache2 -t && apache2 -k graceful\n" - finish_location - ;; - - html|HTML) - start_location - printf " - -\n# Reload web server that you use:\n\n\tnginx -t && nginx -s reload\n\t# or\n\tapache2 -t && apache2 -k graceful\n" - finish_location - ;; - - *) - error_msg "Invalid argument: action=${action}" - esac - ;; - - - backup) - [ -z "${action}" ] && usage - case "${action}" in - - ## full backup needede to restore all of your hidden services and client keys - ## folders/files included: , /services/, /onion_auth/ - M|make) - tor_backup_file="tor-onion-services-backup-$(date +%Y-%m-%d-%H'h'-%M'm').tar.gz" - notice "${cyan}Backing up the services dir, onion_auth dir and the torrc${nocolor}\n" - mkdir -p "${tor_backup_dir}" - ## these lines are necessary to copy the full path when creating the compressed archive - cp "${tor_conf}" "${tor_conf}".rest - printf '\n%s\n\n' "$(grep "ClientOnionAuthDir" "${tor_conf}")" | tee "${tor_conf}".tmp >/dev/null - for service in $(grep "HiddenServiceDir ${tor_data_dir_services}/" "${tor_conf}" | sed "s|HiddenServiceDir ${tor_data_dir_services}/||" | tr "\n" " "); do - printf "\n" | tee -a "${tor_conf}".tmp >/dev/null - service_block print "${service}" "${tor_conf}" | tee -a "${tor_conf}".tmp >/dev/null - printf "\n" | tee -a "${tor_conf}".tmp >/dev/null - done - mv "${tor_conf}".tmp "${tor_conf}" - tar -cpzvf "${tor_backup_dir}"/"${tor_backup_file}" "${tor_data_dir_services}" "${tor_data_dir_auth}" "${tor_conf}" 2>/dev/null - mv "${tor_conf}".rest "${tor_conf}" - chown -R "${USER}:${USER}" "${tor_backup_dir}" - set_owner_permission - ## try every way to find a program to compute a sha 256 message digest - while :; do - command -v sha256sum >/dev/null && checksum_sha256="sha256sum" && break - command -v shasum >/dev/null && checksum_sha256="shasum -a 256" && break - command -v sha256 >/dev/null && checksum_sha256="sha256" && break - command -v openssl >/dev/null && checksum_sha256="openssl dgst -sha256 -r" && break - command -v digest >/dev/null && checksum_sha256="digest -a sha256" && break - break - done - ## but if no program is available (unlikley), don't exec nothing - [ -n "${checksum_sha256}" ] && notice "\nsha256=$(${checksum_sha256} "${tor_backup_dir}"/"${tor_backup_file}")" - ;; - - ## restore backup - ## backup tar file will be extracted and integrated into their respective tor folders - I|integrate) - ## make a separate dir indie the backup dir to unpack files - mkdir -p "${tor_backup_dir}"/integrate - ## get the latest backup - tor_backup_file=$(find "${tor_backup_dir}" -type -f -name "*.tar.gz" | tail -n -1) - notice "${cyan}Integrating backup from file: ${bold}${tor_backup_file}${nocolor}\n" - notice "Extracting the archive\n" - ## extract to integrate directory - tar -xpzvf "${tor_backup_dir}"/"${tor_backup_file}" -C "${tor_backup_dir}"/integrate - chown -R "${USER}:${USER}" "${tor_backup_dir}" - ## place files into their correct directories - cp -rf "${tor_backup_dir}"/integrate"${tor_data_dir_services}"/* "${tor_data_dir_services}"/ - cp -rf "${tor_backup_dir}"/integrate"${tor_data_dir_auth}"/* "${tor_data_dir_auth}"/ - ## TODO: remove this? - ## this is necessary to avoid duplicated configuration lines, - ## but maybe it does not suffice because HS lines will still be repeated if present on the backup and on the current torrc - client_auth_config="$(grep "ClientOnionAuthDir" "${tor_backup_dir}"/integrate"${tor_conf}")" - if [ -n "${client_auth_config}" ]; then - sed -i'' "/ClientOnionAuthDir .*/d" "${tor_conf}" - printf '\n%s\n\n' "${client_auth_config}" "${tor_conf}" - sed -i'' "/ClientOnionAuthDir .*/d" "${tor_backup_dir}"/integrate"${tor_conf}" - fi - ## TODO: should it merge or substitute? - ## - merging ends up in possibly having repeated lines - ## - substituing maybe ends up in losing configuration lines - ## merge the backup torrc with the current torrc - cat_squeeze_blank "${tor_conf}" "${tor_backup_dir}"/integrate"${tor_conf}" | tee "${tor_conf}".tmp >/dev/null - mv "${tor_conf}".tmp "${tor_conf}" - rm -rf "${tor_backup_dir}"/integrate - signal_tor - ;; - - *) - error_msg "Invalid argument: action=${action}" - esac - ;; - - - ## This addon protects against guard discovery and related traffic analysis attacks. - ## A guard discovery attack enables an adversary to determine the guard node(s) that are in use by a Tor client and/or Tor onion service. - ## Once the guard node is known, traffic analysis attacks that can deanonymize an onion service (or onion service user) become easier. - ## TODO: hardening (as in $ systemctl cat tor@default), but got permission denied: unable to read '/run/tor/control.authcookie', also see $ systemd-analyze security vanguards@default.service - ## TODO -> Vanguards sample service configuration for other service managers - vanguards) - [ -z "${action}" ] && usage - [ "${daemon_control}" != "systemctl" ] && error_msg "Unfortunately, OnionJuggler has only implemented Vanguards with Systemd (systemctl).\n Help improve this by submitting a merge request." - - while :; do - command -v python3 >/dev/null && python_path="$(command -v python3)" && break - command -v python >/dev/null && python_path="$(command -v python)" && break - done - [ -z "${python_path}" ] && error_msg "Python is not installed and it is needed for Vanguards." - - vanguards_config(){ - safe_edit tmp tor_conf - ## Keep config with the torrc and torsocks.conf - cp "${tor_data_dir}"/vanguards/vanguards-example.conf "${tor_conf_dir}"/vanguards.conf - sed -i'' "s|tor_control_port =.*|tor_control_port = ${tor_control_port}|g" "${tor_conf_dir}"/vanguards.conf - sed -i'' "s|logfile = .*|logfile = /var/log/tor/vanguards.log|g" "${tor_conf_dir}"/vanguards.conf - ## Control and Authentication methods are needed. Use the easiest to configure if the manual ones are not present, else do nothing. - ## Control methods are Port (default: 9051) and Socket (default: /run/tor/control). Prefer port because socket path may differ on different systems https://github.com/mikeperry-tor/vanguards/pull/54#issuecomment-812185302. - read_tor_files - # shellcheck disable=SC2086 - if ! grep -q "ControlPort ${tor_control_port}" ${tor_config_files} && ! grep -q "ControlSocket" ${tor_config_files}; then - sed -i'' "s/ControlPort .*/ControlPort ${tor_control_port}/" "${tor_conf_tmp}" - # shellcheck disable=SC2086 - grep -q "ControlPort ${tor_control_port}" ${tor_config_files} || printf %s"\nControlPort ${tor_control_port}\n\n" | tee -a "${tor_conf_tmp}" >/dev/null - fi - ## Authentication methods are Cookie (default: 0) and HashedPassword, to read the "control_auth_cookie". Prefer cookie because else a password is needed. If any method was already configured, use it. - # shellcheck disable=SC2086 - if ! grep -q "CookieAuthentication 1" ${tor_config_files} && ! grep -q "HashedControlPassword" ${tor_config_files}; then - sed -i'' "s/CookieAuthentication .*/CookieAuthentication 1/" "${tor_conf_tmp}" - # shellcheck disable=SC2086 - grep -q "CookieAuthentication" ${tor_config_files} || printf "\nCookieAuthentication 1\n\n" | tee -a "${tor_conf_tmp}" >/dev/null - fi - ## Generate Vanguards service - printf %s" -[Unit] -Description=Additional protections for Tor onion services -Wants=${tor_daemon} -After=network.target nss-lookup.target - -[Service] -WorkingDirectory=${tor_data_dir}/vanguards -ExecStart=${python_path} src/vanguards.py --config ${tor_conf_dir}/vanguards.conf -User=${tor_user} -Group=${tor_user} -Type=simple -Restart=always - -[Install] -WantedBy=multi-user.target -" | tee /tmp/vanguards@default.service - cp /tmp/vanguards@default.service /etc/systemd/system/ - printf "\n<><><><><><><><><><>\n" - cat /tmp/vanguards@default.service - printf "\n<><><><><><><><><><>\n" - signal_tor - systemctl daemon-reload - systemctl enable vanguards@default.service - systemctl restart vanguards@default.service - systemctl status vanguards@default.service --no-pager - } - - case "${action}" in - n|on) - if [ ! -d "${tor_data_dir}/vanguards" ]; then - notice "${cyan}Installing Vanguards${nocolor}\n" - git clone https://github.com/mikeperry-tor/vanguards.git "${tor_data_dir}/vanguards" - else - notice "${cyan}Upgrading Vanguards${nocolor}\n" - git -C "${tor_data_dir}"/vanguards pull -p --rebase=false - fi - git -C "${tor_data_dir}"/vanguards reset --hard "${vanguards_commit}" - git -C "${tor_data_dir}"/vanguards show - vanguards_version="$(grep "__version__ = " src/vanguards/__init__.py | tr "\"" " " | sed "s/__version__ = //")" - notice "Installed Vanguards v${vanguards_version}" - cp "${tor_data_dir}"/vanguards/vanguards.1 /usr/local/man/man1/ - vanguards_config - ;; - - f|off) - notice "${red}Removing Vanguards${nocolor}\n" - rm -rfv "${tor_data_dir}"/vanguards - ;; - - l|list) - tail -f -n 25 /var/log/tor/vanguards.log - ;; - - *) - error_msg "Invalid argument: action=${action}" - esac - ;; - + ## complain *) usage esac diff --git a/usr/bin/onionjuggler-cli-auth-client b/usr/bin/onionjuggler-cli-auth-client new file mode 100755 index 0000000..648a7cb --- /dev/null +++ b/usr/bin/onionjuggler-cli-auth-client @@ -0,0 +1,131 @@ +#!/usr/bin/env sh + +## manage client authorization server side (HiddenServiceDir/authorized_clients/) or client side (ClientOnionAuthDir) + +onionjuggler_defaults="/usr/share/onionjuggler/defaults.sh" +[ -e "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} does not exist"; exit 1; } +[ -f "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} is not a regular file"; exit 1; } +[ -r "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} can not be read"; exit 1; } +. "${onionjuggler_defaults}" + +usage(){ + printf %s"Usage: ${me} [--option ] +Complete options: + --on [--onion ] [--client-priv-key ] + create or add client-side authorization, optionally add client's private key + --off [--onion ] remove client-side credential + --list list client-side credentials +" + exit 2 +} + +######################## +#### OPTION PARSING #### + +## hacky getopts +## accepts long (--option) and short (-o) options +## accept argument assignment with space (--option arg | -o arg) or equal sign (--option=arg | -o=arg) +[ -z "${1}" ] && usage +while :; do + shift_n="" + opt_orig="${1}" ## save opt orig for error message to understand which opt failed + # shellcheck disable=SC2034 + arg_possible="${2}" ## need to pass the second positional parameter because maybe it is an argument + clean_opt "${1}" || break + case "${opt}" in + on|off|list|n|f|l) status="${opt}";; + o|onion|o=*|onion=*) get_arg onion;; + client-priv-key|client-priv-key=*) get_arg client_priv_key;; + h|help) usage;; + *) error_msg "Invalid option: '${opt_orig}'";; + esac + ## shift as many times as demanded + ## if empty, shift at least once to pass to next option + shift "${shift_n:-1}" + [ -z "${1}" ] && break +done + +[ "$(id -u)" -ne 0 ] && error_msg "run as root" +case "${status}" in + + ## as the onion service client, add a key given by the onion service operator to authenticate yourself inside ClientOnionAuthDir + ## The suffix '.auth_private' should not be mentioned, it will be automatically inserted when mentioning the name of the file. + ## private key format must be: :descriptor:x25519: + ## use the onion hostname as the file name, this avoid overriding the file by mistake and it indicates outside of the file for which service it refers to (of course it is written inside also) + ## adding to Tor Browser automatically not supported yet + n|on) + [ -z "${onion}" ] && usage + ## removes protocol such as http(s)://, ssh:// and git:// from the front of the address and trailing / at the end of the onion to clean it and only show the hostname (address.onion) + onion="$(printf %s"${onion}\n" | sed "s|.*://||" | sed "s|/$||")" + onion_hostname_without_onion="${onion%.onion}" + [ "${onion_hostname_without_onion%%*[^a-z2-7]*}" ] || error_msg "Onion domain is invalid, it is not within base32 alphabet lower-case encoding [a-z][2-7]" + [ "${#onion_hostname_without_onion}" = "56" ] || error_msg "Onion domain is invalid, LENGTH=${#onion} is different than 56 characters (<56-char-base32>.onion)" + safe_edit tmp tor_conf + read_tor_files + # shellcheck disable=SC2086 + grep -q "ClientOnionAuthDir" ${tor_config_files} || { printf %s"\nClientOnionAuthDir ${tor_data_dir_auth}\n\n" | tee -a "${tor_conf_tmp}"; } + mkdir -p "${tor_data_dir_auth}" + if [ -z "${client_priv_key}" ]; then + ## Generate pem and derive pub and priv keys + "${openssl_cmd}" genpkey -algorithm x25519 -out /tmp/k1.prv.pem + grep -v "PRIVATE KEY" /tmp/k1.prv.pem | base64pem -d | tail -c 32 | base32 | sed 's/=//g' > /tmp/k1.prv.key + "${openssl_cmd}" pkey -in /tmp/k1.prv.pem -pubout | grep -v "PUBLIC KEY" | base64pem -d | tail -c 32 | base32 | sed 's/=//g' > /tmp/k1.pub.key + ## save variables + client_pub_key=$(cat /tmp/k1.pub.key) + client_priv_key=$(cat /tmp/k1.prv.key) + client_priv_key_config="${onion_hostname_without_onion}:descriptor:x25519:${client_priv_key}" + client_pub_key_config="descriptor:x25519:${client_pub_key}" + ## Delete pem and keys + rm -f /tmp/k1.pub.key /tmp/k1.prv.key /tmp/k1.prv.pem + # Client side configuration + printf %s"${client_priv_key_config}\n" | tee "${tor_data_dir_auth}"/"${onion_hostname_without_onion}".auth_private >/dev/null + notice "${bold}Client side authorization configured${nocolor}" + notice "This is your private key, keep it safe, keep it hidden:" + notice "client_priv_key=${client_priv_key}" + notice "client_priv_key_config=${client_priv_key_config}" + notice "\n${bold}Now it depends on the service operator to authorize your client public key${nocolor}" + ## Server side configuration + notice "Send the public key and instructions to the onion service operator of ${onion}" + notice "client_pub_key=${client_pub_key}" + notice "client_pub_key_config=${client_pub_key_config}" + else + client_priv_key_config="${onion_hostname_without_onion}:descriptor:x25519:${client_priv_key}" + printf %s"${client_priv_key_config}\n" | tee "${tor_data_dir_auth}"/"${onion_hostname_without_onion}".auth_private >/dev/null + notice "\n${bold}Client side authorization configured${nocolor}" + notice "As you inserted the private key manually, it ise expected that you have already sent/received the public key to/from the onion service operator" + notice "client_priv_key_config=${client_priv_key_config}" + fi + ;; + + ## as the onion service client, delete '.auth_private' files from ClientOnionAuthDir that are not valid or has no use anymore + f|off) + [ -z "${onion}" ] && usage + onion="$(printf %s"${onion}\n" | sed "s|.*://||" | sed "s|/.*$||")" + auth_client_remove(){ + onion="${1}" + notice "\n${red}Removing ${tor_data_dir_auth}/${onion}.auth_private${nocolor}" + rm -fv "${tor_data_dir_auth}"/"${onion}".auth_private + } + if ! check_folder_is_not_empty "${tor_data_dir_auth}"; then + loop_list auth_client_remove "${onion}" + else + error_msg "ClientOnionAuthDir is empty" + fi + ;; + + l|list) + if ! check_folder_is_not_empty "${tor_data_dir_auth}"; then + notice "ClientOnionAuthDir ${tor_data_dir_auth}" + for auth in "${tor_data_dir_auth}"/*; do + auth="${auth##*/}" + notice "\nFile name: ${bold}${auth}${nocolor}" + notice "Content: ${bold}$(grep "descriptor:x25519:" "${tor_data_dir_auth}"/"${auth}")${nocolor}" + done + printf "\n" + else + error_msg "ClientOnionAuthDir is empty" + fi + ;; + + *) usage;; +esac diff --git a/usr/bin/onionjuggler-cli-auth-server b/usr/bin/onionjuggler-cli-auth-server new file mode 100755 index 0000000..53213d4 --- /dev/null +++ b/usr/bin/onionjuggler-cli-auth-server @@ -0,0 +1,174 @@ +#!/usr/bin/env sh + +## manage client authorization server side (HiddenServiceDir/authorized_clients/) or client side (ClientOnionAuthDir) + +onionjuggler_defaults="/usr/share/onionjuggler/defaults.sh" +[ -e "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} does not exist"; exit 1; } +[ -f "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} is not a regular file"; exit 1; } +[ -r "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} can not be read"; exit 1; } +. "${onionjuggler_defaults}" + + +usage(){ + printf %s"Usage: ${me} [--option ] +Complete options: + --on [--service ] [--client ] [--client-pub-key ] + add client authorization, optionally add client's public key + --off [--service <@all|SERV1,SERV2,...>] [--client <@all|CLIENT1,CLIENT2,...>] + remove client authorization + --list [--service <@all|SERV1,SERV2,...>] + list authorized clients for indicated service +" + exit 2 +} + + +######################## +#### OPTION PARSING #### + +## hacky getopts +## accepts long (--option) and short (-o) options +## accept argument assignment with space (--option arg | -o arg) or equal sign (--option=arg | -o=arg) +[ -z "${1}" ] && usage +while :; do + shift_n="" + opt_orig="${1}" ## save opt orig for error message to understand which opt failed + # shellcheck disable=SC2034 + arg_possible="${2}" ## need to pass the second positional parameter because maybe it is an argument + clean_opt "${1}" || break + case "${opt}" in + on|off|list|n|f|l) status="${opt}";; + s|service|s=*|service=*) get_arg service;; + c|client|c=*|client=*) get_arg client;; + client-pub-key|client-pub-key=*) get_arg client_pub_key;; + h|help) usage;; + *) error_msg "Invalid option: '${opt_orig}'";; + esac + ## shift as many times as demanded + ## if empty, shift at least once to pass to next option + shift "${shift_n:-1}" + [ -z "${1}" ] && break +done + +[ "$(id -u)" -ne 0 ] && error_msg "run as root" +[ -z "${service}" ] && usage +[ "${service}" != "@all" ] && check_service_name +is_service_dir_empty + +case "${status}" in + + ## as the onion service operator, make your onion authenticated by generating a pair or public and private keys, + ## the client pub key is automatically saved inside /authorized_clients/alice.auth + ## the client private key is shown in the screen and the key file deleted + ## the onion service operator should send the private key for the desired client + n|on) + [ -z "${client}" ] && usage + #printf "\n# Generating keys to access onion service (Client Authorization) ...\n" + auth_server_add(){ + service="${1}" + client="${2}" + test_service_exists "${service}" + ## Generate pem and derive pub and priv keys + "${openssl_cmd}" genpkey -algorithm x25519 -out /tmp/k1.prv.pem + grep -v " PRIVATE KEY" /tmp/k1.prv.pem | base64pem -d | tail -c 32 | base32 | sed "s/=//g" > /tmp/k1.prv.key + "${openssl_cmd}" pkey -in /tmp/k1.prv.pem -pubout | grep -v " PUBLIC KEY" | base64pem -d | tail -c 32 | base32 | sed "s/=//g" > /tmp/k1.pub.key + ## save variables + client_pub_key=$(cat /tmp/k1.pub.key) + client_priv_key=$(cat /tmp/k1.prv.key) + client_priv_key_config="${onion_hostname%.onion}:descriptor:x25519:${client_priv_key}" + client_pub_key_config="descriptor:x25519:${client_pub_key}" + # Server side configuration + printf %s"${client_pub_key_config}\n" | tee "${tor_data_dir_services}"/"${service}"/authorized_clients/"${client}".auth >/dev/null + ## Client side configuration + printf "<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>\n" + printf %s"service=${bold}${service}${nocolor}\n" + printf %s"client=${bold}${client}${nocolor}\n" + printf %s"onion_hostname=${bold}${magenta}${onion_hostname}${nocolor}\n" + printf %s"client_pub_key=${bold}${client_pub_key}${nocolor}\n" + printf %s"client_pub_key_config=${bold}${client_pub_key_config}${nocolor}\n" + printf %s"client_priv_key=${bold}${client_priv_key}${nocolor}\n" + printf %s"client_priv_key_config=${bold}${client_priv_key_config}${nocolor}\n" + printf "<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>\n" + ## Delete pem and keys + rm -f /tmp/k1.pub.key /tmp/k1.prv.key /tmp/k1.prv.pem + } + [ "${service}" = "@all" ] && { create_service_list ; service="${service_name_list}" ; } + [ "${client}" = "@all" ] && error_msg "Client name cannot be @all, it is a restricted wildcard for referring to all clients, not a name per se." + if [ -n "${client_pub_key}" ]; then + test_service_exists "${service}" + client_pub_key_config="descriptor:x25519:${client_pub_key}" + printf %s"${client_pub_key_config}" | tee "${tor_data_dir_services}"/"${service}"/authorized_clients/"${client}".auth >/dev/null + notice "\nServer side authorization configured\n" + printf %s" client_pub_key_config=${client_pub_key_config}\n" + notice "\nAs you inserted the public key manually, we expect that the client already has the private key" + else + loop_list auth_server_add "${service}" "${client}" + fi + signal_tor + ;; + + ## as the onion service operator, after making your onion service authenticated, you can also remove a specific client authorization + ## if no clients are present, the service will be available to anyone that has the onion service address + f|off) + + auth_server_remove_clients(){ + service="${1}" + client="${2}" + #notice "Service: ${service}" + if [ "${client}" = "@all" ]; then + rm -fv "${tor_data_dir_services}"/"${service}"/authorized_clients/*.auth + else + rm -fv "${tor_data_dir_services}"/"${service}"/authorized_clients/"${client}".auth + fi + } + + [ -z "${client}" ] && usage + [ -z "${service}" ] && error_msg "service is missing" + [ "${service}" != "@all" ] && check_service_name + + if [ "${service}" = "@all" ]; then + notice "Removing client authorization for:" + notice "Service: @all" + create_service_list; service="${service_name_list}" + if [ "${client}" = "@all" ]; then + notice "Clients: @all.\nThe service is now accessible for anyone with the onion address.\n" + else + notice "If any client remains, the service will still be authenticated." + fi + else + notice "Removing client authorization for:" + notice "Service: ${service}" + if [ "${client}" = "@all" ]; then + notice "Clients: @all.\nThe service is now accessible for anyone with the onion address.\n" + else + notice "If any client remains, the service will still be authenticated." + fi + fi + loop_list auth_server_remove_clients "${service}" "${client}" + printf "\n" + signal_tor + ;; + + l|list) + auth_server_list(){ + service="${1}" + test_service_exists "${service}" + create_client_list "${service}" + notice "\nService: ${service}" + if [ -n "${client_count}" ]; then + [ -n "${client_name_list}" ] && printf %s"Clients: ${client_name_list} (${client_count})\n" + for auth in "${tor_data_dir_services}/${service}/authorized_clients"/*; do + auth="${auth##*/}" + notice "${auth}: $(grep "descriptor:x25519:" "${tor_data_dir_services}"/"${service}"/authorized_clients/"${auth}")${nocolor}" + done + else + notice "Clients: NONE (0)" + fi + } + notice "${blue}Authorized clients for Hidden Services${nocolor}" + [ "${service}" = "@all" ] && { create_service_list; service="${service_name_list}"; } + loop_list auth_server_list "${service}" + ;; + + *) usage +esac diff --git a/usr/bin/onionjuggler-cli-backup b/usr/bin/onionjuggler-cli-backup new file mode 100755 index 0000000..9ad8ff0 --- /dev/null +++ b/usr/bin/onionjuggler-cli-backup @@ -0,0 +1,119 @@ +#!/usr/bin/env sh + +## script name +me="${0##*/}" + +onionjuggler_defaults="/usr/share/onionjuggler/defaults.sh" +[ -e "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} does not exist"; exit 1; } +[ -f "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} is not a regular file"; exit 1; } +[ -r "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} can not be read"; exit 1; } +. "${onionjuggler_defaults}" + + +usage(){ + printf %s"Usage: ${me} [--option ] +Complete options: + -M, --make make a backup + -I, --integrate integrate latest backup +" + exit 2 +} + +######################## +#### OPTION PARSING #### + +## hacky getopts +## accepts long (--option) and short (-o) options +## accept argument assignment with space (--option arg | -o arg) or equal sign (--option=arg | -o=arg) +[ -z "${1}" ] && usage +while :; do + shift_n="" + opt_orig="${1}" ## save opt orig for error message to understand which opt failed + # shellcheck disable=SC2034 + arg_possible="${2}" ## need to pass the second positional parameter because maybe it is an argument + clean_opt "${1}" || break + case "${opt}" in + M|make|I|integrate) status="${opt}";; + h|help) usage;; + *) error_msg "Invalid option: '${opt_orig}'";; + esac + ## shift as many times as demanded + ## if empty, shift at least once to pass to next option + shift "${shift_n:-1}" + [ -z "${1}" ] && break +done + +######################## +######### MAIN ######### + +[ "$(id -u)" -ne 0 ] && error_msg "run as root" +case "${status}" in + + ## full backup needede to restore all of your hidden services and client keys + ## folders/files included: , /services/, /onion_auth/ + M|make) + tor_backup_file="tor-onion-services-backup-$(date +%Y-%m-%d-%H'h'-%M'm').tar.gz" + notice "${cyan}Backing up the services dir, onion_auth dir and the torrc${nocolor}\n" + mkdir -p "${tor_backup_dir}" + ## these lines are necessary to copy the full path when creating the compressed archive + cp "${tor_conf}" "${tor_conf}".rest + printf '\n%s\n\n' "$(grep "ClientOnionAuthDir" "${tor_conf}")" | tee "${tor_conf}".tmp >/dev/null + for service in $(grep "HiddenServiceDir ${tor_data_dir_services}/" "${tor_conf}" | sed "s|HiddenServiceDir ${tor_data_dir_services}/||" | tr "\n" " "); do + printf "\n" | tee -a "${tor_conf}".tmp >/dev/null + service_block print "${service}" "${tor_conf}" | tee -a "${tor_conf}".tmp >/dev/null + printf "\n" | tee -a "${tor_conf}".tmp >/dev/null + done + mv "${tor_conf}".tmp "${tor_conf}" + tar -cpzvf "${tor_backup_dir}"/"${tor_backup_file}" "${tor_data_dir_services}" "${tor_data_dir_auth}" "${tor_conf}" 2>/dev/null + mv "${tor_conf}".rest "${tor_conf}" + chown -R "${USER}:${USER}" "${tor_backup_dir}" + set_owner_permission + ## try every way to find a program to compute a sha 256 message digest + while :; do + has sha256sum && checksum_sha256="sha256sum" && break + has shasum && checksum_sha256="shasum -a 256" && break + has sha256 && checksum_sha256="sha256" && break + has openssl && checksum_sha256="openssl dgst -sha256 -r" && break + has digest && checksum_sha256="digest -a sha256" && break + break + done + ## but if no program is available (unlikley), don't exec nothing + [ -n "${checksum_sha256}" ] && notice "\nsha256=$(${checksum_sha256} "${tor_backup_dir}"/"${tor_backup_file}")" + ;; + + ## restore backup + ## backup tar file will be extracted and integrated into their respective tor folders + I|integrate) + ## make a separate dir indie the backup dir to unpack files + mkdir -p "${tor_backup_dir}"/integrate + ## get the latest backup + tor_backup_file=$(find "${tor_backup_dir}" -type -f -name "*.tar.gz" | tail -n -1) + notice "${cyan}Integrating backup from file: ${bold}${tor_backup_file}${nocolor}\n" + notice "Extracting the archive\n" + ## extract to integrate directory + tar -xpzvf "${tor_backup_dir}"/"${tor_backup_file}" -C "${tor_backup_dir}"/integrate + chown -R "${USER}:${USER}" "${tor_backup_dir}" + ## place files into their correct directories + cp -rf "${tor_backup_dir}"/integrate"${tor_data_dir_services}"/* "${tor_data_dir_services}"/ + cp -rf "${tor_backup_dir}"/integrate"${tor_data_dir_auth}"/* "${tor_data_dir_auth}"/ + ## TODO: remove this? + ## this is necessary to avoid duplicated configuration lines, + ## but maybe it does not suffice because HS lines will still be repeated if present on the backup and on the current torrc + client_auth_config="$(grep "ClientOnionAuthDir" "${tor_backup_dir}"/integrate"${tor_conf}")" + if [ -n "${client_auth_config}" ]; then + sed -i'' "/ClientOnionAuthDir .*/d" "${tor_conf}" + printf '\n%s\n\n' "${client_auth_config}" "${tor_conf}" + sed -i'' "/ClientOnionAuthDir .*/d" "${tor_backup_dir}"/integrate"${tor_conf}" + fi + ## TODO: should it merge or substitute? + ## - merging ends up in possibly having repeated lines + ## - substituing maybe ends up in losing configuration lines + ## merge the backup torrc with the current torrc + cat_squeeze_blank "${tor_conf}" "${tor_backup_dir}"/integrate"${tor_conf}" | tee "${tor_conf}".tmp >/dev/null + mv "${tor_conf}".tmp "${tor_conf}" + rm -rf "${tor_backup_dir}"/integrate + signal_tor + ;; + + *) usage;; +esac diff --git a/usr/bin/onionjuggler-cli-vanguards b/usr/bin/onionjuggler-cli-vanguards new file mode 100755 index 0000000..099ff7d --- /dev/null +++ b/usr/bin/onionjuggler-cli-vanguards @@ -0,0 +1,148 @@ +#!/usr/bin/env sh + + + +## This addon protects against guard discovery and related traffic analysis attacks. +## A guard discovery attack enables an adversary to determine the guard node(s) that are in use by a Tor client and/or Tor onion service. +## Once the guard node is known, traffic analysis attacks that can deanonymize an onion service (or onion service user) become easier. +## TODO: hardening (as in $ systemctl cat tor@default), but got permission denied: unable to read '/run/tor/control.authcookie', also see $ systemd-analyze security vanguards@default.service +## TODO -> Vanguards sample service configuration for other service managers + +## script name +me="${0##*/}" + +onionjuggler_defaults="/usr/share/onionjuggler/defaults.sh" +[ -e "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} does not exist"; exit 1; } +[ -f "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} is not a regular file"; exit 1; } +[ -r "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} can not be read"; exit 1; } +. "${onionjuggler_defaults}" + +usage(){ + printf %s"Usage: ${me} [--option ] +Complete options: + --on install Vanguards addon, if already installed, upgrade + --off remove Vanguards + --list see Vanguards logs +" + exit 2 +} + +######################## +#### OPTION PARSING #### + +## hacky getopts +## accepts long (--option) and short (-o) options +## accept argument assignment with space (--option arg | -o arg) or equal sign (--option=arg | -o=arg) +[ -z "${1}" ] && usage +while :; do + shift_n="" + opt_orig="${1}" ## save opt orig for error message to understand which opt failed + # shellcheck disable=SC2034 + arg_possible="${2}" ## need to pass the second positional parameter because maybe it is an argument + clean_opt "${1}" || break + case "${opt}" in + on|off|list|n|f|l) status="${opt}";; + h|help) usage;; + "") break;; + *) error_msg "Invalid option: '${opt_orig}'";; + esac + ## shift as many times as demanded + ## if empty, shift at least once to pass to next option + shift "${shift_n:-1}" + [ -z "${1}" ] && break +done + +######################## +######### MAIN ######### + +[ "${daemon_control}" != "systemctl" ] && error_msg "Unfortunately, OnionJuggler has only implemented Vanguards with Systemd (systemctl).\n Help improve this by submitting a merge request." + +while :; do + has python3 && python_path="$(command -v python3)" && break + has python && python_path="$(command -v python)" && break +done +[ -z "${python_path}" ] && error_msg "Python is not installed and it is needed for Vanguards." + + +vanguards_config(){ + safe_edit tmp tor_conf + ## Keep config with the torrc and torsocks.conf + cp "${tor_data_dir}"/vanguards/vanguards-example.conf "${tor_conf_dir}"/vanguards.conf + sed -i'' "s|tor_control_port =.*|tor_control_port = ${tor_control_port}|g" "${tor_conf_dir}"/vanguards.conf + sed -i'' "s|logfile = .*|logfile = /var/log/tor/vanguards.log|g" "${tor_conf_dir}"/vanguards.conf + ## Control and Authentication methods are needed. Use the easiest to configure if the manual ones are not present, else do nothing. + ## Control methods are Port (default: 9051) and Socket (default: /run/tor/control). Prefer port because socket path may differ on different systems https://github.com/mikeperry-tor/vanguards/pull/54#issuecomment-812185302. + read_tor_files + # shellcheck disable=SC2086 + if ! grep -q "ControlPort ${tor_control_port}" ${tor_config_files} && ! grep -q "ControlSocket" ${tor_config_files}; then + sed -i'' "s/ControlPort .*/ControlPort ${tor_control_port}/" "${tor_conf_tmp}" + # shellcheck disable=SC2086 + grep -q "ControlPort ${tor_control_port}" ${tor_config_files} || printf %s"\nControlPort ${tor_control_port}\n\n" | tee -a "${tor_conf_tmp}" >/dev/null + fi + ## Authentication methods are Cookie (default: 0) and HashedPassword, to read the "control_auth_cookie". Prefer cookie because else a password is needed. If any method was already configured, use it. + # shellcheck disable=SC2086 + if ! grep -q "CookieAuthentication 1" ${tor_config_files} && ! grep -q "HashedControlPassword" ${tor_config_files}; then + sed -i'' "s/CookieAuthentication .*/CookieAuthentication 1/" "${tor_conf_tmp}" + # shellcheck disable=SC2086 + grep -q "CookieAuthentication" ${tor_config_files} || printf "\nCookieAuthentication 1\n\n" | tee -a "${tor_conf_tmp}" >/dev/null + fi + ## Generate Vanguards service + printf %s" +[Unit] +Description=Additional protections for Tor onion services +Wants=${tor_daemon} +After=network.target nss-lookup.target + +[Service] +WorkingDirectory=${tor_data_dir}/vanguards +ExecStart=${python_path} src/vanguards.py --config ${tor_conf_dir}/vanguards.conf +User=${tor_user} +Group=${tor_user} +Type=simple +Restart=always + +[Install] +WantedBy=multi-user.target +" | tee /tmp/vanguards@default.service + cp /tmp/vanguards@default.service /etc/systemd/system/ + printf "\n<><><><><><><><><><>\n" + cat /tmp/vanguards@default.service + printf "\n<><><><><><><><><><>\n" + signal_tor + systemctl daemon-reload + systemctl enable vanguards@default.service + systemctl restart vanguards@default.service + systemctl status vanguards@default.service --no-pager +} + + +## user option +[ "$(id -u)" -ne 0 ] && error_msg "run as root" +case "${status}" in + n|on) + if [ ! -d "${tor_data_dir}/vanguards" ]; then + notice "${cyan}Installing Vanguards${nocolor}\n" + git clone https://github.com/mikeperry-tor/vanguards.git "${tor_data_dir}/vanguards" + else + notice "${cyan}Upgrading Vanguards${nocolor}\n" + git -C "${tor_data_dir}"/vanguards pull -p --rebase=false + fi + git -C "${tor_data_dir}"/vanguards reset --hard "${vanguards_commit}" + git -C "${tor_data_dir}"/vanguards show + vanguards_version="$(grep "__version__ = " src/vanguards/__init__.py | tr "\"" " " | sed "s/__version__ = //")" + notice "Installed Vanguards v${vanguards_version}" + cp "${tor_data_dir}"/vanguards/vanguards.1 /usr/local/man/man1/ + vanguards_config + ;; + + f|off) + notice "${red}Removing Vanguards${nocolor}\n" + rm -rfv "${tor_data_dir}"/vanguards + ;; + + l|list) + tail -f -n 25 /var/log/tor/vanguards.log + ;; + + *) usage;; +esac diff --git a/usr/bin/onionjuggler-cli-web b/usr/bin/onionjuggler-cli-web new file mode 100755 index 0000000..03f31ad --- /dev/null +++ b/usr/bin/onionjuggler-cli-web @@ -0,0 +1,216 @@ +#!/usr/bin/env sh + +## Webserver management + +## WEB: start serving files with a webserver for a specific onion service and specific website folder +## LOCATION: guide to add onion-location to redirect tor users when using your plainnet site to the onion service address +## https://matt.traudt.xyz/posts/2021-05-03-website-setup/ + +## script name +me="${0##*/}" + +onionjuggler_defaults="/usr/share/onionjuggler/defaults.sh" +[ -e "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} does not exist"; exit 1; } +[ -f "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} is not a regular file"; exit 1; } +[ -r "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} can not be read"; exit 1; } +. "${onionjuggler_defaults}" + + +usage(){ + printf %s"Usage: ${me} [--option ] +Complete options: + --on [--service ] [--folder ] + start serving a website for certain service and its folder + --on [--service ] [--folder ] [--port ] [--no-check-service] + useful for workstations when the tor process is running on the gateway + --off [--service ] stop serving a website for certain service and its folder + --list list enabled websites +" + exit 2 +} + +######################## +#### OPTION PARSING #### + +## hacky getopts +## accepts long (--option) and short (-o) options +## accept argument assignment with space (--option arg | -o arg) or equal sign (--option=arg | -o=arg) +[ -z "${1}" ] && usage +while :; do + shift_n="" + opt_orig="${1}" ## save opt orig for error message to understand which opt failed + # shellcheck disable=SC2034 + arg_possible="${2}" ## need to pass the second positional parameter because maybe it is an argument + clean_opt "${1}" || break + case "${opt}" in + on|off|list|n|f|l) status="${opt}";; + s|service|s=*|service=*) get_arg service;; + o|onion|o=*|onion=*) get_arg onion;; + p|port|p=*|port=*) get_arg port;; + w|folder|w=*|folder=*) get_arg folder;; + no-check-service) no_check_service=1;; + h|help) usage;; + *) error_msg "Invalid option: '${opt_orig}'";; + esac + ## shift as many times as demanded + ## if empty, shift at least once to pass to next option + shift "${shift_n:-1}" + [ -z "${1}" ] && break +done + +######################## +######### MAIN ######### + +[ -z "${status}" ] && usage +if [ "${webserver}" != "nginx" ] && [ "${webserver}" != "apache2" ] && [ "${webserver}" != "openbsd-httpd" ]; then + error_msg "webserver can be either 'nginx' or 'apache2', not '${webserver}'" +fi + +reload_webserver(){ + notice "\nReloading web server to apply new configuration" + case "${webserver}" in + nginx) nginx -t && nginx -s reload;; + apache2) apache2 -t && apache2 -k graceful;; + openbsd-httpd) + openbsd_httpd_test="$(httpd -n -f "${webserver_conf}" 2>&1)" + if [ "${openbsd_httpd_test}" = "no actions, nothing to do" ] || [ "${openbsd_httpd_test}" = "configuration OK" ]; then + rcctl reload httpd + fi + ;; + esac + [ "${?}" -eq 1 ] && error_msg "Failed to reload ${webserver}, you must restart it manually before running this script again." +} + +## user option +[ "$(id -u)" -ne 0 ] && error_msg "run as root" +case "${status}" in + + n|on) + { [ -z "${service}" ] || [ -z "${folder}" ]; } && usage + + if [ "${no_check_service}" = "1" ]; then + is_service_dir_empty + test_service_exists "${service}" + port=$(service_block print "${service}" "${tor_conf}" | grep "HiddenServicePort" | tail -n 1) + only_ports=${port#* } + virtport=${only_ports% *} + target=${only_ports##* } + target_addr="${target%%:*}" + target_port="${target##*:}" + else + [ -z "${port}" ] && error_msg "Missing onion service target port that the webserver will listen to" + port="$(printf '%s\n' "${port}" | tr "," " ")" + virtport="${port% *}" + target="${port#* }" + target_addr="${target%%:*}" + target_port="${target##*:}" + ## happens when user specified only the port without addr + [ "${target_addr}" = "${target_port}" ] && target="127.0.0.1:${target_port}" + is_addr_port "${target}" + fi + + case "${webserver}" in + apache2|openbsd-httpd) printf %s"${target}" | grep -q "unix" && error_msg "Web server '${webserver}' does not accept listening on a unix domain socket." ;; + esac + notice "${cyan}Activating web server for the onion service: ${service}${nocolor}\n" + case "${webserver}" in + nginx|apache2) + [ ! -d "${webserver_conf}" ] && error_msg "webserver_conf=${webserver_conf} directory does not exist" + ## If $folder starts with '~/' or '/', user specified the path, if started with anything else expect a folder inside ${website_dir} + case "${folder}" in + ~/*|/*) :;; + *) folder="${website_dir}/${folder}";; + esac + [ ! -d "${folder}" ] && error_msg "Website folder '${folder}' does not exist." + ;; + openbsd-httpd) + ## TODO: website_dir is not being used here, the chroot dir is /var/www and the root expected is /htdocs + case "${folder}" in + "htdocs"*) folder="/${folder}";; + "/htdocs"*|/*) :;; + *) folder="/htdocs/${folder}";; + esac + [ ! -d "${folder}" ] && error_msg "Folder '${folder}' does not exist." + ;; + esac + + case "${webserver}" in + nginx) + printf %s" +server { + listen ${target}; + server_name ${onion_hostname:-"_"}; + + server_tokens off; + access_log /var/log/nginx/access_${service}.log; + error_log /var/log/nginx/error_${service}.log; + + root ${folder}; + index index.html index.htm index.php; +} +" | tee "${webserver_conf}/${service}-onion.conf" + ;; + apache2) + printf %s" + + ServerName ${onion_hostname:-"_"} + DocumentRoot ${folder} + ErrorLog /var/log/${webserver}/${service}.log + ServerTokens Prod + ServerSignature Off + +" | tee "${webserver_conf}/${service}-onion.conf" + ;; + openbsd-httpd) + printf %s" +server \"${onion_hostname}\" { + listen on ${target_addr} port ${target_port} + root \"${folder}\" +} +" | tee -a "${webserver_conf}" + ;; + esac + reload_webserver + #rm -f /tmp/"${service}"-onion.conf + if [ -n "${onion_hostname}" ]; then + notice "\n# Address: ${magenta}${onion_hostname}:${virtport}${nocolor}" + has qrencode && qrencode -m 2 -t ANSIUTF8 "${onion_hostname}:${virtport}" + fi + ;; + + f|off) + [ -z "${service}" ] && usage + disable_site(){ + service="${1}" + notice "\nStopping website of the service: ${service}" + case "${webserver}" in + nginx|apache2) rm -fv "${webserver_conf}/${service}-onion.conf";; + openbsd-httpd) httpd_service_block delete "${service}" "${webserver_conf}";; + esac + } + loop_list disable_site "${service}" 0 + reload_webserver + ;; + + l|list) + notice "${bold}Web server: ${webserver}${nocolor}\n" + notice "${bold}# Enabled websites:${nocolor}" + case "${webserver}" in + nginx|apache2) + for site in "${webserver_conf}"/*; do + site="${site##*/}" + site="${site%*-onion.conf}" + sites_enabled="$(printf '%s\n%s\n' "${sites_enabled}" "${site}")" + done + ;; + openbsd-httpd) httpd_service_block print "${service}" "${webserver_conf}";; + esac + if [ -n "${sites_enabled}" ]; then + notice "\n${sites_enabled}" + else + error_msg "No website enabled" + fi + ;; + + *) usage;; +esac diff --git a/usr/bin/onionjuggler-tui b/usr/bin/onionjuggler-tui index 5a343e9..9f2db43 100755 --- a/usr/bin/onionjuggler-tui +++ b/usr/bin/onionjuggler-tui @@ -10,23 +10,12 @@ ## Lines that begin with "## " try to explain what's going on. Lines ## that begin with just "#" are disabled commands. -me="${0##*/}" -## colors -nocolor="\033[0m" -#bold="\033[1m" -#nobold="\033[22m" -underline="\033[4m" -nounderline="\033[24m" -red="\033[31m" -#green="\033[32m" -yellow="\033[33m" -#blue="\033[34m" -#magenta="\033[35m" -#cyan="\033[36m" - -## display error message with instructions to use the script correctly. -notice(){ printf %s"${me}: ${1}\n" 1>&2; } -error_msg(){ notice "${red}error: ${1}${nocolor}"; exit 1; } +onionjuggler_defaults="/usr/share/onionjuggler/defaults.sh" +[ -e "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} does not exist"; exit 1; } +[ -f "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} is not a regular file"; exit 1; } +[ -r "${onionjuggler_defaults}" ] || { printf '%s\n' "${onionjuggler_defaults} can not be read"; exit 1; } +. "${onionjuggler_defaults}" + ################### #### VARIABLES #### @@ -34,98 +23,40 @@ i=0 usage(){ printf "Configure the environment for OnionJuggler -\nUsage: onionjuggler-tui [option ] +\nUsage: onionjuggler-tui [--option ] \nOptions: - -C, --config specify alternative onionjuggler configuration file to be read -h, --help show this help message " exit 1 } - -## if option requires argument, check if it was provided, if yes, assign the arg to the opt -## $arg was already assigned, and if valid, will use it for the key value -## usage: get_arg key -get_arg(){ - ## if argument is empty or starts with '-', fail as it possibly is an option - case "${arg}" in ""|-*) error_msg "Option '${opt_orig}' requires an argument.";; esac - ## assign - value="${arg}" - ## Escaping quotes is needed because else it will fail if the argument is quoted - # shellcheck disable=SC2140 - eval "${1}"="\"${value}\"" - - ## shift positional argument two times, as this option demands argument, unless they are separated by equal sign '=' - ## shift_n default value was assigned when trimming hifens '--' from the options - ## if shift_n is equal to zero, '--option arg' - ## if shift_n is not equal to zero, '--option=arg' - [ -z "${shift_n}" ] && shift_n=2 -} - -## hacky getopts -## accepts long (--option) and short (-o) options -## accept argument assignment with space (--option arg | -o arg) or equal sign (--option=arg | -o=arg) while :; do - ## '--option=value' should shift once and '--option value' should shift twice - ## but at this point it is not possible to be sure if option requires an argument - ## reset shift to zero, at the end, if it is still 0, it will be assigned to one - ## has to be zero here so we can check later if option argument is separated by space ' ' or equal sign '=' shift_n="" + # shellcheck disable=SC2034 opt_orig="${1}" ## save opt orig for error message to understand which opt failed - case "${opt_orig}" in - --) shift 1; break;; ## stop option parsing - --*=*) opt="${1%=*}"; opt="${opt#*--}"; arg="${1#*=}"; shift_n=1;; ## long option '--sleep=1' - -*=*) opt="${1%=*}"; opt="${opt#*-}"; arg="${1#*=}"; shift_n=1;; ## short option '-s=1' - --*) opt="${1#*--}"; arg="${2}";; ## long option '--sleep 1' - -*) opt="${1#*-}"; arg="${2}";; ## short option '-s 1' - "") break;; ## options ended - *) usage;; ## not an option - esac + # shellcheck disable=SC2034 + arg_possible="${2}" ## need to pass the second positional parameter because maybe it is an argument + clean_opt "${1}" || break case "${1}" in -h|--help) usage;; - -C|--config|-C=*|--config=*) get_arg ONIONJUGGLER_CONF; export ONIONJUGGLER_CONF;; ## cli has to see the config argument, so export it "") break;; *) error_msg "Invalid option: ${opt}";; esac shift "${shift_n}" done -[ "$(id -u)" -ne 0 ] && error_msg "run as root" - -## 1. source default configuration file first -## 2. source local (user made) configuration files to override the default values -## 3. source the ONIONJUGGLER_CONF specified by the cli argument and if it empty, use the environment variable -[ ! -f /etc/onionjuggler/onionjuggler.conf ] && error_msg "Default configuration file not found: /etc/onionjuggler/onionjuggler.conf" -[ -r /etc/onionjuggler/onionjuggler.conf ] && . /etc/onionjuggler/onionjuggler.conf -for file in /etc/onionjuggler/conf.d/*.conf; do [ -f "${file}" ] && . "${file}"; done -[ -r "${ONIONJUGGLER_CONF}" ] && . "${ONIONJUGGLER_CONF}" - -## if any of the configurations are empty, use default ones -## system -: "${su_cmd:="sudo"}" -: "${dialog_box:="dialog"}" -: "${webserver:="nginx"}" -: "${webserver_conf:="/etc/nginx/sites-enabled"}" -: "${website_dir:="/var/www"}" -## tor defaults -: "${tor_user:="debian-tor"}" -: "${tor_data_dir:="/var/lib/tor"}"; tor_data_dir="${tor_data_dir%*/}" -: "${tor_data_dir_services:="${tor_data_dir}/services"}"; tor_data_dir_services="${tor_data_dir_services%*/}" -: "${tor_data_dir_auth:="${tor_data_dir}/onion_auth"}"; tor_data_dir_auth="${tor_data_dir_auth%*/}" +[ "$(id -u)" -ne 0 ] && error_msg "run as root" ## First try environment variables [SUDO|DOAS]_EDITOR, if empty try VISUAL, if empty try EDITOR, if empty use Vi eval PRIVILEGED_EDITOR='$'"$(printf %s"${su_cmd##*/}" | tr '[:lower:]' '[:upper:]')_EDITOR" editor="${PRIVILEGED_EDITOR:-"${VISUAL:-"${EDITOR:-vi}"}"}" -########################### -######## FUNCTIONS ######## - -if ! command -v onionjuggler-cli >/dev/null && [ ! -f onionjuggler-cli ]; then - error_msg "onionjuggler-cli not found on PATH!" +if ! has onionjuggler-cli || ! has onionjuggler-tui; then + error_msg "onionjuggler-cli and onionjuggler-tui must be installed" fi -! command -v "${dialog_box}" >/dev/null && error_msg "dialog_box=${dialog_box} not found on PATH." +has "${dialog_box}" || error_msg "${dialog_box} is not installed" case "${dialog_box}" in dialog) @@ -147,8 +78,12 @@ case "${dialog_box}" in *) error_msg "dialog_box can be either 'dialog' or 'whiptail', not '${dialog_box}'" esac +########################### +######## FUNCTIONS ######## + ## clear the screen after exiting trap 'tput reset' EXIT +trap 'tput reset; exit' INT return_to_tui(){ notice "${yellow}Press ${underline}ENTER${nounderline}${yellow} to return to the TUI! ${nocolor}" @@ -208,9 +143,9 @@ check_folder_is_not_empty(){ title="OnionJuggler - Main Menu" menu="\nUse enter/spacebar to select:" backtitle="OnionJuggler - Onion Services Manager" -if check_folder_is_not_empty "${tor_data_dir_services}"; then +if ! check_folder_is_not_empty "${tor_data_dir_services}"; then choice_main="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ - "${ok_flag}" "Select" "${no_cancel_flag}" --menu "${menu}" "$((13+whiptail_height))" 80 5 \ + "${ok_flag}" "Select" "${no_cancel_flag}" --menu "${menu}" "$((13+whiptail_height))" 65 5 \ "QUIT" "Exit menu and clear the screen" \ "ENABLE" "Enable a onion service" \ "AUTH_CLIENT" "Manage your client keys of remote service" \ @@ -219,7 +154,7 @@ if check_folder_is_not_empty "${tor_data_dir_services}"; then 3>&1 1>&2 2>&3)" else choice_main="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ - "${ok_flag}" "Select" "${no_cancel_flag}" --menu "${menu}" "$((19+whiptail_height))" 80 11 \ + "${ok_flag}" "Select" "${no_cancel_flag}" --menu "${menu}" "$((18+whiptail_height))" 70 10 \ "QUIT" "Exit menu and clear the screen" \ "ENABLE" "Enable an onion service" \ "DISABLE" "Disable chosen onion service" \ @@ -229,7 +164,6 @@ else "CREDENTIALS" "See credentials (onion address, authorized clients)" \ "WEBSERVER" "Serve a website folder on onion domain" \ "VANGUARDS" "Add to protect against traffic analysis" \ - "LOCATION" "Guided steps to add onion-location to your plainnet website" \ "BACKUP" "Create or integrate backup" \ 3>&1 1>&2 2>&3)" fi @@ -327,35 +261,41 @@ PURGE: Service will be permanently disabled, all the directory data, including h AUTH_SERVER) - i=0 - service_list="" - for service in "${tor_data_dir_services}"/*; do - service="${service##*/}" - ## only include services that have at least one client - if ! check_folder_is_not_empty "${tor_data_dir_services}/${service}/authorized_clients/"; then - i=$((i+1)) - onion_hostname="$(grep ".onion" "${tor_data_dir_services}"/"${service}"/hostname 2>/dev/null)" - service_list_menu="$(printf "%s\n%s\n%s\n" "${service_list}" "${service}" "${onion_hostname%.onion}")" - service_list_checklist="$(printf "%s\n%s\n%s\n%s\n" "${service_list}" "${service}" "${onion_hostname%.onion}" 0)" - fi - done - title="Client Authorization - Server" - menu="\nManage client authorization of your services:" - if [ -z "${service_list_menu}" ]; then - auth_type="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ - "${ok_flag}" "Select" "${cancel_flag}" "Cancel" --menu "${menu}" "$((10+whiptail_height))" 60 2 \ - "GEN" "Generate a new key pair (public and private key)" \ - "PUB" "Register the client public key" \ - 3>&1 1>&2 2>&3)" + option_plugin="onionjuggler-cli-auth-server" + if ! has "${option_plugin}"; then + text="${option_plugin} is not installed" + "${dialog_box}" --backtitle "${backtitle}" --title "${title}" --msgbox "${text}" 10 50 else - auth_type="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ - "${ok_flag}" "Select" "${cancel_flag}" "Cancel" --menu "${menu}" "$((13+whiptail_height))" 60 5 \ - "LIST" "List all clients for specific HiddenService" \ - "EDIT" "Edit an existing authorization" \ - "GEN" "Generate a new key pair (public and private key)" \ - "PUB" "Register the client public key" \ - "DEL" "Remove a client" \ - 3>&1 1>&2 2>&3)" + i=0 + service_list="" + for service in "${tor_data_dir_services}"/*; do + service="${service##*/}" + ## only include services that have at least one client + if ! check_folder_is_not_empty "${tor_data_dir_services}/${service}/authorized_clients/"; then + i=$((i+1)) + onion_hostname="$(grep ".onion" "${tor_data_dir_services}"/"${service}"/hostname 2>/dev/null)" + service_list_menu="$(printf "%s\n%s\n%s\n" "${service_list}" "${service}" "${onion_hostname%.onion}")" + service_list_checklist="$(printf "%s\n%s\n%s\n%s\n" "${service_list}" "${service}" "${onion_hostname%.onion}" 0)" + fi + done + title="Client Authorization - Server" + menu="\nManage client authorization of your services:" + if [ -z "${service_list_menu}" ]; then + auth_type="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ + "${ok_flag}" "Select" "${cancel_flag}" "Cancel" --menu "${menu}" "$((10+whiptail_height))" 60 2 \ + "GEN" "Generate a new key pair (public and private key)" \ + "PUB" "Register the client public key" \ + 3>&1 1>&2 2>&3)" + else + auth_type="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ + "${ok_flag}" "Select" "${cancel_flag}" "Cancel" --menu "${menu}" "$((13+whiptail_height))" 60 5 \ + "LIST" "List all clients for specific HiddenService" \ + "EDIT" "Edit an existing authorization" \ + "GEN" "Generate a new key pair (public and private key)" \ + "PUB" "Register the client public key" \ + "DEL" "Remove a client" \ + 3>&1 1>&2 2>&3)" + fi fi case "${auth_type}" in @@ -489,30 +429,36 @@ PURGE: Service will be permanently disabled, all the directory data, including h AUTH_CLIENT) - i=0 - for onion_auth in "${tor_data_dir_auth}"/*; do - onion_auth="${onion_auth##*/}" - onion_auth_name=${onion_auth%.auth_private} - i=$((i+1)) - onion_auth_list="$(printf "%s\n%s\n%s\n" "${onion_auth_list}" "${onion_auth_name}" ".")" - done - title="Client Authorization - Client" - menu="\nAs the onion service client:" - if [ -z "${onion_auth_list}" ]; then - auth_type="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ - "${ok_flag}" "Select" "${cancel_flag}" "Cancel" --menu "${menu}" "$((10+whiptail_height))" 80 2 \ - "GEN" "Generate a new key pair (public and private key)" \ - "PRIV" "Register your private key" \ - 3>&1 1>&2 2>&3)" + option_plugin="onionjuggler-cli-auth-client" + if ! has "${option_plugin}"; then + text="${option_plugin} is not installed" + "${dialog_box}" --backtitle "${backtitle}" --title "${title}" --msgbox "${text}" 10 50 else - auth_type="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ - "${ok_flag}" "Select" "${cancel_flag}" "Cancel" --menu "${menu}" "$((13+whiptail_height))" 80 5 \ - "LIST" "List all the authorizations inside ClientOnionAuthDir" \ - "EDIT" "Edit your client authorizations" \ - "GEN" "Generate a new key pair (public and private key)" \ - "PRIV" "Register your private key" \ - "DEL" "Remove expired authorization" \ - 3>&1 1>&2 2>&3)" + i=0 + for onion_auth in "${tor_data_dir_auth}"/*; do + onion_auth="${onion_auth##*/}" + onion_auth_name=${onion_auth%.auth_private} + i=$((i+1)) + onion_auth_list="$(printf "%s\n%s\n%s\n" "${onion_auth_list}" "${onion_auth_name}" ".")" + done + title="Client Authorization - Client" + menu="\nAs the onion service client:" + if [ -z "${onion_auth_list}" ]; then + auth_type="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ + "${ok_flag}" "Select" "${cancel_flag}" "Cancel" --menu "${menu}" "$((10+whiptail_height))" 80 2 \ + "GEN" "Generate a new key pair (public and private key)" \ + "PRIV" "Register your private key" \ + 3>&1 1>&2 2>&3)" + else + auth_type="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ + "${ok_flag}" "Select" "${cancel_flag}" "Cancel" --menu "${menu}" "$((13+whiptail_height))" 80 5 \ + "LIST" "List all the authorizations inside ClientOnionAuthDir" \ + "EDIT" "Edit your client authorizations" \ + "GEN" "Generate a new key pair (public and private key)" \ + "PRIV" "Register your private key" \ + "DEL" "Remove expired authorization" \ + 3>&1 1>&2 2>&3)" + fi fi case "${auth_type}" in @@ -584,36 +530,22 @@ PURGE: Service will be permanently disabled, all the directory data, including h ;; - LOCATION) - service_menu menu "Location" - if [ -n "${service_name_list}" ]; then - title="Onion-Location" - choice_action="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ - "${ok_flag}" "Select" "${cancel_flag}" "Exit" --menu "${menu}" "$((11+whiptail_height))" 50 3 \ - "NGINX" "Nginx web server header" \ - "APACHE2" "Apache2 web server header" \ - "HTML" "Html http-equiv attribute" \ + WEBSERVER) + option_plugin="onionjuggler-cli-web" + if ! has "${option_plugin}"; then + text="${option_plugin} is not installed" + "${dialog_box}" --backtitle "${backtitle}" --title "${title}" --msgbox "${text}" 10 50 + else + { [ "${webserver}" != "nginx" ] && [ "${webserver}" != "apache2" ]; } && error_msg "webserver can be either 'nginx' or 'apache2', not '${webserver}'" + title="Web server - Status" + menu="\nWould you like to enable or disable serving a website?" + status="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" --menu "${menu}" "$((12+whiptail_height))" 50 3 \ + "LIST" "List enabled websites" \ + "ON" "Serve website" \ + "OFF" "Stop serving website" \ 3>&1 1>&2 2>&3)" - if [ -n "${choice_action}" ]; then - tput reset - action="$(printf %s"${choice_action}" | tr '[:upper:]' '[:lower:]')" - onionjuggler-cli --location "--${action}" --service "${service_name_list}" - return_to_tui - fi + status="$(printf %s"${status}" | tr '[:upper:]' '[:lower:]')" fi - ;; - - - WEBSERVER) - { [ "${webserver}" != "nginx" ] && [ "${webserver}" != "apache2" ]; } && error_msg "webserver can be either 'nginx' or 'apache2', not '${webserver}'" - title="Web server - Status" - menu="\nWould you like to enable or disable serving a website?" - status="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" --menu "${menu}" "$((12+whiptail_height))" 50 3 \ - "LIST" "List enabled websites" \ - "ON" "Serve website" \ - "OFF" "Stop serving website" \ - 3>&1 1>&2 2>&3)" - status="$(printf %s"${status}" | tr '[:upper:]' '[:lower:]')" case "${status}" in list) tput reset @@ -669,18 +601,24 @@ PURGE: Service will be permanently disabled, all the directory data, including h VANGUARDS) - title="Vanguards" - if test -d "${tor_data_dir}/vanguards"; then - choice_action="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ - "${ok_flag}" "Select" "${cancel_flag}" "Exit" --menu "${menu}" "$((9+whiptail_height))" 50 1 \ - "ON" "Use Vanguards protections" 3>&1 1>&2 2>&3)" + option_plugin="onionjuggler-cli-vanguards" + if ! has "${option_plugin}"; then + text="${option_plugin} is not installed" + "${dialog_box}" --backtitle "${backtitle}" --title "${title}" --msgbox "${text}" 10 50 else - choice_action="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ - "${ok_flag}" "Select" "${cancel_flag}" "Exit" --menu "${menu}" "$((11+whiptail_height))" 50 3 \ - "LIST" "Follow Vanguards logs" \ - "ON" "Upgrade Vanguards configuration and repository" \ - "OFF" "Delete the Vanguards directory" \ - 3>&1 1>&2 2>&3)" + title="Vanguards" + if test -d "${tor_data_dir}/vanguards"; then + choice_action="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ + "${ok_flag}" "Select" "${cancel_flag}" "Exit" --menu "${menu}" "$((9+whiptail_height))" 50 1 \ + "ON" "Use Vanguards protections" 3>&1 1>&2 2>&3)" + else + choice_action="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ + "${ok_flag}" "Select" "${cancel_flag}" "Exit" --menu "${menu}" "$((11+whiptail_height))" 50 3 \ + "LIST" "Follow Vanguards logs" \ + "ON" "Upgrade Vanguards configuration and repository" \ + "OFF" "Delete the Vanguards directory" \ + 3>&1 1>&2 2>&3)" + fi fi if [ "${choice_action}" = "LOGS" ]; then tput reset @@ -695,17 +633,23 @@ PURGE: Service will be permanently disabled, all the directory data, including h BACKUP) - title="Backup" - choice_action="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ - "${ok_flag}" "Select" "${cancel_flag}" "Exit" --menu "${menu}" "$((11+whiptail_height))" 50 2 \ - "INTEGRATE" "Integrate backup to system" \ - "CREATE" "Create backup" \ - 3>&1 1>&2 2>&3)" - if [ -n "${choice_action}" ]; then - action="$(printf %s"${choice_action}" | tr '[:upper:]' '[:lower:]')" - tput reset - onionjuggler-cli --backup --"${action}" - return_to_tui + option_plugin="onionjuggler-cli-backup" + if ! has "${option_plugin}"; then + text="${option_plugin} is not installed" + "${dialog_box}" --backtitle "${backtitle}" --title "${title}" --msgbox "${text}" 10 50 + else + title="Backup" + choice_action="$("${dialog_box}" --clear --backtitle "${backtitle}" --title "${title}" \ + "${ok_flag}" "Select" "${cancel_flag}" "Exit" --menu "${menu}" "$((11+whiptail_height))" 50 2 \ + "INTEGRATE" "Integrate backup to system" \ + "CREATE" "Create backup" \ + 3>&1 1>&2 2>&3)" + if [ -n "${choice_action}" ]; then + action="$(printf %s"${choice_action}" | tr '[:upper:]' '[:lower:]')" + tput reset + onionjuggler-cli --backup --"${action}" + return_to_tui + fi fi ;; diff --git a/usr/share/onionjuggler/defaults.sh b/usr/share/onionjuggler/defaults.sh new file mode 100644 index 0000000..92efd57 --- /dev/null +++ b/usr/share/onionjuggler/defaults.sh @@ -0,0 +1,437 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 + +## Define default values and functions + + +################### +#### VARIABLES #### + +## colors +nocolor="\033[0m" +bold="\033[1m" +#nobold="\033[22m" +underline="\033[4m" +nounderline="\033[24m" +red="\033[31m" +green="\033[32m" +yellow="\033[33m" +blue="\033[34m" +magenta="\033[35m" +cyan="\033[36m" +## signals +get_intr="$(stty -a | sed -n '/.*intr = / {s///;s/;.*$//;p;}')" + +## 1. source default configuration file first +## 2. source local (user made) configuration files to override the default values +[ ! -f /etc/onionjuggler/onionjuggler.conf ] && error_msg "Default configuration file not found: /etc/onionjuggler/onionjuggler.conf" +for file in /etc/onionjuggler/onionjuggler.conf /etc/onionjuggler/conf.d/*.conf; do + [ -r "${file}" ] && . "${file}" +done + +## : ${var:="value"} -> initialize the variable (SC2154) and if empty or unset, use default values +## var=${var%*/} -> removes the trailing slash "/" at the end of directories variables + +## system +: "${su_cmd:="sudo"}" +: "${openssl_cmd:="openssl"}" +: "${webserver:="nginx"}" +: "${webserver_conf:="/etc/nginx/sites-enabled"}" +: "${website_dir:="/var/www"}"; website_dir="${website_dir%*/}" +: "${vanguards_commit:="10942de93f6578f8303f60014f34de2fca345545"}" + +## tor defaults +: "${daemon_control:="systemctl"}"; daemon_control="${daemon_control%*/}" +: "${tor_daemon:="tor@default"}" +: "${tor_user:="debian-tor"}" +: "${tor_conf_user_group:="root:root"}" +: "${tor_data_dir:="/var/lib/tor"}"; tor_data_dir="${tor_data_dir%*/}" +: "${tor_data_dir_services:="${tor_data_dir}/services"}"; tor_data_dir_services="${tor_data_dir_services%*/}" +: "${tor_data_dir_auth:="${tor_data_dir}/onion_auth"}"; tor_data_dir_auth="${tor_data_dir_auth%*/}" +: "${tor_conf_dir:="/etc/tor"}"; tor_conf_dir="${tor_conf_dir%*/}" +: "${tor_conf:="${tor_conf_dir}/torrc"}" +: "${tor_control_port:="9051"}" ## only the port, not the host +: "${tor_backup_dir:="/var/lib/onionjuggler/backup"}"; tor_backup_dir="${tor_backup_dir%*/}" +: "${tor_hiddenserviceport_target_addr:="127.0.0.1"}" + + +################### +#### FUNCTIONS #### + +## display error message with instructions to use the script correctly. +#notice(){ printf %s"${me}: ${1}\n" 1>&2; } +notice(){ printf %s"${1}\n" 1>&2; } +error_msg(){ notice "${red}error: ${1}${nocolor}"; exit 1; } + + +## This is just a simple wrapper around 'command -v' to avoid +## spamming '>/dev/null' throughout this function. This also guards +## against aliases and functions. +## https://github.com/dylanaraps/pfetch/blob/pfetch#L53 +has() { + _cmd="$(command -v "${1}")" 2>/dev/null || return 1 + [ -x "${_cmd}" ] || return 1 +} + + +############################### +########### getopt ############ + +## this getopts might seem complex, so check this template +## https://github.com/nyxnor/scripts/blob/master/getopts.sh + + +## check if argument is within range +## usage: +## $ range_arg key "1-5" +## $ range_arg key "1" "2" "3" "4" "5" +## $ range_arg key "a-cA-C" +## $ range_arg key "a" "b" "c" "A" "B" "C" +range_arg(){ + list="${*}" + eval var='$'"${1}" + range="${list#"${1} "}" + if [ -n "${var:-}" ]; then + success=0 + for tests in ${range}; do + ## it needs to expand for ranges 'a-z' to be evaluated, and not considered as a value to be used + # shellcheck disable=SC2295 + [ "${var%%*[^${tests}]*}" ] && success=1 + done + ## if not withing range, fail and show the fixed range that can be used + [ ${success} -ne 1 ] && error_msg "Option '${opt_orig}' can not be '${var}'! It can only be: ${range}." + fi +} + +## if option requires argument, check if it was provided, if yes, assign the arg to the opt +## $arg was already assigned, and if valid, will use it for the key value +## usage: get_arg key +get_arg(){ + ## if argument is empty or starts with '-', fail as it possibly is an option + case "${arg}" in ""|-*) error_msg "Option '${opt_orig}' requires an argument.";; esac + ## assign + value="${arg}" + ## Escaping quotes is needed because else it will fail if the argument is quoted + # shellcheck disable=SC2140 + eval "${1}"="\"${value}\"" + + ## shift positional argument two times, as this option demands argument, unless they are separated by equal sign '=' + ## shift_n default value was assigned when trimming hifens '--' from the options + ## if shift_n is equal to zero, '--option arg' + ## if shift_n is not equal to zero, '--option=arg' + [ -z "${shift_n}" ] && shift_n=2 +} + +## '--option=value' should shift once and '--option value' should shift twice +## but at this point it is not possible to be sure if option requires an argument +## reset shift to zero, at the end, if it is still 0, it will be assigned to one +## has to be zero here so we can check later if option argument is separated by space ' ' or equal sign '=' +clean_opt(){ + case "${opt_orig}" in + --) shift 1; return 1;; ## stop option parsing + --*=*) opt="${opt_orig%=*}"; opt="${opt#*--}"; arg="${opt_orig#*=}"; shift_n=1;; ## long option '--sleep=1' + -*=*) opt="${opt_orig%=*}"; opt="${opt#*-}"; arg="${opt_orig#*=}"; shift_n=1;; ## short option '-s=1' + --*) opt="${opt_orig#*--}"; arg="${arg_possible}";; ## long option '--sleep 1' + -*) opt="${opt_orig#*-}"; arg="${arg_possible}";; ## short option '-s 1' + "") return 1;; ## options ended + *) usage;; ## not an option + esac +} + +############################### + + +## http://sed.sourceforge.net/local/docs/emulating_unix.txt +## tac is not posix +tac(){ + sed '1!G;h;$!d' "${1}" +} + +## 'cat -s' is not posix +cat_squeeze_blank(){ + while :; do + case "${1}" in + "/"*|[[:alnum:]]*) files="${files} ${1}"; shift;; ## only consider path starting with "/" or alphanumeric + *) break;; ## made to break on pipes and everything else + esac + done + # shellcheck disable=SC2086 + sed '1s/^$//p;/./,/^$/!d' ${files} +} + +## error_msg self explanatory, tor breaks with special chars on the dir name +check_service_name(){ + [ "${service%%*[^a-zA-Z0-9_.-]*}" ] || { + error_msg "Service name \"${service}\" is invalid\nIt must only contain the characters that are: + - letters (a-z A-Z) + - numbers (0-9) + - punctuations limited to hifen (-), underscore (_), dot (.)" + } +} + + +## Elegantly modify files on a temporary directory. Test the configuration with another function. +## If correct, then save file back to its original location. This avoids running with an invalid +## configuration that can make a daemon fail to reload or even start +## Limitation is file name cannot start with a number. +## $ safe_edit tmp variable +## $ safe_edit tmp tor_conf +## modify the "${tor_conf_tmp}" +## use the daemon option to verify config -f "${tor_conf_tmp}" +## $ safe_edit save tor_conf +safe_edit(){ + [ -w "${TMPDIR:="/tmp"}" ] || export TMPDIR="~" + TMPDIR="${TMPDIR%*/}" + key="${2}" + eval file="$(printf '%s\n' '$'"${key}")" + case "${1}" in + tmp) + file_name_tmp="$(mktemp "${TMPDIR}/${file##*/}.XXXXXX")" + notice "Saving a copy of ${file} to ${file_name_tmp}" + chown "${tor_conf_user_group}" "${file_name_tmp}" + cp "${file}" "${file_name_tmp}" + ## assign variable_tmp + eval "${key}"_tmp="${file_name_tmp}" + # shellcheck disable=SC2064 + trap "printf %s\"Exiting script ${me}\nDeleting ${file_name_tmp}\n\"; rm -f ${file_name_tmp}" EXIT INT TERM + ;; + save) + ## get variable_tmp file + eval file_name_tmp='$'"${key}_tmp" + if cmp -s "${file_name_tmp}" "${file}"; then + notice "File ${file_name_tmp} do not differ from ${file}" + notice "Not writing back to original location.${nocolor}" + rm -f "${file_name_tmp}" + else + notice "Moving ${file_name_tmp} back to its original location ${file}" + mv "${file_name_tmp}" "${file}" + fi + ;; + esac +} + + +## Verify tor configuration of the temporary file and if variable is empty, use the main configuration, if wrong, exit. +verify_config_tor(){ + config="${tor_conf_tmp:-"${tor_conf}"}" + ## if User is set on the config, then run tor as root + grep -q "^User" "${config}" && su_tor_cmd="${su_cmd}" + ## user may not be on this config, but on another, so run tor as its user if $su_tor_cmd is empty + : "${su_tor_cmd:="${su_cmd} -u ${tor_user}"}" + notice "Verifying tor configuration file ${config}" + ! ${su_tor_cmd} tor -f "${config}" --verify-config --hush && error_msg "aborting: configuration is invalid" + notice "${green}Configuration OK${nocolor}" + [ -n "${tor_conf_tmp}" ] && safe_edit save tor_conf +} + + +## TODO: vinculate with verify_config_tor() +## TODO: parse this with the modified file and without the original one +## get files tor will read +read_tor_files(){ + if test -f /lib/systemd/system/tor@default.service; then + tor_start_command="$(grep "ExecStart=" /lib/systemd/system/tor@default.service | sed "s/ExecStart=//")" + elif test -f /lib/systemd/system/tor.service; then + tor_start_command="$(grep "ExecStart=" /lib/systemd/system/tor.service | sed "s/ExecStart=//")" + fi + tor_verify_config_output="$(${tor_start_command:="tor"} --verify-config)" + tor_config_files="$(printf '%s\n' "${tor_verify_config_output}" | grep -E " Read configuration file [^ ]*| Including configuration file [^ ]*" | awk '{print $NF}' | sed "s/\"//;s/\".//;s/\/\//\//" | tr "\n" " ")" +} + + +## set correct permissions for tor directories and files +## find helps do the job because it can segreggate directories from files +set_owner_permission(){ + ## data + chown -R "${tor_user}:${tor_user}" "${tor_data_dir}" + find "${tor_data_dir}" -type d -exec chmod 700 {} \; + find "${tor_data_dir}" -type f -exec chmod 600 {} \; + ## conf + chown -R "${tor_conf_user_group}" "${tor_conf_dir}" + find "${tor_conf_dir}" -type d -exec chmod 755 {} \; + find "${tor_conf_dir}" -type f -exec chmod 644 {} \; + +} + + +# reloads tor by default or forces to restart if $1 is not empty +# shellcheck disable=SC2120 +signal_tor(){ + verify_config_tor + set_owner_permission + ## default signal is to reload, but if restart was specified, use it + : "${signal:="reload"}" + [ "${signal}" = "r" ] && signal="reload" + [ "${signal}" = "R" ] && signal="restart" + printf "\n" + notice "${signal}ing tor, please be patient." + notice "Process hanged? Press (${get_intr}) to abort and maintain previous configuration." + case "${daemon_control}" in + systemctl|sv|rcctl) "${daemon_control}" "${signal}" "${tor_daemon}";; + service) "${daemon_control}" "${tor_daemon}" "${signal}";; + /etc/rc.d) "${daemon_control}"/"${tor_daemon}" "${signal}";; + *) error_msg "daemon_control value not supported: ${daemon_control}" + esac + [ "${?}" -eq 1 ] && error_msg "Failed to ${signal} tor. Check logs first, correct the problem them restart tor." + notice "${green}${signal}ed tor succesfully!${nocolor}" + printf "\n" +} + + +## check if variable is integer +is_integer(){ printf %d "${1}" >/dev/null 2>&1 || error_msg "Not an integer: ${1}" ; } + + +## checks if the target is valid. +## Address range from 0.0.0.0 to 255.255.255.255. Port ranges from 0 to 65535 +## this is not perfect but it is better than nothing +is_addr_port(){ + addr_port="${1}" + port="${addr_port##*:}" + addr="${addr_port%%:*}" + + printf %d "${port}" >/dev/null 2>&1 || error_msg "'${port}' is not a valid port, not an integer" + { [ "${port}" -gt 0 ] && [ "${port}" -le 65535 ]; } || \ + error_msg "${port} is not a valid port, not within range: 0-65535" + + for quad in $(printf '%s\n' "${addr}" | tr "." " "); do + printf %d "${quad}" >/dev/null 2>&1 || error_msg "${addr} is not a valid address, ${quad} is not and integer" + { [ "${quad}" -ge 0 ] && [ "${quad}" -le 255 ]; } || \ + error_msg "${addr} is not a valid address, ${quad} is not within range: 0-255" + done +} + + +## returns 1 if is not empty +## no better way to do with posix utilities +check_folder_is_not_empty(){ + dir="${1}" + if [ -d "${dir}" ] && files=$(ls -qAH -- "${dir}") && [ -z "${files}" ]; then + return 1 + else + return 0 + fi +} + + +is_service_dir_empty(){ + check_folder_is_not_empty "${tor_data_dir_services}" || error_msg "Onion services directory is empty. Create a service first before running this command again." +} + + +## test if service exists to continue the script or output error logs. +## if the service exists, will save the hostname for when requested. +test_service_exists(){ + service="${1}" + onion_hostname=$(grep ".onion" "${tor_data_dir_services}"/"${service}"/hostname 2>/dev/null) + [ -z "${onion_hostname}" ] && error_msg "Service does not exist: ${service}" +} + + +## save the clients names that are inside the /authorized_clients/ in list format (CLIENT1,CLIENT2,...) +create_client_list(){ + service="${1}" + client_name_list="" + for client_listed in "${tor_data_dir_services}/${service}/authorized_clients"/*; do + client_listed="${client_listed##*/}" + [ "${client_listed}" = "*" ] && break + client_listed="${client_listed%*.auth}" + client_name_list="$(printf '%s\n%s\n' "${client_name_list}" "${client_listed}")" + done + [ -n "${client_name_list}" ] && client_name_list="$(printf '%s\n' "${client_name_list}" | tr "\n" "," | sed "s/\,$//" | sed "s/^,//")" + client_count="" + # shellcheck disable=SC2086 + [ -n "${client_name_list}" ] && client_count="$(IFS=','; set -f -- ${client_name_list}; printf %s"${#}")" +} + + +## save the service names that have a in list format (SERV1,SERV2,...) +create_service_list(){ + for hs in "${tor_data_dir_services}"/*; do + hs="${hs##*/}" + service_name_list="$(printf '%s\n' "${service_name_list}" "${hs}")" + done +} + +## loops the parameters +## $1 must be the function to loop +## $2 normally is service, but can be any other parameter (accepts list -> SERV1,SERV2,...) +## $3 normally is client, but can be any other (accepts list -> client1,client2...) +## $ loop_list function_name ssh,xmpp,web [alice,bob] +loop_list(){ + for item in $(printf %s"${2}" | tr "," " "); do + case "${3}" in + "") "${1}" "${item}";; + *) for subitem in $(printf %s"${3}" | tr "," " "); do "${1}" "${item}" "${subitem}"; done;; + esac + done +} + +## https://github.com/koalaman/shellcheck/wiki/SC3050 +escape_printf_percent() { printf "%s\n" "$(printf '%s' "${1}" | sed "s/\%/\%/g")"; } + + +## TODO: find a better way to handle commented lines and empty lines +## the problem is that the script only stop at the next HiddenServiceDir, +## but discard every line not starting with HiddenServiceDir +## https://github.com/nyxnor/onionjuggler/issues/51 +service_block(){ + process="${1}" + service="${2}" + file="${3:-"${tor_conf_tmp}"}" + i=0 + ## print the exact match HiddenServiceDir of the requested service that must end with the service name or with "/", also prit n lines below it + match="HiddenServiceDir ${tor_data_dir_services}/${service}" + hs_found="" + hs_lines_delete="" + while IFS="$(printf '\n')" read -r line; do + [ -z "${hs_found}" ] && printf '%s\n' "${line}" | grep -q -E "^${match}$|^${match}/$" && hs_found="1" + if [ "${hs_found}" = "1" ]; then + i=$((i+1)) + case "${line}" in + "HiddenServiceStatistics"*) :;; ## relays only + "HiddenService"*) + ## break on next HiddenService configuration + { [ ${i} -gt 1 ] && [ "${line%% *}" = "HiddenServiceDir" ]; } && break + case "${process}" in + print|printf) printf '%s\n' "${line}";; + delete) + ## delete only works if hs lines are consecutive, + ## meaning no blank lines or commented lines between the wanted hs + if [ -z "${hs_lines_delete}" ]; then + hs_lines_delete="$(printf '%s\n' "${line}")" + else + hs_lines_delete="$(printf '%s\n%s\n' "${hs_lines_delete}" "${line}")" + fi + ;; + esac + ;; + esac + fi + done < "${file}" + + if [ -n "${hs_lines_delete}" ]; then + ## sed is a stream line editor, so lets make the file a single line transforming new lines to carriage return + hs_lines_delete="$(printf '%s\n' "${hs_lines_delete}" | tr "\n" "\r")" + ## then convert the file also as done above, so sed can see the file and pattern on the same format + tr "\n" "\r" < "${file}" | sed "s|${hs_lines_delete}||" | tr "\r" "\n" | tee tmpfile >/dev/null + mv tmpfile "${file}" + fi +} + +## TODO: finish: https://github.com/nyxnor/onionjuggler/issues/32 +httpd_service_block(){ + process="${1}" + service="${2}" + file="${3:-"/etc/httpd.conf"}" + i=0 + test_service_exists "${service}" + grep -A 10 "server \"${onion_hostname}\"" "${file}" | while IFS= read -r line; do + case "${process}" in + print|printf) escape_printf_percent "${line}";; + delete) [ -n "${line}" ] && sed -i'' "s|${line}||" "${file}";; + esac + escape_printf_percent "${line}" | grep -q "^}" && break + done + cat_squeeze_blank "${file}" +}