456 lines
17 KiB
Go
456 lines
17 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package configutil
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/textproto"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
|
"github.com/hashicorp/go-secure-stdlib/tlsutil"
|
|
"github.com/hashicorp/go-sockaddr"
|
|
"github.com/hashicorp/go-sockaddr/template"
|
|
"github.com/hashicorp/hcl"
|
|
"github.com/hashicorp/hcl/hcl/ast"
|
|
)
|
|
|
|
type ListenerTelemetry struct {
|
|
UnusedKeys UnusedKeyMap `hcl:",unusedKeyPositions"`
|
|
UnauthenticatedMetricsAccess bool `hcl:"-"`
|
|
UnauthenticatedMetricsAccessRaw interface{} `hcl:"unauthenticated_metrics_access,alias:UnauthenticatedMetricsAccess"`
|
|
}
|
|
|
|
type ListenerProfiling struct {
|
|
UnusedKeys UnusedKeyMap `hcl:",unusedKeyPositions"`
|
|
UnauthenticatedPProfAccess bool `hcl:"-"`
|
|
UnauthenticatedPProfAccessRaw interface{} `hcl:"unauthenticated_pprof_access,alias:UnauthenticatedPProfAccessRaw"`
|
|
}
|
|
|
|
type ListenerInFlightRequestLogging struct {
|
|
UnusedKeys UnusedKeyMap `hcl:",unusedKeyPositions"`
|
|
UnauthenticatedInFlightAccess bool `hcl:"-"`
|
|
UnauthenticatedInFlightAccessRaw interface{} `hcl:"unauthenticated_in_flight_requests_access,alias:unauthenticatedInFlightAccessRaw"`
|
|
}
|
|
|
|
// Listener is the listener configuration for the server.
|
|
type Listener struct {
|
|
UnusedKeys UnusedKeyMap `hcl:",unusedKeyPositions"`
|
|
RawConfig map[string]interface{}
|
|
|
|
Type string
|
|
Purpose []string `hcl:"-"`
|
|
PurposeRaw interface{} `hcl:"purpose"`
|
|
Role string `hcl:"role"`
|
|
|
|
Address string `hcl:"address"`
|
|
ClusterAddress string `hcl:"cluster_address"`
|
|
MaxRequestSize int64 `hcl:"-"`
|
|
MaxRequestSizeRaw interface{} `hcl:"max_request_size"`
|
|
MaxRequestDuration time.Duration `hcl:"-"`
|
|
MaxRequestDurationRaw interface{} `hcl:"max_request_duration"`
|
|
RequireRequestHeader bool `hcl:"-"`
|
|
RequireRequestHeaderRaw interface{} `hcl:"require_request_header"`
|
|
|
|
TLSDisable bool `hcl:"-"`
|
|
TLSDisableRaw interface{} `hcl:"tls_disable"`
|
|
TLSCertFile string `hcl:"tls_cert_file"`
|
|
TLSKeyFile string `hcl:"tls_key_file"`
|
|
TLSMinVersion string `hcl:"tls_min_version"`
|
|
TLSMaxVersion string `hcl:"tls_max_version"`
|
|
TLSCipherSuites []uint16 `hcl:"-"`
|
|
TLSCipherSuitesRaw string `hcl:"tls_cipher_suites"`
|
|
TLSRequireAndVerifyClientCert bool `hcl:"-"`
|
|
TLSRequireAndVerifyClientCertRaw interface{} `hcl:"tls_require_and_verify_client_cert"`
|
|
TLSClientCAFile string `hcl:"tls_client_ca_file"`
|
|
TLSDisableClientCerts bool `hcl:"-"`
|
|
TLSDisableClientCertsRaw interface{} `hcl:"tls_disable_client_certs"`
|
|
|
|
HTTPReadTimeout time.Duration `hcl:"-"`
|
|
HTTPReadTimeoutRaw interface{} `hcl:"http_read_timeout"`
|
|
HTTPReadHeaderTimeout time.Duration `hcl:"-"`
|
|
HTTPReadHeaderTimeoutRaw interface{} `hcl:"http_read_header_timeout"`
|
|
HTTPWriteTimeout time.Duration `hcl:"-"`
|
|
HTTPWriteTimeoutRaw interface{} `hcl:"http_write_timeout"`
|
|
HTTPIdleTimeout time.Duration `hcl:"-"`
|
|
HTTPIdleTimeoutRaw interface{} `hcl:"http_idle_timeout"`
|
|
|
|
ProxyProtocolBehavior string `hcl:"proxy_protocol_behavior"`
|
|
ProxyProtocolAuthorizedAddrs []*sockaddr.SockAddrMarshaler `hcl:"-"`
|
|
ProxyProtocolAuthorizedAddrsRaw interface{} `hcl:"proxy_protocol_authorized_addrs,alias:ProxyProtocolAuthorizedAddrs"`
|
|
|
|
XForwardedForAuthorizedAddrs []*sockaddr.SockAddrMarshaler `hcl:"-"`
|
|
XForwardedForAuthorizedAddrsRaw interface{} `hcl:"x_forwarded_for_authorized_addrs,alias:XForwardedForAuthorizedAddrs"`
|
|
XForwardedForHopSkips int64 `hcl:"-"`
|
|
XForwardedForHopSkipsRaw interface{} `hcl:"x_forwarded_for_hop_skips,alias:XForwardedForHopSkips"`
|
|
XForwardedForRejectNotPresent bool `hcl:"-"`
|
|
XForwardedForRejectNotPresentRaw interface{} `hcl:"x_forwarded_for_reject_not_present,alias:XForwardedForRejectNotPresent"`
|
|
XForwardedForRejectNotAuthorized bool `hcl:"-"`
|
|
XForwardedForRejectNotAuthorizedRaw interface{} `hcl:"x_forwarded_for_reject_not_authorized,alias:XForwardedForRejectNotAuthorized"`
|
|
|
|
SocketMode string `hcl:"socket_mode"`
|
|
SocketUser string `hcl:"socket_user"`
|
|
SocketGroup string `hcl:"socket_group"`
|
|
|
|
AgentAPI *AgentAPI `hcl:"agent_api"`
|
|
|
|
ProxyAPI *ProxyAPI `hcl:"proxy_api"`
|
|
|
|
Telemetry ListenerTelemetry `hcl:"telemetry"`
|
|
Profiling ListenerProfiling `hcl:"profiling"`
|
|
InFlightRequestLogging ListenerInFlightRequestLogging `hcl:"inflight_requests_logging"`
|
|
|
|
// RandomPort is used only for some testing purposes
|
|
RandomPort bool `hcl:"-"`
|
|
|
|
CorsEnabledRaw interface{} `hcl:"cors_enabled"`
|
|
CorsEnabled bool `hcl:"-"`
|
|
CorsAllowedOrigins []string `hcl:"cors_allowed_origins"`
|
|
CorsAllowedHeaders []string `hcl:"-"`
|
|
CorsAllowedHeadersRaw []string `hcl:"cors_allowed_headers,alias:cors_allowed_headers"`
|
|
|
|
// Custom Http response headers
|
|
CustomResponseHeaders map[string]map[string]string `hcl:"-"`
|
|
CustomResponseHeadersRaw interface{} `hcl:"custom_response_headers"`
|
|
}
|
|
|
|
// AgentAPI allows users to select which parts of the Agent API they want enabled.
|
|
type AgentAPI struct {
|
|
EnableQuit bool `hcl:"enable_quit"`
|
|
}
|
|
|
|
// ProxyAPI allows users to select which parts of the Vault Proxy API they want enabled.
|
|
type ProxyAPI struct {
|
|
EnableQuit bool `hcl:"enable_quit"`
|
|
}
|
|
|
|
func (l *Listener) GoString() string {
|
|
return fmt.Sprintf("*%#v", *l)
|
|
}
|
|
|
|
func (l *Listener) Validate(path string) []ConfigError {
|
|
results := append(ValidateUnusedFields(l.UnusedKeys, path), ValidateUnusedFields(l.Telemetry.UnusedKeys, path)...)
|
|
return append(results, ValidateUnusedFields(l.Profiling.UnusedKeys, path)...)
|
|
}
|
|
|
|
func ParseListeners(result *SharedConfig, list *ast.ObjectList) error {
|
|
var err error
|
|
result.Listeners = make([]*Listener, 0, len(list.Items))
|
|
for i, item := range list.Items {
|
|
var l Listener
|
|
if err := hcl.DecodeObject(&l, item.Val); err != nil {
|
|
return multierror.Prefix(err, fmt.Sprintf("listeners.%d:", i))
|
|
}
|
|
if rendered, err := ParseSingleIPTemplate(l.Address); err != nil {
|
|
return multierror.Prefix(err, fmt.Sprintf("listeners.%d:", i))
|
|
} else {
|
|
l.Address = rendered
|
|
}
|
|
if rendered, err := ParseSingleIPTemplate(l.ClusterAddress); err != nil {
|
|
return multierror.Prefix(err, fmt.Sprintf("listeners.%d:", i))
|
|
} else {
|
|
l.ClusterAddress = rendered
|
|
}
|
|
|
|
// Hacky way, for now, to get the values we want for sanitizing
|
|
var m map[string]interface{}
|
|
if err := hcl.DecodeObject(&m, item.Val); err != nil {
|
|
return multierror.Prefix(err, fmt.Sprintf("listeners.%d:", i))
|
|
}
|
|
l.RawConfig = m
|
|
|
|
// Base values
|
|
{
|
|
switch {
|
|
case l.Type != "":
|
|
case len(item.Keys) == 1:
|
|
l.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
|
|
default:
|
|
return multierror.Prefix(errors.New("listener type must be specified"), fmt.Sprintf("listeners.%d:", i))
|
|
}
|
|
|
|
l.Type = strings.ToLower(l.Type)
|
|
switch l.Type {
|
|
case "tcp", "unix":
|
|
result.found(l.Type, l.Type)
|
|
default:
|
|
return multierror.Prefix(fmt.Errorf("unsupported listener type %q", l.Type), fmt.Sprintf("listeners.%d:", i))
|
|
}
|
|
|
|
if l.PurposeRaw != nil {
|
|
if l.Purpose, err = parseutil.ParseCommaStringSlice(l.PurposeRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("unable to parse 'purpose' in listener type %q: %w", l.Type, err), fmt.Sprintf("listeners.%d:", i))
|
|
}
|
|
for i, v := range l.Purpose {
|
|
l.Purpose[i] = strings.ToLower(v)
|
|
}
|
|
|
|
l.PurposeRaw = nil
|
|
}
|
|
|
|
switch l.Role {
|
|
case "default", "metrics_only", "":
|
|
result.found(l.Type, l.Type)
|
|
default:
|
|
return multierror.Prefix(fmt.Errorf("unsupported listener role %q", l.Role), fmt.Sprintf("listeners.%d:", i))
|
|
}
|
|
}
|
|
|
|
// Request Parameters
|
|
{
|
|
if l.MaxRequestSizeRaw != nil {
|
|
if l.MaxRequestSize, err = parseutil.ParseInt(l.MaxRequestSizeRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("error parsing max_request_size: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.MaxRequestSizeRaw = nil
|
|
}
|
|
|
|
if l.MaxRequestDurationRaw != nil {
|
|
if l.MaxRequestDuration, err = parseutil.ParseDurationSecond(l.MaxRequestDurationRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("error parsing max_request_duration: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
if l.MaxRequestDuration < 0 {
|
|
return multierror.Prefix(errors.New("max_request_duration cannot be negative"), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.MaxRequestDurationRaw = nil
|
|
}
|
|
|
|
if l.RequireRequestHeaderRaw != nil {
|
|
if l.RequireRequestHeader, err = parseutil.ParseBool(l.RequireRequestHeaderRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("invalid value for require_request_header: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.RequireRequestHeaderRaw = nil
|
|
}
|
|
}
|
|
|
|
// TLS Parameters
|
|
{
|
|
if l.TLSDisableRaw != nil {
|
|
if l.TLSDisable, err = parseutil.ParseBool(l.TLSDisableRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("invalid value for tls_disable: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.TLSDisableRaw = nil
|
|
}
|
|
|
|
if l.TLSCipherSuitesRaw != "" {
|
|
if l.TLSCipherSuites, err = tlsutil.ParseCiphers(l.TLSCipherSuitesRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("invalid value for tls_cipher_suites: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
}
|
|
|
|
if l.TLSRequireAndVerifyClientCertRaw != nil {
|
|
if l.TLSRequireAndVerifyClientCert, err = parseutil.ParseBool(l.TLSRequireAndVerifyClientCertRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("invalid value for tls_require_and_verify_client_cert: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.TLSRequireAndVerifyClientCertRaw = nil
|
|
}
|
|
|
|
if l.TLSDisableClientCertsRaw != nil {
|
|
if l.TLSDisableClientCerts, err = parseutil.ParseBool(l.TLSDisableClientCertsRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("invalid value for tls_disable_client_certs: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.TLSDisableClientCertsRaw = nil
|
|
}
|
|
}
|
|
|
|
// HTTP timeouts
|
|
{
|
|
if l.HTTPReadTimeoutRaw != nil {
|
|
if l.HTTPReadTimeout, err = parseutil.ParseDurationSecond(l.HTTPReadTimeoutRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("error parsing http_read_timeout: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.HTTPReadTimeoutRaw = nil
|
|
}
|
|
|
|
if l.HTTPReadHeaderTimeoutRaw != nil {
|
|
if l.HTTPReadHeaderTimeout, err = parseutil.ParseDurationSecond(l.HTTPReadHeaderTimeoutRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("error parsing http_read_header_timeout: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.HTTPReadHeaderTimeoutRaw = nil
|
|
}
|
|
|
|
if l.HTTPWriteTimeoutRaw != nil {
|
|
if l.HTTPWriteTimeout, err = parseutil.ParseDurationSecond(l.HTTPWriteTimeoutRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("error parsing http_write_timeout: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.HTTPWriteTimeoutRaw = nil
|
|
}
|
|
|
|
if l.HTTPIdleTimeoutRaw != nil {
|
|
if l.HTTPIdleTimeout, err = parseutil.ParseDurationSecond(l.HTTPIdleTimeoutRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("error parsing http_idle_timeout: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.HTTPIdleTimeoutRaw = nil
|
|
}
|
|
}
|
|
|
|
// Proxy Protocol config
|
|
{
|
|
if l.ProxyProtocolAuthorizedAddrsRaw != nil {
|
|
if l.ProxyProtocolAuthorizedAddrs, err = parseutil.ParseAddrs(l.ProxyProtocolAuthorizedAddrsRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("error parsing proxy_protocol_authorized_addrs: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
switch l.ProxyProtocolBehavior {
|
|
case "allow_authorized", "deny_authorized":
|
|
if len(l.ProxyProtocolAuthorizedAddrs) == 0 {
|
|
return multierror.Prefix(errors.New("proxy_protocol_behavior set to allow or deny only authorized addresses but no proxy_protocol_authorized_addrs value"), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
}
|
|
|
|
l.ProxyProtocolAuthorizedAddrsRaw = nil
|
|
}
|
|
}
|
|
|
|
// X-Forwarded-For config
|
|
{
|
|
if l.XForwardedForAuthorizedAddrsRaw != nil {
|
|
if l.XForwardedForAuthorizedAddrs, err = parseutil.ParseAddrs(l.XForwardedForAuthorizedAddrsRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("error parsing x_forwarded_for_authorized_addrs: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.XForwardedForAuthorizedAddrsRaw = nil
|
|
}
|
|
|
|
if l.XForwardedForHopSkipsRaw != nil {
|
|
if l.XForwardedForHopSkips, err = parseutil.ParseInt(l.XForwardedForHopSkipsRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("error parsing x_forwarded_for_hop_skips: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
if l.XForwardedForHopSkips < 0 {
|
|
return multierror.Prefix(fmt.Errorf("x_forwarded_for_hop_skips cannot be negative but set to %d", l.XForwardedForHopSkips), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.XForwardedForHopSkipsRaw = nil
|
|
}
|
|
|
|
if l.XForwardedForRejectNotAuthorizedRaw != nil {
|
|
if l.XForwardedForRejectNotAuthorized, err = parseutil.ParseBool(l.XForwardedForRejectNotAuthorizedRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("invalid value for x_forwarded_for_reject_not_authorized: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.XForwardedForRejectNotAuthorizedRaw = nil
|
|
}
|
|
|
|
if l.XForwardedForRejectNotPresentRaw != nil {
|
|
if l.XForwardedForRejectNotPresent, err = parseutil.ParseBool(l.XForwardedForRejectNotPresentRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("invalid value for x_forwarded_for_reject_not_present: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.XForwardedForRejectNotPresentRaw = nil
|
|
}
|
|
}
|
|
|
|
// Telemetry
|
|
{
|
|
if l.Telemetry.UnauthenticatedMetricsAccessRaw != nil {
|
|
if l.Telemetry.UnauthenticatedMetricsAccess, err = parseutil.ParseBool(l.Telemetry.UnauthenticatedMetricsAccessRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("invalid value for telemetry.unauthenticated_metrics_access: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.Telemetry.UnauthenticatedMetricsAccessRaw = nil
|
|
}
|
|
}
|
|
|
|
// Profiling
|
|
{
|
|
if l.Profiling.UnauthenticatedPProfAccessRaw != nil {
|
|
if l.Profiling.UnauthenticatedPProfAccess, err = parseutil.ParseBool(l.Profiling.UnauthenticatedPProfAccessRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("invalid value for profiling.unauthenticated_pprof_access: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.Profiling.UnauthenticatedPProfAccessRaw = nil
|
|
}
|
|
}
|
|
|
|
// InFlight Request logging
|
|
{
|
|
if l.InFlightRequestLogging.UnauthenticatedInFlightAccessRaw != nil {
|
|
if l.InFlightRequestLogging.UnauthenticatedInFlightAccess, err = parseutil.ParseBool(l.InFlightRequestLogging.UnauthenticatedInFlightAccessRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("invalid value for inflight_requests_logging.unauthenticated_in_flight_requests_access: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.InFlightRequestLogging.UnauthenticatedInFlightAccessRaw = ""
|
|
}
|
|
}
|
|
|
|
// CORS
|
|
{
|
|
if l.CorsEnabledRaw != nil {
|
|
if l.CorsEnabled, err = parseutil.ParseBool(l.CorsEnabledRaw); err != nil {
|
|
return multierror.Prefix(fmt.Errorf("invalid value for cors_enabled: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
l.CorsEnabledRaw = nil
|
|
}
|
|
|
|
if strutil.StrListContains(l.CorsAllowedOrigins, "*") && len(l.CorsAllowedOrigins) > 1 {
|
|
return multierror.Prefix(errors.New("cors_allowed_origins must only contain a wildcard or only non-wildcard values"), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
|
|
if len(l.CorsAllowedHeadersRaw) > 0 {
|
|
for _, header := range l.CorsAllowedHeadersRaw {
|
|
l.CorsAllowedHeaders = append(l.CorsAllowedHeaders, textproto.CanonicalMIMEHeaderKey(header))
|
|
}
|
|
}
|
|
}
|
|
|
|
// HTTP Headers
|
|
{
|
|
// if CustomResponseHeadersRaw is nil, we still need to set the default headers
|
|
customHeadersMap, err := ParseCustomResponseHeaders(l.CustomResponseHeadersRaw)
|
|
if err != nil {
|
|
return multierror.Prefix(fmt.Errorf("failed to parse custom_response_headers: %w", err), fmt.Sprintf("listeners.%d", i))
|
|
}
|
|
l.CustomResponseHeaders = customHeadersMap
|
|
l.CustomResponseHeadersRaw = nil
|
|
}
|
|
|
|
result.Listeners = append(result.Listeners, &l)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ParseSingleIPTemplate is used as a helper function to parse out a single IP
|
|
// address from a config parameter.
|
|
// If the input doesn't appear to contain the 'template' format,
|
|
// it will return the specified input unchanged.
|
|
func ParseSingleIPTemplate(ipTmpl string) (string, error) {
|
|
r := regexp.MustCompile("{{.*?}}")
|
|
if !r.MatchString(ipTmpl) {
|
|
return ipTmpl, nil
|
|
}
|
|
|
|
out, err := template.Parse(ipTmpl)
|
|
if err != nil {
|
|
return "", fmt.Errorf("unable to parse address template %q: %v", ipTmpl, err)
|
|
}
|
|
|
|
ips := strings.Split(out, " ")
|
|
switch len(ips) {
|
|
case 0:
|
|
return "", errors.New("no addresses found, please configure one")
|
|
case 1:
|
|
return strings.TrimSpace(ips[0]), nil
|
|
default:
|
|
return "", fmt.Errorf("multiple addresses found (%q), please configure one", out)
|
|
}
|
|
}
|