Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to send access request messages via Discord #11

Merged
merged 2 commits into from
Aug 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions srepanel/common/types.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package common

type Identity struct {
ID uint64 `json:"id"`
Username string `json:"username"`
ID uint64 `json:"id"`
Username string `json:"username"`
AvatarHash string `json:"avatar"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine for now. I suggest putting user information in the SQLite database long term, so the JWT doesn't grow without bounds.

}

type GhidraEndpoint struct {
Expand Down
3 changes: 3 additions & 0 deletions srepanel/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ type config struct {
Discord struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
// TODO allow for more general webhooks
// For now, we assume that the webhook is for Discord
WebhookURL string `json:"webhook_url"`
} `json:"discord"`
Ghidra struct {
Endpoint common.GhidraEndpoint `json:"endpoint"`
Expand Down
37 changes: 37 additions & 0 deletions srepanel/discord/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package discord

// ----------- //
// Channel API //
// ----------- //

// https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure
type EmbedAuthor struct {
Name string `json:"name"`
IconURL string `json:"icon_url"`
}

// https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure
type EmbedField struct {
Name string `json:"name"`
Value string `json:"value"`
Inline bool `json:"inline"`
}

// https://discord.com/developers/docs/resources/channel#embed-object
type Embed struct {
Title string `json:"title"`
Color int `json:"color"`
Author EmbedAuthor `json:"author"`
Fields []EmbedField `json:"fields"`
}

// ----------- //
// Webhook API //
// ----------- //

// https://discord.com/developers/docs/resources/webhook#execute-webhook
type WebhookMessage struct {
Username string `json:"username"`
AvatarURL string `json:"avatar_url"`
Embeds []Embed `json:"embeds"`
}
8 changes: 5 additions & 3 deletions srepanel/discord_auth/auth.go → srepanel/discord/auth.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package discord_auth
package discord

import (
"context"
Expand Down Expand Up @@ -98,6 +98,7 @@ func (c *Auth) GetDiscordIdentity(ctx context.Context, token *oauth2.Token) (ide
User struct {
ID uint64 `json:"id,string"`
Username string `json:"username"`
Avatar string `json:"avatar"`
} `json:"user"`
}
if err := json.NewDecoder(res.Body).Decode(&info); err != nil {
Expand All @@ -109,7 +110,8 @@ func (c *Auth) GetDiscordIdentity(ctx context.Context, token *oauth2.Token) (ide
}

return &common.Identity{
ID: info.User.ID,
Username: info.User.Username,
ID: info.User.ID,
Username: info.User.Username,
AvatarHash: info.User.Avatar,
}, nil
}
11 changes: 6 additions & 5 deletions srepanel/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"os/signal"

"go.mkw.re/ghidra-panel/database"
"go.mkw.re/ghidra-panel/discord_auth"
"go.mkw.re/ghidra-panel/discord"
"go.mkw.re/ghidra-panel/token"
"go.mkw.re/ghidra-panel/web"
)
Expand Down Expand Up @@ -103,14 +103,15 @@ func main() {

redirectURL := cfg.BaseURL + "/redirect"

auth := discord_auth.NewAuth(cfg.Discord.ClientID, cfg.Discord.ClientSecret, redirectURL)
auth := discord.NewAuth(cfg.Discord.ClientID, cfg.Discord.ClientSecret, redirectURL)

issuer := token.NewIssuer((*[32]byte)(secrets.HMACSecret))

webConfig := web.Config{
GhidraEndpoint: &cfg.Ghidra.Endpoint,
Links: cfg.Links,
Dev: *dev,
GhidraEndpoint: &cfg.Ghidra.Endpoint,
Links: cfg.Links,
DiscordWebhookURL: cfg.Discord.WebhookURL,
Dev: *dev,
}
server, err := web.NewServer(&webConfig, db, auth, &issuer, &acls)
if err != nil {
Expand Down
19 changes: 11 additions & 8 deletions srepanel/token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ func NewIssuer(secret *[32]byte) Issuer {
}

type Claims struct {
Sub uint64 `json:"sub,string"`
Name string `json:"name"`
Iat int64 `json:"iat"`
Sub uint64 `json:"sub,string"`
Name string `json:"name"`
AvatarHash string `json:"avatar"`
Iat int64 `json:"iat"`
}

func (c *Claims) String() string {
Expand All @@ -43,9 +44,10 @@ func (c *Claims) String() string {

func (iss Issuer) Issue(ident *common.Identity) string {
claims := &Claims{
Sub: ident.ID,
Name: ident.Username,
Iat: time.Now().Unix(),
Sub: ident.ID,
Name: ident.Username,
AvatarHash: ident.AvatarHash,
Iat: time.Now().Unix(),
}
body := jwtPrefix + claims.String()
return body + "." + iss.sign(body)
Expand Down Expand Up @@ -108,7 +110,8 @@ func (iss Issuer) Verify(jwt string) (ident *common.Identity, ok bool) {

// Reconstruct identity
return &common.Identity{
ID: claims.Sub,
Username: claims.Name,
ID: claims.Sub,
Username: claims.Name,
AvatarHash: claims.AvatarHash,
}, true
}
87 changes: 87 additions & 0 deletions srepanel/web/request_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package web

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"

"go.mkw.re/ghidra-panel/common"
"go.mkw.re/ghidra-panel/discord"
)

func (s *Server) handleRequestAccess(wr http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
http.Error(wr, "Method not allowed", http.StatusMethodNotAllowed)
return
}

ident, ok := s.checkAuth(req)
if !ok {
http.Error(wr, "Not authorized", http.StatusUnauthorized)
return
}

if err := req.ParseForm(); err != nil {
http.Error(wr, "Bad request", http.StatusBadRequest)
return
}

// TODO query ACL to ensure user account exists
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this TODO comment ... If the username doesn't exist in the UserManager, we can just add them before authorizing.


message := s.writeMessage(ident)

// Send access request message
ctx := context.TODO()
payloadBuf, _ := json.Marshal(&message)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.Config.DiscordWebhookURL, bytes.NewReader(payloadBuf))
if err != nil {
http.Error(wr, "Internal server error", http.StatusInternalServerError)
return
}

req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
// TODO properly handle
http.Redirect(wr, req, "/?access_request=failure", http.StatusTemporaryRedirect)
return
}
defer res.Body.Close()

http.Redirect(wr, req, "/?access_request=success", http.StatusTemporaryRedirect)
}

func (s *Server) writeMessage(ident *common.Identity) discord.WebhookMessage {
embedAuthor := discord.EmbedAuthor{
Name: ident.Username,
IconURL: fmt.Sprintf("https://cdn.discordapp.com/avatars/%d/%s.png", ident.ID, ident.AvatarHash),
}

hostnameField := discord.EmbedField{
Name: "Hostname",
Value: s.Config.GhidraEndpoint.Hostname,
Inline: true,
}

portField := discord.EmbedField{
Name: "Port",
Value: strconv.FormatUint(uint64(s.Config.GhidraEndpoint.Port), 10),
Inline: true,
}

ghidraEmbed := discord.Embed{
Title: fmt.Sprintf("%s has requested access to the following Ghidra server:", ident.Username),
Color: 0x77DD77,
Author: embedAuthor,
Fields: []discord.EmbedField{hostnameField, portField},
}

return discord.WebhookMessage{
Username: "Panel",
AvatarURL: "",
Embeds: []discord.Embed{ghidraEmbed},
}
}
14 changes: 8 additions & 6 deletions srepanel/web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

"go.mkw.re/ghidra-panel/common"
"go.mkw.re/ghidra-panel/database"
"go.mkw.re/ghidra-panel/discord_auth"
"go.mkw.re/ghidra-panel/discord"
"go.mkw.re/ghidra-panel/ghidra"
"go.mkw.re/ghidra-panel/token"
)
Expand Down Expand Up @@ -35,23 +35,24 @@ func init() {
}

type Config struct {
GhidraEndpoint *common.GhidraEndpoint
Links []common.Link
Dev bool // developer mode
GhidraEndpoint *common.GhidraEndpoint
Links []common.Link
DiscordWebhookURL string
Dev bool // developer mode
}

type Server struct {
Config *Config
DB *database.DB
Auth *discord_auth.Auth
Auth *discord.Auth
Issuer *token.Issuer
ACLs *ghidra.ACLMon
}

func NewServer(
config *Config,
db *database.DB,
auth *discord_auth.Auth,
auth *discord.Auth,
issuer *token.Issuer,
acls *ghidra.ACLMon,
) (*Server, error) {
Expand All @@ -72,6 +73,7 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/logout", s.handleLogout)

mux.HandleFunc("/update_password", s.handleUpdatePassword)
mux.HandleFunc("/request_access", s.handleRequestAccess)

// Create file server for assets
mux.Handle("/assets/", http.FileServer(http.FS(assets)))
Expand Down
8 changes: 7 additions & 1 deletion srepanel/web/templates/home.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,14 @@
</ul>
{{ else }}
<p>Your account does not have any access to Ghidra repositories.</p>
<p><strong><a href="mailto:richard@mkw.re">Request Access</a></strong></p>
{{ if .UserState.HasPassword }}
<form action="/request_access" method="post">
<button role="button" type="submit" class="outline">Request Access</button>
</form>
{{ else }}
<p>You cannot request access to Ghidra repositories without setting a password. Please set one to continue!</p>
{{ end }}
{{ end }}
</article>
<article>
<header>
Expand Down
3 changes: 2 additions & 1 deletion test_config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"discord": {
"client_id": "xxx",
"client_secret": "xxx"
"client_secret": "xxx",
"webhook_url": "xxx"
},
"base_url": "http://localhost:8080",
"ghidra": {
Expand Down
Loading