mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 06:58:14 +02:00
oai: toggle UI exposer en public (drapeau .oai_public lu en direct au demux, sans redemarrage) + endpoint /api/oai/public
This commit is contained in:
1 parent
6767a816a9
commit
4d9dcb2d6e
4 files changed
+72
-13
No files matched your search
@@ -222,12 +222,11 @@ func runLinkForeground() error {
|
|||||||
|
|
||||||
// On construit le handler une seule fois ; il est servi à travers chaque tunnel.
|
// On construit le handler une seule fois ; il est servi à travers chaque tunnel.
|
||||||
handler := newLinkHandler()
|
handler := newLinkHandler()
|
||||||
// Front TLS de l'accès OpenAI public (nil si JEAN_LINK_ALLOW_OAI != 1). Le
|
// Front TLS de l'accès OpenAI public. Toujours prêt ; c'est le drapeau
|
||||||
// certificat est obtenu à la demande via TLS-ALPN-01 à travers le tunnel.
|
// oaiPublicEnabled (piloté par l'UI, lu en direct au démux) qui autorise ou non
|
||||||
|
// le trafic. Cert obtenu à la demande via TLS-ALPN-01 à travers le tunnel.
|
||||||
oaiTLS := oaiTLSConfig()
|
oaiTLS := oaiTLSConfig()
|
||||||
if oaiTLS != nil {
|
fmt.Printf("%s front OpenAI public prêt (activation en direct via l'UI ; état: %v)\n", green("[oai]"), oaiPublicEnabled())
|
||||||
fmt.Printf("%s accès OpenAI public activé (TLS terminé ici, cert Let's Encrypt à la demande)\n", green("[oai]"))
|
|
||||||
}
|
|
||||||
|
|
||||||
backoff := time.Second
|
backoff := time.Second
|
||||||
for {
|
for {
|
||||||
@@ -435,8 +434,12 @@ func demuxTunnelStream(stream net.Conn, httpLn, oaiLn *chanListener) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
pc := &peekedConn{Conn: stream, r: br}
|
pc := &peekedConn{Conn: stream, r: br}
|
||||||
if oaiLn != nil && b[0] == 0x16 {
|
if b[0] == 0x16 { // handshake TLS = accès OpenAI public
|
||||||
oaiLn.push(pc)
|
if oaiLn != nil && oaiPublicEnabled() {
|
||||||
|
oaiLn.push(pc)
|
||||||
|
} else {
|
||||||
|
stream.Close() // public désactivé → on refuse (fail-closed)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
httpLn.push(pc)
|
httpLn.push(pc)
|
||||||
|
|||||||
@@ -72,15 +72,35 @@ func runOAIFront(rawLn net.Listener, tlsCfg *tls.Config) error {
|
|||||||
return srv.Serve(tls.NewListener(rawLn, tlsCfg))
|
return srv.Serve(tls.NewListener(rawLn, tlsCfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// oaiPublicPath est le drapeau qui active l'accès OpenAI public (piloté par l'UI,
|
||||||
|
// lu en direct → activable/coupable sans redémarrer le service de lien).
|
||||||
|
func oaiPublicPath() string { return filepath.Join(JeanHome(), ".oai_public") }
|
||||||
|
|
||||||
|
// oaiPublicEnabled indique si l'accès OpenAI public est activé pour cette machine.
|
||||||
|
func oaiPublicEnabled() bool {
|
||||||
|
if _, err := os.Stat(oaiPublicPath()); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return os.Getenv("JEAN_LINK_ALLOW_OAI") == "1" // rétro-compat (ancien drapeau env)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setOAIPublic active (on) ou coupe (off) l'accès OpenAI public.
|
||||||
|
func setOAIPublic(on bool) error {
|
||||||
|
if on {
|
||||||
|
return os.WriteFile(oaiPublicPath(), []byte("1\n"), 0o600)
|
||||||
|
}
|
||||||
|
if err := os.Remove(oaiPublicPath()); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// oaiTLSConfig renvoie une config TLS qui, à la demande, obtient/renouvelle via
|
// oaiTLSConfig renvoie une config TLS qui, à la demande, obtient/renouvelle via
|
||||||
// Let's Encrypt (TLS-ALPN-01) le certificat de tout nom en *.oai.ajean.link, et
|
// Let's Encrypt (TLS-ALPN-01) le certificat de tout nom en *.oai.ajean.link, et
|
||||||
// répond elle-même aux challenges ACME. La clé privée est stockée dans
|
// répond elle-même aux challenges ACME. La clé privée est stockée dans
|
||||||
// $JEAN_HOME/certs et ne quitte jamais la machine. Renvoie nil si l'accès OpenAI
|
// $JEAN_HOME/certs et ne quitte jamais la machine. Toujours construite ; c'est le
|
||||||
// public n'est pas autorisé (JEAN_LINK_ALLOW_OAI != 1).
|
// démux (oaiPublicEnabled, lu en direct) qui décide de router ou non le trafic.
|
||||||
func oaiTLSConfig() *tls.Config {
|
func oaiTLSConfig() *tls.Config {
|
||||||
if os.Getenv("JEAN_LINK_ALLOW_OAI") != "1" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
certmagic.Default.Storage = &certmagic.FileStorage{Path: filepath.Join(JeanHome(), "certs")}
|
certmagic.Default.Storage = &certmagic.FileStorage{Path: filepath.Join(JeanHome(), "certs")}
|
||||||
certmagic.DefaultACME.Agreed = true
|
certmagic.DefaultACME.Agreed = true
|
||||||
certmagic.DefaultACME.Email = strings.TrimSpace(os.Getenv("JEAN_ACME_EMAIL"))
|
certmagic.DefaultACME.Email = strings.TrimSpace(os.Getenv("JEAN_ACME_EMAIL"))
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
|||||||
</details>
|
</details>
|
||||||
<details><summary>Accès OpenAI</summary>
|
<details><summary>Accès OpenAI</summary>
|
||||||
<div class="subhead">Endpoint compatible OpenAI<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Branche n'importe quel client compatible OpenAI (OpenCode, Continue, Cursor…) sur cette URL. Modèle : <code>jean</code>. Si une clé est définie, le client doit envoyer <code>Authorization: Bearer <clé></code>.</span></span></div>
|
<div class="subhead">Endpoint compatible OpenAI<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Branche n'importe quel client compatible OpenAI (OpenCode, Continue, Cursor…) sur cette URL. Modèle : <code>jean</code>. Si une clé est définie, le client doit envoyer <code>Authorization: Bearer <clé></code>.</span></span></div>
|
||||||
|
<label class="switchrow" style="margin-top:6px"><span class="switch"><input type="checkbox" id="oai-public-toggle" onchange="toggleOAIPublic()"><span class="slider"></span></span> exposer en public (ajean.link)<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Rend ton IA joignable depuis <b>n'importe où</b> via <code>https://<machine>.oai.ajean.link/v1</code> (pour brancher un SaaS). Le trafic est chiffré <b>de bout en bout</b> jusqu'à ce serveur — le relais ajean.link ne voit rien. Nécessite un abonnement ajean.link actif.</span></span></label>
|
||||||
<div id="oai-public-wrap" style="display:none;margin-top:4px">
|
<div id="oai-public-wrap" style="display:none;margin-top:4px">
|
||||||
<div class="muted" style="font-size:11px;margin-bottom:2px">🌍 public (ajean.link) — accessible partout, TLS de bout en bout jusqu'à ce serveur</div>
|
<div class="muted" style="font-size:11px;margin-bottom:2px">🌍 public (ajean.link) — accessible partout, TLS de bout en bout jusqu'à ce serveur</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -773,6 +774,8 @@ function renderApiKey(d){
|
|||||||
const host = d.host || location.hostname;
|
const host = d.host || location.hostname;
|
||||||
document.getElementById('oai-url').value = 'http://'+host+':'+d.port+'/v1';
|
document.getElementById('oai-url').value = 'http://'+host+':'+d.port+'/v1';
|
||||||
// Endpoint PUBLIC (ajean.link) : affiché seulement si l'accès public est activé.
|
// Endpoint PUBLIC (ajean.link) : affiché seulement si l'accès public est activé.
|
||||||
|
const tg = document.getElementById('oai-public-toggle');
|
||||||
|
if(tg) tg.checked = !!d.oai_public;
|
||||||
const pubWrap = document.getElementById('oai-public-wrap');
|
const pubWrap = document.getElementById('oai-public-wrap');
|
||||||
if(d.oai_public && d.machine){
|
if(d.oai_public && d.machine){
|
||||||
document.getElementById('oai-public-url').value = 'https://'+d.machine+'.oai.ajean.link/v1';
|
document.getElementById('oai-public-url').value = 'https://'+d.machine+'.oai.ajean.link/v1';
|
||||||
@@ -794,6 +797,12 @@ async function apiKeyAction(action){
|
|||||||
toast('application…');
|
toast('application…');
|
||||||
renderApiKey(await jpost('/api/apikey', {action}));
|
renderApiKey(await jpost('/api/apikey', {action}));
|
||||||
}
|
}
|
||||||
|
async function toggleOAIPublic(){
|
||||||
|
const on = document.getElementById('oai-public-toggle').checked;
|
||||||
|
await jpost('/api/oai/public', {enabled:on});
|
||||||
|
toast(on ? 'accès public activé' : 'accès public coupé');
|
||||||
|
loadApiKey();
|
||||||
|
}
|
||||||
async function apiKeySet(){
|
async function apiKeySet(){
|
||||||
const k = await askPrompt('Colle ta clé API (ou laisse vide pour annuler) :', {title:'Définir la clé API', placeholder:'sk-…'});
|
const k = await askPrompt('Colle ta clé API (ou laisse vide pour annuler) :', {title:'Définir la clé API', placeholder:'sk-…'});
|
||||||
if(!k || !k.trim()) return;
|
if(!k || !k.trim()) return;
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ func newWebMux() *http.ServeMux {
|
|||||||
api("/api/agent/toggle", handleAgentToggle)
|
api("/api/agent/toggle", handleAgentToggle)
|
||||||
api("/api/agent/tool-limit", handleToolLimitToggle)
|
api("/api/agent/tool-limit", handleToolLimitToggle)
|
||||||
api("/api/apikey", handleAPIKey)
|
api("/api/apikey", handleAPIKey)
|
||||||
|
api("/api/oai/public", handleOAIPublic)
|
||||||
api("/api/internet", handleInternet)
|
api("/api/internet", handleInternet)
|
||||||
api("/api/memory", handleMemoryMode)
|
api("/api/memory", handleMemoryMode)
|
||||||
// Alias rétro-compat : l'ancien portail ajean.link (dépôt jean-relay) pilote
|
// Alias rétro-compat : l'ancien portail ajean.link (dépôt jean-relay) pilote
|
||||||
@@ -550,11 +551,37 @@ func handleAPIKey(w http.ResponseWriter, r *http.Request) {
|
|||||||
"host": localIP(),
|
"host": localIP(),
|
||||||
// Accès OpenAI PUBLIC via ajean.link (passthrough SNI, VPS aveugle) : si
|
// Accès OpenAI PUBLIC via ajean.link (passthrough SNI, VPS aveugle) : si
|
||||||
// activé, l'URL publique est https://<machine>.oai.ajean.link/v1.
|
// activé, l'URL publique est https://<machine>.oai.ajean.link/v1.
|
||||||
"oai_public": os.Getenv("JEAN_LINK_ALLOW_OAI") == "1",
|
"oai_public": oaiPublicEnabled(),
|
||||||
"machine": machineID(),
|
"machine": machineID(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleOAIPublic pilote le drapeau d'accès OpenAI public (exposition via
|
||||||
|
// ajean.link). GET renvoie l'état ; POST {enabled} l'active/coupe en direct
|
||||||
|
// (aucun redémarrage : le démux du tunnel relit le drapeau à chaque connexion).
|
||||||
|
func handleOAIPublic(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
var req struct {
|
||||||
|
Enabled *bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Enabled != nil {
|
||||||
|
if err := setOAIPublic(*req.Enabled); err != nil {
|
||||||
|
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendJSON(w, 200, map[string]any{
|
||||||
|
"ok": true,
|
||||||
|
"enabled": oaiPublicEnabled(),
|
||||||
|
"machine": machineID(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// localIP best-effort renvoie l'IPv4 LAN primaire de la machine (l'IP source du
|
// localIP best-effort renvoie l'IPv4 LAN primaire de la machine (l'IP source du
|
||||||
// trafic sortant), ou "localhost" à défaut. Sert à annoncer l'endpoint OpenAI
|
// trafic sortant), ou "localhost" à défaut. Sert à annoncer l'endpoint OpenAI
|
||||||
// avec une adresse correcte sur le réseau local MÊME quand l'UI est atteinte via
|
// avec une adresse correcte sur le réseau local MÊME quand l'UI est atteinte via
|
||||||
|
|||||||
Reference in new issue
Block a user