diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6dd29b7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +bin/ \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..3a68a9c --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,33 @@ +name: ci + +on: + push: + tags: + - '*' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + platforms: + - linux/amd64 + - linux/arm64 + push: true + tags: + - okhalid/kasa-exporter:latest + - okhalid/kasa-exporter:${{ github.ref_name }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9e10b53 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:1.21 as build + +WORKDIR /go/src/github.com/dehydr8/kasa-go +COPY . . +RUN CGO_ENABLED=0 go build -o /kasa-exporter + +FROM alpine +COPY --from=build /kasa-exporter /kasa-exporter +ENTRYPOINT ["/kasa-exporter"] \ No newline at end of file diff --git a/Makefile b/Makefile index c159f81..3d6c910 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ build-cli: - go build -o bin/kasa-exporter main.go + go build -o bin/kasa-exporter build-cli-arm: - GOOS=linux GOARCH=arm64 go build -o bin/kasa-exporter-arm main.go + GOOS=linux GOARCH=arm64 go build -o bin/kasa-exporter-arm run: go run main.go \ No newline at end of file diff --git a/go.mod b/go.mod index ff9e289..48ecd35 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21.0 require ( github.com/go-kit/log v0.2.1 github.com/hashicorp/golang-lru/v2 v2.0.7 + github.com/peterbourgon/ff/v4 v4.0.0-alpha.4 github.com/prometheus/client_golang v1.18.0 ) diff --git a/go.sum b/go.sum index 2829ccf..4b14826 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,10 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/peterbourgon/ff/v4 v4.0.0-alpha.4 h1:aiqS8aBlF9PsAKeMddMSfbwp3smONCn3UO8QfUg0Z7Y= +github.com/peterbourgon/ff/v4 v4.0.0-alpha.4/go.mod h1:H/13DK46DKXy7EaIxPhk2Y0EC8aubKm35nBjBe8AAGc= github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= @@ -30,3 +34,5 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/main.go b/main.go index 38e366d..603889a 100644 --- a/main.go +++ b/main.go @@ -3,14 +3,17 @@ package main import ( "crypto/rand" "crypto/rsa" - "flag" "fmt" "net/http" + "os" "github.com/dehydr8/kasa-go/device" "github.com/dehydr8/kasa-go/exporter" "github.com/dehydr8/kasa-go/logger" "github.com/dehydr8/kasa-go/model" + "github.com/dehydr8/kasa-go/util" + "github.com/peterbourgon/ff/v4" + "github.com/peterbourgon/ff/v4/ffhelp" lru "github.com/hashicorp/golang-lru/v2" @@ -24,8 +27,8 @@ type MetricsServer struct { registryCache *lru.Cache[string, *prometheus.Registry] } -func NewMetricsServer(key *rsa.PrivateKey, credentials *model.Credentials) *MetricsServer { - cache, err := lru.New[string, *prometheus.Registry](10) +func NewMetricsServer(key *rsa.PrivateKey, credentials *model.Credentials, cacheMax int) *MetricsServer { + cache, err := lru.New[string, *prometheus.Registry](cacheMax) if err != nil { panic(err) @@ -55,23 +58,46 @@ func (s *MetricsServer) getOrCreate(key string, create func() (*prometheus.Regis } func main() { - lvl := flag.String("log", "info", "debug, info, warn, error") - address := flag.String("address", "", "address to listen on") - port := flag.Int("port", 9500, "port to listen on") - username := flag.String("username", "", "username for kasa login") - password := flag.String("password", "", "password for kasa login") + fs := ff.NewFlagSet(fmt.Sprintf("kasa-exporter (rev %s)", util.Revision)) + + var ( + lvl = fs.StringEnum('l', "log", "log level: debug, info, warn, error", "info", "debug", "warn", "error") + address = fs.String('a', "address", "", "address to listen on") + port = fs.Int('p', "port", 9500, "port to listen on") + username = fs.StringLong("username", "", "username for kasa login") + password = fs.StringLong("password", "", "password for kasa login") + hashedPassword = fs.StringLong("hashed_password", "", "hashed (sha1) password for kasa login") + maxRegistries = fs.IntLong("max_registries", 16, "maximum number of registries to cache") + ) + + if err := ff.Parse(fs, os.Args[1:], + ff.WithEnvVarPrefix("KASA_EXPORTER"), + ff.WithConfigFileFlag("config"), + ff.WithConfigFileParser(ff.PlainParser), + ); err != nil { + fmt.Printf("%s\n", ffhelp.Flags(fs)) + fmt.Printf("err=%v\n", err) + os.Exit(1) + } - flag.Parse() + if *username == "" { + fmt.Printf("%s\n", ffhelp.Flags(fs)) + fmt.Printf("err=%v\n", "username must be specified") + os.Exit(1) + } - if *username == "" || *password == "" { - panic("username and password must be specified") + if *password == "" && *hashedPassword == "" { + fmt.Printf("%s\n", ffhelp.Flags(fs)) + fmt.Printf("err=%v\n", "password or hashed_password must be specified") + os.Exit(1) } logger.SetupLogging(*lvl) credentials := model.Credentials{ - Username: *username, - Password: *password, + Username: *username, + Password: *password, + HashedPassword: *hashedPassword, } logger.Debug("msg", "Generating RSA key") @@ -82,7 +108,7 @@ func main() { panic(err) } - server := NewMetricsServer(key, &credentials) + server := NewMetricsServer(key, &credentials, *maxRegistries) http.HandleFunc("/scrape", server.ScrapeHandler) diff --git a/model/device.go b/model/device.go index c18936c..f1a3e3b 100644 --- a/model/device.go +++ b/model/device.go @@ -3,15 +3,15 @@ package model import "crypto/rsa" type Credentials struct { - Username string - Password string + Username string + Password string + HashedPassword string } type DeviceConfig struct { Address string - Credentials *Credentials - CredentialsHash *string + Credentials *Credentials Key *rsa.PrivateKey } diff --git a/protocol/aes.go b/protocol/aes.go index 5fc8e0b..257350c 100644 --- a/protocol/aes.go +++ b/protocol/aes.go @@ -388,7 +388,12 @@ func (t *AesTransport) hashCredentials(v2 bool) (string, string) { var pass string if v2 { - pass = base64.StdEncoding.EncodeToString([]byte(sha1Hash([]byte(t.config.Credentials.Password)))) + // if we already have the hashed password, use it + if t.config.Credentials.HashedPassword != "" { + pass = base64.StdEncoding.EncodeToString([]byte(t.config.Credentials.HashedPassword)) + } else { + pass = base64.StdEncoding.EncodeToString([]byte(sha1Hash([]byte(t.config.Credentials.Password)))) + } } else { pass = base64.StdEncoding.EncodeToString([]byte(t.config.Credentials.Password)) } diff --git a/util/commit.go b/util/commit.go index 4b2dbd9..5a91080 100644 --- a/util/commit.go +++ b/util/commit.go @@ -1,12 +1,16 @@ package util -import "runtime/debug" +import ( + "fmt" + "runtime/debug" +) -var Commit = func() string { +var Revision = func() string { if info, ok := debug.ReadBuildInfo(); ok { + fmt.Println(info) for _, setting := range info.Settings { if setting.Key == "vcs.revision" { - return setting.Value + return setting.Value[:7] } } }