func (s *testMetaServer) Status(ctx context.Context, _ *pbtypes.Void) (*ServerStatus, error) { md, _ := metadata.FromContext(ctx) if want, got := md["want-access-token"], md["authorization"]; got != want { return nil, grpc.Errorf(codes.Unknown, "got access-token %q, want %q", got, want) } return &ServerStatus{}, nil }
// ListSubscriptions lists matching subscriptions. func (c *SubscriberClient) ListSubscriptions(ctx context.Context, req *pubsubpb.ListSubscriptionsRequest) *SubscriptionIterator { md, _ := metadata.FromContext(ctx) ctx = metadata.NewContext(ctx, metadata.Join(md, c.metadata)) it := &SubscriptionIterator{} it.InternalFetch = func(pageSize int, pageToken string) ([]*pubsubpb.Subscription, string, error) { var resp *pubsubpb.ListSubscriptionsResponse req.PageToken = pageToken if pageSize > math.MaxInt32 { req.PageSize = math.MaxInt32 } else { req.PageSize = int32(pageSize) } err := gax.Invoke(ctx, func(ctx context.Context) error { var err error resp, err = c.subscriberClient.ListSubscriptions(ctx, req) return err }, c.CallOptions.ListSubscriptions...) if err != nil { return nil, "", err } return resp.Subscriptions, resp.NextPageToken, nil } fetch := func(pageSize int, pageToken string) (string, error) { items, nextPageToken, err := it.InternalFetch(pageSize, pageToken) if err != nil { return "", err } it.items = append(it.items, items...) return nextPageToken, nil } it.pageInfo, it.nextFunc = iterator.NewPageInfo(fetch, it.bufLen, it.takeBuf) return it }
func NewRaftProxyHealthServer(local HealthServer, connSelector raftpicker.Interface, cluster raftpicker.RaftCluster, ctxMod func(context.Context) (context.Context, error)) HealthServer { redirectChecker := func(ctx context.Context) (context.Context, error) { s, ok := transport.StreamFromContext(ctx) if !ok { return ctx, grpc.Errorf(codes.InvalidArgument, "remote addr is not found in context") } addr := s.ServerTransport().RemoteAddr().String() md, ok := metadata.FromContext(ctx) if ok && len(md["redirect"]) != 0 { return ctx, grpc.Errorf(codes.ResourceExhausted, "more than one redirect to leader from: %s", md["redirect"]) } if !ok { md = metadata.New(map[string]string{}) } md["redirect"] = append(md["redirect"], addr) return metadata.NewContext(ctx, md), nil } mods := []func(context.Context) (context.Context, error){redirectChecker} mods = append(mods, ctxMod) return &raftProxyHealthServer{ local: local, cluster: cluster, connSelector: connSelector, ctxMods: mods, } }
func newStreamInterceptor(s *etcdserver.EtcdServer) grpc.StreamServerInterceptor { smap := monitorLeader(s) return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { if !api.IsCapabilityEnabled(api.V3rpcCapability) { return rpctypes.ErrGRPCNotCapable } md, ok := metadata.FromContext(ss.Context()) if ok { if ks := md[rpctypes.MetadataRequireLeaderKey]; len(ks) > 0 && ks[0] == rpctypes.MetadataHasLeader { if s.Leader() == types.ID(raft.None) { return rpctypes.ErrGRPCNoLeader } cctx, cancel := context.WithCancel(ss.Context()) ss = serverStreamWithCtx{ctx: cctx, cancel: &cancel, ServerStream: ss} smap.mu.Lock() smap.streams[ss] = struct{}{} smap.mu.Unlock() defer func() { smap.mu.Lock() delete(smap.streams, ss) smap.mu.Unlock() cancel() }() } } return metricsStreamInterceptor(srv, ss, info, handler) } }
func getGRPCTraceID(ctx context.Context) (string, error) { md, ok := metadata.FromContext(ctx) if !ok { id, err := uuid.NewV4() if err != nil { return "", err } return id.String(), nil } tokens := md["trace"] if len(tokens) == 0 { id, err := uuid.NewV4() if err != nil { return "", err } return id.String(), nil } if tokens[0] != "" { return tokens[0], nil } id, err := uuid.NewV4() if err != nil { return "", err } return id.String(), nil }
func MetadataFromContext(ctx context.Context) (metadata.MD, error) { md, ok := metadata.FromContext(ctx) if !ok { return md, ErrContext } return md, nil }
func TestAnnotateContext_ForwardsGrpcMetadata(t *testing.T) { ctx := context.Background() request, err := http.NewRequest("GET", "http://www.example.com", nil) if err != nil { t.Fatalf("http.NewRequest(%q, %q, nil) failed with %v; want success", "GET", "http://www.example.com", err) } request.Header.Add("Some-Irrelevant-Header", "some value") request.Header.Add("Grpc-Metadata-FooBar", "Value1") request.Header.Add("Grpc-Metadata-Foo-BAZ", "Value2") request.Header.Add("Grpc-Metadata-foo-bAz", "Value3") request.Header.Add("Authorization", "Token 1234567890") annotated, err := runtime.AnnotateContext(ctx, request) if err != nil { t.Errorf("runtime.AnnotateContext(ctx, %#v) failed with %v; want success", request, err) return } md, ok := metadata.FromContext(annotated) if got, want := len(md), emptyForwardMetaCount+3; !ok || got != want { t.Errorf("Expected %d metadata items in context; got %d", got, want) } if got, want := md["foobar"], []string{"Value1"}; !reflect.DeepEqual(got, want) { t.Errorf(`md["foobar"] = %q; want %q`, got, want) } if got, want := md["foo-baz"], []string{"Value2", "Value3"}; !reflect.DeepEqual(got, want) { t.Errorf(`md["foo-baz"] = %q want %q`, got, want) } if got, want := md["authorization"], []string{"Token 1234567890"}; !reflect.DeepEqual(got, want) { t.Errorf(`md["authorization"] = %q want %q`, got, want) } }
// LogRPCWithFields will feed any request context into a logrus Entry. func LogRPCWithFields(log *logrus.Logger, ctx context.Context) *logrus.Entry { md, ok := metadata.FromContext(ctx) if !ok { return logrus.NewEntry(log) } return log.WithFields(MetadataToFields(md)) }
func NewRaftProxyResourceAllocatorServer(local ResourceAllocatorServer, connSelector raftselector.ConnProvider, localCtxMod, remoteCtxMod func(context.Context) (context.Context, error)) ResourceAllocatorServer { redirectChecker := func(ctx context.Context) (context.Context, error) { s, ok := transport.StreamFromContext(ctx) if !ok { return ctx, grpc.Errorf(codes.InvalidArgument, "remote addr is not found in context") } addr := s.ServerTransport().RemoteAddr().String() md, ok := metadata.FromContext(ctx) if ok && len(md["redirect"]) != 0 { return ctx, grpc.Errorf(codes.ResourceExhausted, "more than one redirect to leader from: %s", md["redirect"]) } if !ok { md = metadata.New(map[string]string{}) } md["redirect"] = append(md["redirect"], addr) return metadata.NewContext(ctx, md), nil } remoteMods := []func(context.Context) (context.Context, error){redirectChecker} remoteMods = append(remoteMods, remoteCtxMod) var localMods []func(context.Context) (context.Context, error) if localCtxMod != nil { localMods = []func(context.Context) (context.Context, error){localCtxMod} } return &raftProxyResourceAllocatorServer{ local: local, connSelector: connSelector, localCtxMods: localMods, remoteCtxMods: remoteMods, } }
func (s *testServer) UnaryCall(ctx context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) { md, ok := metadata.FromContext(ctx) if ok { if err := grpc.SendHeader(ctx, md); err != nil { grpclog.Fatalf("grpc.SendHeader(%v, %v) = %v, want %v", ctx, md, err, nil) } grpc.SetTrailer(ctx, md) } if s.security != "" { // Check Auth info authInfo, ok := credentials.FromContext(ctx) if !ok { grpclog.Fatalf("Failed to get AuthInfo from ctx.") } var authType string switch info := authInfo.(type) { case credentials.TLSInfo: authType = info.AuthType() default: grpclog.Fatalf("Unknown AuthInfo type") } if authType != s.security { grpclog.Fatalf("Wrong auth type: got %q, want %q", authType, s.security) } } // Simulate some service delay. time.Sleep(time.Second) return &testpb.SimpleResponse{ Payload: newPayload(in.GetResponseType(), in.GetResponseSize()), }, nil }
func setInternalRPCGateway(ctx context.Context) error { // TODO: This function is only necessary as part of a hack to set the // RPC endpoint as part of the first incoming RPC. It shouldn't be necessary // to do that. The correct value should be set via env vars from the start. internalRPCGatewayLock.RLock() if internalRPCGateway != "" { internalRPCGatewayLock.RUnlock() return nil } internalRPCGatewayLock.RUnlock() md, ok := metadata.FromContext(ctx) if !ok { // No metadata. Don't set it. return nil } gateway, ok := md["x-lever-internal-rpc-gateway"] if !ok || len(gateway) == 0 { // Header not found. Don't set it. return nil } if gateway[0] != "" { internalRPCGatewayLock.Lock() internalRPCGateway = gateway[0] internalRPCGatewayLock.Unlock() } return nil }
func (c Client) GetRatePlans(ctx context.Context, hotelIDs []int32, inDate string, outDate string) RatePlanReply { md, _ := metadata.FromContext(ctx) trace.Req(md["traceID"], md["from"], "service.rate", "GetRatePlans") defer trace.Rep(md["traceID"], "service.rate", md["from"], time.Now()) args := &pb.Args{ HotelIds: hotelIDs, InDate: inDate, OutDate: outDate, } reply, err := c.client.GetRates(ctx, args) if err != nil { return RatePlanReply{ RatePlans: []*pb.RatePlan{}, Err: err, } } return RatePlanReply{ RatePlans: reply.RatePlans, Err: nil, } }
func (s *networkServerRPC) ValidateContext(ctx context.Context) error { md, ok := metadata.FromContext(ctx) if !ok { return errors.NewErrInternal("Could not get metadata from context") } var id, token string if ids, ok := md["id"]; ok && len(ids) == 1 { id = ids[0] } if id == "" { return errors.NewErrInvalidArgument("Metadata", "id missing") } if tokens, ok := md["token"]; ok && len(tokens) == 1 { token = tokens[0] } if token == "" { return errors.NewErrInvalidArgument("Metadata", "token missing") } var claims *jwt.StandardClaims claims, err := security.ValidateJWT(token, []byte(s.networkServer.(*networkServer).Identity.PublicKey)) if err != nil { return err } if claims.Subject != id { return errors.NewErrInvalidArgument("Metadata", "token was issued for a different component id") } return nil }
func (s *_ABitOfEverythingServer) List(_ *empty.Empty, stream examples.StreamService_ListServer) error { s.m.Lock() defer s.m.Unlock() err := stream.SendHeader(metadata.New(map[string]string{ "count": fmt.Sprintf("%d", len(s.v)), })) if err != nil { return nil } for _, msg := range s.v { if err := stream.Send(msg); err != nil { return err } } // return error when metadata includes error header if header, ok := metadata.FromContext(stream.Context()); ok { if v, ok := header["error"]; ok { stream.SetTrailer(metadata.New(map[string]string{ "foo": "foo2", "bar": "bar2", })) return grpc.Errorf(codes.InvalidArgument, "error metadata: %v", v) } } return nil }
func isForwardedRequest(ctx context.Context) bool { md, _ := metadata.FromContext(ctx) if len(md[certForwardedKey]) != 1 { return false } return md[certForwardedKey][0] == "true" }
// Reads the OIDC JWT passed in the context and verifies it using the given OIDC client. // Returns the verified identity on success, error otherwise. func VerifiedIdentityFromContext(client *gooidc.Client, ctx context.Context) (*gooidc.Identity, error) { md, ok := metadata.FromContext(ctx) if !ok { return nil, errors.New("missing RPC credentials") } rawJWT, ok := md["jwt"] if !ok { return nil, errors.New("missing OIDC credentials") } if len(rawJWT) != 1 { return nil, errors.New("incorrect JWT data sent") } jwt, err := jose.ParseJWT(rawJWT[0]) if err != nil { return nil, err } if err := client.VerifyJWT(jwt); err != nil { return nil, err } claims, err := jwt.Claims() if err != nil { return nil, err } return gooidc.IdentityFromClaims(claims) }
// WithMetadataForwardTLSInfo reads certificate from context and returns context where // ForwardCert is set based on original certificate. func WithMetadataForwardTLSInfo(ctx context.Context) (context.Context, error) { md, ok := metadata.FromContext(ctx) if !ok { md = metadata.MD{} } ous := []string{} org := "" cn := "" certSubj, err := certSubjectFromContext(ctx) if err == nil { cn = certSubj.CommonName ous = certSubj.OrganizationalUnit if len(certSubj.Organization) > 0 { org = certSubj.Organization[0] } } // If there's no TLS cert, forward with blank TLS metadata. // Note that the presence of this blank metadata is extremely // important. Without it, it would look like manager is making // the request directly. md[certForwardedKey] = []string{"true"} md[certCNKey] = []string{cn} md[certOrgKey] = []string{org} md[certOUKey] = ous peer, ok := peer.FromContext(ctx) if ok { md[remoteAddrKey] = []string{peer.Addr.String()} } return metadata.NewContext(ctx, md), nil }
func TraceClient(ctx context.Context) { if md, ok := metadata.FromContext(ctx); ok { if s, ok := md["client"]; ok { Tracef(ctx, "client %+v", s) } } }
func (s *testServer) EmptyCall(ctx context.Context, in *testpb.Empty) (*testpb.Empty, error) { if _, ok := metadata.FromContext(ctx); ok { // For testing purpose, returns an error if there is attached metadata. return nil, grpc.Errorf(codes.DataLoss, "got extra metadata") } return new(testpb.Empty), nil }
func (hs *helloServer) Say(ctx context.Context, request *pb.Request) (*pb.Response, error) { var ( token *jwt.Token err error ) md, ok := metadata.FromContext(ctx) if !ok { return nil, grpc.Errorf(codes.Unauthenticated, "valid token required.") } jwtToken, ok := md["authorization"] if !ok { return nil, grpc.Errorf(codes.Unauthenticated, "valid token required.") } token, err = validateToken(jwtToken[0], hs.jwtPublicKey) if err != nil { return nil, grpc.Errorf(codes.Unauthenticated, "valid token required.") } response := &pb.Response{ Message: fmt.Sprintf("Hello %s (%s)", request.Name, token.Claims["email"]), } return response, nil }
func TestAnnotateContext_XForwardedFor(t *testing.T) { ctx := context.Background() request, err := http.NewRequest("GET", "http://bar.foo.example.com", nil) if err != nil { t.Fatalf("http.NewRequest(%q, %q, nil) failed with %v; want success", "GET", "http://bar.foo.example.com", err) } request.Header.Add("X-Forwarded-For", "192.0.2.100") // client request.RemoteAddr = "192.0.2.200:12345" // proxy annotated, err := runtime.AnnotateContext(ctx, request) if err != nil { t.Errorf("runtime.AnnotateContext(ctx, %#v) failed with %v; want success", request, err) return } md, ok := metadata.FromContext(annotated) if !ok || len(md) != emptyForwardMetaCount+1 { t.Errorf("Expected %d metadata items in context; got %v", emptyForwardMetaCount+1, md) } if got, want := md["x-forwarded-host"], []string{"bar.foo.example.com"}; !reflect.DeepEqual(got, want) { t.Errorf(`md["host"] = %v; want %v`, got, want) } // Note: it must be in order client, proxy1, proxy2 if got, want := md["x-forwarded-for"], []string{"192.0.2.100, 192.0.2.200"}; !reflect.DeepEqual(got, want) { t.Errorf(`md["x-forwarded-for"] = %v want %v`, got, want) } }
// ListMonitoredResourceDescriptors lists the monitored resource descriptors used by Stackdriver Logging. func (c *Client) ListMonitoredResourceDescriptors(ctx context.Context, req *loggingpb.ListMonitoredResourceDescriptorsRequest) *MonitoredResourceDescriptorIterator { md, _ := metadata.FromContext(ctx) ctx = metadata.NewContext(ctx, metadata.Join(md, c.metadata)) it := &MonitoredResourceDescriptorIterator{} fetch := func(pageSize int, pageToken string) (string, error) { var resp *loggingpb.ListMonitoredResourceDescriptorsResponse req.PageToken = pageToken if pageSize > math.MaxInt32 { req.PageSize = math.MaxInt32 } else { req.PageSize = int32(pageSize) } err := gax.Invoke(ctx, func(ctx context.Context) error { var err error resp, err = c.client.ListMonitoredResourceDescriptors(ctx, req) return err }, c.CallOptions.ListMonitoredResourceDescriptors...) if err != nil { return "", err } it.items = append(it.items, resp.ResourceDescriptors...) return resp.NextPageToken, nil } bufLen := func() int { return len(it.items) } takeBuf := func() interface{} { b := it.items it.items = nil return b } it.pageInfo, it.nextFunc = iterator.NewPageInfo(fetch, bufLen, takeBuf) return it }
func (s *testServer) FullDuplexCall(stream testpb.TestService_FullDuplexCallServer) error { md, ok := metadata.FromContext(stream.Context()) if ok { if err := stream.SendHeader(md); err != nil { return grpc.Errorf(grpc.Code(err), "%v.SendHeader(%v) = %v, want %v", stream, md, err, nil) } stream.SetTrailer(testTrailerMetadata) } for { in, err := stream.Recv() if err == io.EOF { // read done. return nil } if err != nil { return err } if in.Id == errorID { return fmt.Errorf("got error id: %v", in.Id) } if err := stream.Send(&testpb.SimpleResponse{Id: in.Id}); err != nil { return err } } }
// IdentifyContext takes a context and will verify the token in its metadata // with G5 Auth, populating the person's email address in the returned // context's metadata. It will throw an error if there are any failures // authenticating, problems with the metadata, or errors connecting to G5 Auth. func (a *G5Authenticator) IdentifyContext(ctx context.Context) (context.Context, error) { md, ok := metadata.FromContext(ctx) if !ok { return nil, errors.New("no metadata in request") } authorizations := md["authorization"] if i := len(authorizations); i != 1 { return nil, fmt.Errorf("unexpected number of authorization metadatum: %d", i) } parts := strings.Split(authorizations[0], " ") if len(parts) != 2 { return nil, errors.New("bad authorization format") } switch parts[0] { case "magic": if a.config.MagicalTokenOfSupremePower == "" { return nil, errors.New("magic auth is not configured") } if parts[1] == a.config.MagicalTokenOfSupremePower { return context.WithValue(ctx, "identity", serviceToServiceIdentity), nil } return nil, errors.New("bad magic token of supreme power") case "bearer": return a.authenticateBearerToken(parts[1], ctx) default: return nil, errors.New("unknown token type") } }
func (f *framework) CheckGRPCContext(ctx context.Context) error { md, ok := metadata.FromContext(ctx) if !ok { return fmt.Errorf("Can't get grpc.Metadata from context: %v", ctx) } epoch, err := strconv.ParseUint(md["epoch"], 10, 64) if err != nil { return err } // send it to framework central select and check epoch. resChan := make(chan bool, 1) // The select loop might stop running but we still need to return error to user. // We can't use the ctx here because it's the grpc context. select { case f.epochCheckChan <- &epochCheck{ epoch: epoch, resChan: resChan, }: case <-f.globalStop: return fmt.Errorf("framework stopped") } ok = <-resChan if ok { return nil } else { return ErrEpochMismatch } }
// BasicAuthFromContext gets the basic auth from the specified context.Context. // // If no basic auth is present, BasicAuthFromContext returns nil. func BasicAuthFromContext(ctx context.Context) (*BasicAuth, error) { md, ok := metadata.FromContext(ctx) if !ok { return nil, nil } authorization, ok := md["Authorization"] if !ok { authorization, ok = md["authorization"] if !ok { return nil, nil } } if len(authorization) != 1 { return nil, ErrInvalidAuthorization } if !strings.HasPrefix(authorization[0], "Basic ") { return nil, ErrInvalidAuthorization } decoded, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authorization[0], "Basic ")) if err != nil { return nil, err } split := strings.SplitN(string(decoded), ":", 2) if len(split) != 2 { return nil, ErrInvalidAuthorization } return &BasicAuth{ Username: split[0], Password: split[1], }, nil }
func (s *testServer) StreamingOutputCall(args *testpb.StreamingOutputCallRequest, stream testpb.TestService_StreamingOutputCallServer) error { if md, ok := metadata.FromContext(stream.Context()); ok { // For testing purpose, returns an error if there is attached metadata. if len(md) > 0 { return grpc.Errorf(codes.DataLoss, "got extra metadata") } } cs := args.GetResponseParameters() for _, c := range cs { if us := c.GetIntervalUs(); us > 0 { time.Sleep(time.Duration(us) * time.Microsecond) } payload, err := newPayload(args.GetResponseType(), c.GetSize()) if err != nil { return err } if err := stream.Send(&testpb.StreamingOutputCallResponse{ Payload: payload, }); err != nil { return err } } return nil }
func (s *server) SayHello(ctx context.Context, in *echo.EchoRequest) (*echo.EchoReply, error) { log.Println("Got rpc: --> ", in.Name) log.Println(ctx) md, ok := metadata.FromContext(ctx) if ok { // authorization header from context for oauth2 at client. // Verify as access_token to oauth2/tokeninfo // https://developers.google.com/identity/protocols/OAuth2UserAgent#tokeninfo-validation // https://developers.google.com/identity/protocols/OAuth2ServiceAccount // ----------------------------------------------------------------------------- // or if the id_token is sent in, verify digital signature // https://developers.google.com/identity/protocols/OpenIDConnect?hl=en#validatinganidtoken // https://github.com/golang/oauth2/issues/127 // http://stackoverflow.com/questions/26159658/golang-token-validation-error/26287613#26287613 log.Println(md["authorization"]) //log.Println(md["sal"]) } var respmdheader = metadata.MD{ "rpc headerKey": []string{"val"}, } if err := grpc.SendHeader(ctx, respmdheader); err != nil { log.Fatalf("grpc.SendHeader(%v, %v) = %v, want %v", ctx, respmdheader, err, nil) } var respmdfooter = metadata.MD{ "rpc trailerkey": []string{"val"}, } grpc.SetTrailer(ctx, respmdfooter) return &echo.EchoReply{Message: "Hello " + in.Name}, nil }
func (s *testServer) FullDuplexCall(stream testpb.TestService_FullDuplexCallServer) error { md, ok := metadata.FromContext(stream.Context()) if ok { if err := stream.SendHeader(md); err != nil { grpclog.Fatalf("%v.SendHeader(%v) = %v, want %v", stream, md, err, nil) } stream.SetTrailer(md) } for { in, err := stream.Recv() if err == io.EOF { // read done. return nil } if err != nil { return err } cs := in.GetResponseParameters() for _, c := range cs { if us := c.GetIntervalUs(); us > 0 { time.Sleep(time.Duration(us) * time.Microsecond) } if err := stream.Send(&testpb.StreamingOutputCallResponse{ Payload: newPayload(in.GetResponseType(), c.GetSize()), }); err != nil { return err } } } }
func TestAnnotateContext(t *testing.T) { ctx := context.Background() request, err := http.NewRequest("GET", "http://localhost", nil) if err != nil { t.Fatalf("http.NewRequest(%q, %q, nil) failed with %v; want success", "GET", "http://localhost", err) } request.Header.Add("Some-Irrelevant-Header", "some value") annotated := runtime.AnnotateContext(ctx, request) if annotated != ctx { t.Errorf("AnnotateContext(ctx, request) = %v; want %v", annotated, ctx) } request.Header.Add("Grpc-Metadata-FooBar", "Value1") request.Header.Add("Grpc-Metadata-Foo-BAZ", "Value2") request.Header.Add("Grpc-Metadata-foo-bAz", "Value3") request.Header.Add("Authorization", "Token 1234567890") annotated = runtime.AnnotateContext(ctx, request) md, ok := metadata.FromContext(annotated) if !ok || len(md) != 3 { t.Errorf("Expected 3 metadata items in context; got %v", md) } if got, want := md["foobar"], []string{"Value1"}; !reflect.DeepEqual(got, want) { t.Errorf(`md["foobar"] = %q; want %q`, got, want) } if got, want := md["foo-baz"], []string{"Value2", "Value3"}; !reflect.DeepEqual(got, want) { t.Errorf(`md["foo-baz"] = %q want %q`, got, want) } if got, want := md["authorization"], []string{"Token 1234567890"}; !reflect.DeepEqual(got, want) { t.Errorf(`md["authorization"] = %q want %q`, got, want) } }