From 5681d1996b1e1d4293fa5b71295d4b0c2ac9e182 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 25 Apr 2021 18:59:00 +0200 Subject: [PATCH] Add SponsorToken configuration for Easee (#920) --- .gitignore | 1 + cmd/charger.go | 9 +- cmd/config.go | 33 +-- cmd/dump.go | 11 +- cmd/meter.go | 9 +- cmd/root.go | 19 +- cmd/setup.go | 84 +++++-- cmd/vehicle.go | 9 +- evcc.dist.yaml | 4 + internal/charger/easee.go | 5 +- internal/charger/easee/LICENSE | 16 ++ internal/vehicle/cloud.go | 14 +- schema.json | 3 + soc/proto/auth.proto | 18 ++ soc/proto/pb/auth.pb.go | 219 +++++++++++++++++++ soc/proto/pb/auth_grpc.pb.go | 101 +++++++++ soc/proto/pb/vehicle.pb.go | 2 +- soc/proto/vehicle.proto | 18 +- soc/server/server/auth.go | 40 ++++ soc/server/server/init.go | 11 +- soc/server/server/{server.go => vehicle.go} | 36 +-- {internal/vehicle => util}/cloud/api.go | 0 {internal/vehicle => util}/cloud/ca-cert.pem | 0 {internal/vehicle => util}/cloud/cert.go | 0 {internal/vehicle => util}/cloud/client.go | 13 +- util/sponsor/auth.go | 7 + 26 files changed, 549 insertions(+), 133 deletions(-) create mode 100644 internal/charger/easee/LICENSE create mode 100644 soc/proto/auth.proto create mode 100644 soc/proto/pb/auth.pb.go create mode 100644 soc/proto/pb/auth_grpc.pb.go create mode 100644 soc/server/server/auth.go rename soc/server/server/{server.go => vehicle.go} (63%) rename {internal/vehicle => util}/cloud/api.go (100%) rename {internal/vehicle => util}/cloud/ca-cert.pem (100%) rename {internal/vehicle => util}/cloud/cert.go (100%) rename {internal/vehicle => util}/cloud/client.go (75%) create mode 100644 util/sponsor/auth.go diff --git a/.gitignore b/.gitignore index 3b016e77a..ffdb47d2f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ soc-server ca-cert.srl server-key.pem ca-key.pem +cmd/wip diff --git a/cmd/charger.go b/cmd/charger.go index 5467db62b..933642f7d 100644 --- a/cmd/charger.go +++ b/cmd/charger.go @@ -29,14 +29,11 @@ func runCharger(cmd *cobra.Command, args []string) { log.FATAL.Fatal(err) } - // setup mqtt - if conf.Mqtt.Broker != "" { - configureMQTT(conf.Mqtt) + // setup environment + if err := configureEnvironment(conf); err != nil { + log.FATAL.Fatal(err) } - // setup javascript VMs - configureJavascript(conf.Javascript) - if err := cp.configureChargers(conf); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/config.go b/cmd/config.go index 5e5a13727..8a9d36187 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -14,22 +14,23 @@ import ( ) type config struct { - URI string - Log string - Metrics bool - Profile bool - Levels map[string]string - Interval time.Duration - Mqtt mqttConfig - Javascript map[string]interface{} - Influx server.InfluxConfig - HEMS typedConfig - Messaging messagingConfig - Meters []qualifiedConfig - Chargers []qualifiedConfig - Vehicles []qualifiedConfig - Site map[string]interface{} - LoadPoints []map[string]interface{} + URI string + Log string + SponsorToken string + Metrics bool + Profile bool + Levels map[string]string + Interval time.Duration + Mqtt mqttConfig + Javascript map[string]interface{} + Influx server.InfluxConfig + HEMS typedConfig + Messaging messagingConfig + Meters []qualifiedConfig + Chargers []qualifiedConfig + Vehicles []qualifiedConfig + Site map[string]interface{} + LoadPoints []map[string]interface{} } type mqttConfig struct { diff --git a/cmd/dump.go b/cmd/dump.go index 2eae157d8..a98ca33dc 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -31,15 +31,12 @@ func runDump(cmd *cobra.Command, args []string) { log.FATAL.Fatal(err) } - // setup mqtt - if conf.Mqtt.Broker != "" { - configureMQTT(conf.Mqtt) + // setup environment + if err := configureEnvironment(conf); err != nil { + log.FATAL.Fatal(err) } - // setup javascript VMs - configureJavascript(conf.Javascript) - - site, err := loadConfig(conf) + site, err := configureSiteAndLoadpoints(conf) if err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/meter.go b/cmd/meter.go index 5f59b4391..6e0f7dfe0 100644 --- a/cmd/meter.go +++ b/cmd/meter.go @@ -29,14 +29,11 @@ func runMeter(cmd *cobra.Command, args []string) { log.FATAL.Fatal(err) } - // setup mqtt - if conf.Mqtt.Broker != "" { - configureMQTT(conf.Mqtt) + // setup environment + if err := configureEnvironment(conf); err != nil { + log.FATAL.Fatal(err) } - // setup javascript VMs - configureJavascript(conf.Javascript) - if err := cp.configureMeters(conf); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/root.go b/cmd/root.go index d0cc5218c..0bf7938a9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -152,13 +152,16 @@ func run(cmd *cobra.Command, args []string) { uri := viper.GetString("uri") log.INFO.Println("listening at", uri) - // setup mqtt client listener - if conf.Mqtt.Broker != "" { - configureMQTT(conf.Mqtt) + // setup environment + if err := configureEnvironment(conf); err != nil { + log.FATAL.Fatal(err) } - // setup javascript VMs - configureJavascript(conf.Javascript) + // setup loadpoints + site, err := configureSiteAndLoadpoints(conf) + if err != nil { + log.FATAL.Fatal(err) + } // start broadcasting values tee := &util.Tee{} @@ -167,12 +170,6 @@ func run(cmd *cobra.Command, args []string) { cache := util.NewCache() go cache.Run(pipe.NewDropper(ignoreErrors...).Pipe(tee.Attach())) - // setup loadpoints - site, err := loadConfig(conf) - if err != nil { - log.FATAL.Fatal(err) - } - // setup database if conf.Influx.URL != "" { configureDatabase(conf.Influx, site.LoadPoints(), tee.Attach()) diff --git a/cmd/setup.go b/cmd/setup.go index 834264324..4c4e33e30 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "errors" "fmt" "math/rand" @@ -13,8 +14,11 @@ import ( "github.com/andig/evcc/provider/mqtt" "github.com/andig/evcc/push" "github.com/andig/evcc/server" + "github.com/andig/evcc/soc/proto/pb" "github.com/andig/evcc/util" + "github.com/andig/evcc/util/cloud" "github.com/andig/evcc/util/pipe" + "github.com/andig/evcc/util/sponsor" "github.com/spf13/viper" ) @@ -24,6 +28,60 @@ func init() { var cp = &ConfigProvider{} +func loadConfigFile(cfgFile string) (conf config, err error) { + if cfgFile != "" { + log.INFO.Println("using config file", cfgFile) + if err := viper.UnmarshalExact(&conf); err != nil { + log.FATAL.Fatalf("failed parsing config file %s: %v", cfgFile, err) + } + } else { + err = errors.New("missing evcc config") + } + + return conf, err +} + +func configureEnvironment(conf config) (err error) { + // setup sponsorship + if conf.SponsorToken != "" { + err = configureSponsorship(conf.SponsorToken) + } + + // setup mqtt client listener + if err == nil && conf.Mqtt.Broker != "" { + err = configureMQTT(conf.Mqtt) + } + + // setup javascript VMs + if err == nil { + err = configureJavascript(conf.Javascript) + } + + return +} + +func configureSponsorship(token string) error { + host := util.Getenv("GRPC_URI", cloud.Host) + conn, err := cloud.Connection(host) + if err != nil { + return err + } + + client := pb.NewAuthClient(conn) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + res, err := client.IsAuthorized(ctx, &pb.AuthRequest{Token: token}) + if err == nil { + if res.Authorized { + sponsor.Subject = res.Subject + } + } + + return err +} + // setup influx databases func configureDatabase(conf server.InfluxConfig, loadPoints []core.LoadPointAPI, in <-chan util.Param) { influx := server.NewInfluxClient( @@ -47,22 +105,25 @@ func configureDatabase(conf server.InfluxConfig, loadPoints []core.LoadPointAPI, } // setup mqtt -func configureMQTT(conf mqttConfig) { +func configureMQTT(conf mqttConfig) error { log := util.NewLogger("mqtt") clientID := mqtt.ClientID() var err error mqtt.Instance, err = mqtt.RegisteredClient(log, conf.Broker, conf.User, conf.Password, clientID, 1) if err != nil { - log.FATAL.Fatalf("failed configuring mqtt: %v", err) + return fmt.Errorf("failed configuring mqtt: %w", err) } + + return nil } // setup javascript -func configureJavascript(conf map[string]interface{}) { +func configureJavascript(conf map[string]interface{}) error { if err := javascript.Configure(conf); err != nil { - log.FATAL.Fatalf("failed configuring javascript: %v", err) + return fmt.Errorf("failed configuring javascript: %w", err) } + return nil } // setup HEMS @@ -93,7 +154,7 @@ func configureMessengers(conf messagingConfig, cache *util.Cache) chan push.Even return notificationChan } -func loadConfig(conf config) (site *core.Site, err error) { +func configureSiteAndLoadpoints(conf config) (site *core.Site, err error) { if err = cp.configure(conf); err == nil { var loadPoints []*core.LoadPoint loadPoints, err = configureLoadPoints(conf, cp) @@ -138,16 +199,3 @@ func configureLoadPoints(conf config, cp *ConfigProvider) (loadPoints []*core.Lo return loadPoints, nil } - -func loadConfigFile(cfgFile string) (conf config, err error) { - if cfgFile != "" { - log.INFO.Println("using config file", cfgFile) - if err := viper.UnmarshalExact(&conf); err != nil { - log.FATAL.Fatalf("failed parsing config file %s: %v", cfgFile, err) - } - } else { - err = errors.New("missing evcc config") - } - - return conf, err -} diff --git a/cmd/vehicle.go b/cmd/vehicle.go index fc015d73e..917e85a59 100644 --- a/cmd/vehicle.go +++ b/cmd/vehicle.go @@ -29,14 +29,11 @@ func runVehicle(cmd *cobra.Command, args []string) { log.FATAL.Fatal(err) } - // setup mqtt - if conf.Mqtt.Broker != "" { - configureMQTT(conf.Mqtt) + // setup environment + if err := configureEnvironment(conf); err != nil { + log.FATAL.Fatal(err) } - // setup javascript VMs - configureJavascript(conf.Javascript) - if err := cp.configureVehicles(conf); err != nil { log.FATAL.Fatal(err) } diff --git a/evcc.dist.yaml b/evcc.dist.yaml index 92bf71990..14eb939a4 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -1,6 +1,10 @@ uri: 0.0.0.0:7070 # uri for ui interval: 10s # control cycle interval +# sponsor token enables optional features +# request sponsor token at https://cloud.evcc.io +sponsortoken: + # log settings log: error levels: diff --git a/internal/charger/easee.go b/internal/charger/easee.go index f3e3fc92c..f15ef3ebe 100644 --- a/internal/charger/easee.go +++ b/internal/charger/easee.go @@ -1,7 +1,7 @@ package charger import ( - "errors" + "errors" "fmt" "net/http" "time" @@ -10,6 +10,7 @@ import ( "github.com/andig/evcc/internal/charger/easee" "github.com/andig/evcc/util" "github.com/andig/evcc/util/request" + "github.com/andig/evcc/util/sponsor" ) // Easee charger implementation @@ -49,7 +50,7 @@ func NewEaseeFromConfig(other map[string]interface{}) (api.Charger, error) { func NewEasee(user, password, charger string, cache time.Duration) (*Easee, error) { log := util.NewLogger("easee") - if true { + if !sponsor.IsAuthorized() { return nil, errors.New("easee requires evcc sponsorship, register at https://cloud.evcc.io") } diff --git a/internal/charger/easee/LICENSE b/internal/charger/easee/LICENSE new file mode 100644 index 000000000..e42a31f3c --- /dev/null +++ b/internal/charger/easee/LICENSE @@ -0,0 +1,16 @@ +LICENSE + +Copyright (c) 2019-2021 andig + +This modules is NOT covered by the MIT license. All rights reserved. + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/internal/vehicle/cloud.go b/internal/vehicle/cloud.go index fc26077b6..d598ef1fe 100644 --- a/internal/vehicle/cloud.go +++ b/internal/vehicle/cloud.go @@ -3,14 +3,13 @@ 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/cloud" "github.com/andig/evcc/util/request" ) @@ -50,19 +49,18 @@ func NewCloudFromConfig(other map[string]interface{}) (api.Vehicle, error) { return nil, errors.New("missing required token") } - if host := os.Getenv("GRPC_URI"); host != "" { - cloud.Host = host + host := util.Getenv("GRPC_URI", cloud.Host) + conn, err := cloud.Connection(host) + if err != nil { + return nil, err } - 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, + client: pb.NewVehicleClient(conn), } if err == nil { diff --git a/schema.json b/schema.json index a576aa8af..f25e5cf9d 100644 --- a/schema.json +++ b/schema.json @@ -21,6 +21,9 @@ "type": "string" } }, + "sponsortoken": { + "type": "string" + }, "chargers": { "type": "array", "description": "List of chargers", diff --git a/soc/proto/auth.proto b/soc/proto/auth.proto new file mode 100644 index 000000000..105a3b1a6 --- /dev/null +++ b/soc/proto/auth.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +// protoc proto/auth.proto --go_out=. --go-grpc_out=. + +option go_package = "proto/pb"; + +service Auth { + rpc IsAuthorized (AuthRequest) returns (AuthReply) {} +} + +message AuthRequest { + string token = 1; +} + +message AuthReply { + bool authorized = 1; + string subject = 2; +} diff --git a/soc/proto/pb/auth.pb.go b/soc/proto/pb/auth.pb.go new file mode 100644 index 000000000..da3fffb6f --- /dev/null +++ b/soc/proto/pb/auth.pb.go @@ -0,0 +1,219 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.15.8 +// source: proto/auth.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 AuthRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` +} + +func (x *AuthRequest) Reset() { + *x = AuthRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_auth_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AuthRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthRequest) ProtoMessage() {} + +func (x *AuthRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_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 AuthRequest.ProtoReflect.Descriptor instead. +func (*AuthRequest) Descriptor() ([]byte, []int) { + return file_proto_auth_proto_rawDescGZIP(), []int{0} +} + +func (x *AuthRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +type AuthReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Authorized bool `protobuf:"varint,1,opt,name=authorized,proto3" json:"authorized,omitempty"` + Subject string `protobuf:"bytes,2,opt,name=subject,proto3" json:"subject,omitempty"` +} + +func (x *AuthReply) Reset() { + *x = AuthReply{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_auth_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AuthReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthReply) ProtoMessage() {} + +func (x *AuthReply) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_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 AuthReply.ProtoReflect.Descriptor instead. +func (*AuthReply) Descriptor() ([]byte, []int) { + return file_proto_auth_proto_rawDescGZIP(), []int{1} +} + +func (x *AuthReply) GetAuthorized() bool { + if x != nil { + return x.Authorized + } + return false +} + +func (x *AuthReply) GetSubject() string { + if x != nil { + return x.Subject + } + return "" +} + +var File_proto_auth_proto protoreflect.FileDescriptor + +var file_proto_auth_proto_rawDesc = []byte{ + 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0x23, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 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, 0x22, 0x45, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x52, + 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x32, 0x32, + 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x2a, 0x0a, 0x0c, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x0c, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x41, 0x75, 0x74, 0x68, 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_auth_proto_rawDescOnce sync.Once + file_proto_auth_proto_rawDescData = file_proto_auth_proto_rawDesc +) + +func file_proto_auth_proto_rawDescGZIP() []byte { + file_proto_auth_proto_rawDescOnce.Do(func() { + file_proto_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_auth_proto_rawDescData) + }) + return file_proto_auth_proto_rawDescData +} + +var file_proto_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_proto_auth_proto_goTypes = []interface{}{ + (*AuthRequest)(nil), // 0: AuthRequest + (*AuthReply)(nil), // 1: AuthReply +} +var file_proto_auth_proto_depIdxs = []int32{ + 0, // 0: Auth.IsAuthorized:input_type -> AuthRequest + 1, // 1: Auth.IsAuthorized:output_type -> AuthReply + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_proto_auth_proto_init() } +func file_proto_auth_proto_init() { + if File_proto_auth_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proto_auth_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuthRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_auth_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuthReply); 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_auth_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_auth_proto_goTypes, + DependencyIndexes: file_proto_auth_proto_depIdxs, + MessageInfos: file_proto_auth_proto_msgTypes, + }.Build() + File_proto_auth_proto = out.File + file_proto_auth_proto_rawDesc = nil + file_proto_auth_proto_goTypes = nil + file_proto_auth_proto_depIdxs = nil +} diff --git a/soc/proto/pb/auth_grpc.pb.go b/soc/proto/pb/auth_grpc.pb.go new file mode 100644 index 000000000..92e6dabbe --- /dev/null +++ b/soc/proto/pb/auth_grpc.pb.go @@ -0,0 +1,101 @@ +// 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 + +// AuthClient is the client API for Auth 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 AuthClient interface { + IsAuthorized(ctx context.Context, in *AuthRequest, opts ...grpc.CallOption) (*AuthReply, error) +} + +type authClient struct { + cc grpc.ClientConnInterface +} + +func NewAuthClient(cc grpc.ClientConnInterface) AuthClient { + return &authClient{cc} +} + +func (c *authClient) IsAuthorized(ctx context.Context, in *AuthRequest, opts ...grpc.CallOption) (*AuthReply, error) { + out := new(AuthReply) + err := c.cc.Invoke(ctx, "/Auth/IsAuthorized", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AuthServer is the server API for Auth service. +// All implementations must embed UnimplementedAuthServer +// for forward compatibility +type AuthServer interface { + IsAuthorized(context.Context, *AuthRequest) (*AuthReply, error) + mustEmbedUnimplementedAuthServer() +} + +// UnimplementedAuthServer must be embedded to have forward compatible implementations. +type UnimplementedAuthServer struct { +} + +func (UnimplementedAuthServer) IsAuthorized(context.Context, *AuthRequest) (*AuthReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method IsAuthorized not implemented") +} +func (UnimplementedAuthServer) mustEmbedUnimplementedAuthServer() {} + +// UnsafeAuthServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AuthServer will +// result in compilation errors. +type UnsafeAuthServer interface { + mustEmbedUnimplementedAuthServer() +} + +func RegisterAuthServer(s grpc.ServiceRegistrar, srv AuthServer) { + s.RegisterService(&Auth_ServiceDesc, srv) +} + +func _Auth_IsAuthorized_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AuthRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServer).IsAuthorized(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/Auth/IsAuthorized", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServer).IsAuthorized(ctx, req.(*AuthRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Auth_ServiceDesc is the grpc.ServiceDesc for Auth service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Auth_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "Auth", + HandlerType: (*AuthServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "IsAuthorized", + Handler: _Auth_IsAuthorized_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/auth.proto", +} diff --git a/soc/proto/pb/vehicle.pb.go b/soc/proto/pb/vehicle.pb.go index c3fcba23e..ac150212a 100644 --- a/soc/proto/pb/vehicle.pb.go +++ b/soc/proto/pb/vehicle.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.15.6 +// protoc v3.15.8 // source: proto/vehicle.proto package pb diff --git a/soc/proto/vehicle.proto b/soc/proto/vehicle.proto index b2cea7a99..074157ce9 100644 --- a/soc/proto/vehicle.proto +++ b/soc/proto/vehicle.proto @@ -5,25 +5,25 @@ syntax = "proto3"; option go_package = "proto/pb"; service Vehicle { - rpc New (NewRequest) returns (NewReply) {} - rpc SoC (SoCRequest) returns (SoCReply) {} + rpc New (NewRequest) returns (NewReply) {} + rpc SoC (SoCRequest) returns (SoCReply) {} } message NewRequest { - string token = 1; - string type = 2; - map config = 3; + string token = 1; + string type = 2; + map config = 3; } message NewReply { - int64 vehicle_id = 1; + int64 vehicle_id = 1; } message SoCRequest { - string token = 1; - int64 vehicle_id = 2; + string token = 1; + int64 vehicle_id = 2; } message SoCReply { - double soc = 1; + double soc = 1; } diff --git a/soc/server/server/auth.go b/soc/server/server/auth.go new file mode 100644 index 000000000..b817f7d5f --- /dev/null +++ b/soc/server/server/auth.go @@ -0,0 +1,40 @@ +package server + +import ( + "context" + + "github.com/andig/evcc/soc/proto/pb" + "github.com/andig/evcc/soc/server/auth" +) + +type AuthServer struct { + pb.UnimplementedAuthServer +} + +func (s *AuthServer) IsAuthorized(ctx context.Context, r *pb.AuthRequest) (*pb.AuthReply, error) { + authorized, _, claims, err := isAuthorized(r) + + res := &pb.AuthReply{Authorized: authorized} + if err == nil { + res.Subject = claims.Subject + } + + return res, err +} + +type tokenizer interface { + GetToken() string +} + +func isAuthorized(r tokenizer) (bool, string, *auth.Claims, error) { + token := r.GetToken() + + claims, err := auth.ParseToken(token) + if err != nil { + return false, token, claims, err + } + + authorized, err := auth.IsAuthorized(claims.Subject) + + return authorized, token, claims, err +} diff --git a/soc/server/server/init.go b/soc/server/server/init.go index 3c2cde248..4a6f7ef43 100644 --- a/soc/server/server/init.go +++ b/soc/server/server/init.go @@ -58,17 +58,16 @@ func Run() { 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) + + pb.RegisterVehicleServer(grpcServer, &VehicleServer{ + vehicles: make(map[string]map[int64]api.Vehicle), + }) + pb.RegisterAuthServer(grpcServer, &AuthServer{}) log.Fatal(grpcServer.Serve(listener)) } diff --git a/soc/server/server/server.go b/soc/server/server/vehicle.go similarity index 63% rename from soc/server/server/server.go rename to soc/server/server/vehicle.go index af4e39adc..85f6da13b 100644 --- a/soc/server/server/server.go +++ b/soc/server/server/vehicle.go @@ -7,44 +7,23 @@ import ( "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" + "github.com/andig/evcc/util/cloud" ) var vehicleID int64 -type Server struct { +type VehicleServer 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) { +func (s *VehicleServer) vehicle(r vehicler) (api.Vehicle, error) { token := r.GetToken() vehicles, ok := s.vehicles[token] if !ok { @@ -70,11 +49,14 @@ func stringMapToInterface(in map[string]string) map[string]interface{} { return res } -func (s *Server) New(ctx context.Context, r *pb.NewRequest) (*pb.NewReply, error) { - token, claims, err := s.isAuthorized(r) +func (s *VehicleServer) New(ctx context.Context, r *pb.NewRequest) (*pb.NewReply, error) { + authorized, token, claims, err := isAuthorized(r) if err != nil { return nil, err } + if !authorized { + return nil, cloud.ErrNotAuthorized + } typ := r.GetType() config := r.GetConfig() @@ -100,7 +82,7 @@ func (s *Server) New(ctx context.Context, r *pb.NewRequest) (*pb.NewReply, error return &res, nil } -func (s *Server) SoC(ctx context.Context, r *pb.SoCRequest) (*pb.SoCReply, error) { +func (s *VehicleServer) SoC(ctx context.Context, r *pb.SoCRequest) (*pb.SoCReply, error) { v, err := s.vehicle(r) if err != nil { return nil, err diff --git a/internal/vehicle/cloud/api.go b/util/cloud/api.go similarity index 100% rename from internal/vehicle/cloud/api.go rename to util/cloud/api.go diff --git a/internal/vehicle/cloud/ca-cert.pem b/util/cloud/ca-cert.pem similarity index 100% rename from internal/vehicle/cloud/ca-cert.pem rename to util/cloud/ca-cert.pem diff --git a/internal/vehicle/cloud/cert.go b/util/cloud/cert.go similarity index 100% rename from internal/vehicle/cloud/cert.go rename to util/cloud/cert.go diff --git a/internal/vehicle/cloud/client.go b/util/cloud/client.go similarity index 75% rename from internal/vehicle/cloud/client.go rename to util/cloud/client.go index 03b6cb3f5..89971b90b 100644 --- a/internal/vehicle/cloud/client.go +++ b/util/cloud/client.go @@ -5,8 +5,6 @@ import ( "crypto/x509" "fmt" - "github.com/andig/evcc/soc/proto/pb" - "github.com/andig/evcc/util" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) @@ -14,8 +12,7 @@ import ( var Host = "cloud.evcc.io:8080" var ( - conn *grpc.ClientConn - client pb.VehicleClient + conn *grpc.ClientConn ) func loadTLSCredentials() (*tls.Config, error) { @@ -32,7 +29,7 @@ func loadTLSCredentials() (*tls.Config, error) { return config, nil } -func Client(log *util.Logger, uri string) (pb.VehicleClient, error) { +func Connection(uri string) (*grpc.ClientConn, error) { var err error if conn == nil { var tlsConfig *tls.Config @@ -48,9 +45,5 @@ func Client(log *util.Logger, uri string) (pb.VehicleClient, error) { conn, err = grpc.Dial(uri, transportOption) } - if client == nil && err == nil { - client = pb.NewVehicleClient(conn) - } - - return client, err + return conn, err } diff --git a/util/sponsor/auth.go b/util/sponsor/auth.go new file mode 100644 index 000000000..eae92c328 --- /dev/null +++ b/util/sponsor/auth.go @@ -0,0 +1,7 @@ +package sponsor + +var Subject string + +func IsAuthorized() bool { + return len(Subject) > 0 +}