Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bb1362c0e | |||
| 30ffbbdbc5 | |||
| b893e99864 | |||
| c7c51568d8 | |||
| 7793021260 | |||
| 692562818c | |||
| 9e19346fc0 | |||
| cb827169dc | |||
| 027d0173bc | |||
| 6f19b69bbd | |||
| 7e118bfdb0 | |||
|
|
e0868841bf | ||
|
|
c558405d11 | ||
|
|
a1eb035af7 | ||
| 5eedcff042 | |||
|
|
6ac8eb04b4 | ||
|
|
4a4e4cd3fd | ||
|
|
1207093a56 | ||
| 2b6ae03d1a | |||
| ef0f14a512 | |||
| 6ae393ebf2 | |||
| 76d88c220d | |||
|
|
40e015373f | ||
| 50c226b726 | |||
| 070ffb9dff | |||
| 5ac05bd298 | |||
| cf1edf2d31 | |||
|
|
03e8958dd7 | ||
|
|
6cef585071 | ||
| 06678499d4 | |||
| fad17065fe | |||
| 63e0cbeacb | |||
| 231b98dd68 | |||
|
|
160a42ec26 | ||
|
|
b6b33e00c8 | ||
|
|
9e1aed257b | ||
|
|
411907c0ad | ||
|
|
06c7f8f42f | ||
|
|
8b638065d1 | ||
|
|
9ac378eaa5 | ||
|
|
eaaaabe439 | ||
|
|
4122f01644 | ||
|
|
263a5d3973 | ||
|
|
afef3eea62 | ||
| d6c5360a06 | |||
| 0ab21e12f3 | |||
| 832475db56 | |||
| cb16cfa183 | |||
| d083a39a7d | |||
| fc77225740 | |||
| 41df73d7a8 | |||
| 0dc478746a | |||
| f44bbc9eca | |||
|
|
1df0350fc7 | ||
|
|
d42c1fa1c5 | ||
|
|
4fa6e4ab0e | ||
|
|
a3ead16faf | ||
| c8f839de73 | |||
| 0c56a477bc | |||
|
|
763dbec310 | ||
|
|
0e6b60a2c4 | ||
|
|
be34ad263d | ||
|
|
48b03c908d | ||
|
|
70d7ff505b | ||
|
|
68e5822176 | ||
|
|
54dd461733 | ||
|
|
eba03a2f4a |
26
.travis.yml
Normal file
26
.travis.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
arch:
|
||||||
|
- amd64
|
||||||
|
- ppc64le
|
||||||
|
sudo: false
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- tip
|
||||||
|
- 1.9
|
||||||
|
jobs:
|
||||||
|
exclude:
|
||||||
|
- go: 1.9
|
||||||
|
arch: amd64
|
||||||
|
- go: 1.9
|
||||||
|
arch: ppc64le
|
||||||
|
script:
|
||||||
|
- go get golang.org/x/lint/golint
|
||||||
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
- go get github.com/kisom/goutils/...
|
||||||
|
- go test -cover github.com/kisom/goutils/...
|
||||||
|
- golint github.com/kisom/goutils/...
|
||||||
|
notifications:
|
||||||
|
email:
|
||||||
|
recipients:
|
||||||
|
- coder@kyleisom.net
|
||||||
|
on_success: change
|
||||||
|
on_failure: change
|
||||||
27
CHANGELOG
Normal file
27
CHANGELOG
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Release 1.2.1 - 2018-09-15
|
||||||
|
|
||||||
|
+ Add missing format argument to Errorf call in kgz.
|
||||||
|
|
||||||
|
Release 1.2.0 - 2018-09-15
|
||||||
|
|
||||||
|
+ Adds the kgz command line utility.
|
||||||
|
|
||||||
|
Release 1.1.0 - 2017-11-16
|
||||||
|
|
||||||
|
+ A number of new command line utilities were added
|
||||||
|
|
||||||
|
+ atping
|
||||||
|
+ cruntar
|
||||||
|
+ renfnv
|
||||||
|
+
|
||||||
|
+ ski
|
||||||
|
+ subjhash
|
||||||
|
+ yamll
|
||||||
|
|
||||||
|
+ new package: ahash
|
||||||
|
+ package for loading hashes from an algorithm string
|
||||||
|
|
||||||
|
+ new certificate loading functions in the lib package
|
||||||
|
|
||||||
|
+ new package: tee
|
||||||
|
+ emulates tee(1)
|
||||||
18
README.md
18
README.md
@@ -6,8 +6,10 @@ of these in superfluous repositories of their own, I'm putting them here.
|
|||||||
|
|
||||||
Contents:
|
Contents:
|
||||||
|
|
||||||
|
ahash/ Provides hashes from string algorithm specifiers.
|
||||||
assert/ Error handling, assertion-style.
|
assert/ Error handling, assertion-style.
|
||||||
cmd/
|
cmd/
|
||||||
|
atping/ Automated TCP ping, meant for putting in cronjobs.
|
||||||
certchain/ Display the certificate chain from a
|
certchain/ Display the certificate chain from a
|
||||||
TLS connection.
|
TLS connection.
|
||||||
certdump/ Dump certificate information.
|
certdump/ Dump certificate information.
|
||||||
@@ -18,9 +20,13 @@ Contents:
|
|||||||
the time to expiry and checking for revocations.
|
the time to expiry and checking for revocations.
|
||||||
clustersh/ Run commands or transfer files across multiple
|
clustersh/ Run commands or transfer files across multiple
|
||||||
servers via SSH.
|
servers via SSH.
|
||||||
|
cruntar/ Untar an archive with hard links, copying instead of
|
||||||
|
linking.
|
||||||
csrpubdump/ Dump the public key from an X.509 certificate request.
|
csrpubdump/ Dump the public key from an X.509 certificate request.
|
||||||
fragment/ Print a fragment of a file.
|
fragment/ Print a fragment of a file.
|
||||||
jlp/ JSON linter/prettifier.
|
jlp/ JSON linter/prettifier.
|
||||||
|
kgz/ Custom gzip compressor / decompressor that handles 99%
|
||||||
|
of my use cases.
|
||||||
pem2bin/ Dump the binary body of a PEM-encoded block.
|
pem2bin/ Dump the binary body of a PEM-encoded block.
|
||||||
pembody/ Print the body of a PEM certificate.
|
pembody/ Print the body of a PEM certificate.
|
||||||
pemit/ Dump data to a PEM file.
|
pemit/ Dump data to a PEM file.
|
||||||
@@ -28,19 +34,25 @@ Contents:
|
|||||||
current working directory) imports for a Go file.
|
current working directory) imports for a Go file.
|
||||||
readchain/ Print the common name for the certificates
|
readchain/ Print the common name for the certificates
|
||||||
in a bundle.
|
in a bundle.
|
||||||
|
renfnv/ Rename a file to base32-encoded 64-bit FNV-1a hash.
|
||||||
|
rhash/ Compute the digest of remote files.
|
||||||
showimp Display the external imports in a package.
|
showimp Display the external imports in a package.
|
||||||
|
ski Display the SKI for PEM-encoded TLS material.
|
||||||
stealchain/ Dump the verified chain from a TLS
|
stealchain/ Dump the verified chain from a TLS
|
||||||
connection.
|
connection.
|
||||||
|
subjhash/ Print or match subject info from a certificate.
|
||||||
tlskeypair/ Check whether a TLS certificate and key file match.
|
tlskeypair/ Check whether a TLS certificate and key file match.
|
||||||
utc/ Convert times to UTC.
|
utc/ Convert times to UTC.
|
||||||
|
yamll/ A small YAML linter.
|
||||||
die/ Death of a program.
|
die/ Death of a program.
|
||||||
fileutil/ Common file functions.
|
fileutil/ Common file functions.
|
||||||
lib/ Commonly-useful functions for writing Go programs.
|
lib/ Commonly-useful functions for writing Go programs.
|
||||||
logging/ A logging library.
|
logging/ A logging library.
|
||||||
mwc/ MultiwriteCloser implementation.
|
mwc/ MultiwriteCloser implementation.
|
||||||
sbuf/ A byte buffer that can be wiped.
|
sbuf/ A byte buffer that can be wiped.
|
||||||
testio/ Various I/O utilities useful during testing.
|
tee/ Emulate tee(1)'s functionality in io.Writers.
|
||||||
testutil/ Various utility functions useful during testing.
|
testio/ Various I/O utilities useful during testing.
|
||||||
|
testutil/ Various utility functions useful during testing.
|
||||||
|
|
||||||
|
|
||||||
Each program should have a small README in the directory with more
|
Each program should have a small README in the directory with more
|
||||||
|
|||||||
258
ahash/ahash.go
Normal file
258
ahash/ahash.go
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
// Package ahash provides support for hashing data with a selectable
|
||||||
|
// hash function.
|
||||||
|
package ahash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"errors"
|
||||||
|
"hash"
|
||||||
|
"hash/adler32"
|
||||||
|
"hash/crc32"
|
||||||
|
"hash/crc64"
|
||||||
|
"hash/fnv"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"git.sr.ht/~kisom/goutils/assert"
|
||||||
|
"golang.org/x/crypto/blake2b"
|
||||||
|
"golang.org/x/crypto/blake2s"
|
||||||
|
"golang.org/x/crypto/md4"
|
||||||
|
"golang.org/x/crypto/ripemd160"
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sha224Slicer(bs []byte) []byte {
|
||||||
|
sum := sha256.Sum224(bs)
|
||||||
|
return sum[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha256Slicer(bs []byte) []byte {
|
||||||
|
sum := sha256.Sum256(bs)
|
||||||
|
return sum[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha384Slicer(bs []byte) []byte {
|
||||||
|
sum := sha512.Sum384(bs)
|
||||||
|
return sum[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha512Slicer(bs []byte) []byte {
|
||||||
|
sum := sha512.Sum512(bs)
|
||||||
|
return sum[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var sliceFunctions = map[string]func([]byte) []byte{
|
||||||
|
"sha224": sha224Slicer,
|
||||||
|
"sha256": sha256Slicer,
|
||||||
|
"sha384": sha384Slicer,
|
||||||
|
"sha512": sha512Slicer,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash represents a generic hash function that may or may not be secure. It
|
||||||
|
// satisfies the hash.Hash interface.
|
||||||
|
type Hash struct {
|
||||||
|
hash.Hash
|
||||||
|
secure bool
|
||||||
|
algo string
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashAlgo returns the name of the underlying hash algorithm.
|
||||||
|
func (h *Hash) HashAlgo() string {
|
||||||
|
return h.algo
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSecure returns true if the Hash is a cryptographic hash.
|
||||||
|
func (h *Hash) IsSecure() bool {
|
||||||
|
return h.secure
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum32 returns true if the underlying hash is a 32-bit hash; if is, the
|
||||||
|
// uint32 parameter will contain the hash.
|
||||||
|
func (h *Hash) Sum32() (uint32, bool) {
|
||||||
|
h32, ok := h.Hash.(hash.Hash32)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return h32.Sum32(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsHash32 returns true if the underlying hash is a 32-bit hash function.
|
||||||
|
func (h *Hash) IsHash32() bool {
|
||||||
|
_, ok := h.Hash.(hash.Hash32)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum64 returns true if the underlying hash is a 64-bit hash; if is, the
|
||||||
|
// uint64 parameter will contain the hash.
|
||||||
|
func (h *Hash) Sum64() (uint64, bool) {
|
||||||
|
h64, ok := h.Hash.(hash.Hash64)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return h64.Sum64(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsHash64 returns true if the underlying hash is a 64-bit hash function.
|
||||||
|
func (h *Hash) IsHash64() bool {
|
||||||
|
_, ok := h.Hash.(hash.Hash64)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func blakeFunc(bf func(key []byte) (hash.Hash, error)) func() hash.Hash {
|
||||||
|
return func() hash.Hash {
|
||||||
|
h, err := bf(nil)
|
||||||
|
assert.NoError(err, "while constructing a BLAKE2 hash function")
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var secureHashes = map[string]func() hash.Hash{
|
||||||
|
"ripemd160": ripemd160.New,
|
||||||
|
"sha224": sha256.New224,
|
||||||
|
"sha256": sha256.New,
|
||||||
|
"sha384": sha512.New384,
|
||||||
|
"sha512": sha512.New,
|
||||||
|
"sha3-224": sha3.New224,
|
||||||
|
"sha3-256": sha3.New256,
|
||||||
|
"sha3-384": sha3.New384,
|
||||||
|
"sha3-512": sha3.New512,
|
||||||
|
"blake2s-256": blakeFunc(blake2s.New256),
|
||||||
|
"blake2b-256": blakeFunc(blake2b.New256),
|
||||||
|
"blake2b-384": blakeFunc(blake2b.New384),
|
||||||
|
"blake2b-512": blakeFunc(blake2b.New512),
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHash32(f func() hash.Hash32) func() hash.Hash {
|
||||||
|
return func() hash.Hash {
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHash64(f func() hash.Hash64) func() hash.Hash {
|
||||||
|
return func() hash.Hash {
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCRC64(tab uint64) func() hash.Hash {
|
||||||
|
return newHash64(
|
||||||
|
func() hash.Hash64 {
|
||||||
|
return crc64.New(crc64.MakeTable(tab))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var insecureHashes = map[string]func() hash.Hash{
|
||||||
|
"md4": md4.New,
|
||||||
|
"md5": md5.New,
|
||||||
|
"sha1": sha1.New,
|
||||||
|
"adler32": newHash32(adler32.New),
|
||||||
|
"crc32-ieee": newHash32(crc32.NewIEEE),
|
||||||
|
"crc64": newCRC64(crc64.ISO),
|
||||||
|
"crc64-ecma": newCRC64(crc64.ECMA),
|
||||||
|
"fnv1-32a": newHash32(fnv.New32a),
|
||||||
|
"fnv1-32": newHash32(fnv.New32),
|
||||||
|
"fnv1-64a": newHash64(fnv.New64a),
|
||||||
|
"fnv1-64": newHash64(fnv.New64),
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Hash for the specified algorithm.
|
||||||
|
func New(algo string) (*Hash, error) {
|
||||||
|
h := &Hash{algo: algo}
|
||||||
|
|
||||||
|
hf, ok := secureHashes[algo]
|
||||||
|
if ok {
|
||||||
|
h.Hash = hf()
|
||||||
|
h.secure = true
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hf, ok = insecureHashes[algo]
|
||||||
|
if ok {
|
||||||
|
h.Hash = hf()
|
||||||
|
h.secure = false
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("chash: unsupport hash algorithm " + algo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum returns the digest (not the hex digest) of the data using the given
|
||||||
|
// algorithm.
|
||||||
|
func Sum(algo string, data []byte) ([]byte, error) {
|
||||||
|
h, err := New(algo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = h.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SumReader reads all the data from the given io.Reader and returns the
|
||||||
|
// digest (not the hex digest) from the specified algorithm.
|
||||||
|
func SumReader(algo string, r io.Reader) ([]byte, error) {
|
||||||
|
h, err := New(algo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(h, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var insecureHashList, secureHashList, hashList []string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
shl := len(secureHashes) // secure hash list length
|
||||||
|
ihl := len(insecureHashes) // insecure hash list length
|
||||||
|
ahl := shl + ihl // all hash list length
|
||||||
|
|
||||||
|
insecureHashList = make([]string, 0, ihl)
|
||||||
|
secureHashList = make([]string, 0, shl)
|
||||||
|
hashList = make([]string, 0, ahl)
|
||||||
|
|
||||||
|
for algo := range insecureHashes {
|
||||||
|
insecureHashList = append(insecureHashList, algo)
|
||||||
|
}
|
||||||
|
sort.Strings(insecureHashList)
|
||||||
|
|
||||||
|
for algo := range secureHashes {
|
||||||
|
secureHashList = append(secureHashList, algo)
|
||||||
|
}
|
||||||
|
sort.Strings(secureHashList)
|
||||||
|
|
||||||
|
hashList = append(hashList, insecureHashList...)
|
||||||
|
hashList = append(hashList, secureHashList...)
|
||||||
|
sort.Strings(hashList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashList returns a sorted list of all the hash algorithms supported by the
|
||||||
|
// package.
|
||||||
|
func HashList() []string {
|
||||||
|
return hashList[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecureHashList returns a sorted list of all the secure (cryptographic) hash
|
||||||
|
// algorithms supported by the package.
|
||||||
|
func SecureHashList() []string {
|
||||||
|
return secureHashList[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsecureHashList returns a sorted list of all the insecure hash algorithms
|
||||||
|
// supported by the package.
|
||||||
|
func InsecureHashList() []string {
|
||||||
|
return insecureHashList[:]
|
||||||
|
}
|
||||||
141
ahash/ahash_test.go
Normal file
141
ahash/ahash_test.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package ahash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.sr.ht/~kisom/goutils/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSecureHash(t *testing.T) {
|
||||||
|
algo := "sha256"
|
||||||
|
h, err := New(algo)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, h.IsSecure(), algo+" should be a secure hash")
|
||||||
|
assert.BoolT(t, h.HashAlgo() == algo, "hash returned the wrong HashAlgo")
|
||||||
|
assert.BoolT(t, !h.IsHash32(), algo+" isn't actually a 32-bit hash")
|
||||||
|
assert.BoolT(t, !h.IsHash64(), algo+" isn't actually a 64-bit hash")
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
sum, err := Sum(algo, data)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, fmt.Sprintf("%x", sum) == expected, fmt.Sprintf("expected hash %s but have %x", expected, sum))
|
||||||
|
|
||||||
|
data = []byte("hello, world")
|
||||||
|
buf := bytes.NewBuffer(data)
|
||||||
|
expected = "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b"
|
||||||
|
sum, err = SumReader(algo, buf)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, fmt.Sprintf("%x", sum) == expected, fmt.Sprintf("expected hash %s but have %x", expected, sum))
|
||||||
|
|
||||||
|
data = []byte("hello world")
|
||||||
|
_, err = h.Write(data)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
unExpected := "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b"
|
||||||
|
sum = h.Sum(nil)
|
||||||
|
assert.BoolT(t, fmt.Sprintf("%x", sum) != unExpected, fmt.Sprintf("hash shouldn't have returned %x", unExpected))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsecureHash(t *testing.T) {
|
||||||
|
algo := "md5"
|
||||||
|
h, err := New(algo)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, !h.IsSecure(), algo+" shouldn't be a secure hash")
|
||||||
|
assert.BoolT(t, h.HashAlgo() == algo, "hash returned the wrong HashAlgo")
|
||||||
|
assert.BoolT(t, !h.IsHash32(), algo+" isn't actually a 32-bit hash")
|
||||||
|
assert.BoolT(t, !h.IsHash64(), algo+" isn't actually a 64-bit hash")
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var expected = "d41d8cd98f00b204e9800998ecf8427e"
|
||||||
|
sum, err := Sum(algo, data)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, fmt.Sprintf("%x", sum) == expected, fmt.Sprintf("expected hash %s but have %x", expected, sum))
|
||||||
|
|
||||||
|
data = []byte("hello, world")
|
||||||
|
buf := bytes.NewBuffer(data)
|
||||||
|
expected = "e4d7f1b4ed2e42d15898f4b27b019da4"
|
||||||
|
sum, err = SumReader(algo, buf)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, fmt.Sprintf("%x", sum) == expected, fmt.Sprintf("expected hash %s but have %x", expected, sum))
|
||||||
|
|
||||||
|
data = []byte("hello world")
|
||||||
|
_, err = h.Write(data)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
unExpected := "e4d7f1b4ed2e42d15898f4b27b019da4"
|
||||||
|
sum = h.Sum(nil)
|
||||||
|
assert.BoolT(t, fmt.Sprintf("%x", sum) != unExpected, fmt.Sprintf("hash shouldn't have returned %x", unExpected))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHash32(t *testing.T) {
|
||||||
|
algo := "crc32-ieee"
|
||||||
|
h, err := New(algo)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, !h.IsSecure(), algo+" shouldn't be a secure hash")
|
||||||
|
assert.BoolT(t, h.HashAlgo() == algo, "hash returned the wrong HashAlgo")
|
||||||
|
assert.BoolT(t, h.IsHash32(), algo+" is actually a 32-bit hash")
|
||||||
|
assert.BoolT(t, !h.IsHash64(), algo+" isn't actually a 64-bit hash")
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var expected uint32
|
||||||
|
|
||||||
|
h.Write(data)
|
||||||
|
sum, ok := h.Sum32()
|
||||||
|
assert.BoolT(t, ok, algo+" should be able to return a Sum32")
|
||||||
|
assert.BoolT(t, expected == sum, fmt.Sprintf("%s returned the %d but expected %d", algo, sum, expected))
|
||||||
|
|
||||||
|
data = []byte("hello, world")
|
||||||
|
expected = 0xffab723a
|
||||||
|
h.Write(data)
|
||||||
|
sum, ok = h.Sum32()
|
||||||
|
assert.BoolT(t, ok, algo+" should be able to return a Sum32")
|
||||||
|
assert.BoolT(t, expected == sum, fmt.Sprintf("%s returned the %d but expected %d", algo, sum, expected))
|
||||||
|
|
||||||
|
h.Reset()
|
||||||
|
data = []byte("hello world")
|
||||||
|
h.Write(data)
|
||||||
|
sum, ok = h.Sum32()
|
||||||
|
assert.BoolT(t, ok, algo+" should be able to return a Sum32")
|
||||||
|
assert.BoolT(t, expected != sum, fmt.Sprintf("%s returned %d but shouldn't have", algo, sum))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHash64(t *testing.T) {
|
||||||
|
algo := "crc64"
|
||||||
|
h, err := New(algo)
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
assert.BoolT(t, !h.IsSecure(), algo+" shouldn't be a secure hash")
|
||||||
|
assert.BoolT(t, h.HashAlgo() == algo, "hash returned the wrong HashAlgo")
|
||||||
|
assert.BoolT(t, h.IsHash64(), algo+" is actually a 64-bit hash")
|
||||||
|
assert.BoolT(t, !h.IsHash32(), algo+" isn't actually a 32-bit hash")
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var expected uint64
|
||||||
|
|
||||||
|
h.Write(data)
|
||||||
|
sum, ok := h.Sum64()
|
||||||
|
assert.BoolT(t, ok, algo+" should be able to return a Sum64")
|
||||||
|
assert.BoolT(t, expected == sum, fmt.Sprintf("%s returned the %d but expected %d", algo, sum, expected))
|
||||||
|
|
||||||
|
data = []byte("hello, world")
|
||||||
|
expected = 0x16c45c0eb1d9c2ec
|
||||||
|
h.Write(data)
|
||||||
|
sum, ok = h.Sum64()
|
||||||
|
assert.BoolT(t, ok, algo+" should be able to return a Sum64")
|
||||||
|
assert.BoolT(t, expected == sum, fmt.Sprintf("%s returned the %d but expected %d", algo, sum, expected))
|
||||||
|
|
||||||
|
h.Reset()
|
||||||
|
data = []byte("hello world")
|
||||||
|
h.Write(data)
|
||||||
|
sum, ok = h.Sum64()
|
||||||
|
assert.BoolT(t, ok, algo+" should be able to return a Sum64")
|
||||||
|
assert.BoolT(t, expected != sum, fmt.Sprintf("%s returned %d but shouldn't have", algo, sum))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListLengthSanity(t *testing.T) {
|
||||||
|
all := HashList()
|
||||||
|
secure := SecureHashList()
|
||||||
|
insecure := InsecureHashList()
|
||||||
|
|
||||||
|
assert.BoolT(t, len(all) == len(secure)+len(insecure))
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NoDebug, if set to true, will cause all asserts to be ignored.
|
// NoDebug can be set to true to cause all asserts to be ignored.
|
||||||
var NoDebug bool
|
var NoDebug bool
|
||||||
|
|
||||||
func die(what string, a ...string) {
|
func die(what string, a ...string) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/kisom/goutils/die"
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
)
|
)
|
||||||
|
|
||||||
var hasPort = regexp.MustCompile(`:\d+$`)
|
var hasPort = regexp.MustCompile(`:\d+$`)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/cloudflare/cfssl/helpers"
|
"github.com/cloudflare/cfssl/helpers"
|
||||||
@@ -82,6 +84,7 @@ func keyUsages(ku x509.KeyUsage) string {
|
|||||||
uses = append(uses, s)
|
uses = append(uses, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sort.Strings(uses)
|
||||||
|
|
||||||
return strings.Join(uses, ", ")
|
return strings.Join(uses, ", ")
|
||||||
}
|
}
|
||||||
@@ -91,6 +94,7 @@ func extUsage(ext []x509.ExtKeyUsage) string {
|
|||||||
for i := range ext {
|
for i := range ext {
|
||||||
ns = append(ns, extKeyUsages[ext[i]])
|
ns = append(ns, extKeyUsages[ext[i]])
|
||||||
}
|
}
|
||||||
|
sort.Strings(ns)
|
||||||
|
|
||||||
return strings.Join(ns, ", ")
|
return strings.Join(ns, ", ")
|
||||||
}
|
}
|
||||||
@@ -116,7 +120,10 @@ func showBasicConstraints(cert *x509.Certificate) {
|
|||||||
|
|
||||||
const oneTrueDateFormat = "2006-01-02T15:04:05-0700"
|
const oneTrueDateFormat = "2006-01-02T15:04:05-0700"
|
||||||
|
|
||||||
var dateFormat string
|
var (
|
||||||
|
dateFormat string
|
||||||
|
showHash bool // if true, print a SHA256 hash of the certificate's Raw field
|
||||||
|
)
|
||||||
|
|
||||||
func wrapPrint(text string, indent int) {
|
func wrapPrint(text string, indent int) {
|
||||||
tabs := ""
|
tabs := ""
|
||||||
@@ -129,6 +136,9 @@ func wrapPrint(text string, indent int) {
|
|||||||
|
|
||||||
func displayCert(cert *x509.Certificate) {
|
func displayCert(cert *x509.Certificate) {
|
||||||
fmt.Println("CERTIFICATE")
|
fmt.Println("CERTIFICATE")
|
||||||
|
if showHash {
|
||||||
|
fmt.Println(wrap(fmt.Sprintf("SHA256: %x", sha256.Sum256(cert.Raw)), 0))
|
||||||
|
}
|
||||||
fmt.Println(wrap("Subject: "+displayName(cert.Subject), 0))
|
fmt.Println(wrap("Subject: "+displayName(cert.Subject), 0))
|
||||||
fmt.Println(wrap("Issuer: "+displayName(cert.Issuer), 0))
|
fmt.Println(wrap("Issuer: "+displayName(cert.Issuer), 0))
|
||||||
fmt.Printf("\tSignature algorithm: %s / %s\n", sigAlgoPK(cert.SignatureAlgorithm),
|
fmt.Printf("\tSignature algorithm: %s / %s\n", sigAlgoPK(cert.SignatureAlgorithm),
|
||||||
@@ -273,6 +283,7 @@ func displayAllCertsWeb(uri string, leafOnly bool) {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var leafOnly bool
|
var leafOnly bool
|
||||||
|
flag.BoolVar(&showHash, "d", false, "show hashes of raw DER contents")
|
||||||
flag.StringVar(&dateFormat, "s", oneTrueDateFormat, "date `format` in Go time format")
|
flag.StringVar(&dateFormat, "s", oneTrueDateFormat, "date `format` in Go time format")
|
||||||
flag.BoolVar(&leafOnly, "l", false, "only show the leaf certificate")
|
flag.BoolVar(&leafOnly, "l", false, "only show the leaf certificate")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cloudflare/cfssl/helpers"
|
"github.com/cloudflare/cfssl/helpers"
|
||||||
"github.com/kisom/goutils/die"
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
"github.com/kisom/goutils/lib"
|
"git.sr.ht/~kisom/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
var warnOnly bool
|
var warnOnly bool
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
|
|
||||||
"github.com/cloudflare/cfssl/helpers"
|
"github.com/cloudflare/cfssl/helpers"
|
||||||
"github.com/cloudflare/cfssl/revoke"
|
"github.com/cloudflare/cfssl/revoke"
|
||||||
"github.com/kisom/goutils/die"
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
"github.com/kisom/goutils/lib"
|
"git.sr.ht/~kisom/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func printRevocation(cert *x509.Certificate) {
|
func printRevocation(cert *x509.Certificate) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/kisom/goutils/lib"
|
"git.sr.ht/~kisom/goutils/lib"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/crypto/ssh/agent"
|
"golang.org/x/crypto/ssh/agent"
|
||||||
|
|||||||
20
cmd/cruntar/README
Normal file
20
cmd/cruntar/README
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
ChromeOS untar
|
||||||
|
|
||||||
|
This is a tool that is intended to support untarring on SquashFS file
|
||||||
|
systems. In particular, every time it encounters a hard link, it
|
||||||
|
will just create a copy of the file.
|
||||||
|
|
||||||
|
Usage: cruntar [-jmvpz] archive [dest]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-a Shortcut for -m -p: preserve owners and file mode.
|
||||||
|
-j The archive is compressed with bzip2.
|
||||||
|
-m Preserve file modes.
|
||||||
|
-p Preserve ownership.
|
||||||
|
-v Print the name of each file as it is being processed.
|
||||||
|
-z The archive is compressed with gzip.
|
||||||
|
|
||||||
|
I wrote this after running into problems with untarring the
|
||||||
|
gcc-arm-eabi-none toolchain. The shared storage in Termux under
|
||||||
|
ChromeOS doesn't support hard links, so I opted to just make a copy
|
||||||
|
rather than dealing with links and whatnot.
|
||||||
265
cmd/cruntar/main.go
Normal file
265
cmd/cruntar/main.go
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/bzip2"
|
||||||
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
preserveOwners bool
|
||||||
|
preserveMode bool
|
||||||
|
verbose bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupFile(hdr *tar.Header, file *os.File) error {
|
||||||
|
if preserveMode {
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf("\tchmod %0#o\n", hdr.Mode)
|
||||||
|
}
|
||||||
|
err := file.Chmod(os.FileMode(hdr.Mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if preserveOwners {
|
||||||
|
fmt.Printf("\tchown %d:%d\n", hdr.Uid, hdr.Gid)
|
||||||
|
err := file.Chown(hdr.Uid, hdr.Gid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func linkTarget(target, top string) string {
|
||||||
|
if filepath.IsAbs(target) {
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Clean(filepath.Join(target, top))
|
||||||
|
}
|
||||||
|
|
||||||
|
func processFile(tfr *tar.Reader, hdr *tar.Header, top string) error {
|
||||||
|
if verbose {
|
||||||
|
fmt.Println(hdr.Name)
|
||||||
|
}
|
||||||
|
filePath := filepath.Clean(filepath.Join(top, hdr.Name))
|
||||||
|
switch hdr.Typeflag {
|
||||||
|
case tar.TypeReg, tar.TypeRegA:
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(file, tfr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setupFile(hdr, file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case tar.TypeLink:
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
source, err := os.Open(hdr.Linkname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(file, source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setupFile(hdr, file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case tar.TypeSymlink:
|
||||||
|
err := os.Symlink(linkTarget(hdr.Linkname, top), filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case tar.TypeDir:
|
||||||
|
err := os.MkdirAll(filePath, os.FileMode(hdr.Mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var compression = map[string]bool{
|
||||||
|
"gzip": false,
|
||||||
|
"bzip2": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
type bzipCloser struct {
|
||||||
|
r io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (brc *bzipCloser) Read(p []byte) (int, error) {
|
||||||
|
return brc.r.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (brc *bzipCloser) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBzipCloser(r io.ReadCloser) (io.ReadCloser, error) {
|
||||||
|
br := bzip2.NewReader(r)
|
||||||
|
return &bzipCloser{r: br}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var compressFuncs = map[string]func(io.ReadCloser) (io.ReadCloser, error){
|
||||||
|
"gzip": func(r io.ReadCloser) (io.ReadCloser, error) { return gzip.NewReader(r) },
|
||||||
|
"bzip2": newBzipCloser,
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyCompression() bool {
|
||||||
|
var compressed bool
|
||||||
|
for _, v := range compression {
|
||||||
|
if compressed && v {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
compressed = compressed || v
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getReader(r io.ReadCloser) (io.ReadCloser, error) {
|
||||||
|
for c, v := range compression {
|
||||||
|
if v {
|
||||||
|
return compressFuncs[c](r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openArchive(path string) (io.ReadCloser, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := getReader(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var compressFlags struct {
|
||||||
|
z bool
|
||||||
|
j bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCompressFlags() error {
|
||||||
|
if compressFlags.z {
|
||||||
|
compression["gzip"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if compressFlags.j {
|
||||||
|
compression["bzip2"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !verifyCompression() {
|
||||||
|
return errors.New("multiple compression formats specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `ChromeOS untar
|
||||||
|
|
||||||
|
This is a tool that is intended to support untarring on SquashFS file
|
||||||
|
systems. In particular, every time it encounters a hard link, it
|
||||||
|
will just create a copy of the file.
|
||||||
|
|
||||||
|
Usage: cruntar [-jmvpz] archive [dest]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-a Shortcut for -m -p: preserve owners and file mode.
|
||||||
|
-j The archive is compressed with bzip2.
|
||||||
|
-m Preserve file modes.
|
||||||
|
-p Preserve ownership.
|
||||||
|
-v Print the name of each file as it is being processed.
|
||||||
|
-z The archive is compressed with gzip.
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Usage = func() { usage(os.Stderr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var archive, help bool
|
||||||
|
flag.BoolVar(&archive, "a", false, "Shortcut for -m -p: preserve owners and file mode.")
|
||||||
|
flag.BoolVar(&help, "h", false, "print a help message")
|
||||||
|
flag.BoolVar(&compressFlags.j, "j", false, "bzip2 compression")
|
||||||
|
flag.BoolVar(&preserveMode, "m", false, "preserve file modes")
|
||||||
|
flag.BoolVar(&preserveOwners, "p", false, "preserve ownership")
|
||||||
|
flag.BoolVar(&verbose, "v", false, "verbose mode")
|
||||||
|
flag.BoolVar(&compressFlags.z, "z", false, "gzip compression")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if help {
|
||||||
|
usage(os.Stdout)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if archive {
|
||||||
|
preserveMode = true
|
||||||
|
preserveOwners = true
|
||||||
|
}
|
||||||
|
|
||||||
|
err := parseCompressFlags()
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
if flag.NArg() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
top := "./"
|
||||||
|
if flag.NArg() > 1 {
|
||||||
|
top = flag.Arg(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := openArchive(flag.Arg(0))
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
tfr := tar.NewReader(r)
|
||||||
|
for {
|
||||||
|
hdr, err := tfr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
err = processFile(tfr, hdr, top)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Close()
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/kisom/goutils/die"
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -4,16 +4,21 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/kisom/goutils/die"
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
)
|
)
|
||||||
|
|
||||||
func usage() {
|
func init() {
|
||||||
|
flag.Usage = func() { usage(os.Stdout); os.Exit(1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage(w io.Writer) {
|
||||||
progname := filepath.Base(os.Args[0])
|
progname := filepath.Base(os.Args[0])
|
||||||
fmt.Printf(`Usage: %s [-nl] file start [end]
|
fmt.Fprintf(w, `Usage: %s [-nl] file start [end]
|
||||||
|
|
||||||
Print a fragment of a file starting a line 'start' and ending
|
Print a fragment of a file starting a line 'start' and ending
|
||||||
at line 'end', or EOF if no end is specified.
|
at line 'end', or EOF if no end is specified.
|
||||||
@@ -27,7 +32,7 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if flag.NArg() < 2 || flag.NArg() > 3 {
|
if flag.NArg() < 2 || flag.NArg() > 3 {
|
||||||
usage()
|
usage(os.Stderr)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/kisom/goutils/lib"
|
"git.sr.ht/~kisom/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func prettify(file string, validateOnly bool) error {
|
func prettify(file string, validateOnly bool) error {
|
||||||
|
|||||||
23
cmd/kgz/README
Normal file
23
cmd/kgz/README
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
kgz
|
||||||
|
|
||||||
|
kgz is like gzip, but supports compressing and decompressing to a different
|
||||||
|
directory than the source file is in.
|
||||||
|
|
||||||
|
Usage: kgz [-l] source [target]
|
||||||
|
|
||||||
|
If target is a directory, the basename of the sourcefile will be used
|
||||||
|
as the target filename. Compression and decompression is selected
|
||||||
|
based on whether the source filename ends in ".gz".
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-l level Compression level (0-9). Only meaninful when
|
||||||
|
compressing a file.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
182
cmd/kgz/main.go
Normal file
182
cmd/kgz/main.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/flate"
|
||||||
|
"compress/gzip"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const gzipExt = ".gz"
|
||||||
|
|
||||||
|
func compress(path, target string, level int) error {
|
||||||
|
sourceFile, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "opening file for read")
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
destFile, err := os.Create(target)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "opening file for write")
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
|
||||||
|
gzipCompressor, err := gzip.NewWriterLevel(destFile, level)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid compression level")
|
||||||
|
}
|
||||||
|
defer gzipCompressor.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(gzipCompressor, sourceFile)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "compressing file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "stat(2)ing destination file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uncompress(path, target string) error {
|
||||||
|
sourceFile, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "opening file for read")
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
gzipUncompressor, err := gzip.NewReader(sourceFile)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "reading gzip headers")
|
||||||
|
}
|
||||||
|
defer gzipUncompressor.Close()
|
||||||
|
|
||||||
|
destFile, err := os.Create(target)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "opening file for write")
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(destFile, gzipUncompressor)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "uncompressing file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `Usage: %s [-l] source [target]
|
||||||
|
|
||||||
|
kgz is like gzip, but supports compressing and decompressing to a different
|
||||||
|
directory than the source file is in.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-l level Compression level (0-9). Only meaninful when
|
||||||
|
compressing a file.
|
||||||
|
`, os.Args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Usage = func() { usage(os.Stderr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDir(path string) bool {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err == nil {
|
||||||
|
defer file.Close()
|
||||||
|
stat, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.IsDir() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathForUncompressing(source, dest string) (string, error) {
|
||||||
|
if !isDir(dest) {
|
||||||
|
return dest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
source = filepath.Base(source)
|
||||||
|
if !strings.HasSuffix(source, gzipExt) {
|
||||||
|
return "", errors.Errorf("%s is a not gzip-compressed file", source)
|
||||||
|
}
|
||||||
|
outFile := source[:len(source)-len(gzipExt)]
|
||||||
|
outFile = filepath.Join(dest, outFile)
|
||||||
|
return outFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathForCompressing(source, dest string) (string, error) {
|
||||||
|
if !isDir(dest) {
|
||||||
|
return dest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
source = filepath.Base(source)
|
||||||
|
if strings.HasSuffix(source, gzipExt) {
|
||||||
|
return "", errors.Errorf("%s is a gzip-compressed file", source)
|
||||||
|
}
|
||||||
|
|
||||||
|
dest = filepath.Join(dest, source+gzipExt)
|
||||||
|
return dest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var level int
|
||||||
|
var path string
|
||||||
|
var target = "."
|
||||||
|
|
||||||
|
flag.IntVar(&level, "l", flate.DefaultCompression, "compression level")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if flag.NArg() < 1 || flag.NArg() > 2 {
|
||||||
|
usage(os.Stderr)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
path = flag.Arg(0)
|
||||||
|
if flag.NArg() == 2 {
|
||||||
|
target = flag.Arg(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(path, gzipExt) {
|
||||||
|
target, err := pathForUncompressing(path, target)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = uncompress(path, target)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(target)
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target, err := pathForCompressing(path, target)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = compress(path, target, level)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(target)
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
cmd/parts/README
Normal file
9
cmd/parts/README
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
parts: simple parts database for electronic components
|
||||||
|
|
||||||
|
Usage: parts [id] -- query the database for a part
|
||||||
|
parts [-c class] [id] [description] -- store a part in the database
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-f path Path to parts database (default is
|
||||||
|
/home/kyle/.parts.json).
|
||||||
|
|
||||||
142
cmd/parts/main.go
Normal file
142
cmd/parts/main.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dbVersion = "1"
|
||||||
|
|
||||||
|
var dbFile = filepath.Join(os.Getenv("HOME"), ".parts.json")
|
||||||
|
var partsDB = &database{Version: dbVersion}
|
||||||
|
|
||||||
|
type part struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Class string `json:"class,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p part) String() string {
|
||||||
|
return fmt.Sprintf("%s: %s", p.Name, p.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
type database struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
LastUpdate int64 `json:"json"`
|
||||||
|
Parts map[string]part `json:"parts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func help(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `Usage: parts [id] -- query the database for a part
|
||||||
|
parts [-c class] [id] [description] -- store a part in the database
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-f path Path to parts database (default is
|
||||||
|
%s).
|
||||||
|
|
||||||
|
`, dbFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadDatabase() {
|
||||||
|
data, err := ioutil.ReadFile(dbFile)
|
||||||
|
if err != nil && os.IsNotExist(err) {
|
||||||
|
partsDB = &database{
|
||||||
|
Version: dbVersion,
|
||||||
|
Parts: map[string]part{},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, partsDB)
|
||||||
|
die.If(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPart(partName string) {
|
||||||
|
partName = strings.ToLower(partName)
|
||||||
|
for name, part := range partsDB.Parts {
|
||||||
|
if strings.Contains(strings.ToLower(name), partName) {
|
||||||
|
fmt.Println(part.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeDB() {
|
||||||
|
data, err := json.Marshal(partsDB)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(dbFile, data, 0644)
|
||||||
|
die.If(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func storePart(name, class, description string) {
|
||||||
|
p, exists := partsDB.Parts[name]
|
||||||
|
if exists {
|
||||||
|
fmt.Printf("warning: replacing part %s\n", name)
|
||||||
|
fmt.Printf("\t%s\n", p.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
partsDB.Parts[name] = part{
|
||||||
|
Name: name,
|
||||||
|
Class: class,
|
||||||
|
Description: description,
|
||||||
|
}
|
||||||
|
|
||||||
|
writeDB()
|
||||||
|
}
|
||||||
|
|
||||||
|
func listParts() {
|
||||||
|
parts := make([]string, 0, len(partsDB.Parts))
|
||||||
|
for partName := range partsDB.Parts {
|
||||||
|
parts = append(parts, partName)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(parts)
|
||||||
|
for _, partName := range parts {
|
||||||
|
fmt.Println(partsDB.Parts[partName].String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var class string
|
||||||
|
var helpFlag bool
|
||||||
|
|
||||||
|
flag.StringVar(&class, "c", "", "device class")
|
||||||
|
flag.StringVar(&dbFile, "f", dbFile, "`path` to database")
|
||||||
|
flag.BoolVar(&helpFlag, "h", false, "Print a help message.")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if helpFlag {
|
||||||
|
help(os.Stdout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDatabase()
|
||||||
|
|
||||||
|
switch flag.NArg() {
|
||||||
|
case 0:
|
||||||
|
help(os.Stdout)
|
||||||
|
return
|
||||||
|
case 1:
|
||||||
|
partName := flag.Arg(0)
|
||||||
|
if partName == "list" {
|
||||||
|
listParts()
|
||||||
|
} else {
|
||||||
|
findPart(flag.Arg(0))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
description := strings.Join(flag.Args()[1:], " ")
|
||||||
|
storePart(flag.Arg(0), class, description)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/kisom/goutils/lib"
|
"git.sr.ht/~kisom/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/kisom/goutils/assert"
|
"git.sr.ht/~kisom/goutils/assert"
|
||||||
"github.com/kisom/goutils/die"
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
"github.com/kisom/goutils/lib"
|
"git.sr.ht/~kisom/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func usage(w io.Writer) {
|
func usage(w io.Writer) {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kisom/goutils/fileutil"
|
"git.sr.ht/~kisom/goutils/fileutil"
|
||||||
"github.com/kisom/goutils/lib"
|
"git.sr.ht/~kisom/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func hashName(path, encodedHash string) string {
|
func hashName(path, encodedHash string) string {
|
||||||
@@ -46,7 +46,6 @@ func newName(path string) (string, error) {
|
|||||||
func move(dst, src string, force bool) (err error) {
|
func move(dst, src string, force bool) (err error) {
|
||||||
if fileutil.FileDoesExist(dst) && !force {
|
if fileutil.FileDoesExist(dst) && !force {
|
||||||
return fmt.Errorf("%s exists (pass the -f flag to overwrite)", dst)
|
return fmt.Errorf("%s exists (pass the -f flag to overwrite)", dst)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
dstFile, err := os.Create(dst)
|
dstFile, err := os.Create(dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -92,7 +91,7 @@ Options:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.Usage = func () { usage(os.Stdout) }
|
flag.Usage = func() { usage(os.Stdout) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
22
cmd/rhash/README
Normal file
22
cmd/rhash/README
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
rhash: remote hashing tool
|
||||||
|
|
||||||
|
Usage: rhash [-a algo] [-h] [-l set] urls...
|
||||||
|
Compute the hash over each URL.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-a algo Specify the hash algorithm to use; the default is sha256.
|
||||||
|
-h Print this help message.
|
||||||
|
-l set List the hash functions under set. Set can be one of all,
|
||||||
|
secure to list only cryptographic hash functions, or
|
||||||
|
insecure to list only non-cryptographic hash functions.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Compute the SHA256 digest of the LICENSE in this repository:
|
||||||
|
|
||||||
|
$ rhash https://raw.githubusercontent.com/kisom/goutils/7391da8567952f69990194ead2842d21df217c89/LICENSE
|
||||||
|
LICENSE: sha256=620bfadeb698df6c6db73908689a29371a9d4cff32b08c48a5c4307946093980
|
||||||
|
|
||||||
|
Compute the SHA-1 digest of the LICENSE in this repository:
|
||||||
|
|
||||||
|
$ rhash -a sha1 https://raw.githubusercontent.com/kisom/goutils/7391da8567952f69990194ead2842d21df217c89/LICENSE
|
||||||
|
LICENSE: sha1=83c6e2e410715058ed6e7c1572176122c024e367
|
||||||
97
cmd/rhash/main.go
Normal file
97
cmd/rhash/main.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.sr.ht/~kisom/goutils/ahash"
|
||||||
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
|
"git.sr.ht/~kisom/goutils/lib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func usage(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `Usage: %s [-a algo] [-h] [-l set] urls...
|
||||||
|
Compute the hash over each URL.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-a algo Specify the hash algorithm to use; the default is sha256.
|
||||||
|
-h Print this help message.
|
||||||
|
-l set List the hash functions under set. Set can be one of all,
|
||||||
|
secure to list only cryptographic hash functions, or
|
||||||
|
insecure to list only non-cryptographic hash functions.
|
||||||
|
|
||||||
|
`, lib.ProgName())
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Usage = func() { usage(os.Stderr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var algo, list string
|
||||||
|
var help bool
|
||||||
|
flag.StringVar(&algo, "a", "sha256", "hash algorithm to use")
|
||||||
|
flag.BoolVar(&help, "h", false, "print a help message")
|
||||||
|
flag.StringVar(&list, "l", "", "list known hash algorithms (one of all, secure, insecure)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if help {
|
||||||
|
usage(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if list != "" {
|
||||||
|
var hashes []string
|
||||||
|
switch list {
|
||||||
|
case "all":
|
||||||
|
hashes = ahash.HashList()
|
||||||
|
case "secure":
|
||||||
|
hashes = ahash.SecureHashList()
|
||||||
|
case "insecure":
|
||||||
|
hashes = ahash.InsecureHashList()
|
||||||
|
default:
|
||||||
|
die.With("list option must be one of all, secure, or insecure.")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, algo := range hashes {
|
||||||
|
fmt.Printf("- %s\n", algo)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, remote := range flag.Args() {
|
||||||
|
u, err := url.Parse(remote)
|
||||||
|
if err != nil {
|
||||||
|
lib.Warn(err, "parsing %s", remote)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := filepath.Base(u.Path)
|
||||||
|
if name == "" {
|
||||||
|
lib.Warnx("source URL doesn't appear to name a file")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(remote)
|
||||||
|
if err != nil {
|
||||||
|
lib.Warn(err, "fetching %s", remote)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
lib.Warn(err, "fetching %s", remote)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sum, err := ahash.SumReader(algo, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
lib.Err(lib.ExitFailure, err, "while hashing data")
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: %s=%x\n", name, algo, sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kisom/goutils/die"
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
"github.com/kisom/goutils/logging"
|
"git.sr.ht/~kisom/goutils/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -67,6 +67,10 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func walkFile(path string, info os.FileInfo, err error) error {
|
func walkFile(path string, info os.FileInfo, err error) error {
|
||||||
|
if ignores[path] {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
if !sourceRegexp.MatchString(path) {
|
if !sourceRegexp.MatchString(path) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -97,10 +101,24 @@ func walkFile(path string, info os.FileInfo, err error) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ignores = map[string]bool{}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
var ignoreLine string
|
||||||
|
var noVendor bool
|
||||||
|
flag.StringVar(&ignoreLine, "i", "", "comma-separated list of directories to ignore")
|
||||||
|
flag.BoolVar(&noVendor, "nv", false, "ignore the vendor directory")
|
||||||
flag.BoolVar(&debug, "v", false, "log debugging information")
|
flag.BoolVar(&debug, "v", false, "log debugging information")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if noVendor {
|
||||||
|
ignores["vendor"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, word := range strings.Split(ignoreLine, ",") {
|
||||||
|
ignores[strings.TrimSpace(word)] = true
|
||||||
|
}
|
||||||
|
|
||||||
err := filepath.Walk(".", walkFile)
|
err := filepath.Walk(".", walkFile)
|
||||||
die.If(err)
|
die.If(err)
|
||||||
|
|
||||||
|
|||||||
30
cmd/ski/README
Normal file
30
cmd/ski/README
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
ski: print subject public key info
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ski [-hm] files...
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h Print a help message and exit.
|
||||||
|
-m All SKIs should match.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
Printing the SKI of a private key and certificate:
|
||||||
|
|
||||||
|
$ ski *
|
||||||
|
server.key 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA private key)
|
||||||
|
[ski] trailing data in PEM file
|
||||||
|
server.pem 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA certificate)
|
||||||
|
|
||||||
|
Making sure the SKIs match:
|
||||||
|
|
||||||
|
$ ski -m *
|
||||||
|
tyrfingr.key 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA private key)
|
||||||
|
[ski] trailing data in PEM file
|
||||||
|
tyrfingr.pem 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA certificate)
|
||||||
|
|
||||||
|
Making sure the SKIs match with a bad certificate:
|
||||||
|
$ ski -m server.key bad.pem
|
||||||
|
server.key 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA private key)
|
||||||
|
[ski] bad.pem: SKI mismatch (3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E != 90:AF:6A:3A:94:5A:0B:D8:90:EA:12:56:73:DF:43:B4:3A:28:DA:E7)
|
||||||
|
bad.pem 90:AF:6A:3A:94:5A:0B:D8:90:EA:12:56:73:DF:43:B4:3A:28:DA:E7 (RSA certificate)
|
||||||
191
cmd/ski/main.go
Normal file
191
cmd/ski/main.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
|
"encoding/pem"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
|
"git.sr.ht/~kisom/goutils/lib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func usage(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `ski: print subject key info for PEM-encoded files
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ski [-hm] files...
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h Print this help message.
|
||||||
|
-m All SKIs should match; as soon as an SKI mismatch is found,
|
||||||
|
it is reported.
|
||||||
|
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Usage = func() { usage(os.Stderr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(path string) (public []byte, kt, ft string) {
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
data = bytes.TrimSpace(data)
|
||||||
|
p, rest := pem.Decode(data)
|
||||||
|
if len(rest) > 0 {
|
||||||
|
lib.Warnx("trailing data in PEM file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p == nil {
|
||||||
|
die.With("no PEM data found")
|
||||||
|
}
|
||||||
|
|
||||||
|
data = p.Bytes
|
||||||
|
|
||||||
|
switch p.Type {
|
||||||
|
case "PRIVATE KEY", "RSA PRIVATE KEY", "EC PRIVATE KEY":
|
||||||
|
public, kt = parseKey(data)
|
||||||
|
ft = "private key"
|
||||||
|
case "CERTIFICATE":
|
||||||
|
public, kt = parseCertificate(data)
|
||||||
|
ft = "certificate"
|
||||||
|
case "CERTIFICATE REQUEST":
|
||||||
|
public, kt = parseCSR(data)
|
||||||
|
ft = "certificate request"
|
||||||
|
default:
|
||||||
|
die.With("unknown PEM type %s", p.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKey(data []byte) (public []byte, kt string) {
|
||||||
|
privInterface, err := x509.ParsePKCS8PrivateKey(data)
|
||||||
|
if err != nil {
|
||||||
|
privInterface, err = x509.ParsePKCS1PrivateKey(data)
|
||||||
|
if err != nil {
|
||||||
|
privInterface, err = x509.ParseECPrivateKey(data)
|
||||||
|
if err != nil {
|
||||||
|
die.With("couldn't parse private key.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var priv crypto.Signer
|
||||||
|
switch privInterface.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
priv = privInterface.(*rsa.PrivateKey)
|
||||||
|
kt = "RSA"
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
priv = privInterface.(*ecdsa.PrivateKey)
|
||||||
|
kt = "ECDSA"
|
||||||
|
default:
|
||||||
|
die.With("unknown private key type %T", privInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
public, err = x509.MarshalPKIXPublicKey(priv.Public())
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCertificate(data []byte) (public []byte, kt string) {
|
||||||
|
cert, err := x509.ParseCertificate(data)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
pub := cert.PublicKey
|
||||||
|
switch pub.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
kt = "RSA"
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
kt = "ECDSA"
|
||||||
|
default:
|
||||||
|
die.With("unknown public key type %T", pub)
|
||||||
|
}
|
||||||
|
|
||||||
|
public, err = x509.MarshalPKIXPublicKey(pub)
|
||||||
|
die.If(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCSR(data []byte) (public []byte, kt string) {
|
||||||
|
csr, err := x509.ParseCertificateRequest(data)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
pub := csr.PublicKey
|
||||||
|
switch pub.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
kt = "RSA"
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
kt = "ECDSA"
|
||||||
|
default:
|
||||||
|
die.With("unknown public key type %T", pub)
|
||||||
|
}
|
||||||
|
|
||||||
|
public, err = x509.MarshalPKIXPublicKey(pub)
|
||||||
|
die.If(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func dumpHex(in []byte) string {
|
||||||
|
var s string
|
||||||
|
for i := range in {
|
||||||
|
s += fmt.Sprintf("%02X:", in[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Trim(s, ":")
|
||||||
|
}
|
||||||
|
|
||||||
|
type subjectPublicKeyInfo struct {
|
||||||
|
Algorithm pkix.AlgorithmIdentifier
|
||||||
|
SubjectPublicKey asn1.BitString
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var help, shouldMatch bool
|
||||||
|
flag.BoolVar(&help, "h", false, "print a help message and exit")
|
||||||
|
flag.BoolVar(&shouldMatch, "m", false, "all SKIs should match")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if help {
|
||||||
|
usage(os.Stdout)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ski string
|
||||||
|
for _, path := range flag.Args() {
|
||||||
|
public, kt, ft := parse(path)
|
||||||
|
|
||||||
|
var subPKI subjectPublicKeyInfo
|
||||||
|
_, err := asn1.Unmarshal(public, &subPKI)
|
||||||
|
if err != nil {
|
||||||
|
lib.Warn(err, "failed to get subject PKI")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pubHash := sha1.Sum(subPKI.SubjectPublicKey.Bytes)
|
||||||
|
pubHashString := dumpHex(pubHash[:])
|
||||||
|
if ski == "" {
|
||||||
|
ski = pubHashString
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldMatch && ski != pubHashString {
|
||||||
|
lib.Warnx("%s: SKI mismatch (%s != %s)",
|
||||||
|
path, ski, pubHashString)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s %s (%s %s)\n", path, pubHashString, kt, ft)
|
||||||
|
}
|
||||||
|
}
|
||||||
46
cmd/sprox/main.go
Normal file
46
cmd/sprox/main.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
|
)
|
||||||
|
|
||||||
|
func proxy(conn net.Conn, inside string) error {
|
||||||
|
proxyConn, err := net.Dial("tcp", inside)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer proxyConn.Close()
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
io.Copy(conn, proxyConn)
|
||||||
|
}()
|
||||||
|
_, err = io.Copy(proxyConn, conn)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var outside, inside string
|
||||||
|
flag.StringVar(&outside, "f", "8080", "outside port")
|
||||||
|
flag.StringVar(&inside, "p", "4000", "inside port")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
l, err := net.Listen("tcp", "0.0.0.0:"+outside)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
go proxy(conn, "127.0.0.1:"+inside)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
cmd/stealchain-server/README
Normal file
17
cmd/stealchain-server/README
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
stealchain-server
|
||||||
|
|
||||||
|
This is a utility to extract the verified X.509 chain from a TLS
|
||||||
|
connection initiated by another client. It listens on a port, and
|
||||||
|
for each connection, it will dump the certificates that the peer
|
||||||
|
actually sent (and not the verified chain that is built from this).
|
||||||
|
|
||||||
|
It was written to assist in debugging issues with certificate chains.
|
||||||
|
|
||||||
|
There are a few knobs:
|
||||||
|
|
||||||
|
-listen specifies the address to listen on.
|
||||||
|
|
||||||
|
-ca allows the trusted CA roots to be specified via a PEM bundle of
|
||||||
|
root certificates.
|
||||||
|
|
||||||
|
-verify requires that the client present a valid certificate chain.
|
||||||
106
cmd/stealchain-server/main.go
Normal file
106
cmd/stealchain-server/main.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/pem"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := &tls.Config{}
|
||||||
|
|
||||||
|
var sysRoot, listenAddr, certFile, keyFile string
|
||||||
|
var verify bool
|
||||||
|
flag.StringVar(&sysRoot, "ca", "", "provide an alternate CA bundle")
|
||||||
|
flag.StringVar(&listenAddr, "listen", ":443", "address to listen on")
|
||||||
|
flag.StringVar(&certFile, "cert", "", "server certificate to present to clients")
|
||||||
|
flag.StringVar(&keyFile, "key", "", "key for server certificate")
|
||||||
|
flag.BoolVar(&verify, "verify", false, "verify client certificates")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if verify {
|
||||||
|
cfg.ClientAuth = tls.RequireAndVerifyClientCert
|
||||||
|
} else {
|
||||||
|
cfg.ClientAuth = tls.RequestClientCert
|
||||||
|
}
|
||||||
|
if certFile == "" {
|
||||||
|
fmt.Println("[!] missing required flag -cert")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if keyFile == "" {
|
||||||
|
fmt.Println("[!] missing required flag -key")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[!] could not load server key pair: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cfg.Certificates = append(cfg.Certificates, cert)
|
||||||
|
if sysRoot != "" {
|
||||||
|
pemList, err := ioutil.ReadFile(sysRoot)
|
||||||
|
die.If(err)
|
||||||
|
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
if !roots.AppendCertsFromPEM(pemList) {
|
||||||
|
fmt.Printf("[!] no valid roots found")
|
||||||
|
roots = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.RootCAs = roots
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := net.Listen("tcp", listenAddr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
raddr := conn.RemoteAddr()
|
||||||
|
tconn := tls.Server(conn, cfg)
|
||||||
|
err = tconn.Handshake()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[+] %v: failed to complete handshake: %v\n", raddr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cs := tconn.ConnectionState()
|
||||||
|
if len(cs.PeerCertificates) == 0 {
|
||||||
|
fmt.Printf("[+] %v: no chain presented\n", raddr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var chain []byte
|
||||||
|
for _, cert := range cs.PeerCertificates {
|
||||||
|
p := &pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: cert.Raw,
|
||||||
|
}
|
||||||
|
chain = append(chain, pem.EncodeToMemory(p)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonce [16]byte
|
||||||
|
_, err = rand.Read(nonce[:])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fname := fmt.Sprintf("%v-%v.pem", raddr, hex.EncodeToString(nonce[:]))
|
||||||
|
err = ioutil.WriteFile(fname, chain, 0644)
|
||||||
|
die.If(err)
|
||||||
|
fmt.Printf("%v: [+] wrote %v.\n", raddr, fname)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/kisom/goutils/die"
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
20
cmd/subjhash/README
Normal file
20
cmd/subjhash/README
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
subjhash
|
||||||
|
|
||||||
|
This tool prints the SHA-256 hash of an X.509 certificate's subject
|
||||||
|
info or issuer fields. It can also verify that the hashes of the
|
||||||
|
subject are the same between two certificates.
|
||||||
|
|
||||||
|
Usage: subjhash [-im] certs...
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-i Print hash of issuer field.
|
||||||
|
-m Matching mode. This expects arguments to be in the form of
|
||||||
|
pairs of certificates (e.g. previous, new) whose subjects
|
||||||
|
will be compared. For example,
|
||||||
|
|
||||||
|
subjhash -m ca1.pem ca1-renewed.pem \
|
||||||
|
ca2.pem ca2-renewed.pem
|
||||||
|
|
||||||
|
will exit with a non-zero status if the subject in the
|
||||||
|
ca1-renewed.pem certificate doesn't match the subject in the
|
||||||
|
ca.pem certificate; similarly for ca2.
|
||||||
112
cmd/subjhash/main.go
Normal file
112
cmd/subjhash/main.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
|
"git.sr.ht/~kisom/goutils/lib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Usage = func() { usage(os.Stdout); os.Exit(1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `Print hash of subject or issuer fields in certificates.
|
||||||
|
|
||||||
|
Usage: subjhash [-im] certs...
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-i Print hash of issuer field.
|
||||||
|
-m Matching mode. This expects arguments to be in the form of
|
||||||
|
pairs of certificates (e.g. previous, new) whose subjects
|
||||||
|
will be compared. For example,
|
||||||
|
|
||||||
|
subjhash -m ca1.pem ca1-renewed.pem \
|
||||||
|
ca2.pem ca2-renewed.pem
|
||||||
|
|
||||||
|
will exit with a non-zero status if the subject in the
|
||||||
|
ca1-renewed.pem certificate doesn't match the subject in the
|
||||||
|
ca.pem certificate; similarly for ca2.
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB: the Issuer field is *also* a subject field. Also, the returned
|
||||||
|
// hash is *not* hex encoded.
|
||||||
|
func getSubjectInfoHash(cert *x509.Certificate, issuer bool) []byte {
|
||||||
|
if cert == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var subject []byte
|
||||||
|
if issuer {
|
||||||
|
subject = cert.RawIssuer
|
||||||
|
} else {
|
||||||
|
subject = cert.RawSubject
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := sha256.Sum256(subject)
|
||||||
|
return digest[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func printDigests(paths []string, issuer bool) {
|
||||||
|
for _, path := range paths {
|
||||||
|
cert, err := lib.LoadCertificate(path)
|
||||||
|
if err != nil {
|
||||||
|
lib.Warn(err, "failed to load certificate from %s", path)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := getSubjectInfoHash(cert, issuer)
|
||||||
|
fmt.Printf("%x %s\n", digest, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchDigests(paths []string, issuer bool) {
|
||||||
|
if (len(paths) % 2) != 0 {
|
||||||
|
lib.Errx(lib.ExitFailure, "not all certificates are paired")
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalid int
|
||||||
|
for {
|
||||||
|
if len(paths) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fst := paths[0]
|
||||||
|
snd := paths[1]
|
||||||
|
paths = paths[2:]
|
||||||
|
|
||||||
|
fstCert, err := lib.LoadCertificate(fst)
|
||||||
|
die.If(err)
|
||||||
|
sndCert, err := lib.LoadCertificate(snd)
|
||||||
|
die.If(err)
|
||||||
|
if !bytes.Equal(getSubjectInfoHash(fstCert, issuer), getSubjectInfoHash(sndCert, issuer)) {
|
||||||
|
lib.Warnx("certificates don't match: %s and %s", fst, snd)
|
||||||
|
invalid++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if invalid > 0 {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var issuer, match bool
|
||||||
|
flag.BoolVar(&issuer, "i", false, "print the issuer")
|
||||||
|
flag.BoolVar(&match, "m", false, "match mode")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
paths := flag.Args()
|
||||||
|
if match {
|
||||||
|
matchDigests(paths, issuer)
|
||||||
|
} else {
|
||||||
|
printDigests(paths, issuer)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/kisom/goutils/die"
|
"git.sr.ht/~kisom/goutils/die"
|
||||||
)
|
)
|
||||||
|
|
||||||
var validPEMs = map[string]bool{
|
var validPEMs = map[string]bool{
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
format = "2006-01-02 15:04" // Format that will be used for times.
|
format = "2006-01-02 15:04" // Format that will be used for times.
|
||||||
outFormat = format + " MST" // Output format.
|
outFormat = format + " MST" // Output format.
|
||||||
tz = "Local" // String descriptor for timezone.
|
tz = "Local" // String descriptor for timezone.
|
||||||
fromLoc *time.Location = time.Local // Go time.Location for the named timezone.
|
fromLoc = time.Local // Go time.Location for the named timezone.
|
||||||
fromUnix bool // Input times are Unix timestamps.
|
fromUnix bool // Input times are Unix timestamps.
|
||||||
toLoc *time.Location = time.UTC // Go time.Location for output timezone.
|
toLoc = time.UTC // Go time.Location for output timezone.
|
||||||
)
|
)
|
||||||
|
|
||||||
func usage(w io.Writer) {
|
func usage(w io.Writer) {
|
||||||
@@ -72,7 +72,7 @@ Flags:
|
|||||||
|
|
||||||
func usageExamples() {
|
func usageExamples() {
|
||||||
usage(os.Stdout)
|
usage(os.Stdout)
|
||||||
fmt.Println(`
|
fmt.Printf(`
|
||||||
Examples (note that the examples are done in the America/Los_Angeles /
|
Examples (note that the examples are done in the America/Los_Angeles /
|
||||||
PST8PDT time zone):
|
PST8PDT time zone):
|
||||||
|
|
||||||
@@ -134,6 +134,7 @@ PST8PDT time zone):
|
|||||||
(Converting from GMT (offset +0000) to UTC (offset +0000).)
|
(Converting from GMT (offset +0000) to UTC (offset +0000).)
|
||||||
==================================================================
|
==================================================================
|
||||||
2016-06-14 23:46 = 2016-06-14 23:46
|
2016-06-14 23:46 = 2016-06-14 23:46
|
||||||
|
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
141
config/config.go
Normal file
141
config/config.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// Package config implements a simple global configuration system that
|
||||||
|
// supports a file with key=value pairs and environment variables. Note
|
||||||
|
// that the config system is global.
|
||||||
|
//
|
||||||
|
// This package is intended to be used for small daemons: some configuration
|
||||||
|
// file is optionally populated at program start, then this is used to
|
||||||
|
// transparently look up configuration values from either that file or the
|
||||||
|
// environment.
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.sr.ht/~kisom/goutils/config/iniconf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NB: Rather than define a singleton type, everything is defined at
|
||||||
|
// the top-level
|
||||||
|
|
||||||
|
var (
|
||||||
|
vars = map[string]string{}
|
||||||
|
prefix = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetEnvPrefix sets the prefix for all environment variables; it's
|
||||||
|
// assumed to not be needed for files.
|
||||||
|
func SetEnvPrefix(pfx string) {
|
||||||
|
prefix = pfx
|
||||||
|
}
|
||||||
|
|
||||||
|
func addLine(line string) {
|
||||||
|
if strings.HasPrefix(line, "#") || line == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lineParts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(lineParts) != 2 {
|
||||||
|
log.Print("skipping line: ", line)
|
||||||
|
return // silently ignore empty keys
|
||||||
|
}
|
||||||
|
|
||||||
|
lineParts[0] = strings.TrimSpace(lineParts[0])
|
||||||
|
lineParts[1] = strings.TrimSpace(lineParts[1])
|
||||||
|
vars[lineParts[0]] = lineParts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFile scans the file at path for key=value pairs and adds them
|
||||||
|
// to the configuration.
|
||||||
|
func LoadFile(path string) error {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
addLine(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = scanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFileFor scans the ini file at path, loading the default section
|
||||||
|
// and overriding any keys found under section. If strict is true, the
|
||||||
|
// named section must exist (i.e. to catch typos in the section name).
|
||||||
|
func LoadFileFor(path, section string, strict bool) error {
|
||||||
|
cmap, err := iniconf.ParseFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range cmap[iniconf.DefaultSection] {
|
||||||
|
vars[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
smap, ok := cmap[section]
|
||||||
|
if !ok {
|
||||||
|
if strict {
|
||||||
|
return fmt.Errorf("config: section '%s' wasn't found in the config file", section)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range smap {
|
||||||
|
vars[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a value from either a configuration file or the
|
||||||
|
// environment. Note that values from a file will override environment
|
||||||
|
// variables.
|
||||||
|
func Get(key string) string {
|
||||||
|
if v, ok := vars[key]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return os.Getenv(prefix + key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefault retrieves a value from either a configuration file or
|
||||||
|
// the environment. Note that value from a file will override
|
||||||
|
// environment variables. If a value isn't found (e.g. Get returns an
|
||||||
|
// empty string), the default value will be used.
|
||||||
|
func GetDefault(key, def string) string {
|
||||||
|
if v := Get(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require retrieves a value from either a configuration file or the
|
||||||
|
// environment. If the key isn't present, it will call log.Fatal, printing
|
||||||
|
// the missing key.
|
||||||
|
func Require(key string) string {
|
||||||
|
if v, ok := vars[key]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := os.LookupEnv(prefix + key)
|
||||||
|
if !ok {
|
||||||
|
var envMessage string
|
||||||
|
if prefix != "" {
|
||||||
|
envMessage = " (note: looked for the key " + prefix + key
|
||||||
|
envMessage += " in the local env)"
|
||||||
|
}
|
||||||
|
log.Fatalf("missing required configuration value %s%s", key, envMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
66
config/config_test.go
Normal file
66
config/config_test.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testFilePath = "testdata/test.env"
|
||||||
|
|
||||||
|
// Keys
|
||||||
|
kOrder = "ORDER"
|
||||||
|
kSpecies = "SPECIES"
|
||||||
|
kName = "COMMON_NAME"
|
||||||
|
|
||||||
|
// Env
|
||||||
|
eOrder = "corvus"
|
||||||
|
eSpecies = "corvus corax"
|
||||||
|
eName = "northern raven"
|
||||||
|
|
||||||
|
// File
|
||||||
|
fOrder = "stringiformes"
|
||||||
|
fSpecies = "strix aluco"
|
||||||
|
// Name isn't set in the file to test fall through.
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
os.Setenv(kOrder, eOrder)
|
||||||
|
os.Setenv(kSpecies, eSpecies)
|
||||||
|
os.Setenv(kName, eName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadEnvOnly(t *testing.T) {
|
||||||
|
order := Get(kOrder)
|
||||||
|
species := Get(kSpecies)
|
||||||
|
if order != eOrder {
|
||||||
|
t.Errorf("want %s, have %s", eOrder, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
if species != eSpecies {
|
||||||
|
t.Errorf("want %s, have %s", eSpecies, species)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFile(t *testing.T) {
|
||||||
|
err := LoadFile(testFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
order := Get(kOrder)
|
||||||
|
species := Get(kSpecies)
|
||||||
|
name := Get(kName)
|
||||||
|
|
||||||
|
if order != fOrder {
|
||||||
|
t.Errorf("want %s, have %s", fOrder, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
if species != fSpecies {
|
||||||
|
t.Errorf("want %s, have %s", fSpecies, species)
|
||||||
|
}
|
||||||
|
|
||||||
|
if name != eName {
|
||||||
|
t.Errorf("want %s, have %s", eName, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
223
config/iniconf/iniconf.go
Normal file
223
config/iniconf/iniconf.go
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
package iniconf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigMap is shorthand for the type used as a config struct.
|
||||||
|
type ConfigMap map[string]map[string]string
|
||||||
|
|
||||||
|
var (
|
||||||
|
configSection = regexp.MustCompile(`^\s*\[\s*(\w+)\s*\]\s*$`)
|
||||||
|
quotedConfigLine = regexp.MustCompile(`^\s*(\w+)\s*=\s*["'](.*)["']\s*$`)
|
||||||
|
configLine = regexp.MustCompile(`^\s*(\w+)\s*=\s*(.*)\s*$`)
|
||||||
|
commentLine = regexp.MustCompile(`^#.*$`)
|
||||||
|
blankLine = regexp.MustCompile(`^\s*$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultSection is the label for the default ini file section.
|
||||||
|
var DefaultSection = "default"
|
||||||
|
|
||||||
|
// ParseFile attempts to load the named config file.
|
||||||
|
func ParseFile(fileName string) (cfg ConfigMap, err error) {
|
||||||
|
var file *os.File
|
||||||
|
file, err = os.Open(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
return ParseReader(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseReader reads a configuration from an io.Reader.
|
||||||
|
func ParseReader(r io.Reader) (cfg ConfigMap, err error) {
|
||||||
|
cfg = ConfigMap{}
|
||||||
|
buf := bufio.NewReader(r)
|
||||||
|
|
||||||
|
var (
|
||||||
|
line string
|
||||||
|
longLine bool
|
||||||
|
currentSection string
|
||||||
|
lineBytes []byte
|
||||||
|
isPrefix bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
err = nil
|
||||||
|
lineBytes, isPrefix, err = buf.ReadLine()
|
||||||
|
if io.EOF == err {
|
||||||
|
err = nil
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
break
|
||||||
|
} else if isPrefix {
|
||||||
|
line += string(lineBytes)
|
||||||
|
|
||||||
|
longLine = true
|
||||||
|
continue
|
||||||
|
} else if longLine {
|
||||||
|
line += string(lineBytes)
|
||||||
|
longLine = false
|
||||||
|
} else {
|
||||||
|
line = string(lineBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if commentLine.MatchString(line) {
|
||||||
|
continue
|
||||||
|
} else if blankLine.MatchString(line) {
|
||||||
|
continue
|
||||||
|
} else if configSection.MatchString(line) {
|
||||||
|
section := configSection.ReplaceAllString(line,
|
||||||
|
"$1")
|
||||||
|
if section == "" {
|
||||||
|
err = fmt.Errorf("invalid structure in file")
|
||||||
|
break
|
||||||
|
} else if !cfg.SectionInConfig(section) {
|
||||||
|
cfg[section] = make(map[string]string, 0)
|
||||||
|
}
|
||||||
|
currentSection = section
|
||||||
|
} else if configLine.MatchString(line) {
|
||||||
|
regex := configLine
|
||||||
|
if quotedConfigLine.MatchString(line) {
|
||||||
|
regex = quotedConfigLine
|
||||||
|
}
|
||||||
|
if currentSection == "" {
|
||||||
|
currentSection = DefaultSection
|
||||||
|
if !cfg.SectionInConfig(currentSection) {
|
||||||
|
cfg[currentSection] = map[string]string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key := regex.ReplaceAllString(line, "$1")
|
||||||
|
val := regex.ReplaceAllString(line, "$2")
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cfg[currentSection][key] = val
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("invalid config file")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SectionInConfig determines whether a section is in the configuration.
|
||||||
|
func (c ConfigMap) SectionInConfig(section string) bool {
|
||||||
|
_, ok := c[section]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSections returns the list of sections in the config map.
|
||||||
|
func (c ConfigMap) ListSections() (sections []string) {
|
||||||
|
for section := range c {
|
||||||
|
sections = append(sections, section)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile writes out the configuration to a file.
|
||||||
|
func (c ConfigMap) WriteFile(filename string) (err error) {
|
||||||
|
file, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
for _, section := range c.ListSections() {
|
||||||
|
sName := fmt.Sprintf("[ %s ]\n", section)
|
||||||
|
_, err = file.Write([]byte(sName))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range c[section] {
|
||||||
|
line := fmt.Sprintf("%s = %s\n", k, v)
|
||||||
|
_, err = file.Write([]byte(line))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = file.Write([]byte{0x0a})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSection creates a new section in the config map.
|
||||||
|
func (c ConfigMap) AddSection(section string) {
|
||||||
|
if nil != c[section] {
|
||||||
|
c[section] = map[string]string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddKeyVal adds a key value pair to a config map.
|
||||||
|
func (c ConfigMap) AddKeyVal(section, key, val string) {
|
||||||
|
if section == "" {
|
||||||
|
section = DefaultSection
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil == c[section] {
|
||||||
|
c.AddSection(section)
|
||||||
|
}
|
||||||
|
|
||||||
|
c[section][key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue retrieves the value from a key map.
|
||||||
|
func (c ConfigMap) GetValue(section, key string) (val string, present bool) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if section == "" {
|
||||||
|
section = DefaultSection
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := c[section]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val, present = c[section][key]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValueDefault retrieves the value from a key map if present,
|
||||||
|
// otherwise the default value.
|
||||||
|
func (c ConfigMap) GetValueDefault(section, key, value string) (val string) {
|
||||||
|
kval, ok := c.GetValue(section, key)
|
||||||
|
if !ok {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return kval
|
||||||
|
}
|
||||||
|
|
||||||
|
// SectionKeys returns the sections in the config map.
|
||||||
|
func (c ConfigMap) SectionKeys(section string) (keys []string, present bool) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if section == "" {
|
||||||
|
section = DefaultSection
|
||||||
|
}
|
||||||
|
|
||||||
|
cm := c
|
||||||
|
s, ok := cm[section]
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
keys = make([]string, 0, len(s))
|
||||||
|
for key := range s {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys, true
|
||||||
|
}
|
||||||
142
config/iniconf/iniconf_test.go
Normal file
142
config/iniconf/iniconf_test.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package iniconf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FailWithError is a utility for dumping errors and failing the test.
|
||||||
|
func FailWithError(t *testing.T, err error) {
|
||||||
|
fmt.Println("failed")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("[!] ", err.Error())
|
||||||
|
}
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlinkIfExists removes a file if it exists.
|
||||||
|
func UnlinkIfExists(file string) {
|
||||||
|
_, err := os.Stat(file)
|
||||||
|
if err != nil && os.IsNotExist(err) {
|
||||||
|
panic("failed to remove " + file)
|
||||||
|
}
|
||||||
|
os.Remove(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringSlicesEqual compares two string lists, checking that they
|
||||||
|
// contain the same elements.
|
||||||
|
func stringSlicesEqual(slice1, slice2 []string) bool {
|
||||||
|
if len(slice1) != len(slice2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range slice1 {
|
||||||
|
if slice1[i] != slice2[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range slice2 {
|
||||||
|
if slice1[i] != slice2[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoodConfig(t *testing.T) {
|
||||||
|
testFile := "testdata/test.conf"
|
||||||
|
fmt.Printf("[+] validating known-good config... ")
|
||||||
|
cmap, err := ParseFile(testFile)
|
||||||
|
if err != nil {
|
||||||
|
FailWithError(t, err)
|
||||||
|
} else if len(cmap) != 2 {
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
fmt.Println("ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoodConfig2(t *testing.T) {
|
||||||
|
testFile := "testdata/test2.conf"
|
||||||
|
fmt.Printf("[+] validating second known-good config... ")
|
||||||
|
cmap, err := ParseFile(testFile)
|
||||||
|
if err != nil {
|
||||||
|
FailWithError(t, err)
|
||||||
|
} else if len(cmap) != 1 {
|
||||||
|
FailWithError(t, err)
|
||||||
|
} else if len(cmap["default"]) != 3 {
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
fmt.Println("ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadConfig(t *testing.T) {
|
||||||
|
testFile := "testdata/bad.conf"
|
||||||
|
fmt.Printf("[+] ensure invalid config file fails... ")
|
||||||
|
_, err := ParseFile(testFile)
|
||||||
|
if err == nil {
|
||||||
|
err = fmt.Errorf("invalid config file should fail")
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
fmt.Println("ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteConfigFile(t *testing.T) {
|
||||||
|
fmt.Printf("[+] ensure config file is written properly... ")
|
||||||
|
const testFile = "testdata/test.conf"
|
||||||
|
const testOut = "testdata/test.out"
|
||||||
|
|
||||||
|
cmap, err := ParseFile(testFile)
|
||||||
|
if err != nil {
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer UnlinkIfExists(testOut)
|
||||||
|
err = cmap.WriteFile(testOut)
|
||||||
|
if err != nil {
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmap2, err := ParseFile(testOut)
|
||||||
|
if err != nil {
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionList1 := cmap.ListSections()
|
||||||
|
sectionList2 := cmap2.ListSections()
|
||||||
|
sort.Strings(sectionList1)
|
||||||
|
sort.Strings(sectionList2)
|
||||||
|
if !stringSlicesEqual(sectionList1, sectionList2) {
|
||||||
|
err = fmt.Errorf("section lists don't match")
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, section := range sectionList1 {
|
||||||
|
for _, k := range cmap[section] {
|
||||||
|
if cmap[section][k] != cmap2[section][k] {
|
||||||
|
err = fmt.Errorf("config key doesn't match")
|
||||||
|
FailWithError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotedValue(t *testing.T) {
|
||||||
|
testFile := "testdata/test.conf"
|
||||||
|
fmt.Printf("[+] validating quoted value... ")
|
||||||
|
cmap, _ := ParseFile(testFile)
|
||||||
|
val := cmap["sectionName"]["key4"]
|
||||||
|
if val != " space at beginning and end " {
|
||||||
|
FailWithError(t, errors.New("Wrong value in double quotes ["+val+"]"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val = cmap["sectionName"]["key5"]
|
||||||
|
if val != " is quoted with single quotes " {
|
||||||
|
FailWithError(t, errors.New("Wrong value in single quotes ["+val+"]"))
|
||||||
|
}
|
||||||
|
fmt.Println("ok")
|
||||||
|
}
|
||||||
5
config/iniconf/testdata/bad.conf
vendored
Normal file
5
config/iniconf/testdata/bad.conf
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[]
|
||||||
|
|
||||||
|
key
|
||||||
|
another key
|
||||||
|
key = val
|
||||||
13
config/iniconf/testdata/test.conf
vendored
Normal file
13
config/iniconf/testdata/test.conf
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[ sectionName ]
|
||||||
|
key1=some value
|
||||||
|
key2 = some other value
|
||||||
|
# we want to explain the importance and great forethought
|
||||||
|
# in this next value.
|
||||||
|
key3 = unintuitive value
|
||||||
|
key4 = " space at beginning and end "
|
||||||
|
key5 = ' is quoted with single quotes '
|
||||||
|
|
||||||
|
[ anotherSection ]
|
||||||
|
key1 = a value
|
||||||
|
key2 = yet another value
|
||||||
|
key1 = overwrites previous value of a value
|
||||||
3
config/iniconf/testdata/test2.conf
vendored
Normal file
3
config/iniconf/testdata/test2.conf
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
key1 = some value
|
||||||
|
key2 = some other value
|
||||||
|
key3 = unintuitive value
|
||||||
19
config/path.go
Normal file
19
config/path.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//go:build ignore
|
||||||
|
// +build ignore
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultConfigPath returns a sensible default configuration file path.
|
||||||
|
func DefaultConfigPath(dir, base string) string {
|
||||||
|
user, err := user.Current()
|
||||||
|
if err != nil || user.HomeDir == "" {
|
||||||
|
return filepath.Join(dir, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(user.HomeDir, dir, base)
|
||||||
|
}
|
||||||
43
config/path_linux.go
Normal file
43
config/path_linux.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// canUseXDGConfigDir checks whether the XDG config directory exists
|
||||||
|
// and is accessible by the current user. If it is present, it will
|
||||||
|
// be returned. Note that if the directory does not exist, it is
|
||||||
|
// presumed unusable.
|
||||||
|
func canUseXDGConfigDir() (string, bool) {
|
||||||
|
xdgDir := os.Getenv("XDG_CONFIG_DIR")
|
||||||
|
if xdgDir == "" {
|
||||||
|
userDir := os.Getenv("HOME")
|
||||||
|
if userDir == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
xdgDir = filepath.Join(userDir, ".config")
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := os.Stat(xdgDir)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fi.IsDir() {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return xdgDir, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfigPath returns a sensible default configuration file path.
|
||||||
|
func DefaultConfigPath(dir, base string) string {
|
||||||
|
dirPath, ok := canUseXDGConfigDir()
|
||||||
|
if !ok {
|
||||||
|
dirPath = "/etc"
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(dirPath, dir, base)
|
||||||
|
}
|
||||||
7
config/path_test.go
Normal file
7
config/path_test.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestDefaultPath(t *testing.T) {
|
||||||
|
t.Log(DefaultConfigPath("demoapp", "app.conf"))
|
||||||
|
}
|
||||||
2
config/testdata/test.env
vendored
Normal file
2
config/testdata/test.env
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ORDER=stringiformes
|
||||||
|
SPECIES=strix aluco
|
||||||
76
dbg/dbg.go
Normal file
76
dbg/dbg.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Package dbg implements a debug printer.
|
||||||
|
package dbg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A DebugPrinter is a drop-in replacement for fmt.Print*, and also acts as
|
||||||
|
// an io.WriteCloser when enabled.
|
||||||
|
type DebugPrinter struct {
|
||||||
|
// If Enabled is false, the print statements won't do anything.
|
||||||
|
Enabled bool
|
||||||
|
out io.WriteCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close satisfies the Closer interface.
|
||||||
|
func (dbg *DebugPrinter) Close() error {
|
||||||
|
return dbg.out.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write satisfies the Writer interface.
|
||||||
|
func (dbg *DebugPrinter) Write(p []byte) (int, error) {
|
||||||
|
if dbg.Enabled {
|
||||||
|
return dbg.out.Write(p)
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new DebugPrinter on os.Stdout.
|
||||||
|
func New() *DebugPrinter {
|
||||||
|
return &DebugPrinter{
|
||||||
|
out: os.Stdout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToFile sets up a new DebugPrinter to a file, truncating it if it exists.
|
||||||
|
func ToFile(path string) (*DebugPrinter, error) {
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DebugPrinter{
|
||||||
|
out: file,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// To sets up a new DebugPrint to an io.WriteCloser.
|
||||||
|
func To(w io.WriteCloser) *DebugPrinter {
|
||||||
|
return &DebugPrinter{
|
||||||
|
out: w,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print calls fmt.Print if Enabled is true.
|
||||||
|
func (dbg DebugPrinter) Print(v ...interface{}) {
|
||||||
|
if dbg.Enabled {
|
||||||
|
fmt.Fprint(dbg.out, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println calls fmt.Println if Enabled is true.
|
||||||
|
func (dbg DebugPrinter) Println(v ...interface{}) {
|
||||||
|
if dbg.Enabled {
|
||||||
|
fmt.Fprintln(dbg.out, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf calls fmt.Printf if Enabled is true.
|
||||||
|
func (dbg DebugPrinter) Printf(format string, v ...interface{}) {
|
||||||
|
if dbg.Enabled {
|
||||||
|
fmt.Fprintf(dbg.out, format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
120
dbg/dbg_test.go
Normal file
120
dbg/dbg_test.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package dbg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.sr.ht/~kisom/goutils/testio"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
buf := testio.NewBufCloser(nil)
|
||||||
|
dbg := New()
|
||||||
|
dbg.out = buf
|
||||||
|
|
||||||
|
dbg.Print("hello")
|
||||||
|
dbg.Println("hello")
|
||||||
|
dbg.Printf("hello %s", "world")
|
||||||
|
require.Equal(t, 0, buf.Len())
|
||||||
|
|
||||||
|
dbg.Enabled = true
|
||||||
|
dbg.Print("hello") // +5
|
||||||
|
dbg.Println("hello") // +6
|
||||||
|
dbg.Printf("hello %s", "world") // +11
|
||||||
|
require.Equal(t, 22, buf.Len())
|
||||||
|
|
||||||
|
err := dbg.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTo(t *testing.T) {
|
||||||
|
buf := testio.NewBufCloser(nil)
|
||||||
|
dbg := To(buf)
|
||||||
|
|
||||||
|
dbg.Print("hello")
|
||||||
|
dbg.Println("hello")
|
||||||
|
dbg.Printf("hello %s", "world")
|
||||||
|
require.Equal(t, 0, buf.Len())
|
||||||
|
|
||||||
|
dbg.Enabled = true
|
||||||
|
dbg.Print("hello") // +5
|
||||||
|
dbg.Println("hello") // +6
|
||||||
|
dbg.Printf("hello %s", "world") // +11
|
||||||
|
|
||||||
|
require.Equal(t, 22, buf.Len())
|
||||||
|
|
||||||
|
err := dbg.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToFile(t *testing.T) {
|
||||||
|
testFile, err := ioutil.TempFile("", "dbg")
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = testFile.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
testFileName := testFile.Name()
|
||||||
|
defer os.Remove(testFileName)
|
||||||
|
|
||||||
|
dbg, err := ToFile(testFileName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dbg.Print("hello")
|
||||||
|
dbg.Println("hello")
|
||||||
|
dbg.Printf("hello %s", "world")
|
||||||
|
|
||||||
|
stat, err := os.Stat(testFileName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.EqualValues(t, 0, stat.Size())
|
||||||
|
|
||||||
|
dbg.Enabled = true
|
||||||
|
dbg.Print("hello") // +5
|
||||||
|
dbg.Println("hello") // +6
|
||||||
|
dbg.Printf("hello %s", "world") // +11
|
||||||
|
|
||||||
|
stat, err = os.Stat(testFileName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.EqualValues(t, 22, stat.Size())
|
||||||
|
|
||||||
|
err = dbg.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriting(t *testing.T) {
|
||||||
|
data := []byte("hello, world")
|
||||||
|
buf := testio.NewBufCloser(nil)
|
||||||
|
dbg := To(buf)
|
||||||
|
|
||||||
|
n, err := dbg.Write(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.EqualValues(t, 0, n)
|
||||||
|
|
||||||
|
dbg.Enabled = true
|
||||||
|
n, err = dbg.Write(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.EqualValues(t, 12, n)
|
||||||
|
|
||||||
|
err = dbg.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToFileError(t *testing.T) {
|
||||||
|
testFile, err := ioutil.TempFile("", "dbg")
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = testFile.Chmod(0400)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = testFile.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
testFileName := testFile.Name()
|
||||||
|
|
||||||
|
_, err = ToFile(testFileName)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
err = os.Remove(testFileName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
15
go.mod
Normal file
15
go.mod
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
module git.sr.ht/~kisom/goutils
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cloudflare/cfssl v1.5.0
|
||||||
|
github.com/kisom/goutils v1.1.0
|
||||||
|
github.com/kr/text v0.2.0
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/pkg/sftp v1.12.0
|
||||||
|
github.com/stretchr/testify v1.6.1
|
||||||
|
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392
|
||||||
|
golang.org/x/sys v0.0.0-20201126233918-771906719818
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
)
|
||||||
105
go.sum
Normal file
105
go.sum
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4=
|
||||||
|
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||||
|
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||||
|
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||||
|
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
|
||||||
|
github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY=
|
||||||
|
github.com/cloudflare/cfssl v1.5.0 h1:vFJDAvQgFSRbCn9zg8KpSrrEZrBAQ4KO5oNK7SXEyb0=
|
||||||
|
github.com/cloudflare/cfssl v1.5.0/go.mod h1:sPPkBS5L8l8sRc/IOO1jG51Xb34u+TYhL6P//JdODMQ=
|
||||||
|
github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4=
|
||||||
|
github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||||
|
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE=
|
||||||
|
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||||
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo=
|
||||||
|
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||||
|
github.com/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||||
|
github.com/kisom/goutils v1.1.0 h1:z4HEOgAnFq+e1+O4QdVsyDPatJDu5Ei/7w7DRbYjsIA=
|
||||||
|
github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
|
||||||
|
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
|
||||||
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/sftp v1.12.0 h1:/f3b24xrDhkhddlaobPe2JgBqfdt+gC/NYl0QY9IOuI=
|
||||||
|
github.com/pkg/sftp v1.12.0/go.mod h1:fUqqXB5vEgVCZ131L+9say31RAri6aF6KDViawhxKK8=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
|
github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
|
||||||
|
github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
|
||||||
|
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||||
|
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
|
||||||
|
github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
|
||||||
|
github.com/zmap/zcrypto v0.0.0-20200513165325-16679db567ff/go.mod h1:TxpejqcVKQjQaVVmMGfzx5HnmFMdIU+vLtaCyPBfGI4=
|
||||||
|
github.com/zmap/zcrypto v0.0.0-20200911161511-43ff0ea04f21/go.mod h1:TxpejqcVKQjQaVVmMGfzx5HnmFMdIU+vLtaCyPBfGI4=
|
||||||
|
github.com/zmap/zlint/v2 v2.2.1/go.mod h1:ixPWsdq8qLxYRpNUTbcKig3R7WgmspsHGLhCCs6rFAM=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200124225646-8b5121be2f68/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc=
|
||||||
|
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201126233918-771906719818 h1:f1CIuDlJhwANEC2MM87MBEVMr3jl5bifgsfj90XAF9c=
|
||||||
|
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
|
||||||
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// +build freebsd darwin netbsd
|
// +build freebsd darwin,386 netbsd
|
||||||
|
|
||||||
package lib
|
package lib
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// +build unix linux openbsd
|
// +build unix linux openbsd darwin,amd64
|
||||||
|
|
||||||
package lib
|
package lib
|
||||||
|
|
||||||
|
|||||||
80
lib/lib.go
80
lib/lib.go
@@ -2,7 +2,11 @@
|
|||||||
package lib
|
package lib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@@ -79,6 +83,7 @@ var (
|
|||||||
yearDuration = (365 * dayDuration) + (6 * time.Hour)
|
yearDuration = (365 * dayDuration) + (6 * time.Hour)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Duration returns a prettier string for time.Durations.
|
||||||
func Duration(d time.Duration) string {
|
func Duration(d time.Duration) string {
|
||||||
var s string
|
var s string
|
||||||
if d >= yearDuration {
|
if d >= yearDuration {
|
||||||
@@ -102,3 +107,78 @@ func Duration(d time.Duration) string {
|
|||||||
s += fmt.Sprintf("%dh%s", hours, d)
|
s += fmt.Sprintf("%dh%s", hours, d)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadCertificate reads a DER or PEM-encoded certificate from the
|
||||||
|
// byte slice.
|
||||||
|
func ReadCertificate(in []byte) (cert *x509.Certificate, rest []byte, err error) {
|
||||||
|
if len(in) == 0 {
|
||||||
|
err = errors.New("lib: empty certificate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if in[0] == '-' {
|
||||||
|
p, remaining := pem.Decode(in)
|
||||||
|
if p == nil {
|
||||||
|
err = errors.New("lib: invalid PEM file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rest = remaining
|
||||||
|
if p.Type != "CERTIFICATE" {
|
||||||
|
err = fmt.Errorf("lib: expected a CERTIFICATE PEM file, but have %s", p.Type)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
in = p.Bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err = x509.ParseCertificate(in)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadCertificates tries to read all the certificates in a
|
||||||
|
// PEM-encoded collection.
|
||||||
|
func ReadCertificates(in []byte) (certs []*x509.Certificate, err error) {
|
||||||
|
var cert *x509.Certificate
|
||||||
|
for {
|
||||||
|
cert, in, err = ReadCertificate(in)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if cert == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
certs = append(certs, cert)
|
||||||
|
if len(in) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return certs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCertificate tries to read a single certificate from disk. If
|
||||||
|
// the file contains multiple certificates (e.g. a chain), only the
|
||||||
|
// first certificate is returned.
|
||||||
|
func LoadCertificate(path string) (*x509.Certificate, error) {
|
||||||
|
in, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, _, err := ReadCertificate(in)
|
||||||
|
return cert, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCertificates tries to read all the certificates in a file,
|
||||||
|
// returning them in the order that it found them in the file.
|
||||||
|
func LoadCertificates(path string) ([]*x509.Certificate, error) {
|
||||||
|
in, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReadCertificates(in)
|
||||||
|
}
|
||||||
|
|||||||
139
lib/lib_test.go
Normal file
139
lib/lib_test.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.sr.ht/~kisom/goutils/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// some CA certs I found on my computerbox.
|
||||||
|
var testCerts = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE
|
||||||
|
AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw
|
||||||
|
CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ
|
||||||
|
BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND
|
||||||
|
VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb
|
||||||
|
qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY
|
||||||
|
HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo
|
||||||
|
G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA
|
||||||
|
lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr
|
||||||
|
IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/
|
||||||
|
0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH
|
||||||
|
k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47
|
||||||
|
4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO
|
||||||
|
m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa
|
||||||
|
cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl
|
||||||
|
uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI
|
||||||
|
KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls
|
||||||
|
ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG
|
||||||
|
AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2
|
||||||
|
VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT
|
||||||
|
VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG
|
||||||
|
CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA
|
||||||
|
cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA
|
||||||
|
QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA
|
||||||
|
7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA
|
||||||
|
cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA
|
||||||
|
QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA
|
||||||
|
czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu
|
||||||
|
aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt
|
||||||
|
aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud
|
||||||
|
DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF
|
||||||
|
BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp
|
||||||
|
D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU
|
||||||
|
JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m
|
||||||
|
AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD
|
||||||
|
vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms
|
||||||
|
tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH
|
||||||
|
7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h
|
||||||
|
I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA
|
||||||
|
h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF
|
||||||
|
d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H
|
||||||
|
pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UE
|
||||||
|
AwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00x
|
||||||
|
CzAJBgNVBAYTAkVTMB4XDTA4MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEW
|
||||||
|
MBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZF
|
||||||
|
RElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
|
||||||
|
AgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHkWLn7
|
||||||
|
09gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7
|
||||||
|
XBZXehuDYAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5P
|
||||||
|
Grjm6gSSrj0RuVFCPYewMYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAK
|
||||||
|
t0SdE3QrwqXrIhWYENiLxQSfHY9g5QYbm8+5eaA9oiM/Qj9r+hwDezCNzmzAv+Yb
|
||||||
|
X79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbkHQl/Sog4P75n/TSW9R28
|
||||||
|
MHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTTxKJxqvQU
|
||||||
|
fecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI
|
||||||
|
2Sf23EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyH
|
||||||
|
K9caUPgn6C9D4zq92Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEae
|
||||||
|
ZAwUswdbxcJzbPEHXEUkFDWug/FqTYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAP
|
||||||
|
BgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz4SsrSbbXc6GqlPUB53NlTKxQ
|
||||||
|
MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU9QHnc2VMrFAw
|
||||||
|
RAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv
|
||||||
|
bS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWIm
|
||||||
|
fQwng4/F9tqgaHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3
|
||||||
|
gvoFNTPhNahXwOf9jU8/kzJPeGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKe
|
||||||
|
I6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1PwkzQSulgUV1qzOMPPKC8W64iLgpq0i
|
||||||
|
5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1ThCojz2GuHURwCRi
|
||||||
|
ipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oIKiMn
|
||||||
|
MCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZ
|
||||||
|
o5NjEFIqnxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6
|
||||||
|
zqylfDJKZ0DcMDQj3dcEI2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacN
|
||||||
|
GHk0vFQYXlPKNFHtRQrmjseCNj6nOGOpMCwXEGCSn1WHElkQwg9naRHMTh5+Spqt
|
||||||
|
r0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3otkYNbn5XOmeUwssfnHdK
|
||||||
|
Z05phkOTOPu220+DkdRgfks+KzgHVZhepA==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE
|
||||||
|
BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w
|
||||||
|
MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290
|
||||||
|
IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC
|
||||||
|
SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1
|
||||||
|
ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB
|
||||||
|
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv
|
||||||
|
UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX
|
||||||
|
4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9
|
||||||
|
KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/
|
||||||
|
gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb
|
||||||
|
rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ
|
||||||
|
51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F
|
||||||
|
be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe
|
||||||
|
KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F
|
||||||
|
v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn
|
||||||
|
fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7
|
||||||
|
jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz
|
||||||
|
ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt
|
||||||
|
ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL
|
||||||
|
e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70
|
||||||
|
jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz
|
||||||
|
WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V
|
||||||
|
SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j
|
||||||
|
pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX
|
||||||
|
X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok
|
||||||
|
fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R
|
||||||
|
K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU
|
||||||
|
ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU
|
||||||
|
LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT
|
||||||
|
LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestReadCertificate(t *testing.T) {
|
||||||
|
cert, remaining, err := ReadCertificate([]byte(testCerts))
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
|
||||||
|
assert.BoolT(t, len(remaining) > 0, "lib: expected extra data from ReadCertificate")
|
||||||
|
assert.BoolT(t, cert != nil, "lib: expected an actual certificate to have been returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadCertificates(t *testing.T) {
|
||||||
|
certs, err := ReadCertificates([]byte(testCerts))
|
||||||
|
assert.NoErrorT(t, err)
|
||||||
|
|
||||||
|
assert.BoolT(t, len(certs) == 3, fmt.Sprintf("lib: expected three certificates, have %d", len(certs)))
|
||||||
|
for _, cert := range certs {
|
||||||
|
assert.BoolT(t, cert != nil, "lib: expected an actual certificate to have been returned")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kisom/goutils/logging"
|
"git.sr.ht/~kisom/goutils/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = logging.NewConsole()
|
var log = logging.NewConsole()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package logging_test
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kisom/goutils/logging"
|
"git.sr.ht/~kisom/goutils/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = logging.NewConsole()
|
var log = logging.NewConsole()
|
||||||
|
|||||||
@@ -8,11 +8,20 @@ type File struct {
|
|||||||
*LogWriter
|
*LogWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fl *File) Close() {
|
// Close calls close on the underlying log files.
|
||||||
fl.fo.Close()
|
func (fl *File) Close() error {
|
||||||
if fl.fe != nil {
|
if fl.fo != nil {
|
||||||
fl.fe.Close()
|
if err := fl.fo.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fl.fo = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if fl.fe != nil {
|
||||||
|
return fl.fe.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFile creates a new Logger that writes all logs to the file
|
// NewFile creates a new Logger that writes all logs to the file
|
||||||
@@ -50,7 +59,7 @@ func NewSplitFile(outpath, errpath string, overwrite bool) (*File, error) {
|
|||||||
if overwrite {
|
if overwrite {
|
||||||
fl.fo, err = os.Create(outpath)
|
fl.fo, err = os.Create(outpath)
|
||||||
} else {
|
} else {
|
||||||
fl.fo, err = os.OpenFile(outpath, os.O_WRONLY|os.O_APPEND, 0644)
|
fl.fo, err = os.OpenFile(outpath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -60,7 +69,7 @@ func NewSplitFile(outpath, errpath string, overwrite bool) (*File, error) {
|
|||||||
if overwrite {
|
if overwrite {
|
||||||
fl.fe, err = os.Create(errpath)
|
fl.fe, err = os.Create(errpath)
|
||||||
} else {
|
} else {
|
||||||
fl.fe, err = os.OpenFile(errpath, os.O_WRONLY|os.O_APPEND, 0644)
|
fl.fe, err = os.OpenFile(errpath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const (
|
|||||||
LevelFatal
|
LevelFatal
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultLevel is the default logging level when none is provided.
|
||||||
const DefaultLevel = LevelInfo
|
const DefaultLevel = LevelInfo
|
||||||
|
|
||||||
// Cheap integer to fixed-width decimal ASCII. Give a negative width
|
// Cheap integer to fixed-width decimal ASCII. Give a negative width
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type Logger interface {
|
|||||||
Status() error
|
Status() error
|
||||||
|
|
||||||
// Close gives the Logger the opportunity to perform any cleanup.
|
// Close gives the Logger the opportunity to perform any cleanup.
|
||||||
Close()
|
Close() error
|
||||||
|
|
||||||
// Log messages consist of four components:
|
// Log messages consist of four components:
|
||||||
//
|
//
|
||||||
@@ -228,7 +228,7 @@ func (lw *LogWriter) Fatal(actor, event string, attrs map[string]string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal emits a message indicating that the system is in an unsuable
|
// FatalCode emits a message indicating that the system is in an unsuable
|
||||||
// state, and cannot continue to run. The program will exit with the
|
// state, and cannot continue to run. The program will exit with the
|
||||||
// exit code speicfied in the exitcode argument.
|
// exit code speicfied in the exitcode argument.
|
||||||
//
|
//
|
||||||
@@ -244,7 +244,7 @@ func (lw *LogWriter) FatalCode(exitcode int, actor, event string, attrs map[stri
|
|||||||
os.Exit(exitcode)
|
os.Exit(exitcode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal emits a message indicating that the system is in an unsuable
|
// FatalNoDie emits a message indicating that the system is in an unsuable
|
||||||
// state, and cannot continue to run. The program will not exit; it is
|
// state, and cannot continue to run. The program will not exit; it is
|
||||||
// assumed that the caller has some final clean up to perform.
|
// assumed that the caller has some final clean up to perform.
|
||||||
//
|
//
|
||||||
@@ -276,4 +276,94 @@ func (lw *LogWriter) SetLevel(l Level) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close is a no-op that satisfies the Logger interface.
|
// Close is a no-op that satisfies the Logger interface.
|
||||||
func (lw *LogWriter) Close() {}
|
func (lw *LogWriter) Close() error { return nil }
|
||||||
|
|
||||||
|
// Multi allows combining of loggers.
|
||||||
|
type Multi struct {
|
||||||
|
loggers []Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMulti(loggers ...Logger) *Multi {
|
||||||
|
return &Multi{loggers: loggers}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multi) SetLevel(level Level) {
|
||||||
|
for _, l := range m.loggers {
|
||||||
|
l.SetLevel(level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multi) Good() bool {
|
||||||
|
good := true
|
||||||
|
for _, l := range m.loggers {
|
||||||
|
good = good && l.Good()
|
||||||
|
}
|
||||||
|
|
||||||
|
return good
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multi) Status() error {
|
||||||
|
for _, l := range m.loggers {
|
||||||
|
if err := l.Status(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multi) Close() error {
|
||||||
|
for _, l := range m.loggers {
|
||||||
|
l.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multi) Debug(actor, event string, attrs map[string]string) {
|
||||||
|
for _, l := range m.loggers {
|
||||||
|
l.Debug(actor, event, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multi) Info(actor, event string, attrs map[string]string) {
|
||||||
|
for _, l := range m.loggers {
|
||||||
|
l.Info(actor, event, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multi) Warn(actor, event string, attrs map[string]string) {
|
||||||
|
for _, l := range m.loggers {
|
||||||
|
l.Warn(actor, event, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multi) Error(actor, event string, attrs map[string]string) {
|
||||||
|
for _, l := range m.loggers {
|
||||||
|
l.Error(actor, event, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multi) Critical(actor, event string, attrs map[string]string) {
|
||||||
|
for _, l := range m.loggers {
|
||||||
|
l.Critical(actor, event, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multi) Fatal(actor, event string, attrs map[string]string) {
|
||||||
|
for _, l := range m.loggers {
|
||||||
|
l.Fatal(actor, event, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multi) FatalCode(exitcode int, actor, event string, attrs map[string]string) {
|
||||||
|
for _, l := range m.loggers {
|
||||||
|
l.FatalCode(exitcode, actor, event, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multi) FatalNoDie(actor, event string, attrs map[string]string) {
|
||||||
|
for _, l := range m.loggers {
|
||||||
|
l.FatalNoDie(actor, event, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,3 +53,12 @@ func TestDestroyLogFiles(t *testing.T) {
|
|||||||
os.Remove("fw2.log")
|
os.Remove("fw2.log")
|
||||||
os.Remove("fw2.err")
|
os.Remove("fw2.err")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMulti(t *testing.T) {
|
||||||
|
c1 := NewConsole()
|
||||||
|
c2 := NewConsole()
|
||||||
|
m := NewMulti(c1, c2)
|
||||||
|
if !m.Good() {
|
||||||
|
t.Fatal("failed to set up multi logger")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/kisom/goutils/testio"
|
"git.sr.ht/~kisom/goutils/assert"
|
||||||
"github.com/kisom/goutils/assert"
|
"git.sr.ht/~kisom/goutils/testio"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMWC(t *testing.T) {
|
func TestMWC(t *testing.T) {
|
||||||
|
|||||||
49
rand/rand.go
Normal file
49
rand/rand.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Package rand contains utilities for interacting with math/rand, including
|
||||||
|
// seeding from a random sed.
|
||||||
|
package rand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
mrand "math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CryptoUint64 generates a cryptographically-secure 64-bit integer.
|
||||||
|
func CryptoUint64() (uint64, error) {
|
||||||
|
bs := make([]byte, 8)
|
||||||
|
_, err := rand.Read(bs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return binary.BigEndian.Uint64(bs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed initialises the non-cryptographic PRNG with a random,
|
||||||
|
// cryptographically secure value. This is done just as a good
|
||||||
|
// way to make this random. The returned 64-bit value is the seed.
|
||||||
|
func Seed() (uint64, error) {
|
||||||
|
seed, err := CryptoUint64()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB: this is permitted.
|
||||||
|
mrand.Seed(int64(seed))
|
||||||
|
return seed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int is a wrapper for math.Int so only one package needs to be imported.
|
||||||
|
func Int() int {
|
||||||
|
return mrand.Int()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intn is a wrapper for math.Intn so only one package needs to be imported.
|
||||||
|
func Intn(max int) int {
|
||||||
|
return mrand.Intn(max)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intn2 returns a random value between min and max, inclusive.
|
||||||
|
func Intn2(min, max int) int {
|
||||||
|
return Intn(max-min) + min
|
||||||
|
}
|
||||||
74
rand/rand_test.go
Normal file
74
rand/rand_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package rand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
mrand "math/rand"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCryptoUint64(t *testing.T) {
|
||||||
|
n1, err := CryptoUint64()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n2, err := CryptoUint64()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This has such a low chance of occurring that it's likely to be
|
||||||
|
// indicative of a bad CSPRNG.
|
||||||
|
if n1 == n2 {
|
||||||
|
t.Fatalf("repeated random uint64s: %d", n1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntn(t *testing.T) {
|
||||||
|
expected := []int{3081, 4887, 4847, 1059, 3081}
|
||||||
|
mrand.Seed(1)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
n := Intn2(1000, 5000)
|
||||||
|
|
||||||
|
if n != expected[i] {
|
||||||
|
fmt.Printf("invalid sequence at %d: expected %d, have %d", i, expected[i], n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeed(t *testing.T) {
|
||||||
|
seed1, err := Seed()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var seed2 uint64
|
||||||
|
n1 := Int()
|
||||||
|
tries := 0
|
||||||
|
|
||||||
|
for {
|
||||||
|
seed2, err = Seed()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if seed1 != seed2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
tries++
|
||||||
|
|
||||||
|
if tries > 3 {
|
||||||
|
t.Fatal("can't generate two unique seeds")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n2 := Int()
|
||||||
|
|
||||||
|
// Again, this not impossible, merely statistically improbably and a
|
||||||
|
// potential canary for RNG issues.
|
||||||
|
if n1 == n2 {
|
||||||
|
t.Fatalf("repeated integers fresh from two unique seeds: %d/%d -> %d",
|
||||||
|
seed1, seed2, n1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -168,7 +168,7 @@ func TestRWByte(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c != 42 {
|
if c != 42 {
|
||||||
t.Fatal("Expected 42, have %d", c)
|
t.Fatalf("Expected 42, have %d", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = buf.ReadByte()
|
_, err = buf.ReadByte()
|
||||||
|
|||||||
51
seekbuf/seekbuf.go
Normal file
51
seekbuf/seekbuf.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package seekbuf
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
// Buffer is a ReadWriteCloser that supports seeking. It's intended to
|
||||||
|
// replicate the functionality of bytes.Buffer that I use in my projects.
|
||||||
|
//
|
||||||
|
// Note that the seeking is limited to the read marker; all writes are
|
||||||
|
// append-only.
|
||||||
|
type Buffer struct {
|
||||||
|
data []byte
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(data []byte) *Buffer {
|
||||||
|
return &Buffer{
|
||||||
|
data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Read(p []byte) (int, error) {
|
||||||
|
if b.pos >= len(b.data) {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
n := copy(p, b.data[b.pos:])
|
||||||
|
b.pos += n
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Write(p []byte) (int, error) {
|
||||||
|
b.data = append(b.data, p...)
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek sets the read pointer to pos.
|
||||||
|
func (b *Buffer) Seek(pos int) {
|
||||||
|
b.pos = pos
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewind resets the read pointer to 0.
|
||||||
|
func (b *Buffer) Rewind() {
|
||||||
|
b.pos = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close clears all the data out of the buffer and sets the read position to 0.
|
||||||
|
func (b *Buffer) Close() error {
|
||||||
|
b.data = nil
|
||||||
|
b.pos = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
107
tee/tee.go
Normal file
107
tee/tee.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package tee
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WriteStringCloser interface {
|
||||||
|
Write([]byte) (int, error)
|
||||||
|
WriteString(string) (int, error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tee emulates the Unix tee(1) command.
|
||||||
|
type Tee struct {
|
||||||
|
f WriteStringCloser
|
||||||
|
Verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tee) Write(p []byte) (int, error) {
|
||||||
|
n, err := os.Stdout.Write(p)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.f != nil {
|
||||||
|
return t.f.Write(p)
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close calls Close on the underlying file.
|
||||||
|
func (t *Tee) Close() error {
|
||||||
|
return t.f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOut writes to standard output only. The file is created, not
|
||||||
|
// appended to.
|
||||||
|
func NewOut(logFile string) (*Tee, error) {
|
||||||
|
if logFile == "" {
|
||||||
|
return &Tee{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(logFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Tee{f: f}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf formats according to a format specifier and writes to the
|
||||||
|
// tee instance.
|
||||||
|
func (t *Tee) Printf(format string, args ...interface{}) (int, error) {
|
||||||
|
s := fmt.Sprintf(format, args...)
|
||||||
|
n, err := os.Stdout.WriteString(s)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.f == nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.f.WriteString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VPrintf is a variant of Printf that only prints if the Tee's
|
||||||
|
// Verbose flag is set.
|
||||||
|
func (t *Tee) VPrintf(format string, args ...interface{}) (int, error) {
|
||||||
|
if t.Verbose {
|
||||||
|
return t.Printf(format, args...)
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalTee = &Tee{}
|
||||||
|
|
||||||
|
// Open will attempt to open the logFile for the global tee instance.
|
||||||
|
func Open(logFile string) error {
|
||||||
|
f, err := os.Create(logFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
globalTee.f = f
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf formats according to a format specifier and writes to the
|
||||||
|
// global tee.
|
||||||
|
func Printf(format string, args ...interface{}) (int, error) {
|
||||||
|
return globalTee.Printf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VPrintf calls VPrintf on the global tee instance.
|
||||||
|
func VPrintf(format string, args ...interface{}) (int, error) {
|
||||||
|
return globalTee.VPrintf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close calls close on the global tee instance.
|
||||||
|
func Close() error {
|
||||||
|
return globalTee.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetVerbose controls the verbosity of the global tee.
|
||||||
|
func SetVerbose(verbose bool) {
|
||||||
|
globalTee.Verbose = verbose
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -194,6 +194,11 @@ func (buf *BufCloser) Bytes() []byte {
|
|||||||
return buf.buf.Bytes()
|
return buf.buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Len returns the length of the buffer.
|
||||||
|
func (buf *BufCloser) Len() int {
|
||||||
|
return buf.buf.Len()
|
||||||
|
}
|
||||||
|
|
||||||
// NewBufCloser creates and initializes a new BufCloser using buf as
|
// NewBufCloser creates and initializes a new BufCloser using buf as
|
||||||
// its initial contents. It is intended to prepare a BufCloser to read
|
// its initial contents. It is intended to prepare a BufCloser to read
|
||||||
// existing data. It can also be used to size the internal buffer for
|
// existing data. It can also be used to size the internal buffer for
|
||||||
|
|||||||
Reference in New Issue
Block a user