// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package api

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"strconv"
	"strings"
	"time"

	"github.com/mitchellh/mapstructure"
)

const (
	ServiceDefaults    string = "service-defaults"
	ProxyDefaults      string = "proxy-defaults"
	ServiceRouter      string = "service-router"
	ServiceSplitter    string = "service-splitter"
	ServiceResolver    string = "service-resolver"
	IngressGateway     string = "ingress-gateway"
	TerminatingGateway string = "terminating-gateway"
	ServiceIntentions  string = "service-intentions"
	MeshConfig         string = "mesh"
	ExportedServices   string = "exported-services"
	SamenessGroup      string = "sameness-group"
	RateLimitIPConfig  string = "control-plane-request-limit"

	ProxyConfigGlobal string = "global"
	MeshConfigMesh    string = "mesh"
	APIGateway        string = "api-gateway"
	TCPRoute          string = "tcp-route"
	InlineCertificate string = "inline-certificate"
	HTTPRoute         string = "http-route"
	JWTProvider       string = "jwt-provider"
)

const (
	BuiltinAWSLambdaExtension        string = "builtin/aws/lambda"
	BuiltinExtAuthzExtension         string = "builtin/ext-authz"
	BuiltinLuaExtension              string = "builtin/lua"
	BuiltinPropertyOverrideExtension string = "builtin/property-override"
	BuiltinWasmExtension             string = "builtin/wasm"
	// BuiltinValidateExtension should not be exposed directly or accepted as a valid configured
	// extension type, as it is only used indirectly via troubleshooting tools. It is included here
	// for common reference alongside other builtin extensions.
	BuiltinValidateExtension string = "builtin/proxy/validate"
)

type ConfigEntry interface {
	GetKind() string
	GetName() string
	GetPartition() string
	GetNamespace() string
	GetMeta() map[string]string
	GetCreateIndex() uint64
	GetModifyIndex() uint64
}

type MeshGatewayMode string

const (
	// MeshGatewayModeDefault represents no specific mode and should
	// be used to indicate that a different layer of the configuration
	// chain should take precedence
	MeshGatewayModeDefault MeshGatewayMode = ""

	// MeshGatewayModeNone represents that the Upstream Connect connections
	// should be direct and not flow through a mesh gateway.
	MeshGatewayModeNone MeshGatewayMode = "none"

	// MeshGatewayModeLocal represents that the Upstream Connect connections
	// should be made to a mesh gateway in the local datacenter.
	MeshGatewayModeLocal MeshGatewayMode = "local"

	// MeshGatewayModeRemote represents that the Upstream Connect connections
	// should be made to a mesh gateway in a remote datacenter.
	MeshGatewayModeRemote MeshGatewayMode = "remote"
)

// MeshGatewayConfig controls how Mesh Gateways are used for upstream Connect
// services
type MeshGatewayConfig struct {
	// Mode is the mode that should be used for the upstream connection.
	Mode MeshGatewayMode `json:",omitempty"`
}

type ProxyMode string

const (
	// ProxyModeDefault represents no specific mode and should
	// be used to indicate that a different layer of the configuration
	// chain should take precedence
	ProxyModeDefault ProxyMode = ""

	// ProxyModeTransparent represents that inbound and outbound application
	// traffic is being captured and redirected through the proxy.
	ProxyModeTransparent ProxyMode = "transparent"

	// ProxyModeDirect represents that the proxy's listeners must be dialed directly
	// by the local application and other proxies.
	ProxyModeDirect ProxyMode = "direct"
)

type TransparentProxyConfig struct {
	// The port of the listener where outbound application traffic is being redirected to.
	OutboundListenerPort int `json:",omitempty" alias:"outbound_listener_port"`

	// DialedDirectly indicates whether transparent proxies can dial this proxy instance directly.
	// The discovery chain is not considered when dialing a service instance directly.
	// This setting is useful when addressing stateful services, such as a database cluster with a leader node.
	DialedDirectly bool `json:",omitempty" alias:"dialed_directly"`
}

type MutualTLSMode string

