diff --git a/README.md b/README.md index 7fe3afc7572317d95781d502c0a964ffebac8327..a205d65286b57f49f5d3bbe8aeddf92839bcf7b9 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ The service responds with the following JSON: { "bridge_results": { "BRIDGE_LINE_1": { - "functional: BOOL, + "functional": BOOL, + "last_tested": "STRING", "error": "STRING", (only present if "functional" is false) }, ... @@ -68,10 +69,12 @@ The service responds with the following JSON: } In a nutshell, the "bridge_results" dictionary maps bridge lines (as they were -provided in the request) to a dictionary consisting of two keys: "functional" +provided in the request) to a dictionary consisting of three keys: "functional" is set to "true" if tor could fetch the bridge's descriptor. If tor was unable to fetch the bridge's descriptor, "functional" is set to "false" and the -"error" key maps to an error string. +"error" key maps to an error string. The key "last_tested" maps to a string +representation (in ISO 8601 format) of the UTC time and date the bridge was +last tested. In addition to the "bridge_results" dictionary, the response may contain an optional "error" key if the entire test failed (e.g. if bridgestrap failed to @@ -90,10 +93,12 @@ Here are a few examples: { "bridge_results": { "obfs4 1.2.3.4:1234 cert=fJRlJc0T7i2Qkw3SyLQq+M6iTGs9ghLHK65LBy/MQewXJpNOKFq63Om1JHVkLlrmEBbX1w iat-mode=0": { - "functional": true + "functional": true, + "last_tested": "2020-11-12T19:42:16.736853122Z" }, "1.2.3.4:1234": { "functional": false, + "last_tested": "2020-11-10T09:44:45.877531581Z", "error": "timed out waiting for bridge descriptor" } }, @@ -104,6 +109,7 @@ Here are a few examples: "bridge_results": { "1.2.3.4:1234 1234567890ABCDEF1234567890ABCDEF12345678": { "functional": false, + "last_tested": "2020-11-10T09:44:45.877531581Z", "error": "timed out waiting for bridge descriptor" } }, diff --git a/cache.go b/cache.go index 2f81b5381681b28210b30bb32318759d162ec16d..e357e6abacc5a7874dc2e4a32ab34497e5c43ba6 100644 --- a/cache.go +++ b/cache.go @@ -117,8 +117,9 @@ func (tc *TestCache) IsCached(bridgeLine string) *CacheEntry { return r } -// AddEntry adds an entry for the given bridge and test result to our cache. -func (tc *TestCache) AddEntry(bridgeLine string, result error) { +// AddEntry adds an entry for the given bridge, test result, and test time to +// our cache. +func (tc *TestCache) AddEntry(bridgeLine string, result error, lastTested time.Time) { addrPort, err := bridgeLineToAddrPort(bridgeLine) if err != nil { @@ -132,6 +133,6 @@ func (tc *TestCache) AddEntry(bridgeLine string, result error) { errorStr = result.Error() } cacheMutex.Lock() - (*tc)[addrPort] = &CacheEntry{errorStr, time.Now()} + (*tc)[addrPort] = &CacheEntry{errorStr, lastTested} cacheMutex.Unlock() } diff --git a/cache_test.go b/cache_test.go index 06e1ad95f7288d3764a859f0bbb592d34de779f3..5fad2d62a3ba9a44b6aac72e1a76d8dfadc53da1 100644 --- a/cache_test.go +++ b/cache_test.go @@ -21,14 +21,14 @@ func TestCacheFunctions(t *testing.T) { t.Errorf("Cache is empty but marks bridge line as existing.") } - cache.AddEntry(bridgeLine, nil) + cache.AddEntry(bridgeLine, nil, time.Now().UTC()) e = cache.IsCached(bridgeLine) if e == nil { t.Errorf("Could not retrieve existing element from cache.") } testError := fmt.Errorf("bridge is on fire") - cache.AddEntry(bridgeLine, testError) + cache.AddEntry(bridgeLine, testError, time.Now().UTC()) e = cache.IsCached(bridgeLine) if e.Error != testError.Error() { t.Errorf("Got test result %q but expected %q.", e.Error, testError) @@ -45,7 +45,7 @@ func TestCacheExpiration(t *testing.T) { cache[bridgeLine1] = &CacheEntry{"", expiry} bridgeLine2 := "2.2.2.2:2222" - cache[bridgeLine2] = &CacheEntry{"", time.Now()} + cache[bridgeLine2] = &CacheEntry{"", time.Now().UTC()} e := cache.IsCached(bridgeLine1) if e != nil { @@ -72,7 +72,7 @@ func BenchmarkIsCached(b *testing.B) { numCacheEntries := 10000 cache := make(TestCache) for i := 0; i < numCacheEntries; i++ { - cache.AddEntry(getRandAddrPort(), getRandError()) + cache.AddEntry(getRandAddrPort(), getRandError(), time.Now().UTC()) } // How long does it take to iterate over numCacheEntries cache entries? @@ -86,8 +86,8 @@ func TestCacheSerialisation(t *testing.T) { cache := make(TestCache) testError := fmt.Errorf("foo") - cache.AddEntry("1.1.1.1:1", testError) - cache.AddEntry("2.2.2.2:2", fmt.Errorf("bar")) + cache.AddEntry("1.1.1.1:1", testError, time.Now().UTC()) + cache.AddEntry("2.2.2.2:2", fmt.Errorf("bar"), time.Now().UTC()) tmpFh, err := ioutil.TempFile(os.TempDir(), "cache-file-") if err != nil { @@ -132,7 +132,7 @@ func TestCacheConcurrency(t *testing.T) { byte((i>>16)&0xff), byte((i>>8)&0xff), byte(i&0xff)) - cache.AddEntry(fmt.Sprintf("%s:1234", ipAddr.String()), nil) + cache.AddEntry(fmt.Sprintf("%s:1234", ipAddr.String()), nil, time.Now().UTC()) } doneWriting <- true }() diff --git a/handlers.go b/handlers.go index 77ff2a146e4bf78bdcd56cf875008e8415ad06bf..c55be6e70eee7753fdfce0c80475e4d964dd3c1c 100644 --- a/handlers.go +++ b/handlers.go @@ -20,8 +20,9 @@ var FailurePage string // BridgeTest represents the result of a bridge test, sent back to the client // as JSON object. type BridgeTest struct { - Functional bool `json:"functional"` - Error string `json:"error,omitempty"` + Functional bool `json:"functional"` + LastTested time.Time `json:"last_tested"` + Error string `json:"error,omitempty"` } // TestResult represents the result of a test. @@ -98,8 +99,11 @@ func testBridgeLines(bridgeLines []string) *TestResult { for _, bridgeLine := range bridgeLines { if entry := cache.IsCached(bridgeLine); entry != nil { numCached++ - result.Bridges[bridgeLine] = &BridgeTest{Functional: entry.Error == "", - Error: entry.Error} + result.Bridges[bridgeLine] = &BridgeTest{ + Functional: entry.Error == "", + LastTested: entry.Time, + Error: entry.Error, + } } else { remainingBridgeLines = append(remainingBridgeLines, bridgeLine) } @@ -116,7 +120,7 @@ func testBridgeLines(bridgeLines []string) *TestResult { // Cache partial test results and add them to our existing result object. for bridgeLine, bridgeTest := range partialResult.Bridges { - cache.AddEntry(bridgeLine, errors.New(bridgeTest.Error)) + cache.AddEntry(bridgeLine, errors.New(bridgeTest.Error), bridgeTest.LastTested) result.Bridges[bridgeLine] = bridgeTest } } else { diff --git a/tor.go b/tor.go index cc5590a927d506ca86eeddf0082d4637daa30650..e87795738552f6df425ccb007a1396884ceef441 100644 --- a/tor.go +++ b/tor.go @@ -236,10 +236,17 @@ func (c *TorContext) TestBridgeLines(bridgeLines []string) *TestResult { parser.Feed(line) if parser.State == BridgeStateSuccess { log.Printf("Setting %s to 'true'", bridgeLine) - result.Bridges[bridgeLine] = &BridgeTest{Functional: true} + result.Bridges[bridgeLine] = &BridgeTest{ + Functional: true, + LastTested: time.Now().UTC(), + } } else if parser.State == BridgeStateFailure { log.Printf("Setting %s to 'false'", bridgeLine) - result.Bridges[bridgeLine] = &BridgeTest{Functional: false, Error: parser.Reason} + result.Bridges[bridgeLine] = &BridgeTest{ + Functional: false, + Error: parser.Reason, + LastTested: time.Now().UTC(), + } } } @@ -254,8 +261,11 @@ func (c *TorContext) TestBridgeLines(bridgeLines []string) *TestResult { // Mark whatever bridge results we're missing as nonfunctional. for _, bridgeLine := range bridgeLines { if _, exists := result.Bridges[bridgeLine]; !exists { - result.Bridges[bridgeLine] = &BridgeTest{Functional: false, - Error: "timed out waiting for bridge descriptor"} + result.Bridges[bridgeLine] = &BridgeTest{ + Functional: false, + Error: "timed out waiting for bridge descriptor", + LastTested: time.Now().UTC(), + } } } return result