├── tmpskin ├── .gitkeep └── .gitignore ├── mojang ├── filler.go ├── errors.go ├── profiles.go ├── util.go └── httpprofiles.go ├── index.yaml ├── CONTRIBUTORS ├── .gitignore ├── keys.go.enc ├── templates ├── img │ ├── mca1.png │ └── mca2.png ├── base.html ├── frontbase.html ├── static │ └── locales │ │ ├── ru │ │ └── translation.json │ │ ├── it │ │ └── translation.json │ │ ├── en │ │ └── translation.json │ │ └── es │ │ └── translation.json ├── signup.html ├── verification.html ├── minibase.html ├── css │ └── frontend.css └── perform.html ├── minecraft ├── errors.go ├── skin.go ├── profiles.go ├── fallbackskin.go └── sessionserver.go ├── mcassoc ├── errors.go ├── verify.go ├── mcassoc.go ├── embed.go └── datablock.go ├── app.yaml ├── cloudbuild.yaml ├── README.md ├── util ├── mcassoc_genshared.go └── mcassoc.go ├── .gcloudignore ├── LICENSE ├── go.mod ├── dns.go ├── statkeeper └── statkeeper.go ├── mcassoc.go └── go.sum /tmpskin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmpskin/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /mojang/filler.go: -------------------------------------------------------------------------------- 1 | package mojang 2 | -------------------------------------------------------------------------------- /index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | # AUTOGENERATED 3 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Luke Granger-Brown 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | .idea/ 3 | .gitignore 4 | keys.go 5 | -------------------------------------------------------------------------------- /keys.go.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukegb/mcassoc/HEAD/keys.go.enc -------------------------------------------------------------------------------- /templates/img/mca1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukegb/mcassoc/HEAD/templates/img/mca1.png -------------------------------------------------------------------------------- /templates/img/mca2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukegb/mcassoc/HEAD/templates/img/mca2.png -------------------------------------------------------------------------------- /minecraft/errors.go: -------------------------------------------------------------------------------- 1 | package minecraft 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ERR_HAS_NO_SKIN = errors.New(`user has no skin`) 9 | ) 10 | -------------------------------------------------------------------------------- /mcassoc/errors.go: -------------------------------------------------------------------------------- 1 | package mcassoc 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ERR_SKIN_TOO_SMALL = errors.New(`skin must be at least 8x8`) 9 | ) 10 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {{template "layout"}} 2 | 3 | 4 | 5 | {{.}} 6 | 7 | 8 | {{template "content" .}} 9 | 10 | 11 | {{end}} 12 | -------------------------------------------------------------------------------- /mojang/errors.go: -------------------------------------------------------------------------------- 1 | package mojang 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ERR_NO_SUCH_USER = errors.New(`no such user`) 9 | ERR_TOO_MANY_RESULTS = errors.New(`too many results`) 10 | ) 11 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go119 2 | app_engine_apis: true 3 | 4 | handlers: 5 | - url: /static 6 | static_dir: templates/static 7 | secure: always 8 | 9 | - url: /img 10 | static_dir: templates/img 11 | secure: always 12 | 13 | - url: /css 14 | static_dir: templates/css 15 | secure: always 16 | 17 | - url: /.* 18 | script: auto 19 | secure: always 20 | -------------------------------------------------------------------------------- /mojang/profiles.go: -------------------------------------------------------------------------------- 1 | package mojang 2 | 3 | type Profile struct { 4 | Id string 5 | Name string 6 | } 7 | 8 | /*type ProfileCriteria struct { 9 | Name string `json:"name"` 10 | Agent string `json:"agent"` 11 | }*/ 12 | type ProfileCriteria []string 13 | 14 | type ProfileRepository interface { 15 | GetProfilesByCriteria([]ProfileCriteria) []Profile 16 | } 17 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | # Decrypt keys.go.enc into keys.go 3 | - name: "gcr.io/cloud-builders/gcloud" 4 | args: 5 | - kms 6 | - decrypt 7 | - --ciphertext-file=keys.go.enc 8 | - --plaintext-file=keys.go 9 | - --location=global 10 | - --keyring=cloudbuild 11 | - --key=cloudbuild-enc 12 | 13 | # Launch the AppEngine deploy 14 | - name: "gcr.io/cloud-builders/gcloud" 15 | args: ["app", "deploy"] 16 | 17 | timeout: "1600s" 18 | -------------------------------------------------------------------------------- /mcassoc/verify.go: -------------------------------------------------------------------------------- 1 | package mcassoc 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | func Verify(data string, key string, skin image.Image) (bool, error) { 8 | db, err := GenerateDatablock(data, key) 9 | if err != nil { 10 | return false, err 11 | } 12 | 13 | // at the moment I just cheat and take the entire skin 14 | // this is OK for the moment, but in future if the DB needs to go somewhere else, we might have issues 15 | 16 | return CompareDatablocks(db, skin), nil 17 | } 18 | -------------------------------------------------------------------------------- /mojang/util.go: -------------------------------------------------------------------------------- 1 | package mojang 2 | 3 | import "net/http" 4 | 5 | func GetProfileByUsername(c *http.Client, username string) (Profile, error) { 6 | hpr := NewHttpProfileRepository(c) 7 | profiles, err := hpr.GetProfilesByUsername([]string{username}) 8 | if err != nil { 9 | return Profile{}, err 10 | } else if len(profiles) == 0 { 11 | return Profile{}, ERR_NO_SUCH_USER 12 | } else if len(profiles) > 1 { 13 | return Profile{}, ERR_TOO_MANY_RESULTS 14 | } 15 | return profiles[0], nil 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mcassoc 2 | 3 | mcassoc is a free service that allows you to add Minecraft account association to your site or forums. By using skins to verify association, users don't need to provide account details to 3rd-party sites and once they've signed up once they simply need to enter their separate mcassoc credentials if they wish to associate on a future site. 4 | 5 | Integration is simple and sample code is available in both PHP and Python. 6 | 7 | To learn more and get your shared key, visit [mcassoc.lukegb.com](https://mcassoc.lukegb.com/) 8 | 9 | This is not an official Google product. 10 | -------------------------------------------------------------------------------- /util/mcassoc_genshared.go: -------------------------------------------------------------------------------- 1 | //+build skip 2 | 3 | package main 4 | 5 | import ( 6 | "crypto/hmac" 7 | "crypto/sha512" 8 | "encoding/hex" 9 | "log" 10 | "os" 11 | ) 12 | 13 | func generateSharedKey(sesskey, siteid string) []byte { 14 | z := hmac.New(sha512.New, []byte(sesskey)) 15 | z.Write([]byte(siteid)) 16 | key := z.Sum([]byte{}) 17 | return key 18 | } 19 | 20 | func main() { 21 | if len(os.Args) != 3 { 22 | log.Fatalln(os.Args[0], " ") 23 | } 24 | 25 | log.Println("Shared secret for", os.Args[2], "-", hex.EncodeToString(generateSharedKey(os.Args[1], os.Args[2]))) 26 | } 27 | -------------------------------------------------------------------------------- /mcassoc/mcassoc.go: -------------------------------------------------------------------------------- 1 | package mcassoc 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | type Associfier struct { 8 | key string 9 | } 10 | 11 | func (a Associfier) Verify(data string, skin image.Image) (bool, error) { 12 | return Verify(data, a.key, skin) 13 | } 14 | 15 | func (a Associfier) Embed(data string, skin image.Image) (image.Image, error) { 16 | return Embed(data, a.key, skin) 17 | } 18 | 19 | func (a Associfier) generateDatablock(data string) (image.Image, error) { 20 | return GenerateDatablock(data, a.key) 21 | } 22 | 23 | func NewAssocifier(key string) Associfier { 24 | return Associfier{ 25 | key: key, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /minecraft/skin.go: -------------------------------------------------------------------------------- 1 | package minecraft 2 | 3 | import ( 4 | "image" 5 | _ "image/jpeg" 6 | _ "image/png" 7 | "net/http" 8 | ) 9 | 10 | func GetSkin(c *http.Client, pc Profile) (image.Image, error) { 11 | td, err := pc.Textures() 12 | switch { 13 | case err == ERR_HAS_NO_SKIN: 14 | return fallbackSkin(c, pc) 15 | case err != nil: 16 | return nil, err 17 | } 18 | 19 | if skin, ok := td.Textures["SKIN"]; !ok { 20 | return fallbackSkin(c, pc) 21 | } else { 22 | resp, err := c.Get(skin.Url) 23 | if err != nil { 24 | return nil, err 25 | } 26 | defer resp.Body.Close() 27 | 28 | img, _, err := image.Decode(resp.Body) 29 | return img, err 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /mcassoc/embed.go: -------------------------------------------------------------------------------- 1 | package mcassoc 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | ) 7 | 8 | func Embed(data string, key string, skin image.Image) (image.Image, error) { 9 | sb := skin.Bounds() 10 | 11 | if sb.Dx() < DATABLOCK_WIDTH || sb.Dy() < DATABLOCK_HEIGHT { 12 | return nil, ERR_SKIN_TOO_SMALL 13 | } 14 | 15 | db, err := GenerateDatablock(data, key) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | // generate a new skin to work from 21 | outim := image.NewNRGBA(image.Rect(0, 0, sb.Dx(), sb.Dy())) 22 | draw.Draw(outim, sb, skin, image.Point{X: 0, Y: 0}, draw.Over) 23 | draw.Draw(outim, db.Bounds(), db, image.Point{X: 0, Y: 0}, draw.Over) 24 | 25 | return outim, nil 26 | } 27 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Binaries for programs and plugins 17 | *.exe 18 | *.exe~ 19 | *.dll 20 | *.so 21 | *.dylib 22 | # Test binary, build with `go test -c` 23 | *.test 24 | # Output of the go coverage tool, specifically when used with LiteIDE 25 | *.out -------------------------------------------------------------------------------- /templates/frontbase.html: -------------------------------------------------------------------------------- 1 | {{define "layout"}} 2 | 3 | 4 | 5 | 6 | {{.PageData.Title}} 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |

mcassoc

