mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +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.
|
||||
handler := newLinkHandler()
|
||||
// Front TLS de l'accès OpenAI public (nil si JEAN_LINK_ALLOW_OAI != 1). Le
|
||||
// certificat est obtenu à la demande via TLS-ALPN-01 à travers le tunnel.
|
||||
// Front TLS de l'accès OpenAI public. Toujours prêt ; c'est le drapeau
|
||||
// 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()
|
||||
if oaiTLS != nil {
|
||||
fmt.Printf("%s accès OpenAI public activé (TLS terminé ici, cert Let's Encrypt à la demande)\n", green("[oai]"))
|
||||
}
|
||||
fmt.Printf("%s front OpenAI public prêt (activation en direct via l'UI ; état: %v)\n", green("[oai]"), oaiPublicEnabled())
|
||||
|
||||
backoff := time.Second
|
||||
for {
|
||||
@@ -435,8 +434,12 @@ func demuxTunnelStream(stream net.Conn, httpLn, oaiLn *chanListener) {
|
||||
return
|
||||
}
|
||||
pc := &peekedConn{Conn: stream, r: br}
|
||||
if oaiLn != nil && b[0] == 0x16 {
|
||||
oaiLn.push(pc)
|
||||
if b[0] == 0x16 { // handshake TLS = accès OpenAI public
|
||||
if oaiLn != nil && oaiPublicEnabled() {
|
||||
oaiLn.push(pc)
|
||||
} else {
|
||||
stream.Close() // public désactivé → on refuse (fail-closed)
|
||||
}
|
||||
return
|
||||
}
|
||||
httpLn.push(pc)
|
||||
|
||||
@@ -72,15 +72,35 @@ func runOAIFront(rawLn net.Listener, tlsCfg *tls.Config) error {
|
||||
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
|
||||
// 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
|
||||
// $JEAN_HOME/certs et ne quitte jamais la machine. Renvoie nil si l'accès OpenAI
|
||||
// public n'est pas autorisé (JEAN_LINK_ALLOW_OAI != 1).
|
||||
// $JEAN_HOME/certs et ne quitte jamais la machine. Toujours construite ; c'est le
|
||||
// démux (oaiPublicEnabled, lu en direct) qui décide de router ou non le trafic.
|
||||
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.DefaultACME.Agreed = true
|
||||
certmagic.DefaultACME.Email = strings.TrimSpace(os.Getenv("JEAN_ACME_EMAIL"))
|
||||
|
||||
@@ -331,6 +331,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
</details>
|
||||
<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>
|
||||
<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 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">
|
||||
@@ -773,6 +774,8 @@ function renderApiKey(d){
|
||||
const host = d.host || location.hostname;
|
||||
document.getElementById('oai-url').value = 'http://'+host+':'+d.port+'/v1';
|
||||
// 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');
|
||||
if(d.oai_public && d.machine){
|
||||
document.getElementById('oai-public-url').value = 'https://'+d.machine+'.oai.ajean.link/v1';
|
||||
@@ -794,6 +797,12 @@ async function apiKeyAction(action){
|
||||
toast('application…');
|
||||
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(){
|
||||
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;
|
||||
|
||||
@@ -87,6 +87,7 @@ func newWebMux() *http.ServeMux {
|
||||
api("/api/agent/toggle", handleAgentToggle)
|
||||
api("/api/agent/tool-limit", handleToolLimitToggle)
|
||||
api("/api/apikey", handleAPIKey)
|
||||
api("/api/oai/public", handleOAIPublic)
|
||||
api("/api/internet", handleInternet)
|
||||
api("/api/memory", handleMemoryMode)
|
||||
// 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(),
|
||||
// Accès OpenAI PUBLIC via ajean.link (passthrough SNI, VPS aveugle) : si
|
||||
// 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(),
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
|
||||
Reference in new issue
Block a user