const (
	// MutualTLSModeDefault represents no specific mode and should
	// be used to indicate that a different layer of the configuration
	// chain should take precedence.
	MutualTLSModeDefault MutualTLSMode = ""

	// MutualTLSModeStrict requires mTLS for incoming traffic.
	MutualTLSModeStrict MutualTLSMode = "strict"

	// MutualTLSModePermissive allows incoming non-mTLS traffic.
	MutualTLSModePermissive MutualTLSMode = "permissive"
)

// ExposeConfig describes HTTP paths to expose through Envoy outside of Connect.
// Users can expose individual paths and/or all HTTP/GRPC paths for checks.
type ExposeConfig struct {
	// Checks defines whether paths associated with Consul checks will be exposed.
	// This flag triggers exposing all HTTP and GRPC check paths registered for the service.
	Checks bool `json:",omitempty"`

	// Paths is the list of paths exposed through the proxy.
	Paths []ExposePath `json:",omitempty"`
}

// EnvoyExtension has configuration for an extension that patches Envoy resources.
type EnvoyExtension struct {
	Name          string
	Required      bool
	Arguments     map[string]interface{} `bexpr:"-"`
	ConsulVersion string
	EnvoyVersion  string
}

type ExposePath struct {
	// ListenerPort defines the port of the proxy's listener for exposed paths.
	ListenerPort int `json:",omitempty" alias:"listener_port"`

	// Path is the path to expose through the proxy, ie. "/metrics."
	Path string `json:",omitempty"`

	// LocalPathPort is the port that the service is listening on for the given path.
	LocalPathPort int `json:",omitempty" alias:"local_path_port"`

	// Protocol describes the upstream's service protocol.
	// Valid values are "http" and "http2", defaults to "http"
	Protocol string `json:",omitempty"`

	// ParsedFromCheck is set if this path was parsed from a registered check
	ParsedFromCheck bool
}

type LogSinkType string

const (
	DefaultLogSinkType LogSinkType = ""
	FileLogSinkType    LogSinkType = "file"
	StdErrLogSinkType  LogSinkType = "stderr"
	StdOutLogSinkType  LogSinkType = "stdout"
)

// AccessLogsConfig contains the associated default settings for all Envoy instances within the datacenter or partition
type AccessLogsConfig struct {
	// Enabled turns off all access logging
	Enabled bool `json:",omitempty" alias:"enabled"`

	// DisableListenerLogs turns off just listener logs for connections rejected by Envoy because they don't
	// have a matching listener filter.
	DisableListenerLogs bool `json:",omitempty" alias:"disable_listener_logs"`

	// Type selects the output for logs: "file", "stderr". "stdout"
	Type LogSinkType `json:",omitempty" alias:"type"`

	// Path is the output file to write logs
	Path string `json:",omitempty" alias:"path"`

	// The presence of one format string or the other implies the access log string encoding.
	// Defining Both is invalid.
	JSONFormat string `json:",omitempty" alias:"json_format"`
	TextFormat string `json:",omitempty" alias:"text_format"`
}

type UpstreamConfiguration struct {
	// Overrides is a slice of per-service configuration. The name field is
	// required.
	Overrides []*UpstreamConfig `json:",omitempty"`

	// Defaults contains default configuration for all upstreams of a given
	// service. The name field must be empty.
	Defaults *UpstreamConfig `json:",omitempty"`
}

