| 
									
										
										
										
											2019-06-30 16:07:58 -06:00
										 |  |  | // 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. | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-06-14 11:58:28 -06:00
										 |  |  | package caddy | 
					
						
							| 
									
										
										
										
											2019-03-26 19:42:52 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2019-11-04 12:05:20 -07:00
										 |  |  | 	"encoding/json" | 
					
						
							| 
									
										
										
										
											2022-07-13 06:23:55 +12:00
										 |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2022-07-07 07:50:07 +12:00
										 |  |  | 	"net/http" | 
					
						
							| 
									
										
										
										
											2019-11-04 12:05:20 -07:00
										 |  |  | 	"reflect" | 
					
						
							| 
									
										
										
										
											2021-08-16 17:04:47 -04:00
										 |  |  | 	"sync" | 
					
						
							| 
									
										
										
										
											2019-03-26 19:42:52 -06:00
										 |  |  | 	"testing" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-16 17:04:47 -04:00
										 |  |  | var testCfg = []byte(`{ | 
					
						
							|  |  |  | 			"apps": { | 
					
						
							|  |  |  | 				"http": { | 
					
						
							|  |  |  | 					"servers": { | 
					
						
							|  |  |  | 						"myserver": { | 
					
						
							|  |  |  | 							"listen": ["tcp/localhost:8080-8084"], | 
					
						
							|  |  |  | 							"read_timeout": "30s" | 
					
						
							|  |  |  | 						}, | 
					
						
							|  |  |  | 						"yourserver": { | 
					
						
							|  |  |  | 							"listen": ["127.0.0.1:5000"], | 
					
						
							|  |  |  | 							"read_header_timeout": "15s" | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		`) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-04 12:05:20 -07:00
										 |  |  | func TestUnsyncedConfigAccess(t *testing.T) { | 
					
						
							|  |  |  | 	// each test is performed in sequence, so | 
					
						
							|  |  |  | 	// each change builds on the previous ones; | 
					
						
							|  |  |  | 	// the config is not reset between tests | 
					
						
							|  |  |  | 	for i, tc := range []struct { | 
					
						
							|  |  |  | 		method    string | 
					
						
							|  |  |  | 		path      string // rawConfigKey will be prepended | 
					
						
							|  |  |  | 		payload   string | 
					
						
							|  |  |  | 		expect    string // JSON representation of what the whole config is expected to be after the request | 
					
						
							|  |  |  | 		shouldErr bool | 
					
						
							|  |  |  | 	}{ | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			method:  "POST", | 
					
						
							|  |  |  | 			path:    "", | 
					
						
							|  |  |  | 			payload: `{"foo": "bar", "list": ["a", "b", "c"]}`, // starting value | 
					
						
							|  |  |  | 			expect:  `{"foo": "bar", "list": ["a", "b", "c"]}`, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			method:  "POST", | 
					
						
							|  |  |  | 			path:    "/foo", | 
					
						
							|  |  |  | 			payload: `"jet"`, | 
					
						
							|  |  |  | 			expect:  `{"foo": "jet", "list": ["a", "b", "c"]}`, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			method:  "POST", | 
					
						
							|  |  |  | 			path:    "/bar", | 
					
						
							|  |  |  | 			payload: `{"aa": "bb", "qq": "zz"}`, | 
					
						
							|  |  |  | 			expect:  `{"foo": "jet", "bar": {"aa": "bb", "qq": "zz"}, "list": ["a", "b", "c"]}`, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			method: "DELETE", | 
					
						
							|  |  |  | 			path:   "/bar/qq", | 
					
						
							|  |  |  | 			expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`, | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2023-10-11 22:24:29 +02:00
										 |  |  | 		{ | 
					
						
							|  |  |  | 			method:    "DELETE", | 
					
						
							|  |  |  | 			path:      "/bar/qq", | 
					
						
							|  |  |  | 			expect:    `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`, | 
					
						
							|  |  |  | 			shouldErr: true, | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2019-11-04 12:05:20 -07:00
										 |  |  | 		{ | 
					
						
							|  |  |  | 			method:  "POST", | 
					
						
							|  |  |  | 			path:    "/list", | 
					
						
							|  |  |  | 			payload: `"e"`, | 
					
						
							|  |  |  | 			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			method:  "PUT", | 
					
						
							|  |  |  | 			path:    "/list/3", | 
					
						
							|  |  |  | 			payload: `"d"`, | 
					
						
							|  |  |  | 			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e"]}`, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			method: "DELETE", | 
					
						
							|  |  |  | 			path:   "/list/3", | 
					
						
							|  |  |  | 			expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			method:  "PATCH", | 
					
						
							|  |  |  | 			path:    "/list/3", | 
					
						
							|  |  |  | 			payload: `"d"`, | 
					
						
							|  |  |  | 			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d"]}`, | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2019-12-17 10:11:45 -07:00
										 |  |  | 		{ | 
					
						
							|  |  |  | 			method:  "POST", | 
					
						
							|  |  |  | 			path:    "/list/...", | 
					
						
							|  |  |  | 			payload: `["e", "f", "g"]`, | 
					
						
							|  |  |  | 			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e", "f", "g"]}`, | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2019-11-04 12:05:20 -07:00
										 |  |  | 	} { | 
					
						
							|  |  |  | 		err := unsyncedConfigAccess(tc.method, rawConfigKey+tc.path, []byte(tc.payload), nil) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if tc.shouldErr && err == nil { | 
					
						
							|  |  |  | 			t.Fatalf("Test %d: Expected error return value, but got: %v", i, err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if !tc.shouldErr && err != nil { | 
					
						
							|  |  |  | 			t.Fatalf("Test %d: Should not have had error return value, but got: %v", i, err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// decode the expected config so we can do a convenient DeepEqual | 
					
						
							| 
									
										
										
										
											2022-08-02 16:39:09 -04:00
										 |  |  | 		var expectedDecoded any | 
					
						
							| 
									
										
										
										
											2019-11-04 12:05:20 -07:00
										 |  |  | 		err = json.Unmarshal([]byte(tc.expect), &expectedDecoded) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// make sure the resulting config is as we expect it | 
					
						
							|  |  |  | 		if !reflect.DeepEqual(rawCfg[rawConfigKey], expectedDecoded) { | 
					
						
							|  |  |  | 			t.Fatalf("Test %d:\nExpected:\n\t%#v\nActual:\n\t%#v", | 
					
						
							|  |  |  | 				i, expectedDecoded, rawCfg[rawConfigKey]) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-16 17:04:47 -04:00
										 |  |  | // TestLoadConcurrent exercises Load under concurrent conditions | 
					
						
							|  |  |  | // and is most useful under test with `-race` enabled. | 
					
						
							|  |  |  | func TestLoadConcurrent(t *testing.T) { | 
					
						
							|  |  |  | 	var wg sync.WaitGroup | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for i := 0; i < 100; i++ { | 
					
						
							|  |  |  | 		wg.Add(1) | 
					
						
							|  |  |  | 		go func() { | 
					
						
							|  |  |  | 			_ = Load(testCfg, true) | 
					
						
							|  |  |  | 			wg.Done() | 
					
						
							|  |  |  | 		}() | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	wg.Wait() | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-07 07:50:07 +12:00
										 |  |  | type fooModule struct { | 
					
						
							|  |  |  | 	IntField int | 
					
						
							|  |  |  | 	StrField string | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (fooModule) CaddyModule() ModuleInfo { | 
					
						
							|  |  |  | 	return ModuleInfo{ | 
					
						
							|  |  |  | 		ID:  "foo", | 
					
						
							|  |  |  | 		New: func() Module { return new(fooModule) }, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | func (fooModule) Start() error { return nil } | 
					
						
							|  |  |  | func (fooModule) Stop() error  { return nil } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func TestETags(t *testing.T) { | 
					
						
							|  |  |  | 	RegisterModule(fooModule{}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-20 10:09:04 -04:00
										 |  |  | 	if err := Load([]byte(`{"admin": {"listen": "localhost:2999"}, "apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil { | 
					
						
							| 
									
										
										
										
											2022-07-07 07:50:07 +12:00
										 |  |  | 		t.Fatalf("loading: %s", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	const key = "/" + rawConfigKey + "/apps/foo" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// try update the config with the wrong etag | 
					
						
							| 
									
										
										
										
											2022-07-13 06:23:55 +12:00
										 |  |  | 	err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false) | 
					
						
							| 
									
										
										
										
											2022-07-07 07:50:07 +12:00
										 |  |  | 	if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed { | 
					
						
							|  |  |  | 		t.Fatalf("expected precondition failed; got %v", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// get the etag | 
					
						
							|  |  |  | 	hash := etagHasher() | 
					
						
							|  |  |  | 	if err := readConfig(key, hash); err != nil { | 
					
						
							|  |  |  | 		t.Fatalf("reading: %s", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// do the same update with the correct key | 
					
						
							| 
									
										
										
										
											2022-07-13 06:23:55 +12:00
										 |  |  | 	err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false) | 
					
						
							| 
									
										
										
										
											2022-07-07 07:50:07 +12:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		t.Fatalf("expected update to work; got %v", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// now try another update. The hash should no longer match and we should get precondition failed | 
					
						
							| 
									
										
										
										
											2022-07-13 06:23:55 +12:00
										 |  |  | 	err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false) | 
					
						
							| 
									
										
										
										
											2022-07-07 07:50:07 +12:00
										 |  |  | 	if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed { | 
					
						
							|  |  |  | 		t.Fatalf("expected precondition failed; got %v", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-03-26 19:42:52 -06:00
										 |  |  | func BenchmarkLoad(b *testing.B) { | 
					
						
							|  |  |  | 	for i := 0; i < b.N; i++ { | 
					
						
							| 
									
										
										
										
											2021-08-16 17:04:47 -04:00
										 |  |  | 		Load(testCfg, true) | 
					
						
							| 
									
										
										
										
											2019-03-26 19:42:52 -06:00
										 |  |  | 	} | 
					
						
							|  |  |  | } |