-
Notifications
You must be signed in to change notification settings - Fork 57
#724 Enable URL liveness check for release builds #729
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
7ada368
b98d700
99c96d8
45e70e3
3678b10
014001f
13c3757
ab462e7
187548c
97932b9
86daf85
3ecb037
ae9c4dc
4056ef4
9e47934
2d8fc46
68b35e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ on: | |
| env: | ||
| BIN_NAME: dcld | ||
| COSMOVISOR_VERSION: 1.5.0 | ||
| URL_LIVENESS_CHECK_ENABLED: true | ||
|
|
||
| jobs: | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| //go:build !dev | ||
|
|
||
| package config | ||
|
|
||
| const URLLivenessCheckEnabled = true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| //go:build dev | ||
|
|
||
| package config | ||
|
|
||
| const URLLivenessCheckEnabled = false |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| package cli | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/zigbee-alliance/distributed-compliance-ledger/internal/config" | ||
| ) | ||
|
|
||
| const ( | ||
| livenessCheckTimeout = 10 * time.Second | ||
| ) | ||
|
|
||
| var allowed4XXStatusCodes = []int{ | ||
| http.StatusUnauthorized, | ||
| http.StatusForbidden, | ||
| http.StatusUnavailableForLegalReasons, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to include also
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added 405. For the second option, as I know, when the server is down, it should return a 5xx code even requested page does not exist
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, if the server is down - you only get server codes, not resource. |
||
| http.StatusMethodNotAllowed, | ||
| } | ||
| var httpClient = &http.Client{Timeout: livenessCheckTimeout} | ||
|
|
||
| func IsLiveURL(u string) bool { | ||
| if !config.URLLivenessCheckEnabled { | ||
| return true | ||
| } | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), livenessCheckTimeout) | ||
| defer cancel() | ||
|
|
||
| req, err := http.NewRequestWithContext(ctx, http.MethodHead, u, nil) | ||
| if err != nil { | ||
| return false | ||
| } | ||
|
|
||
| resp, err := httpClient.Do(req) | ||
| if err != nil { | ||
| return false | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusBadRequest { | ||
| return true | ||
| } | ||
|
|
||
| for _, code := range allowed4XXStatusCodes { | ||
| if code == resp.StatusCode { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| // CheckURLsForLiveness checks the liveness of the given URLs concurrently and | ||
| // returns unreachable URLs as a list. | ||
| // Empty strings are skipped. | ||
| // | ||
| // Returns an empty list if all non-empty URLs are reachable. | ||
| func CheckURLsForLiveness(urls ...string) []string { | ||
| results := make([]string, len(urls)) | ||
|
|
||
| var wg sync.WaitGroup | ||
| for i, u := range urls { | ||
| if u == "" { | ||
| continue | ||
| } | ||
| // Call each URL concurrently | ||
| wg.Add(1) | ||
| go func(i int, u string) { | ||
| defer wg.Done() | ||
| if !IsLiveURL(u) { | ||
| results[i] = u | ||
| } | ||
| }(i, u) | ||
| } | ||
| wg.Wait() | ||
|
|
||
| var unreachable []string | ||
| for _, u := range results { | ||
| if u != "" { | ||
| unreachable = append(unreachable, u) | ||
| } | ||
| } | ||
|
|
||
| return unreachable | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| // Copyright 2020 DSR Corporation | ||
| // | ||
| // 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. | ||
|
|
||
| //go:build !dev | ||
|
|
||
| package cli | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "net/url" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| const unreachableURL = "http://192.0.2.1:1" | ||
|
|
||
| func TestIsLiveURL(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| statusCode int | ||
| want bool | ||
| }{ | ||
| {"200 OK", http.StatusOK, true}, | ||
| {"301 redirect", http.StatusMovedPermanently, true}, | ||
| {"401 unauthorized", http.StatusUnauthorized, true}, | ||
| {"403 forbidden", http.StatusForbidden, true}, | ||
| {"405 method not allowed", http.StatusMethodNotAllowed, true}, | ||
| {"451 unavailable for legal reasons", http.StatusUnavailableForLegalReasons, true}, | ||
| {"404 not found", http.StatusNotFound, false}, | ||
| {"500 internal server error", http.StatusInternalServerError, false}, | ||
| {"502 bad gateway", http.StatusBadGateway, false}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| require.Equal(t, http.MethodHead, r.Method) | ||
| w.WriteHeader(tt.statusCode) | ||
| })) | ||
| defer srv.Close() | ||
|
|
||
| u, err := url.ParseRequestURI(srv.URL) | ||
| require.NoError(t, err) | ||
|
|
||
| require.Equal(t, tt.want, IsLiveURL(u.String())) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestIsLiveURLUnreachable(t *testing.T) { | ||
| u, err := url.ParseRequestURI(unreachableURL) | ||
| require.NoError(t, err) | ||
|
|
||
| require.False(t, IsLiveURL(u.String())) | ||
| } | ||
|
|
||
| func TestCheckURLsForLiveness(t *testing.T) { | ||
| okSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| w.WriteHeader(http.StatusOK) | ||
| })) | ||
| defer okSrv.Close() | ||
|
|
||
| notFoundSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| w.WriteHeader(http.StatusNotFound) | ||
| })) | ||
| defer notFoundSrv.Close() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| urls []string | ||
| want []string | ||
| }{ | ||
| {"no URLs", nil, nil}, | ||
| {"all empty strings", []string{"", "", ""}, nil}, | ||
| {"all reachable", []string{okSrv.URL, okSrv.URL}, nil}, | ||
| {"single unreachable", []string{okSrv.URL, notFoundSrv.URL}, []string{notFoundSrv.URL}}, | ||
| {"empties skipped", []string{"", okSrv.URL, ""}, nil}, | ||
| { | ||
| "multiple unreachable preserve input order", | ||
| []string{okSrv.URL, notFoundSrv.URL, unreachableURL}, | ||
| []string{notFoundSrv.URL, unreachableURL}, | ||
| }, | ||
| { | ||
| "unreachable later in list", | ||
| []string{okSrv.URL, "", notFoundSrv.URL}, | ||
| []string{notFoundSrv.URL}, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| require.Equal(t, tt.want, CheckURLsForLiveness(tt.urls...)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestCheckURLsForLivenessRunsConcurrently(t *testing.T) { | ||
| const handlerDelay = 200 * time.Millisecond | ||
| const concurrentURLs = 5 | ||
|
|
||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| time.Sleep(handlerDelay) | ||
| w.WriteHeader(http.StatusOK) | ||
| })) | ||
| defer srv.Close() | ||
|
|
||
| urls := make([]string, concurrentURLs) | ||
| for i := range urls { | ||
| urls[i] = srv.URL | ||
| } | ||
|
|
||
| start := time.Now() | ||
| require.Empty(t, CheckURLsForLiveness(urls...)) | ||
| elapsed := time.Since(start) | ||
|
|
||
| // Sequential calls would take approximately concurrentURLs*handlerDelay time | ||
| // Concurrent execution should finish in roughly handlerDelay. | ||
| require.Less(t, elapsed, time.Duration(concurrentURLs)*handlerDelay/2, | ||
| "URL checks did not run concurrently (elapsed %s)", elapsed) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,27 +49,30 @@ func requiredIfBit0Set(fl validator.FieldLevel) bool { | |
| } | ||
|
|
||
| func isValidHttpOrHttpsUrl(fl validator.FieldLevel) bool { //nolint:stylecheck | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reachability checks cannot be implemented here, because it's part of
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update, but it is called(should be called) before the broadcasting txn to the DCLD nodes(before |
||
| return _validURL(fl, "http", "https") | ||
| return validURL(fl, "http", "https") | ||
| } | ||
|
|
||
| func isValidHttpsUrl(fl validator.FieldLevel) bool { //nolint:stylecheck | ||
| return _validURL(fl, "https") | ||
| return validURL(fl, "https") | ||
| } | ||
|
|
||
| func _validURL(fl validator.FieldLevel, allowedSchemas ...string) bool { | ||
| func validURL(fl validator.FieldLevel, allowedSchemes ...string) bool { | ||
| raw := fl.Field().String() | ||
| // Field is empty or omitempty is set, skip checks | ||
| if raw == "" { | ||
| return true | ||
| } | ||
|
|
||
| u, _ := url.Parse(raw) | ||
| if u.Host == "" { | ||
| u, err := url.ParseRequestURI(raw) | ||
| if err != nil || u.Host == "" { | ||
| return false | ||
| } | ||
|
|
||
| for _, schema := range allowedSchemas { | ||
| if u.Scheme == schema { | ||
| return isSchemeAllowed(u.Scheme, allowedSchemes) | ||
| } | ||
|
|
||
| func isSchemeAllowed(scheme string, allowed []string) bool { | ||
| for _, s := range allowed { | ||
| if scheme == s { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move these files into variable just like
PACKAGES?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, because
url_liveness_test.goshould be executed withoutdevtag