mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-06-15 07:00:52 +00:00
Imported Upstream version 1.0.1
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ChecksumInfo represents checksums for a single file
|
||||
type ChecksumInfo struct {
|
||||
Size int64
|
||||
MD5 string
|
||||
SHA1 string
|
||||
SHA256 string
|
||||
SHA512 string
|
||||
}
|
||||
|
||||
// ChecksumsForFile generates size, MD5, SHA1 & SHA256 checksums for given file
|
||||
func ChecksumsForFile(path string) (ChecksumInfo, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return ChecksumInfo{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
w := NewChecksumWriter()
|
||||
|
||||
_, err = io.Copy(w, file)
|
||||
if err != nil {
|
||||
return ChecksumInfo{}, err
|
||||
}
|
||||
|
||||
return w.Sum(), nil
|
||||
}
|
||||
|
||||
// ChecksumWriter is a writer that does checksum calculation on the fly passing data
|
||||
// to real writer
|
||||
type ChecksumWriter struct {
|
||||
sum ChecksumInfo
|
||||
hashes []hash.Hash
|
||||
}
|
||||
|
||||
// Interface check
|
||||
var (
|
||||
_ io.Writer = &ChecksumWriter{}
|
||||
)
|
||||
|
||||
// NewChecksumWriter creates checksum calculator for given writer w
|
||||
func NewChecksumWriter() *ChecksumWriter {
|
||||
return &ChecksumWriter{
|
||||
hashes: []hash.Hash{md5.New(), sha1.New(), sha256.New(), sha512.New()},
|
||||
}
|
||||
}
|
||||
|
||||
// Write implememnts pass-through writing with checksum calculation on the fly
|
||||
func (c *ChecksumWriter) Write(p []byte) (n int, err error) {
|
||||
c.sum.Size += int64(len(p))
|
||||
|
||||
for _, h := range c.hashes {
|
||||
h.Write(p)
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Sum returns caculated ChecksumInfo
|
||||
func (c *ChecksumWriter) Sum() ChecksumInfo {
|
||||
c.sum.MD5 = fmt.Sprintf("%x", c.hashes[0].Sum(nil))
|
||||
c.sum.SHA1 = fmt.Sprintf("%x", c.hashes[1].Sum(nil))
|
||||
c.sum.SHA256 = fmt.Sprintf("%x", c.hashes[2].Sum(nil))
|
||||
c.sum.SHA512 = fmt.Sprintf("%x", c.hashes[3].Sum(nil))
|
||||
|
||||
return c.sum
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type ChecksumSuite struct {
|
||||
tempfile *os.File
|
||||
}
|
||||
|
||||
var _ = Suite(&ChecksumSuite{})
|
||||
|
||||
func (s *ChecksumSuite) SetUpTest(c *C) {
|
||||
s.tempfile, _ = ioutil.TempFile(c.MkDir(), "aptly-test")
|
||||
s.tempfile.WriteString(testString)
|
||||
}
|
||||
|
||||
func (s *ChecksumSuite) TearDownTest(c *C) {
|
||||
s.tempfile.Close()
|
||||
}
|
||||
|
||||
func (s *ChecksumSuite) TestChecksumsForFile(c *C) {
|
||||
info, err := ChecksumsForFile(s.tempfile.Name())
|
||||
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(info.Size, Equals, int64(83))
|
||||
c.Check(info.MD5, Equals, "43470766afbfdca292440eecdceb80fb")
|
||||
c.Check(info.SHA1, Equals, "1743f8408261b4f1eff88e0fca15a7077223fa79")
|
||||
c.Check(info.SHA256, Equals, "f2775692fd3b70bd0faa4054b7afa92d427bf994cd8629741710c4864ee4dc95")
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// CompressFile compresses file specified by source to .gz & .bz2
|
||||
//
|
||||
// It uses internal gzip and external bzip2, see:
|
||||
// https://code.google.com/p/go/issues/detail?id=4828
|
||||
func CompressFile(source *os.File, onlyGzip bool) error {
|
||||
gzPath := source.Name() + ".gz"
|
||||
gzFile, err := os.Create(gzPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gzFile.Close()
|
||||
|
||||
gzWriter := gzip.NewWriter(gzFile)
|
||||
defer gzWriter.Close()
|
||||
|
||||
source.Seek(0, 0)
|
||||
_, err = io.Copy(gzWriter, source)
|
||||
if err != nil || onlyGzip {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("bzip2", "-k", "-f", source.Name())
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"compress/bzip2"
|
||||
"compress/gzip"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type CompressSuite struct {
|
||||
tempfile *os.File
|
||||
}
|
||||
|
||||
var _ = Suite(&CompressSuite{})
|
||||
|
||||
const testString = "Quick brown fox jumps over black dog and runs away... Really far away... who knows?"
|
||||
|
||||
func (s *CompressSuite) SetUpTest(c *C) {
|
||||
s.tempfile, _ = ioutil.TempFile(c.MkDir(), "aptly-test")
|
||||
s.tempfile.WriteString(testString)
|
||||
}
|
||||
|
||||
func (s *CompressSuite) TearDownTest(c *C) {
|
||||
s.tempfile.Close()
|
||||
}
|
||||
|
||||
func (s *CompressSuite) TestCompress(c *C) {
|
||||
err := CompressFile(s.tempfile, false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
file, err := os.Open(s.tempfile.Name() + ".gz")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
gzReader, err := gzip.NewReader(file)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
buf, err := ioutil.ReadAll(gzReader)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
gzReader.Close()
|
||||
file.Close()
|
||||
|
||||
c.Check(string(buf), Equals, testString)
|
||||
|
||||
file, err = os.Open(s.tempfile.Name() + ".bz2")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
bzReader := bzip2.NewReader(file)
|
||||
|
||||
_, err = bzReader.Read(buf)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
file.Close()
|
||||
|
||||
c.Check(string(buf), Equals, testString)
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// ConfigStructure is structure of main configuration
|
||||
type ConfigStructure struct {
|
||||
RootDir string `json:"rootDir"`
|
||||
DownloadConcurrency int `json:"downloadConcurrency"`
|
||||
DownloadLimit int64 `json:"downloadSpeedLimit"`
|
||||
Architectures []string `json:"architectures"`
|
||||
DepFollowSuggests bool `json:"dependencyFollowSuggests"`
|
||||
DepFollowRecommends bool `json:"dependencyFollowRecommends"`
|
||||
DepFollowAllVariants bool `json:"dependencyFollowAllVariants"`
|
||||
DepFollowSource bool `json:"dependencyFollowSource"`
|
||||
GpgDisableSign bool `json:"gpgDisableSign"`
|
||||
GpgDisableVerify bool `json:"gpgDisableVerify"`
|
||||
DownloadSourcePackages bool `json:"downloadSourcePackages"`
|
||||
PpaDistributorID string `json:"ppaDistributorID"`
|
||||
PpaCodename string `json:"ppaCodename"`
|
||||
SkipContentsPublishing bool `json:"skipContentsPublishing"`
|
||||
S3PublishRoots map[string]S3PublishRoot `json:"S3PublishEndpoints"`
|
||||
SwiftPublishRoots map[string]SwiftPublishRoot `json:"SwiftPublishEndpoints"`
|
||||
}
|
||||
|
||||
// S3PublishRoot describes single S3 publishing entry point
|
||||
type S3PublishRoot struct {
|
||||
Region string `json:"region"`
|
||||
Bucket string `json:"bucket"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
AccessKeyID string `json:"awsAccessKeyID"`
|
||||
SecretAccessKey string `json:"awsSecretAccessKey"`
|
||||
SessionToken string `json:"awsSessionToken"`
|
||||
Prefix string `json:"prefix"`
|
||||
ACL string `json:"acl"`
|
||||
StorageClass string `json:"storageClass"`
|
||||
EncryptionMethod string `json:"encryptionMethod"`
|
||||
PlusWorkaround bool `json:"plusWorkaround"`
|
||||
DisableMultiDel bool `json:"disableMultiDel"`
|
||||
ForceSigV2 bool `json:"forceSigV2"`
|
||||
Debug bool `json:"debug"`
|
||||
}
|
||||
|
||||
// SwiftPublishRoot describes single OpenStack Swift publishing entry point
|
||||
type SwiftPublishRoot struct {
|
||||
UserName string `json:"osname"`
|
||||
Password string `json:"password"`
|
||||
AuthURL string `json:"authurl"`
|
||||
Tenant string `json:"tenant"`
|
||||
TenantID string `json:"tenantid"`
|
||||
Domain string `json:"domain"`
|
||||
DomainID string `json:"domainid"`
|
||||
TenantDomain string `json:"tenantdomain"`
|
||||
TenantDomainID string `json:"tenantdomainid"`
|
||||
Prefix string `json:"prefix"`
|
||||
Container string `json:"container"`
|
||||
}
|
||||
|
||||
// Config is configuration for aptly, shared by all modules
|
||||
var Config = ConfigStructure{
|
||||
RootDir: filepath.Join(os.Getenv("HOME"), ".aptly"),
|
||||
DownloadConcurrency: 4,
|
||||
DownloadLimit: 0,
|
||||
Architectures: []string{},
|
||||
DepFollowSuggests: false,
|
||||
DepFollowRecommends: false,
|
||||
DepFollowAllVariants: false,
|
||||
DepFollowSource: false,
|
||||
GpgDisableSign: false,
|
||||
GpgDisableVerify: false,
|
||||
DownloadSourcePackages: false,
|
||||
PpaDistributorID: "ubuntu",
|
||||
PpaCodename: "",
|
||||
S3PublishRoots: map[string]S3PublishRoot{},
|
||||
SwiftPublishRoots: map[string]SwiftPublishRoot{},
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from json file
|
||||
func LoadConfig(filename string, config *ConfigStructure) error {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
dec := json.NewDecoder(f)
|
||||
return dec.Decode(&config)
|
||||
}
|
||||
|
||||
// SaveConfig write configuration to json file
|
||||
func SaveConfig(filename string, config *ConfigStructure) error {
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
encoded, err := json.MarshalIndent(&config, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.Write(encoded)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type ConfigSuite struct {
|
||||
config ConfigStructure
|
||||
}
|
||||
|
||||
var _ = Suite(&ConfigSuite{})
|
||||
|
||||
func (s *ConfigSuite) TestLoadConfig(c *C) {
|
||||
configname := filepath.Join(c.MkDir(), "aptly.json")
|
||||
f, _ := os.Create(configname)
|
||||
f.WriteString(configFile)
|
||||
f.Close()
|
||||
|
||||
err := LoadConfig(configname, &s.config)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(s.config.RootDir, Equals, "/opt/aptly/")
|
||||
c.Check(s.config.DownloadConcurrency, Equals, 33)
|
||||
}
|
||||
|
||||
func (s *ConfigSuite) TestSaveConfig(c *C) {
|
||||
configname := filepath.Join(c.MkDir(), "aptly.json")
|
||||
|
||||
s.config.RootDir = "/tmp/aptly"
|
||||
s.config.DownloadConcurrency = 5
|
||||
s.config.S3PublishRoots = map[string]S3PublishRoot{"test": {
|
||||
Region: "us-east-1",
|
||||
Bucket: "repo"}}
|
||||
|
||||
s.config.SwiftPublishRoots = map[string]SwiftPublishRoot{"test": {
|
||||
Container: "repo"}}
|
||||
|
||||
err := SaveConfig(configname, &s.config)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
f, _ := os.Open(configname)
|
||||
defer f.Close()
|
||||
|
||||
st, _ := f.Stat()
|
||||
buf := make([]byte, st.Size())
|
||||
f.Read(buf)
|
||||
|
||||
c.Check(string(buf), Equals, ""+
|
||||
"{\n"+
|
||||
" \"rootDir\": \"/tmp/aptly\",\n"+
|
||||
" \"downloadConcurrency\": 5,\n"+
|
||||
" \"downloadSpeedLimit\": 0,\n"+
|
||||
" \"architectures\": null,\n"+
|
||||
" \"dependencyFollowSuggests\": false,\n"+
|
||||
" \"dependencyFollowRecommends\": false,\n"+
|
||||
" \"dependencyFollowAllVariants\": false,\n"+
|
||||
" \"dependencyFollowSource\": false,\n"+
|
||||
" \"gpgDisableSign\": false,\n"+
|
||||
" \"gpgDisableVerify\": false,\n"+
|
||||
" \"downloadSourcePackages\": false,\n"+
|
||||
" \"ppaDistributorID\": \"\",\n"+
|
||||
" \"ppaCodename\": \"\",\n"+
|
||||
" \"skipContentsPublishing\": false,\n"+
|
||||
" \"S3PublishEndpoints\": {\n"+
|
||||
" \"test\": {\n"+
|
||||
" \"region\": \"us-east-1\",\n"+
|
||||
" \"bucket\": \"repo\",\n"+
|
||||
" \"endpoint\": \"\",\n"+
|
||||
" \"awsAccessKeyID\": \"\",\n"+
|
||||
" \"awsSecretAccessKey\": \"\",\n"+
|
||||
" \"awsSessionToken\": \"\",\n"+
|
||||
" \"prefix\": \"\",\n"+
|
||||
" \"acl\": \"\",\n"+
|
||||
" \"storageClass\": \"\",\n"+
|
||||
" \"encryptionMethod\": \"\",\n"+
|
||||
" \"plusWorkaround\": false,\n"+
|
||||
" \"disableMultiDel\": false,\n"+
|
||||
" \"forceSigV2\": false,\n"+
|
||||
" \"debug\": false\n"+
|
||||
" }\n"+
|
||||
" },\n"+
|
||||
" \"SwiftPublishEndpoints\": {\n"+
|
||||
" \"test\": {\n"+
|
||||
" \"osname\": \"\",\n"+
|
||||
" \"password\": \"\",\n"+
|
||||
" \"authurl\": \"\",\n"+
|
||||
" \"tenant\": \"\",\n"+
|
||||
" \"tenantid\": \"\",\n"+
|
||||
" \"domain\": \"\",\n"+
|
||||
" \"domainid\": \"\",\n"+
|
||||
" \"tenantdomain\": \"\",\n"+
|
||||
" \"tenantdomainid\": \"\",\n"+
|
||||
" \"prefix\": \"\",\n"+
|
||||
" \"container\": \"repo\"\n"+
|
||||
" }\n"+
|
||||
" }\n"+
|
||||
"}")
|
||||
}
|
||||
|
||||
const configFile = `{"rootDir": "/opt/aptly/", "downloadConcurrency": 33}`
|
||||
@@ -0,0 +1,28 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// CopyFile copeis file from src to dst, not preserving attributes
|
||||
func CopyFile(src, dst string) error {
|
||||
sf, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sf.Close()
|
||||
|
||||
df, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(df, sf)
|
||||
if err != nil {
|
||||
df.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return df.Close()
|
||||
}
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Signer interface describes facility implementing signing of files
|
||||
type Signer interface {
|
||||
Init() error
|
||||
SetKey(keyRef string)
|
||||
SetKeyRing(keyring, secretKeyring string)
|
||||
SetPassphrase(passphrase, passphraseFile string)
|
||||
SetBatch(batch bool)
|
||||
DetachedSign(source string, destination string) error
|
||||
ClearSign(source string, destination string) error
|
||||
}
|
||||
|
||||
// Verifier interface describes signature verification factility
|
||||
type Verifier interface {
|
||||
InitKeyring() error
|
||||
AddKeyring(keyring string)
|
||||
VerifyDetachedSignature(signature, cleartext io.Reader) error
|
||||
IsClearSigned(clearsigned io.Reader) (bool, error)
|
||||
VerifyClearsigned(clearsigned io.Reader, showKeyTip bool) (*GpgKeyInfo, error)
|
||||
ExtractClearsigned(clearsigned io.Reader) (text *os.File, err error)
|
||||
}
|
||||
|
||||
// Test interface
|
||||
var (
|
||||
_ Signer = &GpgSigner{}
|
||||
_ Verifier = &GpgVerifier{}
|
||||
)
|
||||
|
||||
// GpgKey is key in GPG representation
|
||||
type GpgKey string
|
||||
|
||||
// Matches checks two keys for equality
|
||||
func (key1 GpgKey) Matches(key2 GpgKey) bool {
|
||||
if key1 == key2 {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(key1) == 8 && len(key2) == 16 {
|
||||
return key1 == key2[8:]
|
||||
}
|
||||
|
||||
if len(key1) == 16 && len(key2) == 8 {
|
||||
return key1[8:] == key2
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GpgKeyInfo is response from signature verification
|
||||
type GpgKeyInfo struct {
|
||||
GoodKeys []GpgKey
|
||||
MissingKeys []GpgKey
|
||||
}
|
||||
|
||||
// GpgSigner is implementation of Signer interface using gpg
|
||||
type GpgSigner struct {
|
||||
keyRef string
|
||||
keyring, secretKeyring string
|
||||
passphrase, passphraseFile string
|
||||
batch bool
|
||||
}
|
||||
|
||||
// SetBatch control --no-tty flag to gpg
|
||||
func (g *GpgSigner) SetBatch(batch bool) {
|
||||
g.batch = batch
|
||||
}
|
||||
|
||||
// SetKey sets key ID to use when signing files
|
||||
func (g *GpgSigner) SetKey(keyRef string) {
|
||||
g.keyRef = keyRef
|
||||
}
|
||||
|
||||
// SetKeyRing allows to set custom keyring and secretkeyring
|
||||
func (g *GpgSigner) SetKeyRing(keyring, secretKeyring string) {
|
||||
g.keyring, g.secretKeyring = keyring, secretKeyring
|
||||
}
|
||||
|
||||
// SetPassphrase sets passhprase params
|
||||
func (g *GpgSigner) SetPassphrase(passphrase, passphraseFile string) {
|
||||
g.passphrase, g.passphraseFile = passphrase, passphraseFile
|
||||
}
|
||||
|
||||
func (g *GpgSigner) gpgArgs() []string {
|
||||
args := []string{}
|
||||
if g.keyring != "" {
|
||||
args = append(args, "--no-auto-check-trustdb", "--no-default-keyring", "--keyring", g.keyring)
|
||||
}
|
||||
if g.secretKeyring != "" {
|
||||
args = append(args, "--secret-keyring", g.secretKeyring)
|
||||
}
|
||||
|
||||
if g.keyRef != "" {
|
||||
args = append(args, "-u", g.keyRef)
|
||||
}
|
||||
|
||||
if g.passphrase != "" || g.passphraseFile != "" {
|
||||
args = append(args, "--no-use-agent")
|
||||
}
|
||||
|
||||
if g.passphrase != "" {
|
||||
args = append(args, "--passphrase", g.passphrase)
|
||||
}
|
||||
|
||||
if g.passphraseFile != "" {
|
||||
args = append(args, "--passphrase-file", g.passphraseFile)
|
||||
}
|
||||
|
||||
if g.batch {
|
||||
args = append(args, "--no-tty")
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// Init verifies availability of gpg & presence of keys
|
||||
func (g *GpgSigner) Init() error {
|
||||
output, err := exec.Command("gpg", "--list-keys", "--dry-run", "--no-auto-check-trustdb").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to execute gpg: %s (is gpg installed?): %s", err, string(output))
|
||||
}
|
||||
|
||||
if g.keyring == "" && g.secretKeyring == "" && len(output) == 0 {
|
||||
return fmt.Errorf("looks like there are no keys in gpg, please create one (official manual: http://www.gnupg.org/gph/en/manual.html)")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DetachedSign signs file with detached signature in ASCII format
|
||||
func (g *GpgSigner) DetachedSign(source string, destination string) error {
|
||||
fmt.Printf("Signing file '%s' with gpg, please enter your passphrase when prompted:\n", filepath.Base(source))
|
||||
|
||||
args := []string{"-o", destination, "--digest-algo", "SHA256", "--armor", "--yes"}
|
||||
args = append(args, g.gpgArgs()...)
|
||||
args = append(args, "--detach-sign", source)
|
||||
cmd := exec.Command("gpg", args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// ClearSign clear-signs the file
|
||||
func (g *GpgSigner) ClearSign(source string, destination string) error {
|
||||
fmt.Printf("Clearsigning file '%s' with gpg, please enter your passphrase when prompted:\n", filepath.Base(source))
|
||||
args := []string{"-o", destination, "--digest-algo", "SHA256", "--yes"}
|
||||
args = append(args, g.gpgArgs()...)
|
||||
args = append(args, "--clearsign", source)
|
||||
cmd := exec.Command("gpg", args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// GpgVerifier is implementation of Verifier interface using gpgv
|
||||
type GpgVerifier struct {
|
||||
keyRings []string
|
||||
}
|
||||
|
||||
// InitKeyring verifies that gpg is installed and some keys are trusted
|
||||
func (g *GpgVerifier) InitKeyring() error {
|
||||
err := exec.Command("gpgv", "--version").Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to execute gpgv: %s (is gpg installed?)", err)
|
||||
}
|
||||
|
||||
if len(g.keyRings) == 0 {
|
||||
// using default keyring
|
||||
output, err := exec.Command("gpg", "--no-default-keyring", "--no-auto-check-trustdb", "--keyring", "trustedkeys.gpg", "--list-keys").Output()
|
||||
if err == nil && len(output) == 0 {
|
||||
fmt.Printf("\nLooks like your keyring with trusted keys is empty. You might consider importing some keys.\n")
|
||||
fmt.Printf("If you're running Debian or Ubuntu, it's a good idea to import current archive keys by running:\n\n")
|
||||
fmt.Printf(" gpg --no-default-keyring --keyring /usr/share/keyrings/debian-archive-keyring.gpg --export | gpg --no-default-keyring --keyring trustedkeys.gpg --import\n")
|
||||
fmt.Printf("\n(for Ubuntu, use /usr/share/keyrings/ubuntu-archive-keyring.gpg)\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddKeyring adds custom keyring to GPG parameters
|
||||
func (g *GpgVerifier) AddKeyring(keyring string) {
|
||||
g.keyRings = append(g.keyRings, keyring)
|
||||
}
|
||||
|
||||
func (g *GpgVerifier) argsKeyrings() (args []string) {
|
||||
if len(g.keyRings) > 0 {
|
||||
args = make([]string, 0, 2*len(g.keyRings))
|
||||
for _, keyring := range g.keyRings {
|
||||
args = append(args, "--keyring", keyring)
|
||||
}
|
||||
} else {
|
||||
args = []string{"--keyring", "trustedkeys.gpg"}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (g *GpgVerifier) runGpgv(args []string, context string, showKeyTip bool) (*GpgKeyInfo, error) {
|
||||
args = append([]string{"--status-fd", "3"}, args...)
|
||||
cmd := exec.Command("gpgv", args...)
|
||||
|
||||
tempf, err := ioutil.TempFile("", "aptly-gpg-status")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tempf.Close()
|
||||
|
||||
err = os.Remove(tempf.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd.ExtraFiles = []*os.File{tempf}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stderr.Close()
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
_, err = io.Copy(io.MultiWriter(os.Stderr, buffer), stderr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmderr := cmd.Wait()
|
||||
|
||||
tempf.Seek(0, 0)
|
||||
|
||||
statusr := bufio.NewScanner(tempf)
|
||||
|
||||
result := &GpgKeyInfo{}
|
||||
|
||||
for statusr.Scan() {
|
||||
line := strings.TrimSpace(statusr.Text())
|
||||
|
||||
if strings.HasPrefix(line, "[GNUPG:] GOODSIG ") {
|
||||
result.GoodKeys = append(result.GoodKeys, GpgKey(strings.Fields(line)[2]))
|
||||
} else if strings.HasPrefix(line, "[GNUPG:] NO_PUBKEY ") {
|
||||
result.MissingKeys = append(result.MissingKeys, GpgKey(strings.Fields(line)[2]))
|
||||
}
|
||||
}
|
||||
|
||||
if err = statusr.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cmderr != nil {
|
||||
if showKeyTip && len(g.keyRings) == 0 && len(result.MissingKeys) > 0 {
|
||||
fmt.Printf("\nLooks like some keys are missing in your trusted keyring, you may consider importing them from keyserver:\n\n")
|
||||
|
||||
keys := make([]string, len(result.MissingKeys))
|
||||
|
||||
for i := range result.MissingKeys {
|
||||
keys[i] = string(result.MissingKeys[i])
|
||||
}
|
||||
|
||||
fmt.Printf("gpg --no-default-keyring --keyring trustedkeys.gpg --keyserver keys.gnupg.net --recv-keys %s\n\n",
|
||||
strings.Join(keys, " "))
|
||||
|
||||
fmt.Printf("Sometimes keys are stored in repository root in file named Release.key, to import such key:\n\n")
|
||||
fmt.Printf("wget -O - https://some.repo/repository/Release.key | gpg --no-default-keyring --keyring trustedkeys.gpg --import\n\n")
|
||||
}
|
||||
return result, fmt.Errorf("verification of %s failed: %s", context, cmderr)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// VerifyDetachedSignature verifies combination of signature and cleartext using gpgv
|
||||
func (g *GpgVerifier) VerifyDetachedSignature(signature, cleartext io.Reader) error {
|
||||
args := g.argsKeyrings()
|
||||
|
||||
sigf, err := ioutil.TempFile("", "aptly-gpg")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(sigf.Name())
|
||||
defer sigf.Close()
|
||||
|
||||
_, err = io.Copy(sigf, signature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clearf, err := ioutil.TempFile("", "aptly-gpg")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(clearf.Name())
|
||||
defer clearf.Close()
|
||||
|
||||
_, err = io.Copy(clearf, cleartext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args = append(args, sigf.Name(), clearf.Name())
|
||||
_, err = g.runGpgv(args, "detached signature", true)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsClearSigned returns true if file contains signature
|
||||
func (g *GpgVerifier) IsClearSigned(clearsigned io.Reader) (bool, error) {
|
||||
scanner := bufio.NewScanner(clearsigned)
|
||||
for scanner.Scan() {
|
||||
if strings.Index(scanner.Text(), "BEGIN PGP SIGN") != -1 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// VerifyClearsigned verifies clearsigned file using gpgv
|
||||
func (g *GpgVerifier) VerifyClearsigned(clearsigned io.Reader, showKeyTip bool) (*GpgKeyInfo, error) {
|
||||
args := g.argsKeyrings()
|
||||
|
||||
clearf, err := ioutil.TempFile("", "aptly-gpg")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(clearf.Name())
|
||||
defer clearf.Close()
|
||||
|
||||
_, err = io.Copy(clearf, clearsigned)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args = append(args, clearf.Name())
|
||||
return g.runGpgv(args, "clearsigned file", showKeyTip)
|
||||
}
|
||||
|
||||
// ExtractClearsigned extracts cleartext from clearsigned file WITHOUT signature verification
|
||||
func (g *GpgVerifier) ExtractClearsigned(clearsigned io.Reader) (text *os.File, err error) {
|
||||
clearf, err := ioutil.TempFile("", "aptly-gpg")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer os.Remove(clearf.Name())
|
||||
defer clearf.Close()
|
||||
|
||||
_, err = io.Copy(clearf, clearsigned)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
text, err = ioutil.TempFile("", "aptly-gpg")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer os.Remove(text.Name())
|
||||
|
||||
args := []string{"--no-auto-check-trustdb", "--decrypt", "--batch", "--skip-verify", "--output", "-", clearf.Name()}
|
||||
|
||||
cmd := exec.Command("gpg", args...)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stdout.Close()
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = io.Copy(text, stdout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("extraction of clearsigned file failed: %s", err)
|
||||
}
|
||||
|
||||
_, err = text.Seek(0, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type GpgSuite struct{}
|
||||
|
||||
var _ = Suite(&GpgSuite{})
|
||||
|
||||
func (s *GpgSuite) TestGpgKeyMatch(c *C) {
|
||||
c.Check(GpgKey("EC4B033C70096AD1").Matches(GpgKey("EC4B033C70096AD1")), Equals, true)
|
||||
c.Check(GpgKey("37E1C17570096AD1").Matches(GpgKey("EC4B033C70096AD1")), Equals, false)
|
||||
|
||||
c.Check(GpgKey("70096AD1").Matches(GpgKey("70096AD1")), Equals, true)
|
||||
c.Check(GpgKey("70096AD1").Matches(GpgKey("EC4B033C")), Equals, false)
|
||||
|
||||
c.Check(GpgKey("37E1C17570096AD1").Matches(GpgKey("70096AD1")), Equals, true)
|
||||
c.Check(GpgKey("70096AD1").Matches(GpgKey("EC4B033C70096AD1")), Equals, true)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// HumanBytes converts bytes to human readable string
|
||||
func HumanBytes(i int64) (result string) {
|
||||
switch {
|
||||
case i > (512 * 1024 * 1024 * 1024):
|
||||
result = fmt.Sprintf("%.02f TiB", float64(i)/1024/1024/1024/1024)
|
||||
case i > (512 * 1024 * 1024):
|
||||
result = fmt.Sprintf("%.02f GiB", float64(i)/1024/1024/1024)
|
||||
case i > (512 * 1024):
|
||||
result = fmt.Sprintf("%.02f MiB", float64(i)/1024/1024)
|
||||
case i > 512:
|
||||
result = fmt.Sprintf("%.02f KiB", float64(i)/1024)
|
||||
default:
|
||||
result = fmt.Sprintf("%d B", i)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type HumanSuite struct{}
|
||||
|
||||
var _ = Suite(&HumanSuite{})
|
||||
|
||||
func (s *HumanSuite) TestHumanBytes(c *C) {
|
||||
c.Check(HumanBytes(50), Equals, "50 B")
|
||||
c.Check(HumanBytes(968), Equals, "0.95 KiB")
|
||||
c.Check(HumanBytes(20480), Equals, "20.00 KiB")
|
||||
c.Check(HumanBytes(700480), Equals, "0.67 MiB")
|
||||
c.Check(HumanBytes(7000480), Equals, "6.68 MiB")
|
||||
c.Check(HumanBytes(824000480), Equals, "0.77 GiB")
|
||||
c.Check(HumanBytes(82400000480), Equals, "76.74 GiB")
|
||||
c.Check(HumanBytes(824000000480), Equals, "0.75 TiB")
|
||||
c.Check(HumanBytes(824000000000480), Equals, "749.42 TiB")
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// StringsIsSubset checks that subset is strict subset of full, and returns
|
||||
// error formatted with errorFmt otherwise
|
||||
func StringsIsSubset(subset, full []string, errorFmt string) error {
|
||||
for _, checked := range subset {
|
||||
found := false
|
||||
for _, s := range full {
|
||||
if checked == s {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf(errorFmt, checked)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StrSlicesEqual compares two slices for equality
|
||||
func StrSlicesEqual(s1, s2 []string) bool {
|
||||
if len(s1) != len(s2) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, s := range s1 {
|
||||
if s != s2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// StrMapsEqual compares two map[string]string
|
||||
func StrMapsEqual(m1, m2 map[string]string) bool {
|
||||
if len(m1) != len(m2) {
|
||||
return false
|
||||
}
|
||||
|
||||
for k, v := range m1 {
|
||||
v2, ok := m2[k]
|
||||
if !ok || v != v2 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// StrSliceHasItem checks item for presence in slice
|
||||
func StrSliceHasItem(s []string, item string) bool {
|
||||
for _, v := range s {
|
||||
if v == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StrMapSortedKeys returns keys of map[string]string sorted
|
||||
func StrMapSortedKeys(m map[string]string) []string {
|
||||
keys := make([]string, len(m))
|
||||
i := 0
|
||||
for k := range m {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// StrSliceDeduplicate removes dups in slice
|
||||
func StrSliceDeduplicate(s []string) []string {
|
||||
l := len(s)
|
||||
if l < 2 {
|
||||
return s
|
||||
}
|
||||
if l == 2 {
|
||||
if s[0] == s[1] {
|
||||
return s[0:1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
found := make(map[string]bool, l)
|
||||
j := 0
|
||||
for i, x := range s {
|
||||
if !found[x] {
|
||||
found[x] = true
|
||||
s[j] = s[i]
|
||||
j++
|
||||
}
|
||||
}
|
||||
|
||||
return s[:j]
|
||||
}
|
||||
|
||||
// StrSlicesSubstract finds all the strings which are in l but not in r, both slices shoult be sorted
|
||||
func StrSlicesSubstract(l, r []string) []string {
|
||||
var result []string
|
||||
|
||||
// pointer to left and right reflists
|
||||
il, ir := 0, 0
|
||||
// length of reflists
|
||||
ll, lr := len(l), len(r)
|
||||
|
||||
for il < ll || ir < lr {
|
||||
if il == ll {
|
||||
// left list exhausted, we got the result
|
||||
break
|
||||
}
|
||||
if ir == lr {
|
||||
// right list exhausted, append what is left to result
|
||||
result = append(result, l[il:]...)
|
||||
break
|
||||
}
|
||||
|
||||
if l[il] == r[ir] {
|
||||
// r contains entry from l, so we skip it
|
||||
il++
|
||||
ir++
|
||||
} else if l[il] < r[ir] {
|
||||
// item il is not in r, append
|
||||
result = append(result, l[il])
|
||||
il++
|
||||
} else {
|
||||
// skip over to next item in r
|
||||
ir++
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type ListSuite struct {
|
||||
}
|
||||
|
||||
var _ = Suite(&ListSuite{})
|
||||
|
||||
func (s *ListSuite) TestStringsIsSubset(c *C) {
|
||||
err := StringsIsSubset([]string{"a", "b"}, []string{"a", "b", "c"}, "[%s]")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = StringsIsSubset([]string{"b", "a"}, []string{"b", "c"}, "[%s]")
|
||||
c.Assert(err, ErrorMatches, "\\[a\\]")
|
||||
}
|
||||
|
||||
func (s *ListSuite) TestStrSlicesEqual(c *C) {
|
||||
c.Check(StrSlicesEqual(nil, nil), Equals, true)
|
||||
c.Check(StrSlicesEqual(nil, []string{}), Equals, true)
|
||||
c.Check(StrSlicesEqual([]string{}, nil), Equals, true)
|
||||
c.Check(StrSlicesEqual([]string{"a", "b"}, []string{"a", "b"}), Equals, true)
|
||||
|
||||
c.Check(StrSlicesEqual(nil, []string{"a"}), Equals, false)
|
||||
c.Check(StrSlicesEqual([]string{"a", "c"}, []string{"a", "b"}), Equals, false)
|
||||
}
|
||||
|
||||
func (s *ListSuite) TestStrMapsEqual(c *C) {
|
||||
c.Check(StrMapsEqual(map[string]string{}, nil), Equals, true)
|
||||
c.Check(StrMapsEqual(nil, map[string]string{}), Equals, true)
|
||||
c.Check(StrMapsEqual(nil, nil), Equals, true)
|
||||
c.Check(StrMapsEqual(map[string]string{"a": "1", "b": "2"}, map[string]string{"a": "1", "b": "2"}), Equals, true)
|
||||
|
||||
c.Check(StrMapsEqual(map[string]string{"a": "1", "b": "2"}, map[string]string{"a": "1", "b": "3"}), Equals, false)
|
||||
c.Check(StrMapsEqual(map[string]string{"a": "1", "b": "2"}, map[string]string{"a": "1", "c": "2"}), Equals, false)
|
||||
c.Check(StrMapsEqual(map[string]string{"a": "1", "b": "2"}, map[string]string{"a": "1"}), Equals, false)
|
||||
}
|
||||
|
||||
func (s *ListSuite) TestStrSliceHasIteml(c *C) {
|
||||
c.Check(StrSliceHasItem([]string{"a", "b"}, "b"), Equals, true)
|
||||
c.Check(StrSliceHasItem([]string{"a", "b"}, "c"), Equals, false)
|
||||
}
|
||||
|
||||
func (s *ListSuite) TestStrMapSortedKeys(c *C) {
|
||||
c.Check(StrMapSortedKeys(map[string]string{}), DeepEquals, []string{})
|
||||
c.Check(StrMapSortedKeys(map[string]string{"x": "1", "a": "3", "y": "4"}), DeepEquals, []string{"a", "x", "y"})
|
||||
}
|
||||
|
||||
func (s *ListSuite) TestStrSliceDeduplicate(c *C) {
|
||||
c.Check(StrSliceDeduplicate([]string{}), DeepEquals, []string{})
|
||||
c.Check(StrSliceDeduplicate([]string{"a"}), DeepEquals, []string{"a"})
|
||||
c.Check(StrSliceDeduplicate([]string{"a", "b"}), DeepEquals, []string{"a", "b"})
|
||||
c.Check(StrSliceDeduplicate([]string{"a", "a"}), DeepEquals, []string{"a"})
|
||||
c.Check(StrSliceDeduplicate([]string{"a", "b", "c", "a", "a", "b"}), DeepEquals, []string{"a", "b", "c"})
|
||||
c.Check(StrSliceDeduplicate([]string{"a", "b", "c", "d", "e", "f"}), DeepEquals, []string{"a", "b", "c", "d", "e", "f"})
|
||||
}
|
||||
|
||||
func (s *ListSuite) TestStrSlicesSubstract(c *C) {
|
||||
empty := []string(nil)
|
||||
l1 := []string{"r1", "r2", "r3", "r4"}
|
||||
l2 := []string{"r1", "r3"}
|
||||
l3 := []string{"r2", "r4"}
|
||||
l4 := []string{"r4", "r5"}
|
||||
l5 := []string{"r1", "r2", "r3"}
|
||||
|
||||
c.Check(StrSlicesSubstract(l1, empty), DeepEquals, l1)
|
||||
c.Check(StrSlicesSubstract(l1, l2), DeepEquals, l3)
|
||||
c.Check(StrSlicesSubstract(l1, l3), DeepEquals, l2)
|
||||
c.Check(StrSlicesSubstract(l1, l4), DeepEquals, l5)
|
||||
c.Check(StrSlicesSubstract(empty, l1), DeepEquals, empty)
|
||||
c.Check(StrSlicesSubstract(l2, l3), DeepEquals, l2)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Package utils collects various services: simple operations, compression, etc.
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// DirIsAccessible verifies that directory exists and is accessible
|
||||
func DirIsAccessible(filename string) error {
|
||||
_, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("Something went wrong, %v", err)
|
||||
}
|
||||
} else {
|
||||
if unix.Access(filename, unix.W_OK) != nil {
|
||||
return fmt.Errorf("'%s' is inaccessible, check access rights", filename)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Launch gocheck tests
|
||||
func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
Reference in New Issue
Block a user