diff --git a/conf/nginx-nodejs.conf b/conf/nginx-nodejs.conf new file mode 100644 index 0000000..537cba5 --- /dev/null +++ b/conf/nginx-nodejs.conf @@ -0,0 +1,14 @@ +#sub_path_only rewrite ^__PATH__$ __PATH__/ permanent; +location / { + proxy_pass http://127.0.0.1:__PORT__/; + proxy_set_header Host $host; + + proxy_set_header X-Forwarded-Ssl on; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Scheme https; + + proxy_buffering off; + + # Include SSOWAT user panel. + include conf.d/yunohost_panel.conf.inc; +} diff --git a/conf/nodejs-watcher.path b/conf/nodejs-watcher.path new file mode 100644 index 0000000..dcc2af0 --- /dev/null +++ b/conf/nodejs-watcher.path @@ -0,0 +1,8 @@ +[Path] +Unit=__APP__-nodejs-watcher.service + +# Trigger on creation, deletion or change to a file +PathChanged=__INSTALL_DIR__ + +[Install] +WantedBy=multi-user.target diff --git a/conf/nodejs-watcher.service b/conf/nodejs-watcher.service new file mode 100644 index 0000000..7492d53 --- /dev/null +++ b/conf/nodejs-watcher.service @@ -0,0 +1,13 @@ +[Unit] +Description=__APP__ NodeJS restarter +After=network.target +StartLimitIntervalSec=10 +StartLimitBurst=5 + +[Service] +Type=oneshot +ExecStart=/usr/bin/systemctl restart __APP__-nodejs.service +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/conf/nodejs.service b/conf/nodejs.service new file mode 100644 index 0000000..20a6b08 --- /dev/null +++ b/conf/nodejs.service @@ -0,0 +1,54 @@ +[Unit] +Description=__APP__ NodeJS Server +After=network.target + +[Service] +Type=simple +User=__APP__ +Group=__APP__ +WorkingDirectory=__INSTALL_DIR__/www +StandardOutput=append:/var/log/__APP__-nodejs.log +StandardError=inherit +Environment=__YNH_NODE_LOAD_PATH__ +Environment=PORT=__PORT__ +Environment=NODE_ENV=production +ExecStartPre=__YNH_NPM__ install +ExecStartPre=__YNH_NPM__ run build +ExecStart=__YNH_NPM__ run start + +# Sandboxing options to harden security +# Depending on specificities of your service/app, you may need to tweak these +# .. but this should be a good baseline +# Details for these options: https://www.freedesktop.org/software/systemd/man/systemd.exec.html +NoNewPrivileges=yes +PrivateTmp=yes +PrivateDevices=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK +RestrictNamespaces=yes +RestrictRealtime=yes +DevicePolicy=closed +ProtectClock=yes +ProtectHostname=yes +ProtectProc=invisible +ProtectSystem=full +ProtectControlGroups=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +LockPersonality=yes +SystemCallArchitectures=native +SystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @setuid @swap @cpu-emulation @privileged + +# Denying access to capabilities that should not be relevant for webapps +# Doc: https://man7.org/linux/man-pages/man7/capabilities.7.html +CapabilityBoundingSet=~CAP_RAWIO CAP_MKNOD +CapabilityBoundingSet=~CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE +CapabilityBoundingSet=~CAP_SYS_BOOT CAP_SYS_TIME CAP_SYS_MODULE CAP_SYS_PACCT +CapabilityBoundingSet=~CAP_LEASE CAP_LINUX_IMMUTABLE CAP_IPC_LOCK +CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_WAKE_ALARM +CapabilityBoundingSet=~CAP_SYS_TTY_CONFIG +CapabilityBoundingSet=~CAP_MAC_ADMIN CAP_MAC_OVERRIDE +CapabilityBoundingSet=~CAP_NET_ADMIN CAP_NET_BROADCAST CAP_NET_RAW +CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYSLOG + +[Install] +WantedBy=multi-user.target diff --git a/config_panel.toml b/config_panel.toml index b229f0f..3aa910d 100644 --- a/config_panel.toml +++ b/config_panel.toml @@ -52,6 +52,15 @@ name = "My Webapp configuration" default = "low" help = "low: Personal usage, behind the sso. No RAM footprint when not used, but the impact on the processor can be high if many users are using the service.
medium: Low usage, few people or/and publicly accessible. Low RAM footprint, medium processor footprint when used.
high: High usage, frequently visited website. High RAM footprint, but lower on processor usage and quickly responding." + [main.nodejs] + name = "NodeJS configuration" + + [main.nodejs.nodeversion] + ask = "NodeJS version" + type = "select" + choices = ["none", "18", "20", "21"] + default = "none" + # TODO: Add protected_path as tags, which are created as permission "label (path)", so admin can protect a specific path # [main.permissions] # [main.permissions.proteced_path] diff --git a/doc/ADMIN.md b/doc/ADMIN.md index 454a9bb..b5b7547 100644 --- a/doc/ADMIN.md +++ b/doc/ADMIN.md @@ -29,3 +29,16 @@ Once logged in, under the Web directory you will see a `www` folder which contai ### Customizing the nginx configuration If you want to add tweak the nginx configuration for this app, it is recommended to edit `/etc/nginx/conf.d/__DOMAIN__.d/__ID__.d/WHATEVER_NAME.conf` (ensure that the file has the `.conf` extension) and reload the nginx after making sure that the configuration is valid using `nginx -t`. + +{% if nodeversion != 'none' %} + +### Interfacing with NodeJS + +A `package.json` should be available within the `/var/www/__APP__/www`. It is used to `npm install`, `npm run build` then `npm run start`. As such, it should at least define the dependencies and provide the `build` and `install` scripts. + +You should then start a server in `/var/www/__APP__/www/index.js`. +It should listen on the port provided through the `PORT` environment with `process.env.PORT` or statically with __PORT__. + +The server should reload its files after they change, but due to systemd's limitations, it only works for top level folders/files. +If your server does not display the right things, restart the `__APP__-nodejs` service. +{% endif %} diff --git a/doc/ADMIN_fr.md b/doc/ADMIN_fr.md index 3f3b568..92c4431 100644 --- a/doc/ADMIN_fr.md +++ b/doc/ADMIN_fr.md @@ -29,3 +29,7 @@ Après vous être connecté, sous le répertoire Web vous verrez un dossier `www ### Personnaliser la configuration nginx Si vous souhaitez ajuster la configuration nginx pour cette app, il est recommandé d'éditer `/etc/nginx/conf.d/__DOMAIN__.d/__ID__.d/WHATEVER_NAME.conf` (assurez-vous que le fichier a l'extension `.conf`) puis rechargez nginx après vous être assuré que la configuration est valide à l'aide de `nginx -t`. + +### Écouter le bon port dans NodeJS + +Le port d'écoute est accessible par le processus node au travers de la variable d'environment `PORT`. Veillez à ce que votre fichier `.js` principal le récupère bien avec `process.env.PORT` car sa valeur n'est pas prédictible. diff --git a/doc/DESCRIPTION.md b/doc/DESCRIPTION.md index de48a9d..d0c2051 100644 --- a/doc/DESCRIPTION.md +++ b/doc/DESCRIPTION.md @@ -4,4 +4,6 @@ It can also create a MySQL database - which will be backed up and restored with PHP-FPM version can also be selected among `none`, `7.4`, `8.0`, `8.1` and `8.2`. +Finally, NodeJS can alternatively be used instead of PHP, with versions `18`, `20` or `21`. + **Once installed, go to the chosen URL to know the user, domain and port you will have to use for the SFTP access.** The password is one you chosen during the installation. Under the Web directory, you will see a `www` folder which contains the public files served by this app. You can put all the files of your custom Web application inside. diff --git a/doc/DESCRIPTION_fr.md b/doc/DESCRIPTION_fr.md index 2bf7f20..8027f0e 100644 --- a/doc/DESCRIPTION_fr.md +++ b/doc/DESCRIPTION_fr.md @@ -4,4 +4,6 @@ Elle peut également créer une base de données MySQL - qui sera sauvegardée e La version de PHP-FPM peut aussi être choisie, parmi `none`, `7.4`, `8.0`, `8.1` et `8.2`. +Un serveur NodeJS peut finalement être utilisé à la place de PHP, avec les versions `18`, `20` ou `21`. + **Une fois installé, rendez-vous sur l'URL choisie pour connaître l'utilisateur, le domaine et le port que vous devrez utiliser pour l'accès SFTP.** Le mot de passe est celui que vous avez choisi lors de l'installation. Sous le répertoire Web, vous verrez un dossier `www` qui contient les fichiers publics servis par cette application. Vous pouvez mettre tous les fichiers de votre application Web personnalisée à l'intérieur. diff --git a/manifest.toml b/manifest.toml index 2be35b6..c95674f 100644 --- a/manifest.toml +++ b/manifest.toml @@ -48,10 +48,21 @@ ram.runtime = "50M" [install.phpversion] ask.en = "Choose a PHP version you want to use for your app" ask.fr = "Choisissez une version PHP que vous souhaitez utiliser pour votre application" + help.en = "You can only choose NodeJS or PHP, not both" + help.fr = "Vous ne pouvez avoir que NodeJS ou PHP, pas les deux" type = "select" choices = ["none", "7.4", "8.0", "8.1", "8.2"] default = "8.0" + [install.nodeversion] + ask.en = "Choose a NodeJS version you want to use for your app" + ask.fr = "Choisissez une version NodeJS que vous souhaitez utiliser pour votre application" + help.en = "You can only choose NodeJS or PHP, not both" + help.fr = "Vous ne pouvez avoir que NodeJS ou PHP, pas les deux" + type = "select" + choices = ["none", "18", "20", "21"] + default = "none" + [install.database] ask.en = "Do you need a database?" ask.fr = "Avez-vous besoin d'une base de données ?" @@ -64,6 +75,9 @@ ram.runtime = "50M" [resources.install_dir] + [resources.ports] + main.default = 3000 + [resources.permissions] main.url = "/" diff --git a/scripts/_common.sh b/scripts/_common.sh index d400d3d..f6b0dde 100644 --- a/scripts/_common.sh +++ b/scripts/_common.sh @@ -122,3 +122,53 @@ ynh_system_user_del_group() { gpasswd -d "$username" "$group" done } + + +ynh_setup_my_nodeapp() { + # Declare an array to define the options of this helper. + local legacy_args=ai + local -A args_array=([a]=app= [i]=install_dir=) + local app + local install_dir + + ynh_handle_getopts_args "$@" + + ynh_add_systemd_config --service="${app}-nodejs" --template="nodejs.service" + ynh_add_systemd_config --service="${app}-nodejs-watcher" --template="nodejs-watcher.service" + ynh_add_config --template="nodejs-watcher.path" --destination="/etc/systemd/system/${app}-nodejs-watcher.path" + + systemctl enable "${app}-nodejs-watcher.path" --quiet + systemctl daemon-reload + + yunohost service add "${app}-nodejs" --description="$app NodeJS Server" --log="/var/log/$app-nodejs.log" + ynh_systemd_action --service_name="${app}-nodejs" + ynh_systemd_action --service_name="${app}-nodejs-watcher" + ynh_systemd_action --service_name="${app}-nodejs-watcher.path" + + # Add the config manually because yunohost does not support custom nginx confs + ynh_add_config --template="nginx-nodejs.conf" --destination="/etc/nginx/conf.d/$domain.d/$app.conf" + ynh_store_file_checksum --file="/etc/nginx/conf.d/$domain.d/$app.conf" + ynh_systemd_action --service_name=nginx --action=reload + + # Subsequent npm install will write to this folder (as it is within $app's home) + # As such we prepare it with fitting rights + mkdir -p "$install_dir/.npm" + chown $app:$app "$install_dir/.npm" +} + +ynh_remove_my_nodeapp() { + # Declare an array to define the options of this helper. + local legacy_args=a + local -A args_array=([a]=app=) + local app + + ynh_handle_getopts_args "$@" + + yunohost service remove "${app}-nodejs" + + ynh_remove_systemd_config --service="${app}-nodejs" + ynh_remove_systemd_config --service="${app}-nodejs-watcher" + ynh_secure_remove --file="/etc/systemd/system/${app}-nodejs-watcher.path" + + ynh_remove_nodejs +} diff --git a/scripts/backup b/scripts/backup index 4e0ff8c..42bb75c 100644 --- a/scripts/backup +++ b/scripts/backup @@ -36,6 +36,17 @@ then ynh_backup --src_path="/etc/php/${phpversion}/fpm/pool.d/$app.conf" fi +#================================================= +# BACKUP THE NodeJS CONFIGURATION +#================================================= + +if [ $nodeversion != "none" ] +then + ynh_backup --src_path="/etc/systemd/system/${app}-nodejs.service" + ynh_backup --src_path="/etc/systemd/system/${app}-nodejs-watcher.service" + ynh_backup --src_path="/etc/systemd/system/${app}-nodejs-watcher.path" +fi + #================================================= # BACKUP THE MYSQL DATABASE #================================================= diff --git a/scripts/config b/scripts/config index 21b6f17..4089396 100644 --- a/scripts/config +++ b/scripts/config @@ -105,6 +105,11 @@ ynh_app_config_validate() { exit 0 fi fi + + if [ "${changed[nodeversion]}" == "true" ] && [ $nodeversion != "none" ] && [ $phpversion != "none" ] + then + ynh_die --message="You cannot have both PHP and NodeJS, choose only one." + fi } ynh_app_config_apply() { @@ -152,6 +157,19 @@ ynh_app_config_apply() { then ynh_add_fpm_config --phpversion=$phpversion --usage=$fpm_usage --footprint=$fpm_footprint fi + + if [ "${changed[nodeversion]}" == "true" ] + then + if [ "$nodeversion" != "none" ] + then + ynh_install_nodejs --nodejs_version=$nodeversion + ynh_use_nodejs + export port=$(ynh_app_setting_get $app port) + ynh_setup_my_nodeapp --app=$app --install_dir=$install_dir + else + ynh_remove_my_nodeapp --app=$app + fi + fi } ynh_app_config_run $1 diff --git a/scripts/install b/scripts/install index 9ef3cba..78ef054 100644 --- a/scripts/install +++ b/scripts/install @@ -23,6 +23,7 @@ ssh_port=$(grep "^Port" /etc/ssh/sshd_config | awk '{print $2}') ynh_script_progression --message="Validating installation parameters..." --weight=2 [ $with_sftp -eq 0 ] || [ "$password" != "" ] || ynh_die --message="You need to set a password to enable SFTP" +[ $phpversion != "none" ] || [ $nodeversion != "none" ] || ynh_die --message="Either PHP or NodeJS can be used, not both" #================================================= # STORE SETTINGS FROM MANIFEST @@ -134,6 +135,22 @@ then ynh_add_fpm_config --usage=$fpm_usage --footprint=$fpm_footprint --phpversion=$phpversion fi +#================================================= +# NodeJS CONFIGURATION +#================================================= + +if [ $nodeversion != "none" ] +then + ynh_script_progression --message="Configuring NodeJS..." --weight=3 + + ynh_install_nodejs --nodejs_version=$nodeversion + ynh_use_nodejs + + ynh_add_config --template="../sources/www/package.json" --destination="$install_dir/www/package.json" + ynh_add_config --template="../sources/www/index.js" --destination="$install_dir/www/index.js" + ynh_setup_my_nodeapp --app=$app --install_dir=$install_dir +fi + #================================================= # END OF SCRIPT #================================================= diff --git a/scripts/remove b/scripts/remove index f14eaee..3a5bf61 100644 --- a/scripts/remove +++ b/scripts/remove @@ -46,6 +46,16 @@ ynh_script_progression --message="Removing PHP-FPM configuration..." # Remove the dedicated PHP-FPM config ynh_remove_fpm_config +#================================================= +# REMOVE NodeJS CONFIGURATION +#================================================= +ynh_script_progression --message="Removing NodeJS configuration..." + +if [ $nodeversion != "none" ] +then + ynh_remove_my_nodeapp --app=$app +fi + #================================================= # END OF SCRIPT #================================================= diff --git a/scripts/restore b/scripts/restore index cd4f1af..4e79a5f 100644 --- a/scripts/restore +++ b/scripts/restore @@ -81,18 +81,36 @@ then ynh_restore_file --origin_path="/etc/php/${phpversion}/fpm/pool.d/$app.conf" fi +#================================================= +# RESTORE THE NodeJS CONFIGURATION +#================================================= + +if [ $nodeversion != "none" ] +then + ynh_restore_file --origin_path="/etc/systemd/system/${app}-nodejs.service" + ynh_restore_file --origin_path="/etc/systemd/system/${app}-nodejs-watcher.service" + ynh_restore_file --origin_path="/etc/systemd/system/${app}-nodejs-watcher.path" + mkdir -p "$install_dir"/.npm + chown -R $app:$app "$install_dir"/.npm +fi + #================================================= # GENERIC FINALIZATION #================================================= # RELOAD NGINX AND PHP-FPM #================================================= -ynh_script_progression --message="Reloading NGINX web server and PHP-FPM..." +ynh_script_progression --message="Reloading NGINX and the server..." if [ $phpversion != "none" ] then ynh_systemd_action --service_name=php${phpversion}-fpm --action=reload fi +if [ $nodeversion != "none" ] +then + ynh_systemd_action --service_name=$app-nodejs --action=reload +fi + ynh_systemd_action --service_name=nginx --action=reload #================================================= diff --git a/scripts/upgrade b/scripts/upgrade index ef64b1e..b81cd30 100644 --- a/scripts/upgrade +++ b/scripts/upgrade @@ -68,6 +68,12 @@ if [ -z "$phpversion" ]; then ynh_app_setting_set --app=$app --key=phpversion --value=$phpversion fi +# If phpversion doesn't exist, create it. We assume it is the default system one. +if [ -z "$nodeversion" ]; then + nodeversion="none" + ynh_app_setting_set --app=$app --key=nodeversion --value=$nodeversion +fi + # Delete old user if [ -n "$(ynh_app_setting_get --app=$app --key=user)" ] then @@ -103,8 +109,17 @@ then fi # Create a dedicated NGINX config -ynh_add_nginx_config -ynh_add_config --template="example-custom-nginx-config.conf" --destination="$nginx_extra_conf_dir/sample.conf" +# Use a custom nginx config when using nodejs as it is incompatible with the html/php one +if [ $nodeversion == "none" ] +then + ynh_add_nginx_config + ynh_add_config --template="example-custom-nginx-config.conf" --destination="$nginx_extra_conf_dir/sample.conf" +else + # Add the config manually because yunohost does not support custom nginx confs + ynh_add_config --template="nginx-nodejs.conf" --destination="/etc/nginx/conf.d/$domain.d/$app.conf" + ynh_store_file_checksum --file="/etc/nginx/conf.d/$domain.d/$app.conf" + ynh_systemd_action --service_name=nginx --action=reload +fi #================================================= # CREATE DEDICATED USER @@ -156,6 +171,22 @@ setfacl -m g:$app:r-x "$install_dir" setfacl -m g:www-data:r-x "$install_dir" chmod 750 "$install_dir" +#================================================= +# NodeJS CONFIGURATION +#================================================= + +if [ $nodeversion != "none" ] +then + ynh_script_progression --message="Updating NodeJS..." --weight=3 + + ynh_install_nodejs --nodejs_version=$nodeversion + ynh_use_nodejs + + ynh_add_config --template="../sources/www/package.json" --destination="$install_dir/www/package.json" + ynh_add_config --template="../sources/www/index.js" --destination="$install_dir/www/index.js" + ynh_setup_my_nodeapp --app=$app --install_dir=$install_dir +fi + #================================================= # DEACTIVE MAINTENANCE MODE #================================================= diff --git a/sources/www/index.js b/sources/www/index.js new file mode 100644 index 0000000..82f2545 --- /dev/null +++ b/sources/www/index.js @@ -0,0 +1,19 @@ +const http = require('node:http'); +const fs = require('fs'); +const index = fs.readFileSync('index.html').toString(); + +const host = '127.0.0.1'; +var port = process.env.PORT; + port = (typeof port !== 'undefined') ? port : 3000; + +const file = index.replace("
", `

Port configuration

Your node application have to listen on port ${port}. Alternatively, you can get port var from envirronment with the following:

 process.env.PORT; 
`); + +const server = http.createServer((req, res) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.end(file); +}); + +server.listen(port, host, () => { + console.log('Web server running at http://%s:%s', host, port); +}); diff --git a/sources/www/package.json b/sources/www/package.json new file mode 100644 index 0000000..0c9a8bf --- /dev/null +++ b/sources/www/package.json @@ -0,0 +1,11 @@ +{ + "name": "www", + "version": "1.0.0", + "description": "dummy app", + "author": "", + "scripts": { + "start": "node index.js", + "build": "exit 0" + }, + "license": "ISC" +}