mirror of
https://github.com/YunoHost-Apps/galene_ynh.git
synced 2024-09-03 18:36:31 +02:00
First commit
This commit is contained in:
parent
e12b3290ce
commit
54c779a1f1
38 changed files with 7610 additions and 0 deletions
20
scripts/_common.sh
Executable file
20
scripts/_common.sh
Executable file
|
@ -0,0 +1,20 @@
|
|||
#!/bin/bash
|
||||
|
||||
#=================================================
|
||||
# COMMON VARIABLES
|
||||
#=================================================
|
||||
|
||||
# dependencies used by the app
|
||||
pkg_dependencies="deb1 deb2 php$YNH_DEFAULT_PHP_VERSION-deb1 php$YNH_DEFAULT_PHP_VERSION-deb2"
|
||||
|
||||
#=================================================
|
||||
# PERSONAL HELPERS
|
||||
#=================================================
|
||||
|
||||
#=================================================
|
||||
# EXPERIMENTAL HELPERS
|
||||
#=================================================
|
||||
|
||||
#=================================================
|
||||
# FUTURE OFFICIAL HELPERS
|
||||
#=================================================
|
107
scripts/backup
Executable file
107
scripts/backup
Executable file
|
@ -0,0 +1,107 @@
|
|||
#!/bin/bash
|
||||
|
||||
#=================================================
|
||||
# GENERIC START
|
||||
#=================================================
|
||||
# IMPORT GENERIC HELPERS
|
||||
#=================================================
|
||||
|
||||
# Keep this path for calling _common.sh inside the execution's context of backup and restore scripts
|
||||
source ../settings/scripts/_common.sh
|
||||
source /usr/share/yunohost/helpers
|
||||
|
||||
#=================================================
|
||||
# MANAGE SCRIPT FAILURE
|
||||
#=================================================
|
||||
|
||||
ynh_clean_setup () {
|
||||
### Remove this function if there's nothing to clean before calling the remove script.
|
||||
true
|
||||
}
|
||||
# Exit if an error occurs during the execution of the script
|
||||
ynh_abort_if_errors
|
||||
|
||||
#=================================================
|
||||
# LOAD SETTINGS
|
||||
#=================================================
|
||||
ynh_print_info --message="Loading installation settings..."
|
||||
|
||||
app=$YNH_APP_INSTANCE_NAME
|
||||
|
||||
final_path=$(ynh_app_setting_get --app=$app --key=final_path)
|
||||
domain=$(ynh_app_setting_get --app=$app --key=domain)
|
||||
db_name=$(ynh_app_setting_get --app=$app --key=db_name)
|
||||
phpversion=$(ynh_app_setting_get --app=$app --key=phpversion)
|
||||
|
||||
#=================================================
|
||||
# DECLARE DATA AND CONF FILES TO BACKUP
|
||||
#=================================================
|
||||
ynh_print_info --message="Declaring files to be backed up..."
|
||||
|
||||
### N.B. : the following 'ynh_backup' calls are only a *declaration* of what needs
|
||||
### to be backuped and not an actual copy of any file. The actual backup that
|
||||
### creates and fill the archive with the files happens in the core after this
|
||||
### script is called. Hence ynh_backups calls takes basically 0 seconds to run.
|
||||
|
||||
#=================================================
|
||||
# BACKUP THE APP MAIN DIR
|
||||
#=================================================
|
||||
|
||||
ynh_backup --src_path="$final_path"
|
||||
|
||||
#=================================================
|
||||
# BACKUP THE NGINX CONFIGURATION
|
||||
#=================================================
|
||||
|
||||
ynh_backup --src_path="/etc/nginx/conf.d/$domain.d/$app.conf"
|
||||
|
||||
#=================================================
|
||||
# BACKUP THE PHP-FPM CONFIGURATION
|
||||
#=================================================
|
||||
|
||||
ynh_backup --src_path="/etc/php/$phpversion/fpm/pool.d/$app.conf"
|
||||
|
||||
#=================================================
|
||||
# BACKUP FAIL2BAN CONFIGURATION
|
||||
#=================================================
|
||||
|
||||
ynh_backup --src_path="/etc/fail2ban/jail.d/$app.conf"
|
||||
ynh_backup --src_path="/etc/fail2ban/filter.d/$app.conf"
|
||||
|
||||
#=================================================
|
||||
# SPECIFIC BACKUP
|
||||
#=================================================
|
||||
# BACKUP LOGROTATE
|
||||
#=================================================
|
||||
|
||||
ynh_backup --src_path="/etc/logrotate.d/$app"
|
||||
|
||||
#=================================================
|
||||
# BACKUP SYSTEMD
|
||||
#=================================================
|
||||
|
||||
ynh_backup --src_path="/etc/systemd/system/$app.service"
|
||||
|
||||
#=================================================
|
||||
# BACKUP VARIOUS FILES
|
||||
#=================================================
|
||||
|
||||
ynh_backup --src_path="/etc/cron.d/$app"
|
||||
|
||||
ynh_backup --src_path="/etc/$app/"
|
||||
|
||||
#=================================================
|
||||
# BACKUP THE MYSQL DATABASE
|
||||
#=================================================
|
||||
ynh_print_info --message="Backing up the MySQL database..."
|
||||
|
||||
### (However, things like MySQL dumps *do* take some time to run, though the
|
||||
### copy of the generated dump to the archive still happens later)
|
||||
|
||||
ynh_mysql_dump_db --database="$db_name" > db.sql
|
||||
|
||||
#=================================================
|
||||
# END OF SCRIPT
|
||||
#=================================================
|
||||
|
||||
ynh_print_info --message="Backup script completed for $app. (YunoHost will then actually copy those files to the archive)."
|
134
scripts/change_url
Executable file
134
scripts/change_url
Executable file
|
@ -0,0 +1,134 @@
|
|||
#!/bin/bash
|
||||
|
||||
#=================================================
|
||||
# GENERIC STARTING
|
||||
#=================================================
|
||||
# IMPORT GENERIC HELPERS
|
||||
#=================================================
|
||||
|
||||
source _common.sh
|
||||
source /usr/share/yunohost/helpers
|
||||
|
||||
#=================================================
|
||||
# RETRIEVE ARGUMENTS
|
||||
#=================================================
|
||||
|
||||
old_domain=$YNH_APP_OLD_DOMAIN
|
||||
old_path=$YNH_APP_OLD_PATH
|
||||
|
||||
new_domain=$YNH_APP_NEW_DOMAIN
|
||||
new_path=$YNH_APP_NEW_PATH
|
||||
|
||||
app=$YNH_APP_INSTANCE_NAME
|
||||
|
||||
#=================================================
|
||||
# LOAD SETTINGS
|
||||
#=================================================
|
||||
ynh_script_progression --message="Loading installation settings..." --time --weight=1
|
||||
|
||||
# Needed for helper "ynh_add_nginx_config"
|
||||
final_path=$(ynh_app_setting_get --app=$app --key=final_path)
|
||||
|
||||
# Add settings here as needed by your application
|
||||
#db_name=$(ynh_app_setting_get --app=$app --key=db_name)
|
||||
#db_user=$db_name
|
||||
#db_pwd=$(ynh_app_setting_get --app=$app --key=db_pwd)
|
||||
|
||||
#=================================================
|
||||
# BACKUP BEFORE UPGRADE THEN ACTIVE TRAP
|
||||
#=================================================
|
||||
ynh_script_progression --message="Backing up the app before changing its URL (may take a while)..." --time --weight=1
|
||||
|
||||
# Backup the current version of the app
|
||||
ynh_backup_before_upgrade
|
||||
ynh_clean_setup () {
|
||||
# Remove the new domain config file, the remove script won't do it as it doesn't know yet its location.
|
||||
ynh_secure_remove --file="/etc/nginx/conf.d/$new_domain.d/$app.conf"
|
||||
|
||||
# Restore it if the upgrade fails
|
||||
ynh_restore_upgradebackup
|
||||
}
|
||||
# Exit if an error occurs during the execution of the script
|
||||
ynh_abort_if_errors
|
||||
|
||||
#=================================================
|
||||
# CHECK WHICH PARTS SHOULD BE CHANGED
|
||||
#=================================================
|
||||
|
||||
change_domain=0
|
||||
if [ "$old_domain" != "$new_domain" ]
|
||||
then
|
||||
change_domain=1
|
||||
fi
|
||||
|
||||
change_path=0
|
||||
if [ "$old_path" != "$new_path" ]
|
||||
then
|
||||
change_path=1
|
||||
fi
|
||||
|
||||
#=================================================
|
||||
# STANDARD MODIFICATIONS
|
||||
#=================================================
|
||||
# STOP SYSTEMD SERVICE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Stopping a systemd service..." --time --weight=1
|
||||
|
||||
ynh_systemd_action --service_name=$app --action="stop" --log_path="/var/log/$app/$app.log"
|
||||
|
||||
#=================================================
|
||||
# MODIFY URL IN NGINX CONF
|
||||
#=================================================
|
||||
ynh_script_progression --message="Updating NGINX web server configuration..." --time --weight=1
|
||||
|
||||
nginx_conf_path=/etc/nginx/conf.d/$old_domain.d/$app.conf
|
||||
|
||||
# Change the path in the NGINX config file
|
||||
if [ $change_path -eq 1 ]
|
||||
then
|
||||
# Make a backup of the original NGINX config file if modified
|
||||
ynh_backup_if_checksum_is_different --file="$nginx_conf_path"
|
||||
# Set global variables for NGINX helper
|
||||
domain="$old_domain"
|
||||
path_url="$new_path"
|
||||
# Create a dedicated NGINX config
|
||||
ynh_add_nginx_config
|
||||
fi
|
||||
|
||||
# Change the domain for NGINX
|
||||
if [ $change_domain -eq 1 ]
|
||||
then
|
||||
# Delete file checksum for the old conf file location
|
||||
ynh_delete_file_checksum --file="$nginx_conf_path"
|
||||
mv $nginx_conf_path /etc/nginx/conf.d/$new_domain.d/$app.conf
|
||||
# Store file checksum for the new config file location
|
||||
ynh_store_file_checksum --file="/etc/nginx/conf.d/$new_domain.d/$app.conf"
|
||||
fi
|
||||
|
||||
#=================================================
|
||||
# SPECIFIC MODIFICATIONS
|
||||
#=================================================
|
||||
# ...
|
||||
#=================================================
|
||||
|
||||
#=================================================
|
||||
# GENERIC FINALISATION
|
||||
#=================================================
|
||||
# START SYSTEMD SERVICE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Starting a systemd service..." --time --weight=1
|
||||
|
||||
ynh_systemd_action --service_name=$app --action="start" --log_path="/var/log/$app/$app.log"
|
||||
|
||||
#=================================================
|
||||
# RELOAD NGINX
|
||||
#=================================================
|
||||
ynh_script_progression --message="Reloading NGINX web server..." --time --weight=1
|
||||
|
||||
ynh_systemd_action --service_name=nginx --action=reload
|
||||
|
||||
#=================================================
|
||||
# END OF SCRIPT
|
||||
#=================================================
|
||||
|
||||
ynh_script_progression --message="Change of URL completed for $app" --time --last
|
369
scripts/install
Executable file
369
scripts/install
Executable file
|
@ -0,0 +1,369 @@
|
|||
#!/bin/bash
|
||||
|
||||
#=================================================
|
||||
# GENERIC START
|
||||
#=================================================
|
||||
# IMPORT GENERIC HELPERS
|
||||
#=================================================
|
||||
|
||||
source _common.sh
|
||||
source /usr/share/yunohost/helpers
|
||||
|
||||
#=================================================
|
||||
# MANAGE SCRIPT FAILURE
|
||||
#=================================================
|
||||
|
||||
ynh_clean_setup () {
|
||||
### Remove this function if there's nothing to clean before calling the remove script.
|
||||
true
|
||||
}
|
||||
# Exit if an error occurs during the execution of the script
|
||||
ynh_abort_if_errors
|
||||
|
||||
#=================================================
|
||||
# RETRIEVE ARGUMENTS FROM THE MANIFEST
|
||||
#=================================================
|
||||
|
||||
domain=$YNH_APP_ARG_DOMAIN
|
||||
path_url=$YNH_APP_ARG_PATH
|
||||
admin=$YNH_APP_ARG_ADMIN
|
||||
is_public=$YNH_APP_ARG_IS_PUBLIC
|
||||
language=$YNH_APP_ARG_LANGUAGE
|
||||
password=$YNH_APP_ARG_PASSWORD
|
||||
|
||||
### If it's a multi-instance app, meaning it can be installed several times independently
|
||||
### The id of the app as stated in the manifest is available as $YNH_APP_ID
|
||||
### The instance number is available as $YNH_APP_INSTANCE_NUMBER (equals "1", "2"...)
|
||||
### The app instance name is available as $YNH_APP_INSTANCE_NAME
|
||||
### - the first time the app is installed, YNH_APP_INSTANCE_NAME = ynhexample
|
||||
### - the second time the app is installed, YNH_APP_INSTANCE_NAME = ynhexample__2
|
||||
### - ynhexample__{N} for the subsequent installations, with N=3,4...
|
||||
### The app instance name is probably what interests you most, since this is
|
||||
### guaranteed to be unique. This is a good unique identifier to define installation path,
|
||||
### db names...
|
||||
app=$YNH_APP_INSTANCE_NAME
|
||||
|
||||
#=================================================
|
||||
# CHECK IF THE APP CAN BE INSTALLED WITH THESE ARGS
|
||||
#=================================================
|
||||
### About --weight and --time
|
||||
### ynh_script_progression will show to your final users the progression of each scripts.
|
||||
### In order to do that, --weight will represent the relative time of execution compared to the other steps in the script.
|
||||
### --time is a packager option, it will show you the execution time since the previous call.
|
||||
### This option should be removed before releasing your app.
|
||||
### Use the execution time, given by --time, to estimate the weight of a step.
|
||||
### A common way to do it is to set a weight equal to the execution time in second +1.
|
||||
### The execution time is given for the duration since the previous call. So the weight should be applied to this previous call.
|
||||
ynh_script_progression --message="Validating installation parameters..." --time --weight=1
|
||||
|
||||
### If the app uses NGINX as web server (written in HTML/PHP in most cases), the final path should be "/var/www/$app".
|
||||
### If the app provides an internal web server (or uses another application server such as uWSGI), the final path should be "/opt/yunohost/$app"
|
||||
final_path=/var/www/$app
|
||||
test ! -e "$final_path" || ynh_die --message="This path already contains a folder"
|
||||
|
||||
# Register (book) web path
|
||||
ynh_webpath_register --app=$app --domain=$domain --path_url=$path_url
|
||||
|
||||
#=================================================
|
||||
# STORE SETTINGS FROM MANIFEST
|
||||
#=================================================
|
||||
ynh_script_progression --message="Storing installation settings..." --time --weight=1
|
||||
|
||||
ynh_app_setting_set --app=$app --key=domain --value=$domain
|
||||
ynh_app_setting_set --app=$app --key=path --value=$path_url
|
||||
ynh_app_setting_set --app=$app --key=admin --value=$admin
|
||||
ynh_app_setting_set --app=$app --key=is_public --value=$is_public
|
||||
ynh_app_setting_set --app=$app --key=language --value=$language
|
||||
|
||||
#=================================================
|
||||
# STANDARD MODIFICATIONS
|
||||
#=================================================
|
||||
# FIND AND OPEN A PORT
|
||||
#=================================================
|
||||
ynh_script_progression --message="Configuring firewall..." --time --weight=1
|
||||
|
||||
### Use these lines if you have to open a port for the application
|
||||
### `ynh_find_port` will find the first available port starting from the given port.
|
||||
### If you're not using these lines:
|
||||
### - Remove the section "CLOSE A PORT" in the remove script
|
||||
|
||||
# Find an available port
|
||||
port=$(ynh_find_port --port=8095)
|
||||
ynh_app_setting_set --app=$app --key=port --value=$port
|
||||
|
||||
# Optional: Expose this port publicly
|
||||
# (N.B.: you only need to do this if the app actually needs to expose the port publicly.
|
||||
# If you do this and the app doesn't actually need you are CREATING SECURITY HOLES IN THE SERVER !)
|
||||
|
||||
# Open the port
|
||||
# ynh_exec_warn_less yunohost firewall allow --no-upnp TCP $port
|
||||
|
||||
#=================================================
|
||||
# INSTALL DEPENDENCIES
|
||||
#=================================================
|
||||
ynh_script_progression --message="Installing dependencies..." --time --weight=1
|
||||
|
||||
### `ynh_install_app_dependencies` allows you to add any "apt" dependencies to the package.
|
||||
### Those deb packages will be installed as dependencies of this package.
|
||||
### If you're not using this helper:
|
||||
### - Remove the section "REMOVE DEPENDENCIES" in the remove script
|
||||
### - Remove the variable "pkg_dependencies" in _common.sh
|
||||
### - As well as the section "REINSTALL DEPENDENCIES" in the restore script
|
||||
### - And the section "UPGRADE DEPENDENCIES" in the upgrade script
|
||||
|
||||
ynh_install_app_dependencies $pkg_dependencies
|
||||
|
||||
#=================================================
|
||||
# CREATE A MYSQL DATABASE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Creating a MySQL database..." --time --weight=1
|
||||
|
||||
### Use these lines if you need a database for the application.
|
||||
### `ynh_mysql_setup_db` will create a database, an associated user and a ramdom password.
|
||||
### The password will be stored as 'mysqlpwd' into the app settings,
|
||||
### and will be available as $db_pwd
|
||||
### If you're not using these lines:
|
||||
### - Remove the section "BACKUP THE MYSQL DATABASE" in the backup script
|
||||
### - Remove also the section "REMOVE THE MYSQL DATABASE" in the remove script
|
||||
### - As well as the section "RESTORE THE MYSQL DATABASE" in the restore script
|
||||
|
||||
db_name=$(ynh_sanitize_dbid --db_name=$app)
|
||||
db_user=$db_name
|
||||
ynh_app_setting_set --app=$app --key=db_name --value=$db_name
|
||||
ynh_mysql_setup_db --db_user=$db_user --db_name=$db_name
|
||||
|
||||
#=================================================
|
||||
# DOWNLOAD, CHECK AND UNPACK SOURCE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Setting up source files..." --time --weight=1
|
||||
|
||||
### `ynh_setup_source` is used to install an app from a zip or tar.gz file,
|
||||
### downloaded from an upstream source, like a git repository.
|
||||
### `ynh_setup_source` use the file conf/app.src
|
||||
|
||||
ynh_app_setting_set --app=$app --key=final_path --value=$final_path
|
||||
# Download, check integrity, uncompress and patch the source from app.src
|
||||
ynh_setup_source --dest_dir="$final_path"
|
||||
|
||||
#=================================================
|
||||
# NGINX CONFIGURATION
|
||||
#=================================================
|
||||
ynh_script_progression --message="Configuring NGINX web server..." --time --weight=1
|
||||
|
||||
### `ynh_add_nginx_config` will use the file conf/nginx.conf
|
||||
|
||||
# Create a dedicated NGINX config
|
||||
ynh_add_nginx_config
|
||||
|
||||
#=================================================
|
||||
# CREATE DEDICATED USER
|
||||
#=================================================
|
||||
ynh_script_progression --message="Configuring system user..." --time --weight=1
|
||||
|
||||
# Create a system user
|
||||
ynh_system_user_create --username=$app
|
||||
|
||||
#=================================================
|
||||
# PHP-FPM CONFIGURATION
|
||||
#=================================================
|
||||
ynh_script_progression --message="Configuring PHP-FPM..." --time --weight=1
|
||||
|
||||
### `ynh_add_fpm_config` is used to set up a PHP config.
|
||||
### You can remove it if your app doesn't use PHP.
|
||||
### `ynh_add_fpm_config` will use the files conf/php-fpm.conf
|
||||
### If you're not using these lines:
|
||||
### - You can remove these files in conf/.
|
||||
### - Remove the section "BACKUP THE PHP-FPM CONFIGURATION" in the backup script
|
||||
### - Remove also the section "REMOVE PHP-FPM CONFIGURATION" in the remove script
|
||||
### - As well as the section "RESTORE THE PHP-FPM CONFIGURATION" in the restore script
|
||||
### with the reload at the end of the script.
|
||||
### - And the section "PHP-FPM CONFIGURATION" in the upgrade script
|
||||
|
||||
# Create a dedicated PHP-FPM config
|
||||
ynh_add_fpm_config
|
||||
|
||||
#=================================================
|
||||
# SPECIFIC SETUP
|
||||
#=================================================
|
||||
# ...
|
||||
#=================================================
|
||||
|
||||
#=================================================
|
||||
# SETUP SYSTEMD
|
||||
#=================================================
|
||||
ynh_script_progression --message="Configuring a systemd service..." --time --weight=1
|
||||
|
||||
### `ynh_systemd_config` is used to configure a systemd script for an app.
|
||||
### It can be used for apps that use sysvinit (with adaptation) or systemd.
|
||||
### Have a look at the app to be sure this app needs a systemd script.
|
||||
### `ynh_systemd_config` will use the file conf/systemd.service
|
||||
### If you're not using these lines:
|
||||
### - You can remove those files in conf/.
|
||||
### - Remove the section "BACKUP SYSTEMD" in the backup script
|
||||
### - Remove also the section "STOP AND REMOVE SERVICE" in the remove script
|
||||
### - As well as the section "RESTORE SYSTEMD" in the restore script
|
||||
### - And the section "SETUP SYSTEMD" in the upgrade script
|
||||
|
||||
# Create a dedicated systemd config
|
||||
ynh_add_systemd_config
|
||||
|
||||
#=================================================
|
||||
# SETUP APPLICATION WITH CURL
|
||||
#=================================================
|
||||
|
||||
### Use these lines only if the app installation needs to be finalized through
|
||||
### web forms. We generally don't want to ask the final user,
|
||||
### so we're going to use curl to automatically fill the fields and submit the
|
||||
### forms.
|
||||
|
||||
# Set right permissions for curl install
|
||||
chown -R $app: $final_path
|
||||
|
||||
# Set the app as temporarily public for curl call
|
||||
ynh_script_progression --message="Configuring SSOwat..." --time --weight=1
|
||||
ynh_app_setting_set --app=$app --key=skipped_uris --value="/"
|
||||
# Reload SSOwat config
|
||||
yunohost app ssowatconf
|
||||
|
||||
# Reload NGINX
|
||||
ynh_systemd_action --service_name=nginx --action=reload
|
||||
|
||||
# Installation with curl
|
||||
ynh_script_progression --message="Finalizing installation..." --time --weight=1
|
||||
ynh_local_curl "/INSTALL_PATH" "key1=value1" "key2=value2" "key3=value3"
|
||||
|
||||
# Remove the public access
|
||||
if [ $is_public -eq 0 ]
|
||||
then
|
||||
ynh_app_setting_delete --app=$app --key=skipped_uris
|
||||
fi
|
||||
|
||||
#=================================================
|
||||
# MODIFY A CONFIG FILE
|
||||
#=================================================
|
||||
|
||||
### `ynh_replace_string` is used to replace a string in a file.
|
||||
### (It's compatible with sed regular expressions syntax)
|
||||
|
||||
ynh_replace_string --match_string="match_string" --replace_string="replace_string" --target_file="$final_path/CONFIG_FILE"
|
||||
|
||||
#=================================================
|
||||
# STORE THE CONFIG FILE CHECKSUM
|
||||
#=================================================
|
||||
|
||||
### `ynh_store_file_checksum` is used to store the checksum of a file.
|
||||
### That way, during the upgrade script, by using `ynh_backup_if_checksum_is_different`,
|
||||
### you can make a backup of this file before modifying it again if the admin had modified it.
|
||||
|
||||
# Calculate and store the config file checksum into the app settings
|
||||
ynh_store_file_checksum --file="$final_path/CONFIG_FILE"
|
||||
|
||||
#=================================================
|
||||
# GENERIC FINALIZATION
|
||||
#=================================================
|
||||
# SECURE FILES AND DIRECTORIES
|
||||
#=================================================
|
||||
|
||||
### For security reason, any app should set the permissions to root: before anything else.
|
||||
### Then, if write authorization is needed, any access should be given only to directories
|
||||
### that really need such authorization.
|
||||
|
||||
# Set permissions to app files
|
||||
chown -R root: $final_path
|
||||
|
||||
#=================================================
|
||||
# SETUP LOGROTATE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Configuring log rotation..." --time --weight=1
|
||||
|
||||
### `ynh_use_logrotate` is used to configure a logrotate configuration for the logs of this app.
|
||||
### Use this helper only if there is effectively a log file for this app.
|
||||
### If you're not using this helper:
|
||||
### - Remove the section "BACKUP LOGROTATE" in the backup script
|
||||
### - Remove also the section "REMOVE LOGROTATE CONFIGURATION" in the remove script
|
||||
### - As well as the section "RESTORE THE LOGROTATE CONFIGURATION" in the restore script
|
||||
### - And the section "SETUP LOGROTATE" in the upgrade script
|
||||
|
||||
# Use logrotate to manage application logfile(s)
|
||||
ynh_use_logrotate
|
||||
|
||||
#=================================================
|
||||
# INTEGRATE SERVICE IN YUNOHOST
|
||||
#=================================================
|
||||
ynh_script_progression --message="Integrating service in YunoHost..." --time --weight=1
|
||||
|
||||
### `yunohost service add` integrates a service in YunoHost. It then gets
|
||||
### displayed in the admin interface and through the others `yunohost service` commands.
|
||||
### (N.B.: this line only makes sense if the app adds a service to the system!)
|
||||
### If you're not using these lines:
|
||||
### - You can remove these files in conf/.
|
||||
### - Remove the section "REMOVE SERVICE INTEGRATION IN YUNOHOST" in the remove script
|
||||
### - As well as the section "INTEGRATE SERVICE IN YUNOHOST" in the restore script
|
||||
### - And the section "INTEGRATE SERVICE IN YUNOHOST" in the upgrade script
|
||||
|
||||
yunohost service add $app --description="A short description of the app" --log="/var/log/$app/$app.log"
|
||||
|
||||
### Additional options starting with 3.8:
|
||||
###
|
||||
### --needs_exposed_ports "$port" a list of ports that needs to be publicly exposed
|
||||
### which will then be checked by YunoHost's diagnosis system
|
||||
### (N.B. DO NOT USE THIS is the port is only internal!!!)
|
||||
###
|
||||
### --test_status "some command" a custom command to check the status of the service
|
||||
### (only relevant if 'systemctl status' doesn't do a good job)
|
||||
###
|
||||
### --test_conf "some command" some command similar to "nginx -t" that validates the conf of the service
|
||||
###
|
||||
### Re-calling 'yunohost service add' during the upgrade script is the right way
|
||||
### to proceed if you later realize that you need to enable some flags that
|
||||
### weren't enabled on old installs (be careful it'll override the existing
|
||||
### service though so you should re-provide all relevant flags when doing so)
|
||||
|
||||
#=================================================
|
||||
# START SYSTEMD SERVICE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Starting a systemd service..." --time --weight=1
|
||||
|
||||
### `ynh_systemd_action` is used to start a systemd service for an app.
|
||||
### Only needed if you have configure a systemd service
|
||||
### If you're not using these lines:
|
||||
### - Remove the section "STOP SYSTEMD SERVICE" and "START SYSTEMD SERVICE" in the backup script
|
||||
### - As well as the section "START SYSTEMD SERVICE" in the restore script
|
||||
### - As well as the section"STOP SYSTEMD SERVICE" and "START SYSTEMD SERVICE" in the upgrade script
|
||||
### - And the section "STOP SYSTEMD SERVICE" and "START SYSTEMD SERVICE" in the change_url script
|
||||
|
||||
# Start a systemd service
|
||||
ynh_systemd_action --service_name=$app --action="start" --log_path="/var/log/$app/$app.log"
|
||||
|
||||
#=================================================
|
||||
# SETUP FAIL2BAN
|
||||
#=================================================
|
||||
ynh_script_progression --message="Configuring Fail2Ban..." --time --weight=1
|
||||
|
||||
# Create a dedicated Fail2Ban config
|
||||
ynh_add_fail2ban_config --logpath="/var/log/nginx/${domain}-error.log" --failregex="Regex to match into the log for a failed login"
|
||||
|
||||
#=================================================
|
||||
# SETUP SSOWAT
|
||||
#=================================================
|
||||
ynh_script_progression --message="Configuring SSOwat..." --time --weight=1
|
||||
|
||||
# Make app public if necessary
|
||||
if [ $is_public -eq 1 ]
|
||||
then
|
||||
# unprotected_uris allows SSO credentials to be passed anyway.
|
||||
ynh_app_setting_set --app=$app --key=unprotected_uris --value="/"
|
||||
fi
|
||||
|
||||
#=================================================
|
||||
# RELOAD NGINX
|
||||
#=================================================
|
||||
ynh_script_progression --message="Reloading NGINX web server..." --time --weight=1
|
||||
|
||||
ynh_systemd_action --service_name=nginx --action=reload
|
||||
|
||||
#=================================================
|
||||
# END OF SCRIPT
|
||||
#=================================================
|
||||
|
||||
ynh_script_progression --message="Installation of $app completed" --time --last
|
141
scripts/remove
Executable file
141
scripts/remove
Executable file
|
@ -0,0 +1,141 @@
|
|||
#!/bin/bash
|
||||
|
||||
#=================================================
|
||||
# GENERIC START
|
||||
#=================================================
|
||||
# IMPORT GENERIC HELPERS
|
||||
#=================================================
|
||||
|
||||
source _common.sh
|
||||
source /usr/share/yunohost/helpers
|
||||
|
||||
#=================================================
|
||||
# LOAD SETTINGS
|
||||
#=================================================
|
||||
ynh_script_progression --message="Loading installation settings..." --time --weight=1
|
||||
|
||||
app=$YNH_APP_INSTANCE_NAME
|
||||
|
||||
domain=$(ynh_app_setting_get --app=$app --key=domain)
|
||||
port=$(ynh_app_setting_get --app=$app --key=port)
|
||||
db_name=$(ynh_app_setting_get --app=$app --key=db_name)
|
||||
db_user=$db_name
|
||||
final_path=$(ynh_app_setting_get --app=$app --key=final_path)
|
||||
|
||||
#=================================================
|
||||
# STANDARD REMOVE
|
||||
#=================================================
|
||||
# REMOVE SERVICE INTEGRATION IN YUNOHOST
|
||||
#=================================================
|
||||
|
||||
# Remove the service from the list of services known by YunoHost (added from `yunohost service add`)
|
||||
if ynh_exec_warn_less yunohost service status $app >/dev/null
|
||||
then
|
||||
ynh_script_progression --message="Removing $app service integration..." --time --weight=1
|
||||
yunohost service remove $app
|
||||
fi
|
||||
|
||||
#=================================================
|
||||
# STOP AND REMOVE SERVICE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Stopping and removing the systemd service..." --time --weight=1
|
||||
|
||||
# Remove the dedicated systemd config
|
||||
ynh_remove_systemd_config
|
||||
|
||||
#=================================================
|
||||
# REMOVE THE MYSQL DATABASE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Removing the MySQL database..." --time --weight=1
|
||||
|
||||
# Remove a database if it exists, along with the associated user
|
||||
ynh_mysql_remove_db --db_user=$db_user --db_name=$db_name
|
||||
|
||||
#=================================================
|
||||
# REMOVE DEPENDENCIES
|
||||
#=================================================
|
||||
ynh_script_progression --message="Removing dependencies..." --time --weight=1
|
||||
|
||||
# Remove metapackage and its dependencies
|
||||
ynh_remove_app_dependencies
|
||||
|
||||
#=================================================
|
||||
# REMOVE APP MAIN DIR
|
||||
#=================================================
|
||||
ynh_script_progression --message="Removing app main directory..." --time --weight=1
|
||||
|
||||
# Remove the app directory securely
|
||||
ynh_secure_remove --file="$final_path"
|
||||
|
||||
#=================================================
|
||||
# REMOVE NGINX CONFIGURATION
|
||||
#=================================================
|
||||
ynh_script_progression --message="Removing NGINX web server configuration..." --time --weight=1
|
||||
|
||||
# Remove the dedicated NGINX config
|
||||
ynh_remove_nginx_config
|
||||
|
||||
#=================================================
|
||||
# REMOVE PHP-FPM CONFIGURATION
|
||||
#=================================================
|
||||
ynh_script_progression --message="Removing PHP-FPM configuration..." --time --weight=1
|
||||
|
||||
# Remove the dedicated PHP-FPM config
|
||||
ynh_remove_fpm_config
|
||||
|
||||
#=================================================
|
||||
# REMOVE LOGROTATE CONFIGURATION
|
||||
#=================================================
|
||||
ynh_script_progression --message="Removing logrotate configuration..." --time --weight=1
|
||||
|
||||
# Remove the app-specific logrotate config
|
||||
ynh_remove_logrotate
|
||||
|
||||
#=================================================
|
||||
# CLOSE A PORT
|
||||
#=================================================
|
||||
|
||||
if yunohost firewall list | grep -q "\- $port$"
|
||||
then
|
||||
ynh_script_progression --message="Closing port $port..." --time --weight=1
|
||||
ynh_exec_warn_less yunohost firewall disallow TCP $port
|
||||
fi
|
||||
|
||||
#=================================================
|
||||
# REMOVE FAIL2BAN CONFIGURATION
|
||||
#=================================================
|
||||
ynh_script_progression --message="Removing Fail2ban configuration..." --time --weight=1
|
||||
|
||||
# Remove the dedicated Fail2Ban config
|
||||
ynh_remove_fail2ban_config
|
||||
|
||||
#=================================================
|
||||
# SPECIFIC REMOVE
|
||||
#=================================================
|
||||
# REMOVE VARIOUS FILES
|
||||
#=================================================
|
||||
|
||||
# Remove a cron file
|
||||
ynh_secure_remove --file="/etc/cron.d/$app"
|
||||
|
||||
# Remove a directory securely
|
||||
ynh_secure_remove --file="/etc/$app/"
|
||||
|
||||
# Remove the log files
|
||||
ynh_secure_remove --file="/var/log/$app/"
|
||||
|
||||
#=================================================
|
||||
# GENERIC FINALIZATION
|
||||
#=================================================
|
||||
# REMOVE DEDICATED USER
|
||||
#=================================================
|
||||
ynh_script_progression --message="Removing the dedicated system user..." --time --weight=1
|
||||
|
||||
# Delete a system user
|
||||
ynh_system_user_delete --username=$app
|
||||
|
||||
#=================================================
|
||||
# END OF SCRIPT
|
||||
#=================================================
|
||||
|
||||
ynh_script_progression --message="Removal of $app completed" --time --last
|
162
scripts/restore
Executable file
162
scripts/restore
Executable file
|
@ -0,0 +1,162 @@
|
|||
#!/bin/bash
|
||||
|
||||
#=================================================
|
||||
# GENERIC START
|
||||
#=================================================
|
||||
# IMPORT GENERIC HELPERS
|
||||
#=================================================
|
||||
|
||||
# Keep this path for calling _common.sh inside the execution's context of backup and restore scripts
|
||||
source ../settings/scripts/_common.sh
|
||||
source /usr/share/yunohost/helpers
|
||||
|
||||
#=================================================
|
||||
# MANAGE SCRIPT FAILURE
|
||||
#=================================================
|
||||
|
||||
ynh_clean_setup () {
|
||||
#### Remove this function if there's nothing to clean before calling the remove script.
|
||||
true
|
||||
}
|
||||
# Exit if an error occurs during the execution of the script
|
||||
ynh_abort_if_errors
|
||||
|
||||
#=================================================
|
||||
# LOAD SETTINGS
|
||||
#=================================================
|
||||
ynh_script_progression --message="Loading installation settings..." --time --weight=1
|
||||
|
||||
app=$YNH_APP_INSTANCE_NAME
|
||||
|
||||
domain=$(ynh_app_setting_get --app=$app --key=domain)
|
||||
path_url=$(ynh_app_setting_get --app=$app --key=path)
|
||||
final_path=$(ynh_app_setting_get --app=$app --key=final_path)
|
||||
db_name=$(ynh_app_setting_get --app=$app --key=db_name)
|
||||
db_user=$db_name
|
||||
phpversion=$(ynh_app_setting_get --app=$app --key=phpversion)
|
||||
|
||||
#=================================================
|
||||
# CHECK IF THE APP CAN BE RESTORED
|
||||
#=================================================
|
||||
ynh_script_progression --message="Validating restoration parameters..." --time --weight=1
|
||||
|
||||
ynh_webpath_available --domain=$domain --path_url=$path_url \
|
||||
|| ynh_die --message="Path not available: ${domain}${path_url}"
|
||||
test ! -d $final_path \
|
||||
|| ynh_die --message="There is already a directory: $final_path "
|
||||
|
||||
#=================================================
|
||||
# STANDARD RESTORATION STEPS
|
||||
#=================================================
|
||||
# RESTORE THE NGINX CONFIGURATION
|
||||
#=================================================
|
||||
|
||||
ynh_restore_file --origin_path="/etc/nginx/conf.d/$domain.d/$app.conf"
|
||||
|
||||
#=================================================
|
||||
# RESTORE THE APP MAIN DIR
|
||||
#=================================================
|
||||
ynh_script_progression --message="Restoring the app main directory..." --time --weight=1
|
||||
|
||||
ynh_restore_file --origin_path="$final_path"
|
||||
|
||||
#=================================================
|
||||
# RECREATE THE DEDICATED USER
|
||||
#=================================================
|
||||
ynh_script_progression --message="Recreating the dedicated system user..." --time --weight=1
|
||||
|
||||
# Create the dedicated user (if not existing)
|
||||
ynh_system_user_create --username=$app
|
||||
|
||||
#=================================================
|
||||
# RESTORE USER RIGHTS
|
||||
#=================================================
|
||||
|
||||
# Restore permissions on app files
|
||||
chown -R root: $final_path
|
||||
|
||||
#=================================================
|
||||
# RESTORE THE PHP-FPM CONFIGURATION
|
||||
#=================================================
|
||||
|
||||
ynh_restore_file --origin_path="/etc/php/$phpversion/fpm/pool.d/$app.conf"
|
||||
|
||||
#=================================================
|
||||
# RESTORE FAIL2BAN CONFIGURATION
|
||||
#=================================================
|
||||
ynh_script_progression --message="Restoring the Fail2Ban configuration..." --time --weight=1
|
||||
|
||||
ynh_restore_file "/etc/fail2ban/jail.d/$app.conf"
|
||||
ynh_restore_file "/etc/fail2ban/filter.d/$app.conf"
|
||||
ynh_systemd_action --action=restart --service_name=fail2ban
|
||||
|
||||
#=================================================
|
||||
# SPECIFIC RESTORATION
|
||||
#=================================================
|
||||
# REINSTALL DEPENDENCIES
|
||||
#=================================================
|
||||
ynh_script_progression --message="Reinstalling dependencies..." --time --weight=1
|
||||
|
||||
# Define and install dependencies
|
||||
ynh_install_app_dependencies $pkg_dependencies
|
||||
|
||||
#=================================================
|
||||
# RESTORE THE MYSQL DATABASE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Restoring the MySQL database..." --time --weight=1
|
||||
|
||||
db_pwd=$(ynh_app_setting_get --app=$app --key=mysqlpwd)
|
||||
ynh_mysql_setup_db --db_user=$db_user --db_name=$db_name --db_pwd=$db_pwd
|
||||
ynh_mysql_connect_as --user=$db_user --password=$db_pwd --database=$db_name < ./db.sql
|
||||
|
||||
#=================================================
|
||||
# RESTORE SYSTEMD
|
||||
#=================================================
|
||||
ynh_script_progression --message="Restoring the systemd configuration..." --time --weight=1
|
||||
|
||||
ynh_restore_file --origin_path="/etc/systemd/system/$app.service"
|
||||
systemctl enable $app.service --quiet
|
||||
|
||||
#=================================================
|
||||
# INTEGRATE SERVICE IN YUNOHOST
|
||||
#=================================================
|
||||
ynh_script_progression --message="Integrating service in YunoHost..." --time --weight=1
|
||||
|
||||
yunohost service add $app --description="A short description of the app" --log="/var/log/$app/$app.log"
|
||||
|
||||
#=================================================
|
||||
# START SYSTEMD SERVICE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Starting a systemd service..." --time --weight=1
|
||||
|
||||
ynh_systemd_action --service_name=$app --action="start" --log_path="/var/log/$app/$app.log"
|
||||
|
||||
#=================================================
|
||||
# RESTORE VARIOUS FILES
|
||||
#=================================================
|
||||
|
||||
ynh_restore_file --origin_path="/etc/cron.d/$app"
|
||||
|
||||
ynh_restore_file --origin_path="/etc/$app/"
|
||||
|
||||
#=================================================
|
||||
# RESTORE THE LOGROTATE CONFIGURATION
|
||||
#=================================================
|
||||
|
||||
ynh_restore_file --origin_path="/etc/logrotate.d/$app"
|
||||
|
||||
#=================================================
|
||||
# GENERIC FINALIZATION
|
||||
#=================================================
|
||||
# RELOAD NGINX AND PHP-FPM
|
||||
#=================================================
|
||||
ynh_script_progression --message="Reloading NGINX web server and PHP-FPM..." --time --weight=1
|
||||
|
||||
ynh_systemd_action --service_name=php$phpversion-fpm --action=reload
|
||||
ynh_systemd_action --service_name=nginx --action=reload
|
||||
|
||||
#=================================================
|
||||
# END OF SCRIPT
|
||||
#=================================================
|
||||
|
||||
ynh_script_progression --message="Restoration completed for $app" --time --last
|
217
scripts/upgrade
Executable file
217
scripts/upgrade
Executable file
|
@ -0,0 +1,217 @@
|
|||
#!/bin/bash
|
||||
|
||||
#=================================================
|
||||
# GENERIC START
|
||||
#=================================================
|
||||
# IMPORT GENERIC HELPERS
|
||||
#=================================================
|
||||
|
||||
source _common.sh
|
||||
source /usr/share/yunohost/helpers
|
||||
|
||||
#=================================================
|
||||
# LOAD SETTINGS
|
||||
#=================================================
|
||||
ynh_script_progression --message="Loading installation settings..." --time --weight=1
|
||||
|
||||
app=$YNH_APP_INSTANCE_NAME
|
||||
|
||||
domain=$(ynh_app_setting_get --app=$app --key=domain)
|
||||
path_url=$(ynh_app_setting_get --app=$app --key=path)
|
||||
admin=$(ynh_app_setting_get --app=$app --key=admin)
|
||||
is_public=$(ynh_app_setting_get --app=$app --key=is_public)
|
||||
final_path=$(ynh_app_setting_get --app=$app --key=final_path)
|
||||
language=$(ynh_app_setting_get --app=$app --key=language)
|
||||
db_name=$(ynh_app_setting_get --app=$app --key=db_name)
|
||||
|
||||
#=================================================
|
||||
# CHECK VERSION
|
||||
#=================================================
|
||||
|
||||
### This helper will compare the version of the currently installed app and the version of the upstream package.
|
||||
### $upgrade_type can have 2 different values
|
||||
### - UPGRADE_APP if the upstream app version has changed
|
||||
### - UPGRADE_PACKAGE if only the YunoHost package has changed
|
||||
### ynh_check_app_version_changed will stop the upgrade if the app is up to date.
|
||||
### UPGRADE_APP should be used to upgrade the core app only if there's an upgrade to do.
|
||||
upgrade_type=$(ynh_check_app_version_changed)
|
||||
|
||||
#=================================================
|
||||
# ENSURE DOWNWARD COMPATIBILITY
|
||||
#=================================================
|
||||
ynh_script_progression --message="Ensuring downward compatibility..." --time --weight=1
|
||||
|
||||
#
|
||||
# N.B. : the followings setting migrations snippets are provided as *EXAMPLES*
|
||||
# of what you may want to do in some cases (e.g. a setting was not defined on
|
||||
# some legacy installs and you therefore want to initiaze stuff during upgrade)
|
||||
#
|
||||
|
||||
# If db_name doesn't exist, create it
|
||||
#if [ -z "$db_name" ]; then
|
||||
# db_name=$(ynh_sanitize_dbid --db_name=$app)
|
||||
# ynh_app_setting_set --app=$app --key=db_name --value=$db_name
|
||||
#fi
|
||||
|
||||
# If final_path doesn't exist, create it
|
||||
#if [ -z "$final_path" ]; then
|
||||
# final_path=/var/www/$app
|
||||
# ynh_app_setting_set --app=$app --key=final_path --value=$final_path
|
||||
#fi
|
||||
|
||||
#=================================================
|
||||
# BACKUP BEFORE UPGRADE THEN ACTIVE TRAP
|
||||
#=================================================
|
||||
ynh_script_progression --message="Backing up the app before upgrading (may take a while)..." --time --weight=1
|
||||
|
||||
# Backup the current version of the app
|
||||
ynh_backup_before_upgrade
|
||||
ynh_clean_setup () {
|
||||
# Restore it if the upgrade fails
|
||||
ynh_restore_upgradebackup
|
||||
}
|
||||
# Exit if an error occurs during the execution of the script
|
||||
ynh_abort_if_errors
|
||||
|
||||
#=================================================
|
||||
# STANDARD UPGRADE STEPS
|
||||
#=================================================
|
||||
# STOP SYSTEMD SERVICE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Stopping a systemd service..." --time --weight=1
|
||||
|
||||
ynh_systemd_action --service_name=$app --action="stop" --log_path="/var/log/$app/$app.log"
|
||||
|
||||
#=================================================
|
||||
# DOWNLOAD, CHECK AND UNPACK SOURCE
|
||||
#=================================================
|
||||
|
||||
if [ "$upgrade_type" == "UPGRADE_APP" ]
|
||||
then
|
||||
ynh_script_progression --message="Upgrading source files..." --time --weight=1
|
||||
|
||||
# Download, check integrity, uncompress and patch the source from app.src
|
||||
ynh_setup_source --dest_dir="$final_path"
|
||||
fi
|
||||
|
||||
#=================================================
|
||||
# NGINX CONFIGURATION
|
||||
#=================================================
|
||||
ynh_script_progression --message="Upgrading NGINX web server configuration..." --time --weight=1
|
||||
|
||||
# Create a dedicated NGINX config
|
||||
ynh_add_nginx_config
|
||||
|
||||
#=================================================
|
||||
# UPGRADE DEPENDENCIES
|
||||
#=================================================
|
||||
ynh_script_progression --message="Upgrading dependencies..." --time --weight=1
|
||||
|
||||
ynh_install_app_dependencies $pkg_dependencies
|
||||
|
||||
#=================================================
|
||||
# CREATE DEDICATED USER
|
||||
#=================================================
|
||||
ynh_script_progression --message="Making sure dedicated system user exists..." --time --weight=1
|
||||
|
||||
# Create a dedicated user (if not existing)
|
||||
ynh_system_user_create --username=$app
|
||||
|
||||
#=================================================
|
||||
# PHP-FPM CONFIGURATION
|
||||
#=================================================
|
||||
ynh_script_progression --message="Upgrading PHP-FPM configuration..." --time --weight=1
|
||||
|
||||
# Create a dedicated PHP-FPM config
|
||||
ynh_add_fpm_config
|
||||
|
||||
#=================================================
|
||||
# SPECIFIC UPGRADE
|
||||
#=================================================
|
||||
# ...
|
||||
#=================================================
|
||||
|
||||
#=================================================
|
||||
# SETUP SYSTEMD
|
||||
#=================================================
|
||||
ynh_script_progression --message="Upgrading systemd configuration..." --time --weight=1
|
||||
|
||||
# Create a dedicated systemd config
|
||||
ynh_add_systemd_config
|
||||
|
||||
#=================================================
|
||||
# MODIFY A CONFIG FILE
|
||||
#=================================================
|
||||
|
||||
### Verify the checksum of a file, stored by `ynh_store_file_checksum` in the install script.
|
||||
### And create a backup of this file if the checksum is different. So the file will be backed up if the admin had modified it.
|
||||
ynh_backup_if_checksum_is_different --file="$final_path/CONFIG_FILE"
|
||||
|
||||
ynh_replace_string --match_string="match_string" --replace_string="replace_string" --target_file="$final_path/CONFIG_FILE"
|
||||
|
||||
# Recalculate and store the checksum of the file for the next upgrade.
|
||||
ynh_store_file_checksum --file="$final_path/CONFIG_FILE"
|
||||
|
||||
#=================================================
|
||||
# GENERIC FINALIZATION
|
||||
#=================================================
|
||||
# SECURE FILES AND DIRECTORIES
|
||||
#=================================================
|
||||
|
||||
# Set permissions on app files
|
||||
chown -R root: $final_path
|
||||
|
||||
#=================================================
|
||||
# SETUP LOGROTATE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Upgrading logrotate configuration..." --time --weight=1
|
||||
|
||||
# Use logrotate to manage app-specific logfile(s)
|
||||
ynh_use_logrotate --non-append
|
||||
|
||||
#=================================================
|
||||
# INTEGRATE SERVICE IN YUNOHOST
|
||||
#=================================================
|
||||
ynh_script_progression --message="Integrating service in YunoHost..." --time --weight=1
|
||||
|
||||
yunohost service add $app --description="A short description of the app" --log="/var/log/$app/$app.log"
|
||||
|
||||
#=================================================
|
||||
# START SYSTEMD SERVICE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Starting a systemd service..." --time --weight=1
|
||||
|
||||
ynh_systemd_action --service_name=$app --action="start" --log_path="/var/log/$app/$app.log"
|
||||
|
||||
#=================================================
|
||||
# UPGRADE FAIL2BAN
|
||||
#=================================================
|
||||
ynh_script_progression --message="Reconfiguring Fail2Ban..." --time --weight=1
|
||||
|
||||
# Create a dedicated Fail2Ban config
|
||||
ynh_add_fail2ban_config --logpath="/var/log/nginx/${domain}-error.log" --failregex="Regex to match into the log for a failed login"
|
||||
|
||||
#=================================================
|
||||
# SETUP SSOWAT
|
||||
#=================================================
|
||||
ynh_script_progression --message="Upgrading SSOwat configuration..." --time --weight=1
|
||||
|
||||
# Make app public if necessary
|
||||
if [ $is_public -eq 1 ]
|
||||
then
|
||||
# unprotected_uris allows SSO credentials to be passed anyway
|
||||
ynh_app_setting_set --app=$app --key=unprotected_uris --value="/"
|
||||
fi
|
||||
|
||||
#=================================================
|
||||
# RELOAD NGINX
|
||||
#=================================================
|
||||
ynh_script_progression --message="Reloading NGINX web server..." --time --weight=1
|
||||
|
||||
ynh_systemd_action --service_name=nginx --action=reload
|
||||
|
||||
#=================================================
|
||||
# END OF SCRIPT
|
||||
#=================================================
|
||||
|
||||
ynh_script_progression --message="Upgrade of $app completed" --time --last
|
1
sources/data/ice-servers.json
Normal file
1
sources/data/ice-servers.json
Normal file
|
@ -0,0 +1 @@
|
|||
[]
|
1
sources/data/passwd
Normal file
1
sources/data/passwd
Normal file
|
@ -0,0 +1 @@
|
|||
__ADMIN__:__PASSWORD__
|
BIN
sources/galene
Executable file
BIN
sources/galene
Executable file
Binary file not shown.
107
sources/group/client.go
Normal file
107
sources/group/client.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"hash"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
|
||||
"github.com/jech/galene/conn"
|
||||
)
|
||||
|
||||
type RawPassword struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Salt string `json:"salt,omitempty"`
|
||||
Iterations int `json:"iterations,omitempty"`
|
||||
}
|
||||
|
||||
type Password RawPassword
|
||||
|
||||
func (p Password) Match(pw string) (bool, error) {
|
||||
switch p.Type {
|
||||
case "":
|
||||
return p.Key == pw, nil
|
||||
case "pbkdf2":
|
||||
key, err := hex.DecodeString(p.Key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
salt, err := hex.DecodeString(p.Salt)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var h func() hash.Hash
|
||||
switch p.Hash {
|
||||
case "sha-256":
|
||||
h = sha256.New
|
||||
default:
|
||||
return false, errors.New("unknown hash type")
|
||||
}
|
||||
theirKey := pbkdf2.Key(
|
||||
[]byte(pw), salt, p.Iterations, len(key), h,
|
||||
)
|
||||
return bytes.Compare(key, theirKey) == 0, nil
|
||||
default:
|
||||
return false, errors.New("unknown password type")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Password) UnmarshalJSON(b []byte) error {
|
||||
var k string
|
||||
err := json.Unmarshal(b, &k)
|
||||
if err == nil {
|
||||
*p = Password{
|
||||
Key: k,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var r RawPassword
|
||||
err = json.Unmarshal(b, &r)
|
||||
if err == nil {
|
||||
*p = Password(r)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p Password) MarshalJSON() ([]byte, error) {
|
||||
if p.Type == "" && p.Hash == "" && p.Salt == "" && p.Iterations == 0 {
|
||||
return json.Marshal(p.Key)
|
||||
}
|
||||
return json.Marshal(RawPassword(p))
|
||||
}
|
||||
|
||||
type ClientCredentials struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Password *Password `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type ClientPermissions struct {
|
||||
Op bool `json:"op,omitempty"`
|
||||
Present bool `json:"present,omitempty"`
|
||||
Record bool `json:"record,omitempty"`
|
||||
}
|
||||
|
||||
type Challengeable interface {
|
||||
Username() string
|
||||
Challenge(string, ClientCredentials) bool
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
Group() *Group
|
||||
Id() string
|
||||
Challengeable
|
||||
SetPermissions(ClientPermissions)
|
||||
OverridePermissions(*Group) bool
|
||||
PushConn(g *Group, id string, conn conn.Up, tracks []conn.UpTrack, label string) error
|
||||
PushClient(id, username string, add bool) error
|
||||
}
|
||||
|
||||
type Kickable interface {
|
||||
Kick(id, user, message string) error
|
||||
}
|
87
sources/group/client_test.go
Normal file
87
sources/group/client_test.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var pw1 = Password{}
|
||||
var pw2 = Password{Key: "pass"}
|
||||
var pw3 = Password{
|
||||
Type: "pbkdf2",
|
||||
Hash: "sha-256",
|
||||
Key: "fe499504e8f144693fae828e8e371d50e019d0e4c84994fa03f7f445bd8a570a",
|
||||
Salt: "bcc1717851030776",
|
||||
Iterations: 4096,
|
||||
}
|
||||
var pw4 = Password{
|
||||
Type: "bad",
|
||||
}
|
||||
|
||||
func TestGood(t *testing.T) {
|
||||
if match, err := pw2.Match("pass"); err != nil || !match {
|
||||
t.Errorf("pw2 doesn't match (%v)", err)
|
||||
}
|
||||
if match, err := pw3.Match("pass"); err != nil || !match {
|
||||
t.Errorf("pw3 doesn't match (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBad(t *testing.T) {
|
||||
if match, err := pw1.Match("bad"); err != nil || match {
|
||||
t.Errorf("pw1 matches")
|
||||
}
|
||||
if match, err := pw2.Match("bad"); err != nil || match {
|
||||
t.Errorf("pw2 matches")
|
||||
}
|
||||
if match, err := pw3.Match("bad"); err != nil || match {
|
||||
t.Errorf("pw3 matches")
|
||||
}
|
||||
if match, err := pw4.Match("bad"); err == nil || match {
|
||||
t.Errorf("pw4 matches")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
plain, err := json.Marshal(pw2)
|
||||
if err != nil || string(plain) != `"pass"` {
|
||||
t.Errorf("Expected \"pass\", got %v", string(plain))
|
||||
}
|
||||
|
||||
for _, pw := range []Password{pw1, pw2, pw3, pw4} {
|
||||
j, err := json.Marshal(pw)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal: %v", err)
|
||||
}
|
||||
if testing.Verbose() {
|
||||
log.Printf("%v", string(j))
|
||||
}
|
||||
var pw2 Password
|
||||
err = json.Unmarshal(j, &pw2)
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
} else if !reflect.DeepEqual(pw, pw2) {
|
||||
t.Errorf("Expected %v, got %v", pw, pw2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPlain(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
match, err := pw2.Match("bad")
|
||||
if err != nil || match {
|
||||
b.Errorf("pw2 matched")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPBKDF2(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
match, err := pw3.Match("bad")
|
||||
if err != nil || match {
|
||||
b.Errorf("pw3 matched")
|
||||
}
|
||||
}
|
||||
}
|
756
sources/group/group.go
Normal file
756
sources/group/group.go
Normal file
|
@ -0,0 +1,756 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
var Directory string
|
||||
var UseMDNS bool
|
||||
|
||||
var ErrNotAuthorised = errors.New("not authorised")
|
||||
|
||||
type UserError string
|
||||
|
||||
func (err UserError) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
type KickError struct {
|
||||
Id string
|
||||
Username string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (err KickError) Error() string {
|
||||
m := "kicked out"
|
||||
if err.Message != "" {
|
||||
m += "(" + err.Message + ")"
|
||||
}
|
||||
if err.Username != "" {
|
||||
m += " by " + err.Username
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
type ProtocolError string
|
||||
|
||||
func (err ProtocolError) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
var IceFilename string
|
||||
|
||||
var iceConf webrtc.Configuration
|
||||
var iceOnce sync.Once
|
||||
|
||||
func IceConfiguration() webrtc.Configuration {
|
||||
iceOnce.Do(func() {
|
||||
var iceServers []webrtc.ICEServer
|
||||
file, err := os.Open(IceFilename)
|
||||
if err != nil {
|
||||
log.Printf("Open %v: %v", IceFilename, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
d := json.NewDecoder(file)
|
||||
err = d.Decode(&iceServers)
|
||||
if err != nil {
|
||||
log.Printf("Get ICE configuration: %v", err)
|
||||
return
|
||||
}
|
||||
iceConf = webrtc.Configuration{
|
||||
ICEServers: iceServers,
|
||||
}
|
||||
})
|
||||
|
||||
return iceConf
|
||||
}
|
||||
|
||||
type ChatHistoryEntry struct {
|
||||
Id string
|
||||
User string
|
||||
Time int64
|
||||
Kind string
|
||||
Value string
|
||||
}
|
||||
|
||||
const (
|
||||
MinBitrate = 200000
|
||||
)
|
||||
|
||||
type Group struct {
|
||||
name string
|
||||
|
||||
mu sync.Mutex
|
||||
description *description
|
||||
locked *string
|
||||
clients map[string]Client
|
||||
history []ChatHistoryEntry
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func (g *Group) Name() string {
|
||||
return g.name
|
||||
}
|
||||
|
||||
func (g *Group) Locked() (bool, string) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
if g.locked != nil {
|
||||
return true, *g.locked
|
||||
} else {
|
||||
return false, ""
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Group) SetLocked(locked bool, message string) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
if locked {
|
||||
g.locked = &message
|
||||
} else {
|
||||
g.locked = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Group) Public() bool {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.description.Public
|
||||
}
|
||||
|
||||
func (g *Group) Redirect() string {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.description.Redirect
|
||||
}
|
||||
|
||||
func (g *Group) AllowRecording() bool {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.description.AllowRecording
|
||||
}
|
||||
|
||||
var groups struct {
|
||||
mu sync.Mutex
|
||||
groups map[string]*Group
|
||||
api *webrtc.API
|
||||
}
|
||||
|
||||
func (g *Group) API() *webrtc.API {
|
||||
return groups.api
|
||||
}
|
||||
|
||||
func Add(name string, desc *description) (*Group, error) {
|
||||
if name == "" || strings.HasSuffix(name, "/") {
|
||||
return nil, UserError("illegal group name")
|
||||
}
|
||||
|
||||
groups.mu.Lock()
|
||||
defer groups.mu.Unlock()
|
||||
|
||||
if groups.groups == nil {
|
||||
groups.groups = make(map[string]*Group)
|
||||
s := webrtc.SettingEngine{}
|
||||
s.SetSRTPReplayProtectionWindow(512)
|
||||
if !UseMDNS {
|
||||
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
|
||||
}
|
||||
m := webrtc.MediaEngine{}
|
||||
m.RegisterCodec(
|
||||
webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
"video/VP8", 90000, 0,
|
||||
"",
|
||||
[]webrtc.RTCPFeedback{
|
||||
{"goog-remb", ""},
|
||||
{"nack", ""},
|
||||
{"nack", "pli"},
|
||||
{"ccm", "fir"},
|
||||
},
|
||||
},
|
||||
PayloadType: 96,
|
||||
},
|
||||
webrtc.RTPCodecTypeVideo,
|
||||
)
|
||||
m.RegisterCodec(
|
||||
webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
"audio/opus", 48000, 2,
|
||||
"minptime=10;useinbandfec=1",
|
||||
nil,
|
||||
},
|
||||
PayloadType: 111,
|
||||
},
|
||||
webrtc.RTPCodecTypeAudio,
|
||||
)
|
||||
groups.api = webrtc.NewAPI(
|
||||
webrtc.WithSettingEngine(s),
|
||||
webrtc.WithMediaEngine(&m),
|
||||
)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
g := groups.groups[name]
|
||||
if g == nil {
|
||||
if desc == nil {
|
||||
desc, err = GetDescription(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
g = &Group{
|
||||
name: name,
|
||||
description: desc,
|
||||
clients: make(map[string]Client),
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
groups.groups[name] = g
|
||||
return g, nil
|
||||
}
|
||||
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if desc != nil {
|
||||
g.description = desc
|
||||
return g, nil
|
||||
}
|
||||
|
||||
if time.Since(g.description.loadTime) > 5*time.Second {
|
||||
if descriptionChanged(name, g.description) {
|
||||
desc, err := GetDescription(name)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
log.Printf("Reading group %v: %v",
|
||||
name, err)
|
||||
}
|
||||
deleteUnlocked(g)
|
||||
return nil, err
|
||||
}
|
||||
g.description = desc
|
||||
} else {
|
||||
g.description.loadTime = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func Range(f func(g *Group) bool) {
|
||||
groups.mu.Lock()
|
||||
defer groups.mu.Unlock()
|
||||
|
||||
for _, g := range groups.groups {
|
||||
ok := f(g)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetNames() []string {
|
||||
names := make([]string, 0)
|
||||
|
||||
Range(func(g *Group) bool {
|
||||
names = append(names, g.name)
|
||||
return true
|
||||
})
|
||||
return names
|
||||
}
|
||||
|
||||
type SubGroup struct {
|
||||
Name string
|
||||
Clients int
|
||||
}
|
||||
|
||||
func GetSubGroups(parent string) []SubGroup {
|
||||
prefix := parent + "/"
|
||||
subgroups := make([]SubGroup, 0)
|
||||
|
||||
Range(func(g *Group) bool {
|
||||
if strings.HasPrefix(g.name, prefix) {
|
||||
g.mu.Lock()
|
||||
count := len(g.clients)
|
||||
g.mu.Unlock()
|
||||
if count > 0 {
|
||||
subgroups = append(subgroups,
|
||||
SubGroup{g.name, count})
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return subgroups
|
||||
}
|
||||
|
||||
func Get(name string) *Group {
|
||||
groups.mu.Lock()
|
||||
defer groups.mu.Unlock()
|
||||
|
||||
return groups.groups[name]
|
||||
}
|
||||
|
||||
func Delete(name string) bool {
|
||||
groups.mu.Lock()
|
||||
defer groups.mu.Unlock()
|
||||
g := groups.groups[name]
|
||||
if g == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return deleteUnlocked(g)
|
||||
}
|
||||
|
||||
// Called with both groups.mu and g.mu taken.
|
||||
func deleteUnlocked(g *Group) bool {
|
||||
if len(g.clients) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
delete(groups.groups, g.name)
|
||||
return true
|
||||
}
|
||||
|
||||
func Expire() {
|
||||
names := GetNames()
|
||||
now := time.Now()
|
||||
|
||||
for _, name := range names {
|
||||
g := Get(name)
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
old := false
|
||||
|
||||
g.mu.Lock()
|
||||
empty := len(g.clients) == 0
|
||||
if empty && !g.description.Public {
|
||||
age := now.Sub(g.timestamp)
|
||||
old = age > maxHistoryAge(g.description)
|
||||
}
|
||||
// We cannot take groups.mu at this point without a deadlock.
|
||||
g.mu.Unlock()
|
||||
|
||||
if empty && old {
|
||||
// Delete will check if the group is still empty
|
||||
Delete(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AddClient(group string, c Client) (*Group, error) {
|
||||
g, err := Add(group, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if !c.OverridePermissions(g) {
|
||||
perms, err := g.description.GetPermission(group, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.SetPermissions(perms)
|
||||
|
||||
if !perms.Op && g.locked != nil {
|
||||
m := *g.locked
|
||||
if m == "" {
|
||||
m = "group is locked"
|
||||
}
|
||||
return nil, UserError(m)
|
||||
}
|
||||
|
||||
if !perms.Op && g.description.MaxClients > 0 {
|
||||
if len(g.clients) >= g.description.MaxClients {
|
||||
return nil, UserError("too many users")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if g.clients[c.Id()] != nil {
|
||||
return nil, ProtocolError("duplicate client id")
|
||||
}
|
||||
|
||||
g.clients[c.Id()] = c
|
||||
g.timestamp = time.Now()
|
||||
|
||||
go func(clients []Client) {
|
||||
u := c.Username()
|
||||
c.PushClient(c.Id(), u, true)
|
||||
for _, cc := range clients {
|
||||
uu := cc.Username()
|
||||
c.PushClient(cc.Id(), uu, true)
|
||||
cc.PushClient(c.Id(), u, true)
|
||||
}
|
||||
}(g.getClientsUnlocked(c))
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func DelClient(c Client) {
|
||||
g := c.Group()
|
||||
if g == nil {
|
||||
return
|
||||
}
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if g.clients[c.Id()] != c {
|
||||
log.Printf("Deleting unknown client")
|
||||
return
|
||||
}
|
||||
delete(g.clients, c.Id())
|
||||
g.timestamp = time.Now()
|
||||
|
||||
go func(clients []Client) {
|
||||
for _, cc := range clients {
|
||||
cc.PushClient(c.Id(), c.Username(), false)
|
||||
}
|
||||
}(g.getClientsUnlocked(nil))
|
||||
}
|
||||
|
||||
func (g *Group) GetClients(except Client) []Client {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.getClientsUnlocked(except)
|
||||
}
|
||||
|
||||
func (g *Group) getClientsUnlocked(except Client) []Client {
|
||||
clients := make([]Client, 0, len(g.clients))
|
||||
for _, c := range g.clients {
|
||||
if c != except {
|
||||
clients = append(clients, c)
|
||||
}
|
||||
}
|
||||
return clients
|
||||
}
|
||||
|
||||
func (g *Group) GetClient(id string) Client {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.getClientUnlocked(id)
|
||||
}
|
||||
|
||||
func (g *Group) getClientUnlocked(id string) Client {
|
||||
for idd, c := range g.clients {
|
||||
if idd == id {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Group) Range(f func(c Client) bool) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
for _, c := range g.clients {
|
||||
ok := f(c)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Group) Shutdown(message string) {
|
||||
g.Range(func(c Client) bool {
|
||||
cc, ok := c.(Kickable)
|
||||
if ok {
|
||||
cc.Kick("", "", message)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func FromJSTime(tm int64) time.Time {
|
||||
if tm == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Unix(int64(tm)/1000, (int64(tm)%1000)*1000000)
|
||||
}
|
||||
|
||||
func ToJSTime(tm time.Time) int64 {
|
||||
return int64((tm.Sub(time.Unix(0, 0)) + time.Millisecond/2) /
|
||||
time.Millisecond)
|
||||
}
|
||||
|
||||
const maxChatHistory = 50
|
||||
|
||||
func (g *Group) ClearChatHistory() {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.history = nil
|
||||
}
|
||||
|
||||
func (g *Group) AddToChatHistory(id, user string, time int64, kind, value string) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if len(g.history) >= maxChatHistory {
|
||||
copy(g.history, g.history[1:])
|
||||
g.history = g.history[:len(g.history)-1]
|
||||
}
|
||||
g.history = append(g.history,
|
||||
ChatHistoryEntry{Id: id, User: user, Time: time, Kind: kind, Value: value},
|
||||
)
|
||||
}
|
||||
|
||||
func discardObsoleteHistory(h []ChatHistoryEntry, duration time.Duration) []ChatHistoryEntry {
|
||||
i := 0
|
||||
for i < len(h) {
|
||||
if time.Since(FromJSTime(h[i].Time)) <= duration {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i > 0 {
|
||||
copy(h, h[i:])
|
||||
h = h[:len(h)-i]
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (g *Group) GetChatHistory() []ChatHistoryEntry {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
g.history = discardObsoleteHistory(
|
||||
g.history, maxHistoryAge(g.description),
|
||||
)
|
||||
|
||||
h := make([]ChatHistoryEntry, len(g.history))
|
||||
copy(h, g.history)
|
||||
return h
|
||||
}
|
||||
|
||||
func matchClient(group string, c Challengeable, users []ClientCredentials) (bool, bool) {
|
||||
for _, u := range users {
|
||||
if u.Username == "" {
|
||||
if c.Challenge(group, u) {
|
||||
return true, true
|
||||
}
|
||||
} else if u.Username == c.Username() {
|
||||
if c.Challenge(group, u) {
|
||||
return true, true
|
||||
} else {
|
||||
return true, false
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
type description struct {
|
||||
fileName string `json:"-"`
|
||||
loadTime time.Time `json:"-"`
|
||||
modTime time.Time `json:"-"`
|
||||
fileSize int64 `json:"-"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Redirect string `json:"redirect,omitempty"`
|
||||
Public bool `json:"public,omitempty"`
|
||||
MaxClients int `json:"max-clients,omitempty"`
|
||||
MaxHistoryAge int `json:"max-history-age,omitempty"`
|
||||
AllowAnonymous bool `json:"allow-anonymous,omitempty"`
|
||||
AllowRecording bool `json:"allow-recording,omitempty"`
|
||||
AllowSubgroups bool `json:"allow-subgroups,omitempty"`
|
||||
Op []ClientCredentials `json:"op,omitempty"`
|
||||
Presenter []ClientCredentials `json:"presenter,omitempty"`
|
||||
Other []ClientCredentials `json:"other,omitempty"`
|
||||
}
|
||||
|
||||
const DefaultMaxHistoryAge = 4 * time.Hour
|
||||
|
||||
func maxHistoryAge(desc *description) time.Duration {
|
||||
if desc.MaxHistoryAge != 0 {
|
||||
return time.Duration(desc.MaxHistoryAge) * time.Second
|
||||
}
|
||||
return DefaultMaxHistoryAge
|
||||
}
|
||||
|
||||
func openDescriptionFile(name string) (*os.File, string, bool, error) {
|
||||
isParent := false
|
||||
for name != "" {
|
||||
fileName := filepath.Join(
|
||||
Directory, path.Clean("/"+name)+".json",
|
||||
)
|
||||
r, err := os.Open(fileName)
|
||||
if !os.IsNotExist(err) {
|
||||
return r, fileName, isParent, err
|
||||
}
|
||||
isParent = true
|
||||
name, _ = path.Split(name)
|
||||
name = strings.TrimRight(name, "/")
|
||||
}
|
||||
return nil, "", false, os.ErrNotExist
|
||||
}
|
||||
|
||||
func statDescriptionFile(name string) (os.FileInfo, string, bool, error) {
|
||||
isParent := false
|
||||
for name != "" {
|
||||
fileName := filepath.Join(
|
||||
Directory, path.Clean("/"+name)+".json",
|
||||
)
|
||||
fi, err := os.Stat(fileName)
|
||||
if !os.IsNotExist(err) {
|
||||
return fi, fileName, isParent, err
|
||||
}
|
||||
isParent = true
|
||||
name, _ = path.Split(name)
|
||||
name = strings.TrimRight(name, "/")
|
||||
}
|
||||
return nil, "", false, os.ErrNotExist
|
||||
}
|
||||
|
||||
// descriptionChanged returns true if a group's description may have
|
||||
// changed since it was last read.
|
||||
func descriptionChanged(name string, desc *description) bool {
|
||||
fi, fileName, _, err := statDescriptionFile(name)
|
||||
if err != nil || fileName != desc.fileName {
|
||||
return true
|
||||
}
|
||||
|
||||
if fi.Size() != desc.fileSize || fi.ModTime() != desc.modTime {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetDescription(name string) (*description, error) {
|
||||
r, fileName, isParent, err := openDescriptionFile(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
var desc description
|
||||
|
||||
fi, err := r.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := json.NewDecoder(r)
|
||||
err = d.Decode(&desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isParent {
|
||||
if !desc.AllowSubgroups {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
desc.Public = false
|
||||
desc.Description = ""
|
||||
}
|
||||
|
||||
desc.fileName = fileName
|
||||
desc.fileSize = fi.Size()
|
||||
desc.modTime = fi.ModTime()
|
||||
desc.loadTime = time.Now()
|
||||
|
||||
return &desc, nil
|
||||
}
|
||||
|
||||
func (desc *description) GetPermission(group string, c Challengeable) (ClientPermissions, error) {
|
||||
var p ClientPermissions
|
||||
if !desc.AllowAnonymous && c.Username() == "" {
|
||||
return p, UserError("anonymous users not allowed in this group, please choose a username")
|
||||
}
|
||||
if found, good := matchClient(group, c, desc.Op); found {
|
||||
if good {
|
||||
p.Op = true
|
||||
p.Present = true
|
||||
if desc.AllowRecording {
|
||||
p.Record = true
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
return p, ErrNotAuthorised
|
||||
}
|
||||
if found, good := matchClient(group, c, desc.Presenter); found {
|
||||
if good {
|
||||
p.Present = true
|
||||
return p, nil
|
||||
}
|
||||
return p, ErrNotAuthorised
|
||||
}
|
||||
if found, good := matchClient(group, c, desc.Other); found {
|
||||
if good {
|
||||
return p, nil
|
||||
}
|
||||
return p, ErrNotAuthorised
|
||||
}
|
||||
return p, ErrNotAuthorised
|
||||
}
|
||||
|
||||
type Public struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ClientCount int `json:"clientCount"`
|
||||
}
|
||||
|
||||
func GetPublic() []Public {
|
||||
gs := make([]Public, 0)
|
||||
Range(func(g *Group) bool {
|
||||
if g.Public() {
|
||||
gs = append(gs, Public{
|
||||
Name: g.name,
|
||||
Description: g.description.Description,
|
||||
ClientCount: len(g.clients),
|
||||
})
|
||||
}
|
||||
return true
|
||||
})
|
||||
sort.Slice(gs, func(i, j int) bool {
|
||||
return gs[i].Name < gs[j].Name
|
||||
})
|
||||
return gs
|
||||
}
|
||||
|
||||
func ReadPublicGroups() {
|
||||
dir, err := os.Open(Directory)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
fis, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
log.Printf("readPublicGroups: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, fi := range fis {
|
||||
if !strings.HasSuffix(fi.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
name := fi.Name()[:len(fi.Name())-5]
|
||||
desc, err := GetDescription(name)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
log.Printf("Reading group %v: %v", name, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if desc.Public {
|
||||
Add(name, desc)
|
||||
}
|
||||
}
|
||||
}
|
58
sources/group/group_test.go
Normal file
58
sources/group/group_test.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestJSTime(t *testing.T) {
|
||||
tm := time.Now()
|
||||
js := ToJSTime(tm)
|
||||
tm2 := FromJSTime(js)
|
||||
js2 := ToJSTime(tm2)
|
||||
|
||||
if js != js2 {
|
||||
t.Errorf("%v != %v", js, js2)
|
||||
}
|
||||
|
||||
delta := tm.Sub(tm2)
|
||||
if delta < -time.Millisecond/2 || delta > time.Millisecond/2 {
|
||||
t.Errorf("Delta %v, %v, %v", delta, tm, tm2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescriptionJSON(t *testing.T) {
|
||||
d := `
|
||||
{
|
||||
"op":[{"username": "jch","password": "topsecret"}],
|
||||
"max-history-age": 10,
|
||||
"allow-subgroups": true,
|
||||
"presenter":[
|
||||
{"user": "john", "password": "secret"},
|
||||
{}
|
||||
]
|
||||
}`
|
||||
|
||||
var dd description
|
||||
err := json.Unmarshal([]byte(d), &dd)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
ddd, err := json.Marshal(dd)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
var dddd description
|
||||
err = json.Unmarshal([]byte(ddd), &dddd)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(dd, dddd) {
|
||||
t.Errorf("Got %v, expected %v", dddd, dd)
|
||||
}
|
||||
}
|
69
sources/static/404.css
Normal file
69
sources/static/404.css
Normal file
|
@ -0,0 +1,69 @@
|
|||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
padding: 0px 30px;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
max-width: 960px;
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.landing-page {
|
||||
max-width: 960px;
|
||||
height: 475px;
|
||||
margin: 0;
|
||||
box-shadow: 0px 0px 8px 1px #ccc;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: #7e7e7e;
|
||||
font-size: 8em;
|
||||
text-align: center;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.logo .fa {
|
||||
color: #c39999;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
margin: 0;
|
||||
color: #7e7e7e;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
width: 35%;
|
||||
margin: 16px auto 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.home-link {
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 24px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
background: #610a86;
|
||||
color: #fff;
|
||||
border: none;
|
||||
box-shadow: 0 4px 4px 0 #ccc;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
31
sources/static/404.html
Normal file
31
sources/static/404.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Page not Found</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/common.css">
|
||||
<link rel="stylesheet" type="text/css" href="/404.css"/>
|
||||
<link rel="author" href="https://www.irif.fr/~jch/"/>
|
||||
<!-- Font Awesome File -->
|
||||
<link rel="stylesheet" type="text/css" href="/css/fontawesome.min.css">
|
||||
<link href="/css/solid.css" rel="stylesheet" type="text/css">
|
||||
<link href="/css/regular.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="landing-page">
|
||||
<div class="logo">
|
||||
<i class="fas fa-frown" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
<h1> Page not found!</h1>
|
||||
<p> We can't find the page you're looking for.</p>
|
||||
<a href="/" class="home-link">Back to home</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
44
sources/static/common.css
Normal file
44
sources/static/common.css
Normal file
|
@ -0,0 +1,44 @@
|
|||
h1 {
|
||||
font-size: 160%;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.signature {
|
||||
border-top: solid;
|
||||
padding-top: 0;
|
||||
border-width: thin;
|
||||
clear: both;
|
||||
height: 3.125rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
body, html {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Metropolis,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #687281;
|
||||
text-align: left;
|
||||
background-color: #eff3f9;
|
||||
}
|
||||
|
||||
*, :after, :before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: Metropolis,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
}
|
5
sources/static/css/fontawesome.min.css
vendored
Normal file
5
sources/static/css/fontawesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
15
sources/static/css/regular.css
Normal file
15
sources/static/css/regular.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*!
|
||||
* Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url("../webfonts/fa-regular-400.eot");
|
||||
src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
|
||||
|
||||
.far {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 400; }
|
16
sources/static/css/solid.css
Normal file
16
sources/static/css/solid.css
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*!
|
||||
* Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: block;
|
||||
src: url("../webfonts/fa-solid-900.eot");
|
||||
src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); }
|
||||
|
||||
.fa,
|
||||
.fas {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900; }
|
15
sources/static/css/toastify.min.css
vendored
Normal file
15
sources/static/css/toastify.min.css
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Minified by jsDelivr using clean-css v4.2.3.
|
||||
* Original file: /npm/toastify-js@1.9.1/src/toastify.css
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
/*!
|
||||
* Toastify js 1.9.1
|
||||
* https://github.com/apvarun/toastify-js
|
||||
* @license MIT licensed
|
||||
*
|
||||
* Copyright (C) 2018 Varun A P
|
||||
*/
|
||||
.toastify{padding:12px 20px;color:#fff;display:inline-block;box-shadow:0 3px 6px -1px rgba(0,0,0,.12),0 10px 36px -4px rgba(77,96,232,.3);background:-webkit-linear-gradient(315deg,#73a5ff,#5477f5);background:linear-gradient(135deg,#73a5ff,#5477f5);position:fixed;opacity:0;transition:all .4s cubic-bezier(.215,.61,.355,1);border-radius:2px;cursor:pointer;text-decoration:none;max-width:calc(50% - 20px);z-index:2147483647}.toastify.on{opacity:1}.toast-close{opacity:.4;padding:0 5px}.toastify-right{right:15px}.toastify-left{left:15px}.toastify-top{top:-150px}.toastify-bottom{bottom:-150px}.toastify-rounded{border-radius:25px}.toastify-avatar{width:1.5em;height:1.5em;margin:-7px 5px;border-radius:2px}.toastify-center{margin-left:auto;margin-right:auto;left:0;right:0;max-width:fit-content}@media only screen and (max-width:360px){.toastify-left,.toastify-right{margin-left:auto;margin-right:auto;left:0;right:0;max-width:fit-content}}
|
||||
/*# sourceMappingURL=/sm/9c0bbf2acc17f6468f9dd75307f4d772b55e466d0ddceef6dc95ee31ca309918.map */
|
1307
sources/static/galene.css
Normal file
1307
sources/static/galene.css
Normal file
File diff suppressed because it is too large
Load diff
255
sources/static/galene.html
Normal file
255
sources/static/galene.html
Normal file
|
@ -0,0 +1,255 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Galène</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="ScreenOrientation" content="autoRotate:disabled">
|
||||
<link rel="stylesheet" type="text/css" href="/common.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/galene.css"/>
|
||||
<link rel="author" href="https://www.irif.fr/~jch/"/>
|
||||
<!-- Font Awesome File -->
|
||||
<link href="/css/fontawesome.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="/css/solid.css" rel="stylesheet" type="text/css">
|
||||
<link href="/css/regular.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/toastify.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="main" class="app">
|
||||
<div class="row full-height">
|
||||
<nav id="left-sidebar">
|
||||
<div class="users-header">
|
||||
<div class="galene-header">Galène</div>
|
||||
</div>
|
||||
<div class="header-sep"></div>
|
||||
<div id="users"></div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<header>
|
||||
<nav class="topnav navbar navbar-expand navbar-light fixed-top">
|
||||
<div id="header">
|
||||
<div class="collapse" title="Collapse left panel" id="sidebarCollapse">
|
||||
<svg class="svg-inline--fa" aria-hidden="true" data-icon="align-left" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
<path fill="currentColor" d="M288 44v40c0 8.837-7.163 16-16 16H16c-8.837 0-16-7.163-16-16V44c0-8.837 7.163-16 16-16h256c8.837 0 16 7.163 16 16zM0 172v40c0 8.837 7.163 16 16 16h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16zm16 312h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm256-200H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16h256c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 id="title" class="header-title"></h1>
|
||||
</div>
|
||||
|
||||
<ul class="nav-menu">
|
||||
<li>
|
||||
<button id="presentbutton" class="invisible btn btn-success">
|
||||
<i class="fas fa-play" aria-hidden="true"></i><span class="nav-text"> Ready</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button id="unpresentbutton" class="invisible btn btn-cancel">
|
||||
<i class="fas fa-stop" aria-hidden="true"></i><span class="nav-text"> Panic</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<div id="mutebutton" class="nav-link nav-button">
|
||||
<span><i class="fas fa-microphone-slash" aria-hidden="true"></i></span>
|
||||
<label>Mute</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div id="sharebutton" class="invisible nav-link nav-button">
|
||||
<span><i class="fas fa-share-square" aria-hidden="true"></i></span>
|
||||
<label>Share Screen</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div id="unsharebutton" class="invisible nav-link nav-button nav-cancel">
|
||||
<span><i class="fas fa-window-close" aria-hidden="true"></i></span>
|
||||
<label>Unshare Screen</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div id="stopvideobutton" class="invisible nav-link nav-button nav-cancel">
|
||||
<span><i class="fas fa-window-close" aria-hidden="true"></i></span>
|
||||
<label>Stop Video</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="nav-button nav-link nav-more" id="openside">
|
||||
<span><i class="fas fa-ellipsis-v" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="row full-width" id="mainrow">
|
||||
<div class="coln-left" id="left">
|
||||
<div id="chat">
|
||||
<div id="chatbox">
|
||||
<div class="close-chat" id="close-chat" title="Hide chat">
|
||||
<span class="close-icon"></span>
|
||||
</div>
|
||||
<div id="box"></div>
|
||||
<div class="reply">
|
||||
<form id="inputform">
|
||||
<textarea id="input" class="form-reply"></textarea>
|
||||
<input id="inputbutton" type="submit" value="➤" class="btn btn-default"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="resizer"></div>
|
||||
<div class="coln-right" id="right">
|
||||
<span class="show-video blink" id="switch-video"><i class="fas fa-exchange" aria-hidden="true"></i></span>
|
||||
<div class="collapse-video" id="collapse-video">
|
||||
<i class="far fa-comment-alt open-chat" title="Open chat"></i>
|
||||
</div>
|
||||
<div class="video-container no-video" id="video-container">
|
||||
<div id="expand-video" class="expand-video">
|
||||
<div id="peers"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-container invisible" id="login-container">
|
||||
<div class="login-box">
|
||||
<form id="userform" class="userform">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" type="text" name="username"
|
||||
autocomplete="username" class="form-control"/>
|
||||
<label for="password">Password</label>
|
||||
<input id="password" type="password" name="password"
|
||||
autocomplete="current-password" class="form-control"/>
|
||||
<label>Auto ready</label>
|
||||
<div class="present-switch">
|
||||
<p class="switch-radio">
|
||||
<input id="presentoff" type="radio" name="presentradio" value="" checked/>
|
||||
<label for="presentoff">Disabled</label>
|
||||
</p>
|
||||
<p class="switch-radio">
|
||||
<input id="presentmike" type="radio" name="presentradio" value="mike"/>
|
||||
<label for="presentmike">Enable microphone</label>
|
||||
</p>
|
||||
<p class="switch-radio">
|
||||
<input id="presentboth" type="radio" name="presentradio" value="both"/>
|
||||
<label for="presentboth">Enable camera and microphone</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
<div class="connect">
|
||||
<input id="connectbutton" type="submit" class="btn btn-blue" value="Connect"/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sidebarnav" class="sidenav">
|
||||
<div class="sidenav-header">
|
||||
<h2>Settings</h2>
|
||||
<a class="closebtn" id="clodeside"><i class="fas fa-times" aria-hidden="true"></i></a>
|
||||
</div>
|
||||
<div class="sidenav-content" id="optionsdiv">
|
||||
<div id="profile" class="profile invisible">
|
||||
<div class="profile-user">
|
||||
<div class="profile-logo">
|
||||
<span><i class="fas fa-user" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
<div class="profile-info">
|
||||
<span id="userspan"></span>
|
||||
<span id="permspan"></span>
|
||||
</div>
|
||||
<div class="user-logout">
|
||||
<a id="disconnectbutton">
|
||||
<span class="logout-icon"><i class="fas fa-sign-out-alt"></i></span>
|
||||
<span class="logout-text">Logout</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mediaoptions" class="invisible">
|
||||
<fieldset>
|
||||
<legend>Media Options</legend>
|
||||
<label for="videoselect" class="sidenav-label-first">Camera:</label>
|
||||
<select id="videoselect" class="select select-inline">
|
||||
<option value="">off</option>
|
||||
</select>
|
||||
|
||||
<label for="audioselect" class="sidenav-label">Microphone:</label>
|
||||
<select id="audioselect" class="select select-inline">
|
||||
<option value="">off</option>
|
||||
</select>
|
||||
|
||||
<form>
|
||||
<input id="blackboardbox" type="checkbox"/>
|
||||
<label for="blackboardbox">Blackboard mode</label>
|
||||
</form>
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>Other Settings</legend>
|
||||
|
||||
<form id="sendform">
|
||||
<label for="sendselect" class="sidenav-label-first">Send:</label>
|
||||
<select id="sendselect" class="select select-inline">
|
||||
<option value="lowest">lowest</option>
|
||||
<option value="low">low</option>
|
||||
<option value="normal" selected>normal</option>
|
||||
<option value="unlimited">unlimited</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<form id="requestform">
|
||||
<label for="requestselect" class="sidenav-label">Receive:</label>
|
||||
<select id="requestselect" class="select select-inline">
|
||||
<option value="">nothing</option>
|
||||
<option value="audio">audio only</option>
|
||||
<option value="screenshare">screen share</option>
|
||||
<option value="everything" selected>everything</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<form>
|
||||
<input id="activitybox" type="checkbox"/>
|
||||
<label for="activitybox">Activity detection</label>
|
||||
</form>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<form id="fileform">
|
||||
<label for="fileinput" class=".sidenav-label-first">Play local file:</label>
|
||||
<input type="file" id="fileinput" accept="audio/*,video/*" multiple/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="videocontrols-template" class="invisible">
|
||||
<div class="video-controls vc-overlay">
|
||||
<div class="controls-button controls-left">
|
||||
<span class="video-play" title="Play video">
|
||||
<i class="fas fa-play"></i>
|
||||
</span>
|
||||
<span class="volume" title="Volume">
|
||||
<i class="fas fa-volume-up volume-mute" aria-hidden="true"></i>
|
||||
<input class="volume-slider" type="range" max="100" value="100" min="0" step="5" >
|
||||
</span>
|
||||
</div>
|
||||
<div class="controls-button controls-right">
|
||||
<span class="pip" title="Picture In Picture">
|
||||
<i class="far fa-clone" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="fullscreen" title="Fullscreen">
|
||||
<i class="fas fa-expand" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/protocol.js" defer></script>
|
||||
<script src="/scripts/toastify.js" defer></script>
|
||||
<script src="/galene.js" defer></script>
|
||||
</body>
|
||||
</html>
|
2338
sources/static/galene.js
Normal file
2338
sources/static/galene.js
Normal file
File diff suppressed because it is too large
Load diff
42
sources/static/index.html
Normal file
42
sources/static/index.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Galène</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/common.css">
|
||||
<link rel="stylesheet" href="/mainpage.css">
|
||||
<link rel="stylesheet" type="text/css" href="/galene.css"/>
|
||||
<link rel="author" href="https://www.irif.fr/~jch/"/>
|
||||
<!-- Font Awesome File -->
|
||||
<link href="/css/fontawesome.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="/css/solid.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="home">
|
||||
<h1 id="title" class="navbar-brand">Galène</h1>
|
||||
|
||||
<form id="groupform">
|
||||
<label for="group">Group:</label>
|
||||
<input id="group" type="text" name="group" class="form-control form-control-inline"/>
|
||||
<input type="submit" value="Join" class="btn btn-default btn-large"/><br/>
|
||||
</form>
|
||||
|
||||
<div id="public-groups" class="groups">
|
||||
<h2>Public groups</h2>
|
||||
|
||||
<table id="public-groups-table"></table>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="signature">
|
||||
<p><a href="https://galene.org/">Galène</a> by <a href="https://www.irif.fr/~jch/" rel="author">Juliusz Chroboczek</a>
|
||||
</footer>
|
||||
|
||||
<script src="/mainpage.js" defer></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
40
sources/static/mainpage.css
Normal file
40
sources/static/mainpage.css
Normal file
|
@ -0,0 +1,40 @@
|
|||
.groups {
|
||||
}
|
||||
|
||||
.nogroups {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
|
||||
.home {
|
||||
height: calc(100vh - 50px);
|
||||
padding: 1.875rem;
|
||||
}
|
||||
|
||||
#public-groups-table tr a{
|
||||
margin-left: 0.9375rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #0058e4;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0a429c;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 768px) {
|
||||
.home {
|
||||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
}
|
73
sources/static/mainpage.js
Normal file
73
sources/static/mainpage.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) 2020 by Juliusz Chroboczek.
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
'use strict';
|
||||
|
||||
document.getElementById('groupform').onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
let group = document.getElementById('group').value.trim();
|
||||
if(group !== '')
|
||||
location.href = '/group/' + group;
|
||||
};
|
||||
|
||||
async function listPublicGroups() {
|
||||
let div = document.getElementById('public-groups');
|
||||
let table = document.getElementById('public-groups-table');
|
||||
|
||||
let l;
|
||||
try {
|
||||
l = await (await fetch('/public-groups.json')).json();
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
l = [];
|
||||
}
|
||||
|
||||
if (l.length === 0) {
|
||||
table.textContent = '(No groups found.)';
|
||||
div.classList.remove('groups');
|
||||
div.classList.add('nogroups');
|
||||
return;
|
||||
}
|
||||
|
||||
div.classList.remove('nogroups');
|
||||
div.classList.add('groups');
|
||||
|
||||
for(let i = 0; i < l.length; i++) {
|
||||
let group = l[i];
|
||||
let tr = document.createElement('tr');
|
||||
let td = document.createElement('td');
|
||||
let a = document.createElement('a');
|
||||
a.textContent = group.name;
|
||||
a.href = '/group/' + encodeURIComponent(group.name);
|
||||
td.appendChild(a);
|
||||
tr.appendChild(td);
|
||||
let td2 = document.createElement('td');
|
||||
if(group.description)
|
||||
td2.textContent = group.description;
|
||||
tr.appendChild(td2);
|
||||
let td3 = document.createElement('td');
|
||||
td3.textContent = `(${group.clientCount} clients)`;
|
||||
tr.appendChild(td3);
|
||||
table.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
listPublicGroups();
|
1173
sources/static/protocol.js
Normal file
1173
sources/static/protocol.js
Normal file
File diff suppressed because it is too large
Load diff
8
sources/static/scripts/toastify.js
Normal file
8
sources/static/scripts/toastify.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Minified by jsDelivr using Terser v3.14.1.
|
||||
* Original file: /npm/toastify-js@1.9.1/src/toastify.js
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
!function(t,o){"object"==typeof module&&module.exports?module.exports=o():t.Toastify=o()}(this,function(t){var o=function(t){return new o.lib.init(t)};function i(t,o){return o.offset[t]?isNaN(o.offset[t])?o.offset[t]:o.offset[t]+"px":"0px"}function s(t,o){return!(!t||"string"!=typeof o)&&!!(t.className&&t.className.trim().split(/\s+/gi).indexOf(o)>-1)}return o.lib=o.prototype={toastify:"1.9.1",constructor:o,init:function(t){t||(t={}),this.options={},this.toastElement=null,this.options.text=t.text||"Hi there!",this.options.node=t.node,this.options.duration=0===t.duration?0:t.duration||3e3,this.options.selector=t.selector,this.options.callback=t.callback||function(){},this.options.destination=t.destination,this.options.newWindow=t.newWindow||!1,this.options.close=t.close||!1,this.options.gravity="bottom"===t.gravity?"toastify-bottom":"toastify-top",this.options.positionLeft=t.positionLeft||!1,this.options.position=t.position||"",this.options.backgroundColor=t.backgroundColor,this.options.avatar=t.avatar||"",this.options.className=t.className||"",this.options.stopOnFocus=void 0===t.stopOnFocus||t.stopOnFocus,this.options.onClick=t.onClick;return this.options.offset=t.offset||{x:0,y:0},this},buildToast:function(){if(!this.options)throw"Toastify is not initialized";var t=document.createElement("div");if(t.className="toastify on "+this.options.className,this.options.position?t.className+=" toastify-"+this.options.position:!0===this.options.positionLeft?(t.className+=" toastify-left",console.warn("Property `positionLeft` will be depreciated in further versions. Please use `position` instead.")):t.className+=" toastify-right",t.className+=" "+this.options.gravity,this.options.backgroundColor&&(t.style.background=this.options.backgroundColor),this.options.node&&this.options.node.nodeType===Node.ELEMENT_NODE)t.appendChild(this.options.node);else if(t.innerHTML=this.options.text,""!==this.options.avatar){var o=document.createElement("img");o.src=this.options.avatar,o.className="toastify-avatar","left"==this.options.position||!0===this.options.positionLeft?t.appendChild(o):t.insertAdjacentElement("afterbegin",o)}if(!0===this.options.close){var s=document.createElement("span");s.innerHTML="✖",s.className="toast-close",s.addEventListener("click",function(t){t.stopPropagation(),this.removeElement(this.toastElement),window.clearTimeout(this.toastElement.timeOutValue)}.bind(this));var n=window.innerWidth>0?window.innerWidth:screen.width;("left"==this.options.position||!0===this.options.positionLeft)&&n>360?t.insertAdjacentElement("afterbegin",s):t.appendChild(s)}if(this.options.stopOnFocus&&this.options.duration>0){const o=this;t.addEventListener("mouseover",function(o){window.clearTimeout(t.timeOutValue)}),t.addEventListener("mouseleave",function(){t.timeOutValue=window.setTimeout(function(){o.removeElement(t)},o.options.duration)})}if(void 0!==this.options.destination&&t.addEventListener("click",function(t){t.stopPropagation(),!0===this.options.newWindow?window.open(this.options.destination,"_blank"):window.location=this.options.destination}.bind(this)),"function"==typeof this.options.onClick&&void 0===this.options.destination&&t.addEventListener("click",function(t){t.stopPropagation(),this.options.onClick()}.bind(this)),"object"==typeof this.options.offset){var e=i("x",this.options),a=i("y",this.options);const o="left"==this.options.position?e:`-${e}`,s="toastify-top"==this.options.gravity?a:`-${a}`;t.style.transform=`translate(${o}, ${s})`}return t},showToast:function(){var t;if(this.toastElement=this.buildToast(),!(t=void 0===this.options.selector?document.body:document.getElementById(this.options.selector)))throw"Root element is not defined";return t.insertBefore(this.toastElement,t.firstChild),o.reposition(),this.options.duration>0&&(this.toastElement.timeOutValue=window.setTimeout(function(){this.removeElement(this.toastElement)}.bind(this),this.options.duration)),this},hideToast:function(){this.toastElement.timeOutValue&&clearTimeout(this.toastElement.timeOutValue),this.removeElement(this.toastElement)},removeElement:function(t){t.className=t.className.replace(" on",""),window.setTimeout(function(){this.options.node&&this.options.node.parentNode&&this.options.node.parentNode.removeChild(this.options.node),t.parentNode&&t.parentNode.removeChild(t),this.options.callback.call(t),o.reposition()}.bind(this),400)}},o.reposition=function(){for(var t,o={top:15,bottom:15},i={top:15,bottom:15},n={top:15,bottom:15},e=document.getElementsByClassName("toastify"),a=0;a<e.length;a++){t=!0===s(e[a],"toastify-top")?"toastify-top":"toastify-bottom";var p=e[a].offsetHeight;t=t.substr(9,t.length-1);(window.innerWidth>0?window.innerWidth:screen.width)<=360?(e[a].style[t]=n[t]+"px",n[t]+=p+15):!0===s(e[a],"toastify-left")?(e[a].style[t]=o[t]+"px",o[t]+=p+15):(e[a].style[t]=i[t]+"px",i[t]+=p+15)}return this},o.lib.init.prototype=o.lib,o});
|
||||
//# sourceMappingURL=/sm/1df7b098cd6209fd67b5cc8f6f6518b79e5214ec3802d91f56f825883253df69.map
|
19
sources/static/tsconfig.json
Normal file
19
sources/static/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"declaration": true,
|
||||
"noImplicitThis": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true
|
||||
},
|
||||
"files": [
|
||||
"protocol.js",
|
||||
"galene.js"
|
||||
]
|
||||
}
|
BIN
sources/static/webfonts/fa-regular-400.eot
Normal file
BIN
sources/static/webfonts/fa-regular-400.eot
Normal file
Binary file not shown.
BIN
sources/static/webfonts/fa-regular-400.ttf
Normal file
BIN
sources/static/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
sources/static/webfonts/fa-regular-400.woff
Normal file
BIN
sources/static/webfonts/fa-regular-400.woff
Normal file
Binary file not shown.
BIN
sources/static/webfonts/fa-regular-400.woff2
Normal file
BIN
sources/static/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
sources/static/webfonts/fa-solid-900.eot
Normal file
BIN
sources/static/webfonts/fa-solid-900.eot
Normal file
Binary file not shown.
BIN
sources/static/webfonts/fa-solid-900.ttf
Normal file
BIN
sources/static/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
sources/static/webfonts/fa-solid-900.woff
Normal file
BIN
sources/static/webfonts/fa-solid-900.woff
Normal file
Binary file not shown.
BIN
sources/static/webfonts/fa-solid-900.woff2
Normal file
BIN
sources/static/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
Loading…
Reference in a new issue