github.com/fortexxx/gqlgen@v0.10.3-0.20191216030626-ca5ea8b21ead/graphql/handler/extension/apq.go (about) 1 package extension 2 3 import ( 4 "context" 5 "crypto/sha256" 6 "encoding/hex" 7 "fmt" 8 9 "github.com/vektah/gqlparser/gqlerror" 10 11 "github.com/99designs/gqlgen/graphql" 12 "github.com/mitchellh/mapstructure" 13 ) 14 15 const errPersistedQueryNotFound = "PersistedQueryNotFound" 16 17 // AutomaticPersistedQuery saves client upload by optimistically sending only the hashes of queries, if the server 18 // does not yet know what the query is for the hash it will respond telling the client to send the query along with the 19 // hash in the next request. 20 // see https://github.com/apollographql/apollo-link-persisted-queries 21 type AutomaticPersistedQuery struct { 22 Cache graphql.Cache 23 } 24 25 type ApqStats struct { 26 // The hash of the incoming query 27 Hash string 28 29 // SentQuery is true if the incoming request sent the full query 30 SentQuery bool 31 } 32 33 const apqExtension = "APQ" 34 35 var _ interface { 36 graphql.OperationParameterMutator 37 graphql.HandlerExtension 38 } = AutomaticPersistedQuery{} 39 40 func (a AutomaticPersistedQuery) ExtensionName() string { 41 return "AutomaticPersistedQuery" 42 } 43 44 func (a AutomaticPersistedQuery) Validate(schema graphql.ExecutableSchema) error { 45 if a.Cache == nil { 46 return fmt.Errorf("AutomaticPersistedQuery.Cache can not be nil") 47 } 48 return nil 49 } 50 51 func (a AutomaticPersistedQuery) MutateOperationParameters(ctx context.Context, rawParams *graphql.RawParams) *gqlerror.Error { 52 if rawParams.Extensions["persistedQuery"] == nil { 53 return nil 54 } 55 56 var extension struct { 57 Sha256 string `mapstructure:"sha256Hash"` 58 Version int64 `mapstructure:"version"` 59 } 60 61 if err := mapstructure.Decode(rawParams.Extensions["persistedQuery"], &extension); err != nil { 62 return gqlerror.Errorf("invalid APQ extension data") 63 } 64 65 if extension.Version != 1 { 66 return gqlerror.Errorf("unsupported APQ version") 67 } 68 69 fullQuery := false 70 if rawParams.Query == "" { 71 // client sent optimistic query hash without query string, get it from the cache 72 query, ok := a.Cache.Get(extension.Sha256) 73 if !ok { 74 return gqlerror.Errorf(errPersistedQueryNotFound) 75 } 76 rawParams.Query = query.(string) 77 } else { 78 // client sent optimistic query hash with query string, verify and store it 79 if computeQueryHash(rawParams.Query) != extension.Sha256 { 80 return gqlerror.Errorf("provided APQ hash does not match query") 81 } 82 a.Cache.Add(extension.Sha256, rawParams.Query) 83 fullQuery = true 84 } 85 86 graphql.GetOperationContext(ctx).Stats.SetExtension(apqExtension, &ApqStats{ 87 Hash: extension.Sha256, 88 SentQuery: fullQuery, 89 }) 90 91 return nil 92 } 93 94 func GetApqStats(ctx context.Context) *ApqStats { 95 rc := graphql.GetOperationContext(ctx) 96 if rc == nil { 97 return nil 98 } 99 100 s, _ := rc.Stats.GetExtension(apqExtension).(*ApqStats) 101 return s 102 } 103 104 func computeQueryHash(query string) string { 105 b := sha256.Sum256([]byte(query)) 106 return hex.EncodeToString(b[:]) 107 }