diff --git a/link.go b/link.go index 5fd92dd..e3ab18a 100644 --- a/link.go +++ b/link.go @@ -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) diff --git a/oai.go b/oai.go index 69f5f84..bc76bb2 100644 --- a/oai.go +++ b/oai.go @@ -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")) diff --git a/ui/index.html b/ui/index.html index 7fbe155..cceb41b 100644 --- a/ui/index.html +++ b/ui/index.html @@ -331,6 +331,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
Accès OpenAI
Endpoint compatible OpenAI?Branche n'importe quel client compatible OpenAI (OpenCode, Continue, Cursor…) sur cette URL. Modèle : jean. Si une clé est définie, le client doit envoyer Authorization: Bearer <clé>.
+