mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-04 03:27:23 -05:00 
			
		
		
		
	
		
			Some checks are pending
		
		
	
	Tests / test (./cmd/caddy/caddy, ~1.21.0, macos-14, 0, 1.21, mac) (push) Waiting to run
				
			Tests / test (./cmd/caddy/caddy, ~1.21.0, ubuntu-latest, 0, 1.21, linux) (push) Waiting to run
				
			Tests / test (./cmd/caddy/caddy, ~1.22.3, macos-14, 0, 1.22, mac) (push) Waiting to run
				
			Tests / test (./cmd/caddy/caddy, ~1.22.3, ubuntu-latest, 0, 1.22, linux) (push) Waiting to run
				
			Tests / test (./cmd/caddy/caddy.exe, ~1.21.0, windows-latest, True, 1.21, windows) (push) Waiting to run
				
			Tests / test (./cmd/caddy/caddy.exe, ~1.22.3, windows-latest, True, 1.22, windows) (push) Waiting to run
				
			Tests / test (s390x on IBM Z) (push) Waiting to run
				
			Tests / goreleaser-check (push) Waiting to run
				
			Cross-Build / build (~1.22.3, 1.22, aix) (push) Waiting to run
				
			Cross-Build / build (~1.22.3, 1.22, darwin) (push) Waiting to run
				
			Cross-Build / build (~1.22.3, 1.22, dragonfly) (push) Waiting to run
				
			Cross-Build / build (~1.22.3, 1.22, freebsd) (push) Waiting to run
				
			Cross-Build / build (~1.22.3, 1.22, illumos) (push) Waiting to run
				
			Cross-Build / build (~1.22.3, 1.22, linux) (push) Waiting to run
				
			Cross-Build / build (~1.22.3, 1.22, netbsd) (push) Waiting to run
				
			Cross-Build / build (~1.22.3, 1.22, openbsd) (push) Waiting to run
				
			Cross-Build / build (~1.22.3, 1.22, solaris) (push) Waiting to run
				
			Cross-Build / build (~1.22.3, 1.22, windows) (push) Waiting to run
				
			Lint / lint (macos-14, mac) (push) Waiting to run
				
			Lint / lint (ubuntu-latest, linux) (push) Waiting to run
				
			Lint / lint (windows-latest, windows) (push) Waiting to run
				
			Lint / govulncheck (push) Waiting to run
				
			Co-authored-by: Kanashimia <chad@redpilled.dev>
		
			
				
	
	
		
			533 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			533 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2015 Matthew Holt and The Caddy Authors
 | 
						|
//
 | 
						|
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
// you may not use this file except in compliance with the License.
 | 
						|
// You may obtain a copy of the License at
 | 
						|
//
 | 
						|
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
//
 | 
						|
// Unless required by applicable law or agreed to in writing, software
 | 
						|
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
// See the License for the specific language governing permissions and
 | 
						|
// limitations under the License.
 | 
						|
 | 
						|
package caddy
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"runtime"
 | 
						|
	"sync"
 | 
						|
	"testing"
 | 
						|
)
 | 
						|
 | 
						|
