diff --git a/.gitignore b/.gitignore index ab609b270..3b016e77a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,8 @@ buildflags/* linux-*.Dockerfile node_modules release +*.inc +soc-server +ca-cert.srl +server-key.pem +ca-key.pem diff --git a/Makefile b/Makefile index a4c2e00ee..244299c54 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ .PHONY: default all clean install install-ui ui assets lint lint-ui test build test-release release .PHONY: docker publish-testing publish-latest publish-images .PHONY: prepare-image image-rootfs image-update +.PHONY: soc server server-image # build vars TAG_NAME := $(shell test -d .git && git describe --abbrev=0 --tags) @@ -98,3 +99,15 @@ image-rootfs: image-update: gokr-packer -update yes $(IMAGE_OPTIONS) + +soc: + @echo Version: $(VERSION) $(BUILD_DATE) + go build -o evcc-soc $(BUILD_TAGS) $(BUILD_ARGS) github.com/andig/evcc/soc/client + +server: + @echo Version: $(VERSION) $(BUILD_DATE) + go build -o soc-server $(BUILD_TAGS) $(BUILD_ARGS) github.com/andig/evcc/soc/server + +publish-server: + docker build -f soc/Dockerfile --platform linux/amd64 -t andig/evcc-cloud . + docker push andig/evcc-cloud diff --git a/go.mod b/go.mod index a552cffd4..ddccaf8c8 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/bogosj/tesla v0.0.0-20210326132323-e3261e5d6c70 github.com/containrrr/shoutrrr v0.4.0 github.com/denisbrodbeck/machineid v1.0.1 + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dylanmei/iso8601 v0.1.0 github.com/eclipse/paho.mqtt.golang v1.3.1 github.com/fatih/structs v1.1.0 @@ -19,7 +20,6 @@ require ( github.com/godbus/dbus/v5 v5.0.3 github.com/gokrazy/updater v0.0.0-20210106211705-4d92b338dd24 github.com/golang/mock v1.4.4 - github.com/golang/protobuf v1.4.3 // indirect github.com/google/go-github/v32 v32.1.0 github.com/google/uuid v1.1.5 github.com/gorilla/handlers v1.5.1 @@ -47,6 +47,8 @@ require ( github.com/prometheus/common v0.10.0 // indirect github.com/prometheus/procfs v0.2.0 // indirect github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac + github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa + github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect github.com/spf13/cobra v1.1.1 github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/pflag v1.0.5 @@ -54,9 +56,14 @@ require ( github.com/thoas/go-funk v0.7.0 github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c github.com/volkszaehler/mbmd v0.0.0-20210117183837-59dcc46d62d4 - golang.org/x/net v0.0.0-20201216054612-986b41b23924 + golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 + golang.org/x/net v0.0.0-20210326220855-61e056675ecf golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93 - golang.org/x/text v0.3.4 + golang.org/x/sys v0.0.0-20210326220804-49726bf1d181 // indirect + golang.org/x/text v0.3.5 + google.golang.org/genproto v0.0.0-20210325224202-eed09b1b5210 // indirect + google.golang.org/grpc v1.36.1 + google.golang.org/protobuf v1.26.0 gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) diff --git a/go.sum b/go.sum index 4872340c3..be89398bb 100644 --- a/go.sum +++ b/go.sum @@ -87,6 +87,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/containrrr/shoutrrr v0.4.0 h1:yy0Fbx1Z/gmfg66Xy1SGDrgmMLTcYCJQkqTM+mRb14M= github.com/containrrr/shoutrrr v0.4.0/go.mod h1:zqL2BvfC1W4FujrT4b3/ZCLxvD+uoeEpBL7rg9Dqpbg= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -108,6 +109,7 @@ github.com/deepmap/oapi-codegen v1.4.1 h1:GAsp9N5SLYhQTKo+G4iZAJYzHBySpMh3ZP7Lml github.com/deepmap/oapi-codegen v1.4.1/go.mod h1:1jY0YDxfBF3tXk1u3sARJMSUJa9wV0UrVT6o+2mr/zQ= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dylanmei/iso8601 v0.1.0 h1:812NGQDBcqquTfH5Yeo7lwR0nzx/cKdsmf3qMjPURUI= @@ -120,6 +122,7 @@ github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaB github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= @@ -209,8 +212,9 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1 h1:jAbXjIeW2ZSW2AwFxlGTDoc2CjI2XujLkV3ArsZFCvc= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -220,8 +224,9 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II= @@ -241,6 +246,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I= github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -496,6 +502,10 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa h1:jozR3igKlnYCj9IVHOVump59bp07oIRoLQ/CcjMYIUA= +github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= +github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU= +github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -619,6 +629,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -669,8 +680,10 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210326220855-61e056675ecf h1:WUcCxqQqDT0aXO4VnQbfMvp4zh7m1Gb2clVuHUAGGRE= +golang.org/x/net v0.0.0-20210326220855-61e056675ecf/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -743,16 +756,22 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY= golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210326220804-49726bf1d181 h1:64ChN/hjER/taL4YJuA+gpLfIMT+/NFherRZixbxOhg= +golang.org/x/sys v0.0.0-20210326220804-49726bf1d181/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -804,8 +823,9 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201218024724-ae774e9781d2 h1:lHDhNNs7asPT3p01mm8EP3B+bNyyVfg0bcYjhJUYgxw= golang.org/x/tools v0.0.0-20201218024724-ae774e9781d2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -863,6 +883,8 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210325224202-eed09b1b5210 h1:fFxjezD+ZiiYJ6zyfH738tgcWOqfzWl9I1GoepZzrI4= +google.golang.org/genproto v0.0.0-20210325224202-eed09b1b5210/go.mod h1:f2Bd7+2PlaVKmvKQ52aspJZXIDaRQBVdOOBfJ5i8OEs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -876,6 +898,9 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1 h1:cmUfbeGKnz9+2DD/UYsMQXeqbHZqZDs4eQwW0sFOpBY= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -885,8 +910,10 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/vehicle/cloud.go b/internal/vehicle/cloud.go new file mode 100644 index 000000000..fc26077b6 --- /dev/null +++ b/internal/vehicle/cloud.go @@ -0,0 +1,118 @@ +package vehicle + +import ( + "context" + "errors" + "os" + "time" + + "github.com/andig/evcc/api" + "github.com/andig/evcc/internal/vehicle/cloud" + "github.com/andig/evcc/provider" + "github.com/andig/evcc/soc/proto/pb" + "github.com/andig/evcc/util" + "github.com/andig/evcc/util/request" +) + +// Cloud is an api.Vehicle implementation for Cloud cars +type Cloud struct { + *embed + token string + brand string + config map[string]string + client pb.VehicleClient + vehicleID int64 + chargeStateG func() (float64, error) +} + +func init() { + registry.Add("cloud", NewCloudFromConfig) +} + +// NewCloudFromConfig creates a new vehicle +func NewCloudFromConfig(other map[string]interface{}) (api.Vehicle, error) { + cc := struct { + Token string + Title string + Capacity int64 + Brand string + Other map[string]string `mapstructure:",remain"` + Cache time.Duration + }{ + Cache: interval, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + if cc.Token == "" { + return nil, errors.New("missing required token") + } + + if host := os.Getenv("GRPC_URI"); host != "" { + cloud.Host = host + } + + log := util.NewLogger("cloud") + client, err := cloud.Client(log, cloud.Host) + + v := &Cloud{ + embed: &embed{cc.Title, cc.Capacity}, + token: cc.Token, + brand: cc.Brand, + config: cc.Other, + client: client, + } + + if err == nil { + err = v.prepareVehicle() + } + + v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter() + + return v, err +} + +// prepareVehicle obtains new vehicle handle from cloud server +func (v *Cloud) prepareVehicle() error { + req := &pb.NewRequest{ + Token: v.token, + Type: v.brand, + Config: v.config, + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*request.Timeout) + defer cancel() + + res, err := v.client.New(ctx, req) + if err == nil { + v.vehicleID = res.VehicleId + } + + return err +} + +// chargeState implements the api.Vehicle interface +func (v *Cloud) chargeState() (float64, error) { + req := &pb.SoCRequest{ + Token: v.token, + VehicleId: v.vehicleID, + } + + ctx, cancel := context.WithTimeout(context.Background(), request.Timeout) + defer cancel() + + res, err := v.client.SoC(ctx, req) + if errors.As(err, &cloud.ErrVehicleNotAvailable) && v.prepareVehicle() == nil { + req.VehicleId = v.vehicleID + res, err = v.client.SoC(ctx, req) + } + + return res.GetSoc(), err +} + +// SoC implements the api.Vehicle interface +func (v *Cloud) SoC() (float64, error) { + return v.chargeStateG() +} diff --git a/internal/vehicle/cloud/api.go b/internal/vehicle/cloud/api.go new file mode 100644 index 000000000..cbd963035 --- /dev/null +++ b/internal/vehicle/cloud/api.go @@ -0,0 +1,11 @@ +package cloud + +import "errors" + +var ( + // ErrNotAuthorized indicates request token is not authorized + ErrNotAuthorized = errors.New("not authorized") + + // ErrVehicleNotAvailable indicates vehicle not available, client should retry to prepare vehicle + ErrVehicleNotAvailable = errors.New("vehicle not available") +) diff --git a/internal/vehicle/cloud/ca-cert.pem b/internal/vehicle/cloud/ca-cert.pem new file mode 100644 index 000000000..c8bb6225f --- /dev/null +++ b/internal/vehicle/cloud/ca-cert.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE1DCCArwCCQC/TbpAPhcOQTANBgkqhkiG9w0BAQsFADAsMQswCQYDVQQGEwJE +RTEdMBsGCSqGSIb3DQEJARYOY3B1aWRsZUBnbXguZGUwHhcNMjEwMzMxMDc1ODAz +WhcNMjIwMzMxMDc1ODAzWjAsMQswCQYDVQQGEwJERTEdMBsGCSqGSIb3DQEJARYO +Y3B1aWRsZUBnbXguZGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC+ +noDTXbidh/5RFfsLlMJbfMn4Vu91oJb4MM41RQM8UOr3SRU/ZzcoLfWJe2ePpEDp +ZL4piobs8EIjFcWU2C/8La0rygbcDDqUJkW/nViH0Drs5ctgJl/uYeiDLnZiSZO+ +PBoOA0trXxqlJPxuEseJbFaLvVE81DsLBnltDPqhJZEUJPEUIjtfStOhQM0f5YoO +Greewk7P5LUkTAuTIz+xk6uKnC1YF89SKmdPMH26Rcy6q1IqhEQG3tjUliJZiKS9 +eQlnp9gskqNGop9gEsmFIMnhlrTUsVU2q38zLDFcmNDbNkRvv4Tf+7jHo4tLs02m +/fMTikdMNCRMOcJKbdobcwXq4Ghvc85xeM4/8wIQHEDFbLzmxi/4ibFfJjOfQOe9 +VpK+TbeSBTxBw42CH9V0TUOz/LELYkMrJ+zfwMCvTV4Eodmc0gjwiy9aIYdpZXKF +ueqGGljhVCq2XqkaSATFu5CsdPZrfUszLiAVqpuVTMjy3VQ9ICzz1Bd5ICBRFt6l +fDsvfB6SgmB9r1bC+yr/tqe/unzUPajL5mZwn7Jev0uOl6mBNmQpcIEne+9cKedf +vPCv/tUQpwpz6blGacEtTxuM+fWDk+CLpPnId/4PEnIQWRFE2aXQ4nrJSwu3O5+k +vuZNWety8MOSkuQr0toH//p3VNHOtT7L4JWSlDfTZQIDAQABMA0GCSqGSIb3DQEB +CwUAA4ICAQANoLbh4Di1A485ggnKfp9ykfVvg+NduGm+eqr27be033N6IJ7fuKmF +7Ki1E+sp0IJDMdyikbqoHdbroHu2chwp/1GzFxhx3Vo7kswS/ehSxWpHjhtrGjAF +tSDUFR9q4C+km2A3k1ZNyd9C1w+R9Lr16j9lBQoGWmUgFsRf8ED16FMMSK/1mCC0 +1VSydYnYhKmPZxMByTozGTV97wihA0XqXtadbkQoyvcnEuarBvEX3mPA3effqcEG +rreeDp1yzYtQRW85ZASaqgF5CYKhe9NekurZ5Jd+2mQPYRWPFpIFsSHoOLqbfcN1 +/S0Si6LZWq0Mi8cbSi/zq+Eh7Q0w+EXP6Goh0A7M4e1dKt+cfGQpeyeg3cF49Cda +RrTkSv2Sl2jbrShqAsG/HNhyBaI/gtLGi9tig9wAoV2zjGw1Ehs0GcwFq2g/f6Zv +W7rdZIBZT1RIQLczuwsEhv6cFrSM5OU2f5fuKnmeI6uvyz0jRqiMncYRjwI6LotB +sWmdTWi6xsctYirJ3Yip5Tqm01asyUuIeiT+eQ6I9CfHLiGBXEzxlK5PrFqfD7gK +LfB1gCN7SAlxKGepmFMwfF+fDrsurL2T2ePxaaNBLjRAzmpSpH0NedMd+eanuHXV +a/iOL2XmVis7iuFyk5M2XtLFfYBff+mxSPA8d9u5kCpoel0tx3iLmw== +-----END CERTIFICATE----- diff --git a/internal/vehicle/cloud/cert.go b/internal/vehicle/cloud/cert.go new file mode 100644 index 000000000..ec191d251 --- /dev/null +++ b/internal/vehicle/cloud/cert.go @@ -0,0 +1,14 @@ +package cloud + +import ( + "bytes" + _ "embed" +) + +//go:embed ca-cert.pem +var caCert []byte + +func caPEM() []byte { + copy := bytes.NewBuffer(caCert) + return copy.Bytes() +} diff --git a/internal/vehicle/cloud/client.go b/internal/vehicle/cloud/client.go new file mode 100644 index 000000000..03b6cb3f5 --- /dev/null +++ b/internal/vehicle/cloud/client.go @@ -0,0 +1,56 @@ +package cloud + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + + "github.com/andig/evcc/soc/proto/pb" + "github.com/andig/evcc/util" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +var Host = "cloud.evcc.io:8080" + +var ( + conn *grpc.ClientConn + client pb.VehicleClient +) + +func loadTLSCredentials() (*tls.Config, error) { + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(caPEM()) { + return nil, fmt.Errorf("failed to add CA certificate") + } + + // create the credentials and return it + config := &tls.Config{ + RootCAs: certPool, + } + + return config, nil +} + +func Client(log *util.Logger, uri string) (pb.VehicleClient, error) { + var err error + if conn == nil { + var tlsConfig *tls.Config + if tlsConfig, err = loadTLSCredentials(); err != nil { + return nil, fmt.Errorf("cannot load TLS credentials: %w", err) + } + + transportOption := grpc.WithInsecure() + if tlsConfig != nil { + transportOption = grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)) + } + + conn, err = grpc.Dial(uri, transportOption) + } + + if client == nil && err == nil { + client = pb.NewVehicleClient(conn) + } + + return client, err +} diff --git a/soc/Dockerfile b/soc/Dockerfile new file mode 100644 index 000000000..350f64a88 --- /dev/null +++ b/soc/Dockerfile @@ -0,0 +1,33 @@ +# STEP 1 build executable binary +FROM golang:1.16-alpine as builder + +# Install git + SSL ca certificates. +# Git is required for fetching the dependencies. +# Ca-certificates is required to call HTTPS endpoints. +RUN apk update && apk add --no-cache git ca-certificates tzdata alpine-sdk && update-ca-certificates + +WORKDIR /build + +# install go tools and cache modules +COPY Makefile . +COPY go.mod . +COPY go.sum . +RUN make install +RUN go mod download + +# build +COPY . . +RUN make server + + +# STEP 2 build a small image including module support +FROM alpine:3.13 + +ENV TZ=Europe/Berlin + +# Import from builder +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /build/soc-server /usr/local/bin/soc-server + +CMD [ "soc-server" ] diff --git a/soc/cert/ca/ca-cert.pem b/soc/cert/ca/ca-cert.pem new file mode 100644 index 000000000..c8bb6225f --- /dev/null +++ b/soc/cert/ca/ca-cert.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE1DCCArwCCQC/TbpAPhcOQTANBgkqhkiG9w0BAQsFADAsMQswCQYDVQQGEwJE +RTEdMBsGCSqGSIb3DQEJARYOY3B1aWRsZUBnbXguZGUwHhcNMjEwMzMxMDc1ODAz +WhcNMjIwMzMxMDc1ODAzWjAsMQswCQYDVQQGEwJERTEdMBsGCSqGSIb3DQEJARYO +Y3B1aWRsZUBnbXguZGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC+ +noDTXbidh/5RFfsLlMJbfMn4Vu91oJb4MM41RQM8UOr3SRU/ZzcoLfWJe2ePpEDp +ZL4piobs8EIjFcWU2C/8La0rygbcDDqUJkW/nViH0Drs5ctgJl/uYeiDLnZiSZO+ +PBoOA0trXxqlJPxuEseJbFaLvVE81DsLBnltDPqhJZEUJPEUIjtfStOhQM0f5YoO +Greewk7P5LUkTAuTIz+xk6uKnC1YF89SKmdPMH26Rcy6q1IqhEQG3tjUliJZiKS9 +eQlnp9gskqNGop9gEsmFIMnhlrTUsVU2q38zLDFcmNDbNkRvv4Tf+7jHo4tLs02m +/fMTikdMNCRMOcJKbdobcwXq4Ghvc85xeM4/8wIQHEDFbLzmxi/4ibFfJjOfQOe9 +VpK+TbeSBTxBw42CH9V0TUOz/LELYkMrJ+zfwMCvTV4Eodmc0gjwiy9aIYdpZXKF +ueqGGljhVCq2XqkaSATFu5CsdPZrfUszLiAVqpuVTMjy3VQ9ICzz1Bd5ICBRFt6l +fDsvfB6SgmB9r1bC+yr/tqe/unzUPajL5mZwn7Jev0uOl6mBNmQpcIEne+9cKedf +vPCv/tUQpwpz6blGacEtTxuM+fWDk+CLpPnId/4PEnIQWRFE2aXQ4nrJSwu3O5+k +vuZNWety8MOSkuQr0toH//p3VNHOtT7L4JWSlDfTZQIDAQABMA0GCSqGSIb3DQEB +CwUAA4ICAQANoLbh4Di1A485ggnKfp9ykfVvg+NduGm+eqr27be033N6IJ7fuKmF +7Ki1E+sp0IJDMdyikbqoHdbroHu2chwp/1GzFxhx3Vo7kswS/ehSxWpHjhtrGjAF +tSDUFR9q4C+km2A3k1ZNyd9C1w+R9Lr16j9lBQoGWmUgFsRf8ED16FMMSK/1mCC0 +1VSydYnYhKmPZxMByTozGTV97wihA0XqXtadbkQoyvcnEuarBvEX3mPA3effqcEG +rreeDp1yzYtQRW85ZASaqgF5CYKhe9NekurZ5Jd+2mQPYRWPFpIFsSHoOLqbfcN1 +/S0Si6LZWq0Mi8cbSi/zq+Eh7Q0w+EXP6Goh0A7M4e1dKt+cfGQpeyeg3cF49Cda +RrTkSv2Sl2jbrShqAsG/HNhyBaI/gtLGi9tig9wAoV2zjGw1Ehs0GcwFq2g/f6Zv +W7rdZIBZT1RIQLczuwsEhv6cFrSM5OU2f5fuKnmeI6uvyz0jRqiMncYRjwI6LotB +sWmdTWi6xsctYirJ3Yip5Tqm01asyUuIeiT+eQ6I9CfHLiGBXEzxlK5PrFqfD7gK +LfB1gCN7SAlxKGepmFMwfF+fDrsurL2T2ePxaaNBLjRAzmpSpH0NedMd+eanuHXV +a/iOL2XmVis7iuFyk5M2XtLFfYBff+mxSPA8d9u5kCpoel0tx3iLmw== +-----END CERTIFICATE----- diff --git a/soc/cert/ca/cert.go b/soc/cert/ca/cert.go new file mode 100644 index 000000000..eaa9fb94f --- /dev/null +++ b/soc/cert/ca/cert.go @@ -0,0 +1,14 @@ +package ca + +import ( + "bytes" + _ "embed" +) + +//go:embed ca-cert.pem +var cert []byte + +func PEM() []byte { + copy := bytes.NewBuffer(cert) + return copy.Bytes() +} diff --git a/soc/cert/create.sh b/soc/cert/create.sh new file mode 100755 index 000000000..28f6a7754 --- /dev/null +++ b/soc/cert/create.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +set +ex + +rm *.pem + +# 1. Generate CA's private key and self-signed certificate +openssl req -x509 -newkey rsa:4096 -days 365 -nodes -keyout ca-key.pem -out ca-cert.pem -subj "/C=DE/emailAddress=cpuidle@gmx.de" + +echo "CA's self-signed certificate" +openssl x509 -in ca-cert.pem -noout -text + +# 2. Generate web server's private key and certificate signing request (CSR) +openssl req -newkey rsa:4096 -nodes -keyout server-key.pem -out server-req.pem -subj "/C=DE/emailAddress=cpuidle@gmx.de" + +# 3. Use CA's private key to sign web server's CSR and get back the signed certificate +openssl x509 -req -in server-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile server-ext.cnf + +echo "Server's signed certificate" +openssl x509 -in server-cert.pem -noout -text + +# 4. Generate client's private key and certificate signing request (CSR) +openssl req -newkey rsa:4096 -nodes -keyout client-key.pem -out client-req.pem -subj "/C=DE/emailAddress=cpuidle@gmx.de" + +# 5. Use CA's private key to sign client's CSR and get back the signed certificate +openssl x509 -req -in client-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile client-ext.cnf + +echo "Client's signed certificate" +openssl x509 -in client-cert.pem -noout -text + +rm *-req.pem diff --git a/soc/cert/server/cert.go b/soc/cert/server/cert.go new file mode 100644 index 000000000..4a2095aa7 --- /dev/null +++ b/soc/cert/server/cert.go @@ -0,0 +1,28 @@ +package server + +import ( + "bytes" + "crypto/tls" + _ "embed" + + "github.com/andig/evcc/util" +) + +//go:embed server-cert.pem +var cert []byte + +//--go:embed server-key.pem +var key []byte + +func init() { + key = []byte(util.Getenv("SERVER_KEY")) +} + +func PEM() []byte { + copy := bytes.NewBuffer(cert) + return copy.Bytes() +} + +func Certificate() (tls.Certificate, error) { + return tls.X509KeyPair(cert, key) +} diff --git a/soc/cert/server/server-cert.pem b/soc/cert/server/server-cert.pem new file mode 100644 index 000000000..2cb592bdc --- /dev/null +++ b/soc/cert/server/server-cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE+TCCAuGgAwIBAgIJALu7hrWUWtC8MA0GCSqGSIb3DQEBBQUAMCwxCzAJBgNV +BAYTAkRFMR0wGwYJKoZIhvcNAQkBFg5jcHVpZGxlQGdteC5kZTAeFw0yMTAzMzEw +NzU4MDVaFw0yMTA1MzAwNzU4MDVaMCwxCzAJBgNVBAYTAkRFMR0wGwYJKoZIhvcN +AQkBFg5jcHVpZGxlQGdteC5kZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBALzv8oy3yl+sH+7fezx6aDJWf77aCaZmBrQyupXmd8InJazRx8ScmpO+LJ7i +ZtVA1YuHbL2qZaV5SBk15UkRaVRRNqVitQmaRFYDrrAtDMosOhVNGOSorGnrlOqP +eu8LD/bWwZ57NqWE2z7ZTnqNNdgX1urcvzs7jmopKl4i71sKHAJ1jdzEYl+7l7r1 +HdtKU4K6axQ2BBP6dHB9v0wOJQzfER76mSWe+hotCTdIiGnZhaIO5NoW31i3KkPi +277bLse5wGGbD1FvzLPaWf0HyruxARPs49kxuxnYtJ+KUDk5LXpDC6MM2QcUdV/8 +HVkTadONW/gVFKWS7u21NrWH2342K0lz4FyNEwiQGmTCnyT4A9DQmdfWirnXsf4M +Jkag7fWqK/dEFIJK14RHZx+i53UBIoLu6mTT+MrHzdXRNewg9jlSwOYROG80DEMh +WIbTcYqUk9XPSUzzaeg2FuP7ZFEmpbsltsztaEQikd5PO9Dkl+Qzm5eiQMwNxJX5 +bgb6eLQOI+HOtQHSWVTBXeM7Vf0JCfA7RAvKgRk4lK6JGijzGBSLuCLzXAPygiqb +59KgYf2EDaSmjzU+ZCLDR6l81SUDVnQj75obQt/TPBoVgwlCeOfFvLGGXY6tzJrp +8yH59MWpkRB4yQImTzmnoZ5klUjwuShi2bWLFiI0+FmXNuitAgMBAAGjHjAcMBoG +A1UdEQQTMBGCCSouZXZjYy5pb4cEAAAAADANBgkqhkiG9w0BAQUFAAOCAgEAlmh0 +fR34BT1LTZcTZjFgk3a0/MaZ4zK8zdlwtQFHSlOh46t+TL+jps2bmLu9IlZxLF8E +lgSzLu5bWeP8H9HuBgji41RMtwvNUqSFUUMn08Dh/78lFISneqyJZ2CeRzwPYyYs +5xi7DBXitFbvtWj5GP3NQaDhsXAPoXtKXuqab3IE6eSwTJOxm5PJnvVdRTFSRQJW +W3qQFICQxMm6Oif+yCGZLgROj7sJWOOKJecevtAxYebOCWx5nxbXxAvOY3wiRerG +xsS3pT6Q5bVo5OxUjPeRb3Fe969JJEMqGaaGTGGtYP2IVkwarwzKBtcpwgG3NJYv +EYG/MEFkpBl6yYJRRB5D7EyPcBRcDHn53sJqnP+Y8Xq40tjfWhxZ9I+N8VWK9nwS +a9CKUHlwpB6aT/J1GNMiU2RXhQWgDv8qma6Vik7RH5ZmDMmtG18GERHTNdiGKk9b +v5xTphWMHl0NtcGPbPoc8PCrIVkb0frGAX5g6UVCHFfSkiUwLDV1zPhv/tLBa5hX +ZzW0wJzs6nDMj9NleyN1B1UY3mbyNKS2Ti9M9NBNEc+IEUdyj36RNmuvDmVNYTZ+ +eYk28CFfkVT7XQjI1hIpBtu5JqBqic5Kzxg/Xwjmt1izpQBy8C7FFMb0Zs3uvrjo +FHk0eyJ/i830ZKCw8Ovx1fgMElZz8AZb/17aeRY= +-----END CERTIFICATE----- diff --git a/soc/cert/server/server-ext.cnf b/soc/cert/server/server-ext.cnf new file mode 100644 index 000000000..ea89676b0 --- /dev/null +++ b/soc/cert/server/server-ext.cnf @@ -0,0 +1 @@ +subjectAltName=DNS:*.evcc.io,IP:0.0.0.0 \ No newline at end of file diff --git a/soc/client/main.go b/soc/client/main.go new file mode 100644 index 000000000..e5608a9b2 --- /dev/null +++ b/soc/client/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/andig/evcc/internal/vehicle" + "github.com/andig/evcc/util" +) + +func usage() { + fmt.Print(` +soc + +Usage: + soc vehicle [--log level] [--param value [...]] +`) +} + +func main() { + if len(os.Args) < 3 { + usage() + log.Fatal("not enough arguments") + } + + params := make(map[string]interface{}) + params["brand"] = strings.ToLower(os.Args[1]) + + var key string + for _, arg := range os.Args[2:] { + if key == "" { + key = strings.ToLower(strings.TrimLeft(arg, "-")) + } else { + if key == "log" { + util.LogLevel(arg, nil) + } else { + params[key] = arg + } + key = "" + } + } + + if key != "" { + usage() + log.Fatal("unexpected number of parameters") + } + + v, err := vehicle.NewCloudFromConfig(params) + if err != nil { + log.Fatal(err) + } + + soc, err := v.SoC() + if err != nil { + log.Fatal(err) + } + + fmt.Println(soc) +} diff --git a/soc/proto/pb/vehicle.pb.go b/soc/proto/pb/vehicle.pb.go new file mode 100644 index 000000000..c3fcba23e --- /dev/null +++ b/soc/proto/pb/vehicle.pb.go @@ -0,0 +1,373 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.15.6 +// source: proto/vehicle.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type NewRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Config map[string]string `protobuf:"bytes,3,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *NewRequest) Reset() { + *x = NewRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_vehicle_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NewRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NewRequest) ProtoMessage() {} + +func (x *NewRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_vehicle_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NewRequest.ProtoReflect.Descriptor instead. +func (*NewRequest) Descriptor() ([]byte, []int) { + return file_proto_vehicle_proto_rawDescGZIP(), []int{0} +} + +func (x *NewRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *NewRequest) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *NewRequest) GetConfig() map[string]string { + if x != nil { + return x.Config + } + return nil +} + +type NewReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + VehicleId int64 `protobuf:"varint,1,opt,name=vehicle_id,json=vehicleId,proto3" json:"vehicle_id,omitempty"` +} + +func (x *NewReply) Reset() { + *x = NewReply{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_vehicle_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NewReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NewReply) ProtoMessage() {} + +func (x *NewReply) ProtoReflect() protoreflect.Message { + mi := &file_proto_vehicle_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NewReply.ProtoReflect.Descriptor instead. +func (*NewReply) Descriptor() ([]byte, []int) { + return file_proto_vehicle_proto_rawDescGZIP(), []int{1} +} + +func (x *NewReply) GetVehicleId() int64 { + if x != nil { + return x.VehicleId + } + return 0 +} + +type SoCRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + VehicleId int64 `protobuf:"varint,2,opt,name=vehicle_id,json=vehicleId,proto3" json:"vehicle_id,omitempty"` +} + +func (x *SoCRequest) Reset() { + *x = SoCRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_vehicle_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SoCRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SoCRequest) ProtoMessage() {} + +func (x *SoCRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_vehicle_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SoCRequest.ProtoReflect.Descriptor instead. +func (*SoCRequest) Descriptor() ([]byte, []int) { + return file_proto_vehicle_proto_rawDescGZIP(), []int{2} +} + +func (x *SoCRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *SoCRequest) GetVehicleId() int64 { + if x != nil { + return x.VehicleId + } + return 0 +} + +type SoCReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Soc float64 `protobuf:"fixed64,1,opt,name=soc,proto3" json:"soc,omitempty"` +} + +func (x *SoCReply) Reset() { + *x = SoCReply{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_vehicle_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SoCReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SoCReply) ProtoMessage() {} + +func (x *SoCReply) ProtoReflect() protoreflect.Message { + mi := &file_proto_vehicle_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SoCReply.ProtoReflect.Descriptor instead. +func (*SoCReply) Descriptor() ([]byte, []int) { + return file_proto_vehicle_proto_rawDescGZIP(), []int{3} +} + +func (x *SoCReply) GetSoc() float64 { + if x != nil { + return x.Soc + } + return 0 +} + +var File_proto_vehicle_proto protoreflect.FileDescriptor + +var file_proto_vehicle_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x65, 0x68, 0x69, 0x63, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa2, 0x01, 0x0a, 0x0a, 0x4e, 0x65, 0x77, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2f, + 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x4e, 0x65, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, + 0x39, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x29, 0x0a, 0x08, 0x4e, 0x65, + 0x77, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x76, 0x65, 0x68, 0x69, 0x63, 0x6c, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x76, 0x65, 0x68, 0x69, + 0x63, 0x6c, 0x65, 0x49, 0x64, 0x22, 0x41, 0x0a, 0x0a, 0x53, 0x6f, 0x43, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x76, 0x65, 0x68, + 0x69, 0x63, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x76, + 0x65, 0x68, 0x69, 0x63, 0x6c, 0x65, 0x49, 0x64, 0x22, 0x1c, 0x0a, 0x08, 0x53, 0x6f, 0x43, 0x52, + 0x65, 0x70, 0x6c, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x6f, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x01, 0x52, 0x03, 0x73, 0x6f, 0x63, 0x32, 0x4b, 0x0a, 0x07, 0x56, 0x65, 0x68, 0x69, 0x63, 0x6c, + 0x65, 0x12, 0x1f, 0x0a, 0x03, 0x4e, 0x65, 0x77, 0x12, 0x0b, 0x2e, 0x4e, 0x65, 0x77, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x4e, 0x65, 0x77, 0x52, 0x65, 0x70, 0x6c, 0x79, + 0x22, 0x00, 0x12, 0x1f, 0x0a, 0x03, 0x53, 0x6f, 0x43, 0x12, 0x0b, 0x2e, 0x53, 0x6f, 0x43, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x53, 0x6f, 0x43, 0x52, 0x65, 0x70, 0x6c, + 0x79, 0x22, 0x00, 0x42, 0x0a, 0x5a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x62, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proto_vehicle_proto_rawDescOnce sync.Once + file_proto_vehicle_proto_rawDescData = file_proto_vehicle_proto_rawDesc +) + +func file_proto_vehicle_proto_rawDescGZIP() []byte { + file_proto_vehicle_proto_rawDescOnce.Do(func() { + file_proto_vehicle_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_vehicle_proto_rawDescData) + }) + return file_proto_vehicle_proto_rawDescData +} + +var file_proto_vehicle_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_proto_vehicle_proto_goTypes = []interface{}{ + (*NewRequest)(nil), // 0: NewRequest + (*NewReply)(nil), // 1: NewReply + (*SoCRequest)(nil), // 2: SoCRequest + (*SoCReply)(nil), // 3: SoCReply + nil, // 4: NewRequest.ConfigEntry +} +var file_proto_vehicle_proto_depIdxs = []int32{ + 4, // 0: NewRequest.config:type_name -> NewRequest.ConfigEntry + 0, // 1: Vehicle.New:input_type -> NewRequest + 2, // 2: Vehicle.SoC:input_type -> SoCRequest + 1, // 3: Vehicle.New:output_type -> NewReply + 3, // 4: Vehicle.SoC:output_type -> SoCReply + 3, // [3:5] is the sub-list for method output_type + 1, // [1:3] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_proto_vehicle_proto_init() } +func file_proto_vehicle_proto_init() { + if File_proto_vehicle_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proto_vehicle_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NewRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_vehicle_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NewReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_vehicle_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SoCRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_vehicle_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SoCReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_proto_vehicle_proto_rawDesc, + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_vehicle_proto_goTypes, + DependencyIndexes: file_proto_vehicle_proto_depIdxs, + MessageInfos: file_proto_vehicle_proto_msgTypes, + }.Build() + File_proto_vehicle_proto = out.File + file_proto_vehicle_proto_rawDesc = nil + file_proto_vehicle_proto_goTypes = nil + file_proto_vehicle_proto_depIdxs = nil +} diff --git a/soc/proto/pb/vehicle_grpc.pb.go b/soc/proto/pb/vehicle_grpc.pb.go new file mode 100644 index 000000000..891eded08 --- /dev/null +++ b/soc/proto/pb/vehicle_grpc.pb.go @@ -0,0 +1,137 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package pb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// VehicleClient is the client API for Vehicle service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type VehicleClient interface { + New(ctx context.Context, in *NewRequest, opts ...grpc.CallOption) (*NewReply, error) + SoC(ctx context.Context, in *SoCRequest, opts ...grpc.CallOption) (*SoCReply, error) +} + +type vehicleClient struct { + cc grpc.ClientConnInterface +} + +func NewVehicleClient(cc grpc.ClientConnInterface) VehicleClient { + return &vehicleClient{cc} +} + +func (c *vehicleClient) New(ctx context.Context, in *NewRequest, opts ...grpc.CallOption) (*NewReply, error) { + out := new(NewReply) + err := c.cc.Invoke(ctx, "/Vehicle/New", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *vehicleClient) SoC(ctx context.Context, in *SoCRequest, opts ...grpc.CallOption) (*SoCReply, error) { + out := new(SoCReply) + err := c.cc.Invoke(ctx, "/Vehicle/SoC", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// VehicleServer is the server API for Vehicle service. +// All implementations must embed UnimplementedVehicleServer +// for forward compatibility +type VehicleServer interface { + New(context.Context, *NewRequest) (*NewReply, error) + SoC(context.Context, *SoCRequest) (*SoCReply, error) + mustEmbedUnimplementedVehicleServer() +} + +// UnimplementedVehicleServer must be embedded to have forward compatible implementations. +type UnimplementedVehicleServer struct { +} + +func (UnimplementedVehicleServer) New(context.Context, *NewRequest) (*NewReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method New not implemented") +} +func (UnimplementedVehicleServer) SoC(context.Context, *SoCRequest) (*SoCReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method SoC not implemented") +} +func (UnimplementedVehicleServer) mustEmbedUnimplementedVehicleServer() {} + +// UnsafeVehicleServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to VehicleServer will +// result in compilation errors. +type UnsafeVehicleServer interface { + mustEmbedUnimplementedVehicleServer() +} + +func RegisterVehicleServer(s grpc.ServiceRegistrar, srv VehicleServer) { + s.RegisterService(&Vehicle_ServiceDesc, srv) +} + +func _Vehicle_New_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NewRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VehicleServer).New(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/Vehicle/New", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VehicleServer).New(ctx, req.(*NewRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Vehicle_SoC_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SoCRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VehicleServer).SoC(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/Vehicle/SoC", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VehicleServer).SoC(ctx, req.(*SoCRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Vehicle_ServiceDesc is the grpc.ServiceDesc for Vehicle service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Vehicle_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "Vehicle", + HandlerType: (*VehicleServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "New", + Handler: _Vehicle_New_Handler, + }, + { + MethodName: "SoC", + Handler: _Vehicle_SoC_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/vehicle.proto", +} diff --git a/soc/proto/vehicle.proto b/soc/proto/vehicle.proto new file mode 100644 index 000000000..b2cea7a99 --- /dev/null +++ b/soc/proto/vehicle.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +// protoc proto/vehicle.proto --go_out=. --go-grpc_out=. + +option go_package = "proto/pb"; + +service Vehicle { + rpc New (NewRequest) returns (NewReply) {} + rpc SoC (SoCRequest) returns (SoCReply) {} +} + +message NewRequest { + string token = 1; + string type = 2; + map config = 3; +} + +message NewReply { + int64 vehicle_id = 1; +} + +message SoCRequest { + string token = 1; + int64 vehicle_id = 2; +} + +message SoCReply { + double soc = 1; +} diff --git a/soc/server/Dockerfile b/soc/server/Dockerfile new file mode 100644 index 000000000..312ae20f0 --- /dev/null +++ b/soc/server/Dockerfile @@ -0,0 +1,36 @@ +# STEP 1 build executable binary +FROM golang:1.16-alpine as builder + +# Install git + SSL ca certificates. +# Git is required for fetching the dependencies. +# Ca-certificates is required to call HTTPS endpoints. +RUN apk update && apk add --no-cache git ca-certificates tzdata alpine-sdk && update-ca-certificates + +WORKDIR /build + +# install go tools and cache modules +COPY go.mod . +COPY go.sum . +RUN go mod download + +# prepare +COPY . . + +# build +RUN make build + + +# STEP 3 build a small image including module support +FROM alpine:3.13 + +WORKDIR /app + +ENV TZ=Europe/Berlin + +# Import from builder +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /build/evcc-cloud /usr/local/bin/evcc-cloud + +# ENTRYPOINT [ "/app/entrypoint.sh" ] +CMD [ "evcc-cloud" ] diff --git a/soc/server/auth/auth.go b/soc/server/auth/auth.go new file mode 100644 index 000000000..5f883bc5b --- /dev/null +++ b/soc/server/auth/auth.go @@ -0,0 +1,99 @@ +package auth + +import ( + "context" + "errors" + "sync" + "time" + + "github.com/andig/evcc/soc/server/auth/sponsor" + "github.com/andig/evcc/util" + "github.com/dgrijalva/jwt-go" +) + +var ( + TokenExpiry = 365 // days + tokenSecret = util.Getenv("JWT_TOKEN_SECRET") +) + +var ( + mux sync.Mutex + updated time.Time + authorized = make(map[string]bool) +) + +const updateInterval = 5 * time.Minute + +type Claims struct { + Username string `json:"username"` + jwt.StandardClaims +} + +func keyFunc(token *jwt.Token) (interface{}, error) { + return []byte(tokenSecret), nil +} + +func AuthorizedToken(name, login string) (string, error) { + expiry := time.Now().Add(24 * time.Hour * time.Duration(TokenExpiry)) + claims := Claims{ + Username: name, + StandardClaims: jwt.StandardClaims{ + Subject: login, + ExpiresAt: expiry.Unix(), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(tokenSecret)) +} + +func ParseToken(token string) (*Claims, error) { + jwt, err := jwt.ParseWithClaims(token, &Claims{}, keyFunc) + if err != nil { + return nil, err + } + + if !jwt.Valid { + return nil, errors.New("token invalid") + } + + claims, ok := jwt.Claims.(*Claims) + if !ok { + return nil, errors.New("token claims invalid") + } + + return claims, nil +} + +func IsAuthorized(login string) (bool, error) { + mux.Lock() + defer mux.Unlock() + + if login == "demo" { + return true, nil + } + + if _, ok := authorized[login]; !ok { + if time.Since(updated) < updateInterval { + return false, nil + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + all, err := sponsor.Get(ctx) + if err != nil { + return false, err + } + // fmt.Println("sponsors:", all) + + updated = time.Now() + + for _, s := range all { + authorized[s.Login] = true + } + } + + _, ok := authorized[login] + return ok, nil +} diff --git a/soc/server/auth/sponsor/sponsor.go b/soc/server/auth/sponsor/sponsor.go new file mode 100644 index 000000000..be1997da1 --- /dev/null +++ b/soc/server/auth/sponsor/sponsor.go @@ -0,0 +1,79 @@ +package sponsor + +import ( + "context" + + "github.com/andig/evcc/util" + "github.com/shurcooL/githubv4" + "golang.org/x/oauth2" +) + +var client *githubv4.Client + +func init() { + token := util.Getenv("GITHUB_TOKEN") + + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tc := oauth2.NewClient(context.Background(), ts) + + client = githubv4.NewClient(tc) +} + +// Source: https://github.com/tj/sponsors-api/blob/master/server.go + +// Sponsor model +type Sponsor struct { + Name string + Login string + AvatarURL string +} + +// sponsorships query: https://docs.github.com/en/graphql/reference/objects +type sponsorships struct { + Viewer struct { + Login string + SponsorshipsAsMaintainer struct { + PageInfo struct { + EndCursor string + HasNextPage bool + } + + Edges []struct { + Node struct { + Sponsor Sponsor + } + Cursor string + } + } `graphql:"sponsorshipsAsMaintainer(first: 100, after: $cursor, includePrivate: true)"` + } +} + +// Get returns the list of sponsors +func Get(ctx context.Context) ([]Sponsor, error) { + var sponsors []Sponsor + var q sponsorships + var cursor string + + for { + err := client.Query(ctx, &q, map[string]interface{}{ + "cursor": githubv4.String(cursor), + }) + + if err != nil { + return nil, err + } + + for _, edge := range q.Viewer.SponsorshipsAsMaintainer.Edges { + sponsor := edge.Node.Sponsor + sponsors = append(sponsors, sponsor) + } + + if !q.Viewer.SponsorshipsAsMaintainer.PageInfo.HasNextPage { + break + } + + cursor = q.Viewer.SponsorshipsAsMaintainer.PageInfo.EndCursor + } + + return sponsors, nil +} diff --git a/soc/server/main.go b/soc/server/main.go new file mode 100644 index 000000000..3585b2e71 --- /dev/null +++ b/soc/server/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "log" + + compile "github.com/andig/evcc/server" + "github.com/andig/evcc/soc/server/server" + "github.com/andig/evcc/soc/server/ui" +) + +func main() { + log.Printf("soc-server %s (%s)", compile.Version, compile.Commit) + + go ui.Run() + go server.Run() + + select {} +} diff --git a/soc/server/server/init.go b/soc/server/server/init.go new file mode 100644 index 000000000..fdc7aa37d --- /dev/null +++ b/soc/server/server/init.go @@ -0,0 +1,74 @@ +package server + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "log" + "net" + + "github.com/andig/evcc/api" + "github.com/andig/evcc/soc/cert/ca" + "github.com/andig/evcc/soc/cert/server" + "github.com/andig/evcc/soc/proto/pb" + "github.com/andig/evcc/util" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +var ( + port = util.Getenv("GRPC_PORT", "8080") + tlsConfig *tls.Config +) + +func init() { + var err error + if tlsConfig, err = loadTLSCredentials(); err != nil { + log.Fatalf("cannot load TLS credentials: %v", err) + } +} + +func loadTLSCredentials() (*tls.Config, error) { + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(ca.PEM()) { + return nil, fmt.Errorf("failed to add client CA's certificate") + } + + // Load server's certificate and private key + serverCert, err := server.Certificate() + if err != nil { + return nil, err + } + + // Create the credentials and return it + config := &tls.Config{ + Certificates: []tls.Certificate{serverCert}, + ClientAuth: tls.VerifyClientCertIfGiven, + ClientCAs: certPool, + } + + return config, nil +} + +func Run() { + fmt.Println("grpc:", ":"+port) + + listener, err := net.Listen("tcp", ":"+port) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + + srv := &Server{ + vehicles: make(map[string]map[int64]api.Vehicle), + } + + serverOptions := []grpc.ServerOption{} + if tlsConfig != nil { + serverOptions = append(serverOptions, grpc.Creds(credentials.NewTLS(tlsConfig))) + } + + grpcServer := grpc.NewServer(serverOptions...) + pb.RegisterVehicleServer(grpcServer, srv) + + log.Fatal(grpcServer.Serve(listener)) +} diff --git a/soc/server/server/server.go b/soc/server/server/server.go new file mode 100644 index 000000000..af4e39adc --- /dev/null +++ b/soc/server/server/server.go @@ -0,0 +1,115 @@ +package server + +import ( + "context" + "fmt" + "sync/atomic" + + "github.com/andig/evcc/api" + "github.com/andig/evcc/internal/vehicle" + "github.com/andig/evcc/internal/vehicle/cloud" + "github.com/andig/evcc/soc/proto/pb" + "github.com/andig/evcc/soc/server/auth" +) + +var vehicleID int64 + +type Server struct { + vehicles map[string]map[int64]api.Vehicle + pb.UnimplementedVehicleServer +} + +type tokenizer interface { + GetToken() string +} + +func (s *Server) isAuthorized(r tokenizer) (string, *auth.Claims, error) { + token := r.GetToken() + + claims, err := auth.ParseToken(token) + if err != nil { + return token, claims, err + } + + authorized, err := auth.IsAuthorized(claims.Subject) + if err == nil && !authorized { + err = cloud.ErrNotAuthorized + } + + return token, claims, err +} + +type vehicler interface { + tokenizer + GetVehicleId() int64 +} + +func (s *Server) vehicle(r vehicler) (api.Vehicle, error) { + token := r.GetToken() + vehicles, ok := s.vehicles[token] + if !ok { + return nil, cloud.ErrVehicleNotAvailable + } + + id := r.GetVehicleId() + v, ok := vehicles[id] + if !ok { + return nil, cloud.ErrVehicleNotAvailable + } + + return v, nil +} + +func stringMapToInterface(in map[string]string) map[string]interface{} { + res := make(map[string]interface{}) + + for k, v := range in { + res[k] = v + } + + return res +} + +func (s *Server) New(ctx context.Context, r *pb.NewRequest) (*pb.NewReply, error) { + token, claims, err := s.isAuthorized(r) + if err != nil { + return nil, err + } + + typ := r.GetType() + config := r.GetConfig() + + // track vehicle create + fmt.Println(claims.Subject+":", typ) + + v, err := vehicle.NewFromConfig(typ, stringMapToInterface(config)) + if err != nil { + return nil, err + } + + id := atomic.AddInt64(&vehicleID, 1) + if s.vehicles[token] == nil { + s.vehicles[token] = make(map[int64]api.Vehicle) + } + s.vehicles[token][id] = v + + res := pb.NewReply{ + VehicleId: id, + } + + return &res, nil +} + +func (s *Server) SoC(ctx context.Context, r *pb.SoCRequest) (*pb.SoCReply, error) { + v, err := s.vehicle(r) + if err != nil { + return nil, err + } + + soc, err := v.SoC() + res := pb.SoCReply{ + Soc: soc, + } + + return &res, err +} diff --git a/soc/server/ui/index.html b/soc/server/ui/index.html new file mode 100644 index 000000000..54d2522f6 --- /dev/null +++ b/soc/server/ui/index.html @@ -0,0 +1,128 @@ + + + + + + + evcc cloud + + + + + + + + + + + +
+ +
+
+

evcc cloud

+ +
+
+ +
+ {{if .Content}} + {{.Content}} + {{else}} +

Willkommen

+

+ evcc cloud erfordert eine einmalige Anmeldung um evcc cloud mit dem Github Konto des evcc Sponsors zu + verknüpfen. Mit der Verknüpfung wird das Registrierungstoken erzeugt. +

+

+ Die evcc cloud speichert keine Benutzerdaten. Alle Daten werden weiterhin lokal verarbeitet. +

+

+ Anmelden +

+ {{end}} +
+ +
+

Cover template by @mdo.

+
+ +
+ + + + \ No newline at end of file diff --git a/soc/server/ui/ui.go b/soc/server/ui/ui.go new file mode 100644 index 000000000..e4c1aa453 --- /dev/null +++ b/soc/server/ui/ui.go @@ -0,0 +1,237 @@ +package ui + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "html/template" + "io" + "log" + "math/rand" + "net/http" + "os" + "strings" + "time" + + "github.com/andig/evcc/soc/server/auth" + "github.com/andig/evcc/util" + "golang.org/x/crypto/acme/autocert" + "golang.org/x/oauth2" + "golang.org/x/oauth2/github" +) + +//go:embed index.html +var indexHtml string + +var ( + // login ui and callback + redirectURL = util.Getenv("REDIRECT_URL") + + // SSL termination + sslCertDir = util.Getenv("SSL_CERT_DIR", "certs") + + // github oauth application + clientID = util.Getenv("GITHUB_CLIENT_ID") + clientSecret = util.Getenv("GITHUB_CLIENT_SECRET") +) + +var ( + indexTpl *template.Template + + oauthState = randomString() + oauthConfig *oauth2.Config +) + +const ( + ProfileURL = "https://api.github.com/user" + EmailURL = "https://api.github.com/user/emails" +) + +type User struct { + ID int `json:"id"` + Email string `json:"email"` + Bio string `json:"bio"` + Name string `json:"name"` + Login string `json:"login"` + Picture string `json:"avatar_url"` + Location string `json:"location"` +} + +func init() { + clientID = util.Getenv("GITHUB_CLIENT_ID") + clientSecret = util.Getenv("GITHUB_CLIENT_SECRET") + + oauthConfig = &oauth2.Config{ + RedirectURL: redirectURL, + ClientID: clientID, + ClientSecret: clientSecret, + Scopes: []string{}, + Endpoint: github.Endpoint, + } + + indexTpl = template.Must(template.New("index").Parse(indexHtml)) +} + +func randomString() string { + rand.Seed(time.Now().UnixNano()) + chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") + length := 16 + var b strings.Builder + for i := 0; i < length; i++ { + b.WriteRune(chars[rand.Intn(len(chars))]) + } + return b.String() +} + +func handleMain(w http.ResponseWriter, r *http.Request) { + _ = indexTpl.Execute(w, map[string]interface{}{ + "Content": template.HTML(` +

Willkommen

+

+ evcc cloud erfordert eine einmalige Anmeldung um evcc cloud mit dem Github Konto des evcc Sponsors zu + verknüpfen. Mit der Verknüpfung wird das Registrierungstoken erzeugt. +

+

+ Die evcc cloud speichert keine Benutzerdaten. Alle Daten werden weiterhin lokal verarbeitet. +

+

+ Anmelden +

`), + }) +} + +func templateError(w http.ResponseWriter, r *http.Request, err string) { + _ = indexTpl.Execute(w, map[string]interface{}{ + "Content": template.HTML(` +

Fehler

+

` + err + `

`), + }) +} + +func handleLogin(w http.ResponseWriter, r *http.Request) { + url := oauthConfig.AuthCodeURL(oauthState) + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +func handleCallback(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("error") != "" { + err := r.URL.Query().Get("error_description") + templateError(w, r, err) + return + } + + user, err := getUserInfo(r.FormValue("state"), r.FormValue("code")) + if err != nil { + templateError(w, r, err.Error()) + return + } + + authorized, err := auth.IsAuthorized(user.Login) + if err != nil { + templateError(w, r, err.Error()) + return + } + + authMap := map[bool]string{false: "rejected", true: "authorized"} + log.Println(user.Login+":", authMap[authorized]) + + if !authorized { + _ = indexTpl.Execute(w, map[string]interface{}{ + "Content": template.HTML(` +

Inaktiv

+

Github Sponsorship für evcc ist inaktiv. Um evcc cloud zu nutzen, ist es notwendig, evcc zu unterstützen.

+

evcc bei Github unterstützen.

`), + }) + return + } + + jwt, err := auth.AuthorizedToken(user.Name, user.Login) + if err != nil { + templateError(w, r, err.Error()) + return + } + + _ = indexTpl.Execute(w, map[string]interface{}{ + "Content": template.HTML(fmt.Sprintf(` +

Aktiv

+

Github Sponsorship für evcc ist aktiv. Der folgende Token kann für den Zugriff auf evcc cloud genutzt werden.

+

Der Code ist %d Tage gültig und kann jederzeit neu erzeugt werden.

+

`+jwt+`

`, auth.TokenExpiry), + )}) +} + +func getUserInfo(state string, code string) (*User, error) { + if state != oauthState { + return nil, fmt.Errorf("invalid oauth state") + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + token, err := oauthConfig.Exchange(ctx, code) + if err != nil { + return nil, fmt.Errorf("code exchange failed: %s", err.Error()) + } + + var user User + req, err := http.NewRequest("GET", ProfileURL, nil) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", "Bearer "+token.AccessToken) + response, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GitHub API responded with a %d trying to fetch user information", response.StatusCode) + } + + bits, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + err = json.Unmarshal(bits, &user) + if err != nil { + return nil, err + } + + return &user, nil +} + +func Run() { + mux := &http.ServeMux{} + mux.HandleFunc("/", handleMain) + mux.HandleFunc("/login", handleLogin) + mux.HandleFunc("/callback", handleCallback) + + s := &http.Server{ + Addr: ":http", + Handler: mux, + } + + if sslHosts := strings.TrimSpace(os.Getenv("SSL_HOSTS")); sslHosts != "" { + log.Println("sslHosts:", sslHosts) + log.Println("sslCertDir:", sslCertDir) + + certManager := autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(strings.Split(sslHosts, " ")...), + Cache: autocert.DirCache(sslCertDir), + } + + go log.Fatal(http.ListenAndServe(":http", certManager.HTTPHandler(nil))) + + s.Addr = ":https" + s.TLSConfig = certManager.TLSConfig() + + log.Fatal(s.ListenAndServeTLS("", "")) + } else { + log.Fatal(s.ListenAndServe()) + } +} diff --git a/util/env.go b/util/env.go new file mode 100644 index 000000000..9024b6713 --- /dev/null +++ b/util/env.go @@ -0,0 +1,19 @@ +package util + +import ( + "log" + "os" + "strings" +) + +func Getenv(key string, def ...string) string { + res := strings.TrimSpace(os.Getenv(key)) + if res == "" { + if len(def) == 1 { + return def[0] + } + + log.Fatalln("missing", key) + } + return res +}