| // Copyright (c) 2019, Google Inc. |
| // |
| // Permission to use, copy, modify, and/or distribute this software for any |
| // purpose with or without fee is hereby granted, provided that the above |
| // copyright notice and this permission notice appear in all copies. |
| // |
| // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY |
| // SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION |
| // OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN |
| // CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| |
| //go:build interactive |
| // +build interactive |
| |
| package main |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| neturl "net/url" |
| "os" |
| "os/exec" |
| "os/signal" |
| "path/filepath" |
| "reflect" |
| "strconv" |
| "strings" |
| "syscall" |
| |
| "boringssl.googlesource.com/boringssl/util/fipstools/acvp/acvptool/acvp" |
| "golang.org/x/crypto/ssh/terminal" |
| ) |
| |
| const interactiveModeSupported = true |
| |
| func updateTerminalSize(term *terminal.Terminal) { |
| width, height, err := terminal.GetSize(0) |
| if err != nil { |
| return |
| } |
| term.SetSize(width, height) |
| } |
| |
| func skipWS(node *node32) *node32 { |
| for ; node != nil && node.pegRule == ruleWS; node = node.next { |
| } |
| return node |
| } |
| |
| func assertNodeType(node *node32, rule pegRule) { |
| if node.pegRule != rule { |
| panic(fmt.Sprintf("expected %q, found %q", rul3s[rule], rul3s[node.pegRule])) |
| } |
| } |
| |
| type Object interface { |
| String() (string, error) |
| Index(string) (Object, error) |
| Search(acvp.Query) (Object, error) |
| Action(action string, args []string) error |
| } |
| |
| type ServerObjectSet struct { |
| env *Env |
| name string |
| searchKeys map[string][]acvp.Relation |
| resultType reflect.Type |
| subObjects map[string]func(*Env, string) (Object, error) |
| canEnumerate bool |
| } |
| |
| func (set ServerObjectSet) String() (string, error) { |
| if !set.canEnumerate { |
| return "[object set " + set.name + "]", nil |
| } |
| |
| data := reflect.New(reflect.SliceOf(set.resultType)).Interface() |
| if err := set.env.server.GetPaged(data, "acvp/v1/"+set.name, nil); err != nil { |
| return "", err |
| } |
| ret, err := json.MarshalIndent(data, "", " ") |
| return string(ret), err |
| } |
| |
| func (set ServerObjectSet) Index(indexStr string) (Object, error) { |
| index, err := strconv.ParseUint(indexStr, 0, 64) |
| if err != nil { |
| return nil, fmt.Errorf("object set indexes must be unsigned integers, trying to parse %q failed: %s", indexStr, err) |
| } |
| return ServerObject{&set, index}, nil |
| } |
| |
| func (set ServerObjectSet) Search(condition acvp.Query) (Object, error) { |
| if set.searchKeys == nil { |
| return nil, errors.New("this object set cannot be searched") |
| } |
| |
| for _, conj := range condition { |
| NextCondition: |
| for _, cond := range conj { |
| allowed, ok := set.searchKeys[cond.Param] |
| if !ok { |
| return nil, fmt.Errorf("search key %q not valid for this object set", cond.Param) |
| } |
| |
| for _, rel := range allowed { |
| if rel == cond.Relation { |
| continue NextCondition |
| } |
| } |
| |
| return nil, fmt.Errorf("search key %q cannot be used with relation %q", cond.Param, cond.Relation.String()) |
| } |
| } |
| |
| return Search{ServerObjectSet: set, query: condition}, nil |
| } |
| |
| func (set ServerObjectSet) Action(action string, args []string) error { |
| switch action { |
| default: |
| return fmt.Errorf("unknown action %q", action) |
| |
| case "new": |
| if len(args) != 0 { |
| return fmt.Errorf("found %d arguments but %q takes none", len(args), action) |
| } |
| |
| newContents, err := edit("") |
| if err != nil { |
| return err |
| } |
| |
| if strings.TrimSpace(string(newContents)) == "" { |
| io.WriteString(set.env.term, "Resulting file was empty. Ignoring.\n") |
| return nil |
| } |
| |
| var result map[string]interface{} |
| if err := set.env.server.Post(&result, "acvp/v1/"+set.name, newContents); err != nil { |
| return err |
| } |
| |
| // In case it's a testSession that was just created, poke any access token |
| // into the server's lookup table and the cache. |
| if urlInterface, ok := result["url"]; ok { |
| if url, ok := urlInterface.(string); ok { |
| if tokenInterface, ok := result["accessToken"]; ok { |
| if token, ok := tokenInterface.(string); ok { |
| for strings.HasPrefix(url, "/") { |
| url = url[1:] |
| } |
| set.env.server.PrefixTokens[url] = token |
| if len(set.env.config.SessionTokensCache) > 0 { |
| os.WriteFile(filepath.Join(set.env.config.SessionTokensCache, neturl.PathEscape(url))+".token", []byte(token), 0600) |
| } |
| } |
| } |
| } |
| } |
| |
| ret, err := json.MarshalIndent(result, "", " ") |
| if err != nil { |
| return err |
| } |
| set.env.term.Write(ret) |
| return nil |
| } |
| } |
| |
| type ServerObject struct { |
| set *ServerObjectSet |
| index uint64 |
| } |
| |
| func (obj ServerObject) String() (string, error) { |
| data := reflect.New(obj.set.resultType).Interface() |
| if err := obj.set.env.server.Get(data, "acvp/v1/"+obj.set.name+"/"+strconv.FormatUint(obj.index, 10)); err != nil { |
| return "", err |
| } |
| ret, err := json.MarshalIndent(data, "", " ") |
| return string(ret), err |
| } |
| |
| func (obj ServerObject) Index(index string) (Object, error) { |
| if obj.set.subObjects == nil { |
| return nil, errors.New("cannot index " + obj.set.name + " objects") |
| } |
| constr, ok := obj.set.subObjects[index] |
| if !ok { |
| return nil, fmt.Errorf("no such subobject %q", index) |
| } |
| return constr(obj.set.env, fmt.Sprintf("%s/%d", obj.set.name, obj.index)) |
| } |
| |
| func (ServerObject) Search(condition acvp.Query) (Object, error) { |
| return nil, errors.New("cannot search individual object") |
| } |
| |
| func edit(initialContents string) ([]byte, error) { |
| tmp, err := os.CreateTemp("", "acvp*.json") |
| if err != nil { |
| return nil, err |
| } |
| path := tmp.Name() |
| defer os.Remove(path) |
| |
| _, err = io.WriteString(tmp, initialContents) |
| tmp.Close() |
| if err != nil { |
| return nil, err |
| } |
| |
| editor := os.Getenv("EDITOR") |
| if len(editor) == 0 { |
| editor = "vim" |
| } |
| |
| cmd := exec.Command(editor, path) |
| cmd.Stdout = os.Stdout |
| cmd.Stdin = os.Stdin |
| cmd.Stderr = os.Stderr |
| if err := cmd.Run(); err != nil { |
| return nil, err |
| } |
| |
| return os.ReadFile(path) |
| } |
| |
| func (obj ServerObject) Action(action string, args []string) error { |
| switch action { |
| default: |
| return fmt.Errorf("unknown action %q", action) |
| |
| case "edit": |
| if len(args) != 0 { |
| return fmt.Errorf("found %d arguments but %q takes none", len(args), action) |
| } |
| |
| contents, err := obj.String() |
| if err != nil { |
| return err |
| } |
| |
| newContents, err := edit(contents) |
| if err != nil { |
| return err |
| } |
| |
| if trimmed := strings.TrimSpace(string(newContents)); len(trimmed) == 0 || trimmed == strings.TrimSpace(contents) { |
| io.WriteString(obj.set.env.term, "Resulting file was equal or empty. Not updating.\n") |
| return nil |
| } |
| |
| var status acvp.RequestStatus |
| if err := obj.set.env.server.Put(&status, "acvp/v1/"+obj.set.name+"/"+strconv.FormatUint(obj.index, 10), newContents); err != nil { |
| return err |
| } |
| |
| fmt.Fprintf(obj.set.env.term, "%#v\n", status) |
| return nil |
| |
| case "delete": |
| if len(args) != 0 { |
| return fmt.Errorf("found %d arguments but %q takes none", len(args), action) |
| } |
| return obj.set.env.server.Delete("acvp/v1/" + obj.set.name + "/" + strconv.FormatUint(obj.index, 10)) |
| } |
| } |
| |
| type Search struct { |
| ServerObjectSet |
| query acvp.Query |
| } |
| |
| func (search Search) String() (string, error) { |
| data := reflect.New(reflect.SliceOf(search.resultType)).Interface() |
| fmt.Printf("Searching for %#v\n", search.query) |
| if err := search.env.server.GetPaged(data, "acvp/v1/"+search.name, search.query); err != nil { |
| return "", err |
| } |
| ret, err := json.MarshalIndent(data, "", " ") |
| return string(ret), err |
| } |
| |
| func (search Search) Index(_ string) (Object, error) { |
| return nil, errors.New("indexing of search results not supported") |
| } |
| |
| func (search Search) Search(condition acvp.Query) (Object, error) { |
| search.query = append(search.query, condition...) |
| return search, nil |
| } |
| |
| func (Search) Action(_ string, _ []string) error { |
| return errors.New("no actions supported on search objects") |
| } |
| |
| type Algorithms struct { |
| ServerObjectSet |
| } |
| |
| func (algos Algorithms) String() (string, error) { |
| var result struct { |
| Algorithms []map[string]interface{} `json:"algorithms"` |
| } |
| if err := algos.env.server.Get(&result, "acvp/v1/algorithms"); err != nil { |
| return "", err |
| } |
| ret, err := json.MarshalIndent(result.Algorithms, "", " ") |
| return string(ret), err |
| } |
| |
| type Env struct { |
| line string |
| variables map[string]Object |
| server *acvp.Server |
| term *terminal.Terminal |
| config Config |
| } |
| |
| func (e *Env) bytes(node *node32) []byte { |
| return []byte(e.line[node.begin:node.end]) |
| } |
| |
| func (e *Env) contents(node *node32) string { |
| return e.line[node.begin:node.end] |
| } |
| |
| type stringLiteral struct { |
| env *Env |
| contents string |
| } |
| |
| func (s stringLiteral) String() (string, error) { |
| return s.contents, nil |
| } |
| |
| func (stringLiteral) Index(_ string) (Object, error) { |
| return nil, errors.New("cannot index strings") |
| } |
| |
| func (stringLiteral) Search(_ acvp.Query) (Object, error) { |
| return nil, errors.New("cannot search strings") |
| } |
| |
| func (s stringLiteral) Action(action string, args []string) error { |
| switch action { |
| default: |
| return fmt.Errorf("action %q not supported on string literals", action) |
| |
| case "GET": |
| if len(args) != 0 { |
| return fmt.Errorf("found %d arguments but %q takes none", len(args), action) |
| } |
| |
| var results map[string]interface{} |
| if err := s.env.server.Get(&results, s.contents); err != nil { |
| return err |
| } |
| ret, err := json.MarshalIndent(results, "", " ") |
| if err != nil { |
| return err |
| } |
| s.env.term.Write(ret) |
| return nil |
| } |
| } |
| |
| type results struct { |
| env *Env |
| prefix string |
| } |
| |
| func (r results) String() (string, error) { |
| var results map[string]interface{} |
| if err := r.env.server.Get(&results, "acvp/v1/"+r.prefix+"/results"); err != nil { |
| return "", err |
| } |
| ret, err := json.MarshalIndent(results, "", " ") |
| return string(ret), err |
| } |
| |
| func (results) Index(_ string) (Object, error) { |
| return nil, errors.New("cannot index results objects") |
| } |
| |
| func (results) Search(_ acvp.Query) (Object, error) { |
| return nil, errors.New("cannot search results objects") |
| } |
| |
| func (results) Action(_ string, _ []string) error { |
| return errors.New("no actions supported on results objects") |
| } |
| |
| func (e *Env) parseStringLiteral(node *node32) string { |
| assertNodeType(node, ruleStringLiteral) |
| in := e.bytes(node) |
| var buf bytes.Buffer |
| for i := 1; i < len(in)-1; i++ { |
| if in[i] == '\\' { |
| switch in[i+1] { |
| case '\\': |
| buf.WriteByte('\\') |
| case 'n': |
| buf.WriteByte('\n') |
| case '"': |
| buf.WriteByte('"') |
| default: |
| panic("unknown escape") |
| } |
| i++ |
| continue |
| } |
| buf.WriteByte(in[i]) |
| } |
| |
| return buf.String() |
| } |
| |
| func (e *Env) evalExpression(node *node32) (obj Object, err error) { |
| switch node.pegRule { |
| case ruleStringLiteral: |
| return stringLiteral{e, e.parseStringLiteral(node)}, nil |
| |
| case ruleVariable: |
| varName := e.contents(node) |
| obj, ok := e.variables[varName] |
| if !ok { |
| return nil, fmt.Errorf("unknown variable %q", varName) |
| } |
| return obj, nil |
| |
| case ruleIndexing: |
| node = node.up |
| assertNodeType(node, ruleVariable) |
| varName := e.contents(node) |
| obj, ok := e.variables[varName] |
| if !ok { |
| return nil, fmt.Errorf("unknown variable %q", varName) |
| } |
| |
| node = node.next |
| for node != nil { |
| assertNodeType(node, ruleIndex) |
| indexStr := e.contents(node) |
| if obj, err = obj.Index(indexStr); err != nil { |
| return nil, err |
| } |
| node = node.next |
| } |
| |
| return obj, nil |
| |
| case ruleSearch: |
| node = node.up |
| assertNodeType(node, ruleVariable) |
| varName := e.contents(node) |
| obj, ok := e.variables[varName] |
| if !ok { |
| return nil, fmt.Errorf("unknown variable %q", varName) |
| } |
| |
| node = skipWS(node.next) |
| assertNodeType(node, ruleQuery) |
| node = node.up |
| |
| var query acvp.Query |
| for node != nil { |
| assertNodeType(node, ruleConjunctions) |
| query = append(query, e.parseConjunction(node.up)) |
| node = skipWS(node.next) |
| } |
| |
| if len(query) == 0 { |
| return nil, errors.New("cannot have empty query") |
| } |
| |
| return obj.Search(query) |
| } |
| |
| panic("unhandled") |
| } |
| |
| func (e *Env) evalAction(node *node32) error { |
| assertNodeType(node, ruleExpression) |
| obj, err := e.evalExpression(node.up) |
| if err != nil { |
| return err |
| } |
| |
| node = node.next |
| assertNodeType(node, ruleCommand) |
| node = node.up |
| assertNodeType(node, ruleFunction) |
| function := e.contents(node) |
| node = node.next |
| |
| var args []string |
| for node != nil { |
| assertNodeType(node, ruleArgs) |
| node = node.up |
| args = append(args, e.parseStringLiteral(node)) |
| |
| node = skipWS(node.next) |
| } |
| |
| return obj.Action(function, args) |
| } |
| |
| func (e *Env) parseConjunction(node *node32) (ret acvp.Conjunction) { |
| for node != nil { |
| assertNodeType(node, ruleConjunction) |
| ret = append(ret, e.parseCondition(node.up)) |
| |
| node = skipWS(node.next) |
| if node != nil { |
| assertNodeType(node, ruleConjunctions) |
| node = node.up |
| } |
| } |
| return ret |
| } |
| |
| func (e *Env) parseCondition(node *node32) (ret acvp.Condition) { |
| assertNodeType(node, ruleField) |
| ret.Param = e.contents(node) |
| node = skipWS(node.next) |
| |
| assertNodeType(node, ruleRelation) |
| switch e.contents(node) { |
| case "==": |
| ret.Relation = acvp.Equals |
| case "!=": |
| ret.Relation = acvp.NotEquals |
| case "contains": |
| ret.Relation = acvp.Contains |
| case "startsWith": |
| ret.Relation = acvp.StartsWith |
| case "endsWith": |
| ret.Relation = acvp.EndsWith |
| default: |
| panic("relation not handled: " + e.contents(node)) |
| } |
| node = skipWS(node.next) |
| |
| ret.Value = e.parseStringLiteral(node) |
| |
| return ret |
| } |
| |
| func runInteractive(server *acvp.Server, config Config) { |
| oldState, err := terminal.MakeRaw(0) |
| if err != nil { |
| panic(err) |
| } |
| defer terminal.Restore(0, oldState) |
| term := terminal.NewTerminal(os.Stdin, "> ") |
| |
| resizeChan := make(chan os.Signal, 1) |
| go func() { |
| for _ = range resizeChan { |
| updateTerminalSize(term) |
| } |
| }() |
| signal.Notify(resizeChan, syscall.SIGWINCH) |
| |
| env := &Env{variables: make(map[string]Object), server: server, term: term, config: config} |
| env.variables["requests"] = ServerObjectSet{ |
| env: env, |
| name: "requests", |
| resultType: reflect.TypeOf(&acvp.RequestStatus{}), |
| canEnumerate: true, |
| } |
| env.variables["vendors"] = ServerObjectSet{ |
| env: env, |
| name: "vendors", |
| searchKeys: map[string][]acvp.Relation{ |
| "name": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| "website": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| "email": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| "phoneNumber": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| }, |
| subObjects: map[string]func(*Env, string) (Object, error){ |
| "contacts": func(env *Env, prefix string) (Object, error) { |
| return ServerObjectSet{ |
| env: env, |
| name: prefix + "/contacts", |
| resultType: reflect.TypeOf(&acvp.Person{}), |
| canEnumerate: true, |
| }, nil |
| }, |
| "addresses": func(env *Env, prefix string) (Object, error) { |
| return ServerObjectSet{ |
| env: env, |
| name: prefix + "/addresses", |
| resultType: reflect.TypeOf(&acvp.Address{}), |
| canEnumerate: true, |
| }, nil |
| }, |
| }, |
| resultType: reflect.TypeOf(&acvp.Vendor{}), |
| } |
| env.variables["persons"] = ServerObjectSet{ |
| env: env, |
| name: "persons", |
| searchKeys: map[string][]acvp.Relation{ |
| "fullName": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| "email": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| "phoneNumber": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| "vendorId": []acvp.Relation{acvp.Equals, acvp.NotEquals, acvp.LessThan, acvp.LessThanEqual, acvp.GreaterThan, acvp.GreaterThanEqual}, |
| }, |
| resultType: reflect.TypeOf(&acvp.Person{}), |
| } |
| env.variables["modules"] = ServerObjectSet{ |
| env: env, |
| name: "modules", |
| searchKeys: map[string][]acvp.Relation{ |
| "name": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| "version": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| "website": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| "description": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| "type": []acvp.Relation{acvp.Equals, acvp.NotEquals}, |
| "vendorId": []acvp.Relation{acvp.Equals, acvp.NotEquals, acvp.LessThan, acvp.LessThanEqual, acvp.GreaterThan, acvp.GreaterThanEqual}, |
| }, |
| resultType: reflect.TypeOf(&acvp.Module{}), |
| } |
| env.variables["oes"] = ServerObjectSet{ |
| env: env, |
| name: "oes", |
| searchKeys: map[string][]acvp.Relation{ |
| "name": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| }, |
| resultType: reflect.TypeOf(&acvp.OperationalEnvironment{}), |
| } |
| env.variables["deps"] = ServerObjectSet{ |
| env: env, |
| name: "dependencies", |
| searchKeys: map[string][]acvp.Relation{ |
| "name": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| "type": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| "description": []acvp.Relation{acvp.Equals, acvp.StartsWith, acvp.EndsWith, acvp.Contains}, |
| }, |
| resultType: reflect.TypeOf(&acvp.Dependency{}), |
| } |
| env.variables["algos"] = Algorithms{ |
| ServerObjectSet{ |
| env: env, |
| name: "algorithms", |
| resultType: reflect.TypeOf(&acvp.Algorithm{}), |
| canEnumerate: true, |
| }, |
| } |
| env.variables["sessions"] = ServerObjectSet{ |
| env: env, |
| name: "testSessions", |
| resultType: reflect.TypeOf(&acvp.TestSession{}), |
| canEnumerate: true, |
| subObjects: map[string]func(env *Env, prefix string) (Object, error){ |
| "results": func(env *Env, prefix string) (Object, error) { |
| return results{env: env, prefix: prefix}, nil |
| }, |
| }, |
| } |
| |
| for { |
| if env.line, err = term.ReadLine(); err != nil { |
| return |
| } |
| if len(env.line) == 0 { |
| continue |
| } |
| |
| stmt := Statement{Buffer: env.line, Pretty: true} |
| stmt.Init() |
| if err := stmt.Parse(); err != nil { |
| io.WriteString(term, err.Error()) |
| continue |
| } |
| |
| node := skipWS(stmt.AST().up) |
| switch node.pegRule { |
| case ruleExpression: |
| obj, err := env.evalExpression(node.up) |
| var repr string |
| if err == nil { |
| repr, err = obj.String() |
| } |
| |
| if err != nil { |
| fmt.Fprintf(term, "error while evaluating expression: %s\n", err) |
| } else { |
| io.WriteString(term, repr) |
| io.WriteString(term, "\n") |
| } |
| |
| case ruleAction: |
| if err := env.evalAction(node.up); err != nil { |
| io.WriteString(term, err.Error()) |
| io.WriteString(term, "\n") |
| } |
| |
| default: |
| fmt.Fprintf(term, "internal error parsing input.\n") |
| } |
| } |
| } |