diff --git a/cmd/remote/framer.go b/cmd/remote/framer.go new file mode 100644 index 00000000..fc885c95 --- /dev/null +++ b/cmd/remote/framer.go @@ -0,0 +1,60 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "io" + + "golang.org/x/exp/jsonrpc2" +) + +type NewlineFramer struct{} +type newlineReader struct{ in *bufio.Reader } +type newlineWriter struct{ out io.Writer } + +func (NewlineFramer) Reader(rw io.Reader) jsonrpc2.Reader { + return &newlineReader{in: bufio.NewReader(rw)} +} + +func (f NewlineFramer) Writer(rw io.Writer) jsonrpc2.Writer { + return &newlineWriter{out: rw} +} + +func (r *newlineReader) Read(ctx context.Context) (jsonrpc2.Message, int64, error) { + select { + case <-ctx.Done(): + return nil, 0, ctx.Err() + default: + } + var total int64 + + // read a line + line, err := r.in.ReadBytes('\n') + total += int64(len(line)) + if err != nil { + return nil, total, fmt.Errorf("failed reading line: %w", err) + } + + msg, err := jsonrpc2.DecodeMessage(line[:total-1]) + return msg, total, err +} + +func (w *newlineWriter) Write(ctx context.Context, msg jsonrpc2.Message) (int64, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + data, err := jsonrpc2.EncodeMessage(msg) + if err != nil { + return 0, fmt.Errorf("marshaling message: %v", err) + } + n, err := w.out.Write(data) + total := int64(n) + if err == nil { + n, err = w.out.Write([]byte("\n")) + total += int64(n) + } + return total, err +} diff --git a/cmd/remote/main.go b/cmd/remote/main.go new file mode 100644 index 00000000..611baaff --- /dev/null +++ b/cmd/remote/main.go @@ -0,0 +1,155 @@ +package main + +import ( + "context" + "crypto/tls" + "flag" + "log" + "net" + "os" + "os/signal" + "strconv" + "syscall" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/usecases/cem/evcc" + "github.com/enbility/eebus-go/usecases/cem/evsecc" + eglpc "github.com/enbility/eebus-go/usecases/eg/lpc" + "github.com/enbility/eebus-go/usecases/ma/mpc" + shipapi "github.com/enbility/ship-go/api" + "github.com/enbility/ship-go/cert" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +type eebusConfiguration struct { + vendorCode string + deviceBrand string + deviceModel string + serialNumber string +} + +func loadCertificate(config eebusConfiguration, crtPath, keyPath string) tls.Certificate { + certificate, err := tls.LoadX509KeyPair(crtPath, keyPath) + if err != nil { + certificate, err = cert.CreateCertificate(config.vendorCode, config.deviceModel, "DE", config.serialNumber) + if err != nil { + log.Fatal(err) + } + + if err = WriteKey(certificate, keyPath); err != nil { + log.Fatal(err) + } + if err = WriteCertificate(certificate, crtPath); err != nil { + log.Fatal(err) + } + } + + return certificate +} + +func main() { + config := eebusConfiguration{} + + iface := flag.String("iface", "", + "Optional network interface the EEBUS connection should be limited to") + flag.StringVar(&config.vendorCode, "vendor", "", "EEBus vendor code") + flag.StringVar(&config.deviceBrand, "brand", "", "EEBus device brand") + flag.StringVar(&config.deviceModel, "model", "", "EEBus device model") + flag.StringVar(&config.serialNumber, "serial", "", "EEBus device serial") + + flag.Parse() + + if config.serialNumber == "" { + serialNumber, err := os.Hostname() + if err != nil { + log.Fatal(err) + } + config.serialNumber = serialNumber + } + + if config.vendorCode == "" || config.deviceBrand == "" || config.deviceModel == "" { + flag.Usage() + return + } + + certificate := loadCertificate(config, "cert.pem", "key.pem") + + configuration, err := api.NewConfiguration( + config.vendorCode, config.deviceBrand, config.deviceModel, config.serialNumber, + []shipapi.DeviceCategoryType{ + shipapi.DeviceCategoryTypeEnergyManagementSystem, + }, + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{ + model.EntityTypeTypeGridGuard, + model.EntityTypeTypeCEM, + }, + 23292, certificate, time.Second*4) + if *iface != "" { + configuration.SetInterfaces([]string{*iface}) + log.Printf("waiting until %v is up", iface) + for { + ifi, err := net.InterfaceByName(*iface) + if err != nil { + log.Fatal(err) + } + + // wait until interface is up and available for multicast + flags := net.FlagUp | net.FlagMulticast + if (ifi.Flags & flags) == flags { + break + } + time.Sleep(1 * time.Second) + } + log.Printf("interface online, continuing") + } + + r, err := NewRemote(configuration) + if err != nil { + log.Fatal(err) + } + + err = r.RegisterUseCase(model.EntityTypeTypeCEM, "EG-LPC", func(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) api.UseCaseInterface { + return eglpc.NewLPC(localEntity, eventCB) + }) + if err != nil { + log.Fatal(err) + } + + err = r.RegisterUseCase(model.EntityTypeTypeCEM, "MA-MPC", func(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) api.UseCaseInterface { + return mpc.NewMPC(localEntity, eventCB) + }) + if err != nil { + log.Fatal(err) + } + + err = r.RegisterUseCase(model.EntityTypeTypeCEM, "CEM-EVCC", func(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) api.UseCaseInterface { + return evcc.NewEVCC(r.service, localEntity, eventCB) + }) + if err != nil { + log.Fatal(err) + } + + err = r.RegisterUseCase(model.EntityTypeTypeCEM, "CEM-EVSECC", func(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) api.UseCaseInterface { + return evsecc.NewEVSECC(localEntity, eventCB) + }) + if err != nil { + log.Fatal(err) + } + + ctx, cancelCtx := context.WithCancel(context.Background()) + if err = r.Listen(ctx, "tcp", net.JoinHostPort("::", strconv.Itoa(3393))); err != nil { + log.Fatal(err) + } + log.Print("Started") + + // Clean exit to make sure mdns shutdown is invoked + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGTERM) + <-sig + // User exit + + cancelCtx() +} diff --git a/cmd/remote/reflection.go b/cmd/remote/reflection.go new file mode 100644 index 00000000..89a12c77 --- /dev/null +++ b/cmd/remote/reflection.go @@ -0,0 +1,231 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "go/token" + "log" + "reflect" + "strings" + + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "golang.org/x/exp/jsonrpc2" +) + +type rpcServiceFunc interface { + Call(*Remote, string, json.RawMessage) ([]interface{}, error) +} + +func callMethod(remote *Remote, methodName string, method reflect.Value, params []json.RawMessage) ([]interface{}, error) { + methodType := method.Type() + neededParams := methodType.NumIn() + + if len(params) != neededParams { + return nil, jsonrpc2.ErrInvalidParams + } + + var decodedParams []interface{} + for idx := 0; idx < neededParams; idx++ { + paramType := methodType.In(idx) + + var paramValue reflect.Value + if paramType == reflect.TypeFor[spineapi.DeviceRemoteInterface]() { + // convert between DeviceRemoteInterface and DeviceAddressType + paramValue = reflect.New(reflect.TypeFor[model.DeviceAddressType]()) + } else if paramType == reflect.TypeFor[spineapi.EntityRemoteInterface]() { + // convert between EntityRemoteInterface and EntityAddressType + paramValue = reflect.New(reflect.TypeFor[model.EntityAddressType]()) + } else { + paramValue = reflect.New(paramType) + } + + param := paramValue.Interface() + if err := json.Unmarshal(params[idx], ¶m); err != nil { + return nil, jsonrpc2.ErrParse + } + decodedParams = append(decodedParams, param) + } + log.Printf("decoded: %v(%v)", methodName, decodedParams) + + if len(decodedParams) != neededParams { + return nil, jsonrpc2.ErrInvalidParams + } + + methodParams := make([]reflect.Value, neededParams) + for dstIndex := 0; dstIndex < neededParams; dstIndex++ { + paramType := methodType.In(dstIndex) + paramIndex := dstIndex + + if paramType == reflect.TypeFor[spineapi.DeviceRemoteInterface]() { + // convert between DeviceRemoteInterface and DeviceAddressType + address, ok := decodedParams[paramIndex].(*model.DeviceAddressType) + if !ok || address.Device == nil { + return nil, jsonrpc2.ErrInvalidParams + } + + deviceInterface := remote.service.LocalDevice().RemoteDeviceForAddress(*address.Device) + if deviceInterface == nil { + return nil, jsonrpc2.ErrInvalidParams + } + + methodParams[dstIndex] = reflect.ValueOf(deviceInterface) + } else if paramType == reflect.TypeFor[spineapi.EntityRemoteInterface]() { + // convert between EntityRemoteInterface and EntityAddressType + address, ok := decodedParams[paramIndex].(*model.EntityAddressType) + if !ok || address.Device == nil { + return nil, jsonrpc2.ErrInvalidParams + } + + deviceInterface := remote.service.LocalDevice().RemoteDeviceForAddress(*address.Device) + if deviceInterface == nil { + return nil, jsonrpc2.ErrInvalidParams + } + + entityInterface := deviceInterface.Entity(address.Entity) + if entityInterface == nil { + return nil, jsonrpc2.ErrInvalidParams + } + + methodParams[dstIndex] = reflect.ValueOf(entityInterface) + } else if decodedParams[paramIndex] == nil { + // some parameters are optional and allowed to be nil + methodParams[dstIndex] = reflect.New(paramType).Elem() + } else { + methodParams[dstIndex] = reflect.ValueOf(decodedParams[paramIndex]).Elem() + } + } + + output := method.Call(methodParams) + + return transformReturnValues(output), nil +} + +type dynamicReceiverProxy struct{} + +func (svc dynamicReceiverProxy) Call(remote *Remote, methodName string, params json.RawMessage) ([]interface{}, error) { + decodedParams := []json.RawMessage{} + if len(params) > 0 { + if err := json.Unmarshal(params, &decodedParams); err != nil { + return nil, jsonrpc2.ErrParse + } + } + log.Printf("decoded: %v(%v)", methodName, decodedParams) + + var deviceAddress model.AddressDeviceType + var entityAddress model.EntityAddressType + switch { + case json.Unmarshal(decodedParams[0], &deviceAddress) == nil: + deviceInterface := remote.service.LocalDevice().RemoteDeviceForAddress(deviceAddress) + if deviceInterface == nil { + return nil, jsonrpc2.ErrInvalidParams + } + + return svc.call(remote, deviceInterface, methodName, decodedParams[1:]) + case json.Unmarshal(decodedParams[0], &entityAddress) == nil: + deviceInterface := remote.service.LocalDevice().RemoteDeviceForAddress(*entityAddress.Device) + if deviceInterface == nil { + return nil, jsonrpc2.ErrInvalidParams + } + + entityInterface := deviceInterface.Entity(entityAddress.Entity) + if entityInterface == nil { + return nil, jsonrpc2.ErrInvalidParams + } + + return svc.call(remote, entityInterface, methodName, decodedParams[1:]) + default: + return nil, jsonrpc2.ErrMethodNotFound + } +} + +func (svc dynamicReceiverProxy) call(remote *Remote, rcvr any, methodName string, params []json.RawMessage) ([]interface{}, error) { + log.Printf("rcvr: %v", reflect.TypeOf(rcvr)) + method := reflect.ValueOf(rcvr).MethodByName(methodName) + if method.IsZero() { + return nil, jsonrpc2.ErrMethodNotFound + } + + return callMethod(remote, methodName, method, params) +} + +type staticReceiverProxy struct { + name string + rcvr reflect.Value + typ reflect.Type + method map[string]reflect.Value +} + +func newStaticReceiverProxy(rcvr any, name string, useName bool) (*staticReceiverProxy, error) { + c := new(staticReceiverProxy) + c.typ = reflect.TypeOf(rcvr) + c.rcvr = reflect.ValueOf(rcvr) + sname := name + if !useName { + sname = reflect.Indirect(c.rcvr).Type().Name() + } + if sname == "" { + s := "rpc.Register: no service name for type " + c.typ.String() + log.Print(s) + return nil, errors.New(s) + } + if !useName && !token.IsExported(sname) { + s := "rpc.Register: type " + sname + " is not exported" + log.Print(s) + return nil, errors.New(s) + } + sname = strings.ToLower(sname) + c.name = sname + + c.method = make(map[string]reflect.Value) + for m := 0; m < c.typ.NumMethod(); m++ { + method := c.typ.Method(m) + mtype := method.Type + mname := method.Name + + // Method must be exported + if !method.IsExported() { + continue + } + + // all (non-receiver) arguments must be builtin or exported + for i := 1; i < mtype.NumIn(); i++ { + argType := mtype.In(i) + if !isExportedOrBuiltinType(argType) { + panic(fmt.Sprintf("UseCaseProxy.Register: argument type of method %q is not exported: %q\n", mname, argType)) + } + continue + } + for i := 1; i < mtype.NumOut(); i++ { + argType := mtype.Out(i) + if !isExportedOrBuiltinType(argType) { + panic(fmt.Sprintf("UseCaseProxy.Register: return type of method %q is not exported: %q\n", mname, argType)) + } + continue + } + + log.Printf("registering method %s/%s", sname, mname) + // bind receiver into method + c.method[mname] = reflect.ValueOf(rcvr).Method(m) + } + + return c, nil +} + +func (svc *staticReceiverProxy) Call(remote *Remote, methodName string, params json.RawMessage) ([]interface{}, error) { + method, found := svc.method[methodName] + if !found { + return nil, jsonrpc2.ErrNotHandled + } + splitParams := []json.RawMessage{} + if len(params) > 0 { + if err := json.Unmarshal(params, &splitParams); err != nil { + log.Printf("%v", err) + return nil, jsonrpc2.ErrParse + } + } + + return callMethod(remote, methodName, method, splitParams) + +} diff --git a/cmd/remote/rpc.go b/cmd/remote/rpc.go new file mode 100644 index 00000000..7d4f6803 --- /dev/null +++ b/cmd/remote/rpc.go @@ -0,0 +1,266 @@ +package main + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/service" + "golang.org/x/exp/jsonrpc2" + + shipapi "github.com/enbility/ship-go/api" +) + +type Remote struct { + rpc *jsonrpc2.Server + service *service.Service + + connections []*jsonrpc2.Connection + remoteServices []shipapi.RemoteService + + rpcServices map[string]rpcServiceFunc +} + +func NewRemote(configuration *api.Configuration) (*Remote, error) { + r := Remote{ + connections: []*jsonrpc2.Connection{}, + remoteServices: []shipapi.RemoteService{}, + + rpcServices: make(map[string]rpcServiceFunc), + } + r.service = service.NewService(configuration, &r) + + if err := r.service.Setup(); err != nil { + return nil, err + } + r.service.SetLogging(&stdoutLogger{}) + + err := r.registerStaticReceiverProxy("Service", r.service) + if err != nil { + return nil, err + } + + err = r.registerStaticReceiverProxy("Remote", &r) + if err != nil { + return nil, err + } + + err = r.registerStaticReceiverProxy("LocalDevice", r.service.LocalDevice()) + if err != nil { + return nil, err + } + + err = r.registerDynamicReceiverProxy("Call") + if err != nil { + return nil, err + } + + return &r, nil +} + +func (r *Remote) registerStaticReceiverProxy(name string, rcvr any) error { + proxy, err := newStaticReceiverProxy(rcvr, name, true) + if err != nil { + return err + } + r.rpcServices[proxy.name] = proxy + + return nil +} + +func (r *Remote) registerDynamicReceiverProxy(name string) error { + r.rpcServices[strings.ToLower(name)] = dynamicReceiverProxy{} + + return nil +} + +func (r Remote) RemoteServices() []shipapi.RemoteService { + return r.remoteServices +} + +func (r Remote) ConnectedDevices() []string { + remoteDevices := r.service.LocalDevice().RemoteDevices() + skiList := make([]string, len(remoteDevices)) + + for i, dev := range remoteDevices { + skiList[i] = dev.Ski() + } + + return skiList +} + +func (r Remote) LocalSKI() string { + return r.service.LocalService().SKI() +} + +func (r *Remote) Bind(context context.Context, conn *jsonrpc2.Connection) (jsonrpc2.ConnectionOptions, error) { + connOpts := jsonrpc2.ConnectionOptions{ + Framer: NewlineFramer{}, + Preempter: nil, + Handler: jsonrpc2.HandlerFunc(r.handleRPC), + } + + r.connections = append(r.connections, conn) + return connOpts, nil +} + +func (r *Remote) Listen(context context.Context, network, address string) error { + listener, err := jsonrpc2.NetListener(context, network, address, jsonrpc2.NetListenOptions{}) + if err != nil { + return err + } + + conn, err := jsonrpc2.Serve(context, listener, r) + if err != nil { + return err + } + r.rpc = conn + + r.service.Start() + go func() { + <-context.Done() + r.service.Shutdown() + }() + + return nil +} + +func (r *Remote) handleRPC(ctx context.Context, req *jsonrpc2.Request) (interface{}, error) { + if req.IsCall() { + slash := strings.LastIndex(req.Method, "/") + if slash < 0 { + return nil, jsonrpc2.ErrMethodNotFound + } + serviceName := strings.ToLower(req.Method[:slash]) + methodName := req.Method[slash+1:] + + svc, found := r.rpcServices[serviceName] + if !found { + return nil, jsonrpc2.ErrMethodNotFound + } + + output, err := svc.Call(r, methodName, req.Params) + if err != nil { + return nil, err + } + + var resp interface{} + numOut := len(output) + switch numOut { + case 0: + resp = []interface{}{} + default: + resp = output + } + return resp, nil + } else { + // RPC Notification + // TODO: implement + } + + return nil, nil +} + +// Implement api.ServiceReaderInterface +func (r Remote) RemoteSKIConnected(service api.ServiceInterface, ski string) { + // necessary because RemoteSKIConnected is called before remote device actually exists + go func() { + params := make(map[string]interface{}, 1) + params["ski"] = ski + + for { + // wait until RemoteDevice available for SKI + device := service.LocalDevice().RemoteDeviceForSki(ski) + if device != nil && device.Address() != nil { + params["device"] = *device.Address() + break + } + time.Sleep(1 * time.Second) + } + + for _, conn := range r.connections { + _ = conn.Notify(context.Background(), "remote/RemoteSKIConnected", params) + } + }() +} + +func (r Remote) RemoteSKIDisconnected(service api.ServiceInterface, ski string) { + params := make(map[string]interface{}, 1) + params["ski"] = ski + for _, conn := range r.connections { + _ = conn.Notify(context.Background(), "remote/RemoteSKIDisconnected", params) + } +} + +func (r *Remote) VisibleRemoteServicesUpdated(service api.ServiceInterface, entries []shipapi.RemoteService) { + r.remoteServices = entries + + for _, conn := range r.connections { + _ = conn.Notify(context.Background(), "remote/VisibleRemoteServicesUpdated", entries) + } +} + +func (r Remote) ServiceShipIDUpdate(ski string, shipID string) { + params := make(map[string]interface{}, 2) + params["ski"] = ski + params["shipID"] = shipID + + for _, conn := range r.connections { + _ = conn.Notify(context.Background(), "remote/ServiceShipIDUpdate", params) + } +} + +func (r Remote) ServicePairingDetailUpdate(ski string, detail *shipapi.ConnectionStateDetail) { +} + +// Logging interface + +type stdoutLogger struct{} + +func (l *stdoutLogger) Trace(args ...interface{}) { + // l.print("TRACE", args...) +} + +func (l *stdoutLogger) Tracef(format string, args ...interface{}) { + // l.printFormat("TRACE", format, args...) +} + +func (l *stdoutLogger) Debug(args ...interface{}) { + // l.print("DEBUG", args...) +} + +func (l *stdoutLogger) Debugf(format string, args ...interface{}) { + // l.printFormat("DEBUG", format, args...) +} + +func (l *stdoutLogger) Info(args ...interface{}) { + l.print("INFO ", args...) +} + +func (l *stdoutLogger) Infof(format string, args ...interface{}) { + l.printFormat("INFO ", format, args...) +} + +func (l *stdoutLogger) Error(args ...interface{}) { + l.print("ERROR", args...) +} + +func (l *stdoutLogger) Errorf(format string, args ...interface{}) { + l.printFormat("ERROR", format, args...) +} + +func (l *stdoutLogger) currentTimestamp() string { + return time.Now().Format("2006-01-02 15:04:05") +} + +func (l *stdoutLogger) print(msgType string, args ...interface{}) { + value := fmt.Sprintln(args...) + fmt.Printf("%s %s %s", l.currentTimestamp(), msgType, value) +} + +func (l *stdoutLogger) printFormat(msgType, format string, args ...interface{}) { + value := fmt.Sprintf(format, args...) + fmt.Println(l.currentTimestamp(), msgType, value) +} diff --git a/cmd/remote/ucs.go b/cmd/remote/ucs.go new file mode 100644 index 00000000..d1eead23 --- /dev/null +++ b/cmd/remote/ucs.go @@ -0,0 +1,53 @@ +package main + +import ( + "context" + "fmt" + + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +type UseCaseId string +type UseCaseTypeType string + +const ( + UseCaseTypeLPC UseCaseTypeType = "LPC" +) + +type UseCaseBuilder func(spineapi.EntityLocalInterface, api.EntityEventCallback) api.UseCaseInterface + +func (r *Remote) RegisterUseCase(entityType model.EntityTypeType, usecaseId string, builder UseCaseBuilder) error { + // entityType/uc + var identifier UseCaseId = UseCaseId(fmt.Sprintf("%s/%s", entityType, usecaseId)) + + localInterface := r.service.LocalDevice().EntityForType(entityType) + uc := builder(localInterface, func( + ski string, + device spineapi.DeviceRemoteInterface, + entity spineapi.EntityRemoteInterface, + event api.EventType, + ) { + r.PropagateEvent(identifier, ski, device, entity, event) + }) + r.service.AddUseCase(uc) + + return r.registerStaticReceiverProxy(usecaseId, uc) +} + +func (r *Remote) PropagateEvent( + id UseCaseId, + ski string, + device spineapi.DeviceRemoteInterface, + entity spineapi.EntityRemoteInterface, + event api.EventType, +) { + params := make(map[string]interface{}, 2) + params["ski"] = ski + params["device"] = device.Address() + params["entity"] = entity.Address() + for _, conn := range r.connections { + _ = conn.Notify(context.Background(), string(event), params) + } +} diff --git a/cmd/remote/util.go b/cmd/remote/util.go new file mode 100644 index 00000000..2a3bb891 --- /dev/null +++ b/cmd/remote/util.go @@ -0,0 +1,100 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "go/token" + "os" + "reflect" + + "github.com/enbility/spine-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Is this type exported or a builtin? +func isExportedOrBuiltinType(t reflect.Type) bool { + for t.Kind() == reflect.Pointer { + t = t.Elem() + } + // PkgPath will be non-empty even for an exported type, + // so we need to check the type name as well. + return token.IsExported(t.Name()) || t.PkgPath() == "" +} + +func transformReturnValues(values []reflect.Value) []interface{} { + result := make([]interface{}, len(values)) + + for i, e := range values { + switch e.Type() { + case reflect.TypeFor[spineapi.DeviceRemoteInterface](): + result[i] = e.Interface().(spineapi.DeviceRemoteInterface).Address() + case reflect.TypeFor[[]spineapi.DeviceRemoteInterface](): + rawValues := e.Interface().([]api.DeviceRemoteInterface) + transformedValues := make([]model.AddressDeviceType, len(rawValues)) + + for j, r := range rawValues { + transformedValues[j] = *r.Address() + } + result[i] = transformedValues + case reflect.TypeFor[spineapi.EntityRemoteInterface](): + result[i] = e.Interface().(spineapi.EntityRemoteInterface).Address() + case reflect.TypeFor[[]spineapi.EntityRemoteInterface](): + rawValues := e.Interface().([]api.EntityRemoteInterface) + transformedValues := make([]model.EntityAddressType, len(rawValues)) + + for j, r := range rawValues { + transformedValues[j] = *r.Address() + } + result[i] = transformedValues + default: + result[i] = e.Interface() + } + } + + return result +} +func WriteKey(cert tls.Certificate, path string) error { + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + + switch v := cert.PrivateKey.(type) { + case *ecdsa.PrivateKey: + bytes, err := x509.MarshalECPrivateKey(v) + if err != nil { + return err + } + + err = pem.Encode(file, &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: bytes, + }) + default: + return fmt.Errorf("Unable to serialize private key of type %T", v) + } + + return nil +} + +func WriteCertificate(cert tls.Certificate, path string) error { + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + + for _, leaf := range cert.Certificate { + err = pem.Encode(file, &pem.Block{Type: "CERTIFICATE", Bytes: leaf}) + if err != nil { + return err + } + } + + return nil +} diff --git a/go.mod b/go.mod index b960fd80..09d5424d 100644 --- a/go.mod +++ b/go.mod @@ -23,11 +23,14 @@ require ( github.com/stretchr/objx v0.5.2 // indirect gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect go.uber.org/mock v0.4.0 // indirect + golang.org/x/exp/event v0.0.0-20220217172124-1812c5b45e43 // indirect + golang.org/x/exp/jsonrpc2 v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/tools v0.25.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0888b791..e3f10f02 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,10 @@ gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a h1:DxppxFKRqJ8 gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREvu3a57BaK0R1+ztrEzHWiZAihohNLQ6trPxlIqZI= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/exp/event v0.0.0-20220217172124-1812c5b45e43 h1:Yn6OLQDombmcne/0Jf2GiY4qPS5ML2W4KYFyx2uYxGY= +golang.org/x/exp/event v0.0.0-20220217172124-1812c5b45e43/go.mod h1:AVlZHjhWbW/3yOcmKMtJiObwBPJajBlUpQXRijFNrNc= +golang.org/x/exp/jsonrpc2 v0.0.0-20240909161429-701f63a606c0 h1:L2RG+rjAzNncYJI2U7v3rw3Fl2Hk4J27mEjWx6jQhuI= +golang.org/x/exp/jsonrpc2 v0.0.0-20240909161429-701f63a606c0/go.mod h1:Enk5TnT9VR4uKJW7nj3TlYv+R4GOM2KELhqCJxnXVN8= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= @@ -48,6 +52,8 @@ golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=