package main

import (
	"crypto/rand"
	"crypto/subtle"
	"encoding/hex"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
)

// tokoSesi menyimpan token CSRF per sesi (pola synchronizer token).
// Mutex wajib karena map diakses goroutine handler secara bersamaan.
type tokoSesi struct {
	mu    sync.Mutex
	token map[string]string
}

// acak menghasilkan n byte acak kriptografis dalam bentuk hex.
func acak(n int) (string, error) {
	b := make([]byte, n)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}

func (t *tokoSesi) buat() (sid, token, dsToken string, err error) {
	sid, err = acak(32)
	if err != nil {
		return
	}
	token, err = acak(16)
	if err != nil {
		return
	}
	dsToken, err = acak(16)
	if err != nil {
		return
	}
	t.mu.Lock()
	t.token[sid] = token
	t.mu.Unlock()
	return
}

func (t *tokoSesi) tokenUntuk(sid string) (string, bool) {
	t.mu.Lock()
	defer t.mu.Unlock()
	tok, ok := t.token[sid]
	return tok, ok
}

// sama membandingkan dua token dengan waktu konstan.
func sama(a, b string) bool {
	return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}

func namaSameSite(s http.SameSite) string {
	switch s {
	case http.SameSiteLaxMode:
		return "Lax"
	case http.SameSiteStrictMode:
		return "Strict"
	case http.SameSiteNoneMode:
		return "None"
	default:
		return "default"
	}
}

func (t *tokoSesi) login(w http.ResponseWriter, r *http.Request) {
	if r.PostFormValue("username") != "budi" || r.PostFormValue("password") != "sandiRahasia123" {
		http.Error(w, "kredensial salah", http.StatusUnauthorized)
		return
	}
	sid, _, dsToken, err := t.buat()
	if err != nil {
		http.Error(w, "gagal membuat sesi", http.StatusInternalServerError)
		return
	}
	http.SetCookie(w, &http.Cookie{
		Name:     "session_id",
		Value:    sid,
		Path:     "/",
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
	})
	// Cookie double-submit sengaja TIDAK HttpOnly agar JavaScript
	// dapat membacanya lalu menyalin nilainya ke header X-CSRF-Token.
	http.SetCookie(w, &http.Cookie{Name: "csrf_ds", Value: dsToken, Path: "/"})
	http.Redirect(w, r, "/transfer", http.StatusSeeOther)
}

func (t *tokoSesi) halamanTransfer(w http.ResponseWriter, r *http.Request) {
	c, err := r.Cookie("session_id")
	if err != nil {
		http.Error(w, "belum login", http.StatusUnauthorized)
		return
	}
	token, ok := t.tokenUntuk(c.Value)
	if !ok {
		http.Error(w, "sesi tidak valid", http.StatusUnauthorized)
		return
	}
	// Sebagai pengganti halaman HTML, body berisi token tersemat.
	fmt.Fprint(w, token)
}

// transfer adalah endpoint sensitif: hanya menerima POST dengan
// token CSRF yang cocok dengan token tersimpan di sesi server.
func (t *tokoSesi) transfer(w http.ResponseWriter, r *http.Request) {
	c, err := r.Cookie("session_id")
	if err != nil {
		http.Error(w, "belum login", http.StatusUnauthorized)
		return
	}
	token, ok := t.tokenUntuk(c.Value)
	if !ok {
		http.Error(w, "sesi tidak valid", http.StatusUnauthorized)
		return
	}
	dariHeader := r.Header.Get("X-CSRF-Token")
	if dariHeader == "" || !sama(dariHeader, token) {
		http.Error(w, "token CSRF tidak valid", http.StatusForbidden)
		return
	}
	fmt.Fprint(w, "transfer Rp50000 ke Siti berhasil")
}