type UpstreamConfig struct {
	// Name is only accepted within service-defaults.upstreamConfig.overrides .
	Name string `json:",omitempty"`

	// Partition is only accepted within service-defaults.upstreamConfig.overrides .
	Partition string `json:",omitempty"`

	// Namespace is only accepted within service-defaults.upstreamConfig.overrides .
	Namespace string `json:",omitempty"`

	// Peer is only accepted within service-defaults.upstreamConfig.overrides .
	Peer string `json:",omitempty"`

	// EnvoyListenerJSON is a complete override ("escape hatch") for the upstream's
	// listener.
	//
	// Note: This escape hatch is NOT compatible with the discovery chain and
	// will be ignored if a discovery chain is active.
	EnvoyListenerJSON string `json:",omitempty" alias:"envoy_listener_json"`

	// EnvoyClusterJSON is a complete override ("escape hatch") for the upstream's
	// cluster. The Connect client TLS certificate and context will be injected
	// overriding any TLS settings present.
	//
	// Note: This escape hatch is NOT compatible with the discovery chain and
	// will be ignored if a discovery chain is active.
	EnvoyClusterJSON string `json:",omitempty" alias:"envoy_cluster_json"`

	// Protocol describes the upstream's service protocol. Valid values are "tcp",
	// "http" and "grpc". Anything else is treated as tcp. The enables protocol
	// aware features like per-request metrics and connection pooling, tracing,
	// routing etc.
	Protocol string `json:",omitempty"`

	// ConnectTimeoutMs is the number of milliseconds to timeout making a new
	// connection to this upstream. Defaults to 5000 (5 seconds) if not set.
	ConnectTimeoutMs int `json:",omitempty" alias:"connect_timeout_ms"`

	// Limits are the set of limits that are applied to the proxy for a specific upstream of a
	// service instance.
	Limits *UpstreamLimits `json:",omitempty"`

	// PassiveHealthCheck configuration determines how upstream proxy instances will
	// be monitored for removal from the load balancing pool.
	PassiveHealthCheck *PassiveHealthCheck `json:",omitempty" alias:"passive_health_check"`

	// MeshGatewayConfig controls how Mesh Gateways are configured and used
	MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway" `

	// BalanceOutboundConnections indicates that the proxy should attempt to evenly distribute
	// outbound connections across worker threads. Only used by envoy proxies.
	BalanceOutboundConnections string `json:",omitempty" alias:"balance_outbound_connections"`
}

// DestinationConfig represents a virtual service, i.e. one that is external to Consul
type DestinationConfig struct {
	// Addresses of the endpoint; hostname or IP
	Addresses []string `json:",omitempty"`

	// Port allowed within this endpoint
	Port int `json:",omitempty"`
}

type PassiveHealthCheck struct {
	// Interval between health check analysis sweeps. Each sweep may remove
	// hosts or return hosts to the pool.
	Interval time.Duration `json:",omitempty"`

	// MaxFailures is the count of consecutive failures that results in a host
	// being removed from the pool.
	MaxFailures uint32 `alias:"max_failures"`

	// EnforcingConsecutive5xx is the % chance that a host will be actually ejected
	// when an outlier status is detected through consecutive 5xx.
	// This setting can be used to disable ejection or to ramp it up slowly.
	EnforcingConsecutive5xx *uint32 `json:",omitempty" alias:"enforcing_consecutive_5xx"`

	// The maximum % of an upstream cluster that can be ejected due to outlier detection.
	// Defaults to 10% but will eject at least one host regardless of the value.
	MaxEjectionPercent *uint32 `json:",omitempty" alias:"max_ejection_percent"`

	// The base time that a host is ejected for. The real time is equal to the base time
	// multiplied by the number of times the host has been ejected and is capped by
	// max_ejection_time (Default 300s). Defaults to 30000ms or 30s.
	BaseEjectionTime *time.Duration `json:",omitempty" alias:"base_ejection_time"`
}

// UpstreamLimits describes the limits that are associated with a specific
// upstream of a service instance.
type UpstreamLimits struct {
	// MaxConnections is the maximum number of connections the local proxy can
	// make to the upstream service.
	MaxConnections *int `alias:"max_connections"`

	// MaxPendingRequests is the maximum number of requests that will be queued
	// waiting for an available connection. This is mostly applicable to HTTP/1.1
	// clusters since all HTTP/2 requests are streamed over a single
	// connection.
	MaxPendingRequests *int `alias:"max_pending_requests"`

	// MaxConcurrentRequests is the maximum number of in-flight requests that will be allowed
	// to the upstream cluster at a point in time. This is mostly applicable to HTTP/2
	// clusters since all HTTP/1.1 requests are limited by MaxConnections.
	MaxConcurrentRequests *int `alias:"max_concurrent_requests"`
}

