From 21ac3bc5f536eb6d369f0f740e04ea198cf11510 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 10 Aug 2025 12:45:13 -0500 Subject: [PATCH 01/37] preview Signed-off-by: eternal-flame-AD --- v2/generate.go | 3 + v2/generated/protobuf/config.pb.go | 315 ++++++++++ v2/generated/protobuf/config_grpc.pb.go | 160 +++++ v2/generated/protobuf/display.pb.go | 173 ++++++ v2/generated/protobuf/display_grpc.pb.go | 121 ++++ v2/generated/protobuf/infra.pb.go | 254 ++++++++ v2/generated/protobuf/infra_grpc.pb.go | 236 ++++++++ v2/generated/protobuf/meta.pb.go | 654 +++++++++++++++++++++ v2/generated/protobuf/meta_grpc.pb.go | 224 +++++++ v2/generated/protobuf/webhooker.pb.go | 128 ++++ v2/generated/protobuf/webhooker_grpc.pb.go | 122 ++++ v2/go.mod | 15 + v2/go.sum | 34 ++ v2/protobuf/config.proto | 29 + v2/protobuf/display.proto | 16 + v2/protobuf/infra.proto | 27 + v2/protobuf/meta.proto | 65 ++ v2/protobuf/webhooker.proto | 12 + v2/rpc.go | 38 ++ 19 files changed, 2626 insertions(+) create mode 100644 v2/generate.go create mode 100644 v2/generated/protobuf/config.pb.go create mode 100644 v2/generated/protobuf/config_grpc.pb.go create mode 100644 v2/generated/protobuf/display.pb.go create mode 100644 v2/generated/protobuf/display_grpc.pb.go create mode 100644 v2/generated/protobuf/infra.pb.go create mode 100644 v2/generated/protobuf/infra_grpc.pb.go create mode 100644 v2/generated/protobuf/meta.pb.go create mode 100644 v2/generated/protobuf/meta_grpc.pb.go create mode 100644 v2/generated/protobuf/webhooker.pb.go create mode 100644 v2/generated/protobuf/webhooker_grpc.pb.go create mode 100644 v2/go.mod create mode 100644 v2/go.sum create mode 100644 v2/protobuf/config.proto create mode 100644 v2/protobuf/display.proto create mode 100644 v2/protobuf/infra.proto create mode 100644 v2/protobuf/meta.proto create mode 100644 v2/protobuf/webhooker.proto create mode 100644 v2/rpc.go diff --git a/v2/generate.go b/v2/generate.go new file mode 100644 index 0000000..129cfa0 --- /dev/null +++ b/v2/generate.go @@ -0,0 +1,3 @@ +package plugin + +//go:generate protoc -Iprotobuf --go_out=./generated/protobuf --go_opt=paths=source_relative --go-grpc_out=./generated/protobuf --go-grpc_opt=paths=source_relative ./protobuf/meta.proto ./protobuf/infra.proto ./protobuf/config.proto ./protobuf/display.proto ./protobuf/webhooker.proto diff --git a/v2/generated/protobuf/config.pb.go b/v2/generated/protobuf/config.pb.go new file mode 100644 index 0000000..fc53bda --- /dev/null +++ b/v2/generated/protobuf/config.pb.go @@ -0,0 +1,315 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc v6.31.1 +// source: config.proto + +package protobuf + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +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 Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config string `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetConfig() string { + if x != nil { + return x.Config + } + return "" +} + +type ValidateAndSetConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *Config `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateAndSetConfigRequest) Reset() { + *x = ValidateAndSetConfigRequest{} + mi := &file_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateAndSetConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateAndSetConfigRequest) ProtoMessage() {} + +func (x *ValidateAndSetConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateAndSetConfigRequest.ProtoReflect.Descriptor instead. +func (*ValidateAndSetConfigRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{1} +} + +func (x *ValidateAndSetConfigRequest) GetConfig() *Config { + if x != nil { + return x.Config + } + return nil +} + +type ValidateAndSetConfigSuccessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateAndSetConfigSuccessResponse) Reset() { + *x = ValidateAndSetConfigSuccessResponse{} + mi := &file_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateAndSetConfigSuccessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateAndSetConfigSuccessResponse) ProtoMessage() {} + +func (x *ValidateAndSetConfigSuccessResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateAndSetConfigSuccessResponse.ProtoReflect.Descriptor instead. +func (*ValidateAndSetConfigSuccessResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{2} +} + +type ValidateAndSetConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Response: + // + // *ValidateAndSetConfigResponse_Success + // *ValidateAndSetConfigResponse_Error + Response isValidateAndSetConfigResponse_Response `protobuf_oneof:"response"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateAndSetConfigResponse) Reset() { + *x = ValidateAndSetConfigResponse{} + mi := &file_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateAndSetConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateAndSetConfigResponse) ProtoMessage() {} + +func (x *ValidateAndSetConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateAndSetConfigResponse.ProtoReflect.Descriptor instead. +func (*ValidateAndSetConfigResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{3} +} + +func (x *ValidateAndSetConfigResponse) GetResponse() isValidateAndSetConfigResponse_Response { + if x != nil { + return x.Response + } + return nil +} + +func (x *ValidateAndSetConfigResponse) GetSuccess() *ValidateAndSetConfigSuccessResponse { + if x != nil { + if x, ok := x.Response.(*ValidateAndSetConfigResponse_Success); ok { + return x.Success + } + } + return nil +} + +func (x *ValidateAndSetConfigResponse) GetError() *Error { + if x != nil { + if x, ok := x.Response.(*ValidateAndSetConfigResponse_Error); ok { + return x.Error + } + } + return nil +} + +type isValidateAndSetConfigResponse_Response interface { + isValidateAndSetConfigResponse_Response() +} + +type ValidateAndSetConfigResponse_Success struct { + Success *ValidateAndSetConfigSuccessResponse `protobuf:"bytes,1,opt,name=success,proto3,oneof"` +} + +type ValidateAndSetConfigResponse_Error struct { + Error *Error `protobuf:"bytes,2,opt,name=error,proto3,oneof"` +} + +func (*ValidateAndSetConfigResponse_Success) isValidateAndSetConfigResponse_Response() {} + +func (*ValidateAndSetConfigResponse_Error) isValidateAndSetConfigResponse_Response() {} + +var File_config_proto protoreflect.FileDescriptor + +const file_config_proto_rawDesc = "" + + "\n" + + "\fconfig.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\n" + + "meta.proto\" \n" + + "\x06Config\x12\x16\n" + + "\x06config\x18\x01 \x01(\tR\x06config\">\n" + + "\x1bValidateAndSetConfigRequest\x12\x1f\n" + + "\x06config\x18\x01 \x01(\v2\a.ConfigR\x06config\"%\n" + + "#ValidateAndSetConfigSuccessResponse\"\x8c\x01\n" + + "\x1cValidateAndSetConfigResponse\x12@\n" + + "\asuccess\x18\x01 \x01(\v2$.ValidateAndSetConfigSuccessResponseH\x00R\asuccess\x12\x1e\n" + + "\x05error\x18\x02 \x01(\v2\x06.ErrorH\x00R\x05errorB\n" + + "\n" + + "\bresponse2\x93\x01\n" + + "\n" + + "Configurer\x120\n" + + "\rDefaultConfig\x12\x16.google.protobuf.Empty\x1a\a.Config\x12S\n" + + "\x14ValidateAndSetConfig\x12\x1c.ValidateAndSetConfigRequest\x1a\x1d.ValidateAndSetConfigResponseB\x16Z\x14./generated/protobufb\x06proto3" + +var ( + file_config_proto_rawDescOnce sync.Once + file_config_proto_rawDescData []byte +) + +func file_config_proto_rawDescGZIP() []byte { + file_config_proto_rawDescOnce.Do(func() { + file_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc))) + }) + return file_config_proto_rawDescData +} + +var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_config_proto_goTypes = []any{ + (*Config)(nil), // 0: Config + (*ValidateAndSetConfigRequest)(nil), // 1: ValidateAndSetConfigRequest + (*ValidateAndSetConfigSuccessResponse)(nil), // 2: ValidateAndSetConfigSuccessResponse + (*ValidateAndSetConfigResponse)(nil), // 3: ValidateAndSetConfigResponse + (*Error)(nil), // 4: Error + (*emptypb.Empty)(nil), // 5: google.protobuf.Empty +} +var file_config_proto_depIdxs = []int32{ + 0, // 0: ValidateAndSetConfigRequest.config:type_name -> Config + 2, // 1: ValidateAndSetConfigResponse.success:type_name -> ValidateAndSetConfigSuccessResponse + 4, // 2: ValidateAndSetConfigResponse.error:type_name -> Error + 5, // 3: Configurer.DefaultConfig:input_type -> google.protobuf.Empty + 1, // 4: Configurer.ValidateAndSetConfig:input_type -> ValidateAndSetConfigRequest + 0, // 5: Configurer.DefaultConfig:output_type -> Config + 3, // 6: Configurer.ValidateAndSetConfig:output_type -> ValidateAndSetConfigResponse + 5, // [5:7] is the sub-list for method output_type + 3, // [3:5] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_config_proto_init() } +func file_config_proto_init() { + if File_config_proto != nil { + return + } + file_meta_proto_init() + file_config_proto_msgTypes[3].OneofWrappers = []any{ + (*ValidateAndSetConfigResponse_Success)(nil), + (*ValidateAndSetConfigResponse_Error)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_config_proto_goTypes, + DependencyIndexes: file_config_proto_depIdxs, + MessageInfos: file_config_proto_msgTypes, + }.Build() + File_config_proto = out.File + file_config_proto_goTypes = nil + file_config_proto_depIdxs = nil +} diff --git a/v2/generated/protobuf/config_grpc.pb.go b/v2/generated/protobuf/config_grpc.pb.go new file mode 100644 index 0000000..9532432 --- /dev/null +++ b/v2/generated/protobuf/config_grpc.pb.go @@ -0,0 +1,160 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.31.1 +// source: config.proto + +package protobuf + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// 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.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Configurer_DefaultConfig_FullMethodName = "/Configurer/DefaultConfig" + Configurer_ValidateAndSetConfig_FullMethodName = "/Configurer/ValidateAndSetConfig" +) + +// ConfigurerClient is the client API for Configurer 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 ConfigurerClient interface { + DefaultConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Config, error) + ValidateAndSetConfig(ctx context.Context, in *ValidateAndSetConfigRequest, opts ...grpc.CallOption) (*ValidateAndSetConfigResponse, error) +} + +type configurerClient struct { + cc grpc.ClientConnInterface +} + +func NewConfigurerClient(cc grpc.ClientConnInterface) ConfigurerClient { + return &configurerClient{cc} +} + +func (c *configurerClient) DefaultConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Config, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Config) + err := c.cc.Invoke(ctx, Configurer_DefaultConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *configurerClient) ValidateAndSetConfig(ctx context.Context, in *ValidateAndSetConfigRequest, opts ...grpc.CallOption) (*ValidateAndSetConfigResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ValidateAndSetConfigResponse) + err := c.cc.Invoke(ctx, Configurer_ValidateAndSetConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ConfigurerServer is the server API for Configurer service. +// All implementations must embed UnimplementedConfigurerServer +// for forward compatibility. +type ConfigurerServer interface { + DefaultConfig(context.Context, *emptypb.Empty) (*Config, error) + ValidateAndSetConfig(context.Context, *ValidateAndSetConfigRequest) (*ValidateAndSetConfigResponse, error) + mustEmbedUnimplementedConfigurerServer() +} + +// UnimplementedConfigurerServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedConfigurerServer struct{} + +func (UnimplementedConfigurerServer) DefaultConfig(context.Context, *emptypb.Empty) (*Config, error) { + return nil, status.Errorf(codes.Unimplemented, "method DefaultConfig not implemented") +} +func (UnimplementedConfigurerServer) ValidateAndSetConfig(context.Context, *ValidateAndSetConfigRequest) (*ValidateAndSetConfigResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ValidateAndSetConfig not implemented") +} +func (UnimplementedConfigurerServer) mustEmbedUnimplementedConfigurerServer() {} +func (UnimplementedConfigurerServer) testEmbeddedByValue() {} + +// UnsafeConfigurerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ConfigurerServer will +// result in compilation errors. +type UnsafeConfigurerServer interface { + mustEmbedUnimplementedConfigurerServer() +} + +func RegisterConfigurerServer(s grpc.ServiceRegistrar, srv ConfigurerServer) { + // If the following call pancis, it indicates UnimplementedConfigurerServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Configurer_ServiceDesc, srv) +} + +func _Configurer_DefaultConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ConfigurerServer).DefaultConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Configurer_DefaultConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ConfigurerServer).DefaultConfig(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _Configurer_ValidateAndSetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ValidateAndSetConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ConfigurerServer).ValidateAndSetConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Configurer_ValidateAndSetConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ConfigurerServer).ValidateAndSetConfig(ctx, req.(*ValidateAndSetConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Configurer_ServiceDesc is the grpc.ServiceDesc for Configurer service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Configurer_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "Configurer", + HandlerType: (*ConfigurerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "DefaultConfig", + Handler: _Configurer_DefaultConfig_Handler, + }, + { + MethodName: "ValidateAndSetConfig", + Handler: _Configurer_ValidateAndSetConfig_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "config.proto", +} diff --git a/v2/generated/protobuf/display.pb.go b/v2/generated/protobuf/display.pb.go new file mode 100644 index 0000000..0060e9d --- /dev/null +++ b/v2/generated/protobuf/display.pb.go @@ -0,0 +1,173 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc v6.31.1 +// source: display.proto + +package protobuf + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +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 DisplayRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Location string `protobuf:"bytes,1,opt,name=location,proto3" json:"location,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisplayRequest) Reset() { + *x = DisplayRequest{} + mi := &file_display_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisplayRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisplayRequest) ProtoMessage() {} + +func (x *DisplayRequest) ProtoReflect() protoreflect.Message { + mi := &file_display_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisplayRequest.ProtoReflect.Descriptor instead. +func (*DisplayRequest) Descriptor() ([]byte, []int) { + return file_display_proto_rawDescGZIP(), []int{0} +} + +func (x *DisplayRequest) GetLocation() string { + if x != nil { + return x.Location + } + return "" +} + +type DisplayResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Display string `protobuf:"bytes,1,opt,name=display,proto3" json:"display,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisplayResponse) Reset() { + *x = DisplayResponse{} + mi := &file_display_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisplayResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisplayResponse) ProtoMessage() {} + +func (x *DisplayResponse) ProtoReflect() protoreflect.Message { + mi := &file_display_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisplayResponse.ProtoReflect.Descriptor instead. +func (*DisplayResponse) Descriptor() ([]byte, []int) { + return file_display_proto_rawDescGZIP(), []int{1} +} + +func (x *DisplayResponse) GetDisplay() string { + if x != nil { + return x.Display + } + return "" +} + +var File_display_proto protoreflect.FileDescriptor + +const file_display_proto_rawDesc = "" + + "\n" + + "\rdisplay.proto\",\n" + + "\x0eDisplayRequest\x12\x1a\n" + + "\blocation\x18\x01 \x01(\tR\blocation\"+\n" + + "\x0fDisplayResponse\x12\x18\n" + + "\adisplay\x18\x01 \x01(\tR\adisplay29\n" + + "\tDisplayer\x12,\n" + + "\aDisplay\x12\x0f.DisplayRequest\x1a\x10.DisplayResponseB\x16Z\x14./generated/protobufb\x06proto3" + +var ( + file_display_proto_rawDescOnce sync.Once + file_display_proto_rawDescData []byte +) + +func file_display_proto_rawDescGZIP() []byte { + file_display_proto_rawDescOnce.Do(func() { + file_display_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_display_proto_rawDesc), len(file_display_proto_rawDesc))) + }) + return file_display_proto_rawDescData +} + +var file_display_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_display_proto_goTypes = []any{ + (*DisplayRequest)(nil), // 0: DisplayRequest + (*DisplayResponse)(nil), // 1: DisplayResponse +} +var file_display_proto_depIdxs = []int32{ + 0, // 0: Displayer.Display:input_type -> DisplayRequest + 1, // 1: Displayer.Display:output_type -> DisplayResponse + 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_display_proto_init() } +func file_display_proto_init() { + if File_display_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_display_proto_rawDesc), len(file_display_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_display_proto_goTypes, + DependencyIndexes: file_display_proto_depIdxs, + MessageInfos: file_display_proto_msgTypes, + }.Build() + File_display_proto = out.File + file_display_proto_goTypes = nil + file_display_proto_depIdxs = nil +} diff --git a/v2/generated/protobuf/display_grpc.pb.go b/v2/generated/protobuf/display_grpc.pb.go new file mode 100644 index 0000000..91bb439 --- /dev/null +++ b/v2/generated/protobuf/display_grpc.pb.go @@ -0,0 +1,121 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.31.1 +// source: display.proto + +package protobuf + +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.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Displayer_Display_FullMethodName = "/Displayer/Display" +) + +// DisplayerClient is the client API for Displayer 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 DisplayerClient interface { + Display(ctx context.Context, in *DisplayRequest, opts ...grpc.CallOption) (*DisplayResponse, error) +} + +type displayerClient struct { + cc grpc.ClientConnInterface +} + +func NewDisplayerClient(cc grpc.ClientConnInterface) DisplayerClient { + return &displayerClient{cc} +} + +func (c *displayerClient) Display(ctx context.Context, in *DisplayRequest, opts ...grpc.CallOption) (*DisplayResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DisplayResponse) + err := c.cc.Invoke(ctx, Displayer_Display_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DisplayerServer is the server API for Displayer service. +// All implementations must embed UnimplementedDisplayerServer +// for forward compatibility. +type DisplayerServer interface { + Display(context.Context, *DisplayRequest) (*DisplayResponse, error) + mustEmbedUnimplementedDisplayerServer() +} + +// UnimplementedDisplayerServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDisplayerServer struct{} + +func (UnimplementedDisplayerServer) Display(context.Context, *DisplayRequest) (*DisplayResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Display not implemented") +} +func (UnimplementedDisplayerServer) mustEmbedUnimplementedDisplayerServer() {} +func (UnimplementedDisplayerServer) testEmbeddedByValue() {} + +// UnsafeDisplayerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DisplayerServer will +// result in compilation errors. +type UnsafeDisplayerServer interface { + mustEmbedUnimplementedDisplayerServer() +} + +func RegisterDisplayerServer(s grpc.ServiceRegistrar, srv DisplayerServer) { + // If the following call pancis, it indicates UnimplementedDisplayerServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Displayer_ServiceDesc, srv) +} + +func _Displayer_Display_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DisplayRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DisplayerServer).Display(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Displayer_Display_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DisplayerServer).Display(ctx, req.(*DisplayRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Displayer_ServiceDesc is the grpc.ServiceDesc for Displayer service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Displayer_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "Displayer", + HandlerType: (*DisplayerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Display", + Handler: _Displayer_Display_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "display.proto", +} diff --git a/v2/generated/protobuf/infra.pb.go b/v2/generated/protobuf/infra.pb.go new file mode 100644 index 0000000..d82c0c5 --- /dev/null +++ b/v2/generated/protobuf/infra.pb.go @@ -0,0 +1,254 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc v6.31.1 +// source: infra.proto + +package protobuf + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +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 ServerVersionInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Commit string `protobuf:"bytes,2,opt,name=commit,proto3" json:"commit,omitempty"` + BuildDate string `protobuf:"bytes,3,opt,name=buildDate,proto3" json:"buildDate,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerVersionInfo) Reset() { + *x = ServerVersionInfo{} + mi := &file_infra_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerVersionInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerVersionInfo) ProtoMessage() {} + +func (x *ServerVersionInfo) ProtoReflect() protoreflect.Message { + mi := &file_infra_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerVersionInfo.ProtoReflect.Descriptor instead. +func (*ServerVersionInfo) Descriptor() ([]byte, []int) { + return file_infra_proto_rawDescGZIP(), []int{0} +} + +func (x *ServerVersionInfo) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *ServerVersionInfo) GetCommit() string { + if x != nil { + return x.Commit + } + return "" +} + +func (x *ServerVersionInfo) GetBuildDate() string { + if x != nil { + return x.BuildDate + } + return "" +} + +type StorageSaveRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StorageSaveRequest) Reset() { + *x = StorageSaveRequest{} + mi := &file_infra_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StorageSaveRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StorageSaveRequest) ProtoMessage() {} + +func (x *StorageSaveRequest) ProtoReflect() protoreflect.Message { + mi := &file_infra_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StorageSaveRequest.ProtoReflect.Descriptor instead. +func (*StorageSaveRequest) Descriptor() ([]byte, []int) { + return file_infra_proto_rawDescGZIP(), []int{1} +} + +func (x *StorageSaveRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type StorageLoadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StorageLoadResponse) Reset() { + *x = StorageLoadResponse{} + mi := &file_infra_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StorageLoadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StorageLoadResponse) ProtoMessage() {} + +func (x *StorageLoadResponse) ProtoReflect() protoreflect.Message { + mi := &file_infra_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StorageLoadResponse.ProtoReflect.Descriptor instead. +func (*StorageLoadResponse) Descriptor() ([]byte, []int) { + return file_infra_proto_rawDescGZIP(), []int{2} +} + +func (x *StorageLoadResponse) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +var File_infra_proto protoreflect.FileDescriptor + +const file_infra_proto_rawDesc = "" + + "\n" + + "\vinfra.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\n" + + "meta.proto\"c\n" + + "\x11ServerVersionInfo\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + + "\x06commit\x18\x02 \x01(\tR\x06commit\x12\x1c\n" + + "\tbuildDate\x18\x03 \x01(\tR\tbuildDate\"(\n" + + "\x12StorageSaveRequest\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data\")\n" + + "\x13StorageLoadResponse\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data2\xef\x01\n" + + "\x05Infra\x12>\n" + + "\x10GetServerVersion\x12\x16.google.protobuf.Empty\x1a\x12.ServerVersionInfo\x129\n" + + "\n" + + "SaveConfig\x12\x13.StorageSaveRequest\x1a\x16.google.protobuf.Empty\x12:\n" + + "\n" + + "LoadConfig\x12\x16.google.protobuf.Empty\x1a\x14.StorageLoadResponse\x12/\n" + + "\vSendMessage\x12\b.Message\x1a\x16.google.protobuf.EmptyB\x16Z\x14./generated/protobufb\x06proto3" + +var ( + file_infra_proto_rawDescOnce sync.Once + file_infra_proto_rawDescData []byte +) + +func file_infra_proto_rawDescGZIP() []byte { + file_infra_proto_rawDescOnce.Do(func() { + file_infra_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_infra_proto_rawDesc), len(file_infra_proto_rawDesc))) + }) + return file_infra_proto_rawDescData +} + +var file_infra_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_infra_proto_goTypes = []any{ + (*ServerVersionInfo)(nil), // 0: ServerVersionInfo + (*StorageSaveRequest)(nil), // 1: StorageSaveRequest + (*StorageLoadResponse)(nil), // 2: StorageLoadResponse + (*emptypb.Empty)(nil), // 3: google.protobuf.Empty + (*Message)(nil), // 4: Message +} +var file_infra_proto_depIdxs = []int32{ + 3, // 0: Infra.GetServerVersion:input_type -> google.protobuf.Empty + 1, // 1: Infra.SaveConfig:input_type -> StorageSaveRequest + 3, // 2: Infra.LoadConfig:input_type -> google.protobuf.Empty + 4, // 3: Infra.SendMessage:input_type -> Message + 0, // 4: Infra.GetServerVersion:output_type -> ServerVersionInfo + 3, // 5: Infra.SaveConfig:output_type -> google.protobuf.Empty + 2, // 6: Infra.LoadConfig:output_type -> StorageLoadResponse + 3, // 7: Infra.SendMessage:output_type -> google.protobuf.Empty + 4, // [4:8] is the sub-list for method output_type + 0, // [0:4] 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_infra_proto_init() } +func file_infra_proto_init() { + if File_infra_proto != nil { + return + } + file_meta_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_infra_proto_rawDesc), len(file_infra_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_infra_proto_goTypes, + DependencyIndexes: file_infra_proto_depIdxs, + MessageInfos: file_infra_proto_msgTypes, + }.Build() + File_infra_proto = out.File + file_infra_proto_goTypes = nil + file_infra_proto_depIdxs = nil +} diff --git a/v2/generated/protobuf/infra_grpc.pb.go b/v2/generated/protobuf/infra_grpc.pb.go new file mode 100644 index 0000000..20f18fe --- /dev/null +++ b/v2/generated/protobuf/infra_grpc.pb.go @@ -0,0 +1,236 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.31.1 +// source: infra.proto + +package protobuf + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// 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.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Infra_GetServerVersion_FullMethodName = "/Infra/GetServerVersion" + Infra_SaveConfig_FullMethodName = "/Infra/SaveConfig" + Infra_LoadConfig_FullMethodName = "/Infra/LoadConfig" + Infra_SendMessage_FullMethodName = "/Infra/SendMessage" +) + +// InfraClient is the client API for Infra 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 InfraClient interface { + GetServerVersion(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ServerVersionInfo, error) + SaveConfig(ctx context.Context, in *StorageSaveRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + LoadConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StorageLoadResponse, error) + SendMessage(ctx context.Context, in *Message, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type infraClient struct { + cc grpc.ClientConnInterface +} + +func NewInfraClient(cc grpc.ClientConnInterface) InfraClient { + return &infraClient{cc} +} + +func (c *infraClient) GetServerVersion(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ServerVersionInfo, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ServerVersionInfo) + err := c.cc.Invoke(ctx, Infra_GetServerVersion_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *infraClient) SaveConfig(ctx context.Context, in *StorageSaveRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Infra_SaveConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *infraClient) LoadConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StorageLoadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StorageLoadResponse) + err := c.cc.Invoke(ctx, Infra_LoadConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *infraClient) SendMessage(ctx context.Context, in *Message, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Infra_SendMessage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// InfraServer is the server API for Infra service. +// All implementations must embed UnimplementedInfraServer +// for forward compatibility. +type InfraServer interface { + GetServerVersion(context.Context, *emptypb.Empty) (*ServerVersionInfo, error) + SaveConfig(context.Context, *StorageSaveRequest) (*emptypb.Empty, error) + LoadConfig(context.Context, *emptypb.Empty) (*StorageLoadResponse, error) + SendMessage(context.Context, *Message) (*emptypb.Empty, error) + mustEmbedUnimplementedInfraServer() +} + +// UnimplementedInfraServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedInfraServer struct{} + +func (UnimplementedInfraServer) GetServerVersion(context.Context, *emptypb.Empty) (*ServerVersionInfo, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetServerVersion not implemented") +} +func (UnimplementedInfraServer) SaveConfig(context.Context, *StorageSaveRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method SaveConfig not implemented") +} +func (UnimplementedInfraServer) LoadConfig(context.Context, *emptypb.Empty) (*StorageLoadResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method LoadConfig not implemented") +} +func (UnimplementedInfraServer) SendMessage(context.Context, *Message) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method SendMessage not implemented") +} +func (UnimplementedInfraServer) mustEmbedUnimplementedInfraServer() {} +func (UnimplementedInfraServer) testEmbeddedByValue() {} + +// UnsafeInfraServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to InfraServer will +// result in compilation errors. +type UnsafeInfraServer interface { + mustEmbedUnimplementedInfraServer() +} + +func RegisterInfraServer(s grpc.ServiceRegistrar, srv InfraServer) { + // If the following call pancis, it indicates UnimplementedInfraServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Infra_ServiceDesc, srv) +} + +func _Infra_GetServerVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InfraServer).GetServerVersion(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Infra_GetServerVersion_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InfraServer).GetServerVersion(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _Infra_SaveConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StorageSaveRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InfraServer).SaveConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Infra_SaveConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InfraServer).SaveConfig(ctx, req.(*StorageSaveRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Infra_LoadConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InfraServer).LoadConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Infra_LoadConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InfraServer).LoadConfig(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _Infra_SendMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Message) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InfraServer).SendMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Infra_SendMessage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InfraServer).SendMessage(ctx, req.(*Message)) + } + return interceptor(ctx, in, info, handler) +} + +// Infra_ServiceDesc is the grpc.ServiceDesc for Infra service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Infra_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "Infra", + HandlerType: (*InfraServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetServerVersion", + Handler: _Infra_GetServerVersion_Handler, + }, + { + MethodName: "SaveConfig", + Handler: _Infra_SaveConfig_Handler, + }, + { + MethodName: "LoadConfig", + Handler: _Infra_LoadConfig_Handler, + }, + { + MethodName: "SendMessage", + Handler: _Infra_SendMessage_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "infra.proto", +} diff --git a/v2/generated/protobuf/meta.pb.go b/v2/generated/protobuf/meta.pb.go new file mode 100644 index 0000000..4f3f58e --- /dev/null +++ b/v2/generated/protobuf/meta.pb.go @@ -0,0 +1,654 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc v6.31.1 +// source: meta.proto + +package protobuf + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + anypb "google.golang.org/protobuf/types/known/anypb" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +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 Error struct { + state protoimpl.MessageState `protogen:"open.v1"` + Details *anypb.Any `protobuf:"bytes,1,opt,name=details,proto3" json:"details,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Error) Reset() { + *x = Error{} + mi := &file_meta_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Error) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Error) ProtoMessage() {} + +func (x *Error) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Error.ProtoReflect.Descriptor instead. +func (*Error) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{0} +} + +func (x *Error) GetDetails() *anypb.Any { + if x != nil { + return x.Details + } + return nil +} + +func (x *Error) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *Error) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +type UserContext struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Admin bool `protobuf:"varint,3,opt,name=admin,proto3" json:"admin,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserContext) Reset() { + *x = UserContext{} + mi := &file_meta_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserContext) ProtoMessage() {} + +func (x *UserContext) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserContext.ProtoReflect.Descriptor instead. +func (*UserContext) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{1} +} + +func (x *UserContext) GetId() uint64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *UserContext) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UserContext) GetAdmin() bool { + if x != nil { + return x.Admin + } + return false +} + +type Info struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Author string `protobuf:"bytes,2,opt,name=author,proto3" json:"author,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Website string `protobuf:"bytes,4,opt,name=website,proto3" json:"website,omitempty"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + License string `protobuf:"bytes,6,opt,name=license,proto3" json:"license,omitempty"` + ModulePath string `protobuf:"bytes,7,opt,name=module_path,json=modulePath,proto3" json:"module_path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Info) Reset() { + *x = Info{} + mi := &file_meta_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Info) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Info) ProtoMessage() {} + +func (x *Info) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Info.ProtoReflect.Descriptor instead. +func (*Info) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{2} +} + +func (x *Info) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *Info) GetAuthor() string { + if x != nil { + return x.Author + } + return "" +} + +func (x *Info) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Info) GetWebsite() string { + if x != nil { + return x.Website + } + return "" +} + +func (x *Info) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Info) GetLicense() string { + if x != nil { + return x.License + } + return "" +} + +func (x *Info) GetModulePath() string { + if x != nil { + return x.ModulePath + } + return "" +} + +type ExtrasValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Value: + // + // *ExtrasValue_Json + Value isExtrasValue_Value `protobuf_oneof:"value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExtrasValue) Reset() { + *x = ExtrasValue{} + mi := &file_meta_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExtrasValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExtrasValue) ProtoMessage() {} + +func (x *ExtrasValue) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExtrasValue.ProtoReflect.Descriptor instead. +func (*ExtrasValue) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{3} +} + +func (x *ExtrasValue) GetValue() isExtrasValue_Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *ExtrasValue) GetJson() string { + if x != nil { + if x, ok := x.Value.(*ExtrasValue_Json); ok { + return x.Json + } + } + return "" +} + +type isExtrasValue_Value interface { + isExtrasValue_Value() +} + +type ExtrasValue_Json struct { + Json string `protobuf:"bytes,1,opt,name=json,proto3,oneof"` +} + +func (*ExtrasValue_Json) isExtrasValue_Value() {} + +type Message struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Priority int32 `protobuf:"varint,3,opt,name=priority,proto3" json:"priority,omitempty"` + Extras map[string]*ExtrasValue `protobuf:"bytes,4,rep,name=extras,proto3" json:"extras,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Message) Reset() { + *x = Message{} + mi := &file_meta_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{4} +} + +func (x *Message) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *Message) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *Message) GetPriority() int32 { + if x != nil { + return x.Priority + } + return 0 +} + +func (x *Message) GetExtras() map[string]*ExtrasValue { + if x != nil { + return x.Extras + } + return nil +} + +type SetEnableRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Enable bool `protobuf:"varint,1,opt,name=enable,proto3" json:"enable,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetEnableRequest) Reset() { + *x = SetEnableRequest{} + mi := &file_meta_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetEnableRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetEnableRequest) ProtoMessage() {} + +func (x *SetEnableRequest) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetEnableRequest.ProtoReflect.Descriptor instead. +func (*SetEnableRequest) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{5} +} + +func (x *SetEnableRequest) GetEnable() bool { + if x != nil { + return x.Enable + } + return false +} + +type SetEnableSuccessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetEnableSuccessResponse) Reset() { + *x = SetEnableSuccessResponse{} + mi := &file_meta_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetEnableSuccessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetEnableSuccessResponse) ProtoMessage() {} + +func (x *SetEnableSuccessResponse) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetEnableSuccessResponse.ProtoReflect.Descriptor instead. +func (*SetEnableSuccessResponse) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{6} +} + +type SetEnableResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Response: + // + // *SetEnableResponse_Success + // *SetEnableResponse_Error + Response isSetEnableResponse_Response `protobuf_oneof:"response"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetEnableResponse) Reset() { + *x = SetEnableResponse{} + mi := &file_meta_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetEnableResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetEnableResponse) ProtoMessage() {} + +func (x *SetEnableResponse) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetEnableResponse.ProtoReflect.Descriptor instead. +func (*SetEnableResponse) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{7} +} + +func (x *SetEnableResponse) GetResponse() isSetEnableResponse_Response { + if x != nil { + return x.Response + } + return nil +} + +func (x *SetEnableResponse) GetSuccess() *SetEnableSuccessResponse { + if x != nil { + if x, ok := x.Response.(*SetEnableResponse_Success); ok { + return x.Success + } + } + return nil +} + +func (x *SetEnableResponse) GetError() *Error { + if x != nil { + if x, ok := x.Response.(*SetEnableResponse_Error); ok { + return x.Error + } + } + return nil +} + +type isSetEnableResponse_Response interface { + isSetEnableResponse_Response() +} + +type SetEnableResponse_Success struct { + Success *SetEnableSuccessResponse `protobuf:"bytes,1,opt,name=success,proto3,oneof"` +} + +type SetEnableResponse_Error struct { + Error *Error `protobuf:"bytes,2,opt,name=error,proto3,oneof"` +} + +func (*SetEnableResponse_Success) isSetEnableResponse_Response() {} + +func (*SetEnableResponse_Error) isSetEnableResponse_Response() {} + +var File_meta_proto protoreflect.FileDescriptor + +const file_meta_proto_rawDesc = "" + + "\n" + + "\n" + + "meta.proto\x1a\x19google/protobuf/any.proto\x1a\x1bgoogle/protobuf/empty.proto\"e\n" + + "\x05Error\x12.\n" + + "\adetails\x18\x01 \x01(\v2\x14.google.protobuf.AnyR\adetails\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\"G\n" + + "\vUserContext\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x04R\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" + + "\x05admin\x18\x03 \x01(\bR\x05admin\"\xc3\x01\n" + + "\x04Info\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + + "\x06author\x18\x02 \x01(\tR\x06author\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x18\n" + + "\awebsite\x18\x04 \x01(\tR\awebsite\x12 \n" + + "\vdescription\x18\x05 \x01(\tR\vdescription\x12\x18\n" + + "\alicense\x18\x06 \x01(\tR\alicense\x12\x1f\n" + + "\vmodule_path\x18\a \x01(\tR\n" + + "modulePath\",\n" + + "\vExtrasValue\x12\x14\n" + + "\x04json\x18\x01 \x01(\tH\x00R\x04jsonB\a\n" + + "\x05value\"\xcc\x01\n" + + "\aMessage\x12\x18\n" + + "\amessage\x18\x01 \x01(\tR\amessage\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12\x1a\n" + + "\bpriority\x18\x03 \x01(\x05R\bpriority\x12,\n" + + "\x06extras\x18\x04 \x03(\v2\x14.Message.ExtrasEntryR\x06extras\x1aG\n" + + "\vExtrasEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\"\n" + + "\x05value\x18\x02 \x01(\v2\f.ExtrasValueR\x05value:\x028\x01\"*\n" + + "\x10SetEnableRequest\x12\x16\n" + + "\x06enable\x18\x01 \x01(\bR\x06enable\"\x1a\n" + + "\x18SetEnableSuccessResponse\"v\n" + + "\x11SetEnableResponse\x125\n" + + "\asuccess\x18\x01 \x01(\v2\x19.SetEnableSuccessResponseH\x00R\asuccess\x12\x1e\n" + + "\x05error\x18\x02 \x01(\v2\x06.ErrorH\x00R\x05errorB\n" + + "\n" + + "\bresponse26\n" + + "\n" + + "PluginMeta\x12(\n" + + "\aGetInfo\x12\x16.google.protobuf.Empty\x1a\x05.Info2<\n" + + "\x06Plugin\x122\n" + + "\tSetEnable\x12\x11.SetEnableRequest\x1a\x12.SetEnableResponseB\x16Z\x14./generated/protobufb\x06proto3" + +var ( + file_meta_proto_rawDescOnce sync.Once + file_meta_proto_rawDescData []byte +) + +func file_meta_proto_rawDescGZIP() []byte { + file_meta_proto_rawDescOnce.Do(func() { + file_meta_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_meta_proto_rawDesc), len(file_meta_proto_rawDesc))) + }) + return file_meta_proto_rawDescData +} + +var file_meta_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_meta_proto_goTypes = []any{ + (*Error)(nil), // 0: Error + (*UserContext)(nil), // 1: UserContext + (*Info)(nil), // 2: Info + (*ExtrasValue)(nil), // 3: ExtrasValue + (*Message)(nil), // 4: Message + (*SetEnableRequest)(nil), // 5: SetEnableRequest + (*SetEnableSuccessResponse)(nil), // 6: SetEnableSuccessResponse + (*SetEnableResponse)(nil), // 7: SetEnableResponse + nil, // 8: Message.ExtrasEntry + (*anypb.Any)(nil), // 9: google.protobuf.Any + (*emptypb.Empty)(nil), // 10: google.protobuf.Empty +} +var file_meta_proto_depIdxs = []int32{ + 9, // 0: Error.details:type_name -> google.protobuf.Any + 8, // 1: Message.extras:type_name -> Message.ExtrasEntry + 6, // 2: SetEnableResponse.success:type_name -> SetEnableSuccessResponse + 0, // 3: SetEnableResponse.error:type_name -> Error + 3, // 4: Message.ExtrasEntry.value:type_name -> ExtrasValue + 10, // 5: PluginMeta.GetInfo:input_type -> google.protobuf.Empty + 5, // 6: Plugin.SetEnable:input_type -> SetEnableRequest + 2, // 7: PluginMeta.GetInfo:output_type -> Info + 7, // 8: Plugin.SetEnable:output_type -> SetEnableResponse + 7, // [7:9] is the sub-list for method output_type + 5, // [5:7] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_meta_proto_init() } +func file_meta_proto_init() { + if File_meta_proto != nil { + return + } + file_meta_proto_msgTypes[3].OneofWrappers = []any{ + (*ExtrasValue_Json)(nil), + } + file_meta_proto_msgTypes[7].OneofWrappers = []any{ + (*SetEnableResponse_Success)(nil), + (*SetEnableResponse_Error)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_meta_proto_rawDesc), len(file_meta_proto_rawDesc)), + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 2, + }, + GoTypes: file_meta_proto_goTypes, + DependencyIndexes: file_meta_proto_depIdxs, + MessageInfos: file_meta_proto_msgTypes, + }.Build() + File_meta_proto = out.File + file_meta_proto_goTypes = nil + file_meta_proto_depIdxs = nil +} diff --git a/v2/generated/protobuf/meta_grpc.pb.go b/v2/generated/protobuf/meta_grpc.pb.go new file mode 100644 index 0000000..962cb78 --- /dev/null +++ b/v2/generated/protobuf/meta_grpc.pb.go @@ -0,0 +1,224 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.31.1 +// source: meta.proto + +package protobuf + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// 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.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + PluginMeta_GetInfo_FullMethodName = "/PluginMeta/GetInfo" +) + +// PluginMetaClient is the client API for PluginMeta 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 PluginMetaClient interface { + GetInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) +} + +type pluginMetaClient struct { + cc grpc.ClientConnInterface +} + +func NewPluginMetaClient(cc grpc.ClientConnInterface) PluginMetaClient { + return &pluginMetaClient{cc} +} + +func (c *pluginMetaClient) GetInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Info) + err := c.cc.Invoke(ctx, PluginMeta_GetInfo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PluginMetaServer is the server API for PluginMeta service. +// All implementations must embed UnimplementedPluginMetaServer +// for forward compatibility. +type PluginMetaServer interface { + GetInfo(context.Context, *emptypb.Empty) (*Info, error) + mustEmbedUnimplementedPluginMetaServer() +} + +// UnimplementedPluginMetaServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedPluginMetaServer struct{} + +func (UnimplementedPluginMetaServer) GetInfo(context.Context, *emptypb.Empty) (*Info, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetInfo not implemented") +} +func (UnimplementedPluginMetaServer) mustEmbedUnimplementedPluginMetaServer() {} +func (UnimplementedPluginMetaServer) testEmbeddedByValue() {} + +// UnsafePluginMetaServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to PluginMetaServer will +// result in compilation errors. +type UnsafePluginMetaServer interface { + mustEmbedUnimplementedPluginMetaServer() +} + +func RegisterPluginMetaServer(s grpc.ServiceRegistrar, srv PluginMetaServer) { + // If the following call pancis, it indicates UnimplementedPluginMetaServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&PluginMeta_ServiceDesc, srv) +} + +func _PluginMeta_GetInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginMetaServer).GetInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PluginMeta_GetInfo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginMetaServer).GetInfo(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +// PluginMeta_ServiceDesc is the grpc.ServiceDesc for PluginMeta service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var PluginMeta_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "PluginMeta", + HandlerType: (*PluginMetaServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetInfo", + Handler: _PluginMeta_GetInfo_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "meta.proto", +} + +const ( + Plugin_SetEnable_FullMethodName = "/Plugin/SetEnable" +) + +// PluginClient is the client API for Plugin 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 PluginClient interface { + SetEnable(ctx context.Context, in *SetEnableRequest, opts ...grpc.CallOption) (*SetEnableResponse, error) +} + +type pluginClient struct { + cc grpc.ClientConnInterface +} + +func NewPluginClient(cc grpc.ClientConnInterface) PluginClient { + return &pluginClient{cc} +} + +func (c *pluginClient) SetEnable(ctx context.Context, in *SetEnableRequest, opts ...grpc.CallOption) (*SetEnableResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SetEnableResponse) + err := c.cc.Invoke(ctx, Plugin_SetEnable_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PluginServer is the server API for Plugin service. +// All implementations must embed UnimplementedPluginServer +// for forward compatibility. +type PluginServer interface { + SetEnable(context.Context, *SetEnableRequest) (*SetEnableResponse, error) + mustEmbedUnimplementedPluginServer() +} + +// UnimplementedPluginServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedPluginServer struct{} + +func (UnimplementedPluginServer) SetEnable(context.Context, *SetEnableRequest) (*SetEnableResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetEnable not implemented") +} +func (UnimplementedPluginServer) mustEmbedUnimplementedPluginServer() {} +func (UnimplementedPluginServer) testEmbeddedByValue() {} + +// UnsafePluginServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to PluginServer will +// result in compilation errors. +type UnsafePluginServer interface { + mustEmbedUnimplementedPluginServer() +} + +func RegisterPluginServer(s grpc.ServiceRegistrar, srv PluginServer) { + // If the following call pancis, it indicates UnimplementedPluginServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Plugin_ServiceDesc, srv) +} + +func _Plugin_SetEnable_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetEnableRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).SetEnable(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Plugin_SetEnable_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).SetEnable(ctx, req.(*SetEnableRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Plugin_ServiceDesc is the grpc.ServiceDesc for Plugin service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Plugin_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "Plugin", + HandlerType: (*PluginServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SetEnable", + Handler: _Plugin_SetEnable_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "meta.proto", +} diff --git a/v2/generated/protobuf/webhooker.pb.go b/v2/generated/protobuf/webhooker.pb.go new file mode 100644 index 0000000..5f25f56 --- /dev/null +++ b/v2/generated/protobuf/webhooker.pb.go @@ -0,0 +1,128 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc v6.31.1 +// source: webhooker.proto + +package protobuf + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +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 RegisterBasePathRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + BasePath string `protobuf:"bytes,1,opt,name=base_path,json=basePath,proto3" json:"base_path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterBasePathRequest) Reset() { + *x = RegisterBasePathRequest{} + mi := &file_webhooker_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterBasePathRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterBasePathRequest) ProtoMessage() {} + +func (x *RegisterBasePathRequest) ProtoReflect() protoreflect.Message { + mi := &file_webhooker_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterBasePathRequest.ProtoReflect.Descriptor instead. +func (*RegisterBasePathRequest) Descriptor() ([]byte, []int) { + return file_webhooker_proto_rawDescGZIP(), []int{0} +} + +func (x *RegisterBasePathRequest) GetBasePath() string { + if x != nil { + return x.BasePath + } + return "" +} + +var File_webhooker_proto protoreflect.FileDescriptor + +const file_webhooker_proto_rawDesc = "" + + "\n" + + "\x0fwebhooker.proto\x1a\x1bgoogle/protobuf/empty.proto\"6\n" + + "\x17RegisterBasePathRequest\x12\x1b\n" + + "\tbase_path\x18\x01 \x01(\tR\bbasePath2Q\n" + + "\tWebhooker\x12D\n" + + "\x10RegisterBasePath\x12\x18.RegisterBasePathRequest\x1a\x16.google.protobuf.EmptyB\x16Z\x14./generated/protobufb\x06proto3" + +var ( + file_webhooker_proto_rawDescOnce sync.Once + file_webhooker_proto_rawDescData []byte +) + +func file_webhooker_proto_rawDescGZIP() []byte { + file_webhooker_proto_rawDescOnce.Do(func() { + file_webhooker_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_webhooker_proto_rawDesc), len(file_webhooker_proto_rawDesc))) + }) + return file_webhooker_proto_rawDescData +} + +var file_webhooker_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_webhooker_proto_goTypes = []any{ + (*RegisterBasePathRequest)(nil), // 0: RegisterBasePathRequest + (*emptypb.Empty)(nil), // 1: google.protobuf.Empty +} +var file_webhooker_proto_depIdxs = []int32{ + 0, // 0: Webhooker.RegisterBasePath:input_type -> RegisterBasePathRequest + 1, // 1: Webhooker.RegisterBasePath:output_type -> google.protobuf.Empty + 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_webhooker_proto_init() } +func file_webhooker_proto_init() { + if File_webhooker_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_webhooker_proto_rawDesc), len(file_webhooker_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_webhooker_proto_goTypes, + DependencyIndexes: file_webhooker_proto_depIdxs, + MessageInfos: file_webhooker_proto_msgTypes, + }.Build() + File_webhooker_proto = out.File + file_webhooker_proto_goTypes = nil + file_webhooker_proto_depIdxs = nil +} diff --git a/v2/generated/protobuf/webhooker_grpc.pb.go b/v2/generated/protobuf/webhooker_grpc.pb.go new file mode 100644 index 0000000..65afe82 --- /dev/null +++ b/v2/generated/protobuf/webhooker_grpc.pb.go @@ -0,0 +1,122 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.31.1 +// source: webhooker.proto + +package protobuf + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// 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.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Webhooker_RegisterBasePath_FullMethodName = "/Webhooker/RegisterBasePath" +) + +// WebhookerClient is the client API for Webhooker 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 WebhookerClient interface { + RegisterBasePath(ctx context.Context, in *RegisterBasePathRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type webhookerClient struct { + cc grpc.ClientConnInterface +} + +func NewWebhookerClient(cc grpc.ClientConnInterface) WebhookerClient { + return &webhookerClient{cc} +} + +func (c *webhookerClient) RegisterBasePath(ctx context.Context, in *RegisterBasePathRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Webhooker_RegisterBasePath_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// WebhookerServer is the server API for Webhooker service. +// All implementations must embed UnimplementedWebhookerServer +// for forward compatibility. +type WebhookerServer interface { + RegisterBasePath(context.Context, *RegisterBasePathRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedWebhookerServer() +} + +// UnimplementedWebhookerServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedWebhookerServer struct{} + +func (UnimplementedWebhookerServer) RegisterBasePath(context.Context, *RegisterBasePathRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method RegisterBasePath not implemented") +} +func (UnimplementedWebhookerServer) mustEmbedUnimplementedWebhookerServer() {} +func (UnimplementedWebhookerServer) testEmbeddedByValue() {} + +// UnsafeWebhookerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to WebhookerServer will +// result in compilation errors. +type UnsafeWebhookerServer interface { + mustEmbedUnimplementedWebhookerServer() +} + +func RegisterWebhookerServer(s grpc.ServiceRegistrar, srv WebhookerServer) { + // If the following call pancis, it indicates UnimplementedWebhookerServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Webhooker_ServiceDesc, srv) +} + +func _Webhooker_RegisterBasePath_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RegisterBasePathRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WebhookerServer).RegisterBasePath(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Webhooker_RegisterBasePath_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WebhookerServer).RegisterBasePath(ctx, req.(*RegisterBasePathRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Webhooker_ServiceDesc is the grpc.ServiceDesc for Webhooker service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Webhooker_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "Webhooker", + HandlerType: (*WebhookerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "RegisterBasePath", + Handler: _Webhooker_RegisterBasePath_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "webhooker.proto", +} diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..a1305d9 --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,15 @@ +module github.com/gotify/plugin-api/v2 + +go 1.24.5 + +require ( + google.golang.org/grpc v1.74.2 + google.golang.org/protobuf v1.36.7 +) + +require ( + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect +) diff --git a/v2/go.sum b/v2/go.sum new file mode 100644 index 0000000..a09199d --- /dev/null +++ b/v2/go.sum @@ -0,0 +1,34 @@ +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= diff --git a/v2/protobuf/config.proto b/v2/protobuf/config.proto new file mode 100644 index 0000000..aa94653 --- /dev/null +++ b/v2/protobuf/config.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; +import "google/protobuf/empty.proto"; +import "meta.proto"; + +option go_package = "./generated/protobuf"; + +message Config { + string config = 1; +} + +message ValidateAndSetConfigRequest { + Config config = 1; +} + +message ValidateAndSetConfigSuccessResponse { + +} + +message ValidateAndSetConfigResponse { + oneof response { + ValidateAndSetConfigSuccessResponse success = 1; + Error error = 2; + } +} + +service Configurer { + rpc DefaultConfig(google.protobuf.Empty) returns (Config); + rpc ValidateAndSetConfig(ValidateAndSetConfigRequest) returns (ValidateAndSetConfigResponse); +} \ No newline at end of file diff --git a/v2/protobuf/display.proto b/v2/protobuf/display.proto new file mode 100644 index 0000000..4bd775d --- /dev/null +++ b/v2/protobuf/display.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option go_package = "./generated/protobuf"; + +message DisplayRequest { + string location = 1; +} + +message DisplayResponse { + string display = 1; +} + +service Displayer { + rpc Display(DisplayRequest) returns (DisplayResponse); +} + diff --git a/v2/protobuf/infra.proto b/v2/protobuf/infra.proto new file mode 100644 index 0000000..35fa326 --- /dev/null +++ b/v2/protobuf/infra.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +import "meta.proto"; + +option go_package = "./generated/protobuf"; + +message ServerVersionInfo { + string version = 1; + string commit = 2; + string buildDate = 3; +} + +message StorageSaveRequest { + bytes data = 1; +} + +message StorageLoadResponse { + bytes data = 1; +} + +service Infra { + rpc GetServerVersion(google.protobuf.Empty) returns (ServerVersionInfo); + rpc SaveConfig(StorageSaveRequest) returns (google.protobuf.Empty); + rpc LoadConfig(google.protobuf.Empty) returns (StorageLoadResponse); + rpc SendMessage(Message) returns (google.protobuf.Empty); +} \ No newline at end of file diff --git a/v2/protobuf/meta.proto b/v2/protobuf/meta.proto new file mode 100644 index 0000000..81e40db --- /dev/null +++ b/v2/protobuf/meta.proto @@ -0,0 +1,65 @@ +syntax = "proto3"; +import "google/protobuf/any.proto"; +import "google/protobuf/empty.proto"; + +option go_package = "./generated/protobuf"; + +message Error { + google.protobuf.Any details = 1; + string message = 2; + string type = 3; +} + +message UserContext { + uint64 id = 1; + string name = 2; + bool admin = 3; +} + +message Info { + string version = 1; + string author = 2; + string name = 3; + string website = 4; + string description = 5; + string license = 6; + string module_path = 7; +} + + +message ExtrasValue { + oneof value { + string json = 1; + } +} + +message Message { + string message = 1; + string title = 2; + int32 priority = 3; + map extras = 4; +} + +message SetEnableRequest { + bool enable = 1; +} + +message SetEnableSuccessResponse { + +} + +message SetEnableResponse { + oneof response { + SetEnableSuccessResponse success = 1; + Error error = 2; + } +} + +service PluginMeta { + rpc GetInfo(google.protobuf.Empty) returns (Info); +} + +service Plugin { + rpc SetEnable(SetEnableRequest) returns (SetEnableResponse); +} + diff --git a/v2/protobuf/webhooker.proto b/v2/protobuf/webhooker.proto new file mode 100644 index 0000000..047ebe2 --- /dev/null +++ b/v2/protobuf/webhooker.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; +import "google/protobuf/empty.proto"; + +option go_package = "./generated/protobuf"; + +message RegisterBasePathRequest { + string base_path = 1; +} + +service Webhooker { + rpc RegisterBasePath(RegisterBasePathRequest) returns (google.protobuf.Empty); +} \ No newline at end of file diff --git a/v2/rpc.go b/v2/rpc.go new file mode 100644 index 0000000..073e209 --- /dev/null +++ b/v2/rpc.go @@ -0,0 +1,38 @@ +package plugin + +import ( + "context" + "net" + + "github.com/gotify/plugin-api/v2/generated/protobuf" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/emptypb" +) + +// PluginRpcHandler handles and coordinates RPC between the plugin and the server. +type PluginRpcHandler struct { + infraTarget string + server *grpc.Server +} + +func NewPluginRpcHandler(infraTarget string) *PluginRpcHandler { + infraClient, err := grpc.NewClient(infraTarget) + if err != nil { + panic(err) + } + infraRpcClient := protobuf.NewInfraClient(infraClient) + version, err := infraRpcClient.GetServerVersion(context.Background(), &emptypb.Empty{}) + if err != nil { + panic(err) + } + _ = version + + return &PluginRpcHandler{ + infraTarget: infraTarget, + server: grpc.NewServer(), + } +} + +func (h *PluginRpcHandler) Serve(listener net.Listener) error { + return h.server.Serve(listener) +} From 5abcf5d5200f8d913a24f04105a430a5d02b18f4 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 12 Aug 2025 08:31:15 -0500 Subject: [PATCH 02/37] plugin connection auth/security Signed-off-by: eternal-flame-AD --- v2/generated/protobuf/config.pb.go | 133 ++++++++++----- v2/generated/protobuf/config_grpc.pb.go | 13 +- v2/generated/protobuf/display.pb.go | 35 ++-- v2/generated/protobuf/infra.pb.go | 32 +++- v2/generated/protobuf/meta.pb.go | 49 ++++-- v2/generated/protobuf/meta_grpc.pb.go | 40 ++++- v2/generated/protobuf/webhooker.pb.go | 37 ++-- v2/go.mod | 11 ++ v2/go.sum | 30 ++++ v2/pipe_net.go | 34 ++++ v2/pipe_net_test.go | 94 +++++++++++ v2/pipe_not_unix.go | 13 ++ v2/pipe_unix.go | 21 +++ v2/protobuf/config.proto | 10 +- v2/protobuf/display.proto | 4 +- v2/protobuf/infra.proto | 6 +- v2/protobuf/meta.proto | 4 +- v2/protobuf/webhooker.proto | 6 +- v2/rpc.go | 215 ++++++++++++++++++++++-- v2/rpc_test.go | 124 ++++++++++++++ v2/transport_auth.go | 156 +++++++++++++++++ v2/transport_auth_test.go | 74 ++++++++ 22 files changed, 1027 insertions(+), 114 deletions(-) create mode 100644 v2/pipe_net.go create mode 100644 v2/pipe_net_test.go create mode 100644 v2/pipe_not_unix.go create mode 100644 v2/pipe_unix.go create mode 100644 v2/rpc_test.go create mode 100644 v2/transport_auth.go create mode 100644 v2/transport_auth_test.go diff --git a/v2/generated/protobuf/config.pb.go b/v2/generated/protobuf/config.pb.go index fc53bda..b03d649 100644 --- a/v2/generated/protobuf/config.pb.go +++ b/v2/generated/protobuf/config.pb.go @@ -9,7 +9,6 @@ package protobuf import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" - emptypb "google.golang.org/protobuf/types/known/emptypb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -66,16 +65,61 @@ func (x *Config) GetConfig() string { return "" } +type DefaultConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DefaultConfigRequest) Reset() { + *x = DefaultConfigRequest{} + mi := &file_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DefaultConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DefaultConfigRequest) ProtoMessage() {} + +func (x *DefaultConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DefaultConfigRequest.ProtoReflect.Descriptor instead. +func (*DefaultConfigRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{1} +} + +func (x *DefaultConfigRequest) GetUser() *UserContext { + if x != nil { + return x.User + } + return nil +} + type ValidateAndSetConfigRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Config *Config `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + Config *Config `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ValidateAndSetConfigRequest) Reset() { *x = ValidateAndSetConfigRequest{} - mi := &file_config_proto_msgTypes[1] + mi := &file_config_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -87,7 +131,7 @@ func (x *ValidateAndSetConfigRequest) String() string { func (*ValidateAndSetConfigRequest) ProtoMessage() {} func (x *ValidateAndSetConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[1] + mi := &file_config_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -100,7 +144,14 @@ func (x *ValidateAndSetConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateAndSetConfigRequest.ProtoReflect.Descriptor instead. func (*ValidateAndSetConfigRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{1} + return file_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ValidateAndSetConfigRequest) GetUser() *UserContext { + if x != nil { + return x.User + } + return nil } func (x *ValidateAndSetConfigRequest) GetConfig() *Config { @@ -118,7 +169,7 @@ type ValidateAndSetConfigSuccessResponse struct { func (x *ValidateAndSetConfigSuccessResponse) Reset() { *x = ValidateAndSetConfigSuccessResponse{} - mi := &file_config_proto_msgTypes[2] + mi := &file_config_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -130,7 +181,7 @@ func (x *ValidateAndSetConfigSuccessResponse) String() string { func (*ValidateAndSetConfigSuccessResponse) ProtoMessage() {} func (x *ValidateAndSetConfigSuccessResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[2] + mi := &file_config_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -143,7 +194,7 @@ func (x *ValidateAndSetConfigSuccessResponse) ProtoReflect() protoreflect.Messag // Deprecated: Use ValidateAndSetConfigSuccessResponse.ProtoReflect.Descriptor instead. func (*ValidateAndSetConfigSuccessResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{2} + return file_config_proto_rawDescGZIP(), []int{3} } type ValidateAndSetConfigResponse struct { @@ -159,7 +210,7 @@ type ValidateAndSetConfigResponse struct { func (x *ValidateAndSetConfigResponse) Reset() { *x = ValidateAndSetConfigResponse{} - mi := &file_config_proto_msgTypes[3] + mi := &file_config_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -171,7 +222,7 @@ func (x *ValidateAndSetConfigResponse) String() string { func (*ValidateAndSetConfigResponse) ProtoMessage() {} func (x *ValidateAndSetConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[3] + mi := &file_config_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -184,7 +235,7 @@ func (x *ValidateAndSetConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateAndSetConfigResponse.ProtoReflect.Descriptor instead. func (*ValidateAndSetConfigResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{3} + return file_config_proto_rawDescGZIP(), []int{4} } func (x *ValidateAndSetConfigResponse) GetResponse() isValidateAndSetConfigResponse_Response { @@ -232,21 +283,24 @@ var File_config_proto protoreflect.FileDescriptor const file_config_proto_rawDesc = "" + "\n" + - "\fconfig.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\n" + + "\fconfig.proto\x1a\n" + "meta.proto\" \n" + "\x06Config\x12\x16\n" + - "\x06config\x18\x01 \x01(\tR\x06config\">\n" + - "\x1bValidateAndSetConfigRequest\x12\x1f\n" + - "\x06config\x18\x01 \x01(\v2\a.ConfigR\x06config\"%\n" + + "\x06config\x18\x01 \x01(\tR\x06config\"8\n" + + "\x14DefaultConfigRequest\x12 \n" + + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\"`\n" + + "\x1bValidateAndSetConfigRequest\x12 \n" + + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x1f\n" + + "\x06config\x18\x02 \x01(\v2\a.ConfigR\x06config\"%\n" + "#ValidateAndSetConfigSuccessResponse\"\x8c\x01\n" + "\x1cValidateAndSetConfigResponse\x12@\n" + "\asuccess\x18\x01 \x01(\v2$.ValidateAndSetConfigSuccessResponseH\x00R\asuccess\x12\x1e\n" + "\x05error\x18\x02 \x01(\v2\x06.ErrorH\x00R\x05errorB\n" + "\n" + - "\bresponse2\x93\x01\n" + + "\bresponse2\x92\x01\n" + "\n" + - "Configurer\x120\n" + - "\rDefaultConfig\x12\x16.google.protobuf.Empty\x1a\a.Config\x12S\n" + + "Configurer\x12/\n" + + "\rDefaultConfig\x12\x15.DefaultConfigRequest\x1a\a.Config\x12S\n" + "\x14ValidateAndSetConfig\x12\x1c.ValidateAndSetConfigRequest\x1a\x1d.ValidateAndSetConfigResponseB\x16Z\x14./generated/protobufb\x06proto3" var ( @@ -261,28 +315,31 @@ func file_config_proto_rawDescGZIP() []byte { return file_config_proto_rawDescData } -var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_config_proto_goTypes = []any{ (*Config)(nil), // 0: Config - (*ValidateAndSetConfigRequest)(nil), // 1: ValidateAndSetConfigRequest - (*ValidateAndSetConfigSuccessResponse)(nil), // 2: ValidateAndSetConfigSuccessResponse - (*ValidateAndSetConfigResponse)(nil), // 3: ValidateAndSetConfigResponse - (*Error)(nil), // 4: Error - (*emptypb.Empty)(nil), // 5: google.protobuf.Empty + (*DefaultConfigRequest)(nil), // 1: DefaultConfigRequest + (*ValidateAndSetConfigRequest)(nil), // 2: ValidateAndSetConfigRequest + (*ValidateAndSetConfigSuccessResponse)(nil), // 3: ValidateAndSetConfigSuccessResponse + (*ValidateAndSetConfigResponse)(nil), // 4: ValidateAndSetConfigResponse + (*UserContext)(nil), // 5: UserContext + (*Error)(nil), // 6: Error } var file_config_proto_depIdxs = []int32{ - 0, // 0: ValidateAndSetConfigRequest.config:type_name -> Config - 2, // 1: ValidateAndSetConfigResponse.success:type_name -> ValidateAndSetConfigSuccessResponse - 4, // 2: ValidateAndSetConfigResponse.error:type_name -> Error - 5, // 3: Configurer.DefaultConfig:input_type -> google.protobuf.Empty - 1, // 4: Configurer.ValidateAndSetConfig:input_type -> ValidateAndSetConfigRequest - 0, // 5: Configurer.DefaultConfig:output_type -> Config - 3, // 6: Configurer.ValidateAndSetConfig:output_type -> ValidateAndSetConfigResponse - 5, // [5:7] is the sub-list for method output_type - 3, // [3:5] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 5, // 0: DefaultConfigRequest.user:type_name -> UserContext + 5, // 1: ValidateAndSetConfigRequest.user:type_name -> UserContext + 0, // 2: ValidateAndSetConfigRequest.config:type_name -> Config + 3, // 3: ValidateAndSetConfigResponse.success:type_name -> ValidateAndSetConfigSuccessResponse + 6, // 4: ValidateAndSetConfigResponse.error:type_name -> Error + 1, // 5: Configurer.DefaultConfig:input_type -> DefaultConfigRequest + 2, // 6: Configurer.ValidateAndSetConfig:input_type -> ValidateAndSetConfigRequest + 0, // 7: Configurer.DefaultConfig:output_type -> Config + 4, // 8: Configurer.ValidateAndSetConfig:output_type -> ValidateAndSetConfigResponse + 7, // [7:9] is the sub-list for method output_type + 5, // [5:7] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_config_proto_init() } @@ -291,7 +348,7 @@ func file_config_proto_init() { return } file_meta_proto_init() - file_config_proto_msgTypes[3].OneofWrappers = []any{ + file_config_proto_msgTypes[4].OneofWrappers = []any{ (*ValidateAndSetConfigResponse_Success)(nil), (*ValidateAndSetConfigResponse_Error)(nil), } @@ -301,7 +358,7 @@ func file_config_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc)), NumEnums: 0, - NumMessages: 4, + NumMessages: 5, NumExtensions: 0, NumServices: 1, }, diff --git a/v2/generated/protobuf/config_grpc.pb.go b/v2/generated/protobuf/config_grpc.pb.go index 9532432..ff40f72 100644 --- a/v2/generated/protobuf/config_grpc.pb.go +++ b/v2/generated/protobuf/config_grpc.pb.go @@ -11,7 +11,6 @@ import ( grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" - emptypb "google.golang.org/protobuf/types/known/emptypb" ) // This is a compile-time assertion to ensure that this generated file @@ -28,7 +27,7 @@ const ( // // 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 ConfigurerClient interface { - DefaultConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Config, error) + DefaultConfig(ctx context.Context, in *DefaultConfigRequest, opts ...grpc.CallOption) (*Config, error) ValidateAndSetConfig(ctx context.Context, in *ValidateAndSetConfigRequest, opts ...grpc.CallOption) (*ValidateAndSetConfigResponse, error) } @@ -40,7 +39,7 @@ func NewConfigurerClient(cc grpc.ClientConnInterface) ConfigurerClient { return &configurerClient{cc} } -func (c *configurerClient) DefaultConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Config, error) { +func (c *configurerClient) DefaultConfig(ctx context.Context, in *DefaultConfigRequest, opts ...grpc.CallOption) (*Config, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Config) err := c.cc.Invoke(ctx, Configurer_DefaultConfig_FullMethodName, in, out, cOpts...) @@ -64,7 +63,7 @@ func (c *configurerClient) ValidateAndSetConfig(ctx context.Context, in *Validat // All implementations must embed UnimplementedConfigurerServer // for forward compatibility. type ConfigurerServer interface { - DefaultConfig(context.Context, *emptypb.Empty) (*Config, error) + DefaultConfig(context.Context, *DefaultConfigRequest) (*Config, error) ValidateAndSetConfig(context.Context, *ValidateAndSetConfigRequest) (*ValidateAndSetConfigResponse, error) mustEmbedUnimplementedConfigurerServer() } @@ -76,7 +75,7 @@ type ConfigurerServer interface { // pointer dereference when methods are called. type UnimplementedConfigurerServer struct{} -func (UnimplementedConfigurerServer) DefaultConfig(context.Context, *emptypb.Empty) (*Config, error) { +func (UnimplementedConfigurerServer) DefaultConfig(context.Context, *DefaultConfigRequest) (*Config, error) { return nil, status.Errorf(codes.Unimplemented, "method DefaultConfig not implemented") } func (UnimplementedConfigurerServer) ValidateAndSetConfig(context.Context, *ValidateAndSetConfigRequest) (*ValidateAndSetConfigResponse, error) { @@ -104,7 +103,7 @@ func RegisterConfigurerServer(s grpc.ServiceRegistrar, srv ConfigurerServer) { } func _Configurer_DefaultConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(emptypb.Empty) + in := new(DefaultConfigRequest) if err := dec(in); err != nil { return nil, err } @@ -116,7 +115,7 @@ func _Configurer_DefaultConfig_Handler(srv interface{}, ctx context.Context, dec FullMethod: Configurer_DefaultConfig_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ConfigurerServer).DefaultConfig(ctx, req.(*emptypb.Empty)) + return srv.(ConfigurerServer).DefaultConfig(ctx, req.(*DefaultConfigRequest)) } return interceptor(ctx, in, info, handler) } diff --git a/v2/generated/protobuf/display.pb.go b/v2/generated/protobuf/display.pb.go index 0060e9d..438b2dc 100644 --- a/v2/generated/protobuf/display.pb.go +++ b/v2/generated/protobuf/display.pb.go @@ -23,7 +23,8 @@ const ( type DisplayRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Location string `protobuf:"bytes,1,opt,name=location,proto3" json:"location,omitempty"` + User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + Location string `protobuf:"bytes,2,opt,name=location,proto3" json:"location,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -58,6 +59,13 @@ func (*DisplayRequest) Descriptor() ([]byte, []int) { return file_display_proto_rawDescGZIP(), []int{0} } +func (x *DisplayRequest) GetUser() *UserContext { + if x != nil { + return x.User + } + return nil +} + func (x *DisplayRequest) GetLocation() string { if x != nil { return x.Location @@ -113,9 +121,11 @@ var File_display_proto protoreflect.FileDescriptor const file_display_proto_rawDesc = "" + "\n" + - "\rdisplay.proto\",\n" + - "\x0eDisplayRequest\x12\x1a\n" + - "\blocation\x18\x01 \x01(\tR\blocation\"+\n" + + "\rdisplay.proto\x1a\n" + + "meta.proto\"N\n" + + "\x0eDisplayRequest\x12 \n" + + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x1a\n" + + "\blocation\x18\x02 \x01(\tR\blocation\"+\n" + "\x0fDisplayResponse\x12\x18\n" + "\adisplay\x18\x01 \x01(\tR\adisplay29\n" + "\tDisplayer\x12,\n" + @@ -137,15 +147,17 @@ var file_display_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_display_proto_goTypes = []any{ (*DisplayRequest)(nil), // 0: DisplayRequest (*DisplayResponse)(nil), // 1: DisplayResponse + (*UserContext)(nil), // 2: UserContext } var file_display_proto_depIdxs = []int32{ - 0, // 0: Displayer.Display:input_type -> DisplayRequest - 1, // 1: Displayer.Display:output_type -> DisplayResponse - 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 + 2, // 0: DisplayRequest.user:type_name -> UserContext + 0, // 1: Displayer.Display:input_type -> DisplayRequest + 1, // 2: Displayer.Display:output_type -> DisplayResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] 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_display_proto_init() } @@ -153,6 +165,7 @@ func file_display_proto_init() { if File_display_proto != nil { return } + file_meta_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/v2/generated/protobuf/infra.pb.go b/v2/generated/protobuf/infra.pb.go index d82c0c5..5a0ea7a 100644 --- a/v2/generated/protobuf/infra.pb.go +++ b/v2/generated/protobuf/infra.pb.go @@ -84,7 +84,8 @@ func (x *ServerVersionInfo) GetBuildDate() string { type StorageSaveRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + Userid uint64 `protobuf:"varint,1,opt,name=userid,proto3" json:"userid,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -119,6 +120,13 @@ func (*StorageSaveRequest) Descriptor() ([]byte, []int) { return file_infra_proto_rawDescGZIP(), []int{1} } +func (x *StorageSaveRequest) GetUserid() uint64 { + if x != nil { + return x.Userid + } + return 0 +} + func (x *StorageSaveRequest) GetData() []byte { if x != nil { return x.Data @@ -128,7 +136,8 @@ func (x *StorageSaveRequest) GetData() []byte { type StorageLoadResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + Userid uint64 `protobuf:"varint,1,opt,name=userid,proto3" json:"userid,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -163,6 +172,13 @@ func (*StorageLoadResponse) Descriptor() ([]byte, []int) { return file_infra_proto_rawDescGZIP(), []int{2} } +func (x *StorageLoadResponse) GetUserid() uint64 { + if x != nil { + return x.Userid + } + return 0 +} + func (x *StorageLoadResponse) GetData() []byte { if x != nil { return x.Data @@ -179,11 +195,13 @@ const file_infra_proto_rawDesc = "" + "\x11ServerVersionInfo\x12\x18\n" + "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + "\x06commit\x18\x02 \x01(\tR\x06commit\x12\x1c\n" + - "\tbuildDate\x18\x03 \x01(\tR\tbuildDate\"(\n" + - "\x12StorageSaveRequest\x12\x12\n" + - "\x04data\x18\x01 \x01(\fR\x04data\")\n" + - "\x13StorageLoadResponse\x12\x12\n" + - "\x04data\x18\x01 \x01(\fR\x04data2\xef\x01\n" + + "\tbuildDate\x18\x03 \x01(\tR\tbuildDate\"@\n" + + "\x12StorageSaveRequest\x12\x16\n" + + "\x06userid\x18\x01 \x01(\x04R\x06userid\x12\x12\n" + + "\x04data\x18\x02 \x01(\fR\x04data\"A\n" + + "\x13StorageLoadResponse\x12\x16\n" + + "\x06userid\x18\x01 \x01(\x04R\x06userid\x12\x12\n" + + "\x04data\x18\x02 \x01(\fR\x04data2\xef\x01\n" + "\x05Infra\x12>\n" + "\x10GetServerVersion\x12\x16.google.protobuf.Empty\x1a\x12.ServerVersionInfo\x129\n" + "\n" + diff --git a/v2/generated/protobuf/meta.pb.go b/v2/generated/protobuf/meta.pb.go index 4f3f58e..3b26348 100644 --- a/v2/generated/protobuf/meta.pb.go +++ b/v2/generated/protobuf/meta.pb.go @@ -371,7 +371,8 @@ func (x *Message) GetExtras() map[string]*ExtrasValue { type SetEnableRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Enable bool `protobuf:"varint,1,opt,name=enable,proto3" json:"enable,omitempty"` + User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + Enable bool `protobuf:"varint,2,opt,name=enable,proto3" json:"enable,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -406,6 +407,13 @@ func (*SetEnableRequest) Descriptor() ([]byte, []int) { return file_meta_proto_rawDescGZIP(), []int{5} } +func (x *SetEnableRequest) GetUser() *UserContext { + if x != nil { + return x.User + } + return nil +} + func (x *SetEnableRequest) GetEnable() bool { if x != nil { return x.Enable @@ -564,9 +572,10 @@ const file_meta_proto_rawDesc = "" + "\x06extras\x18\x04 \x03(\v2\x14.Message.ExtrasEntryR\x06extras\x1aG\n" + "\vExtrasEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\"\n" + - "\x05value\x18\x02 \x01(\v2\f.ExtrasValueR\x05value:\x028\x01\"*\n" + - "\x10SetEnableRequest\x12\x16\n" + - "\x06enable\x18\x01 \x01(\bR\x06enable\"\x1a\n" + + "\x05value\x18\x02 \x01(\v2\f.ExtrasValueR\x05value:\x028\x01\"L\n" + + "\x10SetEnableRequest\x12 \n" + + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x16\n" + + "\x06enable\x18\x02 \x01(\bR\x06enable\"\x1a\n" + "\x18SetEnableSuccessResponse\"v\n" + "\x11SetEnableResponse\x125\n" + "\asuccess\x18\x01 \x01(\v2\x19.SetEnableSuccessResponseH\x00R\asuccess\x12\x1e\n" + @@ -575,8 +584,9 @@ const file_meta_proto_rawDesc = "" + "\bresponse26\n" + "\n" + "PluginMeta\x12(\n" + - "\aGetInfo\x12\x16.google.protobuf.Empty\x1a\x05.Info2<\n" + - "\x06Plugin\x122\n" + + "\aGetInfo\x12\x16.google.protobuf.Empty\x1a\x05.Info2l\n" + + "\x06Plugin\x12.\n" + + "\rGetPluginInfo\x12\x16.google.protobuf.Empty\x1a\x05.Info\x122\n" + "\tSetEnable\x12\x11.SetEnableRequest\x1a\x12.SetEnableResponseB\x16Z\x14./generated/protobufb\x06proto3" var ( @@ -608,18 +618,21 @@ var file_meta_proto_goTypes = []any{ var file_meta_proto_depIdxs = []int32{ 9, // 0: Error.details:type_name -> google.protobuf.Any 8, // 1: Message.extras:type_name -> Message.ExtrasEntry - 6, // 2: SetEnableResponse.success:type_name -> SetEnableSuccessResponse - 0, // 3: SetEnableResponse.error:type_name -> Error - 3, // 4: Message.ExtrasEntry.value:type_name -> ExtrasValue - 10, // 5: PluginMeta.GetInfo:input_type -> google.protobuf.Empty - 5, // 6: Plugin.SetEnable:input_type -> SetEnableRequest - 2, // 7: PluginMeta.GetInfo:output_type -> Info - 7, // 8: Plugin.SetEnable:output_type -> SetEnableResponse - 7, // [7:9] is the sub-list for method output_type - 5, // [5:7] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 1, // 2: SetEnableRequest.user:type_name -> UserContext + 6, // 3: SetEnableResponse.success:type_name -> SetEnableSuccessResponse + 0, // 4: SetEnableResponse.error:type_name -> Error + 3, // 5: Message.ExtrasEntry.value:type_name -> ExtrasValue + 10, // 6: PluginMeta.GetInfo:input_type -> google.protobuf.Empty + 10, // 7: Plugin.GetPluginInfo:input_type -> google.protobuf.Empty + 5, // 8: Plugin.SetEnable:input_type -> SetEnableRequest + 2, // 9: PluginMeta.GetInfo:output_type -> Info + 2, // 10: Plugin.GetPluginInfo:output_type -> Info + 7, // 11: Plugin.SetEnable:output_type -> SetEnableResponse + 9, // [9:12] is the sub-list for method output_type + 6, // [6:9] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name } func init() { file_meta_proto_init() } diff --git a/v2/generated/protobuf/meta_grpc.pb.go b/v2/generated/protobuf/meta_grpc.pb.go index 962cb78..4ccd135 100644 --- a/v2/generated/protobuf/meta_grpc.pb.go +++ b/v2/generated/protobuf/meta_grpc.pb.go @@ -122,13 +122,15 @@ var PluginMeta_ServiceDesc = grpc.ServiceDesc{ } const ( - Plugin_SetEnable_FullMethodName = "/Plugin/SetEnable" + Plugin_GetPluginInfo_FullMethodName = "/Plugin/GetPluginInfo" + Plugin_SetEnable_FullMethodName = "/Plugin/SetEnable" ) // PluginClient is the client API for Plugin 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 PluginClient interface { + GetPluginInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) SetEnable(ctx context.Context, in *SetEnableRequest, opts ...grpc.CallOption) (*SetEnableResponse, error) } @@ -140,6 +142,16 @@ func NewPluginClient(cc grpc.ClientConnInterface) PluginClient { return &pluginClient{cc} } +func (c *pluginClient) GetPluginInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Info) + err := c.cc.Invoke(ctx, Plugin_GetPluginInfo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *pluginClient) SetEnable(ctx context.Context, in *SetEnableRequest, opts ...grpc.CallOption) (*SetEnableResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SetEnableResponse) @@ -154,6 +166,7 @@ func (c *pluginClient) SetEnable(ctx context.Context, in *SetEnableRequest, opts // All implementations must embed UnimplementedPluginServer // for forward compatibility. type PluginServer interface { + GetPluginInfo(context.Context, *emptypb.Empty) (*Info, error) SetEnable(context.Context, *SetEnableRequest) (*SetEnableResponse, error) mustEmbedUnimplementedPluginServer() } @@ -165,6 +178,9 @@ type PluginServer interface { // pointer dereference when methods are called. type UnimplementedPluginServer struct{} +func (UnimplementedPluginServer) GetPluginInfo(context.Context, *emptypb.Empty) (*Info, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPluginInfo not implemented") +} func (UnimplementedPluginServer) SetEnable(context.Context, *SetEnableRequest) (*SetEnableResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SetEnable not implemented") } @@ -189,6 +205,24 @@ func RegisterPluginServer(s grpc.ServiceRegistrar, srv PluginServer) { s.RegisterService(&Plugin_ServiceDesc, srv) } +func _Plugin_GetPluginInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).GetPluginInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Plugin_GetPluginInfo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).GetPluginInfo(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _Plugin_SetEnable_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SetEnableRequest) if err := dec(in); err != nil { @@ -214,6 +248,10 @@ var Plugin_ServiceDesc = grpc.ServiceDesc{ ServiceName: "Plugin", HandlerType: (*PluginServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "GetPluginInfo", + Handler: _Plugin_GetPluginInfo_Handler, + }, { MethodName: "SetEnable", Handler: _Plugin_SetEnable_Handler, diff --git a/v2/generated/protobuf/webhooker.pb.go b/v2/generated/protobuf/webhooker.pb.go index 5f25f56..fe1b033 100644 --- a/v2/generated/protobuf/webhooker.pb.go +++ b/v2/generated/protobuf/webhooker.pb.go @@ -24,7 +24,8 @@ const ( type RegisterBasePathRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - BasePath string `protobuf:"bytes,1,opt,name=base_path,json=basePath,proto3" json:"base_path,omitempty"` + User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + BasePath string `protobuf:"bytes,2,opt,name=base_path,json=basePath,proto3" json:"base_path,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -59,6 +60,13 @@ func (*RegisterBasePathRequest) Descriptor() ([]byte, []int) { return file_webhooker_proto_rawDescGZIP(), []int{0} } +func (x *RegisterBasePathRequest) GetUser() *UserContext { + if x != nil { + return x.User + } + return nil +} + func (x *RegisterBasePathRequest) GetBasePath() string { if x != nil { return x.BasePath @@ -70,9 +78,11 @@ var File_webhooker_proto protoreflect.FileDescriptor const file_webhooker_proto_rawDesc = "" + "\n" + - "\x0fwebhooker.proto\x1a\x1bgoogle/protobuf/empty.proto\"6\n" + - "\x17RegisterBasePathRequest\x12\x1b\n" + - "\tbase_path\x18\x01 \x01(\tR\bbasePath2Q\n" + + "\x0fwebhooker.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\n" + + "meta.proto\"X\n" + + "\x17RegisterBasePathRequest\x12 \n" + + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x1b\n" + + "\tbase_path\x18\x02 \x01(\tR\bbasePath2Q\n" + "\tWebhooker\x12D\n" + "\x10RegisterBasePath\x12\x18.RegisterBasePathRequest\x1a\x16.google.protobuf.EmptyB\x16Z\x14./generated/protobufb\x06proto3" @@ -91,16 +101,18 @@ func file_webhooker_proto_rawDescGZIP() []byte { var file_webhooker_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_webhooker_proto_goTypes = []any{ (*RegisterBasePathRequest)(nil), // 0: RegisterBasePathRequest - (*emptypb.Empty)(nil), // 1: google.protobuf.Empty + (*UserContext)(nil), // 1: UserContext + (*emptypb.Empty)(nil), // 2: google.protobuf.Empty } var file_webhooker_proto_depIdxs = []int32{ - 0, // 0: Webhooker.RegisterBasePath:input_type -> RegisterBasePathRequest - 1, // 1: Webhooker.RegisterBasePath:output_type -> google.protobuf.Empty - 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 + 1, // 0: RegisterBasePathRequest.user:type_name -> UserContext + 0, // 1: Webhooker.RegisterBasePath:input_type -> RegisterBasePathRequest + 2, // 2: Webhooker.RegisterBasePath:output_type -> google.protobuf.Empty + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] 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_webhooker_proto_init() } @@ -108,6 +120,7 @@ func file_webhooker_proto_init() { if File_webhooker_proto != nil { return } + file_meta_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/v2/go.mod b/v2/go.mod index a1305d9..c8a0b83 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -3,13 +3,24 @@ module github.com/gotify/plugin-api/v2 go 1.24.5 require ( + github.com/gotify/plugin-api v1.0.0 google.golang.org/grpc v1.74.2 google.golang.org/protobuf v1.36.7 ) require ( + github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect + github.com/gin-gonic/gin v1.3.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/json-iterator/go v1.1.5 // indirect + github.com/mattn/go-isatty v0.0.4 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + gopkg.in/go-playground/validator.v8 v8.18.2 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/v2/go.sum b/v2/go.sum index a09199d..ca9c38e 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,13 +1,34 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= +github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gotify/plugin-api v1.0.0 h1:kab40p2TEPLzjmcafOc7JOz75aTsYQyS2PXtElH8xmI= +github.com/gotify/plugin-api v1.0.0/go.mod h1:xZfEyqVK/Zvu3RwA/CtpuiwFmzFDxifrrqMaH9BHnyU= +github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= @@ -20,8 +41,11 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= @@ -32,3 +56,9 @@ google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/v2/pipe_net.go b/v2/pipe_net.go new file mode 100644 index 0000000..9834d3c --- /dev/null +++ b/v2/pipe_net.go @@ -0,0 +1,34 @@ +package plugin + +import ( + "context" + "crypto/tls" + "net" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func newTCPListener() (net.Listener, error) { + listener, err := net.Listen("tcp", "[::1]:0") + if err != nil { + return nil, err + } + return listener, nil +} + +type GrpcPipeTLS struct { + address string + tlsConfig *tls.Config +} + +func NewGrpcPipeTLS(address string, config *tls.Config) *GrpcPipeTLS { + return &GrpcPipeTLS{ + address: address, + tlsConfig: config, + } +} + +func (p *GrpcPipeTLS) Dial(ctx context.Context) (*grpc.ClientConn, error) { + return grpc.NewClient(p.address, grpc.WithTransportCredentials(credentials.NewTLS(p.tlsConfig))) +} diff --git a/v2/pipe_net_test.go b/v2/pipe_net_test.go new file mode 100644 index 0000000..f871384 --- /dev/null +++ b/v2/pipe_net_test.go @@ -0,0 +1,94 @@ +package plugin + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "net" + "testing" + "time" + + "github.com/gotify/plugin-api/v2/generated/protobuf" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/protobuf/types/known/emptypb" +) + +type dummyInfraServer struct { + protobuf.UnimplementedInfraServer +} + +func (s *dummyInfraServer) GetServerVersion(ctx context.Context, req *emptypb.Empty) (*protobuf.ServerVersionInfo, error) { + return &protobuf.ServerVersionInfo{ + Version: "test", + Commit: "test", + BuildDate: time.Now().Format(time.RFC3339), + }, nil +} + +func TestGrpcPipeNet(t *testing.T) { + serverPub, serverPriv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + tlsClient, err := NewEphemeralTLSClient() + if err != nil { + t.Fatal(err) + } + listener, err := newTCPListener() + if err != nil { + t.Fatal(err) + } + defer listener.Close() + serverCsrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: buildPluginTLSName("test"), + }, + DNSNames: []string{ + buildPluginTLSName("test"), + }, + PublicKey: serverPub, + }, serverPriv) + serverCsr, err := x509.ParseCertificateRequest(serverCsrBytes) + if err != nil { + t.Fatal(err) + } + serverCertBytes, err := tlsClient.SignPluginCSR("test", serverCsr) + if err != nil { + t.Fatal(err) + } + clientPipe := NewGrpcPipeTLS(fmt.Sprintf("[::1]:%d", listener.Addr().(*net.TCPAddr).Port), tlsClient.ClientTLSConfig("test")) + + serverTLSConfig := tlsClient.ServerTLSConfig() + serverTLSConfig.Certificates = []tls.Certificate{ + { + Certificate: [][]byte{serverCertBytes}, + PrivateKey: serverPriv, + }, + } + + server := grpc.NewServer(grpc.Creds(credentials.NewTLS(serverTLSConfig))) + protobuf.RegisterInfraServer(server, &dummyInfraServer{}) + go server.Serve(listener) + defer server.GracefulStop() + + conn, err := clientPipe.Dial(context.Background()) + if err != nil { + t.Fatal(err) + } + + infraClient := protobuf.NewInfraClient(conn) + version, err := infraClient.GetServerVersion(context.Background(), &emptypb.Empty{}) + if err != nil { + t.Fatal(err) + } + if version.Version != "test" { + t.Fatal("expected test, got ", version.Version) + } + + defer conn.Close() +} diff --git a/v2/pipe_not_unix.go b/v2/pipe_not_unix.go new file mode 100644 index 0000000..f376597 --- /dev/null +++ b/v2/pipe_not_unix.go @@ -0,0 +1,13 @@ +//go:build !unix + +package plugin + +import "net" + +func newListener() (net.Listener, error) { + listener, err := net.Listen("tcp", "[::1]:0") + if err != nil { + return nil, err + } + return listener, nil +} diff --git a/v2/pipe_unix.go b/v2/pipe_unix.go new file mode 100644 index 0000000..b6c8344 --- /dev/null +++ b/v2/pipe_unix.go @@ -0,0 +1,21 @@ +//go:build unix + +package plugin + +import ( + "net" + "os" + "path/filepath" +) + +func newListener() (net.Listener, error) { + tmpDir, err := os.MkdirTemp("", "gotify-plugin-*") + if err != nil { + return nil, err + } + if err := os.Chmod(tmpDir, 0700); err != nil { + return nil, err + } + pipePath := filepath.Join(tmpDir, "plugin.sock") + return net.Listen("unix", pipePath) +} diff --git a/v2/protobuf/config.proto b/v2/protobuf/config.proto index aa94653..45333e2 100644 --- a/v2/protobuf/config.proto +++ b/v2/protobuf/config.proto @@ -1,5 +1,4 @@ syntax = "proto3"; -import "google/protobuf/empty.proto"; import "meta.proto"; option go_package = "./generated/protobuf"; @@ -8,8 +7,13 @@ message Config { string config = 1; } +message DefaultConfigRequest { + UserContext user = 1; +} + message ValidateAndSetConfigRequest { - Config config = 1; + UserContext user = 1; + Config config = 2; } message ValidateAndSetConfigSuccessResponse { @@ -24,6 +28,6 @@ message ValidateAndSetConfigResponse { } service Configurer { - rpc DefaultConfig(google.protobuf.Empty) returns (Config); + rpc DefaultConfig(DefaultConfigRequest) returns (Config); rpc ValidateAndSetConfig(ValidateAndSetConfigRequest) returns (ValidateAndSetConfigResponse); } \ No newline at end of file diff --git a/v2/protobuf/display.proto b/v2/protobuf/display.proto index 4bd775d..39a2df5 100644 --- a/v2/protobuf/display.proto +++ b/v2/protobuf/display.proto @@ -1,9 +1,11 @@ syntax = "proto3"; +import "meta.proto"; option go_package = "./generated/protobuf"; message DisplayRequest { - string location = 1; + UserContext user = 1; + string location = 2; } message DisplayResponse { diff --git a/v2/protobuf/infra.proto b/v2/protobuf/infra.proto index 35fa326..b67ca5b 100644 --- a/v2/protobuf/infra.proto +++ b/v2/protobuf/infra.proto @@ -12,11 +12,13 @@ message ServerVersionInfo { } message StorageSaveRequest { - bytes data = 1; + uint64 userid = 1; + bytes data = 2; } message StorageLoadResponse { - bytes data = 1; + uint64 userid = 1; + bytes data = 2; } service Infra { diff --git a/v2/protobuf/meta.proto b/v2/protobuf/meta.proto index 81e40db..f0dd153 100644 --- a/v2/protobuf/meta.proto +++ b/v2/protobuf/meta.proto @@ -41,7 +41,8 @@ message Message { } message SetEnableRequest { - bool enable = 1; + UserContext user = 1; + bool enable = 2; } message SetEnableSuccessResponse { @@ -60,6 +61,7 @@ service PluginMeta { } service Plugin { + rpc GetPluginInfo(google.protobuf.Empty) returns (Info); rpc SetEnable(SetEnableRequest) returns (SetEnableResponse); } diff --git a/v2/protobuf/webhooker.proto b/v2/protobuf/webhooker.proto index 047ebe2..048a1c7 100644 --- a/v2/protobuf/webhooker.proto +++ b/v2/protobuf/webhooker.proto @@ -1,10 +1,12 @@ syntax = "proto3"; import "google/protobuf/empty.proto"; +import "meta.proto"; option go_package = "./generated/protobuf"; -message RegisterBasePathRequest { - string base_path = 1; +message RegisterBasePathRequest { + UserContext user = 1; + string base_path = 2; } service Webhooker { diff --git a/v2/rpc.go b/v2/rpc.go index 073e209..3e57ef7 100644 --- a/v2/rpc.go +++ b/v2/rpc.go @@ -2,21 +2,33 @@ package plugin import ( "context" + "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "fmt" + "log" "net" + "os" "github.com/gotify/plugin-api/v2/generated/protobuf" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" "google.golang.org/protobuf/types/known/emptypb" ) -// PluginRpcHandler handles and coordinates RPC between the plugin and the server. -type PluginRpcHandler struct { - infraTarget string - server *grpc.Server +type GrpcDialer interface { + Dial(ctx context.Context) (*grpc.ClientConn, error) } -func NewPluginRpcHandler(infraTarget string) *PluginRpcHandler { - infraClient, err := grpc.NewClient(infraTarget) +type PluginRpc struct { + infraDialer GrpcDialer + pluginServer *grpc.Server +} + +func NewPluginRpc(infraDialer GrpcDialer) *PluginRpc { + infraClient, err := infraDialer.Dial(context.Background()) if err != nil { panic(err) } @@ -27,12 +39,193 @@ func NewPluginRpcHandler(infraTarget string) *PluginRpcHandler { } _ = version - return &PluginRpcHandler{ - infraTarget: infraTarget, - server: grpc.NewServer(), + return &PluginRpc{ + infraDialer: infraDialer, + pluginServer: grpc.NewServer(), + } +} + +func (h *PluginRpc) Serve(listener net.Listener) error { + return h.pluginServer.Serve(listener) +} + +type ServerVersionInfo struct { + Version string + Commit string + BuildDate string +} + +type infraServerImpl struct { + version ServerVersionInfo + protobuf.UnimplementedInfraServer +} + +func (s *infraServerImpl) GetServerVersion(ctx context.Context, req *emptypb.Empty) (*protobuf.ServerVersionInfo, error) { + peer, ok := peer.FromContext(ctx) + if !ok { + return nil, fmt.Errorf("no peer in context") } + authInfo := peer.AuthInfo.(*infraTlsAuthInfo) + log.Printf("GetServerVersion: server name %s, module name %s", authInfo.TLSInfo.State.ServerName, authInfo.moduleName) + return &protobuf.ServerVersionInfo{ + Version: s.version.Version, + Commit: s.version.Commit, + BuildDate: s.version.BuildDate, + }, nil } -func (h *PluginRpcHandler) Serve(listener net.Listener) error { - return h.server.Serve(listener) +type pluginConnection struct { + info *protobuf.Info + conn *grpc.ClientConn +} + +type ServerMux struct { + tlsClient *EphemeralTLSClient + infraAddr net.Addr + infraListener net.Listener + infraServer *grpc.Server + pluginDNSToModulePath map[string]string + pluginConnections map[string]pluginConnection +} + +type infraTlsCreds struct { + pluginDNSToModulePath map[string]string + credentials.TransportCredentials +} + +type infraTlsAuthInfo struct { + moduleName string + credentials.TLSInfo +} + +func (c *infraTlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { + netConn, authInfo, err := c.TransportCredentials.ServerHandshake(rawConn) + if err != nil { + log.Printf("ServerHandshake: error %v", err) + rawConn.Close() + return nil, nil, err + } + protocolInfo := authInfo.(credentials.TLSInfo) + serverName := protocolInfo.State.VerifiedChains[0][0].DNSNames[0] + moduleName, ok := c.pluginDNSToModulePath[serverName] + if !ok { + log.Printf("ServerHandshake: unknown server name %s", serverName) + netConn.Close() + rawConn.Close() + return nil, nil, fmt.Errorf("unknown server name %s", serverName) + } + + return netConn, &infraTlsAuthInfo{ + moduleName: moduleName, + TLSInfo: protocolInfo, + }, nil +} + +func NewServerMux(info ServerVersionInfo) *ServerMux { + tlsClient, err := NewEphemeralTLSClient() + if err != nil { + panic(err) + } + _, infraPriv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + panic(err) + } + infraCsrBytes, err := x509.CreateCertificateRequest(rand.Reader, new(x509.CertificateRequest), infraPriv) + if err != nil { + panic(err) + } + infraCsr, err := x509.ParseCertificateRequest(infraCsrBytes) + if err != nil { + panic(err) + } + if err := infraCsr.CheckSignature(); err != nil { + panic(err) + } + infraCert, err := tlsClient.SignCSR(ServerTLSName, infraCsr) + if err != nil { + panic(err) + } + infraCertParsed, err := x509.ParseCertificate(infraCert) + if err != nil { + panic(err) + } + caCertPool := x509.NewCertPool() + caCertPool.AddCert(tlsClient.caCert) + infraTlsConfig := &tls.Config{ + Certificates: []tls.Certificate{ + { + Certificate: [][]byte{infraCert, tlsClient.caCert.Raw}, + PrivateKey: infraPriv, + Leaf: infraCertParsed, + }, + }, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: caCertPool, + ServerName: ServerTLSName, + } + + pluginDNSToModulePath := make(map[string]string) + infraServer := grpc.NewServer(grpc.Creds(&infraTlsCreds{ + pluginDNSToModulePath: pluginDNSToModulePath, + TransportCredentials: credentials.NewTLS(infraTlsConfig), + })) + protobuf.RegisterInfraServer(infraServer, &infraServerImpl{ + version: info, + }) + listener, err := newListener() + if err != nil { + panic(err) + } + go infraServer.Serve(listener) + + return &ServerMux{ + tlsClient: tlsClient, + infraAddr: listener.Addr(), + infraListener: listener, + infraServer: infraServer, + pluginDNSToModulePath: pluginDNSToModulePath, + pluginConnections: make(map[string]pluginConnection), + } +} + +func (s *ServerMux) InfraAddr() net.Addr { + return s.infraAddr +} + +func (s *ServerMux) CACert() *x509.Certificate { + return s.tlsClient.caCert +} + +func (s *ServerMux) SignPluginCSR(moduleName string, csr *x509.CertificateRequest) ([]byte, error) { + return s.tlsClient.SignPluginCSR(moduleName, csr) +} + +func (s *ServerMux) RegisterPlugin(target string, moduleName string) (*grpc.ClientConn, error) { + grpcConn, err := grpc.NewClient(target, grpc.WithTransportCredentials(credentials.NewTLS(s.tlsClient.ClientTLSConfig(moduleName)))) + if err != nil { + return nil, err + } + s.pluginDNSToModulePath[buildPluginTLSName(moduleName)] = moduleName + pluginClient := protobuf.NewPluginClient(grpcConn) + pluginInfo, err := pluginClient.GetPluginInfo(context.Background(), &emptypb.Empty{}) + if err != nil { + return nil, err + } + s.pluginConnections[moduleName] = pluginConnection{ + info: pluginInfo, + conn: grpcConn, + } + return grpcConn, nil +} + +func (s *ServerMux) Close() error { + for _, conn := range s.pluginConnections { + conn.conn.Close() + } + s.infraServer.GracefulStop() + if s.infraAddr.Network() == "unix" { + os.Remove(s.infraAddr.String()) + } + s.infraListener.Close() + return nil } diff --git a/v2/rpc_test.go b/v2/rpc_test.go new file mode 100644 index 0000000..aaf9f04 --- /dev/null +++ b/v2/rpc_test.go @@ -0,0 +1,124 @@ +package plugin + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "log" + "testing" + "time" + + "github.com/gotify/plugin-api/v2/generated/protobuf" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/protobuf/types/known/emptypb" +) + +type dummyPlugin struct { + protobuf.UnimplementedPluginServer +} + +var dummyPluginInfo = &protobuf.Info{ + Name: "dummy", + Version: "test", + Description: "dummy plugin", + Author: "gotify", + License: "MIT", + ModulePath: "dummy.example", +} + +func (p *dummyPlugin) GetPluginInfo(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { + return dummyPluginInfo, nil +} + +func TestRPC(t *testing.T) { + rpc := NewServerMux(ServerVersionInfo{ + Version: "test", + Commit: "test", + BuildDate: time.Now().Format(time.RFC3339), + }) + defer rpc.Close() + _, pluginPriv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + pluginCsrBytes, err := x509.CreateCertificateRequest(rand.Reader, new(x509.CertificateRequest), pluginPriv) + pluginCsr, err := x509.ParseCertificateRequest(pluginCsrBytes) + if err != nil { + t.Fatal(err) + } + if err := pluginCsr.CheckSignature(); err != nil { + t.Fatal(err) + } + pluginCert, err := rpc.SignPluginCSR(dummyPluginInfo.ModulePath, pluginCsr) + if err != nil { + t.Fatal(err) + } + pluginCertParsed, err := x509.ParseCertificate(pluginCert) + if err != nil { + t.Fatal(err) + } + pluginTlsConfig := rpc.tlsClient.ServerTLSConfig() + pluginTlsConfig.Certificates = []tls.Certificate{ + { + Certificate: [][]byte{pluginCert}, + PrivateKey: pluginPriv, + }, + } + pluginListener, err := newListener() + if err != nil { + t.Fatal(err) + } + defer pluginListener.Close() + pluginListenerTarget := pluginListener.Addr().String() + if pluginListener.Addr().Network() == "unix" { + pluginListenerTarget = "unix://" + pluginListenerTarget + } + + pluginServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(pluginTlsConfig))) + protobuf.RegisterPluginServer(pluginServer, &dummyPlugin{}) + go pluginServer.Serve(pluginListener) + defer pluginServer.GracefulStop() + + conn, err := rpc.RegisterPlugin(pluginListenerTarget, dummyPluginInfo.ModulePath) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + caCertPool := x509.NewCertPool() + caCertPool.AddCert(rpc.CACert()) + pluginClientTlsConfig := &tls.Config{ + RootCAs: caCertPool, + ServerName: ServerTLSName, + Certificates: []tls.Certificate{ + { + Certificate: [][]byte{pluginCert}, + PrivateKey: pluginPriv, + Leaf: pluginCertParsed, + }, + }, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: caCertPool, + } + + infraAddr := rpc.InfraAddr() + infraAddrTarget := infraAddr.String() + if infraAddr.Network() == "unix" { + infraAddrTarget = "unix://" + infraAddrTarget + } + + pluginClient, err := grpc.NewClient(infraAddrTarget, grpc.WithTransportCredentials(credentials.NewTLS(pluginClientTlsConfig))) + if err != nil { + t.Fatal(err) + } + defer pluginClient.Close() + pluginInfraClient := protobuf.NewInfraClient(pluginClient) + version, err := pluginInfraClient.GetServerVersion(context.Background(), &emptypb.Empty{}) + if err != nil { + t.Fatal(err) + } + log.Printf("plugin version: %s", version.Version) +} diff --git a/v2/transport_auth.go b/v2/transport_auth.go new file mode 100644 index 0000000..f4146e0 --- /dev/null +++ b/v2/transport_auth.go @@ -0,0 +1,156 @@ +package plugin + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "fmt" + "time" +) + +const ServerTLSName = "gotify.home.arpa" + +func buildPluginTLSName(moduleName string) string { + moduleNameHash := sha256.Sum256([]byte(moduleName)) + hashHex := hex.EncodeToString(moduleNameHash[:]) + return fmt.Sprintf("%s.plugins.gotify.home.arpa", hashHex) +} + +type EphemeralTLSClient struct { + caCert *x509.Certificate + caPriv ed25519.PrivateKey + tlsConfig *tls.Config +} + +func (s *EphemeralTLSClient) createCertPool() *x509.CertPool { + pool := x509.NewCertPool() + pool.AddCert(s.caCert) + return pool +} + +func (s *EphemeralTLSClient) ServerTLSConfig() *tls.Config { + return &tls.Config{ + ServerName: ServerTLSName, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: s.createCertPool(), + } +} + +func (s *EphemeralTLSClient) ClientTLSConfig(moduleName string) *tls.Config { + return &tls.Config{ + Certificates: []tls.Certificate{ + { + Certificate: [][]byte{s.caCert.Raw}, + PrivateKey: s.caPriv, + }, + }, + RootCAs: s.createCertPool(), + ServerName: buildPluginTLSName(moduleName), + } +} + +func (s *EphemeralTLSClient) SignCSR(dnsName string, csr *x509.CertificateRequest) ([]byte, error) { + if err := csr.CheckSignature(); err != nil { + return nil, err + } + certTemplate := &x509.Certificate{ + BasicConstraintsValid: true, + Subject: pkix.Name{ + CommonName: dnsName, + }, + DNSNames: []string{ + dnsName, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 365), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + }, + IsCA: false, + } + certBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, s.caCert, csr.PublicKey, s.caPriv) + if err != nil { + return nil, err + } + return certBytes, nil +} + +func (s *EphemeralTLSClient) SignPluginCSR(moduleName string, csr *x509.CertificateRequest) ([]byte, error) { + return s.SignCSR(buildPluginTLSName(moduleName), csr) +} + +func NewEphemeralTLSClient() (*EphemeralTLSClient, error) { + caPub, caPriv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + caCertTemplate := &x509.Certificate{ + BasicConstraintsValid: true, + Subject: pkix.Name{ + CommonName: "gotify Plugin client CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 365), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageClientAuth, + x509.ExtKeyUsageServerAuth, + }, + IsCA: true, + } + caCertBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, caPub, caPriv) + caCert, err := x509.ParseCertificate(caCertBytes) + if err != nil { + return nil, err + } + clientPub, clientPriv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + clientCertTemplate := &x509.Certificate{ + BasicConstraintsValid: true, + Subject: pkix.Name{ + CommonName: ServerTLSName, + }, + DNSNames: []string{ + ServerTLSName, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 365), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageClientAuth, + }, + IsCA: false, + } + clientCertBytes, err := x509.CreateCertificate(rand.Reader, clientCertTemplate, caCert, clientPub, caPriv) + if err != nil { + return nil, err + } + certPool := x509.NewCertPool() + certPool.AddCert(caCert) + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{ + { + Certificate: [][]byte{clientCertBytes}, + PrivateKey: clientPriv, + }, + { + Certificate: [][]byte{caCertBytes}, + PrivateKey: caPriv, + }, + }, + RootCAs: certPool, + } + return &EphemeralTLSClient{ + caCert: caCert, + caPriv: caPriv, + tlsConfig: tlsConfig, + }, nil +} diff --git a/v2/transport_auth_test.go b/v2/transport_auth_test.go new file mode 100644 index 0000000..c4eaf78 --- /dev/null +++ b/v2/transport_auth_test.go @@ -0,0 +1,74 @@ +package plugin + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "net" + "testing" + "time" +) + +func TestEphemeralTLSClient(t *testing.T) { + client, err := NewEphemeralTLSClient() + if err != nil { + t.Fatal(err) + } + + pluginTlsName := buildPluginTLSName("test") + _, serverPriv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + serverCSRBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: pluginTlsName, + }, + DNSNames: []string{ + buildPluginTLSName("test"), + }, + }, serverPriv) + if err != nil { + t.Fatal(err) + } + serverCSR, err := x509.ParseCertificateRequest(serverCSRBytes) + if err != nil { + t.Fatal(err) + } + serverCert, err := client.SignPluginCSR("test", serverCSR) + if err != nil { + t.Fatal(err) + } + + s, c := net.Pipe() + defer s.Close() + defer c.Close() + go func() { + serverTLSConfig := client.ServerTLSConfig() + serverTLSConfig.Certificates = []tls.Certificate{ + { + Certificate: [][]byte{serverCert}, + PrivateKey: serverPriv, + }, + } + tlsServer := tls.Server(s, serverTLSConfig) + _, err = tlsServer.Write([]byte("hello")) + if err != nil { + panic(err) + } + }() + + tlsClient := tls.Client(c, client.ClientTLSConfig("test")) + tlsClient.SetDeadline(time.Now().Add(time.Second * 1)) + buf := make([]byte, 1024) + n, err := tlsClient.Read(buf) + if err != nil { + t.Fatal(err) + } + if string(buf[:n]) != "hello" { + t.Fatal("expected hello, got ", string(buf[:n])) + } + +} From 8a89a6b98466ed0329a8779231a4af917a8f4e23 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Fri, 15 Aug 2025 12:12:49 -0500 Subject: [PATCH 03/37] temp Signed-off-by: eternal-flame-AD --- v2/generated/protobuf/infra.pb.go | 28 +++++---- v2/generated/protobuf/infra_grpc.pb.go | 38 ++++++++++++ v2/go.mod | 14 +---- v2/go.sum | 29 +-------- v2/protobuf/infra.proto | 1 + v2/rpc.go | 85 +++++++++++++++++++++----- v2/rpc_test.go | 46 ++++++-------- 7 files changed, 150 insertions(+), 91 deletions(-) diff --git a/v2/generated/protobuf/infra.pb.go b/v2/generated/protobuf/infra.pb.go index 5a0ea7a..c49ed63 100644 --- a/v2/generated/protobuf/infra.pb.go +++ b/v2/generated/protobuf/infra.pb.go @@ -201,8 +201,9 @@ const file_infra_proto_rawDesc = "" + "\x04data\x18\x02 \x01(\fR\x04data\"A\n" + "\x13StorageLoadResponse\x12\x16\n" + "\x06userid\x18\x01 \x01(\x04R\x06userid\x12\x12\n" + - "\x04data\x18\x02 \x01(\fR\x04data2\xef\x01\n" + - "\x05Infra\x12>\n" + + "\x04data\x18\x02 \x01(\fR\x04data2\x98\x02\n" + + "\x05Infra\x12'\n" + + "\x06WhoAmI\x12\x16.google.protobuf.Empty\x1a\x05.Info\x12>\n" + "\x10GetServerVersion\x12\x16.google.protobuf.Empty\x1a\x12.ServerVersionInfo\x129\n" + "\n" + "SaveConfig\x12\x13.StorageSaveRequest\x1a\x16.google.protobuf.Empty\x12:\n" + @@ -229,18 +230,21 @@ var file_infra_proto_goTypes = []any{ (*StorageLoadResponse)(nil), // 2: StorageLoadResponse (*emptypb.Empty)(nil), // 3: google.protobuf.Empty (*Message)(nil), // 4: Message + (*Info)(nil), // 5: Info } var file_infra_proto_depIdxs = []int32{ - 3, // 0: Infra.GetServerVersion:input_type -> google.protobuf.Empty - 1, // 1: Infra.SaveConfig:input_type -> StorageSaveRequest - 3, // 2: Infra.LoadConfig:input_type -> google.protobuf.Empty - 4, // 3: Infra.SendMessage:input_type -> Message - 0, // 4: Infra.GetServerVersion:output_type -> ServerVersionInfo - 3, // 5: Infra.SaveConfig:output_type -> google.protobuf.Empty - 2, // 6: Infra.LoadConfig:output_type -> StorageLoadResponse - 3, // 7: Infra.SendMessage:output_type -> google.protobuf.Empty - 4, // [4:8] is the sub-list for method output_type - 0, // [0:4] is the sub-list for method input_type + 3, // 0: Infra.WhoAmI:input_type -> google.protobuf.Empty + 3, // 1: Infra.GetServerVersion:input_type -> google.protobuf.Empty + 1, // 2: Infra.SaveConfig:input_type -> StorageSaveRequest + 3, // 3: Infra.LoadConfig:input_type -> google.protobuf.Empty + 4, // 4: Infra.SendMessage:input_type -> Message + 5, // 5: Infra.WhoAmI:output_type -> Info + 0, // 6: Infra.GetServerVersion:output_type -> ServerVersionInfo + 3, // 7: Infra.SaveConfig:output_type -> google.protobuf.Empty + 2, // 8: Infra.LoadConfig:output_type -> StorageLoadResponse + 3, // 9: Infra.SendMessage:output_type -> google.protobuf.Empty + 5, // [5:10] is the sub-list for method output_type + 0, // [0:5] 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 diff --git a/v2/generated/protobuf/infra_grpc.pb.go b/v2/generated/protobuf/infra_grpc.pb.go index 20f18fe..3ed1d24 100644 --- a/v2/generated/protobuf/infra_grpc.pb.go +++ b/v2/generated/protobuf/infra_grpc.pb.go @@ -20,6 +20,7 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( + Infra_WhoAmI_FullMethodName = "/Infra/WhoAmI" Infra_GetServerVersion_FullMethodName = "/Infra/GetServerVersion" Infra_SaveConfig_FullMethodName = "/Infra/SaveConfig" Infra_LoadConfig_FullMethodName = "/Infra/LoadConfig" @@ -30,6 +31,7 @@ const ( // // 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 InfraClient interface { + WhoAmI(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) GetServerVersion(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ServerVersionInfo, error) SaveConfig(ctx context.Context, in *StorageSaveRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) LoadConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StorageLoadResponse, error) @@ -44,6 +46,16 @@ func NewInfraClient(cc grpc.ClientConnInterface) InfraClient { return &infraClient{cc} } +func (c *infraClient) WhoAmI(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Info) + err := c.cc.Invoke(ctx, Infra_WhoAmI_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *infraClient) GetServerVersion(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ServerVersionInfo, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ServerVersionInfo) @@ -88,6 +100,7 @@ func (c *infraClient) SendMessage(ctx context.Context, in *Message, opts ...grpc // All implementations must embed UnimplementedInfraServer // for forward compatibility. type InfraServer interface { + WhoAmI(context.Context, *emptypb.Empty) (*Info, error) GetServerVersion(context.Context, *emptypb.Empty) (*ServerVersionInfo, error) SaveConfig(context.Context, *StorageSaveRequest) (*emptypb.Empty, error) LoadConfig(context.Context, *emptypb.Empty) (*StorageLoadResponse, error) @@ -102,6 +115,9 @@ type InfraServer interface { // pointer dereference when methods are called. type UnimplementedInfraServer struct{} +func (UnimplementedInfraServer) WhoAmI(context.Context, *emptypb.Empty) (*Info, error) { + return nil, status.Errorf(codes.Unimplemented, "method WhoAmI not implemented") +} func (UnimplementedInfraServer) GetServerVersion(context.Context, *emptypb.Empty) (*ServerVersionInfo, error) { return nil, status.Errorf(codes.Unimplemented, "method GetServerVersion not implemented") } @@ -135,6 +151,24 @@ func RegisterInfraServer(s grpc.ServiceRegistrar, srv InfraServer) { s.RegisterService(&Infra_ServiceDesc, srv) } +func _Infra_WhoAmI_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InfraServer).WhoAmI(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Infra_WhoAmI_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InfraServer).WhoAmI(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _Infra_GetServerVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { @@ -214,6 +248,10 @@ var Infra_ServiceDesc = grpc.ServiceDesc{ ServiceName: "Infra", HandlerType: (*InfraServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "WhoAmI", + Handler: _Infra_WhoAmI_Handler, + }, { MethodName: "GetServerVersion", Handler: _Infra_GetServerVersion_Handler, diff --git a/v2/go.mod b/v2/go.mod index c8a0b83..9b8c66c 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -3,24 +3,16 @@ module github.com/gotify/plugin-api/v2 go 1.24.5 require ( - github.com/gotify/plugin-api v1.0.0 + github.com/stretchr/testify v1.3.0 google.golang.org/grpc v1.74.2 google.golang.org/protobuf v1.36.7 ) require ( - github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect - github.com/gin-gonic/gin v1.3.0 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/json-iterator/go v1.1.5 // indirect - github.com/mattn/go-isatty v0.0.4 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.1 // indirect - github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 // indirect + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect - gopkg.in/go-playground/validator.v8 v8.18.2 // indirect - gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/v2/go.sum b/v2/go.sum index ca9c38e..198eaaf 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,34 +1,20 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= -github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= -github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= -github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gotify/plugin-api v1.0.0 h1:kab40p2TEPLzjmcafOc7JOz75aTsYQyS2PXtElH8xmI= -github.com/gotify/plugin-api v1.0.0/go.mod h1:xZfEyqVK/Zvu3RwA/CtpuiwFmzFDxifrrqMaH9BHnyU= -github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= -github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4= -github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= @@ -41,11 +27,8 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= @@ -56,9 +39,3 @@ google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= -gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/v2/protobuf/infra.proto b/v2/protobuf/infra.proto index b67ca5b..4ecbb54 100644 --- a/v2/protobuf/infra.proto +++ b/v2/protobuf/infra.proto @@ -22,6 +22,7 @@ message StorageLoadResponse { } service Infra { + rpc WhoAmI(google.protobuf.Empty) returns (Info); rpc GetServerVersion(google.protobuf.Empty) returns (ServerVersionInfo); rpc SaveConfig(StorageSaveRequest) returns (google.protobuf.Empty); rpc LoadConfig(google.protobuf.Empty) returns (StorageLoadResponse); diff --git a/v2/rpc.go b/v2/rpc.go index 3e57ef7..105b0e0 100644 --- a/v2/rpc.go +++ b/v2/rpc.go @@ -56,17 +56,12 @@ type ServerVersionInfo struct { } type infraServerImpl struct { + server *ServerMux version ServerVersionInfo protobuf.UnimplementedInfraServer } func (s *infraServerImpl) GetServerVersion(ctx context.Context, req *emptypb.Empty) (*protobuf.ServerVersionInfo, error) { - peer, ok := peer.FromContext(ctx) - if !ok { - return nil, fmt.Errorf("no peer in context") - } - authInfo := peer.AuthInfo.(*infraTlsAuthInfo) - log.Printf("GetServerVersion: server name %s, module name %s", authInfo.TLSInfo.State.ServerName, authInfo.moduleName) return &protobuf.ServerVersionInfo{ Version: s.version.Version, Commit: s.version.Commit, @@ -74,18 +69,46 @@ func (s *infraServerImpl) GetServerVersion(ctx context.Context, req *emptypb.Emp }, nil } -type pluginConnection struct { +func (s *infraServerImpl) WhoAmI(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { + peer, ok := peer.FromContext(ctx) + if !ok { + return nil, fmt.Errorf("no peer in context") + } + authInfo := peer.AuthInfo.(*infraTlsAuthInfo) + return s.server.GetPluginInfo(authInfo.moduleName) +} + +type PluginConnection struct { info *protobuf.Info conn *grpc.ClientConn } type ServerMux struct { + version ServerVersionInfo tlsClient *EphemeralTLSClient infraAddr net.Addr infraListener net.Listener infraServer *grpc.Server pluginDNSToModulePath map[string]string - pluginConnections map[string]pluginConnection + pluginConnections map[string]PluginConnection + protobuf.UnimplementedInfraServer +} + +func (s *ServerMux) GetServerVersion(ctx context.Context, req *emptypb.Empty) (*protobuf.ServerVersionInfo, error) { + return &protobuf.ServerVersionInfo{ + Version: s.version.Version, + Commit: s.version.Commit, + BuildDate: s.version.BuildDate, + }, nil +} + +func (s *ServerMux) WhoAmI(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { + peer, ok := peer.FromContext(ctx) + if !ok { + return nil, fmt.Errorf("no peer in context") + } + authInfo := peer.AuthInfo.(*infraTlsAuthInfo) + return s.GetPluginInfo(authInfo.moduleName) } type infraTlsCreds struct { @@ -121,6 +144,7 @@ func (c *infraTlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, credentials }, nil } +// NewServerMux creates a server-side mux with an infra server that handles plugin-to-server calls. func NewServerMux(info ServerVersionInfo) *ServerMux { tlsClient, err := NewEphemeralTLSClient() if err != nil { @@ -169,33 +193,41 @@ func NewServerMux(info ServerVersionInfo) *ServerMux { pluginDNSToModulePath: pluginDNSToModulePath, TransportCredentials: credentials.NewTLS(infraTlsConfig), })) - protobuf.RegisterInfraServer(infraServer, &infraServerImpl{ - version: info, - }) + listener, err := newListener() if err != nil { panic(err) } - go infraServer.Serve(listener) - - return &ServerMux{ + mux := &ServerMux{ + version: info, tlsClient: tlsClient, infraAddr: listener.Addr(), infraListener: listener, infraServer: infraServer, pluginDNSToModulePath: pluginDNSToModulePath, - pluginConnections: make(map[string]pluginConnection), + pluginConnections: make(map[string]PluginConnection), } + protobuf.RegisterInfraServer(infraServer, &infraServerImpl{ + server: mux, + version: info, + }) + + go infraServer.Serve(listener) + + return mux } +// InfraAddr returns the address of the infra server for plugin-to-server callbacks. func (s *ServerMux) InfraAddr() net.Addr { return s.infraAddr } +// CACert returns the CA certificate for mutual TLS authentication. func (s *ServerMux) CACert() *x509.Certificate { return s.tlsClient.caCert } +// SignPluginCSR signs a certificate request for a plugin. func (s *ServerMux) SignPluginCSR(moduleName string, csr *x509.CertificateRequest) ([]byte, error) { return s.tlsClient.SignPluginCSR(moduleName, csr) } @@ -205,19 +237,40 @@ func (s *ServerMux) RegisterPlugin(target string, moduleName string) (*grpc.Clie if err != nil { return nil, err } + if _, exists := s.pluginDNSToModulePath[buildPluginTLSName(moduleName)]; exists { + return nil, fmt.Errorf("plugin %s already registered", moduleName) + } s.pluginDNSToModulePath[buildPluginTLSName(moduleName)] = moduleName pluginClient := protobuf.NewPluginClient(grpcConn) pluginInfo, err := pluginClient.GetPluginInfo(context.Background(), &emptypb.Empty{}) if err != nil { return nil, err } - s.pluginConnections[moduleName] = pluginConnection{ + s.pluginConnections[moduleName] = PluginConnection{ info: pluginInfo, conn: grpcConn, } return grpcConn, nil } +// GetPluginInfo returns the info of a plugin. +func (s *ServerMux) GetPluginInfo(moduleName string) (*protobuf.Info, error) { + conn, ok := s.pluginConnections[moduleName] + if !ok { + return nil, fmt.Errorf("plugin %s not registered", moduleName) + } + return conn.info, nil +} + +// GetPluginConnection returns the connection to the plugin for Server-to-Plugin calls. +func (s *ServerMux) GetPluginConnection(moduleName string) (*grpc.ClientConn, error) { + conn, ok := s.pluginConnections[moduleName] + if !ok { + return nil, fmt.Errorf("plugin %s not registered", moduleName) + } + return conn.conn, nil +} + func (s *ServerMux) Close() error { for _, conn := range s.pluginConnections { conn.conn.Close() diff --git a/v2/rpc_test.go b/v2/rpc_test.go index aaf9f04..cd0705b 100644 --- a/v2/rpc_test.go +++ b/v2/rpc_test.go @@ -6,11 +6,11 @@ import ( "crypto/rand" "crypto/tls" "crypto/x509" - "log" "testing" "time" "github.com/gotify/plugin-api/v2/generated/protobuf" + "github.com/stretchr/testify/assert" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/protobuf/types/known/emptypb" @@ -41,25 +41,15 @@ func TestRPC(t *testing.T) { }) defer rpc.Close() _, pluginPriv, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } + assert.NoError(t, err) pluginCsrBytes, err := x509.CreateCertificateRequest(rand.Reader, new(x509.CertificateRequest), pluginPriv) pluginCsr, err := x509.ParseCertificateRequest(pluginCsrBytes) - if err != nil { - t.Fatal(err) - } - if err := pluginCsr.CheckSignature(); err != nil { - t.Fatal(err) - } + assert.NoError(t, err) + assert.NoError(t, pluginCsr.CheckSignature()) pluginCert, err := rpc.SignPluginCSR(dummyPluginInfo.ModulePath, pluginCsr) - if err != nil { - t.Fatal(err) - } + assert.NoError(t, err) pluginCertParsed, err := x509.ParseCertificate(pluginCert) - if err != nil { - t.Fatal(err) - } + assert.NoError(t, err) pluginTlsConfig := rpc.tlsClient.ServerTLSConfig() pluginTlsConfig.Certificates = []tls.Certificate{ { @@ -68,9 +58,8 @@ func TestRPC(t *testing.T) { }, } pluginListener, err := newListener() - if err != nil { - t.Fatal(err) - } + assert.NoError(t, err) + defer pluginListener.Close() pluginListenerTarget := pluginListener.Addr().String() if pluginListener.Addr().Network() == "unix" { @@ -83,9 +72,7 @@ func TestRPC(t *testing.T) { defer pluginServer.GracefulStop() conn, err := rpc.RegisterPlugin(pluginListenerTarget, dummyPluginInfo.ModulePath) - if err != nil { - t.Fatal(err) - } + assert.NoError(t, err) defer conn.Close() caCertPool := x509.NewCertPool() @@ -117,8 +104,15 @@ func TestRPC(t *testing.T) { defer pluginClient.Close() pluginInfraClient := protobuf.NewInfraClient(pluginClient) version, err := pluginInfraClient.GetServerVersion(context.Background(), &emptypb.Empty{}) - if err != nil { - t.Fatal(err) - } - log.Printf("plugin version: %s", version.Version) + assert.NoError(t, err) + assert.Equal(t, version.Version, rpc.version.Version) + info, err := pluginInfraClient.WhoAmI(context.Background(), &emptypb.Empty{}) + assert.NoError(t, err) + + assert.Equal(t, info.Name, dummyPluginInfo.Name) + assert.Equal(t, info.Version, dummyPluginInfo.Version) + assert.Equal(t, info.Description, dummyPluginInfo.Description) + assert.Equal(t, info.Author, dummyPluginInfo.Author) + assert.Equal(t, info.License, dummyPluginInfo.License) + assert.Equal(t, info.ModulePath, dummyPluginInfo.ModulePath) } From e6fc00f4cce2b0e2c308cf15eff4d5f8b0367cdd Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Fri, 15 Aug 2025 12:26:24 -0500 Subject: [PATCH 04/37] move ServerMux to server repo Signed-off-by: eternal-flame-AD --- v2/pipe_net_test.go | 4 +- v2/pipe_not_unix.go | 2 +- v2/pipe_unix.go | 2 +- v2/rpc.go | 243 -------------------------------------- v2/rpc_test.go | 118 ------------------ v2/transport_auth.go | 10 +- v2/transport_auth_test.go | 4 +- 7 files changed, 13 insertions(+), 370 deletions(-) delete mode 100644 v2/rpc_test.go diff --git a/v2/pipe_net_test.go b/v2/pipe_net_test.go index f871384..517005c 100644 --- a/v2/pipe_net_test.go +++ b/v2/pipe_net_test.go @@ -46,10 +46,10 @@ func TestGrpcPipeNet(t *testing.T) { defer listener.Close() serverCsrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ Subject: pkix.Name{ - CommonName: buildPluginTLSName("test"), + CommonName: BuildPluginTLSName("test"), }, DNSNames: []string{ - buildPluginTLSName("test"), + BuildPluginTLSName("test"), }, PublicKey: serverPub, }, serverPriv) diff --git a/v2/pipe_not_unix.go b/v2/pipe_not_unix.go index f376597..0901c44 100644 --- a/v2/pipe_not_unix.go +++ b/v2/pipe_not_unix.go @@ -4,7 +4,7 @@ package plugin import "net" -func newListener() (net.Listener, error) { +func NewListener() (net.Listener, error) { listener, err := net.Listen("tcp", "[::1]:0") if err != nil { return nil, err diff --git a/v2/pipe_unix.go b/v2/pipe_unix.go index b6c8344..5813c82 100644 --- a/v2/pipe_unix.go +++ b/v2/pipe_unix.go @@ -8,7 +8,7 @@ import ( "path/filepath" ) -func newListener() (net.Listener, error) { +func NewListener() (net.Listener, error) { tmpDir, err := os.MkdirTemp("", "gotify-plugin-*") if err != nil { return nil, err diff --git a/v2/rpc.go b/v2/rpc.go index 105b0e0..b4cd67d 100644 --- a/v2/rpc.go +++ b/v2/rpc.go @@ -2,19 +2,10 @@ package plugin import ( "context" - "crypto/ed25519" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "fmt" - "log" "net" - "os" "github.com/gotify/plugin-api/v2/generated/protobuf" "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/peer" "google.golang.org/protobuf/types/known/emptypb" ) @@ -48,237 +39,3 @@ func NewPluginRpc(infraDialer GrpcDialer) *PluginRpc { func (h *PluginRpc) Serve(listener net.Listener) error { return h.pluginServer.Serve(listener) } - -type ServerVersionInfo struct { - Version string - Commit string - BuildDate string -} - -type infraServerImpl struct { - server *ServerMux - version ServerVersionInfo - protobuf.UnimplementedInfraServer -} - -func (s *infraServerImpl) GetServerVersion(ctx context.Context, req *emptypb.Empty) (*protobuf.ServerVersionInfo, error) { - return &protobuf.ServerVersionInfo{ - Version: s.version.Version, - Commit: s.version.Commit, - BuildDate: s.version.BuildDate, - }, nil -} - -func (s *infraServerImpl) WhoAmI(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { - peer, ok := peer.FromContext(ctx) - if !ok { - return nil, fmt.Errorf("no peer in context") - } - authInfo := peer.AuthInfo.(*infraTlsAuthInfo) - return s.server.GetPluginInfo(authInfo.moduleName) -} - -type PluginConnection struct { - info *protobuf.Info - conn *grpc.ClientConn -} - -type ServerMux struct { - version ServerVersionInfo - tlsClient *EphemeralTLSClient - infraAddr net.Addr - infraListener net.Listener - infraServer *grpc.Server - pluginDNSToModulePath map[string]string - pluginConnections map[string]PluginConnection - protobuf.UnimplementedInfraServer -} - -func (s *ServerMux) GetServerVersion(ctx context.Context, req *emptypb.Empty) (*protobuf.ServerVersionInfo, error) { - return &protobuf.ServerVersionInfo{ - Version: s.version.Version, - Commit: s.version.Commit, - BuildDate: s.version.BuildDate, - }, nil -} - -func (s *ServerMux) WhoAmI(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { - peer, ok := peer.FromContext(ctx) - if !ok { - return nil, fmt.Errorf("no peer in context") - } - authInfo := peer.AuthInfo.(*infraTlsAuthInfo) - return s.GetPluginInfo(authInfo.moduleName) -} - -type infraTlsCreds struct { - pluginDNSToModulePath map[string]string - credentials.TransportCredentials -} - -type infraTlsAuthInfo struct { - moduleName string - credentials.TLSInfo -} - -func (c *infraTlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { - netConn, authInfo, err := c.TransportCredentials.ServerHandshake(rawConn) - if err != nil { - log.Printf("ServerHandshake: error %v", err) - rawConn.Close() - return nil, nil, err - } - protocolInfo := authInfo.(credentials.TLSInfo) - serverName := protocolInfo.State.VerifiedChains[0][0].DNSNames[0] - moduleName, ok := c.pluginDNSToModulePath[serverName] - if !ok { - log.Printf("ServerHandshake: unknown server name %s", serverName) - netConn.Close() - rawConn.Close() - return nil, nil, fmt.Errorf("unknown server name %s", serverName) - } - - return netConn, &infraTlsAuthInfo{ - moduleName: moduleName, - TLSInfo: protocolInfo, - }, nil -} - -// NewServerMux creates a server-side mux with an infra server that handles plugin-to-server calls. -func NewServerMux(info ServerVersionInfo) *ServerMux { - tlsClient, err := NewEphemeralTLSClient() - if err != nil { - panic(err) - } - _, infraPriv, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - panic(err) - } - infraCsrBytes, err := x509.CreateCertificateRequest(rand.Reader, new(x509.CertificateRequest), infraPriv) - if err != nil { - panic(err) - } - infraCsr, err := x509.ParseCertificateRequest(infraCsrBytes) - if err != nil { - panic(err) - } - if err := infraCsr.CheckSignature(); err != nil { - panic(err) - } - infraCert, err := tlsClient.SignCSR(ServerTLSName, infraCsr) - if err != nil { - panic(err) - } - infraCertParsed, err := x509.ParseCertificate(infraCert) - if err != nil { - panic(err) - } - caCertPool := x509.NewCertPool() - caCertPool.AddCert(tlsClient.caCert) - infraTlsConfig := &tls.Config{ - Certificates: []tls.Certificate{ - { - Certificate: [][]byte{infraCert, tlsClient.caCert.Raw}, - PrivateKey: infraPriv, - Leaf: infraCertParsed, - }, - }, - ClientAuth: tls.RequireAndVerifyClientCert, - ClientCAs: caCertPool, - ServerName: ServerTLSName, - } - - pluginDNSToModulePath := make(map[string]string) - infraServer := grpc.NewServer(grpc.Creds(&infraTlsCreds{ - pluginDNSToModulePath: pluginDNSToModulePath, - TransportCredentials: credentials.NewTLS(infraTlsConfig), - })) - - listener, err := newListener() - if err != nil { - panic(err) - } - mux := &ServerMux{ - version: info, - tlsClient: tlsClient, - infraAddr: listener.Addr(), - infraListener: listener, - infraServer: infraServer, - pluginDNSToModulePath: pluginDNSToModulePath, - pluginConnections: make(map[string]PluginConnection), - } - protobuf.RegisterInfraServer(infraServer, &infraServerImpl{ - server: mux, - version: info, - }) - - go infraServer.Serve(listener) - - return mux -} - -// InfraAddr returns the address of the infra server for plugin-to-server callbacks. -func (s *ServerMux) InfraAddr() net.Addr { - return s.infraAddr -} - -// CACert returns the CA certificate for mutual TLS authentication. -func (s *ServerMux) CACert() *x509.Certificate { - return s.tlsClient.caCert -} - -// SignPluginCSR signs a certificate request for a plugin. -func (s *ServerMux) SignPluginCSR(moduleName string, csr *x509.CertificateRequest) ([]byte, error) { - return s.tlsClient.SignPluginCSR(moduleName, csr) -} - -func (s *ServerMux) RegisterPlugin(target string, moduleName string) (*grpc.ClientConn, error) { - grpcConn, err := grpc.NewClient(target, grpc.WithTransportCredentials(credentials.NewTLS(s.tlsClient.ClientTLSConfig(moduleName)))) - if err != nil { - return nil, err - } - if _, exists := s.pluginDNSToModulePath[buildPluginTLSName(moduleName)]; exists { - return nil, fmt.Errorf("plugin %s already registered", moduleName) - } - s.pluginDNSToModulePath[buildPluginTLSName(moduleName)] = moduleName - pluginClient := protobuf.NewPluginClient(grpcConn) - pluginInfo, err := pluginClient.GetPluginInfo(context.Background(), &emptypb.Empty{}) - if err != nil { - return nil, err - } - s.pluginConnections[moduleName] = PluginConnection{ - info: pluginInfo, - conn: grpcConn, - } - return grpcConn, nil -} - -// GetPluginInfo returns the info of a plugin. -func (s *ServerMux) GetPluginInfo(moduleName string) (*protobuf.Info, error) { - conn, ok := s.pluginConnections[moduleName] - if !ok { - return nil, fmt.Errorf("plugin %s not registered", moduleName) - } - return conn.info, nil -} - -// GetPluginConnection returns the connection to the plugin for Server-to-Plugin calls. -func (s *ServerMux) GetPluginConnection(moduleName string) (*grpc.ClientConn, error) { - conn, ok := s.pluginConnections[moduleName] - if !ok { - return nil, fmt.Errorf("plugin %s not registered", moduleName) - } - return conn.conn, nil -} - -func (s *ServerMux) Close() error { - for _, conn := range s.pluginConnections { - conn.conn.Close() - } - s.infraServer.GracefulStop() - if s.infraAddr.Network() == "unix" { - os.Remove(s.infraAddr.String()) - } - s.infraListener.Close() - return nil -} diff --git a/v2/rpc_test.go b/v2/rpc_test.go deleted file mode 100644 index cd0705b..0000000 --- a/v2/rpc_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package plugin - -import ( - "context" - "crypto/ed25519" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "testing" - "time" - - "github.com/gotify/plugin-api/v2/generated/protobuf" - "github.com/stretchr/testify/assert" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/protobuf/types/known/emptypb" -) - -type dummyPlugin struct { - protobuf.UnimplementedPluginServer -} - -var dummyPluginInfo = &protobuf.Info{ - Name: "dummy", - Version: "test", - Description: "dummy plugin", - Author: "gotify", - License: "MIT", - ModulePath: "dummy.example", -} - -func (p *dummyPlugin) GetPluginInfo(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { - return dummyPluginInfo, nil -} - -func TestRPC(t *testing.T) { - rpc := NewServerMux(ServerVersionInfo{ - Version: "test", - Commit: "test", - BuildDate: time.Now().Format(time.RFC3339), - }) - defer rpc.Close() - _, pluginPriv, err := ed25519.GenerateKey(rand.Reader) - assert.NoError(t, err) - pluginCsrBytes, err := x509.CreateCertificateRequest(rand.Reader, new(x509.CertificateRequest), pluginPriv) - pluginCsr, err := x509.ParseCertificateRequest(pluginCsrBytes) - assert.NoError(t, err) - assert.NoError(t, pluginCsr.CheckSignature()) - pluginCert, err := rpc.SignPluginCSR(dummyPluginInfo.ModulePath, pluginCsr) - assert.NoError(t, err) - pluginCertParsed, err := x509.ParseCertificate(pluginCert) - assert.NoError(t, err) - pluginTlsConfig := rpc.tlsClient.ServerTLSConfig() - pluginTlsConfig.Certificates = []tls.Certificate{ - { - Certificate: [][]byte{pluginCert}, - PrivateKey: pluginPriv, - }, - } - pluginListener, err := newListener() - assert.NoError(t, err) - - defer pluginListener.Close() - pluginListenerTarget := pluginListener.Addr().String() - if pluginListener.Addr().Network() == "unix" { - pluginListenerTarget = "unix://" + pluginListenerTarget - } - - pluginServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(pluginTlsConfig))) - protobuf.RegisterPluginServer(pluginServer, &dummyPlugin{}) - go pluginServer.Serve(pluginListener) - defer pluginServer.GracefulStop() - - conn, err := rpc.RegisterPlugin(pluginListenerTarget, dummyPluginInfo.ModulePath) - assert.NoError(t, err) - defer conn.Close() - - caCertPool := x509.NewCertPool() - caCertPool.AddCert(rpc.CACert()) - pluginClientTlsConfig := &tls.Config{ - RootCAs: caCertPool, - ServerName: ServerTLSName, - Certificates: []tls.Certificate{ - { - Certificate: [][]byte{pluginCert}, - PrivateKey: pluginPriv, - Leaf: pluginCertParsed, - }, - }, - ClientAuth: tls.RequireAndVerifyClientCert, - ClientCAs: caCertPool, - } - - infraAddr := rpc.InfraAddr() - infraAddrTarget := infraAddr.String() - if infraAddr.Network() == "unix" { - infraAddrTarget = "unix://" + infraAddrTarget - } - - pluginClient, err := grpc.NewClient(infraAddrTarget, grpc.WithTransportCredentials(credentials.NewTLS(pluginClientTlsConfig))) - if err != nil { - t.Fatal(err) - } - defer pluginClient.Close() - pluginInfraClient := protobuf.NewInfraClient(pluginClient) - version, err := pluginInfraClient.GetServerVersion(context.Background(), &emptypb.Empty{}) - assert.NoError(t, err) - assert.Equal(t, version.Version, rpc.version.Version) - info, err := pluginInfraClient.WhoAmI(context.Background(), &emptypb.Empty{}) - assert.NoError(t, err) - - assert.Equal(t, info.Name, dummyPluginInfo.Name) - assert.Equal(t, info.Version, dummyPluginInfo.Version) - assert.Equal(t, info.Description, dummyPluginInfo.Description) - assert.Equal(t, info.Author, dummyPluginInfo.Author) - assert.Equal(t, info.License, dummyPluginInfo.License) - assert.Equal(t, info.ModulePath, dummyPluginInfo.ModulePath) -} diff --git a/v2/transport_auth.go b/v2/transport_auth.go index f4146e0..ea23874 100644 --- a/v2/transport_auth.go +++ b/v2/transport_auth.go @@ -14,7 +14,7 @@ import ( const ServerTLSName = "gotify.home.arpa" -func buildPluginTLSName(moduleName string) string { +func BuildPluginTLSName(moduleName string) string { moduleNameHash := sha256.Sum256([]byte(moduleName)) hashHex := hex.EncodeToString(moduleNameHash[:]) return fmt.Sprintf("%s.plugins.gotify.home.arpa", hashHex) @@ -49,10 +49,14 @@ func (s *EphemeralTLSClient) ClientTLSConfig(moduleName string) *tls.Config { }, }, RootCAs: s.createCertPool(), - ServerName: buildPluginTLSName(moduleName), + ServerName: BuildPluginTLSName(moduleName), } } +func (s *EphemeralTLSClient) CACert() *x509.Certificate { + return s.caCert +} + func (s *EphemeralTLSClient) SignCSR(dnsName string, csr *x509.CertificateRequest) ([]byte, error) { if err := csr.CheckSignature(); err != nil { return nil, err @@ -82,7 +86,7 @@ func (s *EphemeralTLSClient) SignCSR(dnsName string, csr *x509.CertificateReques } func (s *EphemeralTLSClient) SignPluginCSR(moduleName string, csr *x509.CertificateRequest) ([]byte, error) { - return s.SignCSR(buildPluginTLSName(moduleName), csr) + return s.SignCSR(BuildPluginTLSName(moduleName), csr) } func NewEphemeralTLSClient() (*EphemeralTLSClient, error) { diff --git a/v2/transport_auth_test.go b/v2/transport_auth_test.go index c4eaf78..9b57188 100644 --- a/v2/transport_auth_test.go +++ b/v2/transport_auth_test.go @@ -17,7 +17,7 @@ func TestEphemeralTLSClient(t *testing.T) { t.Fatal(err) } - pluginTlsName := buildPluginTLSName("test") + pluginTlsName := BuildPluginTLSName("test") _, serverPriv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) @@ -27,7 +27,7 @@ func TestEphemeralTLSClient(t *testing.T) { CommonName: pluginTlsName, }, DNSNames: []string{ - buildPluginTLSName("test"), + BuildPluginTLSName("test"), }, }, serverPriv) if err != nil { From 93c24e85a6d5b67c50da58c11ce43924e7d92518 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sat, 16 Aug 2025 16:37:40 -0500 Subject: [PATCH 05/37] wip: swap to single connection model --- v2/generate.go | 2 +- v2/generated/protobuf/infra.pb.go | 276 ----------------- v2/generated/protobuf/infra_grpc.pb.go | 274 ----------------- v2/generated/protobuf/meta.pb.go | 402 +++++++++++++++++-------- v2/generated/protobuf/meta_grpc.pb.go | 162 ++-------- v2/go.mod | 14 +- v2/go.sum | 28 ++ v2/pipe_net_test.go | 14 +- v2/protobuf/infra.proto | 30 -- v2/protobuf/meta.proto | 35 ++- v2/rpc.go | 41 --- 11 files changed, 386 insertions(+), 892 deletions(-) delete mode 100644 v2/generated/protobuf/infra.pb.go delete mode 100644 v2/generated/protobuf/infra_grpc.pb.go delete mode 100644 v2/protobuf/infra.proto delete mode 100644 v2/rpc.go diff --git a/v2/generate.go b/v2/generate.go index 129cfa0..270c081 100644 --- a/v2/generate.go +++ b/v2/generate.go @@ -1,3 +1,3 @@ package plugin -//go:generate protoc -Iprotobuf --go_out=./generated/protobuf --go_opt=paths=source_relative --go-grpc_out=./generated/protobuf --go-grpc_opt=paths=source_relative ./protobuf/meta.proto ./protobuf/infra.proto ./protobuf/config.proto ./protobuf/display.proto ./protobuf/webhooker.proto +//go:generate protoc -Iprotobuf --go_out=./generated/protobuf --go_opt=paths=source_relative --go-grpc_out=./generated/protobuf --go-grpc_opt=paths=source_relative ./protobuf/meta.proto ./protobuf/config.proto ./protobuf/display.proto ./protobuf/webhooker.proto diff --git a/v2/generated/protobuf/infra.pb.go b/v2/generated/protobuf/infra.pb.go deleted file mode 100644 index c49ed63..0000000 --- a/v2/generated/protobuf/infra.pb.go +++ /dev/null @@ -1,276 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.7 -// protoc v6.31.1 -// source: infra.proto - -package protobuf - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - emptypb "google.golang.org/protobuf/types/known/emptypb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -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 ServerVersionInfo struct { - state protoimpl.MessageState `protogen:"open.v1"` - Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` - Commit string `protobuf:"bytes,2,opt,name=commit,proto3" json:"commit,omitempty"` - BuildDate string `protobuf:"bytes,3,opt,name=buildDate,proto3" json:"buildDate,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ServerVersionInfo) Reset() { - *x = ServerVersionInfo{} - mi := &file_infra_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ServerVersionInfo) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ServerVersionInfo) ProtoMessage() {} - -func (x *ServerVersionInfo) ProtoReflect() protoreflect.Message { - mi := &file_infra_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ServerVersionInfo.ProtoReflect.Descriptor instead. -func (*ServerVersionInfo) Descriptor() ([]byte, []int) { - return file_infra_proto_rawDescGZIP(), []int{0} -} - -func (x *ServerVersionInfo) GetVersion() string { - if x != nil { - return x.Version - } - return "" -} - -func (x *ServerVersionInfo) GetCommit() string { - if x != nil { - return x.Commit - } - return "" -} - -func (x *ServerVersionInfo) GetBuildDate() string { - if x != nil { - return x.BuildDate - } - return "" -} - -type StorageSaveRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Userid uint64 `protobuf:"varint,1,opt,name=userid,proto3" json:"userid,omitempty"` - Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StorageSaveRequest) Reset() { - *x = StorageSaveRequest{} - mi := &file_infra_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StorageSaveRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StorageSaveRequest) ProtoMessage() {} - -func (x *StorageSaveRequest) ProtoReflect() protoreflect.Message { - mi := &file_infra_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StorageSaveRequest.ProtoReflect.Descriptor instead. -func (*StorageSaveRequest) Descriptor() ([]byte, []int) { - return file_infra_proto_rawDescGZIP(), []int{1} -} - -func (x *StorageSaveRequest) GetUserid() uint64 { - if x != nil { - return x.Userid - } - return 0 -} - -func (x *StorageSaveRequest) GetData() []byte { - if x != nil { - return x.Data - } - return nil -} - -type StorageLoadResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Userid uint64 `protobuf:"varint,1,opt,name=userid,proto3" json:"userid,omitempty"` - Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StorageLoadResponse) Reset() { - *x = StorageLoadResponse{} - mi := &file_infra_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StorageLoadResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StorageLoadResponse) ProtoMessage() {} - -func (x *StorageLoadResponse) ProtoReflect() protoreflect.Message { - mi := &file_infra_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StorageLoadResponse.ProtoReflect.Descriptor instead. -func (*StorageLoadResponse) Descriptor() ([]byte, []int) { - return file_infra_proto_rawDescGZIP(), []int{2} -} - -func (x *StorageLoadResponse) GetUserid() uint64 { - if x != nil { - return x.Userid - } - return 0 -} - -func (x *StorageLoadResponse) GetData() []byte { - if x != nil { - return x.Data - } - return nil -} - -var File_infra_proto protoreflect.FileDescriptor - -const file_infra_proto_rawDesc = "" + - "\n" + - "\vinfra.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\n" + - "meta.proto\"c\n" + - "\x11ServerVersionInfo\x12\x18\n" + - "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + - "\x06commit\x18\x02 \x01(\tR\x06commit\x12\x1c\n" + - "\tbuildDate\x18\x03 \x01(\tR\tbuildDate\"@\n" + - "\x12StorageSaveRequest\x12\x16\n" + - "\x06userid\x18\x01 \x01(\x04R\x06userid\x12\x12\n" + - "\x04data\x18\x02 \x01(\fR\x04data\"A\n" + - "\x13StorageLoadResponse\x12\x16\n" + - "\x06userid\x18\x01 \x01(\x04R\x06userid\x12\x12\n" + - "\x04data\x18\x02 \x01(\fR\x04data2\x98\x02\n" + - "\x05Infra\x12'\n" + - "\x06WhoAmI\x12\x16.google.protobuf.Empty\x1a\x05.Info\x12>\n" + - "\x10GetServerVersion\x12\x16.google.protobuf.Empty\x1a\x12.ServerVersionInfo\x129\n" + - "\n" + - "SaveConfig\x12\x13.StorageSaveRequest\x1a\x16.google.protobuf.Empty\x12:\n" + - "\n" + - "LoadConfig\x12\x16.google.protobuf.Empty\x1a\x14.StorageLoadResponse\x12/\n" + - "\vSendMessage\x12\b.Message\x1a\x16.google.protobuf.EmptyB\x16Z\x14./generated/protobufb\x06proto3" - -var ( - file_infra_proto_rawDescOnce sync.Once - file_infra_proto_rawDescData []byte -) - -func file_infra_proto_rawDescGZIP() []byte { - file_infra_proto_rawDescOnce.Do(func() { - file_infra_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_infra_proto_rawDesc), len(file_infra_proto_rawDesc))) - }) - return file_infra_proto_rawDescData -} - -var file_infra_proto_msgTypes = make([]protoimpl.MessageInfo, 3) -var file_infra_proto_goTypes = []any{ - (*ServerVersionInfo)(nil), // 0: ServerVersionInfo - (*StorageSaveRequest)(nil), // 1: StorageSaveRequest - (*StorageLoadResponse)(nil), // 2: StorageLoadResponse - (*emptypb.Empty)(nil), // 3: google.protobuf.Empty - (*Message)(nil), // 4: Message - (*Info)(nil), // 5: Info -} -var file_infra_proto_depIdxs = []int32{ - 3, // 0: Infra.WhoAmI:input_type -> google.protobuf.Empty - 3, // 1: Infra.GetServerVersion:input_type -> google.protobuf.Empty - 1, // 2: Infra.SaveConfig:input_type -> StorageSaveRequest - 3, // 3: Infra.LoadConfig:input_type -> google.protobuf.Empty - 4, // 4: Infra.SendMessage:input_type -> Message - 5, // 5: Infra.WhoAmI:output_type -> Info - 0, // 6: Infra.GetServerVersion:output_type -> ServerVersionInfo - 3, // 7: Infra.SaveConfig:output_type -> google.protobuf.Empty - 2, // 8: Infra.LoadConfig:output_type -> StorageLoadResponse - 3, // 9: Infra.SendMessage:output_type -> google.protobuf.Empty - 5, // [5:10] is the sub-list for method output_type - 0, // [0:5] 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_infra_proto_init() } -func file_infra_proto_init() { - if File_infra_proto != nil { - return - } - file_meta_proto_init() - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_infra_proto_rawDesc), len(file_infra_proto_rawDesc)), - NumEnums: 0, - NumMessages: 3, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_infra_proto_goTypes, - DependencyIndexes: file_infra_proto_depIdxs, - MessageInfos: file_infra_proto_msgTypes, - }.Build() - File_infra_proto = out.File - file_infra_proto_goTypes = nil - file_infra_proto_depIdxs = nil -} diff --git a/v2/generated/protobuf/infra_grpc.pb.go b/v2/generated/protobuf/infra_grpc.pb.go deleted file mode 100644 index 3ed1d24..0000000 --- a/v2/generated/protobuf/infra_grpc.pb.go +++ /dev/null @@ -1,274 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v6.31.1 -// source: infra.proto - -package protobuf - -import ( - context "context" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" - emptypb "google.golang.org/protobuf/types/known/emptypb" -) - -// 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.64.0 or later. -const _ = grpc.SupportPackageIsVersion9 - -const ( - Infra_WhoAmI_FullMethodName = "/Infra/WhoAmI" - Infra_GetServerVersion_FullMethodName = "/Infra/GetServerVersion" - Infra_SaveConfig_FullMethodName = "/Infra/SaveConfig" - Infra_LoadConfig_FullMethodName = "/Infra/LoadConfig" - Infra_SendMessage_FullMethodName = "/Infra/SendMessage" -) - -// InfraClient is the client API for Infra 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 InfraClient interface { - WhoAmI(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) - GetServerVersion(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ServerVersionInfo, error) - SaveConfig(ctx context.Context, in *StorageSaveRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) - LoadConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StorageLoadResponse, error) - SendMessage(ctx context.Context, in *Message, opts ...grpc.CallOption) (*emptypb.Empty, error) -} - -type infraClient struct { - cc grpc.ClientConnInterface -} - -func NewInfraClient(cc grpc.ClientConnInterface) InfraClient { - return &infraClient{cc} -} - -func (c *infraClient) WhoAmI(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Info) - err := c.cc.Invoke(ctx, Infra_WhoAmI_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *infraClient) GetServerVersion(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ServerVersionInfo, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(ServerVersionInfo) - err := c.cc.Invoke(ctx, Infra_GetServerVersion_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *infraClient) SaveConfig(ctx context.Context, in *StorageSaveRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, Infra_SaveConfig_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *infraClient) LoadConfig(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StorageLoadResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(StorageLoadResponse) - err := c.cc.Invoke(ctx, Infra_LoadConfig_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *infraClient) SendMessage(ctx context.Context, in *Message, opts ...grpc.CallOption) (*emptypb.Empty, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, Infra_SendMessage_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// InfraServer is the server API for Infra service. -// All implementations must embed UnimplementedInfraServer -// for forward compatibility. -type InfraServer interface { - WhoAmI(context.Context, *emptypb.Empty) (*Info, error) - GetServerVersion(context.Context, *emptypb.Empty) (*ServerVersionInfo, error) - SaveConfig(context.Context, *StorageSaveRequest) (*emptypb.Empty, error) - LoadConfig(context.Context, *emptypb.Empty) (*StorageLoadResponse, error) - SendMessage(context.Context, *Message) (*emptypb.Empty, error) - mustEmbedUnimplementedInfraServer() -} - -// UnimplementedInfraServer must be embedded to have -// forward compatible implementations. -// -// NOTE: this should be embedded by value instead of pointer to avoid a nil -// pointer dereference when methods are called. -type UnimplementedInfraServer struct{} - -func (UnimplementedInfraServer) WhoAmI(context.Context, *emptypb.Empty) (*Info, error) { - return nil, status.Errorf(codes.Unimplemented, "method WhoAmI not implemented") -} -func (UnimplementedInfraServer) GetServerVersion(context.Context, *emptypb.Empty) (*ServerVersionInfo, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetServerVersion not implemented") -} -func (UnimplementedInfraServer) SaveConfig(context.Context, *StorageSaveRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method SaveConfig not implemented") -} -func (UnimplementedInfraServer) LoadConfig(context.Context, *emptypb.Empty) (*StorageLoadResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method LoadConfig not implemented") -} -func (UnimplementedInfraServer) SendMessage(context.Context, *Message) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method SendMessage not implemented") -} -func (UnimplementedInfraServer) mustEmbedUnimplementedInfraServer() {} -func (UnimplementedInfraServer) testEmbeddedByValue() {} - -// UnsafeInfraServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to InfraServer will -// result in compilation errors. -type UnsafeInfraServer interface { - mustEmbedUnimplementedInfraServer() -} - -func RegisterInfraServer(s grpc.ServiceRegistrar, srv InfraServer) { - // If the following call pancis, it indicates UnimplementedInfraServer was - // embedded by pointer and is nil. This will cause panics if an - // unimplemented method is ever invoked, so we test this at initialization - // time to prevent it from happening at runtime later due to I/O. - if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { - t.testEmbeddedByValue() - } - s.RegisterService(&Infra_ServiceDesc, srv) -} - -func _Infra_WhoAmI_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(emptypb.Empty) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(InfraServer).WhoAmI(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Infra_WhoAmI_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(InfraServer).WhoAmI(ctx, req.(*emptypb.Empty)) - } - return interceptor(ctx, in, info, handler) -} - -func _Infra_GetServerVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(emptypb.Empty) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(InfraServer).GetServerVersion(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Infra_GetServerVersion_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(InfraServer).GetServerVersion(ctx, req.(*emptypb.Empty)) - } - return interceptor(ctx, in, info, handler) -} - -func _Infra_SaveConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(StorageSaveRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(InfraServer).SaveConfig(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Infra_SaveConfig_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(InfraServer).SaveConfig(ctx, req.(*StorageSaveRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _Infra_LoadConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(emptypb.Empty) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(InfraServer).LoadConfig(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Infra_LoadConfig_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(InfraServer).LoadConfig(ctx, req.(*emptypb.Empty)) - } - return interceptor(ctx, in, info, handler) -} - -func _Infra_SendMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Message) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(InfraServer).SendMessage(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Infra_SendMessage_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(InfraServer).SendMessage(ctx, req.(*Message)) - } - return interceptor(ctx, in, info, handler) -} - -// Infra_ServiceDesc is the grpc.ServiceDesc for Infra service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var Infra_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "Infra", - HandlerType: (*InfraServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "WhoAmI", - Handler: _Infra_WhoAmI_Handler, - }, - { - MethodName: "GetServerVersion", - Handler: _Infra_GetServerVersion_Handler, - }, - { - MethodName: "SaveConfig", - Handler: _Infra_SaveConfig_Handler, - }, - { - MethodName: "LoadConfig", - Handler: _Infra_LoadConfig_Handler, - }, - { - MethodName: "SendMessage", - Handler: _Infra_SendMessage_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "infra.proto", -} diff --git a/v2/generated/protobuf/meta.pb.go b/v2/generated/protobuf/meta.pb.go index 3b26348..fed1727 100644 --- a/v2/generated/protobuf/meta.pb.go +++ b/v2/generated/protobuf/meta.pb.go @@ -143,6 +143,126 @@ func (x *UserContext) GetAdmin() bool { return false } +type Capabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + Displayer *uint32 `protobuf:"varint,1,opt,name=Displayer,proto3,oneof" json:"Displayer,omitempty"` + Configurer *uint32 `protobuf:"varint,2,opt,name=Configurer,proto3,oneof" json:"Configurer,omitempty"` + Webhooker *uint32 `protobuf:"varint,3,opt,name=Webhooker,proto3,oneof" json:"Webhooker,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Capabilities) Reset() { + *x = Capabilities{} + mi := &file_meta_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Capabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Capabilities) ProtoMessage() {} + +func (x *Capabilities) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Capabilities.ProtoReflect.Descriptor instead. +func (*Capabilities) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{2} +} + +func (x *Capabilities) GetDisplayer() uint32 { + if x != nil && x.Displayer != nil { + return *x.Displayer + } + return 0 +} + +func (x *Capabilities) GetConfigurer() uint32 { + if x != nil && x.Configurer != nil { + return *x.Configurer + } + return 0 +} + +func (x *Capabilities) GetWebhooker() uint32 { + if x != nil && x.Webhooker != nil { + return *x.Webhooker + } + return 0 +} + +type ServerVersionInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Commit string `protobuf:"bytes,2,opt,name=commit,proto3" json:"commit,omitempty"` + BuildDate string `protobuf:"bytes,3,opt,name=buildDate,proto3" json:"buildDate,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerVersionInfo) Reset() { + *x = ServerVersionInfo{} + mi := &file_meta_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerVersionInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerVersionInfo) ProtoMessage() {} + +func (x *ServerVersionInfo) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerVersionInfo.ProtoReflect.Descriptor instead. +func (*ServerVersionInfo) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{3} +} + +func (x *ServerVersionInfo) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *ServerVersionInfo) GetCommit() string { + if x != nil { + return x.Commit + } + return "" +} + +func (x *ServerVersionInfo) GetBuildDate() string { + if x != nil { + return x.BuildDate + } + return "" +} + type Info struct { state protoimpl.MessageState `protogen:"open.v1"` Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` @@ -152,13 +272,14 @@ type Info struct { Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` License string `protobuf:"bytes,6,opt,name=license,proto3" json:"license,omitempty"` ModulePath string `protobuf:"bytes,7,opt,name=module_path,json=modulePath,proto3" json:"module_path,omitempty"` + Capabilities *Capabilities `protobuf:"bytes,8,opt,name=capabilities,proto3" json:"capabilities,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Info) Reset() { *x = Info{} - mi := &file_meta_proto_msgTypes[2] + mi := &file_meta_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -170,7 +291,7 @@ func (x *Info) String() string { func (*Info) ProtoMessage() {} func (x *Info) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[2] + mi := &file_meta_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -183,7 +304,7 @@ func (x *Info) ProtoReflect() protoreflect.Message { // Deprecated: Use Info.ProtoReflect.Descriptor instead. func (*Info) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{2} + return file_meta_proto_rawDescGZIP(), []int{4} } func (x *Info) GetVersion() string { @@ -235,6 +356,13 @@ func (x *Info) GetModulePath() string { return "" } +func (x *Info) GetCapabilities() *Capabilities { + if x != nil { + return x.Capabilities + } + return nil +} + type ExtrasValue struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Value: @@ -247,7 +375,7 @@ type ExtrasValue struct { func (x *ExtrasValue) Reset() { *x = ExtrasValue{} - mi := &file_meta_proto_msgTypes[3] + mi := &file_meta_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -259,7 +387,7 @@ func (x *ExtrasValue) String() string { func (*ExtrasValue) ProtoMessage() {} func (x *ExtrasValue) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[3] + mi := &file_meta_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -272,7 +400,7 @@ func (x *ExtrasValue) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtrasValue.ProtoReflect.Descriptor instead. func (*ExtrasValue) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{3} + return file_meta_proto_rawDescGZIP(), []int{5} } func (x *ExtrasValue) GetValue() isExtrasValue_Value { @@ -313,7 +441,7 @@ type Message struct { func (x *Message) Reset() { *x = Message{} - mi := &file_meta_proto_msgTypes[4] + mi := &file_meta_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -325,7 +453,7 @@ func (x *Message) String() string { func (*Message) ProtoMessage() {} func (x *Message) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[4] + mi := &file_meta_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -338,7 +466,7 @@ func (x *Message) ProtoReflect() protoreflect.Message { // Deprecated: Use Message.ProtoReflect.Descriptor instead. func (*Message) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{4} + return file_meta_proto_rawDescGZIP(), []int{6} } func (x *Message) GetMessage() string { @@ -379,7 +507,7 @@ type SetEnableRequest struct { func (x *SetEnableRequest) Reset() { *x = SetEnableRequest{} - mi := &file_meta_proto_msgTypes[5] + mi := &file_meta_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -391,7 +519,7 @@ func (x *SetEnableRequest) String() string { func (*SetEnableRequest) ProtoMessage() {} func (x *SetEnableRequest) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[5] + mi := &file_meta_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -404,7 +532,7 @@ func (x *SetEnableRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetEnableRequest.ProtoReflect.Descriptor instead. func (*SetEnableRequest) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{5} + return file_meta_proto_rawDescGZIP(), []int{7} } func (x *SetEnableRequest) GetUser() *UserContext { @@ -421,27 +549,32 @@ func (x *SetEnableRequest) GetEnable() bool { return false } -type SetEnableSuccessResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` +type UserUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Update: + // + // *UserUpdate_Message + // *UserUpdate_Config + Update isUserUpdate_Update `protobuf_oneof:"update"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SetEnableSuccessResponse) Reset() { - *x = SetEnableSuccessResponse{} - mi := &file_meta_proto_msgTypes[6] +func (x *UserUpdate) Reset() { + *x = UserUpdate{} + mi := &file_meta_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SetEnableSuccessResponse) String() string { +func (x *UserUpdate) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SetEnableSuccessResponse) ProtoMessage() {} +func (*UserUpdate) ProtoMessage() {} -func (x *SetEnableSuccessResponse) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[6] +func (x *UserUpdate) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -452,37 +585,75 @@ func (x *SetEnableSuccessResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SetEnableSuccessResponse.ProtoReflect.Descriptor instead. -func (*SetEnableSuccessResponse) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{6} +// Deprecated: Use UserUpdate.ProtoReflect.Descriptor instead. +func (*UserUpdate) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{8} } -type SetEnableResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Response: - // - // *SetEnableResponse_Success - // *SetEnableResponse_Error - Response isSetEnableResponse_Response `protobuf_oneof:"response"` +func (x *UserUpdate) GetUpdate() isUserUpdate_Update { + if x != nil { + return x.Update + } + return nil +} + +func (x *UserUpdate) GetMessage() *Message { + if x != nil { + if x, ok := x.Update.(*UserUpdate_Message); ok { + return x.Message + } + } + return nil +} + +func (x *UserUpdate) GetConfig() string { + if x != nil { + if x, ok := x.Update.(*UserUpdate_Config); ok { + return x.Config + } + } + return "" +} + +type isUserUpdate_Update interface { + isUserUpdate_Update() +} + +type UserUpdate_Message struct { + Message *Message `protobuf:"bytes,1,opt,name=message,proto3,oneof"` +} + +type UserUpdate_Config struct { + Config string `protobuf:"bytes,2,opt,name=config,proto3,oneof"` +} + +func (*UserUpdate_Message) isUserUpdate_Update() {} + +func (*UserUpdate_Config) isUserUpdate_Update() {} + +type UserInstanceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServerVersion *ServerVersionInfo `protobuf:"bytes,1,opt,name=serverVersion,proto3" json:"serverVersion,omitempty"` + User *UserContext `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SetEnableResponse) Reset() { - *x = SetEnableResponse{} - mi := &file_meta_proto_msgTypes[7] +func (x *UserInstanceRequest) Reset() { + *x = UserInstanceRequest{} + mi := &file_meta_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SetEnableResponse) String() string { +func (x *UserInstanceRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SetEnableResponse) ProtoMessage() {} +func (*UserInstanceRequest) ProtoMessage() {} -func (x *SetEnableResponse) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[7] +func (x *UserInstanceRequest) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -493,52 +664,25 @@ func (x *SetEnableResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SetEnableResponse.ProtoReflect.Descriptor instead. -func (*SetEnableResponse) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{7} +// Deprecated: Use UserInstanceRequest.ProtoReflect.Descriptor instead. +func (*UserInstanceRequest) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{9} } -func (x *SetEnableResponse) GetResponse() isSetEnableResponse_Response { +func (x *UserInstanceRequest) GetServerVersion() *ServerVersionInfo { if x != nil { - return x.Response + return x.ServerVersion } return nil } -func (x *SetEnableResponse) GetSuccess() *SetEnableSuccessResponse { +func (x *UserInstanceRequest) GetUser() *UserContext { if x != nil { - if x, ok := x.Response.(*SetEnableResponse_Success); ok { - return x.Success - } - } - return nil -} - -func (x *SetEnableResponse) GetError() *Error { - if x != nil { - if x, ok := x.Response.(*SetEnableResponse_Error); ok { - return x.Error - } + return x.User } return nil } -type isSetEnableResponse_Response interface { - isSetEnableResponse_Response() -} - -type SetEnableResponse_Success struct { - Success *SetEnableSuccessResponse `protobuf:"bytes,1,opt,name=success,proto3,oneof"` -} - -type SetEnableResponse_Error struct { - Error *Error `protobuf:"bytes,2,opt,name=error,proto3,oneof"` -} - -func (*SetEnableResponse_Success) isSetEnableResponse_Response() {} - -func (*SetEnableResponse_Error) isSetEnableResponse_Response() {} - var File_meta_proto protoreflect.FileDescriptor const file_meta_proto_rawDesc = "" + @@ -552,7 +696,22 @@ const file_meta_proto_rawDesc = "" + "\vUserContext\x12\x0e\n" + "\x02id\x18\x01 \x01(\x04R\x02id\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" + - "\x05admin\x18\x03 \x01(\bR\x05admin\"\xc3\x01\n" + + "\x05admin\x18\x03 \x01(\bR\x05admin\"\xa4\x01\n" + + "\fCapabilities\x12!\n" + + "\tDisplayer\x18\x01 \x01(\rH\x00R\tDisplayer\x88\x01\x01\x12#\n" + + "\n" + + "Configurer\x18\x02 \x01(\rH\x01R\n" + + "Configurer\x88\x01\x01\x12!\n" + + "\tWebhooker\x18\x03 \x01(\rH\x02R\tWebhooker\x88\x01\x01B\f\n" + + "\n" + + "_DisplayerB\r\n" + + "\v_ConfigurerB\f\n" + + "\n" + + "_Webhooker\"c\n" + + "\x11ServerVersionInfo\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + + "\x06commit\x18\x02 \x01(\tR\x06commit\x12\x1c\n" + + "\tbuildDate\x18\x03 \x01(\tR\tbuildDate\"\xf6\x01\n" + "\x04Info\x12\x18\n" + "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + "\x06author\x18\x02 \x01(\tR\x06author\x12\x12\n" + @@ -561,7 +720,8 @@ const file_meta_proto_rawDesc = "" + "\vdescription\x18\x05 \x01(\tR\vdescription\x12\x18\n" + "\alicense\x18\x06 \x01(\tR\alicense\x12\x1f\n" + "\vmodule_path\x18\a \x01(\tR\n" + - "modulePath\",\n" + + "modulePath\x121\n" + + "\fcapabilities\x18\b \x01(\v2\r.CapabilitiesR\fcapabilities\",\n" + "\vExtrasValue\x12\x14\n" + "\x04json\x18\x01 \x01(\tH\x00R\x04jsonB\a\n" + "\x05value\"\xcc\x01\n" + @@ -575,19 +735,18 @@ const file_meta_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\v2\f.ExtrasValueR\x05value:\x028\x01\"L\n" + "\x10SetEnableRequest\x12 \n" + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x16\n" + - "\x06enable\x18\x02 \x01(\bR\x06enable\"\x1a\n" + - "\x18SetEnableSuccessResponse\"v\n" + - "\x11SetEnableResponse\x125\n" + - "\asuccess\x18\x01 \x01(\v2\x19.SetEnableSuccessResponseH\x00R\asuccess\x12\x1e\n" + - "\x05error\x18\x02 \x01(\v2\x06.ErrorH\x00R\x05errorB\n" + - "\n" + - "\bresponse26\n" + + "\x06enable\x18\x02 \x01(\bR\x06enable\"V\n" + "\n" + - "PluginMeta\x12(\n" + - "\aGetInfo\x12\x16.google.protobuf.Empty\x1a\x05.Info2l\n" + + "UserUpdate\x12$\n" + + "\amessage\x18\x01 \x01(\v2\b.MessageH\x00R\amessage\x12\x18\n" + + "\x06config\x18\x02 \x01(\tH\x00R\x06configB\b\n" + + "\x06update\"q\n" + + "\x13UserInstanceRequest\x128\n" + + "\rserverVersion\x18\x01 \x01(\v2\x12.ServerVersionInfoR\rserverVersion\x12 \n" + + "\x04user\x18\x02 \x01(\v2\f.UserContextR\x04user2p\n" + "\x06Plugin\x12.\n" + - "\rGetPluginInfo\x12\x16.google.protobuf.Empty\x1a\x05.Info\x122\n" + - "\tSetEnable\x12\x11.SetEnableRequest\x1a\x12.SetEnableResponseB\x16Z\x14./generated/protobufb\x06proto3" + "\rGetPluginInfo\x12\x16.google.protobuf.Empty\x1a\x05.Info\x126\n" + + "\x0fRunUserInstance\x12\x14.UserInstanceRequest\x1a\v.UserUpdate0\x01B\x16Z\x14./generated/protobufb\x06proto3" var ( file_meta_proto_rawDescOnce sync.Once @@ -601,38 +760,40 @@ func file_meta_proto_rawDescGZIP() []byte { return file_meta_proto_rawDescData } -var file_meta_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_meta_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_meta_proto_goTypes = []any{ - (*Error)(nil), // 0: Error - (*UserContext)(nil), // 1: UserContext - (*Info)(nil), // 2: Info - (*ExtrasValue)(nil), // 3: ExtrasValue - (*Message)(nil), // 4: Message - (*SetEnableRequest)(nil), // 5: SetEnableRequest - (*SetEnableSuccessResponse)(nil), // 6: SetEnableSuccessResponse - (*SetEnableResponse)(nil), // 7: SetEnableResponse - nil, // 8: Message.ExtrasEntry - (*anypb.Any)(nil), // 9: google.protobuf.Any - (*emptypb.Empty)(nil), // 10: google.protobuf.Empty + (*Error)(nil), // 0: Error + (*UserContext)(nil), // 1: UserContext + (*Capabilities)(nil), // 2: Capabilities + (*ServerVersionInfo)(nil), // 3: ServerVersionInfo + (*Info)(nil), // 4: Info + (*ExtrasValue)(nil), // 5: ExtrasValue + (*Message)(nil), // 6: Message + (*SetEnableRequest)(nil), // 7: SetEnableRequest + (*UserUpdate)(nil), // 8: UserUpdate + (*UserInstanceRequest)(nil), // 9: UserInstanceRequest + nil, // 10: Message.ExtrasEntry + (*anypb.Any)(nil), // 11: google.protobuf.Any + (*emptypb.Empty)(nil), // 12: google.protobuf.Empty } var file_meta_proto_depIdxs = []int32{ - 9, // 0: Error.details:type_name -> google.protobuf.Any - 8, // 1: Message.extras:type_name -> Message.ExtrasEntry - 1, // 2: SetEnableRequest.user:type_name -> UserContext - 6, // 3: SetEnableResponse.success:type_name -> SetEnableSuccessResponse - 0, // 4: SetEnableResponse.error:type_name -> Error - 3, // 5: Message.ExtrasEntry.value:type_name -> ExtrasValue - 10, // 6: PluginMeta.GetInfo:input_type -> google.protobuf.Empty - 10, // 7: Plugin.GetPluginInfo:input_type -> google.protobuf.Empty - 5, // 8: Plugin.SetEnable:input_type -> SetEnableRequest - 2, // 9: PluginMeta.GetInfo:output_type -> Info - 2, // 10: Plugin.GetPluginInfo:output_type -> Info - 7, // 11: Plugin.SetEnable:output_type -> SetEnableResponse - 9, // [9:12] is the sub-list for method output_type - 6, // [6:9] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 11, // 0: Error.details:type_name -> google.protobuf.Any + 2, // 1: Info.capabilities:type_name -> Capabilities + 10, // 2: Message.extras:type_name -> Message.ExtrasEntry + 1, // 3: SetEnableRequest.user:type_name -> UserContext + 6, // 4: UserUpdate.message:type_name -> Message + 3, // 5: UserInstanceRequest.serverVersion:type_name -> ServerVersionInfo + 1, // 6: UserInstanceRequest.user:type_name -> UserContext + 5, // 7: Message.ExtrasEntry.value:type_name -> ExtrasValue + 12, // 8: Plugin.GetPluginInfo:input_type -> google.protobuf.Empty + 9, // 9: Plugin.RunUserInstance:input_type -> UserInstanceRequest + 4, // 10: Plugin.GetPluginInfo:output_type -> Info + 8, // 11: Plugin.RunUserInstance:output_type -> UserUpdate + 10, // [10:12] is the sub-list for method output_type + 8, // [8:10] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_meta_proto_init() } @@ -640,12 +801,13 @@ func file_meta_proto_init() { if File_meta_proto != nil { return } - file_meta_proto_msgTypes[3].OneofWrappers = []any{ + file_meta_proto_msgTypes[2].OneofWrappers = []any{} + file_meta_proto_msgTypes[5].OneofWrappers = []any{ (*ExtrasValue_Json)(nil), } - file_meta_proto_msgTypes[7].OneofWrappers = []any{ - (*SetEnableResponse_Success)(nil), - (*SetEnableResponse_Error)(nil), + file_meta_proto_msgTypes[8].OneofWrappers = []any{ + (*UserUpdate_Message)(nil), + (*UserUpdate_Config)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -653,9 +815,9 @@ func file_meta_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_meta_proto_rawDesc), len(file_meta_proto_rawDesc)), NumEnums: 0, - NumMessages: 9, + NumMessages: 11, NumExtensions: 0, - NumServices: 2, + NumServices: 1, }, GoTypes: file_meta_proto_goTypes, DependencyIndexes: file_meta_proto_depIdxs, diff --git a/v2/generated/protobuf/meta_grpc.pb.go b/v2/generated/protobuf/meta_grpc.pb.go index 4ccd135..6375102 100644 --- a/v2/generated/protobuf/meta_grpc.pb.go +++ b/v2/generated/protobuf/meta_grpc.pb.go @@ -20,110 +20,8 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - PluginMeta_GetInfo_FullMethodName = "/PluginMeta/GetInfo" -) - -// PluginMetaClient is the client API for PluginMeta 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 PluginMetaClient interface { - GetInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) -} - -type pluginMetaClient struct { - cc grpc.ClientConnInterface -} - -func NewPluginMetaClient(cc grpc.ClientConnInterface) PluginMetaClient { - return &pluginMetaClient{cc} -} - -func (c *pluginMetaClient) GetInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Info) - err := c.cc.Invoke(ctx, PluginMeta_GetInfo_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// PluginMetaServer is the server API for PluginMeta service. -// All implementations must embed UnimplementedPluginMetaServer -// for forward compatibility. -type PluginMetaServer interface { - GetInfo(context.Context, *emptypb.Empty) (*Info, error) - mustEmbedUnimplementedPluginMetaServer() -} - -// UnimplementedPluginMetaServer must be embedded to have -// forward compatible implementations. -// -// NOTE: this should be embedded by value instead of pointer to avoid a nil -// pointer dereference when methods are called. -type UnimplementedPluginMetaServer struct{} - -func (UnimplementedPluginMetaServer) GetInfo(context.Context, *emptypb.Empty) (*Info, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetInfo not implemented") -} -func (UnimplementedPluginMetaServer) mustEmbedUnimplementedPluginMetaServer() {} -func (UnimplementedPluginMetaServer) testEmbeddedByValue() {} - -// UnsafePluginMetaServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to PluginMetaServer will -// result in compilation errors. -type UnsafePluginMetaServer interface { - mustEmbedUnimplementedPluginMetaServer() -} - -func RegisterPluginMetaServer(s grpc.ServiceRegistrar, srv PluginMetaServer) { - // If the following call pancis, it indicates UnimplementedPluginMetaServer was - // embedded by pointer and is nil. This will cause panics if an - // unimplemented method is ever invoked, so we test this at initialization - // time to prevent it from happening at runtime later due to I/O. - if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { - t.testEmbeddedByValue() - } - s.RegisterService(&PluginMeta_ServiceDesc, srv) -} - -func _PluginMeta_GetInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(emptypb.Empty) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(PluginMetaServer).GetInfo(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: PluginMeta_GetInfo_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(PluginMetaServer).GetInfo(ctx, req.(*emptypb.Empty)) - } - return interceptor(ctx, in, info, handler) -} - -// PluginMeta_ServiceDesc is the grpc.ServiceDesc for PluginMeta service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var PluginMeta_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "PluginMeta", - HandlerType: (*PluginMetaServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "GetInfo", - Handler: _PluginMeta_GetInfo_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "meta.proto", -} - -const ( - Plugin_GetPluginInfo_FullMethodName = "/Plugin/GetPluginInfo" - Plugin_SetEnable_FullMethodName = "/Plugin/SetEnable" + Plugin_GetPluginInfo_FullMethodName = "/Plugin/GetPluginInfo" + Plugin_RunUserInstance_FullMethodName = "/Plugin/RunUserInstance" ) // PluginClient is the client API for Plugin service. @@ -131,7 +29,7 @@ const ( // 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 PluginClient interface { GetPluginInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) - SetEnable(ctx context.Context, in *SetEnableRequest, opts ...grpc.CallOption) (*SetEnableResponse, error) + RunUserInstance(ctx context.Context, in *UserInstanceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[UserUpdate], error) } type pluginClient struct { @@ -152,22 +50,31 @@ func (c *pluginClient) GetPluginInfo(ctx context.Context, in *emptypb.Empty, opt return out, nil } -func (c *pluginClient) SetEnable(ctx context.Context, in *SetEnableRequest, opts ...grpc.CallOption) (*SetEnableResponse, error) { +func (c *pluginClient) RunUserInstance(ctx context.Context, in *UserInstanceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[UserUpdate], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(SetEnableResponse) - err := c.cc.Invoke(ctx, Plugin_SetEnable_FullMethodName, in, out, cOpts...) + stream, err := c.cc.NewStream(ctx, &Plugin_ServiceDesc.Streams[0], Plugin_RunUserInstance_FullMethodName, cOpts...) if err != nil { return nil, err } - return out, nil + x := &grpc.GenericClientStream[UserInstanceRequest, UserUpdate]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil } +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Plugin_RunUserInstanceClient = grpc.ServerStreamingClient[UserUpdate] + // PluginServer is the server API for Plugin service. // All implementations must embed UnimplementedPluginServer // for forward compatibility. type PluginServer interface { GetPluginInfo(context.Context, *emptypb.Empty) (*Info, error) - SetEnable(context.Context, *SetEnableRequest) (*SetEnableResponse, error) + RunUserInstance(*UserInstanceRequest, grpc.ServerStreamingServer[UserUpdate]) error mustEmbedUnimplementedPluginServer() } @@ -181,8 +88,8 @@ type UnimplementedPluginServer struct{} func (UnimplementedPluginServer) GetPluginInfo(context.Context, *emptypb.Empty) (*Info, error) { return nil, status.Errorf(codes.Unimplemented, "method GetPluginInfo not implemented") } -func (UnimplementedPluginServer) SetEnable(context.Context, *SetEnableRequest) (*SetEnableResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method SetEnable not implemented") +func (UnimplementedPluginServer) RunUserInstance(*UserInstanceRequest, grpc.ServerStreamingServer[UserUpdate]) error { + return status.Errorf(codes.Unimplemented, "method RunUserInstance not implemented") } func (UnimplementedPluginServer) mustEmbedUnimplementedPluginServer() {} func (UnimplementedPluginServer) testEmbeddedByValue() {} @@ -223,24 +130,17 @@ func _Plugin_GetPluginInfo_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } -func _Plugin_SetEnable_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(SetEnableRequest) - if err := dec(in); err != nil { - return nil, err +func _Plugin_RunUserInstance_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(UserInstanceRequest) + if err := stream.RecvMsg(m); err != nil { + return err } - if interceptor == nil { - return srv.(PluginServer).SetEnable(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Plugin_SetEnable_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(PluginServer).SetEnable(ctx, req.(*SetEnableRequest)) - } - return interceptor(ctx, in, info, handler) + return srv.(PluginServer).RunUserInstance(m, &grpc.GenericServerStream[UserInstanceRequest, UserUpdate]{ServerStream: stream}) } +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Plugin_RunUserInstanceServer = grpc.ServerStreamingServer[UserUpdate] + // Plugin_ServiceDesc is the grpc.ServiceDesc for Plugin service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -252,11 +152,13 @@ var Plugin_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetPluginInfo", Handler: _Plugin_GetPluginInfo_Handler, }, + }, + Streams: []grpc.StreamDesc{ { - MethodName: "SetEnable", - Handler: _Plugin_SetEnable_Handler, + StreamName: "RunUserInstance", + Handler: _Plugin_RunUserInstance_Handler, + ServerStreams: true, }, }, - Streams: []grpc.StreamDesc{}, Metadata: "meta.proto", } diff --git a/v2/go.mod b/v2/go.mod index 9b8c66c..c8a0b83 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -3,16 +3,24 @@ module github.com/gotify/plugin-api/v2 go 1.24.5 require ( - github.com/stretchr/testify v1.3.0 + github.com/gotify/plugin-api v1.0.0 google.golang.org/grpc v1.74.2 google.golang.org/protobuf v1.36.7 ) require ( - github.com/davecgh/go-spew v1.1.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect + github.com/gin-gonic/gin v1.3.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/json-iterator/go v1.1.5 // indirect + github.com/mattn/go-isatty v0.0.4 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + gopkg.in/go-playground/validator.v8 v8.18.2 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/v2/go.sum b/v2/go.sum index 198eaaf..5444529 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,20 +1,37 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= +github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gotify/plugin-api v1.0.0 h1:kab40p2TEPLzjmcafOc7JOz75aTsYQyS2PXtElH8xmI= +github.com/gotify/plugin-api v1.0.0/go.mod h1:xZfEyqVK/Zvu3RwA/CtpuiwFmzFDxifrrqMaH9BHnyU= +github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= @@ -27,8 +44,11 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= @@ -39,3 +59,11 @@ google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +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/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/v2/pipe_net_test.go b/v2/pipe_net_test.go index 517005c..bb2483f 100644 --- a/v2/pipe_net_test.go +++ b/v2/pipe_net_test.go @@ -19,7 +19,13 @@ import ( ) type dummyInfraServer struct { - protobuf.UnimplementedInfraServer + protobuf.UnimplementedPluginServer +} + +func (s *dummyInfraServer) GetPluginInfo(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { + return &protobuf.Info{ + Version: "test", + }, nil } func (s *dummyInfraServer) GetServerVersion(ctx context.Context, req *emptypb.Empty) (*protobuf.ServerVersionInfo, error) { @@ -72,7 +78,7 @@ func TestGrpcPipeNet(t *testing.T) { } server := grpc.NewServer(grpc.Creds(credentials.NewTLS(serverTLSConfig))) - protobuf.RegisterInfraServer(server, &dummyInfraServer{}) + protobuf.RegisterPluginServer(server, &dummyInfraServer{}) go server.Serve(listener) defer server.GracefulStop() @@ -81,8 +87,8 @@ func TestGrpcPipeNet(t *testing.T) { t.Fatal(err) } - infraClient := protobuf.NewInfraClient(conn) - version, err := infraClient.GetServerVersion(context.Background(), &emptypb.Empty{}) + infraClient := protobuf.NewPluginClient(conn) + version, err := infraClient.GetPluginInfo(context.Background(), &emptypb.Empty{}) if err != nil { t.Fatal(err) } diff --git a/v2/protobuf/infra.proto b/v2/protobuf/infra.proto deleted file mode 100644 index 4ecbb54..0000000 --- a/v2/protobuf/infra.proto +++ /dev/null @@ -1,30 +0,0 @@ -syntax = "proto3"; - -import "google/protobuf/empty.proto"; -import "meta.proto"; - -option go_package = "./generated/protobuf"; - -message ServerVersionInfo { - string version = 1; - string commit = 2; - string buildDate = 3; -} - -message StorageSaveRequest { - uint64 userid = 1; - bytes data = 2; -} - -message StorageLoadResponse { - uint64 userid = 1; - bytes data = 2; -} - -service Infra { - rpc WhoAmI(google.protobuf.Empty) returns (Info); - rpc GetServerVersion(google.protobuf.Empty) returns (ServerVersionInfo); - rpc SaveConfig(StorageSaveRequest) returns (google.protobuf.Empty); - rpc LoadConfig(google.protobuf.Empty) returns (StorageLoadResponse); - rpc SendMessage(Message) returns (google.protobuf.Empty); -} \ No newline at end of file diff --git a/v2/protobuf/meta.proto b/v2/protobuf/meta.proto index f0dd153..d880fe9 100644 --- a/v2/protobuf/meta.proto +++ b/v2/protobuf/meta.proto @@ -16,6 +16,18 @@ message UserContext { bool admin = 3; } +message Capabilities { + optional uint32 Displayer = 1; + optional uint32 Configurer = 2; + optional uint32 Webhooker = 3; +} + +message ServerVersionInfo { + string version = 1; + string commit = 2; + string buildDate = 3; +} + message Info { string version = 1; string author = 2; @@ -24,9 +36,9 @@ message Info { string description = 5; string license = 6; string module_path = 7; + Capabilities capabilities = 8; } - message ExtrasValue { oneof value { string json = 1; @@ -45,23 +57,20 @@ message SetEnableRequest { bool enable = 2; } -message SetEnableSuccessResponse { - -} - -message SetEnableResponse { - oneof response { - SetEnableSuccessResponse success = 1; - Error error = 2; - } +message UserUpdate { + oneof update { + Message message = 1; + string config = 2; + } } -service PluginMeta { - rpc GetInfo(google.protobuf.Empty) returns (Info); +message UserInstanceRequest { + ServerVersionInfo serverVersion = 1; + UserContext user = 2; } service Plugin { rpc GetPluginInfo(google.protobuf.Empty) returns (Info); - rpc SetEnable(SetEnableRequest) returns (SetEnableResponse); + rpc RunUserInstance(UserInstanceRequest) returns (stream UserUpdate); } diff --git a/v2/rpc.go b/v2/rpc.go deleted file mode 100644 index b4cd67d..0000000 --- a/v2/rpc.go +++ /dev/null @@ -1,41 +0,0 @@ -package plugin - -import ( - "context" - "net" - - "github.com/gotify/plugin-api/v2/generated/protobuf" - "google.golang.org/grpc" - "google.golang.org/protobuf/types/known/emptypb" -) - -type GrpcDialer interface { - Dial(ctx context.Context) (*grpc.ClientConn, error) -} - -type PluginRpc struct { - infraDialer GrpcDialer - pluginServer *grpc.Server -} - -func NewPluginRpc(infraDialer GrpcDialer) *PluginRpc { - infraClient, err := infraDialer.Dial(context.Background()) - if err != nil { - panic(err) - } - infraRpcClient := protobuf.NewInfraClient(infraClient) - version, err := infraRpcClient.GetServerVersion(context.Background(), &emptypb.Empty{}) - if err != nil { - panic(err) - } - _ = version - - return &PluginRpc{ - infraDialer: infraDialer, - pluginServer: grpc.NewServer(), - } -} - -func (h *PluginRpc) Serve(listener net.Listener) error { - return h.pluginServer.Serve(listener) -} From b5e9886e8f4419baaad66264aaf34cf93be6f61d Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 03:48:33 -0500 Subject: [PATCH 06/37] shim impl Signed-off-by: eternal-flame-AD --- v2/cli_flags.go | 42 +++ v2/generate.go | 2 +- v2/generated/protobuf/config.pb.go | 84 ++--- v2/generated/protobuf/meta.pb.go | 336 +++++++++++++++----- v2/generated/protobuf/meta_grpc.pb.go | 95 +++++- v2/generated/protobuf/webhooker.pb.go | 141 --------- v2/generated/protobuf/webhooker_grpc.pb.go | 122 -------- v2/protobuf/config.proto | 8 +- v2/protobuf/meta.proto | 41 ++- v2/protobuf/webhooker.proto | 14 - v2/shim_v1.go | 337 +++++++++++++++++++++ v2/transport_auth.go | 14 +- 12 files changed, 796 insertions(+), 440 deletions(-) create mode 100644 v2/cli_flags.go delete mode 100644 v2/generated/protobuf/webhooker.pb.go delete mode 100644 v2/generated/protobuf/webhooker_grpc.pb.go delete mode 100644 v2/protobuf/webhooker.proto create mode 100644 v2/shim_v1.go diff --git a/v2/cli_flags.go b/v2/cli_flags.go new file mode 100644 index 0000000..ce54926 --- /dev/null +++ b/v2/cli_flags.go @@ -0,0 +1,42 @@ +package plugin + +import ( + "flag" + "os" +) + +type PluginCliFlags struct { + flagSet *flag.FlagSet + CAData []byte + CertData []byte + KeyData []byte +} + +func ParsePluginCLIFlags(args []string) (*PluginCliFlags, error) { + flagSet := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + var caFile string + var certFile string + var keyFile string + flagSet.StringVar(&certFile, "cert-file", "", "Path to the certificate file for Transport Auth.") + flagSet.StringVar(&keyFile, "key-file", "", "Path to the key file for Transport Auth.") + flagSet.StringVar(&caFile, "ca-file", "", "Path to the CA file for Transport Auth.") + flagSet.Parse(args) + certData, err := os.ReadFile(certFile) + if err != nil { + return nil, err + } + keyData, err := os.ReadFile(keyFile) + if err != nil { + return nil, err + } + caData, err := os.ReadFile(caFile) + if err != nil { + return nil, err + } + return &PluginCliFlags{ + flagSet: flagSet, + CAData: caData, + CertData: certData, + KeyData: keyData, + }, nil +} diff --git a/v2/generate.go b/v2/generate.go index 270c081..90cad6a 100644 --- a/v2/generate.go +++ b/v2/generate.go @@ -1,3 +1,3 @@ package plugin -//go:generate protoc -Iprotobuf --go_out=./generated/protobuf --go_opt=paths=source_relative --go-grpc_out=./generated/protobuf --go-grpc_opt=paths=source_relative ./protobuf/meta.proto ./protobuf/config.proto ./protobuf/display.proto ./protobuf/webhooker.proto +//go:generate protoc -Iprotobuf --go_out=./generated/protobuf --go_opt=paths=source_relative --go-grpc_out=./generated/protobuf --go-grpc_opt=paths=source_relative ./protobuf/meta.proto ./protobuf/config.proto ./protobuf/display.proto diff --git a/v2/generated/protobuf/config.pb.go b/v2/generated/protobuf/config.pb.go index b03d649..8fe4a50 100644 --- a/v2/generated/protobuf/config.pb.go +++ b/v2/generated/protobuf/config.pb.go @@ -9,6 +9,7 @@ package protobuf import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -161,42 +162,6 @@ func (x *ValidateAndSetConfigRequest) GetConfig() *Config { return nil } -type ValidateAndSetConfigSuccessResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ValidateAndSetConfigSuccessResponse) Reset() { - *x = ValidateAndSetConfigSuccessResponse{} - mi := &file_config_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ValidateAndSetConfigSuccessResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ValidateAndSetConfigSuccessResponse) ProtoMessage() {} - -func (x *ValidateAndSetConfigSuccessResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ValidateAndSetConfigSuccessResponse.ProtoReflect.Descriptor instead. -func (*ValidateAndSetConfigSuccessResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{3} -} - type ValidateAndSetConfigResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Response: @@ -210,7 +175,7 @@ type ValidateAndSetConfigResponse struct { func (x *ValidateAndSetConfigResponse) Reset() { *x = ValidateAndSetConfigResponse{} - mi := &file_config_proto_msgTypes[4] + mi := &file_config_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -222,7 +187,7 @@ func (x *ValidateAndSetConfigResponse) String() string { func (*ValidateAndSetConfigResponse) ProtoMessage() {} func (x *ValidateAndSetConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[4] + mi := &file_config_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -235,7 +200,7 @@ func (x *ValidateAndSetConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateAndSetConfigResponse.ProtoReflect.Descriptor instead. func (*ValidateAndSetConfigResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{4} + return file_config_proto_rawDescGZIP(), []int{3} } func (x *ValidateAndSetConfigResponse) GetResponse() isValidateAndSetConfigResponse_Response { @@ -245,7 +210,7 @@ func (x *ValidateAndSetConfigResponse) GetResponse() isValidateAndSetConfigRespo return nil } -func (x *ValidateAndSetConfigResponse) GetSuccess() *ValidateAndSetConfigSuccessResponse { +func (x *ValidateAndSetConfigResponse) GetSuccess() *emptypb.Empty { if x != nil { if x, ok := x.Response.(*ValidateAndSetConfigResponse_Success); ok { return x.Success @@ -268,7 +233,7 @@ type isValidateAndSetConfigResponse_Response interface { } type ValidateAndSetConfigResponse_Success struct { - Success *ValidateAndSetConfigSuccessResponse `protobuf:"bytes,1,opt,name=success,proto3,oneof"` + Success *emptypb.Empty `protobuf:"bytes,1,opt,name=success,proto3,oneof"` } type ValidateAndSetConfigResponse_Error struct { @@ -283,7 +248,7 @@ var File_config_proto protoreflect.FileDescriptor const file_config_proto_rawDesc = "" + "\n" + - "\fconfig.proto\x1a\n" + + "\fconfig.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\n" + "meta.proto\" \n" + "\x06Config\x12\x16\n" + "\x06config\x18\x01 \x01(\tR\x06config\"8\n" + @@ -291,10 +256,9 @@ const file_config_proto_rawDesc = "" + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\"`\n" + "\x1bValidateAndSetConfigRequest\x12 \n" + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x1f\n" + - "\x06config\x18\x02 \x01(\v2\a.ConfigR\x06config\"%\n" + - "#ValidateAndSetConfigSuccessResponse\"\x8c\x01\n" + - "\x1cValidateAndSetConfigResponse\x12@\n" + - "\asuccess\x18\x01 \x01(\v2$.ValidateAndSetConfigSuccessResponseH\x00R\asuccess\x12\x1e\n" + + "\x06config\x18\x02 \x01(\v2\a.ConfigR\x06config\"~\n" + + "\x1cValidateAndSetConfigResponse\x122\n" + + "\asuccess\x18\x01 \x01(\v2\x16.google.protobuf.EmptyH\x00R\asuccess\x12\x1e\n" + "\x05error\x18\x02 \x01(\v2\x06.ErrorH\x00R\x05errorB\n" + "\n" + "\bresponse2\x92\x01\n" + @@ -315,26 +279,26 @@ func file_config_proto_rawDescGZIP() []byte { return file_config_proto_rawDescData } -var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_config_proto_goTypes = []any{ - (*Config)(nil), // 0: Config - (*DefaultConfigRequest)(nil), // 1: DefaultConfigRequest - (*ValidateAndSetConfigRequest)(nil), // 2: ValidateAndSetConfigRequest - (*ValidateAndSetConfigSuccessResponse)(nil), // 3: ValidateAndSetConfigSuccessResponse - (*ValidateAndSetConfigResponse)(nil), // 4: ValidateAndSetConfigResponse - (*UserContext)(nil), // 5: UserContext - (*Error)(nil), // 6: Error + (*Config)(nil), // 0: Config + (*DefaultConfigRequest)(nil), // 1: DefaultConfigRequest + (*ValidateAndSetConfigRequest)(nil), // 2: ValidateAndSetConfigRequest + (*ValidateAndSetConfigResponse)(nil), // 3: ValidateAndSetConfigResponse + (*UserContext)(nil), // 4: UserContext + (*emptypb.Empty)(nil), // 5: google.protobuf.Empty + (*Error)(nil), // 6: Error } var file_config_proto_depIdxs = []int32{ - 5, // 0: DefaultConfigRequest.user:type_name -> UserContext - 5, // 1: ValidateAndSetConfigRequest.user:type_name -> UserContext + 4, // 0: DefaultConfigRequest.user:type_name -> UserContext + 4, // 1: ValidateAndSetConfigRequest.user:type_name -> UserContext 0, // 2: ValidateAndSetConfigRequest.config:type_name -> Config - 3, // 3: ValidateAndSetConfigResponse.success:type_name -> ValidateAndSetConfigSuccessResponse + 5, // 3: ValidateAndSetConfigResponse.success:type_name -> google.protobuf.Empty 6, // 4: ValidateAndSetConfigResponse.error:type_name -> Error 1, // 5: Configurer.DefaultConfig:input_type -> DefaultConfigRequest 2, // 6: Configurer.ValidateAndSetConfig:input_type -> ValidateAndSetConfigRequest 0, // 7: Configurer.DefaultConfig:output_type -> Config - 4, // 8: Configurer.ValidateAndSetConfig:output_type -> ValidateAndSetConfigResponse + 3, // 8: Configurer.ValidateAndSetConfig:output_type -> ValidateAndSetConfigResponse 7, // [7:9] is the sub-list for method output_type 5, // [5:7] is the sub-list for method input_type 5, // [5:5] is the sub-list for extension type_name @@ -348,7 +312,7 @@ func file_config_proto_init() { return } file_meta_proto_init() - file_config_proto_msgTypes[4].OneofWrappers = []any{ + file_config_proto_msgTypes[3].OneofWrappers = []any{ (*ValidateAndSetConfigResponse_Success)(nil), (*ValidateAndSetConfigResponse_Error)(nil), } @@ -358,7 +322,7 @@ func file_config_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc)), NumEnums: 0, - NumMessages: 5, + NumMessages: 4, NumExtensions: 0, NumServices: 1, }, diff --git a/v2/generated/protobuf/meta.pb.go b/v2/generated/protobuf/meta.pb.go index fed1727..9ac9ba8 100644 --- a/v2/generated/protobuf/meta.pb.go +++ b/v2/generated/protobuf/meta.pb.go @@ -23,6 +23,61 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type Capability int32 + +const ( + Capability_DISPLAYER Capability = 0 + Capability_MESSENGER Capability = 1 + Capability_CONFIGURER Capability = 2 + Capability_STORAGER Capability = 3 + Capability_WEBHOOKER Capability = 4 +) + +// Enum value maps for Capability. +var ( + Capability_name = map[int32]string{ + 0: "DISPLAYER", + 1: "MESSENGER", + 2: "CONFIGURER", + 3: "STORAGER", + 4: "WEBHOOKER", + } + Capability_value = map[string]int32{ + "DISPLAYER": 0, + "MESSENGER": 1, + "CONFIGURER": 2, + "STORAGER": 3, + "WEBHOOKER": 4, + } +) + +func (x Capability) Enum() *Capability { + p := new(Capability) + *p = x + return p +} + +func (x Capability) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Capability) Descriptor() protoreflect.EnumDescriptor { + return file_meta_proto_enumTypes[0].Descriptor() +} + +func (Capability) Type() protoreflect.EnumType { + return &file_meta_proto_enumTypes[0] +} + +func (x Capability) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Capability.Descriptor instead. +func (Capability) EnumDescriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{0} +} + type Error struct { state protoimpl.MessageState `protogen:"open.v1"` Details *anypb.Any `protobuf:"bytes,1,opt,name=details,proto3" json:"details,omitempty"` @@ -549,31 +604,27 @@ func (x *SetEnableRequest) GetEnable() bool { return false } -type UserUpdate struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Update: - // - // *UserUpdate_Message - // *UserUpdate_Config - Update isUserUpdate_Update `protobuf_oneof:"update"` +type PluginCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + Capabilities []Capability `protobuf:"varint,1,rep,packed,name=capabilities,proto3,enum=Capability" json:"capabilities,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *UserUpdate) Reset() { - *x = UserUpdate{} +func (x *PluginCapabilities) Reset() { + *x = PluginCapabilities{} mi := &file_meta_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *UserUpdate) String() string { +func (x *PluginCapabilities) String() string { return protoimpl.X.MessageStringOf(x) } -func (*UserUpdate) ProtoMessage() {} +func (*PluginCapabilities) ProtoMessage() {} -func (x *UserUpdate) ProtoReflect() protoreflect.Message { +func (x *PluginCapabilities) ProtoReflect() protoreflect.Message { mi := &file_meta_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -585,63 +636,138 @@ func (x *UserUpdate) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use UserUpdate.ProtoReflect.Descriptor instead. -func (*UserUpdate) Descriptor() ([]byte, []int) { +// Deprecated: Use PluginCapabilities.ProtoReflect.Descriptor instead. +func (*PluginCapabilities) Descriptor() ([]byte, []int) { return file_meta_proto_rawDescGZIP(), []int{8} } -func (x *UserUpdate) GetUpdate() isUserUpdate_Update { +func (x *PluginCapabilities) GetCapabilities() []Capability { + if x != nil { + return x.Capabilities + } + return nil +} + +type InstanceUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Update: + // + // *InstanceUpdate_Capabilities + // *InstanceUpdate_Message + // *InstanceUpdate_Storage + Update isInstanceUpdate_Update `protobuf_oneof:"update"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InstanceUpdate) Reset() { + *x = InstanceUpdate{} + mi := &file_meta_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InstanceUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstanceUpdate) ProtoMessage() {} + +func (x *InstanceUpdate) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InstanceUpdate.ProtoReflect.Descriptor instead. +func (*InstanceUpdate) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{9} +} + +func (x *InstanceUpdate) GetUpdate() isInstanceUpdate_Update { if x != nil { return x.Update } return nil } -func (x *UserUpdate) GetMessage() *Message { +func (x *InstanceUpdate) GetCapabilities() *PluginCapabilities { + if x != nil { + if x, ok := x.Update.(*InstanceUpdate_Capabilities); ok { + return x.Capabilities + } + } + return nil +} + +func (x *InstanceUpdate) GetMessage() *Message { if x != nil { - if x, ok := x.Update.(*UserUpdate_Message); ok { + if x, ok := x.Update.(*InstanceUpdate_Message); ok { return x.Message } } return nil } -func (x *UserUpdate) GetConfig() string { +func (x *InstanceUpdate) GetStorage() []byte { if x != nil { - if x, ok := x.Update.(*UserUpdate_Config); ok { - return x.Config + if x, ok := x.Update.(*InstanceUpdate_Storage); ok { + return x.Storage } } - return "" + return nil } -type isUserUpdate_Update interface { - isUserUpdate_Update() +type isInstanceUpdate_Update interface { + isInstanceUpdate_Update() } -type UserUpdate_Message struct { - Message *Message `protobuf:"bytes,1,opt,name=message,proto3,oneof"` +type InstanceUpdate_Capabilities struct { + // which capabilities are displayed to the user + Capabilities *PluginCapabilities `protobuf:"bytes,1,opt,name=capabilities,proto3,oneof"` } -type UserUpdate_Config struct { - Config string `protobuf:"bytes,2,opt,name=config,proto3,oneof"` +type InstanceUpdate_Message struct { + // send a message to the user + Message *Message `protobuf:"bytes,2,opt,name=message,proto3,oneof"` } -func (*UserUpdate_Message) isUserUpdate_Update() {} +type InstanceUpdate_Storage struct { + // update persistent storage + Storage []byte `protobuf:"bytes,3,opt,name=storage,proto3,oneof"` +} + +func (*InstanceUpdate_Capabilities) isInstanceUpdate_Update() {} + +func (*InstanceUpdate_Message) isInstanceUpdate_Update() {} -func (*UserUpdate_Config) isUserUpdate_Update() {} +func (*InstanceUpdate_Storage) isInstanceUpdate_Update() {} type UserInstanceRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ServerVersion *ServerVersionInfo `protobuf:"bytes,1,opt,name=serverVersion,proto3" json:"serverVersion,omitempty"` - User *UserContext `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // the server version info + ServerVersion *ServerVersionInfo `protobuf:"bytes,1,opt,name=serverVersion,proto3" json:"serverVersion,omitempty"` + // the user context + User *UserContext `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"` + // the webhook base path + WebhookBasePath *string `protobuf:"bytes,3,opt,name=webhookBasePath,proto3,oneof" json:"webhookBasePath,omitempty"` + // the config + Config []byte `protobuf:"bytes,4,opt,name=config,proto3,oneof" json:"config,omitempty"` + // the storage + Storage []byte `protobuf:"bytes,5,opt,name=storage,proto3,oneof" json:"storage,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UserInstanceRequest) Reset() { *x = UserInstanceRequest{} - mi := &file_meta_proto_msgTypes[9] + mi := &file_meta_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -653,7 +779,7 @@ func (x *UserInstanceRequest) String() string { func (*UserInstanceRequest) ProtoMessage() {} func (x *UserInstanceRequest) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[9] + mi := &file_meta_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -666,7 +792,7 @@ func (x *UserInstanceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UserInstanceRequest.ProtoReflect.Descriptor instead. func (*UserInstanceRequest) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{9} + return file_meta_proto_rawDescGZIP(), []int{10} } func (x *UserInstanceRequest) GetServerVersion() *ServerVersionInfo { @@ -683,6 +809,27 @@ func (x *UserInstanceRequest) GetUser() *UserContext { return nil } +func (x *UserInstanceRequest) GetWebhookBasePath() string { + if x != nil && x.WebhookBasePath != nil { + return *x.WebhookBasePath + } + return "" +} + +func (x *UserInstanceRequest) GetConfig() []byte { + if x != nil { + return x.Config + } + return nil +} + +func (x *UserInstanceRequest) GetStorage() []byte { + if x != nil { + return x.Storage + } + return nil +} + var File_meta_proto protoreflect.FileDescriptor const file_meta_proto_rawDesc = "" + @@ -735,18 +882,37 @@ const file_meta_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\v2\f.ExtrasValueR\x05value:\x028\x01\"L\n" + "\x10SetEnableRequest\x12 \n" + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x16\n" + - "\x06enable\x18\x02 \x01(\bR\x06enable\"V\n" + - "\n" + - "UserUpdate\x12$\n" + - "\amessage\x18\x01 \x01(\v2\b.MessageH\x00R\amessage\x12\x18\n" + - "\x06config\x18\x02 \x01(\tH\x00R\x06configB\b\n" + - "\x06update\"q\n" + + "\x06enable\x18\x02 \x01(\bR\x06enable\"E\n" + + "\x12PluginCapabilities\x12/\n" + + "\fcapabilities\x18\x01 \x03(\x0e2\v.CapabilityR\fcapabilities\"\x97\x01\n" + + "\x0eInstanceUpdate\x129\n" + + "\fcapabilities\x18\x01 \x01(\v2\x13.PluginCapabilitiesH\x00R\fcapabilities\x12$\n" + + "\amessage\x18\x02 \x01(\v2\b.MessageH\x00R\amessage\x12\x1a\n" + + "\astorage\x18\x03 \x01(\fH\x00R\astorageB\b\n" + + "\x06update\"\x87\x02\n" + "\x13UserInstanceRequest\x128\n" + "\rserverVersion\x18\x01 \x01(\v2\x12.ServerVersionInfoR\rserverVersion\x12 \n" + - "\x04user\x18\x02 \x01(\v2\f.UserContextR\x04user2p\n" + + "\x04user\x18\x02 \x01(\v2\f.UserContextR\x04user\x12-\n" + + "\x0fwebhookBasePath\x18\x03 \x01(\tH\x00R\x0fwebhookBasePath\x88\x01\x01\x12\x1b\n" + + "\x06config\x18\x04 \x01(\fH\x01R\x06config\x88\x01\x01\x12\x1d\n" + + "\astorage\x18\x05 \x01(\fH\x02R\astorage\x88\x01\x01B\x12\n" + + "\x10_webhookBasePathB\t\n" + + "\a_configB\n" + + "\n" + + "\b_storage*W\n" + + "\n" + + "Capability\x12\r\n" + + "\tDISPLAYER\x10\x00\x12\r\n" + + "\tMESSENGER\x10\x01\x12\x0e\n" + + "\n" + + "CONFIGURER\x10\x02\x12\f\n" + + "\bSTORAGER\x10\x03\x12\r\n" + + "\tWEBHOOKER\x10\x042\xe3\x01\n" + "\x06Plugin\x12.\n" + "\rGetPluginInfo\x12\x16.google.protobuf.Empty\x1a\x05.Info\x126\n" + - "\x0fRunUserInstance\x12\x14.UserInstanceRequest\x1a\v.UserUpdate0\x01B\x16Z\x14./generated/protobufb\x06proto3" + "\tSetEnable\x12\x11.SetEnableRequest\x1a\x16.google.protobuf.Empty\x125\n" + + "\vUserUpdates\x12\f.UserContext\x1a\x16.google.protobuf.Empty(\x01\x12:\n" + + "\x0fRunUserInstance\x12\x14.UserInstanceRequest\x1a\x0f.InstanceUpdate0\x01B\x16Z\x14./generated/protobufb\x06proto3" var ( file_meta_proto_rawDescOnce sync.Once @@ -760,40 +926,49 @@ func file_meta_proto_rawDescGZIP() []byte { return file_meta_proto_rawDescData } -var file_meta_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_meta_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_meta_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_meta_proto_goTypes = []any{ - (*Error)(nil), // 0: Error - (*UserContext)(nil), // 1: UserContext - (*Capabilities)(nil), // 2: Capabilities - (*ServerVersionInfo)(nil), // 3: ServerVersionInfo - (*Info)(nil), // 4: Info - (*ExtrasValue)(nil), // 5: ExtrasValue - (*Message)(nil), // 6: Message - (*SetEnableRequest)(nil), // 7: SetEnableRequest - (*UserUpdate)(nil), // 8: UserUpdate - (*UserInstanceRequest)(nil), // 9: UserInstanceRequest - nil, // 10: Message.ExtrasEntry - (*anypb.Any)(nil), // 11: google.protobuf.Any - (*emptypb.Empty)(nil), // 12: google.protobuf.Empty + (Capability)(0), // 0: Capability + (*Error)(nil), // 1: Error + (*UserContext)(nil), // 2: UserContext + (*Capabilities)(nil), // 3: Capabilities + (*ServerVersionInfo)(nil), // 4: ServerVersionInfo + (*Info)(nil), // 5: Info + (*ExtrasValue)(nil), // 6: ExtrasValue + (*Message)(nil), // 7: Message + (*SetEnableRequest)(nil), // 8: SetEnableRequest + (*PluginCapabilities)(nil), // 9: PluginCapabilities + (*InstanceUpdate)(nil), // 10: InstanceUpdate + (*UserInstanceRequest)(nil), // 11: UserInstanceRequest + nil, // 12: Message.ExtrasEntry + (*anypb.Any)(nil), // 13: google.protobuf.Any + (*emptypb.Empty)(nil), // 14: google.protobuf.Empty } var file_meta_proto_depIdxs = []int32{ - 11, // 0: Error.details:type_name -> google.protobuf.Any - 2, // 1: Info.capabilities:type_name -> Capabilities - 10, // 2: Message.extras:type_name -> Message.ExtrasEntry - 1, // 3: SetEnableRequest.user:type_name -> UserContext - 6, // 4: UserUpdate.message:type_name -> Message - 3, // 5: UserInstanceRequest.serverVersion:type_name -> ServerVersionInfo - 1, // 6: UserInstanceRequest.user:type_name -> UserContext - 5, // 7: Message.ExtrasEntry.value:type_name -> ExtrasValue - 12, // 8: Plugin.GetPluginInfo:input_type -> google.protobuf.Empty - 9, // 9: Plugin.RunUserInstance:input_type -> UserInstanceRequest - 4, // 10: Plugin.GetPluginInfo:output_type -> Info - 8, // 11: Plugin.RunUserInstance:output_type -> UserUpdate - 10, // [10:12] is the sub-list for method output_type - 8, // [8:10] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 13, // 0: Error.details:type_name -> google.protobuf.Any + 3, // 1: Info.capabilities:type_name -> Capabilities + 12, // 2: Message.extras:type_name -> Message.ExtrasEntry + 2, // 3: SetEnableRequest.user:type_name -> UserContext + 0, // 4: PluginCapabilities.capabilities:type_name -> Capability + 9, // 5: InstanceUpdate.capabilities:type_name -> PluginCapabilities + 7, // 6: InstanceUpdate.message:type_name -> Message + 4, // 7: UserInstanceRequest.serverVersion:type_name -> ServerVersionInfo + 2, // 8: UserInstanceRequest.user:type_name -> UserContext + 6, // 9: Message.ExtrasEntry.value:type_name -> ExtrasValue + 14, // 10: Plugin.GetPluginInfo:input_type -> google.protobuf.Empty + 8, // 11: Plugin.SetEnable:input_type -> SetEnableRequest + 2, // 12: Plugin.UserUpdates:input_type -> UserContext + 11, // 13: Plugin.RunUserInstance:input_type -> UserInstanceRequest + 5, // 14: Plugin.GetPluginInfo:output_type -> Info + 14, // 15: Plugin.SetEnable:output_type -> google.protobuf.Empty + 14, // 16: Plugin.UserUpdates:output_type -> google.protobuf.Empty + 10, // 17: Plugin.RunUserInstance:output_type -> InstanceUpdate + 14, // [14:18] is the sub-list for method output_type + 10, // [10:14] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_meta_proto_init() } @@ -805,22 +980,25 @@ func file_meta_proto_init() { file_meta_proto_msgTypes[5].OneofWrappers = []any{ (*ExtrasValue_Json)(nil), } - file_meta_proto_msgTypes[8].OneofWrappers = []any{ - (*UserUpdate_Message)(nil), - (*UserUpdate_Config)(nil), + file_meta_proto_msgTypes[9].OneofWrappers = []any{ + (*InstanceUpdate_Capabilities)(nil), + (*InstanceUpdate_Message)(nil), + (*InstanceUpdate_Storage)(nil), } + file_meta_proto_msgTypes[10].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_meta_proto_rawDesc), len(file_meta_proto_rawDesc)), - NumEnums: 0, - NumMessages: 11, + NumEnums: 1, + NumMessages: 12, NumExtensions: 0, NumServices: 1, }, GoTypes: file_meta_proto_goTypes, DependencyIndexes: file_meta_proto_depIdxs, + EnumInfos: file_meta_proto_enumTypes, MessageInfos: file_meta_proto_msgTypes, }.Build() File_meta_proto = out.File diff --git a/v2/generated/protobuf/meta_grpc.pb.go b/v2/generated/protobuf/meta_grpc.pb.go index 6375102..8fa9f39 100644 --- a/v2/generated/protobuf/meta_grpc.pb.go +++ b/v2/generated/protobuf/meta_grpc.pb.go @@ -21,6 +21,8 @@ const _ = grpc.SupportPackageIsVersion9 const ( Plugin_GetPluginInfo_FullMethodName = "/Plugin/GetPluginInfo" + Plugin_SetEnable_FullMethodName = "/Plugin/SetEnable" + Plugin_UserUpdates_FullMethodName = "/Plugin/UserUpdates" Plugin_RunUserInstance_FullMethodName = "/Plugin/RunUserInstance" ) @@ -28,8 +30,14 @@ const ( // // 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 PluginClient interface { + // get the plugin info GetPluginInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) - RunUserInstance(ctx context.Context, in *UserInstanceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[UserUpdate], error) + // set the enable state of a plugin instance + SetEnable(ctx context.Context, in *SetEnableRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // updates to user information + UserUpdates(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[UserContext, emptypb.Empty], error) + // run a user instance + RunUserInstance(ctx context.Context, in *UserInstanceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[InstanceUpdate], error) } type pluginClient struct { @@ -50,13 +58,36 @@ func (c *pluginClient) GetPluginInfo(ctx context.Context, in *emptypb.Empty, opt return out, nil } -func (c *pluginClient) RunUserInstance(ctx context.Context, in *UserInstanceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[UserUpdate], error) { +func (c *pluginClient) SetEnable(ctx context.Context, in *SetEnableRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &Plugin_ServiceDesc.Streams[0], Plugin_RunUserInstance_FullMethodName, cOpts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Plugin_SetEnable_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } - x := &grpc.GenericClientStream[UserInstanceRequest, UserUpdate]{ClientStream: stream} + return out, nil +} + +func (c *pluginClient) UserUpdates(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[UserContext, emptypb.Empty], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &Plugin_ServiceDesc.Streams[0], Plugin_UserUpdates_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[UserContext, emptypb.Empty]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Plugin_UserUpdatesClient = grpc.ClientStreamingClient[UserContext, emptypb.Empty] + +func (c *pluginClient) RunUserInstance(ctx context.Context, in *UserInstanceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[InstanceUpdate], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &Plugin_ServiceDesc.Streams[1], Plugin_RunUserInstance_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[UserInstanceRequest, InstanceUpdate]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } @@ -67,14 +98,20 @@ func (c *pluginClient) RunUserInstance(ctx context.Context, in *UserInstanceRequ } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type Plugin_RunUserInstanceClient = grpc.ServerStreamingClient[UserUpdate] +type Plugin_RunUserInstanceClient = grpc.ServerStreamingClient[InstanceUpdate] // PluginServer is the server API for Plugin service. // All implementations must embed UnimplementedPluginServer // for forward compatibility. type PluginServer interface { + // get the plugin info GetPluginInfo(context.Context, *emptypb.Empty) (*Info, error) - RunUserInstance(*UserInstanceRequest, grpc.ServerStreamingServer[UserUpdate]) error + // set the enable state of a plugin instance + SetEnable(context.Context, *SetEnableRequest) (*emptypb.Empty, error) + // updates to user information + UserUpdates(grpc.ClientStreamingServer[UserContext, emptypb.Empty]) error + // run a user instance + RunUserInstance(*UserInstanceRequest, grpc.ServerStreamingServer[InstanceUpdate]) error mustEmbedUnimplementedPluginServer() } @@ -88,7 +125,13 @@ type UnimplementedPluginServer struct{} func (UnimplementedPluginServer) GetPluginInfo(context.Context, *emptypb.Empty) (*Info, error) { return nil, status.Errorf(codes.Unimplemented, "method GetPluginInfo not implemented") } -func (UnimplementedPluginServer) RunUserInstance(*UserInstanceRequest, grpc.ServerStreamingServer[UserUpdate]) error { +func (UnimplementedPluginServer) SetEnable(context.Context, *SetEnableRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetEnable not implemented") +} +func (UnimplementedPluginServer) UserUpdates(grpc.ClientStreamingServer[UserContext, emptypb.Empty]) error { + return status.Errorf(codes.Unimplemented, "method UserUpdates not implemented") +} +func (UnimplementedPluginServer) RunUserInstance(*UserInstanceRequest, grpc.ServerStreamingServer[InstanceUpdate]) error { return status.Errorf(codes.Unimplemented, "method RunUserInstance not implemented") } func (UnimplementedPluginServer) mustEmbedUnimplementedPluginServer() {} @@ -130,16 +173,41 @@ func _Plugin_GetPluginInfo_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } +func _Plugin_SetEnable_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetEnableRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).SetEnable(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Plugin_SetEnable_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).SetEnable(ctx, req.(*SetEnableRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Plugin_UserUpdates_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(PluginServer).UserUpdates(&grpc.GenericServerStream[UserContext, emptypb.Empty]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Plugin_UserUpdatesServer = grpc.ClientStreamingServer[UserContext, emptypb.Empty] + func _Plugin_RunUserInstance_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(UserInstanceRequest) if err := stream.RecvMsg(m); err != nil { return err } - return srv.(PluginServer).RunUserInstance(m, &grpc.GenericServerStream[UserInstanceRequest, UserUpdate]{ServerStream: stream}) + return srv.(PluginServer).RunUserInstance(m, &grpc.GenericServerStream[UserInstanceRequest, InstanceUpdate]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type Plugin_RunUserInstanceServer = grpc.ServerStreamingServer[UserUpdate] +type Plugin_RunUserInstanceServer = grpc.ServerStreamingServer[InstanceUpdate] // Plugin_ServiceDesc is the grpc.ServiceDesc for Plugin service. // It's only intended for direct use with grpc.RegisterService, @@ -152,8 +220,17 @@ var Plugin_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetPluginInfo", Handler: _Plugin_GetPluginInfo_Handler, }, + { + MethodName: "SetEnable", + Handler: _Plugin_SetEnable_Handler, + }, }, Streams: []grpc.StreamDesc{ + { + StreamName: "UserUpdates", + Handler: _Plugin_UserUpdates_Handler, + ClientStreams: true, + }, { StreamName: "RunUserInstance", Handler: _Plugin_RunUserInstance_Handler, diff --git a/v2/generated/protobuf/webhooker.pb.go b/v2/generated/protobuf/webhooker.pb.go deleted file mode 100644 index fe1b033..0000000 --- a/v2/generated/protobuf/webhooker.pb.go +++ /dev/null @@ -1,141 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.7 -// protoc v6.31.1 -// source: webhooker.proto - -package protobuf - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - emptypb "google.golang.org/protobuf/types/known/emptypb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -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 RegisterBasePathRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` - BasePath string `protobuf:"bytes,2,opt,name=base_path,json=basePath,proto3" json:"base_path,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RegisterBasePathRequest) Reset() { - *x = RegisterBasePathRequest{} - mi := &file_webhooker_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RegisterBasePathRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RegisterBasePathRequest) ProtoMessage() {} - -func (x *RegisterBasePathRequest) ProtoReflect() protoreflect.Message { - mi := &file_webhooker_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RegisterBasePathRequest.ProtoReflect.Descriptor instead. -func (*RegisterBasePathRequest) Descriptor() ([]byte, []int) { - return file_webhooker_proto_rawDescGZIP(), []int{0} -} - -func (x *RegisterBasePathRequest) GetUser() *UserContext { - if x != nil { - return x.User - } - return nil -} - -func (x *RegisterBasePathRequest) GetBasePath() string { - if x != nil { - return x.BasePath - } - return "" -} - -var File_webhooker_proto protoreflect.FileDescriptor - -const file_webhooker_proto_rawDesc = "" + - "\n" + - "\x0fwebhooker.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\n" + - "meta.proto\"X\n" + - "\x17RegisterBasePathRequest\x12 \n" + - "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x1b\n" + - "\tbase_path\x18\x02 \x01(\tR\bbasePath2Q\n" + - "\tWebhooker\x12D\n" + - "\x10RegisterBasePath\x12\x18.RegisterBasePathRequest\x1a\x16.google.protobuf.EmptyB\x16Z\x14./generated/protobufb\x06proto3" - -var ( - file_webhooker_proto_rawDescOnce sync.Once - file_webhooker_proto_rawDescData []byte -) - -func file_webhooker_proto_rawDescGZIP() []byte { - file_webhooker_proto_rawDescOnce.Do(func() { - file_webhooker_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_webhooker_proto_rawDesc), len(file_webhooker_proto_rawDesc))) - }) - return file_webhooker_proto_rawDescData -} - -var file_webhooker_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_webhooker_proto_goTypes = []any{ - (*RegisterBasePathRequest)(nil), // 0: RegisterBasePathRequest - (*UserContext)(nil), // 1: UserContext - (*emptypb.Empty)(nil), // 2: google.protobuf.Empty -} -var file_webhooker_proto_depIdxs = []int32{ - 1, // 0: RegisterBasePathRequest.user:type_name -> UserContext - 0, // 1: Webhooker.RegisterBasePath:input_type -> RegisterBasePathRequest - 2, // 2: Webhooker.RegisterBasePath:output_type -> google.protobuf.Empty - 2, // [2:3] is the sub-list for method output_type - 1, // [1:2] 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_webhooker_proto_init() } -func file_webhooker_proto_init() { - if File_webhooker_proto != nil { - return - } - file_meta_proto_init() - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_webhooker_proto_rawDesc), len(file_webhooker_proto_rawDesc)), - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_webhooker_proto_goTypes, - DependencyIndexes: file_webhooker_proto_depIdxs, - MessageInfos: file_webhooker_proto_msgTypes, - }.Build() - File_webhooker_proto = out.File - file_webhooker_proto_goTypes = nil - file_webhooker_proto_depIdxs = nil -} diff --git a/v2/generated/protobuf/webhooker_grpc.pb.go b/v2/generated/protobuf/webhooker_grpc.pb.go deleted file mode 100644 index 65afe82..0000000 --- a/v2/generated/protobuf/webhooker_grpc.pb.go +++ /dev/null @@ -1,122 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v6.31.1 -// source: webhooker.proto - -package protobuf - -import ( - context "context" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" - emptypb "google.golang.org/protobuf/types/known/emptypb" -) - -// 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.64.0 or later. -const _ = grpc.SupportPackageIsVersion9 - -const ( - Webhooker_RegisterBasePath_FullMethodName = "/Webhooker/RegisterBasePath" -) - -// WebhookerClient is the client API for Webhooker 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 WebhookerClient interface { - RegisterBasePath(ctx context.Context, in *RegisterBasePathRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) -} - -type webhookerClient struct { - cc grpc.ClientConnInterface -} - -func NewWebhookerClient(cc grpc.ClientConnInterface) WebhookerClient { - return &webhookerClient{cc} -} - -func (c *webhookerClient) RegisterBasePath(ctx context.Context, in *RegisterBasePathRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, Webhooker_RegisterBasePath_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// WebhookerServer is the server API for Webhooker service. -// All implementations must embed UnimplementedWebhookerServer -// for forward compatibility. -type WebhookerServer interface { - RegisterBasePath(context.Context, *RegisterBasePathRequest) (*emptypb.Empty, error) - mustEmbedUnimplementedWebhookerServer() -} - -// UnimplementedWebhookerServer must be embedded to have -// forward compatible implementations. -// -// NOTE: this should be embedded by value instead of pointer to avoid a nil -// pointer dereference when methods are called. -type UnimplementedWebhookerServer struct{} - -func (UnimplementedWebhookerServer) RegisterBasePath(context.Context, *RegisterBasePathRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method RegisterBasePath not implemented") -} -func (UnimplementedWebhookerServer) mustEmbedUnimplementedWebhookerServer() {} -func (UnimplementedWebhookerServer) testEmbeddedByValue() {} - -// UnsafeWebhookerServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to WebhookerServer will -// result in compilation errors. -type UnsafeWebhookerServer interface { - mustEmbedUnimplementedWebhookerServer() -} - -func RegisterWebhookerServer(s grpc.ServiceRegistrar, srv WebhookerServer) { - // If the following call pancis, it indicates UnimplementedWebhookerServer was - // embedded by pointer and is nil. This will cause panics if an - // unimplemented method is ever invoked, so we test this at initialization - // time to prevent it from happening at runtime later due to I/O. - if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { - t.testEmbeddedByValue() - } - s.RegisterService(&Webhooker_ServiceDesc, srv) -} - -func _Webhooker_RegisterBasePath_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(RegisterBasePathRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(WebhookerServer).RegisterBasePath(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Webhooker_RegisterBasePath_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(WebhookerServer).RegisterBasePath(ctx, req.(*RegisterBasePathRequest)) - } - return interceptor(ctx, in, info, handler) -} - -// Webhooker_ServiceDesc is the grpc.ServiceDesc for Webhooker service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var Webhooker_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "Webhooker", - HandlerType: (*WebhookerServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "RegisterBasePath", - Handler: _Webhooker_RegisterBasePath_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "webhooker.proto", -} diff --git a/v2/protobuf/config.proto b/v2/protobuf/config.proto index 45333e2..4f5d7bb 100644 --- a/v2/protobuf/config.proto +++ b/v2/protobuf/config.proto @@ -1,4 +1,6 @@ syntax = "proto3"; + +import "google/protobuf/empty.proto"; import "meta.proto"; option go_package = "./generated/protobuf"; @@ -16,13 +18,9 @@ message ValidateAndSetConfigRequest { Config config = 2; } -message ValidateAndSetConfigSuccessResponse { - -} - message ValidateAndSetConfigResponse { oneof response { - ValidateAndSetConfigSuccessResponse success = 1; + google.protobuf.Empty success = 1; Error error = 2; } } diff --git a/v2/protobuf/meta.proto b/v2/protobuf/meta.proto index d880fe9..fd2fd1d 100644 --- a/v2/protobuf/meta.proto +++ b/v2/protobuf/meta.proto @@ -57,20 +57,53 @@ message SetEnableRequest { bool enable = 2; } -message UserUpdate { +enum Capability { + DISPLAYER = 0; + MESSENGER = 1; + CONFIGURER = 2; + STORAGER = 3; + WEBHOOKER = 4; +} + +message PluginCapabilities { + repeated Capability capabilities = 1; +} + +message InstanceUpdate { oneof update { - Message message = 1; - string config = 2; + // which capabilities are displayed to the user + PluginCapabilities capabilities = 1; + // send a message to the user + Message message = 2; + // update persistent storage + bytes storage = 3; } } message UserInstanceRequest { + // the server version info ServerVersionInfo serverVersion = 1; + // the user context UserContext user = 2; + // the webhook base path + optional string webhookBasePath = 3; + // the config + optional bytes config = 4; + // the storage + optional bytes storage = 5; } service Plugin { + // get the plugin info rpc GetPluginInfo(google.protobuf.Empty) returns (Info); - rpc RunUserInstance(UserInstanceRequest) returns (stream UserUpdate); + + // set the enable state of a plugin instance + rpc SetEnable(SetEnableRequest) returns (google.protobuf.Empty); + + // updates to user information + rpc UserUpdates(stream UserContext) returns (google.protobuf.Empty); + + // run a user instance + rpc RunUserInstance(UserInstanceRequest) returns (stream InstanceUpdate); } diff --git a/v2/protobuf/webhooker.proto b/v2/protobuf/webhooker.proto deleted file mode 100644 index 048a1c7..0000000 --- a/v2/protobuf/webhooker.proto +++ /dev/null @@ -1,14 +0,0 @@ -syntax = "proto3"; -import "google/protobuf/empty.proto"; -import "meta.proto"; - -option go_package = "./generated/protobuf"; - -message RegisterBasePathRequest { - UserContext user = 1; - string base_path = 2; -} - -service Webhooker { - rpc RegisterBasePath(RegisterBasePathRequest) returns (google.protobuf.Empty); -} \ No newline at end of file diff --git a/v2/shim_v1.go b/v2/shim_v1.go new file mode 100644 index 0000000..f66b6dd --- /dev/null +++ b/v2/shim_v1.go @@ -0,0 +1,337 @@ +package plugin + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "log" + "math" + "net" + "net/http" + "net/url" + "strings" + "sync" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/protobuf/types/known/emptypb" + "gopkg.in/yaml.v2" + + "github.com/gin-gonic/gin" + papiv1 "github.com/gotify/plugin-api" + "github.com/gotify/plugin-api/v2/generated/protobuf" +) + +type GrpcDialer interface { + Dial(ctx context.Context) (*grpc.ClientConn, error) +} + +type CompatV1 struct { + GetPluginInfo func() *papiv1.Info + GetInstance func(user *papiv1.UserContext) (papiv1.Plugin, error) +} + +type PluginShim struct { + mu *sync.RWMutex + compatV1 *CompatV1 + gin *gin.Engine + instances map[uint64]papiv1.Plugin + pluginServer *grpc.Server + pluginInfo *papiv1.Info + protobuf.UnimplementedPluginServer + protobuf.UnimplementedDisplayerServer + protobuf.UnimplementedConfigurerServer +} + +func (s *PluginShim) GetPluginInfo(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { + return &protobuf.Info{ + Version: s.pluginInfo.Version, + Author: s.pluginInfo.Author, + Name: s.pluginInfo.Name, + Website: s.pluginInfo.Website, + Description: s.pluginInfo.Description, + License: s.pluginInfo.License, + ModulePath: s.pluginInfo.ModulePath, + }, nil +} + +type shimV1MessageHandler struct { + stream *protobuf.Plugin_RunUserInstanceServer +} + +func (h *shimV1MessageHandler) SendMessage(msg papiv1.Message) error { + return (*h.stream).Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Message{ + Message: &protobuf.Message{ + Message: msg.Message, + }, + }, + }) +} + +type shimV1StorageHandler struct { + currentStorage []byte + stream *protobuf.Plugin_RunUserInstanceServer +} + +func (h *shimV1StorageHandler) Save(b []byte) error { + return (*h.stream).Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Storage{ + Storage: b, + }, + }) +} + +func (h *shimV1StorageHandler) Load() (b []byte, err error) { + copy(h.currentStorage, b) + return +} + +func (s *PluginShim) SetEnable(ctx context.Context, req *protobuf.SetEnableRequest) (*emptypb.Empty, error) { + s.mu.RLock() + instance, ok := s.instances[uint64(req.User.Id)] + s.mu.RUnlock() + if !ok { + return nil, errors.New("instance not found") + } + if req.Enable { + return new(emptypb.Empty), instance.Enable() + } else { + return new(emptypb.Empty), instance.Disable() + } +} + +func (s *PluginShim) Display(ctx context.Context, req *protobuf.DisplayRequest) (*protobuf.DisplayResponse, error) { + s.mu.RLock() + instance, ok := s.instances[uint64(req.User.Id)] + s.mu.RUnlock() + if !ok { + return nil, errors.New("instance not found") + } + if displayer, ok := instance.(papiv1.Displayer); ok { + location, err := url.Parse(req.Location) + if err != nil { + return nil, err + } + return &protobuf.DisplayResponse{ + Display: displayer.GetDisplay(location), + }, nil + } + return nil, errors.New("instance does not implement displayer") +} + +func (s *PluginShim) DefaultConfig(ctx context.Context, req *protobuf.DefaultConfigRequest) (*protobuf.Config, error) { + s.mu.RLock() + instance, ok := s.instances[uint64(req.User.Id)] + s.mu.RUnlock() + if !ok { + return nil, errors.New("instance not found") + } + if configurer, ok := instance.(papiv1.Configurer); ok { + defaultConfig := configurer.DefaultConfig() + bytes, err := yaml.Marshal(defaultConfig) + if err != nil { + return nil, err + } + return &protobuf.Config{ + Config: string(bytes), + }, nil + } + return nil, errors.New("instance does not implement configurer") +} + +func (s *PluginShim) ValidateAndSetConfig(ctx context.Context, req *protobuf.ValidateAndSetConfigRequest) (*protobuf.ValidateAndSetConfigResponse, error) { + s.mu.RLock() + instance, ok := s.instances[uint64(req.User.Id)] + s.mu.RUnlock() + if !ok { + return nil, errors.New("instance not found") + } + if configurer, ok := instance.(papiv1.Configurer); ok { + var currentConfig interface{} + if req.Config != nil { + yaml.Unmarshal([]byte(req.Config.Config), ¤tConfig) + } + if err := configurer.ValidateAndSetConfig(currentConfig); err != nil { + return &protobuf.ValidateAndSetConfigResponse{ + Response: &protobuf.ValidateAndSetConfigResponse_Error{ + Error: &protobuf.Error{ + Message: err.Error(), + }, + }, + }, nil + } + return &protobuf.ValidateAndSetConfigResponse{ + Response: &protobuf.ValidateAndSetConfigResponse_Success{ + Success: new(emptypb.Empty), + }, + }, nil + } + return nil, errors.New("instance does not implement configurer") +} + +func (s *PluginShim) RunUserInstance(req *protobuf.UserInstanceRequest, stream protobuf.Plugin_RunUserInstanceServer) error { + if req.User.Id > math.MaxUint { + return errors.New("user id is too large") + } + instance, err := s.compatV1.GetInstance(&papiv1.UserContext{ + ID: uint(req.User.Id), + Name: req.User.Name, + Admin: req.User.Admin, + }) + if err != nil { + return err + } + + // enable supported capabilities + var capabilities []protobuf.Capability + if _, ok := instance.(papiv1.Displayer); ok { + capabilities = append(capabilities, protobuf.Capability_DISPLAYER) + } + if _, ok := instance.(papiv1.Messenger); ok { + capabilities = append(capabilities, protobuf.Capability_MESSENGER) + } + if _, ok := instance.(papiv1.Configurer); ok { + capabilities = append(capabilities, protobuf.Capability_CONFIGURER) + } + if _, ok := instance.(papiv1.Storager); ok { + capabilities = append(capabilities, protobuf.Capability_STORAGER) + } + if _, ok := instance.(papiv1.Webhooker); ok { + capabilities = append(capabilities, protobuf.Capability_WEBHOOKER) + } + + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capabilities{ + Capabilities: &protobuf.PluginCapabilities{ + Capabilities: capabilities, + }, + }, + }); err != nil { + return err + } + + if messenger, ok := instance.(papiv1.Messenger); ok { + messenger.SetMessageHandler(&shimV1MessageHandler{ + stream: &stream, + }) + } + + if configurer, ok := instance.(papiv1.Configurer); ok { + currentConfig := configurer.DefaultConfig() + if req.Config != nil { + yaml.Unmarshal(req.Config, ¤tConfig) + if err := configurer.ValidateAndSetConfig(currentConfig); err != nil { + return err + } + } + } + + if storager, ok := instance.(papiv1.Storager); ok { + storageHandler := &shimV1StorageHandler{ + currentStorage: req.Storage, + stream: &stream, + } + storager.SetStorageHandler(storageHandler) + } + + if webhooker, ok := instance.(papiv1.Webhooker); ok { + if req.WebhookBasePath != nil { + group := s.gin.Group(*req.WebhookBasePath) + webhooker.RegisterWebhook(*req.WebhookBasePath, group) + } + } + + s.mu.Lock() + s.instances[uint64(req.User.Id)] = instance + s.mu.Unlock() + + return nil +} + +func NewPluginRpc(compatV1 *CompatV1, cliArgs []string) (*PluginShim, error) { + pluginInfo := compatV1.GetPluginInfo() + tlsName := BuildPluginTLSName(pluginInfo.Name) + + cliFlags, err := ParsePluginCLIFlags(cliArgs) + if err != nil { + log.Fatalf("Failed to parse CLI flags: %v", err) + } + rootCAs := x509.NewCertPool() + caCert, err := x509.ParseCertificate(cliFlags.CAData) + if err != nil { + return nil, err + } + rootCAs.AddCert(caCert) + + leafCert, err := x509.ParseCertificate(cliFlags.CertData) + if err != nil { + return nil, err + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{ + { + Certificate: [][]byte{cliFlags.CertData}, + PrivateKey: cliFlags.KeyData, + Leaf: leafCert, + }, + { + Certificate: [][]byte{caCert.Raw}, + }, + }, + RootCAs: rootCAs, + ServerName: tlsName, + ClientAuth: tls.RequireAndVerifyClientCert, + VerifyConnection: func(state tls.ConnectionState) error { + if state.ServerName != ServerTLSName { + return errors.New("not implemented: client must be the gotify server itself for now") + } + return nil + }, + } + + rpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig))) + + gin := gin.Default() + + self := &PluginShim{ + mu: &sync.RWMutex{}, + instances: make(map[uint64]papiv1.Plugin), + compatV1: compatV1, + gin: gin, + pluginServer: rpcServer, + pluginInfo: pluginInfo, + } + + protobuf.RegisterPluginServer(rpcServer, self) + protobuf.RegisterDisplayerServer(rpcServer, self) + protobuf.RegisterConfigurerServer(rpcServer, self) + + return self, nil +} + +func (h *PluginShim) ServeHTTP(w http.ResponseWriter, r *http.Request) { + pluginHostName := BuildPluginTLSName(h.pluginInfo.ModulePath) + if r.Host == pluginHostName { + if r.ProtoMajor != 2 { + http.Error(w, "Must use HTTP/2", http.StatusHTTPVersionNotSupported) + return + } + if !strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") { + http.Error(w, "Must use application/grpc content type", http.StatusUnsupportedMediaType) + } + h.pluginServer.ServeHTTP(w, r) + return + } + + if h.gin != nil { + h.gin.ServeHTTP(w, r) + return + } +} + +func (h *PluginShim) Serve(listener net.Listener) error { + return h.pluginServer.Serve(listener) +} diff --git a/v2/transport_auth.go b/v2/transport_auth.go index ea23874..6a11bbd 100644 --- a/v2/transport_auth.go +++ b/v2/transport_auth.go @@ -3,21 +3,25 @@ package plugin import ( "crypto/ed25519" "crypto/rand" - "crypto/sha256" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/hex" "fmt" + "slices" + "strings" "time" ) -const ServerTLSName = "gotify.home.arpa" +const ServerTLSName = "server.gotify.home.arpa" func BuildPluginTLSName(moduleName string) string { - moduleNameHash := sha256.Sum256([]byte(moduleName)) - hashHex := hex.EncodeToString(moduleNameHash[:]) - return fmt.Sprintf("%s.plugins.gotify.home.arpa", hashHex) + moduleNameParts := strings.Split(moduleName, "/") + for i := range moduleNameParts { + moduleNameParts[i] = hex.EncodeToString([]byte(moduleNameParts[i])) + } + slices.Reverse(moduleNameParts) + return fmt.Sprintf("%s.plugins.gotify.home.arpa", strings.Join(moduleNameParts, ".")) } type EphemeralTLSClient struct { From f59c2eba0b369885899e3bc3780971f5c0891b2c Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 03:51:02 -0500 Subject: [PATCH 07/37] userid overflow checks Signed-off-by: eternal-flame-AD --- v2/shim_v1.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/v2/shim_v1.go b/v2/shim_v1.go index f66b6dd..bfd47f8 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -89,6 +89,9 @@ func (h *shimV1StorageHandler) Load() (b []byte, err error) { } func (s *PluginShim) SetEnable(ctx context.Context, req *protobuf.SetEnableRequest) (*emptypb.Empty, error) { + if req.User.Id > math.MaxUint { + return nil, errors.New("user id is too large") + } s.mu.RLock() instance, ok := s.instances[uint64(req.User.Id)] s.mu.RUnlock() @@ -103,6 +106,9 @@ func (s *PluginShim) SetEnable(ctx context.Context, req *protobuf.SetEnableReque } func (s *PluginShim) Display(ctx context.Context, req *protobuf.DisplayRequest) (*protobuf.DisplayResponse, error) { + if req.User.Id > math.MaxUint { + return nil, errors.New("user id is too large") + } s.mu.RLock() instance, ok := s.instances[uint64(req.User.Id)] s.mu.RUnlock() @@ -122,6 +128,9 @@ func (s *PluginShim) Display(ctx context.Context, req *protobuf.DisplayRequest) } func (s *PluginShim) DefaultConfig(ctx context.Context, req *protobuf.DefaultConfigRequest) (*protobuf.Config, error) { + if req.User.Id > math.MaxUint { + return nil, errors.New("user id is too large") + } s.mu.RLock() instance, ok := s.instances[uint64(req.User.Id)] s.mu.RUnlock() @@ -142,6 +151,9 @@ func (s *PluginShim) DefaultConfig(ctx context.Context, req *protobuf.DefaultCon } func (s *PluginShim) ValidateAndSetConfig(ctx context.Context, req *protobuf.ValidateAndSetConfigRequest) (*protobuf.ValidateAndSetConfigResponse, error) { + if req.User.Id > math.MaxUint { + return nil, errors.New("user id is too large") + } s.mu.RLock() instance, ok := s.instances[uint64(req.User.Id)] s.mu.RUnlock() From 1d055c179fe75da86b6fbe12cdb962708e8ded65 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 03:59:05 -0500 Subject: [PATCH 08/37] use SNI to mux webhooker Signed-off-by: eternal-flame-AD --- v2/pipe_net_test.go | 4 ++-- v2/shim_v1.go | 14 +++++++++++--- v2/transport_auth.go | 13 +++++++++---- v2/transport_auth_test.go | 4 ++-- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/v2/pipe_net_test.go b/v2/pipe_net_test.go index bb2483f..4a867d1 100644 --- a/v2/pipe_net_test.go +++ b/v2/pipe_net_test.go @@ -52,10 +52,10 @@ func TestGrpcPipeNet(t *testing.T) { defer listener.Close() serverCsrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ Subject: pkix.Name{ - CommonName: BuildPluginTLSName("test"), + CommonName: BuildPluginTLSName(purposePluginRPC, "test"), }, DNSNames: []string{ - BuildPluginTLSName("test"), + BuildPluginTLSName(purposePluginRPC, "test"), }, PublicKey: serverPub, }, serverPriv) diff --git a/v2/shim_v1.go b/v2/shim_v1.go index bfd47f8..50f431a 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -264,7 +264,7 @@ func (s *PluginShim) RunUserInstance(req *protobuf.UserInstanceRequest, stream p func NewPluginRpc(compatV1 *CompatV1, cliArgs []string) (*PluginShim, error) { pluginInfo := compatV1.GetPluginInfo() - tlsName := BuildPluginTLSName(pluginInfo.Name) + tlsName := BuildPluginTLSName(purposePluginRPC, pluginInfo.Name) cliFlags, err := ParsePluginCLIFlags(cliArgs) if err != nil { @@ -325,16 +325,24 @@ func NewPluginRpc(compatV1 *CompatV1, cliArgs []string) (*PluginShim, error) { } func (h *PluginShim) ServeHTTP(w http.ResponseWriter, r *http.Request) { - pluginHostName := BuildPluginTLSName(h.pluginInfo.ModulePath) - if r.Host == pluginHostName { + if r.TLS == nil { + http.Error(w, "Must use TLS", http.StatusUpgradeRequired) + return + } + + pluginRpcHostName := BuildPluginTLSName(purposePluginRPC, h.pluginInfo.ModulePath) + + if r.TLS.ServerName == pluginRpcHostName { if r.ProtoMajor != 2 { http.Error(w, "Must use HTTP/2", http.StatusHTTPVersionNotSupported) return } if !strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") { http.Error(w, "Must use application/grpc content type", http.StatusUnsupportedMediaType) + return } h.pluginServer.ServeHTTP(w, r) + return } diff --git a/v2/transport_auth.go b/v2/transport_auth.go index 6a11bbd..400fad6 100644 --- a/v2/transport_auth.go +++ b/v2/transport_auth.go @@ -13,15 +13,20 @@ import ( "time" ) +const ( + purposePluginRPC = "rpc.plugin" + purposePluginWebhook = "webhook.plugin" +) + const ServerTLSName = "server.gotify.home.arpa" -func BuildPluginTLSName(moduleName string) string { +func BuildPluginTLSName(purpose string, moduleName string) string { moduleNameParts := strings.Split(moduleName, "/") for i := range moduleNameParts { moduleNameParts[i] = hex.EncodeToString([]byte(moduleNameParts[i])) } slices.Reverse(moduleNameParts) - return fmt.Sprintf("%s.plugins.gotify.home.arpa", strings.Join(moduleNameParts, ".")) + return fmt.Sprintf("%s.%s.plugins.gotify.home.arpa", strings.Join(moduleNameParts, "."), purpose) } type EphemeralTLSClient struct { @@ -53,7 +58,7 @@ func (s *EphemeralTLSClient) ClientTLSConfig(moduleName string) *tls.Config { }, }, RootCAs: s.createCertPool(), - ServerName: BuildPluginTLSName(moduleName), + ServerName: BuildPluginTLSName(purposePluginRPC, moduleName), } } @@ -90,7 +95,7 @@ func (s *EphemeralTLSClient) SignCSR(dnsName string, csr *x509.CertificateReques } func (s *EphemeralTLSClient) SignPluginCSR(moduleName string, csr *x509.CertificateRequest) ([]byte, error) { - return s.SignCSR(BuildPluginTLSName(moduleName), csr) + return s.SignCSR(BuildPluginTLSName(purposePluginRPC, moduleName), csr) } func NewEphemeralTLSClient() (*EphemeralTLSClient, error) { diff --git a/v2/transport_auth_test.go b/v2/transport_auth_test.go index 9b57188..f5d87d8 100644 --- a/v2/transport_auth_test.go +++ b/v2/transport_auth_test.go @@ -17,7 +17,7 @@ func TestEphemeralTLSClient(t *testing.T) { t.Fatal(err) } - pluginTlsName := BuildPluginTLSName("test") + pluginTlsName := BuildPluginTLSName(purposePluginRPC, "test") _, serverPriv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) @@ -27,7 +27,7 @@ func TestEphemeralTLSClient(t *testing.T) { CommonName: pluginTlsName, }, DNSNames: []string{ - BuildPluginTLSName("test"), + BuildPluginTLSName(purposePluginRPC, "test"), }, }, serverPriv) if err != nil { From 9863b121591ce20fee79b42db6c1c0b04937fa0a Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 04:00:03 -0500 Subject: [PATCH 09/37] fixup! use SNI to mux webhooker --- v2/shim_v1.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/v2/shim_v1.go b/v2/shim_v1.go index 50f431a..7b93fdd 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -346,10 +346,13 @@ func (h *PluginShim) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - if h.gin != nil { + pluginWebhookHostName := BuildPluginTLSName(purposePluginWebhook, h.pluginInfo.ModulePath) + if r.TLS.ServerName == pluginWebhookHostName { h.gin.ServeHTTP(w, r) return } + + http.Error(w, "Virtual host not found", http.StatusNotFound) } func (h *PluginShim) Serve(listener net.Listener) error { From 44933739546363950f727cf652b7a6b35720ed6b Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 04:00:50 -0500 Subject: [PATCH 10/37] fixup! fixup! use SNI to mux webhooker --- v2/transport_auth.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/v2/transport_auth.go b/v2/transport_auth.go index 400fad6..6de63a8 100644 --- a/v2/transport_auth.go +++ b/v2/transport_auth.go @@ -14,8 +14,8 @@ import ( ) const ( - purposePluginRPC = "rpc.plugin" - purposePluginWebhook = "webhook.plugin" + purposePluginRPC = "rpc" + purposePluginWebhook = "webhook" ) const ServerTLSName = "server.gotify.home.arpa" @@ -26,7 +26,7 @@ func BuildPluginTLSName(purpose string, moduleName string) string { moduleNameParts[i] = hex.EncodeToString([]byte(moduleNameParts[i])) } slices.Reverse(moduleNameParts) - return fmt.Sprintf("%s.%s.plugins.gotify.home.arpa", strings.Join(moduleNameParts, "."), purpose) + return fmt.Sprintf("%s.%s.plugin.gotify.home.arpa", purpose, strings.Join(moduleNameParts, ".")) } type EphemeralTLSClient struct { From bd59ee34cf75361b507d8fcd547484f1caece628 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 04:18:00 -0500 Subject: [PATCH 11/37] v1 shim Signed-off-by: eternal-flame-AD --- v2/shim_v1.go | 80 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/v2/shim_v1.go b/v2/shim_v1.go index 7b93fdd..1880157 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -7,14 +7,13 @@ import ( "errors" "log" "math" - "net" "net/http" "net/url" + "plugin" "strings" "sync" "google.golang.org/grpc" - "google.golang.org/grpc/credentials" "google.golang.org/protobuf/types/known/emptypb" "gopkg.in/yaml.v2" @@ -32,19 +31,54 @@ type CompatV1 struct { GetInstance func(user *papiv1.UserContext) (papiv1.Plugin, error) } -type PluginShim struct { +// NewCompatV1FromPlugin creates a new CompatV1 from a native Go plugin. +func NewCompatV1FromPlugin(plugin *plugin.Plugin) (*CompatV1, error) { + getPluginInfo, err := plugin.Lookup("GetGotifyPluginInfo") + if err != nil { + return nil, err + } + getInstance, err := plugin.Lookup("NewGotifyPlugin") + if err != nil { + return nil, err + } + + getPluginInfoChecked, ok := getPluginInfo.(func() *papiv1.Info) + if !ok { + return nil, errors.New("GetGotifyPluginInfo is not a function") + } + getInstanceCheckedWithErr, ok := getInstance.(func(user *papiv1.UserContext) (papiv1.Plugin, error)) + if !ok { + if getInstanceCheckedWithoutErr, ok := getInstance.(func(user *papiv1.UserContext) papiv1.Plugin); ok { + getInstanceCheckedWithErr = func(user *papiv1.UserContext) (papiv1.Plugin, error) { + return getInstanceCheckedWithoutErr(user), nil + } + } else { + return nil, errors.New("NewGotifyPlugin is not a function") + } + } + + return &CompatV1{ + GetPluginInfo: getPluginInfoChecked, + GetInstance: getInstanceCheckedWithErr, + }, nil +} + +// CompatV1Shim is a shim that acts like a plugin server and delegates request to +// something that implements a V1-style API interface. +type CompatV1Shim struct { mu *sync.RWMutex compatV1 *CompatV1 gin *gin.Engine instances map[uint64]papiv1.Plugin pluginServer *grpc.Server pluginInfo *papiv1.Info + http.Server protobuf.UnimplementedPluginServer protobuf.UnimplementedDisplayerServer protobuf.UnimplementedConfigurerServer } -func (s *PluginShim) GetPluginInfo(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { +func (s *CompatV1Shim) GetPluginInfo(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { return &protobuf.Info{ Version: s.pluginInfo.Version, Author: s.pluginInfo.Author, @@ -88,7 +122,7 @@ func (h *shimV1StorageHandler) Load() (b []byte, err error) { return } -func (s *PluginShim) SetEnable(ctx context.Context, req *protobuf.SetEnableRequest) (*emptypb.Empty, error) { +func (s *CompatV1Shim) SetEnable(ctx context.Context, req *protobuf.SetEnableRequest) (*emptypb.Empty, error) { if req.User.Id > math.MaxUint { return nil, errors.New("user id is too large") } @@ -105,7 +139,7 @@ func (s *PluginShim) SetEnable(ctx context.Context, req *protobuf.SetEnableReque } } -func (s *PluginShim) Display(ctx context.Context, req *protobuf.DisplayRequest) (*protobuf.DisplayResponse, error) { +func (s *CompatV1Shim) Display(ctx context.Context, req *protobuf.DisplayRequest) (*protobuf.DisplayResponse, error) { if req.User.Id > math.MaxUint { return nil, errors.New("user id is too large") } @@ -127,7 +161,7 @@ func (s *PluginShim) Display(ctx context.Context, req *protobuf.DisplayRequest) return nil, errors.New("instance does not implement displayer") } -func (s *PluginShim) DefaultConfig(ctx context.Context, req *protobuf.DefaultConfigRequest) (*protobuf.Config, error) { +func (s *CompatV1Shim) DefaultConfig(ctx context.Context, req *protobuf.DefaultConfigRequest) (*protobuf.Config, error) { if req.User.Id > math.MaxUint { return nil, errors.New("user id is too large") } @@ -150,7 +184,7 @@ func (s *PluginShim) DefaultConfig(ctx context.Context, req *protobuf.DefaultCon return nil, errors.New("instance does not implement configurer") } -func (s *PluginShim) ValidateAndSetConfig(ctx context.Context, req *protobuf.ValidateAndSetConfigRequest) (*protobuf.ValidateAndSetConfigResponse, error) { +func (s *CompatV1Shim) ValidateAndSetConfig(ctx context.Context, req *protobuf.ValidateAndSetConfigRequest) (*protobuf.ValidateAndSetConfigResponse, error) { if req.User.Id > math.MaxUint { return nil, errors.New("user id is too large") } @@ -183,7 +217,7 @@ func (s *PluginShim) ValidateAndSetConfig(ctx context.Context, req *protobuf.Val return nil, errors.New("instance does not implement configurer") } -func (s *PluginShim) RunUserInstance(req *protobuf.UserInstanceRequest, stream protobuf.Plugin_RunUserInstanceServer) error { +func (s *CompatV1Shim) RunUserInstance(req *protobuf.UserInstanceRequest, stream protobuf.Plugin_RunUserInstanceServer) error { if req.User.Id > math.MaxUint { return errors.New("user id is too large") } @@ -262,7 +296,7 @@ func (s *PluginShim) RunUserInstance(req *protobuf.UserInstanceRequest, stream p return nil } -func NewPluginRpc(compatV1 *CompatV1, cliArgs []string) (*PluginShim, error) { +func NewPluginRpc(compatV1 *CompatV1, cliArgs []string) (*CompatV1Shim, error) { pluginInfo := compatV1.GetPluginInfo() tlsName := BuildPluginTLSName(purposePluginRPC, pluginInfo.Name) @@ -296,19 +330,14 @@ func NewPluginRpc(compatV1 *CompatV1, cliArgs []string) (*PluginShim, error) { RootCAs: rootCAs, ServerName: tlsName, ClientAuth: tls.RequireAndVerifyClientCert, - VerifyConnection: func(state tls.ConnectionState) error { - if state.ServerName != ServerTLSName { - return errors.New("not implemented: client must be the gotify server itself for now") - } - return nil - }, + ClientCAs: rootCAs, } - rpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig))) + rpcServer := grpc.NewServer() gin := gin.Default() - self := &PluginShim{ + self := &CompatV1Shim{ mu: &sync.RWMutex{}, instances: make(map[uint64]papiv1.Plugin), compatV1: compatV1, @@ -321,10 +350,19 @@ func NewPluginRpc(compatV1 *CompatV1, cliArgs []string) (*PluginShim, error) { protobuf.RegisterDisplayerServer(rpcServer, self) protobuf.RegisterConfigurerServer(rpcServer, self) + protocols := new(http.Protocols) + protocols.SetHTTP1(true) + protocols.SetHTTP2(true) + self.Server = http.Server{ + Handler: self, + TLSConfig: tlsConfig, + Protocols: protocols, + } + return self, nil } -func (h *PluginShim) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *CompatV1Shim) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.TLS == nil { http.Error(w, "Must use TLS", http.StatusUpgradeRequired) return @@ -354,7 +392,3 @@ func (h *PluginShim) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "Virtual host not found", http.StatusNotFound) } - -func (h *PluginShim) Serve(listener net.Listener) error { - return h.pluginServer.Serve(listener) -} From f237b5a398071f485ed8462836061012c84f20e5 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 16:49:42 -0500 Subject: [PATCH 12/37] fixup userid conversion Signed-off-by: eternal-flame-AD --- v2/shim_v1.go | 86 ++++++++++++++++++++++++--------------------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/v2/shim_v1.go b/v2/shim_v1.go index 1880157..afd6f64 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -73,20 +73,24 @@ type CompatV1Shim struct { pluginServer *grpc.Server pluginInfo *papiv1.Info http.Server +} + +type compatV1ShimServer struct { + shim *CompatV1Shim protobuf.UnimplementedPluginServer protobuf.UnimplementedDisplayerServer protobuf.UnimplementedConfigurerServer } -func (s *CompatV1Shim) GetPluginInfo(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { +func (s *compatV1ShimServer) GetPluginInfo(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { return &protobuf.Info{ - Version: s.pluginInfo.Version, - Author: s.pluginInfo.Author, - Name: s.pluginInfo.Name, - Website: s.pluginInfo.Website, - Description: s.pluginInfo.Description, - License: s.pluginInfo.License, - ModulePath: s.pluginInfo.ModulePath, + Version: s.shim.pluginInfo.Version, + Author: s.shim.pluginInfo.Author, + Name: s.shim.pluginInfo.Name, + Website: s.shim.pluginInfo.Website, + Description: s.shim.pluginInfo.Description, + License: s.shim.pluginInfo.License, + ModulePath: s.shim.pluginInfo.ModulePath, }, nil } @@ -122,13 +126,10 @@ func (h *shimV1StorageHandler) Load() (b []byte, err error) { return } -func (s *CompatV1Shim) SetEnable(ctx context.Context, req *protobuf.SetEnableRequest) (*emptypb.Empty, error) { - if req.User.Id > math.MaxUint { - return nil, errors.New("user id is too large") - } - s.mu.RLock() - instance, ok := s.instances[uint64(req.User.Id)] - s.mu.RUnlock() +func (s *compatV1ShimServer) SetEnable(ctx context.Context, req *protobuf.SetEnableRequest) (*emptypb.Empty, error) { + s.shim.mu.RLock() + instance, ok := s.shim.instances[req.User.Id] + s.shim.mu.RUnlock() if !ok { return nil, errors.New("instance not found") } @@ -139,13 +140,10 @@ func (s *CompatV1Shim) SetEnable(ctx context.Context, req *protobuf.SetEnableReq } } -func (s *CompatV1Shim) Display(ctx context.Context, req *protobuf.DisplayRequest) (*protobuf.DisplayResponse, error) { - if req.User.Id > math.MaxUint { - return nil, errors.New("user id is too large") - } - s.mu.RLock() - instance, ok := s.instances[uint64(req.User.Id)] - s.mu.RUnlock() +func (s *compatV1ShimServer) Display(ctx context.Context, req *protobuf.DisplayRequest) (*protobuf.DisplayResponse, error) { + s.shim.mu.RLock() + instance, ok := s.shim.instances[req.User.Id] + s.shim.mu.RUnlock() if !ok { return nil, errors.New("instance not found") } @@ -161,13 +159,10 @@ func (s *CompatV1Shim) Display(ctx context.Context, req *protobuf.DisplayRequest return nil, errors.New("instance does not implement displayer") } -func (s *CompatV1Shim) DefaultConfig(ctx context.Context, req *protobuf.DefaultConfigRequest) (*protobuf.Config, error) { - if req.User.Id > math.MaxUint { - return nil, errors.New("user id is too large") - } - s.mu.RLock() - instance, ok := s.instances[uint64(req.User.Id)] - s.mu.RUnlock() +func (s *compatV1ShimServer) DefaultConfig(ctx context.Context, req *protobuf.DefaultConfigRequest) (*protobuf.Config, error) { + s.shim.mu.RLock() + instance, ok := s.shim.instances[req.User.Id] + s.shim.mu.RUnlock() if !ok { return nil, errors.New("instance not found") } @@ -184,13 +179,10 @@ func (s *CompatV1Shim) DefaultConfig(ctx context.Context, req *protobuf.DefaultC return nil, errors.New("instance does not implement configurer") } -func (s *CompatV1Shim) ValidateAndSetConfig(ctx context.Context, req *protobuf.ValidateAndSetConfigRequest) (*protobuf.ValidateAndSetConfigResponse, error) { - if req.User.Id > math.MaxUint { - return nil, errors.New("user id is too large") - } - s.mu.RLock() - instance, ok := s.instances[uint64(req.User.Id)] - s.mu.RUnlock() +func (s *compatV1ShimServer) ValidateAndSetConfig(ctx context.Context, req *protobuf.ValidateAndSetConfigRequest) (*protobuf.ValidateAndSetConfigResponse, error) { + s.shim.mu.RLock() + instance, ok := s.shim.instances[req.User.Id] + s.shim.mu.RUnlock() if !ok { return nil, errors.New("instance not found") } @@ -217,11 +209,11 @@ func (s *CompatV1Shim) ValidateAndSetConfig(ctx context.Context, req *protobuf.V return nil, errors.New("instance does not implement configurer") } -func (s *CompatV1Shim) RunUserInstance(req *protobuf.UserInstanceRequest, stream protobuf.Plugin_RunUserInstanceServer) error { +func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, stream protobuf.Plugin_RunUserInstanceServer) error { if req.User.Id > math.MaxUint { return errors.New("user id is too large") } - instance, err := s.compatV1.GetInstance(&papiv1.UserContext{ + instance, err := s.shim.compatV1.GetInstance(&papiv1.UserContext{ ID: uint(req.User.Id), Name: req.User.Name, Admin: req.User.Admin, @@ -284,14 +276,14 @@ func (s *CompatV1Shim) RunUserInstance(req *protobuf.UserInstanceRequest, stream if webhooker, ok := instance.(papiv1.Webhooker); ok { if req.WebhookBasePath != nil { - group := s.gin.Group(*req.WebhookBasePath) + group := s.shim.gin.Group(*req.WebhookBasePath) webhooker.RegisterWebhook(*req.WebhookBasePath, group) } } - s.mu.Lock() - s.instances[uint64(req.User.Id)] = instance - s.mu.Unlock() + s.shim.mu.Lock() + s.shim.instances[req.User.Id] = instance + s.shim.mu.Unlock() return nil } @@ -346,9 +338,13 @@ func NewPluginRpc(compatV1 *CompatV1, cliArgs []string) (*CompatV1Shim, error) { pluginInfo: pluginInfo, } - protobuf.RegisterPluginServer(rpcServer, self) - protobuf.RegisterDisplayerServer(rpcServer, self) - protobuf.RegisterConfigurerServer(rpcServer, self) + selfServer := &compatV1ShimServer{ + shim: self, + } + + protobuf.RegisterPluginServer(rpcServer, selfServer) + protobuf.RegisterDisplayerServer(rpcServer, selfServer) + protobuf.RegisterConfigurerServer(rpcServer, selfServer) protocols := new(http.Protocols) protocols.SetHTTP1(true) From ee70ba46a650206d0c3cbf9bcca1ff153a3a0e1a Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 16:52:23 -0500 Subject: [PATCH 13/37] rearrange code order Signed-off-by: eternal-flame-AD --- v2/shim_v1.go | 235 +++++++++++++++++++++++++------------------------- 1 file changed, 119 insertions(+), 116 deletions(-) diff --git a/v2/shim_v1.go b/v2/shim_v1.go index afd6f64..ca8760d 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -26,6 +26,8 @@ type GrpcDialer interface { Dial(ctx context.Context) (*grpc.ClientConn, error) } +// CompatV1 is a shim that acts like a plugin server and delegates request to +// something that implements a V1-style API interface. type CompatV1 struct { GetPluginInfo func() *papiv1.Info GetInstance func(user *papiv1.UserContext) (papiv1.Plugin, error) @@ -75,23 +77,106 @@ type CompatV1Shim struct { http.Server } -type compatV1ShimServer struct { - shim *CompatV1Shim - protobuf.UnimplementedPluginServer - protobuf.UnimplementedDisplayerServer - protobuf.UnimplementedConfigurerServer +// NewCompatV1Rpc creates a new CompatV1Shim server. +func NewCompatV1Rpc(compatV1 *CompatV1, cliArgs []string) (*CompatV1Shim, error) { + pluginInfo := compatV1.GetPluginInfo() + tlsName := BuildPluginTLSName(purposePluginRPC, pluginInfo.Name) + + cliFlags, err := ParsePluginCLIFlags(cliArgs) + if err != nil { + log.Fatalf("Failed to parse CLI flags: %v", err) + } + rootCAs := x509.NewCertPool() + caCert, err := x509.ParseCertificate(cliFlags.CAData) + if err != nil { + return nil, err + } + rootCAs.AddCert(caCert) + + leafCert, err := x509.ParseCertificate(cliFlags.CertData) + if err != nil { + return nil, err + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{ + { + Certificate: [][]byte{cliFlags.CertData}, + PrivateKey: cliFlags.KeyData, + Leaf: leafCert, + }, + { + Certificate: [][]byte{caCert.Raw}, + }, + }, + RootCAs: rootCAs, + ServerName: tlsName, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: rootCAs, + } + + rpcServer := grpc.NewServer() + + gin := gin.Default() + + self := &CompatV1Shim{ + mu: &sync.RWMutex{}, + instances: make(map[uint64]papiv1.Plugin), + compatV1: compatV1, + gin: gin, + pluginServer: rpcServer, + pluginInfo: pluginInfo, + } + + selfServer := &compatV1ShimServer{ + shim: self, + } + + protobuf.RegisterPluginServer(rpcServer, selfServer) + protobuf.RegisterDisplayerServer(rpcServer, selfServer) + protobuf.RegisterConfigurerServer(rpcServer, selfServer) + + protocols := new(http.Protocols) + protocols.SetHTTP1(true) + protocols.SetHTTP2(true) + self.Server = http.Server{ + Handler: self, + TLSConfig: tlsConfig, + Protocols: protocols, + } + + return self, nil } -func (s *compatV1ShimServer) GetPluginInfo(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { - return &protobuf.Info{ - Version: s.shim.pluginInfo.Version, - Author: s.shim.pluginInfo.Author, - Name: s.shim.pluginInfo.Name, - Website: s.shim.pluginInfo.Website, - Description: s.shim.pluginInfo.Description, - License: s.shim.pluginInfo.License, - ModulePath: s.shim.pluginInfo.ModulePath, - }, nil +func (h *CompatV1Shim) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.TLS == nil { + http.Error(w, "Must use TLS", http.StatusUpgradeRequired) + return + } + + pluginRpcHostName := BuildPluginTLSName(purposePluginRPC, h.pluginInfo.ModulePath) + + if r.TLS.ServerName == pluginRpcHostName { + if r.ProtoMajor != 2 { + http.Error(w, "Must use HTTP/2", http.StatusHTTPVersionNotSupported) + return + } + if !strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") { + http.Error(w, "Must use application/grpc content type", http.StatusUnsupportedMediaType) + return + } + h.pluginServer.ServeHTTP(w, r) + + return + } + + pluginWebhookHostName := BuildPluginTLSName(purposePluginWebhook, h.pluginInfo.ModulePath) + if r.TLS.ServerName == pluginWebhookHostName { + h.gin.ServeHTTP(w, r) + return + } + + http.Error(w, "Virtual host not found", http.StatusNotFound) } type shimV1MessageHandler struct { @@ -126,6 +211,25 @@ func (h *shimV1StorageHandler) Load() (b []byte, err error) { return } +type compatV1ShimServer struct { + shim *CompatV1Shim + protobuf.UnimplementedPluginServer + protobuf.UnimplementedDisplayerServer + protobuf.UnimplementedConfigurerServer +} + +func (s *compatV1ShimServer) GetPluginInfo(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { + return &protobuf.Info{ + Version: s.shim.pluginInfo.Version, + Author: s.shim.pluginInfo.Author, + Name: s.shim.pluginInfo.Name, + Website: s.shim.pluginInfo.Website, + Description: s.shim.pluginInfo.Description, + License: s.shim.pluginInfo.License, + ModulePath: s.shim.pluginInfo.ModulePath, + }, nil +} + func (s *compatV1ShimServer) SetEnable(ctx context.Context, req *protobuf.SetEnableRequest) (*emptypb.Empty, error) { s.shim.mu.RLock() instance, ok := s.shim.instances[req.User.Id] @@ -287,104 +391,3 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, return nil } - -func NewPluginRpc(compatV1 *CompatV1, cliArgs []string) (*CompatV1Shim, error) { - pluginInfo := compatV1.GetPluginInfo() - tlsName := BuildPluginTLSName(purposePluginRPC, pluginInfo.Name) - - cliFlags, err := ParsePluginCLIFlags(cliArgs) - if err != nil { - log.Fatalf("Failed to parse CLI flags: %v", err) - } - rootCAs := x509.NewCertPool() - caCert, err := x509.ParseCertificate(cliFlags.CAData) - if err != nil { - return nil, err - } - rootCAs.AddCert(caCert) - - leafCert, err := x509.ParseCertificate(cliFlags.CertData) - if err != nil { - return nil, err - } - - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{ - { - Certificate: [][]byte{cliFlags.CertData}, - PrivateKey: cliFlags.KeyData, - Leaf: leafCert, - }, - { - Certificate: [][]byte{caCert.Raw}, - }, - }, - RootCAs: rootCAs, - ServerName: tlsName, - ClientAuth: tls.RequireAndVerifyClientCert, - ClientCAs: rootCAs, - } - - rpcServer := grpc.NewServer() - - gin := gin.Default() - - self := &CompatV1Shim{ - mu: &sync.RWMutex{}, - instances: make(map[uint64]papiv1.Plugin), - compatV1: compatV1, - gin: gin, - pluginServer: rpcServer, - pluginInfo: pluginInfo, - } - - selfServer := &compatV1ShimServer{ - shim: self, - } - - protobuf.RegisterPluginServer(rpcServer, selfServer) - protobuf.RegisterDisplayerServer(rpcServer, selfServer) - protobuf.RegisterConfigurerServer(rpcServer, selfServer) - - protocols := new(http.Protocols) - protocols.SetHTTP1(true) - protocols.SetHTTP2(true) - self.Server = http.Server{ - Handler: self, - TLSConfig: tlsConfig, - Protocols: protocols, - } - - return self, nil -} - -func (h *CompatV1Shim) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.TLS == nil { - http.Error(w, "Must use TLS", http.StatusUpgradeRequired) - return - } - - pluginRpcHostName := BuildPluginTLSName(purposePluginRPC, h.pluginInfo.ModulePath) - - if r.TLS.ServerName == pluginRpcHostName { - if r.ProtoMajor != 2 { - http.Error(w, "Must use HTTP/2", http.StatusHTTPVersionNotSupported) - return - } - if !strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") { - http.Error(w, "Must use application/grpc content type", http.StatusUnsupportedMediaType) - return - } - h.pluginServer.ServeHTTP(w, r) - - return - } - - pluginWebhookHostName := BuildPluginTLSName(purposePluginWebhook, h.pluginInfo.ModulePath) - if r.TLS.ServerName == pluginWebhookHostName { - h.gin.ServeHTTP(w, r) - return - } - - http.Error(w, "Virtual host not found", http.StatusNotFound) -} From 4cf2383dd29396aa63febd6abdcf3b630baf674c Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 16:59:23 -0500 Subject: [PATCH 14/37] protobuf comments Signed-off-by: eternal-flame-AD --- v2/generate.go | 2 +- v2/generated/protobuf/config_grpc.pb.go | 4 + v2/generated/protobuf/display_grpc.pb.go | 4 + v2/generated/protobuf/meta.pb.go | 19 +-- v2/generated/protobuf/meta_grpc.pb.go | 41 +---- v2/generated/protobuf/server_events.pb.go | 158 ++++++++++++++++++ .../protobuf/server_events_grpc.pb.go | 119 +++++++++++++ v2/protobuf/config.proto | 1 + v2/protobuf/display.proto | 1 + v2/protobuf/meta.proto | 5 +- v2/protobuf/server_events.proto | 16 ++ 11 files changed, 321 insertions(+), 49 deletions(-) create mode 100644 v2/generated/protobuf/server_events.pb.go create mode 100644 v2/generated/protobuf/server_events_grpc.pb.go create mode 100644 v2/protobuf/server_events.proto diff --git a/v2/generate.go b/v2/generate.go index 90cad6a..f9d33fd 100644 --- a/v2/generate.go +++ b/v2/generate.go @@ -1,3 +1,3 @@ package plugin -//go:generate protoc -Iprotobuf --go_out=./generated/protobuf --go_opt=paths=source_relative --go-grpc_out=./generated/protobuf --go-grpc_opt=paths=source_relative ./protobuf/meta.proto ./protobuf/config.proto ./protobuf/display.proto +//go:generate protoc -Iprotobuf --go_out=./generated/protobuf --go_opt=paths=source_relative --go-grpc_out=./generated/protobuf --go-grpc_opt=paths=source_relative ./protobuf/meta.proto ./protobuf/config.proto ./protobuf/display.proto ./protobuf/server_events.proto diff --git a/v2/generated/protobuf/config_grpc.pb.go b/v2/generated/protobuf/config_grpc.pb.go index ff40f72..b0cb128 100644 --- a/v2/generated/protobuf/config_grpc.pb.go +++ b/v2/generated/protobuf/config_grpc.pb.go @@ -26,6 +26,8 @@ const ( // ConfigurerClient is the client API for Configurer 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. +// +// A service that allows plugins to be configured through the Gotify server. type ConfigurerClient interface { DefaultConfig(ctx context.Context, in *DefaultConfigRequest, opts ...grpc.CallOption) (*Config, error) ValidateAndSetConfig(ctx context.Context, in *ValidateAndSetConfigRequest, opts ...grpc.CallOption) (*ValidateAndSetConfigResponse, error) @@ -62,6 +64,8 @@ func (c *configurerClient) ValidateAndSetConfig(ctx context.Context, in *Validat // ConfigurerServer is the server API for Configurer service. // All implementations must embed UnimplementedConfigurerServer // for forward compatibility. +// +// A service that allows plugins to be configured through the Gotify server. type ConfigurerServer interface { DefaultConfig(context.Context, *DefaultConfigRequest) (*Config, error) ValidateAndSetConfig(context.Context, *ValidateAndSetConfigRequest) (*ValidateAndSetConfigResponse, error) diff --git a/v2/generated/protobuf/display_grpc.pb.go b/v2/generated/protobuf/display_grpc.pb.go index 91bb439..30bf8ca 100644 --- a/v2/generated/protobuf/display_grpc.pb.go +++ b/v2/generated/protobuf/display_grpc.pb.go @@ -25,6 +25,8 @@ const ( // DisplayerClient is the client API for Displayer 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. +// +// A service that allows plugins to display content to the user. type DisplayerClient interface { Display(ctx context.Context, in *DisplayRequest, opts ...grpc.CallOption) (*DisplayResponse, error) } @@ -50,6 +52,8 @@ func (c *displayerClient) Display(ctx context.Context, in *DisplayRequest, opts // DisplayerServer is the server API for Displayer service. // All implementations must embed UnimplementedDisplayerServer // for forward compatibility. +// +// A service that allows plugins to display content to the user. type DisplayerServer interface { Display(context.Context, *DisplayRequest) (*DisplayResponse, error) mustEmbedUnimplementedDisplayerServer() diff --git a/v2/generated/protobuf/meta.pb.go b/v2/generated/protobuf/meta.pb.go index 9ac9ba8..f79b3ea 100644 --- a/v2/generated/protobuf/meta.pb.go +++ b/v2/generated/protobuf/meta.pb.go @@ -907,11 +907,10 @@ const file_meta_proto_rawDesc = "" + "\n" + "CONFIGURER\x10\x02\x12\f\n" + "\bSTORAGER\x10\x03\x12\r\n" + - "\tWEBHOOKER\x10\x042\xe3\x01\n" + + "\tWEBHOOKER\x10\x042\xac\x01\n" + "\x06Plugin\x12.\n" + "\rGetPluginInfo\x12\x16.google.protobuf.Empty\x1a\x05.Info\x126\n" + - "\tSetEnable\x12\x11.SetEnableRequest\x1a\x16.google.protobuf.Empty\x125\n" + - "\vUserUpdates\x12\f.UserContext\x1a\x16.google.protobuf.Empty(\x01\x12:\n" + + "\tSetEnable\x12\x11.SetEnableRequest\x1a\x16.google.protobuf.Empty\x12:\n" + "\x0fRunUserInstance\x12\x14.UserInstanceRequest\x1a\x0f.InstanceUpdate0\x01B\x16Z\x14./generated/protobufb\x06proto3" var ( @@ -958,14 +957,12 @@ var file_meta_proto_depIdxs = []int32{ 6, // 9: Message.ExtrasEntry.value:type_name -> ExtrasValue 14, // 10: Plugin.GetPluginInfo:input_type -> google.protobuf.Empty 8, // 11: Plugin.SetEnable:input_type -> SetEnableRequest - 2, // 12: Plugin.UserUpdates:input_type -> UserContext - 11, // 13: Plugin.RunUserInstance:input_type -> UserInstanceRequest - 5, // 14: Plugin.GetPluginInfo:output_type -> Info - 14, // 15: Plugin.SetEnable:output_type -> google.protobuf.Empty - 14, // 16: Plugin.UserUpdates:output_type -> google.protobuf.Empty - 10, // 17: Plugin.RunUserInstance:output_type -> InstanceUpdate - 14, // [14:18] is the sub-list for method output_type - 10, // [10:14] is the sub-list for method input_type + 11, // 12: Plugin.RunUserInstance:input_type -> UserInstanceRequest + 5, // 13: Plugin.GetPluginInfo:output_type -> Info + 14, // 14: Plugin.SetEnable:output_type -> google.protobuf.Empty + 10, // 15: Plugin.RunUserInstance:output_type -> InstanceUpdate + 13, // [13:16] is the sub-list for method output_type + 10, // [10:13] is the sub-list for method input_type 10, // [10:10] is the sub-list for extension type_name 10, // [10:10] is the sub-list for extension extendee 0, // [0:10] is the sub-list for field type_name diff --git a/v2/generated/protobuf/meta_grpc.pb.go b/v2/generated/protobuf/meta_grpc.pb.go index 8fa9f39..000fe7a 100644 --- a/v2/generated/protobuf/meta_grpc.pb.go +++ b/v2/generated/protobuf/meta_grpc.pb.go @@ -22,20 +22,20 @@ const _ = grpc.SupportPackageIsVersion9 const ( Plugin_GetPluginInfo_FullMethodName = "/Plugin/GetPluginInfo" Plugin_SetEnable_FullMethodName = "/Plugin/SetEnable" - Plugin_UserUpdates_FullMethodName = "/Plugin/UserUpdates" Plugin_RunUserInstance_FullMethodName = "/Plugin/RunUserInstance" ) // PluginClient is the client API for Plugin 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. +// +// The base plugin service, which includes a plugin metadata endpoint, a per-user master switch, +// and a user instance stream. type PluginClient interface { // get the plugin info GetPluginInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) // set the enable state of a plugin instance SetEnable(ctx context.Context, in *SetEnableRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) - // updates to user information - UserUpdates(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[UserContext, emptypb.Empty], error) // run a user instance RunUserInstance(ctx context.Context, in *UserInstanceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[InstanceUpdate], error) } @@ -68,22 +68,9 @@ func (c *pluginClient) SetEnable(ctx context.Context, in *SetEnableRequest, opts return out, nil } -func (c *pluginClient) UserUpdates(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[UserContext, emptypb.Empty], error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &Plugin_ServiceDesc.Streams[0], Plugin_UserUpdates_FullMethodName, cOpts...) - if err != nil { - return nil, err - } - x := &grpc.GenericClientStream[UserContext, emptypb.Empty]{ClientStream: stream} - return x, nil -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type Plugin_UserUpdatesClient = grpc.ClientStreamingClient[UserContext, emptypb.Empty] - func (c *pluginClient) RunUserInstance(ctx context.Context, in *UserInstanceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[InstanceUpdate], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &Plugin_ServiceDesc.Streams[1], Plugin_RunUserInstance_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &Plugin_ServiceDesc.Streams[0], Plugin_RunUserInstance_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -103,13 +90,14 @@ type Plugin_RunUserInstanceClient = grpc.ServerStreamingClient[InstanceUpdate] // PluginServer is the server API for Plugin service. // All implementations must embed UnimplementedPluginServer // for forward compatibility. +// +// The base plugin service, which includes a plugin metadata endpoint, a per-user master switch, +// and a user instance stream. type PluginServer interface { // get the plugin info GetPluginInfo(context.Context, *emptypb.Empty) (*Info, error) // set the enable state of a plugin instance SetEnable(context.Context, *SetEnableRequest) (*emptypb.Empty, error) - // updates to user information - UserUpdates(grpc.ClientStreamingServer[UserContext, emptypb.Empty]) error // run a user instance RunUserInstance(*UserInstanceRequest, grpc.ServerStreamingServer[InstanceUpdate]) error mustEmbedUnimplementedPluginServer() @@ -128,9 +116,6 @@ func (UnimplementedPluginServer) GetPluginInfo(context.Context, *emptypb.Empty) func (UnimplementedPluginServer) SetEnable(context.Context, *SetEnableRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method SetEnable not implemented") } -func (UnimplementedPluginServer) UserUpdates(grpc.ClientStreamingServer[UserContext, emptypb.Empty]) error { - return status.Errorf(codes.Unimplemented, "method UserUpdates not implemented") -} func (UnimplementedPluginServer) RunUserInstance(*UserInstanceRequest, grpc.ServerStreamingServer[InstanceUpdate]) error { return status.Errorf(codes.Unimplemented, "method RunUserInstance not implemented") } @@ -191,13 +176,6 @@ func _Plugin_SetEnable_Handler(srv interface{}, ctx context.Context, dec func(in return interceptor(ctx, in, info, handler) } -func _Plugin_UserUpdates_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(PluginServer).UserUpdates(&grpc.GenericServerStream[UserContext, emptypb.Empty]{ServerStream: stream}) -} - -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type Plugin_UserUpdatesServer = grpc.ClientStreamingServer[UserContext, emptypb.Empty] - func _Plugin_RunUserInstance_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(UserInstanceRequest) if err := stream.RecvMsg(m); err != nil { @@ -226,11 +204,6 @@ var Plugin_ServiceDesc = grpc.ServiceDesc{ }, }, Streams: []grpc.StreamDesc{ - { - StreamName: "UserUpdates", - Handler: _Plugin_UserUpdates_Handler, - ClientStreams: true, - }, { StreamName: "RunUserInstance", Handler: _Plugin_RunUserInstance_Handler, diff --git a/v2/generated/protobuf/server_events.pb.go b/v2/generated/protobuf/server_events.pb.go new file mode 100644 index 0000000..a67fc6f --- /dev/null +++ b/v2/generated/protobuf/server_events.pb.go @@ -0,0 +1,158 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc v6.31.1 +// source: server_events.proto + +package protobuf + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +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 ServerEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Event: + // + // *ServerEvent_User + Event isServerEvent_Event `protobuf_oneof:"event"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerEvent) Reset() { + *x = ServerEvent{} + mi := &file_server_events_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerEvent) ProtoMessage() {} + +func (x *ServerEvent) ProtoReflect() protoreflect.Message { + mi := &file_server_events_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerEvent.ProtoReflect.Descriptor instead. +func (*ServerEvent) Descriptor() ([]byte, []int) { + return file_server_events_proto_rawDescGZIP(), []int{0} +} + +func (x *ServerEvent) GetEvent() isServerEvent_Event { + if x != nil { + return x.Event + } + return nil +} + +func (x *ServerEvent) GetUser() *UserContext { + if x != nil { + if x, ok := x.Event.(*ServerEvent_User); ok { + return x.User + } + } + return nil +} + +type isServerEvent_Event interface { + isServerEvent_Event() +} + +type ServerEvent_User struct { + User *UserContext `protobuf:"bytes,1,opt,name=user,proto3,oneof"` +} + +func (*ServerEvent_User) isServerEvent_Event() {} + +var File_server_events_proto protoreflect.FileDescriptor + +const file_server_events_proto_rawDesc = "" + + "\n" + + "\x13server_events.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\n" + + "meta.proto\":\n" + + "\vServerEvent\x12\"\n" + + "\x04user\x18\x01 \x01(\v2\f.UserContextH\x00R\x04userB\a\n" + + "\x05event2M\n" + + "\x13ServerEventReceiver\x126\n" + + "\fServerEvents\x12\f.ServerEvent\x1a\x16.google.protobuf.Empty(\x01B\x16Z\x14./generated/protobufb\x06proto3" + +var ( + file_server_events_proto_rawDescOnce sync.Once + file_server_events_proto_rawDescData []byte +) + +func file_server_events_proto_rawDescGZIP() []byte { + file_server_events_proto_rawDescOnce.Do(func() { + file_server_events_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_server_events_proto_rawDesc), len(file_server_events_proto_rawDesc))) + }) + return file_server_events_proto_rawDescData +} + +var file_server_events_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_server_events_proto_goTypes = []any{ + (*ServerEvent)(nil), // 0: ServerEvent + (*UserContext)(nil), // 1: UserContext + (*emptypb.Empty)(nil), // 2: google.protobuf.Empty +} +var file_server_events_proto_depIdxs = []int32{ + 1, // 0: ServerEvent.user:type_name -> UserContext + 0, // 1: ServerEventReceiver.ServerEvents:input_type -> ServerEvent + 2, // 2: ServerEventReceiver.ServerEvents:output_type -> google.protobuf.Empty + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] 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_server_events_proto_init() } +func file_server_events_proto_init() { + if File_server_events_proto != nil { + return + } + file_meta_proto_init() + file_server_events_proto_msgTypes[0].OneofWrappers = []any{ + (*ServerEvent_User)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_server_events_proto_rawDesc), len(file_server_events_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_server_events_proto_goTypes, + DependencyIndexes: file_server_events_proto_depIdxs, + MessageInfos: file_server_events_proto_msgTypes, + }.Build() + File_server_events_proto = out.File + file_server_events_proto_goTypes = nil + file_server_events_proto_depIdxs = nil +} diff --git a/v2/generated/protobuf/server_events_grpc.pb.go b/v2/generated/protobuf/server_events_grpc.pb.go new file mode 100644 index 0000000..9a88b41 --- /dev/null +++ b/v2/generated/protobuf/server_events_grpc.pb.go @@ -0,0 +1,119 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.31.1 +// source: server_events.proto + +package protobuf + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// 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.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ServerEventReceiver_ServerEvents_FullMethodName = "/ServerEventReceiver/ServerEvents" +) + +// ServerEventReceiverClient is the client API for ServerEventReceiver 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. +// +// An optional RPC service that allows plugins to accept updates from the server. +type ServerEventReceiverClient interface { + ServerEvents(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[ServerEvent, emptypb.Empty], error) +} + +type serverEventReceiverClient struct { + cc grpc.ClientConnInterface +} + +func NewServerEventReceiverClient(cc grpc.ClientConnInterface) ServerEventReceiverClient { + return &serverEventReceiverClient{cc} +} + +func (c *serverEventReceiverClient) ServerEvents(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[ServerEvent, emptypb.Empty], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &ServerEventReceiver_ServiceDesc.Streams[0], ServerEventReceiver_ServerEvents_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[ServerEvent, emptypb.Empty]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ServerEventReceiver_ServerEventsClient = grpc.ClientStreamingClient[ServerEvent, emptypb.Empty] + +// ServerEventReceiverServer is the server API for ServerEventReceiver service. +// All implementations must embed UnimplementedServerEventReceiverServer +// for forward compatibility. +// +// An optional RPC service that allows plugins to accept updates from the server. +type ServerEventReceiverServer interface { + ServerEvents(grpc.ClientStreamingServer[ServerEvent, emptypb.Empty]) error + mustEmbedUnimplementedServerEventReceiverServer() +} + +// UnimplementedServerEventReceiverServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedServerEventReceiverServer struct{} + +func (UnimplementedServerEventReceiverServer) ServerEvents(grpc.ClientStreamingServer[ServerEvent, emptypb.Empty]) error { + return status.Errorf(codes.Unimplemented, "method ServerEvents not implemented") +} +func (UnimplementedServerEventReceiverServer) mustEmbedUnimplementedServerEventReceiverServer() {} +func (UnimplementedServerEventReceiverServer) testEmbeddedByValue() {} + +// UnsafeServerEventReceiverServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ServerEventReceiverServer will +// result in compilation errors. +type UnsafeServerEventReceiverServer interface { + mustEmbedUnimplementedServerEventReceiverServer() +} + +func RegisterServerEventReceiverServer(s grpc.ServiceRegistrar, srv ServerEventReceiverServer) { + // If the following call pancis, it indicates UnimplementedServerEventReceiverServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ServerEventReceiver_ServiceDesc, srv) +} + +func _ServerEventReceiver_ServerEvents_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(ServerEventReceiverServer).ServerEvents(&grpc.GenericServerStream[ServerEvent, emptypb.Empty]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ServerEventReceiver_ServerEventsServer = grpc.ClientStreamingServer[ServerEvent, emptypb.Empty] + +// ServerEventReceiver_ServiceDesc is the grpc.ServiceDesc for ServerEventReceiver service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ServerEventReceiver_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ServerEventReceiver", + HandlerType: (*ServerEventReceiverServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "ServerEvents", + Handler: _ServerEventReceiver_ServerEvents_Handler, + ClientStreams: true, + }, + }, + Metadata: "server_events.proto", +} diff --git a/v2/protobuf/config.proto b/v2/protobuf/config.proto index 4f5d7bb..537cb84 100644 --- a/v2/protobuf/config.proto +++ b/v2/protobuf/config.proto @@ -25,6 +25,7 @@ message ValidateAndSetConfigResponse { } } +// A service that allows plugins to be configured through the Gotify server. service Configurer { rpc DefaultConfig(DefaultConfigRequest) returns (Config); rpc ValidateAndSetConfig(ValidateAndSetConfigRequest) returns (ValidateAndSetConfigResponse); diff --git a/v2/protobuf/display.proto b/v2/protobuf/display.proto index 39a2df5..cc4a07f 100644 --- a/v2/protobuf/display.proto +++ b/v2/protobuf/display.proto @@ -12,6 +12,7 @@ message DisplayResponse { string display = 1; } +// A service that allows plugins to display content to the user. service Displayer { rpc Display(DisplayRequest) returns (DisplayResponse); } diff --git a/v2/protobuf/meta.proto b/v2/protobuf/meta.proto index fd2fd1d..5ff62a4 100644 --- a/v2/protobuf/meta.proto +++ b/v2/protobuf/meta.proto @@ -93,6 +93,8 @@ message UserInstanceRequest { optional bytes storage = 5; } +// The base plugin service, which includes a plugin metadata endpoint, a per-user master switch, +// and a user instance stream. service Plugin { // get the plugin info rpc GetPluginInfo(google.protobuf.Empty) returns (Info); @@ -100,9 +102,6 @@ service Plugin { // set the enable state of a plugin instance rpc SetEnable(SetEnableRequest) returns (google.protobuf.Empty); - // updates to user information - rpc UserUpdates(stream UserContext) returns (google.protobuf.Empty); - // run a user instance rpc RunUserInstance(UserInstanceRequest) returns (stream InstanceUpdate); } diff --git a/v2/protobuf/server_events.proto b/v2/protobuf/server_events.proto new file mode 100644 index 0000000..5e1c227 --- /dev/null +++ b/v2/protobuf/server_events.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; +import "google/protobuf/empty.proto"; +import "meta.proto"; + +option go_package = "./generated/protobuf"; + +message ServerEvent { + oneof event { + UserContext user = 1; + } +} + +// An optional RPC service that allows plugins to accept updates from the server. +service ServerEventReceiver { + rpc ServerEvents(stream ServerEvent) returns (google.protobuf.Empty); +} From d6c86da8a71a48f95ab2b9bcd1213d56ff69f6d3 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 16:59:56 -0500 Subject: [PATCH 15/37] go mod tidy Signed-off-by: eternal-flame-AD --- v2/go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v2/go.mod b/v2/go.mod index c8a0b83..3073bd1 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -3,14 +3,15 @@ module github.com/gotify/plugin-api/v2 go 1.24.5 require ( + github.com/gin-gonic/gin v1.3.0 github.com/gotify/plugin-api v1.0.0 google.golang.org/grpc v1.74.2 google.golang.org/protobuf v1.36.7 + gopkg.in/yaml.v2 v2.2.2 ) require ( github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect - github.com/gin-gonic/gin v1.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/json-iterator/go v1.1.5 // indirect github.com/mattn/go-isatty v0.0.4 // indirect @@ -22,5 +23,4 @@ require ( golang.org/x/text v0.25.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect gopkg.in/go-playground/validator.v8 v8.18.2 // indirect - gopkg.in/yaml.v2 v2.2.2 // indirect ) From 4e137244221001180011b64ab74fba10e3bf4256 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 17:10:31 -0500 Subject: [PATCH 16/37] more protobuf docs Signed-off-by: eternal-flame-AD --- v2/generated/protobuf/config.pb.go | 46 ++-- v2/generated/protobuf/display.pb.go | 50 +++- v2/generated/protobuf/meta.pb.go | 309 ++++++++++------------ v2/generated/protobuf/server_events.pb.go | 1 + v2/protobuf/config.proto | 9 +- v2/protobuf/display.proto | 7 +- v2/protobuf/meta.proto | 22 +- v2/protobuf/server_events.proto | 1 + v2/shim_v1.go | 94 +++++-- 9 files changed, 296 insertions(+), 243 deletions(-) diff --git a/v2/generated/protobuf/config.pb.go b/v2/generated/protobuf/config.pb.go index 8fe4a50..acee500 100644 --- a/v2/generated/protobuf/config.pb.go +++ b/v2/generated/protobuf/config.pb.go @@ -23,8 +23,9 @@ const ( ) type Config struct { - state protoimpl.MessageState `protogen:"open.v1"` - Config string `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // The YAML configuration data. + Config string `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -67,8 +68,9 @@ func (x *Config) GetConfig() string { } type DefaultConfigRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // The user context the configuration belongs to. + User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -111,9 +113,11 @@ func (x *DefaultConfigRequest) GetUser() *UserContext { } type ValidateAndSetConfigRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` - Config *Config `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // The user context the configuration belongs to. + User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + // The YAML configuration data. + Config *Config `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -164,10 +168,12 @@ func (x *ValidateAndSetConfigRequest) GetConfig() *Config { type ValidateAndSetConfigResponse struct { state protoimpl.MessageState `protogen:"open.v1"` + // The response to the request. + // // Types that are valid to be assigned to Response: // // *ValidateAndSetConfigResponse_Success - // *ValidateAndSetConfigResponse_Error + // *ValidateAndSetConfigResponse_ValidationError Response isValidateAndSetConfigResponse_Response `protobuf_oneof:"response"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -219,10 +225,10 @@ func (x *ValidateAndSetConfigResponse) GetSuccess() *emptypb.Empty { return nil } -func (x *ValidateAndSetConfigResponse) GetError() *Error { +func (x *ValidateAndSetConfigResponse) GetValidationError() *Error { if x != nil { - if x, ok := x.Response.(*ValidateAndSetConfigResponse_Error); ok { - return x.Error + if x, ok := x.Response.(*ValidateAndSetConfigResponse_ValidationError); ok { + return x.ValidationError } } return nil @@ -233,16 +239,18 @@ type isValidateAndSetConfigResponse_Response interface { } type ValidateAndSetConfigResponse_Success struct { + // The success response. Success *emptypb.Empty `protobuf:"bytes,1,opt,name=success,proto3,oneof"` } -type ValidateAndSetConfigResponse_Error struct { - Error *Error `protobuf:"bytes,2,opt,name=error,proto3,oneof"` +type ValidateAndSetConfigResponse_ValidationError struct { + // The validation error response. + ValidationError *Error `protobuf:"bytes,2,opt,name=validation_error,json=validationError,proto3,oneof"` } func (*ValidateAndSetConfigResponse_Success) isValidateAndSetConfigResponse_Response() {} -func (*ValidateAndSetConfigResponse_Error) isValidateAndSetConfigResponse_Response() {} +func (*ValidateAndSetConfigResponse_ValidationError) isValidateAndSetConfigResponse_Response() {} var File_config_proto protoreflect.FileDescriptor @@ -256,10 +264,10 @@ const file_config_proto_rawDesc = "" + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\"`\n" + "\x1bValidateAndSetConfigRequest\x12 \n" + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x1f\n" + - "\x06config\x18\x02 \x01(\v2\a.ConfigR\x06config\"~\n" + + "\x06config\x18\x02 \x01(\v2\a.ConfigR\x06config\"\x93\x01\n" + "\x1cValidateAndSetConfigResponse\x122\n" + - "\asuccess\x18\x01 \x01(\v2\x16.google.protobuf.EmptyH\x00R\asuccess\x12\x1e\n" + - "\x05error\x18\x02 \x01(\v2\x06.ErrorH\x00R\x05errorB\n" + + "\asuccess\x18\x01 \x01(\v2\x16.google.protobuf.EmptyH\x00R\asuccess\x123\n" + + "\x10validation_error\x18\x02 \x01(\v2\x06.ErrorH\x00R\x0fvalidationErrorB\n" + "\n" + "\bresponse2\x92\x01\n" + "\n" + @@ -294,7 +302,7 @@ var file_config_proto_depIdxs = []int32{ 4, // 1: ValidateAndSetConfigRequest.user:type_name -> UserContext 0, // 2: ValidateAndSetConfigRequest.config:type_name -> Config 5, // 3: ValidateAndSetConfigResponse.success:type_name -> google.protobuf.Empty - 6, // 4: ValidateAndSetConfigResponse.error:type_name -> Error + 6, // 4: ValidateAndSetConfigResponse.validation_error:type_name -> Error 1, // 5: Configurer.DefaultConfig:input_type -> DefaultConfigRequest 2, // 6: Configurer.ValidateAndSetConfig:input_type -> ValidateAndSetConfigRequest 0, // 7: Configurer.DefaultConfig:output_type -> Config @@ -314,7 +322,7 @@ func file_config_proto_init() { file_meta_proto_init() file_config_proto_msgTypes[3].OneofWrappers = []any{ (*ValidateAndSetConfigResponse_Success)(nil), - (*ValidateAndSetConfigResponse_Error)(nil), + (*ValidateAndSetConfigResponse_ValidationError)(nil), } type x struct{} out := protoimpl.TypeBuilder{ diff --git a/v2/generated/protobuf/display.pb.go b/v2/generated/protobuf/display.pb.go index 438b2dc..fd80206 100644 --- a/v2/generated/protobuf/display.pb.go +++ b/v2/generated/protobuf/display.pb.go @@ -22,9 +22,11 @@ const ( ) type DisplayRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` - Location string `protobuf:"bytes,2,opt,name=location,proto3" json:"location,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // The user context the display belongs to. + User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + // The base URL of the plugin control panel. + Location string `protobuf:"bytes,2,opt,name=location,proto3" json:"location,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -74,8 +76,11 @@ func (x *DisplayRequest) GetLocation() string { } type DisplayResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Display string `protobuf:"bytes,1,opt,name=display,proto3" json:"display,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Response: + // + // *DisplayResponse_Markdown + Response isDisplayResponse_Response `protobuf_oneof:"response"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -110,13 +115,33 @@ func (*DisplayResponse) Descriptor() ([]byte, []int) { return file_display_proto_rawDescGZIP(), []int{1} } -func (x *DisplayResponse) GetDisplay() string { +func (x *DisplayResponse) GetResponse() isDisplayResponse_Response { if x != nil { - return x.Display + return x.Response + } + return nil +} + +func (x *DisplayResponse) GetMarkdown() string { + if x != nil { + if x, ok := x.Response.(*DisplayResponse_Markdown); ok { + return x.Markdown + } } return "" } +type isDisplayResponse_Response interface { + isDisplayResponse_Response() +} + +type DisplayResponse_Markdown struct { + // The display response in markdown format. + Markdown string `protobuf:"bytes,1,opt,name=markdown,proto3,oneof"` +} + +func (*DisplayResponse_Markdown) isDisplayResponse_Response() {} + var File_display_proto protoreflect.FileDescriptor const file_display_proto_rawDesc = "" + @@ -125,9 +150,11 @@ const file_display_proto_rawDesc = "" + "meta.proto\"N\n" + "\x0eDisplayRequest\x12 \n" + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x1a\n" + - "\blocation\x18\x02 \x01(\tR\blocation\"+\n" + - "\x0fDisplayResponse\x12\x18\n" + - "\adisplay\x18\x01 \x01(\tR\adisplay29\n" + + "\blocation\x18\x02 \x01(\tR\blocation\";\n" + + "\x0fDisplayResponse\x12\x1c\n" + + "\bmarkdown\x18\x01 \x01(\tH\x00R\bmarkdownB\n" + + "\n" + + "\bresponse29\n" + "\tDisplayer\x12,\n" + "\aDisplay\x12\x0f.DisplayRequest\x1a\x10.DisplayResponseB\x16Z\x14./generated/protobufb\x06proto3" @@ -166,6 +193,9 @@ func file_display_proto_init() { return } file_meta_proto_init() + file_display_proto_msgTypes[1].OneofWrappers = []any{ + (*DisplayResponse_Markdown)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/v2/generated/protobuf/meta.pb.go b/v2/generated/protobuf/meta.pb.go index f79b3ea..2089bc0 100644 --- a/v2/generated/protobuf/meta.pb.go +++ b/v2/generated/protobuf/meta.pb.go @@ -258,66 +258,6 @@ func (x *Capabilities) GetWebhooker() uint32 { return 0 } -type ServerVersionInfo struct { - state protoimpl.MessageState `protogen:"open.v1"` - Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` - Commit string `protobuf:"bytes,2,opt,name=commit,proto3" json:"commit,omitempty"` - BuildDate string `protobuf:"bytes,3,opt,name=buildDate,proto3" json:"buildDate,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ServerVersionInfo) Reset() { - *x = ServerVersionInfo{} - mi := &file_meta_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ServerVersionInfo) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ServerVersionInfo) ProtoMessage() {} - -func (x *ServerVersionInfo) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ServerVersionInfo.ProtoReflect.Descriptor instead. -func (*ServerVersionInfo) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{3} -} - -func (x *ServerVersionInfo) GetVersion() string { - if x != nil { - return x.Version - } - return "" -} - -func (x *ServerVersionInfo) GetCommit() string { - if x != nil { - return x.Commit - } - return "" -} - -func (x *ServerVersionInfo) GetBuildDate() string { - if x != nil { - return x.BuildDate - } - return "" -} - type Info struct { state protoimpl.MessageState `protogen:"open.v1"` Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` @@ -334,7 +274,7 @@ type Info struct { func (x *Info) Reset() { *x = Info{} - mi := &file_meta_proto_msgTypes[4] + mi := &file_meta_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -346,7 +286,7 @@ func (x *Info) String() string { func (*Info) ProtoMessage() {} func (x *Info) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[4] + mi := &file_meta_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -359,7 +299,7 @@ func (x *Info) ProtoReflect() protoreflect.Message { // Deprecated: Use Info.ProtoReflect.Descriptor instead. func (*Info) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{4} + return file_meta_proto_rawDescGZIP(), []int{3} } func (x *Info) GetVersion() string { @@ -430,7 +370,7 @@ type ExtrasValue struct { func (x *ExtrasValue) Reset() { *x = ExtrasValue{} - mi := &file_meta_proto_msgTypes[5] + mi := &file_meta_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -442,7 +382,7 @@ func (x *ExtrasValue) String() string { func (*ExtrasValue) ProtoMessage() {} func (x *ExtrasValue) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[5] + mi := &file_meta_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -455,7 +395,7 @@ func (x *ExtrasValue) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtrasValue.ProtoReflect.Descriptor instead. func (*ExtrasValue) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{5} + return file_meta_proto_rawDescGZIP(), []int{4} } func (x *ExtrasValue) GetValue() isExtrasValue_Value { @@ -496,7 +436,7 @@ type Message struct { func (x *Message) Reset() { *x = Message{} - mi := &file_meta_proto_msgTypes[6] + mi := &file_meta_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -508,7 +448,7 @@ func (x *Message) String() string { func (*Message) ProtoMessage() {} func (x *Message) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[6] + mi := &file_meta_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -521,7 +461,7 @@ func (x *Message) ProtoReflect() protoreflect.Message { // Deprecated: Use Message.ProtoReflect.Descriptor instead. func (*Message) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{6} + return file_meta_proto_rawDescGZIP(), []int{5} } func (x *Message) GetMessage() string { @@ -562,7 +502,7 @@ type SetEnableRequest struct { func (x *SetEnableRequest) Reset() { *x = SetEnableRequest{} - mi := &file_meta_proto_msgTypes[7] + mi := &file_meta_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -574,7 +514,7 @@ func (x *SetEnableRequest) String() string { func (*SetEnableRequest) ProtoMessage() {} func (x *SetEnableRequest) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[7] + mi := &file_meta_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -587,7 +527,7 @@ func (x *SetEnableRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetEnableRequest.ProtoReflect.Descriptor instead. func (*SetEnableRequest) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{7} + return file_meta_proto_rawDescGZIP(), []int{6} } func (x *SetEnableRequest) GetUser() *UserContext { @@ -604,55 +544,11 @@ func (x *SetEnableRequest) GetEnable() bool { return false } -type PluginCapabilities struct { - state protoimpl.MessageState `protogen:"open.v1"` - Capabilities []Capability `protobuf:"varint,1,rep,packed,name=capabilities,proto3,enum=Capability" json:"capabilities,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PluginCapabilities) Reset() { - *x = PluginCapabilities{} - mi := &file_meta_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PluginCapabilities) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PluginCapabilities) ProtoMessage() {} - -func (x *PluginCapabilities) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[8] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PluginCapabilities.ProtoReflect.Descriptor instead. -func (*PluginCapabilities) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{8} -} - -func (x *PluginCapabilities) GetCapabilities() []Capability { - if x != nil { - return x.Capabilities - } - return nil -} - type InstanceUpdate struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Update: // - // *InstanceUpdate_Capabilities + // *InstanceUpdate_Capable // *InstanceUpdate_Message // *InstanceUpdate_Storage Update isInstanceUpdate_Update `protobuf_oneof:"update"` @@ -662,7 +558,7 @@ type InstanceUpdate struct { func (x *InstanceUpdate) Reset() { *x = InstanceUpdate{} - mi := &file_meta_proto_msgTypes[9] + mi := &file_meta_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -674,7 +570,7 @@ func (x *InstanceUpdate) String() string { func (*InstanceUpdate) ProtoMessage() {} func (x *InstanceUpdate) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[9] + mi := &file_meta_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -687,7 +583,7 @@ func (x *InstanceUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use InstanceUpdate.ProtoReflect.Descriptor instead. func (*InstanceUpdate) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{9} + return file_meta_proto_rawDescGZIP(), []int{7} } func (x *InstanceUpdate) GetUpdate() isInstanceUpdate_Update { @@ -697,13 +593,13 @@ func (x *InstanceUpdate) GetUpdate() isInstanceUpdate_Update { return nil } -func (x *InstanceUpdate) GetCapabilities() *PluginCapabilities { +func (x *InstanceUpdate) GetCapable() Capability { if x != nil { - if x, ok := x.Update.(*InstanceUpdate_Capabilities); ok { - return x.Capabilities + if x, ok := x.Update.(*InstanceUpdate_Capable); ok { + return x.Capable } } - return nil + return Capability_DISPLAYER } func (x *InstanceUpdate) GetMessage() *Message { @@ -728,9 +624,9 @@ type isInstanceUpdate_Update interface { isInstanceUpdate_Update() } -type InstanceUpdate_Capabilities struct { - // which capabilities are displayed to the user - Capabilities *PluginCapabilities `protobuf:"bytes,1,opt,name=capabilities,proto3,oneof"` +type InstanceUpdate_Capable struct { + // enable support for a feature, must be one of the capabilities supported by the server + Capable Capability `protobuf:"varint,1,opt,name=capable,proto3,enum=Capability,oneof"` } type InstanceUpdate_Message struct { @@ -743,7 +639,7 @@ type InstanceUpdate_Storage struct { Storage []byte `protobuf:"bytes,3,opt,name=storage,proto3,oneof"` } -func (*InstanceUpdate_Capabilities) isInstanceUpdate_Update() {} +func (*InstanceUpdate_Capable) isInstanceUpdate_Update() {} func (*InstanceUpdate_Message) isInstanceUpdate_Update() {} @@ -767,7 +663,7 @@ type UserInstanceRequest struct { func (x *UserInstanceRequest) Reset() { *x = UserInstanceRequest{} - mi := &file_meta_proto_msgTypes[10] + mi := &file_meta_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -779,7 +675,7 @@ func (x *UserInstanceRequest) String() string { func (*UserInstanceRequest) ProtoMessage() {} func (x *UserInstanceRequest) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[10] + mi := &file_meta_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -792,7 +688,7 @@ func (x *UserInstanceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UserInstanceRequest.ProtoReflect.Descriptor instead. func (*UserInstanceRequest) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{10} + return file_meta_proto_rawDescGZIP(), []int{8} } func (x *UserInstanceRequest) GetServerVersion() *ServerVersionInfo { @@ -830,6 +726,75 @@ func (x *UserInstanceRequest) GetStorage() []byte { return nil } +type ServerVersionInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Commit string `protobuf:"bytes,2,opt,name=commit,proto3" json:"commit,omitempty"` + BuildDate string `protobuf:"bytes,3,opt,name=buildDate,proto3" json:"buildDate,omitempty"` + // supported capabilities of the gotify server itself + Capabilities []Capability `protobuf:"varint,4,rep,packed,name=capabilities,proto3,enum=Capability" json:"capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerVersionInfo) Reset() { + *x = ServerVersionInfo{} + mi := &file_meta_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerVersionInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerVersionInfo) ProtoMessage() {} + +func (x *ServerVersionInfo) ProtoReflect() protoreflect.Message { + mi := &file_meta_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerVersionInfo.ProtoReflect.Descriptor instead. +func (*ServerVersionInfo) Descriptor() ([]byte, []int) { + return file_meta_proto_rawDescGZIP(), []int{9} +} + +func (x *ServerVersionInfo) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *ServerVersionInfo) GetCommit() string { + if x != nil { + return x.Commit + } + return "" +} + +func (x *ServerVersionInfo) GetBuildDate() string { + if x != nil { + return x.BuildDate + } + return "" +} + +func (x *ServerVersionInfo) GetCapabilities() []Capability { + if x != nil { + return x.Capabilities + } + return nil +} + var File_meta_proto protoreflect.FileDescriptor const file_meta_proto_rawDesc = "" + @@ -854,11 +819,7 @@ const file_meta_proto_rawDesc = "" + "_DisplayerB\r\n" + "\v_ConfigurerB\f\n" + "\n" + - "_Webhooker\"c\n" + - "\x11ServerVersionInfo\x12\x18\n" + - "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + - "\x06commit\x18\x02 \x01(\tR\x06commit\x12\x1c\n" + - "\tbuildDate\x18\x03 \x01(\tR\tbuildDate\"\xf6\x01\n" + + "_Webhooker\"\xf6\x01\n" + "\x04Info\x12\x18\n" + "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + "\x06author\x18\x02 \x01(\tR\x06author\x12\x12\n" + @@ -882,11 +843,9 @@ const file_meta_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\v2\f.ExtrasValueR\x05value:\x028\x01\"L\n" + "\x10SetEnableRequest\x12 \n" + "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x16\n" + - "\x06enable\x18\x02 \x01(\bR\x06enable\"E\n" + - "\x12PluginCapabilities\x12/\n" + - "\fcapabilities\x18\x01 \x03(\x0e2\v.CapabilityR\fcapabilities\"\x97\x01\n" + - "\x0eInstanceUpdate\x129\n" + - "\fcapabilities\x18\x01 \x01(\v2\x13.PluginCapabilitiesH\x00R\fcapabilities\x12$\n" + + "\x06enable\x18\x02 \x01(\bR\x06enable\"\x85\x01\n" + + "\x0eInstanceUpdate\x12'\n" + + "\acapable\x18\x01 \x01(\x0e2\v.CapabilityH\x00R\acapable\x12$\n" + "\amessage\x18\x02 \x01(\v2\b.MessageH\x00R\amessage\x12\x1a\n" + "\astorage\x18\x03 \x01(\fH\x00R\astorageB\b\n" + "\x06update\"\x87\x02\n" + @@ -899,7 +858,12 @@ const file_meta_proto_rawDesc = "" + "\x10_webhookBasePathB\t\n" + "\a_configB\n" + "\n" + - "\b_storage*W\n" + + "\b_storage\"\x94\x01\n" + + "\x11ServerVersionInfo\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + + "\x06commit\x18\x02 \x01(\tR\x06commit\x12\x1c\n" + + "\tbuildDate\x18\x03 \x01(\tR\tbuildDate\x12/\n" + + "\fcapabilities\x18\x04 \x03(\x0e2\v.CapabilityR\fcapabilities*W\n" + "\n" + "Capability\x12\r\n" + "\tDISPLAYER\x10\x00\x12\r\n" + @@ -926,41 +890,40 @@ func file_meta_proto_rawDescGZIP() []byte { } var file_meta_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_meta_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_meta_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_meta_proto_goTypes = []any{ (Capability)(0), // 0: Capability (*Error)(nil), // 1: Error (*UserContext)(nil), // 2: UserContext (*Capabilities)(nil), // 3: Capabilities - (*ServerVersionInfo)(nil), // 4: ServerVersionInfo - (*Info)(nil), // 5: Info - (*ExtrasValue)(nil), // 6: ExtrasValue - (*Message)(nil), // 7: Message - (*SetEnableRequest)(nil), // 8: SetEnableRequest - (*PluginCapabilities)(nil), // 9: PluginCapabilities - (*InstanceUpdate)(nil), // 10: InstanceUpdate - (*UserInstanceRequest)(nil), // 11: UserInstanceRequest - nil, // 12: Message.ExtrasEntry - (*anypb.Any)(nil), // 13: google.protobuf.Any - (*emptypb.Empty)(nil), // 14: google.protobuf.Empty + (*Info)(nil), // 4: Info + (*ExtrasValue)(nil), // 5: ExtrasValue + (*Message)(nil), // 6: Message + (*SetEnableRequest)(nil), // 7: SetEnableRequest + (*InstanceUpdate)(nil), // 8: InstanceUpdate + (*UserInstanceRequest)(nil), // 9: UserInstanceRequest + (*ServerVersionInfo)(nil), // 10: ServerVersionInfo + nil, // 11: Message.ExtrasEntry + (*anypb.Any)(nil), // 12: google.protobuf.Any + (*emptypb.Empty)(nil), // 13: google.protobuf.Empty } var file_meta_proto_depIdxs = []int32{ - 13, // 0: Error.details:type_name -> google.protobuf.Any + 12, // 0: Error.details:type_name -> google.protobuf.Any 3, // 1: Info.capabilities:type_name -> Capabilities - 12, // 2: Message.extras:type_name -> Message.ExtrasEntry + 11, // 2: Message.extras:type_name -> Message.ExtrasEntry 2, // 3: SetEnableRequest.user:type_name -> UserContext - 0, // 4: PluginCapabilities.capabilities:type_name -> Capability - 9, // 5: InstanceUpdate.capabilities:type_name -> PluginCapabilities - 7, // 6: InstanceUpdate.message:type_name -> Message - 4, // 7: UserInstanceRequest.serverVersion:type_name -> ServerVersionInfo - 2, // 8: UserInstanceRequest.user:type_name -> UserContext - 6, // 9: Message.ExtrasEntry.value:type_name -> ExtrasValue - 14, // 10: Plugin.GetPluginInfo:input_type -> google.protobuf.Empty - 8, // 11: Plugin.SetEnable:input_type -> SetEnableRequest - 11, // 12: Plugin.RunUserInstance:input_type -> UserInstanceRequest - 5, // 13: Plugin.GetPluginInfo:output_type -> Info - 14, // 14: Plugin.SetEnable:output_type -> google.protobuf.Empty - 10, // 15: Plugin.RunUserInstance:output_type -> InstanceUpdate + 0, // 4: InstanceUpdate.capable:type_name -> Capability + 6, // 5: InstanceUpdate.message:type_name -> Message + 10, // 6: UserInstanceRequest.serverVersion:type_name -> ServerVersionInfo + 2, // 7: UserInstanceRequest.user:type_name -> UserContext + 0, // 8: ServerVersionInfo.capabilities:type_name -> Capability + 5, // 9: Message.ExtrasEntry.value:type_name -> ExtrasValue + 13, // 10: Plugin.GetPluginInfo:input_type -> google.protobuf.Empty + 7, // 11: Plugin.SetEnable:input_type -> SetEnableRequest + 9, // 12: Plugin.RunUserInstance:input_type -> UserInstanceRequest + 4, // 13: Plugin.GetPluginInfo:output_type -> Info + 13, // 14: Plugin.SetEnable:output_type -> google.protobuf.Empty + 8, // 15: Plugin.RunUserInstance:output_type -> InstanceUpdate 13, // [13:16] is the sub-list for method output_type 10, // [10:13] is the sub-list for method input_type 10, // [10:10] is the sub-list for extension type_name @@ -974,22 +937,22 @@ func file_meta_proto_init() { return } file_meta_proto_msgTypes[2].OneofWrappers = []any{} - file_meta_proto_msgTypes[5].OneofWrappers = []any{ + file_meta_proto_msgTypes[4].OneofWrappers = []any{ (*ExtrasValue_Json)(nil), } - file_meta_proto_msgTypes[9].OneofWrappers = []any{ - (*InstanceUpdate_Capabilities)(nil), + file_meta_proto_msgTypes[7].OneofWrappers = []any{ + (*InstanceUpdate_Capable)(nil), (*InstanceUpdate_Message)(nil), (*InstanceUpdate_Storage)(nil), } - file_meta_proto_msgTypes[10].OneofWrappers = []any{} + file_meta_proto_msgTypes[8].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_meta_proto_rawDesc), len(file_meta_proto_rawDesc)), NumEnums: 1, - NumMessages: 12, + NumMessages: 11, NumExtensions: 0, NumServices: 1, }, diff --git a/v2/generated/protobuf/server_events.pb.go b/v2/generated/protobuf/server_events.pb.go index a67fc6f..35c4634 100644 --- a/v2/generated/protobuf/server_events.pb.go +++ b/v2/generated/protobuf/server_events.pb.go @@ -83,6 +83,7 @@ type isServerEvent_Event interface { } type ServerEvent_User struct { + // A user has been created or updated. User *UserContext `protobuf:"bytes,1,opt,name=user,proto3,oneof"` } diff --git a/v2/protobuf/config.proto b/v2/protobuf/config.proto index 537cb84..c179462 100644 --- a/v2/protobuf/config.proto +++ b/v2/protobuf/config.proto @@ -6,22 +6,29 @@ import "meta.proto"; option go_package = "./generated/protobuf"; message Config { + // The YAML configuration data. string config = 1; } message DefaultConfigRequest { + // The user context the configuration belongs to. UserContext user = 1; } message ValidateAndSetConfigRequest { + // The user context the configuration belongs to. UserContext user = 1; + // The YAML configuration data. Config config = 2; } message ValidateAndSetConfigResponse { + // The response to the request. oneof response { + // The success response. google.protobuf.Empty success = 1; - Error error = 2; + // The validation error response. + Error validation_error = 2; } } diff --git a/v2/protobuf/display.proto b/v2/protobuf/display.proto index cc4a07f..0954f8c 100644 --- a/v2/protobuf/display.proto +++ b/v2/protobuf/display.proto @@ -4,12 +4,17 @@ import "meta.proto"; option go_package = "./generated/protobuf"; message DisplayRequest { + // The user context the display belongs to. UserContext user = 1; + // The base URL of the plugin control panel. string location = 2; } message DisplayResponse { - string display = 1; + oneof response { + // The display response in markdown format. + string markdown = 1; + } } // A service that allows plugins to display content to the user. diff --git a/v2/protobuf/meta.proto b/v2/protobuf/meta.proto index 5ff62a4..50cbd82 100644 --- a/v2/protobuf/meta.proto +++ b/v2/protobuf/meta.proto @@ -22,12 +22,6 @@ message Capabilities { optional uint32 Webhooker = 3; } -message ServerVersionInfo { - string version = 1; - string commit = 2; - string buildDate = 3; -} - message Info { string version = 1; string author = 2; @@ -65,14 +59,10 @@ enum Capability { WEBHOOKER = 4; } -message PluginCapabilities { - repeated Capability capabilities = 1; -} - message InstanceUpdate { oneof update { - // which capabilities are displayed to the user - PluginCapabilities capabilities = 1; + // enable support for a feature, must be one of the capabilities supported by the server + Capability capable = 1; // send a message to the user Message message = 2; // update persistent storage @@ -93,6 +83,14 @@ message UserInstanceRequest { optional bytes storage = 5; } +message ServerVersionInfo { + string version = 1; + string commit = 2; + string buildDate = 3; + // supported capabilities of the gotify server itself + repeated Capability capabilities = 4; +} + // The base plugin service, which includes a plugin metadata endpoint, a per-user master switch, // and a user instance stream. service Plugin { diff --git a/v2/protobuf/server_events.proto b/v2/protobuf/server_events.proto index 5e1c227..5656c48 100644 --- a/v2/protobuf/server_events.proto +++ b/v2/protobuf/server_events.proto @@ -6,6 +6,7 @@ option go_package = "./generated/protobuf"; message ServerEvent { oneof event { + // A user has been created or updated. UserContext user = 1; } } diff --git a/v2/shim_v1.go b/v2/shim_v1.go index ca8760d..d769cde 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -10,6 +10,7 @@ import ( "net/http" "net/url" "plugin" + "slices" "strings" "sync" @@ -257,7 +258,9 @@ func (s *compatV1ShimServer) Display(ctx context.Context, req *protobuf.DisplayR return nil, err } return &protobuf.DisplayResponse{ - Display: displayer.GetDisplay(location), + Response: &protobuf.DisplayResponse_Markdown{ + Markdown: displayer.GetDisplay(location), + }, }, nil } return nil, errors.New("instance does not implement displayer") @@ -297,8 +300,8 @@ func (s *compatV1ShimServer) ValidateAndSetConfig(ctx context.Context, req *prot } if err := configurer.ValidateAndSetConfig(currentConfig); err != nil { return &protobuf.ValidateAndSetConfigResponse{ - Response: &protobuf.ValidateAndSetConfigResponse_Error{ - Error: &protobuf.Error{ + Response: &protobuf.ValidateAndSetConfigResponse_ValidationError{ + ValidationError: &protobuf.Error{ Message: err.Error(), }, }, @@ -327,46 +330,83 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, } // enable supported capabilities - var capabilities []protobuf.Capability if _, ok := instance.(papiv1.Displayer); ok { - capabilities = append(capabilities, protobuf.Capability_DISPLAYER) + if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_DISPLAYER) { + stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_DISPLAYER, + }, + }) + } else { + return errors.New("displayer not supported by server but V1 API does not support backwards compatibility") + } } if _, ok := instance.(papiv1.Messenger); ok { - capabilities = append(capabilities, protobuf.Capability_MESSENGER) + if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_MESSENGER) { + stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_MESSENGER, + }, + }) + } else { + return errors.New("messenger not supported by server but V1 API does not support backwards compatibility") + } } if _, ok := instance.(papiv1.Configurer); ok { - capabilities = append(capabilities, protobuf.Capability_CONFIGURER) + if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_CONFIGURER) { + stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_CONFIGURER, + }, + }) + } else { + return errors.New("configurer not supported by server but V1 API does not support backwards compatibility") + } } if _, ok := instance.(papiv1.Storager); ok { - capabilities = append(capabilities, protobuf.Capability_STORAGER) + if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_STORAGER) { + stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_STORAGER, + }, + }) + } else { + return errors.New("storager not supported by server but V1 API does not support backwards compatibility") + } } if _, ok := instance.(papiv1.Webhooker); ok { - capabilities = append(capabilities, protobuf.Capability_WEBHOOKER) - } - - if err := stream.Send(&protobuf.InstanceUpdate{ - Update: &protobuf.InstanceUpdate_Capabilities{ - Capabilities: &protobuf.PluginCapabilities{ - Capabilities: capabilities, - }, - }, - }); err != nil { - return err + if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_WEBHOOKER) { + stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_WEBHOOKER, + }, + }) + } else { + return errors.New("webhooker not supported by server but V1 API does not support backwards compatibility") + } } if messenger, ok := instance.(papiv1.Messenger); ok { - messenger.SetMessageHandler(&shimV1MessageHandler{ - stream: &stream, - }) + if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_MESSENGER) { + messenger.SetMessageHandler(&shimV1MessageHandler{ + stream: &stream, + }) + } else { + return errors.New("messenger not supported by server but V1 API does not support backwards compatibility") + } } if configurer, ok := instance.(papiv1.Configurer); ok { - currentConfig := configurer.DefaultConfig() - if req.Config != nil { - yaml.Unmarshal(req.Config, ¤tConfig) - if err := configurer.ValidateAndSetConfig(currentConfig); err != nil { - return err + if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_CONFIGURER) { + currentConfig := configurer.DefaultConfig() + if req.Config != nil { + yaml.Unmarshal(req.Config, ¤tConfig) + if err := configurer.ValidateAndSetConfig(currentConfig); err != nil { + return err + } } + } else { + return errors.New("configurer not supported by server but V1 API does not support backwards compatibility") } } From 1a6973093571d852204ca8c3b4d14388bc96bcbd Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 20:16:22 -0500 Subject: [PATCH 17/37] example tests Signed-off-by: eternal-flame-AD --- .github/workflows/test.yml | 33 ++++ v2/anon_unix.go | 21 +++ v2/anon_windows.go | 25 +++ v2/cli_flags.go | 47 ++--- v2/examples_v1/echo/echo.go | 120 +++++++++++++ v2/examples_v1/echo/echo_test.go | 229 +++++++++++++++++++++++++ v2/examples_v1/minimal/minimal.go | 35 ++++ v2/examples_v1/minimal/minimal_test.go | 112 ++++++++++++ v2/generated/protobuf/meta.pb.go | 19 +- v2/generated/protobuf/meta_grpc.pb.go | 46 ++++- v2/go.mod | 2 +- v2/pipe_net_test.go | 4 +- v2/pipe_not_unix.go | 11 +- v2/pipe_unix.go | 13 +- v2/protobuf/meta.proto | 3 + v2/shim_v1.go | 145 ++++++++++++---- v2/transport_auth.go | 55 +++++- v2/transport_auth_test.go | 4 +- 18 files changed, 842 insertions(+), 82 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 v2/anon_unix.go create mode 100644 v2/anon_windows.go create mode 100644 v2/examples_v1/echo/echo.go create mode 100644 v2/examples_v1/echo/echo_test.go create mode 100644 v2/examples_v1/minimal/minimal.go create mode 100644 v2/examples_v1/minimal/minimal_test.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..36175c7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: Test + +on: + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.24.6 + - name: Install dependencies + run: go mod download + - name: Test + run: cd v2 && go test -v ./... + + test-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.24.6 + - name: Install dependencies + run: go mod download + - name: Test + run: cd v2 && go test -v ./... \ No newline at end of file diff --git a/v2/anon_unix.go b/v2/anon_unix.go new file mode 100644 index 0000000..122762c --- /dev/null +++ b/v2/anon_unix.go @@ -0,0 +1,21 @@ +//go:build unix + +package plugin + +import ( + "golang.org/x/sys/unix" +) + +func NewAnonPipe(rx *uintptr, tx *uintptr, cloexec bool) error { + var tmp [2]int + var flags int + if cloexec { + flags = unix.O_CLOEXEC + } + if err := unix.Pipe2(tmp[:], flags); err != nil { + return err + } + *rx = uintptr(tmp[0]) + *tx = uintptr(tmp[1]) + return nil +} diff --git a/v2/anon_windows.go b/v2/anon_windows.go new file mode 100644 index 0000000..022643e --- /dev/null +++ b/v2/anon_windows.go @@ -0,0 +1,25 @@ +//go:build windows + +package plugin + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +func NewAnonPipe(rx *uintptr, tx *uintptr, cloexec bool) error { + var tmp [2]windows.Handle + var sa windows.SecurityAttributes + sa.Length = uint32(unsafe.Sizeof(sa)) + if !cloexec { + sa.InheritHandle = 1 + } + err := windows.CreatePipe(&tmp[0], &tmp[1], &sa, 0) + if err != nil { + return err + } + *rx = uintptr(tmp[0]) + *tx = uintptr(tmp[1]) + return nil +} diff --git a/v2/cli_flags.go b/v2/cli_flags.go index ce54926..c550922 100644 --- a/v2/cli_flags.go +++ b/v2/cli_flags.go @@ -6,37 +6,44 @@ import ( ) type PluginCliFlags struct { - flagSet *flag.FlagSet - CAData []byte - CertData []byte - KeyData []byte + flagSet *flag.FlagSet + KexReqFile *os.File + KexRespFile *os.File + Debug bool } func ParsePluginCLIFlags(args []string) (*PluginCliFlags, error) { flagSet := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) - var caFile string - var certFile string - var keyFile string - flagSet.StringVar(&certFile, "cert-file", "", "Path to the certificate file for Transport Auth.") - flagSet.StringVar(&keyFile, "key-file", "", "Path to the key file for Transport Auth.") - flagSet.StringVar(&caFile, "ca-file", "", "Path to the CA file for Transport Auth.") + var kexReqFileName string + var kexRespFileName string + var debug bool + flagSet.StringVar(&kexReqFileName, "kex-req-file", "", "File name for the key exchange for Transport Auth.") + flagSet.StringVar(&kexRespFileName, "kex-resp-file", "", "File name for the key exchange for Transport Auth.") + flagSet.BoolVar(&debug, "debug", false, "Enable debug mode.") flagSet.Parse(args) - certData, err := os.ReadFile(certFile) - if err != nil { - return nil, err - } - keyData, err := os.ReadFile(keyFile) + + kexReqFile, err := os.OpenFile(kexReqFileName, os.O_WRONLY, 0) if err != nil { return nil, err } - caData, err := os.ReadFile(caFile) + kexRespFile, err := os.OpenFile(kexRespFileName, os.O_RDONLY, 0) if err != nil { return nil, err } return &PluginCliFlags{ - flagSet: flagSet, - CAData: caData, - CertData: certData, - KeyData: keyData, + flagSet: flagSet, + KexReqFile: kexReqFile, + KexRespFile: kexRespFile, + Debug: debug, }, nil } + +func (f *PluginCliFlags) Close() error { + if err := f.KexReqFile.Close(); err != nil { + return err + } + if err := f.KexRespFile.Close(); err != nil { + return err + } + return nil +} diff --git a/v2/examples_v1/echo/echo.go b/v2/examples_v1/echo/echo.go new file mode 100644 index 0000000..fb47cfd --- /dev/null +++ b/v2/examples_v1/echo/echo.go @@ -0,0 +1,120 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/url" + + "github.com/gin-gonic/gin" + "github.com/gotify/plugin-api" +) + +// GetGotifyPluginInfo returns gotify plugin info. +func GetGotifyPluginInfo() plugin.Info { + return plugin.Info{ + ModulePath: "github.com/gotify/server/v2/plugin/example/echo", + Name: "test plugin", + } +} + +// EchoPlugin is the gotify plugin instance. +type EchoPlugin struct { + msgHandler plugin.MessageHandler + storageHandler plugin.StorageHandler + config *Config + basePath string +} + +// SetStorageHandler implements plugin.Storager +func (c *EchoPlugin) SetStorageHandler(h plugin.StorageHandler) { + c.storageHandler = h +} + +// SetMessageHandler implements plugin.Messenger. +func (c *EchoPlugin) SetMessageHandler(h plugin.MessageHandler) { + c.msgHandler = h +} + +// Storage defines the plugin storage scheme +type Storage struct { + CalledTimes int `json:"called_times"` +} + +// Config defines the plugin config scheme +type Config struct { + MagicString string `yaml:"magic_string"` +} + +// DefaultConfig implements plugin.Configurer +func (c *EchoPlugin) DefaultConfig() interface{} { + return &Config{ + MagicString: "hello world", + } +} + +// ValidateAndSetConfig implements plugin.Configurer +func (c *EchoPlugin) ValidateAndSetConfig(config interface{}) error { + c.config = config.(*Config) + return nil +} + +// Enable enables the plugin. +func (c *EchoPlugin) Enable() error { + log.Println("echo plugin enabled") + return nil +} + +// Disable disables the plugin. +func (c *EchoPlugin) Disable() error { + log.Println("echo plugin disbled") + return nil +} + +// RegisterWebhook implements plugin.Webhooker. +func (c *EchoPlugin) RegisterWebhook(baseURL string, g *gin.RouterGroup) { + c.basePath = baseURL + g.GET("/echo", func(ctx *gin.Context) { + + storage, _ := c.storageHandler.Load() + conf := new(Storage) + json.Unmarshal(storage, conf) + conf.CalledTimes++ + newStorage, _ := json.Marshal(conf) + c.storageHandler.Save(newStorage) + + c.msgHandler.SendMessage(plugin.Message{ + Title: "Hello received", + Message: fmt.Sprintf("echo server received a hello message %d times", conf.CalledTimes), + Priority: 2, + Extras: map[string]any{ + "plugin::name": "echo", + }, + }) + ctx.Writer.WriteString(fmt.Sprintf("Magic string is: %s\r\nEcho server running at %secho", c.config.MagicString, c.basePath)) + }) +} + +// GetDisplay implements plugin.Displayer. +func (c *EchoPlugin) GetDisplay(location *url.URL) string { + loc := &url.URL{ + Path: c.basePath, + } + if location != nil { + loc.Scheme = location.Scheme + loc.Host = location.Host + } + loc = loc.ResolveReference(&url.URL{ + Path: "echo", + }) + return "Echo plugin running at: " + loc.String() +} + +// NewGotifyPluginInstance creates a plugin instance for a user context. +func NewGotifyPluginInstance(ctx plugin.UserContext) plugin.Plugin { + return &EchoPlugin{} +} + +func main() { + panic("this should be built as go plugin") +} diff --git a/v2/examples_v1/echo/echo_test.go b/v2/examples_v1/echo/echo_test.go new file mode 100644 index 0000000..5ebd5f0 --- /dev/null +++ b/v2/examples_v1/echo/echo_test.go @@ -0,0 +1,229 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http/httptest" + "os" + "reflect" + "slices" + "strings" + "testing" + + papiv1 "github.com/gotify/plugin-api" + "github.com/gotify/plugin-api/v2" + "github.com/gotify/plugin-api/v2/generated/protobuf" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/protobuf/types/known/emptypb" +) + +func TestEcho(t *testing.T) { + pluginInfo := GetGotifyPluginInfo() + + client, err := plugin.NewEphemeralTLSClient() + if err != nil { + t.Fatal(err) + } + + var reqRx, reqTx uintptr + var respRx, respTx uintptr + + plugin.NewAnonPipe(&reqRx, &reqTx, true) + plugin.NewAnonPipe(&respRx, &respTx, true) + + go func() { + reqFileRx := os.NewFile(reqRx, fmt.Sprintf("/proc/self/fd/%d", reqRx)) + defer reqFileRx.Close() + respFileTx := os.NewFile(respTx, fmt.Sprintf("/proc/self/fd/%d", respTx)) + defer respFileTx.Close() + if err := client.Kex(reqFileRx, respFileTx); err != nil { + panic(err) + } + }() + + compatV1, err := plugin.NewCompatV1Rpc(&plugin.CompatV1{ + GetPluginInfo: GetGotifyPluginInfo, + GetInstance: func(user papiv1.UserContext) (papiv1.Plugin, error) { + return NewGotifyPluginInstance(user), nil + }, + }, []string{ + "-kex-req-file", fmt.Sprintf("/proc/self/fd/%d", reqTx), + "-kex-resp-file", fmt.Sprintf("/proc/self/fd/%d", respRx), + }) + if err != nil { + t.Fatal(err) + } + + listener, addr, err := plugin.NewListener() + if err != nil { + t.Fatal(err) + } + go func() { + compatV1.ServeTLS(listener, "", "") + }() + + rpcClient, err := grpc.NewClient(addr, grpc.WithTransportCredentials(credentials.NewTLS(client.ClientTLSConfig(pluginInfo.ModulePath)))) + if err != nil { + t.Fatal(err) + } + pluginClient := protobuf.NewPluginClient(rpcClient) + + version, err := pluginClient.GetPluginInfo(context.Background(), &emptypb.Empty{}) + if err != nil { + t.Fatal(err) + } + if version.Name != pluginInfo.Name { + t.Fatal("expected ", pluginInfo.Name, " got ", version.Name) + } + if version.Version != pluginInfo.Version { + t.Fatal("expected ", pluginInfo.Version, " got ", version.Version) + } + + testUser := &papiv1.UserContext{ + ID: 1, + Name: "alice", + Admin: true, + } + webhookBasePath := "/plugin/echo-test/" + stream, err := pluginClient.RunUserInstance(context.Background(), &protobuf.UserInstanceRequest{ + User: &protobuf.UserContext{ + Id: uint64(testUser.ID), + Name: testUser.Name, + Admin: testUser.Admin, + }, + ServerVersion: &protobuf.ServerVersionInfo{ + Version: "1.0.0", + Capabilities: []protobuf.Capability{ + protobuf.Capability_DISPLAYER, + protobuf.Capability_CONFIGURER, + protobuf.Capability_WEBHOOKER, + protobuf.Capability_MESSENGER, + protobuf.Capability_STORAGER, + }, + }, + WebhookBasePath: &webhookBasePath, + Config: []byte{}, + Storage: []byte{}, + }) + if err != nil { + panic(err) + } + defer stream.CloseSend() + + var registeredCapabilities []protobuf.Capability + var passed bool + var storage []byte + expectedMagicString := "hello world" + ratchet := 1 + for { + msg, err := stream.Recv() + if err != nil { + if err == io.EOF { + break + } + panic(err) + } + switch msg.Update.(type) { + case *protobuf.InstanceUpdate_Capable: + registeredCapabilities = append(registeredCapabilities, msg.GetCapable()) + case *protobuf.InstanceUpdate_Message: + msg := msg.GetMessage() + extrasNameJson := msg.GetExtras()["plugin::name"].GetJson() + if extrasNameJson != "\"echo\"" { + t.Fatal("expected ", "echo", " got ", extrasNameJson) + } + case *protobuf.InstanceUpdate_Storage: + storage = msg.GetStorage() + var decodedStorage Storage + json.Unmarshal(storage, &decodedStorage) + if decodedStorage.CalledTimes < ratchet { + t.Fatal("expected ", ratchet, " got ", decodedStorage.CalledTimes) + } else if decodedStorage.CalledTimes == ratchet+1 { + ratchet++ + } else if decodedStorage.CalledTimes > ratchet+1 { + t.Fatal("expected ", ratchet+1, " got ", decodedStorage.CalledTimes) + } + } + + slices.Sort(registeredCapabilities) + expectedCapabilities := []protobuf.Capability{ + protobuf.Capability_DISPLAYER, + protobuf.Capability_CONFIGURER, + protobuf.Capability_WEBHOOKER, + protobuf.Capability_MESSENGER, + protobuf.Capability_STORAGER, + } + slices.Sort(expectedCapabilities) + if reflect.DeepEqual(registeredCapabilities, expectedCapabilities) { + + displayerClient := protobuf.NewDisplayerClient(rpcClient) + displayResponse, err := displayerClient.Display(context.Background(), &protobuf.DisplayRequest{ + User: &protobuf.UserContext{ + Id: uint64(testUser.ID), + Name: testUser.Name, + Admin: testUser.Admin, + }, + Location: "https://gotify.example.com/", + }) + if err != nil { + panic(err) + } + if displayResponse.GetMarkdown() != "Echo plugin running at: https://gotify.example.com/plugin/echo-test/echo" { + t.Fatal("expected ", "Echo plugin running at: https://gotify.example.com/plugin/echo-test/echo", " got ", displayResponse.GetMarkdown()) + } + + tlsWebhookName := plugin.BuildPluginTLSName(plugin.PurposePluginWebhook, pluginInfo.ModulePath) + + for range 3 { + testreq := httptest.NewRequest("GET", "https://"+tlsWebhookName+"/plugin/echo-test/echo", nil) + recorder := httptest.NewRecorder() + compatV1.ServeHTTP(recorder, testreq) + if recorder.Code != 200 { + t.Fatal("expected 200 got ", recorder.Code) + } + if !strings.Contains(recorder.Body.String(), "Magic string is: "+expectedMagicString) { + t.Fatal("expected ", "Magic string is: "+expectedMagicString, " got ", recorder.Body.String()) + } + if !strings.Contains(recorder.Body.String(), "Echo server running at "+webhookBasePath+"echo") { + t.Fatal("expected ", "Echo server running at "+webhookBasePath+"echo", " got ", recorder.Body.String()) + } + configurerClient := protobuf.NewConfigurerClient(rpcClient) + expectedMagicString = fmt.Sprintf("test_%d", ratchet) + resp, err := configurerClient.ValidateAndSetConfig(context.Background(), &protobuf.ValidateAndSetConfigRequest{ + User: &protobuf.UserContext{ + Id: uint64(testUser.ID), + Name: testUser.Name, + Admin: testUser.Admin, + }, + Config: &protobuf.Config{ + Config: "magic_string: " + expectedMagicString, + }, + }) + if err != nil { + t.Fatal(err) + } + if resp.GetResponse().(*protobuf.ValidateAndSetConfigResponse_Success) == nil { + t.Fatal("expected success") + } + } + + passed = true + _, err = pluginClient.GracefulShutdown(context.Background(), &emptypb.Empty{}) + if err != nil { + t.Fatal(err) + } + } + if len(registeredCapabilities) > len(expectedCapabilities) { + t.Fatal("more capabilities than expected") + } + } + if !passed { + t.Fatal("test failed: connection closed before all capabilities were tested") + } + if ratchet != 3 { + t.Fatal("expected called times to be 3 got ", ratchet) + } +} diff --git a/v2/examples_v1/minimal/minimal.go b/v2/examples_v1/minimal/minimal.go new file mode 100644 index 0000000..7dbbde6 --- /dev/null +++ b/v2/examples_v1/minimal/minimal.go @@ -0,0 +1,35 @@ +package main + +import ( + "github.com/gotify/plugin-api" +) + +// GetGotifyPluginInfo returns gotify plugin info +func GetGotifyPluginInfo() plugin.Info { + return plugin.Info{ + Name: "minimal plugin", + ModulePath: "github.com/gotify/server/v2/example/minimal", + } +} + +// Plugin is plugin instance +type Plugin struct{} + +// Enable implements plugin.Plugin +func (c *Plugin) Enable() error { + return nil +} + +// Disable implements plugin.Plugin +func (c *Plugin) Disable() error { + return nil +} + +// NewGotifyPluginInstance creates a plugin instance for a user context. +func NewGotifyPluginInstance(ctx plugin.UserContext) plugin.Plugin { + return &Plugin{} +} + +func main() { + panic("this should be built as go plugin") +} diff --git a/v2/examples_v1/minimal/minimal_test.go b/v2/examples_v1/minimal/minimal_test.go new file mode 100644 index 0000000..5b17cc1 --- /dev/null +++ b/v2/examples_v1/minimal/minimal_test.go @@ -0,0 +1,112 @@ +package main + +import ( + "context" + "fmt" + "io" + "os" + "testing" + + papiv1 "github.com/gotify/plugin-api" + "github.com/gotify/plugin-api/v2" + "github.com/gotify/plugin-api/v2/generated/protobuf" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/protobuf/types/known/emptypb" +) + +func TestEcho(t *testing.T) { + pluginInfo := GetGotifyPluginInfo() + + client, err := plugin.NewEphemeralTLSClient() + if err != nil { + t.Fatal(err) + } + + var reqRx, reqTx uintptr + var respRx, respTx uintptr + + plugin.NewAnonPipe(&reqRx, &reqTx, true) + plugin.NewAnonPipe(&respRx, &respTx, true) + + go func() { + reqFileRx := os.NewFile(reqRx, fmt.Sprintf("/proc/self/fd/%d", reqRx)) + defer reqFileRx.Close() + respFileTx := os.NewFile(respTx, fmt.Sprintf("/proc/self/fd/%d", respTx)) + defer respFileTx.Close() + client.Kex(reqFileRx, respFileTx) + }() + + compatV1, err := plugin.NewCompatV1Rpc(&plugin.CompatV1{ + GetPluginInfo: GetGotifyPluginInfo, + GetInstance: func(user papiv1.UserContext) (papiv1.Plugin, error) { + return NewGotifyPluginInstance(user), nil + }, + }, []string{ + "-kex-req-file", fmt.Sprintf("/proc/self/fd/%d", reqTx), + "-kex-resp-file", fmt.Sprintf("/proc/self/fd/%d", respRx), + }) + if err != nil { + t.Fatal(err) + } + + listener, addr, err := plugin.NewListener() + if err != nil { + t.Fatal(err) + } + go func() { + compatV1.ServeTLS(listener, "", "") + }() + + rpcClient, err := grpc.NewClient(addr, grpc.WithTransportCredentials(credentials.NewTLS(client.ClientTLSConfig(pluginInfo.ModulePath)))) + if err != nil { + t.Fatal(err) + } + + pluginClient := protobuf.NewPluginClient(rpcClient) + version, err := pluginClient.GetPluginInfo(context.Background(), &emptypb.Empty{}) + if err != nil { + t.Fatal(err) + } + if version.Name != pluginInfo.Name { + t.Fatal("expected ", pluginInfo.Name, " got ", version.Name) + } + if version.Version != pluginInfo.Version { + t.Fatal("expected ", pluginInfo.Version, " got ", version.Version) + } + + pluginClient.SetEnable(context.Background(), &protobuf.SetEnableRequest{ + User: &protobuf.UserContext{ + Id: uint64(1), + Name: "test", + Admin: false, + }, + Enable: true, + }) + if err != nil { + t.Fatal(err) + } + + stream, err := pluginClient.RunUserInstance(context.Background(), &protobuf.UserInstanceRequest{ + User: &protobuf.UserContext{ + Id: uint64(1), + Name: "test", + Admin: false, + }, + }) + if err != nil { + t.Fatal(err) + } + + pluginClient.GracefulShutdown(context.Background(), &emptypb.Empty{}) + stream.CloseSend() + for { + _, err := stream.Recv() + if err != nil { + if err == io.EOF { + break + } + t.Fatal(err) + } + } +} diff --git a/v2/generated/protobuf/meta.pb.go b/v2/generated/protobuf/meta.pb.go index 2089bc0..7ad7a95 100644 --- a/v2/generated/protobuf/meta.pb.go +++ b/v2/generated/protobuf/meta.pb.go @@ -871,10 +871,11 @@ const file_meta_proto_rawDesc = "" + "\n" + "CONFIGURER\x10\x02\x12\f\n" + "\bSTORAGER\x10\x03\x12\r\n" + - "\tWEBHOOKER\x10\x042\xac\x01\n" + + "\tWEBHOOKER\x10\x042\xf0\x01\n" + "\x06Plugin\x12.\n" + "\rGetPluginInfo\x12\x16.google.protobuf.Empty\x1a\x05.Info\x126\n" + - "\tSetEnable\x12\x11.SetEnableRequest\x1a\x16.google.protobuf.Empty\x12:\n" + + "\tSetEnable\x12\x11.SetEnableRequest\x1a\x16.google.protobuf.Empty\x12B\n" + + "\x10GracefulShutdown\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12:\n" + "\x0fRunUserInstance\x12\x14.UserInstanceRequest\x1a\x0f.InstanceUpdate0\x01B\x16Z\x14./generated/protobufb\x06proto3" var ( @@ -920,12 +921,14 @@ var file_meta_proto_depIdxs = []int32{ 5, // 9: Message.ExtrasEntry.value:type_name -> ExtrasValue 13, // 10: Plugin.GetPluginInfo:input_type -> google.protobuf.Empty 7, // 11: Plugin.SetEnable:input_type -> SetEnableRequest - 9, // 12: Plugin.RunUserInstance:input_type -> UserInstanceRequest - 4, // 13: Plugin.GetPluginInfo:output_type -> Info - 13, // 14: Plugin.SetEnable:output_type -> google.protobuf.Empty - 8, // 15: Plugin.RunUserInstance:output_type -> InstanceUpdate - 13, // [13:16] is the sub-list for method output_type - 10, // [10:13] is the sub-list for method input_type + 13, // 12: Plugin.GracefulShutdown:input_type -> google.protobuf.Empty + 9, // 13: Plugin.RunUserInstance:input_type -> UserInstanceRequest + 4, // 14: Plugin.GetPluginInfo:output_type -> Info + 13, // 15: Plugin.SetEnable:output_type -> google.protobuf.Empty + 13, // 16: Plugin.GracefulShutdown:output_type -> google.protobuf.Empty + 8, // 17: Plugin.RunUserInstance:output_type -> InstanceUpdate + 14, // [14:18] is the sub-list for method output_type + 10, // [10:14] is the sub-list for method input_type 10, // [10:10] is the sub-list for extension type_name 10, // [10:10] is the sub-list for extension extendee 0, // [0:10] is the sub-list for field type_name diff --git a/v2/generated/protobuf/meta_grpc.pb.go b/v2/generated/protobuf/meta_grpc.pb.go index 000fe7a..7405648 100644 --- a/v2/generated/protobuf/meta_grpc.pb.go +++ b/v2/generated/protobuf/meta_grpc.pb.go @@ -20,9 +20,10 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Plugin_GetPluginInfo_FullMethodName = "/Plugin/GetPluginInfo" - Plugin_SetEnable_FullMethodName = "/Plugin/SetEnable" - Plugin_RunUserInstance_FullMethodName = "/Plugin/RunUserInstance" + Plugin_GetPluginInfo_FullMethodName = "/Plugin/GetPluginInfo" + Plugin_SetEnable_FullMethodName = "/Plugin/SetEnable" + Plugin_GracefulShutdown_FullMethodName = "/Plugin/GracefulShutdown" + Plugin_RunUserInstance_FullMethodName = "/Plugin/RunUserInstance" ) // PluginClient is the client API for Plugin service. @@ -36,6 +37,8 @@ type PluginClient interface { GetPluginInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) // set the enable state of a plugin instance SetEnable(ctx context.Context, in *SetEnableRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // graceful shutdown + GracefulShutdown(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) // run a user instance RunUserInstance(ctx context.Context, in *UserInstanceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[InstanceUpdate], error) } @@ -68,6 +71,16 @@ func (c *pluginClient) SetEnable(ctx context.Context, in *SetEnableRequest, opts return out, nil } +func (c *pluginClient) GracefulShutdown(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Plugin_GracefulShutdown_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *pluginClient) RunUserInstance(ctx context.Context, in *UserInstanceRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[InstanceUpdate], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &Plugin_ServiceDesc.Streams[0], Plugin_RunUserInstance_FullMethodName, cOpts...) @@ -98,6 +111,8 @@ type PluginServer interface { GetPluginInfo(context.Context, *emptypb.Empty) (*Info, error) // set the enable state of a plugin instance SetEnable(context.Context, *SetEnableRequest) (*emptypb.Empty, error) + // graceful shutdown + GracefulShutdown(context.Context, *emptypb.Empty) (*emptypb.Empty, error) // run a user instance RunUserInstance(*UserInstanceRequest, grpc.ServerStreamingServer[InstanceUpdate]) error mustEmbedUnimplementedPluginServer() @@ -116,6 +131,9 @@ func (UnimplementedPluginServer) GetPluginInfo(context.Context, *emptypb.Empty) func (UnimplementedPluginServer) SetEnable(context.Context, *SetEnableRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method SetEnable not implemented") } +func (UnimplementedPluginServer) GracefulShutdown(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method GracefulShutdown not implemented") +} func (UnimplementedPluginServer) RunUserInstance(*UserInstanceRequest, grpc.ServerStreamingServer[InstanceUpdate]) error { return status.Errorf(codes.Unimplemented, "method RunUserInstance not implemented") } @@ -176,6 +194,24 @@ func _Plugin_SetEnable_Handler(srv interface{}, ctx context.Context, dec func(in return interceptor(ctx, in, info, handler) } +func _Plugin_GracefulShutdown_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).GracefulShutdown(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Plugin_GracefulShutdown_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).GracefulShutdown(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _Plugin_RunUserInstance_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(UserInstanceRequest) if err := stream.RecvMsg(m); err != nil { @@ -202,6 +238,10 @@ var Plugin_ServiceDesc = grpc.ServiceDesc{ MethodName: "SetEnable", Handler: _Plugin_SetEnable_Handler, }, + { + MethodName: "GracefulShutdown", + Handler: _Plugin_GracefulShutdown_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/v2/go.mod b/v2/go.mod index 3073bd1..faf1f4c 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -5,6 +5,7 @@ go 1.24.5 require ( github.com/gin-gonic/gin v1.3.0 github.com/gotify/plugin-api v1.0.0 + golang.org/x/sys v0.33.0 google.golang.org/grpc v1.74.2 google.golang.org/protobuf v1.36.7 gopkg.in/yaml.v2 v2.2.2 @@ -19,7 +20,6 @@ require ( github.com/modern-go/reflect2 v1.0.1 // indirect github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 // indirect golang.org/x/net v0.40.0 // indirect - golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect gopkg.in/go-playground/validator.v8 v8.18.2 // indirect diff --git a/v2/pipe_net_test.go b/v2/pipe_net_test.go index 4a867d1..b6d823f 100644 --- a/v2/pipe_net_test.go +++ b/v2/pipe_net_test.go @@ -52,10 +52,10 @@ func TestGrpcPipeNet(t *testing.T) { defer listener.Close() serverCsrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ Subject: pkix.Name{ - CommonName: BuildPluginTLSName(purposePluginRPC, "test"), + CommonName: BuildPluginTLSName(PurposePluginRPC, "test"), }, DNSNames: []string{ - BuildPluginTLSName(purposePluginRPC, "test"), + BuildPluginTLSName(PurposePluginRPC, "test"), }, PublicKey: serverPub, }, serverPriv) diff --git a/v2/pipe_not_unix.go b/v2/pipe_not_unix.go index 0901c44..70469d0 100644 --- a/v2/pipe_not_unix.go +++ b/v2/pipe_not_unix.go @@ -2,12 +2,15 @@ package plugin -import "net" +import ( + "fmt" + "net" +) -func NewListener() (net.Listener, error) { +func NewListener() (net.Listener, string, error) { listener, err := net.Listen("tcp", "[::1]:0") if err != nil { - return nil, err + return nil, "", err } - return listener, nil + return listener, fmt.Sprintf("dns://%s", listener.Addr().String()), nil } diff --git a/v2/pipe_unix.go b/v2/pipe_unix.go index 5813c82..8f93439 100644 --- a/v2/pipe_unix.go +++ b/v2/pipe_unix.go @@ -3,19 +3,24 @@ package plugin import ( + "fmt" "net" "os" "path/filepath" ) -func NewListener() (net.Listener, error) { +func NewListener() (net.Listener, string, error) { tmpDir, err := os.MkdirTemp("", "gotify-plugin-*") if err != nil { - return nil, err + return nil, "", err } if err := os.Chmod(tmpDir, 0700); err != nil { - return nil, err + return nil, "", err } pipePath := filepath.Join(tmpDir, "plugin.sock") - return net.Listen("unix", pipePath) + listener, err := net.Listen("unix", pipePath) + if err != nil { + return nil, "", err + } + return listener, fmt.Sprintf("unix://%s", pipePath), nil } diff --git a/v2/protobuf/meta.proto b/v2/protobuf/meta.proto index 50cbd82..ed3a9a0 100644 --- a/v2/protobuf/meta.proto +++ b/v2/protobuf/meta.proto @@ -99,6 +99,9 @@ service Plugin { // set the enable state of a plugin instance rpc SetEnable(SetEnableRequest) returns (google.protobuf.Empty); + + // graceful shutdown + rpc GracefulShutdown(google.protobuf.Empty) returns (google.protobuf.Empty); // run a user instance rpc RunUserInstance(UserInstanceRequest) returns (stream InstanceUpdate); diff --git a/v2/shim_v1.go b/v2/shim_v1.go index d769cde..3ec97d0 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -2,14 +2,21 @@ package plugin import ( "context" + "crypto/ed25519" + "crypto/rand" "crypto/tls" "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" "errors" + "io" "log" "math" "net/http" "net/url" "plugin" + "reflect" "slices" "strings" "sync" @@ -30,8 +37,8 @@ type GrpcDialer interface { // CompatV1 is a shim that acts like a plugin server and delegates request to // something that implements a V1-style API interface. type CompatV1 struct { - GetPluginInfo func() *papiv1.Info - GetInstance func(user *papiv1.UserContext) (papiv1.Plugin, error) + GetPluginInfo func() papiv1.Info + GetInstance func(user papiv1.UserContext) (papiv1.Plugin, error) } // NewCompatV1FromPlugin creates a new CompatV1 from a native Go plugin. @@ -45,14 +52,14 @@ func NewCompatV1FromPlugin(plugin *plugin.Plugin) (*CompatV1, error) { return nil, err } - getPluginInfoChecked, ok := getPluginInfo.(func() *papiv1.Info) + getPluginInfoChecked, ok := getPluginInfo.(func() papiv1.Info) if !ok { return nil, errors.New("GetGotifyPluginInfo is not a function") } - getInstanceCheckedWithErr, ok := getInstance.(func(user *papiv1.UserContext) (papiv1.Plugin, error)) + getInstanceCheckedWithErr, ok := getInstance.(func(user papiv1.UserContext) (papiv1.Plugin, error)) if !ok { - if getInstanceCheckedWithoutErr, ok := getInstance.(func(user *papiv1.UserContext) papiv1.Plugin); ok { - getInstanceCheckedWithErr = func(user *papiv1.UserContext) (papiv1.Plugin, error) { + if getInstanceCheckedWithoutErr, ok := getInstance.(func(user papiv1.UserContext) papiv1.Plugin); ok { + getInstanceCheckedWithErr = func(user papiv1.UserContext) (papiv1.Plugin, error) { return getInstanceCheckedWithoutErr(user), nil } } else { @@ -69,62 +76,104 @@ func NewCompatV1FromPlugin(plugin *plugin.Plugin) (*CompatV1, error) { // CompatV1Shim is a shim that acts like a plugin server and delegates request to // something that implements a V1-style API interface. type CompatV1Shim struct { + shutdown chan struct{} + shutdownOnce *sync.Once mu *sync.RWMutex compatV1 *CompatV1 gin *gin.Engine instances map[uint64]papiv1.Plugin pluginServer *grpc.Server - pluginInfo *papiv1.Info + pluginInfo papiv1.Info http.Server } // NewCompatV1Rpc creates a new CompatV1Shim server. func NewCompatV1Rpc(compatV1 *CompatV1, cliArgs []string) (*CompatV1Shim, error) { pluginInfo := compatV1.GetPluginInfo() - tlsName := BuildPluginTLSName(purposePluginRPC, pluginInfo.Name) cliFlags, err := ParsePluginCLIFlags(cliArgs) if err != nil { log.Fatalf("Failed to parse CLI flags: %v", err) } + defer cliFlags.Close() + rootCAs := x509.NewCertPool() - caCert, err := x509.ParseCertificate(cliFlags.CAData) + + // perform key exchange through secure file descriptors + _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, err } - rootCAs.AddCert(caCert) + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: BuildPluginTLSName("*", pluginInfo.ModulePath), + }, + }, priv) - leafCert, err := x509.ParseCertificate(cliFlags.CertData) if err != nil { return nil, err } + if _, err := cliFlags.KexReqFile.Write(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: csrBytes, + })); err != nil { + return nil, err + } + + var certBytes []byte + var certificateChain []tls.Certificate + for { + var buf [2048]byte + n, err := cliFlags.KexRespFile.Read(buf[:]) + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + certBytes = append(certBytes, buf[:n]...) + + for block, rest := pem.Decode(certBytes); block != nil; block, rest = pem.Decode(rest) { + if block.Type == "CERTIFICATE" { + parsedCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + // Server signs with IsCA=false, so we can add all of them to the root CA pool without + // trusting things we shouldn't. + rootCAs.AddCert(parsedCert) + certificateChain = append(certificateChain, tls.Certificate{ + Certificate: [][]byte{block.Bytes}, + Leaf: parsedCert, + }) + } + certBytes = rest + } + } + + certificateChain[0].PrivateKey = priv tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{ - { - Certificate: [][]byte{cliFlags.CertData}, - PrivateKey: cliFlags.KeyData, - Leaf: leafCert, - }, - { - Certificate: [][]byte{caCert.Raw}, - }, - }, - RootCAs: rootCAs, - ServerName: tlsName, - ClientAuth: tls.RequireAndVerifyClientCert, - ClientCAs: rootCAs, + Certificates: certificateChain, + RootCAs: rootCAs, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: rootCAs, } rpcServer := grpc.NewServer() + if !cliFlags.Debug { + gin.SetMode(gin.ReleaseMode) + } - gin := gin.Default() + ginEngine := gin.Default() self := &CompatV1Shim{ + shutdown: make(chan struct{}), + shutdownOnce: &sync.Once{}, mu: &sync.RWMutex{}, instances: make(map[uint64]papiv1.Plugin), compatV1: compatV1, - gin: gin, + gin: ginEngine, pluginServer: rpcServer, pluginInfo: pluginInfo, } @@ -155,7 +204,7 @@ func (h *CompatV1Shim) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - pluginRpcHostName := BuildPluginTLSName(purposePluginRPC, h.pluginInfo.ModulePath) + pluginRpcHostName := BuildPluginTLSName(PurposePluginRPC, h.pluginInfo.ModulePath) if r.TLS.ServerName == pluginRpcHostName { if r.ProtoMajor != 2 { @@ -171,7 +220,7 @@ func (h *CompatV1Shim) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - pluginWebhookHostName := BuildPluginTLSName(purposePluginWebhook, h.pluginInfo.ModulePath) + pluginWebhookHostName := BuildPluginTLSName(PurposePluginWebhook, h.pluginInfo.ModulePath) if r.TLS.ServerName == pluginWebhookHostName { h.gin.ServeHTTP(w, r) return @@ -185,10 +234,25 @@ type shimV1MessageHandler struct { } func (h *shimV1MessageHandler) SendMessage(msg papiv1.Message) error { + extras := make(map[string]*protobuf.ExtrasValue) + for k, v := range msg.Extras { + jsonValue, err := json.Marshal(v) + if err != nil { + return err + } + extras[k] = &protobuf.ExtrasValue{ + Value: &protobuf.ExtrasValue_Json{ + Json: string(jsonValue), + }, + } + } return (*h.stream).Send(&protobuf.InstanceUpdate{ Update: &protobuf.InstanceUpdate_Message{ Message: &protobuf.Message{ - Message: msg.Message, + Message: msg.Message, + Title: msg.Title, + Priority: int32(msg.Priority), + Extras: extras, }, }, }) @@ -200,6 +264,7 @@ type shimV1StorageHandler struct { } func (h *shimV1StorageHandler) Save(b []byte) error { + h.currentStorage = slices.Clone(b) return (*h.stream).Send(&protobuf.InstanceUpdate{ Update: &protobuf.InstanceUpdate_Storage{ Storage: b, @@ -208,7 +273,7 @@ func (h *shimV1StorageHandler) Save(b []byte) error { } func (h *shimV1StorageHandler) Load() (b []byte, err error) { - copy(h.currentStorage, b) + b = slices.Clone(h.currentStorage) return } @@ -294,9 +359,13 @@ func (s *compatV1ShimServer) ValidateAndSetConfig(ctx context.Context, req *prot return nil, errors.New("instance not found") } if configurer, ok := instance.(papiv1.Configurer); ok { - var currentConfig interface{} + currentConfig := configurer.DefaultConfig() if req.Config != nil { - yaml.Unmarshal([]byte(req.Config.Config), ¤tConfig) + if reflect.TypeOf(currentConfig).Kind() == reflect.Pointer { + yaml.Unmarshal([]byte(req.Config.Config), currentConfig) + } else { + yaml.Unmarshal([]byte(req.Config.Config), ¤tConfig) + } } if err := configurer.ValidateAndSetConfig(currentConfig); err != nil { return &protobuf.ValidateAndSetConfigResponse{ @@ -316,11 +385,18 @@ func (s *compatV1ShimServer) ValidateAndSetConfig(ctx context.Context, req *prot return nil, errors.New("instance does not implement configurer") } +func (s *compatV1ShimServer) GracefulShutdown(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) { + s.shim.shutdownOnce.Do(func() { + close(s.shim.shutdown) + }) + return new(emptypb.Empty), nil +} + func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, stream protobuf.Plugin_RunUserInstanceServer) error { if req.User.Id > math.MaxUint { return errors.New("user id is too large") } - instance, err := s.shim.compatV1.GetInstance(&papiv1.UserContext{ + instance, err := s.shim.compatV1.GetInstance(papiv1.UserContext{ ID: uint(req.User.Id), Name: req.User.Name, Admin: req.User.Admin, @@ -429,5 +505,6 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, s.shim.instances[req.User.Id] = instance s.shim.mu.Unlock() + <-s.shim.shutdown return nil } diff --git a/v2/transport_auth.go b/v2/transport_auth.go index 6de63a8..992450c 100644 --- a/v2/transport_auth.go +++ b/v2/transport_auth.go @@ -7,15 +7,17 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/hex" + "encoding/pem" "fmt" + "io" "slices" "strings" "time" ) const ( - purposePluginRPC = "rpc" - purposePluginWebhook = "webhook" + PurposePluginRPC = "rpc" + PurposePluginWebhook = "webhook" ) const ServerTLSName = "server.gotify.home.arpa" @@ -58,7 +60,7 @@ func (s *EphemeralTLSClient) ClientTLSConfig(moduleName string) *tls.Config { }, }, RootCAs: s.createCertPool(), - ServerName: BuildPluginTLSName(purposePluginRPC, moduleName), + ServerName: BuildPluginTLSName(PurposePluginRPC, moduleName), } } @@ -95,7 +97,52 @@ func (s *EphemeralTLSClient) SignCSR(dnsName string, csr *x509.CertificateReques } func (s *EphemeralTLSClient) SignPluginCSR(moduleName string, csr *x509.CertificateRequest) ([]byte, error) { - return s.SignCSR(BuildPluginTLSName(purposePluginRPC, moduleName), csr) + return s.SignCSR(BuildPluginTLSName("*", moduleName), csr) +} + +func (s *EphemeralTLSClient) Kex(req io.Reader, resp io.Writer) error { + var csr *x509.CertificateRequest + var csrBytes []byte + for csr == nil { + var buf [2048]byte + n, err := req.Read(buf[:]) + if err != nil { + return err + } + csrBytes = append(csrBytes, buf[:n]...) + block, _ := pem.Decode(csrBytes) + if block == nil { + continue + } + + if block.Type == "CERTIFICATE REQUEST" { + csrParsed, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil { + return err + } + csr = csrParsed + } + } + dnsName := csr.Subject.CommonName + certBytes, err := s.SignCSR(dnsName, csr) + if err != nil { + return err + } + _, err = resp.Write(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + })) + if err != nil { + return err + } + _, err = resp.Write(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: s.caCert.Raw, + })) + if err != nil { + return err + } + return nil } func NewEphemeralTLSClient() (*EphemeralTLSClient, error) { diff --git a/v2/transport_auth_test.go b/v2/transport_auth_test.go index f5d87d8..830c567 100644 --- a/v2/transport_auth_test.go +++ b/v2/transport_auth_test.go @@ -17,7 +17,7 @@ func TestEphemeralTLSClient(t *testing.T) { t.Fatal(err) } - pluginTlsName := BuildPluginTLSName(purposePluginRPC, "test") + pluginTlsName := BuildPluginTLSName(PurposePluginRPC, "test") _, serverPriv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) @@ -27,7 +27,7 @@ func TestEphemeralTLSClient(t *testing.T) { CommonName: pluginTlsName, }, DNSNames: []string{ - BuildPluginTLSName(purposePluginRPC, "test"), + BuildPluginTLSName(PurposePluginRPC, "test"), }, }, serverPriv) if err != nil { From b922133b470f71ebf14c9552ab9ad8e6a1ad946e Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 20:27:33 -0500 Subject: [PATCH 18/37] always test TCP implementation Signed-off-by: eternal-flame-AD --- v2/cli_flags.go | 38 ++++++++++++++++++++++---- v2/examples_v1/minimal/minimal_test.go | 23 ++++++++++++---- v2/pipe_not_unix.go | 7 +---- v2/pipe_tcp.go | 14 ++++++++++ 4 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 v2/pipe_tcp.go diff --git a/v2/cli_flags.go b/v2/cli_flags.go index c550922..3d04659 100644 --- a/v2/cli_flags.go +++ b/v2/cli_flags.go @@ -3,6 +3,8 @@ package plugin import ( "flag" "os" + "strconv" + "strings" ) type PluginCliFlags struct { @@ -17,16 +19,40 @@ func ParsePluginCLIFlags(args []string) (*PluginCliFlags, error) { var kexReqFileName string var kexRespFileName string var debug bool - flagSet.StringVar(&kexReqFileName, "kex-req-file", "", "File name for the key exchange for Transport Auth.") - flagSet.StringVar(&kexRespFileName, "kex-resp-file", "", "File name for the key exchange for Transport Auth.") + flagSet.StringVar(&kexReqFileName, "kex-req-file", "", "File name for the key exchange for Transport Auth. /proc/self/fd/* can be used to open a file descriptor cross platform.") + flagSet.StringVar(&kexRespFileName, "kex-resp-file", "", "File name for the key exchange for Transport Auth. /proc/self/fd/* can be used to open a file descriptor cross platform.") flagSet.BoolVar(&debug, "debug", false, "Enable debug mode.") flagSet.Parse(args) - kexReqFile, err := os.OpenFile(kexReqFileName, os.O_WRONLY, 0) - if err != nil { - return nil, err + var kexReqFile *os.File + var kexRespFile *os.File + var err error + + if fdNumber, found := strings.CutPrefix(kexReqFileName, "/proc/self/fd/"); found { + fdNumber, err := strconv.ParseUint(fdNumber, 10, 64) + kexReqFile = os.NewFile(uintptr(fdNumber), kexReqFileName) + if err != nil { + return nil, err + } + } else { + kexReqFile, err = os.OpenFile(kexReqFileName, os.O_WRONLY, 0) + if err != nil { + return nil, err + } } - kexRespFile, err := os.OpenFile(kexRespFileName, os.O_RDONLY, 0) + if fdNumber, found := strings.CutPrefix(kexRespFileName, "/proc/self/fd/"); found { + fdNumber, err := strconv.ParseUint(fdNumber, 10, 64) + kexRespFile = os.NewFile(uintptr(fdNumber), kexRespFileName) + if err != nil { + return nil, err + } + } else { + kexRespFile, err = os.OpenFile(kexRespFileName, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + } + if err != nil { return nil, err } diff --git a/v2/examples_v1/minimal/minimal_test.go b/v2/examples_v1/minimal/minimal_test.go index 5b17cc1..e9ed525 100644 --- a/v2/examples_v1/minimal/minimal_test.go +++ b/v2/examples_v1/minimal/minimal_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "net" "os" "testing" @@ -15,7 +16,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" ) -func TestEcho(t *testing.T) { +func testEchoImpl(t *testing.T, listener net.Listener, addr string) { pluginInfo := GetGotifyPluginInfo() client, err := plugin.NewEphemeralTLSClient() @@ -50,10 +51,6 @@ func TestEcho(t *testing.T) { t.Fatal(err) } - listener, addr, err := plugin.NewListener() - if err != nil { - t.Fatal(err) - } go func() { compatV1.ServeTLS(listener, "", "") }() @@ -110,3 +107,19 @@ func TestEcho(t *testing.T) { } } } + +func TestEcho(t *testing.T) { + listener, addr, err := plugin.NewListener() + if err != nil { + t.Fatal(err) + } + testEchoImpl(t, listener, addr) +} + +func TestEchoTCP(t *testing.T) { + listener, addr, err := plugin.NewTCPListener() + if err != nil { + t.Fatal(err) + } + testEchoImpl(t, listener, addr) +} diff --git a/v2/pipe_not_unix.go b/v2/pipe_not_unix.go index 70469d0..50b120e 100644 --- a/v2/pipe_not_unix.go +++ b/v2/pipe_not_unix.go @@ -3,14 +3,9 @@ package plugin import ( - "fmt" "net" ) func NewListener() (net.Listener, string, error) { - listener, err := net.Listen("tcp", "[::1]:0") - if err != nil { - return nil, "", err - } - return listener, fmt.Sprintf("dns://%s", listener.Addr().String()), nil + return NewTCPListener() } diff --git a/v2/pipe_tcp.go b/v2/pipe_tcp.go new file mode 100644 index 0000000..3170f1f --- /dev/null +++ b/v2/pipe_tcp.go @@ -0,0 +1,14 @@ +package plugin + +import ( + "fmt" + "net" +) + +func NewTCPListener() (net.Listener, string, error) { + listener, err := net.Listen("tcp", "[::1]:0") + if err != nil { + return nil, "", err + } + return listener, fmt.Sprintf("dns:///%s", listener.Addr().String()), nil +} From c55cc24ad19bdd643bd903bb651fd276f6687743 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 20:31:15 -0500 Subject: [PATCH 19/37] create transport package Signed-off-by: eternal-flame-AD --- v2/examples_v1/echo/echo_test.go | 11 ++++++----- v2/examples_v1/minimal/minimal_test.go | 11 ++++++----- v2/shim_v1.go | 7 ++++--- v2/{ => transport}/anon_unix.go | 2 +- v2/{ => transport}/anon_windows.go | 2 +- v2/{ => transport}/pipe_net.go | 2 +- v2/{ => transport}/pipe_net_test.go | 2 +- v2/{ => transport}/pipe_not_unix.go | 2 +- v2/{ => transport}/pipe_tcp.go | 2 +- v2/{ => transport}/pipe_unix.go | 2 +- v2/{ => transport}/transport_auth.go | 2 +- v2/{ => transport}/transport_auth_test.go | 2 +- 12 files changed, 25 insertions(+), 22 deletions(-) rename v2/{ => transport}/anon_unix.go (94%) rename v2/{ => transport}/anon_windows.go (95%) rename v2/{ => transport}/pipe_net.go (97%) rename v2/{ => transport}/pipe_net_test.go (99%) rename v2/{ => transport}/pipe_not_unix.go (86%) rename v2/{ => transport}/pipe_tcp.go (93%) rename v2/{ => transport}/pipe_unix.go (96%) rename v2/{ => transport}/transport_auth.go (99%) rename v2/{ => transport}/transport_auth_test.go (98%) diff --git a/v2/examples_v1/echo/echo_test.go b/v2/examples_v1/echo/echo_test.go index 5ebd5f0..2648721 100644 --- a/v2/examples_v1/echo/echo_test.go +++ b/v2/examples_v1/echo/echo_test.go @@ -15,6 +15,7 @@ import ( papiv1 "github.com/gotify/plugin-api" "github.com/gotify/plugin-api/v2" "github.com/gotify/plugin-api/v2/generated/protobuf" + "github.com/gotify/plugin-api/v2/transport" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/protobuf/types/known/emptypb" @@ -23,7 +24,7 @@ import ( func TestEcho(t *testing.T) { pluginInfo := GetGotifyPluginInfo() - client, err := plugin.NewEphemeralTLSClient() + client, err := transport.NewEphemeralTLSClient() if err != nil { t.Fatal(err) } @@ -31,8 +32,8 @@ func TestEcho(t *testing.T) { var reqRx, reqTx uintptr var respRx, respTx uintptr - plugin.NewAnonPipe(&reqRx, &reqTx, true) - plugin.NewAnonPipe(&respRx, &respTx, true) + transport.NewAnonPipe(&reqRx, &reqTx, true) + transport.NewAnonPipe(&respRx, &respTx, true) go func() { reqFileRx := os.NewFile(reqRx, fmt.Sprintf("/proc/self/fd/%d", reqRx)) @@ -57,7 +58,7 @@ func TestEcho(t *testing.T) { t.Fatal(err) } - listener, addr, err := plugin.NewListener() + listener, addr, err := transport.NewListener() if err != nil { t.Fatal(err) } @@ -175,7 +176,7 @@ func TestEcho(t *testing.T) { t.Fatal("expected ", "Echo plugin running at: https://gotify.example.com/plugin/echo-test/echo", " got ", displayResponse.GetMarkdown()) } - tlsWebhookName := plugin.BuildPluginTLSName(plugin.PurposePluginWebhook, pluginInfo.ModulePath) + tlsWebhookName := transport.BuildPluginTLSName(transport.PurposePluginWebhook, pluginInfo.ModulePath) for range 3 { testreq := httptest.NewRequest("GET", "https://"+tlsWebhookName+"/plugin/echo-test/echo", nil) diff --git a/v2/examples_v1/minimal/minimal_test.go b/v2/examples_v1/minimal/minimal_test.go index e9ed525..2b26257 100644 --- a/v2/examples_v1/minimal/minimal_test.go +++ b/v2/examples_v1/minimal/minimal_test.go @@ -11,6 +11,7 @@ import ( papiv1 "github.com/gotify/plugin-api" "github.com/gotify/plugin-api/v2" "github.com/gotify/plugin-api/v2/generated/protobuf" + "github.com/gotify/plugin-api/v2/transport" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/protobuf/types/known/emptypb" @@ -19,7 +20,7 @@ import ( func testEchoImpl(t *testing.T, listener net.Listener, addr string) { pluginInfo := GetGotifyPluginInfo() - client, err := plugin.NewEphemeralTLSClient() + client, err := transport.NewEphemeralTLSClient() if err != nil { t.Fatal(err) } @@ -27,8 +28,8 @@ func testEchoImpl(t *testing.T, listener net.Listener, addr string) { var reqRx, reqTx uintptr var respRx, respTx uintptr - plugin.NewAnonPipe(&reqRx, &reqTx, true) - plugin.NewAnonPipe(&respRx, &respTx, true) + transport.NewAnonPipe(&reqRx, &reqTx, true) + transport.NewAnonPipe(&respRx, &respTx, true) go func() { reqFileRx := os.NewFile(reqRx, fmt.Sprintf("/proc/self/fd/%d", reqRx)) @@ -109,7 +110,7 @@ func testEchoImpl(t *testing.T, listener net.Listener, addr string) { } func TestEcho(t *testing.T) { - listener, addr, err := plugin.NewListener() + listener, addr, err := transport.NewListener() if err != nil { t.Fatal(err) } @@ -117,7 +118,7 @@ func TestEcho(t *testing.T) { } func TestEchoTCP(t *testing.T) { - listener, addr, err := plugin.NewTCPListener() + listener, addr, err := transport.NewTCPListener() if err != nil { t.Fatal(err) } diff --git a/v2/shim_v1.go b/v2/shim_v1.go index 3ec97d0..6d79082 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -28,6 +28,7 @@ import ( "github.com/gin-gonic/gin" papiv1 "github.com/gotify/plugin-api" "github.com/gotify/plugin-api/v2/generated/protobuf" + "github.com/gotify/plugin-api/v2/transport" ) type GrpcDialer interface { @@ -106,7 +107,7 @@ func NewCompatV1Rpc(compatV1 *CompatV1, cliArgs []string) (*CompatV1Shim, error) } csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ Subject: pkix.Name{ - CommonName: BuildPluginTLSName("*", pluginInfo.ModulePath), + CommonName: transport.BuildPluginTLSName("*", pluginInfo.ModulePath), }, }, priv) @@ -204,7 +205,7 @@ func (h *CompatV1Shim) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - pluginRpcHostName := BuildPluginTLSName(PurposePluginRPC, h.pluginInfo.ModulePath) + pluginRpcHostName := transport.BuildPluginTLSName(transport.PurposePluginRPC, h.pluginInfo.ModulePath) if r.TLS.ServerName == pluginRpcHostName { if r.ProtoMajor != 2 { @@ -220,7 +221,7 @@ func (h *CompatV1Shim) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - pluginWebhookHostName := BuildPluginTLSName(PurposePluginWebhook, h.pluginInfo.ModulePath) + pluginWebhookHostName := transport.BuildPluginTLSName(transport.PurposePluginWebhook, h.pluginInfo.ModulePath) if r.TLS.ServerName == pluginWebhookHostName { h.gin.ServeHTTP(w, r) return diff --git a/v2/anon_unix.go b/v2/transport/anon_unix.go similarity index 94% rename from v2/anon_unix.go rename to v2/transport/anon_unix.go index 122762c..ae80aa7 100644 --- a/v2/anon_unix.go +++ b/v2/transport/anon_unix.go @@ -1,6 +1,6 @@ //go:build unix -package plugin +package transport import ( "golang.org/x/sys/unix" diff --git a/v2/anon_windows.go b/v2/transport/anon_windows.go similarity index 95% rename from v2/anon_windows.go rename to v2/transport/anon_windows.go index 022643e..1431910 100644 --- a/v2/anon_windows.go +++ b/v2/transport/anon_windows.go @@ -1,6 +1,6 @@ //go:build windows -package plugin +package transport import ( "unsafe" diff --git a/v2/pipe_net.go b/v2/transport/pipe_net.go similarity index 97% rename from v2/pipe_net.go rename to v2/transport/pipe_net.go index 9834d3c..331a3f3 100644 --- a/v2/pipe_net.go +++ b/v2/transport/pipe_net.go @@ -1,4 +1,4 @@ -package plugin +package transport import ( "context" diff --git a/v2/pipe_net_test.go b/v2/transport/pipe_net_test.go similarity index 99% rename from v2/pipe_net_test.go rename to v2/transport/pipe_net_test.go index b6d823f..5e33489 100644 --- a/v2/pipe_net_test.go +++ b/v2/transport/pipe_net_test.go @@ -1,4 +1,4 @@ -package plugin +package transport import ( "context" diff --git a/v2/pipe_not_unix.go b/v2/transport/pipe_not_unix.go similarity index 86% rename from v2/pipe_not_unix.go rename to v2/transport/pipe_not_unix.go index 50b120e..88693f6 100644 --- a/v2/pipe_not_unix.go +++ b/v2/transport/pipe_not_unix.go @@ -1,6 +1,6 @@ //go:build !unix -package plugin +package transport import ( "net" diff --git a/v2/pipe_tcp.go b/v2/transport/pipe_tcp.go similarity index 93% rename from v2/pipe_tcp.go rename to v2/transport/pipe_tcp.go index 3170f1f..167b166 100644 --- a/v2/pipe_tcp.go +++ b/v2/transport/pipe_tcp.go @@ -1,4 +1,4 @@ -package plugin +package transport import ( "fmt" diff --git a/v2/pipe_unix.go b/v2/transport/pipe_unix.go similarity index 96% rename from v2/pipe_unix.go rename to v2/transport/pipe_unix.go index 8f93439..d674250 100644 --- a/v2/pipe_unix.go +++ b/v2/transport/pipe_unix.go @@ -1,6 +1,6 @@ //go:build unix -package plugin +package transport import ( "fmt" diff --git a/v2/transport_auth.go b/v2/transport/transport_auth.go similarity index 99% rename from v2/transport_auth.go rename to v2/transport/transport_auth.go index 992450c..4179eb3 100644 --- a/v2/transport_auth.go +++ b/v2/transport/transport_auth.go @@ -1,4 +1,4 @@ -package plugin +package transport import ( "crypto/ed25519" diff --git a/v2/transport_auth_test.go b/v2/transport/transport_auth_test.go similarity index 98% rename from v2/transport_auth_test.go rename to v2/transport/transport_auth_test.go index 830c567..0dd653e 100644 --- a/v2/transport_auth_test.go +++ b/v2/transport/transport_auth_test.go @@ -1,4 +1,4 @@ -package plugin +package transport import ( "crypto/ed25519" From 5555f785b2eb2efe85e492ce5bfbfc98abe491a3 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 24 Aug 2025 20:51:18 -0500 Subject: [PATCH 20/37] add basic pipe test Signed-off-by: eternal-flame-AD --- v2/transport/pipe_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 v2/transport/pipe_test.go diff --git a/v2/transport/pipe_test.go b/v2/transport/pipe_test.go new file mode 100644 index 0000000..9c8ceb5 --- /dev/null +++ b/v2/transport/pipe_test.go @@ -0,0 +1,39 @@ +package transport + +import ( + "net" + "testing" +) + +func TestPipe(t *testing.T) { + listener, _, err := NewListener() + if err != nil { + panic(err) + } + defer listener.Close() + + go func() { + conn, err := net.Dial("unix", listener.Addr().String()) + if err != nil { + panic(err) + } + conn.Write([]byte("test")) + conn.Close() + }() + + accepted, err := listener.Accept() + if err != nil { + panic(err) + } + var buf [1024]byte + n, err := accepted.Read(buf[:]) + if err != nil { + panic(err) + } + if string(buf[:n]) != "test" { + panic("expected test, got " + string(buf[:n])) + } + accepted.Write([]byte("test")) + accepted.Close() + +} From caadabdf3a453bc5ea38a72990f6e2ab3ea39c2d Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 2 Sep 2025 11:10:13 -0500 Subject: [PATCH 21/37] check stream errors in shim Signed-off-by: eternal-flame-AD --- v2/shim_v1.go | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/v2/shim_v1.go b/v2/shim_v1.go index 6d79082..3631fdb 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -409,55 +409,65 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, // enable supported capabilities if _, ok := instance.(papiv1.Displayer); ok { if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_DISPLAYER) { - stream.Send(&protobuf.InstanceUpdate{ + if err := stream.Send(&protobuf.InstanceUpdate{ Update: &protobuf.InstanceUpdate_Capable{ Capable: protobuf.Capability_DISPLAYER, }, - }) + }); err != nil { + return err + } } else { return errors.New("displayer not supported by server but V1 API does not support backwards compatibility") } } if _, ok := instance.(papiv1.Messenger); ok { if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_MESSENGER) { - stream.Send(&protobuf.InstanceUpdate{ + if err := stream.Send(&protobuf.InstanceUpdate{ Update: &protobuf.InstanceUpdate_Capable{ Capable: protobuf.Capability_MESSENGER, }, - }) + }); err != nil { + return err + } } else { return errors.New("messenger not supported by server but V1 API does not support backwards compatibility") } } if _, ok := instance.(papiv1.Configurer); ok { if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_CONFIGURER) { - stream.Send(&protobuf.InstanceUpdate{ + if err := stream.Send(&protobuf.InstanceUpdate{ Update: &protobuf.InstanceUpdate_Capable{ Capable: protobuf.Capability_CONFIGURER, }, - }) + }); err != nil { + return err + } } else { return errors.New("configurer not supported by server but V1 API does not support backwards compatibility") } } if _, ok := instance.(papiv1.Storager); ok { if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_STORAGER) { - stream.Send(&protobuf.InstanceUpdate{ + if err := stream.Send(&protobuf.InstanceUpdate{ Update: &protobuf.InstanceUpdate_Capable{ Capable: protobuf.Capability_STORAGER, }, - }) + }); err != nil { + return err + } } else { return errors.New("storager not supported by server but V1 API does not support backwards compatibility") } } if _, ok := instance.(papiv1.Webhooker); ok { if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_WEBHOOKER) { - stream.Send(&protobuf.InstanceUpdate{ + if err := stream.Send(&protobuf.InstanceUpdate{ Update: &protobuf.InstanceUpdate_Capable{ Capable: protobuf.Capability_WEBHOOKER, }, - }) + }); err != nil { + return err + } } else { return errors.New("webhooker not supported by server but V1 API does not support backwards compatibility") } From 582f2a11177156fdbd9dbc91756bb748bd0c2795 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 2 Sep 2025 11:10:39 -0500 Subject: [PATCH 22/37] change test workflow branch Signed-off-by: eternal-flame-AD --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 36175c7..9f3bc52 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,8 +2,8 @@ name: Test on: push: - branches: - - main + + pull_request: jobs: test: From 8b7a780eb730949925a2764ac2d7ded99925dfc7 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 2 Sep 2025 11:18:04 -0500 Subject: [PATCH 23/37] suggestions in shim_v1.go Signed-off-by: eternal-flame-AD --- v2/shim_v1.go | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/v2/shim_v1.go b/v2/shim_v1.go index 3631fdb..2fb012e 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -35,8 +35,7 @@ type GrpcDialer interface { Dial(ctx context.Context) (*grpc.ClientConn, error) } -// CompatV1 is a shim that acts like a plugin server and delegates request to -// something that implements a V1-style API interface. +// CompatV1 is an API interface that is compatible with the V1 API. type CompatV1 struct { GetPluginInfo func() papiv1.Info GetInstance func(user papiv1.UserContext) (papiv1.Plugin, error) @@ -260,11 +259,14 @@ func (h *shimV1MessageHandler) SendMessage(msg papiv1.Message) error { } type shimV1StorageHandler struct { + mutex *sync.RWMutex currentStorage []byte stream *protobuf.Plugin_RunUserInstanceServer } func (h *shimV1StorageHandler) Save(b []byte) error { + h.mutex.Lock() + defer h.mutex.Unlock() h.currentStorage = slices.Clone(b) return (*h.stream).Send(&protobuf.InstanceUpdate{ Update: &protobuf.InstanceUpdate_Storage{ @@ -274,6 +276,8 @@ func (h *shimV1StorageHandler) Save(b []byte) error { } func (h *shimV1StorageHandler) Load() (b []byte, err error) { + h.mutex.RLock() + defer h.mutex.RUnlock() b = slices.Clone(h.currentStorage) return } @@ -285,6 +289,16 @@ type compatV1ShimServer struct { protobuf.UnimplementedConfigurerServer } +func (s *CompatV1Shim) getInstanceByUserId(userId uint64) (papiv1.Plugin, error) { + s.mu.RLock() + instance, ok := s.instances[userId] + s.mu.RUnlock() + if !ok { + return nil, errors.New("instance not found") + } + return instance, nil +} + func (s *compatV1ShimServer) GetPluginInfo(ctx context.Context, req *emptypb.Empty) (*protobuf.Info, error) { return &protobuf.Info{ Version: s.shim.pluginInfo.Version, @@ -312,11 +326,9 @@ func (s *compatV1ShimServer) SetEnable(ctx context.Context, req *protobuf.SetEna } func (s *compatV1ShimServer) Display(ctx context.Context, req *protobuf.DisplayRequest) (*protobuf.DisplayResponse, error) { - s.shim.mu.RLock() - instance, ok := s.shim.instances[req.User.Id] - s.shim.mu.RUnlock() - if !ok { - return nil, errors.New("instance not found") + instance, err := s.shim.getInstanceByUserId(req.User.Id) + if err != nil { + return nil, err } if displayer, ok := instance.(papiv1.Displayer); ok { location, err := url.Parse(req.Location) @@ -333,11 +345,9 @@ func (s *compatV1ShimServer) Display(ctx context.Context, req *protobuf.DisplayR } func (s *compatV1ShimServer) DefaultConfig(ctx context.Context, req *protobuf.DefaultConfigRequest) (*protobuf.Config, error) { - s.shim.mu.RLock() - instance, ok := s.shim.instances[req.User.Id] - s.shim.mu.RUnlock() - if !ok { - return nil, errors.New("instance not found") + instance, err := s.shim.getInstanceByUserId(req.User.Id) + if err != nil { + return nil, err } if configurer, ok := instance.(papiv1.Configurer); ok { defaultConfig := configurer.DefaultConfig() @@ -353,11 +363,9 @@ func (s *compatV1ShimServer) DefaultConfig(ctx context.Context, req *protobuf.De } func (s *compatV1ShimServer) ValidateAndSetConfig(ctx context.Context, req *protobuf.ValidateAndSetConfigRequest) (*protobuf.ValidateAndSetConfigResponse, error) { - s.shim.mu.RLock() - instance, ok := s.shim.instances[req.User.Id] - s.shim.mu.RUnlock() - if !ok { - return nil, errors.New("instance not found") + instance, err := s.shim.getInstanceByUserId(req.User.Id) + if err != nil { + return nil, err } if configurer, ok := instance.(papiv1.Configurer); ok { currentConfig := configurer.DefaultConfig() @@ -487,7 +495,9 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_CONFIGURER) { currentConfig := configurer.DefaultConfig() if req.Config != nil { - yaml.Unmarshal(req.Config, ¤tConfig) + if err := yaml.Unmarshal(req.Config, ¤tConfig); err != nil { + return err + } if err := configurer.ValidateAndSetConfig(currentConfig); err != nil { return err } @@ -499,6 +509,7 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, if storager, ok := instance.(papiv1.Storager); ok { storageHandler := &shimV1StorageHandler{ + mutex: &sync.RWMutex{}, currentStorage: req.Storage, stream: &stream, } From c09e0d37238051e1f4f3dd87925d3373c46005ef Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 2 Sep 2025 11:20:03 -0500 Subject: [PATCH 24/37] Upgrade to yaml.v3 Signed-off-by: eternal-flame-AD --- v2/go.mod | 3 ++- v2/go.sum | 2 ++ v2/shim_v1.go | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/v2/go.mod b/v2/go.mod index faf1f4c..1b627c8 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -8,7 +8,7 @@ require ( golang.org/x/sys v0.33.0 google.golang.org/grpc v1.74.2 google.golang.org/protobuf v1.36.7 - gopkg.in/yaml.v2 v2.2.2 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -23,4 +23,5 @@ require ( golang.org/x/text v0.25.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect gopkg.in/go-playground/validator.v8 v8.18.2 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/v2/go.sum b/v2/go.sum index 5444529..f8d61bb 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -67,3 +67,5 @@ gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2G gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2/shim_v1.go b/v2/shim_v1.go index 2fb012e..fa42d72 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -23,7 +23,7 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/gin-gonic/gin" papiv1 "github.com/gotify/plugin-api" From 12f5aeb6e9154c44ba29431cccc7610216047b95 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 2 Sep 2025 11:20:43 -0500 Subject: [PATCH 25/37] test typo correction Signed-off-by: eternal-flame-AD --- v2/examples_v1/minimal/minimal_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/v2/examples_v1/minimal/minimal_test.go b/v2/examples_v1/minimal/minimal_test.go index 2b26257..e248877 100644 --- a/v2/examples_v1/minimal/minimal_test.go +++ b/v2/examples_v1/minimal/minimal_test.go @@ -17,7 +17,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" ) -func testEchoImpl(t *testing.T, listener net.Listener, addr string) { +func testMinimalImpl(t *testing.T, listener net.Listener, addr string) { pluginInfo := GetGotifyPluginInfo() client, err := transport.NewEphemeralTLSClient() @@ -109,18 +109,18 @@ func testEchoImpl(t *testing.T, listener net.Listener, addr string) { } } -func TestEcho(t *testing.T) { +func TestMinimal(t *testing.T) { listener, addr, err := transport.NewListener() if err != nil { t.Fatal(err) } - testEchoImpl(t, listener, addr) + testMinimalImpl(t, listener, addr) } -func TestEchoTCP(t *testing.T) { +func TestMinimalTCP(t *testing.T) { listener, addr, err := transport.NewTCPListener() if err != nil { t.Fatal(err) } - testEchoImpl(t, listener, addr) + testMinimalImpl(t, listener, addr) } From eabaf46325f21c7ee42785aee72edcf9bf94722c Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 2 Sep 2025 11:32:05 -0500 Subject: [PATCH 26/37] hoist PEM reading function Signed-off-by: eternal-flame-AD --- v2/shim_v1.go | 44 ++++++++++++++-------------------- v2/transport/pem_file.go | 32 +++++++++++++++++++++++++ v2/transport/transport_auth.go | 33 ++++++++++++------------- 3 files changed, 67 insertions(+), 42 deletions(-) create mode 100644 v2/transport/pem_file.go diff --git a/v2/shim_v1.go b/v2/shim_v1.go index fa42d72..0f8806e 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -10,7 +10,6 @@ import ( "encoding/json" "encoding/pem" "errors" - "io" "log" "math" "net/http" @@ -120,35 +119,28 @@ func NewCompatV1Rpc(compatV1 *CompatV1, cliArgs []string) (*CompatV1Shim, error) return nil, err } - var certBytes []byte var certificateChain []tls.Certificate - for { - var buf [2048]byte - n, err := cliFlags.KexRespFile.Read(buf[:]) - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - certBytes = append(certBytes, buf[:n]...) - for block, rest := pem.Decode(certBytes); block != nil; block, rest = pem.Decode(rest) { - if block.Type == "CERTIFICATE" { - parsedCert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, err - } - // Server signs with IsCA=false, so we can add all of them to the root CA pool without - // trusting things we shouldn't. - rootCAs.AddCert(parsedCert) - certificateChain = append(certificateChain, tls.Certificate{ - Certificate: [][]byte{block.Bytes}, - Leaf: parsedCert, - }) + if err := transport.IteratePEMFile(cliFlags.KexRespFile, func(block *pem.Block) (continueIterate bool, err error) { + if block.Type == "CERTIFICATE" { + parsedCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return false, err } - certBytes = rest + rootCAs.AddCert(parsedCert) + certificateChain = append(certificateChain, tls.Certificate{ + Certificate: [][]byte{block.Bytes}, + Leaf: parsedCert, + }) + return true, nil } + return true, nil + }); err != nil { + return nil, err + } + + if len(certificateChain) == 0 { + return nil, errors.New("no certificate chain found in kex response file") } certificateChain[0].PrivateKey = priv diff --git a/v2/transport/pem_file.go b/v2/transport/pem_file.go new file mode 100644 index 0000000..ef88f5d --- /dev/null +++ b/v2/transport/pem_file.go @@ -0,0 +1,32 @@ +package transport + +import ( + "encoding/pem" + "io" +) + +func IteratePEMFile(r io.Reader, callback func(block *pem.Block) (continueIterate bool, err error)) error { + var bufferBytes []byte + for { + var buf [2048]byte + n, err := r.Read(buf[:]) + if err != nil { + if err == io.EOF { + break + } + return err + } + bufferBytes = append(bufferBytes, buf[:n]...) + + for block, rest := pem.Decode(bufferBytes); block != nil; block, rest = pem.Decode(rest) { + continueIterate, err := callback(block) + if err != nil { + return err + } + if !continueIterate { + return nil + } + } + } + return nil +} diff --git a/v2/transport/transport_auth.go b/v2/transport/transport_auth.go index 4179eb3..c631f39 100644 --- a/v2/transport/transport_auth.go +++ b/v2/transport/transport_auth.go @@ -8,6 +8,7 @@ import ( "crypto/x509/pkix" "encoding/hex" "encoding/pem" + "errors" "fmt" "io" "slices" @@ -102,27 +103,25 @@ func (s *EphemeralTLSClient) SignPluginCSR(moduleName string, csr *x509.Certific func (s *EphemeralTLSClient) Kex(req io.Reader, resp io.Writer) error { var csr *x509.CertificateRequest - var csrBytes []byte - for csr == nil { - var buf [2048]byte - n, err := req.Read(buf[:]) - if err != nil { - return err - } - csrBytes = append(csrBytes, buf[:n]...) - block, _ := pem.Decode(csrBytes) - if block == nil { - continue - } + if err := IteratePEMFile(req, func(block *pem.Block) (continueIterate bool, err error) { if block.Type == "CERTIFICATE REQUEST" { - csrParsed, err := x509.ParseCertificateRequest(block.Bytes) + csr, err = x509.ParseCertificateRequest(block.Bytes) if err != nil { - return err + return false, err } - csr = csrParsed + + return false, nil } + return true, nil + }); err != nil { + return err + } + + if csr == nil { + return errors.New("no certificate request found in kex request file") } + dnsName := csr.Subject.CommonName certBytes, err := s.SignCSR(dnsName, csr) if err != nil { @@ -165,6 +164,9 @@ func NewEphemeralTLSClient() (*EphemeralTLSClient, error) { IsCA: true, } caCertBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, caPub, caPriv) + if err != nil { + return nil, err + } caCert, err := x509.ParseCertificate(caCertBytes) if err != nil { return nil, err @@ -203,7 +205,6 @@ func NewEphemeralTLSClient() (*EphemeralTLSClient, error) { }, { Certificate: [][]byte{caCertBytes}, - PrivateKey: caPriv, }, }, RootCAs: certPool, From 60a3ba0f46c666a0007e480b81c278100f550f5f Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 2 Sep 2025 11:33:06 -0500 Subject: [PATCH 27/37] cli_flags suggestions Signed-off-by: eternal-flame-AD --- v2/cli_flags.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/v2/cli_flags.go b/v2/cli_flags.go index 3d04659..a4aad54 100644 --- a/v2/cli_flags.go +++ b/v2/cli_flags.go @@ -22,7 +22,9 @@ func ParsePluginCLIFlags(args []string) (*PluginCliFlags, error) { flagSet.StringVar(&kexReqFileName, "kex-req-file", "", "File name for the key exchange for Transport Auth. /proc/self/fd/* can be used to open a file descriptor cross platform.") flagSet.StringVar(&kexRespFileName, "kex-resp-file", "", "File name for the key exchange for Transport Auth. /proc/self/fd/* can be used to open a file descriptor cross platform.") flagSet.BoolVar(&debug, "debug", false, "Enable debug mode.") - flagSet.Parse(args) + if err := flagSet.Parse(args); err != nil { + return nil, err + } var kexReqFile *os.File var kexRespFile *os.File @@ -42,10 +44,10 @@ func ParsePluginCLIFlags(args []string) (*PluginCliFlags, error) { } if fdNumber, found := strings.CutPrefix(kexRespFileName, "/proc/self/fd/"); found { fdNumber, err := strconv.ParseUint(fdNumber, 10, 64) - kexRespFile = os.NewFile(uintptr(fdNumber), kexRespFileName) if err != nil { return nil, err } + kexRespFile = os.NewFile(uintptr(fdNumber), kexRespFileName) } else { kexRespFile, err = os.OpenFile(kexRespFileName, os.O_RDONLY, 0) if err != nil { @@ -53,9 +55,6 @@ func ParsePluginCLIFlags(args []string) (*PluginCliFlags, error) { } } - if err != nil { - return nil, err - } return &PluginCliFlags{ flagSet: flagSet, KexReqFile: kexReqFile, From bdd4117d9d5579a6a673e5e0e22d007d60e8fed0 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 2 Sep 2025 11:37:22 -0500 Subject: [PATCH 28/37] try to fix pipe_test on windows Signed-off-by: eternal-flame-AD --- v2/transport/pipe_test.go | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/v2/transport/pipe_test.go b/v2/transport/pipe_test.go index 9c8ceb5..582caef 100644 --- a/v2/transport/pipe_test.go +++ b/v2/transport/pipe_test.go @@ -1,37 +1,55 @@ package transport import ( + "log" "net" + "net/url" "testing" ) func TestPipe(t *testing.T) { - listener, _, err := NewListener() + listener, addrURL, err := NewListener() if err != nil { - panic(err) + t.Fatalf("failed to create listener: %v", err) } defer listener.Close() + urlParsed, err := url.Parse(addrURL) + if err != nil { + t.Fatalf("failed to parse address URL: %v", err) + } + + var family string + if urlParsed.Scheme == "unix" { + family = "unix" + } else if urlParsed.Scheme == "dns" { + family = "tcp" + } else { + t.Fatalf("unsupported address URL scheme: %s", urlParsed.Scheme) + } + go func() { - conn, err := net.Dial("unix", listener.Addr().String()) + conn, err := net.Dial(family, listener.Addr().String()) if err != nil { - panic(err) + log.Panicf("failed to dial listener: %v", err) + } + if _, err := conn.Write([]byte("test")); err != nil { + log.Panicf("failed to write to listener: %v", err) } - conn.Write([]byte("test")) conn.Close() }() accepted, err := listener.Accept() if err != nil { - panic(err) + t.Fatalf("failed to accept listener: %v", err) } var buf [1024]byte n, err := accepted.Read(buf[:]) if err != nil { - panic(err) + t.Fatalf("failed to read from listener: %v", err) } if string(buf[:n]) != "test" { - panic("expected test, got " + string(buf[:n])) + t.Fatalf("expected test, got %s", string(buf[:n])) } accepted.Write([]byte("test")) accepted.Close() From 4fee5973d9b0b7ffbfb44028bc7f577322e96acf Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 2 Sep 2025 11:38:06 -0500 Subject: [PATCH 29/37] Try to fix pipe_test on windows Signed-off-by: eternal-flame-AD --- v2/transport/pipe_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/v2/transport/pipe_test.go b/v2/transport/pipe_test.go index 582caef..100f510 100644 --- a/v2/transport/pipe_test.go +++ b/v2/transport/pipe_test.go @@ -20,11 +20,13 @@ func TestPipe(t *testing.T) { } var family string - if urlParsed.Scheme == "unix" { + + switch urlParsed.Scheme { + case "unix": family = "unix" - } else if urlParsed.Scheme == "dns" { + case "dns": family = "tcp" - } else { + default: t.Fatalf("unsupported address URL scheme: %s", urlParsed.Scheme) } From b493e1cc1916342ec69a474de596b331d190194e Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 2 Sep 2025 12:46:37 -0500 Subject: [PATCH 30/37] remove unused certificates and TLS configs Signed-off-by: eternal-flame-AD --- v2/transport/transport_auth.go | 48 +++------------------------------- 1 file changed, 4 insertions(+), 44 deletions(-) diff --git a/v2/transport/transport_auth.go b/v2/transport/transport_auth.go index c631f39..2b78ee8 100644 --- a/v2/transport/transport_auth.go +++ b/v2/transport/transport_auth.go @@ -33,9 +33,8 @@ func BuildPluginTLSName(purpose string, moduleName string) string { } type EphemeralTLSClient struct { - caCert *x509.Certificate - caPriv ed25519.PrivateKey - tlsConfig *tls.Config + caCert *x509.Certificate + caPriv ed25519.PrivateKey } func (s *EphemeralTLSClient) createCertPool() *x509.CertPool { @@ -171,47 +170,8 @@ func NewEphemeralTLSClient() (*EphemeralTLSClient, error) { if err != nil { return nil, err } - clientPub, clientPriv, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - return nil, err - } - clientCertTemplate := &x509.Certificate{ - BasicConstraintsValid: true, - Subject: pkix.Name{ - CommonName: ServerTLSName, - }, - DNSNames: []string{ - ServerTLSName, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour * 24 * 365), - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageClientAuth, - }, - IsCA: false, - } - clientCertBytes, err := x509.CreateCertificate(rand.Reader, clientCertTemplate, caCert, clientPub, caPriv) - if err != nil { - return nil, err - } - certPool := x509.NewCertPool() - certPool.AddCert(caCert) - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{ - { - Certificate: [][]byte{clientCertBytes}, - PrivateKey: clientPriv, - }, - { - Certificate: [][]byte{caCertBytes}, - }, - }, - RootCAs: certPool, - } return &EphemeralTLSClient{ - caCert: caCert, - caPriv: caPriv, - tlsConfig: tlsConfig, + caCert: caCert, + caPriv: caPriv, }, nil } From cd7743e0f83b54c1e59bcedfe3cceb40d1da8c9f Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 2 Sep 2025 12:47:54 -0500 Subject: [PATCH 31/37] use IPv4 loopback address Signed-off-by: eternal-flame-AD --- v2/transport/pipe_tcp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/transport/pipe_tcp.go b/v2/transport/pipe_tcp.go index 167b166..68552db 100644 --- a/v2/transport/pipe_tcp.go +++ b/v2/transport/pipe_tcp.go @@ -6,7 +6,7 @@ import ( ) func NewTCPListener() (net.Listener, string, error) { - listener, err := net.Listen("tcp", "[::1]:0") + listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, "", err } From 35d54d4f3b141785acef44461314aa1f366f66cf Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Tue, 2 Sep 2025 12:49:22 -0500 Subject: [PATCH 32/37] rename ServerVersionInfo -> ServerInfo Signed-off-by: eternal-flame-AD --- v2/examples_v1/echo/echo_test.go | 2 +- v2/generated/protobuf/config.pb.go | 2 +- v2/generated/protobuf/config_grpc.pb.go | 2 +- v2/generated/protobuf/display.pb.go | 2 +- v2/generated/protobuf/display_grpc.pb.go | 2 +- v2/generated/protobuf/meta.pb.go | 53 ++++++++++--------- v2/generated/protobuf/meta_grpc.pb.go | 2 +- v2/generated/protobuf/server_events.pb.go | 2 +- .../protobuf/server_events_grpc.pb.go | 2 +- v2/protobuf/meta.proto | 6 +-- v2/shim_v1.go | 14 ++--- v2/transport/pipe_net_test.go | 4 +- 12 files changed, 48 insertions(+), 45 deletions(-) diff --git a/v2/examples_v1/echo/echo_test.go b/v2/examples_v1/echo/echo_test.go index 2648721..c4edb63 100644 --- a/v2/examples_v1/echo/echo_test.go +++ b/v2/examples_v1/echo/echo_test.go @@ -95,7 +95,7 @@ func TestEcho(t *testing.T) { Name: testUser.Name, Admin: testUser.Admin, }, - ServerVersion: &protobuf.ServerVersionInfo{ + ServerInfo: &protobuf.ServerInfo{ Version: "1.0.0", Capabilities: []protobuf.Capability{ protobuf.Capability_DISPLAYER, diff --git a/v2/generated/protobuf/config.pb.go b/v2/generated/protobuf/config.pb.go index acee500..3911362 100644 --- a/v2/generated/protobuf/config.pb.go +++ b/v2/generated/protobuf/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.7 -// protoc v6.31.1 +// protoc v6.32.0 // source: config.proto package protobuf diff --git a/v2/generated/protobuf/config_grpc.pb.go b/v2/generated/protobuf/config_grpc.pb.go index b0cb128..b8f7af1 100644 --- a/v2/generated/protobuf/config_grpc.pb.go +++ b/v2/generated/protobuf/config_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v6.31.1 +// - protoc v6.32.0 // source: config.proto package protobuf diff --git a/v2/generated/protobuf/display.pb.go b/v2/generated/protobuf/display.pb.go index fd80206..6348688 100644 --- a/v2/generated/protobuf/display.pb.go +++ b/v2/generated/protobuf/display.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.7 -// protoc v6.31.1 +// protoc v6.32.0 // source: display.proto package protobuf diff --git a/v2/generated/protobuf/display_grpc.pb.go b/v2/generated/protobuf/display_grpc.pb.go index 30bf8ca..f13dc36 100644 --- a/v2/generated/protobuf/display_grpc.pb.go +++ b/v2/generated/protobuf/display_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v6.31.1 +// - protoc v6.32.0 // source: display.proto package protobuf diff --git a/v2/generated/protobuf/meta.pb.go b/v2/generated/protobuf/meta.pb.go index 7ad7a95..f81beca 100644 --- a/v2/generated/protobuf/meta.pb.go +++ b/v2/generated/protobuf/meta.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.7 -// protoc v6.31.1 +// protoc v6.32.0 // source: meta.proto package protobuf @@ -647,8 +647,8 @@ func (*InstanceUpdate_Storage) isInstanceUpdate_Update() {} type UserInstanceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // the server version info - ServerVersion *ServerVersionInfo `protobuf:"bytes,1,opt,name=serverVersion,proto3" json:"serverVersion,omitempty"` + // the server info + ServerInfo *ServerInfo `protobuf:"bytes,1,opt,name=serverInfo,proto3" json:"serverInfo,omitempty"` // the user context User *UserContext `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"` // the webhook base path @@ -691,9 +691,9 @@ func (*UserInstanceRequest) Descriptor() ([]byte, []int) { return file_meta_proto_rawDescGZIP(), []int{8} } -func (x *UserInstanceRequest) GetServerVersion() *ServerVersionInfo { +func (x *UserInstanceRequest) GetServerInfo() *ServerInfo { if x != nil { - return x.ServerVersion + return x.ServerInfo } return nil } @@ -726,7 +726,7 @@ func (x *UserInstanceRequest) GetStorage() []byte { return nil } -type ServerVersionInfo struct { +type ServerInfo struct { state protoimpl.MessageState `protogen:"open.v1"` Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` Commit string `protobuf:"bytes,2,opt,name=commit,proto3" json:"commit,omitempty"` @@ -737,20 +737,20 @@ type ServerVersionInfo struct { sizeCache protoimpl.SizeCache } -func (x *ServerVersionInfo) Reset() { - *x = ServerVersionInfo{} +func (x *ServerInfo) Reset() { + *x = ServerInfo{} mi := &file_meta_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ServerVersionInfo) String() string { +func (x *ServerInfo) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ServerVersionInfo) ProtoMessage() {} +func (*ServerInfo) ProtoMessage() {} -func (x *ServerVersionInfo) ProtoReflect() protoreflect.Message { +func (x *ServerInfo) ProtoReflect() protoreflect.Message { mi := &file_meta_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -762,33 +762,33 @@ func (x *ServerVersionInfo) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ServerVersionInfo.ProtoReflect.Descriptor instead. -func (*ServerVersionInfo) Descriptor() ([]byte, []int) { +// Deprecated: Use ServerInfo.ProtoReflect.Descriptor instead. +func (*ServerInfo) Descriptor() ([]byte, []int) { return file_meta_proto_rawDescGZIP(), []int{9} } -func (x *ServerVersionInfo) GetVersion() string { +func (x *ServerInfo) GetVersion() string { if x != nil { return x.Version } return "" } -func (x *ServerVersionInfo) GetCommit() string { +func (x *ServerInfo) GetCommit() string { if x != nil { return x.Commit } return "" } -func (x *ServerVersionInfo) GetBuildDate() string { +func (x *ServerInfo) GetBuildDate() string { if x != nil { return x.BuildDate } return "" } -func (x *ServerVersionInfo) GetCapabilities() []Capability { +func (x *ServerInfo) GetCapabilities() []Capability { if x != nil { return x.Capabilities } @@ -848,9 +848,11 @@ const file_meta_proto_rawDesc = "" + "\acapable\x18\x01 \x01(\x0e2\v.CapabilityH\x00R\acapable\x12$\n" + "\amessage\x18\x02 \x01(\v2\b.MessageH\x00R\amessage\x12\x1a\n" + "\astorage\x18\x03 \x01(\fH\x00R\astorageB\b\n" + - "\x06update\"\x87\x02\n" + - "\x13UserInstanceRequest\x128\n" + - "\rserverVersion\x18\x01 \x01(\v2\x12.ServerVersionInfoR\rserverVersion\x12 \n" + + "\x06update\"\xfa\x01\n" + + "\x13UserInstanceRequest\x12+\n" + + "\n" + + "serverInfo\x18\x01 \x01(\v2\v.ServerInfoR\n" + + "serverInfo\x12 \n" + "\x04user\x18\x02 \x01(\v2\f.UserContextR\x04user\x12-\n" + "\x0fwebhookBasePath\x18\x03 \x01(\tH\x00R\x0fwebhookBasePath\x88\x01\x01\x12\x1b\n" + "\x06config\x18\x04 \x01(\fH\x01R\x06config\x88\x01\x01\x12\x1d\n" + @@ -858,8 +860,9 @@ const file_meta_proto_rawDesc = "" + "\x10_webhookBasePathB\t\n" + "\a_configB\n" + "\n" + - "\b_storage\"\x94\x01\n" + - "\x11ServerVersionInfo\x12\x18\n" + + "\b_storage\"\x8d\x01\n" + + "\n" + + "ServerInfo\x12\x18\n" + "\aversion\x18\x01 \x01(\tR\aversion\x12\x16\n" + "\x06commit\x18\x02 \x01(\tR\x06commit\x12\x1c\n" + "\tbuildDate\x18\x03 \x01(\tR\tbuildDate\x12/\n" + @@ -903,7 +906,7 @@ var file_meta_proto_goTypes = []any{ (*SetEnableRequest)(nil), // 7: SetEnableRequest (*InstanceUpdate)(nil), // 8: InstanceUpdate (*UserInstanceRequest)(nil), // 9: UserInstanceRequest - (*ServerVersionInfo)(nil), // 10: ServerVersionInfo + (*ServerInfo)(nil), // 10: ServerInfo nil, // 11: Message.ExtrasEntry (*anypb.Any)(nil), // 12: google.protobuf.Any (*emptypb.Empty)(nil), // 13: google.protobuf.Empty @@ -915,9 +918,9 @@ var file_meta_proto_depIdxs = []int32{ 2, // 3: SetEnableRequest.user:type_name -> UserContext 0, // 4: InstanceUpdate.capable:type_name -> Capability 6, // 5: InstanceUpdate.message:type_name -> Message - 10, // 6: UserInstanceRequest.serverVersion:type_name -> ServerVersionInfo + 10, // 6: UserInstanceRequest.serverInfo:type_name -> ServerInfo 2, // 7: UserInstanceRequest.user:type_name -> UserContext - 0, // 8: ServerVersionInfo.capabilities:type_name -> Capability + 0, // 8: ServerInfo.capabilities:type_name -> Capability 5, // 9: Message.ExtrasEntry.value:type_name -> ExtrasValue 13, // 10: Plugin.GetPluginInfo:input_type -> google.protobuf.Empty 7, // 11: Plugin.SetEnable:input_type -> SetEnableRequest diff --git a/v2/generated/protobuf/meta_grpc.pb.go b/v2/generated/protobuf/meta_grpc.pb.go index 7405648..6f0f8f7 100644 --- a/v2/generated/protobuf/meta_grpc.pb.go +++ b/v2/generated/protobuf/meta_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v6.31.1 +// - protoc v6.32.0 // source: meta.proto package protobuf diff --git a/v2/generated/protobuf/server_events.pb.go b/v2/generated/protobuf/server_events.pb.go index 35c4634..8fb6d39 100644 --- a/v2/generated/protobuf/server_events.pb.go +++ b/v2/generated/protobuf/server_events.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.7 -// protoc v6.31.1 +// protoc v6.32.0 // source: server_events.proto package protobuf diff --git a/v2/generated/protobuf/server_events_grpc.pb.go b/v2/generated/protobuf/server_events_grpc.pb.go index 9a88b41..3768f4a 100644 --- a/v2/generated/protobuf/server_events_grpc.pb.go +++ b/v2/generated/protobuf/server_events_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v6.31.1 +// - protoc v6.32.0 // source: server_events.proto package protobuf diff --git a/v2/protobuf/meta.proto b/v2/protobuf/meta.proto index ed3a9a0..a2803a9 100644 --- a/v2/protobuf/meta.proto +++ b/v2/protobuf/meta.proto @@ -71,8 +71,8 @@ message InstanceUpdate { } message UserInstanceRequest { - // the server version info - ServerVersionInfo serverVersion = 1; + // the server info + ServerInfo serverInfo = 1; // the user context UserContext user = 2; // the webhook base path @@ -83,7 +83,7 @@ message UserInstanceRequest { optional bytes storage = 5; } -message ServerVersionInfo { +message ServerInfo { string version = 1; string commit = 2; string buildDate = 3; diff --git a/v2/shim_v1.go b/v2/shim_v1.go index 0f8806e..2d09485 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -408,7 +408,7 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, // enable supported capabilities if _, ok := instance.(papiv1.Displayer); ok { - if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_DISPLAYER) { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_DISPLAYER) { if err := stream.Send(&protobuf.InstanceUpdate{ Update: &protobuf.InstanceUpdate_Capable{ Capable: protobuf.Capability_DISPLAYER, @@ -421,7 +421,7 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, } } if _, ok := instance.(papiv1.Messenger); ok { - if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_MESSENGER) { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_MESSENGER) { if err := stream.Send(&protobuf.InstanceUpdate{ Update: &protobuf.InstanceUpdate_Capable{ Capable: protobuf.Capability_MESSENGER, @@ -434,7 +434,7 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, } } if _, ok := instance.(papiv1.Configurer); ok { - if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_CONFIGURER) { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_CONFIGURER) { if err := stream.Send(&protobuf.InstanceUpdate{ Update: &protobuf.InstanceUpdate_Capable{ Capable: protobuf.Capability_CONFIGURER, @@ -447,7 +447,7 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, } } if _, ok := instance.(papiv1.Storager); ok { - if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_STORAGER) { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_STORAGER) { if err := stream.Send(&protobuf.InstanceUpdate{ Update: &protobuf.InstanceUpdate_Capable{ Capable: protobuf.Capability_STORAGER, @@ -460,7 +460,7 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, } } if _, ok := instance.(papiv1.Webhooker); ok { - if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_WEBHOOKER) { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_WEBHOOKER) { if err := stream.Send(&protobuf.InstanceUpdate{ Update: &protobuf.InstanceUpdate_Capable{ Capable: protobuf.Capability_WEBHOOKER, @@ -474,7 +474,7 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, } if messenger, ok := instance.(papiv1.Messenger); ok { - if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_MESSENGER) { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_MESSENGER) { messenger.SetMessageHandler(&shimV1MessageHandler{ stream: &stream, }) @@ -484,7 +484,7 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, } if configurer, ok := instance.(papiv1.Configurer); ok { - if slices.Contains(req.ServerVersion.Capabilities, protobuf.Capability_CONFIGURER) { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_CONFIGURER) { currentConfig := configurer.DefaultConfig() if req.Config != nil { if err := yaml.Unmarshal(req.Config, ¤tConfig); err != nil { diff --git a/v2/transport/pipe_net_test.go b/v2/transport/pipe_net_test.go index 5e33489..425dbbd 100644 --- a/v2/transport/pipe_net_test.go +++ b/v2/transport/pipe_net_test.go @@ -28,8 +28,8 @@ func (s *dummyInfraServer) GetPluginInfo(ctx context.Context, req *emptypb.Empty }, nil } -func (s *dummyInfraServer) GetServerVersion(ctx context.Context, req *emptypb.Empty) (*protobuf.ServerVersionInfo, error) { - return &protobuf.ServerVersionInfo{ +func (s *dummyInfraServer) GetServerVersion(ctx context.Context, req *emptypb.Empty) (*protobuf.ServerInfo, error) { + return &protobuf.ServerInfo{ Version: "test", Commit: "test", BuildDate: time.Now().Format(time.RFC3339), From 7822885663592ad349860d6b6b3ae5c5721fe30f Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Wed, 3 Sep 2025 01:52:40 -0500 Subject: [PATCH 33/37] allow passing kex file descriptors through environment variables Signed-off-by: eternal-flame-AD --- v2/cli_flags.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/v2/cli_flags.go b/v2/cli_flags.go index a4aad54..9ada2d2 100644 --- a/v2/cli_flags.go +++ b/v2/cli_flags.go @@ -3,6 +3,7 @@ package plugin import ( "flag" "os" + "slices" "strconv" "strings" ) @@ -19,9 +20,9 @@ func ParsePluginCLIFlags(args []string) (*PluginCliFlags, error) { var kexReqFileName string var kexRespFileName string var debug bool - flagSet.StringVar(&kexReqFileName, "kex-req-file", "", "File name for the key exchange for Transport Auth. /proc/self/fd/* can be used to open a file descriptor cross platform.") - flagSet.StringVar(&kexRespFileName, "kex-resp-file", "", "File name for the key exchange for Transport Auth. /proc/self/fd/* can be used to open a file descriptor cross platform.") - flagSet.BoolVar(&debug, "debug", false, "Enable debug mode.") + flagSet.StringVar(&kexReqFileName, "kex-req-file", os.Getenv("GOTIFY_PLUGIN_KEX_REQ_FILE"), "File name for the key exchange for Transport Auth. /proc/self/fd/* can be used to open a file descriptor cross platform.") + flagSet.StringVar(&kexRespFileName, "kex-resp-file", os.Getenv("GOTIFY_PLUGIN_KEX_RESP_FILE"), "File name for the key exchange for Transport Auth. /proc/self/fd/* can be used to open a file descriptor cross platform.") + flagSet.BoolVar(&debug, "debug", slices.Contains([]string{"true", "1", "yes", "y"}, strings.ToLower(os.Getenv("GOTIFY_PLUGIN_DEBUG"))), "Enable debug mode.") if err := flagSet.Parse(args); err != nil { return nil, err } From 11e6d505e2df9b5e96e6f930da3507a2cb243f16 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Wed, 3 Sep 2025 02:56:59 -0500 Subject: [PATCH 34/37] remove SetEnable RPC Signed-off-by: eternal-flame-AD --- v2/examples_v1/minimal/minimal.go | 6 +- v2/examples_v1/minimal/minimal_test.go | 81 ++++--- v2/generated/protobuf/meta.pb.go | 160 ++++++-------- v2/generated/protobuf/meta_grpc.pb.go | 40 ---- v2/go.mod | 3 + v2/go.sum | 6 +- v2/protobuf/meta.proto | 16 +- v2/shim_v1.go | 280 ++++++++++++++----------- 8 files changed, 296 insertions(+), 296 deletions(-) diff --git a/v2/examples_v1/minimal/minimal.go b/v2/examples_v1/minimal/minimal.go index 7dbbde6..fb0a4e8 100644 --- a/v2/examples_v1/minimal/minimal.go +++ b/v2/examples_v1/minimal/minimal.go @@ -13,15 +13,19 @@ func GetGotifyPluginInfo() plugin.Info { } // Plugin is plugin instance -type Plugin struct{} +type Plugin struct { + enabled bool +} // Enable implements plugin.Plugin func (c *Plugin) Enable() error { + c.enabled = true return nil } // Disable implements plugin.Plugin func (c *Plugin) Disable() error { + c.enabled = false return nil } diff --git a/v2/examples_v1/minimal/minimal_test.go b/v2/examples_v1/minimal/minimal_test.go index e248877..d8b366d 100644 --- a/v2/examples_v1/minimal/minimal_test.go +++ b/v2/examples_v1/minimal/minimal_test.go @@ -7,17 +7,22 @@ import ( "net" "os" "testing" + "time" papiv1 "github.com/gotify/plugin-api" "github.com/gotify/plugin-api/v2" "github.com/gotify/plugin-api/v2/generated/protobuf" "github.com/gotify/plugin-api/v2/transport" + "github.com/stretchr/testify/assert" "google.golang.org/grpc" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/keepalive" "google.golang.org/protobuf/types/known/emptypb" ) func testMinimalImpl(t *testing.T, listener net.Listener, addr string) { + assert := assert.New(t) + pluginInfo := GetGotifyPluginInfo() client, err := transport.NewEphemeralTLSClient() @@ -39,53 +44,63 @@ func testMinimalImpl(t *testing.T, listener net.Listener, addr string) { client.Kex(reqFileRx, respFileTx) }() + var thisInstance *Plugin + instanceInitialized := make(chan struct{}) + compatV1, err := plugin.NewCompatV1Rpc(&plugin.CompatV1{ GetPluginInfo: GetGotifyPluginInfo, GetInstance: func(user papiv1.UserContext) (papiv1.Plugin, error) { - return NewGotifyPluginInstance(user), nil + thisInstance = NewGotifyPluginInstance(user).(*Plugin) + defer close(instanceInitialized) + return thisInstance, nil }, }, []string{ "-kex-req-file", fmt.Sprintf("/proc/self/fd/%d", reqTx), "-kex-resp-file", fmt.Sprintf("/proc/self/fd/%d", respRx), }) - if err != nil { - t.Fatal(err) - } + assert.NoError(err) go func() { compatV1.ServeTLS(listener, "", "") }() - rpcClient, err := grpc.NewClient(addr, grpc.WithTransportCredentials(credentials.NewTLS(client.ClientTLSConfig(pluginInfo.ModulePath)))) - if err != nil { - t.Fatal(err) - } + rpcClient, err := grpc.NewClient(addr, grpc.WithTransportCredentials(credentials.NewTLS(client.ClientTLSConfig(pluginInfo.ModulePath))), grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: 10 * time.Millisecond, + PermitWithoutStream: true, + })) + assert.NoError(err) pluginClient := protobuf.NewPluginClient(rpcClient) version, err := pluginClient.GetPluginInfo(context.Background(), &emptypb.Empty{}) - if err != nil { - t.Fatal(err) - } - if version.Name != pluginInfo.Name { - t.Fatal("expected ", pluginInfo.Name, " got ", version.Name) - } - if version.Version != pluginInfo.Version { - t.Fatal("expected ", pluginInfo.Version, " got ", version.Version) - } + assert.NoError(err) + assert.Equal(pluginInfo.Name, version.Name) + assert.Equal(pluginInfo.Version, version.Version) - pluginClient.SetEnable(context.Background(), &protobuf.SetEnableRequest{ + stream, err := pluginClient.RunUserInstance(context.Background(), &protobuf.UserInstanceRequest{ User: &protobuf.UserContext{ Id: uint64(1), Name: "test", Admin: false, }, - Enable: true, }) - if err != nil { - t.Fatal(err) + assert.NoError(err) + + assert.NoError(stream.CloseSend()) + <-instanceInitialized + assert.True(thisInstance.enabled, "plugin should be enabled after connect") + + pluginClient.GracefulShutdown(context.Background(), &emptypb.Empty{}) + for { + _, err := stream.Recv() + if err != nil { + if err == io.EOF { + break + } + t.Fatal(err) + } } - stream, err := pluginClient.RunUserInstance(context.Background(), &protobuf.UserInstanceRequest{ + streamHang, err := pluginClient.RunUserInstance(context.Background(), &protobuf.UserInstanceRequest{ User: &protobuf.UserContext{ Id: uint64(1), Name: "test", @@ -95,16 +110,32 @@ func testMinimalImpl(t *testing.T, listener net.Listener, addr string) { if err != nil { t.Fatal(err) } + if err := streamHang.CloseSend(); err != nil { + t.Fatal(err) + } + _, err = streamHang.Recv() + assert.Error(err, "expected error when not sending keepalive") + assert.False(thisInstance.enabled, "plugin should be disabled after hang") + streamReentrant, err := pluginClient.RunUserInstance(context.Background(), &protobuf.UserInstanceRequest{ + User: &protobuf.UserContext{ + Id: uint64(1), + Name: "test", + Admin: false, + }, + }) + assert.NoError(err) pluginClient.GracefulShutdown(context.Background(), &emptypb.Empty{}) - stream.CloseSend() + if err := streamReentrant.CloseSend(); err != nil { + assert.NoError(err) + } for { - _, err := stream.Recv() + _, err := streamReentrant.Recv() if err != nil { if err == io.EOF { break } - t.Fatal(err) + assert.NoError(err) } } } diff --git a/v2/generated/protobuf/meta.pb.go b/v2/generated/protobuf/meta.pb.go index f81beca..d263d5e 100644 --- a/v2/generated/protobuf/meta.pb.go +++ b/v2/generated/protobuf/meta.pb.go @@ -492,62 +492,11 @@ func (x *Message) GetExtras() map[string]*ExtrasValue { return nil } -type SetEnableRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - User *UserContext `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` - Enable bool `protobuf:"varint,2,opt,name=enable,proto3" json:"enable,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SetEnableRequest) Reset() { - *x = SetEnableRequest{} - mi := &file_meta_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SetEnableRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SetEnableRequest) ProtoMessage() {} - -func (x *SetEnableRequest) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[6] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SetEnableRequest.ProtoReflect.Descriptor instead. -func (*SetEnableRequest) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{6} -} - -func (x *SetEnableRequest) GetUser() *UserContext { - if x != nil { - return x.User - } - return nil -} - -func (x *SetEnableRequest) GetEnable() bool { - if x != nil { - return x.Enable - } - return false -} - type InstanceUpdate struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Update: // + // *InstanceUpdate_Ping // *InstanceUpdate_Capable // *InstanceUpdate_Message // *InstanceUpdate_Storage @@ -558,7 +507,7 @@ type InstanceUpdate struct { func (x *InstanceUpdate) Reset() { *x = InstanceUpdate{} - mi := &file_meta_proto_msgTypes[7] + mi := &file_meta_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -570,7 +519,7 @@ func (x *InstanceUpdate) String() string { func (*InstanceUpdate) ProtoMessage() {} func (x *InstanceUpdate) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[7] + mi := &file_meta_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -583,7 +532,7 @@ func (x *InstanceUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use InstanceUpdate.ProtoReflect.Descriptor instead. func (*InstanceUpdate) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{7} + return file_meta_proto_rawDescGZIP(), []int{6} } func (x *InstanceUpdate) GetUpdate() isInstanceUpdate_Update { @@ -593,6 +542,15 @@ func (x *InstanceUpdate) GetUpdate() isInstanceUpdate_Update { return nil } +func (x *InstanceUpdate) GetPing() *emptypb.Empty { + if x != nil { + if x, ok := x.Update.(*InstanceUpdate_Ping); ok { + return x.Ping + } + } + return nil +} + func (x *InstanceUpdate) GetCapable() Capability { if x != nil { if x, ok := x.Update.(*InstanceUpdate_Capable); ok { @@ -624,21 +582,28 @@ type isInstanceUpdate_Update interface { isInstanceUpdate_Update() } +type InstanceUpdate_Ping struct { + // ping the server to keep the connection alive + Ping *emptypb.Empty `protobuf:"bytes,1,opt,name=ping,proto3,oneof"` +} + type InstanceUpdate_Capable struct { // enable support for a feature, must be one of the capabilities supported by the server - Capable Capability `protobuf:"varint,1,opt,name=capable,proto3,enum=Capability,oneof"` + Capable Capability `protobuf:"varint,2,opt,name=capable,proto3,enum=Capability,oneof"` } type InstanceUpdate_Message struct { // send a message to the user - Message *Message `protobuf:"bytes,2,opt,name=message,proto3,oneof"` + Message *Message `protobuf:"bytes,3,opt,name=message,proto3,oneof"` } type InstanceUpdate_Storage struct { // update persistent storage - Storage []byte `protobuf:"bytes,3,opt,name=storage,proto3,oneof"` + Storage []byte `protobuf:"bytes,4,opt,name=storage,proto3,oneof"` } +func (*InstanceUpdate_Ping) isInstanceUpdate_Update() {} + func (*InstanceUpdate_Capable) isInstanceUpdate_Update() {} func (*InstanceUpdate_Message) isInstanceUpdate_Update() {} @@ -663,7 +628,7 @@ type UserInstanceRequest struct { func (x *UserInstanceRequest) Reset() { *x = UserInstanceRequest{} - mi := &file_meta_proto_msgTypes[8] + mi := &file_meta_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -675,7 +640,7 @@ func (x *UserInstanceRequest) String() string { func (*UserInstanceRequest) ProtoMessage() {} func (x *UserInstanceRequest) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[8] + mi := &file_meta_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -688,7 +653,7 @@ func (x *UserInstanceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UserInstanceRequest.ProtoReflect.Descriptor instead. func (*UserInstanceRequest) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{8} + return file_meta_proto_rawDescGZIP(), []int{7} } func (x *UserInstanceRequest) GetServerInfo() *ServerInfo { @@ -739,7 +704,7 @@ type ServerInfo struct { func (x *ServerInfo) Reset() { *x = ServerInfo{} - mi := &file_meta_proto_msgTypes[9] + mi := &file_meta_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -751,7 +716,7 @@ func (x *ServerInfo) String() string { func (*ServerInfo) ProtoMessage() {} func (x *ServerInfo) ProtoReflect() protoreflect.Message { - mi := &file_meta_proto_msgTypes[9] + mi := &file_meta_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -764,7 +729,7 @@ func (x *ServerInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerInfo.ProtoReflect.Descriptor instead. func (*ServerInfo) Descriptor() ([]byte, []int) { - return file_meta_proto_rawDescGZIP(), []int{9} + return file_meta_proto_rawDescGZIP(), []int{8} } func (x *ServerInfo) GetVersion() string { @@ -840,14 +805,12 @@ const file_meta_proto_rawDesc = "" + "\x06extras\x18\x04 \x03(\v2\x14.Message.ExtrasEntryR\x06extras\x1aG\n" + "\vExtrasEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\"\n" + - "\x05value\x18\x02 \x01(\v2\f.ExtrasValueR\x05value:\x028\x01\"L\n" + - "\x10SetEnableRequest\x12 \n" + - "\x04user\x18\x01 \x01(\v2\f.UserContextR\x04user\x12\x16\n" + - "\x06enable\x18\x02 \x01(\bR\x06enable\"\x85\x01\n" + - "\x0eInstanceUpdate\x12'\n" + - "\acapable\x18\x01 \x01(\x0e2\v.CapabilityH\x00R\acapable\x12$\n" + - "\amessage\x18\x02 \x01(\v2\b.MessageH\x00R\amessage\x12\x1a\n" + - "\astorage\x18\x03 \x01(\fH\x00R\astorageB\b\n" + + "\x05value\x18\x02 \x01(\v2\f.ExtrasValueR\x05value:\x028\x01\"\xb3\x01\n" + + "\x0eInstanceUpdate\x12,\n" + + "\x04ping\x18\x01 \x01(\v2\x16.google.protobuf.EmptyH\x00R\x04ping\x12'\n" + + "\acapable\x18\x02 \x01(\x0e2\v.CapabilityH\x00R\acapable\x12$\n" + + "\amessage\x18\x03 \x01(\v2\b.MessageH\x00R\amessage\x12\x1a\n" + + "\astorage\x18\x04 \x01(\fH\x00R\astorageB\b\n" + "\x06update\"\xfa\x01\n" + "\x13UserInstanceRequest\x12+\n" + "\n" + @@ -874,10 +837,9 @@ const file_meta_proto_rawDesc = "" + "\n" + "CONFIGURER\x10\x02\x12\f\n" + "\bSTORAGER\x10\x03\x12\r\n" + - "\tWEBHOOKER\x10\x042\xf0\x01\n" + + "\tWEBHOOKER\x10\x042\xb8\x01\n" + "\x06Plugin\x12.\n" + - "\rGetPluginInfo\x12\x16.google.protobuf.Empty\x1a\x05.Info\x126\n" + - "\tSetEnable\x12\x11.SetEnableRequest\x1a\x16.google.protobuf.Empty\x12B\n" + + "\rGetPluginInfo\x12\x16.google.protobuf.Empty\x1a\x05.Info\x12B\n" + "\x10GracefulShutdown\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12:\n" + "\x0fRunUserInstance\x12\x14.UserInstanceRequest\x1a\x0f.InstanceUpdate0\x01B\x16Z\x14./generated/protobufb\x06proto3" @@ -894,7 +856,7 @@ func file_meta_proto_rawDescGZIP() []byte { } var file_meta_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_meta_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_meta_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_meta_proto_goTypes = []any{ (Capability)(0), // 0: Capability (*Error)(nil), // 1: Error @@ -903,35 +865,32 @@ var file_meta_proto_goTypes = []any{ (*Info)(nil), // 4: Info (*ExtrasValue)(nil), // 5: ExtrasValue (*Message)(nil), // 6: Message - (*SetEnableRequest)(nil), // 7: SetEnableRequest - (*InstanceUpdate)(nil), // 8: InstanceUpdate - (*UserInstanceRequest)(nil), // 9: UserInstanceRequest - (*ServerInfo)(nil), // 10: ServerInfo - nil, // 11: Message.ExtrasEntry - (*anypb.Any)(nil), // 12: google.protobuf.Any - (*emptypb.Empty)(nil), // 13: google.protobuf.Empty + (*InstanceUpdate)(nil), // 7: InstanceUpdate + (*UserInstanceRequest)(nil), // 8: UserInstanceRequest + (*ServerInfo)(nil), // 9: ServerInfo + nil, // 10: Message.ExtrasEntry + (*anypb.Any)(nil), // 11: google.protobuf.Any + (*emptypb.Empty)(nil), // 12: google.protobuf.Empty } var file_meta_proto_depIdxs = []int32{ - 12, // 0: Error.details:type_name -> google.protobuf.Any + 11, // 0: Error.details:type_name -> google.protobuf.Any 3, // 1: Info.capabilities:type_name -> Capabilities - 11, // 2: Message.extras:type_name -> Message.ExtrasEntry - 2, // 3: SetEnableRequest.user:type_name -> UserContext + 10, // 2: Message.extras:type_name -> Message.ExtrasEntry + 12, // 3: InstanceUpdate.ping:type_name -> google.protobuf.Empty 0, // 4: InstanceUpdate.capable:type_name -> Capability 6, // 5: InstanceUpdate.message:type_name -> Message - 10, // 6: UserInstanceRequest.serverInfo:type_name -> ServerInfo + 9, // 6: UserInstanceRequest.serverInfo:type_name -> ServerInfo 2, // 7: UserInstanceRequest.user:type_name -> UserContext 0, // 8: ServerInfo.capabilities:type_name -> Capability 5, // 9: Message.ExtrasEntry.value:type_name -> ExtrasValue - 13, // 10: Plugin.GetPluginInfo:input_type -> google.protobuf.Empty - 7, // 11: Plugin.SetEnable:input_type -> SetEnableRequest - 13, // 12: Plugin.GracefulShutdown:input_type -> google.protobuf.Empty - 9, // 13: Plugin.RunUserInstance:input_type -> UserInstanceRequest - 4, // 14: Plugin.GetPluginInfo:output_type -> Info - 13, // 15: Plugin.SetEnable:output_type -> google.protobuf.Empty - 13, // 16: Plugin.GracefulShutdown:output_type -> google.protobuf.Empty - 8, // 17: Plugin.RunUserInstance:output_type -> InstanceUpdate - 14, // [14:18] is the sub-list for method output_type - 10, // [10:14] is the sub-list for method input_type + 12, // 10: Plugin.GetPluginInfo:input_type -> google.protobuf.Empty + 12, // 11: Plugin.GracefulShutdown:input_type -> google.protobuf.Empty + 8, // 12: Plugin.RunUserInstance:input_type -> UserInstanceRequest + 4, // 13: Plugin.GetPluginInfo:output_type -> Info + 12, // 14: Plugin.GracefulShutdown:output_type -> google.protobuf.Empty + 7, // 15: Plugin.RunUserInstance:output_type -> InstanceUpdate + 13, // [13:16] is the sub-list for method output_type + 10, // [10:13] is the sub-list for method input_type 10, // [10:10] is the sub-list for extension type_name 10, // [10:10] is the sub-list for extension extendee 0, // [0:10] is the sub-list for field type_name @@ -946,19 +905,20 @@ func file_meta_proto_init() { file_meta_proto_msgTypes[4].OneofWrappers = []any{ (*ExtrasValue_Json)(nil), } - file_meta_proto_msgTypes[7].OneofWrappers = []any{ + file_meta_proto_msgTypes[6].OneofWrappers = []any{ + (*InstanceUpdate_Ping)(nil), (*InstanceUpdate_Capable)(nil), (*InstanceUpdate_Message)(nil), (*InstanceUpdate_Storage)(nil), } - file_meta_proto_msgTypes[8].OneofWrappers = []any{} + file_meta_proto_msgTypes[7].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_meta_proto_rawDesc), len(file_meta_proto_rawDesc)), NumEnums: 1, - NumMessages: 11, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, diff --git a/v2/generated/protobuf/meta_grpc.pb.go b/v2/generated/protobuf/meta_grpc.pb.go index 6f0f8f7..dec3a4e 100644 --- a/v2/generated/protobuf/meta_grpc.pb.go +++ b/v2/generated/protobuf/meta_grpc.pb.go @@ -21,7 +21,6 @@ const _ = grpc.SupportPackageIsVersion9 const ( Plugin_GetPluginInfo_FullMethodName = "/Plugin/GetPluginInfo" - Plugin_SetEnable_FullMethodName = "/Plugin/SetEnable" Plugin_GracefulShutdown_FullMethodName = "/Plugin/GracefulShutdown" Plugin_RunUserInstance_FullMethodName = "/Plugin/RunUserInstance" ) @@ -35,8 +34,6 @@ const ( type PluginClient interface { // get the plugin info GetPluginInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) - // set the enable state of a plugin instance - SetEnable(ctx context.Context, in *SetEnableRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // graceful shutdown GracefulShutdown(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) // run a user instance @@ -61,16 +58,6 @@ func (c *pluginClient) GetPluginInfo(ctx context.Context, in *emptypb.Empty, opt return out, nil } -func (c *pluginClient) SetEnable(ctx context.Context, in *SetEnableRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, Plugin_SetEnable_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *pluginClient) GracefulShutdown(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) @@ -109,8 +96,6 @@ type Plugin_RunUserInstanceClient = grpc.ServerStreamingClient[InstanceUpdate] type PluginServer interface { // get the plugin info GetPluginInfo(context.Context, *emptypb.Empty) (*Info, error) - // set the enable state of a plugin instance - SetEnable(context.Context, *SetEnableRequest) (*emptypb.Empty, error) // graceful shutdown GracefulShutdown(context.Context, *emptypb.Empty) (*emptypb.Empty, error) // run a user instance @@ -128,9 +113,6 @@ type UnimplementedPluginServer struct{} func (UnimplementedPluginServer) GetPluginInfo(context.Context, *emptypb.Empty) (*Info, error) { return nil, status.Errorf(codes.Unimplemented, "method GetPluginInfo not implemented") } -func (UnimplementedPluginServer) SetEnable(context.Context, *SetEnableRequest) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method SetEnable not implemented") -} func (UnimplementedPluginServer) GracefulShutdown(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method GracefulShutdown not implemented") } @@ -176,24 +158,6 @@ func _Plugin_GetPluginInfo_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } -func _Plugin_SetEnable_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(SetEnableRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(PluginServer).SetEnable(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Plugin_SetEnable_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(PluginServer).SetEnable(ctx, req.(*SetEnableRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _Plugin_GracefulShutdown_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { @@ -234,10 +198,6 @@ var Plugin_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetPluginInfo", Handler: _Plugin_GetPluginInfo_Handler, }, - { - MethodName: "SetEnable", - Handler: _Plugin_SetEnable_Handler, - }, { MethodName: "GracefulShutdown", Handler: _Plugin_GracefulShutdown_Handler, diff --git a/v2/go.mod b/v2/go.mod index 1b627c8..28a6d5c 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -5,6 +5,7 @@ go 1.24.5 require ( github.com/gin-gonic/gin v1.3.0 github.com/gotify/plugin-api v1.0.0 + github.com/stretchr/testify v1.11.1 golang.org/x/sys v0.33.0 google.golang.org/grpc v1.74.2 google.golang.org/protobuf v1.36.7 @@ -12,12 +13,14 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/json-iterator/go v1.1.5 // indirect github.com/mattn/go-isatty v0.0.4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/text v0.25.0 // indirect diff --git a/v2/go.sum b/v2/go.sum index f8d61bb..0b00e57 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,5 +1,6 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= @@ -28,8 +29,9 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4= github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= diff --git a/v2/protobuf/meta.proto b/v2/protobuf/meta.proto index a2803a9..cdd6479 100644 --- a/v2/protobuf/meta.proto +++ b/v2/protobuf/meta.proto @@ -46,11 +46,6 @@ message Message { map extras = 4; } -message SetEnableRequest { - UserContext user = 1; - bool enable = 2; -} - enum Capability { DISPLAYER = 0; MESSENGER = 1; @@ -61,12 +56,14 @@ enum Capability { message InstanceUpdate { oneof update { + // ping the server to keep the connection alive + google.protobuf.Empty ping = 1; // enable support for a feature, must be one of the capabilities supported by the server - Capability capable = 1; + Capability capable = 2; // send a message to the user - Message message = 2; + Message message = 3; // update persistent storage - bytes storage = 3; + bytes storage = 4; } } @@ -97,9 +94,6 @@ service Plugin { // get the plugin info rpc GetPluginInfo(google.protobuf.Empty) returns (Info); - // set the enable state of a plugin instance - rpc SetEnable(SetEnableRequest) returns (google.protobuf.Empty); - // graceful shutdown rpc GracefulShutdown(google.protobuf.Empty) returns (google.protobuf.Empty); diff --git a/v2/shim_v1.go b/v2/shim_v1.go index 2d09485..6c33e20 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -19,8 +19,11 @@ import ( "slices" "strings" "sync" + "testing" + "time" "google.golang.org/grpc" + "google.golang.org/grpc/keepalive" "google.golang.org/protobuf/types/known/emptypb" "gopkg.in/yaml.v3" @@ -34,6 +37,16 @@ type GrpcDialer interface { Dial(ctx context.Context) (*grpc.ClientConn, error) } +var ( + httpTimeout = 10 * time.Second +) + +func init() { + if testing.Testing() { + httpTimeout = 100 * time.Millisecond + } +} + // CompatV1 is an API interface that is compatible with the V1 API. type CompatV1 struct { GetPluginInfo func() papiv1.Info @@ -152,7 +165,10 @@ func NewCompatV1Rpc(compatV1 *CompatV1, cliArgs []string) (*CompatV1Shim, error) ClientCAs: rootCAs, } - rpcServer := grpc.NewServer() + rpcServer := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ + MinTime: httpTimeout, + PermitWithoutStream: true, + }), grpc.ConnectionTimeout(httpTimeout)) if !cliFlags.Debug { gin.SetMode(gin.ReleaseMode) } @@ -182,9 +198,12 @@ func NewCompatV1Rpc(compatV1 *CompatV1, cliArgs []string) (*CompatV1Shim, error) protocols.SetHTTP1(true) protocols.SetHTTP2(true) self.Server = http.Server{ - Handler: self, - TLSConfig: tlsConfig, - Protocols: protocols, + Handler: self, + TLSConfig: tlsConfig, + Protocols: protocols, + ReadTimeout: httpTimeout, + ReadHeaderTimeout: httpTimeout, + WriteTimeout: httpTimeout, } return self, nil @@ -303,20 +322,6 @@ func (s *compatV1ShimServer) GetPluginInfo(ctx context.Context, req *emptypb.Emp }, nil } -func (s *compatV1ShimServer) SetEnable(ctx context.Context, req *protobuf.SetEnableRequest) (*emptypb.Empty, error) { - s.shim.mu.RLock() - instance, ok := s.shim.instances[req.User.Id] - s.shim.mu.RUnlock() - if !ok { - return nil, errors.New("instance not found") - } - if req.Enable { - return new(emptypb.Empty), instance.Enable() - } else { - return new(emptypb.Empty), instance.Disable() - } -} - func (s *compatV1ShimServer) Display(ctx context.Context, req *protobuf.DisplayRequest) (*protobuf.DisplayResponse, error) { instance, err := s.shim.getInstanceByUserId(req.User.Id) if err != nil { @@ -390,135 +395,176 @@ func (s *compatV1ShimServer) GracefulShutdown(ctx context.Context, req *emptypb. s.shim.shutdownOnce.Do(func() { close(s.shim.shutdown) }) - return new(emptypb.Empty), nil + return &emptypb.Empty{}, nil } func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, stream protobuf.Plugin_RunUserInstanceServer) error { if req.User.Id > math.MaxUint { return errors.New("user id is too large") } - instance, err := s.shim.compatV1.GetInstance(papiv1.UserContext{ - ID: uint(req.User.Id), - Name: req.User.Name, - Admin: req.User.Admin, + + unlockOnce := new(sync.Once) + + s.shim.mu.Lock() + + defer unlockOnce.Do(func() { + s.shim.mu.Unlock() }) - if err != nil { - return err - } - // enable supported capabilities - if _, ok := instance.(papiv1.Displayer); ok { - if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_DISPLAYER) { - if err := stream.Send(&protobuf.InstanceUpdate{ - Update: &protobuf.InstanceUpdate_Capable{ - Capable: protobuf.Capability_DISPLAYER, - }, - }); err != nil { - return err + instance, alreadyRunning := s.shim.instances[req.User.Id] + + if !alreadyRunning { + var err error + instance, err = s.shim.compatV1.GetInstance(papiv1.UserContext{ + ID: uint(req.User.Id), + Name: req.User.Name, + Admin: req.User.Admin, + }) + if err != nil { + return err + } + + // enable supported capabilities + if _, ok := instance.(papiv1.Displayer); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_DISPLAYER) { + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_DISPLAYER, + }, + }); err != nil { + return err + } + } else { + return errors.New("displayer not supported by server but V1 API does not support backwards compatibility") } - } else { - return errors.New("displayer not supported by server but V1 API does not support backwards compatibility") } - } - if _, ok := instance.(papiv1.Messenger); ok { - if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_MESSENGER) { - if err := stream.Send(&protobuf.InstanceUpdate{ - Update: &protobuf.InstanceUpdate_Capable{ - Capable: protobuf.Capability_MESSENGER, - }, - }); err != nil { - return err + if _, ok := instance.(papiv1.Messenger); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_MESSENGER) { + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_MESSENGER, + }, + }); err != nil { + return err + } + } else { + return errors.New("messenger not supported by server but V1 API does not support backwards compatibility") } - } else { - return errors.New("messenger not supported by server but V1 API does not support backwards compatibility") } - } - if _, ok := instance.(papiv1.Configurer); ok { - if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_CONFIGURER) { - if err := stream.Send(&protobuf.InstanceUpdate{ - Update: &protobuf.InstanceUpdate_Capable{ - Capable: protobuf.Capability_CONFIGURER, - }, - }); err != nil { - return err + if _, ok := instance.(papiv1.Configurer); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_CONFIGURER) { + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_CONFIGURER, + }, + }); err != nil { + return err + } + } else { + return errors.New("configurer not supported by server but V1 API does not support backwards compatibility") } - } else { - return errors.New("configurer not supported by server but V1 API does not support backwards compatibility") } - } - if _, ok := instance.(papiv1.Storager); ok { - if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_STORAGER) { - if err := stream.Send(&protobuf.InstanceUpdate{ - Update: &protobuf.InstanceUpdate_Capable{ - Capable: protobuf.Capability_STORAGER, - }, - }); err != nil { - return err + if _, ok := instance.(papiv1.Storager); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_STORAGER) { + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_STORAGER, + }, + }); err != nil { + return err + } + } else { + return errors.New("storager not supported by server but V1 API does not support backwards compatibility") } - } else { - return errors.New("storager not supported by server but V1 API does not support backwards compatibility") } - } - if _, ok := instance.(papiv1.Webhooker); ok { - if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_WEBHOOKER) { - if err := stream.Send(&protobuf.InstanceUpdate{ - Update: &protobuf.InstanceUpdate_Capable{ - Capable: protobuf.Capability_WEBHOOKER, - }, - }); err != nil { - return err + if _, ok := instance.(papiv1.Webhooker); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_WEBHOOKER) { + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: protobuf.Capability_WEBHOOKER, + }, + }); err != nil { + return err + } + } else { + return errors.New("webhooker not supported by server but V1 API does not support backwards compatibility") } - } else { - return errors.New("webhooker not supported by server but V1 API does not support backwards compatibility") } - } - if messenger, ok := instance.(papiv1.Messenger); ok { - if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_MESSENGER) { - messenger.SetMessageHandler(&shimV1MessageHandler{ - stream: &stream, - }) - } else { - return errors.New("messenger not supported by server but V1 API does not support backwards compatibility") + if messenger, ok := instance.(papiv1.Messenger); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_MESSENGER) { + messenger.SetMessageHandler(&shimV1MessageHandler{ + stream: &stream, + }) + } else { + return errors.New("messenger not supported by server but V1 API does not support backwards compatibility") + } } - } - if configurer, ok := instance.(papiv1.Configurer); ok { - if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_CONFIGURER) { - currentConfig := configurer.DefaultConfig() - if req.Config != nil { - if err := yaml.Unmarshal(req.Config, ¤tConfig); err != nil { - return err - } - if err := configurer.ValidateAndSetConfig(currentConfig); err != nil { - return err + if configurer, ok := instance.(papiv1.Configurer); ok { + if slices.Contains(req.ServerInfo.Capabilities, protobuf.Capability_CONFIGURER) { + currentConfig := configurer.DefaultConfig() + if req.Config != nil { + if err := yaml.Unmarshal(req.Config, ¤tConfig); err != nil { + return err + } + if err := configurer.ValidateAndSetConfig(currentConfig); err != nil { + return err + } } + } else { + return errors.New("configurer not supported by server but V1 API does not support backwards compatibility") } - } else { - return errors.New("configurer not supported by server but V1 API does not support backwards compatibility") } - } - if storager, ok := instance.(papiv1.Storager); ok { - storageHandler := &shimV1StorageHandler{ - mutex: &sync.RWMutex{}, - currentStorage: req.Storage, - stream: &stream, + if storager, ok := instance.(papiv1.Storager); ok { + storageHandler := &shimV1StorageHandler{ + mutex: &sync.RWMutex{}, + currentStorage: req.Storage, + stream: &stream, + } + storager.SetStorageHandler(storageHandler) } - storager.SetStorageHandler(storageHandler) - } - if webhooker, ok := instance.(papiv1.Webhooker); ok { - if req.WebhookBasePath != nil { - group := s.shim.gin.Group(*req.WebhookBasePath) - webhooker.RegisterWebhook(*req.WebhookBasePath, group) + if webhooker, ok := instance.(papiv1.Webhooker); ok { + if req.WebhookBasePath != nil { + group := s.shim.gin.Group(*req.WebhookBasePath) + webhooker.RegisterWebhook(*req.WebhookBasePath, group) + } } } - s.shim.mu.Lock() + if err := instance.Enable(); err != nil { + return err + } + + defer instance.Disable() + s.shim.instances[req.User.Id] = instance - s.shim.mu.Unlock() + unlockOnce.Do(func() { + s.shim.mu.Unlock() + }) + + ticker := time.NewTicker(15 * time.Second) - <-s.shim.shutdown - return nil + if testing.Testing() { + ticker.Stop() + ticker = time.NewTicker(5 * time.Millisecond) + } + + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Ping{ + Ping: new(emptypb.Empty), + }, + }); err != nil { + return err + } + case <-s.shim.shutdown: + return nil + } + } } From dd63957fb784a0a0b16e5c0e6c744ecc097a56b9 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Wed, 3 Sep 2025 02:58:42 -0500 Subject: [PATCH 35/37] compute ping rate Signed-off-by: eternal-flame-AD --- v2/shim_v1.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/v2/shim_v1.go b/v2/shim_v1.go index 6c33e20..02f9c6f 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -39,11 +39,13 @@ type GrpcDialer interface { var ( httpTimeout = 10 * time.Second + pingRate = 4 * time.Second ) func init() { if testing.Testing() { httpTimeout = 100 * time.Millisecond + pingRate = 10 * time.Millisecond } } @@ -545,12 +547,7 @@ func (s *compatV1ShimServer) RunUserInstance(req *protobuf.UserInstanceRequest, s.shim.mu.Unlock() }) - ticker := time.NewTicker(15 * time.Second) - - if testing.Testing() { - ticker.Stop() - ticker = time.NewTicker(5 * time.Millisecond) - } + ticker := time.NewTicker(pingRate) defer ticker.Stop() for { From abf55e80c0b9320a42e788546144fb956a1b0efa Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Wed, 3 Sep 2025 16:23:03 -0500 Subject: [PATCH 36/37] hoise kex logic to cli Signed-off-by: eternal-flame-AD --- v2/{cli_flags.go => cli.go} | 72 ++++++++++++++++++++++++++++++++++--- v2/shim_v1.go | 58 +++--------------------------- 2 files changed, 72 insertions(+), 58 deletions(-) rename v2/{cli_flags.go => cli.go} (50%) diff --git a/v2/cli_flags.go b/v2/cli.go similarity index 50% rename from v2/cli_flags.go rename to v2/cli.go index 9ada2d2..3f77faf 100644 --- a/v2/cli_flags.go +++ b/v2/cli.go @@ -1,21 +1,32 @@ package plugin import ( + "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" "flag" "os" "slices" "strconv" "strings" + + "github.com/gotify/plugin-api/v2/transport" ) -type PluginCliFlags struct { +// / PluginCli implements the CLI interface for a Gotify plugin. +type PluginCli struct { flagSet *flag.FlagSet KexReqFile *os.File KexRespFile *os.File Debug bool } -func ParsePluginCLIFlags(args []string) (*PluginCliFlags, error) { +// ParsePluginCli parses the CLI arguments and returns a PluginCli instance. +func ParsePluginCli(args []string) (*PluginCli, error) { flagSet := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) var kexReqFileName string var kexRespFileName string @@ -56,7 +67,7 @@ func ParsePluginCLIFlags(args []string) (*PluginCliFlags, error) { } } - return &PluginCliFlags{ + return &PluginCli{ flagSet: flagSet, KexReqFile: kexReqFile, KexRespFile: kexRespFile, @@ -64,7 +75,60 @@ func ParsePluginCLIFlags(args []string) (*PluginCliFlags, error) { }, nil } -func (f *PluginCliFlags) Close() error { +// Kex performs the key exchange through secure file descriptors provided in the arguments. +func (f *PluginCli) Kex(modulePath string, certPool *x509.CertPool) (certChain []tls.Certificate, err error) { + // perform key exchange through secure file descriptors + _, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: transport.BuildPluginTLSName("*", modulePath), + }, + }, priv) + + if err != nil { + return nil, err + } + if _, err := f.KexReqFile.Write(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: csrBytes, + })); err != nil { + return nil, err + } + + var certificateChain []tls.Certificate + + if err := transport.IteratePEMFile(f.KexRespFile, func(block *pem.Block) (continueIterate bool, err error) { + if block.Type == "CERTIFICATE" { + parsedCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return false, err + } + certPool.AddCert(parsedCert) + certificateChain = append(certificateChain, tls.Certificate{ + Certificate: [][]byte{block.Bytes}, + Leaf: parsedCert, + }) + return true, nil + } + return true, nil + }); err != nil { + return nil, err + } + + if len(certificateChain) == 0 { + return nil, errors.New("no certificate chain found in kex response file") + } + + certificateChain[0].PrivateKey = priv + + return certificateChain, nil +} + +// Close closes any file descriptors associated with the PluginCli instance. +func (f *PluginCli) Close() error { if err := f.KexReqFile.Close(); err != nil { return err } diff --git a/v2/shim_v1.go b/v2/shim_v1.go index 02f9c6f..53dc22b 100644 --- a/v2/shim_v1.go +++ b/v2/shim_v1.go @@ -2,13 +2,9 @@ package plugin import ( "context" - "crypto/ed25519" - "crypto/rand" "crypto/tls" "crypto/x509" - "crypto/x509/pkix" "encoding/json" - "encoding/pem" "errors" "log" "math" @@ -105,60 +101,14 @@ type CompatV1Shim struct { func NewCompatV1Rpc(compatV1 *CompatV1, cliArgs []string) (*CompatV1Shim, error) { pluginInfo := compatV1.GetPluginInfo() - cliFlags, err := ParsePluginCLIFlags(cliArgs) + cli, err := ParsePluginCli(cliArgs) if err != nil { log.Fatalf("Failed to parse CLI flags: %v", err) } - defer cliFlags.Close() + defer cli.Close() rootCAs := x509.NewCertPool() - - // perform key exchange through secure file descriptors - _, priv, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - return nil, err - } - csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: transport.BuildPluginTLSName("*", pluginInfo.ModulePath), - }, - }, priv) - - if err != nil { - return nil, err - } - if _, err := cliFlags.KexReqFile.Write(pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE REQUEST", - Bytes: csrBytes, - })); err != nil { - return nil, err - } - - var certificateChain []tls.Certificate - - if err := transport.IteratePEMFile(cliFlags.KexRespFile, func(block *pem.Block) (continueIterate bool, err error) { - if block.Type == "CERTIFICATE" { - parsedCert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return false, err - } - rootCAs.AddCert(parsedCert) - certificateChain = append(certificateChain, tls.Certificate{ - Certificate: [][]byte{block.Bytes}, - Leaf: parsedCert, - }) - return true, nil - } - return true, nil - }); err != nil { - return nil, err - } - - if len(certificateChain) == 0 { - return nil, errors.New("no certificate chain found in kex response file") - } - - certificateChain[0].PrivateKey = priv + certificateChain, err := cli.Kex(pluginInfo.ModulePath, rootCAs) tlsConfig := &tls.Config{ Certificates: certificateChain, @@ -171,7 +121,7 @@ func NewCompatV1Rpc(compatV1 *CompatV1, cliArgs []string) (*CompatV1Shim, error) MinTime: httpTimeout, PermitWithoutStream: true, }), grpc.ConnectionTimeout(httpTimeout)) - if !cliFlags.Debug { + if !cli.Debug { gin.SetMode(gin.ReleaseMode) } From bfe8e528137f8b47b969955d56b5ad6b05793cf0 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Wed, 3 Sep 2025 16:28:10 -0500 Subject: [PATCH 37/37] check for nil rootCAs Signed-off-by: eternal-flame-AD --- v2/cli.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/v2/cli.go b/v2/cli.go index 3f77faf..977718e 100644 --- a/v2/cli.go +++ b/v2/cli.go @@ -106,7 +106,9 @@ func (f *PluginCli) Kex(modulePath string, certPool *x509.CertPool) (certChain [ if err != nil { return false, err } - certPool.AddCert(parsedCert) + if certPool != nil { + certPool.AddCert(parsedCert) + } certificateChain = append(certificateChain, tls.Certificate{ Certificate: [][]byte{block.Bytes}, Leaf: parsedCert,