// donasi memakai pola double-submit cookie: cookie csrf_ds
// dibandingkan dengan header X-CSRF-Token tanpa penyimpanan server.
func donasi(w http.ResponseWriter, r *http.Request) {
	c, err := r.Cookie("csrf_ds")
	if err != nil {
		http.Error(w, "cookie csrf_ds tidak ada", http.StatusForbidden)
		return
	}
	dariHeader := r.Header.Get("X-CSRF-Token")
	if dariHeader == "" || !sama(dariHeader, c.Value) {
		http.Error(w, "token CSRF tidak cocok dengan cookie", http.StatusForbidden)
		return
	}
	fmt.Fprint(w, "donasi diterima")
}

// kirim menjalankan satu permintaan dan mengembalikan status, isi, serta cookie respons.
func kirim(client *http.Client, method, target, form, tokenHeader string, cookies ...*http.Cookie) (int, string, []*http.Cookie) {
	var b io.Reader
	if form != "" {
		b = strings.NewReader(form)
	}
	req, err := http.NewRequest(method, target, b)
	if err != nil {
		log.Fatal(err)
	}
	if form != "" {
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}
	if tokenHeader != "" {
		req.Header.Set("X-CSRF-Token", tokenHeader)
	}
	for _, c := range cookies {
		req.AddCookie(c)
	}
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	isi, _ := io.ReadAll(resp.Body)
	return resp.StatusCode, strings.TrimSpace(string(isi)), resp.Cookies()
}

func main() {
	toko := &tokoSesi{token: map[string]string{}}
	mux := http.NewServeMux()
	mux.HandleFunc("POST /login", toko.login)
	mux.HandleFunc("GET /transfer", toko.halamanTransfer)
	mux.HandleFunc("POST /transfer", toko.transfer)
	mux.HandleFunc("POST /donasi", donasi)
	server := httptest.NewServer(mux)
	defer server.Close()
	// Klien tanpa redirect otomatis agar status 303 terlihat.
	client := &http.Client{
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}

	kode, _, cookies := kirim(client, "POST", server.URL+"/login", "username=budi&password=sandiRahasia123", "")
	var cookieSesi, cookieDS *http.Cookie
	for _, c := range cookies {
		switch c.Name {
		case "session_id":
			cookieSesi = c
		case "csrf_ds":
			cookieDS = c
		}
	}
	fmt.Printf("POST /login                              : %d (sesi dibuat)\n", kode)

	// Halaman form: token CSRF tersemat sebagai hidden input (di sini: body).
	_, tokenDariForm, _ := kirim(client, "GET", server.URL+"/transfer", "", "", cookieSesi)
	fmt.Println("GET  /transfer mengambil halaman form    : 200")

	kode, isi, _ := kirim(client, "POST", server.URL+"/transfer", "jumlah=50000&tujuan=Siti", "", cookieSesi)
	fmt.Printf("POST /transfer tanpa token CSRF          : %d (%s)\n", kode, isi)

	kode, isi, _ = kirim(client, "POST", server.URL+"/transfer", "jumlah=50000&tujuan=Siti", tokenDariForm, cookieSesi)
	fmt.Printf("POST /transfer dengan token CSRF benar   : %d (%s)\n", kode, isi)

	kode, isi, _ = kirim(client, "POST", server.URL+"/transfer", "jumlah=50000&tujuan=Siti", "palsu"+tokenDariForm, cookieSesi)
	fmt.Printf("POST /transfer dengan token CSRF palsu   : %d (%s)\n", kode, isi)

	kode, isi, _ = kirim(client, "POST", server.URL+"/donasi", "jumlah=100000", cookieDS.Value, cookieDS)
	fmt.Printf("POST /donasi double-submit cocok         : %d (%s)\n", kode, isi)

	kode, isi, _ = kirim(client, "POST", server.URL+"/donasi", "jumlah=100000", "nilai-berbeda", cookieDS)
	fmt.Printf("POST /donasi header beda dari cookie     : %d (%s)\n", kode, isi)

	fmt.Printf("cookie session_id: SameSite=%s, HttpOnly=%v\n", namaSameSite(cookieSesi.SameSite), cookieSesi.HttpOnly)
	fmt.Printf("cookie csrf_ds   : HttpOnly=%v (sengaja terbaca JavaScript)\n", cookieDS.HttpOnly)
}