type ServiceConfigEntry struct {
	Kind                      string
	Name                      string
	Partition                 string                  `json:",omitempty"`
	Namespace                 string                  `json:",omitempty"`
	Protocol                  string                  `json:",omitempty"`
	Mode                      ProxyMode               `json:",omitempty"`
	TransparentProxy          *TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"`
	MutualTLSMode             MutualTLSMode           `json:",omitempty" alias:"mutual_tls_mode"`
	MeshGateway               MeshGatewayConfig       `json:",omitempty" alias:"mesh_gateway"`
	Expose                    ExposeConfig            `json:",omitempty"`
	ExternalSNI               string                  `json:",omitempty" alias:"external_sni"`
	UpstreamConfig            *UpstreamConfiguration  `json:",omitempty" alias:"upstream_config"`
	Destination               *DestinationConfig      `json:",omitempty"`
	MaxInboundConnections     int                     `json:",omitempty" alias:"max_inbound_connections"`
	LocalConnectTimeoutMs     int                     `json:",omitempty" alias:"local_connect_timeout_ms"`
	LocalRequestTimeoutMs     int                     `json:",omitempty" alias:"local_request_timeout_ms"`
	BalanceInboundConnections string                  `json:",omitempty" alias:"balance_inbound_connections"`
	EnvoyExtensions           []EnvoyExtension        `json:",omitempty" alias:"envoy_extensions"`
	Meta                      map[string]string       `json:",omitempty"`
	CreateIndex               uint64
	ModifyIndex               uint64
}

func (s *ServiceConfigEntry) GetKind() string            { return s.Kind }
func (s *ServiceConfigEntry) GetName() string            { return s.Name }
func (s *ServiceConfigEntry) GetPartition() string       { return s.Partition }
func (s *ServiceConfigEntry) GetNamespace() string       { return s.Namespace }
func (s *ServiceConfigEntry) GetMeta() map[string]string { return s.Meta }
func (s *ServiceConfigEntry) GetCreateIndex() uint64     { return s.CreateIndex }
func (s *ServiceConfigEntry) GetModifyIndex() uint64     { return s.ModifyIndex }

type ProxyConfigEntry struct {
	Kind                 string
	Name                 string
	Partition            string                               `json:",omitempty"`
	Namespace            string                               `json:",omitempty"`
	Mode                 ProxyMode                            `json:",omitempty"`
	TransparentProxy     *TransparentProxyConfig              `json:",omitempty" alias:"transparent_proxy"`
	MutualTLSMode        MutualTLSMode                        `json:",omitempty" alias:"mutual_tls_mode"`
	Config               map[string]interface{}               `json:",omitempty"`
	MeshGateway          MeshGatewayConfig                    `json:",omitempty" alias:"mesh_gateway"`
	Expose               ExposeConfig                         `json:",omitempty"`
	AccessLogs           *AccessLogsConfig                    `json:",omitempty" alias:"access_logs"`
	EnvoyExtensions      []EnvoyExtension                     `json:",omitempty" alias:"envoy_extensions"`
	FailoverPolicy       *ServiceResolverFailoverPolicy       `json:",omitempty" alias:"failover_policy"`
	PrioritizeByLocality *ServiceResolverPrioritizeByLocality `json:",omitempty" alias:"prioritize_by_locality"`

	Meta        map[string]string `json:",omitempty"`
	CreateIndex uint64
	ModifyIndex uint64
}

func (p *ProxyConfigEntry) GetKind() string            { return p.Kind }
func (p *ProxyConfigEntry) GetName() string            { return ProxyConfigGlobal }
func (p *ProxyConfigEntry) GetPartition() string       { return p.Partition }
func (p *ProxyConfigEntry) GetNamespace() string       { return p.Namespace }
func (p *ProxyConfigEntry) GetMeta() map[string]string { return p.Meta }
func (p *ProxyConfigEntry) GetCreateIndex() uint64     { return p.CreateIndex }
func (p *ProxyConfigEntry) GetModifyIndex() uint64     { return p.ModifyIndex }

