Compare commits

..

12 Commits

Author SHA1 Message Date
f3b4838cf6 Overhauling certlib.
LICENSE to Apache 2.0.
2025-11-15 22:00:29 -08:00
8ed30e9960 certlib: linter autofixes 2025-11-15 21:10:09 -08:00
c7de3919b0 log: linting fixes 2025-11-15 21:06:16 -08:00
840066004a logging: linter fixes 2025-11-15 21:02:19 -08:00
9fb93a3802 mwc: linter fixes 2025-11-15 20:39:21 -08:00
ecc7e5ab1e rand: remove unused package 2025-11-15 20:37:02 -08:00
a934c42aa1 temp fix before removing 2025-11-15 20:36:14 -08:00
948986ba60 testutil: remove unused code
It was probably a WIP for something else; it was started in
2016 and not touched since.
2025-11-15 20:25:37 -08:00
3be86573aa testio: linting fixes 2025-11-15 20:24:00 -08:00
e3a6355edb tee: add tests; linter fixes.
Additionally, disable reassign in testing files.
2025-11-15 20:18:09 -08:00
66d16acebc seekbuf: linter fixes 2025-11-15 19:58:41 -08:00
fdff2e0afe sbuf: linter fixes 2025-11-15 19:53:18 -08:00
34 changed files with 1129 additions and 663 deletions

View File

@@ -228,6 +228,8 @@ linters:
# Such cases aren't reported by default. # Such cases aren't reported by default.
# Default: false # Default: false
check-type-assertions: true check-type-assertions: true
exclude-functions:
- (*git.wntrmute.dev/kyle/goutils/sbuf.Buffer).Write
exhaustive: exhaustive:
# Program elements to check for exhaustiveness. # Program elements to check for exhaustiveness.
@@ -341,11 +343,6 @@ linters:
skip-single-param: true skip-single-param: true
mnd: mnd:
# List of function patterns to exclude from analysis.
# Values always ignored: `time.Date`,
# `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,
# `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.
# Default: []
ignored-functions: ignored-functions:
- args.Error - args.Error
- flag.Arg - flag.Arg
@@ -449,6 +446,10 @@ linters:
linters: [ testpackage ] linters: [ testpackage ]
- path: 'dbg/dbg_test.go' - path: 'dbg/dbg_test.go'
linters: [ testpackage ] linters: [ testpackage ]
- path: 'log/logger.go'
linters: [ forbidigo ]
- path: 'logging/example_test.go'
linters: [ testableexamples ]
- source: 'TODO' - source: 'TODO'
linters: [ godot ] linters: [ godot ]
- text: 'should have a package comment' - text: 'should have a package comment'
@@ -470,4 +471,5 @@ linters:
- goconst - goconst
- gosec - gosec
- noctx - noctx
- reassign
- wrapcheck - wrapcheck

View File

@@ -1,3 +1,21 @@
Unreleased - 2025-11-15
"Error handling modernization" (in progress)
- Introduced typed, wrapped errors via certlib/certerr.Error (Source, Kind, Op, Err) with Unwrap.
- Standardized helper constructors: DecodeError, ParsingError, VerifyError, LoadingError.
- Preserved sentinel errors (e.g., ErrEncryptedPrivateKey, ErrInvalidPEMType, ErrEmptyCertificate) for errors.Is.
- Refactored certlib to use certerr in key paths (CSR parsing/verification, PEM cert pool, certificate read/load).
- Migrated logging/file.go and cmd/kgz away from github.com/pkg/errors to stdlib wrapping.
- Removed dependency on github.com/pkg/errors; ran go mod tidy.
- Added package docs for certerr and a README section on error handling and matching.
- Added unit tests for certerr (Is/As and message formatting).
Planned next steps:
- Continue refactoring remaining error paths for consistent wrapping.
- Add focused tests for key flows (encrypted private key, CSR invalid PEM types, etc.).
- Run golangci-lint (errorlint, errcheck) and address findings.
Release 1.2.1 - 2018-09-15 Release 1.2.1 - 2018-09-15
+ Add missing format argument to Errorf call in kgz. + Add missing format argument to Errorf call in kgz.

197
LICENSE
View File

@@ -1,19 +1,194 @@
Copyright (c) 2015-2023 Kyle Isom <kyle@tyrfingr.is> Copyright 2025 K. Isom <kyle@imap.cc>
Permission to use, copy, modify, and distribute this software for any Licensed under the Apache License, Version 2.0 (the "License");
purpose with or without fee is hereby granted, provided that the above you may not use this file except in compliance with the License.
copyright notice and this permission notice appear in all copies. You may obtain a copy of the License at
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES http://www.apache.org/licenses/LICENSE-2.0
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
======================================================================= =======================================================================
The backoff package (written during my time at Cloudflare) is released The backoff package (written during my time at Cloudflare) is released
under the following license: under the following license:

View File

@@ -78,3 +78,43 @@ Each program should have a small README in the directory with more
information. information.
All code here is licensed under the ISC license. All code here is licensed under the ISC license.
Error handling
--------------
This repo standardizes on Go 1.13+ error wrapping and matching. Libraries and
CLIs should:
- Wrap causes with context using `fmt.Errorf("context: %w", err)`.
- Use typed, structured errors from `certlib/certerr` for certificate-related
operations. These include a typed `*certerr.Error` with `Source` and `Kind`.
- Match errors programmatically:
- `errors.Is(err, certerr.ErrEncryptedPrivateKey)` to detect sentinel states.
- `errors.As(err, &e)` (where `var e *certerr.Error`) to inspect
`e.Source`/`e.Kind`.
Examples:
```
cert, err := certlib.LoadCertificate(path)
if err != nil {
// sentinel match
if errors.Is(err, certerr.ErrEmptyCertificate) {
// handle empty input
}
// typed error match
var ce *certerr.Error
if errors.As(err, &ce) {
switch ce.Kind {
case certerr.KindParse:
// parse error handling
case certerr.KindLoad:
// file loading error handling
}
}
}
```
Avoid including sensitive data (keys, passwords, tokens) in error messages.

33
certlib/certerr/doc.go Normal file
View File

@@ -0,0 +1,33 @@
// Package certerr provides typed errors and helpers for certificate-related
// operations across the repository. It standardizes error construction and
// matching so callers can reliably branch on error source/kind using the
// Go 1.13+ `errors.Is` and `errors.As` helpers.
//
// Guidelines
// - Always wrap underlying causes using the helper constructors or with
// fmt.Errorf("context: %w", err).
// - Do not include sensitive data (keys, passwords, tokens) in error
// messages; add only non-sensitive, actionable context.
// - Prefer programmatic checks via errors.Is (for sentinel errors) and
// errors.As (to retrieve *certerr.Error) rather than relying on error
// string contents.
//
// Typical usage
//
// if err := doParse(); err != nil {
// return certerr.ParsingError(certerr.ErrorSourceCertificate, err)
// }
//
// Callers may branch on error kinds and sources:
//
// var e *certerr.Error
// if errors.As(err, &e) {
// switch e.Kind {
// case certerr.KindParse:
// // handle parse error
// }
// }
//
// Sentinel errors are provided for common conditions like
// `certerr.ErrEncryptedPrivateKey` and can be matched with `errors.Is`.
package certerr

View File

