hancock

Check-in [ef3dc07f7e]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:cmd/hancock: refactor to use Store interface
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: ef3dc07f7ec205bd485dd92be792dc033e46889140fd970ccf46b007c58293bf
User & Date: dnc 2019-12-13 04:22:14
Context
2020-01-03
02:29
store: consistent error when testimony not found; expose closer check-in: d22c07e59b user: dnc tags: trunk
2019-12-13
04:22
cmd/hancock: refactor to use Store interface check-in: ef3dc07f7e user: dnc tags: trunk
04:19
model: NewAuthority helper formats authority string consistently check-in: e0fadea8cc user: dnc tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to cmd/hancock/hancock.go.

76
77
78
79
80
81
82


83

84
85
86


87
88
89
90
91
92
93
...
110
111
112
113
114
115
116
















































//go:generate sh -c "go doc | dumbdown > README.md"

import (
	"flag"
	"fmt"
	"log"




	"github.com/pkg/errors"
	"src.d10.dev/command"
	"src.d10.dev/command/config"


)

func main() {
	command.RegisterCommand(command.Command{
		Application: `hancock`,
		Description: `Create and inspect Hancock Protocol activity.`,
	})
................................................................................
	log.SetPrefix(fmt.Sprintf("hancock %s: ", flag.CommandLine.Args()[0]))

	err = command.CurrentOperation().Operate()
	command.CheckUsage(err)

	command.Exit()
}






















































>
>

>



>
>







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
...
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168

//go:generate sh -c "go doc | dumbdown > README.md"

import (
	"flag"
	"fmt"
	"log"
	"net/url"
	"os"

	"github.com/boltdb/bolt"
	"github.com/pkg/errors"
	"src.d10.dev/command"
	"src.d10.dev/command/config"
	"src.d10.dev/hancock/store"
	"src.d10.dev/hancock/store/boltstore"
)

func main() {
	command.RegisterCommand(command.Command{
		Application: `hancock`,
		Description: `Create and inspect Hancock Protocol activity.`,
	})
................................................................................
	log.SetPrefix(fmt.Sprintf("hancock %s: ", flag.CommandLine.Args()[0]))

	err = command.CurrentOperation().Operate()
	command.CheckUsage(err)

	command.Exit()
}

var stor map[string]store.Store

// TODO(dnc): close all stores that implement io.Closer

// helper to construct a store from a command line flag
func storeFromFlag(flag string) (store.Store, error) {
	if stor == nil {
		stor = make(map[string]store.Store)
	}
	s, ok := stor[flag]
	if ok {
		return s, nil
	}

	u, err := url.Parse(flag)
	if err != nil {
		return nil, err
	}

	switch u.Scheme {
	case "file":
		fi, err := os.Stat(u.Path)
		if err != nil {
			return nil, err
		}
		switch mode := fi.Mode(); {
		case mode.IsDir():
			s = store.NewFileStore(u.Path)
		case mode.IsRegular():
			db, err := bolt.Open(u.Path, 0600, nil)
			if err != nil {
				return nil, err
			}
			s, err = boltstore.NewBoltStore(db)
			if err != nil {
				return nil, err
			}
		}

		// TODO(dnc): case "http(s)"

	default:
		return nil, fmt.Errorf("unexpected store sheme (%q)", u.Scheme)
	}
	return s, nil
}

Changes to cmd/hancock/testimony.go.

155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172


173



174


175
176
177
178
179
180
181
182
183
			Authority: authorityEncoded,
			Content:   man.Content,
			Encoded:   rawMan,
			Signature: sig,
		}

		if !*noopFlag {
			ft := fileTestimony{Testimony: testimony, FileManifest: man} // testimony with decoded manifest
			stored := false
			for _, storeURL := range storeFlag {
				if storeURL == "" {
					continue // explicit -index="" on command line
				}
				err = publishTestimonyToStore(storeURL, ft)
				if err != nil {
					command.Errorf("%q failed to publish testimony to index (%s): %s", man.Path, storeURL, err)
				} else {
					stored = true


					command.V(2).Infof("%q testimony stored (%s)", man.Path, storeURL)



				}


			}
			if stored {
				command.V(1).Infof("%q testimony published", man.Path)
			}
		}
	}

	return nil
}







|





|

|
<
<
>
>
|
>
>
>

>
>









155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170


171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
			Authority: authorityEncoded,
			Content:   man.Content,
			Encoded:   rawMan,
			Signature: sig,
		}

		if !*noopFlag {
			//ft := fileTestimony{Testimony: testimony, FileManifest: man} // testimony with decoded manifest
			stored := false
			for _, storeURL := range storeFlag {
				if storeURL == "" {
					continue // explicit -index="" on command line
				}
				stor, err := storeFromFlag(storeURL)
				if err != nil {
					command.Errorf("failed to publish testimony (%q) to %q: %s", man.Path, storeURL, err)


					continue
				}
				tkey, err := stor.PutTestimony(testimony)
				if err != nil {
					command.Errorf("failed to publish testimony (%q) to %q: %s", man.Path, storeURL, err)
					continue
				}
				stored = true
				command.V(2).Infof("published testimony (%q) to %q; key: %v", man.Path, storeURL, tkey)
			}
			if stored {
				command.V(1).Infof("%q testimony published", man.Path)
			}
		}
	}

	return nil
}

Changes to cmd/hancock/verify.go.

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
..
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125

126
127
128
129
130
131
132
133
134
135
136
137
138

139
140
141
142
143
144
145
146
147
148
149
150
151
152

153
154
155
156
157
158
159
160
161
...
163
164
165
166
167
168
169

170
171
172
173
174
175
176


177
178
179
180
181
182
183
...
188
189
190
191
192
193
194
195
196
197
198

199
200
201
202
203
204
205
206
207
208
209
210
211
212
213



214
215
216
217
218
219
220

221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241


242
243


244
245
246
247
248
249
250
251
252
253
254
255
256

257
258
259



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
//     [authority]
//         d10 = ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEMLF+xyqVxGP9iK5UK/v/PFqGJbnmKZ6LRK3qmr8tEi
//
// (values are in the format of "~/ssh/.authorized_keys")
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/pkg/errors"
	"golang.org/x/crypto/ssh"
	"src.d10.dev/command"
	"src.d10.dev/hancock/model"
)

// Produce a summary of human-readable information with a minimum of
// redundancy.
type SummaryMessages map[string]int // key is human-readable message, count is how many times it was produced

type Summary map[string]SummaryMessages // key is directory that was inspected

func (summary Summary) summarize(typ string, currentFile string, manifest model.FileManifest, authority Authority) {
	// Produce summary.  Messages repeated for multiple
	// files should appear once in the summary.  So, omit
	// file-specific info.
	currentDir := filepath.Dir(currentFile)

	_, ok := summary[currentDir]
	if !ok {
		summary[currentDir] = make(SummaryMessages)
	}
	timestamp := time.Unix(manifest.Time, 0)

	for _, m := range manifest.Message {
		msg := fmt.Sprintf("%s (%s) by %s: %s", typ, model.QualityString(manifest.Quality), authority.name, m)
		count, _ := summary[currentDir][msg]
		summary[currentDir][msg] = count + 1

		if command.V(1) { // control verbosity
			if currentFile == manifest.Path {
				command.Errorf("%q %s on %s: %s", currentFile, manifest.Role, timestamp.Format(time.RFC850), msg)
			} else {
				command.Errorf("%q (as %q) %s on %s: %s", currentFile, manifest.Path, manifest.Role, timestamp.Format(time.RFC850), msg)
			}
		}
	}
	if len(manifest.Message) == 0 {
		// no explicit message
		msg := fmt.Sprintf("%s (%s) by %s", typ, model.QualityString(manifest.Quality), authority.name)
		count, _ := summary[currentDir][msg]
		summary[currentDir][msg] = count + 1

		if command.V(1) { // control verbosity
			if currentFile == manifest.Path {
				command.Errorf("%q %s on %s: %s", currentFile, manifest.Role, timestamp.Format(time.RFC850), msg)
			} else {
................................................................................
			}
		}

	}

}

// authorized keys, from config files
type Authority struct {
	name   string
	public ssh.PublicKey
}

func init() {
	command.RegisterOperation(command.Operation{
		Handler:     verifyMain,
		Name:        "verify",
		Syntax:      "verify -index <URL> [-r] <file or directory> [...]",
		Description: "Check the authenticity of a source file.",
	})
}

var (
	verifyIndexFlag command.StringSet
)