func makeConfigEntry(kind, name string) (ConfigEntry, error) {
	switch kind {
	case ServiceDefaults:
		return &ServiceConfigEntry{Kind: kind, Name: name}, nil
	case ProxyDefaults:
		return &ProxyConfigEntry{Kind: kind, Name: name}, nil
	case ServiceRouter:
		return &ServiceRouterConfigEntry{Kind: kind, Name: name}, nil
	case ServiceSplitter:
		return &ServiceSplitterConfigEntry{Kind: kind, Name: name}, nil
	case ServiceResolver:
		return &ServiceResolverConfigEntry{Kind: kind, Name: name}, nil
	case IngressGateway:
		return &IngressGatewayConfigEntry{Kind: kind, Name: name}, nil
	case TerminatingGateway:
		return &TerminatingGatewayConfigEntry{Kind: kind, Name: name}, nil
	case ServiceIntentions:
		return &ServiceIntentionsConfigEntry{Kind: kind, Name: name}, nil
	case MeshConfig:
		return &MeshConfigEntry{}, nil
	case ExportedServices:
		return &ExportedServicesConfigEntry{Name: name}, nil
	case SamenessGroup:
		return &SamenessGroupConfigEntry{Kind: kind, Name: name}, nil
	case APIGateway:
		return &APIGatewayConfigEntry{Kind: kind, Name: name}, nil
	case TCPRoute:
		return &TCPRouteConfigEntry{Kind: kind, Name: name}, nil
	case InlineCertificate:
		return &InlineCertificateConfigEntry{Kind: kind, Name: name}, nil
	case HTTPRoute:
		return &HTTPRouteConfigEntry{Kind: kind, Name: name}, nil
	case RateLimitIPConfig:
		return &RateLimitIPConfigEntry{Kind: kind, Name: name}, nil
	case JWTProvider:
		return &JWTProviderConfigEntry{Kind: kind, Name: name}, nil
	default:
		return nil, fmt.Errorf("invalid config entry kind: %s", kind)
	}
}

func MakeConfigEntry(kind, name string) (ConfigEntry, error) {
	return makeConfigEntry(kind, name)
}

// DecodeConfigEntry will decode the result of using json.Unmarshal of a config
// entry into a map[string]interface{}.
//
// Important caveats:
//
// - This will NOT work if the map[string]interface{} was produced using HCL
// decoding as that requires more extensive parsing to work around the issues
// with map[string][]interface{} that arise.
//
// - This will only decode fields using their camel case json field
// representations.
func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) {
	var entry ConfigEntry

	kindVal, ok := raw["Kind"]
	if !ok {
		kindVal, ok = raw["kind"]
	}
	if !ok {
		return nil, fmt.Errorf("Payload does not contain a kind/Kind key at the top level")
	}

	if kindStr, ok := kindVal.(string); ok {
		newEntry, err := makeConfigEntry(kindStr, "")
		if err != nil {
			return nil, err
		}
		entry = newEntry
	} else {
		return nil, fmt.Errorf("Kind value in payload is not a string")
	}

	decodeConf := &mapstructure.DecoderConfig{
		DecodeHook: mapstructure.ComposeDecodeHookFunc(
			mapstructure.StringToTimeDurationHookFunc(),
			mapstructure.StringToTimeHookFunc(time.RFC3339),
		),
		Result:           &entry,
		WeaklyTypedInput: true,
	}

	decoder, err := mapstructure.NewDecoder(decodeConf)
	if err != nil {
		return nil, err
	}

	return entry, decoder.Decode(raw)
}

func DecodeConfigEntryFromJSON(data []byte) (ConfigEntry, error) {
	var raw map[string]interface{}
	if err := json.Unmarshal(data, &raw); err != nil {
		return nil, err
	}

	return DecodeConfigEntry(raw)
}

func decodeConfigEntrySlice(raw []map[string]interface{}) ([]ConfigEntry, error) {
	var entries []ConfigEntry
	for _, rawEntry := range raw {
		entry, err := DecodeConfigEntry(rawEntry)
		if err != nil {
			return nil, err
		}
		entries = append(entries, entry)
	}
	return entries, nil
}

// ConfigEntries can be used to query the Config endpoints
type ConfigEntries struct {
	c *Client
}

// Config returns a handle to the Config endpoints
func (c *Client) ConfigEntries() *ConfigEntries {
	return &ConfigEntries{c}
}

func (conf *ConfigEntries) Get(kind string, name string, q *QueryOptions) (ConfigEntry, *QueryMeta, error) {
	if kind == "" || name == "" {
		return nil, nil, fmt.Errorf("Both kind and name parameters must not be empty")
	}

	entry, err := makeConfigEntry(kind, name)
	if err != nil {
		return nil, nil, err
	}

	r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s/%s", kind, name))
	r.setQueryOptions(q)
	rtt, resp, err := conf.c.doRequest(r)
	if err != nil {
		return nil, nil, err
	}
	defer closeResponseBody(resp)
	if err := requireOK(resp); err != nil {
		return nil, nil, err
	}

	qm := &QueryMeta{}
	parseQueryMeta(resp, qm)
	qm.RequestTime = rtt

	if err := decodeBody(resp, entry); err != nil {
		return nil, nil, err
	}

	return entry, qm, nil
}