@@ -37,6 +37,48 @@ const (
ErrorSourceKeypair ErrorSourceType = 5 ErrorSourceKeypair ErrorSourceType = 5
) )
// ErrorKind is a broad classification describing what went wrong.
type ErrorKind uint8
const (
KindParse ErrorKind = iota + 1
KindDecode
KindVerify
KindLoad
)
func (k ErrorKind) String() string {
switch k {
case KindParse:
return "parse"
case KindDecode:
return "decode"
case KindVerify:
return "verify"
case KindLoad:
return "load"
default:
return "unknown"
}
}
// Error is a typed, wrapped error with structured context for programmatic checks.
// It implements error and supports errors.Is/As via Unwrap.
type Error struct {
Source ErrorSourceType // which domain produced the error (certificate, private key, etc.)
Kind ErrorKind // operation category (parse, decode, verify, load)
Op string // optional operation or function name
Err error // wrapped cause
}
func (e *Error) Error() string {
// Keep message format consistent with existing helpers: "failed to <kind> <source>: <err>"
// Do not include Op by default to preserve existing output expectations.
return fmt.Sprintf("failed to %s %s: %v", e.Kind.String(), e.Source.String(), e.Err)
}
func (e *Error) Unwrap() error { return e.Err }
// InvalidPEMType is used to indicate that we were expecting one type of PEM // InvalidPEMType is used to indicate that we were expecting one type of PEM
// file, but saw another. // file, but saw another.
type InvalidPEMType struct { type InvalidPEMType struct {
@@ -61,19 +103,19 @@ func ErrInvalidPEMType(have string, want ...string) error {
} }
func LoadingError(t ErrorSourceType, err error) error { func LoadingError(t ErrorSourceType, err error) error {
return fmt.Errorf("failed to load %s from disk: %w", t, err) return &Error{Source: t, Kind: KindLoad, Err: err}
} }
func ParsingError(t ErrorSourceType, err error) error { func ParsingError(t ErrorSourceType, err error) error {
return fmt.Errorf("failed to parse %s: %w", t, err) return &Error{Source: t, Kind: KindParse, Err: err}
} }
func DecodeError(t ErrorSourceType, err error) error { func DecodeError(t ErrorSourceType, err error) error {
return fmt.Errorf("failed to decode %s: %w", t, err) return &Error{Source: t, Kind: KindDecode, Err: err}
} }
func VerifyError(t ErrorSourceType, err error) error { func VerifyError(t ErrorSourceType, err error) error {
return fmt.Errorf("failed to verify %s: %w", t, err) return &Error{Source: t, Kind: KindVerify, Err: err}
} }
var ErrEncryptedPrivateKey = errors.New("private key is encrypted") var ErrEncryptedPrivateKey = errors.New("private key is encrypted")

View File

@@ -0,0 +1,55 @@
package certerr
import (
"errors"
"strings"
"testing"
)
func TestTypedErrorWrappingAndFormatting(t *testing.T) {
cause := errors.New("bad data")
err := DecodeError(ErrorSourceCertificate, cause)
// Ensure we can retrieve the typed error
var e *Error
if !errors.As(err, &e) {
t.Fatalf("expected errors.As to retrieve *certerr.Error, got %T", err)
}
if e.Kind != KindDecode {
t.Fatalf("unexpected kind: %v", e.Kind)
}
if e.Source != ErrorSourceCertificate {
t.Fatalf("unexpected source: %v", e.Source)
}
// Check message format (no trailing punctuation enforced by content)
msg := e.Error()
if !strings.Contains(msg, "failed to decode certificate") || !strings.Contains(msg, "bad data") {
t.Fatalf("unexpected error message: %q", msg)
}
}
func TestErrorsIsOnWrappedSentinel(t *testing.T) {
err := DecodeError(ErrorSourcePrivateKey, ErrEncryptedPrivateKey)
if !errors.Is(err, ErrEncryptedPrivateKey) {
t.Fatalf("expected errors.Is to match ErrEncryptedPrivateKey")
}
}
func TestInvalidPEMTypeMessageSingle(t *testing.T) {
err := ErrInvalidPEMType("FOO", "CERTIFICATE")
want := "invalid PEM type: have FOO, expected CERTIFICATE"
if err.Error() != want {
t.Fatalf("unexpected error message: got %q, want %q", err.Error(), want)
}
}
func TestInvalidPEMTypeMessageMultiple(t *testing.T) {
err := ErrInvalidPEMType("FOO", "CERTIFICATE", "NEW CERTIFICATE REQUEST")
if !strings.Contains(
err.Error(),
"invalid PEM type: have FOO, expected one of CERTIFICATE, NEW CERTIFICATE REQUEST",
) {
t.Fatalf("unexpected error message: %q", err.Error())
}
}

View File

@@ -4,7 +4,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors" "errors"
"io/ioutil" "os"
"git.wntrmute.dev/kyle/goutils/certlib/certerr" "git.wntrmute.dev/kyle/goutils/certlib/certerr"
) )
@@ -13,28 +13,31 @@ import (
// byte slice. // byte slice.
func ReadCertificate(in []byte) (cert *x509.Certificate, rest []byte, err error) { func ReadCertificate(in []byte) (cert *x509.Certificate, rest []byte, err error) {
if len(in) == 0 { if len(in) == 0 {
err = certerr.ErrEmptyCertificate return nil, nil, certerr.ParsingError(certerr.ErrorSourceCertificate, certerr.ErrEmptyCertificate)
return
} }
if in[0] == '-' { if in[0] == '-' {
p, remaining := pem.Decode(in) p, remaining := pem.Decode(in)
if p == nil { if p == nil {
err = errors.New("certlib: invalid PEM file") return nil, nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("invalid PEM file"))
return
} }
rest = remaining rest = remaining
if p.Type != "CERTIFICATE" { if p.Type != "CERTIFICATE" {
err = certerr.ErrInvalidPEMType(p.Type, "CERTIFICATE") return nil, rest, certerr.ParsingError(
return certerr.ErrorSourceCertificate,
certerr.ErrInvalidPEMType(p.Type, "CERTIFICATE"),
)
} }
in = p.Bytes in = p.Bytes
} }
cert, err = x509.ParseCertificate(in) cert, err = x509.ParseCertificate(in)
return if err != nil {
return nil, rest, certerr.ParsingError(certerr.ErrorSourceCertificate, err)
}
return cert, rest, nil
} }
// ReadCertificates tries to read all the certificates in a // ReadCertificates tries to read all the certificates in a
@@ -64,9 +67,9 @@ func ReadCertificates(in []byte) (certs []*x509.Certificate, err error) {
// the file contains multiple certificates (e.g. a chain), only the // the file contains multiple certificates (e.g. a chain), only the
// first certificate is returned. // first certificate is returned.
func LoadCertificate(path string) (*x509.Certificate, error) { func LoadCertificate(path string) (*x509.Certificate, error) {
in, err := ioutil.ReadFile(path) in, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, certerr.LoadingError(certerr.ErrorSourceCertificate, err)
} }
cert, _, err := ReadCertificate(in) cert, _, err := ReadCertificate(in)
@@ -76,9 +79,9 @@ func LoadCertificate(path string) (*x509.Certificate, error) {
// LoadCertificates tries to read all the certificates in a file, // LoadCertificates tries to read all the certificates in a file,
// returning them in the order that it found them in the file. // returning them in the order that it found them in the file.
func LoadCertificates(path string) ([]*x509.Certificate, error) { func LoadCertificates(path string) ([]*x509.Certificate, error) {
in, err := ioutil.ReadFile(path) in, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, certerr.LoadingError(certerr.ErrorSourceCertificate, err)
} }
return ReadCertificates(in) return ReadCertificates(in)

View File

@@ -65,10 +65,10 @@ const OneYear = 8760 * time.Hour
// OneDay is a time.Duration representing a day's worth of seconds. // OneDay is a time.Duration representing a day's worth of seconds.
const OneDay = 24 * time.Hour const OneDay = 24 * time.Hour
// DelegationUsage is the OID for the DelegationUseage extensions // DelegationUsage is the OID for the DelegationUseage extensions.
var DelegationUsage = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 44363, 44} var DelegationUsage = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 44363, 44}
// DelegationExtension // DelegationExtension.
var DelegationExtension = pkix.Extension{ var DelegationExtension = pkix.Extension{
Id: DelegationUsage, Id: DelegationUsage,
Critical: false, Critical: false,
@@ -89,24 +89,28 @@ var Jul2012 = InclusiveDate(2012, time.July, 01)
// issuing certificates valid for more than 39 months. // issuing certificates valid for more than 39 months.
var Apr2015 = InclusiveDate(2015, time.April, 01) var Apr2015 = InclusiveDate(2015, time.April, 01)
// KeyLength returns the bit size of ECDSA or RSA PublicKey // KeyLength returns the bit size of ECDSA or RSA PublicKey.
func KeyLength(key interface{}) int { func KeyLength(key any) int {
if key == nil { switch k := key.(type) {
case *ecdsa.PublicKey:
if k == nil {
return 0 return 0
} }
if ecdsaKey, ok := key.(*ecdsa.PublicKey); ok { return k.Curve.Params().BitSize
return ecdsaKey.Curve.Params().BitSize case *rsa.PublicKey:
} else if rsaKey, ok := key.(*rsa.PublicKey); ok { if k == nil {
return rsaKey.N.BitLen()
}
return 0 return 0
}
return k.N.BitLen()
default:
return 0
}
} }
// ExpiryTime returns the time when the certificate chain is expired. // ExpiryTime returns the time when the certificate chain is expired.
func ExpiryTime(chain []*x509.Certificate) (notAfter time.Time) { func ExpiryTime(chain []*x509.Certificate) (notAfter time.Time) {
if len(chain) == 0 { if len(chain) == 0 {
return return notAfter
} }
notAfter = chain[0].NotAfter notAfter = chain[0].NotAfter
@@ -115,7 +119,7 @@ func ExpiryTime(chain []*x509.Certificate) (notAfter time.Time) {
notAfter = cert.NotAfter notAfter = cert.NotAfter
} }
} }
return return notAfter
} }
// MonthsValid returns the number of months for which a certificate is valid. // MonthsValid returns the number of months for which a certificate is valid.
@@ -144,109 +148,99 @@ func ValidExpiry(c *x509.Certificate) bool {
maxMonths = 39 maxMonths = 39
case issued.After(Jul2012): case issued.After(Jul2012):
maxMonths = 60 maxMonths = 60
case issued.Before(Jul2012): default:
maxMonths = 120 maxMonths = 120
} }
if MonthsValid(c) > maxMonths { return MonthsValid(c) <= maxMonths
return false }
}
return true // SignatureString returns the TLS signature string corresponding to
// an X509 signature algorithm.
var signatureString = map[x509.SignatureAlgorithm]string{
x509.MD2WithRSA: "MD2WithRSA",
x509.MD5WithRSA: "MD5WithRSA",
x509.SHA1WithRSA: "SHA1WithRSA",
x509.SHA256WithRSA: "SHA256WithRSA",
x509.SHA384WithRSA: "SHA384WithRSA",
x509.SHA512WithRSA: "SHA512WithRSA",
x509.DSAWithSHA1: "DSAWithSHA1",
x509.DSAWithSHA256: "DSAWithSHA256",
x509.ECDSAWithSHA1: "ECDSAWithSHA1",
x509.ECDSAWithSHA256: "ECDSAWithSHA256",
x509.ECDSAWithSHA384: "ECDSAWithSHA384",
x509.ECDSAWithSHA512: "ECDSAWithSHA512",
} }
// SignatureString returns the TLS signature string corresponding to // SignatureString returns the TLS signature string corresponding to
// an X509 signature algorithm. // an X509 signature algorithm.
func SignatureString(alg x509.SignatureAlgorithm) string { func SignatureString(alg x509.SignatureAlgorithm) string {
switch alg { if s, ok := signatureString[alg]; ok {
case x509.MD2WithRSA: return s
return "MD2WithRSA"
case x509.MD5WithRSA:
return "MD5WithRSA"
case x509.SHA1WithRSA:
return "SHA1WithRSA"
case x509.SHA256WithRSA:
return "SHA256WithRSA"
case x509.SHA384WithRSA:
return "SHA384WithRSA"
case x509.SHA512WithRSA:
return "SHA512WithRSA"
case x509.DSAWithSHA1:
return "DSAWithSHA1"
case x509.DSAWithSHA256:
return "DSAWithSHA256"
case x509.ECDSAWithSHA1:
return "ECDSAWithSHA1"
case x509.ECDSAWithSHA256:
return "ECDSAWithSHA256"
case x509.ECDSAWithSHA384:
return "ECDSAWithSHA384"
case x509.ECDSAWithSHA512:
return "ECDSAWithSHA512"
default:
return "Unknown Signature"
} }
return "Unknown Signature"
}
// HashAlgoString returns the hash algorithm name contains in the signature
// method.
var hashAlgoString = map[x509.SignatureAlgorithm]string{
x509.MD2WithRSA: "MD2",
x509.MD5WithRSA: "MD5",
x509.SHA1WithRSA: "SHA1",
x509.SHA256WithRSA: "SHA256",
x509.SHA384WithRSA: "SHA384",
x509.SHA512WithRSA: "SHA512",
x509.DSAWithSHA1: "SHA1",
x509.DSAWithSHA256: "SHA256",
x509.ECDSAWithSHA1: "SHA1",
x509.ECDSAWithSHA256: "SHA256",
x509.ECDSAWithSHA384: "SHA384",
x509.ECDSAWithSHA512: "SHA512",
} }
// HashAlgoString returns the hash algorithm name contains in the signature // HashAlgoString returns the hash algorithm name contains in the signature
// method. // method.
func HashAlgoString(alg x509.SignatureAlgorithm) string { func HashAlgoString(alg x509.SignatureAlgorithm) string {
switch alg { if s, ok := hashAlgoString[alg]; ok {
case x509.MD2WithRSA: return s
return "MD2"
case x509.MD5WithRSA:
return "MD5"
case x509.SHA1WithRSA:
return "SHA1"
case x509.SHA256WithRSA:
return "SHA256"
case x509.SHA384WithRSA:
return "SHA384"
case x509.SHA512WithRSA:
return "SHA512"
case x509.DSAWithSHA1:
return "SHA1"
case x509.DSAWithSHA256:
return "SHA256"
case x509.ECDSAWithSHA1:
return "SHA1"
case x509.ECDSAWithSHA256:
return "SHA256"
case x509.ECDSAWithSHA384:
return "SHA384"
case x509.ECDSAWithSHA512:
return "SHA512"
default:
return "Unknown Hash Algorithm"
} }
return "Unknown Hash Algorithm"
} }
// StringTLSVersion returns underlying enum values from human names for TLS // StringTLSVersion returns underlying enum values from human names for TLS
// versions, defaults to current golang default of TLS 1.0 // versions, defaults to current golang default of TLS 1.0.
func StringTLSVersion(version string) uint16 { func StringTLSVersion(version string) uint16 {
switch version { switch version {
case "1.3":
return tls.VersionTLS13
case "1.2": case "1.2":
return tls.VersionTLS12 return tls.VersionTLS12
case "1.1": case "1.1":
return tls.VersionTLS11 return tls.VersionTLS11
case "1.0":
return tls.VersionTLS10
default: default:
// Default to Go's historical default of TLS 1.0 for unknown values
return tls.VersionTLS10 return tls.VersionTLS10
} }
} }
// EncodeCertificatesPEM encodes a number of x509 certificates to PEM // EncodeCertificatesPEM encodes a number of x509 certificates to PEM.
func EncodeCertificatesPEM(certs []*x509.Certificate) []byte { func EncodeCertificatesPEM(certs []*x509.Certificate) []byte {
var buffer bytes.Buffer var buffer bytes.Buffer
for _, cert := range certs { for _, cert := range certs {
pem.Encode(&buffer, &pem.Block{ if err := pem.Encode(&buffer, &pem.Block{
Type: "CERTIFICATE", Type: "CERTIFICATE",
Bytes: cert.Raw, Bytes: cert.Raw,
}) }); err != nil {
return nil
}
} }
return buffer.Bytes() return buffer.Bytes()
} }
// EncodeCertificatePEM encodes a single x509 certificates to PEM // EncodeCertificatePEM encodes a single x509 certificates to PEM.
func EncodeCertificatePEM(cert *x509.Certificate) []byte { func EncodeCertificatePEM(cert *x509.Certificate) []byte {
return EncodeCertificatesPEM([]*x509.Certificate{cert}) return EncodeCertificatesPEM([]*x509.Certificate{cert})
} }
@@ -269,7 +263,10 @@ func ParseCertificatesPEM(certsPEM []byte) ([]*x509.Certificate, error) {
certs = append(certs, cert...) certs = append(certs, cert...)
} }
if len(certsPEM) > 0 { if len(certsPEM) > 0 {
return nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("trailing data at end of certificate")) return nil, certerr.DecodeError(
certerr.ErrorSourceCertificate,
errors.New("trailing data at end of certificate"),
)
} }
return certs, nil return certs, nil
} }
@@ -278,29 +275,40 @@ func ParseCertificatesPEM(certsPEM []byte) ([]*x509.Certificate, error) {
// either PKCS #7, PKCS #12, or raw x509. // either PKCS #7, PKCS #12, or raw x509.
func ParseCertificatesDER(certsDER []byte, password string) (certs []*x509.Certificate, key crypto.Signer, err error) { func ParseCertificatesDER(certsDER []byte, password string) (certs []*x509.Certificate, key crypto.Signer, err error) {
certsDER = bytes.TrimSpace(certsDER) certsDER = bytes.TrimSpace(certsDER)
pkcs7data, err := pkcs7.ParsePKCS7(certsDER)
if err != nil { // First, try PKCS #7
var pkcs12data interface{} if pkcs7data, err7 := pkcs7.ParsePKCS7(certsDER); err7 == nil {
certs = make([]*x509.Certificate, 1) if pkcs7data.ContentInfo != "SignedData" {
pkcs12data, certs[0], err = pkcs12.Decode(certsDER, password) return nil, nil, certerr.DecodeError(
if err != nil { certerr.ErrorSourceCertificate,
errors.New("can only extract certificates from signed data content info"),
)
}
certs = pkcs7data.Content.SignedData.Certificates
if certs == nil {
return nil, nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("no certificates decoded"))
}
return certs, nil, nil
}
// Next, try PKCS #12
if pkcs12data, cert, err12 := pkcs12.Decode(certsDER, password); err12 == nil {
signer, ok := pkcs12data.(crypto.Signer)
if !ok {
return nil, nil, certerr.DecodeError(
certerr.ErrorSourcePrivateKey,
errors.New("PKCS12 data does not contain a private key"),
)
}
return []*x509.Certificate{cert}, signer, nil
}
// Finally, attempt to parse raw X.509 certificates
certs, err = x509.ParseCertificates(certsDER) certs, err = x509.ParseCertificates(certsDER)
if err != nil { if err != nil {
return nil, nil, certerr.DecodeError(certerr.ErrorSourceCertificate, err) return nil, nil, certerr.DecodeError(certerr.ErrorSourceCertificate, err)
} }
} else { return certs, nil, nil
key = pkcs12data.(crypto.Signer)
}
} else {
if pkcs7data.ContentInfo != "SignedData" {
return nil, nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("can only extract certificates from signed data content info"))
}
certs = pkcs7data.Content.SignedData.Certificates
}
if certs == nil {
return nil, key, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("no certificates decoded"))
}
return certs, key, nil
} }
// ParseSelfSignedCertificatePEM parses a PEM-encoded certificate and check if it is self-signed. // ParseSelfSignedCertificatePEM parses a PEM-encoded certificate and check if it is self-signed.
@@ -320,17 +328,26 @@ func ParseSelfSignedCertificatePEM(certPEM []byte) (*x509.Certificate, error) {
// can handle PEM encoded PKCS #7 structures. // can handle PEM encoded PKCS #7 structures.
func ParseCertificatePEM(certPEM []byte) (*x509.Certificate, error) { func ParseCertificatePEM(certPEM []byte) (*x509.Certificate, error) {
certPEM = bytes.TrimSpace(certPEM) certPEM = bytes.TrimSpace(certPEM)
cert, rest, err := ParseOneCertificateFromPEM(certPEM) certs, rest, err := ParseOneCertificateFromPEM(certPEM)
if err != nil { if err != nil {
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err) return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err)
} else if cert == nil {
return nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("no certificate decoded"))
} else if len(rest) > 0 {
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("the PEM file should contain only one object"))
} else if len(cert) > 1 {
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("the PKCS7 object in the PEM file should contain only one certificate"))
} }
return cert[0], nil if certs == nil {
return nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("no certificate decoded"))
}
if len(rest) > 0 {
return nil, certerr.ParsingError(
certerr.ErrorSourceCertificate,
errors.New("the PEM file should contain only one object"),
)
}
if len(certs) > 1 {
return nil, certerr.ParsingError(
certerr.ErrorSourceCertificate,
errors.New("the PKCS7 object in the PEM file should contain only one certificate"),
)
}
return certs[0], nil
} }
// ParseOneCertificateFromPEM attempts to parse one PEM encoded certificate object, // ParseOneCertificateFromPEM attempts to parse one PEM encoded certificate object,
@@ -338,7 +355,6 @@ func ParseCertificatePEM(certPEM []byte) (*x509.Certificate, error) {
// multiple certificates, from the top of certsPEM, which itself may // multiple certificates, from the top of certsPEM, which itself may
// contain multiple PEM encoded certificate objects. // contain multiple PEM encoded certificate objects.
func ParseOneCertificateFromPEM(certsPEM []byte) ([]*x509.Certificate, []byte, error) { func ParseOneCertificateFromPEM(certsPEM []byte) ([]*x509.Certificate, []byte, error) {
block, rest := pem.Decode(certsPEM) block, rest := pem.Decode(certsPEM)
if block == nil { if block == nil {
return nil, rest, nil return nil, rest, nil
@@ -384,7 +400,7 @@ func PEMToCertPool(pemCerts []byte) (*x509.CertPool, error) {
certPool := x509.NewCertPool() certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(pemCerts) { if !certPool.AppendCertsFromPEM(pemCerts) {
return nil, errors.New("failed to load cert pool") return nil, certerr.LoadingError(certerr.ErrorSourceCertificate, errors.New("failed to load cert pool"))
} }
return certPool, nil return certPool, nil
@@ -441,7 +457,10 @@ func ParseCSR(in []byte) (csr *x509.CertificateRequest, rest []byte, err error)
p, rest := pem.Decode(in) p, rest := pem.Decode(in)
if p != nil { if p != nil {
if p.Type != "NEW CERTIFICATE REQUEST" && p.Type != "CERTIFICATE REQUEST" { if p.Type != "NEW CERTIFICATE REQUEST" && p.Type != "CERTIFICATE REQUEST" {
return nil, rest, certerr.ParsingError(certerr.ErrorSourceCSR, certerr.ErrInvalidPEMType(p.Type, "NEW CERTIFICATE REQUEST", "CERTIFICATE REQUEST")) return nil, rest, certerr.ParsingError(
certerr.ErrorSourceCSR,
certerr.ErrInvalidPEMType(p.Type, "NEW CERTIFICATE REQUEST", "CERTIFICATE REQUEST"),
)
} }
csr, err = x509.ParseCertificateRequest(p.Bytes) csr, err = x509.ParseCertificateRequest(p.Bytes)
@@ -450,12 +469,12 @@ func ParseCSR(in []byte) (csr *x509.CertificateRequest, rest []byte, err error)
} }
if err != nil { if err != nil {
return nil, rest, err return nil, rest, certerr.ParsingError(certerr.ErrorSourceCSR, err)
} }
err = csr.CheckSignature() err = csr.CheckSignature()
if err != nil { if err != nil {
return nil, rest, err return nil, rest, certerr.VerifyError(certerr.ErrorSourceCSR, err)
} }
return csr, rest, nil return csr, rest, nil
@@ -472,7 +491,7 @@ func ParseCSRPEM(csrPEM []byte) (*x509.CertificateRequest, error) {
csrObject, err := x509.ParseCertificateRequest(block.Bytes) csrObject, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil { if err != nil {
return nil, err return nil, certerr.ParsingError(certerr.ErrorSourceCSR, err)
} }
return csrObject, nil return csrObject, nil
@@ -509,7 +528,7 @@ func SignerAlgo(priv crypto.Signer) x509.SignatureAlgorithm {
} }
} }
// LoadClientCertificate load key/certificate from pem files // LoadClientCertificate load key/certificate from pem files.
func LoadClientCertificate(certFile string, keyFile string) (*tls.Certificate, error) { func LoadClientCertificate(certFile string, keyFile string) (*tls.Certificate, error) {
if certFile != "" && keyFile != "" { if certFile != "" && keyFile != "" {
cert, err := tls.LoadX509KeyPair(certFile, keyFile) cert, err := tls.LoadX509KeyPair(certFile, keyFile)
@@ -521,7 +540,7 @@ func LoadClientCertificate(certFile string, keyFile string) (*tls.Certificate, e
return nil, nil return nil, nil
} }
// CreateTLSConfig creates a tls.Config object from certs and roots // CreateTLSConfig creates a tls.Config object from certs and roots.
func CreateTLSConfig(remoteCAs *x509.CertPool, cert *tls.Certificate) *tls.Config { func CreateTLSConfig(remoteCAs *x509.CertPool, cert *tls.Certificate) *tls.Config {
var certs []tls.Certificate var certs []tls.Certificate
if cert != nil { if cert != nil {
@@ -554,7 +573,10 @@ func DeserializeSCTList(serializedSCTList []byte) ([]ct.SignedCertificateTimesta
return nil, err return nil, err
} }
if len(rest) != 0 { if len(rest) != 0 {
return nil, certerr.ParsingError(certerr.ErrorSourceSCTList, errors.New("serialized SCT list contained trailing garbage")) return nil, certerr.ParsingError(
certerr.ErrorSourceSCTList,
errors.New("serialized SCT list contained trailing garbage"),
)
} }
list := make([]ct.SignedCertificateTimestamp, len(sctList.SCTList)) list := make([]ct.SignedCertificateTimestamp, len(sctList.SCTList))
@@ -565,7 +587,10 @@ func DeserializeSCTList(serializedSCTList []byte) ([]ct.SignedCertificateTimesta
return nil, err return nil, err
} }
if len(rest) != 0 { if len(rest) != 0 {
return nil, certerr.ParsingError(certerr.ErrorSourceSCTList, errors.New("serialized SCT list contained trailing garbage")) return nil, certerr.ParsingError(
certerr.ErrorSourceSCTList,
errors.New("serialized SCT list contained trailing garbage"),
)
} }
list[i] = sct list[i] = sct
} }
@@ -611,20 +636,16 @@ func SCTListFromOCSPResponse(response *ocsp.Response) ([]ct.SignedCertificateTim
// the subsequent file. If no prefix is provided, valFile is assumed to be a // the subsequent file. If no prefix is provided, valFile is assumed to be a
// file path. // file path.
func ReadBytes(valFile string) ([]byte, error) { func ReadBytes(valFile string) ([]byte, error) {
switch splitVal := strings.SplitN(valFile, ":", 2); len(splitVal) { prefix, rest, found := strings.Cut(valFile, ":")
case 1: if !found {
return os.ReadFile(valFile) return os.ReadFile(valFile)
case 2:
switch splitVal[0] {
case "env":
return []byte(os.Getenv(splitVal[1])), nil
case "file":
return os.ReadFile(splitVal[1])
default:
return nil, fmt.Errorf("unknown prefix: %s", splitVal[0])
} }
switch prefix {
case "env":
return []byte(os.Getenv(rest)), nil
case "file":
return os.ReadFile(rest)
default: default:
return nil, fmt.Errorf("multiple prefixes: %s", return nil, fmt.Errorf("unknown prefix: %s", prefix)
strings.Join(splitVal[:len(splitVal)-1], ", "))
} }
} }

View File

@@ -93,7 +93,7 @@ type signedData struct {
Version int Version int
DigestAlgorithms asn1.RawValue DigestAlgorithms asn1.RawValue
ContentInfo asn1.RawValue ContentInfo asn1.RawValue
Certificates asn1.RawValue `asn1:"optional" asn1:"tag:0"` Certificates asn1.RawValue `asn1:"optional"`
Crls asn1.RawValue `asn1:"optional"` Crls asn1.RawValue `asn1:"optional"`
SignerInfos asn1.RawValue SignerInfos asn1.RawValue
} }
@@ -207,7 +207,10 @@ func populateEncryptedData(msg *PKCS7, contentBytes []byte) error {
return certerr.ParsingError(certerr.ErrorSourceCertificate, err) return certerr.ParsingError(certerr.ErrorSourceCertificate, err)
} }
if ed.Version != 0 { if ed.Version != 0 {
return certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("only PKCS #7 encryptedData version 0 is supported")) return certerr.ParsingError(
certerr.ErrorSourceCertificate,
errors.New("only PKCS #7 encryptedData version 0 is supported"),
)
} }
msg.Content.EncryptedData = ed msg.Content.EncryptedData = ed
return nil return nil
@@ -216,7 +219,6 @@ func populateEncryptedData(msg *PKCS7, contentBytes []byte) error {
// ParsePKCS7 attempts to parse the DER encoded bytes of a // ParsePKCS7 attempts to parse the DER encoded bytes of a
// PKCS7 structure. // PKCS7 structure.
func ParsePKCS7(raw []byte) (msg *PKCS7, err error) { func ParsePKCS7(raw []byte) (msg *PKCS7, err error) {
pkcs7, err := unmarshalInit(raw) pkcs7, err := unmarshalInit(raw)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -240,9 +242,11 @@ func ParsePKCS7(raw []byte) (msg *PKCS7, err error) {
return nil, err return nil, err
} }
default: default:
return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("only PKCS# 7 content of type data, signed data or encrypted data can be parsed")) return nil, certerr.ParsingError(
certerr.ErrorSourceCertificate,
errors.New("only PKCS# 7 content of type data, signed data or encrypted data can be parsed"),
)
} }
return msg, nil return msg, nil
} }

View File

@@ -89,7 +89,7 @@ func ldapURL(url string) bool {
// - false, false: an error was encountered while checking revocations. // - false, false: an error was encountered while checking revocations.
// - false, true: the certificate was checked successfully, and it is not revoked. // - false, true: the certificate was checked successfully, and it is not revoked.
// - true, true: the certificate was checked successfully, and it is revoked. // - true, true: the certificate was checked successfully, and it is revoked.
// - true, false: failure to check revocation status causes verification to fail // - true, false: failure to check revocation status causes verification to fail.
func revCheck(cert *x509.Certificate) (revoked, ok bool, err error) { func revCheck(cert *x509.Certificate) (revoked, ok bool, err error) {
for _, url := range cert.CRLDistributionPoints { for _, url := range cert.CRLDistributionPoints {
if ldapURL(url) { if ldapURL(url) {
@@ -154,7 +154,6 @@ func getIssuer(cert *x509.Certificate) *x509.Certificate {
} }
return issuer return issuer
} }
// check a cert against a specific CRL. Returns the same bool pair // check a cert against a specific CRL. Returns the same bool pair
@@ -343,21 +342,21 @@ func sendOCSPRequest(server string, req []byte, leaf, issuer *x509.Certificate)
var crlRead = io.ReadAll var crlRead = io.ReadAll
// SetCRLFetcher sets the function to use to read from the http response body // SetCRLFetcher sets the function to use to read from the http response body.
func SetCRLFetcher(fn func(io.Reader) ([]byte, error)) { func SetCRLFetcher(fn func(io.Reader) ([]byte, error)) {
crlRead = fn crlRead = fn
} }
var remoteRead = io.ReadAll var remoteRead = io.ReadAll
// SetRemoteFetcher sets the function to use to read from the http response body // SetRemoteFetcher sets the function to use to read from the http response body.
func SetRemoteFetcher(fn func(io.Reader) ([]byte, error)) { func SetRemoteFetcher(fn func(io.Reader) ([]byte, error)) {
remoteRead = fn remoteRead = fn
} }
var ocspRead = io.ReadAll var ocspRead = io.ReadAll
// SetOCSPFetcher sets the function to use to read from the http response body // SetOCSPFetcher sets the function to use to read from the http response body.
func SetOCSPFetcher(fn func(io.Reader) ([]byte, error)) { func SetOCSPFetcher(fn func(io.Reader) ([]byte, error)) {
ocspRead = fn ocspRead = fn
} }

View File

@@ -50,7 +50,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// to indicate that this is the case. // to indicate that this is the case.
// 2014/05/22 14:18:17 Certificate expired 2014-04-04 14:14:20 +0000 UTC // 2014/05/22 14:18:17 Certificate expired 2014-04-04 14:14:20 +0000 UTC
// 2014/05/22 14:18:17 Revoked certificate: misc/intermediate_ca/ActalisServerAuthenticationCA.crt // 2014/05/22 14:18:17 Revoked certificate: misc/intermediate_ca/ActalisServerAuthenticationCA.crt.
var expiredCert = mustParse(`-----BEGIN CERTIFICATE----- var expiredCert = mustParse(`-----BEGIN CERTIFICATE-----
MIIEXTCCA8agAwIBAgIEBycURTANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQGEwJV MIIEXTCCA8agAwIBAgIEBycURTANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQGEwJV
UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU
@@ -80,7 +80,7 @@ sESPRwHkcMUNdAp37FLweUw=
// 2014/05/22 14:18:31 Serial number match: intermediate is revoked. // 2014/05/22 14:18:31 Serial number match: intermediate is revoked.
// 2014/05/22 14:18:31 certificate is revoked via CRL // 2014/05/22 14:18:31 certificate is revoked via CRL
// 2014/05/22 14:18:31 Revoked certificate: misc/intermediate_ca/MobileArmorEnterpriseCA.crt // 2014/05/22 14:18:31 Revoked certificate: misc/intermediate_ca/MobileArmorEnterpriseCA.crt.
var revokedCert = mustParse(`-----BEGIN CERTIFICATE----- var revokedCert = mustParse(`-----BEGIN CERTIFICATE-----
MIIEEzCCAvugAwIBAgILBAAAAAABGMGjftYwDQYJKoZIhvcNAQEFBQAwcTEoMCYG MIIEEzCCAvugAwIBAgILBAAAAAABGMGjftYwDQYJKoZIhvcNAQEFBQAwcTEoMCYG
A1UEAxMfR2xvYmFsU2lnbiBSb290U2lnbiBQYXJ0bmVycyBDQTEdMBsGA1UECxMU A1UEAxMfR2xvYmFsU2lnbiBSb290U2lnbiBQYXJ0bmVycyBDQTEdMBsGA1UECxMU
@@ -106,7 +106,7 @@ Kz5vh+5tmytUPKA8hUgmLWe94lMb7Uqq2wgZKsqun5DAWleKu81w7wEcOrjiiB+x
jeBHq7OnpWm+ccTOPCE6H4ZN4wWVS7biEBUdop/8HgXBPQHWAdjL jeBHq7OnpWm+ccTOPCE6H4ZN4wWVS7biEBUdop/8HgXBPQHWAdjL
-----END CERTIFICATE-----`) -----END CERTIFICATE-----`)
// A Comodo intermediate CA certificate with issuer url, CRL url and OCSP url // A Comodo intermediate CA certificate with issuer url, CRL url and OCSP url.
var goodComodoCA = (`-----BEGIN CERTIFICATE----- var goodComodoCA = (`-----BEGIN CERTIFICATE-----
MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB
hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
@@ -182,7 +182,6 @@ func TestGood(t *testing.T) {
} else if revoked { } else if revoked {
t.Fatalf("good certificate should not have been marked as revoked") t.Fatalf("good certificate should not have been marked as revoked")
} }
} }
func TestLdap(t *testing.T) { func TestLdap(t *testing.T) {
@@ -230,7 +229,6 @@ func TestBadCRLSet(t *testing.T) {
t.Fatalf("key emptystring should be deleted from CRLSet") t.Fatalf("key emptystring should be deleted from CRLSet")
} }
delete(CRLSet, "") delete(CRLSet, "")
} }
func TestCachedCRLSet(t *testing.T) { func TestCachedCRLSet(t *testing.T) {
@@ -241,13 +239,11 @@ func TestCachedCRLSet(t *testing.T) {
} }
func TestRemoteFetchError(t *testing.T) { func TestRemoteFetchError(t *testing.T) {
badurl := ":" badurl := ":"
if _, err := fetchRemote(badurl); err == nil { if _, err := fetchRemote(badurl); err == nil {
t.Fatalf("fetching bad url should result in non-nil error") t.Fatalf("fetching bad url should result in non-nil error")
} }
} }
func TestNoOCSPServers(t *testing.T) { func TestNoOCSPServers(t *testing.T) {

View File

@@ -9,8 +9,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/pkg/errors"
) )
const gzipExt = ".gz" const gzipExt = ".gz"
@@ -18,25 +16,25 @@ const gzipExt = ".gz"
func compress(path, target string, level int) error { func compress(path, target string, level int) error {
sourceFile, err := os.Open(path) sourceFile, err := os.Open(path)
if err != nil { if err != nil {
return errors.Wrap(err, "opening file for read") return fmt.Errorf("opening file for read: %w", err)
} }
defer sourceFile.Close() defer sourceFile.Close()
destFile, err := os.Create(target) destFile, err := os.Create(target)
if err != nil { if err != nil {
return errors.Wrap(err, "opening file for write") return fmt.Errorf("opening file for write: %w", err)
} }
defer destFile.Close() defer destFile.Close()
gzipCompressor, err := gzip.NewWriterLevel(destFile, level) gzipCompressor, err := gzip.NewWriterLevel(destFile, level)
if err != nil { if err != nil {
return errors.Wrap(err, "invalid compression level") return fmt.Errorf("invalid compression level: %w", err)
} }
defer gzipCompressor.Close() defer gzipCompressor.Close()
_, err = io.Copy(gzipCompressor, sourceFile) _, err = io.Copy(gzipCompressor, sourceFile)
if err != nil { if err != nil {
return errors.Wrap(err, "compressing file") return fmt.Errorf("compressing file: %w", err)
} }
return nil return nil
@@ -45,25 +43,25 @@ func compress(path, target string, level int) error {
func uncompress(path, target string) error { func uncompress(path, target string) error {
sourceFile, err := os.Open(path) sourceFile, err := os.Open(path)
if err != nil { if err != nil {
return errors.Wrap(err, "opening file for read") return fmt.Errorf("opening file for read: %w", err)
} }
defer sourceFile.Close() defer sourceFile.Close()
gzipUncompressor, err := gzip.NewReader(sourceFile) gzipUncompressor, err := gzip.NewReader(sourceFile)
if err != nil { if err != nil {
return errors.Wrap(err, "reading gzip headers") return fmt.Errorf("reading gzip headers: %w", err)
} }
defer gzipUncompressor.Close() defer gzipUncompressor.Close()
destFile, err := os.Create(target) destFile, err := os.Create(target)
if err != nil { if err != nil {
return errors.Wrap(err, "opening file for write") return fmt.Errorf("opening file for write: %w", err)
} }
defer destFile.Close() defer destFile.Close()
_, err = io.Copy(destFile, gzipUncompressor) _, err = io.Copy(destFile, gzipUncompressor)
if err != nil { if err != nil {
return errors.Wrap(err, "uncompressing file") return fmt.Errorf("uncompressing file: %w", err)
} }
return nil return nil
@@ -109,7 +107,7 @@ func pathForUncompressing(source, dest string) (string, error) {
source = filepath.Base(source) source = filepath.Base(source)
if !strings.HasSuffix(source, gzipExt) { if !strings.HasSuffix(source, gzipExt) {
return "", errors.Errorf("%s is a not gzip-compressed file", source) return "", fmt.Errorf("%s is a not gzip-compressed file", source)
} }
outFile := source[:len(source)-len(gzipExt)] outFile := source[:len(source)-len(gzipExt)]
outFile = filepath.Join(dest, outFile) outFile = filepath.Join(dest, outFile)
@@ -123,7 +121,7 @@ func pathForCompressing(source, dest string) (string, error) {
source = filepath.Base(source) source = filepath.Base(source)
if strings.HasSuffix(source, gzipExt) { if strings.HasSuffix(source, gzipExt) {
return "", errors.Errorf("%s is a gzip-compressed file", source) return "", fmt.Errorf("%s is a gzip-compressed file", source)
} }
dest = filepath.Join(dest, source+gzipExt) dest = filepath.Join(dest, source+gzipExt)

2
go.mod
View File

@@ -5,7 +5,6 @@ go 1.24.0
require ( require (
github.com/hashicorp/go-syslog v1.0.0 github.com/hashicorp/go-syslog v1.0.0
github.com/kr/text v0.2.0 github.com/kr/text v0.2.0
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.12.0 github.com/pkg/sftp v1.12.0
golang.org/x/crypto v0.44.0 golang.org/x/crypto v0.44.0
golang.org/x/sys v0.38.0 golang.org/x/sys v0.38.0
@@ -20,5 +19,6 @@ require (
require ( require (
github.com/kr/fs v0.1.0 // indirect github.com/kr/fs v0.1.0 // indirect
github.com/kr/pretty v0.1.0 // indirect github.com/kr/pretty v0.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
) )

6
go.sum
View File

@@ -25,19 +25,15 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b h1:Qwe1rC8PSniVfAFPFJeyUkB+zcysC3RgJBAGk7eqBEU=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=

View File

@@ -1,4 +1,4 @@
// Package syslog is a syslog-type facility for logging. // Package log is a syslog-type facility for logging.
package log package log
import ( import (
@@ -17,7 +17,7 @@ type logger struct {
writeConsole bool writeConsole bool
} }
func (log *logger) printf(p gsyslog.Priority, format string, args ...interface{}) { func (log *logger) printf(p gsyslog.Priority, format string, args ...any) {
if !strings.HasSuffix(format, "\n") { if !strings.HasSuffix(format, "\n") {
format += "\n" format += "\n"
} }
@@ -28,33 +28,33 @@ func (log *logger) printf(p gsyslog.Priority, format string, args ...interface{}
} }
if log.l != nil { if log.l != nil {
log.l.WriteLevel(p, []byte(fmt.Sprintf(format, args...))) _ = log.l.WriteLevel(p, fmt.Appendf(nil, format, args...))
} }
} }
func (log *logger) print(p gsyslog.Priority, args ...interface{}) { func (log *logger) print(p gsyslog.Priority, args ...any) {
if p <= log.p && log.writeConsole { if p <= log.p && log.writeConsole {
fmt.Printf("%s [%s] ", prioritiev[p], timestamp()) fmt.Printf("%s [%s] ", prioritiev[p], timestamp())
fmt.Print(args...) fmt.Print(args...)
} }
if log.l != nil { if log.l != nil {
log.l.WriteLevel(p, []byte(fmt.Sprint(args...))) _ = log.l.WriteLevel(p, fmt.Append(nil, args...))
} }
} }
func (log *logger) println(p gsyslog.Priority, args ...interface{}) { func (log *logger) println(p gsyslog.Priority, args ...any) {
if p <= log.p && log.writeConsole { if p <= log.p && log.writeConsole {
fmt.Printf("%s [%s] ", prioritiev[p], timestamp()) fmt.Printf("%s [%s] ", prioritiev[p], timestamp())
fmt.Println(args...) fmt.Println(args...)
} }
if log.l != nil { if log.l != nil {
log.l.WriteLevel(p, []byte(fmt.Sprintln(args...))) _ = log.l.WriteLevel(p, fmt.Appendln(nil, args...))
} }
} }
func (log *logger) spew(args ...interface{}) { func (log *logger) spew(args ...any) {
if log.p == gsyslog.LOG_DEBUG { if log.p == gsyslog.LOG_DEBUG {
spew.Dump(args...) spew.Dump(args...)
} }
@@ -160,109 +160,109 @@ func Setup(opts *Options) error {
return nil return nil
} }
func Debug(args ...interface{}) { func Debug(args ...any) {
log.print(gsyslog.LOG_DEBUG, args...) log.print(gsyslog.LOG_DEBUG, args...)
} }
func Info(args ...interface{}) { func Info(args ...any) {
log.print(gsyslog.LOG_INFO, args...) log.print(gsyslog.LOG_INFO, args...)
} }
func Notice(args ...interface{}) { func Notice(args ...any) {
log.print(gsyslog.LOG_NOTICE, args...) log.print(gsyslog.LOG_NOTICE, args...)
} }
func Warning(args ...interface{}) { func Warning(args ...any) {
log.print(gsyslog.LOG_WARNING, args...) log.print(gsyslog.LOG_WARNING, args...)
} }
func Err(args ...interface{}) { func Err(args ...any) {
log.print(gsyslog.LOG_ERR, args...) log.print(gsyslog.LOG_ERR, args...)
} }
func Crit(args ...interface{}) { func Crit(args ...any) {
log.print(gsyslog.LOG_CRIT, args...) log.print(gsyslog.LOG_CRIT, args...)
} }
func Alert(args ...interface{}) { func Alert(args ...any) {
log.print(gsyslog.LOG_ALERT, args...) log.print(gsyslog.LOG_ALERT, args...)
} }
func Emerg(args ...interface{}) { func Emerg(args ...any) {
log.print(gsyslog.LOG_EMERG, args...) log.print(gsyslog.LOG_EMERG, args...)
} }
func Debugln(args ...interface{}) { func Debugln(args ...any) {
log.println(gsyslog.LOG_DEBUG, args...) log.println(gsyslog.LOG_DEBUG, args...)
} }
func Infoln(args ...interface{}) { func Infoln(args ...any) {
log.println(gsyslog.LOG_INFO, args...) log.println(gsyslog.LOG_INFO, args...)
} }
func Noticeln(args ...interface{}) { func Noticeln(args ...any) {
log.println(gsyslog.LOG_NOTICE, args...) log.println(gsyslog.LOG_NOTICE, args...)
} }
func Warningln(args ...interface{}) { func Warningln(args ...any) {
log.print(gsyslog.LOG_WARNING, args...) log.print(gsyslog.LOG_WARNING, args...)
} }
func Errln(args ...interface{}) { func Errln(args ...any) {
log.println(gsyslog.LOG_ERR, args...) log.println(gsyslog.LOG_ERR, args...)
} }
func Critln(args ...interface{}) { func Critln(args ...any) {
log.println(gsyslog.LOG_CRIT, args...) log.println(gsyslog.LOG_CRIT, args...)
} }
func Alertln(args ...interface{}) { func Alertln(args ...any) {
log.println(gsyslog.LOG_ALERT, args...) log.println(gsyslog.LOG_ALERT, args...)
} }
func Emergln(args ...interface{}) { func Emergln(args ...any) {
log.println(gsyslog.LOG_EMERG, args...) log.println(gsyslog.LOG_EMERG, args...)
} }
func Debugf(format string, args ...interface{}) { func Debugf(format string, args ...any) {
log.printf(gsyslog.LOG_DEBUG, format, args...) log.printf(gsyslog.LOG_DEBUG, format, args...)
} }
func Infof(format string, args ...interface{}) { func Infof(format string, args ...any) {
log.printf(gsyslog.LOG_INFO, format, args...) log.printf(gsyslog.LOG_INFO, format, args...)
} }
func Noticef(format string, args ...interface{}) { func Noticef(format string, args ...any) {
log.printf(gsyslog.LOG_NOTICE, format, args...) log.printf(gsyslog.LOG_NOTICE, format, args...)
} }
func Warningf(format string, args ...interface{}) { func Warningf(format string, args ...any) {
log.printf(gsyslog.LOG_WARNING, format, args...) log.printf(gsyslog.LOG_WARNING, format, args...)
} }
func Errf(format string, args ...interface{}) { func Errf(format string, args ...any) {
log.printf(gsyslog.LOG_ERR, format, args...) log.printf(gsyslog.LOG_ERR, format, args...)
} }
func Critf(format string, args ...interface{}) { func Critf(format string, args ...any) {
log.printf(gsyslog.LOG_CRIT, format, args...) log.printf(gsyslog.LOG_CRIT, format, args...)
} }
func Alertf(format string, args ...interface{}) { func Alertf(format string, args ...any) {
log.printf(gsyslog.LOG_ALERT, format, args...) log.printf(gsyslog.LOG_ALERT, format, args...)
} }
func Emergf(format string, args ...interface{}) { func Emergf(format string, args ...any) {
log.printf(gsyslog.LOG_EMERG, format, args...) log.printf(gsyslog.LOG_EMERG, format, args...)
os.Exit(1) os.Exit(1)
} }
func Fatal(args ...interface{}) { func Fatal(args ...any) {
log.println(gsyslog.LOG_ERR, args...) log.println(gsyslog.LOG_ERR, args...)
os.Exit(1) os.Exit(1)
} }
func Fatalf(format string, args ...interface{}) { func Fatalf(format string, args ...any) {
log.printf(gsyslog.LOG_ERR, format, args...) log.printf(gsyslog.LOG_ERR, format, args...)
os.Exit(1) os.Exit(1)
} }
@@ -279,7 +279,7 @@ func FatalError(err error, message string) {
} }
// Spew will pretty print the args if the logger is set to DEBUG priority. // Spew will pretty print the args if the logger is set to DEBUG priority.
func Spew(args ...interface{}) { func Spew(args ...any) {
log.spew(args...) log.spew(args...)
} }

View File

@@ -11,5 +11,4 @@
// This produces the output message // This produces the output message
// //
// [2016-04-01T15:04:30-0700] [ERROR] [actor:serialiser event:failed to open file] error=is a directory path=data.bin // [2016-04-01T15:04:30-0700] [ERROR] [actor:serialiser event:failed to open file] error=is a directory path=data.bin
//
package logging package logging

View File

@@ -25,8 +25,8 @@ func main() {
log.Info("example", "filelog test", nil) log.Info("example", "filelog test", nil)
exampleNewFromFile() exampleNewFromFile()
os.Remove("example.log") _ = os.Remove("example.log")
os.Remove("example.err") _ = os.Remove("example.err")
} }
func exampleNewFromFile() { func exampleNewFromFile() {

View File

@@ -1,6 +1,9 @@
package logging package logging
import "os" import (
"fmt"
"os"
)
// File writes its logs to file. // File writes its logs to file.
type File struct { type File struct {
@@ -8,22 +11,6 @@ type File struct {
*LogWriter *LogWriter
} }
// Close calls close on the underlying log files.
func (fl *File) Close() error {
if fl.fo != nil {
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
// specified by path. If overwrite is specified, the log file will be // specified by path. If overwrite is specified, the log file will be
// truncated before writing. Otherwise, the log file will be appended // truncated before writing. Otherwise, the log file will be appended
@@ -36,7 +23,7 @@ func NewFile(path string, overwrite bool) (*File, error) {
if overwrite { if overwrite {
fl.fo, err = os.Create(path) fl.fo, err = os.Create(path)
} else { } else {
fl.fo, err = os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0644) fl.fo, err = os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0600) // #nosec G302
} }
if err != nil { if err != nil {
@@ -59,7 +46,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|os.O_CREATE, 0644) fl.fo, err = os.OpenFile(outpath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
} }
if err != nil { if err != nil {
@@ -69,14 +56,51 @@ 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|os.O_CREATE, 0644) fl.fe, err = os.OpenFile(errpath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
} }
if err != nil { if err != nil {
fl.Close() if closeErr := fl.Close(); closeErr != nil {
return nil, fmt.Errorf("failed to open error log: cleanup close failed: %v: %w", closeErr, err)
}
return nil, err return nil, err
} }
fl.LogWriter = NewLogWriter(fl.fo, fl.fe) fl.LogWriter = NewLogWriter(fl.fo, fl.fe)
return fl, nil return fl, nil
} }
// Close calls close on the underlying log files.
func (fl *File) Close() error {
if fl.fo != nil {
if err := fl.fo.Close(); err != nil {
return err
}
fl.fo = nil
}
if fl.fe != nil {
return fl.fe.Close()
}
return nil
}
func (fl *File) Flush() error {
if err := fl.fo.Sync(); err != nil {
return err
}
return fl.fe.Sync()
}
func (fl *File) Chmod(mode os.FileMode) error {
if err := fl.fo.Chmod(mode); err != nil {
return fmt.Errorf("failed to chmod output log: %w", err)
}
if err := fl.fe.Chmod(mode); err != nil {
return fmt.Errorf("failed to chmod error log: %w", err)
}
return nil
}

View File

@@ -32,31 +32,6 @@ const (
// DefaultLevel is the default logging level when none is provided. // 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
// to avoid zero-padding. (From log/log.go in the standard library).
func itoa(i int, wid int) string {
// Assemble decimal in reverse order.
var b [20]byte
bp := len(b) - 1
for i >= 10 || wid > 1 {
wid--
q := i / 10
b[bp] = byte('0' + i - q*10)
bp--
i = q
}
// i < 10
b[bp] = byte('0' + i)
return string(b[bp:])
}
func writeToOut(level Level) bool {
if level < LevelWarning {
return true
}
return false
}
var levelPrefix = [...]string{ var levelPrefix = [...]string{
LevelDebug: "DEBUG", LevelDebug: "DEBUG",
LevelInfo: "INFO", LevelInfo: "INFO",

View File

@@ -1,6 +1,7 @@
package logging package logging
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -14,7 +15,7 @@ import (
// 1. The **level** attaches a notion of priority to the log message. // 1. The **level** attaches a notion of priority to the log message.
// Several log levels are available: // Several log levels are available:
// //
// + FATAL (32): the system is in an unsuable state, and cannot // + FATAL (32): the system is in an unusable state and cannot
// continue to run. Most of the logging for this will cause the // continue to run. Most of the logging for this will cause the
// program to exit with an error code. // program to exit with an error code.
// + CRITICAL (16): critical conditions. The error, if uncorrected, is // + CRITICAL (16): critical conditions. The error, if uncorrected, is
@@ -34,9 +35,9 @@ import (
// typically much higher than errors). For example, repeated // typically much higher than errors). For example, repeated
// warnings might be a sign that the system is under attack. // warnings might be a sign that the system is under attack.
// + INFO (2): informational message. This is a normal log message // + INFO (2): informational message. This is a normal log message
// that is used to deliver information, such as recording // used to deliver information, such as recording requests. Ops
// requests. Ops teams are never paged for informational // teams are never paged for informational messages. This is the
// messages. This is the default log level. // default log level.
// + DEBUG (1): debug-level message. These are only used during // + DEBUG (1): debug-level message. These are only used during
// development or if a deployed system repeatedly sees abnormal // development or if a deployed system repeatedly sees abnormal
// errors. // errors.
@@ -65,10 +66,10 @@ import (
// //
// will cover the header: // will cover the header:
// //
// + ``$1`` contains the timestamp // + “$1“ contains the timestamp
// + ``$2`` contains the level // + “$2“ contains the level
// + ``$3`` contains the actor // + “$3“ contains the actor
// + ``$4`` contains the event // + “$4“ contains the event.
type Logger interface { type Logger interface {
// SetLevel sets the minimum log level. // SetLevel sets the minimum log level.
SetLevel(Level) SetLevel(Level)
@@ -131,7 +132,7 @@ func (lw *LogWriter) output(w io.Writer, lvl Level, actor, event string, attrs m
} }
// Debug emits a debug-level message. These are only used during // Debug emits a debug-level message. These are only used during
// development or if a deployed system repeatedly sees abnormal // development, or if a deployed system repeatedly sees abnormal
// errors. // errors.
// //
// Actor specifies the component emitting the message; event indicates // Actor specifies the component emitting the message; event indicates
@@ -213,7 +214,7 @@ func (lw *LogWriter) Critical(actor, event string, attrs map[string]string) {
lw.output(lw.we, LevelCritical, actor, event, attrs) lw.output(lw.we, LevelCritical, actor, event, attrs)
} }
// Fatal emits a message indicating that the system is in an unsuable // Fatal emits a message indicating that the system is in an unusable
// state, and cannot continue to run. The program will exit with exit // state, and cannot continue to run. The program will exit with exit
// code 1. // code 1.
// //
@@ -229,9 +230,9 @@ func (lw *LogWriter) Fatal(actor, event string, attrs map[string]string) {
os.Exit(1) os.Exit(1)
} }
// FatalCode emits a message indicating that the system is in an unsuable // FatalCode emits a message indicating that the system is in an unusable
// 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 specified in the exitcode argument.
// //
// Actor specifies the component emitting the message; event indicates // Actor specifies the component emitting the message; event indicates
// the event that caused the log message to be emitted. attrs is a map // the event that caused the log message to be emitted. attrs is a map
@@ -245,7 +246,7 @@ func (lw *LogWriter) FatalCode(exitcode int, actor, event string, attrs map[stri
os.Exit(exitcode) os.Exit(exitcode)
} }
// FatalNoDie emits a message indicating that the system is in an unsuable // FatalNoDie emits a message indicating that the system is in an unusable
// 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.
// //
@@ -314,11 +315,17 @@ func (m *Multi) Status() error {
} }
func (m *Multi) Close() error { func (m *Multi) Close() error {
var errs []error
for _, l := range m.loggers { for _, l := range m.loggers {
l.Close() if err := l.Close(); err != nil {
errs = append(errs, err)
}
} }
if len(errs) == 0 {
return nil return nil
}
return errors.Join(errs...)
} }
func (m *Multi) Debug(actor, event string, attrs map[string]string) { func (m *Multi) Debug(actor, event string, attrs map[string]string) {

View File

@@ -1,30 +1,32 @@
package logging package logging_test
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"os" "os"
"testing" "testing"
"git.wntrmute.dev/kyle/goutils/logging"
) )
// A list of implementations that should be tested. // A list of implementations that should be tested.
var implementations []Logger var implementations []logging.Logger
func init() { func init() {
lw := NewLogWriter(&bytes.Buffer{}, nil) lw := logging.NewLogWriter(&bytes.Buffer{}, nil)
cw := NewConsole() cw := logging.NewConsole()
implementations = append(implementations, lw) implementations = append(implementations, lw)
implementations = append(implementations, cw) implementations = append(implementations, cw)
} }
func TestFileSetup(t *testing.T) { func TestFileSetup(t *testing.T) {
fw1, err := NewFile("fw1.log", true) fw1, err := logging.NewFile("fw1.log", true)
if err != nil { if err != nil {
t.Fatalf("failed to create new file logger: %v", err) t.Fatalf("failed to create new file logger: %v", err)
} }
fw2, err := NewSplitFile("fw2.log", "fw2.err", true) fw2, err := logging.NewSplitFile("fw2.log", "fw2.err", true)
if err != nil { if err != nil {
t.Fatalf("failed to create new split file logger: %v", err) t.Fatalf("failed to create new split file logger: %v", err)
} }
@@ -33,7 +35,7 @@ func TestFileSetup(t *testing.T) {
implementations = append(implementations, fw2) implementations = append(implementations, fw2)
} }
func TestImplementations(t *testing.T) { func TestImplementations(_ *testing.T) {
for _, l := range implementations { for _, l := range implementations {
l.Info("TestImplementations", "Info message", l.Info("TestImplementations", "Info message",
map[string]string{"type": fmt.Sprintf("%T", l)}) map[string]string{"type": fmt.Sprintf("%T", l)})
@@ -44,20 +46,30 @@ func TestImplementations(t *testing.T) {
func TestCloseLoggers(t *testing.T) { func TestCloseLoggers(t *testing.T) {
for _, l := range implementations { for _, l := range implementations {
l.Close() if err := l.Close(); err != nil {
t.Errorf("failed to close logger: %v", err)
}
} }
} }
func TestDestroyLogFiles(t *testing.T) { func TestDestroyLogFiles(t *testing.T) {
os.Remove("fw1.log") if err := os.Remove("fw1.log"); err != nil {
os.Remove("fw2.log") t.Errorf("failed to remove fw1.log: %v", err)
os.Remove("fw2.err") }
if err := os.Remove("fw2.log"); err != nil {
t.Errorf("failed to remove fw2.log: %v", err)
}
if err := os.Remove("fw2.err"); err != nil {
t.Errorf("failed to remove fw2.err: %v", err)
}
} }
func TestMulti(t *testing.T) { func TestMulti(t *testing.T) {
c1 := NewConsole() c1 := logging.NewConsole()
c2 := NewConsole() c2 := logging.NewConsole()
m := NewMulti(c1, c2) m := logging.NewMulti(c1, c2)
if !m.Good() { if !m.Good() {
t.Fatal("failed to set up multi logger") t.Fatal("failed to set up multi logger")
} }

View File

@@ -8,15 +8,15 @@ type mwc struct {
} }
// Write implements the Writer interface. // Write implements the Writer interface.
func (t *mwc) Write(p []byte) (n int, err error) { func (t *mwc) Write(p []byte) (int, error) {
for _, w := range t.wcs { for _, w := range t.wcs {
n, err = w.Write(p) n, err := w.Write(p)
if err != nil { if err != nil {
return return n, err
} }
if n != len(p) { if n != len(p) {
err = io.ErrShortWrite err = io.ErrShortWrite
return return n, err
} }
} }
return len(p), nil return len(p), nil

View File

@@ -1,10 +1,11 @@
package mwc package mwc_test
import ( import (
"bytes" "bytes"
"testing" "testing"
"git.wntrmute.dev/kyle/goutils/assert" "git.wntrmute.dev/kyle/goutils/assert"
"git.wntrmute.dev/kyle/goutils/mwc"
"git.wntrmute.dev/kyle/goutils/testio" "git.wntrmute.dev/kyle/goutils/testio"
) )
@@ -12,7 +13,7 @@ func TestMWC(t *testing.T) {
buf1 := testio.NewBufCloser(nil) buf1 := testio.NewBufCloser(nil)
buf2 := testio.NewBufCloser(nil) buf2 := testio.NewBufCloser(nil)
mwc := MultiWriteCloser(buf1, buf2) mwc := mwc.MultiWriteCloser(buf1, buf2)
_, err := mwc.Write([]byte("hello, world")) _, err := mwc.Write([]byte("hello, world"))
assert.NoErrorT(t, err) assert.NoErrorT(t, err)
@@ -30,15 +31,15 @@ func TestMWCShort(t *testing.T) {
buf3 := testio.NewBrokenWriter(5) buf3 := testio.NewBrokenWriter(5)
buf4 := testio.NewSilentBrokenWriter(5) buf4 := testio.NewSilentBrokenWriter(5)
mwc := MultiWriteCloser(buf1, buf2, buf3) multiWriter := mwc.MultiWriteCloser(buf1, buf2, buf3)
defer mwc.Close() defer multiWriter.Close()
_, err := mwc.Write([]byte("hello, world")) _, err := multiWriter.Write([]byte("hello, world"))
assert.ErrorT(t, err, "expected a short write error", "but no error occurred") assert.ErrorT(t, err, "expected a short write error", "but no error occurred")
mwc.Close() multiWriter.Close()
mwc = MultiWriteCloser(buf1, buf2, buf4) multiWriter = mwc.MultiWriteCloser(buf1, buf2, buf4)
_, err = mwc.Write([]byte("hello, world")) _, err = multiWriter.Write([]byte("hello, world"))
assert.ErrorT(t, err, "expected a short write error", "but no error occurred") assert.ErrorT(t, err, "expected a short write error", "but no error occurred")
} }
@@ -47,7 +48,7 @@ func TestMWCClose(t *testing.T) {
buf2 := testio.NewBufCloser(nil) buf2 := testio.NewBufCloser(nil)
buf3 := testio.NewBrokenCloser(nil) buf3 := testio.NewBrokenCloser(nil)
mwc := MultiWriteCloser(buf1, buf2, buf3) mwc := mwc.MultiWriteCloser(buf1, buf2, buf3)
_, err := mwc.Write([]byte("hello, world")) _, err := mwc.Write([]byte("hello, world"))
assert.NoErrorT(t, err) assert.NoErrorT(t, err)

View File

@@ -1,49 +0,0 @@
// 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
}

View File

@@ -1,74 +0,0 @@
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)
}
}

View File

@@ -15,7 +15,7 @@ func zero(in []byte, n int) {
stop = len(in) stop = len(in)
} }
for i := 0; i < stop; i++ { for i := range stop {
in[i] ^= in[i] in[i] ^= in[i]
} }
} }
@@ -37,7 +37,10 @@ func NewBuffer(n int) *Buffer {
// original data will be wiped. // original data will be wiped.
func NewBufferFrom(p []byte) *Buffer { func NewBufferFrom(p []byte) *Buffer {
buf := NewBuffer(len(p)) buf := NewBuffer(len(p))
buf.Write(p) _, err := buf.Write(p)
if err != nil {
return nil
}
zero(p, len(p)) zero(p, len(p))
return buf return buf
} }
@@ -54,10 +57,7 @@ func (buf *Buffer) Read(p []byte) (int, error) {
return 0, io.EOF return 0, io.EOF
} }
copyLength := len(p) copyLength := min(len(p), len(buf.buf))
if copyLength > len(buf.buf) {
copyLength = len(buf.buf)
}
copy(p, buf.buf) copy(p, buf.buf)
zero(buf.buf, len(p)) zero(buf.buf, len(p))
@@ -91,10 +91,7 @@ func (buf *Buffer) Write(p []byte) (int, error) {
r := len(buf.buf) + len(p) r := len(buf.buf) + len(p)
if cap(buf.buf) < r { if cap(buf.buf) < r {
l := r l := r
for { for l <= r {
if l > r {
break
}
l *= 2 l *= 2
} }
buf.grow(l - cap(buf.buf)) buf.grow(l - cap(buf.buf))
@@ -107,7 +104,7 @@ func (buf *Buffer) Write(p []byte) (int, error) {
func (buf *Buffer) WriteByte(c byte) error { func (buf *Buffer) WriteByte(c byte) error {
r := len(buf.buf) + 1 r := len(buf.buf) + 1
if cap(buf.buf) < r { if cap(buf.buf) < r {
l := r * 2 l := r << 1
buf.grow(l - cap(buf.buf)) buf.grow(l - cap(buf.buf))
} }
buf.buf = append(buf.buf, c) buf.buf = append(buf.buf, c)
@@ -138,7 +135,7 @@ func (buf *Buffer) Bytes() []byte {
} }
p := make([]byte, buf.Len()) p := make([]byte, buf.Len())
buf.Read(p) _, _ = buf.Read(p)
buf.Close() buf.Close()
return p return p
} }

View File

@@ -1,15 +1,16 @@
package sbuf package sbuf_test
import ( import (
"bytes" "bytes"
"crypto/rand" "crypto/rand"
"testing" "testing"
"git.wntrmute.dev/kyle/goutils/sbuf"
"golang.org/x/crypto/nacl/box" "golang.org/x/crypto/nacl/box"
) )
var ( var (
buf = &Buffer{} buf = &sbuf.Buffer{}
testMessage1 = []byte("round and round and round we go, where we stop, no one knows") testMessage1 = []byte("round and round and round we go, where we stop, no one knows")
testMessage2 = []byte("the deconstruction of falling stars") testMessage2 = []byte("the deconstruction of falling stars")
) )
@@ -113,23 +114,23 @@ func TestShortRead(t *testing.T) {
} }
func TestNewBuffer(t *testing.T) { func TestNewBuffer(t *testing.T) {
buf := NewBuffer(32) testBuffer := sbuf.NewBuffer(32)
if len(buf.buf) != 0 { if testBuffer.Len() != 0 {
t.Fatalf("expected new buffer length to be 0, have %d", t.Fatalf("expected new buffer length to be 0, have %d",
len(buf.buf)) testBuffer.Len())
} }
if cap(buf.buf) != 32 { if testBuffer.Cap() != 32 {
t.Fatalf("expected new buffer capacity to be 0, have %d", t.Fatalf("expected new buffer capacity to be 0, have %d",
cap(buf.buf)) testBuffer.Cap())
} }
} }
func TestNewBufferFrom(t *testing.T) { func TestNewBufferFrom(t *testing.T) {
p := make([]byte, len(testMessage1)) p := make([]byte, len(testMessage1))
copy(p, testMessage1) copy(p, testMessage1)
buf := NewBufferFrom(p) testBuffer := sbuf.NewBufferFrom(p)
if !bytes.Equal(buf.buf, testMessage1) { if !bytes.Equal(testBuffer.Bytes(), testMessage1) {
t.Fatal("new buffer wasn't constructed properly") t.Fatal("new buffer wasn't constructed properly")
} }
} }
@@ -137,10 +138,10 @@ func TestNewBufferFrom(t *testing.T) {
func TestBytes(t *testing.T) { func TestBytes(t *testing.T) {
p := make([]byte, len(testMessage1)) p := make([]byte, len(testMessage1))
copy(p, testMessage1) copy(p, testMessage1)
buf := NewBufferFrom(p) testBuffer := sbuf.NewBufferFrom(p)
out := buf.Bytes() out := testBuffer.Bytes()
if buf.buf != nil { if testBuffer.Len() != 0 {
t.Fatal("buffer was not closed") t.Fatal("buffer was not closed")
} }
@@ -148,21 +149,21 @@ func TestBytes(t *testing.T) {
t.Fatal("buffer did not return the right data") t.Fatal("buffer did not return the right data")
} }
out = buf.Bytes() out = testBuffer.Bytes()
if out != nil { if out != nil {
t.Fatal("a closed buffer should return nil for Bytes") t.Fatal("a closed buffer should return nil for Bytes")
} }
} }
func TestRWByte(t *testing.T) { func TestRWByte(t *testing.T) {
buf := NewBuffer(0) testBuffer := sbuf.NewBuffer(0)
c := byte(42) c := byte(42)
err := buf.WriteByte(c) err := testBuffer.WriteByte(c)
if err != nil { if err != nil {
t.Fatalf("%v", err) t.Fatalf("%v", err)
} }
c, err = buf.ReadByte() c, err = testBuffer.ReadByte()
if err != nil { if err != nil {
t.Fatalf("%v", err) t.Fatalf("%v", err)
} }
@@ -171,22 +172,21 @@ func TestRWByte(t *testing.T) {
t.Fatalf("Expected 42, have %d", c) t.Fatalf("Expected 42, have %d", c)
} }
_, err = buf.ReadByte() _, err = testBuffer.ReadByte()
if err == nil { if err == nil {
t.Fatal("Expected EOF") t.Fatal("Expected EOF")
} }
} }
func BenchmarkRead(b *testing.B) { func BenchmarkRead(b *testing.B) {
b.N = 2000
pub, priv, err := box.GenerateKey(rand.Reader) pub, priv, err := box.GenerateKey(rand.Reader)
if err != nil { if err != nil {
b.Fatalf("%v", err) b.Fatalf("%v", err)
} }
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for b.Loop() {
_, err := buf.Write(priv[:]) _, err = buf.Write(priv[:])
if err != nil { if err != nil {
b.Fatalf("%v", err) b.Fatalf("%v", err)
} }
@@ -204,11 +204,11 @@ func BenchmarkFixed(b *testing.B) {
b.Fatalf("%v", err) b.Fatalf("%v", err)
} }
buf = NewBuffer(64 * b.N) buf = sbuf.NewBuffer(64 * b.N)
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for b.Loop() {
_, err := buf.Write(priv[:]) _, err = buf.Write(priv[:])
if err != nil { if err != nil {
b.Fatalf("%v", err) b.Fatalf("%v", err)
} }

View File

@@ -1,18 +1,20 @@
package seekbuf package seekbuf_test
import ( import (
"fmt" "fmt"
"testing" "testing"
"git.wntrmute.dev/kyle/goutils/assert" "git.wntrmute.dev/kyle/goutils/assert"
"git.wntrmute.dev/kyle/goutils/seekbuf"
) )
func TestSeeking(t *testing.T) { func TestSeeking(t *testing.T) {
partA := []byte("hello, ") partA := []byte("hello, ")
partB := []byte("world!") partB := []byte("world!")
buf := New(partA) buf := seekbuf.New(partA)
assert.BoolT(t, buf.Len() == len(partA), fmt.Sprintf("on init: have length %d, want length %d", buf.Len(), len(partA))) assert.BoolT(t, buf.Len() == len(partA),
fmt.Sprintf("on init: have length %d, want length %d", buf.Len(), len(partA)))
b := make([]byte, 32) b := make([]byte, 32)
@@ -32,7 +34,8 @@ func TestSeeking(t *testing.T) {
partsLen := len(partA) + len(partB) partsLen := len(partA) + len(partB)
buf.Rewind() buf.Rewind()
assert.BoolT(t, buf.Len() == partsLen, fmt.Sprintf("after rewinding: have length %d, want length %d", buf.Len(), partsLen)) assert.BoolT(t, buf.Len() == partsLen,
fmt.Sprintf("after rewinding: have length %d, want length %d", buf.Len(), partsLen))
buf.Close() buf.Close()
assert.BoolT(t, buf.Len() == 0, fmt.Sprintf("after closing, have length %d, want length 0", buf.Len())) assert.BoolT(t, buf.Len() == 0, fmt.Sprintf("after closing, have length %d, want length 0", buf.Len()))

View File

@@ -17,23 +17,6 @@ type Tee struct {
Verbose bool 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 // NewOut writes to standard output only. The file is created, not
// appended to. // appended to.
func NewOut(logFile string) (*Tee, error) { func NewOut(logFile string) (*Tee, error) {
@@ -48,9 +31,32 @@ func NewOut(logFile string) (*Tee, error) {
return &Tee{f: f}, nil return &Tee{f: f}, nil
} }
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 if present.
// It is safe to call Close on a Tee with no file; in that case, it returns nil.
func (t *Tee) Close() error {
if t == nil || t.f == nil {
return nil
}
err := t.f.Close()
t.f = nil
return err
}
// Printf formats according to a format specifier and writes to the // Printf formats according to a format specifier and writes to the
// tee instance. // tee instance.
func (t *Tee) Printf(format string, args ...interface{}) (int, error) { func (t *Tee) Printf(format string, args ...any) (int, error) {
s := fmt.Sprintf(format, args...) s := fmt.Sprintf(format, args...)
n, err := os.Stdout.WriteString(s) n, err := os.Stdout.WriteString(s)
if err != nil { if err != nil {
@@ -66,7 +72,7 @@ func (t *Tee) Printf(format string, args ...interface{}) (int, error) {
// VPrintf is a variant of Printf that only prints if the Tee's // VPrintf is a variant of Printf that only prints if the Tee's
// Verbose flag is set. // Verbose flag is set.
func (t *Tee) VPrintf(format string, args ...interface{}) (int, error) { func (t *Tee) VPrintf(format string, args ...any) (int, error) {
if t.Verbose { if t.Verbose {
return t.Printf(format, args...) return t.Printf(format, args...)
} }
@@ -87,12 +93,12 @@ func Open(logFile string) error {
// Printf formats according to a format specifier and writes to the // Printf formats according to a format specifier and writes to the
// global tee. // global tee.
func Printf(format string, args ...interface{}) (int, error) { func Printf(format string, args ...any) (int, error) {
return globalTee.Printf(format, args...) return globalTee.Printf(format, args...)
} }
// VPrintf calls VPrintf on the global tee instance. // VPrintf calls VPrintf on the global tee instance.
func VPrintf(format string, args ...interface{}) (int, error) { func VPrintf(format string, args ...any) (int, error) {
return globalTee.VPrintf(format, args...) return globalTee.VPrintf(format, args...)
} }

197
tee/tee_test.go Normal file
View File

@@ -0,0 +1,197 @@
package tee_test
import (
"io"
"os"
"path/filepath"
"testing"
tee "git.wntrmute.dev/kyle/goutils/tee"
)
// captureStdout redirects os.Stdout for the duration of fn and returns what was written.
func captureStdout(t *testing.T, fn func()) string {
t.Helper()
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("pipe: %v", err)
}
old := os.Stdout
os.Stdout = w
defer func() { os.Stdout = old }()
fn()
// Close writer to unblock reader and restore stdout
_ = w.Close()
b, _ := io.ReadAll(r)
_ = r.Close()
return string(b)
}
func TestNewOutEmpty_WritesToStdoutOnly(t *testing.T) {
teeInst, err := tee.NewOut("")
if err != nil {
t.Fatalf("NewOut: %v", err)
}
out := captureStdout(t, func() {
var n int
if n, err = teeInst.Write([]byte("abc")); err != nil || n != 3 {
t.Fatalf("Write got n=%d err=%v", n, err)
}
if n, err = teeInst.Printf("-%d-", 7); err != nil || n != len("-7-") {
t.Fatalf("Printf got n=%d err=%v", n, err)
}
})
if out != "abc-7-" {
t.Fatalf("stdout = %q, want %q", out, "abc-7-")
}
}
func TestNewOutWithFile_WritesToBoth(t *testing.T) {
dir := t.TempDir()
logPath := filepath.Join(dir, "log.txt")
teeInst, err := tee.NewOut(logPath)
if err != nil {
t.Fatalf("NewOut: %v", err)
}
defer func() { _ = teeInst.Close() }()
out := captureStdout(t, func() {
if _, err = teeInst.Write([]byte("x")); err != nil {
t.Fatalf("Write: %v", err)
}
if _, err = teeInst.Printf("%s", "y"); err != nil {
t.Fatalf("Printf: %v", err)
}
})
if out != "xy" {
t.Fatalf("stdout = %q, want %q", out, "xy")
}
// Close to flush and release the file before reading
if err = teeInst.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
data, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(data) != "xy" {
t.Fatalf("file content = %q, want %q", string(data), "xy")
}
}
func TestVPrintf_VerboseToggle(t *testing.T) {
teeInst := &tee.Tee{} // stdout only
out := captureStdout(t, func() {
if n, err := teeInst.VPrintf("hello"); err != nil || n != 0 {
t.Fatalf("VPrintf (quiet) got n=%d err=%v", n, err)
}
})
if out != "" {
t.Fatalf("stdout = %q, want empty when not verbose", out)
}
teeInst.Verbose = true
out = captureStdout(t, func() {
if n, err := teeInst.VPrintf("%s", "hello"); err != nil || n != len("hello") {
t.Fatalf("VPrintf (verbose) got n=%d err=%v", n, err)
}
})
if out != "hello" {
t.Fatalf("stdout = %q, want %q", out, "hello")
}
}
func TestWrite_StdoutErrorDoesNotWriteToFile(t *testing.T) {
dir := t.TempDir()
logPath := filepath.Join(dir, "log.txt")
teeInst, err := tee.NewOut(logPath)
if err != nil {
t.Fatalf("NewOut: %v", err)
}
defer func() { _ = teeInst.Close() }()
// Replace stdout with a closed pipe writer to force write error.
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("pipe: %v", err)
}
old := os.Stdout
os.Stdout = w
_ = w.Close() // immediately close to cause EPIPE on write
defer func() {
os.Stdout = old
_ = r.Close()
}()
var n int
if n, err = teeInst.Write([]byte("abc")); err == nil {
t.Fatalf("expected error writing to closed stdout, got n=%d err=nil", n)
}
// Ensure file remained empty because stdout write failed first.
_ = teeInst.Close()
data, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if len(data) != 0 {
t.Fatalf("file content = %q, want empty due to stdout failure", string(data))
}
}
func TestGlobal_OpenPrintfVPrintfClose(t *testing.T) {
// Ensure a clean slate for global tee
_ = tee.Close()
tee.SetVerbose(false)
dir := t.TempDir()
logPath := filepath.Join(dir, "glog.txt")
if err := tee.Open(logPath); err != nil {
t.Fatalf("Open: %v", err)
}
out := captureStdout(t, func() {
if _, err := tee.Printf("A"); err != nil {
t.Fatalf("Printf: %v", err)
}
// Not verbose yet, should not print
if n, err := tee.VPrintf("B"); err != nil || n != 0 {
t.Fatalf("VPrintf (quiet) n=%d err=%v", n, err)
}
tee.SetVerbose(true)
if _, err := tee.VPrintf("C%d", 1); err != nil {
t.Fatalf("VPrintf (verbose): %v", err)
}
})
if out != "AC1" {
t.Fatalf("stdout = %q, want %q", out, "AC1")
}
if err := tee.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
data, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(data) != "AC1" {
t.Fatalf("file content = %q, want %q", string(data), "AC1")
}
// Reset global tee for other tests/packages
_ = tee.Close()
tee.SetVerbose(false)
}

View File

@@ -169,6 +169,26 @@ type BufCloser struct {
buf *bytes.Buffer buf *bytes.Buffer
} }
// NewBufCloser creates and initializes a new BufCloser using buf as
// 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
// writing. To do that, buf should have the desired capacity but a
// length of zero.
func NewBufCloser(buf []byte) *BufCloser {
bc := new(BufCloser)
bc.buf = bytes.NewBuffer(buf)
return bc
}
// NewBufCloserString creates and initializes a new Buffer using
// string s as its initial contents. It is intended to prepare a
// buffer to read an existing string.
func NewBufCloserString(s string) *BufCloser {
buf := new(BufCloser)
buf.buf = bytes.NewBufferString(s)
return buf
}
// Write writes the data to the BufCloser. // Write writes the data to the BufCloser.
func (buf *BufCloser) Write(p []byte) (int, error) { func (buf *BufCloser) Write(p []byte) (int, error) {
return buf.buf.Write(p) return buf.buf.Write(p)
@@ -199,26 +219,6 @@ func (buf *BufCloser) Len() int {
return buf.buf.Len() return buf.buf.Len()
} }
// NewBufCloser creates and initializes a new BufCloser using buf as
// 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
// writing. To do that, buf should have the desired capacity but a
// length of zero.
func NewBufCloser(buf []byte) *BufCloser {
bc := new(BufCloser)
bc.buf = bytes.NewBuffer(buf)
return bc
}
// NewBufCloserString creates and initializes a new Buffer using
// string s as its initial contents. It is intended to prepare a
// buffer to read an existing string.
func NewBufCloserString(s string) *BufCloser {
buf := new(BufCloser)
buf.buf = bytes.NewBufferString(s)
return buf
}
// A LoggingBuffer is an io.ReadWriter that prints the hex value of // A LoggingBuffer is an io.ReadWriter that prints the hex value of
// the data for all reads and writes. // the data for all reads and writes.
type LoggingBuffer struct { type LoggingBuffer struct {
@@ -323,6 +323,26 @@ type BrokenCloser struct {
buf *bytes.Buffer buf *bytes.Buffer
} }
// NewBrokenCloser creates and initializes a new BrokenCloser using buf as
// its initial contents. It is intended to prepare a BrokenCloser to read
// existing data. It can also be used to size the internal buffer for
// writing. To do that, buf should have the desired capacity but a
// length of zero.
func NewBrokenCloser(buf []byte) *BrokenCloser {
bc := new(BrokenCloser)
bc.buf = bytes.NewBuffer(buf)
return bc
}
// NewBrokenCloserString creates and initializes a new Buffer using
// string s as its initial contents. It is intended to prepare a
// buffer to read an existing string.
func NewBrokenCloserString(s string) *BrokenCloser {
buf := new(BrokenCloser)
buf.buf = bytes.NewBufferString(s)
return buf
}
// Write writes the data to the BrokenCloser. // Write writes the data to the BrokenCloser.
func (buf *BrokenCloser) Write(p []byte) (int, error) { func (buf *BrokenCloser) Write(p []byte) (int, error) {
return buf.buf.Write(p) return buf.buf.Write(p)
@@ -347,23 +367,3 @@ func (buf *BrokenCloser) Reset() {
func (buf *BrokenCloser) Bytes() []byte { func (buf *BrokenCloser) Bytes() []byte {
return buf.buf.Bytes() return buf.buf.Bytes()
} }
// NewBrokenCloser creates and initializes a new BrokenCloser using buf as
// its initial contents. It is intended to prepare a BrokenCloser to read
// existing data. It can also be used to size the internal buffer for
// writing. To do that, buf should have the desired capacity but a
// length of zero.
func NewBrokenCloser(buf []byte) *BrokenCloser {
bc := new(BrokenCloser)
bc.buf = bytes.NewBuffer(buf)
return bc
}
// NewBrokenCloserString creates and initializes a new Buffer using
// string s as its initial contents. It is intended to prepare a
// buffer to read an existing string.
func NewBrokenCloserString(s string) *BrokenCloser {
buf := new(BrokenCloser)
buf.buf = bytes.NewBufferString(s)
return buf
}

View File

@@ -1,13 +1,15 @@
package testio package testio_test
import ( import (
"bytes" "bytes"
"os" "os"
"testing" "testing"
"git.wntrmute.dev/kyle/goutils/testio"
) )
func TestBrokenWriter(t *testing.T) { func TestBrokenWriter(t *testing.T) {
buf := NewBrokenWriter(2) buf := testio.NewBrokenWriter(2)
data := []byte{1, 2} data := []byte{1, 2}
n, err := buf.Write(data) n, err := buf.Write(data)
@@ -39,7 +41,7 @@ func TestBufCloser(t *testing.T) {
var data = []byte{1, 2} var data = []byte{1, 2}
var read = make([]byte, 2) var read = make([]byte, 2)
buf := NewBufCloser(data) buf := testio.NewBufCloser(data)
_, err := buf.Read(read) _, err := buf.Read(read)
if err != nil { if err != nil {
t.Fatalf("%v", err) t.Fatalf("%v", err)
@@ -54,7 +56,7 @@ func TestBufCloser(t *testing.T) {
buf.Reset() buf.Reset()
s := "hi" s := "hi"
buf = NewBufCloserString(s) buf = testio.NewBufCloserString(s)
read = buf.Bytes() read = buf.Bytes()
if string(read) != s { if string(read) != s {
@@ -65,7 +67,7 @@ func TestBufCloser(t *testing.T) {
func TestLoggingBuffer(t *testing.T) { func TestLoggingBuffer(t *testing.T) {
src := &bytes.Buffer{} src := &bytes.Buffer{}
data := []byte("AB") data := []byte("AB")
lb := NewLoggingBuffer(src) lb := testio.NewLoggingBuffer(src)
_, err := lb.Write(data) _, err := lb.Write(data)
if err != nil { if err != nil {
t.Fatalf("%v", err) t.Fatalf("%v", err)
@@ -82,8 +84,8 @@ func TestLoggingBuffer(t *testing.T) {
} }
expected := "[TEST] [WRITE] 4142\n" expected := "[TEST] [WRITE] 4142\n"
if string(out.Bytes()) != expected { if out.String() != expected {
t.Fatalf("expected '%s', have '%s'", expected, string(out.Bytes())) t.Fatalf("expected '%s', have '%s'", expected, out.String())
} }
out.Reset() out.Reset()
@@ -96,8 +98,8 @@ func TestLoggingBuffer(t *testing.T) {
} }
expected = "[TEST] [READ] 4142\n" expected = "[TEST] [READ] 4142\n"
if string(out.Bytes()) != expected { if out.String() != expected {
t.Fatalf("expected '%s', have '%s'", expected, string(out.Bytes())) t.Fatalf("expected '%s', have '%s'", expected, out.String())
} }
out.Reset() out.Reset()
@@ -112,8 +114,8 @@ func TestLoggingBuffer(t *testing.T) {
} }
expected = "[READ] 4142\n" expected = "[READ] 4142\n"
if string(out.Bytes()) != expected { if out.String() != expected {
t.Fatalf("expected '%s', have '%s'", expected, string(out.Bytes())) t.Fatalf("expected '%s', have '%s'", expected, out.String())
} }
src.Reset() src.Reset()
@@ -124,8 +126,8 @@ func TestLoggingBuffer(t *testing.T) {
} }
func TestBrokenReadWriter(t *testing.T) { func TestBrokenReadWriter(t *testing.T) {
brw := NewBrokenReadWriter(0, 0) brw := testio.NewBrokenReadWriter(0, 0)
lb := NewLoggingBuffer(brw) lb := testio.NewLoggingBuffer(brw)
var p = make([]byte, 2) var p = make([]byte, 2)
var data = []byte("HI") var data = []byte("HI")
@@ -177,7 +179,7 @@ func TestBrokenReadWriter(t *testing.T) {
} }
func TestBufferConn(t *testing.T) { func TestBufferConn(t *testing.T) {
bc := NewBufferConn() bc := testio.NewBufferConn()
client := []byte("AB") client := []byte("AB")
peer := []byte("XY") peer := []byte("XY")

View File

@@ -1,16 +0,0 @@
package testutil
import "io/ioutil"
// TempName generates a new temporary file name. The caller should
// remove the temporary file when done.
func TempName() (string, error) {
tmpf, err := ioutil.TempFile("", "transport_cachedkp_")
if err != nil {
return "", err
}
name := tmpf.Name()
tmpf.Close()
return name, nil
}