All checks were successful
Docker Build / build-and-push-image (push) Successful in 53s
514 lines
13 KiB
Go
514 lines
13 KiB
Go
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 := `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Query Form</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
background-color: #f4f4f4;
|
|
text-align: center;
|
|
margin-top: 50px;
|
|
}
|
|
form {
|
|
margin: 0 auto;
|
|
max-width: 300px;
|
|
padding: 20px;
|
|
background-color: white;
|
|
border-radius: 10px;
|
|
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1);
|
|
}
|
|
input[type="number"], input[type="submit"] {
|
|
width: 100%;
|
|
padding: 10px;
|
|
margin: 5px 0;
|
|
border: 1px solid #ccc;
|
|
border-radius: 5px;
|
|
}
|
|
input[type="submit"] {
|
|
background-color: #5cb85c;
|
|
color: white;
|
|
border: none;
|
|
cursor: pointer;
|
|
}
|
|
input[type="submit"]:hover {
|
|
background-color: #4cae4c;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Query Form</h1>
|
|
<form action="/query" method="get">
|
|
<label for="param">Enter a number:</label>
|
|
<input type="number" id="param" name="param" required>
|
|
<input type="submit" value="Submit">
|
|
</form>
|
|
</body>
|
|
</html>
|
|
`
|
|
fmt.Fprint(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, "<h1>Result: %d</h1>", 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 len(oauthAllowedHosts) > 0 && !slices.Contains(oauthAllowedHosts, hostHeader) {
|
|
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 := `
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
<title>OAuth2 Callback</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
background-color: #f4f4f4;
|
|
padding: 20px;
|
|
}
|
|
h2, h3 {
|
|
color: #333;
|
|
}
|
|
p {
|
|
font-size: 16px;
|
|
}
|
|
button {
|
|
padding: 10px 20px;
|
|
background-color: #007BFF;
|
|
color: white;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: background-color 0.3s;
|
|
}
|
|
button:hover {
|
|
background-color: #0056b3;
|
|
}
|
|
.fade-out {
|
|
animation: fadeOut 2s forwards;
|
|
}
|
|
@keyframes fadeOut {
|
|
0% { opacity: 1; }
|
|
100% { opacity: 0; }
|
|
}
|
|
pre {
|
|
background-color: #eaeaea;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
}
|
|
</style>
|
|
<script>
|
|
function copyToClipboard(text) {
|
|
navigator.clipboard.writeText(text).then(function() {
|
|
var msg = document.getElementById('copy-message');
|
|
msg.textContent = 'Copied to clipboard!';
|
|
msg.classList.add('fade-out');
|
|
setTimeout(function() {
|
|
msg.textContent = ''; // Hide message after fading out
|
|
}, 2000); // Hide after 2 seconds
|
|
}, function(err) {
|
|
alert('Error copying to clipboard: ' + err);
|
|
});
|
|
}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
{{if and .Code .State}}
|
|
<h2>Succès: Code et State reçus</h2>
|
|
<p>Code: {{.Code}}</p>
|
|
<p>State: {{.State}}</p>
|
|
<button onclick="copyToClipboard('Code: {{.Code}}\t State: {{.State}}')">Copier dans le presse-papier</button>
|
|
<p id="copy-message" style="color: green; font-weight: bold;"></p>
|
|
{{else}}
|
|
<h2>Aucun code et state reçus</h2>
|
|
{{end}}
|
|
{{if .Other}}
|
|
<h3>Autres paramètres reçus:</h3>
|
|
<pre>
|
|
{{range $key, $value := .Other}}
|
|
{{$key}}: {{$value}}
|
|
{{end}}
|
|
</pre>
|
|
{{else}}
|
|
<p>Aucun autre paramètre reçu.</p>
|
|
{{end}}
|
|
</body>
|
|
</html>
|
|
`
|
|
|
|
// 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 := `
|
|
<html>
|
|
<head>
|
|
<title>OAuth2 Callback Test</title>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
margin: 0;
|
|
padding: 20px;
|
|
background-color: #f4f4f4;
|
|
}
|
|
h1 {
|
|
color: #333;
|
|
}
|
|
form {
|
|
margin-top: 20px;
|
|
}
|
|
input[type="text"], button {
|
|
padding: 10px;
|
|
margin-top: 10px;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
button {
|
|
background-color: #007BFF;
|
|
color: white;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: background-color 0.3s;
|
|
}
|
|
button:hover {
|
|
background-color: #0056b3;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Welcome to OAuth2 Test</h1>
|
|
<form action="/oauth-callback" method="get">
|
|
<label>Paramètre Code:</label><br>
|
|
<input type="text" name="code"><br>
|
|
<label>Paramètre State:</label><br>
|
|
<input type="text" name="state"><br>
|
|
<button type="submit">Envoyer</button>
|
|
</form>
|
|
</body>
|
|
</html>
|
|
`
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Write([]byte(html))
|
|
}
|
|
|
|
// Handler pour afficher les cookies et le client
|
|
func displayInfoHandler(w http.ResponseWriter, r *http.Request) {
|
|
// Get the cookies
|
|
cookies := r.Cookies()
|
|
|
|
// Get the user IP
|
|
userIP, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
log.Printf("Failed to get user IP: %v", err)
|
|
}
|
|
|
|
// Get the headers
|
|
headers := map[string]string{}
|
|
for name, values := range r.Header {
|
|
headers[name] = strings.Join(values, ", ")
|
|
}
|
|
|
|
// Display the information in a simple HTML page
|
|
tmpl := `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Info</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
background-color: #f4f4f4;
|
|
padding: 20px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Cookies:</h1>
|
|
<ul>
|
|
{{range .Cookies}}
|
|
<li>{{.}}</li>
|
|
{{end}}
|
|
</ul>
|
|
|
|
<h1>User IP:</h1>
|
|
<p>{{.UserIP}}</p>
|
|
|
|
<h1>Headers:</h1>
|
|
<pre>
|
|
{{range $key, $value := .Headers}}
|
|
{{$key}}: {{$value}}
|
|
{{end}}
|
|
</pre>
|
|
</body>
|
|
</html>
|
|
`
|
|
|
|
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)
|
|
}
|
|
}
|