func (conf *ConfigEntries) List(kind string, q *QueryOptions) ([]ConfigEntry, *QueryMeta, error) {
	if kind == "" {
		return nil, nil, fmt.Errorf("The kind parameter must not be empty")
	}

	r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s", kind))
	r.setQueryOptions(q)
	rtt, resp, err := conf.c.doRequest(r)
	if err != nil {
		return nil, nil, err
	}
	defer closeResponseBody(resp)
	if err := requireOK(resp); err != nil {
		return nil, nil, err
	}

	qm := &QueryMeta{}
	parseQueryMeta(resp, qm)
	qm.RequestTime = rtt

	var raw []map[string]interface{}
	if err := decodeBody(resp, &raw); err != nil {
		return nil, nil, err
	}

	entries, err := decodeConfigEntrySlice(raw)
	if err != nil {
		return nil, nil, err
	}

	return entries, qm, nil
}

func (conf *ConfigEntries) Set(entry ConfigEntry, w *WriteOptions) (bool, *WriteMeta, error) {
	return conf.set(entry, nil, w)
}

func (conf *ConfigEntries) CAS(entry ConfigEntry, index uint64, w *WriteOptions) (bool, *WriteMeta, error) {
	return conf.set(entry, map[string]string{"cas": strconv.FormatUint(index, 10)}, w)
}

func (conf *ConfigEntries) set(entry ConfigEntry, params map[string]string, w *WriteOptions) (bool, *WriteMeta, error) {
	r := conf.c.newRequest("PUT", "/v1/config")
	r.setWriteOptions(w)
	for param, value := range params {
		r.params.Set(param, value)
	}
	r.obj = entry
	rtt, resp, err := conf.c.doRequest(r)
	if err != nil {
		return false, nil, err
	}
	defer closeResponseBody(resp)
	if err := requireOK(resp); err != nil {
		return false, nil, err
	}

	var buf bytes.Buffer
	if _, err := io.Copy(&buf, resp.Body); err != nil {
		return false, nil, fmt.Errorf("Failed to read response: %v", err)
	}
	res := strings.Contains(buf.String(), "true")

	wm := &WriteMeta{RequestTime: rtt}
	return res, wm, nil
}

func (conf *ConfigEntries) Delete(kind string, name string, w *WriteOptions) (*WriteMeta, error) {
	_, wm, err := conf.delete(kind, name, nil, w)
	return wm, err
}

// DeleteCAS performs a Check-And-Set deletion of the given config entry, and
// returns true if it was successful. If the provided index no longer matches
// the entry's ModifyIndex (i.e. it was modified by another process) then the
// operation will fail and return false.
func (conf *ConfigEntries) DeleteCAS(kind, name string, index uint64, w *WriteOptions) (bool, *WriteMeta, error) {
	return conf.delete(kind, name, map[string]string{"cas": strconv.FormatUint(index, 10)}, w)
}

func (conf *ConfigEntries) delete(kind, name string, params map[string]string, w *WriteOptions) (bool, *WriteMeta, error) {
	if kind == "" || name == "" {
		return false, nil, fmt.Errorf("Both kind and name parameters must not be empty")
	}

	r := conf.c.newRequest("DELETE", fmt.Sprintf("/v1/config/%s/%s", kind, name))
	r.setWriteOptions(w)
	for param, value := range params {
		r.params.Set(param, value)
	}

	rtt, resp, err := conf.c.doRequest(r)
	if err != nil {
		return false, nil, err
	}
	defer closeResponseBody(resp)

	if err := requireOK(resp); err != nil {
		return false, nil, err
	}

	var buf bytes.Buffer
	if _, err := io.Copy(&buf, resp.Body); err != nil {
		return false, nil, fmt.Errorf("Failed to read response: %v", err)
	}

	res := strings.Contains(buf.String(), "true")
	wm := &WriteMeta{RequestTime: rtt}
	return res, wm, nil
}