Skip to content
Snippets Groups Projects
Commit b2e977f1 authored by Ophestra's avatar Ophestra
Browse files

web server and oauth routes

parents
No related branches found
No related tags found
No related merge requests found
Pipeline #908 canceled
tournament-website
*.conf
.idea/
package main
import (
"context"
"log"
"time"
)
func cleanup() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("error shutting down web server: %s", err)
}
}
package main
import (
"flag"
"github.com/BurntSushi/toml"
"log"
"os"
)
type conf struct {
System systemConf `toml:"system"`
Server serverConf `toml:"server"`
Discord discordConf `toml:"discord"`
}
type serverConf struct {
Host string `toml:"host"`
Unix bool `toml:"unix"`
Port uint16 `toml:"port"`
Proxy bool `toml:"proxy"`
TrustedProxies []string `toml:"trusted_proxies"`
BaseURL string `toml:"base_url"`
Secret string `toml:"secret"`
}
type systemConf struct {
Verbose bool `toml:"verbose"`
Store string `toml:"store"`
}
type discordConf struct {
ClientID string `toml:"client_id"`
ClientSecret string `toml:"client_secret"`
}
var (
config conf
confPath string
confRead = false
)
func init() {
flag.StringVar(&confPath, "c", "server.conf", "specify path to configuration file")
}
func confLoad() {
if confRead {
panic("configuration read called when already read")
}
defer func() { confRead = true }()
if meta, err := toml.DecodeFile(confPath, &config); err != nil {
if !os.IsNotExist(err) {
log.Fatalf("error parsing configuration: %s", err)
}
var file *os.File
if file, err = os.Create(confPath); err != nil {
log.Fatalf("error creating configuration file: %s", err)
} else if err = toml.NewEncoder(file).Encode(defConf); err != nil {
log.Fatalf("error generating default configuration: %s", err)
}
config = defConf
return
} else {
for _, key := range meta.Undecoded() {
log.Printf("unused key in configuration file: %s", key.String())
}
}
}
var defConf = conf{
System: systemConf{
Verbose: false,
Store: "db",
},
Server: serverConf{
Host: "127.0.0.1",
Unix: false,
Port: 7777,
Proxy: true,
TrustedProxies: []string{
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
},
BaseURL: "http://localhost:7777/",
Secret: "RANDOM_STRING",
},
Discord: discordConf{
ClientID: "",
ClientSecret: "",
},
}
go.mod 0 → 100644
module git.randomchars.net/levatax/tournament-website
go 1.17
require (
github.com/BurntSushi/toml v0.4.1
github.com/gin-gonic/gin v1.7.4
)
require (
github.com/bwmarrin/discordgo v0.23.2 // indirect
github.com/gin-contrib/sessions v0.0.4 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/golang/protobuf v1.4.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.0 // indirect
github.com/gorilla/websocket v1.4.0 // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 // indirect
google.golang.org/appengine v1.6.6 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
)
go.sum 0 → 100644
This diff is collapsed.
main.go 0 → 100644
package main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
)
func main() {
flag.Parse()
confLoad()
webSetup()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
go func() {
defer func() { cleanup() }()
for {
s := <-sig
switch s {
case os.Interrupt:
println()
log.Print("shutting down")
return
default:
log.Print("shutting down")
return
}
}
}()
serve()
}
package oauth
import (
"fmt"
"github.com/bwmarrin/discordgo"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
json "github.com/json-iterator/go"
"golang.org/x/oauth2"
"io/ioutil"
"net/http"
)
// Discord OAuth2 scope.
const (
ScopeIdentify = "identify"
ScopeBot = "bot"
ScopeEmail = "email"
ScopeGuilds = "guilds"
ScopeGuildsJoin = "guilds.join"
ScopeConnections = "connections"
ScopeGroupDMJoin = "gdm.join"
ScopeMessagesRead = "messages.read"
ScopeRPC = "rpc"
ScopeRPCAPI = "rpc.api"
ScopeRPCNotificationsRead = "rpc.notifications.read"
ScopeWebhookIncoming = "webhook.Incoming"
)
// Conf stores configuration of OAuth with Discord.
var Conf *oauth2.Config
var endpoint = oauth2.Endpoint{
AuthURL: discordgo.EndpointOauth2 + "authorize",
TokenURL: discordgo.EndpointOauth2 + "token",
}
// Endpoint returns discord OAuth2 endpoints.
func Endpoint() oauth2.Endpoint {
return endpoint
}
// GetToken gets a token stored in session in a context.
func GetToken(context *gin.Context) *oauth2.Token {
session := sessions.Default(context)
tokenJSON := session.Get("token")
var token = &oauth2.Token{}
err := json.UnmarshalFromString(fmt.Sprint(tokenJSON), token)
if err != nil {
RemoveToken(context)
return nil
}
return token
}
// StoreToken stores a token in session in a context.
func StoreToken(context *gin.Context, token *oauth2.Token) {
session := sessions.Default(context)
tokenJSON, err := json.Marshal(token)
if err != nil {
panic(err)
}
session.Set("token", string(tokenJSON))
_ = session.Save()
}
// RemoveToken removes a token in session in a context.
func RemoveToken(context *gin.Context) {
session := sessions.Default(context)
session.Delete("token")
_ = session.Save()
}
// Client returns pointer to an http.Client.
func Client(context *gin.Context) *http.Client {
return Conf.Client(context, GetToken(context))
}
// GetSelf returns UserInfo of authenticated user.
func GetSelf(context *gin.Context) *discordgo.User {
token := GetToken(context)
if token == nil {
return nil
}
client := Client(context)
response, err := client.Get(discordgo.EndpointUser("@me"))
if err != nil {
RemoveToken(context)
return nil
}
defer func() { _ = response.Body.Close() }()
if response.StatusCode == http.StatusUnauthorized {
RemoveToken(context)
return nil
}
var user *discordgo.User
data, err := ioutil.ReadAll(response.Body)
if err != nil {
panic(err)
}
err = json.Unmarshal(data, &user)
if err != nil {
panic(err)
}
return user
}
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
"net/http"
"runtime/debug"
)
func recovery() gin.HandlerFunc {
return func(context *gin.Context) {
defer func() {
p := recover()
if p != nil {
log.Printf("panic in web server %s", p)
context.String(http.StatusInternalServerError, "Internal Server Error")
fmt.Println(string(debug.Stack()))
}
}()
context.Next()
}
}
package main
import (
"git.randomchars.net/levatax/tournament-website/oauth"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"net/http"
)
const formPath = "/kayit"
func registerRoutes() {
router.GET("/", func(context *gin.Context) {
// TODO
context.HTML(http.StatusOK, "index.tmpl", gin.H{})
})
router.GET(formPath, func(context *gin.Context) {
// TODO
context.HTML(http.StatusOK, "form.tmpl", gin.H{})
})
// OAuth
router.GET("/auth/logout", func(context *gin.Context) {
oauth.RemoveToken(context)
context.Redirect(http.StatusTemporaryRedirect, "/")
})
router.GET("/auth/login", func(context *gin.Context) {
session := sessions.Default(context)
oauthState := uuid.New().String()
session.Set("state", oauthState)
_ = session.Save()
context.Redirect(http.StatusTemporaryRedirect, oauth.Conf.AuthCodeURL(oauthState))
})
router.GET("/auth/callback", func(context *gin.Context) {
session := sessions.Default(context)
if context.Request.FormValue("state") != session.Get("state") {
context.JSON(http.StatusBadRequest, gin.H{
"error": "state does not match",
})
return
}
session.Delete("state")
token, err := oauth.Conf.Exchange(context, context.Request.FormValue("code"))
if err != nil {
panic(err)
}
oauth.StoreToken(context, token)
_ = session.Save()
context.Redirect(http.StatusTemporaryRedirect, formPath)
})
}
web.go 0 → 100644
package main
import (
"embed"
"git.randomchars.net/levatax/tournament-website/oauth"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
"html/template"
"io/fs"
"log"
"net"
"net/http"
"os"
"strconv"
)
//go:embed assets
var assets embed.FS
var (
router *gin.Engine
listener net.Listener
server = http.Server{}
)
func init() {
gin.SetMode(gin.ReleaseMode)
}
func webSetup() {
if d, ok := os.LookupEnv("GIN_DEBUG"); ok {
if b, err := strconv.ParseBool(d); err == nil {
if b {
gin.SetMode(gin.DebugMode)
}
}
}
router = gin.New()
router.ForwardedByClientIP = config.Server.Proxy
router.TrustedProxies = config.Server.TrustedProxies
if config.System.Verbose {
router.Use(gin.Logger())
router.Use(gin.Recovery())
} else {
router.Use(recovery())
}
router.NoRoute(func(context *gin.Context) {
context.Redirect(http.StatusTemporaryRedirect, "/")
})
if config.Server.Unix {
if l, err := net.Listen("unix", config.Server.Host); err != nil {
log.Fatalf("error listening on socket: %s", err)
} else {
listener = l
}
if err := os.Chmod(config.Server.Host, 0777); err != nil {
log.Printf("error chmod: %s", err)
}
log.Printf("web server listening on socket %s", config.Server.Host)
} else {
if l, err := net.Listen("tcp", config.Server.Host+":"+strconv.Itoa(int(config.Server.Port))); err != nil {
log.Fatalf("error listening on TCP port: %s", err)
} else {
listener = l
}
log.Printf("web server listening on %s:%d", config.Server.Host, config.Server.Port)
}
server.Handler = router
if stat, err := os.Stat("assets/static"); err == nil && stat.IsDir() {
log.Print("serving static assets from filesystem")
router.Static("/static", "assets/static")
} else {
log.Print("serving bundled static assets")
var public fs.FS
public, err = fs.Sub(assets, "assets/static")
if err != nil {
log.Fatalf("error subdirectory: %s", err)
}
router.StaticFS("/static", http.FS(public))
}
if templates, err := template.ParseFS(assets, "assets/templates/*.tmpl"); err != nil {
log.Fatalf("error parsing templates: %s", err)
} else {
router.SetHTMLTemplate(templates)
}
// Cookie store and setup cookie middleware
router.Use(sessions.Sessions("tournament", cookie.NewStore([]byte(config.Server.Secret))))
// Set oauth client ID
oauth.Conf = &oauth2.Config{
ClientID: config.Discord.ClientID,
ClientSecret: config.Discord.ClientSecret,
Endpoint: oauth.Endpoint(),
RedirectURL: config.Server.BaseURL + "auth/callback",
Scopes: []string{oauth.ScopeIdentify, oauth.ScopeGuilds},
}
registerRoutes()
}
func serve() {
if err := server.Serve(listener); err == http.ErrServerClosed {
log.Printf("web server closed")
} else {
log.Printf("error serve: %s", err)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment