1// SPDX-License-Identifier: ISC
2// Copyright (c) 2014-2020 Bitmark Inc.
3// Use of this source code is governed by an ISC
4// license that can be found in the LICENSE file.
5
6package main
7
8import (
9	"bytes"
10	"crypto/tls"
11	"encoding/json"
12	"fmt"
13	"io/ioutil"
14	"net"
15	"os"
16	"path/filepath"
17	"strconv"
18	"strings"
19
20	"github.com/bitmark-inc/bitmarkd/account"
21	"github.com/bitmark-inc/bitmarkd/block"
22	"github.com/bitmark-inc/bitmarkd/blockheader"
23	"github.com/bitmark-inc/bitmarkd/fault"
24	"github.com/bitmark-inc/bitmarkd/zmqutil"
25	"github.com/bitmark-inc/exitwithstatus"
26	"github.com/bitmark-inc/logger"
27)
28
29const (
30	peerPublicKeyFilename  = "peer.public"
31	peerPrivateKeyFilename = "peer.private"
32
33	rpcCertificateKeyFilename = "rpc.crt"
34	rpcPrivateKeyFilename     = "rpc.key"
35
36	proofPublicKeyFilename      = "proof.public"
37	proofPrivateKeyFilename     = "proof.private"
38	proofLiveSigningKeyFilename = "proof.live"
39	proofTestSigningKeyFilename = "proof.test"
40)
41
42// setup command handler
43//
44// commands that run to create key and certificate files these
45// commands cannot access any internal database or states or the
46// configuration file
47func processSetupCommand(program string, arguments []string) bool {
48
49	command := "help"
50	if len(arguments) > 0 {
51		command = arguments[0]
52		arguments = arguments[1:]
53	}
54
55	switch command {
56	case "gen-peer-identity", "peer":
57		publicKeyFilename := getFilenameWithDirectory(arguments, peerPublicKeyFilename)
58		privateKeyFilename := getFilenameWithDirectory(arguments, peerPrivateKeyFilename)
59		err := zmqutil.MakeKeyPair(publicKeyFilename, privateKeyFilename)
60		if nil != err {
61			fmt.Printf("generate private key: %q and public key: %q error: %s\n", privateKeyFilename, publicKeyFilename, err)
62			exitwithstatus.Exit(1)
63		}
64		fmt.Printf("generated private key: %q and public key: %q\n", privateKeyFilename, publicKeyFilename)
65
66	case "gen-rpc-cert", "rpc":
67		certificateFilename := getFilenameWithDirectory(arguments, rpcCertificateKeyFilename)
68		privateKeyFilename := getFilenameWithDirectory(arguments, rpcPrivateKeyFilename)
69
70		addresses := []string{}
71		if len(arguments) >= 2 {
72			for _, a := range arguments[1:] {
73				if "" != a {
74					addresses = append(addresses, a)
75				}
76			}
77		}
78
79		err := makeSelfSignedCertificate("rpc", certificateFilename, privateKeyFilename, 0 != len(addresses), addresses)
80		if nil != err {
81			fmt.Printf("generate RPC key: %q and certificate: %q error: %s\n", privateKeyFilename, certificateFilename, err)
82			exitwithstatus.Exit(1)
83		}
84		fmt.Printf("generated RPC key: %q and certificate: %q\n", privateKeyFilename, certificateFilename)
85
86	case "gen-proof-identity", "proof":
87		publicKeyFilename := getFilenameWithDirectory(arguments, proofPublicKeyFilename)
88		privateKeyFilename := getFilenameWithDirectory(arguments, proofPrivateKeyFilename)
89		err := zmqutil.MakeKeyPair(publicKeyFilename, privateKeyFilename)
90		if nil != err {
91			fmt.Printf("generate private key: %q and public key: %q error: %s\n", privateKeyFilename, publicKeyFilename, err)
92			exitwithstatus.Exit(1)
93		}
94
95		liveSigningKeyFilename := getFilenameWithDirectory(arguments, proofLiveSigningKeyFilename)
96		testSigningKeyFilename := getFilenameWithDirectory(arguments, proofTestSigningKeyFilename)
97
98		if err := makeSigningKey(false, liveSigningKeyFilename); nil != err {
99			fmt.Printf("generate the signing key for livenet: %q error: %s\n", liveSigningKeyFilename, err)
100			goto signing_key_failed
101		}
102		if err := makeSigningKey(true, testSigningKeyFilename); nil != err {
103			fmt.Printf(" generate the signing key for testnet: %q error: %s\n", testSigningKeyFilename, err)
104			goto signing_key_failed
105		}
106
107		fmt.Printf("generated private key: %q and public key: %q\n", privateKeyFilename, publicKeyFilename)
108		fmt.Printf("generated signing keys: %q and %q\n", liveSigningKeyFilename, testSigningKeyFilename)
109		return true
110
111	signing_key_failed:
112		_ = os.Remove(publicKeyFilename)
113		_ = os.Remove(privateKeyFilename)
114		_ = os.Remove(liveSigningKeyFilename)
115		_ = os.Remove(testSigningKeyFilename)
116		exitwithstatus.Exit(1)
117
118	case "dns-txt", "txt":
119		return false // defer processing until configuration is read
120
121	case "start", "run":
122		return false // continue processing
123
124	case "block", "b", "save-blocks", "save", "load-blocks", "load", "delete-down", "dd":
125		return false // defer processing until database is loaded
126
127	case "config-test", "cfg":
128		return false
129
130	case "version", "v":
131		fmt.Printf("%s\n", version)
132		return true
133
134	default:
135		switch command {
136		case "help", "h", "?":
137		case "", " ":
138			fmt.Printf("error: missing command\n")
139		default:
140			fmt.Printf("error: no such command: %q\n", command)
141		}
142		fmt.Printf("usage: %s [--help] [--verbose] [--quiet] --config-file=FILE [[command|help] arguments...]", program)
143
144		fmt.Printf("supported commands:\n\n")
145		fmt.Printf("  help                       (h)      - display this message\n\n")
146		fmt.Printf("  version                    (v)      - display version sting\n\n")
147
148		fmt.Printf("  gen-peer-identity [DIR]    (peer)   - create private key in: %q\n", "DIR/"+peerPrivateKeyFilename)
149		fmt.Printf("                                        and the public key in: %q\n", "DIR/"+peerPublicKeyFilename)
150		fmt.Printf("\n")
151
152		fmt.Printf("  gen-rpc-cert [DIR]         (rpc)    - create private key in:  %q\n", "DIR/"+rpcPrivateKeyFilename)
153		fmt.Printf("                                        and the certificate in: %q\n", "DIR/"+rpcCertificateKeyFilename)
154		fmt.Printf("\n")
155
156		fmt.Printf("  gen-rpc-cert [DIR] [IPs...]         - create private key in:  %q\n", "DIR/"+rpcPrivateKeyFilename)
157		fmt.Printf("                                        and the certificate in: %q\n", "DIR/"+rpcCertificateKeyFilename)
158		fmt.Printf("\n")
159
160		fmt.Printf("  gen-proof-identity [DIR]   (proof)  - create private key in: %q\n", "DIR/"+proofPrivateKeyFilename)
161		fmt.Printf("                                        the public key in:     %q\n", "DIR/"+proofPublicKeyFilename)
162		fmt.Printf("                                        and signing keys in:  %q and: %q\n", "DIR/"+proofLiveSigningKeyFilename, "DIR/"+proofTestSigningKeyFilename)
163		fmt.Printf("\n")
164
165		fmt.Printf("  dns-txt                    (txt)    - display the data to put in a dbs TXT record\n")
166		fmt.Printf("\n")
167
168		fmt.Printf("  start                      (run)    - just run the program, same as no arguments\n")
169		fmt.Printf("                                        for convienience when passing script arguments\n")
170		fmt.Printf("\n")
171
172		fmt.Printf("  config-test                (cfg)    - just check the configuration file\n")
173		fmt.Printf("\n")
174
175		fmt.Printf("  block S [E [FILE]]         (b)      - dump block(s) as a JSON structures to stdout/file\n")
176		fmt.Printf("\n")
177
178		fmt.Printf("  save-blocks FILE           (save)   - dump all blocks to a file\n")
179		fmt.Printf("\n")
180
181		fmt.Printf("  load-blocks FILE           (load)   - restore all blocks from a file\n")
182		fmt.Printf("                                        only runs if database is deleted first\n")
183		fmt.Printf("\n")
184
185		fmt.Printf("  delete-down NUMBER         (dd)     - delete blocks in descending order\n")
186		fmt.Printf("\n")
187
188		exitwithstatus.Exit(1)
189	}
190
191	// indicate processing complete and preform normal exit from main
192	return true
193}
194
195// configuration file enquiry commands
196// have configuration file read and decoded, but nothing else
197func processConfigCommand(arguments []string, options *Configuration) bool {
198
199	command := "help"
200	if len(arguments) > 0 {
201		command = arguments[0]
202	}
203
204	switch command {
205	case "dns-txt", "txt":
206		dnsTXT(options)
207
208	case "config-test", "cfg":
209		b, err := json.Marshal(options)
210		if err != nil {
211			exitwithstatus.Message("error: %s", err)
212		}
213		var out bytes.Buffer
214		json.Indent(&out, b, "", "  ")
215		out.WriteTo(os.Stdout)
216		os.Stdout.WriteString("\n")
217
218	default: // unknown commands fall through to data command
219		return false
220	}
221
222	// indicate processing complete and perform normal exit from main
223	return true
224}
225
226// data command handler
227// the internal block and transaction pools are enabled so these commands can
228// access and/or change these databases
229func processDataCommand(log *logger.L, arguments []string, options *Configuration) bool {
230
231	command := "help"
232	if len(arguments) > 0 {
233		command = arguments[0]
234		arguments = arguments[1:]
235	}
236
237	switch command {
238
239	case "start", "run":
240		return false // continue processing
241
242	case "block", "b":
243		if len(arguments) < 1 {
244			exitwithstatus.Message("missing block number argument")
245		}
246
247		n, err := strconv.ParseUint(arguments[0], 10, 64)
248		if nil != err {
249			exitwithstatus.Message("error in block number: %s", err)
250		}
251		if n < 2 {
252			exitwithstatus.Message("error: invalid block number: %d must be greater than 1", n)
253		}
254
255		output := "-"
256
257		// optional end range
258		nEnd := n
259		if len(arguments) > 1 {
260
261			nEnd, err = strconv.ParseUint(arguments[1], 10, 64)
262			if nil != err {
263				exitwithstatus.Message("error in ending block number: %s", err)
264			}
265			if nEnd < n {
266				exitwithstatus.Message("error: invalid ending block number: %d must be greater than 1", n)
267			}
268		}
269
270		if len(arguments) > 2 {
271			output = strings.TrimSpace(arguments[2])
272		}
273		fd := os.Stdout
274
275		if output != "" && output != "-" {
276			fd, err = os.Create(output)
277			if nil != err {
278				exitwithstatus.Message("error: creating: %q error: %s", output, err)
279			}
280		}
281
282		fmt.Fprintf(fd, "[\n")
283		for ; n <= nEnd; n += 1 {
284			block, err := dumpBlock(n)
285			if nil != err {
286				exitwithstatus.Message("dump block error: %s", err)
287			}
288			s, err := json.MarshalIndent(block, "  ", "  ")
289			if nil != err {
290				exitwithstatus.Message("dump block JSON error: %s", err)
291			}
292
293			fmt.Fprintf(fd, "  %s,\n", s)
294		}
295		fmt.Fprintf(fd, "{}]\n")
296		fd.Close()
297
298	case "save-blocks", "save":
299		if len(arguments) < 1 {
300			exitwithstatus.Message("missing file name argument")
301		}
302		filename := arguments[0]
303		if "" == filename {
304			exitwithstatus.Message("missing file name")
305		}
306		err := saveBinaryBlocks(filename)
307		if nil != err {
308			exitwithstatus.Message("failed writing: %q  error: %s", filename, err)
309		}
310
311	case "load-blocks", "load":
312		if len(arguments) < 1 {
313			exitwithstatus.Message("missing file name argument")
314		}
315		filename := arguments[0]
316		if "" == filename {
317			exitwithstatus.Message("missing file name")
318		}
319		err := restoreBinaryBlocks(filename)
320		if nil != err {
321			exitwithstatus.Message("failed writing: %q  error: %s", filename, err)
322		}
323
324	case "delete-down", "dd":
325		// delete blocks down to a given block number
326		if len(arguments) < 1 {
327			exitwithstatus.Message("missing block number argument")
328		}
329
330		n, err := strconv.ParseUint(arguments[0], 10, 64)
331		if nil != err {
332			exitwithstatus.Message("error in block number: %s", err)
333		}
334		if n < 2 {
335			exitwithstatus.Message("error: invalid block number: %d must be greater than 1", n)
336		}
337		err = block.DeleteDownToBlock(n)
338		if nil != err {
339			exitwithstatus.Message("block delete error: %s", err)
340		}
341		fmt.Printf("reduced height to: %d\n", blockheader.Height())
342
343	default:
344		exitwithstatus.Message("error: no such command: %s", command)
345
346	}
347
348	// indicate processing complete and perform normal exit from main
349	return true
350}
351
352// print out the DNS TXT record
353func dnsTXT(options *Configuration) {
354	//   <TAG> a=<IPv4;IPv6> c=<PEER-PORT> r=<RPC-PORT> f=<SHA3-256(cert)> p=<PUBLIC-KEY>
355	const txtRecord = `TXT "bitmark=v3 a=%s c=%d r=%d f=%x p=%x"` + "\n"
356
357	rpc := options.ClientRPC
358
359	keypair, err := tls.X509KeyPair([]byte(rpc.Certificate), []byte(rpc.PrivateKey))
360	if nil != err {
361		exitwithstatus.Message("error: cannot decode certificate: %q  error: %s", rpc.Certificate, err)
362	}
363
364	fingerprint := CertificateFingerprint(keypair.Certificate[0])
365
366	if 0 == len(rpc.Announce) {
367		exitwithstatus.Message("error: no rpc announce fields given")
368	}
369
370	rpcIP4, rpcIP6, rpcPort := getFirstConnections(rpc.Announce)
371	if 0 == rpcPort {
372		exitwithstatus.Message("error: cannot determine rpc port")
373	}
374
375	peering := options.Peering
376
377	publicKey, err := zmqutil.ReadPublicKey(peering.PublicKey)
378	if nil != err {
379		exitwithstatus.Message("error: cannot read public key: %q  error: %s", peering.PublicKey, err)
380	}
381
382	if 0 == len(peering.Announce) {
383		exitwithstatus.Message("error: no rpc announce fields given")
384	}
385
386	listenIP4, listenIP6, listenPort := getFirstConnections(peering.Announce)
387	if 0 == listenPort {
388		exitwithstatus.Message("error: cannot determine listen port")
389	}
390
391	IPs := ""
392	if "" != rpcIP4 && rpcIP4 == listenIP4 {
393		IPs = rpcIP4
394	}
395	if "" != rpcIP6 && rpcIP6 == listenIP6 {
396		if "" == IPs {
397			IPs = rpcIP6
398		} else {
399			IPs += ";" + rpcIP6
400		}
401	}
402
403	fmt.Printf("rpc fingerprint: %x\n", fingerprint)
404	fmt.Printf("rpc port:        %d\n", rpcPort)
405	fmt.Printf("public key:      %x\n", publicKey)
406	fmt.Printf("connect port:    %d\n", listenPort)
407	fmt.Printf("IP4 IP6:         %s\n", IPs)
408
409	fmt.Printf(txtRecord, IPs, listenPort, rpcPort, fingerprint, publicKey)
410}
411
412// extract first IP4 and/or IP6 connection
413func getFirstConnections(connections []string) (string, string, int) {
414
415	initialPort := 0
416	IP4 := ""
417	IP6 := ""
418
419scan_connections:
420	for i, c := range connections {
421		if "" == c {
422			continue scan_connections
423		}
424		v6, IP, port, err := splitConnection(c)
425		if nil != err {
426			exitwithstatus.Message("error: cannot decode[%d]: %q  error: %s", i, c, err)
427		}
428		if v6 {
429			if "" == IP6 {
430				IP6 = IP
431				if 0 == initialPort || port == initialPort {
432					initialPort = port
433				}
434			}
435		} else {
436			if "" == IP4 {
437				IP4 = IP
438				if 0 == initialPort || port == initialPort {
439					initialPort = port
440				}
441			}
442		}
443	}
444	return IP4, IP6, initialPort
445}
446
447// split connection into ip and port
448func splitConnection(hostPort string) (bool, string, int, error) {
449	host, port, err := net.SplitHostPort(hostPort)
450	if nil != err {
451		return false, "", 0, fault.InvalidIpAddress
452	}
453
454	IP := net.ParseIP(strings.TrimSpace(host))
455	if nil == IP {
456		return false, "", 0, fault.InvalidIpAddress
457	}
458
459	numericPort, err := strconv.Atoi(strings.TrimSpace(port))
460	if nil != err {
461		return false, "", 0, err
462	}
463	if numericPort < 1 || numericPort > 65535 {
464		return false, "", 0, fault.InvalidPortNumber
465	}
466
467	if nil != IP.To4() {
468		return false, IP.String(), numericPort, nil
469	}
470	return true, "[" + IP.String() + "]", numericPort, nil
471}
472
473// get the working directory; if not set in the arguments
474// it's set to the current directory
475func getFilenameWithDirectory(arguments []string, name string) string {
476	dir := "."
477	if len(arguments) >= 1 {
478		dir = arguments[0]
479	}
480
481	return filepath.Join(dir, name)
482}
483
484func makeSigningKey(testnet bool, fileName string) error {
485	seed, err := account.NewBase58EncodedSeedV2(testnet)
486	if nil != err {
487		return err
488	}
489
490	data := "SEED:" + seed + "\n"
491	if err = ioutil.WriteFile(fileName, []byte(data), 0600); nil != err {
492		return fmt.Errorf("error writing signing key file error: %s", err)
493	}
494
495	return nil
496}
497