mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-24 19:39:11 +00:00
add basic oauth bounces
This commit is contained in:
parent
bebfc862e8
commit
a23184efd2
13 changed files with 262 additions and 1 deletions
2
go.mod
2
go.mod
|
@ -9,6 +9,8 @@ require (
|
||||||
github.com/gorilla/websocket v1.4.2 // indirect
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
github.com/joho/godotenv v1.3.0
|
github.com/joho/godotenv v1.3.0
|
||||||
github.com/lampjaw/discordclient v0.0.0-20200923011548-6558fc9e89df
|
github.com/lampjaw/discordclient v0.0.0-20200923011548-6558fc9e89df
|
||||||
|
github.com/onsi/gomega v1.8.1
|
||||||
|
github.com/segmentio/ksuid v1.0.3
|
||||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 // indirect
|
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 // indirect
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
|
||||||
k8s.io/klog v1.0.0
|
k8s.io/klog v1.0.0
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -150,6 +150,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/segmentio/ksuid v1.0.3 h1:FoResxvleQwYiPAVKe1tMUlEirodZqlqglIuFsdDntY=
|
||||||
|
github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||||
|
|
3
hack/functions-local/README.md
Normal file
3
hack/functions-local/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# FaaS local dev shim
|
||||||
|
|
||||||
|
This tool provides a thin layer to emulate Google Cloud Functions for Roleypoly backend components, as well as a weak UI to understand the app layout. This is not used in production.
|
|
@ -8,6 +8,9 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/functions-framework-go/funcframework"
|
"github.com/GoogleCloudPlatform/functions-framework-go/funcframework"
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
|
|
||||||
|
botjoin "github.com/roleypoly/roleypoly/src/functions/bot-join"
|
||||||
sessiondata "github.com/roleypoly/roleypoly/src/functions/session-data"
|
sessiondata "github.com/roleypoly/roleypoly/src/functions/session-data"
|
||||||
sessionprewarm "github.com/roleypoly/roleypoly/src/functions/session-prewarm"
|
sessionprewarm "github.com/roleypoly/roleypoly/src/functions/session-prewarm"
|
||||||
)
|
)
|
||||||
|
@ -15,6 +18,7 @@ import (
|
||||||
var mappings map[string]http.HandlerFunc = map[string]http.HandlerFunc{
|
var mappings map[string]http.HandlerFunc = map[string]http.HandlerFunc{
|
||||||
"/session-prewarm": sessionprewarm.SessionPrewarm,
|
"/session-prewarm": sessionprewarm.SessionPrewarm,
|
||||||
"/session-data": sessiondata.SessionData,
|
"/session-data": sessiondata.SessionData,
|
||||||
|
"/bot-join": botjoin.BotJoin,
|
||||||
}
|
}
|
||||||
|
|
||||||
var port string
|
var port string
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
// GetenvValue is a holder type for Getenv to translate any Getenv strings to real types
|
// GetenvValue is a holder type for Getenv to translate any Getenv strings to real types
|
||||||
type GetenvValue struct {
|
type GetenvValue struct {
|
||||||
|
key string
|
||||||
value string
|
value string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@ func Getenv(key string, defaultValue ...string) GetenvValue {
|
||||||
|
|
||||||
return GetenvValue{
|
return GetenvValue{
|
||||||
value: strings.TrimSpace(value),
|
value: strings.TrimSpace(value),
|
||||||
|
key: key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,3 +65,11 @@ func (g GetenvValue) Number() int {
|
||||||
func (g GetenvValue) JSON(target interface{}) error {
|
func (g GetenvValue) JSON(target interface{}) error {
|
||||||
return json.Unmarshal([]byte(g.value), target)
|
return json.Unmarshal([]byte(g.value), target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g GetenvValue) OrFatal() GetenvValue {
|
||||||
|
if g.value == "" {
|
||||||
|
panic("Getenv value was empty and shouldn't be. key: " + g.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/onsi/gomega"
|
||||||
"github.com/roleypoly/roleypoly/src/common"
|
"github.com/roleypoly/roleypoly/src/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -121,3 +122,17 @@ func TestEnvconfigJSON(t *testing.T) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnvconfigFatal(t *testing.T) {
|
||||||
|
O := gomega.NewWithT(t)
|
||||||
|
O.Expect(func() {
|
||||||
|
_ = common.Getenv("test__thing_that_doesnt_exist").OrFatal().String()
|
||||||
|
}).Should(gomega.Panic(), "Getenv without a value should panic")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvconfigNotFatal(t *testing.T) {
|
||||||
|
O := gomega.NewWithT(t)
|
||||||
|
O.Expect(func() {
|
||||||
|
_ = common.Getenv("test__string").OrFatal().String()
|
||||||
|
}).ShouldNot(gomega.Panic(), "Getenv with a value should not panic")
|
||||||
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ func assertAuthLevel(err error, requiredAuthLevel AuthLevel, assertedAuthLevel A
|
||||||
|
|
||||||
// AuthMustMatch will assert the current session's authorization group/level; only can match for Guest, User, and Super.
|
// AuthMustMatch will assert the current session's authorization group/level; only can match for Guest, User, and Super.
|
||||||
func AuthMustMatch(request *http.Request, authLevel AuthLevel) error {
|
func AuthMustMatch(request *http.Request, authLevel AuthLevel) error {
|
||||||
authCookie, err := request.Cookie("Authorization")
|
_, err := request.Cookie("Authorization")
|
||||||
if errors.Is(err, http.ErrNoCookie) {
|
if errors.Is(err, http.ErrNoCookie) {
|
||||||
// No cookie is present, assert guest.
|
// No cookie is present, assert guest.
|
||||||
return assertAuthLevel(ErrNotAuthorized, authLevel, AuthGuest)
|
return assertAuthLevel(ErrNotAuthorized, authLevel, AuthGuest)
|
||||||
|
@ -36,4 +36,5 @@ func AuthMustMatch(request *http.Request, authLevel AuthLevel) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
66
src/common/faas/bounce.go
Normal file
66
src/common/faas/bounce.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package faas
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var bounceHTML = template.Must(template.New("bounceHTML").Parse(
|
||||||
|
`<!doctype html>
|
||||||
|
<meta charset="utf8">
|
||||||
|
<title>Redirecting...</title>
|
||||||
|
<meta http-equiv="refresh" content="0;URL='{{.Location}}'">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #453E3D;
|
||||||
|
color: #AB9B9A;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #AB9B9A;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<p>
|
||||||
|
Redirecting you to <a href="{{.Location}}">{{.Location}}</a>
|
||||||
|
</p>
|
||||||
|
`,
|
||||||
|
))
|
||||||
|
|
||||||
|
type bounceData struct {
|
||||||
|
Location string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounce will do a 303 See Other response with url.
|
||||||
|
func Bounce(rw http.ResponseWriter, url string) {
|
||||||
|
rw.Header().Add("location", url)
|
||||||
|
rw.WriteHeader(303)
|
||||||
|
bounceHTML.Execute(rw, bounceData{Location: url})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stash will save the specified URL for later use in Unstash(), e.g. after an OAuth bounce
|
||||||
|
func Stash(rw http.ResponseWriter, url string) {
|
||||||
|
if url == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie := http.Cookie{
|
||||||
|
Name: "rp_stashed_url",
|
||||||
|
Value: url,
|
||||||
|
HttpOnly: true,
|
||||||
|
Expires: time.Now().Add(5 * time.Minute),
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.Header().Add("set-cookie", cookie.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unstash will redirect/Bounce() to a previously stashed URL or the defaultURL, whichever is available.
|
||||||
|
func Unstash(rw http.ResponseWriter, req *http.Request, defaultURL string) {
|
||||||
|
redirectURL := defaultURL
|
||||||
|
cookie, _ := req.Cookie("rp_stashed_url")
|
||||||
|
|
||||||
|
if cookie != nil && cookie.Expires.After(time.Now()) && cookie.Value != "" {
|
||||||
|
redirectURL = cookie.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
Bounce(rw, redirectURL)
|
||||||
|
}
|
10
src/functions/bot-join/README.md
Normal file
10
src/functions/bot-join/README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Bot Join Bounce Service
|
||||||
|
|
||||||
|
This function sends a user to the necessary Discord.com Bot OAuth flow.
|
||||||
|
|
||||||
|
Two cases may be present:
|
||||||
|
|
||||||
|
- General: The user will be sent to allow any of their relevant servers to join
|
||||||
|
- This flow would be triggered from a generalized button
|
||||||
|
- Specific: The user will be sent to join one of their servers.
|
||||||
|
- This flow would be triggered from server picker
|
45
src/functions/bot-join/botjoin.go
Normal file
45
src/functions/bot-join/botjoin.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package botjoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/roleypoly/roleypoly/src/common"
|
||||||
|
"github.com/roleypoly/roleypoly/src/common/faas"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
validGuildID = regexp.MustCompile(`^[0-9]+$`)
|
||||||
|
redirectPathTemplate = template.Must(
|
||||||
|
template.New("redirect").Parse(
|
||||||
|
`https://discord.com/api/oauth2/authorize?client_id={{.ClientID}}&scope=bot&permissions={{.Permissions}}{{if .GuildID}}&guild_id={{.GuildID}}&disable_guild_select=true{{end}}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
clientID = common.Getenv("BOT_CLIENT_ID").String()
|
||||||
|
)
|
||||||
|
|
||||||
|
type redirectPathData struct {
|
||||||
|
ClientID string
|
||||||
|
Permissions int
|
||||||
|
GuildID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func BotJoin(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
guildID := r.URL.Query().Get("guild")
|
||||||
|
if !validGuildID.MatchString(guildID) {
|
||||||
|
guildID = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
pathData := redirectPathData{
|
||||||
|
ClientID: clientID,
|
||||||
|
Permissions: 268435456, // MANAGE_ROLES
|
||||||
|
GuildID: guildID,
|
||||||
|
}
|
||||||
|
|
||||||
|
pathBuffer := bytes.Buffer{}
|
||||||
|
redirectPathTemplate.Execute(&pathBuffer, pathData)
|
||||||
|
|
||||||
|
faas.Bounce(rw, pathBuffer.String())
|
||||||
|
}
|
36
src/functions/bot-join/botjoin_test.go
Normal file
36
src/functions/bot-join/botjoin_test.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package botjoin_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/onsi/gomega"
|
||||||
|
botjoin "github.com/roleypoly/roleypoly/src/functions/bot-join"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGeneral(t *testing.T) {
|
||||||
|
O := gomega.NewWithT(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/bot-join", nil)
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
|
||||||
|
botjoin.BotJoin(resp, req)
|
||||||
|
|
||||||
|
result := resp.Result()
|
||||||
|
O.Expect(result.StatusCode).Should(gomega.BeIdenticalTo(303))
|
||||||
|
O.Expect(result.Header.Get("location")).ShouldNot(gomega.ContainSubstring("guild_id"))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeneralSpecific(t *testing.T) {
|
||||||
|
O := gomega.NewWithT(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/bot-join?guild=386659935687147521", nil)
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
|
||||||
|
botjoin.BotJoin(resp, req)
|
||||||
|
|
||||||
|
result := resp.Result()
|
||||||
|
O.Expect(result.StatusCode).Should(gomega.BeIdenticalTo(303))
|
||||||
|
O.Expect(result.Header.Get("location")).Should(gomega.ContainSubstring("guild_id=386659935687147521"))
|
||||||
|
}
|
43
src/functions/login-bounce/loginbounce.go
Normal file
43
src/functions/login-bounce/loginbounce.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package loginbounce
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/http"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/segmentio/ksuid"
|
||||||
|
|
||||||
|
"github.com/roleypoly/roleypoly/src/common"
|
||||||
|
"github.com/roleypoly/roleypoly/src/common/faas"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
redirectPathTemplate = template.Must(
|
||||||
|
template.New("redirect").Parse(
|
||||||
|
`https://discord.com/api/oauth2/authorize?client_id={{.ClientID}}&scope=identify,guilds&redirect_uri={{.RedirectURI}}&state={{.State}}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
clientID = common.Getenv("BOT_CLIENT_ID").String()
|
||||||
|
redirectURI = common.Getenv("OAUTH_REDIRECT_URI").String()
|
||||||
|
)
|
||||||
|
|
||||||
|
type redirectPathData struct {
|
||||||
|
ClientID string
|
||||||
|
RedirectURI string
|
||||||
|
State string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoginBounce(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
faas.Stash(rw, r.URL.Query().Get("redirect_url"))
|
||||||
|
|
||||||
|
pathData := redirectPathData{
|
||||||
|
ClientID: clientID,
|
||||||
|
RedirectURI: redirectURI,
|
||||||
|
State: ksuid.New().String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
pathBuffer := bytes.Buffer{}
|
||||||
|
redirectPathTemplate.Execute(&pathBuffer, pathData)
|
||||||
|
|
||||||
|
faas.Bounce(rw, pathBuffer.String())
|
||||||
|
}
|
24
src/functions/login-bounce/loginbounce_test.go
Normal file
24
src/functions/login-bounce/loginbounce_test.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package loginbounce_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/onsi/gomega"
|
||||||
|
loginbounce "github.com/roleypoly/roleypoly/src/functions/login-bounce"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBounce(t *testing.T) {
|
||||||
|
O := gomega.NewWithT(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/login-bounce?redirect_url=https://localhost:6600/test", nil)
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
|
||||||
|
loginbounce.LoginBounce(rw, req)
|
||||||
|
|
||||||
|
resp := rw.Result()
|
||||||
|
|
||||||
|
O.Expect(resp.StatusCode).Should(gomega.BeIdenticalTo(303))
|
||||||
|
O.Expect(resp.Header.Get("location")).Should(gomega.ContainSubstring("identify,guild"))
|
||||||
|
O.Expect(resp.Header.Get("set-cookie")).Should(gomega.ContainSubstring("https://localhost:6600/test"))
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue