cloudflare/cloudflared

Public

mirrored fromhttps://github.com/cloudflare/cloudflaredAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2020.6.1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

dbconnect/client.go

145lines · modecode

1package dbconnect
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/url"
8 "strings"
9 "time"
10 "unicode"
11 "unicode/utf8"
12)
13
14// Client is an interface to talk to any database.
15//
16// Currently, the only implementation is SQLClient, but its structure
17// should be designed to handle a MongoClient or RedisClient in the future.
18type Client interface {
19 Ping(context.Context) error
20 Submit(context.Context, *Command) (interface{}, error)
21}
22
23// NewClient creates a database client based on its URL scheme.
24func NewClient(ctx context.Context, originURL *url.URL) (Client, error) {
25 return NewSQLClient(ctx, originURL)
26}
27
28// Command is a standard, non-vendor format for submitting database commands.
29//
30// When determining the scope of this struct, refer to the following litmus test:
31// Could this (roughly) conform to SQL, Document-based, and Key-value command formats?
32type Command struct {
33 Statement string `json:"statement"`
34 Arguments Arguments `json:"arguments,omitempty"`
35 Mode string `json:"mode,omitempty"`
36 Isolation string `json:"isolation,omitempty"`
37 Timeout time.Duration `json:"timeout,omitempty"`
38}
39
40// Validate enforces the contract of Command: non empty statement (both in length and logic),
41// lowercase mode and isolation, non-zero timeout, and valid Arguments.
42func (cmd *Command) Validate() error {
43 if cmd.Statement == "" {
44 return fmt.Errorf("cannot provide an empty statement")
45 }
46
47 if strings.Map(func(char rune) rune {
48 if char == ';' || unicode.IsSpace(char) {
49 return -1
50 }
51 return char
52 }, cmd.Statement) == "" {
53 return fmt.Errorf("cannot provide a statement with no logic: '%s'", cmd.Statement)
54 }
55
56 cmd.Mode = strings.ToLower(cmd.Mode)
57 cmd.Isolation = strings.ToLower(cmd.Isolation)
58
59 if cmd.Timeout.Nanoseconds() <= 0 {
60 cmd.Timeout = 24 * time.Hour
61 }
62
63 return cmd.Arguments.Validate()
64}
65
66// UnmarshalJSON converts a byte representation of JSON into a Command, which is also validated.
67func (cmd *Command) UnmarshalJSON(data []byte) error {
68 // Alias is required to avoid infinite recursion from the default UnmarshalJSON.
69 type Alias Command
70 alias := &struct {
71 *Alias
72 }{
73 Alias: (*Alias)(cmd),
74 }
75
76 err := json.Unmarshal(data, &alias)
77 if err == nil {
78 err = cmd.Validate()
79 }
80
81 return err
82}
83
84// Arguments is a wrapper for either map-based or array-based Command arguments.
85//
86// Each field is mutually-exclusive and some Client implementations may not
87// support both fields (eg. MySQL does not accept named arguments).
88type Arguments struct {
89 Named map[string]interface{}
90 Positional []interface{}
91}
92
93// Validate enforces the contract of Arguments: non nil, mutually exclusive, and no empty or reserved keys.
94func (args *Arguments) Validate() error {
95 if args.Named == nil {
96 args.Named = map[string]interface{}{}
97 }
98 if args.Positional == nil {
99 args.Positional = []interface{}{}
100 }
101
102 if len(args.Named) > 0 && len(args.Positional) > 0 {
103 return fmt.Errorf("both named and positional arguments cannot be specified: %+v and %+v", args.Named, args.Positional)
104 }
105
106 for key := range args.Named {
107 if key == "" {
108 return fmt.Errorf("named arguments cannot contain an empty key: %+v", args.Named)
109 }
110 if !utf8.ValidString(key) {
111 return fmt.Errorf("named argument does not conform to UTF-8 encoding: %s", key)
112 }
113 if strings.HasPrefix(key, "_") {
114 return fmt.Errorf("named argument cannot start with a reserved keyword '_': %s", key)
115 }
116 if unicode.IsNumber([]rune(key)[0]) {
117 return fmt.Errorf("named argument cannot start with a number: %s", key)
118 }
119 }
120
121 return nil
122}
123
124// UnmarshalJSON converts a byte representation of JSON into Arguments, which is also validated.
125func (args *Arguments) UnmarshalJSON(data []byte) error {
126 var obj interface{}
127 err := json.Unmarshal(data, &obj)
128 if err != nil {
129 return err
130 }
131
132 named, ok := obj.(map[string]interface{})
133 if ok {
134 args.Named = named
135 } else {
136 positional, ok := obj.([]interface{})
137 if ok {
138 args.Positional = positional
139 } else {
140 return fmt.Errorf("arguments must either be an object {\"0\":\"val\"} or an array [\"val\"]: %s", string(data))
141 }
142 }
143
144 return args.Validate()
145}
146