func verifyMain() error {
	cfg, err := command.Config()
	command.Check(err)

	defaultIndex := cfg.Section("").Key("index").MustString("https://hancock.beyondcentral.com")
	defaultIPFS := cfg.Section("").Key("ipfs").MustString("http://localhost:8080")

	strictFlag := command.OperationFlagSet.Bool("strict", false, "strict mode requires source to be endorsed")
	command.OperationFlagSet.Var(&verifyIndexFlag, "index", fmt.Sprintf("URL of authenticity index (default %s)", defaultIndex))
	ipfsFlag := command.OperationFlagSet.String("ipfs", defaultIPFS, "URL of manifests")

	recurseFlag := command.OperationFlagSet.Bool("r", false, "verify all files in directory")

	err = command.OperationFlagSet.Parse(command.Args()[1:])
	if err != nil {
		return err
	}
	if len(verifyIndexFlag) == 0 && len(defaultIndex) > 0 {
		verifyIndexFlag = append(verifyIndexFlag, strings.Split(defaultIndex, ",")...) // comma separated list allowed in config file
	}
	if len(verifyIndexFlag) == 0 {
		return errors.New("Index is required.  Use `-index <URL>` or configuration file")
	}
	// TODO(dnc): validate an index is reachable


	if len(command.OperationFlagSet.Args()) == 0 {
		return errors.New("Expected <file or directory> parameter.")
	}

	authorities := make([]Authority, 0)
	for _, key := range cfg.Section("authority").Keys() {
		public, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Value()))
		if err != nil {
			command.Check(errors.Wrapf(err, "failed to parse public key (%q %q)", key, key.Value()))
		}
		authorities = append(authorities, Authority{key.Name(), public})
	}


	httpclient := http.Client{
		Timeout: time.Duration(10 * time.Second),
	}

	noRecurse := errors.New("no recurse") // prepare for filepath.Walk

	// Exit status
	status := 0
	summary := make(Summary)
