From f317d4f4194ef0dc65ca9c3b6ba8cdd4fd484b08 Mon Sep 17 00:00:00 2001 From: Raghavendra Balgi Date: Wed, 13 May 2020 14:07:23 +0530 Subject: [PATCH] Persistent Connections and Bitmap Codec Util (#55) * Minor refactoring * WIP persistent connections * Final for ISO host persistent connections * Final for ISO host persistent connections --- README.md | 1 + cmd/isoserver/isoserver.go | 2 +- cmd/isosim/isosim.go | 9 +- cmd/isosim/version.go | 4 +- githooks/update_commit_id.sh | 2 +- go.mod | 2 +- go.sum | 4 +- internal/db/data_set_manager.go | 2 +- internal/db/db.go | 5 +- internal/db/db_test.go | 2 +- internal/iso/field.go | 12 +- internal/iso/field_assembler.go | 23 ++- internal/iso/field_parser.go | 17 +- internal/iso/field_test.go | 46 ++--- internal/iso/iso.go | 36 ++-- internal/iso/iso_bitmap.go | 2 +- internal/iso/iso_bitmap_test.go | 12 +- internal/iso/iso_msg_assemble_test.go | 4 +- internal/iso/iso_msg_parse_test.go | 2 +- internal/iso/message.go | 2 + internal/iso/server/iso_def.go | 2 +- internal/iso/server/iso_msg_process.go | 8 +- internal/iso/server/iso_server.go | 12 +- internal/iso/server/iso_server_test.go | 2 +- internal/iso/server/response_builder.go | 2 +- internal/iso/spec.go | 49 ++++- internal/iso/types.go | 7 +- internal/services/crypto/endpoint.go | 4 +- .../services/{v0 => }/data/json_field_rep.go | 0 .../{v0 => }/data/json_field_rep_test.go | 2 +- .../{v0 => }/data/server_definition.go | 0 .../isoserver_handlers.go | 21 ++- internal/services/handlers/mg_hist_handler.go | 70 +++++++ .../misc => handlers}/misc_handler.go | 66 +------ internal/services/service_registration.go | 15 +- .../services/v0/handlers/isoserver/isoserv.go | 25 --- internal/services/websim/endpoints.go | 2 +- internal/services/websim/service.go | 173 +++++++++++++++++- test/testdata/appdata/isosim.bdb | Bin 262144 -> 524288 bytes test/testdata/specs/iso_specs.yaml | 15 ++ 40 files changed, 468 insertions(+), 196 deletions(-) rename internal/services/{v0 => }/data/json_field_rep.go (100%) rename internal/services/{v0 => }/data/json_field_rep_test.go (87%) rename internal/services/{v0 => }/data/server_definition.go (100%) rename internal/services/{v0/handlers/isoserver => handlers}/isoserver_handlers.go (90%) create mode 100644 internal/services/handlers/mg_hist_handler.go rename internal/services/{v0/handlers/misc => handlers}/misc_handler.go (68%) delete mode 100644 internal/services/v0/handlers/isoserver/isoserv.go diff --git a/README.md b/README.md index c1f9470..cb2e510 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![GoDev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/rkbalgi/isosim?tab=doc) ![build](https://github.com/rkbalgi/isosim/workflows/build/badge.svg) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/rkbalgi/isosim?include_prereleases&style=flat) +![Docker Pulls](https://img.shields.io/docker/pulls/rkbalgi/isosim?color=%23FF6528&label=docker%20pulls) # ISO WebSim A very short screencast - [https://youtu.be/vSRZ_nzU-Jg](https://youtu.be/vSRZ_nzU-Jg) diff --git a/cmd/isoserver/isoserver.go b/cmd/isoserver/isoserver.go index 0a8ae26..1d4e641 100644 --- a/cmd/isoserver/isoserver.go +++ b/cmd/isoserver/isoserver.go @@ -7,7 +7,7 @@ import ( "io/ioutil" "isosim/internal/iso" "isosim/internal/iso/server" - "isosim/internal/services/v0/data" + "isosim/internal/services/data" "os" "sync" ) diff --git a/cmd/isosim/isosim.go b/cmd/isosim/isosim.go index 257e825..61c8080 100644 --- a/cmd/isosim/isosim.go +++ b/cmd/isosim/isosim.go @@ -49,7 +49,7 @@ func main() { log.SetLevel(log.ErrorLevel) default: log.Warn("Invalid log-level specified, will default to DEBUG") - log.SetLevel(log.InfoLevel) + log.SetLevel(log.DebugLevel) } log.SetFormatter(&log.TextFormatter{ForceColors: true, DisableColors: false}) @@ -81,7 +81,12 @@ func main() { certFile := os.Getenv("TLS_CERT_FILE") keyFile := os.Getenv("TLS_KEY_FILE") - log.Infof("Using certificate file - %s, key file: %s", certFile, keyFile) + log.Infof("TLS settings: Using Certificate file : %s, Key file: %s", certFile, keyFile) + + if certFile == "" || keyFile == "" { + log.Fatalf("SSL enabled, but certificate/key file unspecified.") + } + log.Fatal(http.ListenAndServeTLS(":"+strconv.Itoa(*httpPort), certFile, keyFile, nil)) } else { log.Fatal(http.ListenAndServe(":"+strconv.Itoa(*httpPort), nil)) diff --git a/cmd/isosim/version.go b/cmd/isosim/version.go index c3bab1b..fc28269 100644 --- a/cmd/isosim/version.go +++ b/cmd/isosim/version.go @@ -1,4 +1,4 @@ package main -var version = "0.8.0" -var build = "d01aad2c" +var version = "0.10.0" +var build = "087f55ec" diff --git a/githooks/update_commit_id.sh b/githooks/update_commit_id.sh index eb9ed0b..d57e55f 100644 --- a/githooks/update_commit_id.sh +++ b/githooks/update_commit_id.sh @@ -3,5 +3,5 @@ commit_id=`git log -1 HEAD | head -1 | awk '{print substr($2,0,8)}'` echo "package main -var version=\"0.8.0\" +var version=\"0.10.0\" var build=\"$commit_id\"" > ../cmd/isosim/version.go diff --git a/go.mod b/go.mod index 96a0668..783c2ec 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( github.com/go-kit/kit v0.10.0 github.com/google/uuid v1.0.0 - github.com/rkbalgi/libiso v0.1.5-0.20200424111508-cb62515a9b4a + github.com/rkbalgi/libiso v0.1.5-0.20200513063448-de9fd4ba7bce github.com/sirupsen/logrus v1.4.2 github.com/stretchr/testify v1.5.1 go.etcd.io/bbolt v1.3.3 diff --git a/go.sum b/go.sum index ecca6e9..ccd258e 100644 --- a/go.sum +++ b/go.sum @@ -206,8 +206,8 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rkbalgi/libiso v0.1.5-0.20200424111508-cb62515a9b4a h1:GBq34G1CNMVLs1C2hOt/wTNJns2/h6NJrgSZ5xbJUk8= -github.com/rkbalgi/libiso v0.1.5-0.20200424111508-cb62515a9b4a/go.mod h1:h4uRr0I72BG7sajrG5uljWWfQq3SPZTE21W7NOVadvY= +github.com/rkbalgi/libiso v0.1.5-0.20200513063448-de9fd4ba7bce h1:ID0vpI8Ad2Eh0yeNnoRPnk0LrddQVpotCN5u5zlfUdA= +github.com/rkbalgi/libiso v0.1.5-0.20200513063448-de9fd4ba7bce/go.mod h1:h4uRr0I72BG7sajrG5uljWWfQq3SPZTE21W7NOVadvY= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/internal/db/data_set_manager.go b/internal/db/data_set_manager.go index 5705b9c..4afeb18 100644 --- a/internal/db/data_set_manager.go +++ b/internal/db/data_set_manager.go @@ -14,7 +14,7 @@ import ( "strings" "sync" - "isosim/internal/services/v0/data" + "isosim/internal/services/data" ) type dataSetManager struct{} diff --git a/internal/db/db.go b/internal/db/db.go index a2cc8c3..de41d3c 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -9,11 +9,11 @@ import ( "github.com/google/uuid" log "github.com/sirupsen/logrus" bolt "go.etcd.io/bbolt" - "isosim/internal/services/v0/data" + "isosim/internal/services/data" "time" ) -var timeFormat = "2006-01-02T15" +const timeFormat = "2006-01-02T15" // DbMessage is an entry of a request/response that will be persisted to // storage @@ -116,6 +116,7 @@ func ReadLast(specID int, msgID int, n int) ([]string, error) { k, v := c.Last() if k == nil || v == nil { + now = now.Add(-1 * time.Hour) continue } for len(res) < n { diff --git a/internal/db/db_test.go b/internal/db/db_test.go index bd09973..5720885 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func Test_ReadWriteToBold(t *testing.T) { +func Test_ReadWriteToBolt(t *testing.T) { //t.SkipNow() if err := Init("."); err != nil { diff --git a/internal/iso/field.go b/internal/iso/field.go index f1c6f76..09d9181 100644 --- a/internal/iso/field.go +++ b/internal/iso/field.go @@ -11,12 +11,18 @@ import ( log "github.com/sirupsen/logrus" ) +const ( + Fixed = "fixed" + Variable = "variable" + Bitmapped = "bitmap" +) + // NewField is a constructor for Field func NewField(info []string) (*Field, error) { fieldInfo := &Field{} switch info[0] { - case "fixed": + case Fixed: { fieldInfo.Type = FixedType hasConstraints := false @@ -54,7 +60,7 @@ func NewField(info []string) (*Field, error) { } } - case "bitmap": + case Bitmapped: { fieldInfo.Type = BitmappedType if err := setEncoding(&(*fieldInfo).DataEncoding, info[1]); err != nil { @@ -67,7 +73,7 @@ func NewField(info []string) (*Field, error) { } } - case "variable": + case Variable: { fieldInfo.Type = VariableType hasConstraints := false diff --git a/internal/iso/field_assembler.go b/internal/iso/field_assembler.go index cf32eac..75fae7b 100644 --- a/internal/iso/field_assembler.go +++ b/internal/iso/field_assembler.go @@ -10,7 +10,7 @@ import ( ) // assemble assembles all the field into the dst Buffer buf -func assemble(buf *bytes.Buffer, parsedMsg *ParsedMsg, fieldData *FieldData) error { +func assemble(buf *bytes.Buffer, meta *MetaInfo, parsedMsg *ParsedMsg, fieldData *FieldData) error { asmLog := log.WithFields(log.Fields{"component": "assembler"}) @@ -23,6 +23,9 @@ func assemble(buf *bytes.Buffer, parsedMsg *ParsedMsg, fieldData *FieldData) err // from the children (nested fields) else we take it from the parent field if !fieldData.Field.HasChildren() { asmLog.Debugf("Field %s, Length: %d, Value: %s\n", field.Name, len(fieldData.Data), hex.EncodeToString(fieldData.Data)) + if field.Key == true { + meta.MessageKey += fieldData.Value() + } buf.Write(fieldData.Data) } case VariableType: @@ -41,12 +44,15 @@ func assemble(buf *bytes.Buffer, parsedMsg *ParsedMsg, fieldData *FieldData) err } asmLog.Debugf("Field %s, LL (Variable): %s, Value: %s\n", field.Name, hex.EncodeToString(lenBuf.Bytes()), hex.EncodeToString(fieldData.Data)) + if field.Key == true { + meta.MessageKey += fieldData.Value() + } buf.Write(lenBuf.Bytes()) buf.Write(fieldData.Data) } } case BitmappedType: - asmLog.Debugf("Field %s, Length (bitmapped): -, Value: %s\n", field.Name, hex.EncodeToString(fieldData.Bitmap.Bytes())) + asmLog.Debugf("Field %s, Length (Bitmapped): -, Value: %s\n", field.Name, hex.EncodeToString(fieldData.Bitmap.Bytes())) buf.Write(fieldData.Bitmap.Bytes()) } @@ -57,7 +63,7 @@ func assemble(buf *bytes.Buffer, parsedMsg *ParsedMsg, fieldData *FieldData) err bmp := fieldData.Bitmap for _, childField := range fieldData.Field.Children { if bmp.IsOn(childField.Position) { - if err := assemble(buf, parsedMsg, parsedMsg.FieldDataMap[childField.ID]); err != nil { + if err := assemble(buf, meta, parsedMsg, parsedMsg.FieldDataMap[childField.ID]); err != nil { return err } } @@ -66,19 +72,22 @@ func assemble(buf *bytes.Buffer, parsedMsg *ParsedMsg, fieldData *FieldData) err if field.Type == FixedType { tempBuf := bytes.Buffer{} for _, cf := range fieldData.Field.Children { - if err := assemble(&tempBuf, parsedMsg, parsedMsg.FieldDataMap[cf.ID]); err != nil { + if err := assemble(&tempBuf, meta, parsedMsg, parsedMsg.FieldDataMap[cf.ID]); err != nil { return err } } buf.Write(tempBuf.Bytes()) fieldData.Data = tempBuf.Bytes() + if field.Key == true { + meta.MessageKey += fieldData.Value() + } asmLog.Debugf("Field %s, Length (Fixed): %d, Value: %s\n", field.Name, len(fieldData.Data), hex.EncodeToString(fieldData.Data)) } else if field.Type == VariableType { //assemble all child fields and then construct the parent tempBuf := bytes.Buffer{} for _, cf := range fieldData.Field.Children { - if err := assemble(&tempBuf, parsedMsg, parsedMsg.FieldDataMap[cf.ID]); err != nil { + if err := assemble(&tempBuf, meta, parsedMsg, parsedMsg.FieldDataMap[cf.ID]); err != nil { return err } } @@ -96,6 +105,10 @@ func assemble(buf *bytes.Buffer, parsedMsg *ParsedMsg, fieldData *FieldData) err fieldData.Data = tempBuf.Bytes() asmLog.Debugf("Field %s, LL (Variable): %s, Value: %s\n", field.Name, hex.EncodeToString(lenBuf.Bytes()), hex.EncodeToString(fieldData.Data)) + if field.Key == true { + meta.MessageKey += fieldData.Value() + } + buf.Write(lenBuf.Bytes()) buf.Write(tempBuf.Bytes()) diff --git a/internal/iso/field_parser.go b/internal/iso/field_parser.go index d1524cb..9c5eea7 100644 --- a/internal/iso/field_parser.go +++ b/internal/iso/field_parser.go @@ -8,7 +8,6 @@ import ( "fmt" "strconv" - "github.com/rkbalgi/libiso/encoding/ebcdic" log "github.com/sirupsen/logrus" ) @@ -25,8 +24,13 @@ var ErrInvalidEncoding = errors.New("isosim: Invalid encoding") type ParsedMsg struct { IsRequest bool Msg *Message + //A map of Id to FieldData FieldDataMap map[int]*FieldData + + // MessageKey is a value that unique identifies a transaction + // (usually a combination of fields like STAN, PAN etc) + MessageKey string } // Get returns the field-data from the parsed message given its name @@ -57,6 +61,7 @@ func (pMsg *ParsedMsg) Copy() *ParsedMsg { } newParsedMsg.Msg = pMsg.Msg + newParsedMsg.MessageKey = pMsg.MessageKey return newParsedMsg @@ -111,7 +116,9 @@ func parseFixed(buf *bytes.Buffer, parsedMsg *ParsedMsg, field *Field) error { if fieldData.Data, err = NextBytes(buf, field.Size); err != nil { return err } - + if field.Key { + parsedMsg.MessageKey += fieldData.Value() + } log.WithFields(log.Fields{"component": "parser"}).Debugf("Field %s, Length: %d, Value: %s\n", field.Name, field.Size, hex.EncodeToString(fieldData.Data)) parsedMsg.FieldDataMap[field.ID] = fieldData @@ -200,8 +207,7 @@ func parseVariable(buf *bytes.Buffer, parsedMsg *ParsedMsg, field *Field) error return err } case EBCDIC: - - if length, err = strconv.ParseUint(ebcdic.EncodeToString(lenData), 10, 64); err != nil { + if length, err = strconv.ParseUint(EBCDIC.EncodeToString(lenData), 10, 64); err != nil { return err } default: @@ -222,6 +228,9 @@ func parseVariable(buf *bytes.Buffer, parsedMsg *ParsedMsg, field *Field) error if fieldData.Data, err = NextBytes(buf, int(length)); err != nil { return err } + if field.Key { + parsedMsg.MessageKey += fieldData.Value() + } log.WithFields(log.Fields{"component": "parser"}).Debugf("Field %s, Length: %d, Value: %s\n", field.Name, length, hex.EncodeToString(fieldData.Data)) diff --git a/internal/iso/field_test.go b/internal/iso/field_test.go index 5cc998e..06d970b 100644 --- a/internal/iso/field_test.go +++ b/internal/iso/field_test.go @@ -17,7 +17,7 @@ func Test_BitmapField(t *testing.T) { t.Run("parse binary bitmap field - success", func(t *testing.T) { data, _ := hex.DecodeString("F000001018010002E0200000100201000000200004040201") - f := &Field{ID: 10, Name: StandardNameBitmap, Type: BitmappedType, DataEncoding: BINARY} + f := &Field{ID: 10, Name: IsoBitmap, Type: BitmappedType, DataEncoding: BINARY} p := &ParsedMsg{Msg: &Message{fieldByIdMap: make(map[int]*Field), fieldByName: make(map[string]*Field)}, FieldDataMap: make(map[int]*FieldData)} @@ -28,7 +28,7 @@ func Test_BitmapField(t *testing.T) { assert.Nil(t, err) for _, pos := range onBits { - if !p.Get(StandardNameBitmap).Bitmap.IsOn(pos) { + if !p.Get(IsoBitmap).Bitmap.IsOn(pos) { t.Fatalf("%d position is not set", pos) } } @@ -37,7 +37,7 @@ func Test_BitmapField(t *testing.T) { t.Run("parse binary bitmap field - success (primary only)", func(t *testing.T) { data, _ := hex.DecodeString("7000001018010002") - f := &Field{ID: 10, Name: StandardNameBitmap, Type: BitmappedType, DataEncoding: BINARY} + f := &Field{ID: 10, Name: IsoBitmap, Type: BitmappedType, DataEncoding: BINARY} p := &ParsedMsg{Msg: &Message{fieldByIdMap: make(map[int]*Field), fieldByName: make(map[string]*Field)}, FieldDataMap: make(map[int]*FieldData)} @@ -48,7 +48,7 @@ func Test_BitmapField(t *testing.T) { assert.Nil(t, err) for _, pos := range []int{2, 3, 4, 28, 36, 37, 48, 63} { - if !p.Get(StandardNameBitmap).Bitmap.IsOn(pos) { + if !p.Get(IsoBitmap).Bitmap.IsOn(pos) { t.Fatalf("%d position is not set", pos) } } @@ -57,7 +57,7 @@ func Test_BitmapField(t *testing.T) { t.Run("parse binary bitmap field - success (primary and secondary)", func(t *testing.T) { data, _ := hex.DecodeString("F0000010180100026020000010020100") - f := &Field{ID: 10, Name: StandardNameBitmap, Type: BitmappedType, DataEncoding: BINARY} + f := &Field{ID: 10, Name: IsoBitmap, Type: BitmappedType, DataEncoding: BINARY} p := &ParsedMsg{Msg: &Message{fieldByIdMap: make(map[int]*Field), fieldByName: make(map[string]*Field)}, FieldDataMap: make(map[int]*FieldData)} @@ -68,7 +68,7 @@ func Test_BitmapField(t *testing.T) { assert.Nil(t, err) for _, pos := range []int{1, 2, 3, 4, 28, 36, 37, 48, 63, 66, 67, 75, 100, 111, 120} { - if !p.Get(StandardNameBitmap).Bitmap.IsOn(pos) { + if !p.Get(IsoBitmap).Bitmap.IsOn(pos) { t.Fatalf("%d position is not set", pos) } } @@ -77,7 +77,7 @@ func Test_BitmapField(t *testing.T) { t.Run("parse binary bitmap field - failure", func(t *testing.T) { data, _ := hex.DecodeString("F000000018010002E0200000100201000000200004040201") - info := &Field{ID: 10, Name: StandardNameBitmap, Type: BitmappedType, DataEncoding: BINARY} + info := &Field{ID: 10, Name: IsoBitmap, Type: BitmappedType, DataEncoding: BINARY} p := &ParsedMsg{Msg: &Message{fieldByIdMap: make(map[int]*Field), fieldByName: make(map[string]*Field)}, FieldDataMap: make(map[int]*FieldData)} @@ -87,14 +87,14 @@ func Test_BitmapField(t *testing.T) { err := parseBitmap(buf, p, field) assert.Nil(t, err) - assert.False(t, p.Get(StandardNameBitmap).Bitmap.IsOn(28)) + assert.False(t, p.Get(IsoBitmap).Bitmap.IsOn(28)) }) t.Run("parse ASCII bitmap field - success", func(t *testing.T) { data, _ := hex.DecodeString("463030303030313031383031303030324530323030303030313030323031303030303030323030303034303430323031") - info := &Field{ID: 10, Name: StandardNameBitmap, Type: BitmappedType, DataEncoding: ASCII} + info := &Field{ID: 10, Name: IsoBitmap, Type: BitmappedType, DataEncoding: ASCII} p := &ParsedMsg{Msg: &Message{fieldByIdMap: make(map[int]*Field), fieldByName: make(map[string]*Field)}, FieldDataMap: make(map[int]*FieldData)} @@ -105,7 +105,7 @@ func Test_BitmapField(t *testing.T) { assert.Nil(t, err) for _, pos := range onBits { - if !p.Get(StandardNameBitmap).Bitmap.IsOn(pos) { + if !p.Get(IsoBitmap).Bitmap.IsOn(pos) { t.Fatalf("%d position is not set", pos) } } @@ -116,7 +116,7 @@ func Test_BitmapField(t *testing.T) { ebcdicBmp := hex.EncodeToString(ebcdic.Decode("F000001018010002E0200000100201000000200004040201")) data, _ := hex.DecodeString(ebcdicBmp) - info := &Field{ID: 10, Name: StandardNameBitmap, Type: BitmappedType, DataEncoding: EBCDIC} + info := &Field{ID: 10, Name: IsoBitmap, Type: BitmappedType, DataEncoding: EBCDIC} p := &ParsedMsg{Msg: &Message{fieldByIdMap: make(map[int]*Field), fieldByName: make(map[string]*Field)}, FieldDataMap: make(map[int]*FieldData)} @@ -127,7 +127,7 @@ func Test_BitmapField(t *testing.T) { assert.Nil(t, err) for _, pos := range onBits { - if !p.Get(StandardNameBitmap).Bitmap.IsOn(pos) { + if !p.Get(IsoBitmap).Bitmap.IsOn(pos) { t.Fatalf("%d position is not set", pos) } } @@ -160,6 +160,8 @@ func Test_FixedField(t *testing.T) { func Test_VariableField(t *testing.T) { + meta := &MetaInfo{} + name := "VariableField" t.Run("variable field with ascii and ascii", func(t *testing.T) { fieldInfo := &Field{ID: 9, Name: name, Type: VariableType, @@ -186,14 +188,14 @@ func Test_VariableField(t *testing.T) { //also assemble the field and check the length indicator buf2 := &bytes.Buffer{} - if err := assemble(buf2, parsedMsg, parsedMsg.Get(name)); err != nil { + if err := assemble(buf2, meta, parsedMsg, parsedMsg.Get(name)); err != nil { t.Fatal(err) } assert.Equal(t, []byte{0x30, 0x34, 0x31, 0x32, 0x33, 0x34}, buf2.Bytes()) buf2.Reset() parsedMsg.Get(name).Set("covid19") - if err := assemble(buf2, parsedMsg, parsedMsg.Get(name)); err != nil { + if err := assemble(buf2, meta, parsedMsg, parsedMsg.Get(name)); err != nil { t.Fatal(err) } assert.Equal(t, append([]byte{0x30, 0x37}, []byte("covid19")...), buf2.Bytes()) @@ -230,7 +232,7 @@ func Test_VariableField(t *testing.T) { //also assemble the field and check the length indicator buf2 := &bytes.Buffer{} - if err := assemble(buf2, parsedMsg, parsedMsg.Get(fieldName)); err != nil { + if err := assemble(buf2, meta, parsedMsg, parsedMsg.Get(fieldName)); err != nil { t.Fatal(err) } assert.Equal(t, append([]byte{0x11}, []byte("Hello World")...), buf2.Bytes()) @@ -264,7 +266,7 @@ func Test_VariableField(t *testing.T) { //also assemble the field and check the length indicator buf2 := &bytes.Buffer{} - if err := assemble(buf2, parsedMsg, parsedMsg.Get(fieldName)); err != nil { + if err := assemble(buf2, meta, parsedMsg, parsedMsg.Get(fieldName)); err != nil { t.Fatal(err) } assert.Equal(t, append([]byte{0x00, 0x11}, []byte("Hello World")...), buf2.Bytes()) @@ -299,7 +301,7 @@ func Test_VariableField(t *testing.T) { //also assemble the field and check the length indicator buf2 := &bytes.Buffer{} - if err := assemble(buf2, parsedMsg, parsedMsg.Get(fieldName)); err != nil { + if err := assemble(buf2, meta, parsedMsg, parsedMsg.Get(fieldName)); err != nil { t.Fatal(err) } assert.Equal(t, append([]byte{0x0e}, []byte("2020!! covid19")...), buf2.Bytes()) @@ -334,7 +336,7 @@ func Test_VariableField(t *testing.T) { //also assemble the field and check the length indicator buf2 := &bytes.Buffer{} - if err := assemble(buf2, parsedMsg, parsedMsg.Get(fieldName)); err != nil { + if err := assemble(buf2, meta, parsedMsg, parsedMsg.Get(fieldName)); err != nil { t.Fatal(err) } assert.Equal(t, append([]byte{0x00, 0x0e}, []byte("2020!! covid19")...), buf2.Bytes()) @@ -369,7 +371,7 @@ func Test_VariableField(t *testing.T) { //also assemble the field and check the length indicator buf2 := &bytes.Buffer{} - if err := assemble(buf2, parsedMsg, parsedMsg.Get(fieldName)); err != nil { + if err := assemble(buf2, meta, parsedMsg, parsedMsg.Get(fieldName)); err != nil { t.Fatal(err) } assert.Equal(t, append([]byte{0x00, 0x0F}, fdata...), buf2.Bytes()) @@ -404,7 +406,7 @@ func Test_VariableField(t *testing.T) { //also assemble the field and check the length indicator buf2 := &bytes.Buffer{} - if err := assemble(buf2, parsedMsg, parsedMsg.Get(fieldName)); err != nil { + if err := assemble(buf2, meta, parsedMsg, parsedMsg.Get(fieldName)); err != nil { t.Fatal(err) } assert.Equal(t, append([]byte{0x00, 0x15}, fdata...), buf2.Bytes()) @@ -439,7 +441,7 @@ func Test_VariableField(t *testing.T) { //also assemble the field and check the length indicator buf2 := &bytes.Buffer{} - if err := assemble(buf2, parsedMsg, parsedMsg.Get(fieldName)); err != nil { + if err := assemble(buf2, meta, parsedMsg, parsedMsg.Get(fieldName)); err != nil { t.Fatal(err) } assert.Equal(t, append([]byte{0x31, 0x35}, fdata...), buf2.Bytes()) @@ -474,7 +476,7 @@ func Test_VariableField(t *testing.T) { //also assemble the field and check the length indicator buf2 := &bytes.Buffer{} - if err := assemble(buf2, parsedMsg, parsedMsg.Get(fieldName)); err != nil { + if err := assemble(buf2, meta, parsedMsg, parsedMsg.Get(fieldName)); err != nil { t.Fatal(err) } assert.Equal(t, append([]byte{0x00, 0x0F}, fdata...), buf2.Bytes()) diff --git a/internal/iso/iso.go b/internal/iso/iso.go index 04d7c5d..b898f9f 100644 --- a/internal/iso/iso.go +++ b/internal/iso/iso.go @@ -2,16 +2,17 @@ package iso import ( "bytes" + "time" ) // HTMLDir will point to the directory containing the static assets (HTML/JS/CSS etc) var HTMLDir string const ( - // StandardNameMessageType is a constant that indicates the Message Type or the MTI - // (This name has special meaning within the context of ISO8483 and cannot be name anything else. The same restrictions apply for 'Bitmap') - StandardNameMessageType = "Message Type" - StandardNameBitmap = "Bitmap" + // IsoMessageType is a constant that indicates the Message Type or the MTI + // (This name has special meaning within the context of ISO8583 and cannot be named anything else. The same restrictions apply for 'Bitmap') + IsoMessageType = "Message Type" + IsoBitmap = "Bitmap" ) // Iso is a handle into accessing the details of a ISO message(via the parsedMsg) @@ -19,11 +20,20 @@ type Iso struct { parsedMsg *ParsedMsg } +// MetaInfo provides additional information about an operation performed +// For example, in response to a parse or assemble op +type MetaInfo struct { + // OpTime is time taken by an operation + OpTime time.Duration + // MessageKey is a key that can be used to uniquely identify a transaction + MessageKey string +} + // FromParsedMsg constructs a new Iso from a parsedMsg func FromParsedMsg(parsedMsg *ParsedMsg) *Iso { isoMsg := &Iso{parsedMsg: parsedMsg} - bmpField := parsedMsg.Msg.fieldByName[StandardNameBitmap] + bmpField := parsedMsg.Msg.fieldByName[IsoBitmap] //if the bitmap field is not set then initialize it to a empty bitmap if _, ok := parsedMsg.FieldDataMap[bmpField.ID]; !ok { @@ -43,7 +53,7 @@ func (iso *Iso) Set(fieldName string, value string) error { return ErrUnknownField } - bmpField := iso.parsedMsg.Get(StandardNameBitmap) + bmpField := iso.parsedMsg.Get(IsoBitmap) if field.ParentId == bmpField.Field.ID { iso.Bitmap().SetOn(field.Position) iso.Bitmap().Set(field.Position, value) @@ -70,7 +80,7 @@ func (iso *Iso) Get(fieldName string) *FieldData { // Bitmap returns the Bitmap from the Iso message func (iso *Iso) Bitmap() *Bitmap { - field := iso.parsedMsg.Msg.Field(StandardNameBitmap) + field := iso.parsedMsg.Msg.Field(IsoBitmap) fieldData := iso.parsedMsg.FieldDataMap[field.ID].Bitmap if fieldData != nil && fieldData.parsedMsg == nil { fieldData.parsedMsg = iso.parsedMsg @@ -85,16 +95,20 @@ func (iso *Iso) ParsedMsg() *ParsedMsg { } // Assemble assembles the raw form of the message -func (iso *Iso) Assemble() ([]byte, error) { +func (iso *Iso) Assemble() ([]byte, *MetaInfo, error) { msg := iso.parsedMsg.Msg buf := new(bytes.Buffer) + meta := &MetaInfo{} + t1 := time.Now() for _, field := range msg.Fields { - if err := assemble(buf, iso.parsedMsg, iso.parsedMsg.FieldDataMap[field.ID]); err != nil { - return nil, err + if err := assemble(buf, meta, iso.parsedMsg, iso.parsedMsg.FieldDataMap[field.ID]); err != nil { + return nil, nil, err } } - return buf.Bytes(), nil + meta.OpTime = time.Since(t1) + + return buf.Bytes(), meta, nil } diff --git a/internal/iso/iso_bitmap.go b/internal/iso/iso_bitmap.go index 082befa..7e6ae47 100644 --- a/internal/iso/iso_bitmap.go +++ b/internal/iso/iso_bitmap.go @@ -30,7 +30,7 @@ func NewBitmap() *Bitmap { func emptyBitmap(parsedMsg *ParsedMsg) *Bitmap { bmp := NewBitmap() bmp.parsedMsg = parsedMsg - bmp.field = parsedMsg.Msg.Field(StandardNameBitmap) + bmp.field = parsedMsg.Msg.Field(IsoBitmap) return bmp } diff --git a/internal/iso/iso_bitmap_test.go b/internal/iso/iso_bitmap_test.go index 5b4feb6..8b4e5cb 100644 --- a/internal/iso/iso_bitmap_test.go +++ b/internal/iso/iso_bitmap_test.go @@ -15,7 +15,7 @@ func TestBitmap_IsOn(t *testing.T) { data, _ := hex.DecodeString("F000001018010002E0200000100201000000200004040201") - field := &Field{ID: 10, Name: StandardNameBitmap, Type: BitmappedType, DataEncoding: BINARY} + field := &Field{ID: 10, Name: IsoBitmap, Type: BitmappedType, DataEncoding: BINARY} p := &ParsedMsg{Msg: &Message{fieldByIdMap: make(map[int]*Field), fieldByName: make(map[string]*Field)}, FieldDataMap: make(map[int]*FieldData)} p.Msg.addField(field) @@ -26,7 +26,7 @@ func TestBitmap_IsOn(t *testing.T) { for i := 1; i < 193; i++ { - if p.Get(StandardNameBitmap).Bitmap.IsOn(i) { + if p.Get(IsoBitmap).Bitmap.IsOn(i) { t.Logf("%d is On", i) } @@ -38,7 +38,7 @@ func Test_AssembleBitmapField(t *testing.T) { t.Run("Assemble Bitmap - BINARY", func(t *testing.T) { bmp := NewBitmap() - bmp.field = &Field{ID: 10, Name: StandardNameBitmap, Type: BitmappedType, DataEncoding: BINARY} + bmp.field = &Field{ID: 10, Name: IsoBitmap, Type: BitmappedType, DataEncoding: BINARY} for _, pos := range []int{1, 2, 3, 4, 5, 6, 7, 55, 56, 60, 65, 91, 129, 192} { bmp.SetOn(pos) @@ -51,7 +51,7 @@ func Test_AssembleBitmapField(t *testing.T) { t.Run("Assemble Bitmap - ASCII", func(t *testing.T) { bmp := NewBitmap() - bmp.field = &Field{ID: 10, Name: StandardNameBitmap, Type: BitmappedType, DataEncoding: ASCII} + bmp.field = &Field{ID: 10, Name: IsoBitmap, Type: BitmappedType, DataEncoding: ASCII} for _, pos := range []int{1, 2, 3, 4, 5, 6, 7, 55, 56, 60, 65, 91, 129, 192} { bmp.SetOn(pos) } @@ -63,7 +63,7 @@ func Test_AssembleBitmapField(t *testing.T) { t.Run("Assemble Bitmap - EBCDIC", func(t *testing.T) { bmp := NewBitmap() - bmp.field = &Field{ID: 10, Name: StandardNameBitmap, Type: BitmappedType, DataEncoding: EBCDIC} + bmp.field = &Field{ID: 10, Name: IsoBitmap, Type: BitmappedType, DataEncoding: EBCDIC} for _, pos := range []int{1, 2, 3, 4, 5, 6, 7, 55, 56, 60, 65, 91, 129, 192} { bmp.SetOn(pos) } @@ -77,7 +77,7 @@ func Test_AssembleBitmapField(t *testing.T) { func Test_GenerateBitmap(t *testing.T) { bmp := NewBitmap() - bmp.field = &Field{ID: 10, Name: StandardNameBitmap, Type: BitmappedType, DataEncoding: BINARY} + bmp.field = &Field{ID: 10, Name: IsoBitmap, Type: BitmappedType, DataEncoding: BINARY} bmp.SetOn(2) bmp.SetOn(3) diff --git a/internal/iso/iso_msg_assemble_test.go b/internal/iso/iso_msg_assemble_test.go index cc0fa60..dc199f9 100644 --- a/internal/iso/iso_msg_assemble_test.go +++ b/internal/iso/iso_msg_assemble_test.go @@ -19,7 +19,7 @@ func Test_AssembleMsg(t *testing.T) { isoMsg := msg.NewIso() //setting directly - isoMsg.Set(StandardNameMessageType, "1100") + isoMsg.Set(IsoMessageType, "1100") isoMsg.Set("Fixed2_ASCII", "123") isoMsg.Set("Fixed3_EBCDIC", "456") isoMsg.Set("FxdField6_WithSubFields", "12345678") @@ -30,7 +30,7 @@ func Test_AssembleMsg(t *testing.T) { isoMsg.Bitmap().Set(60, "0987aefe") isoMsg.Bitmap().Set(91, "field91") - if assembledMsg, err := isoMsg.Assemble(); err != nil { + if assembledMsg, _, err := isoMsg.Assemble(); err != nil { t.Fatal(err) return } else { diff --git a/internal/iso/iso_msg_parse_test.go b/internal/iso/iso_msg_parse_test.go index 1a7932d..e12037f 100644 --- a/internal/iso/iso_msg_parse_test.go +++ b/internal/iso/iso_msg_parse_test.go @@ -31,7 +31,7 @@ func Test_ParseMsg(t *testing.T) { } assert.Equal(t, "1100", parsedMsg.Get("Message Type").Value()) - bmp := parsedMsg.Get(StandardNameBitmap).Bitmap + bmp := parsedMsg.Get(IsoBitmap).Bitmap assert.Equal(t, "hello", bmp.Get(56).Value()) //sub fields of fixed field diff --git a/internal/iso/message.go b/internal/iso/message.go index c0ee791..d934dc1 100644 --- a/internal/iso/message.go +++ b/internal/iso/message.go @@ -20,6 +20,8 @@ type Message struct { Name string `yaml:"name"` ID int `yaml:"id"` Fields []*Field `yaml:"fields"` + // HeaderMatch + HeaderMatch []string `yaml:"header_match"` // internal aux fields fieldByIdMap map[int]*Field diff --git a/internal/iso/server/iso_def.go b/internal/iso/server/iso_def.go index 611abf7..cbd6e5c 100644 --- a/internal/iso/server/iso_def.go +++ b/internal/iso/server/iso_def.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" "isosim/internal/db" - "isosim/internal/services/v0/data" + "isosim/internal/services/data" "sync" ) diff --git a/internal/iso/server/iso_msg_process.go b/internal/iso/server/iso_msg_process.go index 0326d82..82077de 100644 --- a/internal/iso/server/iso_msg_process.go +++ b/internal/iso/server/iso_msg_process.go @@ -6,7 +6,7 @@ import ( "fmt" log "github.com/sirupsen/logrus" "isosim/internal/iso" - "isosim/internal/services/v0/data" + "isosim/internal/services/data" "strconv" "strings" ) @@ -74,7 +74,7 @@ func processInternal(data []byte, pServerDef *data.ServerDef, msgSelConfig data. log.Debugln("[", pc.MatchConditionType+"] Processing condition matched.") buildResponse(isoMsg, &pc) - response, err := isoMsg.Assemble() + response, _, err := isoMsg.Assemble() return response, true, err case "StringEquals": @@ -84,7 +84,7 @@ func processInternal(data []byte, pServerDef *data.ServerDef, msgSelConfig data. log.Debugln("[", pc.MatchConditionType+"] Processing condition matched.") //set the response fields buildResponse(isoMsg, &pc) - response, err := isoMsg.Assemble() + response, _, err := isoMsg.Assemble() return response, true, err } @@ -132,7 +132,7 @@ func processInternal(data []byte, pServerDef *data.ServerDef, msgSelConfig data. log.Debugln(pc.MatchConditionType + "] Processing condition matched.") //set the response fields buildResponse(isoMsg, &pc) - response, err := isoMsg.Assemble() + response, _, err := isoMsg.Assemble() return response, true, err } diff --git a/internal/iso/server/iso_server.go b/internal/iso/server/iso_server.go index ac4d75e..557f8c7 100644 --- a/internal/iso/server/iso_server.go +++ b/internal/iso/server/iso_server.go @@ -12,7 +12,7 @@ import ( net2 "github.com/rkbalgi/libiso/net" log "github.com/sirupsen/logrus" "io" - "isosim/internal/services/v0/data" + "isosim/internal/services/data" "net" "strconv" "sync" @@ -146,15 +146,15 @@ func closeOnError(connection net.Conn, err error) { } -func handleConnection(connection net.Conn, pServerDef *data.ServerDef) { +func handleConnection(connection net.Conn, def *data.ServerDef) { slog := log.WithFields(log.Fields{"type": "server"}) buf := new(bytes.Buffer) - mliType, err := getMliFromString(pServerDef.MliType) + mliType, err := getMliFromString(def.MliType) if err != nil { - log.Errorf("isosim: Invalid MLIType %s specified", pServerDef.MliType) + log.Errorf("isosim: Invalid MLIType %s specified", def.MliType) return } var mliLen uint32 = 2 @@ -212,13 +212,12 @@ func handleConnection(connection net.Conn, pServerDef *data.ServerDef) { slog.Traceln("msgLen = ", msgLen, " Read = ", n) if uint32(len(buf.Bytes())) == msgLen { //we have a complete msg - complete = true var msgData = make([]byte, msgLen) copy(msgData, buf.Bytes()) slog.Debugf("Received Request, \n%s\n", hex.Dump(msgData)) buf.Reset() - go handleRequest(connection, msgData, pServerDef, mliType) + go handleRequest(connection, msgData, def, mliType) } } @@ -230,6 +229,7 @@ func handleConnection(connection net.Conn, pServerDef *data.ServerDef) { } func getMliFromString(mliType string) (net2.MliType, error) { + switch mliType { case "2e", "2E": return net2.Mli2e, nil diff --git a/internal/iso/server/iso_server_test.go b/internal/iso/server/iso_server_test.go index f58ae3e..cdfcb5f 100644 --- a/internal/iso/server/iso_server_test.go +++ b/internal/iso/server/iso_server_test.go @@ -96,7 +96,7 @@ func Test_IsoServer_MessageProcessing(t *testing.T) { func sendAndVerify(t *testing.T, ncc *netutil.NetCatClient, spec *iso.Spec, isoReqMsg *iso.Iso, expectedF39 string) { - data, err := isoReqMsg.Assemble() + data, _, err := isoReqMsg.Assemble() if err != nil { t.Fatal(err) } diff --git a/internal/iso/server/response_builder.go b/internal/iso/server/response_builder.go index 4605b8b..86f41d7 100644 --- a/internal/iso/server/response_builder.go +++ b/internal/iso/server/response_builder.go @@ -3,7 +3,7 @@ package server import ( log "github.com/sirupsen/logrus" "isosim/internal/iso" - "isosim/internal/services/v0/data" + "isosim/internal/services/data" ) func buildResponse(isoMsg *iso.Iso, pc *data.ProcessingCondition) { diff --git a/internal/iso/spec.go b/internal/iso/spec.go index 8fd6976..3dada75 100644 --- a/internal/iso/spec.go +++ b/internal/iso/spec.go @@ -13,8 +13,11 @@ var specMap = make(map[string]*Spec, 10) // Spec represents an ISO8583 specification type Spec struct { - Name string `yaml:"name"` - ID int `yaml:"id"` + Name string `yaml:"name"` + ID int `yaml:"id"` + + HeaderFields []*Field `yaml:"header_fields"` + Messages []*Message `yaml:"messages"` } @@ -85,6 +88,48 @@ func (spec *Spec) MessageByName(msgName string) *Message { } +// FindTargetMsg parses any defined header fields and returns a message +func (spec *Spec) FindTargetMsg(data []byte) *Message { + + if spec.HeaderFields == nil || len(spec.HeaderFields) == 0 { + return nil + } + + matchKey := "" + buf := bytes.NewBuffer(data) + parsedMsg := &ParsedMsg{ + IsRequest: false, + Msg: nil, + FieldDataMap: make(map[int]*FieldData), + MessageKey: ""} + + for _, field := range spec.HeaderFields { + if err := parse(buf, parsedMsg, field); err != nil { + log.Errorf("Failed to parse header fields: %s, Error: %v", field.Name, err) + return nil + } + } + for _, field := range spec.HeaderFields { + matchKey += parsedMsg.GetById(field.ID).Value() + } + + if matchKey == "" { + log.Errorf("isosim: No match key found!") + return nil + } + + for _, msg := range spec.Messages { + for _, hm := range msg.HeaderMatch { + if hm == matchKey { + return msg + } + } + } + + return nil + +} + func printAllSpecsInfo() { buf := bytes.NewBufferString("") diff --git a/internal/iso/types.go b/internal/iso/types.go index bcdb568..6684461 100644 --- a/internal/iso/types.go +++ b/internal/iso/types.go @@ -94,7 +94,7 @@ func (e Encoding) AsString() string { return string(e) } -// Field represents a Field in the ISO message +// Field represents a field in the ISO message type Field struct { Name string `yaml:"name"` ID int `yaml:"id"` @@ -113,11 +113,14 @@ type Field struct { msg *Message `yaml:"-"json:"-"` //for bitmap only - fieldsByPosition map[int]*Field + fieldsByPosition map[int]*Field + ParentId int ValueGeneratorType string `yaml:"gen_type"` PinGenProps *PinGenProps `yaml:"pin_gen_props,omitempty"` MacGenProps *MacGenProps `yaml:"mac_gen_props,omitempty"` + + Key bool `yaml:"key"` } type FieldConstraints struct { diff --git a/internal/services/crypto/endpoint.go b/internal/services/crypto/endpoint.go index db27563..e84bbb2 100644 --- a/internal/services/crypto/endpoint.go +++ b/internal/services/crypto/endpoint.go @@ -8,7 +8,7 @@ import ( "github.com/go-kit/kit/endpoint" log "github.com/sirupsen/logrus" "isosim/internal/iso" - "isosim/internal/services/v0/data" + "isosim/internal/services/data" "strings" ) @@ -98,7 +98,7 @@ func macGenEndpoint(s Service) endpoint.Endpoint { if parsedMsg, err := msg.ParseJSON(string(jsonStr)); err != nil { return MacGenResponse{Err: err}, nil } else { - macData, err = iso.FromParsedMsg(parsedMsg).Assemble() + macData, _, err = iso.FromParsedMsg(parsedMsg).Assemble() if err != nil { return MacGenResponse{Err: err}, nil } diff --git a/internal/services/v0/data/json_field_rep.go b/internal/services/data/json_field_rep.go similarity index 100% rename from internal/services/v0/data/json_field_rep.go rename to internal/services/data/json_field_rep.go diff --git a/internal/services/v0/data/json_field_rep_test.go b/internal/services/data/json_field_rep_test.go similarity index 87% rename from internal/services/v0/data/json_field_rep_test.go rename to internal/services/data/json_field_rep_test.go index 702c23a..08444fd 100644 --- a/internal/services/v0/data/json_field_rep_test.go +++ b/internal/services/data/json_field_rep_test.go @@ -11,7 +11,7 @@ import ( func init() { log.SetLevel(log.TraceLevel) - err := iso.ReadSpecs(filepath.Join("..", "..", "..", "..", "test", "testdata", "specs")) + err := iso.ReadSpecs(filepath.Join("..", "..", "..", "test", "testdata", "specs")) if err != nil { fmt.Print(err) } diff --git a/internal/services/v0/data/server_definition.go b/internal/services/data/server_definition.go similarity index 100% rename from internal/services/v0/data/server_definition.go rename to internal/services/data/server_definition.go diff --git a/internal/services/v0/handlers/isoserver/isoserver_handlers.go b/internal/services/handlers/isoserver_handlers.go similarity index 90% rename from internal/services/v0/handlers/isoserver/isoserver_handlers.go rename to internal/services/handlers/isoserver_handlers.go index c309107..95f824b 100644 --- a/internal/services/v0/handlers/isoserver/isoserver_handlers.go +++ b/internal/services/handlers/isoserver_handlers.go @@ -1,4 +1,4 @@ -package isoserver +package handlers import ( "encoding/json" @@ -14,6 +14,17 @@ import ( "strconv" ) +func AddAll() { + + addIsoServerHandlers() + addIsoServerSaveDefHandler() + fetchDefHandler() + startServerHandler() + addGetActiveServersHandler() + stopServerHandler() + +} + func addIsoServerHandlers() { log.Print("Adding ISO server handler .. ") @@ -180,3 +191,11 @@ func addGetActiveServersHandler() { }) } + +func sendError(rw http.ResponseWriter, errorMsg string) { + log.Debugln("isosim: ISO-Server Error. Error = " + errorMsg) + rw.Header().Set("X-IsoSim-ErrorText", errorMsg) + rw.WriteHeader(http.StatusBadRequest) + _, _ = rw.Write([]byte(errorMsg)) + +} diff --git a/internal/services/handlers/mg_hist_handler.go b/internal/services/handlers/mg_hist_handler.go new file mode 100644 index 0000000..fd059c6 --- /dev/null +++ b/internal/services/handlers/mg_hist_handler.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "isosim/internal/db" + "net/http" + "strconv" +) + +// defaultFormat is the default format of response (the other supported format is HTML) +const defaultFormat = "json" + +// MsgHistoryHandler register a HTTP handler for fetching historic messages for a given spec, message +func MsgHistoryHandler() { + + http.HandleFunc("/iso/v1/websim/msg_hist/last_n", func(rw http.ResponseWriter, req *http.Request) { + + rw.Header().Add("Access-Control-Allow-Origin", "http://localhost:3000") + + if err := req.ParseForm(); err != nil { + _, _ = rw.Write([]byte(err.Error())) + rw.WriteHeader(http.StatusBadRequest) + return + } + + format := defaultFormat + + msgId, _ := strconv.Atoi(req.Form.Get("msg_id")) + specId, _ := strconv.Atoi(req.Form.Get("spec_id")) + count, _ := strconv.Atoi(req.Form.Get("count")) + format = req.Form.Get("format") //can be json or html + + if format != "html" { + format = defaultFormat + } + + if res, err := db.ReadLast(specId, msgId, count); err != nil { + _, _ = rw.Write([]byte(err.Error())) + rw.WriteHeader(http.StatusBadRequest) + } else { + + if format == defaultFormat { + jsonResp, _ := json.Marshal(res) + _, _ = rw.Write(jsonResp) + return + } + + buf := bytes.Buffer{} + if len(res) > 0 { + buf.Write([]byte(``)) + } else { + rw.Write([]byte("No records found..")) + return + } + + for _, tmp := range res { + buf.Write([]byte(`
`)) + buf.Write([]byte(tmp)) + buf.Write([]byte("
")) + } + + buf.Write([]byte(``)) + rw.Header().Add("Content-Type", "text/html") + _, _ = rw.Write(buf.Bytes()) + } + + }) + +} diff --git a/internal/services/v0/handlers/misc/misc_handler.go b/internal/services/handlers/misc_handler.go similarity index 68% rename from internal/services/v0/handlers/misc/misc_handler.go rename to internal/services/handlers/misc_handler.go index 447c8c4..8f28fe8 100644 --- a/internal/services/v0/handlers/misc/misc_handler.go +++ b/internal/services/handlers/misc_handler.go @@ -1,13 +1,10 @@ -package misc +package handlers import ( - "bytes" "encoding/hex" - "encoding/json" "github.com/rkbalgi/libiso/hsm" "github.com/rkbalgi/libiso/net" log "github.com/sirupsen/logrus" - "isosim/internal/db" "isosim/internal/iso" "net/http" @@ -25,63 +22,8 @@ func init() { } -const defaultFormat = "json" - func AddMiscHandlers() { - http.HandleFunc("/iso/v1/websim/msg_hist/last_n", func(rw http.ResponseWriter, req *http.Request) { - - rw.Header().Add("Access-Control-Allow-Origin", "http://localhost:3000") - - if err := req.ParseForm(); err != nil { - _, _ = rw.Write([]byte(err.Error())) - rw.WriteHeader(http.StatusBadRequest) - return - } - - format := defaultFormat - - msgId, _ := strconv.Atoi(req.Form.Get("msg_id")) - specId, _ := strconv.Atoi(req.Form.Get("spec_id")) - count, _ := strconv.Atoi(req.Form.Get("count")) - format = req.Form.Get("format") //can be json or html - - if format == "" { - format = defaultFormat - } - - if res, err := db.ReadLast(specId, msgId, count); err != nil { - _, _ = rw.Write([]byte(err.Error())) - rw.WriteHeader(http.StatusBadRequest) - } else { - - if format == defaultFormat { - jsonResp, _ := json.Marshal(res) - _, _ = rw.Write(jsonResp) - return - } - - buf := bytes.Buffer{} - if len(res) > 0 { - buf.Write([]byte(``)) - } else { - rw.Write([]byte("No records found..")) - return - } - - for _, tmp := range res { - buf.Write([]byte(`
`)) - buf.Write([]byte(tmp)) - buf.Write([]byte("
")) - } - - buf.Write([]byte(``)) - rw.Header().Add("Content-Type", "text/html") - _, _ = rw.Write(buf.Bytes()) - } - - }) - http.HandleFunc("/iso/misc", func(rw http.ResponseWriter, req *http.Request) { http.ServeFile(rw, req, filepath.Join(iso.HTMLDir, "misc.html")) @@ -203,9 +145,3 @@ func AddMiscHandlers() { }) } - -func sendError(rw http.ResponseWriter, err string) { - rw.WriteHeader(500) - rw.Write([]byte(err)) - -} diff --git a/internal/services/service_registration.go b/internal/services/service_registration.go index 4662c7b..97ca835 100644 --- a/internal/services/service_registration.go +++ b/internal/services/service_registration.go @@ -4,8 +4,7 @@ import ( log "github.com/sirupsen/logrus" "isosim/internal/iso" "isosim/internal/services/crypto" - "isosim/internal/services/v0/handlers/isoserver" - "isosim/internal/services/v0/handlers/misc" + "isosim/internal/services/handlers" "isosim/internal/services/websim" "net/http" "path/filepath" @@ -54,7 +53,8 @@ func setRoutes() { subDir = "css" case strings.HasSuffix(req.RequestURI, ".js"): subDir = "js" - case strings.HasSuffix(req.RequestURI, ".ttf"), + case + strings.HasSuffix(req.RequestURI, ".ttf"), strings.HasSuffix(req.RequestURI, ".woff"), strings.HasSuffix(req.RequestURI, ".woff2"): subDir = "media" @@ -64,18 +64,21 @@ func setRoutes() { } if strings.HasPrefix(req.RequestURI, "/iso/v0/") && subDir != "" { - http.ServeFile(rw, req, filepath.Join(iso.HTMLDir, fileName)) } else { http.ServeFile(rw, req, filepath.Join(iso.HTMLDir, "react-fe", "build", "static", subDir, fileName)) } }) - isoserver.AddAll() - misc.AddMiscHandlers() + //old legacy handlers + handlers.AddAll() + handlers.AddMiscHandlers() //v1 websim.RegisterHTTPTransport() crypto.RegisterHTTPTransport() + //misc + handlers.MsgHistoryHandler() + } diff --git a/internal/services/v0/handlers/isoserver/isoserv.go b/internal/services/v0/handlers/isoserver/isoserv.go deleted file mode 100644 index ca903fe..0000000 --- a/internal/services/v0/handlers/isoserver/isoserv.go +++ /dev/null @@ -1,25 +0,0 @@ -package isoserver - -import ( - log "github.com/sirupsen/logrus" - "net/http" -) - -func AddAll() { - - addIsoServerHandlers() - addIsoServerSaveDefHandler() - fetchDefHandler() - startServerHandler() - addGetActiveServersHandler() - stopServerHandler() - -} - -func sendError(rw http.ResponseWriter, errorMsg string) { - log.Debugln("isosim: ISO-Server Error. Error = " + errorMsg) - rw.Header().Set("X-IsoSim-ErrorText", errorMsg) - rw.WriteHeader(http.StatusBadRequest) - _, _ = rw.Write([]byte(errorMsg)) - -} diff --git a/internal/services/websim/endpoints.go b/internal/services/websim/endpoints.go index 80e6445..e147d70 100644 --- a/internal/services/websim/endpoints.go +++ b/internal/services/websim/endpoints.go @@ -7,7 +7,7 @@ import ( "github.com/go-kit/kit/log" netutil "github.com/rkbalgi/libiso/net" "isosim/internal/iso" - "isosim/internal/services/v0/data" + "isosim/internal/services/data" ) type GetAllSpecsRequest struct{} diff --git a/internal/services/websim/service.go b/internal/services/websim/service.go index 0ced5da..9c64e6e 100644 --- a/internal/services/websim/service.go +++ b/internal/services/websim/service.go @@ -10,12 +10,14 @@ import ( "fmt" net2 "github.com/rkbalgi/libiso/net" log "github.com/sirupsen/logrus" + "io" "isosim/internal/db" "isosim/internal/iso" - "isosim/internal/services/v0/data" + "isosim/internal/services/data" "net" "sort" "strconv" + "sync" "time" ) @@ -25,6 +27,14 @@ type NetOptions struct { MLIType net2.MliType } +// cached network connections +var cachedNCC = sync.Map{} +var cachedNCCMu = sync.Mutex{} + +// inflight messages for persistent connections +var inFlightsMu = sync.RWMutex{} +var inFlights = make(map[string]chan *isoResponse) + // Service exposes the ISO WebSim API required by the frontend (browser) type Service interface { GetAllSpecs(ctx context.Context) ([]UISpec, error) @@ -39,6 +49,12 @@ type Service interface { type serviceImpl struct{} +type isoResponse struct { + responseMsg *iso.ParsedMsg + responseData []byte + err error +} + func New() Service { var service Service service = serviceImpl{} @@ -71,7 +87,7 @@ func (i serviceImpl) SendToHost(ctx context.Context, specId int, msgId int, netO } isoMsg := iso.FromParsedMsg(parsedMsg) - reqIsoMsg, err := isoMsg.Assemble() + reqIsoMsg, meta, err := isoMsg.Assemble() if err != nil { log.Errorln("Failed to assemble message", err.Error()) return nil, err @@ -79,6 +95,22 @@ func (i serviceImpl) SendToHost(ctx context.Context, specId int, msgId int, netO isoServerAddr := fmt.Sprintf("%s:%d", hostIpAddr.String(), netOpts.Port) + var ncc *net2.NetCatClient + if meta.MessageKey != "" { + // try to use a already open/cached connection + if ncc, err = getOrCreateNetClient(isoServerAddr, spec, netOpts.MLIType); err != nil { + return nil, err + } + } else { + //create a fresh connection + ncc = net2.NewNetCatClient(isoServerAddr, netOpts.MLIType) + if err := ncc.OpenConnection(); err != nil { + log.Errorln("Failed to connect to ISO Host @ " + isoServerAddr + " Error: " + err.Error()) + return nil, err + } + defer ncc.Close() + } + // log the message to db dbMsg := db.DbMessage{ SpecID: specId, @@ -88,28 +120,57 @@ func (i serviceImpl) SendToHost(ctx context.Context, specId int, msgId int, netO RequestMsg: hex.EncodeToString(reqIsoMsg), ParsedRequestMsg: ToJsonList(parsedMsg), } + defer func() { if err := db.Write(dbMsg); err != nil { - log.Warn("isosim: Failed to write to db..", err) + log.Warn("Failed to write message to hist-db (bolt)", err) } }() log.Debugf("Sending to Iso server @address - %s\n", isoServerAddr) - - ncc := net2.NewNetCatClient(isoServerAddr, netOpts.MLIType) - if err := ncc.OpenConnection(); err != nil { - log.Errorln("Failed to connect to ISO Host @ " + isoServerAddr + " Error: " + err.Error()) + log.Debugf("Assembled request msg = \n %s\nMliType = %v\n", hex.Dump(reqIsoMsg), netOpts.MLIType) + + if meta.MessageKey != "" { + + log.Debugf("Sending message with key - %s to server: %s\n", parsedMsg.MessageKey, isoServerAddr) + responseChan := make(chan *isoResponse, 0) + ctx, cancelFunc := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFunc() + + go send(ncc, reqIsoMsg, meta.MessageKey, responseChan) + for { + select { + case res := <-responseChan: + { + if res.err != nil { + return nil, err + } else { + respJson := ToJsonList(res.responseMsg) + dbMsg.ResponseTS = time.Now().Unix() + dbMsg.ResponseMsg = hex.EncodeToString(res.responseData) + dbMsg.ParsedResponseMsg = respJson + + return &respJson, nil + } + + } + case <-ctx.Done(): + close(responseChan) + timedOut(meta.MessageKey) + return nil, fmt.Errorf("isosim: Message with key [%s] timed out", meta.MessageKey) + } + } return nil, err + } - defer ncc.Close() - log.Debugf("Assembled request msg = \n%s\n, MliType = %v\n", hex.Dump(reqIsoMsg), netOpts.MLIType) + // non-persistent (i.e. connection per message) connections if err := ncc.Write(reqIsoMsg); err != nil { log.Errorln("Failed to send data to ISO Host Error= " + err.Error()) return nil, err } - responseData, err := ncc.ReadNextPacket() + responseData, err := ncc.Read(&net2.ReadOptions{Deadline: time.Now().Add(5 * time.Second)}) if err != nil { log.Errorln("Failed to read response from ISO Host. Error = " + err.Error()) return nil, err @@ -131,6 +192,98 @@ func (i serviceImpl) SendToHost(ctx context.Context, specId int, msgId int, netO } +func timedOut(key string) { + inFlightsMu.Lock() + delete(inFlights, key) + inFlightsMu.Unlock() + +} + +func send(ncc *net2.NetCatClient, reqData []byte, key string, responseChan chan *isoResponse) { + + inFlightsMu.Lock() + inFlights[key] = responseChan + inFlightsMu.Unlock() + + if err := ncc.Write(reqData); err != nil { + log.Errorln("Failed to send data to ISO Host Error= " + err.Error()) + responseChan <- &isoResponse{err: err} + return + } + +} + +func getOrCreateNetClient(addr string, spec *iso.Spec, mliType net2.MliType) (*net2.NetCatClient, error) { + + if ncc, ok := cachedNCC.Load(addr); ok { + return ncc.(*net2.NetCatClient), nil + } else { + + cachedNCCMu.Lock() + defer cachedNCCMu.Unlock() + + //do another check holding the lock + if ncc, ok := cachedNCC.Load(addr); ok { + return ncc.(*net2.NetCatClient), nil + } + + ncc := net2.NewNetCatClient(addr, mliType) + if err := ncc.OpenConnection(); err != nil { + log.Errorln("Failed to connect to ISO Host @ " + addr + " Error: " + err.Error()) + return nil, err + } + log.Infoln("Opened a persistent connection to ISO Host @ " + addr) + + //socket reader goroutine + go func(ncc *net2.NetCatClient) { + + for { + responseData, err := ncc.Read(nil) + if err != nil { + log.Errorln("Failed to read response from ISO Host. Error = " + err.Error()) + if err == io.EOF { + // socket closed + log.Errorf("Persistent connection to %s for spec: %s dropped.", addr, spec.Name) + cachedNCC.Delete(addr) + return + } + + } + + log.Debugf("Received response from ISO Host = %s, Addr: ", hex.EncodeToString(responseData)) + + msg := spec.FindTargetMsg(responseData) + if msg == nil { + log.Errorln("isosim: Unable to determine msg for incoming data " + hex.EncodeToString(responseData)) + return + } + log.Debugln("Message identified from incoming data = " + msg.Name) + if responseMsg, err := msg.Parse(responseData); err != nil { + log.Errorln("Failed to parse response from ISO Host. Error = " + err.Error()) + return + } else { + inFlightsMu.Lock() + log.Tracef("Looking up %s in in-flights: %v\n", responseMsg.MessageKey, inFlights) + resChan := inFlights[responseMsg.MessageKey] + if resChan != nil { + resChan <- &isoResponse{err: nil, responseData: responseData, responseMsg: responseMsg} + } else { + log.Infoln("Message with key - %s possibly timed out", responseMsg.MessageKey) + } + delete(inFlights, responseMsg.MessageKey) + inFlightsMu.Unlock() + } + } + + }(ncc) + + cachedNCC.Store(addr, ncc) + return ncc, nil + + } + +} + // ParseTrace parses a provided trace and returns a list of parsed fields func (serviceImpl) ParseTrace(ctx context.Context, specId int, msgId int, msgTrace string) (*[]data.JsonFieldDataRep, error) { diff --git a/test/testdata/appdata/isosim.bdb b/test/testdata/appdata/isosim.bdb index 2cbca1c093ad4cdf489b84d2d4f0c8fc61aa4350..088961c3fb6ffeb614571fb3d5271bc43388278c 100644 GIT binary patch delta 12064 zcmd^FdvILkb=P;VWy$M@*0yXU+c9eyY)h8p``o*C?G#zIj31C%1{@<~S^K0s{lJqn z!$X2lkJABTd_f<9gg7B7Z8I$>!B!_7CJvK!iVFqOl6EG+$S&zj36n84ZVJuxW7#&_jyEbVv>Fwvqn%QoLY2Rp=Sq%G^Kpov`yD-Z#hu9XW!nt?R^o(DsYu&*_sF$SOMiuyzgY{fOK^*JkABuKAICSKok5ul!f|KfKin65R=U`}5jD z;^0>;SZKSRSt?uhR0bzPgKc`p2wBXn_037F^-XE*z)a6h!;g#Jp?x2cEbSU&_EJ(y zmZMc>t(Q)_W^jIYBRb@7gJ%cZ>B~*rz6tMNLPsx>tI_Ia{yuvA3#6Ss^%0pxXVzA; zc!YNTBiN26(+}WfWBr#%N9{;bOR$h|rt^@v4(>4|d`H{QH-FE=+I95$ zqCT7cx~QA<^ni8~{Y9H@&@=C9v*_uck!=tONjkWZ%)pCpre8UyH3XmRZl%BfLx`BY zt^Cur(_`b>DtdU7w9+>dWE(wQ(wl>!9m_{*$xM3cYJCOW*QVFey$j$Y2F_{_KLgA8 z_YR(eH$FX}?V>NdtE~3dU}>zK^kTCg!=(zp7x`OMx7 z?)(IMm2G!yBLno&X4rW&KUlm&So#N~g&!Kl_$RK9FK^t0h zzAg0qjgZ5*5tVxn2*90Dn}!T=={`*Q)T`vGrMkE}+sHXZ!^%&*IvJU+ku_|&RTeyy zTLq6Rw(4yNoX$uT_T@8{c%(JcZEbox{l+T&HXgeG zLBR(fuE$MS9&EoLq7cC91{SLrAY;Ee2T60%(3WiXWfE3N%4yPc%PHmZcDjn`37(W_ zI=yR!z8HekGF)c44oOLk?`kXg-ptPFPlZ9$K=1gD)=GbDkxu&K?`e@7RPnp@6|z%h z?xZRCKu%v7?yqph%gw|HjQ7IG=gv?bha|KqAOpJsp29Ex6KP(<_TFr{fA{@7SZivf z2keq+lK!<0G-#?|&?!^YmfO42F9;DxJcD z(>SNK1}_avhyw;m@T5WS$PjMJk=KbS!6y8cb#%^O3*4eh&uYD6E$uqw=h4e!^K|J1 zH(yMr%TqhSui?4+QE_0AEO-W^!|6^M$wPt_-)iPFm=B zR=$w)r`%>UtUi`W&%R1n^|8##>hm`GcqN6cE>PnKq>d(=pmMzaH}L0p0+z#fKqXDq zlR62cl?7uNvW^K`{lGs1n_*$Q?^q=HkIF3HLcf{Uq1N0Ezz@z!aR@$nTuUZ{wb21mB-2^gbWGx@bSo3ttax<34!<{|$Rk*V< zEccq_d_TkEV5p~)etL|o48HZrdTO|25ih`D@>fJe8-3zWw74^{E=BeO#cCL_dglWjWMuPvH}R?_tgA-3_6ZB1q` zmd#`D8SJY8>3S8I(Y~U-P8Fck-xIeg#-BK+x%9E3UKdD&07@w^13p}Svda44%$|0Y z=RoglmJ7OR_+z~@$!>k*l&Hs3z`m@<8Ow%(Y+$}$CKiy8K`hiVfAE}O4`VqI+;x+{ zxkay#0`lY(W`hyg$0%!RrN4exdjmH5IbIw;*=3?c=uWn@L|p%gIhLs=n}bEKl!GXNL`L{%j#Zm3Z`KgQTO<^ z7mTJ@7q&7;^-p7|UoY~&qNiRZOEC6*M{peW*p1~u7lk5>iGJlCjbmK|ml+xF`#W-Q zBKzhwVKP2SCGTObv{mTP_n+4eRM7I^$j7bpuG>jR@E1KiX{ZVs;6ttS-%gOZ3kzw( z&D%NT;4;js+ApEc$^zU;=<^*8eFzGEaOK7oRfy=FH2F2yvEg$DD`6@af=NQA8Xjmx zeHXmGN(88^g4hbneM87}Sz5Hy&)KAnN5>O-J*_viq`6wKoDAF}CvO*PL#)?`^(wLc z{Nud~kEQCzZZ)*8H#}}|Vm}3t)XTE!n@-*{is0c%nmS-l(8^&`hXfKHB6Y51)FDW1 z6q(1W3AD7GYCjpYSJV(y3xPTJ%j!C}S_x611mu##G(^iSDx`w2s<2|tYWAt`Xeq|C zSu7#2Qgf%)xX{k|PQmu3=;Y8wsF~76Uok{8UNRY{W8^r@9cf7u1&%n#x>r>sS&JD( zoEbf&D_n_6Za@>;k8Anmm?j*ZcL8DLs)y zsL94VJh{do5`o~A%ARAS^QeC~Lhx}*g}~go5h12Qh&@Kuu~WtaDM z*sI4GyyE4BN*DAHSF%?(x4peROb< zHqxahXkxoIZ;Q;a{+Mubc-DkwIw5l|Un)DG7*xqQ)w5ZQ(6BtSLLtSn;*18(QCu9* zX%D9|fT04?SW7d3qhOQQLfbZI%PZOXsxb3-zozn8;WN~R+v6cA_|#C(@{tq7V=Bth znbO{3Ayf2=C?2E@zImCPPahwQ5LZx-TrZ*?rQMLfr`<}f!uJqadpGZ@+jbq`rAo>t zVPWpB%GG6=5*_I~FF{1envJ|HZ2m%=UQ0o-;V^`~p5igHq={wIFXhUm&tM|(7>+Y( zSI`*1QZQH17zUkDLVG@}#9F?J<@3#fI z`%BQtulXtf681i_F=zTrjLe_{7lD^?eRR;6adJ?vPx6YjR#YTevDS&TtXS8GwX9fO z(TX)PtV79qbJk5*>z0XE?t*(w~fNgR%9I*!me#GgR?~ ze;RwiiP-v;*!qpw`j@fwzsJ`95nJn8SEa>CjjzW*1ccYt{1j#bQIEV8Tc4>N`}sco zt7NlfSVo88bTG9EU5+<=C*C4YnkGg9-e&N-iYLA7$T4j}J-|39$!5)yo!S+&ew;LV zRZ3OKE1GUP?R5B-Rg|SF?{`=k8+1S?XWHpJ-S;XK8;~aFW8uQjgsPRbSTh8L%W&i76poO+V|A68F&jq<90_Ej--L}{Oq=}NYHm4vw{@dsZ4eeutFT|$oL5PjmucI}gmoQ*Vi$rK^z|W+uTCzRSc4lKoVsZiy{3l? zHcd(@2Sr}!l5+0AxQw9djGRdkr(rr-i@P*IMXwULhFvVUmRm#?%}FXV=2O^`O`lm_lAvSOl%KjWr#XPPGYQ! ztgD(25l>5f#DHO2Aq^}MJe?7q2K`XxL&U=X3ZM;xho_?r01(5QK-dW6ga!kB;&@4}dXp_)snJa zo9OY?(29wv9m9wviVaGwx=_lLAz8}+gHy^DO162SUt5B5mY?QapFNCleat4-Fe%wx z@<1g!Mmv`+$XKfnN)==vsSSOzE~0Ru$swMq(#IM&iJOu#&r|kGQK3*ST1Cf>s*{)5 zct@#}-~y)HGZ(~|zsg3GTYlN5ufcBH<}$vL`{X~%rOZqg2A_yh3~7BhZ4pjZ#~}Y}3GWh~{uigw%DjDjseqPB=P(-`mg` zbbX_4shp#gKqD!Xyh7w+A}2l+BB@g!&}~OfeHfFS`hX{5Rh?-lY~W%%M0TiZ5KcsS z%kv%xKCwN%dyLoWs&zeh{UYyqAyb&{&Q!+mNPwC@M8r<`rdHnf%h3}K-_AgDV`?z@ zC4=OO-Plve-Mcf^=06o{^M5*TLfbFlu;dq9!_KjyDt7xa4h90i2`knn+hBvmo;$Ebx;^ zhF#q;X6%qd=$N4Qtj7m9Awy)sQH71WIA3F6$|jtWRJg}SJVBEO;EX^xlvF{*CLiK( ziI}D_X;E=m?B4JU(v+o+vKCgIrI1IvmaB;qj9zb2r{9#0%TD)T3b&h9I&z*y&fH7o zbgt-_c=GczIwix-R&f*p+cQ;Bh&@pO&qBwQXfmtf+)d>;P?UA$q>%iIMlr5pGo&6W z>BaEyNl5F$PmUs!9PWw^A{?*)XIi#Go;V7d%S^eD_8g>bUU(2=%CO1Eu(=Y$#%4n`-rsLs^MxZd@1$H`P z5s>qrtk4_rxKMYkqA-jiHY|YCxxYCK7bgy+Tur}{)8^BCt6)(7&eu8D4NvDVv407C zUgx;BlO7${+GuEcW&OoQkB!*Eyo82ceD?M;@CA_*q*t-}$j=7g+0d~gm&md@(i4zV znI~Jd#yO$Thex5iPjqVEYap8_Norqg^buPyHjK5O+7hhbnr<6 zJZrL#xE4Rcl!v~PH5}Jc;(M+Y_b86zO`a;>Hn#t${zii-*YbVc{AD#g;(n93m!B`* r=Wi4LPu2hfhMN>BrHx(ezcn@M#eH}k_`Q$q9UDHX4}XTgnGOF7R)VUQ delta 1668 zcmdUv&1(}u7{=#ISU2752n7okohpHp`jIxK79mmvg+&g#bYm7^$&10 zW%sBzk7h5s2T^hsgn)t|2!etiw}9YHoSB`i>q{kqCkJL9ezWhqGyBf7<9WjKxG!)e z9J+M(#5_vd6{FAN3!mrfg%ioF&wB0yF7QG)ye9*Xn}9W=uU6RFHL~#~4ZI^_f$jGN ztlz394@(VJ4(nZNC5t|L>IR(Hq1|R@$EQt-OscIp5j8j6KoG0Rk*&vP%-e%T4;ejd z^oY@l(OIKaqeqR_qUK{au8_$_IO$=IZzc5s*%(V!3-}TZd}4RTbfnsIUv@`!F1zBF zfZqcC1o#_ZER*E(6#?&2T$huw1Y*DCh%NsA@?OQ!#n;XzwL+`S z8snMqOoh;vdpWh1INGVlVI9jRI_cpG5;>Lp7yi?Fq<-wz#?0Gvq1N;K;8^9j{ z4*~xGto~+4IUuv=gc`_k3Gke@;}TA;uQOZ>7OCB}`^D|NSWUgysmW}rah(^TQGOg% z$G`P!x+;oVx81l+E#4t!1FFsg+4G