diff --git a/.gitignore b/.gitignore index 2d84d31..9a9c8f1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ ssh-sync ssh-sync.exe ssh-sync-installer.exe win-build/Output/ +__debug_bin* \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 5d8ef13..b075b58 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,16 +1,15 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", "configurations": [ { - "name": "Launch Package", + "name": "Attach to Sunbeam", "type": "go", - "request": "launch", - "mode": "auto", - "program": "main.go", - "args": ["upload"] + "debugAdapter": "dlv-dap", + "request": "attach", + "mode": "remote", + "remotePath": "${workspaceFolder}", + "port": 2345, + "host": "127.0.0.1", + "preLaunchTask": "Run headless dlv" // Here ! } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..e7bafdf --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,33 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run headless dlv", + "type": "process", + "command": "dlv", + "args": [ + "debug", + "--headless", + "--listen=:2345", + "--api-version=2", + "${workspaceFolder}/main.go", + "--", + "interactive" + ], + "isBackground": true, + "problemMatcher": { + "owner": "go", + "pattern": { + "regexp": "^API server listening at: .+:\\d+$", + "line": 1, + "message": 1 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^API server listening at:", + "endsPattern": "^API server listening at:" + } + } + } + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod index 315f203..2e4ddc4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/therealpaulgg/ssh-sync -go 1.19 +go 1.21 require ( github.com/gobwas/ws v1.1.0 @@ -12,9 +12,18 @@ require ( ) require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/charmbracelet/bubbletea v1.1.0 // indirect + github.com/charmbracelet/lipgloss v0.13.1 // indirect + github.com/charmbracelet/x/ansi v0.3.2 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/goccy/go-json v0.10.2 // indirect @@ -24,13 +33,26 @@ require ( github.com/lestrrat-go/httprc v1.0.5 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index aa699c5..1e9a70f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,29 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= +github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= +github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/lipgloss v0.13.1 h1:Oik/oqDTMVA01GetT4JdEC033dNzWoQHdWnHnQmXE2A= +github.com/charmbracelet/lipgloss v0.13.1/go.mod h1:zaYVJ2xKSKEnTEEbX6uAHabh2d975RJ+0yfkFpRBz5U= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY= +github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -5,6 +31,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= @@ -32,10 +60,46 @@ github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55F github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= +github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw= github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= @@ -53,9 +117,22 @@ golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index 714247b..0b6e946 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "os" "github.com/therealpaulgg/ssh-sync/pkg/actions" + "github.com/therealpaulgg/ssh-sync/pkg/actions/interactive" "github.com/urfave/cli/v2" ) @@ -45,8 +46,9 @@ func main() { Action: actions.Download, }, { - Name: "challenge-response", - Action: actions.ChallengeResponse, + Name: "challenge-response", + ArgsUsage: "[challenge-phrase]", + Action: actions.ChallengeResponse, }, { Name: "list-machines", @@ -61,6 +63,12 @@ func main() { Name: "reset", Action: actions.Reset, }, + { + Name: "interactive", + Description: "Uses a TUI mode for interacting with keys and config", + Usage: "Interactively manage your ssh keys with a TUI", + Action: interactive.Interactive, + }, }, } if err := app.Run(os.Args); err != nil { diff --git a/pkg/actions/challenge-response.go b/pkg/actions/challenge-response.go index 983324b..28f6afb 100644 --- a/pkg/actions/challenge-response.go +++ b/pkg/actions/challenge-response.go @@ -14,7 +14,7 @@ import ( ) func ChallengeResponse(c *cli.Context) error { - setup, err := checkIfSetup() + setup, err := utils.CheckIfSetup() if err != nil { return err } @@ -46,11 +46,13 @@ func ChallengeResponse(c *cli.Context) error { return err } defer conn.Close() - fmt.Print("Please enter the challenge phrase: ") - scanner := bufio.NewScanner(os.Stdin) - var answer string - if err := utils.ReadLineFromStdin(scanner, &answer); err != nil { - return err + answer := c.Args().First() + if answer == "" { + fmt.Print("Please enter the challenge phrase: ") + scanner := bufio.NewScanner(os.Stdin) + if err := utils.ReadLineFromStdin(scanner, &answer); err != nil { + return err + } } if err := utils.WriteClientMessage(&conn, dto.ChallengeResponseDto{ Challenge: answer, diff --git a/pkg/actions/download.go b/pkg/actions/download.go index b992bf0..9f929b5 100644 --- a/pkg/actions/download.go +++ b/pkg/actions/download.go @@ -1,22 +1,21 @@ package actions import ( - "encoding/json" - "errors" + "bufio" "fmt" - "net/http" "os" - "strconv" + "path/filepath" "github.com/samber/lo" "github.com/therealpaulgg/ssh-sync/pkg/dto" "github.com/therealpaulgg/ssh-sync/pkg/models" + "github.com/therealpaulgg/ssh-sync/pkg/retrieval" "github.com/therealpaulgg/ssh-sync/pkg/utils" "github.com/urfave/cli/v2" ) func Download(c *cli.Context) error { - setup, err := checkIfSetup() + setup, err := utils.CheckIfSetup() if err != nil { return err } @@ -28,39 +27,10 @@ func Download(c *cli.Context) error { if err != nil { return err } - token, err := utils.GetToken() + data, err := retrieval.GetUserData(profile) if err != nil { return err } - dataUrl := profile.ServerUrl - dataUrl.Path = "/api/v1/data" - req, err := http.NewRequest("GET", dataUrl.String(), nil) - if err != nil { - return err - } - req.Header.Add("Authorization", "Bearer "+token) - res, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - if res.StatusCode != 200 { - return errors.New("failed to get data. status code: " + strconv.Itoa(res.StatusCode)) - } - var data dto.DataDto - if err := json.NewDecoder(res.Body).Decode(&data); err != nil { - return err - } - masterKey, err := utils.RetrieveMasterKey() - if err != nil { - return err - } - for i, key := range data.Keys { - decryptedKey, err := utils.DecryptWithMasterKey(key.Data, masterKey) - if err != nil { - return err - } - data.Keys[i].Data = decryptedKey - } isSafeMode := c.Bool("safe-mode") var directory string if isSafeMode { @@ -83,6 +53,52 @@ func Download(c *cli.Context) error { return err } } + + err = checkForDeletedKeys(data.Keys, directory) + + if err != nil { + return err + } fmt.Println("Successfully downloaded keys.") return nil } + +func checkForDeletedKeys(keys []dto.KeyDto, directory string) error { + sshDir, err := utils.GetAndCreateSshDirectory(directory) + if err != nil { + return err + } + err = filepath.WalkDir(sshDir, func(p string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + if d.Name() == "config" { + return nil + } + _, exists := lo.Find(keys, func(key dto.KeyDto) bool { + return key.Filename == d.Name() + }) + if exists { + return nil + } + fmt.Printf("Key %s detected on your filesystem that is not in the database. Delete? (y/n): ", d.Name()) + var answer string + scanner := bufio.NewScanner(os.Stdin) + if err := utils.ReadLineFromStdin(scanner, &answer); err != nil { + return err + } + if answer == "y" { + if err := os.Remove(p); err != nil { + return err + } + } + return nil + }) + if err != nil { + return err + } + return nil +} diff --git a/pkg/actions/interactive/main.go b/pkg/actions/interactive/main.go new file mode 100644 index 0000000..545d665 --- /dev/null +++ b/pkg/actions/interactive/main.go @@ -0,0 +1,33 @@ +package interactive + +import ( + "fmt" + "os" + + "github.com/therealpaulgg/ssh-sync/pkg/actions/interactive/states" + "github.com/therealpaulgg/ssh-sync/pkg/utils" + "github.com/urfave/cli/v2" + + tea "github.com/charmbracelet/bubbletea" +) + +func Interactive(c *cli.Context) error { + // get user data + setup, err := utils.CheckIfSetup() + if err != nil { + return err + } + if !setup { + fmt.Fprintln(os.Stderr, "ssh-sync has not been set up on this system. Please set up before continuing.") + return nil + } + + model := states.NewModel() + + p := tea.NewProgram(model, tea.WithAltScreen(), tea.WithMouseCellMotion()) + if _, err := p.Run(); err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + return err + } + return nil +} diff --git a/pkg/actions/interactive/states/common.go b/pkg/actions/interactive/states/common.go new file mode 100644 index 0000000..a039b1d --- /dev/null +++ b/pkg/actions/interactive/states/common.go @@ -0,0 +1,63 @@ +package states + +import ( + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type baseState struct { + width int + height int +} + +func (b *baseState) SetSize(width, height int) { + b.width = width + b.height = height +} + +// State represents a single screen or state in the application + +type State interface { + Update(msg tea.Msg) (State, tea.Cmd) + View() string + SetSize(width, height int) + Initialize() + PrettyName() string +} + +// Init code that should be always run after a new state is created +func (b *baseState) Initialize() { + b.SetSize(b.width, b.height) +} + +// item represents a selectable item in a list +type item struct { + title, desc string + index int +} + +func (i item) Title() string { return i.title } +func (i item) Description() string { return i.desc } +func (i item) FilterValue() string { return i.title } + +// Helper functions +func headerView(title string, width int) string { + titleStyle := TitleStyle.Render(title) + line := strings.Repeat("─", max(0, width-lipgloss.Width(titleStyle)-4)) + return lipgloss.JoinHorizontal(lipgloss.Center, titleStyle, BasicColorStyle.Render(line)) +} + +func footerView(info string, width int) string { + infoStyle := InfoStyle.Render(info) + line := strings.Repeat("─", max(0, width-lipgloss.Width(infoStyle)-4)) + return lipgloss.JoinHorizontal(lipgloss.Center, BasicColorStyle.Render(line), infoStyle) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/pkg/actions/interactive/states/delete-ssh-key.go b/pkg/actions/interactive/states/delete-ssh-key.go new file mode 100644 index 0000000..d452ded --- /dev/null +++ b/pkg/actions/interactive/states/delete-ssh-key.go @@ -0,0 +1,74 @@ +package states + +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/therealpaulgg/ssh-sync/pkg/dto" + "github.com/therealpaulgg/ssh-sync/pkg/retrieval" + "github.com/therealpaulgg/ssh-sync/pkg/utils" +) + +// DeleteSSHKey +type DeleteSSHKey struct { + baseState + key dto.KeyDto +} + +func NewDeleteSSHKey(b baseState, key dto.KeyDto) *DeleteSSHKey { + d := &DeleteSSHKey{ + key: key, + baseState: b, + } + d.Initialize() + return d +} + +func (d *DeleteSSHKey) PrettyName() string { + return "Delete Key" +} + +func (d *DeleteSSHKey) Update(msg tea.Msg) (State, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl+c": + return d, tea.Quit + case "y", "Y": + err := d.deleteKey() + if err != nil { + return NewErrorState(d.baseState, err), nil + } + sshKeyManager, err := NewSSHKeyManager(d.baseState) + if err != nil { + return NewErrorState(d.baseState, err), nil + } + return sshKeyManager, nil + case "n", "N", "backspace": + return NewSSHKeyOptions(d.baseState, d.key), nil + } + } + return d, nil +} + +func (d *DeleteSSHKey) deleteKey() error { + profile, err := utils.GetProfile() + if err != nil { + return err + } + err = retrieval.DeleteKey(profile, d.key) + return err +} + +func (d *DeleteSSHKey) View() string { + content := fmt.Sprintf("Are you sure you want to delete the key %s? (y/n)", d.key.Filename) + return content +} + +func (d *DeleteSSHKey) SetSize(width, height int) { + d.baseState.SetSize(width, height) +} + +func (d *DeleteSSHKey) Initialize() { + d.SetSize(d.width, d.height) +} diff --git a/pkg/actions/interactive/states/error-state.go b/pkg/actions/interactive/states/error-state.go new file mode 100644 index 0000000..8bece82 --- /dev/null +++ b/pkg/actions/interactive/states/error-state.go @@ -0,0 +1,40 @@ +package states + +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" +) + +// ErrorState +type ErrorState struct { + baseState + err error +} + +func NewErrorState(b baseState, err error) *ErrorState { + e := &ErrorState{ + err: err, + baseState: b, + } + e.Initialize() + return e +} + +func (e *ErrorState) PrettyName() string { + return "Error" +} + +func (e *ErrorState) Update(msg tea.Msg) (State, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + if msg.String() == "backspace" || msg.String() == "q" { + return NewMainMenu(e.baseState), nil + } + } + return e, nil +} + +func (e *ErrorState) View() string { + return fmt.Sprintf("An error occurred: %v\nPress 'backspace' or 'q' to return to the main menu.", e.err) +} diff --git a/pkg/actions/interactive/states/main-menu.go b/pkg/actions/interactive/states/main-menu.go new file mode 100644 index 0000000..a844573 --- /dev/null +++ b/pkg/actions/interactive/states/main-menu.go @@ -0,0 +1,68 @@ +package states + +import ( + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +// MainMenu +type MainMenu struct { + baseState + list list.Model +} + +func NewMainMenu(b baseState) *MainMenu { + items := []list.Item{ + item{title: "Manage SSH Keys", desc: "View and manage your SSH keys"}, + } + l := list.New(items, list.NewDefaultDelegate(), 0, 0) + l.Title = "Main Menu" + m := &MainMenu{ + list: l, + baseState: b, + } + m.Initialize() + return m +} + +func (m *MainMenu) PrettyName() string { + return m.list.Title +} + +func (m *MainMenu) Update(msg tea.Msg) (State, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl+c": + return m, tea.Quit + case "enter": + i := m.list.SelectedItem().(item) + switch i.title { + case "Manage SSH Keys": + sshKeyManager, err := NewSSHKeyManager(m.baseState) + if err != nil { + return NewErrorState(m.baseState, err), nil + } + sshKeyManager.height = m.height + sshKeyManager.width = m.width + return sshKeyManager, nil + } + } + } + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func (m *MainMenu) View() string { + return m.list.View() +} + +func (m *MainMenu) SetSize(width, height int) { + m.baseState.SetSize(width, height) + m.list.SetSize(width, height) +} + +func (m *MainMenu) Initialize() { + m.SetSize(m.width, m.height) +} diff --git a/pkg/actions/interactive/states/main.go b/pkg/actions/interactive/states/main.go new file mode 100644 index 0000000..ad12c74 --- /dev/null +++ b/pkg/actions/interactive/states/main.go @@ -0,0 +1,55 @@ +package states + +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type Model struct { + currentState State + width int + height int +} + +func NewModel() Model { + return Model{ + currentState: NewMainMenu(baseState{}), + } +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.WindowSizeMsg: + h, v := AppStyle.GetFrameSize() + headerAndFooterHeight := lipgloss.Height(m.Header()) + lipgloss.Height(m.Footer()) + adjustedWidth, adjustedHeight := msg.Width-h, msg.Height-v-headerAndFooterHeight + m.width = adjustedWidth + m.height = adjustedHeight + m.currentState.SetSize(m.width, m.height) + } + m.currentState, cmd = m.currentState.Update(msg) + return m, cmd +} + +func (m Model) Header() string { + return headerView(m.currentState.PrettyName(), m.width) +} + +func (m Model) Footer() string { + return footerView(m.currentState.PrettyName(), m.width) +} + +func (m Model) View() string { + return AppStyle.Render(fmt.Sprintf("%s\n%s\n%s", + m.Header(), + m.currentState.View(), + m.Footer(), + )) +} diff --git a/pkg/actions/interactive/states/ssh-key-content.go b/pkg/actions/interactive/states/ssh-key-content.go new file mode 100644 index 0000000..8c0ff73 --- /dev/null +++ b/pkg/actions/interactive/states/ssh-key-content.go @@ -0,0 +1,107 @@ +package states + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + ke "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/therealpaulgg/ssh-sync/pkg/dto" +) + +// SSHKeyContent +type SSHKeyContent struct { + baseState + viewport viewport.Model + help help.Model + keymap help.KeyMap + key dto.KeyDto +} + +type SSHKeyContentHelp struct { +} + +func (s *SSHKeyContentHelp) ShortHelp() []key.Binding { + keymap := viewport.DefaultKeyMap() + return []ke.Binding{ + keymap.Up, + keymap.Down, + ke.NewBinding(ke.WithKeys("backspace"), ke.WithHelp("backspace", "back")), + ke.NewBinding(ke.WithKeys("q"), ke.WithHelp("q", "quit")), + ke.NewBinding(ke.WithKeys("?"), ke.WithHelp("?", "more")), + } +} + +func (s *SSHKeyContentHelp) FullHelp() [][]key.Binding { + keymap := viewport.DefaultKeyMap() + return [][]key.Binding{ + []ke.Binding{ + keymap.Up, + keymap.Down, + keymap.PageUp, + keymap.PageDown, + keymap.HalfPageUp, + keymap.HalfPageDown, + }, + []ke.Binding{ + ke.NewBinding(ke.WithKeys("backspace"), ke.WithHelp("backspace", "back")), + ke.NewBinding(ke.WithKeys("q"), ke.WithHelp("q", "quit")), + ke.NewBinding(ke.WithKeys("?"), ke.WithHelp("?", "close help")), + }, + } +} + +func NewSSHKeyContent(b baseState, key dto.KeyDto) *SSHKeyContent { + v := viewport.New(80, 20) + v.SetContent(string(key.Data)) + c := &SSHKeyContent{ + viewport: v, + key: key, + baseState: b, + } + c.help = help.New() + c.keymap = &SSHKeyContentHelp{} + c.Initialize() + return c +} + +func (s *SSHKeyContent) PrettyName() string { + return "Key Content" +} + +func (s *SSHKeyContent) Update(msg tea.Msg) (State, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + if msg.String() == "q" || msg.String() == "ctrl+c" { + return s, tea.Quit + } + if msg.String() == "backspace" { + return NewSSHKeyOptions(s.baseState, s.key), nil + } + if msg.String() == "?" { + s.help.ShowAll = !s.help.ShowAll + s.SetSize(s.width, s.height) + return s, nil + } + } + var cmd tea.Cmd + s.viewport, cmd = s.viewport.Update(msg) + return s, cmd +} + +func (s *SSHKeyContent) View() string { + return fmt.Sprintf("%s\n%s", s.viewport.View(), s.help.View(s.keymap)) +} + +func (s *SSHKeyContent) SetSize(width, height int) { + s.baseState.SetSize(width, height) + s.viewport.Width = width + s.viewport.Height = height - lipgloss.Height(s.help.View(s.keymap)) +} + +func (s *SSHKeyContent) Initialize() { + s.SetSize(s.width, s.height) +} diff --git a/pkg/actions/interactive/states/ssh-key-manager.go b/pkg/actions/interactive/states/ssh-key-manager.go new file mode 100644 index 0000000..c7e5e49 --- /dev/null +++ b/pkg/actions/interactive/states/ssh-key-manager.go @@ -0,0 +1,84 @@ +package states + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/therealpaulgg/ssh-sync/pkg/dto" + "github.com/therealpaulgg/ssh-sync/pkg/retrieval" + "github.com/therealpaulgg/ssh-sync/pkg/utils" +) + +// SSHKeyManager +type SSHKeyManager struct { + baseState + list list.Model + keys []dto.KeyDto +} + +func NewSSHKeyManager(baseState baseState) (*SSHKeyManager, error) { + profile, err := utils.GetProfile() + if err != nil { + return nil, fmt.Errorf("failed to get profile: %w", err) + } + data, err := retrieval.GetUserData(profile) + if err != nil { + return nil, fmt.Errorf("failed to get user data: %w", err) + } + + items := make([]list.Item, len(data.Keys)) + for i, key := range data.Keys { + items[i] = item{title: key.Filename, desc: "", index: i} + } + + l := list.New(items, list.NewDefaultDelegate(), 0, 0) + l.Title = "SSH Keys" + + m := &SSHKeyManager{ + list: l, + keys: data.Keys, + baseState: baseState, + } + m.Initialize() + return m, nil +} + +func (s *SSHKeyManager) PrettyName() string { + return s.list.Title +} + +func (s *SSHKeyManager) Update(msg tea.Msg) (State, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl+c": + return s, tea.Quit + case "enter": + if !s.list.SettingFilter() { + selected := s.list.SelectedItem().(item) + return NewSSHKeyOptions(s.baseState, s.keys[selected.index]), nil + } + case "backspace": + if !s.list.SettingFilter() { + return NewMainMenu(s.baseState), nil + } + } + } + var cmd tea.Cmd + s.list, cmd = s.list.Update(msg) + return s, cmd +} + +func (s *SSHKeyManager) View() string { + return s.list.View() +} + +func (s *SSHKeyManager) SetSize(width, height int) { + s.baseState.SetSize(width, height) + s.list.SetSize(width, height) +} + +func (s *SSHKeyManager) Initialize() { + s.SetSize(s.width, s.height) +} diff --git a/pkg/actions/interactive/states/ssh-key-options.go b/pkg/actions/interactive/states/ssh-key-options.go new file mode 100644 index 0000000..4abb579 --- /dev/null +++ b/pkg/actions/interactive/states/ssh-key-options.go @@ -0,0 +1,75 @@ +package states + +import ( + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/therealpaulgg/ssh-sync/pkg/dto" +) + +// SSHKeyOptions +type SSHKeyOptions struct { + baseState + list list.Model + selectedKey dto.KeyDto +} + +func NewSSHKeyOptions(b baseState, key dto.KeyDto) *SSHKeyOptions { + items := []list.Item{ + item{title: "View Content", desc: "View the content of the SSH key"}, + item{title: "Delete Key", desc: "Delete the SSH key from the store"}, + } + l := list.New(items, list.NewDefaultDelegate(), 0, 0) + l.Title = "Options for " + key.Filename + // l.SetShowHelp(false) + s := &SSHKeyOptions{ + list: l, + selectedKey: key, + baseState: b, + } + s.Initialize() + return s +} + +func (s *SSHKeyOptions) PrettyName() string { + return "Key Options" +} + +func (s *SSHKeyOptions) Update(msg tea.Msg) (State, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl+c": + return s, tea.Quit + case "enter": + i := s.list.SelectedItem().(item) + switch i.title { + case "View Content": + return NewSSHKeyContent(s.baseState, s.selectedKey), nil + case "Delete Key": + return NewDeleteSSHKey(s.baseState, s.selectedKey), nil + } + case "backspace": + sshKeyManager, err := NewSSHKeyManager(s.baseState) + if err != nil { + return NewErrorState(s.baseState, err), nil + } + return sshKeyManager, nil + } + } + var cmd tea.Cmd + s.list, cmd = s.list.Update(msg) + return s, cmd +} + +func (s *SSHKeyOptions) View() string { + return s.list.View() +} + +func (s *SSHKeyOptions) SetSize(width, height int) { + s.baseState.SetSize(width, height) + s.list.SetSize(width, height) +} + +func (s *SSHKeyOptions) Initialize() { + s.SetSize(s.width, s.height) +} diff --git a/pkg/actions/interactive/states/styles.go b/pkg/actions/interactive/states/styles.go new file mode 100644 index 0000000..cb14145 --- /dev/null +++ b/pkg/actions/interactive/states/styles.go @@ -0,0 +1,26 @@ +package states + +import "github.com/charmbracelet/lipgloss" + +var ( + // using this is causing problems with rendering due to caching...WHY?!?!?!?!?!? + // if the layout shifts by even one character, previous screen's state may be displayed, resulting in glitches + AppStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Padding(1, 2) + }() + TitleStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Right = "├" + return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1).BorderForeground(lipgloss.Color("#FF00FF")).Bold(true) + }() + + InfoStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Left = "┤" + return TitleStyle.BorderStyle(b) + }() + + BasicColorStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Foreground(lipgloss.Color("#FF00FF")) + }() +) diff --git a/pkg/actions/list-machines.go b/pkg/actions/list-machines.go index 895034d..0eb9370 100644 --- a/pkg/actions/list-machines.go +++ b/pkg/actions/list-machines.go @@ -10,7 +10,7 @@ import ( ) func ListMachines(c *cli.Context) error { - setup, err := checkIfSetup() + setup, err := utils.CheckIfSetup() if err != nil { return err } diff --git a/pkg/actions/remove-machine.go b/pkg/actions/remove-machine.go index 6d81608..76e689a 100644 --- a/pkg/actions/remove-machine.go +++ b/pkg/actions/remove-machine.go @@ -13,7 +13,7 @@ import ( ) func RemoveMachine(c *cli.Context) error { - setup, err := checkIfSetup() + setup, err := utils.CheckIfSetup() if err != nil { return err } diff --git a/pkg/actions/reset.go b/pkg/actions/reset.go index 3be0227..8879d2a 100644 --- a/pkg/actions/reset.go +++ b/pkg/actions/reset.go @@ -16,7 +16,7 @@ import ( ) func Reset(c *cli.Context) error { - setup, err := checkIfSetup() + setup, err := utils.CheckIfSetup() if err != nil { return err } diff --git a/pkg/actions/setup.go b/pkg/actions/setup.go index 01f3a21..11b46bb 100644 --- a/pkg/actions/setup.go +++ b/pkg/actions/setup.go @@ -28,24 +28,6 @@ import ( "github.com/urfave/cli/v2" ) -func checkIfSetup() (bool, error) { - // check if ~/.ssh-sync/profile.json exists - // if it does, return true - // if it doesn't, return false - user, err := user.Current() - if err != nil { - return false, err - } - p := filepath.Join(user.HomeDir, ".ssh-sync", "profile.json") - if _, err := os.Stat(p); err != nil { - if errors.Is(err, os.ErrNotExist) { - return false, nil - } - return false, err - } - return true, nil -} - func generateKey() (*ecdsa.PrivateKey, *ecdsa.PublicKey, error) { priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) pub := &priv.PublicKey @@ -338,7 +320,7 @@ func Setup(c *cli.Context) error { // there will be a profile.json file containing the machine name and the username // there will also be a keypair. // check if setup has been completed before - setup, err := checkIfSetup() + setup, err := utils.CheckIfSetup() if err != nil { return err } diff --git a/pkg/actions/upload.go b/pkg/actions/upload.go index 18f3281..fbd5252 100644 --- a/pkg/actions/upload.go +++ b/pkg/actions/upload.go @@ -19,7 +19,7 @@ import ( ) func Upload(c *cli.Context) error { - setup, err := checkIfSetup() + setup, err := utils.CheckIfSetup() if err != nil { return err } @@ -76,6 +76,9 @@ func Upload(c *cli.Context) error { if err != nil { return err } + if len(hosts) == 0 { + return errors.New("your ssh config is empty. Please add some hosts to your ssh config so data can be uploaded.") + } continue } f, err := os.OpenFile(filepath.Join(p, file.Name()), os.O_RDONLY, 0600) diff --git a/pkg/retrieval/data.go b/pkg/retrieval/data.go new file mode 100644 index 0000000..f206ee9 --- /dev/null +++ b/pkg/retrieval/data.go @@ -0,0 +1,72 @@ +package retrieval + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/therealpaulgg/ssh-sync/pkg/dto" + "github.com/therealpaulgg/ssh-sync/pkg/models" + "github.com/therealpaulgg/ssh-sync/pkg/utils" +) + +func GetUserData(profile *models.Profile) (dto.DataDto, error) { + var data dto.DataDto + token, err := utils.GetToken() + if err != nil { + return data, err + } + dataUrl := profile.ServerUrl + dataUrl.Path = "/api/v1/data" + req, err := http.NewRequest("GET", dataUrl.String(), nil) + if err != nil { + return data, err + } + req.Header.Add("Authorization", "Bearer "+token) + res, err := http.DefaultClient.Do(req) + if err != nil { + return data, err + } + if res.StatusCode != 200 { + return data, errors.New("failed to get data. status code: " + strconv.Itoa(res.StatusCode)) + } + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return data, err + } + masterKey, err := utils.RetrieveMasterKey() + if err != nil { + return data, err + } + for i, key := range data.Keys { + decryptedKey, err := utils.DecryptWithMasterKey(key.Data, masterKey) + if err != nil { + return data, err + } + data.Keys[i].Data = decryptedKey + } + return data, nil +} + +func DeleteKey(profile *models.Profile, key dto.KeyDto) error { + token, err := utils.GetToken() + if err != nil { + return err + } + dataUrl := profile.ServerUrl + dataUrl.Path = fmt.Sprintf("/api/v1/data/key/%s", key.ID) + req, err := http.NewRequest("DELETE", dataUrl.String(), nil) + if err != nil { + return err + } + req.Header.Add("Authorization", "Bearer "+token) + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + if res.StatusCode != 200 { + return errors.New("failed to delete data. status code: " + strconv.Itoa(res.StatusCode)) + } + return nil +} diff --git a/pkg/retrieval/data_test.go b/pkg/retrieval/data_test.go new file mode 100644 index 0000000..b381831 --- /dev/null +++ b/pkg/retrieval/data_test.go @@ -0,0 +1,73 @@ +package retrieval + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/therealpaulgg/ssh-sync/pkg/dto" + "github.com/therealpaulgg/ssh-sync/pkg/models" +) + +func TestDownloadData(t *testing.T) { + // Arrange + // TODO generate ecdsa key + // TODO encrypt key with a user's private key + key := []byte{} + profile := &models.Profile{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode([]dto.DataDto{ + { + ID: uuid.New(), + Username: "test", + Keys: []dto.KeyDto{ + { + ID: uuid.New(), + UserID: uuid.New(), + Filename: "test", + Data: key, + }, + }, + SshConfig: []dto.SshConfigDto{ + { + Host: "test", + Values: map[string][]string{ + "foo": {"bar"}, + }, + IdentityFiles: []string{"test"}, + }, + }, + }, + }) + })) + url, _ := url.Parse(server.URL) + profile.ServerUrl = *url + // Act + data, err := GetUserData(profile) + // Assert + assert.Nil(t, err) + assert.Equal(t, 1, len(data.Keys)) +} + +func TestDeleteKey(t *testing.T) { + // Arrange + key := dto.KeyDto{ + ID: uuid.New(), + UserID: uuid.New(), + Filename: "test", + } + profile := &models.Profile{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + url, _ := url.Parse(server.URL) + profile.ServerUrl = *url + // Act + err := DeleteKey(profile, key) + // Assert + assert.Nil(t, err) +} diff --git a/pkg/utils/setup.go b/pkg/utils/setup.go new file mode 100644 index 0000000..55197a0 --- /dev/null +++ b/pkg/utils/setup.go @@ -0,0 +1,26 @@ +package utils + +import ( + "errors" + "os" + "os/user" + "path/filepath" +) + +func CheckIfSetup() (bool, error) { + // check if ~/.ssh-sync/profile.json exists + // if it does, return true + // if it doesn't, return false + user, err := user.Current() + if err != nil { + return false, err + } + p := filepath.Join(user.HomeDir, ".ssh-sync", "profile.json") + if _, err := os.Stat(p); err != nil { + if errors.Is(err, os.ErrNotExist) { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/pkg/utils/write.go b/pkg/utils/write.go index e948791..a71092b 100644 --- a/pkg/utils/write.go +++ b/pkg/utils/write.go @@ -11,18 +11,29 @@ import ( "github.com/therealpaulgg/ssh-sync/pkg/models" ) -func WriteConfig(hosts []models.Host, sshDirectory string) error { +func GetAndCreateSshDirectory(sshDirectory string) (string, error) { user, err := user.Current() if err != nil { - return err + return "", err } p := filepath.Join(user.HomeDir, sshDirectory) if _, err := os.Stat(p); errors.Is(err, os.ErrNotExist) { - if err := os.MkdirAll(p, 0700); err != nil { - return err + return "", err } } + return p, nil +} + +func WriteConfig(hosts []models.Host, sshDirectory string) error { + user, err := user.Current() + if err != nil { + return err + } + p, err := GetAndCreateSshDirectory(sshDirectory) + if err != nil { + return err + } file, err := os.OpenFile(filepath.Join(p, "config"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err @@ -51,16 +62,10 @@ func WriteConfig(hosts []models.Host, sshDirectory string) error { } func WriteKey(key []byte, filename string, sshDirectory string) error { - user, err := user.Current() + p, err := GetAndCreateSshDirectory(sshDirectory) if err != nil { return err } - p := filepath.Join(user.HomeDir, sshDirectory) - if _, err := os.Stat(p); errors.Is(err, os.ErrNotExist) { - if err := os.MkdirAll(p, 0700); err != nil { - return err - } - } _, err = os.OpenFile(filepath.Join(p, filename), os.O_RDONLY, 0600) if err != nil && !errors.Is(err, os.ErrNotExist) { return err