From c5aebc03f46c6face3cda17931f438c70c7889bf Mon Sep 17 00:00:00 2001 From: Jesper Handskemager Date: Fri, 16 Sep 2022 15:43:30 +0200 Subject: [PATCH] Initialize project --- config.yaml | 7 ++ go.mod | 13 ++++ go.sum | 16 ++++ index.html | 19 +++++ main.go | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++ steam.go | 136 +++++++++++++++++++++++++++++++++ 6 files changed, 403 insertions(+) create mode 100644 config.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 index.html create mode 100644 main.go create mode 100644 steam.go diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..e0ccc01 --- /dev/null +++ b/config.yaml @@ -0,0 +1,7 @@ +config: + MYSQL_DB: "cshub" + MYSQL_USER: "root" + MYSQL_PASS: "" + MYSQL_HOST: "127.0.0.1:3306" + DOMAIN: "steam.csgohub.xyz" + PORT: ":8383" \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1b9b794 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/jesperbakhandskemager/csgo-hub-backend + +go 1.19 + +require ( + github.com/emily33901/go-csfriendcode v0.0.0-20200914202423-31b418e4b897 + github.com/go-sql-driver/mysql v1.6.0 + github.com/gorilla/mux v1.8.0 + github.com/yohcop/openid-go v1.0.0 + gopkg.in/yaml.v2 v2.4.0 +) + +require golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..eb00d23 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/emily33901/go-csfriendcode v0.0.0-20200914202423-31b418e4b897 h1:/2VV8RykOeIbKbFRaA37wvz3LylTjlwmqRPN1uL90Z4= +github.com/emily33901/go-csfriendcode v0.0.0-20200914202423-31b418e4b897/go.mod h1:Fc5k4zUsP3edBqsb8AZdUE/S1ON4D0sVHI4Rcu2pA7Y= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/yohcop/openid-go v1.0.0 h1:EciJ7ZLETHR3wOtxBvKXx9RV6eyHZpCaSZ1inbBaUXE= +github.com/yohcop/openid-go v1.0.0/go.mod h1:/408xiwkeItSPJZSTPF7+VtZxPkPrRRpRNK2vjGh6yI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/index.html b/index.html new file mode 100644 index 0000000..b22df5b --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + Steam OpenID + + +
+ {{if .user}} +

{{.user}} Linked to your Discord account

+ {{else}} +

Authenticate Steam

+ + Sign in through Steam + + {{end}} + {{if .error}} +

An error occured

