diff --git a/pkg/source/firefly/config.go b/pkg/source/firefly/config.go index c1d4b6f..c80f068 100644 --- a/pkg/source/firefly/config.go +++ b/pkg/source/firefly/config.go @@ -1,7 +1,9 @@ package firefly import ( + "crypto/x509" "fmt" + "net/http" "os" "github.com/TheThingsNetwork/go-utils/handlers/cli" @@ -14,23 +16,26 @@ import ( ) type config struct { - // appID string - apiURL string apiKey string + apiURL string - joinEUI string - + appID string frequencyPlanID string + joinEUI string macVersion string + + dryRun bool + withSession bool } var logger *apex.Logger func flagSet() *pflag.FlagSet { flags := &pflag.FlagSet{} - // flags.String(flagWithPrefix("app-id"), os.Getenv("FIREFLY_APP_ID"), "Firefly app ID") + flags.String(flagWithPrefix("app-id"), os.Getenv("FIREFLY_APP_ID"), "Firefly app ID") flags.String(flagWithPrefix("api-url"), os.Getenv("FIREFLY_API_URL"), "Firefly API URL") flags.String(flagWithPrefix("api-key"), os.Getenv("FIREFLY_API_KEY"), "Firefly API key") + flags.String(flagWithPrefix("ca-file"), os.Getenv("FIREFLY_CA_FILE"), "Firefly CA file for TLS (optional)") flags.String(flagWithPrefix("join-eui"), os.Getenv("JOIN_EUI"), "JoinEUI of exported devices") return flags } @@ -55,10 +60,10 @@ func getConfig(flags *pflag.FlagSet) (*config, error) { } api.SetLogger(logger) ttnlog.Set(ttnapex.Wrap(logger)) - // appID := stringFlag(flagWithPrefix("app-id")) - // if appID == "" { - // return nil, errNoAppID - // } + appID := stringFlag(flagWithPrefix("app-id")) + if appID == "" { + return nil, errNoAppID + } apiURL := stringFlag(flagWithPrefix("api-url")) if apiURL == "" { return nil, errNoAPIURL @@ -73,12 +78,28 @@ func getConfig(flags *pflag.FlagSet) (*config, error) { if joinEUI == "" { return nil, errNoJoinEUI } + if caPath := stringFlag(flagWithPrefix("ca-file")); caPath != "" { + pemBytes, err := os.ReadFile(caPath) + if err != nil { + return nil, err + } + rootCAs := http.DefaultTransport.(*http.Transport).TLSClientConfig.RootCAs + if rootCAs == nil { + if rootCAs, err = x509.SystemCertPool(); err != nil { + rootCAs = x509.NewCertPool() + } + } + rootCAs.AppendCertsFromPEM(pemBytes) + http.DefaultTransport.(*http.Transport).TLSClientConfig.RootCAs = rootCAs + } return &config{ - // appID: appID, - apiURL: apiURL, apiKey: apiKey, + apiURL: apiURL, + appID: appID, joinEUI: joinEUI, + + dryRun: boolFlag("dry-run"), }, nil } diff --git a/pkg/source/firefly/errors.go b/pkg/source/firefly/errors.go index c102ce0..f086631 100644 --- a/pkg/source/firefly/errors.go +++ b/pkg/source/firefly/errors.go @@ -3,8 +3,8 @@ package firefly import "go.thethings.network/lorawan-stack/v3/pkg/errors" var ( - // errNoAppID = errors.DefineInvalidArgument("no_app_id", "no app id") - errNoAPIURL = errors.DefineInvalidArgument("no_api_url", "no api url") errNoAPIKEY = errors.DefineInvalidArgument("no_api_key", "no api key") + errNoAPIURL = errors.DefineInvalidArgument("no_api_url", "no api url") + errNoAppID = errors.DefineInvalidArgument("no_app_id", "no app id") errNoJoinEUI = errors.DefineInvalidArgument("no_join_eui", "no join eui") ) diff --git a/pkg/source/firefly/source.go b/pkg/source/firefly/source.go index 8685716..7729d35 100644 --- a/pkg/source/firefly/source.go +++ b/pkg/source/firefly/source.go @@ -2,14 +2,16 @@ package firefly import ( "context" - "fmt" "strings" + pbtypes "github.com/gogo/protobuf/types" "github.com/spf13/pflag" + "go.thethings.network/lorawan-stack/v3/pkg/log" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "go.thethings.network/lorawan-stack/v3/pkg/types" "go.thethings.network/lorawan-stack-migrate/pkg/source" + "go.thethings.network/lorawan-stack-migrate/pkg/source/firefly/api" "go.thethings.network/lorawan-stack-migrate/pkg/source/firefly/devices" ) @@ -44,15 +46,22 @@ func (s Source) ExportDevice(devEUI string) (*ttnpb.EndDevice, error) { // Formatters: &ttnpb.MessagePayloadFormatters{}, FrequencyPlanId: s.config.frequencyPlanID, Ids: &ttnpb.EndDeviceIdentifiers{ - ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: fmt.Sprintf("firefly-%v", ffdev.OrganizationID)}, + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: "firefly-" + s.config.appID}, DeviceId: "eui-" + strings.ToLower(ffdev.EUI), }, - MacSettings: &ttnpb.MACSettings{Rx2DataRateIndex: &ttnpb.DataRateIndexValue{Value: ttnpb.DataRateIndex(ffdev.Rx2DataRate)}}, + LorawanVersion: ttnpb.MACVersion_MAC_V1_0_2, + LorawanPhyVersion: ttnpb.PHYVersion_RP001_V1_0_2_REV_B, + MacSettings: &ttnpb.MACSettings{ + Rx2DataRateIndex: &ttnpb.DataRateIndexValue{Value: ttnpb.DataRateIndex(ffdev.Rx2DataRate)}, + StatusCountPeriodicity: &pbtypes.UInt32Value{Value: 0}, + StatusTimePeriodicity: pbtypes.DurationProto(0), + }, // MacState: &ttnpb.MACState{}, - RootKeys: &ttnpb.RootKeys{AppKey: &ttnpb.KeyEnvelope{}}, - Session: &ttnpb.Session{Keys: &ttnpb.SessionKeys{AppSKey: &ttnpb.KeyEnvelope{}, NwkSEncKey: &ttnpb.KeyEnvelope{}}}, + RootKeys: &ttnpb.RootKeys{}, SupportsClassC: ffdev.ClassC, + SupportsJoin: ffdev.ApplicationKey != "", } + v3dev.Attributes = make(map[string]string) v3dev.Ids.DevEui, err = unmarshalTextToBytes(&types.EUI64{}, ffdev.EUI) if err != nil { return nil, err @@ -61,22 +70,51 @@ func (s Source) ExportDevice(devEUI string) (*ttnpb.EndDevice, error) { if err != nil { return nil, err } - v3dev.RootKeys.AppKey.Key, err = unmarshalTextToBytes(&types.AES128Key{}, ffdev.ApplicationKey) - if err != nil { - return nil, err - } - v3dev.Session.DevAddr, err = unmarshalTextToBytes(&types.DevAddr{}, ffdev.Address) - if err != nil { - return nil, err + if v3dev.SupportsJoin { + v3dev.RootKeys.AppKey = &ttnpb.KeyEnvelope{} + v3dev.RootKeys.AppKey.Key, err = unmarshalTextToBytes(&types.AES128Key{}, ffdev.ApplicationKey) + if err != nil { + return nil, err + } } - v3dev.Session.Keys.AppSKey.Key, err = unmarshalTextToBytes(&types.AES128Key{}, ffdev.ApplicationSessionKey) - if err != nil { - return nil, err + hasSession := ffdev.Address != "" && ffdev.NetworkSessionKey != "" && ffdev.ApplicationSessionKey != "" + if hasSession || !v3dev.SupportsJoin { + v3dev.Session = &ttnpb.Session{Keys: &ttnpb.SessionKeys{AppSKey: &ttnpb.KeyEnvelope{}, NwkSEncKey: &ttnpb.KeyEnvelope{}}} + v3dev.Session.DevAddr, err = unmarshalTextToBytes(&types.DevAddr{}, ffdev.Address) + if err != nil { + return nil, err + } + v3dev.Session.Keys.AppSKey.Key, err = unmarshalTextToBytes(&types.AES128Key{}, ffdev.ApplicationSessionKey) + if err != nil { + return nil, err + } + v3dev.Session.Keys.NwkSEncKey.Key, err = unmarshalTextToBytes(&types.AES128Key{}, ffdev.NetworkSessionKey) + if err != nil { + return nil, err + } + v3dev.Session.LastAFCntDown = uint32(ffdev.FrameCounter) + v3dev.Session.LastNFCntDown = uint32(ffdev.FrameCounter) + packet, err := devices.GetLastPacket() + if err != nil { + return nil, err + } + v3dev.Session.LastFCntUp = uint32(packet.FCnt) } - v3dev.Session.Keys.NwkSEncKey.Key, err = unmarshalTextToBytes(&types.AES128Key{}, ffdev.NetworkSessionKey) - if err != nil { - return nil, err + + if !s.config.dryRun { + log.FromContext(s.ctx).WithFields(log.Fields( + "device_id", ffdev.Name, + "device_eui", ffdev.EUI, + )).Info("Clearing device keys") + r, err := api.PutDeviceUpdate(devEUI, map[string]string{ + "address": "", "application_key": "", "application_session_key": "", + }) + if err != nil { + return nil, err + } + r.Body.Close() } + return v3dev, nil } @@ -84,7 +122,7 @@ func (s Source) ExportDevice(devEUI string) (*ttnpb.EndDevice, error) { func (s Source) RangeDevices(appID string, f func(source.Source, string) error) error { // req, err := http.Get(fmt.Sprintf("http://%s/api/v1/applications/%s/euis?auth=%s", s.config.apiURL, appID, s.config.apiKey)) var ( - devs []*devices.Device + devs []devices.Device err error ) logger.WithField("app-id", appID).Debug("App ID")