iCanText is designed to be radically decentralized. However, for users to find each other and initiate a connection, a public "rendezvous point" is necessary. This is the role of the signaling server, or "endpoint".
By default, iCanText uses the https://p.sayseeshow.io endpoint. While this service is minimalist and privacy-respecting (it logs nothing), the mere act of using it reveals to our servers that a connection attempt has occurred.
By deploying your own endpoint on a server you control, you achieve a higher level of confidentiality:
This guide is for deployment on a standard shared web hosting. You will need:
.htaccess file (for Apache servers).On your server, create the following folder and file structure. For example, at the root of your site https://your-domain.com/:
/ (your site's root) ├── canaux/ (This folder must have write permissions, e.g., 755) ├── canal.php (The main script) ├── .htaccess (The Apache configuration file) ├── cors-handler.php (Handler for CORS requests) └── cors_config.php (Configuration for allowed domains)
Security Note: The CORS configuration below is secure because it only allows specific domains. You can edit the $allowedOrigins list in cors_config.php to add your own domains if you host a modified version of the iCanText application.
cors_config.php fileThis file centralizes the domains authorized to connect to your endpoint.
File: cors_config.php
<?php
// File: cors_config.php
$allowedOrigins = [
'https://sayseeshow.io',
'https://icansend.com',
'https://icanphone.com',
'https://icantext.com'
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins, true)) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Credentials: true');
}cors-handler.php fileThis script handles "pre-flight" (OPTIONS) requests sent by browsers.
File: cors-handler.php
<?php
// File: cors-handler.php
// This script is called by .htaccess only for OPTIONS requests.
require_once __DIR__ . '/cors_config.php';
// OPTIONS (pre-flight) requests only need these headers.
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
header('Access-Control-Max-Age: 86400');
header("HTTP/1.1 204 No Content");
exit(0);canal.php fileThis PHP code uses the CORS configuration.
File: canal.php
<?php
/*
* File-based WebRTC signaling endpoint.
*/
require_once __DIR__ . '/cors_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
require __DIR__ . '/cors-handler.php';
exit(0);
}
define('MAX_INPUT_SIZE', 15 * 1024);
define('MAX_FILE_SIZE', 150 * 1024);
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
$fichier = $_GET['fichier'] ?? '';
if (preg_match('/^[a-zA-Z0-9_-]+$/', $fichier) !== 1) {
header("HTTP/1.1 400 Bad Request");
exit('Invalid channel name.');
}
$chemin_fichier = __DIR__ . '/canaux/' . basename($fichier);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$contenu = file_get_contents('php://input');
if (strlen($contenu) > MAX_INPUT_SIZE) {
header("HTTP/1.1 413 Payload Too Large");
exit('Payload exceeds limit.');
}
if (!$contenu) {
header("HTTP/1.1 400 Bad Request");
exit('No content received.');
}
$fp = fopen($chemin_fichier, 'a');
if (!$fp) {
header("HTTP/1.1 500 Internal Server Error");
exit('Could not open channel file.');
}
if (flock($fp, LOCK_EX)) {
clearstatcache(true, $chemin_fichier);
if (file_exists($chemin_fichier) && filesize($chemin_fichier) + strlen($contenu) > MAX_FILE_SIZE) {
flock($fp, LOCK_UN);
fclose($fp);
header("HTTP/1.1 409 Conflict");
exit('Channel is full.');
}
fwrite($fp, $contenu . "\n---\n");
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
header("HTTP/1.1 201 Created");
exit;
} else {
fclose($fp);
header("HTTP/1.1 503 Service Unavailable");
exit('Could not acquire lock.');
}
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
if (!file_exists($chemin_fichier)) {
header("HTTP/1.1 404 Not Found");
exit;
}
$fp = fopen($chemin_fichier, 'r');
if (!$fp) {
header("HTTP/1.1 500 Internal Server Error");
exit('Could not open file.');
}
if (flock($fp, LOCK_EX)) {
$contenu = stream_get_contents($fp);
unlink($chemin_fichier);
flock($fp, LOCK_UN);
fclose($fp);
header('Content-Type: text/plain; charset=utf-8');
echo $contenu;
exit;
} else {
fclose($fp);
header("HTTP/1.1 503 Service Unavailable");
exit('Could not acquire lock.');
}
}
header("HTTP/1.1 405 Method Not Allowed");
exit;
.htaccess fileThis version of the .htaccess file correctly routes OPTIONS requests to the new handler.
File: .htaccess
RewriteEngine On
Options -Indexes
# 1. Capture the channel name in an environment variable.
RewriteRule ^canaux/([a-zA-Z0-9_-]+)$ - [E=CHANNEL_ID:$1]
# 2. Rule for OPTIONS (pre-flight) requests.
# These are ALWAYS sent to the dedicated PHP script to handle CORS.
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^canaux/.*$ /cors-handler.php [L]
# 3. Rule for GET requests (only if the data file exists).
RewriteCond %{REQUEST_METHOD} GET
RewriteCond %{DOCUMENT_ROOT}/canaux/%{ENV:CHANNEL_ID} -f
RewriteRule ^canaux/.*$ /canal.php?fichier=%{ENV:CHANNEL_ID} [L,QSA]
# 4. Rule for POST requests (always, to allow creation).
RewriteCond %{REQUEST_METHOD} POST
RewriteRule ^canaux/.*$ /canal.php?fichier=%{ENV:CHANNEL_ID} [L,QSA]Once the files are in place on your server, your endpoint is ready! To use it, simply construct a specific URL to launch iCanText and share it with your community members.
The URL consists of the application address, followed by a # fragment containing the endpoint parameter.
URL Format:
https://icantext.com/#&endpoint=https://your-domain.comReplace https://your-domain.com with the base URL where you installed the files. All users who access iCanText via this link will use your private signaling server instead of the default one.
Important: To ensure privacy, all members of your workspace must use the same URL with your custom endpoint. We recommend bookmarking this URL or sharing it as the single entry point for your group.
The PHP code and .htaccess configuration provided on this page are free software. We believe that transparency and collaboration are essential for security.
We have chosen to release this code under the MIT License. It is one of the most permissive open-source licenses. It allows you to:
The MIT License requires only one simple obligation: you must include the original copyright notice in any copy or modification of the software you distribute. The notice to retain is as follows:
Copyright (c) 2025 S.A.S. SAY SEE SHOW ( https://sayseeshow.com )The software is provided "as is", without warranty of any kind.
iCanText est conçu pour être radicalement décentralisé. Cependant, pour que les utilisateurs puissent se trouver et initier une connexion, un "lieu de rendez-vous" public est nécessaire. C'est le rôle du serveur de signalisation, ou "endpoint".
Par défaut, iCanText utilise l'endpoint https://p.sayseeshow.io. Bien que ce service soit minimaliste et respectueux de la vie privée (il n'enregistre rien), le simple fait de l'utiliser révèle à nos serveurs qu'une tentative de connexion a eu lieu.
En déployant votre propre endpoint sur un serveur que vous contrôlez, vous atteignez un niveau de confidentialité supérieur :
Ce guide s'adresse à un déploiement sur un hébergement web mutualisé standard. Vous aurez besoin de :
.htaccess (pour les serveurs Apache).Sur votre serveur, créez la structure de dossiers et de fichiers suivante. Par exemple, à la racine de votre site https://votre-domaine.com/ :
/ (racine de votre site) ├── canaux/ (Ce dossier doit avoir les permissions d'écriture, ex: 755) ├── canal.php (Le script principal) ├── .htaccess (Le fichier de configuration Apache) ├── cors-handler.php (Gestionnaire pour les requêtes CORS) └── cors_config.php (Configuration des domaines autorisés)
Note de sécurité : La configuration CORS ci-dessous est sécurisée car elle n'autorise que des domaines spécifiques. Vous pouvez éditer la liste $allowedOrigins dans cors_config.php pour ajouter vos propres domaines si vous hébergez une version modifiée de l'application iCanText.
cors_config.phpCe fichier centralise les domaines autorisés à se connecter à votre endpoint.
Fichier : cors_config.php
<?php
// Fichier : cors_config.php
$allowedOrigins = [
'https://sayseeshow.io',
'https://icansend.com',
'https://icanphone.com',
'https://icantext.com'
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins, true)) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Credentials: true');
}cors-handler.phpCe script gère les requêtes "pre-flight" (OPTIONS) envoyées par les navigateurs.
Fichier : cors-handler.php
<?php
// Fichier : cors-handler.php
// Ce script est appelé par le .htaccess uniquement pour les requêtes OPTIONS.
require_once __DIR__ . '/cors_config.php';
// Les requêtes OPTIONS (pre-flight) n'ont besoin que de ces en-têtes.
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
header('Access-Control-Max-Age: 86400');
header("HTTP/1.1 204 No Content");
exit(0);canal.phpCe code php utilise la configuration CORS.
Fichier : canal.php
<?php
/*
* Endpoint de signalisation WebRTC basé sur des fichiers.
*/
require_once __DIR__ . '/cors_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
require __DIR__ . '/cors-handler.php';
exit(0);
}
define('MAX_INPUT_SIZE', 15 * 1024);
define('MAX_FILE_SIZE', 150 * 1024);
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
$fichier = $_GET['fichier'] ?? '';
if (preg_match('/^[a-zA-Z0-9_-]+$/', $fichier) !== 1) {
header("HTTP/1.1 400 Bad Request");
exit('Nom de canal invalide.');
}
$chemin_fichier = __DIR__ . '/canaux/' . basename($fichier);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$contenu = file_get_contents('php://input');
if (strlen($contenu) > MAX_INPUT_SIZE) {
header("HTTP/1.1 413 Payload Too Large");
exit('La charge utile dépasse la limite.');
}
if (!$contenu) {
header("HTTP/1.1 400 Bad Request");
exit('Aucun contenu reçu.');
}
$fp = fopen($chemin_fichier, 'a');
if (!$fp) {
header("HTTP/1.1 500 Internal Server Error");
exit('Impossible d\'ouvrir le fichier de canal.');
}
if (flock($fp, LOCK_EX)) {
clearstatcache(true, $chemin_fichier);
if (file_exists($chemin_fichier) && filesize($chemin_fichier) + strlen($contenu) > MAX_FILE_SIZE) {
flock($fp, LOCK_UN);
fclose($fp);
header("HTTP/1.1 409 Conflict");
exit('Le canal est plein.');
}
fwrite($fp, $contenu . "\n---\n");
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
header("HTTP/1.1 201 Created");
exit;
} else {
fclose($fp);
header("HTTP/1.1 503 Service Unavailable");
exit('Impossible d\'acquérir un verrou.');
}
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
if (!file_exists($chemin_fichier)) {
header("HTTP/1.1 404 Not Found");
exit;
}
$fp = fopen($chemin_fichier, 'r');
if (!$fp) {
header("HTTP/1.1 500 Internal Server Error");
exit('Impossible d\'ouvrir le fichier.');
}
if (flock($fp, LOCK_EX)) {
$contenu = stream_get_contents($fp);
unlink($chemin_fichier);
flock($fp, LOCK_UN);
fclose($fp);
header('Content-Type: text/plain; charset=utf-8');
echo $contenu;
exit;
} else {
fclose($fp);
header("HTTP/1.1 503 Service Unavailable");
exit('Impossible d\'acquérir un verrou.');
}
}
header("HTTP/1.1 405 Method Not Allowed");
exit;
.htaccessCette version du .htaccess redirige correctement les requêtes OPTIONS vers le nouveau gestionnaire.
Fichier : .htaccess
RewriteEngine On
Options -Indexes
# 1. Capture du nom du canal dans une variable d'environnement.
RewriteRule ^canaux/([a-zA-Z0-9_-]+)$ - [E=CHANNEL_ID:$1]
# 2. Règle pour les requêtes OPTIONS (pre-flight)
# On les envoie TOUJOURS au script PHP dédié pour gérer le CORS.
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^canaux/.*$ /cors-handler.php [L]
# 3. Règle pour les requêtes GET (uniquement si le fichier de données existe).
RewriteCond %{REQUEST_METHOD} GET
RewriteCond %{DOCUMENT_ROOT}/canaux/%{ENV:CHANNEL_ID} -f
RewriteRule ^canaux/.*$ /canal.php?fichier=%{ENV:CHANNEL_ID} [L,QSA]
# 4. Règle pour les requêtes POST (toujours, pour permettre la création).
RewriteCond %{REQUEST_METHOD} POST
RewriteRule ^canaux/.*$ /canal.php?fichier=%{ENV:CHANNEL_ID} [L,QSA]Une fois les fichiers en place sur votre serveur, votre endpoint est prêt ! Pour l'utiliser, il suffit de construire une URL spécifique pour lancer iCanText et de la partager avec les membres de votre communauté.
L'URL se compose de l'adresse de l'application, suivie d'un fragment # contenant le paramètre endpoint.
Format de l'URL :
https://icantext.com/#&endpoint=https://votre-domaine.comRemplacez https://votre-domaine.com par l'URL de base où vous avez installé les fichiers. Tous les utilisateurs qui accèdent à iCanText via ce lien utiliseront votre serveur de signalisation privé au lieu de celui par défaut.
Important : Pour garantir la confidentialité, tous les membres de votre espace de travail doivent utiliser la même URL avec votre endpoint personnalisé. Nous vous recommandons de mettre cette URL dans vos favoris ou de la partager comme point d'entrée unique pour votre groupe.
Le code PHP et la configuration .htaccess fournis sur cette page sont des logiciels libres. Nous pensons que la transparence et la collaboration sont essentielles à la sécurité.
Nous avons choisi de publier ce code sous la Licence MIT. C'est l'une des licences open source les plus permissives. Elle vous autorise à :
La licence MIT vous impose une seule obligation simple : inclure l'avis de droit d'auteur original dans toute copie ou modification du logiciel que vous distribuez. L'avis à conserver est le suivant :
Copyright (c) 2025 S.A.S. SAY SEE SHOW ( https://sayseeshow.fr )Le logiciel est fourni "en l'état", sans aucune garantie.