+ {{end}} +
+ + diff --git a/main.go b/main.go new file mode 100644 index 0000000..9c729d6 --- /dev/null +++ b/main.go @@ -0,0 +1,212 @@ +package main + +import ( + "crypto/rand" + "database/sql" + "encoding/json" + "fmt" + "io" + "log" + "math/big" + "net/http" + "os" + + _ "github.com/go-sql-driver/mysql" + "github.com/gorilla/mux" + "gopkg.in/yaml.v2" +) + +var domain string +var port string + +type YAMLFile struct { + Config Config `yaml:"config"` +} + +type Config struct { + MYSQL_DB string `yaml:"MYSQL_DB"` + MYSQL_USER string `yaml:"MYSQL_USER"` + MYSQL_PASS string `yaml:"MYSQL_PASS"` + MYSQL_HOST string `yaml:"MYSQL_HOST"` + DOMAIN string `yaml:"DOMAIN"` + PORT string `yaml:"PORT"` +} + +func ReadConfig() (*Config, error) { + config := &YAMLFile{} + cfgFile, err := os.ReadFile("./config.yaml") + if err != nil { + return nil, err + } + err = yaml.Unmarshal(cfgFile, config) + return &config.Config, err +} + +type user struct { + Id int `json:"id"` + CreatedAt string `json:"created_at"` + DiscordId string `json:"discord_id"` + FriendCode string `json:"friend_code"` +} + +func ReturnSingleUser(w http.ResponseWriter, r *http.Request) { + var u user + vars := mux.Vars(r) + id := vars["id"] + + query := `SELECT id, created_at, discord_id, friend_code FROM users where discord_id = ?` + err := db.QueryRow(query, id).Scan(&u.Id, &u.CreatedAt, &u.DiscordId, &u.FriendCode) + if err != nil { + log.Print(err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Bad Request") + return + } + + json.NewEncoder(w).Encode(u) +} + +func GetMultipleUsers(w http.ResponseWriter, r *http.Request) { + // Read body + b, err := io.ReadAll(r.Body) + defer r.Body.Close() + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + // Unmarshal + var getUsers []user + err = json.Unmarshal(b, &getUsers) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), 500) + return + } + + var returnUsers []user + + for _, us := range getUsers { + var u user + query := `SELECT id, created_at, discord_id, friend_code FROM users where discord_id = ?` + err := db.QueryRow(query, us.DiscordId).Scan(&u.Id, &u.CreatedAt, &u.DiscordId, &u.FriendCode) + if err != nil { + log.Print(err) + } + returnUsers = append(returnUsers, u) + } + + json.NewEncoder(w).Encode(returnUsers) +} + +func CreateUser(w http.ResponseWriter, r *http.Request) { + if r.URL.Host != "localhost:8383" { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "Unauthorized") + return + } + // Read body + b, err := io.ReadAll(r.Body) + defer r.Body.Close() + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + // Unmarshal + var msg user + err = json.Unmarshal(b, &msg) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), 500) + return + } + + _, err = db.Exec(`INSERT INTO users(discord_id, friend_code) VALUES (?, ?)`, msg.DiscordId, msg.FriendCode) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Bad request") + } + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, "OK") +} + +func CreateToken(w http.ResponseWriter, r *http.Request) { + if r.Host != "localhost:8383" { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, "Unauthorized") + return + } + token, err := GenerateRandomString(8) + if err != nil { + return + } + + vars := mux.Vars(r) + discord := vars["discord"] + if len(discord) != 18 { + fmt.Fprintf(w, "Bad request") + return + } + + _, err = db.Exec(`INSERT INTO tokens(discord_id, token) VALUES (?, ?)`, discord, token) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Bad request") + return + } + + json.NewEncoder(w).Encode(token) +} + +func GenerateRandomString(n int) (string, error) { + const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-" + ret := make([]byte, n) + for i := 0; i < n; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) + if err != nil { + return "", err + } + ret[i] = letters[num.Int64()] + } + + return string(ret), nil +} + +var db *sql.DB + +func main() { + var err error + config, err := ReadConfig() + if err != nil { + log.Fatal(err) + } + domain = config.DOMAIN + port = config.PORT + + db, err = sql.Open("mysql", config.MYSQL_USER+":"+config.MYSQL_PASS+"@("+config.MYSQL_HOST+")/"+config.MYSQL_DB+"?parseTime=true") + if err != nil { + log.Fatal(err) + } + err = db.Ping() + if err != nil { + log.Fatal(err) + } + defer db.Close() + log.Println("Database connection established") + + router := mux.NewRouter() + + router.HandleFunc("/", indexHandler) + router.HandleFunc("/discover", discoverHandler) + router.HandleFunc("/openidcallback", callbackHandler) + router.HandleFunc("/{token}", indexHandler) + // router.HandleFunc("/api/v1/user/{id}", ReturnSingleUser).Methods("GET") + router.HandleFunc("/api/v1/users", GetMultipleUsers) + //router.HandleFunc("/api/v1/user", CreateUser).Methods("POST") + router.HandleFunc("/api/v1/token/{discord}", CreateToken) + http.ListenAndServe(port, router) +} diff --git a/steam.go b/steam.go new file mode 100644 index 0000000..8dd2c7d --- /dev/null +++ b/steam.go @@ -0,0 +1,136 @@ +package main + +import ( + "fmt" + "html/template" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/emily33901/go-csfriendcode" + "github.com/gorilla/mux" + "github.com/yohcop/openid-go" +) + +// Load the templates once +var templateDir = "./" +var indexTemplate = template.Must(template.ParseFiles(templateDir + "index.html")) + +// NoOpDiscoveryCache implements the DiscoveryCache interface and doesn't cache anything. +// For a simple website, I'm not sure you need a cache. +type NoOpDiscoveryCache struct{} + +// Put is a no op. +func (n *NoOpDiscoveryCache) Put(id string, info openid.DiscoveredInfo) {} + +// Get always returns nil. +func (n *NoOpDiscoveryCache) Get(id string) openid.DiscoveredInfo { + return nil +} + +var nonceStore = openid.NewSimpleNonceStore() +var discoveryCache = &NoOpDiscoveryCache{} + +// indexHandler serves up the index template with the "Sign in through STEAM" button. +func indexHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + token := vars["token"] + query := `SELECT token FROM tokens where token = BINARY ?` + err := db.QueryRow(query, token).Scan(&token) + if err != nil { + log.Print(err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Bad request") + return + } + log.Println(token) + expiration := time.Now().Add(time.Hour) + cookie := http.Cookie{Name: "token", Value: token, Expires: expiration} + http.SetCookie(w, &cookie) + indexTemplate.Execute(w, nil) +} + +// discoverHandler calls the Steam openid API and redirects to steam for login. +func discoverHandler(w http.ResponseWriter, r *http.Request) { + url, err := openid.RedirectURL( + "http://steamcommunity.com/openid", + "http://"+domain+"/openidcallback", + "http://"+domain+"/") + + if err != nil { + log.Printf("Error creating redirect URL: %q\n", err) + } else { + http.Redirect(w, r, url, http.StatusSeeOther) + } +} + +func ClearToken(token string) error { + _, err := db.Exec(`DELETE FROM tokens where token = ?`, token) + return err +} + +// callbackHandler handles the response back from Steam. It verifies the callback and then renders +// the index template with the logged in user's id. +func callbackHandler(w http.ResponseWriter, r *http.Request) { + fullURL := "http://" + domain + r.URL.String() + + id, err := openid.Verify(fullURL, discoveryCache, nonceStore) + if err != nil { + log.Printf("Error verifying: %q\n", err) + } else { + log.Printf("NonceStore: %+v\n", nonceStore) + data := make(map[string]string) + println(id) + steamId, err := strconv.Atoi(strings.Split(id, "/id/")[1]) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Bad request") + return + } + + friendCode := csfriendcode.Encode(uint64(steamId)) + cookie, _ := r.Cookie("token") + token := cookie.Value + var discordId string + + query := `SELECT discord_id FROM tokens where token = ?` + err = db.QueryRow(query, token).Scan(&discordId) + if err != nil { + log.Print(err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Bad request") + return + } + ClearToken(token) + + var checkId, checkFriend string + query = `SELECT discord_id, friend_code FROM users where discord_id = ?` + err = db.QueryRow(query, discordId).Scan(&checkId, &checkFriend) + if err == nil { + _, err = db.Exec(`UPDATE users SET friend_code = ? WHERE discord_id = ?`, friendCode, discordId) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Bad request") + } + log.Print(err) + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, "Account link updated") + return + } + + _, err = db.Exec(`INSERT INTO users(discord_id, friend_code) VALUES (?, ?)`, discordId, friendCode) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Bad request") + } + + w.WriteHeader(http.StatusCreated) + data["user"] = id + indexTemplate.Execute(w, data) + } +}