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) oauthAllowedHost = os.Getenv("OAUTH_ALLOWED_HOST") // URL authorized for device code oauthDeviceCodeAllowedIPs = strings.Split(os.Getenv("OAUTH_DEVICE_CODE_ALLOWED_IPS"), ",") // IPS autorisées pour /retrieve ) // 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 := ` Query Form

Query Form

` fmt.Fprintf(w, tmpl) } // selectHandler handles HTTP requests and executes a SQL query. func selectHandler(w http.ResponseWriter, r *http.Request) { // Get the 'param' query parameter paramStr := r.URL.Query().Get("param") if paramStr == "" { http.Error(w, "Missing 'param' query parameter", http.StatusBadRequest) return } // Convert the param to an integer param, err := strconv.Atoi(paramStr) if err != nil { http.Error(w, "Invalid 'param' query parameter. Must be an integer.", http.StatusBadRequest) return } // Prepare the SQL query to prevent SQL injection query := "SELECT 42 + $1" // Execute the query with the provided parameter var result int err = db.QueryRow(query, param).Scan(&result) if err != nil { log.Printf("Failed to execute query: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } // Return the result in a simple HTML response fmt.Fprintf(w, "

Result: %d

", result) } // Structure de base pour passer les données au template HTML type CallbackData struct { Code string State string Other map[string]string } // oauth2_callback handles HTTP requests and display a message according to queryParams func oauth2_callback(w http.ResponseWriter, r *http.Request) { // Vérifier le référent (ou origine) hostHeader := strings.Trim(r.Header.Get("X-Forwarded-Host"), "[]") if oauthAllowedHost != "" && hostHeader != oauthAllowedHost { fmt.Fprintln(os.Stderr, "X-Forwarded-Host: "+hostHeader) fmt.Fprintln(os.Stderr, "received headers") for key, value := range r.Header { fmt.Fprintf(os.Stderr, "%s='%s'\n", key, value) } http.Error(w, "Access denied: invalid referer or origin", http.StatusForbidden) return } // Récupération des paramètres query queryParams := r.URL.Query() code := queryParams.Get("code") state := queryParams.Get("state") // Stocker state et code dans le cache avec TTL de 5 minutes if state != "" && code != "" { c.Set(state, code, cache.DefaultExpiration) } // Construction d'une map pour les autres paramètres otherParams := make(map[string]string) for key, values := range queryParams { if key != "code" && key != "state" { otherParams[key] = strings.Join(values, ", ") } } // Définition du template HTML avec le comportement demandé tmpl := ` OAuth2 Callback {{if and .Code .State}}

Succès: Code et State reçus

Code: {{.Code}}

State: {{.State}}

{{else}}

Aucun code et state reçus

{{end}} {{if .Other}}

Autres paramètres reçus:

			{{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 := r.Header.Get("X-Forwarded-For") if err != nil || !slices.Contains(oauthDeviceCodeAllowedIPs, userIP) && !slices.Contains(oauthDeviceCodeAllowedIPs, userIPforwarded) { fmt.Fprintln(os.Stderr, "denied userIP: "+userIP) // 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 := ` OAuth2 Callback Test

Welcome to OAuth2 Test





` w.Header().Set("Content-Type", "text/html") w.Write([]byte(html)) } 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) /* Gitea doesn't come with device flow # https://github.com/go-gitea/gitea/issues/27309 https://gitea.arcodange.duckdns.org/.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) } }