................................................................................
	// TODO(dnc): goroutines for performance
	for _, arg := range command.OperationFlagSet.Args() {
		err = filepath.Walk(arg, func(currentFile string, info os.FileInfo, err error) error {
			if err != nil {
				command.Error(err) // log error
				return nil         // continue walk
			}

			if info.IsDir() {
				if *recurseFlag == false {
					return noRecurse // end of walk
				} else {
					return nil // noop (we only process files)
				}
			}



			verified := false
			repudiated := false
			_ = repudiated // not yet used

			f, err := os.Open(currentFile)
			if err != nil {
................................................................................
			cid := model.NewSha256CID(nil)
			_, err = io.Copy(cid, f)
			if err != nil {
				command.Error(err)
				return nil // continue walk
			}

			for _, authority := range authorities {
				key, err := model.IndexKey(cid.Encode(), authority.public)
				command.Check(err)


				for _, index := range verifyIndexFlag {
					// TODO(dnc): support filesystem index
					u, err := url.Parse(index)
					command.Check(err) // TODO(dnc): validate earlier
					u.Path = path.Join(u.Path, "index", path.Join(key...))
					resp, err := http.Get(u.String())
					if err != nil {
						command.Info(u.Path, err)
						continue
					}
					defer resp.Body.Close()

					switch resp.StatusCode {
					case http.StatusOK:
						body, err := ioutil.ReadAll(resp.Body)



						if err != nil {
							command.Error(err)
							continue
						}
						// body is the CID of testimony regarding the source CID

						u, err = url.Parse(*ipfsFlag)

						command.Check(err) // TODO(dnc): validate earlier
						u.Path = path.Join(u.Path, "ipfs", string(body))
						resp, err = httpclient.Get(u.String())
						if err != nil {
							command.Info(err)
							continue
						}
						defer resp.Body.Close()
						switch resp.StatusCode {
						case http.StatusOK:
							// decode the body
							dec := json.NewDecoder(resp.Body)
							var testimony model.Testimony
							err = dec.Decode(&testimony)
							if err != nil {
								command.Info(err)
								continue
							}

							var manifest model.FileManifest
							err = json.Unmarshal(testimony.Encoded, &manifest)


							if err != nil {
								command.Info(err)


								continue
							}
							err = manifest.Check()
							if err != nil {
								command.Info(err)
								continue
							}

							// checks
							err = testimony.Verify()
							if err != nil {
								// BUG(dnc): may not be an error, if another valid testimony applies to the same file.
								command.Info(errors.Wrapf(err, "invalid testimony (%q)", currentFile))

								continue
							}




							// path check is disabled because an identical file may appear in more than one place.  TODO(dnc) fix this, or enable strict mode
							//if manifest.Path != currentFile {
							//	command.Infof("path mismatch (local: %q, testimony: %q)", currentFile, manifest.Path)
							//	continue
							//}

							if manifest.Quality&model.Valid == 0 {
								// Testimony is repudiation.
								repudiated = true
								status = 1 // non-zero exit status

								summary.summarize("repudiated", currentFile, manifest, authority)

								continue
							}

							verified = true // TODO(dnc): consider a count of weighted authorities, rather than simple verified or not

							summary.summarize("confirmed", currentFile, manifest, authority)

						default:
							command.V(2).Info(currentFile, u.String(), resp.Status)
							continue
						} // end ipfs status
					default:
						command.V(2).Info(currentFile, u.String(), resp.Status)
					} // end index status
				} // end index loop
			} // end authority loop

			if !verified {
				if *strictFlag {
					// command.Error sets exit status code (not zero)
					command.Errorf("%q not verified", currentFile)
					command.Exit() // exit early when strict mode (should this be only when verbose is off?)







<


<
<
<

<





<










|












|













|







 







<
<
<
<
<
<




|





|






<
|


<
<
>






|
|

|
|

|
>





|

|



|

<
>
|
<







 







>







>
>







 







|
|
<

>
|
|
<
<
<
<
<
<
<
|
<

<
<
<
>
>
>
|
<
<
<
<
<
<
>
|
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
|
<
<
<
>
>
|
<
>
>
|
|
<
<
<
<
|
<
|
|
|
|
<
>
|
|

>
>
>
|
|
|
|
|

|
|
|
|

|

|
|

|

|
<
<
<
<
|
<
<
<
<







23
24
25
26
27
28
29

30
31



32

33
34
35
36
37

38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
..
84
85
86
87
88
89
90






91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107

108
109
110


111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138

139
140

141
142
143
144
145
146
147
...
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
...
177
178
179
180
181
182
183
184
185

186
187
188
189







190

191



192
193
194
195






196
197





198










199



200
201
202

203
204
205
206




207

208
209
210
211

212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237




238




239
240
241
242
243
244
245
//     [authority]
//         d10 = ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEMLF+xyqVxGP9iK5UK/v/PFqGJbnmKZ6LRK3qmr8tEi
//
// (values are in the format of "~/ssh/.authorized_keys")
package main

import (

	"fmt"
	"io"



	"os"

	"path/filepath"
	"strings"
	"time"

	"github.com/pkg/errors"

	"src.d10.dev/command"
	"src.d10.dev/hancock/model"
)

// Produce a summary of human-readable information with a minimum of
// redundancy.
type SummaryMessages map[string]int // key is human-readable message, count is how many times it was produced

type Summary map[string]SummaryMessages // key is directory that was inspected

func (summary Summary) summarize(typ string, currentFile string, manifest model.FileManifest, authorityName string) {
	// Produce summary.  Messages repeated for multiple
	// files should appear once in the summary.  So, omit
	// file-specific info.
	currentDir := filepath.Dir(currentFile)

	_, ok := summary[currentDir]
	if !ok {
		summary[currentDir] = make(SummaryMessages)
	}
	timestamp := time.Unix(manifest.Time, 0)

	for _, m := range manifest.Message {
		msg := fmt.Sprintf("%s (%s) by %s: %s", typ, model.QualityString(manifest.Quality), authorityName, m)
		count, _ := summary[currentDir][msg]
		summary[currentDir][msg] = count + 1

		if command.V(1) { // control verbosity
			if currentFile == manifest.Path {
				command.Errorf("%q %s on %s: %s", currentFile, manifest.Role, timestamp.Format(time.RFC850), msg)
			} else {
				command.Errorf("%q (as %q) %s on %s: %s", currentFile, manifest.Path, manifest.Role, timestamp.Format(time.RFC850), msg)
			}
		}
	}
	if len(manifest.Message) == 0 {
		// no explicit message
		msg := fmt.Sprintf("%s (%s) by %s", typ, model.QualityString(manifest.Quality), authorityName)
		count, _ := summary[currentDir][msg]
		summary[currentDir][msg] = count + 1

		if command.V(1) { // control verbosity
			if currentFile == manifest.Path {
				command.Errorf("%q %s on %s: %s", currentFile, manifest.Role, timestamp.Format(time.RFC850), msg)
			} else {
................................................................................
			}
		}

	}

}







func init() {
	command.RegisterOperation(command.Operation{
		Handler:     verifyMain,
		Name:        "verify",
		Syntax:      "verify -store <URL> [-r] <file or directory> [...]",
		Description: "Check the authenticity of a source file.",
	})
}

var (
	verifyStoreFlag command.StringSet
)

func verifyMain() error {
	cfg, err := command.Config()
	command.Check(err)


	defaultStore := cfg.Section("").Key("store").MustString("http://localhost:3344")

	strictFlag := command.OperationFlagSet.Bool("strict", false, "strict mode requires source to be endorsed")


	command.OperationFlagSet.Var(&verifyStoreFlag, "store", fmt.Sprintf("URL of testimony store (default %s)", defaultStore))
	recurseFlag := command.OperationFlagSet.Bool("r", false, "verify all files in directory")

	err = command.OperationFlagSet.Parse(command.Args()[1:])
	if err != nil {
		return err
	}
	if len(verifyStoreFlag) == 0 && len(defaultStore) > 0 {
		verifyStoreFlag = append(verifyStoreFlag, strings.Split(defaultStore, ",")...) // comma separated list allowed in config file
	}
	if len(verifyStoreFlag) == 0 {
		return errors.New("Testimony store is required.  Use `-store <URL>` or configuration file")
	}
	// TODO(dnc): confirm store is reachable
	command.V(3).Info("testimony store:", verifyStoreFlag)

	if len(command.OperationFlagSet.Args()) == 0 {
		return errors.New("Expected <file or directory> parameter.")
	}

	authority := make(map[string]model.Authority)
	for _, key := range cfg.Section("authority").Keys() {
		a, err := model.NewAuthority(key.Value())
		if err != nil {
			command.Check(errors.Wrapf(err, "failed to parse public key (%q %q)", key, key.Value()))
		}
		authority[key.Name()] = a
	}

	if len(authority) == 0 {
		return errors.New("Trusted authority must be configured.")

	}

	noRecurse := errors.New("no recurse") // prepare for filepath.Walk

	// Exit status
	status := 0
	summary := make(Summary)
................................................................................
	// TODO(dnc): goroutines for performance
	for _, arg := range command.OperationFlagSet.Args() {
		err = filepath.Walk(arg, func(currentFile string, info os.FileInfo, err error) error {
			if err != nil {
				command.Error(err) // log error
				return nil         // continue walk
			}

			if info.IsDir() {
				if *recurseFlag == false {
					return noRecurse // end of walk
				} else {
					return nil // noop (we only process files)
				}
			}

			command.V(3).Infof("visiting file %q", currentFile) // verbose

			verified := false
			repudiated := false
			_ = repudiated // not yet used

			f, err := os.Open(currentFile)
			if err != nil {
................................................................................
			cid := model.NewSha256CID(nil)
			_, err = io.Copy(cid, f)
			if err != nil {
				command.Error(err)
				return nil // continue walk
			}

			for authorityName, auth := range authority {
				command.V(3).Infof("file %q; authority %q (%q)", currentFile, authorityName, auth) // verbose


				tkey := model.TestimonyKey{
					Authority: auth,
					Content:   cid.Encode(),







				}





				for _, storeFlag := range verifyStoreFlag {
					command.V(3).Infof("querying %q for %q (%v)", storeFlag, currentFile, tkey)
					store, err := storeFromFlag(storeFlag)
					if err != nil {






						err = fmt.Errorf("failed to access testimony store (%q): %w", storeFlag, err)
						command.Check(err)





					}














					// TODO(dnc): concurrency for performance
					testimony, err := store.GetTestimony(tkey)
					if err != nil {

						// don't exit, as the testimony may be retrieved from another store
						command.Infof("failed to retrieve testimony via %q: %s", storeFlag, err)
						continue
					}






					// checks
					err = testimony.Verify()
					if err != nil {
						// may not be an error, if another valid testimony applies to the same file.

						command.Info(fmt.Errorf("invalid testimony (authority %q; file %q) ignored", authorityName, currentFile))
						continue
					}

					var manifest model.FileManifest
					err = model.Decode(&manifest, testimony.Encoded)

					// path check is disabled because an identical file may appear in more than one place.  TODO(dnc) fix this, or enable strict mode
					//if manifest.Path != currentFile {
					//	command.Infof("path mismatch (local: %q, testimony: %q)", currentFile, manifest.Path)
					//	continue
					//}

					if manifest.Quality&model.Valid == 0 {
						// Testimony is repudiation.
						repudiated = true
						status = 1 // non-zero exit status

						summary.summarize("repudiated", currentFile, manifest, authorityName)

						continue
					}

					verified = true // TODO(dnc): consider a count of weighted authorities, rather than simple verified or not

					summary.summarize("confirmed", currentFile, manifest, authorityName)




				} // end store loop




			} // end authority loop

			if !verified {
				if *strictFlag {
					// command.Error sets exit status code (not zero)
					command.Errorf("%q not verified", currentFile)
					command.Exit() // exit early when strict mode (should this be only when verbose is off?)