Skip to content

Commit

Permalink
qrg: Implement grpc methods for gateway qr code parser service
Browse files Browse the repository at this point in the history
  • Loading branch information
Vlad Vitan committed Aug 28, 2024
1 parent 6f8044d commit 8ec181c
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 14 deletions.
75 changes: 75 additions & 0 deletions pkg/qrcodegenerator/grpc_gateways.go
Original file line number Diff line number Diff line change
@@ -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
}
130 changes: 130 additions & 0 deletions pkg/qrcodegenerator/grpc_gateways_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
})
}
}
45 changes: 37 additions & 8 deletions pkg/qrcodegenerator/qrcode/gateways/gateways.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand All @@ -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')
}

Expand All @@ -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.
Expand All @@ -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()
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
},
},
{
Expand Down
10 changes: 10 additions & 0 deletions pkg/qrcodegenerator/qrcodegenerator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -35,9 +36,11 @@ type QRCodeGenerator struct {
ctx context.Context

endDevices *enddevices.Server
gateways *gateways.Server

grpc struct {
endDeviceQRCodeGenerator *endDeviceQRCodeGeneratorServer
gatewayQRCodeGenerator *gatewayQRCodeGeneratorServer
}
}

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 8ec181c

Please sign in to comment.