forked from TripwireTeam/tripwire
skins work!!!!!!!!!!!!
This commit is contained in:
parent
c266a5d9fd
commit
77ce652e28
12 changed files with 239 additions and 68 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@ secrets.go
|
|||
tripwire
|
||||
skins/
|
||||
capes/
|
||||
keys/
|
||||
|
|
22
auth.go
22
auth.go
|
@ -1,13 +1,18 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func rootEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, YggdrasilInfo{
|
||||
skinDomains := []string{
|
||||
config.BaseUrl,
|
||||
}
|
||||
|
||||
response := YggdrasilInfo{
|
||||
Status: "OK",
|
||||
RuntimeMode: "productionMode",
|
||||
AppAuthor: "Tripwire Team",
|
||||
|
@ -16,7 +21,20 @@ func rootEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||
AppName: "tripwire.yggdrasil",
|
||||
ImplVersion: "5.2.0",
|
||||
AppOwner: "Who knows?",
|
||||
})
|
||||
|
||||
//Used by authlib-injector
|
||||
SkinDomains: skinDomains,
|
||||
}
|
||||
|
||||
if publicKey != nil {
|
||||
pubkey, err := x509.MarshalPKIXPublicKey(publicKey)
|
||||
if err == nil {
|
||||
pubKeyString := string(encodePem(pubkey, "PUBLIC KEY"))
|
||||
response.PublicKey = &pubKeyString
|
||||
}
|
||||
}
|
||||
|
||||
sendJSON(w, response)
|
||||
}
|
||||
|
||||
func authenticateEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
type Configuration struct {
|
||||
BaseUrl string `yaml:"baseUrl"`
|
||||
Protocol string `yaml:"protocol"`
|
||||
DebugMode bool `yaml:"debugMode"`
|
||||
MaxTextureSize int `yaml:"maxTextureSize"`
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
# The URL the Tripwire instance is hosted on.
|
||||
baseUrl: "http://localhost:10000"
|
||||
# The primary domain the Tripwire instance is hosted on.
|
||||
# It shouldn't be a local IP in production.
|
||||
baseUrl: "localhost:10000"
|
||||
|
||||
# The protocol to be used for Tripwire
|
||||
protocol: "http://"
|
||||
|
||||
# Show debug output (if unsure, set to false)
|
||||
debugMode: true
|
||||
|
|
5
go.mod
5
go.mod
|
@ -8,3 +8,8 @@ require (
|
|||
github.com/mattn/go-sqlite3 v1.14.13
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect
|
||||
)
|
||||
|
|
4
go.sum
4
go.sum
|
@ -4,6 +4,10 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
|||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
|
||||
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU=
|
||||
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
17
main.go
17
main.go
|
@ -38,11 +38,6 @@ func handleRequests() {
|
|||
r.Use(logger)
|
||||
r.NotFoundHandler = http.HandlerFunc(notfoundlogger)
|
||||
|
||||
err := loadConfig()
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to load config.yaml:", err)
|
||||
}
|
||||
|
||||
// todo: make this cleaner if possible
|
||||
registerAuthEndpoints(r)
|
||||
registerSessionEndpoints(r)
|
||||
|
@ -53,10 +48,22 @@ func handleRequests() {
|
|||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 && os.Args[1] == "gen-keys" {
|
||||
genKeys()
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Tripwire initializing...")
|
||||
os.Mkdir("skins", 0755)
|
||||
os.Mkdir("capes", 0755)
|
||||
|
||||
err := loadConfig()
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to load config.yaml:", err)
|
||||
}
|
||||
|
||||
initDB()
|
||||
initKeys()
|
||||
|
||||
handleRequests()
|
||||
|
||||
|
|
42
session.go
42
session.go
|
@ -12,6 +12,20 @@ import (
|
|||
|
||||
func profileEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
uuid := mux.Vars(r)["uuid"]
|
||||
query := r.URL.Query()
|
||||
|
||||
if len(uuid) == 32 {
|
||||
uuid = growUUID(uuid)
|
||||
}
|
||||
|
||||
if len(uuid) != 36 {
|
||||
sendError(w, YggError{
|
||||
Code: 400,
|
||||
Error: "Bad Request",
|
||||
ErrorMessage: "Invalid UUID.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
exists, err := playerExistsByUUID(uuid)
|
||||
if err != nil {
|
||||
|
@ -33,7 +47,9 @@ func profileEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
response, err := generateProfileResponse(uuid, player.Username)
|
||||
signed := query.Has("unsigned") && query.Get("unsigned") == "false"
|
||||
|
||||
response, err := generateProfileResponse(uuid, player.Username, signed)
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
|
@ -55,7 +71,7 @@ func hasJoinedEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
response, err := generateProfileResponse(player.UUID, params.Get("username"))
|
||||
response, err := generateProfileResponse(player.UUID, params.Get("username"), true)
|
||||
if err != nil {
|
||||
handleError(w, err)
|
||||
return
|
||||
|
@ -100,10 +116,10 @@ func registerSessionEndpoints(r *mux.Router) {
|
|||
r.HandleFunc(prefix+"/session/minecraft/hasJoined", hasJoinedEndpoint).Methods("GET")
|
||||
}
|
||||
|
||||
func generateProfileResponse(uuid string, username string) (ProfileResponse, error) {
|
||||
func generateProfileResponse(uuid string, username string, signed bool) (ProfileResponse, error) {
|
||||
// todo: make this more visually appealing if possible
|
||||
skin := SkinTexture{}
|
||||
skin.Url = config.BaseUrl + "/getTexture/" + uuid + "?type=skin"
|
||||
skin.Url = config.Protocol + config.BaseUrl + "/getTexture/" + uuid + "?type=skin"
|
||||
skin.Metadata = SkinMetadata{}
|
||||
skin.Metadata.Model = "default"
|
||||
|
||||
|
@ -116,7 +132,7 @@ func generateProfileResponse(uuid string, username string) (ProfileResponse, err
|
|||
_, err := os.Stat("capes/" + uuid + ".png")
|
||||
if err == nil {
|
||||
cape := &Texture{}
|
||||
cape.Url = config.BaseUrl + "/getTexture/" + uuid + "?type=cape"
|
||||
cape.Url = config.Protocol + config.BaseUrl + "/getTexture/" + uuid + "?type=cape"
|
||||
textures.Textures.Cape = cape
|
||||
}
|
||||
|
||||
|
@ -124,6 +140,7 @@ func generateProfileResponse(uuid string, username string) (ProfileResponse, err
|
|||
if err != nil {
|
||||
return ProfileResponse{}, err
|
||||
}
|
||||
|
||||
encodedTextures := base64.StdEncoding.EncodeToString(marshalledTextures)
|
||||
|
||||
response := ProfileResponse{}
|
||||
|
@ -135,6 +152,19 @@ func generateProfileResponse(uuid string, username string) (ProfileResponse, err
|
|||
Value: encodedTextures,
|
||||
},
|
||||
}
|
||||
|
||||
if signed && privateKey != nil {
|
||||
signedTextures, err := signWithPrivateKey(string(encodedTextures))
|
||||
if err != nil {
|
||||
return ProfileResponse{}, err
|
||||
}
|
||||
|
||||
b64 := make([]byte, base64.StdEncoding.EncodedLen(len(signedTextures)))
|
||||
base64.StdEncoding.Encode(b64, signedTextures)
|
||||
|
||||
stringified := string(b64)
|
||||
response.Properties[0].Signature = &stringified
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
|
@ -144,7 +174,7 @@ func shrinkUUID(uuid string) string {
|
|||
|
||||
func growUUID(uuid string) string {
|
||||
if len(uuid) == 32 {
|
||||
return uuid[0:7] + "-" + uuid[8:11] + "-" + uuid[12:15] + "-" + uuid[16:19] + "-" + uuid[20:31]
|
||||
return uuid[0:8] + "-" + uuid[8:12] + "-" + uuid[12:16] + "-" + uuid[16:20] + "-" + uuid[20:32]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
39
sha.go
39
sha.go
|
@ -1,39 +0,0 @@
|
|||
// Ripped from https://gist.github.com/toqueteos/5372776
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func authDigest(s string) string {
|
||||
h := sha1.New()
|
||||
io.WriteString(h, s)
|
||||
hash := h.Sum(nil)
|
||||
|
||||
negative := (hash[0] & 0x80) == 0x80
|
||||
if negative {
|
||||
hash = twosComplement(hash)
|
||||
}
|
||||
|
||||
res := strings.TrimLeft(hex.EncodeToString(hash), "0")
|
||||
if negative {
|
||||
res = "-" + res
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func twosComplement(p []byte) []byte {
|
||||
carry := true
|
||||
for i := len(p) - 1; i >= 0; i-- {
|
||||
p[i] = byte(^p[i])
|
||||
if carry {
|
||||
carry = p[i] == 0xff
|
||||
p[i]++
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
124
signature.go
Normal file
124
signature.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var publicKey *rsa.PublicKey
|
||||
var privateKey *rsa.PrivateKey
|
||||
|
||||
func initKeys() {
|
||||
pubKeyBytes, err1 := os.ReadFile("keys/public.key")
|
||||
privKeyBytes, err2 := os.ReadFile("keys/private.key")
|
||||
if err1 != nil || err2 != nil {
|
||||
log.Println("WARNING: At least one key half could not be opened, players will not have any textures!")
|
||||
log.Println("Try generating a keypair by running \"tripwire gen-keys\".")
|
||||
if config.DebugMode {
|
||||
log.Println(err1)
|
||||
log.Println(err2)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
pubDer, _ := pem.Decode(pubKeyBytes)
|
||||
privDer, _ := pem.Decode(privKeyBytes)
|
||||
|
||||
pubKey, err1 := x509.ParsePKIXPublicKey(pubDer.Bytes)
|
||||
privKey, err2 := x509.ParsePKCS8PrivateKey(privDer.Bytes)
|
||||
if err1 != nil || err2 != nil {
|
||||
log.Println("WARNING: At least one key half could not be loaded, players will not have any textures!")
|
||||
log.Println("Try generating a keypair by running \"tripwire gen-keys\".")
|
||||
if config.DebugMode {
|
||||
log.Println(err1)
|
||||
log.Println(err2)
|
||||
}
|
||||
return
|
||||
}
|
||||
publicKey = pubKey.(*rsa.PublicKey)
|
||||
privateKey = privKey.(*rsa.PrivateKey)
|
||||
}
|
||||
|
||||
func genKeys() {
|
||||
os.Mkdir("keys", 0700)
|
||||
_, err1 := os.Stat("keys/public.key")
|
||||
_, err2 := os.Stat("keys/public.key")
|
||||
if err1 == nil || err2 == nil {
|
||||
log.Println(
|
||||
"Error: At least one key half is already present. " +
|
||||
"If you are having errors reading the key " +
|
||||
"files, you have likely incorrectly configured " +
|
||||
"folder permissions.",
|
||||
)
|
||||
log.Println(
|
||||
"If you would like to generate a new keypair anyway, " +
|
||||
"delete the keys folder and run this command again.",
|
||||
)
|
||||
os.Exit(1)
|
||||
}
|
||||
if !errors.Is(err1, fs.ErrNotExist) || !errors.Is(err2, fs.ErrNotExist) {
|
||||
log.Fatalln(
|
||||
"Error: Could not access keys folder. " +
|
||||
"Try recreating the folder, or running this command " +
|
||||
"as a user that has permissions to view it.",
|
||||
)
|
||||
}
|
||||
log.Println("Generating RSA keypair at 4096 bits...")
|
||||
privkey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
pubkeygen, err := x509.MarshalPKIXPublicKey(&privkey.PublicKey)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(
|
||||
"keys/public.key",
|
||||
encodePem(pubkeygen, "PUBLIC KEY"),
|
||||
0600,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
privkeygen, err := x509.MarshalPKCS8PrivateKey(privkey)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(
|
||||
"keys/private.key",
|
||||
encodePem(privkeygen, "PRIVATE KEY"),
|
||||
0600,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
log.Println("Done!")
|
||||
}
|
||||
|
||||
func signWithPrivateKey(value string) ([]byte, error) {
|
||||
hasher := sha1.New()
|
||||
hasher.Write([]byte(value))
|
||||
|
||||
return rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA1, hasher.Sum(nil))
|
||||
}
|
||||
|
||||
func encodePem(in []byte, name string) []byte {
|
||||
return pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: name,
|
||||
Bytes: in,
|
||||
},
|
||||
)
|
||||
}
|
23
types.go
23
types.go
|
@ -67,19 +67,22 @@ type PlayerData struct {
|
|||
}
|
||||
|
||||
type YggdrasilInfo struct {
|
||||
Status string `json:"Status"`
|
||||
RuntimeMode string `json:"Runtime-Mode"`
|
||||
AppAuthor string `json:"Application-Author"`
|
||||
AppDescription string `json:"Application-Description"`
|
||||
SpecVersion string `json:"Specification-Version"`
|
||||
AppName string `json:"Application-Name"`
|
||||
ImplVersion string `json:"Implementation-Version"`
|
||||
AppOwner string `json:"Application-Owner"`
|
||||
Status string `json:"Status"`
|
||||
RuntimeMode string `json:"Runtime-Mode"`
|
||||
AppAuthor string `json:"Application-Author"`
|
||||
AppDescription string `json:"Application-Description"`
|
||||
SpecVersion string `json:"Specification-Version"`
|
||||
AppName string `json:"Application-Name"`
|
||||
ImplVersion string `json:"Implementation-Version"`
|
||||
AppOwner string `json:"Application-Owner"`
|
||||
SkinDomains []string `json:"skinDomains"`
|
||||
PublicKey *string `json:"signaturePublickey"`
|
||||
}
|
||||
|
||||
type Property struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Signature *string `json:"signature"`
|
||||
}
|
||||
|
||||
type ProfileResponse struct {
|
||||
|
|
21
web.go
21
web.go
|
@ -69,7 +69,7 @@ func logInEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func getResourceEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
func getTextureEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
uuid := mux.Vars(r)["uuid"]
|
||||
query := r.URL.Query()
|
||||
if !query.Has("type") {
|
||||
|
@ -107,7 +107,7 @@ func getResourceEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||
w.Write(skin)
|
||||
}
|
||||
|
||||
func setResourceEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
func setTextureEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
uuid := mux.Vars(r)["uuid"]
|
||||
query := r.URL.Query()
|
||||
if !query.Has("type") {
|
||||
|
@ -124,6 +124,19 @@ func setResourceEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if len(uuid) == 32 {
|
||||
uuid = growUUID(uuid)
|
||||
}
|
||||
|
||||
if len(uuid) != 36 {
|
||||
sendError(w, YggError{
|
||||
Code: 400,
|
||||
Error: "Bad Request",
|
||||
ErrorMessage: "Invalid UUID.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
r.ParseMultipartForm(int64(config.MaxTextureSize))
|
||||
|
||||
token := r.FormValue("token")
|
||||
|
@ -196,6 +209,6 @@ func registerWebEndpoints(r *mux.Router) {
|
|||
r.PathPrefix(webDir).
|
||||
Handler(http.StripPrefix(webDir, http.FileServer(http.Dir("."+webDir))))
|
||||
|
||||
r.HandleFunc("/getTexture/{uuid}", getResourceEndpoint).Methods("GET")
|
||||
r.HandleFunc("/setTexture/{uuid}", setResourceEndpoint).Methods("POST")
|
||||
r.HandleFunc("/getTexture/{uuid}", getTextureEndpoint).Methods("GET")
|
||||
r.HandleFunc("/setTexture/{uuid}", setTextureEndpoint).Methods("POST")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue