From 8ec181c18c6da83c4db0e5d71efac587f86993d4 Mon Sep 17 00:00:00 2001 From: Vlad Vitan Date: Wed, 28 Aug 2024 16:57:54 +0200 Subject: [PATCH] qrg: Implement grpc methods for gateway qr code parser service --- pkg/qrcodegenerator/grpc_gateways.go | 75 ++++++++++ pkg/qrcodegenerator/grpc_gateways_test.go | 130 ++++++++++++++++++ .../qrcode/gateways/gateways.go | 45 ++++-- .../qrcode/gateways/ttigpro1.go | 10 +- .../qrcode/gateways/ttigpro1_test.go | 2 +- pkg/qrcodegenerator/qrcodegenerator.go | 10 ++ 6 files changed, 258 insertions(+), 14 deletions(-) create mode 100644 pkg/qrcodegenerator/grpc_gateways.go create mode 100644 pkg/qrcodegenerator/grpc_gateways_test.go diff --git a/pkg/qrcodegenerator/grpc_gateways.go b/pkg/qrcodegenerator/grpc_gateways.go new file mode 100644 index 00000000000..aa78473f59d --- /dev/null +++ b/pkg/qrcodegenerator/grpc_gateways.go @@ -0,0 +1,75 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package qrcodegenerator + +import ( + "context" + "errors" + + "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/gateways" + "go.thethings.network/lorawan-stack/v3/pkg/rpcmetadata" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" +) + +var errCorruptedData = errors.New("corrupted data") + +type gatewayQRCodeGeneratorServer struct { + ttnpb.UnimplementedGatewayQRCodeGeneratorServer + + QRG *QRCodeGenerator +} + +// GetFormat implements EndDeviceQRCodeGenerator. +func (s *gatewayQRCodeGeneratorServer) GetFormat(ctx context.Context, req *ttnpb.GetQRCodeFormatRequest) (*ttnpb.QRCodeFormat, error) { + _, err := rpcmetadata.WithForwardedAuth(ctx, s.QRG.AllowInsecureForCredentials()) + if err != nil { + return nil, err + } + format := s.QRG.gateways.GetGatewayFormat(req.FormatId) + if format == nil { + return nil, errFormatNotFound.New() + } + return format.Format(), nil +} + +// Parse implements EndDeviceQRCodeGenerator. +func (s *gatewayQRCodeGeneratorServer) Parse(ctx context.Context, req *ttnpb.ParseGatewayQRCodeRequest) (*ttnpb.ParseGatewayQRCodeResponse, error) { + _, err := rpcmetadata.WithForwardedAuth(ctx, s.QRG.AllowInsecureForCredentials()) + if err != nil { + return nil, err + } + + data, err := s.QRG.gateways.Parse(req.FormatId, req.QrCode) + if err != nil { + return nil, err + } + + ttigpro1Data, ok := data.(*gateways.TTIgpro1) + if !ok { + return nil, errCorruptedData + } + + return &ttnpb.ParseGatewayQRCodeResponse{ + FormatId: data.FormatID(), + ClaimGatewayRequest: &ttnpb.ClaimGatewayRequest{ + SourceGateway: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers_{ + AuthenticatedIdentifiers: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers{ + GatewayEui: ttigpro1Data.GtwEUI[:], + AuthenticationCode: ttigpro1Data.OwnerToken, + }, + }, + }, + }, nil +} diff --git a/pkg/qrcodegenerator/grpc_gateways_test.go b/pkg/qrcodegenerator/grpc_gateways_test.go new file mode 100644 index 00000000000..33878f5c682 --- /dev/null +++ b/pkg/qrcodegenerator/grpc_gateways_test.go @@ -0,0 +1,130 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package qrcodegenerator_test + +import ( + "testing" + + "github.com/smarty/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/component" + componenttest "go.thethings.network/lorawan-stack/v3/pkg/component/test" + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/log" + . "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator" + "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/gateways" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/util/test" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" +) + +func TestGatewayQRCodeParsing(t *testing.T) { + a := assertions.New(t) + ctx := log.NewContext(test.Context(), test.GetLogger(t)) + + c := componenttest.NewComponent(t, &component.Config{}) + ttigpro1 := new(gateways.TTIgpro1Format) + qrg, err := New(c, &Config{}, WithGatewayFormat(ttigpro1.ID(), ttigpro1)) + test.Must(qrg, err) + + componenttest.StartComponent(t, c) + defer c.Close() + + mustHavePeer(ctx, c, ttnpb.ClusterRole_QR_CODE_GENERATOR) + + client := ttnpb.NewGatewayQRCodeGeneratorClient(c.LoopbackConn()) + + for _, tc := range []struct { + Name string + FormatID string + GetQRData func() []byte + Assertion func(*assertions.Assertion, *ttnpb.ParseGatewayQRCodeResponse, error) bool + }{ + { + Name: "EmptyData", + GetQRData: func() []byte { + return []byte{} + }, + Assertion: func(a *assertions.Assertion, resp *ttnpb.ParseGatewayQRCodeResponse, err error) bool { + if !a.So(resp, should.BeNil) { + return false + } + if !a.So(errors.IsInvalidArgument(err), should.BeTrue) { + return false + } + return true + }, + }, + { + Name: "UnknownFormat", + FormatID: "unknown", + GetQRData: func() []byte { + return []byte(`https://ttig.pro/c/ec656efffe000128/abcdef123456`) + }, + Assertion: func(a *assertions.Assertion, resp *ttnpb.ParseGatewayQRCodeResponse, err error) bool { + if !a.So(resp, should.BeNil) { + return false + } + if !a.So(errors.IsInvalidArgument(err), should.BeTrue) { + return false + } + return true + }, + }, + { + Name: "InvalidFormat", + FormatID: "tr005", + GetQRData: func() []byte { + return []byte(`https://ttig.pro/c/ec656efffe000128/abcdef123456`) + }, + Assertion: func(a *assertions.Assertion, resp *ttnpb.ParseGatewayQRCodeResponse, err error) bool { + if !a.So(resp, should.BeNil) { + return false + } + if !a.So(errors.IsInvalidArgument(err), should.BeTrue) { + return false + } + return true + }, + }, + { + Name: "ValidTTIgpro1", + FormatID: ttigpro1.ID(), + GetQRData: func() []byte { + return []byte(`https://ttig.pro/c/ec656efffe000128/abcdef123456`) + }, + Assertion: func(a *assertions.Assertion, resp *ttnpb.ParseGatewayQRCodeResponse, err error) bool { + if !a.So(resp, should.NotBeNil) { + return false + } + if !a.So(err, should.BeNil) { + return false + } + a.So(resp.FormatId, should.Equal, ttigpro1.ID()) + + return true + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + resp, err := client.Parse(ctx, &ttnpb.ParseGatewayQRCodeRequest{ + FormatId: tc.FormatID, + QrCode: tc.GetQRData(), + }, c.WithClusterAuth()) + if !a.So(tc.Assertion(a, resp, err), should.BeTrue) { + t.FailNow() + } + }) + } +} diff --git a/pkg/qrcodegenerator/qrcode/gateways/gateways.go b/pkg/qrcodegenerator/qrcode/gateways/gateways.go index 7b78944e78f..88ca6a328ac 100644 --- a/pkg/qrcodegenerator/qrcode/gateways/gateways.go +++ b/pkg/qrcodegenerator/qrcode/gateways/gateways.go @@ -49,14 +49,15 @@ type gatewayFormat struct { // Server provides methods for gateways QR codes. type Server struct { - gatewaysFormats []gatewayFormat + gatewayFormats []gatewayFormat } // New returns a new Server. func New(ctx context.Context) *Server { s := &Server{ - // Newer formats should be added to this slice first to preferentially match with those first. - gatewaysFormats: []gatewayFormat{ + // Newer formats should be added to this slice first to + // preferentially match with those first. + gatewayFormats: []gatewayFormat{ { id: formatIDttigpro1, format: new(TTIgpro1Format), @@ -66,18 +67,46 @@ func New(ctx context.Context) *Server { return s } +// RegisterGatewayFormat registers the given gateway QR code format. +func (s *Server) RegisterGatewayFormat(id string, f Format) { + s.gatewayFormats = append(s.gatewayFormats, gatewayFormat{ + id: id, + format: f, + }) +} + +// GetGatewayFormats returns the registered gateway QR code formats. +func (s *Server) GetGatewayFormats() map[string]Format { + ret := make(map[string]Format) + for _, gtwFormat := range s.gatewayFormats { + ret[gtwFormat.id] = gtwFormat.format + } + return ret +} + +// GetGatewayFormat returns the format by ID. +func (s *Server) GetGatewayFormat(id string) Format { + for _, gtwFormat := range s.gatewayFormats { + if gtwFormat.id == id { + return gtwFormat.format + } + } + return nil +} + +// Formats returns the registered gateway QR code formats. func (s *Server) Formats() []*ttnpb.QRCodeFormat { - formats := make([]*ttnpb.QRCodeFormat, 0, len(s.gatewaysFormats)) - for _, gtwFormat := range s.gatewaysFormats { + formats := make([]*ttnpb.QRCodeFormat, 0, len(s.gatewayFormats)) + for _, gtwFormat := range s.gatewayFormats { formats = append(formats, gtwFormat.format.Format()) } return formats } -// Parse attempts to parse the given QR code data. -// It returns the parser and the format ID that successfully parsed the QR code. +// Parse the given QR code data. If formatID is provided, only that format is used. +// Otherwise, the first format registered will be used. func (s *Server) Parse(formatID string, data []byte) (ret Data, err error) { - for _, gtwFormat := range s.gatewaysFormats { + for _, gtwFormat := range s.gatewayFormats { // If format ID is provided, use only that. Otherwise, // default to the first format listed in gatewayFormats. if formatID != "" && formatID != gtwFormat.id { diff --git a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go index afdf3b9b978..fc055cfcdf3 100644 --- a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go +++ b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go @@ -32,7 +32,7 @@ const ( // TTIgpro1 is a format for gateway identification QR codes. type TTIgpro1 struct { GtwEUI types.EUI64 // lowercase base16, 16 chars long - OwnerToken string // base62, 12 chars long + OwnerToken []byte // base62, 12 chars long } // ttigpro1Regex is the regular expression to match the TTIgpro1 format. @@ -68,7 +68,7 @@ func isBase16Char(c rune) bool { } // validateOwnerToken checks if the owner token is 12 base62 characters. -func (m TTIgpro1) validateOwnerToken(s string) error { +func (m TTIgpro1) validateOwnerToken(s []byte) error { if len(s) != OwnerTokenLength { return errInvalidLength } @@ -80,7 +80,7 @@ func (m TTIgpro1) validateOwnerToken(s string) error { return nil } -func isBase62Char(c rune) bool { +func isBase62Char(c byte) bool { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') } @@ -89,7 +89,7 @@ func (m *TTIgpro1) MarshalText() ([]byte, error) { return nil, err } loweredEUI := strings.ToLower(m.GtwEUI.String()) - return []byte("https://ttig.pro/c/" + loweredEUI + "/" + m.OwnerToken), nil + return []byte("https://ttig.pro/c/" + loweredEUI + "/" + string(m.OwnerToken)), nil } // UnmarshalText implements the TextUnmarshaler interface. @@ -104,7 +104,7 @@ func (m *TTIgpro1) UnmarshalText(text []byte) error { return err } - m.OwnerToken = matches[2] + m.OwnerToken = []byte(matches[2]) return m.Validate() } diff --git a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go index 7cd6d2b37ab..1c240067790 100644 --- a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go +++ b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go @@ -37,7 +37,7 @@ func TestTTIgpro1(t *testing.T) { Data: []byte("https://ttig.pro/c/ec656efffe000128/abcdef123456"), Expected: TTIgpro1{ GtwEUI: types.EUI64{0xec, 0x65, 0x6e, 0xff, 0xfe, 0x00, 0x01, 0x28}, - OwnerToken: "abcdef123456", + OwnerToken: []byte{'a', 'b', 'c', 'd', 'e', 'f', '1', '2', '3', '4', '5', '6'}, }, }, { diff --git a/pkg/qrcodegenerator/qrcodegenerator.go b/pkg/qrcodegenerator/qrcodegenerator.go index 6cd66e13d8f..782c0ac363d 100644 --- a/pkg/qrcodegenerator/qrcodegenerator.go +++ b/pkg/qrcodegenerator/qrcodegenerator.go @@ -23,6 +23,7 @@ import ( "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/log" "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/enddevices" + "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/gateways" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "google.golang.org/grpc" ) @@ -35,9 +36,11 @@ type QRCodeGenerator struct { ctx context.Context endDevices *enddevices.Server + gateways *gateways.Server grpc struct { endDeviceQRCodeGenerator *endDeviceQRCodeGeneratorServer + gatewayQRCodeGenerator *gatewayQRCodeGeneratorServer } } @@ -72,6 +75,13 @@ func WithEndDeviceFormat(id string, f enddevices.Format) Option { } } +// WithGatewayFormat configures QRCodeGenerator with a GatewayFormat. +func WithGatewayFormat(id string, f gateways.Format) Option { + return func(qrg *QRCodeGenerator) { + qrg.gateways.RegisterGatewayFormat(id, f) + } +} + // Context returns the context of the QR Code Generator. func (qrg *QRCodeGenerator) Context() context.Context { return qrg.ctx