-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
208 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/base64" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
// rekorEntry is unstructured, we create this type simply | ||
// to know what we're talking about passing it around | ||
type rekorLogEntry struct { | ||
Kind string | ||
APIVersion string `json:"apiVersion"` | ||
Spec interface{} | ||
} | ||
|
||
// rekorTreeState represents the current state of the transparency log (size | ||
// etc) | ||
type rekorTreeState struct { | ||
RootHash string | ||
SignedTreeHead string | ||
TreeSize uint | ||
} | ||
|
||
type rekorClient struct { | ||
baseURL string | ||
currentIndex uint | ||
|
||
*http.Client | ||
} | ||
|
||
func newRekorClient(baseURL string) (*rekorClient, error) { | ||
rc := rekorClient{ | ||
baseURL: baseURL, | ||
currentIndex: 0, | ||
Client: new(http.Client), | ||
} | ||
|
||
// Grab the latest signed tree state and use the tree size as a starting | ||
// point to start iterating log entries. Its not the very tip of the log, | ||
// but its close enough for us. | ||
state, err := rc.getTreeState() | ||
if err != nil { | ||
// If this bailed... we're going to guess its probably misconfiguration | ||
// not a temporary outage. Lets just bail hard. | ||
return nil, fmt.Errorf("failed to get initial tree state. Is rekor server configured correctly? Failured caused by %w", err) | ||
} | ||
rc.currentIndex = state.TreeSize | ||
|
||
return &rc, nil | ||
} | ||
|
||
func (rc *rekorClient) getLogEntry(index uint) (*rekorLogEntry, error) { | ||
url := fmt.Sprintf("%s/api/v1/log/entries?logIndex=%d", rc.baseURL, index) | ||
|
||
req, err := http.NewRequest(`GET`, url, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
req.Header.Set(`Accept`, `application/json`) | ||
resp, err := rc.Do(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer resp.Body.Close() | ||
|
||
m := make(map[string]interface{}) | ||
err = json.NewDecoder(resp.Body).Decode(&m) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var body string | ||
// The key is entry UUID, but we have no idea what that is apriori so we | ||
// grab it by looping over key-value pairs and breaking after one | ||
for _, v := range m { | ||
m, ok := v.(map[string]interface{}) | ||
if !ok { | ||
return nil, errors.New(`malformed rekor entry response`) | ||
} | ||
body, ok = m["body"].(string) | ||
if !ok { | ||
return nil, errors.New(`malformed rekor entry response`) | ||
} | ||
break | ||
} | ||
|
||
var entry rekorLogEntry | ||
err = json.NewDecoder( | ||
base64.NewDecoder(base64.URLEncoding, strings.NewReader(body)), | ||
).Decode(&entry) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &entry, nil | ||
} | ||
|
||
func (rc *rekorClient) getNextLogEntry() (*rekorLogEntry, error) { | ||
entry, err := rc.getLogEntry(rc.currentIndex) | ||
if err != nil { | ||
return nil, err | ||
} | ||
rc.currentIndex++ | ||
return entry, nil | ||
} | ||
|
||
func (rc *rekorClient) getTreeState() (*rekorTreeState, error) { | ||
url := fmt.Sprintf("%s/api/v1/log", rc.baseURL) | ||
|
||
req, err := http.NewRequest(`GET`, url, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
req.Header.Set(`Accept`, `application/json`) | ||
resp, err := rc.Do(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer resp.Body.Close() | ||
|
||
var state rekorTreeState | ||
err = json.NewDecoder(resp.Body).Decode(&state) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &state, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package main | ||
|
||
import ( | ||
"io" | ||
"net/http" | ||
"net/http/httptest" | ||
"os" | ||
"testing" | ||
) | ||
|
||
func newTestServer(routes map[string]string) *httptest.Server { | ||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
responseFile, ok := routes[r.URL.Path] | ||
if !ok { | ||
http.NotFound(w, r) | ||
return | ||
} | ||
|
||
f, err := os.Open(responseFile) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
defer f.Close() | ||
|
||
_, err = io.Copy(w, f) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
})) | ||
|
||
return ts | ||
} | ||
|
||
func TestGetLogEntry(t *testing.T) { | ||
ts := newTestServer(map[string]string{ | ||
`/api/v1/log`: `testdata/rekor-api-log.json`, | ||
`/api/v1/log/entries`: `testdata/rekor-api-log-entry.json`, | ||
}) | ||
|
||
rc, err := newRekorClient(ts.URL) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
entry, err := rc.getLogEntry(1) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if entry.Kind != `rekord` { | ||
t.Error(`expected rekord type`) | ||
} | ||
if entry.APIVersion != `0.0.1` { | ||
t.Error(`expected api version 0.0.1`) | ||
} | ||
} | ||
|
||
func TestGetTreeState(t *testing.T) { | ||
ts := newTestServer(map[string]string{ | ||
`/api/v1/log`: `testdata/rekor-api-log.json`, | ||
}) | ||
|
||
rc, err := newRekorClient(ts.URL) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
_, err = rc.getTreeState() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"073970a07c978b7a9ff15b69fe15d87dfb58fd5756086e3d1fb671c2d0bd95c0":{"attestation":{},"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImRhdGEiOnsiaGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjU0NDczODVjZjliOTIwNGE4YTRlNWVmMWI5NzFmNDAzMzQ5Yjk4OWM5ODcwZGE1NjAyNTk3NjhkNTNlYmQzZDAifX0sInNpZ25hdHVyZSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVIxQWdVMGxIVGtGVVZWSkZMUzB0TFMwS0NtbFJTRXRDUVVGQ1EwRkJNRVpwUlVWalowTlZXRWMzT0dGa2FqWm9SMVZLU25KbVFtOUtNRFJ3U0c5R1FXd3ZPVEJOUlZkSVIzaHZZVmMxYTJNd1FuY0tZMjA1TUdJeU5YUlpWMnh6VEcxT2RtSlJRVXREVWtGdGREaEhaMjVVYVd0bGF6ZFRSRUZEV0VKNGVXcEROMHA0WTI5M2VWZE5kbmRtTVVkTWIwSXpad3BwVm5STldsQkRZazV6Umt0NGJuVktjemQwWlRSWGFsRnRiVUoxUWxOa1ZtazJabGRLU1hkSmRucEZjMVZpZWk5MVRsaFRVMlpEUkZwa1MwSXhNMEZDQ2tOdFZtTnBTRk5ITTBWd1ZuVjZUSGhXZEhObGIxb3djMlZwTkdjNGFHRkZWSEEzTVcxNUwwRkhTRVpuVkc1emExVnRjMWxwVG1wSlkyWm5LMVZTTHprS09IUnVkVmRXV0U1UVQyNHljbkJSZFZGaldqZE1lVXhqYjFCd1UyVnplbXAzTW1SaWVHdElVVXh5Y1U5eGJFcExNM2wyV1doelpubHNWa2xXTHpsemR3cHNjMlZxTm5SU00zUnZZWEJRWkdsMFJHbDRNV1EwYVhobE5EaGhTa1pxUzFKRlUzUk5SVFZOVWtWUk1XczVWRzFrVVZBMVQzUmtkbVF6UlRSTUwzVnZDbVp1ZG5GaFZ5OXNRVlJ5VWtkalR6bEJVVFZaYzJGS1JUZDZTSGxyYWtaTlFYUmFhMlZKTjA5U2JXeHpaRFpNZDIxR1dYRnRlblUxYzJadVJIbzRja2NLYjJzckwzWkhLMG93WjNCUVoyMDJhR1psUzFGYVlrazNSR1JwWkVONGIwVkRPRFZxU21JMlYzTjVUSEVyVWtkVVEzWTFlbVUwYjA1eFMwNW1kWFZyVHdwUmVqTnNVR0pJWml0RVRISk5aMHRFYkRKSWRYSkRTRVI2Yldad2RESXJTWFU1WkVsTWVGRnhOVWR5WTNWSFprRkxiRGM1Tms0eFpUUnRRemM0V0c1Q0NtaFRUelJ2VEhkVFRrVmFaaXRJWm1GWU5GTnNMMDV4Ukc5NFNrbGlhQ3MyTkRZek9HTjRNRDBLUFRsSlpYSUtMUzB0TFMxRlRrUWdVRWRRSUZOSlIwNUJWRlZTUlMwdExTMHRDZz09IiwiZm9ybWF0IjoicGdwIiwicHVibGljS2V5Ijp7ImNvbnRlbnQiOiJMUzB0TFMxQ1JVZEpUaUJRUjFBZ1VGVkNURWxESUV0RldTQkNURTlEU3kwdExTMHRDZ3A0YzBST1FrWXJZMGxOTUVKRVFVTmhPRWMzVWtReWRqTnRhWGROZEhoV1lWcHBNMHBDVm5WbFZrRnhTRXRETkdWTGIwMVROVWhOUTFKdkswaGFWbEpCQ2pjd1dHMXpWSEJZTVZveFoxcFJkWFZETUVkRVdUSTJhRUpvWldwQmNUTm9lREp5ZGpZdk9IRTVNRUoyVjBkSU9YUldaVWR3VERGellVbHROVEpuUlZJS1dIbFdaMmQ2Tld0QlF6QlROblpOYmpka2NqSmxkRUpyVjFkUUswOXFNRFZUTURKWldrSlVXV2Q0Y0U5aWVXVmpWVk5qY1V0T1ZHcHpiRnBSUWtneVpBcFRTSFZyTTI4eVdqZG9UVFE1VlRCc04zcGlWM2MwYjBsVUsyeEJVbU56WVdwUlZIZFhhbXhwWVZCRUwwaFNhbFF5YmxKUGFFbG9hWFJsWkM5M1ozbDZDbmxrU1hFMVpUWnpNVGhXVEdOVU56VnhWMjV5V2xoT1VGZEdkMll5TlZKWVdUTjFkR3RYSzBkWE5XNVJaVTQ0TUZFeWExSkZaMnQ0Um5NMVFXUTFWMW9LZGtVM2REZ3ZhSGcxZW0xemJGbzBkR1pHTkhOcE0xRmFaVWxSUW1GaldXSjNlRTFRVTBSbU9XOUdSM2hrUjBaVU9EZzVkMHBNUjJkWGJYSXhWR3RRVFFwalRqQTJkM2hCVWtkMGVFNHdlall4UmtwVWFXcE1WMUppYWxjemRXNUpPV2hqVVdOVmJFNHZVU3N4Tm05MFNIQmxTMVpuTkc5WE1EQkRjblpYVDBRMkNuRnJUR05OUkRRNWVWVkVPR1pUUjBJclVrVnVhVUpoT0RsRE9XdFJWVFJUUzJSbmMweE1MMUVyU2tzclUzazVTMjFKUkhKdE1XRTBSR1pRTVhCelptVUtUR3BoY25welZscG1TMVZJWm5kalFVVlJSVUZCWXpCcFZFaFdjbHBUUWtsaFZ6VnJZM2xCT0dKSGFIQmliVko2VVVoQ2VXSXpVblppYlRGb1lWZDNkUXBaTWpsMFVITk1Ra1pCVVZSQlVXZEJVR2haYUVKSVNVRnNSbmgxTDBkdVdTdHZVbXhEVTJFemQyRkRaRTlMVWpaQ1VVcG1ia05FVGtGb2MwUkNVV3RFQ25kdFkwRkNVWE5LUTBGalEwSm9WVXREVVdkTVFXZFJWMEZuVFVKQmFEUkNRV2hsUVVGQmIwcEZRMkV6ZDJGRFpFOUxValphTVd0TUx6RkpTekIyWkdVS1dsZzFjalZUWldKT2VGUkpUbE5CUVhaWmEzSkxVbmxLTldZM2JFOU5PV2RNUjBsMVl6SkdiMDVWYm1wV1VWUXdja2xIT1RBeE9XZzBPSEJEZVRreFpncFlha1JFVWsxWk9XZDZSbGRYUTJkSGJsaG9NV2hYU1ROTk4wSktSalpaUlRaMU5rUllSM04yZFZWd1IzSk9aVnBCUnpacmEyRjZRWFZCYm01V01HdERDakE0ZW05U2NrRmFRM1pzY0dGYWNubGtPR2wwWWl0eVZpdFJTM0EzUVhjeWJFRkpTREZsTm1SM1RUUlNURVpxZG1ack9FeEtXSGhxU2tGdlVHMTNObXdLVEhjeE9HTTNiMWMyVWt4UE9WRllVVGhsVFRaeU1uWklTSEJ0TUZSMVpIWmFlV0ZtVG5WRE16SkhSR3hOV1RSMU1GWXhSR0k0VEhONWJWQnpRV2gxUVFveVNubzBMMHRRY1RaMVMzZEpkRzFXU3pSd2JtUm1SVVIxTmtReFZHOXZSRmxZYVhCMFdXRm1aSFpWTXpOd1ZWRjRkMGh2WmxSVVprVTFlbHAzTWxCbENteElNMjVhWkhOblNGaEhVSGhLVEV4TmNVOXdWelJETDJOTk5scFJWbWRaVTNSV2NqQnVkbFUyTml0UmFsRjJjMnRWV2xJd05tUmtSWHB1UW5CSFNuTUtkSEJ0YWpsQlpTOUhVbGs0UlU1dVRqa3ZNa2RtUlhWeWRIb3paRXRPVlZwdmFrMTVNVFV6YW1OSE1GVXhlbnBvTVRFMVYwbzNkRGgzU0VKMU5GTTBjQW93WjBVclVrRnhlWFJCWTBsYVJHUXlUbE5PY25vNFZuSTVSa1U1ZUN0bVlYUTVSVkpzWW01a1FVSkZOV2xXT0hOTE1DdEdZVzVYZDJkak4wRjZVVkptQ201RFJFNUJVWGRCZEVKdmRHaG1ZMUo2Y2pONGNqTlFPWEEzVVVOTmQwdDFhVzl1ZGsxRGJUaFhaM2RPVXpSRGNHaHhielZPVDNJeWFVMXFhMHhRTUVvS2IyMW5Ta3hXV0RWT0sySnlkamg1TkVnNGNsbFFkMHRDTVRadkwyaEJPRWxpUjJKd1dYbHRNMFpqZVd0VWQyTmlWMkowVUZSTVJYUmtRMVZRVEZsVVJBcE9RelZNUjBwd1p6TmxPRFpaWmxGMFFVNDJMMDF1V25sWlQyMXNSSGd5VjBkMGRFeGtiWE5CVTBkV2RYZzJRVlpLY1VsMkszZ3dObFZMU2tWdFN6TjBDbXBzUlZaTGVXY3hNbEpGZW5sbE5VbFVObkZGVTBkd1QzcHZNbGxzVjFWeFNWUjNMMEZoVUZFeVduaFZZWGgyV1VadlZVOWpkMmRqWkc1SWEyZHphRWtLVDI0NWFDOU9TRlZ0VURNeVYxRjJjV3RSVFhWVllWQkpUbEp6UXpnelMzWlVSRWRzZVdaVFNGWkdlazFoTkdoRVRXaEZZMWg2TkdGamFXNWtOVmRVWlFwNmVVeG5XbWhQWWpkalRtVkRlRFI0WTNKMFVFSTJWVGRDVWk5R1ZreDZURUpzUVhwMWVtcHBSV2haZDBwdk0wRlBUWEZHYjFJMWJVRnhhR3gxZEU1UENuTnplVzltWW5GVVowZGlVMHhrYW1KWVVDOWhSWFJuZWpKTlZqbHVMMjlqTVZOQ09FaGxXazh2TVRkS2VXZHVlbkoxU1V0NUt5OXNUMWRQZW5RcmFsWUtWa1p3Vm5sb01YVmxPR3hHTjNsdFMxSTBkSE5zSzJsSlZtSnhibEIyY0Uxb1RFOUpRbkZZUm00eVowMURhMGR2U2t4NU4wOUliekpYUVVWS1IyeDBNd3BUZDNCaWNtcHFNVUZDUlVKQlFVaERkMUIzUlVkQlJVbEJRMWxYU1ZGU2VVRktVbU5pZG5od01sQnhSVnBSYTIxME9FZG5ibFJwYTJWblZVTllOWGRuQ25wUlNXSkVRVlZLUVRoS2JrRkJRVXREVWtGdGREaEhaMjVVYVd0bGFXNXBSRUZEUlVGbWExcHhMelJTY0RKaFRrRTBaR0p2U2pkVlJsaEVUMkZTYTFZS09VMUxiMFZhUm5GVVRVNXZka1JNTlhob1RXeG5iRkJRZFM5c0syUm9WR2Q0WkdWS09VVldTRzlsZW5SaU9EazJWUzl3VDNWQ1VuTnVPVlowVnpSWkx3cHFaV2xYTjBWNVRsaEJaQzlQY25adVJtSjRLemRwV0V4eGRYQmFTa3BHVkdrdmFqbFNhRlpaVG5OdGJEZHpaV0pVVUdWQ2JrZEVRVGt4Y1dKRE5IaElDbkJSVmtSRGRXcDROamxXZUU4MVJURk1VMjlvUTAwclR5ODFka3hDYlRocE1XOHZibUpHYldKNU4xWkRlVXRsVWtSbWFIUm1PVzVET0RSeGMwVTVSM0VLVlRjdlRGTnBhemxpWm5oTlYySndjVGg1YTI1MGJWTXpZVEJ6ZW1NMFlsWkdjR1Y2UW5CdFRtSXdRVlpqUWl0VWJUbG5WMjFGZW1ocFRITTJSa3RCVGdwSmJuRk9kVmgxUWt3NVVFTmhZemNyYlZVcll6SnRRbWRIVDFKSFpERmtXazh6VWtNNE9YcEdNM2hDUWxsdVEwOWxOV05CVFVac1l6RllSM05zYkhOSkNtUjZaSEprV0haaVRrSjZMMm8zTVhCMVRqaHZSbGx0TDFoaVZtTnBaVTh3VkdaUmFVUmpWSFE0UzJscFVqbFVRVVE1TDFBMU9UTlNUV3hNVDBkVE9IQUthSFpLWW1sR2IxcG1XRWhqYkhOYVJraHRPRVJSVVdFNU5FbGFkMVJDT0cwMFowSldNRTB5V0ZOMlpFaHZNekJzYzNGcWRGcGhXbWxUY2xKb05ISnphQXB1TVRSd1lrRmhWR1JoUzBWUVkzWjBkV1ppVlhWWE1FbHFXV1F5YTNCSlZDOTBaejBLUFdoYVdGVUtMUzB0TFMxRlRrUWdVRWRRSUZCVlFreEpReUJMUlZrZ1FreFBRMHN0TFMwdExRPT0ifX19LCJraW5kIjoicmVrb3JkIn0=","integratedTime":1610469905,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d","logIndex":1,"verification":{"inclusionProof":{"hashes":["b08416d417acdb0610d4a030d8f697f9d0a718024681a00fa0b9ba67072a38b5","766ae2c918bbc083a6cce41f6ff3a3cf1a8153b86f594303ce16ded44c99647b","d17f9cb9776767672a026878d31368d6620c12c0b6eb537c7788f975771d5488","3d3952e9ac03dd00d6817cb1ba7e4ca832fdc9472fd066f6eb6550f4a6d3cff1","2b5d2e57470fbaa592f6311b5665ec2870297c39dda407fe8ee45f146df27dbc","8a0cd237f42edf76d50ac9a539108a4f0c73cefd87a27e5ede32ee5153d79a6c","24fe0dc0e19a75cd7bd5f1a58c5aff086bbefcc6c777b70b21a87a5bfd8c8c23","d0b0d0cf8fc7a914cad93607ac5bfd511afd8d9b3729da87cb4d243b53385f17","7e777698d7e8ceb92a03c05cde3632ab12aa4fc3513064a723d68370b287c9d3","0d0f2102d842d56843727e1848872655501af276613f8d04b6ccad806a334d57","34f60d3658ce812525e34e8b6858678be09d9d44b8f9d8c61814be5706c615cf","15ca1b46d6458b9eecd3e1669a8c2848449d158655de9d24f78fd70ad9955e3b","6cde371d35dfc14a44ac42c1ed55025066a261d39333f1c7ff25b7955da48f2f","6d96b436ff041cd3b73f9c291b223b2cc47ec13ab92f97a5d625d15f9aa439fb","70ebf715c1f35f512c68cedbd83d47aba5e5ed309ddbba9472f1786e3b2e40c3","7e6076f58c8241594c9592de95fa4d698b23985025a135049299abc901d5de03","1891d104464a0d35c122f6b4a5ac1b146dc9f43dc86e86f0bc07491b1f50c410","93b48fbede85f163c9a6c96ea5a4af34c212ae946b8870fdbcf9e61987fa5c3c","df963b882a1c8375d0723e4f7e496752a62561f02da4755d2dff311e42ad2d16","102f917eca6d86ded7489a37f41e03d48ae52577788a5a6a2d1a5958cdbbf851"],"logIndex":1,"rootHash":"f01b84f1a63140fed275bde5a7aaf65d6d2e3414da23bb507075922ffbfb0870","treeSize":1025778},"signedEntryTimestamp":"MEQCIACZZ+5AEfc5HUJh6h5Q/eYMTHnACGYbfEvrKfZqsIhlAiBRp+cC7oJObW22emGozXZYzRgyDqc/JTlb7QeeN33A9Q=="}}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"rootHash":"f01b84f1a63140fed275bde5a7aaf65d6d2e3414da23bb507075922ffbfb0870","signedTreeHead":"Rekor\n1025778\n8BuE8aYxQP7Sdb3lp6r2XW0uNBTaI7tQcHWSL/v7CHA=\nTimestamp: 1641356019166006899\n\n— rekor.sigstore.dev wNI9ajBFAiEAw2VUt8zfLDbuuSdLIc/EWA8oX5aPtKY9HQQqorPZfU8CIDIFPDAxHp46dy5GSJYvmaiB7xhpLgq21k8oyckjvOTE\n","treeSize":1025778} |