17 |
18 | 21 |
22 |
23 | {{template "content" .Data}} 24 | {{template "scripts"}} 25 | 26 | 27 | {{end}} -------------------------------------------------------------------------------- /minecraft/profiles.go: -------------------------------------------------------------------------------- 1 | package minecraft 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | ) 7 | 8 | type ProfileTextureData struct { 9 | Url string 10 | } 11 | 12 | type ProfilePropertyTextures struct { 13 | Timestamp uint64 14 | ProfileId string 15 | ProfileName string 16 | IsPublic bool 17 | Textures map[string]ProfileTextureData 18 | } 19 | 20 | type ProfileProperty struct { 21 | Name string 22 | Value string 23 | Signature string 24 | } 25 | 26 | type Profile struct { 27 | Id string 28 | Name string 29 | Properties []ProfileProperty 30 | } 31 | 32 | func (pc Profile) Textures() (*ProfilePropertyTextures, error) { 33 | for _, pp := range pc.Properties { 34 | if pp.Name == "textures" { 35 | decValue, err := base64.StdEncoding.DecodeString(pp.Value) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | var pps ProfilePropertyTextures 41 | err = json.Unmarshal(decValue, &pps) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return &pps, nil 47 | } 48 | } 49 | 50 | return nil, ERR_HAS_NO_SKIN 51 | } 52 | -------------------------------------------------------------------------------- /minecraft/fallbackskin.go: -------------------------------------------------------------------------------- 1 | package minecraft 2 | 3 | import ( 4 | "image" 5 | _ "image/png" 6 | "net/http" 7 | ) 8 | 9 | const ( 10 | alexURL = "http://assets.mojang.com/SkinTemplates/alex.png" 11 | steveURL = "http://assets.mojang.com/SkinTemplates/steve.png" 12 | ) 13 | 14 | func isEven(c byte) bool { 15 | switch { 16 | case c >= '0' && c <= '9': 17 | return (c & 1) == 0 18 | case c >= 'a' && c <= 'f': 19 | return (c & 1) == 1 20 | default: 21 | panic("Invalid digit " + string(c)) 22 | } 23 | } 24 | 25 | func isAlex(pc Profile) bool { 26 | uuid := pc.Id 27 | return (isEven(uuid[7]) != isEven(uuid[16+7])) != (isEven(uuid[15]) != isEven(uuid[16+15])) 28 | } 29 | 30 | func downloadSkin(c *http.Client, url string) (image.Image, error) { 31 | resp, err := c.Get(url) 32 | if err != nil { 33 | return nil, err 34 | } 35 | defer resp.Body.Close() 36 | 37 | img, _, err := image.Decode(resp.Body) 38 | return img, err 39 | } 40 | 41 | func fallbackSkin(c *http.Client, pc Profile) (image.Image, error) { 42 | if isAlex(pc) { 43 | return downloadSkin(c, alexURL) 44 | } else { 45 | return downloadSkin(c, steveURL) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /util/mcassoc.go: -------------------------------------------------------------------------------- 1 | //+build skip 2 | 3 | package main 4 | 5 | import ( 6 | mcassoc "github.com/lukegb/mcassoc/mcassoc" 7 | minecraft "github.com/lukegb/mcassoc/minecraft" 8 | mojang "github.com/lukegb/mcassoc/mojang" 9 | "image/png" 10 | "log" 11 | "os" 12 | ) 13 | 14 | func main() { 15 | if len(os.Args) != 4 { 16 | log.Fatalln(os.Args[0], " ") 17 | } 18 | 19 | a := mcassoc.NewAssocifier(os.Args[1]) 20 | 21 | profile, err := mojang.GetProfileByUsername(os.Args[2]) 22 | if err != nil { 23 | log.Fatal("GetProfileByUsername", err) 24 | } 25 | log.Println("profile {name:", profile.Name, ", id:", profile.Id, "}") 26 | 27 | mcprofile, err := minecraft.GetProfile(profile.Id) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | log.Println(mcprofile) 32 | log.Println(mcprofile.Textures()) 33 | 34 | img, err := minecraft.GetSkin(mcprofile) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | emimg, err := a.Embed(profile.Id, img) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | file, err := os.Create(os.Args[3]) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | defer file.Close() 49 | if err = png.Encode(file, emimg); err != nil { 50 | log.Fatal(err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Luke Granger-Brown and Google Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lukegb/mcassoc 2 | 3 | go 1.19 4 | 5 | require ( 6 | cloud.google.com/go v0.109.0 7 | cloud.google.com/go/storage v1.29.0 8 | github.com/gorilla/mux v1.8.0 9 | github.com/miekg/dns v1.1.50 10 | github.com/stathat/go v1.0.0 11 | google.golang.org/appengine/v2 v2.0.2 12 | ) 13 | 14 | require ( 15 | cloud.google.com/go/compute v1.15.1 // indirect 16 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 17 | cloud.google.com/go/iam v0.10.0 // indirect 18 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 19 | github.com/golang/protobuf v1.5.2 // indirect 20 | github.com/google/go-cmp v0.5.9 // indirect 21 | github.com/google/uuid v1.3.0 // indirect 22 | github.com/googleapis/enterprise-certificate-proxy v0.2.1 // indirect 23 | github.com/googleapis/gax-go/v2 v2.7.0 // indirect 24 | go.opencensus.io v0.24.0 // indirect 25 | golang.org/x/crypto v0.5.0 // indirect 26 | golang.org/x/mod v0.7.0 // indirect 27 | golang.org/x/net v0.5.0 // indirect 28 | golang.org/x/oauth2 v0.4.0 // indirect 29 | golang.org/x/sys v0.4.0 // indirect 30 | golang.org/x/text v0.6.0 // indirect 31 | golang.org/x/tools v0.5.0 // indirect 32 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 33 | google.golang.org/api v0.108.0 // indirect 34 | google.golang.org/appengine v1.6.7 // indirect 35 | google.golang.org/genproto v0.0.0-20230119192704-9d59e20e5cd1 // indirect 36 | google.golang.org/grpc v1.52.0 // indirect 37 | google.golang.org/protobuf v1.28.1 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /dns.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | 9 | "github.com/miekg/dns" 10 | ) 11 | 12 | const resolver = "8.8.8.8:53" 13 | 14 | func lookupIP(ctx context.Context, host string) (addrs []net.IP, err error) { 15 | return net.LookupIP(host) 16 | } 17 | 18 | func lookupTXT(ctx context.Context, host string) ([]string, error) { 19 | msg := &dns.Msg{ 20 | MsgHdr: dns.MsgHdr{ 21 | RecursionDesired: true, 22 | CheckingDisabled: false, 23 | }, 24 | Question: []dns.Question{{ 25 | Name: dns.Fqdn(host), 26 | Qtype: dns.TypeTXT, 27 | Qclass: uint16(dns.ClassINET), 28 | }}, 29 | } 30 | msg.Id = dns.Id() 31 | 32 | conn, err := net.Dial("udp", resolver) 33 | if err != nil { 34 | return nil, fmt.Errorf("socket.Dial(%v, %v): %v", "udp", resolver, err) 35 | } 36 | defer conn.Close() 37 | 38 | dnsc := &dns.Conn{Conn: conn} 39 | defer dnsc.Close() 40 | log.Printf("Making DNS query: %v", msg) 41 | if err := dnsc.WriteMsg(msg); err != nil { 42 | return nil, fmt.Errorf("dnsc.WriteMsg(%v): %v", msg, err) 43 | } 44 | rmsg, err := dnsc.ReadMsg() 45 | if err != nil { 46 | return nil, fmt.Errorf("dnsc.ReadMsg(): %v", err) 47 | } 48 | log.Printf("Got DNS response: %v", rmsg) 49 | 50 | var answers []string 51 | for _, m := range rmsg.Answer { 52 | m, ok := m.(*dns.TXT) 53 | if !ok { 54 | continue 55 | } 56 | 57 | if m.Hdr.Name != msg.Question[0].Name { 58 | continue 59 | } 60 | answers = append(answers, m.Txt...) 61 | } 62 | log.Printf("Extracted TXTs: %v", answers) 63 | return answers, nil 64 | } 65 | -------------------------------------------------------------------------------- /statkeeper/statkeeper.go: -------------------------------------------------------------------------------- 1 | package statkeeper 2 | 3 | import ( 4 | "github.com/stathat/go" 5 | ) 6 | 7 | type StatKeeper interface { 8 | NewAssocAttempt() 9 | AssocComplete() 10 | AssocFail() 11 | MojangRequestOK() 12 | MojangRequestFail() 13 | McRequestOK() 14 | McRequestFail() 15 | } 16 | 17 | type VoidStatKeeper struct { 18 | } 19 | 20 | func (VoidStatKeeper) NewAssocAttempt() {} 21 | func (VoidStatKeeper) AssocComplete() {} 22 | func (VoidStatKeeper) AssocFail() {} 23 | func (VoidStatKeeper) MojangRequestOK() {} 24 | func (VoidStatKeeper) MojangRequestFail() {} 25 | func (VoidStatKeeper) McRequestOK() {} 26 | func (VoidStatKeeper) McRequestFail() {} 27 | 28 | type StatHatStatKeeper struct { 29 | ezKey string 30 | } 31 | 32 | func NewStatHatStatKeeper(ezKey string) *StatHatStatKeeper { 33 | return &StatHatStatKeeper{ 34 | ezKey: ezKey, 35 | } 36 | } 37 | 38 | func (sk *StatHatStatKeeper) count(name string, count int) { 39 | stathat.PostEZCount(name, sk.ezKey, count) 40 | } 41 | 42 | func (sk *StatHatStatKeeper) NewAssocAttempt() { 43 | sk.count("assoc start", 1) 44 | } 45 | 46 | func (sk *StatHatStatKeeper) AssocComplete() { 47 | sk.count("assoc complete", 1) 48 | } 49 | 50 | func (sk *StatHatStatKeeper) AssocFail() { 51 | sk.count("assoc fail", 1) 52 | } 53 | 54 | func (sk *StatHatStatKeeper) MojangRequestOK() { 55 | sk.count("mojang request ok", 1) 56 | } 57 | 58 | func (sk *StatHatStatKeeper) MojangRequestFail() { 59 | sk.count("mojang request fail", 1) 60 | } 61 | 62 | func (sk *StatHatStatKeeper) McRequestOK() { 63 | sk.count("minecraft request ok", 1) 64 | } 65 | 66 | func (sk *StatHatStatKeeper) McRequestFail() { 67 | sk.count("minecraft request fail", 1) 68 | } 69 | 70 | var GLOBAL StatKeeper = VoidStatKeeper{} 71 | -------------------------------------------------------------------------------- /minecraft/sessionserver.go: -------------------------------------------------------------------------------- 1 | package minecraft 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | const ( 14 | SESSION_SERVER = "https://sessionserver.mojang.com" 15 | PROFILE_URL_FMT = "%s/session/minecraft/profile/%s" 16 | ) 17 | 18 | type ProfileCacheEntry struct { 19 | profile Profile 20 | expiry time.Time 21 | } 22 | 23 | type ProfileCache map[string]ProfileCacheEntry 24 | 25 | type ProfileClient struct { 26 | x ProfileCache 27 | } 28 | 29 | func (pc *ProfileClient) GetProfile(c *http.Client, uuid string) (Profile, error) { 30 | var err error 31 | 32 | log.Println("Requesting profile for", uuid) 33 | 34 | var resp *http.Response 35 | if resp, err = c.Get(fmt.Sprintf(PROFILE_URL_FMT, SESSION_SERVER, uuid)); err != nil { 36 | return Profile{}, err 37 | } 38 | defer resp.Body.Close() 39 | 40 | if resp.StatusCode == 204 || resp.StatusCode == 429 { 41 | if cacheEntry, ok := pc.x[uuid]; ok { 42 | return cacheEntry.profile, nil 43 | } else { 44 | return Profile{}, errors.New("mojang returned 204 when I have nothing cached!") 45 | } 46 | } 47 | 48 | var respBytes []byte 49 | respBytes, err = ioutil.ReadAll(resp.Body) 50 | 51 | log.Println("Got", string(respBytes), "with response code", resp.Status, "and headers", resp.Header) 52 | 53 | var profile Profile 54 | if err = json.Unmarshal(respBytes, &profile); err != nil { 55 | return Profile{}, err 56 | } 57 | 58 | // cache it 59 | cacheEntry := ProfileCacheEntry{ 60 | profile: profile, 61 | expiry: time.Now().Add(1 * time.Hour), 62 | } 63 | pc.x[uuid] = cacheEntry 64 | 65 | return profile, nil 66 | } 67 | 68 | func NewProfileClient() *ProfileClient { 69 | return &ProfileClient{ 70 | x: make(ProfileCache), 71 | } 72 | } 73 | 74 | func GetProfile(c *http.Client, uuid string) (Profile, error) { 75 | return NewProfileClient().GetProfile(c, uuid) 76 | } 77 | -------------------------------------------------------------------------------- /mojang/httpprofiles.go: -------------------------------------------------------------------------------- 1 | package mojang 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | ) 11 | 12 | const ( 13 | PROFILES_PER_REQUEST = 100 14 | MOJANG_SERVER = "https://api.mojang.com" 15 | PROFILE_URL_FMT = "%s/profiles/minecraft" 16 | LOG_TAG = "[HttpProfileRepository]" 17 | ) 18 | 19 | type HttpProfileRepository struct { 20 | c *http.Client 21 | } 22 | 23 | func (hpr HttpProfileRepository) GetProfilesByUsername(usernames []string) (profiles []Profile, err error) { 24 | log.Println(LOG_TAG, "fetching profiles by usernames:", usernames) 25 | 26 | for startAt := 0; startAt < len(usernames); startAt += PROFILES_PER_REQUEST { 27 | endAt := startAt + PROFILES_PER_REQUEST 28 | if endAt > len(usernames) { 29 | endAt = len(usernames) 30 | } 31 | 32 | var jsonCriteria []byte 33 | if jsonCriteria, err = json.Marshal(usernames[startAt:endAt]); err != nil { 34 | return nil, err 35 | } 36 | 37 | if res, err := hpr.getProfilesByUsernamePage(jsonCriteria); err != nil { 38 | return profiles, err 39 | } else { 40 | profiles = append(profiles, res...) 41 | } 42 | } 43 | 44 | return 45 | } 46 | 47 | func (hpr HttpProfileRepository) getProfilesByUsernamePage(jsonCriteria []byte) ([]Profile, error) { 48 | var resp *http.Response 49 | var err error 50 | 51 | r := bytes.NewReader(jsonCriteria) 52 | targetUrl := fmt.Sprintf(PROFILE_URL_FMT, MOJANG_SERVER) 53 | if resp, err = hpr.c.Post(targetUrl, "application/json", r); err != nil { 54 | return []Profile{}, err 55 | } 56 | defer resp.Body.Close() 57 | 58 | var retBytes []byte 59 | retBytes, err = ioutil.ReadAll(resp.Body) 60 | 61 | log.Println("Got", string(retBytes)) 62 | 63 | result := make([]Profile, 0) 64 | if err = json.Unmarshal(retBytes, &result); err != nil { 65 | return []Profile{}, err 66 | } 67 | 68 | return result, nil 69 | } 70 | 71 | func NewHttpProfileRepository(c *http.Client) HttpProfileRepository { 72 | return HttpProfileRepository{ 73 | c: c, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /templates/static/locales/ru/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "localiser": { 3 | "credit": "Sirse (sirse.ru)" 4 | }, 5 | "please_wait": "Пожалуйста, подождите...", 6 | "step1": { 7 | "header": [ 8 | "Здравствуйте! Данный сайт запрашивает привязку вашего Minecraft акаунта
", 9 | "Для привязки вам необходимо ответить на несколько вопросов" 10 | ], 11 | "error": "Ошибка: Аккаунт Minecraft с таким именем пользователя не найден", 12 | "textbox": "Введите ваше имя пользователя в Minecraft?", 13 | "button": "Далее" 14 | }, 15 | "step2_register": { 16 | "header": [ 17 | "Требуется регистрация в MCAssoc
", 18 | "Пожалуйста, введите пароль, не совпадающий с паролем от Mojang аккаунта и этого сайта!.
", 19 | "Он потребуется вам, чтобы в дальнейшем привязать дополнительные аккаунты" 20 | ], 21 | "error": "Ошибка", 22 | "textbox": "Введите уникальный пароль для MCAssoc:", 23 | "button": "Далее" 24 | }, 25 | "step2_login": { 26 | "header": [ 27 | "Добро пожаловать! Вы уже выполняли привязку через MCAssoc
", 28 | "Для продолжения введите ваш пароль от Minecraft Associator" 29 | ], 30 | "error": "Ошибка: Неверный пароль. Забыли пароль или не выполняли привязку ранее?", 31 | "textbox": "Введите ваш пароль от MCAssoc:", 32 | "button": "Далее", 33 | "forgotten_link": "Забыл пароль/Не выполнял привязку ранее" 34 | }, 35 | "step3": { 36 | "header": [ 37 | "Последний шаг, вы почти закончили!
", 38 | "Для подтверждения аккаунта нам необходимо сменить ваш Minecraft скин
", 39 | "Просто нажмите на кнопку ниже, вы будете перемещены на Minecraft.net - авторизуйтесь и подтвердите смену скина
", 40 | "
", 41 | "После привязки вы можете сменить скин на свой, однако, при повторной привязке потребуется пройти все шаги заново." 42 | ], 43 | "button": "Сменить Minecraft скин", 44 | "text": "После смены скина просто закройте окно и привязка продолжится. Проверка скина может занять несколько секунд" 45 | }, 46 | "fatal_error": { 47 | "header": "Произошла ошибка...", 48 | "text": "Произошла ошибка, пожалуйста, попробуйте позднее." 49 | }, 50 | "branding": { 51 | "header": "Minecraft Account Association", 52 | "footer": "Предоставлено mcassoc.lukegb.com" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /templates/static/locales/it/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "localiser": { 3 | "credit": "xion87" 4 | }, 5 | "please_wait": "Solo un momento...", 6 | "step1": { 7 | "header": [ 8 | "Ciao! Stai per associare un account di Minecraft nel sito.
", 9 | "Per procedere, devi rispondere ad alcune domande." 10 | ], 11 | "error": "Whoops! Non riesco a trovare nessun account con quel nome. Mi dispiace.", 12 | "textbox": "Quale è il tuo nome del tuo account di Minecraft?", 13 | "button": "Avanti" 14 | }, 15 | "step2_register": { 16 | "header": [ 17 | "Vedo che non lo hai mai fatto.
", 18 | "Digita una password provvisoria che servirà per riassociare l'account in futuro.
", 19 | "Sarai in grado di usarla senza fare la verifica." 20 | ], 21 | "error": "Ehm, cosa?", 22 | "textbox": "Digita una password:", 23 | "button": "Avanti" 24 | }, 25 | "step2_login": { 26 | "header": [ 27 | "Bentornato! Vedo che hai già eseguito questo passaggio.
", 28 | "Per continuare, digita la tua password della prima associazione." 29 | ], 30 | "error": "Ops! Non è giusta. Riprova, se non la ricordi, clicca qui per resettare la password.", 31 | "textbox": "Digita la password per la associazione:", 32 | "button": "Avanti", 33 | "forgotten_link": "Mi sono dimenticato la password/Non l'ho mai avuta" 34 | }, 35 | "step3": { 36 | "header": [ 37 | "Ultimo passaggio! Hai quasi finito.
", 38 | "Per verificare il tuo account, devi cambiare la tua skin.
", 39 | "Clicca questo pulsante per collegarti a Minecraft.net, effettua il login, e accetta il cambiamento skin.
", 40 | "
", 41 | "La prossima volta che riassocerai il tuo account con il sito, ti sarà richiesta solo la password della associazione, e sarà fatta!" 42 | ], 43 | "button": "Cambia la Skin Minecraft", 44 | "text": "Una volta fatto il cambio skin, attendi lasciando aperta questa finestra. Richiederà solo qualche secondo una volta accettata la skin!" 45 | }, 46 | "fatal_error": { 47 | "header": "Mi dispiace...", 48 | "text": "C'è stato un problema. Riprova." 49 | }, 50 | "branding": { 51 | "header": "Verifica Account Premium Minecraft", 52 | "footer": "MCAssoc: un servizio aperto. Contattami su IRC (lukegb@Esper) o via e-mail." 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /templates/static/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "localiser": { 3 | "credit": "Luke Granger-Brown" 4 | }, 5 | "please_wait": "Just a moment...", 6 | "step1": { 7 | "header": [ 8 | "Hello! The site you're on wishes to confirm your Minecraft account.
", 9 | "In order to do this, I need to ask you a few questions." 10 | ], 11 | "error": "Whoops! I couldn't find a Minecraft user with that username. Sorry about that.", 12 | "textbox": "What's your Minecraft username?", 13 | "button": "Next" 14 | }, 15 | "step2_register": { 16 | "header": [ 17 | "I see you haven't done this before.
", 18 | "Please pick a password that is not the same as the one used for your Mojang account or the site from which you came.
", 19 | "You'll be able to use this if you need to complete this process again." 20 | ], 21 | "error": "Erm, what?", 22 | "textbox": "Pick a Minecraft Associator password:", 23 | "button": "Next" 24 | }, 25 | "step2_login": { 26 | "header": [ 27 | "Welcome back! I see you've done this before, either here or on a different site.
", 28 | "To continue, simply enter your Minecraft Associator password." 29 | ], 30 | "error": "Whoops! I don't think that was correct. If you can't remember what it was, or you haven't associated your account before, please reset your password.", 31 | "textbox": "Enter your Minecraft Associator password:", 32 | "button": "Next", 33 | "forgotten_link": "I've forgotten it/I haven't done this before" 34 | }, 35 | "step3": { 36 | "header": [ 37 | "Last step! You're almost done.
", 38 | "To verify your Minecraft account, I need you to change your skin.
", 39 | "Just click the button below and you'll be taken to Minecraft.net - just log in, and accept the skin change.
", 40 | "
", 41 | "If you keep your skin the same, then the next time you try to associate your account with a website, I'll just ask you for the password you just set, and away we go!" 42 | ], 43 | "button": "Change my Minecraft skin", 44 | "text": "Once you're done, just close the window and I'll complete the process. It may take a few seconds once you've accepted the skin change, but I'll try to be as quick as I can!" 45 | }, 46 | "fatal_error": { 47 | "header": "Sorry about this...", 48 | "text": "An error we couldn't recover from occurred. You might want to try again later." 49 | }, 50 | "branding": { 51 | "header": "Minecraft Account Association", 52 | "footer": "MCAssoc: an open service. Want in? Talk to me on IRC (lukegb@Esper) or by email." 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /templates/static/locales/es/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "localiser": { 3 | "credit": "Todo..." 4 | }, 5 | "please_wait": "Un momento...", 6 | "step1": { 7 | "header": [ 8 | "¡Hola! El sitio en el que estás quiere confirmar tu cuenta de Minecraft.
", 9 | "Para esto, te vamos a realizar unas preguntas." 10 | ], 11 | "error": "¡oops! No se ha encontrado ninguna cuenta de Minecraft con el nombre introducido.", 12 | "textbox": "¿Cuál es tu nombre de usuario de Minecraft?", 13 | "button": "Siguiente" 14 | }, 15 | "step2_register": { 16 | "header": [ 17 | "Parece que no has realizado este proceso nunca.
", 18 | "Por favor, selecciona una contraseña que no sea igual a la que utilizas para tu cuenta de Mojang o para este sitio.
", 19 | "Podrás utilizar esta contraseña si necesitas iniciar el proceso de nuevo, o en otro sitio." 20 | ], 21 | "error": "Erm, what?", 22 | "textbox": "Elije una contraseña para la vinculación de cuentas:", 23 | "button": "Siguiente" 24 | }, 25 | "step2_login": { 26 | "header": [ 27 | "¡Bienvenido de nuevo! Parece que has realizado ya este proceso anteriormente, o en otro sitio.
", 28 | "Para continuar, simplemente introduce la contraseña de vinculación." 29 | ], 30 | "error": "*¡oops!* Los datos introducidos no parecen correctos. Si no recuerdas los datos, o no has realizado este proceso nunca pincha en Recordar datos.", 31 | "textbox": "Introduce tu contraseña de vinculación:", 32 | "button": "Siguiente", 33 | "forgotten_link": "Recordar datos/No he realizado esto nunca" 34 | }, 35 | "step3": { 36 | "header": [ 37 | "¡Último paso! Ya casi lo has hecho.
", 38 | "Para verificar tu cuenta de Minecraft, necesitamos que cambies tu skin.
", 39 | "Solo haz click en el botón abajo y serás enviado a Minecraft.net - Inicia sesión, y acepta el cambio de skin.
", 40 | "
", 41 | "Si mantienes la misma skin, entonces la próxima vez podrás intentar asociar tu cuenta con una web, solo te pediremos la contraseña que acabas de elegir, y ¡listo!" 42 | ], 43 | "button": "Cambiar mi skin de Minecraft", 44 | "text": "Cuando hayas terminado, cierra la ventana y completaremos el proceso. Este proceso puede durar unos segundos hasta que aceptes el cambio de skin, ¡pero intentaremos ser lo más rápidos que podamos!" 45 | }, 46 | "fatal_error": { 47 | "header": "Lo sentimos...", 48 | "text": "Ha ocurrido un error. Puedes intentarlo más tarde." 49 | }, 50 | "branding": { 51 | "header": "Vinculación de cuenta de Minecraft", 52 | "footer": "MCAssoc: un servicio abierto. ¿Lo quieres? Háblame en IRC (lukegb@Esper) o por email." 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /mcassoc/datablock.go: -------------------------------------------------------------------------------- 1 | package mcassoc 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha512" 6 | "image" 7 | "image/color" 8 | ) 9 | 10 | const ( 11 | DATABLOCK_WIDTH = 8 12 | DATABLOCK_HEIGHT = 8 13 | ) 14 | 15 | var ( 16 | PRESENCE_PATTERN = []color.NRGBA{ 17 | {0, 0, 0, 255}, 18 | {30, 0, 0, 255}, 19 | {0, 30, 0, 255}, 20 | {0, 0, 30, 255}, 21 | {30, 30, 0, 255}, 22 | {0, 30, 30, 255}, 23 | {30, 0, 30, 255}, 24 | {30, 30, 30, 255}, 25 | } 26 | ) 27 | 28 | // This implementation could, for instance, be changed to actually mess around with PNGs 29 | // instead of storing the data as pixels in the image :P 30 | 31 | func GenerateDatablock(data string, key string) (image.Image, error) { 32 | im := image.NewNRGBA(image.Rect(0, 0, DATABLOCK_WIDTH, DATABLOCK_HEIGHT)) 33 | 34 | hash := hmac.New(sha512.New, []byte(key)) 35 | hash.Write([]byte(data)) 36 | result := hash.Sum([]byte{}) 37 | 38 | for x := 0; x < DATABLOCK_WIDTH; x++ { 39 | for y := 1; y < DATABLOCK_HEIGHT; y++ { 40 | databytePos := (3 * (x + DATABLOCK_WIDTH*y)) % len(result) 41 | 42 | im.Set(x, y, color.NRGBA{ 43 | R: result[databytePos%len(result)], 44 | G: result[(databytePos+1)%len(result)], 45 | B: result[(databytePos+2)%len(result)], 46 | A: 255, 47 | }) 48 | } 49 | } 50 | 51 | // add the presence marker 52 | xw := DATABLOCK_WIDTH 53 | if len(PRESENCE_PATTERN) < xw { 54 | xw = len(PRESENCE_PATTERN) 55 | } 56 | for x := 0; x < xw; x++ { 57 | im.Set(x, 0, PRESENCE_PATTERN[x]) 58 | } 59 | 60 | return im, nil 61 | } 62 | 63 | func HasDatablock(theirs image.Image) bool { 64 | tb := theirs.Bounds() 65 | 66 | cm := color.NRGBAModel 67 | 68 | // add the presence marker 69 | xw := DATABLOCK_WIDTH 70 | if len(PRESENCE_PATTERN) < xw { 71 | xw = len(PRESENCE_PATTERN) 72 | } 73 | for x := 0; x < xw; x++ { 74 | if cm.Convert(theirs.At(tb.Min.X+x, 0)) != PRESENCE_PATTERN[x] { 75 | return false 76 | } 77 | } 78 | return true 79 | } 80 | 81 | func CompareDatablocks(ours image.Image, theirs image.Image) bool { 82 | ob := ours.Bounds() 83 | tb := theirs.Bounds() 84 | 85 | cm := ours.ColorModel() 86 | 87 | // fail fast - this is OK since the attacker already knows the size of the secret 88 | if ob.Dx() < DATABLOCK_WIDTH || ob.Dy() < DATABLOCK_HEIGHT || tb.Dx() < DATABLOCK_WIDTH || tb.Dy() < DATABLOCK_HEIGHT { 89 | return false 90 | } 91 | 92 | isSame := true 93 | for x := 0; x < DATABLOCK_WIDTH; x++ { 94 | ox := ob.Min.X + x 95 | tx := tb.Min.X + x 96 | 97 | for y := 0; y < DATABLOCK_HEIGHT; y++ { 98 | oy := ob.Min.Y + y 99 | ty := tb.Min.Y + y 100 | 101 | oc := ours.At(ox, oy) 102 | tc := cm.Convert(theirs.At(tx, ty)) 103 | 104 | if oc != tc { 105 | isSame = false 106 | } 107 | } 108 | } 109 | 110 | return isSame 111 | } 112 | -------------------------------------------------------------------------------- /templates/signup.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 |
4 |

What is it?

5 |

mcassoc is a free service that allows you to add Minecraft account association to your site or forums. By 6 | using skins to verify association, users don't need to provide account details to 3rd-party sites and once 7 | they've signed up once they simply need to enter their separate mcassoc credentials if they wish to associate 8 | on a future site.

9 |
10 | 11 |
12 |

Client libraries

13 |

Sample code for interfacing with the mcassoc service is available in the following languages:

14 | 18 |
19 |
20 |
21 |

Features

22 |
    23 |
  • Skin-based account verification
  • 24 |
  • Provides the account UUID you can use for storing associations
  • 25 |
  • Customizable text and background colours
  • 26 |
  • Easy to use with provided libraries
  • 27 |
28 |

Screenshots

29 |
    30 |
  • 31 |
    32 | 33 |
  • 34 |
  • 35 | 36 |
    37 | 38 |
    39 |
  • 40 |
41 |
42 |
43 |

Sign up

44 | {{if .HasError}} 45 |

Error: Please provide a valid domain. The domain must resolve to a valid IP address.

46 |
47 |

To get your shared key, fill in the form below. You'll need to prove you own the domain in question.

48 | 49 |
50 | 51 |
52 | {{else}} 53 |

To get your shared key, fill in the form below. You'll need to prove you own the domain in question.

54 | 55 |
56 | 57 |
58 | {{end}} 59 | 60 | 61 |
62 |
63 | {{end}} 64 | 65 | {{define "scripts"}} 66 | 67 | 68 | 73 | {{end}} 74 | -------------------------------------------------------------------------------- /templates/verification.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 | {{if .IsTestDomain}} 4 |
5 |

Verification result

6 |

Because this is a test domain in a reserved TLD, you do not need to complete verification. 7 |

Test domain key

8 |

Your shared key is {{.TestKey}}

9 |
10 | {{else}} 11 |
12 |

Verify domain ownership with HTTP request

13 |

This is the simplest verification method. In order to verify domain ownership, please create a file at the following URL:

14 | 15 |

The file should contain the following contents. Please don't include any other characters in the file.

16 | 17 |

Please make sure that if you're using CloudFlare or another CDN service that the HTTP request isn't blocked. Ensure that "I'm under attack" mode is disabled.

18 |

19 |
20 |
21 |

Verify domain ownership with TXT record

22 |

If you prefer, you can verify domain ownership by creating a TXT record. The TXT record should be created on the following domain:

23 | 24 |

The TXT record should contain the following information:

25 | 26 |

Please don't add extra characters to the contents of the TXT record.

27 |

28 |
29 |
30 |

Verification result

31 |

The result of the verification and, if successful, the shared key will be displayed below once you have chosen a verification method. 32 |

Congrats, you've successfully verified your domain. You can now use the shared key below in your applications.
33 |

34 |
35 | {{end}} 36 |
37 | {{end}} 38 | 39 | {{define "scripts"}} 40 | 41 | 70 | {{end}} 71 | -------------------------------------------------------------------------------- /templates/minibase.html: -------------------------------------------------------------------------------- 1 | {{define "layout"}} 2 | 3 | 4 | 5 | {{.PageData.Title}} 6 | 7 | 8 | 9 | 118 | 119 | 120 |
121 |
122 |

Minecraft Account Association

123 |
124 |
125 | {{template "content" .Data}} 126 |
127 | 130 |
131 | {{template "js" .Data}} 132 | 133 | 134 | {{end}} 135 | -------------------------------------------------------------------------------- /templates/css/frontend.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,300,400,600,700,800); 2 | @import url(//fonts.googleapis.com/css?family=Source+Sans+Pro:300,400); 3 | 4 | /* Reset */ 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, embed, 15 | figure, figcaption, footer, header, hgroup, 16 | menu, nav, output, ruby, section, summary, 17 | time, mark, audio, video { 18 | margin: 0; 19 | padding: 0; 20 | border: 0; 21 | font-size: 100%; 22 | font: inherit; 23 | vertical-align: baseline; 24 | } 25 | /* HTML5 display-role reset for older browsers */ 26 | article, aside, details, figcaption, figure, 27 | footer, header, hgroup, menu, nav, section { 28 | display: block; 29 | } 30 | body { 31 | line-height: 1; 32 | } 33 | ol, ul { 34 | list-style: inside; 35 | margin: 5px 0; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | *, *:before, *:after { 51 | -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; 52 | } 53 | 54 | 55 | body { 56 | font-family: 'Source Sans Pro', sans-serif; 57 | font-size: 100%; 58 | color: #333; 59 | } 60 | 61 | section { 62 | margin-top: 20px; 63 | } 64 | 65 | header { 66 | background-color: #2c3e50; 67 | width: 100%; 68 | padding: 20px; 69 | } 70 | 71 | header h1 { 72 | font-weight: 300; 73 | font-size: 2.5rem; 74 | } 75 | 76 | header h1 a { 77 | color: #ffffff; 78 | text-decoration: none; 79 | } 80 | 81 | h2 { 82 | font-size: 1.5rem; 83 | color: #000; 84 | font-weight: 400; 85 | } 86 | 87 | p { 88 | font-size: 15px; 89 | line-height: 1.4em; 90 | font-family: "Open Sans"; 91 | word-wrap: break-word; 92 | } 93 | 94 | li { 95 | line-height: 1.4em; 96 | } 97 | 98 | #title { 99 | display: inline; 100 | float: left; 101 | } 102 | 103 | #links { 104 | float: right; 105 | display: inline; 106 | } 107 | 108 | #links h1 { 109 | text-align: right; 110 | } 111 | 112 | #links a { 113 | color: #fff; 114 | } 115 | 116 | .clear { 117 | clear: both; 118 | } 119 | 120 | section h2 { 121 | margin-bottom: 5px; 122 | } 123 | 124 | .slider { 125 | position: relative; 126 | list-style: none; 127 | overflow: hidden; 128 | width: 100%; 129 | padding: 0; 130 | margin: 0; 131 | max-width: 600px; 132 | } 133 | 134 | .slider li { 135 | -webkit-backface-visibility: hidden; 136 | position: absolute; 137 | display: none; 138 | width: 100%; 139 | left: 0; 140 | top: 0; 141 | } 142 | 143 | .slider li:first-child { 144 | position: relative; 145 | display: block; 146 | float: left; 147 | } 148 | 149 | .slider img { 150 | display: block; 151 | height: auto; 152 | float: left; 153 | width: 100%; 154 | border: 0; 155 | } 156 | 157 | .slider-content { 158 | background-color: rgba(0,0,0,0.75); 159 | position: absolute; 160 | color: #fff; 161 | padding: 20px; 162 | font-family: helvetica, arial, sans-serif; 163 | font-weight: 400; 164 | bottom: 0; 165 | width: 100%; 166 | } 167 | 168 | .slider-content label { 169 | font-size: 1em; 170 | line-height: 1em; 171 | } 172 | 173 | .slider-content span { 174 | font-size: .5em; 175 | line-height: .65em; 176 | } 177 | 178 | input[type="text"] { 179 | border-radius: 2px; 180 | padding: 8px 6px; 181 | border: 1px solid #C6C6C6; 182 | resize: none; 183 | background: none repeat scroll 0% 0% #FFF; 184 | vertical-align: middle; 185 | width: 100%; 186 | } 187 | 188 | input[type="text"].error { 189 | border: 1px solid #c60000 !important; 190 | } 191 | 192 | input[type="text"]:focus { 193 | border-color: rgba(153, 153, 153, 0.9); 194 | box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2) inset; 195 | outline: 0px none; 196 | } 197 | 198 | input[readonly] { 199 | margin-top: 0.5em; 200 | margin-bottom: 1em; 201 | } 202 | 203 | button { 204 | cursor: pointer; 205 | background-color: #2c3e50; 206 | color: #fff; 207 | border: 0; 208 | padding: 10px; 209 | margin: 5px 0; 210 | } 211 | 212 | button:hover { 213 | background-color: #46586A; 214 | } 215 | 216 | form { 217 | margin-top: 5px; 218 | } 219 | 220 | #screenshots { 221 | margin-top: 25px; 222 | } 223 | 224 | hr { 225 | border: 0; 226 | background-color: #aaa; 227 | height: 1px; 228 | } 229 | .alert { 230 | background-color: #F2DEDE; 231 | color: #963B3B; 232 | border: 1px solid #B84C4C; 233 | padding: 1em; 234 | border-radius: 3px; 235 | margin-top: 1em; 236 | margin-bottom: 1em; 237 | } 238 | .alert strong { 239 | margin-bottom: 0.5em; 240 | display: block; 241 | } 242 | .result { 243 | padding: 1.5em; 244 | border-radius: 3px; 245 | background-color: #eee; 246 | border: 1px solid #ddd; 247 | margin-top: 1em; 248 | } 249 | 250 | .result .header { 251 | font-weight: bold; 252 | font-size: 1.5em; 253 | margin-bottom: 0.5em; 254 | } 255 | 256 | .result .details { 257 | font-family: monospace; 258 | font-size: 0.95em; 259 | } 260 | 261 | section.verification-result { 262 | margin-bottom: 1em; 263 | } -------------------------------------------------------------------------------- /templates/perform.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 |
4 |
5 | Just a moment... 6 |
7 |
8 |
9 |
10 | Hello! The site you're on wishes to confirm your Minecraft account.
11 | In order to do this, I need to ask you a few questions. 12 |
13 |
14 |
Whoops! I couldn't find a Minecraft user with that username. Sorry about that.
15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | I see you haven't done this before.
31 | Please pick a password that is not the same as the one used for your Mojang account or the site from which you came.
32 | You'll be able to use this if you need to complete this process again. 33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | Welcome back! I see you've done this before, either here or on a different site.
51 | To continue, simply enter your Minecraft Associator password. 52 |
53 |
54 |
Whoops! I don't think that was correct. If you can't remember what it was, or you haven't associated your account before, please click I've forgotten it.
55 |
56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 |
64 |
65 |
66 | I've forgotten it/I haven't done this before 67 |
68 |
69 |
70 |
71 | Last step! You're almost done.
72 | To verify your Minecraft account, I need you to change your skin.
73 | Just click the button below and you'll be taken to Minecraft.net - just log in, and accept the skin change.

74 | If you keep your skin the same, then the next time you try to associate your account with a website, I'll just ask you for the password you just set, and away we go! 75 |
76 |
77 | Change my Minecraft skin
78 |
79 | Once you're done, just close the window and I'll complete the process. It may take a few seconds once you've accepted the skin change, but I'll try to be as quick as I can! 80 |
81 |
82 |
83 |
84 | Sorry about this...
85 | An error we couldn't recover from occurred. You might want to try again later. 86 |
87 |
88 |
89 | 90 |
91 | 92 |
93 | {{end}} 94 | {{define "js"}} 95 | 96 | 250 | {{end}} 251 | -------------------------------------------------------------------------------- /mcassoc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/hmac" 6 | "crypto/rand" 7 | "crypto/sha1" 8 | "crypto/sha512" 9 | "crypto/subtle" 10 | "encoding/base64" 11 | "encoding/hex" 12 | "encoding/json" 13 | "fmt" 14 | "html/template" 15 | "image/png" 16 | "io/ioutil" 17 | "log" 18 | "net/http" 19 | "net/url" 20 | "os" 21 | "strings" 22 | "time" 23 | 24 | "google.golang.org/appengine/v2/urlfetch" 25 | 26 | "cloud.google.com/go/storage" 27 | 28 | "github.com/gorilla/mux" 29 | mcassoc "github.com/lukegb/mcassoc/mcassoc" 30 | minecraft "github.com/lukegb/mcassoc/minecraft" 31 | mojang "github.com/lukegb/mcassoc/mojang" 32 | statkeeper "github.com/lukegb/mcassoc/statkeeper" 33 | ) 34 | 35 | var ( 36 | authenticator mcassoc.Associfier 37 | profileClient *minecraft.ProfileClient 38 | ) 39 | 40 | type TemplatePageData struct { 41 | Title string 42 | } 43 | 44 | type TemplateData struct { 45 | PageData TemplatePageData 46 | Data interface{} 47 | } 48 | 49 | type SigningData struct { 50 | Username string `json:"username"` 51 | UUID string `json:"uuid"` 52 | Now int64 `json:"now"` 53 | Key string `json:"key"` 54 | } 55 | 56 | type SkinColourBit struct { 57 | Background string 58 | Text string 59 | Link string 60 | } 61 | 62 | type SkinColour struct { 63 | Border SkinColourBit 64 | Box SkinColourBit 65 | Main SkinColourBit 66 | 67 | Button SkinColourBit 68 | 69 | Branding bool 70 | } 71 | 72 | func generateSharedKey(siteid string) []byte { 73 | z := hmac.New(sha512.New, sesskey) 74 | z.Write([]byte(siteid)) 75 | key := z.Sum([]byte{}) 76 | return key 77 | } 78 | 79 | func generateDomainVerificationKey(domain string, ip string, sessionId string) []byte { 80 | z := hmac.New(sha512.New, dvKey) 81 | t := time.Now() 82 | keyContents := domain + t.Format("20060102") + ip + sessionId 83 | z.Write([]byte(keyContents)) 84 | key := z.Sum([]byte(" ")) // pad it so we never get equals signs (TXT records love those) 85 | return key 86 | } 87 | 88 | func generateDataBlob(data SigningData, siteid string) string { 89 | databytes, _ := json.Marshal(data) 90 | datahash := generateHashOfBlob(databytes, siteid, true) 91 | return base64.StdEncoding.EncodeToString(datahash) 92 | } 93 | func generateHashOfBlob(data []byte, siteid string, doappend bool) []byte { 94 | skey := generateSharedKey(siteid) 95 | x := hmac.New(sha1.New, skey) 96 | x.Write(data) 97 | if doappend { 98 | return x.Sum(data) 99 | } 100 | return x.Sum([]byte{}) 101 | } 102 | 103 | type Gettable interface { 104 | Get(key string) string 105 | } 106 | 107 | func getOr(vs Gettable, what string, def string) string { 108 | val := vs.Get(what) 109 | if val == "" { 110 | return def 111 | } 112 | return val 113 | } 114 | 115 | func isDomainValid(ctx context.Context, domain string) bool { 116 | _, err := lookupIP(ctx, domain) 117 | return err == nil 118 | } 119 | 120 | func getDomainVerificationUrl(domain string, code string) string { 121 | return "http://" + domain + "/mcassoc-" + code + ".txt" 122 | } 123 | 124 | func unwrapSkinColour(vs Gettable) SkinColour { 125 | return SkinColour{ 126 | Border: SkinColourBit{ 127 | Background: getOr(vs, "c:bdr:b", "darkblue"), 128 | Text: getOr(vs, "c:bdr:t", "white"), 129 | Link: getOr(vs, "c:bdr:l", "white"), 130 | }, 131 | Box: SkinColourBit{ 132 | Background: getOr(vs, "c:box:b", "skyblue"), 133 | Text: getOr(vs, "c:box:t", "black"), 134 | Link: getOr(vs, "c:bdr:l", "black"), 135 | }, 136 | Main: SkinColourBit{ 137 | Background: getOr(vs, "c:mn:b", "white"), 138 | Text: getOr(vs, "c:mn:t", "black"), 139 | Link: getOr(vs, "c:mn:l", "black"), 140 | }, 141 | Button: SkinColourBit{ 142 | Background: getOr(vs, "c:btn:b", "#0078e7"), 143 | Text: getOr(vs, "c:btn:t", "white"), 144 | Link: "ignored", 145 | }, 146 | Branding: getOr(vs, "showBranding", "true") != "false", 147 | } 148 | } 149 | 150 | func HomePage(w http.ResponseWriter, r *http.Request) { 151 | t := template.Must(template.ParseFiles("templates/frontbase.html", "templates/signup.html")) 152 | 153 | t.ExecuteTemplate(w, "layout", TemplateData{ 154 | PageData: TemplatePageData{ 155 | Title: "Minecraft Account Association", 156 | }, 157 | Data: struct { 158 | HasError bool 159 | }{ 160 | HasError: r.FormValue("err") == "domain", 161 | }, 162 | }) 163 | } 164 | 165 | func getActualRemoteAddr(r *http.Request) string { 166 | // We get the port which we want to strip out 167 | return strings.Split(r.RemoteAddr, ":")[0] 168 | } 169 | 170 | func SignUp(w http.ResponseWriter, r *http.Request) { 171 | ctx := r.Context() 172 | 173 | //TODO: DRY 174 | sessionId := addSessionIdIfNotExists(w, r) 175 | if r.Method != "POST" { 176 | w.Header().Set("Allow", "POST") 177 | w.WriteHeader(http.StatusMethodNotAllowed) 178 | w.Write([]byte("must be a POST request")) 179 | return 180 | } 181 | 182 | err := r.ParseForm() 183 | if err != nil { 184 | w.WriteHeader(http.StatusBadRequest) 185 | w.Write([]byte("data invalid")) 186 | return 187 | } 188 | domain := r.Form.Get("domain") 189 | // RFC2606: .test, .example, .invalid, .localhost 190 | isTestDomain := strings.HasSuffix(domain, ".test") || strings.HasSuffix(domain, ".example") || strings.HasSuffix(domain, ".invalid") || strings.HasSuffix(domain, ".localhost") 191 | if !isTestDomain && !isDomainValid(ctx, domain) { 192 | http.Redirect(w, r, "/?err=domain", 301) 193 | return 194 | } 195 | 196 | data := generateDomainVerificationKey(domain, getActualRemoteAddr(r), sessionId) 197 | 198 | t := template.Must(template.ParseFiles("templates/frontbase.html", "templates/verification.html")) 199 | value := base64.URLEncoding.EncodeToString(data) 200 | 201 | var testKey string 202 | if isTestDomain { 203 | testKey = hex.EncodeToString(generateSharedKey(domain)) 204 | } 205 | 206 | t.ExecuteTemplate(w, "layout", TemplateData{ 207 | PageData: TemplatePageData{ 208 | Title: "Minecraft Account Association", 209 | }, 210 | Data: struct { 211 | Key string 212 | URL string 213 | UserDomain string 214 | 215 | IsTestDomain bool 216 | TestKey string 217 | }{ 218 | Key: value, 219 | URL: "http://" + domain + "/mcassoc-" + value + ".txt", 220 | UserDomain: domain, 221 | 222 | IsTestDomain: isTestDomain, 223 | TestKey: testKey, 224 | }, 225 | }) 226 | } 227 | 228 | func ApiDomainVerificationDns(w http.ResponseWriter, r *http.Request) { 229 | ctx := r.Context() 230 | domain := r.Form.Get("domain") 231 | var txtContentsArray []string 232 | txtContentsArray, err := lookupTXT(ctx, "mcassocverify."+domain) 233 | if len(txtContentsArray) != 1 { 234 | w.WriteHeader(http.StatusForbidden) 235 | w.Write([]byte("Invalid number of TXT records (" + fmt.Sprintf("%v", len(txtContentsArray)) + ") for name " + "mcassocverify." + domain + ".")) 236 | if len(txtContentsArray) == 0 { 237 | w.Write([]byte(" If you've just added this record, it can take a while for DNS changes to propagate.")) 238 | } 239 | return 240 | } 241 | var txtContents string 242 | txtContents = txtContentsArray[0] 243 | if err != nil { 244 | w.WriteHeader(http.StatusForbidden) 245 | w.Write([]byte("An error was encountered while attempting to lookup the TXT record. Ensure it exists and try again later.")) 246 | return 247 | } 248 | 249 | key := "code=" + base64.URLEncoding.EncodeToString(generateDomainVerificationKey(domain, getActualRemoteAddr(r), getSessionId(r))) 250 | if key != txtContents { 251 | w.WriteHeader(http.StatusForbidden) 252 | w.Write([]byte("The TXT record contents was " + txtContents + " but the expected value was " + key)) 253 | return 254 | } 255 | 256 | w.Write([]byte(hex.EncodeToString(generateSharedKey(domain)))) 257 | 258 | } 259 | 260 | func addSessionIdIfNotExists(w http.ResponseWriter, r *http.Request) string { 261 | if !hasSessionId(r) { 262 | return generateSessionId(w) 263 | } 264 | return getSessionId(r) 265 | } 266 | 267 | func hasSessionId(r *http.Request) bool { 268 | return getSessionId(r) != "" 269 | } 270 | 271 | func getSessionId(r *http.Request) string { 272 | cookie, err := r.Cookie("SessionId") 273 | if err == nil && cookie != nil { 274 | return cookie.Value 275 | } 276 | return "" 277 | } 278 | 279 | func generateSessionId(w http.ResponseWriter) string { 280 | bytes := make([]byte, 10) 281 | 282 | _, err := rand.Read(bytes) 283 | if err != nil { 284 | panic("Random generation failed!") 285 | } 286 | stringId := base64.URLEncoding.EncodeToString(bytes) 287 | cookie := &http.Cookie{Name: "SessionId", Value: stringId, HttpOnly: true} 288 | http.SetCookie(w, cookie) 289 | return stringId 290 | } 291 | 292 | func ApiDomainVerification(w http.ResponseWriter, r *http.Request) { 293 | ctx := r.Context() 294 | 295 | if !hasSessionId(r) { 296 | w.WriteHeader(http.StatusForbidden) 297 | w.Write([]byte("Cookies must be enabled to perform verification.")) 298 | return 299 | } 300 | 301 | err := r.ParseForm() 302 | if err != nil { 303 | w.WriteHeader(http.StatusBadRequest) 304 | w.Write([]byte("data invalid")) 305 | return 306 | } 307 | 308 | if r.Form.Get("verificationType") == "txt" { 309 | ApiDomainVerificationDns(w, r) 310 | return 311 | } 312 | if r.Method != "POST" { 313 | w.Header().Set("Allow", "POST") 314 | 315 | w.WriteHeader(http.StatusMethodNotAllowed) 316 | w.Write([]byte("must be a POST request")) 317 | return 318 | } 319 | 320 | domain := r.Form.Get("domain") 321 | key := base64.URLEncoding.EncodeToString(generateDomainVerificationKey(domain, getActualRemoteAddr(r), getSessionId(r))) 322 | url := getDomainVerificationUrl(domain, key) 323 | var resp *http.Response 324 | resp, err = urlfetch.Client(ctx).Get(url) 325 | if err != nil { 326 | w.WriteHeader(http.StatusForbidden) 327 | w.Write([]byte("An error was encountered in opening a connection.")) 328 | return 329 | } 330 | 331 | defer resp.Body.Close() 332 | if resp.StatusCode != 200 { 333 | w.WriteHeader(http.StatusForbidden) 334 | w.Write([]byte("URL must return HTTP 200 in response to GET. URL visited was " + url)) 335 | return 336 | } 337 | 338 | contents, err := ioutil.ReadAll(resp.Body) 339 | 340 | if err != nil || strings.TrimSpace(string(contents)) != key { 341 | w.WriteHeader(http.StatusForbidden) 342 | w.Write([]byte("Please ensure the file contains the key and no extra characters.")) 343 | return 344 | } 345 | 346 | w.Write([]byte(hex.EncodeToString(generateSharedKey(domain)))) 347 | } 348 | 349 | func TestPage(w http.ResponseWriter, r *http.Request) { 350 | if r.Method != "POST" { 351 | w.Header().Set("Allow", "POST") 352 | w.WriteHeader(http.StatusMethodNotAllowed) 353 | w.Write([]byte("must be a POST request")) 354 | return 355 | } 356 | 357 | err := r.ParseForm() 358 | if err != nil { 359 | w.WriteHeader(http.StatusBadRequest) 360 | w.Write([]byte("data invalid")) 361 | return 362 | } 363 | 364 | data := r.Form.Get("data") 365 | databytes, err := base64.StdEncoding.DecodeString(data) 366 | if err != nil { 367 | w.Write([]byte("invalid base64 data")) 368 | return 369 | } 370 | 371 | if len(databytes) < 20 { 372 | w.Write([]byte("data too short?!?")) 373 | return 374 | } 375 | sigbytes := databytes[len(databytes)-20:] 376 | databytes = databytes[:len(databytes)-20] 377 | 378 | mysigbytes := generateHashOfBlob(databytes, "_", false) 379 | sigok := subtle.ConstantTimeCompare(sigbytes, mysigbytes) == 1 380 | sigokchar := "no" 381 | if sigok { 382 | sigokchar = "yes" 383 | } 384 | 385 | w.Write([]byte("\ndata: ")) 386 | w.Write(databytes) 387 | w.Write([]byte("\nsignature OK? " + sigokchar)) 388 | if sigok { 389 | dataobj := new(SigningData) 390 | err := json.Unmarshal(databytes, dataobj) 391 | if err != nil { 392 | w.Write([]byte("\nfailed to unmarshal JSON: " + err.Error())) 393 | } else { 394 | w.Write([]byte("\nunmarshalled OK")) 395 | tsfresh := "no" 396 | now := time.Now().UTC().Unix() 397 | if dataobj.Now > (now-30) && dataobj.Now < (now+30) { 398 | tsfresh = fmt.Sprintf("yes (%d seconds old)", now-dataobj.Now) 399 | } 400 | w.Write([]byte("\ntimestamp 'fresh'? " + tsfresh)) 401 | } 402 | } 403 | } 404 | 405 | func PerformPage(w http.ResponseWriter, r *http.Request) { 406 | v := r.URL.Query() 407 | siteID := v.Get("siteid") 408 | postbackURL := v.Get("postback") 409 | key := v.Get("key") 410 | mcuser := v.Get("mcusername") 411 | 412 | skinColours := unwrapSkinColour(v) 413 | 414 | if pbu, err := url.Parse(postbackURL); err != nil || (pbu.Scheme != "http" && pbu.Scheme != "https") { 415 | w.WriteHeader(http.StatusBadRequest) 416 | w.Write([]byte("postback must be a HTTP/HTTPS url")) 417 | return 418 | } 419 | 420 | // check that the required fields are set 421 | if siteID == "" || postbackURL == "" || key == "" { 422 | w.WriteHeader(http.StatusBadRequest) 423 | w.Write([]byte("required parameter(s) missing")) 424 | return 425 | } 426 | 427 | t := template.Must(template.ParseFiles("templates/minibase.html", "templates/perform.html")) 428 | 429 | statkeeper.GLOBAL.NewAssocAttempt() 430 | 431 | t.ExecuteTemplate(w, "layout", TemplateData{ 432 | PageData: TemplatePageData{ 433 | Title: "Minecraft Account Association", 434 | }, 435 | Data: struct { 436 | SiteID string 437 | PostbackURL string 438 | Key string 439 | MCUser string 440 | SkinColour SkinColour 441 | }{ 442 | SiteID: siteID, 443 | PostbackURL: postbackURL, 444 | Key: key, 445 | MCUser: mcuser, 446 | SkinColour: skinColours, 447 | }, 448 | }) 449 | } 450 | 451 | func ApiCheckUserPage(w http.ResponseWriter, r *http.Request) { 452 | ctx := r.Context() 453 | 454 | if r.Method != "POST" { 455 | w.Header().Set("Allow", "POST") 456 | w.WriteHeader(http.StatusMethodNotAllowed) 457 | w.Write([]byte("must be a POST request")) 458 | return 459 | } 460 | 461 | err := r.ParseForm() 462 | if err != nil { 463 | w.WriteHeader(http.StatusBadRequest) 464 | w.Write([]byte("data invalid")) 465 | return 466 | } 467 | 468 | je := json.NewEncoder(w) 469 | 470 | mcusername := r.Form.Get("mcusername") 471 | 472 | // get their uuid from mojang 473 | user, err := mojang.GetProfileByUsername(urlfetch.Client(ctx), mcusername) 474 | if err != nil { 475 | if err == mojang.ERR_NO_SUCH_USER { 476 | je.Encode(struct { 477 | Error string `json:"error"` 478 | }{ 479 | Error: "no such user", 480 | }) 481 | return 482 | } else { 483 | statkeeper.GLOBAL.MojangRequestFail() 484 | statkeeper.GLOBAL.AssocFail() 485 | log.Printf("%s", fmt.Sprintln("error while getting mojang profile", mcusername, err)) 486 | w.WriteHeader(http.StatusInternalServerError) 487 | return 488 | } 489 | } else { 490 | statkeeper.GLOBAL.MojangRequestOK() 491 | } 492 | 493 | // so we can get their skin data 494 | mcprofile, err := profileClient.GetProfile(urlfetch.Client(ctx), user.Id) 495 | if err != nil { 496 | statkeeper.GLOBAL.McRequestOK() 497 | log.Printf("%s", fmt.Sprintln("error while getting minecraft profile", mcusername, user.Id, err)) 498 | 499 | w.WriteHeader(http.StatusInternalServerError) 500 | return 501 | } else { 502 | statkeeper.GLOBAL.McRequestFail() 503 | statkeeper.GLOBAL.AssocFail() 504 | } 505 | 506 | // so we can get their skin 507 | skinim, err := minecraft.GetSkin(urlfetch.Client(ctx), mcprofile) 508 | if err != nil { 509 | log.Printf("%s", fmt.Sprintln("error while getting skin", mcusername, user.Id, mcprofile, err)) 510 | w.WriteHeader(http.StatusInternalServerError) 511 | return 512 | } 513 | 514 | // so we can check if it has a datablock in it 515 | je.Encode(struct { 516 | MCUsername string `json:"mcusername"` 517 | UUID string `json:"uuid"` 518 | Exists bool `json:"exists"` 519 | }{ 520 | MCUsername: mcusername, 521 | UUID: user.Id, 522 | Exists: mcassoc.HasDatablock(skinim), 523 | }) 524 | } 525 | 526 | func ApiAuthenticateUserPage(w http.ResponseWriter, r *http.Request) { 527 | ctx := r.Context() 528 | 529 | if r.Method != "POST" { 530 | w.Header().Set("Allow", "POST") 531 | w.WriteHeader(http.StatusMethodNotAllowed) 532 | w.Write([]byte("must be a POST request")) 533 | return 534 | } 535 | 536 | err := r.ParseForm() 537 | if err != nil { 538 | w.WriteHeader(http.StatusBadRequest) 539 | w.Write([]byte("data invalid")) 540 | return 541 | } 542 | 543 | je := json.NewEncoder(w) 544 | 545 | uuid := r.Form.Get("uuid") 546 | password := r.Form.Get("password") 547 | 548 | mcprofile, err := profileClient.GetProfile(urlfetch.Client(ctx), uuid) 549 | if err != nil { 550 | statkeeper.GLOBAL.McRequestFail() 551 | statkeeper.GLOBAL.AssocFail() 552 | log.Printf("%s", fmt.Sprintln("error while getting minecraft profile", uuid, err)) 553 | w.WriteHeader(http.StatusInternalServerError) 554 | return 555 | } else { 556 | statkeeper.GLOBAL.McRequestOK() 557 | } 558 | 559 | skinim, err := minecraft.GetSkin(urlfetch.Client(ctx), mcprofile) 560 | if err != nil { 561 | log.Printf("%s", fmt.Sprintln("error while getting skin", uuid, mcprofile, err)) 562 | w.WriteHeader(http.StatusInternalServerError) 563 | return 564 | } 565 | 566 | passwordok, err := authenticator.Verify(password, skinim) 567 | if err != nil { 568 | log.Printf("%s", fmt.Sprintln("error verifying datablock", uuid, mcprofile, err)) 569 | w.WriteHeader(http.StatusInternalServerError) 570 | return 571 | } 572 | 573 | postbackurl := "" 574 | postbackdata := "" 575 | if passwordok { 576 | // yay! 577 | postbackstr := r.Form.Get("data[postback]") 578 | postback, err := url.Parse(postbackstr) 579 | if err != nil || (postback.Scheme != "http" && postback.Scheme != "https") { 580 | w.WriteHeader(http.StatusPreconditionFailed) 581 | return 582 | } 583 | 584 | postbackdata = generateDataBlob(SigningData{ 585 | Now: time.Now().UTC().Unix(), 586 | UUID: mcprofile.Id, 587 | Username: mcprofile.Name, 588 | Key: r.Form.Get("data[key]"), 589 | }, r.Form.Get("data[siteid]")) 590 | postbackurl = postback.String() 591 | 592 | statkeeper.GLOBAL.AssocComplete() 593 | } 594 | 595 | je.Encode(struct { 596 | MCUsername string `json:"mcusername"` 597 | UUID string `json:"uuid"` 598 | Correct bool `json:"correct"` 599 | Postback string `json:"postback"` 600 | PostbackData string `json:"postbackdata"` 601 | }{ 602 | MCUsername: mcprofile.Name, 603 | UUID: mcprofile.Id, 604 | Correct: passwordok, 605 | Postback: postbackurl, 606 | PostbackData: postbackdata, 607 | }) 608 | } 609 | 610 | func ApiCreateUserPage(w http.ResponseWriter, r *http.Request) { 611 | ctx := r.Context() 612 | if r.Method != "POST" { 613 | w.Header().Set("Allow", "POST") 614 | w.WriteHeader(http.StatusMethodNotAllowed) 615 | w.Write([]byte("must be a POST request")) 616 | return 617 | } 618 | 619 | err := r.ParseForm() 620 | if err != nil { 621 | w.WriteHeader(http.StatusBadRequest) 622 | w.Write([]byte("data invalid")) 623 | return 624 | } 625 | 626 | je := json.NewEncoder(w) 627 | 628 | uuid := r.Form.Get("uuid") 629 | password := r.Form.Get("password") 630 | 631 | mcprofile, err := profileClient.GetProfile(urlfetch.Client(ctx), uuid) 632 | if err != nil { 633 | statkeeper.GLOBAL.McRequestFail() 634 | statkeeper.GLOBAL.AssocFail() 635 | log.Printf("%s", fmt.Sprintln("error while getting minecraft profile", uuid, err)) 636 | w.WriteHeader(http.StatusInternalServerError) 637 | return 638 | } else { 639 | statkeeper.GLOBAL.McRequestOK() 640 | } 641 | 642 | skinim, err := minecraft.GetSkin(urlfetch.Client(ctx), mcprofile) 643 | if err != nil { 644 | log.Printf("%s", fmt.Sprintln("error while getting skin", uuid, mcprofile, err)) 645 | w.WriteHeader(http.StatusInternalServerError) 646 | return 647 | } 648 | 649 | authedim, err := authenticator.Embed(password, skinim) 650 | if err != nil { 651 | log.Printf("%s", fmt.Sprintln("error while embedding into skin", uuid, mcprofile, err)) 652 | w.WriteHeader(http.StatusInternalServerError) 653 | return 654 | } 655 | 656 | newFilename := fmt.Sprintf("%s.png", uuid) 657 | b, err := bucket(r) 658 | if err != nil { 659 | log.Printf("error retrieving GCS bucket", err) 660 | w.WriteHeader(http.StatusInternalServerError) 661 | return 662 | } 663 | obj := b.Object(newFilename) 664 | imw := obj.NewWriter(ctx) 665 | imw.ContentType = "image/png" 666 | 667 | if err := png.Encode(imw, authedim); err != nil { 668 | log.Printf("%s", fmt.Sprintln("error while writing authed skin image", uuid, mcprofile, err)) 669 | imw.Close() 670 | w.WriteHeader(http.StatusInternalServerError) 671 | return 672 | } 673 | 674 | if err := imw.Close(); err != nil { 675 | log.Printf("%s", fmt.Sprintln("error closing authed skin image writer", uuid, mcprofile, err)) 676 | w.WriteHeader(http.StatusInternalServerError) 677 | return 678 | } 679 | 680 | attrs, err := obj.Attrs(ctx) 681 | if err != nil { 682 | log.Printf("%s", fmt.Sprintln("error retrieving newly-saved object attrs", uuid, mcprofile, err)) 683 | w.WriteHeader(http.StatusInternalServerError) 684 | return 685 | } 686 | 687 | je.Encode(struct { 688 | URL string `json:"url"` 689 | }{ 690 | URL: attrs.MediaLink, 691 | }) 692 | } 693 | 694 | func bucket(req *http.Request) (*storage.BucketHandle, error) { 695 | ctx := req.Context() 696 | client, err := storage.NewClient(ctx) 697 | if err != nil { 698 | return nil, err 699 | } 700 | return client.Bucket("mcassoc-ng.appspot.com"), nil 701 | } 702 | 703 | func init() { 704 | // load the authentication keys 705 | authenticator = mcassoc.NewAssocifier(authkey) 706 | profileClient = minecraft.NewProfileClient() 707 | 708 | if stathatkey != "" { 709 | sh := statkeeper.NewStatHatStatKeeper(stathatkey) 710 | statkeeper.GLOBAL = sh 711 | } 712 | } 713 | 714 | func Handler() http.Handler { 715 | r := mux.NewRouter() 716 | r.HandleFunc("/", HomePage) 717 | r.HandleFunc("/signup", SignUp) 718 | r.HandleFunc("/perform", PerformPage) 719 | r.HandleFunc("/test", TestPage) 720 | r.HandleFunc("/api/domain/verify", ApiDomainVerification) 721 | r.HandleFunc("/api/user/check", ApiCheckUserPage) 722 | r.HandleFunc("/api/user/create", ApiCreateUserPage) 723 | r.HandleFunc("/api/user/authenticate", ApiAuthenticateUserPage) 724 | r.PathPrefix("/static/").Handler(http.FileServer(http.Dir("templates/"))) 725 | r.PathPrefix("/css/").Handler(http.FileServer(http.Dir("templates/"))) 726 | r.PathPrefix("/img/").Handler(http.FileServer(http.Dir("templates/"))) 727 | return r 728 | } 729 | 730 | func main() { 731 | handler := Handler() 732 | http.Handle("/", handler) 733 | 734 | port := os.Getenv("PORT") 735 | if port == "" { 736 | port = "8080" 737 | log.Printf("Defaulting to port %s", port) 738 | } 739 | 740 | log.Printf("Listening on port %s", port) 741 | if err := http.ListenAndServe(":"+port, nil); err != nil { 742 | log.Fatalf("%v", err) 743 | } 744 | } 745 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.36.0 h1:+aCSj7tOo2LODWVEuZDZeGCckdt6MlSF+X/rB3wUiS8= 4 | cloud.google.com/go v0.36.0/go.mod h1:RUoy9p/M4ge0HzT8L+SDZ8jg+Q6fth0CiBuhFJpSV40= 5 | cloud.google.com/go v0.109.0 h1:38CZoKGlCnPZjGdyj0ZfpoGae0/wgNfy5F0byyxg0Gk= 6 | cloud.google.com/go v0.109.0/go.mod h1:2sYycXt75t/CSB5R9M2wPU1tJmire7AQZTPtITcGBVE= 7 | cloud.google.com/go/compute v1.15.1 h1:7UGq3QknM33pw5xATlpzeoomNxsacIVvTqTTvbfajmE= 8 | cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= 9 | cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= 10 | cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= 11 | cloud.google.com/go/iam v0.10.0 h1:fpP/gByFs6US1ma53v7VxhvbJpO2Aapng6wabJ99MuI= 12 | cloud.google.com/go/iam v0.10.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM= 13 | cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI= 14 | cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= 15 | dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= 16 | dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= 17 | dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= 18 | dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= 19 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 20 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 21 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 22 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 23 | github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= 24 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 25 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 26 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 27 | github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 28 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 31 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 32 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 33 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 34 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 35 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 36 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 37 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 38 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 39 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 40 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 41 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 42 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 43 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 44 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 45 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 46 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 47 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 48 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 49 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 50 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 51 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 52 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 53 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 54 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 55 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 56 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 57 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 58 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 59 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 60 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 61 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 62 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 63 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 64 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 65 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 66 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 67 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 68 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 69 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 70 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 71 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 72 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 73 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 74 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 75 | github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= 76 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 77 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 78 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 79 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 80 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 81 | github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg= 82 | github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= 83 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= 84 | github.com/googleapis/gax-go/v2 v2.0.3 h1:siORttZ36U2R/WjiJuDz8znElWBiAlO9rVt+mqJt0Cc= 85 | github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= 86 | github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= 87 | github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= 88 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 89 | github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= 90 | github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 91 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 92 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 93 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 94 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 95 | github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= 96 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 97 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 98 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 99 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 100 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 101 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 102 | github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= 103 | github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0= 104 | github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 105 | github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= 106 | github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= 107 | github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= 108 | github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= 109 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 110 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 111 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 112 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 113 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 114 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 115 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 116 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 117 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 118 | github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= 119 | github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= 120 | github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= 121 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 122 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 123 | github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= 124 | github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= 125 | github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= 126 | github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= 127 | github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= 128 | github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= 129 | github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= 130 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 131 | github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= 132 | github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= 133 | github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= 134 | github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= 135 | github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= 136 | github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= 137 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 138 | github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= 139 | github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= 140 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= 141 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= 142 | github.com/stathat/go v1.0.0 h1:HFIS5YkyaI6tXu7JXIRRZBLRvYstdNZm034zcCeaybI= 143 | github.com/stathat/go v1.0.0/go.mod h1:+9Eg2szqkcOGWv6gfheJmBBsmq9Qf5KDbzy8/aYYR0c= 144 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 145 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 146 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 147 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 148 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 149 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 150 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 151 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 152 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 153 | go.opencensus.io v0.18.0 h1:Mk5rgZcggtbvtAun5aJzAtjKKN/t0R3jJPlWILlv938= 154 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 155 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 156 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 157 | go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= 158 | golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= 159 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= 160 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 161 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 162 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 163 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 164 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 165 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 166 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 167 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 168 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 169 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 170 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 171 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 172 | golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= 173 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 174 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 175 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 176 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 177 | golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 178 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 179 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 180 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 181 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 182 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 183 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 184 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 185 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 186 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 187 | golang.org/x/net v0.0.0-20220708220712-1185a9018129 h1:vucSRfWwTsoXro7P+3Cjlr6flUMtzCwzlvkxEQtHHB0= 188 | golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 189 | golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= 190 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 191 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 192 | golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 193 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE= 194 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 195 | golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= 196 | golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= 197 | golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 198 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 199 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= 200 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 201 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 202 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 203 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 204 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 205 | golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 206 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 207 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 208 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 210 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 211 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 212 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 213 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 214 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 215 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= 216 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 217 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 218 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 219 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 220 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 221 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 222 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 223 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 224 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 225 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 226 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 227 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 228 | golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= 229 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 230 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 231 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 232 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 233 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 234 | golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 235 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 236 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 237 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 238 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 239 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 240 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 241 | golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= 242 | golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= 243 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 244 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 245 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 246 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 247 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= 248 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 249 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 250 | google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 251 | google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI= 252 | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= 253 | google.golang.org/api v0.108.0 h1:WVBc/faN0DkKtR43Q/7+tPny9ZoLZdIiAyG5Q9vFClg= 254 | google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= 255 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 256 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 257 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 258 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 259 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 260 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 261 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 262 | google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk= 263 | google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E= 264 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 265 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 266 | google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 267 | google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 268 | google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922 h1:mBVYJnbrXLA/ZCBTCe7PtEgAUP+1bg92qTaFoPHdz+8= 269 | google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4= 270 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 271 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 272 | google.golang.org/genproto v0.0.0-20230119192704-9d59e20e5cd1 h1:wSjSSQW7LuPdv3m1IrSN33nVxH/kID6OIKy+FMwGB2k= 273 | google.golang.org/genproto v0.0.0-20230119192704-9d59e20e5cd1/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= 274 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 275 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 276 | google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk= 277 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 278 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 279 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 280 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 281 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 282 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 283 | google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk= 284 | google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= 285 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 286 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 287 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 288 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 289 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 290 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 291 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 292 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 293 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 294 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 295 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 296 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 297 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 298 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 299 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 300 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 301 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 302 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 303 | grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= 304 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 305 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 306 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 307 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 308 | sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= 309 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 310 | --------------------------------------------------------------------------------