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  }