diff --git a/.gitignore b/.gitignore index d24e2fa..029484d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ .idea/ -/mcm-register* \ No newline at end of file +/mineauth* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d609584 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# mineauth + +Users can log into a server (which validates their account) and you can run hooks after. + +```text +Usage of mineauth: + -community string + URL to community + -debug + Debug Logging + -hook value + Hook to run + -ping string + Message for the server list (default "Login to authenticate!") + -port int + Port to listen on (default 25565) + -timeout int + HTTP timeout (default 15) + +``` + +## License + +[MIT](LICENSE) \ No newline at end of file diff --git a/_hooks/discord/discord.go b/_hooks/discord/discord.go new file mode 100644 index 0000000..767eaf7 --- /dev/null +++ b/_hooks/discord/discord.go @@ -0,0 +1,76 @@ +package main + +import ( + "bytes" + _ "embed" + "encoding/json" + "fmt" + "net/http" + "os" +) + +//go:embed webhook.txt +var webhookURL string + +type profile struct { + ID string `json:"id"` + Name string `json:"name"` + IP string `json:"ip"` +} + +func (p *profile) UUID() string { + return fmt.Sprintf("%s-%s-%s-%s-%s", p.ID[:8], p.ID[8:12], p.ID[12:16], p.ID[16:20], p.ID[20:32]) +} + +func main() { + var profile profile + if err := json.NewDecoder(os.Stdin).Decode(&profile); err != nil { + panic(err) + } + + payloadHook := webhook{ + Username: profile.Name, + Embeds: []embed{ + { + Fields: []field{ + { + Name: "UUID", + Value: profile.UUID(), + }, + { + Name: "IP", + Value: profile.IP, + }, + }, + }, + }, + } + payload, err := json.Marshal(payloadHook) + if err != nil { + panic(err) + } + + res, err := http.Post(webhookURL, "application/json", bytes.NewReader(payload)) + if err != nil { + panic(err) + } + + if res.StatusCode != http.StatusNoContent { + panic(fmt.Errorf("received non-200 status: %s", res.Status)) + } + +} + +type webhook struct { + Username string `json:"username"` + Embeds []embed `json:"embeds"` +} + +type embed struct { + Fields []field `json:"fields"` +} + +type field struct { + Name string `json:"name"` + Value string `json:"value"` +} diff --git a/_hooks/discord/webhook.txt b/_hooks/discord/webhook.txt new file mode 100644 index 0000000..fe90309 --- /dev/null +++ b/_hooks/discord/webhook.txt @@ -0,0 +1 @@ +https://discord.com/api/webhooks// \ No newline at end of file diff --git a/go.mod b/go.mod index e4bb00a..40dd5ef 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.canopymc.net/Etzelia/mcm-register +module git.canopymc.net/Etzelia/mineauth go 1.16 diff --git a/main.go b/main.go index 719d446..13885d4 100644 --- a/main.go +++ b/main.go @@ -2,19 +2,27 @@ package main import ( "flag" - "github.com/peterbourgon/ff/v3" - "github.com/peterbourgon/ff/v3/fftoml" - "go.jolheiser.com/beaver" + "git.canopymc.net/Etzelia/mineauth/server" "net/http" "os" "time" + + "github.com/peterbourgon/ff/v3" + "github.com/peterbourgon/ff/v3/fftoml" + "go.jolheiser.com/beaver" ) func main() { - fs := flag.NewFlagSet("afk", flag.ExitOnError) + fs := flag.NewFlagSet("mineauth", flag.ExitOnError) portFlag := fs.Int("port", 25565, "Port to listen on") timeoutFlag := fs.Int("timeout", 15, "HTTP timeout") - discordFlag := fs.String("discord", "", "Discord invite link") + pingMessageFlag := fs.String("ping", "Login to authenticate!", "Message for the server list") + communityFlag := fs.String("community", "", "URL to community") + hooksFlag := make([]string, 0) + fs.Func("hook", "Hook to run", func(hook string) error { + hooksFlag = append(hooksFlag, hook) + return nil + }) debugFlag := fs.Bool("debug", false, "Debug Logging") if err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("MCM_REGISTER"), @@ -31,13 +39,18 @@ func main() { http.DefaultClient.Timeout = time.Second * time.Duration(*timeoutFlag) - server, err := NewServer(*discordFlag) + serv, err := server.New(server.Options{ + PingMessage: *pingMessageFlag, + CommunityURL: *communityFlag, + Hooks: hooksFlag, + }) if err != nil { beaver.Error(err) return } - if err := server.Start(*portFlag); err != nil { + beaver.Infof("Listening on http://localhost:%d", *portFlag) + if err := serv.Start(*portFlag); err != nil { beaver.Error(err) } } diff --git a/cfb.go b/server/cfb.go similarity index 98% rename from cfb.go rename to server/cfb.go index 2919a6e..5d1b7a2 100644 --- a/cfb.go +++ b/server/cfb.go @@ -1,4 +1,4 @@ -package main +package server import "crypto/cipher" diff --git a/digest.go b/server/digest.go similarity index 98% rename from digest.go rename to server/digest.go index ae86a49..2af68c6 100644 --- a/digest.go +++ b/server/digest.go @@ -1,4 +1,4 @@ -package main +package server import ( "crypto/sha1" diff --git a/encryption.go b/server/encryption.go similarity index 98% rename from encryption.go rename to server/encryption.go index cd3e3b5..f667106 100644 --- a/encryption.go +++ b/server/encryption.go @@ -1,4 +1,4 @@ -package main +package server import ( "bytes" @@ -9,11 +9,12 @@ import ( "encoding/json" "errors" "fmt" - "github.com/Tnze/go-mc/net" - pk "github.com/Tnze/go-mc/net/packet" "io" "net/http" "net/url" + + "github.com/Tnze/go-mc/net" + pk "github.com/Tnze/go-mc/net/packet" ) var hasJoinedURL = func() *url.URL { @@ -37,6 +38,7 @@ func (s *Server) encryptionRequest(conn net.Conn) ([]byte, error) { type profile struct { ID string `json:"id"` Name string `json:"name"` + IP string `json:"ip"` } func (p *profile) UUID() string { diff --git a/favicon.png b/server/favicon.png similarity index 100% rename from favicon.png rename to server/favicon.png diff --git a/ping.go b/server/ping.go similarity index 88% rename from ping.go rename to server/ping.go index 65fd791..3c811ea 100644 --- a/ping.go +++ b/server/ping.go @@ -1,9 +1,10 @@ -package main +package server import ( "encoding/base64" "encoding/json" "fmt" + "github.com/Tnze/go-mc/bot" "github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/net" @@ -22,7 +23,7 @@ func (s *Server) acceptListPing(conn net.Conn) { switch p.ID { case 0x00: - err = conn.WritePacket(pk.Marshal(0x00, pk.String(listResp()))) + err = conn.WritePacket(pk.Marshal(0x00, pk.String(s.listResp()))) case 0x01: err = conn.WritePacket(p) } @@ -38,7 +39,7 @@ type player struct { } // listResp return server status as JSON string -func listResp() string { +func (s *Server) listResp() string { var list struct { Version struct { Name string `json:"name"` @@ -58,7 +59,7 @@ func listResp() string { list.Players.Max = 0 list.Players.Online = 0 list.Players.Sample = []player{} - list.Description = chat.Message{Text: "Login to register!"} + list.Description = chat.Message{Text: s.opts.PingMessage} list.FavIcon = fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(favicon)) data, err := json.Marshal(list) diff --git a/server.go b/server/server.go similarity index 71% rename from server.go rename to server/server.go index b7768cb..86ab2af 100644 --- a/server.go +++ b/server/server.go @@ -1,28 +1,39 @@ -package main +package server import ( + "bytes" "crypto/rand" "crypto/rsa" "crypto/x509" _ "embed" + "encoding/json" "fmt" "github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/net" pk "github.com/Tnze/go-mc/net/packet" "github.com/google/uuid" "go.jolheiser.com/beaver" + gonet "net" + "os/exec" + "strings" ) //go:embed favicon.png var favicon []byte type Server struct { - privateKey *rsa.PrivateKey - publicKey []byte - discordInvite string + privateKey *rsa.PrivateKey + publicKey []byte + opts Options } -func NewServer(discordInvite string) (*Server, error) { +type Options struct { + CommunityURL string + PingMessage string + Hooks []string +} + +func New(opts Options) (*Server, error) { private, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { return nil, fmt.Errorf("error generate private key: %v", err) @@ -36,14 +47,13 @@ func NewServer(discordInvite string) (*Server, error) { private.Precompute() return &Server{ - privateKey: private, - publicKey: public, - discordInvite: discordInvite, + privateKey: private, + publicKey: public, + opts: opts, }, nil } func (s *Server) Start(port int) error { - beaver.Infof("Listening on http://localhost:%d", port) l, err := net.ListenMC(fmt.Sprintf(":%d", port)) if err != nil { return fmt.Errorf("listen error: %v", err) @@ -96,7 +106,34 @@ func (s *Server) handlePlaying(conn net.Conn) { return } - // TODO Register + profile.IP, _, err = gonet.SplitHostPort(conn.Socket.RemoteAddr().String()) + if err != nil { + beaver.Errorf("could not get user IP: %v", err) + return + } + + payload, err := json.Marshal(profile) + if err != nil { + beaver.Errorf("could not marshal payload: %v", err) + return + } + + for _, hook := range s.opts.Hooks { + go func(h string) { + s := strings.Split(h, " ") + var args []string + if len(s) > 1 { + args = s[1:] + } + cmd := exec.Command(s[0], args...) + cmd.Stdin = bytes.NewReader(payload) + out, err := cmd.CombinedOutput() + if err != nil { + beaver.Errorf("could not run hook `%s`: %v", h, err) + beaver.Warn(string(out)) + } + }(hook) + } econn, err := encryptedConn(conn, secret) if err != nil { @@ -104,9 +141,9 @@ func (s *Server) handlePlaying(conn net.Conn) { return } - msg := fmt.Sprintf("Thanks for registering, %s!", profile.Name) - if s.discordInvite != "" { - msg += fmt.Sprintf("\n\nJoin the Discord\n%s", s.discordInvite) + msg := fmt.Sprintf("Thanks for authenticating, %s!", profile.Name) + if s.opts.CommunityURL != "" { + msg += fmt.Sprintf("\n\nJoin the community\n%s", s.opts.CommunityURL) } packet := pk.Marshal(0x00, chat.Message{Text: msg},