func TestReplacer(t *testing.T) {
 | 
						|
	type testCase struct {
 | 
						|
		input, expect, empty string
 | 
						|
	}
 | 
						|
 | 
						|
	rep := testReplacer()
 | 
						|
 | 
						|
	// ReplaceAll
 | 
						|
	for i, tc := range []testCase{
 | 
						|
		{
 | 
						|
			input:  "{",
 | 
						|
			expect: "{",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `\{`,
 | 
						|
			expect: `{`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  "foo{",
 | 
						|
			expect: "foo{",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `foo\{`,
 | 
						|
			expect: `foo{`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  "foo{bar",
 | 
						|
			expect: "foo{bar",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `foo\{bar`,
 | 
						|
			expect: `foo{bar`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  "foo{bar}",
 | 
						|
			expect: "foo",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `foo\{bar\}`,
 | 
						|
			expect: `foo{bar}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  "}",
 | 
						|
			expect: "}",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `\}`,
 | 
						|
			expect: `}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  "{}",
 | 
						|
			expect: "",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `\{\}`,
 | 
						|
			expect: `{}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `{"json": "object"}`,
 | 
						|
			expect: "",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `\{"json": "object"}`,
 | 
						|
			expect: `{"json": "object"}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `\{"json": "object"\}`,
 | 
						|
			expect: `{"json": "object"}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `\{"json": "object{bar}"\}`,
 | 
						|
			expect: `{"json": "object"}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `\{"json": \{"nested": "object"\}\}`,
 | 
						|
			expect: `{"json": {"nested": "object"}}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `\{"json": \{"nested": "{bar}"\}\}`,
 | 
						|
			expect: `{"json": {"nested": ""}}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `pre \{"json": \{"nested": "{bar}"\}\}`,
 | 
						|
			expect: `pre {"json": {"nested": ""}}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `\{"json": \{"nested": "{bar}"\}\} post`,
 | 
						|
			expect: `{"json": {"nested": ""}} post`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `pre \{"json": \{"nested": "{bar}"\}\} post`,
 | 
						|
			expect: `pre {"json": {"nested": ""}} post`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `{{`,
 | 
						|
			expect: "{{",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `{{}`,
 | 
						|
			expect: "",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `{"json": "object"\}`,
 | 
						|
			expect: "",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `{unknown}`,
 | 
						|
			empty:  "-",
 | 
						|
			expect: "-",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `back\slashes`,
 | 
						|
			expect: `back\slashes`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `double back\\slashes`,
 | 
						|
			expect: `double back\\slashes`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `placeholder {with \{ brace} in name`,
 | 
						|
			expect: `placeholder  in name`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `placeholder {with \} brace} in name`,
 | 
						|
			expect: `placeholder  in name`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `placeholder {with \} \} braces} in name`,
 | 
						|
			expect: `placeholder  in name`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `\{'group':'default','max_age':3600,'endpoints':[\{'url':'https://some.domain.local/a/d/g'\}],'include_subdomains':true\}`,
 | 
						|
			expect: `{'group':'default','max_age':3600,'endpoints':[{'url':'https://some.domain.local/a/d/g'}],'include_subdomains':true}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `{}{}{}{\\\\}\\\\`,
 | 
						|
			expect: `{\\\}\\\\`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  string([]byte{0x26, 0x00, 0x83, 0x7B, 0x84, 0x07, 0x5C, 0x7D, 0x84}),
 | 
						|
			expect: string([]byte{0x26, 0x00, 0x83, 0x7B, 0x84, 0x07, 0x7D, 0x84}),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			input:  `\\}`,
 | 
						|
			expect: `\}`,
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		actual := rep.ReplaceAll(tc.input, tc.empty)
 | 
						|
		if actual != tc.expect {
 | 
						|
			t.Errorf("Test %d: '%s': expected '%s' but got '%s'",
 | 
						|
				i, tc.input, tc.expect, actual)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestReplacerSet(t *testing.T) {
 | 
						|
	rep := testReplacer()
 | 
						|
 | 
						|
	for _, tc := range []struct {
 | 
						|
		variable string
 | 
						|
		value    any
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			variable: "test1",
 | 
						|
			value:    "val1",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "asdf",
 | 
						|
			value:    "123",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "numbers",
 | 
						|
			value:    123.456,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "äöü",
 | 
						|
			value:    "öö_äü",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "with space",
 | 
						|
			value:    "space value",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "1",
 | 
						|
			value:    "test-123",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "mySuper_IP",
 | 
						|
			value:    "1.2.3.4",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "testEmpty",
 | 
						|
			value:    "",
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		rep.Set(tc.variable, tc.value)
 | 
						|
 | 
						|
		// test if key is added
 | 
						|
		if val, ok := rep.static[tc.variable]; ok {
 | 
						|
			if val != tc.value {
 | 
						|
				t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			t.Errorf("Expected existing key '%s' found nothing", tc.variable)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// test if all keys are still there (by length)
 | 
						|
	length := len(rep.static)
 | 
						|
	if len(rep.static) != 8 {
 | 
						|
		t.Errorf("Expected length '%v' got '%v'", 7, length)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestReplacerReplaceKnown(t *testing.T) {
 | 
						|
	rep := Replacer{
 | 
						|
		mapMutex: &sync.RWMutex{},
 | 
						|
		providers: []replacementProvider{
 | 
						|
			// split our possible vars to two functions (to test if both functions are called)
 | 
						|
			ReplacerFunc(func(key string) (val any, ok bool) {
 | 
						|
				switch key {
 | 
						|
				case "test1":
 | 
						|
					return "val1", true
 | 
						|
				case "asdf":
 | 
						|
					return "123", true
 | 
						|
				case "äöü":
 | 
						|
					return "öö_äü", true
 | 
						|
				case "with space":
 | 
						|
					return "space value", true
 | 
						|
				default:
 | 
						|
					return "NOOO", false
 | 
						|
				}
 | 
						|
			}),
 | 
						|
			ReplacerFunc(func(key string) (val any, ok bool) {
 | 
						|
				switch key {
 | 
						|
				case "1":
 | 
						|
					return "test-123", true
 | 
						|
				case "mySuper_IP":
 | 
						|
					return "1.2.3.4", true
 | 
						|
				case "testEmpty":
 | 
						|
					return "", true
 | 
						|
				default:
 | 
						|
					return "NOOO", false
 | 
						|
				}
 | 
						|
			}),
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range []struct {
 | 
						|
		testInput string
 | 
						|
		expected  string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			// test vars without space
 | 
						|
			testInput: "{test1}{asdf}{äöü}{1}{with space}{mySuper_IP}",
 | 
						|
			expected:  "val1123öö_äütest-123space value1.2.3.4",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			// test vars with space
 | 
						|
			testInput: "{test1} {asdf} {äöü} {1} {with space} {mySuper_IP} ",
 | 
						|
			expected:  "val1 123 öö_äü test-123 space value 1.2.3.4 ",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			// test with empty val
 | 
						|
			testInput: "{test1} {testEmpty} {asdf} {1} ",
 | 
						|
			expected:  "val1 EMPTY 123 test-123 ",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			// test vars with not finished placeholders
 | 
						|
			testInput: "{te{test1}{as{{df{1}",
 | 
						|
			expected:  "{teval1{as{{dftest-123",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			// test with non existing vars
 | 
						|
			testInput: "{test1} {nope} {1} ",
 | 
						|
			expected:  "val1 {nope} test-123 ",
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		actual := rep.ReplaceKnown(tc.testInput, "EMPTY")
 | 
						|
 | 
						|
		// test if all are replaced as expected
 | 
						|
		if actual != tc.expected {
 | 
						|
			t.Errorf("Expected '%s' got '%s' for '%s'", tc.expected, actual, tc.testInput)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestReplacerDelete(t *testing.T) {
 | 
						|
	rep := Replacer{
 | 
						|
		mapMutex: &sync.RWMutex{},
 | 
						|
		static: map[string]any{
 | 
						|
			"key1": "val1",
 | 
						|
			"key2": "val2",
 | 
						|
			"key3": "val3",
 | 
						|
			"key4": "val4",
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	startLen := len(rep.static)
 | 
						|
 | 
						|
	toDel := []string{
 | 
						|
		"key2", "key4",
 | 
						|
	}
 | 
						|
 | 
						|
	for _, key := range toDel {
 | 
						|
		rep.Delete(key)
 | 
						|
 | 
						|
		// test if key is removed from static map
 | 
						|
		if _, ok := rep.static[key]; ok {
 | 
						|
			t.Errorf("Expected '%s' to be removed. It is still in static map.", key)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// check if static slice is smaller
 | 
						|
	expected := startLen - len(toDel)
 | 
						|
	actual := len(rep.static)
 | 
						|
	if len(rep.static) != expected {
 | 
						|
		t.Errorf("Expected length '%v' got length '%v'", expected, actual)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestReplacerMap(t *testing.T) {
 | 
						|
	rep := testReplacer()
 | 
						|
 | 
						|
	for i, tc := range []ReplacerFunc{
 | 
						|
		func(key string) (val any, ok bool) {
 | 
						|
			return "", false
 | 
						|
		},
 | 
						|
		func(key string) (val any, ok bool) {
 | 
						|
			return "", false
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		rep.Map(tc)
 | 
						|
 | 
						|
		// test if function (which listens on specific key) is added by checking length
 | 
						|
		if len(rep.providers) == i+1 {
 | 
						|
			// check if the last function is the one we just added
 | 
						|
			pTc := fmt.Sprintf("%p", tc)
 | 
						|
			pRep := fmt.Sprintf("%p", rep.providers[i])
 | 
						|
			if pRep != pTc {
 | 
						|
				t.Errorf("Expected func pointer '%s' got '%s'", pTc, pRep)
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			t.Errorf("Expected providers length '%v' got length '%v'", i+1, len(rep.providers))
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestReplacerNew(t *testing.T) {
 | 
						|
	repl := NewReplacer()
 | 
						|
 | 
						|
	if len(repl.providers) != 3 {
 | 
						|
		t.Errorf("Expected providers length '%v' got length '%v'", 3, len(repl.providers))
 | 
						|
	}
 | 
						|
 | 
						|
	// test if default global replacements are added as the first provider
 | 
						|
	hostname, _ := os.Hostname()
 | 
						|
	wd, _ := os.Getwd()
 | 
						|
	os.Setenv("CADDY_REPLACER_TEST", "envtest")
 | 
						|
	defer os.Setenv("CADDY_REPLACER_TEST", "")
 | 
						|
 | 
						|
	for _, tc := range []struct {
 | 
						|
		variable string
 | 
						|
		value    string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			variable: "system.hostname",
 | 
						|
			value:    hostname,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "system.slash",
 | 
						|
			value:    string(filepath.Separator),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "system.os",
 | 
						|
			value:    runtime.GOOS,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "system.arch",
 | 
						|
			value:    runtime.GOARCH,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "system.wd",
 | 
						|
			value:    wd,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "env.CADDY_REPLACER_TEST",
 | 
						|
			value:    "envtest",
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		if val, ok := repl.providers[0].replace(tc.variable); ok {
 | 
						|
			if val != tc.value {
 | 
						|
				t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			t.Errorf("Expected key '%s' to be recognized by first provider", tc.variable)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// test if file provider is added as the second provider
 | 
						|
	for _, tc := range []struct {
 | 
						|
		variable string
 | 
						|
		value    string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			variable: "file.caddytest/integration/testdata/foo.txt",
 | 
						|
			value:    "foo",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "file.caddytest/integration/testdata/foo_with_trailing_newline.txt",
 | 
						|
			value:    "foo",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "file.caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt",
 | 
						|
			value:    "foo" + getEOL(),
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		if val, ok := repl.providers[1].replace(tc.variable); ok {
 | 
						|
			if val != tc.value {
 | 
						|
				t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			t.Errorf("Expected key '%s' to be recognized by second provider", tc.variable)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func getEOL() string {
 | 
						|
	if os.PathSeparator == '\\' {
 | 
						|
		return "\r\n" // Windows EOL
 | 
						|
	}
 | 
						|
	return "\n" // Unix and modern macOS EOL
 | 
						|
}
 | 
						|
 | 
						|
func TestReplacerNewWithoutFile(t *testing.T) {
 | 
						|
	repl := NewReplacer().WithoutFile()
 | 
						|
 | 
						|
	for _, tc := range []struct {
 | 
						|
		variable string
 | 
						|
		value    string
 | 
						|
		notFound bool
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			variable: "file.caddytest/integration/testdata/foo.txt",
 | 
						|
			notFound: true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			variable: "system.os",
 | 
						|
			value:    runtime.GOOS,
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		if val, ok := repl.Get(tc.variable); ok && !tc.notFound {
 | 
						|
			if val != tc.value {
 | 
						|
				t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
 | 
						|
			}
 | 
						|
		} else if !tc.notFound {
 | 
						|
			t.Errorf("Expected key '%s' to be recognized", tc.variable)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func BenchmarkReplacer(b *testing.B) {
 | 
						|
	type testCase struct {
 | 
						|
		name, input, empty string
 | 
						|
	}
 | 
						|
 | 
						|
	rep := testReplacer()
 | 
						|
	rep.Set("str", "a string")
 | 
						|
	rep.Set("int", 123.456)
 | 
						|
 | 
						|
	for _, bm := range []testCase{
 | 
						|
		{
 | 
						|
			name:  "no placeholder",
 | 
						|
			input: `simple string`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:  "string replacement",
 | 
						|
			input: `str={str}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:  "int replacement",
 | 
						|
			input: `int={int}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:  "placeholder",
 | 
						|
			input: `{"json": "object"}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:  "escaped placeholder",
 | 
						|
			input: `\{"json": \{"nested": "{bar}"\}\}`,
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		b.Run(bm.name, func(b *testing.B) {
 | 
						|
			for i := 0; i < b.N; i++ {
 | 
						|
				rep.ReplaceAll(bm.input, bm.empty)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func testReplacer() Replacer {
 | 
						|
	return Replacer{
 | 
						|
		providers: make([]replacementProvider, 0),
 | 
						|
		static:    make(map[string]any),
 | 
						|
		mapMutex:  &sync.RWMutex{},
 | 
						|
	}
 | 
						|
}
 |