1
0
Fork 0
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:
ericgaspar 2021-01-09 12:31:07 +01:00
parent e12b3290ce
commit 54c779a1f1
No known key found for this signature in database
GPG key ID: 574F281483054D44
38 changed files with 7610 additions and 0 deletions

20
scripts/_common.sh Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View file

@ -0,0 +1 @@
[]

1
sources/data/passwd Normal file
View file

@ -0,0 +1 @@
__ADMIN__:__PASSWORD__

BIN
sources/galene Executable file

Binary file not shown.

107
sources/group/client.go Normal file
View 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
}

View 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
View 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)
}
}
}

View 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
View 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
View 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
View 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;
}

File diff suppressed because one or more lines are too long

View 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; }

View 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
View 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

File diff suppressed because it is too large Load diff

255
sources/static/galene.html Normal file
View 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="&#10148;" 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

File diff suppressed because it is too large Load diff

42
sources/static/index.html Normal file
View 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>

View 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;
}
}

View 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

File diff suppressed because it is too large Load diff

View 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="&#10006;",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

View 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"
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.