/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/pvl/interp.go
1 109 строк
32 KB
chrisnojima-zoom
Version 670 - clean2 (#29122)
08 июн 2026, 19:31
Не верифицирован
08 июн 2026, 19:31
1943197
Код
Авторство
О чём код?
// Copyright 2016 Keybase, Inc. All rights reserved. Use of // this source code is governed by the included BSD license. package pvl import ( "bytes" b64 "encoding/base64" "fmt" "net" "net/url" "regexp" "strings" "github.com/PuerkitoBio/goquery" "github.com/keybase/client/go/jsonhelpers" libkb "github.com/keybase/client/go/libkb" keybase1 "github.com/keybase/client/go/protocol/keybase1" jsonw "github.com/keybase/go-jsonw" "github.com/miekg/dns" ) // SupportedVersion is which version of PVL is supported by this client. const SupportedVersion int = 1 // state of execution in a script // copies of a scriptState will point to the same internal mutable data, so be careful type scriptState struct { WhichScript int PC int Service keybase1.ProofType Regs namedRegsStore Sig []byte HasFetched bool // nil until fetched FetchResult *fetchResult } type fetchResult struct { fetchMode fetchMode // One of these 3 must be filled. String string HTML *goquery.Document JSON *jsonw.Wrapper } type regexDescriptor struct { Template string CaseInsensitive bool MultiLine bool } type fetchMode string const ( fetchModeJSON fetchMode = "json" fetchModeHTML fetchMode = "html" fetchModeString fetchMode = "string" fetchModeDNS fetchMode = "dns" ) type commandName string const ( cmdAssertRegexMatch commandName = "assert_regex_match" cmdAssertFindBase64 commandName = "assert_find_base64" cmdAssertCompare commandName = "assert_compare" cmdWhitespaceNormalize commandName = "whitespace_normalize" cmdRegexCapture commandName = "regex_capture" cmdReplaceAll commandName = "replace_all" cmdParseURL commandName = "parse_url" cmdFetch commandName = "fetch" cmdParseHTML commandName = "parse_html" cmdSelectorJSON commandName = "selector_json" cmdSelectorCSS commandName = "selector_css" cmdFill commandName = "fill" ) type stateMaker func(int) (scriptState, libkb.ProofError) // ProofInfo contains all the data about a proof PVL needs to check it. // It can be derived from a RemoteProofChainLink and SigHint. type ProofInfo struct { ArmoredSig string Username string RemoteUsername string Hostname string Protocol string APIURL string stubDNS *stubDNSEngine } // NewProofInfo creates a new ProofInfo func NewProofInfo(link libkb.RemoteProofChainLink, h libkb.SigHint) ProofInfo { return ProofInfo{ ArmoredSig: link.GetArmoredSig(), RemoteUsername: link.GetRemoteUsername(), Username: link.GetUsername(), Hostname: link.GetHostname(), Protocol: link.GetProtocol(), APIURL: h.GetAPIURL(), } } // CheckProof verifies one proof by running the pvl on the provided proof information. func CheckProof(m libkb.MetaContext, pvlS string, service keybase1.ProofType, info ProofInfo) libkb.ProofError { m1 := newMetaContext(m, info.stubDNS) perr := checkProofInner(m1, pvlS, service, info) if perr != nil { debug(m1, "CheckProof failed: %v", perr) } if perr != nil && perr.GetProofStatus() == keybase1.ProofStatus_INVALID_PVL { return libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Invalid proof verification instructions! Let us know at https://github.com/keybase/keybase-issues/new") } return perr } func checkProofInner(m metaContext, pvlS string, service keybase1.ProofType, info ProofInfo) libkb.ProofError { pvl, err := parse(pvlS) if err != nil { if strings.Contains(err.Error(), "cannot unmarshal string into Go struct") && strings.Contains(pvlS, "from iced tests") { return libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Corrupted pvl in merkle tree from iced tests. To fix see test/merkle_pvl.iced. : %v", err) } return libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Could not parse pvl: %v", err) } if perr := validateChunk(m, &pvl, service); perr != nil { return perr } sigBody, sigIDBase, err := libkb.OpenSig(info.ArmoredSig) if err != nil { return libkb.NewProofError(keybase1.ProofStatus_BAD_SIGNATURE, "Bad signature: %v", err) } scripts, perr := chunkGetScripts(&pvl, service) if perr != nil { return perr } // validate hostname webish := (service == keybase1.ProofType_DNS || service == keybase1.ProofType_GENERIC_WEB_SITE) if webish { if !validateDomain(info.Hostname) { return libkb.NewProofError(keybase1.ProofStatus_BAD_SIGNATURE, "Bad hostname in sig: %s", info.Hostname) } } // validate protocol if service == keybase1.ProofType_GENERIC_WEB_SITE { _, ok := validateProtocol(info.Protocol, []string{"http", "https"}) if !ok { return libkb.NewProofError(keybase1.ProofStatus_BAD_SIGNATURE, "Bad protocol in sig: %s", info.Protocol) } } sigID := sigIDBase.ToSigIDLegacy() mknewstate := func(i int) (scriptState, libkb.ProofError) { state := scriptState{ WhichScript: i, PC: 0, Service: service, Regs: *newNamedRegsStore(), Sig: sigBody, HasFetched: false, FetchResult: nil, } err := setupRegs(m, &state.Regs, info, sigBody, sigID, service) return state, err } var errs []libkb.ProofError if service == keybase1.ProofType_DNS { if perr = runDNS(m, info.Hostname, scripts, mknewstate, sigID.ToMediumID()); perr != nil { errs = append(errs, perr) } } else { // Run the scripts in order. // If any succeed, the proof succeeds. // If one fails, the next takes over. // If all fail, log and report errors. for i, script := range scripts { state, perr := mknewstate(i) if perr != nil { return perr } perr = runScript(m, &script, state) if perr == nil { return nil } errs = append(errs, perr) } } switch len(errs) { case 0: return nil case 1: return errs[0] default: for _, err := range errs { debug(m, "multiple failures include: %v", err) } // Arbitrarily use the error code of the first error return libkb.NewProofError(errs[0].GetProofStatus(), "Multiple errors while verifying proof") } } func setupRegs(m metaContext, regs *namedRegsStore, info ProofInfo, sigBody []byte, sigID keybase1.SigID, service keybase1.ProofType) libkb.ProofError { webish := (service == keybase1.ProofType_DNS || service == keybase1.ProofType_GENERIC_WEB_SITE) // hint_url if err := regs.Set("hint_url", info.APIURL); err != nil { return err } // username_service if webish { if err := regs.Ban("username_service"); err != nil { return err } } else { if err := regs.Set("username_service", info.RemoteUsername); err != nil { return err } } // username_keybase if err := regs.Set("username_keybase", info.Username); err != nil { return err } // sig // Store it b64 encoded. This is rarely used, assert_find_base64 is better. if err := regs.Set("sig", b64.StdEncoding.EncodeToString(sigBody)); err != nil { return err } // sig_id_medium if err := regs.Set("sig_id_medium", sigID.ToMediumID()); err != nil { return err } // sig_id_short if err := regs.Set("sig_id_short", sigID.ToShortID()); err != nil { return err } // hostname if webish { if err := regs.Set("hostname", info.Hostname); err != nil { return err } } else { if err := regs.Ban("hostname"); err != nil { return err } } // protocol if service == keybase1.ProofType_GENERIC_WEB_SITE { canonicalProtocol, ok := validateProtocol(info.Protocol, []string{"http", "https"}) if !ok { return libkb.NewProofError(keybase1.ProofStatus_BAD_SIGNATURE, "Bad protocol in sig: %s", info.Protocol) } if err := regs.Set("protocol", canonicalProtocol); err != nil { return err } } else if err := regs.Ban("protocol"); err != nil { return err } return nil } // Get the list of scripts for a given service. func chunkGetScripts(pvl *pvlT, service keybase1.ProofType) ([]scriptT, libkb.ProofError) { scripts, ok := pvl.Services.Map[service] if !ok { return nil, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "No entry for service: %v", service) } return scripts, nil } // Check that a chunk of PVL is valid code. // Will always accept valid code, but may not always notice invalidities. func validateChunk(m metaContext, pvl *pvlT, service keybase1.ProofType) libkb.ProofError { if pvl.PvlVersion != SupportedVersion { return libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "PVL is for the wrong version %v != %v", pvl.PvlVersion, SupportedVersion) } debug(m, "valid version:%v revision:%v", pvl.PvlVersion, pvl.Revision) scripts, perr := chunkGetScripts(pvl, service) if perr != nil { return perr } if len(scripts) == 0 { return libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Empty scripts list for service: %v", service) } // Scan all the scripts (for this service) for errors. Report the first error. var errs []libkb.ProofError for whichscript, script := range scripts { perr = validateScript(m, &script, service, whichscript) errs = append(errs, perr) } return errs[0] } func validateScript(m metaContext, script *scriptT, service keybase1.ProofType, whichscript int) libkb.ProofError { // Scan the script. // Does not validate each instruction's format. (That is done when running it) // Validate each instruction's "error" field. logerr := func(m metaContext, service keybase1.ProofType, whichscript int, pc int, format string, arg ...any) libkb.ProofError { debugWithPosition(m, service, whichscript, pc, format, arg...) return libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, format, arg...) } modeknown := false var mode fetchMode if service == keybase1.ProofType_DNS { modeknown = true mode = fetchModeDNS } if len(script.Instructions) < 1 { return logerr(m, service, whichscript, 0, "Empty script") } for i, ins := range script.Instructions { if ins.variantsFilled() != 1 { return logerr(m, service, whichscript, i, "exactly 1 variant must appear in instruction") } switch { // These can always run, but must be cases so that the default case works. case ins.AssertRegexMatch != nil: case ins.AssertFindBase64 != nil: case ins.AssertCompare != nil: case ins.WhitespaceNormalize != nil: case ins.RegexCapture != nil: case ins.ReplaceAll != nil: case ins.ParseURL != nil: case ins.Fill != nil: case ins.Fetch != nil: // A script can contain only <=1 fetches. // A DNS script cannot contain fetches. fetchType := ins.Fetch.Kind if service == keybase1.ProofType_DNS { return logerr(m, service, whichscript, i, "DNS script cannot contain fetch instruction") } if modeknown { return logerr(m, service, whichscript, i, "Script cannot contain multiple fetch instructions") } switch fetchMode(fetchType) { case fetchModeString: modeknown = true mode = fetchModeString case fetchModeHTML: modeknown = true mode = fetchModeHTML case fetchModeJSON: modeknown = true mode = fetchModeJSON default: return logerr(m, service, whichscript, i, "Unsupported fetch type: %v", fetchType) } case ins.ParseHTML != nil: case ins.SelectorJSON != nil: // Can only select after fetching. switch { case service == keybase1.ProofType_DNS: return logerr(m, service, whichscript, i, "DNS script cannot use json selector") case !modeknown: return logerr(m, service, whichscript, i, "Script cannot select before fetch") case mode != fetchModeJSON: return logerr(m, service, whichscript, i, "Script contains json selector in non-html mode") } case ins.SelectorCSS != nil: // Can only select one of text, attr, or data. if ins.SelectorCSS.Attr != "" && ins.SelectorCSS.Data { return logerr(m, service, whichscript, i, "Script contains css selector with both 'attr' and 'data' set") } default: return logerr(m, service, whichscript, i, "Unsupported PVL instruction: %v", ins) } } return nil } // Run each script on each TXT record of each domain. // Succeed if any succeed. func runDNS(m metaContext, userdomain string, scripts []scriptT, mknewstate stateMaker, sigIDMedium string) libkb.ProofError { domains := []string{userdomain, "_keybase." + userdomain} var errs []libkb.ProofError for _, d := range domains { debug(m, "Trying DNS for domain: %v", d) err := runDNSOne(m, d, scripts, mknewstate, sigIDMedium) if err != nil { errs = append(errs, err) } else { return nil } } // Return only the error for the first domain error if len(errs) == 0 { return nil } var descs []string for _, err := range errs { descs = append(descs, err.GetDesc()) } // Use the code from the first error return libkb.NewProofError(errs[0].GetProofStatus(), "%s", strings.Join(descs, "; ")) } func formatDNSServer(srv string) string { if strings.Contains(srv, ":") { return fmt.Sprintf("[%s]:53", srv) } return srv + ":53" } func runDNSTXTQuery(m metaContext, domain string) (res []string, err error) { // Attempt to use the built-in resolver first, but this might fail on mobile. // The reason for that is currently (as of Go 1.8), LookupTXT does not properly // use the cgo DNS routines if they are configured to be used (like they are for mobile). // As for now, we can use a different library to specify our own name servers, since the // Go resolver will attempt to use /etc/resolv.conf, which is not a thing on mobile. if res, err = net.LookupTXT(domain); err != nil { debug(m, "DNS LookupTXT failed: %s", err.Error()) } else { return res, nil } // Google IPv4 and IPV6 addresses publicServers := []string{ formatDNSServer("8.8.8.8"), formatDNSServer("2001:4860:4860::8888"), } var fetchedSrvs []string if m.G().GetDNSNameServerFetcher() != nil { fetchedSrvs = m.G().GetDNSNameServerFetcher().GetServers() for i := 0; i < len(fetchedSrvs); i++ { fetchedSrvs[i] = formatDNSServer(fetchedSrvs[i]) } } servers := fetchedSrvs servers = append(servers, publicServers...) var r *dns.Msg c := dns.Client{} msg := dns.Msg{} found := false for _, srv := range servers { debug(m, "DNS trying backup server: %s", srv) msg.SetQuestion(domain+".", dns.TypeTXT) r, _, err = c.Exchange(&msg, srv) if err != nil { debug(m, "DNS backup server failed; %s", err.Error()) } else { found = true break } } if !found { return res, fmt.Errorf("failed to lookup DNS: %s", domain) } for _, ans := range r.Answer { if record, ok := ans.(*dns.TXT); ok { if len(record.Txt) > 0 { res = append(res, record.Txt[len(record.Txt)-1]) } } } return res, err } // Run each script on each TXT record of the domain. func runDNSOne(m metaContext, domain string, scripts []scriptT, mknewstate stateMaker, sigIDMedium string) libkb.ProofError { // Fetch TXT records var txts []string var err error if m.getStubDNS() == nil { txts, err = runDNSTXTQuery(m, domain) } else { txts, err = m.getStubDNS().LookupTXT(domain) } if err != nil { return libkb.NewProofError(keybase1.ProofStatus_DNS_ERROR, "DNS failure for %s: %s", domain, err) } for _, record := range txts { debug(m, "For DNS domain '%s' got TXT record: '%s'", domain, record) // Try all scripts. for i, script := range scripts { state, err := mknewstate(i) if err != nil { return err } if err = state.Regs.Set("txt", record); err != nil { return err } if err = runScript(m, &script, state); err == nil { return nil } // Discard error, it has already been reported by stepInstruction. } } return libkb.NewProofError(keybase1.ProofStatus_NOT_FOUND, "Checked %d TXT entries of %s, but didn't find signature keybase-site-verification=%s", len(txts), domain, sigIDMedium) } func runScript(m metaContext, script *scriptT, startstate scriptState) libkb.ProofError { state := startstate if len(script.Instructions) < 1 { perr := libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Empty scripts are not allowed.") debugWithStateError(m, state, perr) return perr } for i, ins := range script.Instructions { // Sanity check. if state.PC != i { perr := libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Execution failure, PC mismatch %v %v", state.PC, i) debugWithStateError(m, state, perr) return perr } newstate, perr := stepInstruction(m, ins, state) if perr != nil { return perr } state = newstate state.PC++ } // Script executed successfully with no errors. return nil } // stepInstruction decides which instruction to run. func stepInstruction(m metaContext, ins instructionT, state scriptState) (scriptState, libkb.ProofError) { debugWithState(m, state, "Running instruction %v: %v", ins.Name(), ins) var newState scriptState var stepErr libkb.ProofError var customErrSpec *errorT switch { case ins.AssertRegexMatch != nil: newState, stepErr = stepAssertRegexMatch(m, *ins.AssertRegexMatch, state) customErrSpec = ins.AssertRegexMatch.Error case ins.AssertFindBase64 != nil: newState, stepErr = stepAssertFindBase64(m, *ins.AssertFindBase64, state) customErrSpec = ins.AssertFindBase64.Error case ins.AssertCompare != nil: newState, stepErr = stepAssertCompare(m, *ins.AssertCompare, state) customErrSpec = ins.AssertCompare.Error case ins.WhitespaceNormalize != nil: newState, stepErr = stepWhitespaceNormalize(m, *ins.WhitespaceNormalize, state) customErrSpec = ins.WhitespaceNormalize.Error case ins.RegexCapture != nil: newState, stepErr = stepRegexCapture(m, *ins.RegexCapture, state) customErrSpec = ins.RegexCapture.Error case ins.ReplaceAll != nil: newState, stepErr = stepReplaceAll(m, *ins.ReplaceAll, state) customErrSpec = ins.ReplaceAll.Error case ins.ParseURL != nil: newState, stepErr = stepParseURL(m, *ins.ParseURL, state) customErrSpec = ins.ParseURL.Error case ins.Fetch != nil: newState, stepErr = stepFetch(m, *ins.Fetch, state) customErrSpec = ins.Fetch.Error case ins.ParseHTML != nil: newState, stepErr = stepParseHTML(m, *ins.ParseHTML, state) customErrSpec = ins.ParseHTML.Error case ins.SelectorJSON != nil: newState, stepErr = stepSelectorJSON(m, *ins.SelectorJSON, state) customErrSpec = ins.SelectorJSON.Error case ins.SelectorCSS != nil: newState, stepErr = stepSelectorCSS(m, *ins.SelectorCSS, state) customErrSpec = ins.SelectorCSS.Error case ins.Fill != nil: newState, stepErr = stepFill(m, *ins.Fill, state) customErrSpec = ins.Fill.Error default: newState = state stepErr = libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Invalid instruction: %v", ins) } if stepErr != nil { debugWithStateError(m, state, stepErr) stepErr = replaceCustomError(m, state, customErrSpec, stepErr) } return newState, stepErr } func stepAssertRegexMatch(m metaContext, ins assertRegexMatchT, state scriptState) (scriptState, libkb.ProofError) { rdesc := regexDescriptor{ Template: ins.Pattern, CaseInsensitive: ins.CaseInsensitive, MultiLine: ins.MultiLine, } from, err := state.Regs.Get(ins.From) if err != nil { return state, err } re, err := interpretRegex(m, state, rdesc) if err != nil { return state, err } if re.MatchString(from) == ins.Negate { negate := "not " if ins.Negate { negate = "" } debugWithState(m, state, "Regex did %smatch:\n %v\n %v\n %q", negate, rdesc.Template, re, from) return state, libkb.NewProofError(keybase1.ProofStatus_CONTENT_FAILURE, "Regex did %smatch (%v)", negate, rdesc.Template) } return state, nil } func stepAssertFindBase64(m metaContext, ins assertFindBase64T, state scriptState) (scriptState, libkb.ProofError) { if ins.Needle != "sig" { return state, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Can only assert_find_base64 for sig") } haystack, err := state.Regs.Get(ins.Haystack) if err != nil { return state, err } if libkb.FindBase64Block(haystack, state.Sig, false) { return state, nil } return state, libkb.NewProofError(keybase1.ProofStatus_TEXT_NOT_FOUND, "Signature not found") } func stepAssertCompare(m metaContext, ins assertCompareT, state scriptState) (scriptState, libkb.ProofError) { a, err := state.Regs.Get(ins.A) if err != nil { return state, err } b, err := state.Regs.Get(ins.B) if err != nil { return state, err } var same bool switch ins.Cmp { case "exact": same = (a == b) case "cicmp": same = libkb.Cicmp(a, b) case "stripdots-then-cicmp": norm := func(s string) string { return strings.ToLower(strings.ReplaceAll(s, ".", "")) } same = libkb.Cicmp(norm(a), norm(b)) default: return state, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Unsupported comparison method: '%v'", ins.Cmp) } if !same { debugWithState(m, state, "Comparison (%v) failed\n %v != %v\n '%v' != '%v'", ins.Cmp, ins.A, ins.B, a, b) return state, libkb.NewProofError(keybase1.ProofStatus_CONTENT_FAILURE, "Comparison (%v) failed '%v' != '%v'", ins.Cmp, a, b) } return state, nil } func stepWhitespaceNormalize(m metaContext, ins whitespaceNormalizeT, state scriptState) (scriptState, libkb.ProofError) { from, err := state.Regs.Get(ins.From) if err != nil { return state, err } normed := libkb.WhitespaceNormalize(from) err = state.Regs.Set(ins.Into, normed) return state, err } func stepRegexCapture(m metaContext, ins regexCaptureT, state scriptState) (scriptState, libkb.ProofError) { rdesc := regexDescriptor{ Template: ins.Pattern, CaseInsensitive: ins.CaseInsensitive, MultiLine: ins.MultiLine, } from, err := state.Regs.Get(ins.From) if err != nil { return state, err } re, err := interpretRegex(m, state, rdesc) if err != nil { return state, err } // There must be some registers to write results to. if len(ins.Into) == 0 { return state, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Into list cannot be empty") } match := re.FindStringSubmatch(from) // Assert that the match matched and has at least one capture group. // -1 for the ignored first element of match if len(match)-1 < len(ins.Into) { debugWithState(m, state, "Regex capture did not match enough groups:\n %v\n %v\n %q\n %v", rdesc.Template, re, from, match) return state, libkb.NewProofError(keybase1.ProofStatus_CONTENT_FAILURE, "Regex capture did not match enough groups (%v)", rdesc.Template) } for i := 0; i < len(ins.Into); i++ { err := state.Regs.Set(ins.Into[i], match[i+1]) if err != nil { return state, err } } return state, nil } func stepReplaceAll(m metaContext, ins replaceAllT, state scriptState) (scriptState, libkb.ProofError) { from, err := state.Regs.Get(ins.From) if err != nil { return state, err } replaced := strings.ReplaceAll(from, ins.Old, ins.New) if err = state.Regs.Set(ins.Into, replaced); err != nil { return state, err } return state, nil } func stepParseURL(m metaContext, ins parseURLT, state scriptState) (scriptState, libkb.ProofError) { s, err := state.Regs.Get(ins.From) if err != nil { return state, err } u, err2 := url.Parse(s) if err2 != nil { return state, libkb.NewProofError(keybase1.ProofStatus_FAILED_PARSE, "Could not parse url: '%v'", s) } if ins.Path != "" { err := state.Regs.Set(ins.Path, u.Path) if err != nil { return state, err } } if ins.Host != "" { err := state.Regs.Set(ins.Host, u.Host) if err != nil { return state, err } } if ins.Scheme != "" { err := state.Regs.Set(ins.Scheme, u.Scheme) if err != nil { return state, err } } return state, nil } func stepFetch(m metaContext, ins fetchT, state scriptState) (scriptState, libkb.ProofError) { if state.FetchResult != nil { return state, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Script cannot contain more than one fetch") } if state.Service == keybase1.ProofType_DNS { return state, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Script cannot fetch in DNS mode") } from, err := state.Regs.Get(ins.From) if err != nil { return state, err } if state.Service == keybase1.ProofType_ROOTER { from2, err := rooterRewriteURL(m, from) if err != nil { return state, libkb.NewProofError(keybase1.ProofStatus_FAILED_PARSE, "Could not rewrite rooter URL: %v", err) } from = from2 } switch fetchMode(ins.Kind) { case fetchModeString: debugWithState(m, state, "fetchurl: %v", from) res, err1 := m.G().GetExternalAPI().GetText(m.MetaContext, libkb.APIArg{Endpoint: from}) if err1 != nil { return state, libkb.XapiError(err1, from) } state.FetchResult = &fetchResult{ fetchMode: fetchModeString, String: res.Body, } err := state.Regs.Set(ins.Into, state.FetchResult.String) if err != nil { return state, err } return state, nil case fetchModeJSON: if ins.Into != "" { return state, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "JSON fetch must not specify 'into' register") } res, err1 := m.G().GetExternalAPI().Get(m.MetaContext, libkb.APIArg{Endpoint: from}) if err1 != nil { return state, libkb.XapiError(err1, from) } state.FetchResult = &fetchResult{ fetchMode: fetchModeJSON, JSON: res.Body, } return state, nil case fetchModeHTML: if ins.Into != "" { return state, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "HTML fetch must not specify 'into' register") } res, err1 := m.G().GetExternalAPI().GetHTML(m.MetaContext, libkb.APIArg{Endpoint: from}) if err1 != nil { return state, libkb.XapiError(err1, from) } state.FetchResult = &fetchResult{ fetchMode: fetchModeHTML, HTML: res.GoQuery, } return state, nil default: return state, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Unsupported fetch kind %v", ins.Kind) } } func stepParseHTML(m metaContext, ins parseHTMLT, state scriptState) (scriptState, libkb.ProofError) { from, err := state.Regs.Get(ins.From) if err != nil { return state, err } gq, err2 := goquery.NewDocumentFromReader(bytes.NewBuffer([]byte(from))) if err2 != nil { return state, libkb.NewProofError(keybase1.ProofStatus_FAILED_PARSE, "Failed to parse html from '%v': %v", ins.From, err2) } state.FetchResult = &fetchResult{ fetchMode: fetchModeHTML, HTML: gq, } return state, nil } func stepSelectorJSON(m metaContext, ins selectorJSONT, state scriptState) (scriptState, libkb.ProofError) { if state.FetchResult == nil || state.FetchResult.fetchMode != fetchModeJSON { return state, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Cannot use json selector with non-json fetch result") } if len(ins.Selectors) < 1 { return state, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Json selector list must contain at least 1 element") } results, perr := runSelectorJSONInner(m, state, state.FetchResult.JSON, ins.Selectors) if perr != nil { return state, perr } if len(results) < 1 { return state, libkb.NewProofError(keybase1.ProofStatus_CONTENT_FAILURE, "Json selector did not match any values") } s := strings.Join(results, " ") err := state.Regs.Set(ins.Into, s) return state, err } func stepSelectorCSS(m metaContext, ins selectorCSST, state scriptState) (scriptState, libkb.ProofError) { if state.FetchResult == nil || state.FetchResult.fetchMode != fetchModeHTML { return state, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Cannot use css selector with non-html fetch result") } selection, perr := runCSSSelectorInner(m, state.FetchResult.HTML.Selection, ins.Selectors) if perr != nil { return state, perr } if selection.Size() < 1 { return state, libkb.NewProofError(keybase1.ProofStatus_CONTENT_FAILURE, "No elements matched by selector") } if selection.Size() > 1 && !ins.Multi { return state, libkb.NewProofError(keybase1.ProofStatus_CONTENT_FAILURE, "CSS selector matched too many elements") } // Get the text, attribute, or data. var res string switch { case ins.Attr != "": res = selectionAttr(selection, ins.Attr) case ins.Data: res = selectionData(selection) default: res = selectionText(selection) } err := state.Regs.Set(ins.Into, res) return state, err } func stepFill(m metaContext, ins fillT, state scriptState) (scriptState, libkb.ProofError) { s, err := substituteExact(ins.With, state) if err != nil { debugWithState(m, state, "Fill did not succeed:\n %v\n %v\n %v", ins.With, err, ins.Into) return state, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Could not fill variable (%v): %v", ins.Into, err) } err = state.Regs.Set(ins.Into, s) return state, err } // Run a PVL CSS selector. // selectors is a list like [ "div .foo", 0, ".bar"] ]. // Each string runs a selector, each integer runs a Eq. func runCSSSelectorInner(m metaContext, html *goquery.Selection, selectors []keybase1.SelectorEntry, ) (*goquery.Selection, libkb.ProofError) { if len(selectors) < 1 { return nil, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "CSS selectors array must not be empty") } selection := html for _, selector := range selectors { switch { case selector.IsIndex: selection = selection.Eq(selector.Index) case selector.IsKey: selection = selection.Find(selector.Key) case selector.IsContents: selection = selection.Contents() default: return nil, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "CSS selector entry must be a string, int, or 'contents' %v", selector) } } return selection, nil } func runSelectorJSONInner(m metaContext, state scriptState, selectedObject *jsonw.Wrapper, selectors []keybase1.SelectorEntry, ) ([]string, libkb.ProofError) { logger := func(format string, args ...any) { debugWithState(m, state, format, args...) } jsonResults, perr := jsonhelpers.AtSelectorPath(selectedObject, selectors, logger, libkb.NewInvalidPVLSelectorError) if perrInner, _ := perr.(libkb.ProofError); perrInner != nil { return nil, perrInner } if perr != nil { return nil, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "json select error in pvl interp") } results := []string{} for _, object := range jsonResults { s, err := jsonhelpers.JSONStringSimple(object) if err != nil { logger("JSON could not read object: %v (%v)", err, object) continue } results = append(results, s) } return results, nil } // Take a regex descriptor, do variable substitution, and build a regex. func interpretRegex(m metaContext, state scriptState, rdesc regexDescriptor) (*regexp.Regexp, libkb.ProofError) { // Check that regex is bounded by "^" and "$". if !strings.HasPrefix(rdesc.Template, "^") { return nil, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Could not build regex: %v (%v)", "must start with '^'", rdesc.Template) } if !strings.HasSuffix(rdesc.Template, "$") { return nil, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Could not build regex: %v (%v)", "must end with '$'", rdesc.Template) } optstring := "" if rdesc.CaseInsensitive { optstring += "i" } if rdesc.MultiLine { optstring += "m" } prefix := "" if len(optstring) > 0 { prefix = "(?" + optstring + ")" } // Do variable interpolation. prepattern, perr := substituteReEscape(rdesc.Template, state) if perr != nil { return nil, perr } pattern := prefix + prepattern // Build the regex. re, err := regexp.Compile(pattern) if err != nil { debugWithState(m, state, "Could not compile regex: %v\n %v\n %v", err, rdesc.Template, pattern) return nil, libkb.NewProofError(keybase1.ProofStatus_INVALID_PVL, "Could not compile regex: %v (%v)", err, rdesc.Template) } return re, nil } // Use a custom error spec to derive an error. // spec can be none // Copies over the error code if none is specified. func replaceCustomError(m metaContext, state scriptState, spec *errorT, err1 libkb.ProofError) libkb.ProofError { if err1 == nil { return err1 } // Don't rewrite invalid_pvl errors if err1.GetProofStatus() == keybase1.ProofStatus_INVALID_PVL { return err1 } if spec == nil { return err1 } if (spec.Status != err1.GetProofStatus()) || (spec.Description != err1.GetDesc()) { newDesc := spec.Description subbedDesc, subErr := substituteExact(spec.Description, state) if subErr == nil { newDesc = subbedDesc } err2 := libkb.NewProofError(spec.Status, "%s", newDesc) debugWithState(m, state, "Replacing error with custom error") debugWithStateError(m, state, err2) return err2 } return err1 }