package main import ( "database/sql" "fmt" "html/template" "log" "net" "net/http" "os" "slices" "strconv" "strings" "time" "github.com/patrickmn/go-cache" _ "github.com/lib/pq" // PostgreSQL driver ) var ( db *sql.DB // Global database connection c = cache.New(5*time.Minute, 10*time.Minute) oauthAllowedHosts = strings.Split(os.Getenv("OAUTH_ALLOWED_HOSTS"), ",") // authorized HOSTS for device code oauthDeviceCodeAllowedIPs = strings.Split(os.Getenv("OAUTH_DEVICE_CODE_ALLOWED_IPS"), ",") // IPS autorisées pour /retrieve _, localNetwork, _ = net.ParseCIDR("192.168.0.0/16") _, localNetworkIPV6, _ = net.ParseCIDR("2a01:cb04:dff:cf00::/56") _, k3sNetwork, _ = net.ParseCIDR("10.42.0.0/16") ) // dbConnection initializes the database connection. func dbConnection() (*sql.DB, error) { connStr := os.Getenv("DATABASE_URL") // You should set this env var, e.g., postgres://username:password@localhost/dbname?sslmode=disable return sql.Open("postgres", connStr) } // livenessHandler is used by Kubernetes to check if the app is alive. func livenessHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "OK") } // readinessHandler is used by Kubernetes to check if the app is ready to serve traffic. func readinessHandler(w http.ResponseWriter, r *http.Request) { // Check if the database connection is alive err := db.Ping() if err != nil { log.Printf("Readiness probe failed: Database connection error: %v", err) w.WriteHeader(http.StatusServiceUnavailable) fmt.Fprintf(w, "NOT READY") return } // If the database is reachable, return 200 OK w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "OK") } // indexHandler serves the HTML form for the query. func indexHandler(w http.ResponseWriter, r *http.Request) { tmpl := `
Code: {{.Code}}
State: {{.State}}
{{else}}
{{range $key, $value := .Other}}
{{$key}}: {{$value}}
{{end}}
{{else}}
Aucun autre paramètre reçu.
{{end}} ` // Parsing du template t, err := template.New("callback").Parse(tmpl) if err != nil { http.Error(w, "Error parsing template", http.StatusInternalServerError) return } // Remplissage des données et exécution du template data := CallbackData{ Code: code, State: state, Other: otherParams, } w.Header().Set("Content-Type", "text/html") if err := t.Execute(w, data); err != nil { http.Error(w, "Error rendering template", http.StatusInternalServerError) } } // Handler pour récupérer un code à partir d'un state avec une restriction d'IP func retrieveHandler(w http.ResponseWriter, r *http.Request) { // Récupérer l'IP de l'utilisateur userIP, _, err := net.SplitHostPort(r.RemoteAddr) userIPforwarded := net.ParseIP(r.Header.Get("X-Forwarded-For")) ip := userIPforwarded if ip == nil { ip = net.ParseIP(userIP) } if err != nil || !slices.Contains(oauthDeviceCodeAllowedIPs, ip.String()) && !localNetwork.Contains(ip) && !localNetworkIPV6.Contains(ip) && !k3sNetwork.Contains(ip) { fmt.Fprintln(os.Stderr, "denied userIP: "+userIP+" forwarded: "+userIPforwarded.String()) fmt.Fprintf(os.Stderr, "alowed ips: %+v", oauthDeviceCodeAllowedIPs) // Parcourir tous les headers for name, values := range r.Header { // name représente le nom de l'en-tête // values est une slice contenant toutes les valeurs associées à cet en-tête for _, value := range values { fmt.Fprintf(os.Stderr, "%s: %s\n", name, value) } } http.Error(w, "Access denied: invalid IP", http.StatusForbidden) return } // Récupérer le paramètre `state` depuis l'URL state := r.URL.Query().Get("state") if state == "" { http.Error(w, "State parameter is required", http.StatusBadRequest) return } // Vérifier si le state existe dans le cache code, found := c.Get(state) if !found { http.Error(w, "State not found or expired", http.StatusNotFound) return } // Retourner le code associé au state w.WriteHeader(http.StatusOK) fmt.Fprint(w, code) } func test_oauth2_callback(w http.ResponseWriter, r *http.Request) { html := `{{.UserIP}}
{{range $key, $value := .Headers}}
{{$key}}: {{$value}}
{{end}}
`
data := struct {
Cookies []string
UserIP string
Headers map[string]string
}{
Cookies: []string{},
UserIP: userIP,
Headers: headers,
}
for _, cookie := range cookies {
data.Cookies = append(data.Cookies, fmt.Sprintf("%s=%s", cookie.Name, cookie.Value))
}
w.Header().Set("Content-Type", "text/html")
tmplBytes, err := template.New("info").Parse(tmpl)
if err != nil {
http.Error(w, "Error parsing template", http.StatusInternalServerError)
fmt.Printf("%+v\n", err)
return
}
err = tmplBytes.Execute(w, data)
if err != nil {
http.Error(w, "Error rendering template", http.StatusInternalServerError)
fmt.Printf("%+v\n", err)
return
}
}
func main() {
var err error
// Initialize the database connection once at startup
db, err = dbConnection()
if err != nil {
log.Fatalf("Failed to connect to the database: %v", err)
}
defer db.Close()
// Define the handler for the `/` route (serves HTML form)
http.HandleFunc("/", indexHandler)
// Define the handler for the `/query` route (executes SQL query)
http.HandleFunc("/query", selectHandler)
// Define the handler for the `/liveness` probe
http.HandleFunc("/liveness", livenessHandler)
// Define the handler for the `/readiness` probe
http.HandleFunc("/readiness", readinessHandler)
http.HandleFunc("/display-info", displayInfoHandler)
/*
Gitea doesn't come with device flow # https://github.com/go-gitea/gitea/issues/27309
https://gitea.arcodange.lab/.well-known/openid-configuration
"grant_types_supported": [
"authorization_code",
"refresh_token"
]
So we can use the authorization_code and redirect to this endpoint
and then the client can poll for the code matching the state it chose
*/
http.HandleFunc("/oauth-callback", oauth2_callback)
// Define the handler to exchange a state for a code
http.HandleFunc("/retrieve", retrieveHandler)
http.HandleFunc("/test-oauth-callback", test_oauth2_callback)
// Start the HTTP server
port := ":8080"
log.Printf("Server starting on port %s\n", port)
err = http.ListenAndServe(port, nil)
if err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}