diff --git a/README.md b/README.md index f501b16..3ac88f9 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ The sitemap shows the various URLs and hosts that have been accessed via the pro In the proxy page, hit `ctrl-r` on an entry and it will be sent to the replay page, where you can modify the request and re-issue it. If you hit `ctrl-r` in the Replay page, it'll duplicated the current item. +The replays support a history of your sent data. As you modify requests and send them, the history will grow. You can go back and view the previous requests. Editing a previous request that has a response will automatically create a new history entry so you don't lose your old request data. + #### Editing Highlight the request text box and hit `ctrl-e`. This will open the request in VI and let you edit it. @@ -112,6 +114,8 @@ You can then open that file with any editor and changes will auto-load into Glor ![replayer](./gif/replayer.gif) +You can have multiple external editors open; however, only the one currently focused in glorp will auto-send. + ### Log Page This is the general log info page and takes no user input. Glorp is set up such that any call to `log.Println` or similar will end up in this view. diff --git a/go.mod b/go.mod index 6f5d415..88948d4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/denandz/glorp -go 1.15 +go 1.16 require ( github.com/fsnotify/fsnotify v1.6.0 diff --git a/go.sum b/go.sum index e4cd02e..64b6b31 100644 --- a/go.sum +++ b/go.sum @@ -35,7 +35,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= @@ -115,7 +114,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/tests/oldsave.json b/tests/oldsave.json new file mode 100644 index 0000000..9eca85e --- /dev/null +++ b/tests/oldsave.json @@ -0,0 +1,132 @@ +{ + "Replays": [ + { + "ID": "1445586a66b80188", + "Host": "example.com", + "Port": "443", + "TLS": true, + "RawRequest": "R0VUIC8gSFRUUC8xLjENCkhvc3Q6IGV4YW1wbGUuY29tDQpBY2NlcHQ6ICovKg0KQ29ubmVjdGlvbjogY2xvc2UNClVzZXItQWdlbnQ6IGN1cmwvNy43NC4wDQoNCg==", + "RawResponse": "SFRUUC8xLjEgMjAwIE9LDQpBY2NlcHQtUmFuZ2VzOiBieXRlcw0KQWdlOiA0MjU4MDkNCkNhY2hlLUNvbnRyb2w6IG1heC1hZ2U9NjA0ODAwDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD1VVEYtOA0KRGF0ZTogVHVlLCAwMiBKYW4gMjAyNCAyMzoyNDoyOCBHTVQNCkV0YWc6ICIzMTQ3NTI2OTQ3Ig0KRXhwaXJlczogVHVlLCAwOSBKYW4gMjAyNCAyMzoyNDoyOCBHTVQNCkxhc3QtTW9kaWZpZWQ6IFRodSwgMTcgT2N0IDIwMTkgMDc6MTg6MjYgR01UDQpTZXJ2ZXI6IEVDUyAobGFhLzdCRDYpDQpWYXJ5OiBBY2NlcHQtRW5jb2RpbmcNClgtQ2FjaGU6IEhJVA0KQ29udGVudC1MZW5ndGg6IDEyNTYNCkNvbm5lY3Rpb246IGNsb3NlDQoNCjwhZG9jdHlwZSBodG1sPgo8aHRtbD4KPGhlYWQ+CiAgICA8dGl0bGU+RXhhbXBsZSBEb21haW48L3RpdGxlPgoKICAgIDxtZXRhIGNoYXJzZXQ9InV0Zi04IiAvPgogICAgPG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC10eXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiIC8+CiAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEiIC8+CiAgICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgYm9keSB7CiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2YwZjBmMjsKICAgICAgICBtYXJnaW46IDA7CiAgICAgICAgcGFkZGluZzogMDsKICAgICAgICBmb250LWZhbWlseTogLWFwcGxlLXN5c3RlbSwgc3lzdGVtLXVpLCBCbGlua01hY1N5c3RlbUZvbnQsICJTZWdvZSBVSSIsICJPcGVuIFNhbnMiLCAiSGVsdmV0aWNhIE5ldWUiLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmOwogICAgICAgIAogICAgfQogICAgZGl2IHsKICAgICAgICB3aWR0aDogNjAwcHg7CiAgICAgICAgbWFyZ2luOiA1ZW0gYXV0bzsKICAgICAgICBwYWRkaW5nOiAyZW07CiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2ZkZmRmZjsKICAgICAgICBib3JkZXItcmFkaXVzOiAwLjVlbTsKICAgICAgICBib3gtc2hhZG93OiAycHggM3B4IDdweCAycHggcmdiYSgwLDAsMCwwLjAyKTsKICAgIH0KICAgIGE6bGluaywgYTp2aXNpdGVkIHsKICAgICAgICBjb2xvcjogIzM4NDg4ZjsKICAgICAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7CiAgICB9CiAgICBAbWVkaWEgKG1heC13aWR0aDogNzAwcHgpIHsKICAgICAgICBkaXYgewogICAgICAgICAgICBtYXJnaW46IDAgYXV0bzsKICAgICAgICAgICAgd2lkdGg6IGF1dG87CiAgICAgICAgfQogICAgfQogICAgPC9zdHlsZT4gICAgCjwvaGVhZD4KCjxib2R5Pgo8ZGl2PgogICAgPGgxPkV4YW1wbGUgRG9tYWluPC9oMT4KICAgIDxwPlRoaXMgZG9tYWluIGlzIGZvciB1c2UgaW4gaWxsdXN0cmF0aXZlIGV4YW1wbGVzIGluIGRvY3VtZW50cy4gWW91IG1heSB1c2UgdGhpcwogICAgZG9tYWluIGluIGxpdGVyYXR1cmUgd2l0aG91dCBwcmlvciBjb29yZGluYXRpb24gb3IgYXNraW5nIGZvciBwZXJtaXNzaW9uLjwvcD4KICAgIDxwPjxhIGhyZWY9Imh0dHBzOi8vd3d3LmlhbmEub3JnL2RvbWFpbnMvZXhhbXBsZSI+TW9yZSBpbmZvcm1hdGlvbi4uLjwvYT48L3A+CjwvZGl2Pgo8L2JvZHk+CjwvaHRtbD4K", + "ResponseTime": "797.977292ms" + } + ], + "Proxyentries": [ + { + "_id": "1445586a66b80188", + "startedDateTime": "2024-01-02T23:24:06.021266539Z", + "time": 1436, + "request": { + "method": "GET", + "url": "https://example.com/", + "httpVersion": "HTTP/1.1", + "bodySize": 0, + "Raw": "R0VUIC8gSFRUUC8xLjENCkhvc3Q6IGV4YW1wbGUuY29tDQpVc2VyLUFnZW50OiBjdXJsLzcuNzQuMA0KQWNjZXB0OiAqLyoNCkFjY2VwdC1FbmNvZGluZzogZ3ppcA0KDQo=", + "Host": "example.com", + "TLS": false + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "headers": { + "Accept-Ranges": [ + "bytes" + ], + "Age": [ + "385056" + ], + "Cache-Control": [ + "max-age=604800" + ], + "Content-Length": [ + "1256" + ], + "Content-Type": [ + "text/html; charset=UTF-8" + ], + "Date": [ + "Tue, 02 Jan 2024 23:24:07 GMT" + ], + "Etag": [ + "\"3147526947\"" + ], + "Expires": [ + "Tue, 09 Jan 2024 23:24:07 GMT" + ], + "Last-Modified": [ + "Thu, 17 Oct 2019 07:18:26 GMT" + ], + "Server": [ + "ECS (laa/7BA2)" + ], + "Vary": [ + "Accept-Encoding" + ], + "X-Cache": [ + "HIT" + ] + }, + "bodySize": 1256, + "Raw": "SFRUUC8xLjEgMjAwIE9LDQpDb250ZW50LUxlbmd0aDogMTI1Ng0KQWNjZXB0LVJhbmdlczogYnl0ZXMNCkFnZTogMzg1MDU2DQpDYWNoZS1Db250cm9sOiBtYXgtYWdlPTYwNDgwMA0KQ29udGVudC1UeXBlOiB0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgNCkRhdGU6IFR1ZSwgMDIgSmFuIDIwMjQgMjM6MjQ6MDcgR01UDQpFdGFnOiAiMzE0NzUyNjk0NyINCkV4cGlyZXM6IFR1ZSwgMDkgSmFuIDIwMjQgMjM6MjQ6MDcgR01UDQpMYXN0LU1vZGlmaWVkOiBUaHUsIDE3IE9jdCAyMDE5IDA3OjE4OjI2IEdNVA0KU2VydmVyOiBFQ1MgKGxhYS83QkEyKQ0KVmFyeTogQWNjZXB0LUVuY29kaW5nDQpYLUNhY2hlOiBISVQNCg0KPCFkb2N0eXBlIGh0bWw+CjxodG1sPgo8aGVhZD4KICAgIDx0aXRsZT5FeGFtcGxlIERvbWFpbjwvdGl0bGU+CgogICAgPG1ldGEgY2hhcnNldD0idXRmLTgiIC8+CiAgICA8bWV0YSBodHRwLWVxdWl2PSJDb250ZW50LXR5cGUiIGNvbnRlbnQ9InRleHQvaHRtbDsgY2hhcnNldD11dGYtOCIgLz4KICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSIgLz4KICAgIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICBib2R5IHsKICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjZjBmMGYyOwogICAgICAgIG1hcmdpbjogMDsKICAgICAgICBwYWRkaW5nOiAwOwogICAgICAgIGZvbnQtZmFtaWx5OiAtYXBwbGUtc3lzdGVtLCBzeXN0ZW0tdWksIEJsaW5rTWFjU3lzdGVtRm9udCwgIlNlZ29lIFVJIiwgIk9wZW4gU2FucyIsICJIZWx2ZXRpY2EgTmV1ZSIsIEhlbHZldGljYSwgQXJpYWwsIHNhbnMtc2VyaWY7CiAgICAgICAgCiAgICB9CiAgICBkaXYgewogICAgICAgIHdpZHRoOiA2MDBweDsKICAgICAgICBtYXJnaW46IDVlbSBhdXRvOwogICAgICAgIHBhZGRpbmc6IDJlbTsKICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjZmRmZGZmOwogICAgICAgIGJvcmRlci1yYWRpdXM6IDAuNWVtOwogICAgICAgIGJveC1zaGFkb3c6IDJweCAzcHggN3B4IDJweCByZ2JhKDAsMCwwLDAuMDIpOwogICAgfQogICAgYTpsaW5rLCBhOnZpc2l0ZWQgewogICAgICAgIGNvbG9yOiAjMzg0ODhmOwogICAgICAgIHRleHQtZGVjb3JhdGlvbjogbm9uZTsKICAgIH0KICAgIEBtZWRpYSAobWF4LXdpZHRoOiA3MDBweCkgewogICAgICAgIGRpdiB7CiAgICAgICAgICAgIG1hcmdpbjogMCBhdXRvOwogICAgICAgICAgICB3aWR0aDogYXV0bzsKICAgICAgICB9CiAgICB9CiAgICA8L3N0eWxlPiAgICAKPC9oZWFkPgoKPGJvZHk+CjxkaXY+CiAgICA8aDE+RXhhbXBsZSBEb21haW48L2gxPgogICAgPHA+VGhpcyBkb21haW4gaXMgZm9yIHVzZSBpbiBpbGx1c3RyYXRpdmUgZXhhbXBsZXMgaW4gZG9jdW1lbnRzLiBZb3UgbWF5IHVzZSB0aGlzCiAgICBkb21haW4gaW4gbGl0ZXJhdHVyZSB3aXRob3V0IHByaW9yIGNvb3JkaW5hdGlvbiBvciBhc2tpbmcgZm9yIHBlcm1pc3Npb24uPC9wPgogICAgPHA+PGEgaHJlZj0iaHR0cHM6Ly93d3cuaWFuYS5vcmcvZG9tYWlucy9leGFtcGxlIj5Nb3JlIGluZm9ybWF0aW9uLi4uPC9hPjwvcD4KPC9kaXY+CjwvYm9keT4KPC9odG1sPgo=" + } + }, + { + "_id": "4f96cb38fd8fb1fc", + "startedDateTime": "2024-01-02T23:24:11.772473767Z", + "time": 190, + "request": { + "method": "GET", + "url": "https://example.com/asdf", + "httpVersion": "HTTP/1.1", + "bodySize": 0, + "Raw": "R0VUIC9hc2RmIEhUVFAvMS4xDQpIb3N0OiBleGFtcGxlLmNvbQ0KVXNlci1BZ2VudDogY3VybC83Ljc0LjANCkFjY2VwdDogKi8qDQpBY2NlcHQtRW5jb2Rpbmc6IGd6aXANCg0K", + "Host": "example.com", + "TLS": false + }, + "response": { + "status": 404, + "statusText": "Not Found", + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "headers": { + "Accept-Ranges": [ + "bytes" + ], + "Age": [ + "187982" + ], + "Cache-Control": [ + "max-age=604800" + ], + "Content-Length": [ + "433" + ], + "Content-Type": [ + "text/html; charset=UTF-8" + ], + "Date": [ + "Tue, 02 Jan 2024 23:24:11 GMT" + ], + "Expires": [ + "Tue, 09 Jan 2024 23:24:11 GMT" + ], + "Last-Modified": [ + "Sun, 31 Dec 2023 19:11:09 GMT" + ], + "Server": [ + "ECS (laa/7B44)" + ], + "Vary": [ + "Accept-Encoding" + ], + "X-Cache": [ + "404-HIT" + ] + }, + "bodySize": 433, + "Raw": "SFRUUC8xLjEgNDA0IE5vdCBGb3VuZA0KQ29udGVudC1MZW5ndGg6IDQzMw0KQWNjZXB0LVJhbmdlczogYnl0ZXMNCkFnZTogMTg3OTgyDQpDYWNoZS1Db250cm9sOiBtYXgtYWdlPTYwNDgwMA0KQ29udGVudC1UeXBlOiB0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgNCkRhdGU6IFR1ZSwgMDIgSmFuIDIwMjQgMjM6MjQ6MTEgR01UDQpFeHBpcmVzOiBUdWUsIDA5IEphbiAyMDI0IDIzOjI0OjExIEdNVA0KTGFzdC1Nb2RpZmllZDogU3VuLCAzMSBEZWMgMjAyMyAxOToxMTowOSBHTVQNClNlcnZlcjogRUNTIChsYWEvN0I0NCkNClZhcnk6IEFjY2VwdC1FbmNvZGluZw0KWC1DYWNoZTogNDA0LUhJVA0KDQo8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJpc28tODg1OS0xIj8+CjwhRE9DVFlQRSBodG1sIFBVQkxJQyAiLS8vVzNDLy9EVEQgWEhUTUwgMS4wIFRyYW5zaXRpb25hbC8vRU4iCiAgICAgICAgICJodHRwOi8vd3d3LnczLm9yZy9UUi94aHRtbDEvRFREL3hodG1sMS10cmFuc2l0aW9uYWwuZHRkIj4KPGh0bWwgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIHhtbDpsYW5nPSJlbiIgbGFuZz0iZW4iPgoJPGhlYWQ+CgkJPHRpdGxlPjQwNCAtIE5vdCBGb3VuZDwvdGl0bGU+Cgk8L2hlYWQ+Cgk8Ym9keT4KCQk8aDE+NDA0IC0gTm90IEZvdW5kPC9oMT4KCQk8c2NyaXB0IHR5cGU9InRleHQvamF2YXNjcmlwdCIgc3JjPSIvL29iai5hYy5iY29uLmVjZG5zLm5ldC9lY190cG1fYmNvbi5qcyI+PC9zY3JpcHQ+Cgk8L2JvZHk+CjwvaHRtbD4K" + } + } + ] +} diff --git a/tests/savev1.1.json b/tests/savev1.1.json new file mode 100644 index 0000000..b7745ac --- /dev/null +++ b/tests/savev1.1.json @@ -0,0 +1,148 @@ +{ + "Version": "v1.1", + "Replays": [ + { + "ID": "1445586a66b80188", + "Entries": [ + { + "ID": "1445586a66b80188", + "Host": "example.com", + "Port": "443", + "TLS": true, + "RawRequest": "R0VUIC8gSFRUUC8xLjENCkhvc3Q6IGV4YW1wbGUuY29tDQpBY2NlcHQ6ICovKg0KQ29ubmVjdGlvbjogY2xvc2UNClVzZXItQWdlbnQ6IGN1cmwvNy43NC4wDQoNCg==", + "RawResponse": "SFRUUC8xLjEgMjAwIE9LDQpBY2NlcHQtUmFuZ2VzOiBieXRlcw0KQWdlOiA0MjU4MDkNCkNhY2hlLUNvbnRyb2w6IG1heC1hZ2U9NjA0ODAwDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD1VVEYtOA0KRGF0ZTogVHVlLCAwMiBKYW4gMjAyNCAyMzoyNDoyOCBHTVQNCkV0YWc6ICIzMTQ3NTI2OTQ3Ig0KRXhwaXJlczogVHVlLCAwOSBKYW4gMjAyNCAyMzoyNDoyOCBHTVQNCkxhc3QtTW9kaWZpZWQ6IFRodSwgMTcgT2N0IDIwMTkgMDc6MTg6MjYgR01UDQpTZXJ2ZXI6IEVDUyAobGFhLzdCRDYpDQpWYXJ5OiBBY2NlcHQtRW5jb2RpbmcNClgtQ2FjaGU6IEhJVA0KQ29udGVudC1MZW5ndGg6IDEyNTYNCkNvbm5lY3Rpb246IGNsb3NlDQoNCjwhZG9jdHlwZSBodG1sPgo8aHRtbD4KPGhlYWQ+CiAgICA8dGl0bGU+RXhhbXBsZSBEb21haW48L3RpdGxlPgoKICAgIDxtZXRhIGNoYXJzZXQ9InV0Zi04IiAvPgogICAgPG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC10eXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiIC8+CiAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEiIC8+CiAgICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgYm9keSB7CiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2YwZjBmMjsKICAgICAgICBtYXJnaW46IDA7CiAgICAgICAgcGFkZGluZzogMDsKICAgICAgICBmb250LWZhbWlseTogLWFwcGxlLXN5c3RlbSwgc3lzdGVtLXVpLCBCbGlua01hY1N5c3RlbUZvbnQsICJTZWdvZSBVSSIsICJPcGVuIFNhbnMiLCAiSGVsdmV0aWNhIE5ldWUiLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmOwogICAgICAgIAogICAgfQogICAgZGl2IHsKICAgICAgICB3aWR0aDogNjAwcHg7CiAgICAgICAgbWFyZ2luOiA1ZW0gYXV0bzsKICAgICAgICBwYWRkaW5nOiAyZW07CiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2ZkZmRmZjsKICAgICAgICBib3JkZXItcmFkaXVzOiAwLjVlbTsKICAgICAgICBib3gtc2hhZG93OiAycHggM3B4IDdweCAycHggcmdiYSgwLDAsMCwwLjAyKTsKICAgIH0KICAgIGE6bGluaywgYTp2aXNpdGVkIHsKICAgICAgICBjb2xvcjogIzM4NDg4ZjsKICAgICAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7CiAgICB9CiAgICBAbWVkaWEgKG1heC13aWR0aDogNzAwcHgpIHsKICAgICAgICBkaXYgewogICAgICAgICAgICBtYXJnaW46IDAgYXV0bzsKICAgICAgICAgICAgd2lkdGg6IGF1dG87CiAgICAgICAgfQogICAgfQogICAgPC9zdHlsZT4gICAgCjwvaGVhZD4KCjxib2R5Pgo8ZGl2PgogICAgPGgxPkV4YW1wbGUgRG9tYWluPC9oMT4KICAgIDxwPlRoaXMgZG9tYWluIGlzIGZvciB1c2UgaW4gaWxsdXN0cmF0aXZlIGV4YW1wbGVzIGluIGRvY3VtZW50cy4gWW91IG1heSB1c2UgdGhpcwogICAgZG9tYWluIGluIGxpdGVyYXR1cmUgd2l0aG91dCBwcmlvciBjb29yZGluYXRpb24gb3IgYXNraW5nIGZvciBwZXJtaXNzaW9uLjwvcD4KICAgIDxwPjxhIGhyZWY9Imh0dHBzOi8vd3d3LmlhbmEub3JnL2RvbWFpbnMvZXhhbXBsZSI+TW9yZSBpbmZvcm1hdGlvbi4uLjwvYT48L3A+CjwvZGl2Pgo8L2JvZHk+CjwvaHRtbD4K", + "ResponseTime": "797.977292ms" + }, + { + "ID": "1445586a66b80188", + "Host": "example.com", + "Port": "443", + "TLS": true, + "RawRequest": "R0VUIC8gSFRUUC8xLjENCkhvc3Q6IGV4YW1wbGUuY29tDQpBY2NlcHQ6ICovKg0KQ29ubmVjdGlvbjogY2xvc2UNClVzZXItQWdlbnQ6IGZvbw0KDQo=", + "RawResponse": "SFRUUC8xLjEgMjAwIE9LDQpBY2NlcHQtUmFuZ2VzOiBieXRlcw0KQWdlOiA0Mjk5NzcNCkNhY2hlLUNvbnRyb2w6IG1heC1hZ2U9NjA0ODAwDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD1VVEYtOA0KRGF0ZTogV2VkLCAwMyBKYW4gMjAyNCAwMDozMzo1NiBHTVQNCkV0YWc6ICIzMTQ3NTI2OTQ3Ig0KRXhwaXJlczogV2VkLCAxMCBKYW4gMjAyNCAwMDozMzo1NiBHTVQNCkxhc3QtTW9kaWZpZWQ6IFRodSwgMTcgT2N0IDIwMTkgMDc6MTg6MjYgR01UDQpTZXJ2ZXI6IEVDUyAobGFhLzdCRDYpDQpWYXJ5OiBBY2NlcHQtRW5jb2RpbmcNClgtQ2FjaGU6IEhJVA0KQ29udGVudC1MZW5ndGg6IDEyNTYNCkNvbm5lY3Rpb246IGNsb3NlDQoNCjwhZG9jdHlwZSBodG1sPgo8aHRtbD4KPGhlYWQ+CiAgICA8dGl0bGU+RXhhbXBsZSBEb21haW48L3RpdGxlPgoKICAgIDxtZXRhIGNoYXJzZXQ9InV0Zi04IiAvPgogICAgPG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC10eXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiIC8+CiAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEiIC8+CiAgICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgYm9keSB7CiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2YwZjBmMjsKICAgICAgICBtYXJnaW46IDA7CiAgICAgICAgcGFkZGluZzogMDsKICAgICAgICBmb250LWZhbWlseTogLWFwcGxlLXN5c3RlbSwgc3lzdGVtLXVpLCBCbGlua01hY1N5c3RlbUZvbnQsICJTZWdvZSBVSSIsICJPcGVuIFNhbnMiLCAiSGVsdmV0aWNhIE5ldWUiLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmOwogICAgICAgIAogICAgfQogICAgZGl2IHsKICAgICAgICB3aWR0aDogNjAwcHg7CiAgICAgICAgbWFyZ2luOiA1ZW0gYXV0bzsKICAgICAgICBwYWRkaW5nOiAyZW07CiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2ZkZmRmZjsKICAgICAgICBib3JkZXItcmFkaXVzOiAwLjVlbTsKICAgICAgICBib3gtc2hhZG93OiAycHggM3B4IDdweCAycHggcmdiYSgwLDAsMCwwLjAyKTsKICAgIH0KICAgIGE6bGluaywgYTp2aXNpdGVkIHsKICAgICAgICBjb2xvcjogIzM4NDg4ZjsKICAgICAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7CiAgICB9CiAgICBAbWVkaWEgKG1heC13aWR0aDogNzAwcHgpIHsKICAgICAgICBkaXYgewogICAgICAgICAgICBtYXJnaW46IDAgYXV0bzsKICAgICAgICAgICAgd2lkdGg6IGF1dG87CiAgICAgICAgfQogICAgfQogICAgPC9zdHlsZT4gICAgCjwvaGVhZD4KCjxib2R5Pgo8ZGl2PgogICAgPGgxPkV4YW1wbGUgRG9tYWluPC9oMT4KICAgIDxwPlRoaXMgZG9tYWluIGlzIGZvciB1c2UgaW4gaWxsdXN0cmF0aXZlIGV4YW1wbGVzIGluIGRvY3VtZW50cy4gWW91IG1heSB1c2UgdGhpcwogICAgZG9tYWluIGluIGxpdGVyYXR1cmUgd2l0aG91dCBwcmlvciBjb29yZGluYXRpb24gb3IgYXNraW5nIGZvciBwZXJtaXNzaW9uLjwvcD4KICAgIDxwPjxhIGhyZWY9Imh0dHBzOi8vd3d3LmlhbmEub3JnL2RvbWFpbnMvZXhhbXBsZSI+TW9yZSBpbmZvcm1hdGlvbi4uLjwvYT48L3A+CjwvZGl2Pgo8L2JvZHk+CjwvaHRtbD4K", + "ResponseTime": "882.761078ms" + } + ], + "Selected": 1 + } + ], + "Proxyentries": [ + { + "_id": "1445586a66b80188", + "startedDateTime": "2024-01-02T23:24:06.021266539Z", + "time": 1436, + "request": { + "method": "GET", + "url": "https://example.com/", + "httpVersion": "HTTP/1.1", + "bodySize": 0, + "Raw": "R0VUIC8gSFRUUC8xLjENCkhvc3Q6IGV4YW1wbGUuY29tDQpVc2VyLUFnZW50OiBjdXJsLzcuNzQuMA0KQWNjZXB0OiAqLyoNCkFjY2VwdC1FbmNvZGluZzogZ3ppcA0KDQo=", + "Host": "example.com", + "TLS": false + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "headers": { + "Accept-Ranges": [ + "bytes" + ], + "Age": [ + "385056" + ], + "Cache-Control": [ + "max-age=604800" + ], + "Content-Length": [ + "1256" + ], + "Content-Type": [ + "text/html; charset=UTF-8" + ], + "Date": [ + "Tue, 02 Jan 2024 23:24:07 GMT" + ], + "Etag": [ + "\"3147526947\"" + ], + "Expires": [ + "Tue, 09 Jan 2024 23:24:07 GMT" + ], + "Last-Modified": [ + "Thu, 17 Oct 2019 07:18:26 GMT" + ], + "Server": [ + "ECS (laa/7BA2)" + ], + "Vary": [ + "Accept-Encoding" + ], + "X-Cache": [ + "HIT" + ] + }, + "bodySize": 1256, + "Raw": "SFRUUC8xLjEgMjAwIE9LDQpDb250ZW50LUxlbmd0aDogMTI1Ng0KQWNjZXB0LVJhbmdlczogYnl0ZXMNCkFnZTogMzg1MDU2DQpDYWNoZS1Db250cm9sOiBtYXgtYWdlPTYwNDgwMA0KQ29udGVudC1UeXBlOiB0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgNCkRhdGU6IFR1ZSwgMDIgSmFuIDIwMjQgMjM6MjQ6MDcgR01UDQpFdGFnOiAiMzE0NzUyNjk0NyINCkV4cGlyZXM6IFR1ZSwgMDkgSmFuIDIwMjQgMjM6MjQ6MDcgR01UDQpMYXN0LU1vZGlmaWVkOiBUaHUsIDE3IE9jdCAyMDE5IDA3OjE4OjI2IEdNVA0KU2VydmVyOiBFQ1MgKGxhYS83QkEyKQ0KVmFyeTogQWNjZXB0LUVuY29kaW5nDQpYLUNhY2hlOiBISVQNCg0KPCFkb2N0eXBlIGh0bWw+CjxodG1sPgo8aGVhZD4KICAgIDx0aXRsZT5FeGFtcGxlIERvbWFpbjwvdGl0bGU+CgogICAgPG1ldGEgY2hhcnNldD0idXRmLTgiIC8+CiAgICA8bWV0YSBodHRwLWVxdWl2PSJDb250ZW50LXR5cGUiIGNvbnRlbnQ9InRleHQvaHRtbDsgY2hhcnNldD11dGYtOCIgLz4KICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSIgLz4KICAgIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICBib2R5IHsKICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjZjBmMGYyOwogICAgICAgIG1hcmdpbjogMDsKICAgICAgICBwYWRkaW5nOiAwOwogICAgICAgIGZvbnQtZmFtaWx5OiAtYXBwbGUtc3lzdGVtLCBzeXN0ZW0tdWksIEJsaW5rTWFjU3lzdGVtRm9udCwgIlNlZ29lIFVJIiwgIk9wZW4gU2FucyIsICJIZWx2ZXRpY2EgTmV1ZSIsIEhlbHZldGljYSwgQXJpYWwsIHNhbnMtc2VyaWY7CiAgICAgICAgCiAgICB9CiAgICBkaXYgewogICAgICAgIHdpZHRoOiA2MDBweDsKICAgICAgICBtYXJnaW46IDVlbSBhdXRvOwogICAgICAgIHBhZGRpbmc6IDJlbTsKICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjZmRmZGZmOwogICAgICAgIGJvcmRlci1yYWRpdXM6IDAuNWVtOwogICAgICAgIGJveC1zaGFkb3c6IDJweCAzcHggN3B4IDJweCByZ2JhKDAsMCwwLDAuMDIpOwogICAgfQogICAgYTpsaW5rLCBhOnZpc2l0ZWQgewogICAgICAgIGNvbG9yOiAjMzg0ODhmOwogICAgICAgIHRleHQtZGVjb3JhdGlvbjogbm9uZTsKICAgIH0KICAgIEBtZWRpYSAobWF4LXdpZHRoOiA3MDBweCkgewogICAgICAgIGRpdiB7CiAgICAgICAgICAgIG1hcmdpbjogMCBhdXRvOwogICAgICAgICAgICB3aWR0aDogYXV0bzsKICAgICAgICB9CiAgICB9CiAgICA8L3N0eWxlPiAgICAKPC9oZWFkPgoKPGJvZHk+CjxkaXY+CiAgICA8aDE+RXhhbXBsZSBEb21haW48L2gxPgogICAgPHA+VGhpcyBkb21haW4gaXMgZm9yIHVzZSBpbiBpbGx1c3RyYXRpdmUgZXhhbXBsZXMgaW4gZG9jdW1lbnRzLiBZb3UgbWF5IHVzZSB0aGlzCiAgICBkb21haW4gaW4gbGl0ZXJhdHVyZSB3aXRob3V0IHByaW9yIGNvb3JkaW5hdGlvbiBvciBhc2tpbmcgZm9yIHBlcm1pc3Npb24uPC9wPgogICAgPHA+PGEgaHJlZj0iaHR0cHM6Ly93d3cuaWFuYS5vcmcvZG9tYWlucy9leGFtcGxlIj5Nb3JlIGluZm9ybWF0aW9uLi4uPC9hPjwvcD4KPC9kaXY+CjwvYm9keT4KPC9odG1sPgo=" + } + }, + { + "_id": "4f96cb38fd8fb1fc", + "startedDateTime": "2024-01-02T23:24:11.772473767Z", + "time": 190, + "request": { + "method": "GET", + "url": "https://example.com/asdf", + "httpVersion": "HTTP/1.1", + "bodySize": 0, + "Raw": "R0VUIC9hc2RmIEhUVFAvMS4xDQpIb3N0OiBleGFtcGxlLmNvbQ0KVXNlci1BZ2VudDogY3VybC83Ljc0LjANCkFjY2VwdDogKi8qDQpBY2NlcHQtRW5jb2Rpbmc6IGd6aXANCg0K", + "Host": "example.com", + "TLS": false + }, + "response": { + "status": 404, + "statusText": "Not Found", + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "headers": { + "Accept-Ranges": [ + "bytes" + ], + "Age": [ + "187982" + ], + "Cache-Control": [ + "max-age=604800" + ], + "Content-Length": [ + "433" + ], + "Content-Type": [ + "text/html; charset=UTF-8" + ], + "Date": [ + "Tue, 02 Jan 2024 23:24:11 GMT" + ], + "Expires": [ + "Tue, 09 Jan 2024 23:24:11 GMT" + ], + "Last-Modified": [ + "Sun, 31 Dec 2023 19:11:09 GMT" + ], + "Server": [ + "ECS (laa/7B44)" + ], + "Vary": [ + "Accept-Encoding" + ], + "X-Cache": [ + "404-HIT" + ] + }, + "bodySize": 433, + "Raw": "SFRUUC8xLjEgNDA0IE5vdCBGb3VuZA0KQ29udGVudC1MZW5ndGg6IDQzMw0KQWNjZXB0LVJhbmdlczogYnl0ZXMNCkFnZTogMTg3OTgyDQpDYWNoZS1Db250cm9sOiBtYXgtYWdlPTYwNDgwMA0KQ29udGVudC1UeXBlOiB0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgNCkRhdGU6IFR1ZSwgMDIgSmFuIDIwMjQgMjM6MjQ6MTEgR01UDQpFeHBpcmVzOiBUdWUsIDA5IEphbiAyMDI0IDIzOjI0OjExIEdNVA0KTGFzdC1Nb2RpZmllZDogU3VuLCAzMSBEZWMgMjAyMyAxOToxMTowOSBHTVQNClNlcnZlcjogRUNTIChsYWEvN0I0NCkNClZhcnk6IEFjY2VwdC1FbmNvZGluZw0KWC1DYWNoZTogNDA0LUhJVA0KDQo8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJpc28tODg1OS0xIj8+CjwhRE9DVFlQRSBodG1sIFBVQkxJQyAiLS8vVzNDLy9EVEQgWEhUTUwgMS4wIFRyYW5zaXRpb25hbC8vRU4iCiAgICAgICAgICJodHRwOi8vd3d3LnczLm9yZy9UUi94aHRtbDEvRFREL3hodG1sMS10cmFuc2l0aW9uYWwuZHRkIj4KPGh0bWwgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiIHhtbDpsYW5nPSJlbiIgbGFuZz0iZW4iPgoJPGhlYWQ+CgkJPHRpdGxlPjQwNCAtIE5vdCBGb3VuZDwvdGl0bGU+Cgk8L2hlYWQ+Cgk8Ym9keT4KCQk8aDE+NDA0IC0gTm90IEZvdW5kPC9oMT4KCQk8c2NyaXB0IHR5cGU9InRleHQvamF2YXNjcmlwdCIgc3JjPSIvL29iai5hYy5iY29uLmVjZG5zLm5ldC9lY190cG1fYmNvbi5qcyI+PC9zY3JpcHQ+Cgk8L2JvZHk+CjwvaHRtbD4K" + } + } + ] +} diff --git a/views/replayview.go b/views/replayview.go index 22e8a27..eccf4ac 100644 --- a/views/replayview.go +++ b/views/replayview.go @@ -9,6 +9,7 @@ import ( "os/exec" "runtime" "strconv" + "sync" "time" "github.com/denandz/glorp/replay" @@ -20,12 +21,15 @@ import ( // ReplayView - struct that holds the main replayview elements type ReplayView struct { - Layout *tview.Pages // The main replay view, all others should be underneath Layout - Table *tview.Table // the main table that lists all the replay items - request *TextPrimitive // http request box - response *TextPrimitive // http response box - responseMeta *tview.Table // metadata for size recieved and time taken - goButton *tview.Button // send button + Layout *tview.Pages // The main replay view, all others should be underneath Layout + Table *tview.Table // the main table that lists all the replay items + request *TextPrimitive // http request box + response *TextPrimitive // http response box + responseMeta *tview.Table // metadata for size recieved and time taken + goButton *tview.Button // send button + backButton *tview.Button // back history button + forwardButton *tview.Button // forward history button + history *tview.TextView // indicator for which item in the replay history we're lookingat host *tview.InputField // host field input port *tview.InputField // port input @@ -36,22 +40,53 @@ type ReplayView struct { id string // id of the currently selected replay item - entries map[string]*replay.Request // list of request in the replayer - could probably use the row identifier as the key, support renaming + replays map[string]*ReplayRequests // list of request in the replayer - could probably use the row identifier as the key, support renaming } -// AddItem method - can be called to add a replay.Request entry object +// ReplayRequests - hold an array of requests for a replay item and the currently selected array ID +type ReplayRequests struct { + ID string // The ID as displayed in the table + elements []*replay.Request // an array of replay requests + index int // the currently selected request + mu sync.Mutex +} + +func (view *ReplayView) LoadReplays(rr *ReplayRequests) { + newID := rr.ID + + if _, ok := view.replays[newID]; ok { + i := 1 + newID = fmt.Sprintf("%s-%d", rr.ID, i) + for _, ok := view.replays[newID]; ok; _, ok = view.replays[newID] { + i++ + newID = fmt.Sprintf("%s-%d", rr.ID, i) + } + } + log.Printf("[+] ReplayView - AddItem - Adding replay item with ID: %s\n", newID) + view.replays[newID] = rr + + rows := view.Table.GetRowCount() + view.Table.SetCell(rows, 0, tview.NewTableCell(newID)) +} + +// AddItem method - create a new replay entry func (view *ReplayView) AddItem(r *replay.Request) { newID := r.ID - if _, ok := view.entries[newID]; ok { + if _, ok := view.replays[newID]; ok { i := 1 newID = fmt.Sprintf("%s-%d", r.ID, i) - for _, ok := view.entries[newID]; ok; _, ok = view.entries[newID] { + for _, ok := view.replays[newID]; ok; _, ok = view.replays[newID] { i++ newID = fmt.Sprintf("%s-%d", r.ID, i) } } log.Printf("[+] ReplayView - AddItem - Adding replay item with ID: %s\n", newID) - view.entries[newID] = r + view.replays[newID] = &ReplayRequests{ + ID: newID, + elements: make([]*replay.Request, 1), + index: 0, + } + view.replays[newID].elements[0] = r r.ID = newID rows := view.Table.GetRowCount() @@ -59,24 +94,27 @@ func (view *ReplayView) AddItem(r *replay.Request) { } // RenameItem - change the name of an entry -func (view *ReplayView) RenameItem(r *replay.Request, newName string) { - if _, ok := view.entries[newName]; ok { +func (view *ReplayView) RenameItem(rr *ReplayRequests, newName string) { + rr.mu.Lock() + defer rr.mu.Unlock() + + if _, ok := view.replays[newName]; ok { log.Println("[!] Replay entry with ID " + newName + " already exists") return } - if _, ok := view.entries[r.ID]; ok && newName != r.ID { + if _, ok := view.replays[rr.ID]; ok && newName != rr.ID { n := view.Table.GetRowCount() for i := 0; i < n; i++ { - if view.Table.GetCell(i, 0).Text == r.ID { - originalID := r.ID - r.ID = newName + if view.Table.GetCell(i, 0).Text == rr.ID { + originalID := rr.ID + rr.ID = newName // updating table - view.Table.SetCell(i, 0, tview.NewTableCell(r.ID)) + view.Table.SetCell(i, 0, tview.NewTableCell(rr.ID)) // updating map - view.entries[newName] = r - delete(view.entries, originalID) + view.replays[newName] = rr + delete(view.replays, originalID) break } } @@ -84,22 +122,52 @@ func (view *ReplayView) RenameItem(r *replay.Request, newName string) { } // DeleteItem - delete an entry -func (view *ReplayView) DeleteItem(r *replay.Request) { - delete(view.entries, r.ID) +func (view *ReplayView) DeleteItem(rr *ReplayRequests) { + delete(view.replays, rr.ID) n := view.Table.GetRowCount() for i := 0; i < n; i++ { - if view.Table.GetCell(i, 0).Text == r.ID { + if view.Table.GetCell(i, 0).Text == rr.ID { view.Table.RemoveRow(i) break } } } +// setRawRequest - sets the raw request bytes. If the replay element currently has +// a response, duplicate it into a fresh one so we dont lose data +func updateRawRequest(rr *ReplayRequests, r *replay.Request, data []byte) (req *replay.Request) { + rr.mu.Lock() + defer rr.mu.Unlock() + + if len(r.RawResponse) > 0 { + new_request := r.Copy() + new_request.RawResponse = nil + new_request.ResponseTime = "" + new_request.RawRequest = data + + // Copy() ignores these pointers, manually move them across + new_request.ExternalFile = r.ExternalFile + new_request.Watcher = r.Watcher + r.ExternalFile = nil + r.Watcher = nil + + rr.elements = append(rr.elements, &new_request) + rr.index = len(rr.elements) - 1 + req = &new_request + } else { + r.RawRequest = data + req = r + } + + return +} + // externalEditor - called when the user toggles the external editor checkbox // If enabled, creates a goroutine to monitor an external file that is used // to update the request as it's loaded func (view *ReplayView) useExternalEditor(app *tview.Application, enable bool) { - if req, ok := view.entries[view.id]; ok { + if rr, ok := view.replays[view.id]; ok { + req := rr.elements[rr.index] if enable { file, err := ioutil.TempFile(os.TempDir(), "glorp") if err != nil { @@ -150,10 +218,10 @@ func (view *ReplayView) useExternalEditor(app *tview.Application, enable bool) { return } - req.RawRequest = dat + req = updateRawRequest(rr, req, dat) // if we are focused, redraw and fire if view.id == req.ID { - view.refreshReplay(req) + view.refreshReplay(rr) if view.autoSend.IsChecked() { view.sendRequest(app, req.ID) } else { @@ -196,7 +264,7 @@ func (view *ReplayView) useExternalEditor(app *tview.Application, enable bool) { req.ExternalFile = nil } - view.refreshReplay(req) + view.refreshReplay(rr) } } @@ -207,7 +275,7 @@ func (view *ReplayView) GetView() (title string, content tview.Primitive) { // Init - Initialization method for the replayer view func (view *ReplayView) Init(app *tview.Application) { - view.entries = make(map[string]*replay.Request) + view.replays = make(map[string]*ReplayRequests) view.responseMeta = tview.NewTable() view.responseMeta.SetCell(0, 0, tview.NewTableCell("Size:").SetTextColor(tcell.ColorMediumPurple)) view.responseMeta.SetCell(0, 2, tview.NewTableCell("Time:").SetTextColor(tcell.ColorMediumPurple)) @@ -220,16 +288,45 @@ func (view *ReplayView) Init(app *tview.Application) { view.request = NewTextPrimitive() view.request.SetBorder(true).SetTitle("Request") - // go and cancel buttons + // go and history buttons + view.backButton = tview.NewButton("<") + view.backButton.SetSelectedFunc(func() { + if view.id != "" { + view.replays[view.id].mu.Lock() + defer view.replays[view.id].mu.Unlock() + view.replays[view.id].index-- + if view.replays[view.id].index < 0 { + view.replays[view.id].index = len(view.replays[view.id].elements) - 1 + } + view.refreshReplay(view.replays[view.id]) + } + }) + + view.forwardButton = tview.NewButton(">") + view.forwardButton.SetSelectedFunc(func() { + if view.id != "" { + view.replays[view.id].mu.Lock() + defer view.replays[view.id].mu.Unlock() + view.replays[view.id].index = (view.replays[view.id].index + 1) % len(view.replays[view.id].elements) + view.refreshReplay(view.replays[view.id]) + } + }) + view.goButton = tview.NewButton("Go") + view.goButton.SetSelectedFunc(func() { + view.sendRequest(app, view.id) + }) + + view.history = tview.NewTextView() + view.history.SetTextAlign(tview.AlignCenter) // Host, Port, TLS and Auto-content-length fields view.host = tview.NewInputField() view.host.SetLabelColor(tcell.ColorMediumPurple) view.host.SetLabel("Host ") view.host.SetChangedFunc(func(text string) { - if req, ok := view.entries[view.id]; ok { - req.Host = text + if rr, ok := view.replays[view.id]; ok { + rr.elements[rr.index].Host = text } }) @@ -237,8 +334,8 @@ func (view *ReplayView) Init(app *tview.Application) { view.port.SetLabel("Port ").SetAcceptanceFunc(tview.InputFieldInteger) view.port.SetLabelColor(tcell.ColorMediumPurple) view.port.SetChangedFunc(func(text string) { - if req, ok := view.entries[view.id]; ok { - req.Port = text + if rr, ok := view.replays[view.id]; ok { + rr.elements[rr.index].Port = text } }) @@ -246,8 +343,8 @@ func (view *ReplayView) Init(app *tview.Application) { view.tls.SetLabelColor(tcell.ColorMediumPurple) view.tls.SetLabel("TLS ") view.tls.SetChangedFunc(func(checked bool) { - if req, ok := view.entries[view.id]; ok { - req.TLS = checked + if rr, ok := view.replays[view.id]; ok { + rr.elements[rr.index].TLS = checked } }) @@ -269,10 +366,6 @@ func (view *ReplayView) Init(app *tview.Application) { view.response = NewTextPrimitive() view.response.SetBorder(true).SetTitle("Response") - view.goButton.SetSelectedFunc(func() { - view.sendRequest(app, view.id) - }) - view.Table = tview.NewTable() view.Table.SetBorders(false).SetSeparator(tview.Borders.Vertical) @@ -282,7 +375,7 @@ func (view *ReplayView) Init(app *tview.Application) { view.Table.SetSelectedFunc(func(row int, column int) { view.id = view.Table.GetCell(row, 0).Text if view.id != "" { - view.refreshReplay(view.entries[view.id]) + view.refreshReplay(view.replays[view.id]) } }) @@ -296,13 +389,14 @@ func (view *ReplayView) Init(app *tview.Application) { view.id = view.Table.GetCell(row, 0).Text if view.id != "" { - view.refreshReplay(view.entries[view.id]) + view.refreshReplay(view.replays[view.id]) } }) view.request.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Key() == tcell.KeyCtrlE { - if req, ok := view.entries[view.id]; ok && !view.externalEditor.IsChecked() { + switch event.Key() { + case tcell.KeyCtrlE: + if rr, ok := view.replays[view.id]; ok && !view.externalEditor.IsChecked() { if runtime.GOOS == "windows" { log.Println("[!] Built-in editors are not supported under windows yet") return event @@ -310,6 +404,7 @@ func (view *ReplayView) Init(app *tview.Application) { app.EnableMouse(false) app.Suspend(func() { + req := rr.elements[rr.index] file, err := ioutil.TempFile(os.TempDir(), "glorp") if err != nil { log.Println(err) @@ -334,29 +429,36 @@ func (view *ReplayView) Init(app *tview.Application) { return } - req.RawRequest = dat - - view.refreshReplay(req) + //req.entries[req.index].RawRequest = dat + updateRawRequest(rr, req, dat) + view.refreshReplay(rr) if view.autoSend.IsChecked() { - view.sendRequest(app, req.ID) + view.sendRequest(app, rr.ID) } }) app.EnableMouse(true) } - } else if event.Key() == tcell.KeyCtrlS { - if req, ok := view.entries[view.id]; ok { - saveModal(app, view.Layout, req.RawRequest) + + case tcell.KeyCtrlS: + if req, ok := view.replays[view.id]; ok { + saveModal(app, view.Layout, req.elements[req.index].RawRequest) } - } + case tcell.KeyLeft: + view.backButton.InputHandler()(tcell.NewEventKey(tcell.KeyEnter, 'q', 0), func(p tview.Primitive) {}) + + case tcell.KeyRight: + view.forwardButton.InputHandler()(tcell.NewEventKey(tcell.KeyEnter, 'q', 0), func(p tview.Primitive) {}) + + } return event }) // ctrl-e on the respose box should drop into read-only VI displaying data view.response.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyCtrlE { - if req, ok := view.entries[view.id]; ok { + if req, ok := view.replays[view.id]; ok { if runtime.GOOS == "windows" { log.Println("[!] Built-in editors are not supported under windows yet") return event @@ -371,7 +473,7 @@ func (view *ReplayView) Init(app *tview.Application) { } defer os.Remove(file.Name()) - file.Write(req.RawResponse) + file.Write(req.elements[req.index].RawResponse) file.Close() cmd := exec.Command("/usr/bin/view", "-b", file.Name()) cmd.Stdout = os.Stdout @@ -385,8 +487,8 @@ func (view *ReplayView) Init(app *tview.Application) { app.EnableMouse(true) } } else if event.Key() == tcell.KeyCtrlS { - if req, ok := view.entries[view.id]; ok { - saveModal(app, view.Layout, req.RawResponse) + if req, ok := view.replays[view.id]; ok { + saveModal(app, view.Layout, req.elements[req.index].RawResponse) } } @@ -406,7 +508,10 @@ func (view *ReplayView) Init(app *tview.Application) { requestFlexView.SetDirection(tview.FlexRow) requestFlexView.AddItem(connectionForm, 2, 1, false) requestFlexView.AddItem(view.request, 0, 8, false) - requestFlexView.AddItem(view.goButton, 1, 1, false) + + bottomRow := tview.NewFlex() + bottomRow.AddItem(view.goButton, 0, 7, false).AddItem(view.backButton, 0, 1, false).AddItem(view.history, 0, 1, false).AddItem(view.forwardButton, 0, 1, false) + requestFlexView.AddItem(bottomRow, 1, 1, false) responseFlexView := tview.NewFlex() responseFlexView.SetDirection(tview.FlexRow) @@ -427,6 +532,8 @@ func (view *ReplayView) Init(app *tview.Application) { view.autoSend, view.request, view.goButton, + view.backButton, + view.forwardButton, view.response, } @@ -451,9 +558,9 @@ func (view *ReplayView) Init(app *tview.Application) { app.SetFocus(focusRing.Value.(tview.Primitive)) case tcell.KeyCtrlX: - if _, ok := view.entries[view.id]; ok { + if _, ok := view.replays[view.id]; ok { stringModal(app, view.Layout, "Rename Replay", view.id, func(s string) { - if r, ok := view.entries[view.id]; ok { + if r, ok := view.replays[view.id]; ok { view.RenameItem(r, s) view.id = r.ID } @@ -461,7 +568,7 @@ func (view *ReplayView) Init(app *tview.Application) { } case tcell.KeyCtrlD: - if entry, ok := view.entries[view.id]; ok { + if entry, ok := view.replays[view.id]; ok { boolModal(app, view.Layout, "Delete "+entry.ID+"?", func(b bool) { if b { view.DeleteItem(entry) @@ -470,8 +577,10 @@ func (view *ReplayView) Init(app *tview.Application) { } case tcell.KeyCtrlR: - if req, ok := view.entries[view.id]; ok { - replayEntry := req.Copy() + if rr, ok := view.replays[view.id]; ok { + replayEntry := rr.elements[rr.index].Copy() + replayEntry.RawResponse = nil + replayEntry.ResponseTime = "" view.AddItem(&replayEntry) } @@ -489,20 +598,21 @@ func (view *ReplayView) Init(app *tview.Application) { } // refresh the replay view, loading a specific request -func (view *ReplayView) refreshReplay(r *replay.Request) { +func (view *ReplayView) refreshReplay(rr *ReplayRequests) { view.request.Clear() view.response.Clear() - view.host.SetText(r.Host) - view.port.SetText(r.Port) - view.tls.SetChecked(r.TLS) + view.host.SetText(rr.elements[rr.index].Host) + view.port.SetText(rr.elements[rr.index].Port) + view.tls.SetChecked(rr.elements[rr.index].TLS) + view.history.SetText(strconv.Itoa(rr.index + 1)) - fmt.Fprint(view.request, string(r.RawRequest)) - fmt.Fprint(view.response, string(r.RawResponse)) + fmt.Fprint(view.request, string(rr.elements[rr.index].RawRequest)) + fmt.Fprint(view.response, string(rr.elements[rr.index].RawResponse)) // if an external file exists, show it in the request title - if r.ExternalFile != nil { - view.request.SetTitle("Request - File " + r.ExternalFile.Name()) + if rr.elements[rr.index].ExternalFile != nil { + view.request.SetTitle("Request - File " + rr.elements[rr.index].ExternalFile.Name()) view.request.SetBorderColor(tcell.ColorDarkSeaGreen) view.externalEditor.SetChecked(true) } else { @@ -519,13 +629,25 @@ func (view *ReplayView) refreshReplay(r *replay.Request) { view.response.ScrollToBeginning() - view.responseMeta.SetCell(0, 1, tview.NewTableCell(strconv.Itoa(len(r.RawResponse)))) - view.responseMeta.SetCell(0, 3, tview.NewTableCell(r.ResponseTime)) + view.responseMeta.SetCell(0, 1, tview.NewTableCell(strconv.Itoa(len(rr.elements[rr.index].RawResponse)))) + view.responseMeta.SetCell(0, 3, tview.NewTableCell(rr.elements[rr.index].ResponseTime)) } -// Send the request +// Send the request - save the response as a new entry func (view *ReplayView) sendRequest(app *tview.Application, id string) { - if req, ok := view.entries[id]; ok { + if rr, ok := view.replays[id]; ok { + rr.mu.Lock() + defer rr.mu.Unlock() + + if len(rr.elements[rr.index].RawResponse) > 0 { + // A response already exists, create a new item to track history + r := rr.elements[rr.index].Copy() + rr.elements = append(rr.elements, &r) + rr.index = len(rr.elements) - 1 // set the selected item to the last entry + } + + req := rr.elements[rr.index] + view.response.Clear() if view.updateContentLength.IsChecked() { @@ -536,9 +658,9 @@ func (view *ReplayView) sendRequest(app *tview.Application, id string) { go func() { size, err := req.SendRequest() - if req == view.entries[view.id] { + if req == view.replays[view.id].elements[view.replays[view.id].index] { if size > 0 { - view.refreshReplay(req) + view.refreshReplay(rr) } else { view.responseMeta.SetCell(0, 1, tview.NewTableCell("ERROR")) view.responseMeta.SetCell(0, 3, tview.NewTableCell("ERROR")) diff --git a/views/saveview.go b/views/saveview.go index 7514fb2..f28a9c8 100644 --- a/views/saveview.go +++ b/views/saveview.go @@ -2,7 +2,7 @@ package views import ( "encoding/json" - "io/ioutil" + "io" "log" "os" "sort" @@ -19,10 +19,24 @@ type SaveRestoreView struct { } type savefile struct { + Version string `json:",omitempty"` + Replays []ReplaySaves + Proxyentries []modifier.Entry +} + +// old style save file +type legacysavefile struct { Replays []replay.Request Proxyentries []modifier.Entry } +// Like ReplayRequests, but we want to store each replay directly in here +type ReplaySaves struct { + ID string // The ID as displayed in the table + Entries []replay.Request // an array of replay requests + Selected int // the currently selected request +} + // GetView - should return a title and the top-level primitive func (view *SaveRestoreView) GetView() (title string, content tview.Primitive) { return "Save/Load", view.Layout @@ -72,15 +86,26 @@ func (view *SaveRestoreView) Init(app *tview.Application, replays *ReplayView, p } // Save - spool the replay and proxy state off to a file -func Save(filename string, replays *ReplayView, proxy *ProxyView) bool { +func Save(filename string, replayview *ReplayView, proxy *ProxyView) bool { if filename == "" { return false } - var replayentries []replay.Request + //var replayentries []replay.Request + var replays []ReplaySaves + + for _, v := range replayview.replays { + rs := ReplaySaves{ + ID: v.ID, + Selected: v.index, + Entries: make([]replay.Request, len(v.elements)), + } + + for i, v := range v.elements { + rs.Entries[i] = v.Copy() + } - for _, value := range replays.entries { - replayentries = append(replayentries, *value) + replays = append(replays, rs) } var proxyentries []modifier.Entry @@ -96,7 +121,8 @@ func Save(filename string, replays *ReplayView, proxy *ProxyView) bool { }) s := &savefile{ - Replays: replayentries, + Version: "v1.1", + Replays: replays, Proxyentries: proxyentries, } @@ -108,7 +134,7 @@ func Save(filename string, replays *ReplayView, proxy *ProxyView) bool { return false } - err = ioutil.WriteFile(filename, jsonData, 0644) + err = os.WriteFile(filename, jsonData, 0644) if err != nil { log.Println(err) return false @@ -118,8 +144,8 @@ func Save(filename string, replays *ReplayView, proxy *ProxyView) bool { return true } -// Load - needs to read a json file, clear out the proxy and replay ables and repopulate them -func Load(filename string, replays *ReplayView, prox *ProxyView, sitemap *SiteMapView) bool { +// Load - needs to read a json file, clear out the proxy and replay tables and repopulate them +func Load(filename string, replayview *ReplayView, prox *ProxyView, sitemap *SiteMapView) bool { f, err := os.Open(filename) if err != nil { log.Println(err) @@ -127,7 +153,7 @@ func Load(filename string, replays *ReplayView, prox *ProxyView, sitemap *SiteMa } defer f.Close() - fileBytes, err := ioutil.ReadAll(f) + fileBytes, err := io.ReadAll(f) if err != nil { log.Println(err) return false @@ -140,17 +166,58 @@ func Load(filename string, replays *ReplayView, prox *ProxyView, sitemap *SiteMa return false } - replays.Table.Clear() + if s.Version == "v1.1" { + replayview.Table.Clear() - prox.Logger.Reset() - for _, v := range s.Proxyentries { - prox.Logger.AddEntry(v) - } - prox.reloadtable() - sitemap.reload() + prox.Logger.Reset() + for _, v := range s.Proxyentries { + prox.Logger.AddEntry(v) + } + prox.reloadtable() + sitemap.reload() + + for _, v := range s.Replays { + rr := ReplayRequests{ + ID: v.ID, + index: v.Selected, + elements: make([]*replay.Request, len(v.Entries)), + } - for i := range s.Replays { - replays.AddItem(&s.Replays[i]) + log.Printf("[+] Loaded %d replay entries for id %s", len(v.Entries), v.ID) + + for i, request := range v.Entries { + r := request.Copy() + rr.elements[i] = &r + } + + replayview.LoadReplays(&rr) + } + } else if s.Version == "" { + // old style save file + log.Printf("[+] No version number in save file - loading old-style save") + + ls := new(legacysavefile) + err = json.Unmarshal(fileBytes, &ls) + if err != nil { + log.Println(err) + return false + } + + replayview.Table.Clear() + + prox.Logger.Reset() + for _, v := range ls.Proxyentries { + prox.Logger.AddEntry(v) + } + prox.reloadtable() + sitemap.reload() + + for i := range ls.Replays { + replayview.AddItem(&ls.Replays[i]) + } + } else { + log.Printf("[!] Unknown save file version") + return false } return true diff --git a/views/saveview_test.go b/views/saveview_test.go new file mode 100644 index 0000000..f0926e7 --- /dev/null +++ b/views/saveview_test.go @@ -0,0 +1,41 @@ +package views + +import ( + "testing" +) + +func TestLoad(t *testing.T) { + _, proxyview, sitemapview, replayview, _ := initializeTestApp() + + if Load("../tests/savev1.1.json", replayview, proxyview, sitemapview) == false { + t.Errorf("TestLoad Load: got %v want %v", false, true) + } + + if l := len(replayview.replays); l != 1 { + t.Errorf("TestLoad unexpected number of replays: got %v want %v", l, 1) + } + + if l := len(replayview.replays["1445586a66b80188"].elements); l != 2 { + t.Errorf("TestLoad unexpected number of replays: got %v want %v", l, 1) + } + + if l := len(proxyview.Logger.GetEntries()); l != 2 { + t.Errorf("TestLoad unexpected number of proxy entires: got %v want %v", l, 2) + } +} + +func TestLegacyLoad(t *testing.T) { + _, proxyview, sitemapview, replayview, _ := initializeTestApp() + + if Load("../tests/oldsave.json", replayview, proxyview, sitemapview) == false { + t.Errorf("TestLegacyLoad Load: got %v want %v", false, true) + } + + if l := len(replayview.replays); l != 1 { + t.Errorf("TestLegacyLoad unexpected number of replays: got %v want %v", l, 1) + } + + if l := len(proxyview.Logger.GetEntries()); l != 2 { + t.Errorf("TestLegacyLoad unexpected number of proxy entires: got %v want %v", l, 2) + } +} diff --git a/views/test.go b/views/test.go new file mode 100644 index 0000000..d044774 --- /dev/null +++ b/views/test.go @@ -0,0 +1,39 @@ +package views + +import ( + "github.com/denandz/glorp/modifier" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +func initializeTestApp() (*tview.Application, *ProxyView, *SiteMapView, *ReplayView, *SaveRestoreView) { + simScreen := tcell.NewSimulationScreen("UTF-8") + simScreen.Init() + simScreen.SetSize(10, 10) + + app := tview.NewApplication() + app.SetScreen(simScreen) + + // create the replayview stuff + replayview := new(ReplayView) + replayview.Init(app) + + proxychan := make(chan modifier.Notification, 1024) + sitemapchan := make(chan modifier.Notification, 1024) + logger := modifier.NewLogger(app, proxychan, sitemapchan) + //proxy.StartProxy(logger, config) + + // create the main proxy window + proxyview := new(ProxyView) + proxyview.Init(app, replayview, logger, proxychan) + + // sitemap view + sitemapview := new(SiteMapView) + sitemapview.Init(app, proxyview.Logger, sitemapchan) + + // save view + saveview := new(SaveRestoreView) + saveview.Init(app, replayview, proxyview, sitemapview) + + return app, proxyview, sitemapview, replayview, saveview +}