├── .github └── workflows │ └── release.yaml ├── .gitignore ├── go.mod ├── go.sum ├── internal ├── client │ ├── client.go │ ├── code_server.go │ ├── login.go │ ├── ping.go │ ├── proxy.go │ ├── request.go │ └── user.go ├── cmd │ ├── bind.go │ ├── cmd.go │ └── version.go ├── config │ ├── dir.go │ └── file.go ├── ideproxy │ ├── doc.go │ └── proxy.go └── version │ └── version.go ├── main.go └── pkg └── agentlogin ├── doc.go └── login.go /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: 4 | - created 5 | 6 | name: Build Release 7 | jobs: 8 | release: 9 | name: Build release packages 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | goos: [linux, darwin] 15 | goarch: [amd64, arm64, ppc64le] 16 | exclude: 17 | - goos: darwin 18 | goarch: ppc64le 19 | 20 | env: 21 | GOOS: ${{ matrix.goos }} 22 | GOARCH: ${{ matrix.goarch }} 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - uses: actions/setup-go@v2 28 | with: 29 | go-version: '^1.19' 30 | 31 | - run: go build -v -o cloud-agent-$GOOS-$GOARCH -ldflags "-X go.coder.com/cloud-agent/internal/version.Version=$(echo $GITHUB_REF | cut -d/ -f3)" 32 | 33 | - run: gh release upload $(echo $GITHUB_REF | cut -d/ -f3) ./cloud-agent-$GOOS-$GOARCH 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .idea 4 | .dbstash 5 | .dbtemp 6 | .vscode 7 | vendor 8 | cloud-agent 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.coder.com/cloud-agent 2 | 3 | go 1.19 4 | 5 | require ( 6 | cdr.dev/slog v1.3.0 7 | github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce 8 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 9 | github.com/spf13/pflag v1.0.5 10 | go.coder.com/cli v0.4.0 11 | go.coder.com/flog v0.0.0-20200908145530-d7adc3802a47 12 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 13 | nhooyr.io/websocket v1.8.7 14 | ) 15 | 16 | require ( 17 | github.com/alecthomas/chroma v0.7.0 // indirect 18 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect 19 | github.com/dlclark/regexp2 v1.2.0 // indirect 20 | github.com/fatih/color v1.7.0 // indirect 21 | github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect 22 | github.com/klauspost/compress v1.10.3 // indirect 23 | github.com/mattn/go-colorable v0.1.7 // indirect 24 | github.com/mattn/go-isatty v0.0.12 // indirect 25 | go.opencensus.io v0.22.2 // indirect 26 | golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect 27 | golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cdr.dev/slog v1.3.0 h1:MYN1BChIaVEGxdS7I5cpdyMC0+WfJfK8BETAfzfLUGQ= 2 | cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns= 3 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 5 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 6 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 7 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 8 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 9 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 10 | cloud.google.com/go v0.49.0 h1:CH+lkubJzcPYB1Ggupcq0+k8Ni2ILdG2lYjDIgavDBQ= 11 | cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY= 12 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 13 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 14 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 15 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 16 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 17 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 18 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 19 | github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= 20 | github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= 21 | github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= 22 | github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= 23 | github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= 24 | github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw= 25 | github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY= 26 | github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= 27 | github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= 28 | github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= 29 | github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= 30 | github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= 31 | github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= 32 | github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= 33 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 34 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 35 | github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= 36 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= 37 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= 38 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 39 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 40 | github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 41 | github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= 42 | github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 43 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 44 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 45 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 46 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 47 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 48 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 49 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 50 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 51 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 52 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 53 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 54 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 55 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 56 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 57 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 58 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 59 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= 60 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 61 | github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= 62 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 63 | github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= 64 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 65 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 66 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 67 | github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE= 68 | github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 69 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 70 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 71 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 72 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 73 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 74 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 75 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 76 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= 77 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 78 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 79 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 80 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 81 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 82 | github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 83 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 84 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 85 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 86 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 87 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 88 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 89 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 90 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 91 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 92 | github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= 93 | github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 94 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 95 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 96 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 97 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 98 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 99 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 100 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 101 | github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce h1:7UnVY3T/ZnHUrfviiAgIUjg2PXxsQfs5bphsG8F7Keo= 102 | github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 103 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 104 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 105 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 106 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 107 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 108 | github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= 109 | github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 110 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 111 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 112 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 113 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 114 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 115 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 116 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 117 | github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= 118 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 119 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 120 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 121 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 122 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 123 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 124 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 125 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 126 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 127 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 128 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 129 | github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= 130 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= 131 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 132 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 133 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 134 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 135 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 136 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 137 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 138 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 139 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 140 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 141 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 142 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 143 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 144 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 145 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 146 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 147 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 148 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 149 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 150 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 151 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 152 | go.coder.com/cli v0.4.0 h1:PruDGwm/CPFndyK/eMowZG3vzg5CgohRWeXWCTr3zi8= 153 | go.coder.com/cli v0.4.0/go.mod h1:hRTOURCR3LJF1FRW9arecgrzX+AHG7mfYMwThPIgq+w= 154 | go.coder.com/flog v0.0.0-20200908145530-d7adc3802a47 h1:zbGjCAUaQYbpulB3wMoWcW6XsbCUqjepKpjJYXDdjQk= 155 | go.coder.com/flog v0.0.0-20200908145530-d7adc3802a47/go.mod h1:AAHV/3oT6dVxWwMYBec1NhrgRm8ANPKpOgmYwJT3dQk= 156 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 157 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 158 | go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= 159 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 160 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 161 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 162 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 163 | golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= 164 | golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 165 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 166 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 167 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 168 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 169 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 170 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 171 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 172 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 173 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 174 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 175 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 176 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 177 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 178 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 179 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 180 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 181 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 182 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 183 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 184 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 185 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 186 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 187 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 188 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 189 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 190 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 191 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 192 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 193 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= 194 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 195 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 196 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 197 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 198 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 199 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 200 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 201 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 202 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 203 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 204 | golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 205 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 206 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 207 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 208 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 210 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 211 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 212 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 213 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 214 | golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 215 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 216 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= 217 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 218 | golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 h1:v1W7bwXHsnLLloWYTVEdvGvA7BHMeBYsPcF0GLDxIRs= 219 | golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 220 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 221 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 222 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 223 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 224 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 225 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 226 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 227 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 228 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 229 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 230 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 231 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 232 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 233 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 234 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 235 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 236 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 237 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 238 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 239 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 240 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 241 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 242 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 243 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 244 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 245 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 246 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 247 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 248 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 249 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 250 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 251 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 252 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 253 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 254 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 255 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 256 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 257 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 258 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 259 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 260 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 261 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 262 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 263 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 264 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 265 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 h1:aQktFqmDE2yjveXJlVIfslDFmFnUXSqG0i6KRcJAeMc= 266 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 267 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 268 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 269 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 270 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 271 | google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= 272 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 273 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 274 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 275 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 276 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 277 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 278 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 279 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 280 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 281 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 282 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 283 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 284 | nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= 285 | nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= 286 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 287 | -------------------------------------------------------------------------------- /internal/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | type Client struct { 8 | Token string 9 | BaseURL *url.URL 10 | } 11 | -------------------------------------------------------------------------------- /internal/client/code_server.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "golang.org/x/xerrors" 9 | ) 10 | 11 | func (c *Client) CodeServer(id string) (*CodeServer, error) { 12 | path := fmt.Sprintf("/api/servers/%v", id) 13 | 14 | var response CodeServer 15 | err := c.requestBody("GET", path, nil, &response) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &response, nil 21 | } 22 | 23 | type AccessURLResponse struct { 24 | URL string `json:"url"` 25 | } 26 | 27 | func (c *Client) AccessURL(id string) (string, error) { 28 | path := fmt.Sprintf("/api/servers/%v/access-url", id) 29 | 30 | var response AccessURLResponse 31 | err := c.requestBody("GET", path, nil, &response) 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | return response.URL, nil 37 | } 38 | 39 | type CodeServer struct { 40 | ID string `json:"id"` 41 | UserID string `json:"user_id"` 42 | Name string `json:"name"` 43 | Hostname string `json:"hostname"` 44 | CreatedAt time.Time `json:"created_at"` 45 | LastConnectionAt time.Time `json:"last_connection_at"` 46 | } 47 | 48 | // RegisterServerRequest is the request body sent in a 49 | // register server request. 50 | type RegisterServerRequest struct { 51 | Name string `json:"name"` 52 | Hostname string `json:"hostname"` 53 | } 54 | 55 | func (c *Client) RegisterCodeServer(name string) (*CodeServer, error) { 56 | const path = "/api/servers" 57 | hostname, err := os.Hostname() 58 | if err != nil { 59 | return nil, xerrors.Errorf("get hostname: %w", err) 60 | } 61 | 62 | var response CodeServer 63 | err = c.requestBody("POST", path, 64 | &RegisterServerRequest{ 65 | Name: name, 66 | Hostname: hostname, 67 | }, 68 | &response, 69 | ) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return &response, nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/client/login.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/pkg/browser" 10 | "golang.org/x/xerrors" 11 | "nhooyr.io/websocket" 12 | 13 | "go.coder.com/cloud-agent/pkg/agentlogin" 14 | "go.coder.com/flog" 15 | ) 16 | 17 | func init() { 18 | browser.Stderr = ioutil.Discard 19 | browser.Stdout = ioutil.Discard 20 | } 21 | 22 | // Login performs the login flow for an agent. It returns the resulting 23 | // session token to use for authenticated routes. 24 | func Login(addr, serverName string) (string, error) { 25 | ctx := context.Background() 26 | 27 | u, err := url.Parse(addr) 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | query := url.Values{} 33 | query.Add(agentlogin.ServerNameQueryParam, serverName) 34 | 35 | loginURL := &url.URL{ 36 | Scheme: u.Scheme, 37 | Host: u.Host, 38 | Path: "/login", 39 | RawQuery: query.Encode(), 40 | } 41 | 42 | conn, resp, err := websocket.Dial(ctx, loginURL.String(), &websocket.DialOptions{ 43 | HTTPHeader: http.Header{ 44 | "User-Agent": []string{userAgent()}, 45 | }, 46 | }) 47 | if resp != nil && resp.StatusCode != http.StatusSwitchingProtocols { 48 | return "", bodyError(resp) 49 | } 50 | if err != nil { 51 | return "", err 52 | } 53 | defer conn.Close(websocket.StatusInternalError, "") 54 | 55 | client := &agentlogin.Client{ 56 | Ctx: ctx, 57 | Conn: conn, 58 | } 59 | 60 | url, err := client.ReadAuthURL() 61 | if err != nil { 62 | return "", xerrors.Errorf("read auth url: %w", err) 63 | } 64 | 65 | err = browser.OpenURL(url) 66 | if err != nil { 67 | flog.Info("visit %s to login", url) 68 | } 69 | 70 | token, err := client.ReadSessionToken() 71 | if err != nil { 72 | return "", xerrors.Errorf("read session token: %w", err) 73 | } 74 | 75 | return token, nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/client/ping.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "time" 7 | 8 | "golang.org/x/xerrors" 9 | "nhooyr.io/websocket" 10 | "nhooyr.io/websocket/wsjson" 11 | ) 12 | 13 | type pingMsg struct { 14 | LatencyMS int64 `json:"latency_ms"` 15 | Tolerable bool `json:"tolerable"` 16 | Error string `json:"error"` 17 | } 18 | 19 | // Ping determines the websocket latency of the agent connection 20 | // to the server. A value of true is returned if the latency is 21 | // tolerable. 22 | func Ping(baseURL string) (time.Duration, bool, error) { 23 | var ctx = context.Background() 24 | 25 | uri, err := url.Parse(baseURL) 26 | if err != nil { 27 | return 0, false, xerrors.Errorf("parse url: %w", err) 28 | } 29 | uri.Path = "/latency" 30 | 31 | conn, _, err := websocket.Dial(context.Background(), uri.String(), nil) 32 | if err != nil { 33 | return 0, false, xerrors.Errorf("dial server: %w", err) 34 | } 35 | 36 | var msg pingMsg 37 | err = wsjson.Read(ctx, conn, &msg) 38 | if err != nil { 39 | return 0, false, xerrors.Errorf("read msg: %w", err) 40 | } 41 | if msg.Error != "" { 42 | return 0, false, xerrors.New(msg.Error) 43 | } 44 | 45 | return time.Duration(msg.LatencyMS) * time.Millisecond, msg.Tolerable, nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/client/proxy.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "golang.org/x/xerrors" 9 | "nhooyr.io/websocket" 10 | ) 11 | 12 | func (c *Client) ProxyAgent(ctx context.Context, id string) (*websocket.Conn, error) { 13 | ws, resp, err := websocket.Dial(ctx, //nolint:bodyclose 14 | fmt.Sprintf("%v/proxy/ide/%v/server", 15 | c.BaseURL.String(), 16 | id, 17 | ), 18 | &websocket.DialOptions{ 19 | HTTPHeader: http.Header{ 20 | "User-Agent": []string{userAgent()}, 21 | sessionHeader: []string{c.Token}, 22 | }, 23 | }) 24 | if resp != nil && resp.StatusCode != http.StatusSwitchingProtocols { 25 | return nil, bodyError(resp) 26 | } 27 | if err != nil { 28 | return nil, xerrors.Errorf("dial cproxy: %w", err) 29 | } 30 | 31 | return ws, nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/client/request.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | 9 | "go.coder.com/cloud-agent/internal/version" 10 | "golang.org/x/xerrors" 11 | ) 12 | 13 | const ( 14 | sessionHeader = "Session-Token" 15 | ) 16 | 17 | func (c *Client) request(method, path string, body interface{}) (*http.Response, error) { 18 | var rd io.Reader 19 | if body != nil { 20 | b, err := json.Marshal(body) 21 | if err != nil { 22 | return nil, xerrors.Errorf("marshal body: %w", err) 23 | } 24 | rd = bytes.NewReader(b) 25 | } 26 | 27 | req, err := http.NewRequest(method, c.BaseURL.String()+path, rd) 28 | if err != nil { 29 | return nil, xerrors.Errorf("new request: %w", err) 30 | } 31 | 32 | req.Header.Set(sessionHeader, c.Token) 33 | req.Header.Set("User-Agent", userAgent()) 34 | 35 | return http.DefaultClient.Do(req) 36 | } 37 | 38 | func (c *Client) requestBody(method, path string, request, response interface{}) error { 39 | resp, err := c.request(method, path, request) 40 | if err != nil { 41 | return err 42 | } 43 | defer resp.Body.Close() 44 | 45 | if resp.StatusCode != 200 && resp.StatusCode != 201 { 46 | return bodyError(resp) 47 | } 48 | 49 | err = json.NewDecoder(resp.Body).Decode(&response) 50 | if err != nil { 51 | return xerrors.Errorf("unmarshal response: %w", err) 52 | } 53 | return nil 54 | } 55 | 56 | type apiError struct { 57 | Err struct { 58 | Msg string `json:"msg"` 59 | } `json:"error"` 60 | } 61 | 62 | func bodyError(resp *http.Response) error { 63 | var apiErr apiError 64 | 65 | err := json.NewDecoder(resp.Body).Decode(&apiErr) 66 | if err != nil { 67 | return xerrors.Errorf("decode err: %w", err) 68 | } 69 | 70 | return xerrors.New(apiErr.Err.Msg) 71 | } 72 | 73 | func userAgent() string { 74 | return "CoderCloud/" + version.Version 75 | } 76 | -------------------------------------------------------------------------------- /internal/client/user.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // User describe a Coder Cloud user. 8 | type User struct { 9 | ID string `json:"id" ` 10 | Name string `json:"name"` 11 | Username string `json:"username"` 12 | Email string `json:"email"` 13 | CreatedAt time.Time `json:"created_at"` 14 | } 15 | 16 | func (c *Client) Me() (*User, error) { 17 | const path = "/api/users/me" 18 | 19 | var response User 20 | err := c.requestBody("GET", path, nil, &response) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return &response, nil 26 | 27 | } 28 | -------------------------------------------------------------------------------- /internal/cmd/bind.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "os" 7 | "regexp" 8 | "strings" 9 | "time" 10 | 11 | "cdr.dev/slog/sloggers/sloghuman" 12 | "github.com/spf13/pflag" 13 | "golang.org/x/xerrors" 14 | 15 | "go.coder.com/cli" 16 | "go.coder.com/cloud-agent/internal/client" 17 | "go.coder.com/cloud-agent/internal/config" 18 | "go.coder.com/cloud-agent/internal/ideproxy" 19 | "go.coder.com/flog" 20 | ) 21 | 22 | var ( 23 | DefaultCloudURL = "https://cloud.coder.com" 24 | ) 25 | 26 | var codeServerNameRx = regexp.MustCompile("^[a-z0-9][a-z0-9_]{0,50}$") 27 | 28 | type bindCmd struct { 29 | cloudURL string 30 | codeServerAddr string 31 | } 32 | 33 | func (c *bindCmd) Spec() cli.CommandSpec { 34 | return cli.CommandSpec{ 35 | Name: "bind", 36 | Usage: "[NAME]", 37 | Desc: "Bind a server to --link. A name will be generated from the hostname if one is not provided.", 38 | } 39 | } 40 | 41 | func (c *bindCmd) RegisterFlags(fl *pflag.FlagSet) { 42 | fl.StringVar(&c.cloudURL, "cloud-url", DefaultCloudURL, "The Coder Cloud URL to connect to.") 43 | fl.StringVar(&c.codeServerAddr, 44 | "code-server-addr", 45 | "localhost:8080", 46 | "The address of the code-server instance to proxy.", 47 | ) 48 | } 49 | 50 | func (c *bindCmd) Run(fl *pflag.FlagSet) { 51 | var ( 52 | err error 53 | ctx = context.Background() 54 | ) 55 | 56 | name := fl.Arg(0) 57 | if name == "" { 58 | // Generate a name based on the hostname if one is not provided. 59 | name, err = genServerName() 60 | if err != nil { 61 | flog.Fatal("Failed to generate server name: %v", err.Error()) 62 | } 63 | } 64 | 65 | if !codeServerNameRx.MatchString(name) { 66 | flog.Fatal("Name must conform to regex %s", codeServerNameRx.String()) 67 | } 68 | 69 | cloudURL, err := url.Parse(c.cloudURL) 70 | if err != nil { 71 | flog.Fatal("Invalid Cloud URL: %v", err.Error()) 72 | } 73 | 74 | token, err := config.SessionToken.Read() 75 | if xerrors.Is(err, os.ErrNotExist) { 76 | checkLatency(c.cloudURL) 77 | token, err = login(cloudURL.String(), name) 78 | } 79 | if err != nil { 80 | flog.Fatal("Failed to login: %v", err) 81 | } 82 | 83 | cli := client.Client{ 84 | Token: token, 85 | BaseURL: cloudURL, 86 | } 87 | 88 | // Register the server with Coder Cloud. This is an idempotent 89 | // operation. 90 | cs, err := cli.RegisterCodeServer(name) 91 | if err != nil { 92 | flog.Fatal("Failed to register server: %v", err) 93 | } 94 | 95 | // Get the Access URL for the user. 96 | url, err := cli.AccessURL(cs.ID) 97 | if err != nil { 98 | flog.Fatal("Failed to query server: %v", err) 99 | } 100 | 101 | agent := &ideproxy.Agent{ 102 | Log: sloghuman.Make(os.Stderr), 103 | CodeServerID: cs.ID, 104 | SessionToken: token, 105 | CloudProxyURL: c.cloudURL, 106 | CodeServerAddr: c.codeServerAddr, 107 | } 108 | 109 | proxy := func() { 110 | err = agent.Proxy(ctx) 111 | if err != nil { 112 | flog.Error("Connection disrupted, re-establishing connection: %v", err.Error()) 113 | } 114 | } 115 | 116 | flog.Info("code-server --link is deprecated. While the servers will remain online,") 117 | flog.Info("we are not releasing new features or bugfixes. A future code-server") 118 | flog.Info("release will include a v2 with new features. If you would") 119 | flog.Info("like early access, reach out on https://cdr.co/join-community") 120 | flog.Info("") 121 | 122 | 123 | flog.Info("Proxying code-server, you can access your IDE at %v", url) 124 | 125 | proxy() 126 | 127 | // Avoid a super tight loop. 128 | ticker := time.NewTicker(time.Second) 129 | for range ticker.C { 130 | proxy() 131 | } 132 | } 133 | 134 | func login(url, serverName string) (string, error) { 135 | token, err := client.Login(url, serverName) 136 | if err != nil { 137 | return "", xerrors.Errorf("unable to login: %w", err) 138 | } 139 | 140 | err = config.SessionToken.Write(token) 141 | if err != nil { 142 | return "", xerrors.Errorf("write session token to file: %w", err) 143 | } 144 | 145 | return token, nil 146 | } 147 | 148 | func genServerName() (string, error) { 149 | hostname, err := os.Hostname() 150 | if err != nil { 151 | xerrors.Errorf("get hostname: %w", err) 152 | } 153 | 154 | hostname = strings.ToLower(hostname) 155 | 156 | // Only use the first token. 157 | hostname = strings.Split(hostname, ".")[0] 158 | // '-' are not allowed, convert them to '_'. 159 | return strings.Replace(hostname, "-", "_", -1), nil 160 | } 161 | 162 | func checkLatency(cloudURL string) { 163 | latency, tolerable, err := client.Ping(cloudURL) 164 | if err != nil { 165 | flog.Fatal("ping server: %s", err.Error()) 166 | } 167 | 168 | if !tolerable { 169 | flog.Fatal("Unfortunately we cannot ensure a good user experience with your connection latency (%s). Efforts are underway to accommodate users in most areas.", latency) 170 | } 171 | 172 | flog.Info("Detected an acceptable latency of %s", latency) 173 | } 174 | -------------------------------------------------------------------------------- /internal/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/pflag" 5 | "go.coder.com/cli" 6 | ) 7 | 8 | func Make() cli.Command { 9 | return &rootCmd{} 10 | } 11 | 12 | var _ interface { 13 | cli.Command 14 | cli.ParentCommand 15 | } = &rootCmd{} 16 | 17 | type rootCmd struct { 18 | } 19 | 20 | func (c *rootCmd) Spec() cli.CommandSpec { 21 | return cli.CommandSpec{ 22 | Name: "agent", 23 | Usage: "[GLOBAL FLAGS] COMMAND [COMMAND FLAGS] [ARGS...]", 24 | Desc: `Run the Coder Cloud Agent.`, 25 | } 26 | } 27 | 28 | func (c *rootCmd) Subcommands() []cli.Command { 29 | return []cli.Command{ 30 | &bindCmd{}, 31 | &versionCmd{}, 32 | } 33 | } 34 | 35 | func (c *rootCmd) Run(fl *pflag.FlagSet) { 36 | fl.Usage() 37 | } 38 | -------------------------------------------------------------------------------- /internal/cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/pflag" 7 | 8 | "go.coder.com/cli" 9 | "go.coder.com/cloud-agent/internal/version" 10 | ) 11 | 12 | type versionCmd struct{} 13 | 14 | func (c *versionCmd) Spec() cli.CommandSpec { 15 | return cli.CommandSpec{ 16 | Name: "version", 17 | Usage: "", 18 | Desc: "Print the agent version.", 19 | } 20 | } 21 | 22 | func (c *versionCmd) Run(fl *pflag.FlagSet) { 23 | fmt.Println(version.Version) 24 | } 25 | -------------------------------------------------------------------------------- /internal/config/dir.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | ) 10 | 11 | var ( 12 | // ConfigDir is the directory where agent config is stored. 13 | ConfigDir = "coder-cloud" 14 | ) 15 | 16 | func dir() (string, error) { 17 | conf, err := os.UserConfigDir() 18 | if runtime.GOOS == "darwin" { 19 | // No one uses macOS's ~/Library for CLI apps... 20 | // Sigh. 21 | // See https://github.com/golang/go/issues/29960#issuecomment-499842130 22 | conf, err = xdgConfigDir() 23 | } 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | return filepath.Join(conf, ConfigDir), nil 29 | } 30 | 31 | // Copied directly from stdlib's os.UserConfigDir. 32 | func xdgConfigDir() (string, error) { 33 | dir := os.Getenv("XDG_CONFIG_HOME") 34 | if dir == "" { 35 | dir = os.Getenv("HOME") 36 | if dir == "" { 37 | return "", errors.New("neither $XDG_CONFIG_HOME nor $HOME are defined") 38 | } 39 | dir += "/.config" 40 | } 41 | return dir, nil 42 | } 43 | 44 | // open opens a file in the configuration directory, 45 | // creating all intermediate directories. 46 | func open(path string, flag int, mode os.FileMode) (*os.File, error) { 47 | dir, err := dir() 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | path = filepath.Join(dir, path) 53 | 54 | err = os.MkdirAll(filepath.Dir(path), 0750) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return os.OpenFile(path, flag, mode) 60 | } 61 | 62 | func write(path string, mode os.FileMode, dat []byte) error { 63 | fi, err := open(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, mode) 64 | if err != nil { 65 | return err 66 | } 67 | defer fi.Close() 68 | _, err = fi.Write(dat) 69 | return err 70 | } 71 | 72 | func read(path string) ([]byte, error) { 73 | fi, err := open(path, os.O_RDONLY, 0) 74 | if err != nil { 75 | return nil, err 76 | } 77 | defer fi.Close() 78 | return ioutil.ReadAll(fi) 79 | } 80 | 81 | func rm(path string) error { 82 | dir, err := dir() 83 | if err != nil { 84 | return err 85 | } 86 | 87 | return os.Remove(filepath.Join(dir, path)) 88 | } 89 | -------------------------------------------------------------------------------- /internal/config/file.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "golang.org/x/xerrors" 4 | 5 | var ( 6 | // SessionToken is the file containing the session token. 7 | SessionToken File = "session" 8 | ) 9 | 10 | // File is a thin wrapper around os.File for conveniently interacting 11 | // with the FS. 12 | type File string 13 | 14 | // Write writes to the file with 0600 perms. 15 | func (f File) Write(s string) error { 16 | return write(string(f), 0600, []byte(s)) 17 | } 18 | 19 | // Read reads the file, returning the string representation. 20 | func (f File) Read() (string, error) { 21 | b, err := read(string(f)) 22 | return string(b), err 23 | } 24 | 25 | // Delete deletes the file. 26 | func (f File) Delete() error { 27 | return rm(string(f)) 28 | } 29 | 30 | // ReadFiles reads and returns the contents of all the provided 31 | // Files. It should not be used with large files. 32 | // TODO: maybe we just make a JSON config at this point... 33 | func ReadFiles(files ...File) (map[File]string, error) { 34 | m := make(map[File]string, len(files)) 35 | for _, file := range files { 36 | val, err := file.Read() 37 | if err != nil { 38 | return nil, xerrors.Errorf("read %s", file) 39 | } 40 | m[file] = val 41 | } 42 | 43 | return m, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/ideproxy/doc.go: -------------------------------------------------------------------------------- 1 | // Package ideproxy contains logic used by the agent to proxy 2 | // connections from a code-server process to Coder Cloud. 3 | package ideproxy 4 | -------------------------------------------------------------------------------- /internal/ideproxy/proxy.go: -------------------------------------------------------------------------------- 1 | package ideproxy 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "fmt" 7 | "io" 8 | "net" 9 | "net/http" 10 | "net/http/httputil" 11 | "net/url" 12 | 13 | "cdr.dev/slog" 14 | "github.com/hashicorp/yamux" 15 | "go.coder.com/cloud-agent/internal/client" 16 | "golang.org/x/xerrors" 17 | "nhooyr.io/websocket" 18 | ) 19 | 20 | const sessionHeader = "Session-Token" 21 | 22 | // Agent is the agent running on a user's personal machine. 23 | type Agent struct { 24 | Log slog.Logger 25 | CodeServerID string 26 | SessionToken string 27 | CodeServerAddr string 28 | CodeServerPassword string 29 | CloudProxyURL string 30 | } 31 | 32 | // Proxy proxies a Coder Cloud connection to a local code server instance. 33 | func (a *Agent) Proxy(ctx context.Context) error { 34 | l, err := net.Listen("tcp", ":0") 35 | if err != nil { 36 | return xerrors.Errorf("listen on local port: %w", err) 37 | } 38 | defer l.Close() 39 | 40 | baseURL, err := url.Parse(a.CloudProxyURL) 41 | if err != nil { 42 | return xerrors.Errorf("invalid cloud URL: %w", err) 43 | } 44 | 45 | go func() { 46 | err := http.Serve(l, 47 | codeServerReverseProxy(a.CodeServerAddr, a.CodeServerPassword)) 48 | a.Log.Warn(ctx, "code-server proxy exited", slog.Error(err)) 49 | }() 50 | 51 | client := &client.Client{ 52 | BaseURL: baseURL, 53 | Token: a.SessionToken, 54 | } 55 | 56 | ws, err := client.ProxyAgent(ctx, a.CodeServerID) 57 | if err != nil { 58 | return xerrors.Errorf("proxy agent: %w", err) 59 | } 60 | 61 | conn := websocket.NetConn(ctx, ws, websocket.MessageBinary) 62 | 63 | err = proxyCodeServer(ctx, a.Log, conn, l.Addr().String()) 64 | if err != nil && !xerrors.Is(err, io.EOF) { 65 | return xerrors.Errorf("proxy code-server: %w", err) 66 | } 67 | return nil 68 | } 69 | 70 | // proxyCodeServer proxies a Coder Cloud connection to the local code-server. 71 | func proxyCodeServer(ctx context.Context, log slog.Logger, proxyConn net.Conn, addr string) error { 72 | stream, err := yamux.Server(proxyConn, nil) 73 | if err != nil { 74 | return xerrors.Errorf("multiplex stream: %w", err) 75 | } 76 | 77 | for { 78 | conn, err := stream.Accept() 79 | if err != nil { 80 | return xerrors.Errorf("accept stream: %w", err) 81 | } 82 | 83 | go func() { 84 | csConn, err := net.Dial("tcp", addr) 85 | if err != nil { 86 | log.Error(ctx, "dial code-server", slog.Error(err)) 87 | return 88 | } 89 | // Bicopy closes the streams. 90 | bicopy(ctx, csConn, conn) 91 | }() 92 | } 93 | } 94 | 95 | func codeServerReverseProxy(addr, password string) http.Handler { 96 | rp := httputil.NewSingleHostReverseProxy(&url.URL{ 97 | Scheme: "http", 98 | Host: addr, 99 | }) 100 | 101 | dir := rp.Director 102 | rp.Director = func(r *http.Request) { 103 | if password != "" { 104 | r.AddCookie(&http.Cookie{ 105 | Name: "key", 106 | Value: fmt.Sprintf("%x", sha256.Sum256([]byte(password))), 107 | }) 108 | } 109 | dir(r) 110 | } 111 | 112 | return rp 113 | } 114 | 115 | // bicopy copies all of the data between the two connections 116 | // and will close them after one or both of them are done writing. 117 | // If the context is cancelled, both of the connections will be 118 | // closed. 119 | // 120 | // NOTE: This function will block until the copying is done or the 121 | // context is canceled. 122 | func bicopy(ctx context.Context, c1, c2 io.ReadWriteCloser) { 123 | defer c1.Close() 124 | defer c2.Close() 125 | 126 | ctx, cancel := context.WithCancel(ctx) 127 | 128 | copy := func(dst io.WriteCloser, src io.Reader) { 129 | defer cancel() 130 | _, _ = io.Copy(dst, src) 131 | } 132 | 133 | go copy(c1, c2) 134 | go copy(c2, c1) 135 | 136 | <-ctx.Done() 137 | } 138 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // Version is the version of the agent. 4 | var Version string 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "go.coder.com/cli" 7 | "go.coder.com/cloud-agent/internal/cmd" 8 | ) 9 | 10 | func main() { 11 | log.SetFlags(0) 12 | cli.RunRoot(cmd.Make()) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/agentlogin/doc.go: -------------------------------------------------------------------------------- 1 | // Package agentlogin contains a thin abstraction for reading and 2 | // writing messages passed over the agent login websocket. 3 | package agentlogin 4 | -------------------------------------------------------------------------------- /pkg/agentlogin/login.go: -------------------------------------------------------------------------------- 1 | package agentlogin 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "cdr.dev/slog" 8 | "golang.org/x/xerrors" 9 | "nhooyr.io/websocket" 10 | "nhooyr.io/websocket/wsjson" 11 | ) 12 | 13 | // ServerNameQueryParam is the query parameter indicating the 14 | // name of the server that login is being performed for. It is mainly 15 | // used as a mechanism for redirecting the user to the server IDE once 16 | // authentication is successful. 17 | const ServerNameQueryParam = "server_name" 18 | 19 | // Server implements the server-side for the agent login flow. 20 | type Server struct { 21 | Ctx context.Context 22 | Conn *websocket.Conn 23 | Log slog.Logger 24 | } 25 | 26 | const ( 27 | msgTypeAuthURL = "auth_url" 28 | msgTypeError = "error" 29 | msgTypeToken = "token" 30 | ) 31 | 32 | type loginMsg struct { 33 | Type string `json:"type"` 34 | Msg string `json:"msg"` 35 | } 36 | 37 | // WriteAuthURL writes the Auth Code URL to the websocket. 38 | func (s *Server) WriteAuthURL(url string) bool { 39 | return write(s.Ctx, s.Log, s.Conn, loginMsg{ 40 | Type: msgTypeAuthURL, 41 | Msg: url, 42 | }) 43 | } 44 | 45 | // WriteError writes an error that occurred during the login 46 | // process to the client. 47 | func (s *Server) WriteError(err string) bool { 48 | return write(s.Ctx, s.Log, s.Conn, loginMsg{ 49 | Type: msgTypeError, 50 | Msg: err, 51 | }) 52 | } 53 | 54 | // WriteSessionToken writes the session token to the webscoket. 55 | func (s *Server) WriteSessionToken(token string) bool { 56 | return write(s.Ctx, s.Log, s.Conn, loginMsg{ 57 | Type: msgTypeToken, 58 | Msg: token, 59 | }) 60 | } 61 | 62 | // Client implements the client-side of the agent login 63 | // flow. 64 | type Client struct { 65 | Ctx context.Context 66 | Conn *websocket.Conn 67 | } 68 | 69 | // ReadAuthURL reads the auth code URL endpoint from the websocket. 70 | func (c *Client) ReadAuthURL() (string, error) { 71 | return readLoginMsg(c.Ctx, c.Conn, msgTypeAuthURL) 72 | } 73 | 74 | // ReadSessionToken reads the session token that is created from a successful 75 | // login from the websocket. 76 | func (c *Client) ReadSessionToken() (string, error) { 77 | return readLoginMsg(c.Ctx, c.Conn, msgTypeToken) 78 | } 79 | 80 | func readLoginMsg(ctx context.Context, c *websocket.Conn, msgType string) (string, error) { 81 | var msg loginMsg 82 | 83 | err := wsjson.Read(ctx, c, &msg) 84 | if err != nil { 85 | return "", xerrors.Errorf("read msg: %w", err) 86 | } 87 | if msg.Type == msgTypeError { 88 | return "", xerrors.New(msg.Msg) 89 | } 90 | if msg.Type != msgType { 91 | return "", xerrors.Errorf("unexpected message type %v", msg.Type) 92 | } 93 | 94 | return msg.Msg, nil 95 | } 96 | 97 | // Write writes the provided message to the connection, logging and returning false if an error occurs. 98 | func write(ctx context.Context, log slog.Logger, c *websocket.Conn, msg interface{}) bool { 99 | err := wsjson.Write(ctx, c, msg) 100 | if err != nil { 101 | logLevel(log, err)(ctx, "write websocket message", 102 | slog.F("msg", msg), 103 | slog.Error(err), 104 | ) 105 | } 106 | 107 | return err == nil 108 | } 109 | 110 | func logLevel(log slog.Logger, err error) func(context.Context, string, ...slog.Field) { 111 | if xerrors.Is(err, io.EOF) || xerrors.Is(err, context.Canceled) { 112 | return log.Warn 113 | } 114 | var closeErr websocket.CloseError 115 | if xerrors.As(err, &closeErr) { 116 | switch closeErr.Code { 117 | case 118 | websocket.StatusNoStatusRcvd, 119 | websocket.StatusGoingAway, 120 | websocket.StatusNormalClosure: 121 | return log.Warn 122 | } 123 | } 124 | return log.Error 125 | } 126 | --------------------------------------------------------------------------------