├── red ├── .gitignore ├── testdata │ └── corpus │ │ ├── empty │ │ ├── da39a3ee5e6b4b0d3255bfef95601890afd80709 │ │ ├── 3c585604e87f855973731fea83e21fab9392d2fc-2 │ │ ├── 562ab9c9c3330099c997ce780371b1a300ee43b1-3 │ │ ├── 8a087534519cfee10f74beff720d325ed0990430-3 │ │ ├── 4dbce30e8a7a9d9f984eb74404e317e6f1d22448-4 │ │ ├── 4ef6d6652a7daa418f2fbabe069e98c30346d80e-4 │ │ ├── 462d0697162da73c65e2a901e545655de2202408-5 │ │ ├── d7722a9470e3037943844f2695af70f7e82c8463-5 │ │ ├── 0d8a2a1f130e90811e7db5ed5ac6a57a63720e0a-1 │ │ ├── 1d1463c7b16f2bb1d9f2302b2033e4c3c3c3048e-7 │ │ ├── 1daa74d200edf41123ba08006bd271db8147580f-8 │ │ ├── 1fcc927d5a3a889428cf615f42f0a1dd294ab61b-6 │ │ ├── 51a5ee3cd5192b0f9a1c75d42938999e63a1de25-11 │ │ ├── 5ece2d086cd2dbe7c157e5b9f9c2f16fb177ee94-11 │ │ ├── 900e0c400f398f9526267212a68892bca0a04313-8 │ │ ├── a124f504b9782c21739b39c7a62843ee86efb4a2-7 │ │ ├── a46fd47f5dd4fce06cb23d9f75e43ab82da0b181-9 │ │ ├── aa17a4d08f14d0b4aa423b66b0ba6e044c3a0bee-9 │ │ ├── af5c54e21dc8767c4e25bf6ec5aa5d2186098a49-13 │ │ ├── c9accb6bf733cfefb2cb5b55bda895fc4bcec68a-12 │ │ ├── d7c306505980fd707dea754c5bcd72d6dafb3aac-6 │ │ ├── eff3a941b53b42c204e72596a2bb85fd5d3a72d6-5 │ │ ├── fa8df6bec92a2a4a8b1d0b8897b5b7923947b86e-14 │ │ ├── ff7f3d3d6ed89b9090575fa52d2d93e34de04a3f-10 │ │ ├── 36c1e654a730c6d98fe8d0653ed169781864c3f2-1 │ │ ├── 396ace6e8a4c08a19a8151c0b4c2c9ca4a72877d-2 │ │ ├── 31f70ab0a44bcf36bbafb434265706c5ba9c2ff0 │ │ ├── d4ea6644b61cd5a31a9c0c684715796e158881f8 │ │ ├── d138a88f2804f3265ce57756102126b8dd81b9e5-1 │ │ ├── d73402c4bb6c03dd3516913248f7f41201ba7860 │ │ ├── 91290761f5e919f1e33490f2d98f2d0e16a0985a │ │ ├── 336f18ff75cc7cb4a1229712f0ab6695a2c56776 │ │ ├── 1083fb89b14f3420e5855a9789366aeb1cfb6e0b-1 │ │ ├── 9ba5c36213839037cdce69b14217ca957bb4e211 │ │ ├── aaca1c76aace26bef278dcb4ce9e625a5924b9e2 │ │ ├── 6c05cf4d9c8c3e62f2393f65c23464ff89cabcb6 │ │ ├── 2542ddd0c8814867a98481a42eb5b856ec436b08-1 │ │ └── 7700302e46625aad3fab2a0c8e0ac964144caded ├── authmethod_string.go ├── errorcode_string.go ├── channeltype_string.go ├── Makefile.fuzz ├── serverticket.go ├── minidataheader.go ├── clientauthmethod.go ├── clientticket.go ├── linkheader.go ├── capability_test.go ├── serverticket_test.go ├── fuzz.go ├── clientauthmethod_test.go ├── clientticket_test.go ├── capability.go ├── red.go ├── linkheader_test.go ├── clientlinkmessage_test.go ├── clientlinkmessage.go ├── serverlinkmessage.go └── serverlinkmessage_test.go ├── .gitignore ├── .stickler.yml ├── .github ├── dependabot.yml └── workflows │ └── go-test.yml ├── go.mod ├── flow.go ├── LICENSE.md ├── README.md ├── logger.go ├── auth_test.go ├── sessions_test.go ├── go.sum ├── logger_test.go ├── options.go ├── sessions.go ├── proxy.go ├── example └── proxy.go ├── proxy_test.go ├── tenant_test.go ├── compute.go ├── auth.go └── tenant.go /red/.gitignore: -------------------------------------------------------------------------------- 1 | red-fuzz.zip -------------------------------------------------------------------------------- /red/testdata/corpus/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | -------------------------------------------------------------------------------- /red/testdata/corpus/da39a3ee5e6b4b0d3255bfef95601890afd80709: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /red/testdata/corpus/3c585604e87f855973731fea83e21fab9392d2fc-2: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /red/testdata/corpus/562ab9c9c3330099c997ce780371b1a300ee43b1-3: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /red/testdata/corpus/8a087534519cfee10f74beff720d325ed0990430-3: -------------------------------------------------------------------------------- 1 | reflect.Select -------------------------------------------------------------------------------- /red/testdata/corpus/4dbce30e8a7a9d9f984eb74404e317e6f1d22448-4: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /red/testdata/corpus/4ef6d6652a7daa418f2fbabe069e98c30346d80e-4: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /red/testdata/corpus/462d0697162da73c65e2a901e545655de2202408-5: -------------------------------------------------------------------------------- 1 | 186264514923B@- -------------------------------------------------------------------------------- /red/testdata/corpus/d7722a9470e3037943844f2695af70f7e82c8463-5: -------------------------------------------------------------------------------- 1 | 227373675443#3205947875 -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | golint: 3 | fixer: true 4 | fixers: 5 | enable: true 6 | files: 7 | ignore: 8 | - '*_string.go' -------------------------------------------------------------------------------- /red/testdata/corpus/0d8a2a1f130e90811e7db5ed5ac6a57a63720e0a-1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/0d8a2a1f130e90811e7db5ed5ac6a57a63720e0a-1 -------------------------------------------------------------------------------- /red/testdata/corpus/1d1463c7b16f2bb1d9f2302b2033e4c3c3c3048e-7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/1d1463c7b16f2bb1d9f2302b2033e4c3c3c3048e-7 -------------------------------------------------------------------------------- /red/testdata/corpus/1daa74d200edf41123ba08006bd271db8147580f-8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/1daa74d200edf41123ba08006bd271db8147580f-8 -------------------------------------------------------------------------------- /red/testdata/corpus/1fcc927d5a3a889428cf615f42f0a1dd294ab61b-6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/1fcc927d5a3a889428cf615f42f0a1dd294ab61b-6 -------------------------------------------------------------------------------- /red/testdata/corpus/51a5ee3cd5192b0f9a1c75d42938999e63a1de25-11: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/51a5ee3cd5192b0f9a1c75d42938999e63a1de25-11 -------------------------------------------------------------------------------- /red/testdata/corpus/5ece2d086cd2dbe7c157e5b9f9c2f16fb177ee94-11: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/5ece2d086cd2dbe7c157e5b9f9c2f16fb177ee94-11 -------------------------------------------------------------------------------- /red/testdata/corpus/900e0c400f398f9526267212a68892bca0a04313-8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/900e0c400f398f9526267212a68892bca0a04313-8 -------------------------------------------------------------------------------- /red/testdata/corpus/a124f504b9782c21739b39c7a62843ee86efb4a2-7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/a124f504b9782c21739b39c7a62843ee86efb4a2-7 -------------------------------------------------------------------------------- /red/testdata/corpus/a46fd47f5dd4fce06cb23d9f75e43ab82da0b181-9: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/a46fd47f5dd4fce06cb23d9f75e43ab82da0b181-9 -------------------------------------------------------------------------------- /red/testdata/corpus/aa17a4d08f14d0b4aa423b66b0ba6e044c3a0bee-9: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/aa17a4d08f14d0b4aa423b66b0ba6e044c3a0bee-9 -------------------------------------------------------------------------------- /red/testdata/corpus/af5c54e21dc8767c4e25bf6ec5aa5d2186098a49-13: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/af5c54e21dc8767c4e25bf6ec5aa5d2186098a49-13 -------------------------------------------------------------------------------- /red/testdata/corpus/c9accb6bf733cfefb2cb5b55bda895fc4bcec68a-12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/c9accb6bf733cfefb2cb5b55bda895fc4bcec68a-12 -------------------------------------------------------------------------------- /red/testdata/corpus/d7c306505980fd707dea754c5bcd72d6dafb3aac-6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/d7c306505980fd707dea754c5bcd72d6dafb3aac-6 -------------------------------------------------------------------------------- /red/testdata/corpus/eff3a941b53b42c204e72596a2bb85fd5d3a72d6-5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/eff3a941b53b42c204e72596a2bb85fd5d3a72d6-5 -------------------------------------------------------------------------------- /red/testdata/corpus/fa8df6bec92a2a4a8b1d0b8897b5b7923947b86e-14: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/fa8df6bec92a2a4a8b1d0b8897b5b7923947b86e-14 -------------------------------------------------------------------------------- /red/testdata/corpus/ff7f3d3d6ed89b9090575fa52d2d93e34de04a3f-10: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsimonetti/go-spice/HEAD/red/testdata/corpus/ff7f3d3d6ed89b9090575fa52d2d93e34de04a3f-10 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /red/testdata/corpus/36c1e654a730c6d98fe8d0653ed169781864c3f2-1: -------------------------------------------------------------------------------- 1 | ErrorO@rrorErrorErrorInvalidMagicErrorInvalidDat|ErrorVersionMismatchErrorNeedSecuredErrorNeedUnsecuredErrorPe%!(BADPREC)rmissionDeniedErrorB(BADINDEX)onIDEr`orChannelNotAvailab -------------------------------------------------------------------------------- /red/testdata/corpus/396ace6e8a4c08a19a8151c0b4c2c9ca4a72877d-2: -------------------------------------------------------------------------------- 1 | ErrorO@rrorErrorErrorInvalidM[gicErrorInvalidDat|ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%!(BADPREC)rmissionDeniedErrorB(BADINDEX)onIDErle `or -------------------------------------------------------------------------------- /red/testdata/corpus/31f70ab0a44bcf36bbafb434265706c5ba9c2ff0: -------------------------------------------------------------------------------- 1 | T@rrorErrorErrorInvalidM[ErrorO [gicErrorInvalidDat|ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%!(BADPREC)rmissionDeniedErrorB(BADINDEX)onIDErle +`or|ErrorVersionMismatchErrorNeedSecuredEProrNeedU -------------------------------------------------------------------------------- /red/testdata/corpus/d4ea6644b61cd5a31a9c0c684715796e158881f8: -------------------------------------------------------------------------------- 1 | ErrorO@rrorErrorErrorInvalidM[gicErrorInvalidDat|ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%!rmissionDeniedErrorB(BADINDEX)onIDErle BADPREC`J_runG__1_qyu_y_9_kh2u_D__SjN0_6_sL_H7x_s_B_xwwV__5_49nF9k_3we0_JL590x6MIB11wS___26u -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jsimonetti/go-spice 2 | 3 | go 1.13 4 | 5 | replace acln.ro/zerocopy => github.com/acln0/zerocopy v0.0.0-20190410132315-ac749309e897 6 | 7 | require ( 8 | acln.ro/zerocopy v0.0.0-20190410132315-ac749309e897 9 | github.com/pkg/errors v0.9.1 10 | github.com/sirupsen/logrus v1.9.3 11 | ) 12 | -------------------------------------------------------------------------------- /red/testdata/corpus/d138a88f2804f3265ce57756102126b8dd81b9e5-1: -------------------------------------------------------------------------------- 1 | __5_M_C_C805F@rrorErrorErrorInvalidM[gicErrorInvalidDatErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre!(BADPREC)rmissionDeniedErrorB(BADINDEX)onIDErle ErrorO@rrorErrorErrorInvalidM[_ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%!BAD -------------------------------------------------------------------------------- /red/testdata/corpus/d73402c4bb6c03dd3516913248f7f41201ba7860: -------------------------------------------------------------------------------- 1 | __5_M_C_C6b86F@rrorErrorErrorInvalidM[gicErrorInvalidDatErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre!(BADPREC)rmissionDeniedErrorB(BADINDEX)onIDErle ErrorO@rrorErrorErrorInvalidM[_ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%!BADPREC -------------------------------------------------------------------------------- /red/testdata/corpus/91290761f5e919f1e33490f2d98f2d0e16a0985a: -------------------------------------------------------------------------------- 1 | ErrorO@rrorErrorErrorInvalidM[gicErrorInvalidDat|ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%!(BADPREC)rmissionDeniedErrorB[BADINDEX`onIDErle )&or@rrorErrorErrorInvalidM[!rmissionDeniedErrorB(BADINDEX)onIDErle +{ErrorO@rrorErrorErrorInvalidM[ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredEr -------------------------------------------------------------------------------- /red/testdata/corpus/336f18ff75cc7cb4a1229712f0ab6695a2c56776: -------------------------------------------------------------------------------- 1 | T(BADINDEX)rrorErrorErrorInvalidM[ErrorO ^*gicErrorInvalidDat|ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%!()rmissionDeniedErrorB(BADINDEX)onIDErle +`ErrorO$rrorErrorErrorInvalidM[|ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%(BADPREC)'BADINDEX'onIDErle@ }4o6?`or\ErrorVersionMismatchErro -------------------------------------------------------------------------------- /red/testdata/corpus/1083fb89b14f3420e5855a9789366aeb1cfb6e0b-1: -------------------------------------------------------------------------------- 1 | ErrorO@rrorErrorErrorInvalidM[gicErrorInvalidDat|ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%!(BADPREC)rmissionDeniedErrorB[BADINDEX`onIDErle \n)&or@rrorErrorErrorInvalidM[!rmissionDeniedErrorB(BADINDEX)o[fnIDErle +{ErrorO@rro2ErrorErrorInvalidM[ErrorVersionMismatchErrorNeedSecured9094947017729282379150390625EProrNeetU -------------------------------------------------------------------------------- /red/testdata/corpus/9ba5c36213839037cdce69b14217ca957bb4e211: -------------------------------------------------------------------------------- 1 | rrorErrorErrorInvalidM[CP_q [gicErrorInvalidDat|ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%!(BADPREC)rmissionDeniedErrorB(BADINDEX)onIDErle +`or|ErrorO%@[gicErrorInvalidDat|ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorregicErrorInvalidDatErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre!(BADPREC)rmissionDeniedErrorB(X8_9K7y_2CT88S)onIDErle -------------------------------------------------------------------------------- /red/testdata/corpus/aaca1c76aace26bef278dcb4ce9e625a5924b9e2: -------------------------------------------------------------------------------- 1 | ErrorO@rrorErrorErrorInvalidM[gicErrorInvalidDat|ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre!(_mkJyv_)rmissionDeniedErrorB[BADINDEX`onIDErle \n(&or@te_2_61_00Xwf00[!A_Aw2__wG6WZLhFFiE__ckfQ2Hfpk__cQj6La3fia_gn7t__n_X(_k_i_4zt_1v_______Q_x_eYU_0___1k_x__s_0_n__NN___049eg_k4___2q_Em8__3Mcm4sU40_Y_Z)o[fnIDErlerrorErrorErrorInvalidM +{ErrorO@0IM[?ErrorVersionMismatchErrorNeedSecured909494701772928237915 -------------------------------------------------------------------------------- /red/authmethod_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=AuthMethod"; DO NOT EDIT. 2 | 3 | package red 4 | 5 | import "fmt" 6 | 7 | const _AuthMethod_name = "AuthMethodSpiceAuthMethodSASL" 8 | 9 | var _AuthMethod_index = [...]uint8{0, 15, 29} 10 | 11 | func (i AuthMethod) String() string { 12 | i -= 1 13 | if i >= AuthMethod(len(_AuthMethod_index)-1) { 14 | return fmt.Sprintf("AuthMethod(%d)", i+1) 15 | } 16 | return _AuthMethod_name[_AuthMethod_index[i]:_AuthMethod_index[i+1]] 17 | } 18 | -------------------------------------------------------------------------------- /red/errorcode_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ErrorCode"; DO NOT EDIT. 2 | 3 | package red 4 | 5 | import "fmt" 6 | 7 | const _ErrorCode_name = "ErrorOkErrorErrorErrorInvalidMagicErrorInvalidDataErrorVersionMismatchErrorNeedSecuredErrorNeedUnsecuredErrorPermissionDeniedErrorBadConnectionIDErrorChannelNotAvailable" 8 | 9 | var _ErrorCode_index = [...]uint8{0, 7, 17, 34, 50, 70, 86, 104, 125, 145, 169} 10 | 11 | func (i ErrorCode) String() string { 12 | if i >= ErrorCode(len(_ErrorCode_index)-1) { 13 | return fmt.Sprintf("ErrorCode(%d)", i) 14 | } 15 | return _ErrorCode_name[_ErrorCode_index[i]:_ErrorCode_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /red/channeltype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ChannelType"; DO NOT EDIT. 2 | 3 | package red 4 | 5 | import "fmt" 6 | 7 | const _ChannelType_name = "ChannelMainChannelDisplayChannelInputsChannelCursorChannelPlaybackChannelRecordChannelTunnelChannelSmartcardChannelUSBRedirChannelPort" 8 | 9 | var _ChannelType_index = [...]uint8{0, 11, 25, 38, 51, 66, 79, 92, 108, 123, 134} 10 | 11 | func (i ChannelType) String() string { 12 | i -= 1 13 | if i >= ChannelType(len(_ChannelType_index)-1) { 14 | return fmt.Sprintf("ChannelType(%d)", i+1) 15 | } 16 | return _ChannelType_name[_ChannelType_index[i]:_ChannelType_index[i+1]] 17 | } 18 | -------------------------------------------------------------------------------- /red/testdata/corpus/6c05cf4d9c8c3e62f2393f65c23464ff89cabcb6: -------------------------------------------------------------------------------- 1 | __5_M_C_C805F@rrorErrorErrorInvalidM[gicErrorInvalidDatErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre!(BADPREC)rmissionDeniedErrorB(BADINDEX)onIDErle ~ErrorOErrorO}rrorErrorErrorInvalidM[rmissionDeniedErrorBErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre![6fo]rmissionDeniedErrorB[BADINDEX`onIDErle ^\n(<or@te_2_61_00Xwf00[A_Aw2__wG6WZLhFFiE__ckfQ2Hfpk__cQj6La3fia_gn7t__n_X(1999N7_37ybB_YJeR7h85_B_0_U_1__L_Yoa_USVa__5n__Y_i56Y7o3CFOw6em_zQ_4r_q32_S)o[fnIDErlerrorErrorErrorInvalidM <{ErrorO@0IM[__8_e8t__o_Bwb2X239I_B94___j26_TO_8_GP__OCq_lW7P__Bp7p__mFr9e2b74S3_D8____DrrorErrorErrorInvalidM[_ErrorVersionMismatchError -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Go 3 | jobs: 4 | test: 5 | name: Test 6 | strategy: 7 | matrix: 8 | go-version: [1.16.x, 1.17.x] 9 | platform: [ubuntu-latest, macos-latest, windows-latest] 10 | runs-on: ${{ matrix.platform }} 11 | steps: 12 | - name: Install Go 13 | uses: actions/setup-go@v1 14 | with: 15 | go-version: ${{ matrix.go-version }} 16 | id: go 17 | 18 | - name: Checkout code 19 | uses: actions/checkout@v1 20 | 21 | - name: Download dependencies 22 | run: go mod download 23 | 24 | - name: Go Vet 25 | run: go vet ./... 26 | 27 | - name: Test 28 | run: go test ./... 29 | 30 | -------------------------------------------------------------------------------- /red/Makefile.fuzz: -------------------------------------------------------------------------------- 1 | # Makefile for fuzzing 2 | # 3 | # Use go-fuzz and needs the tools installed. 4 | # See https://blog.cloudflare.com/dns-parser-meet-go-fuzzer/ 5 | # 6 | # Installing go-fuzz: 7 | # $ make -f Makefile.fuzz get 8 | # Installs: 9 | # * github.com/dvyukov/go-fuzz/go-fuzz 10 | # * get github.com/dvyukov/go-fuzz/go-fuzz-build 11 | 12 | all: build 13 | 14 | .PHONY: build 15 | build: 16 | go-fuzz-build -tags fuzz github.com/jsimonetti/go-spice/red 17 | 18 | .PHONY: fuzz 19 | fuzz: 20 | go-fuzz -bin=red-fuzz.zip -workdir=testdata 21 | 22 | .PHONY: get 23 | get: build 24 | go get github.com/dvyukov/go-fuzz/go-fuzz 25 | go get github.com/dvyukov/go-fuzz/go-fuzz-build 26 | 27 | .PHONY: clean 28 | clean: 29 | rm *-fuzz.zip 30 | -------------------------------------------------------------------------------- /red/testdata/corpus/2542ddd0c8814867a98481a42eb5b856ec436b08-1: -------------------------------------------------------------------------------- 1 | ErrorO@rrorErrorErrorInvalidM[gicErrorInvalidDat|ErrorVersionMis atchErrorNeedSecuredEProrNeedUnsecuredErrorre%!rmissionDeniedErrorB(BADINDEX)onIDErle m_38Pv_f9__2_S___P__8WlI65wMml_Z__16_9_5j0Co_aRHp_f9_3k_eTURJ5_lLC3_Ah2_K&rrorErrorErrorInvalidM[_ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%ErrorO.@rrorErrorErrorInvalidM[j_vS_g__9svUi_J1_3585____zUYp4_ia8GC9_0Ho_7oqb_o_e09iErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%!(BADPREC)rmissionDeniedErrorB[BADINDEX`_aw__L1OUQz__gWu0A ror\n)&Q_@SrorErrorErrorInvalidM[rmissionDeniedErrorB(BADINDEX)0[ BADINDEX{ErrorO@ErrorVersionMismatchErrorNeedSecured9094947017729282379 -------------------------------------------------------------------------------- /red/testdata/corpus/7700302e46625aad3fab2a0c8e0ac964144caded: -------------------------------------------------------------------------------- 1 | __5_M_C_C6b86FrrorErrorErrorInvalidM[gicErrorInvalidDatErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre!(BADPREC)rmissionDeniedErrorB(BADINDEX)onIDErle{ _38Pv_f9__2_S___P__8WlI65wMml_Z__16_9_5j0Co_aRHp_f9_3k_eTURJ5_lLC3_Ah2_K&rrorErrorErrorInvalidM[_ErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%ErrorO.@rrorErrorErrorInvalidM[j_vS_g__9svUi_J1_3585____zUYp4_ia8GC9_0Ho_7oqc_o_e09iErrorVersionMismatchErrorNeedSecuredEProrNeedUnsecuredErrorre%!(BADPREC)rmissionDeniedErrorB[BADINDEX`_aw__L1OUQz__gWu0A \n)&Q_@rrorErrorErrorInvalidM[rmissionDeniedErrorB(BADINDEX)0[ BADINDEX{ErrorO@ErrorVersionMismatchErrorNeedSecured9094947017729282379150390625EProrNeetUBADP -------------------------------------------------------------------------------- /flow.go: -------------------------------------------------------------------------------- 1 | package spice 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | 7 | "acln.ro/zerocopy" 8 | ) 9 | 10 | // flow is a connection pipe to couple tenant to compute connections 11 | type flow struct { 12 | tenant net.Conn 13 | compute net.Conn 14 | } 15 | 16 | // newFlow returns a new flow 17 | func newFlow(tenant net.Conn, compute net.Conn) *flow { 18 | flow := &flow{ 19 | tenant: tenant, 20 | compute: compute, 21 | } 22 | return flow 23 | } 24 | 25 | // Pipe will start piping the connections together 26 | func (f *flow) Pipe() error { 27 | f.pipe(f.compute, f.tenant) 28 | return nil 29 | } 30 | 31 | func (f *flow) pipe(src, dst net.Conn) (sent, received int64) { 32 | if src == nil || dst == nil { 33 | return 34 | } 35 | 36 | var wg sync.WaitGroup 37 | wg.Add(2) 38 | go func() { 39 | sent, _ = zerocopy.Transfer(src, dst) 40 | wg.Done() 41 | }() 42 | go func() { 43 | received, _ = zerocopy.Transfer(dst, src) 44 | wg.Done() 45 | }() 46 | wg.Wait() 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /red/serverticket.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import "encoding/binary" 4 | 5 | // ServerTicket is a spice packet send by the server in response to 6 | // a ClientTicket packet 7 | type ServerTicket struct { 8 | Result ErrorCode 9 | } 10 | 11 | // MarshalBinary marshals a Packet into a byte slice. 12 | func (p *ServerTicket) MarshalBinary() ([]byte, error) { 13 | p.finish() 14 | b := make([]byte, 4) 15 | binary.LittleEndian.PutUint32(b[0:4], uint32(p.Result)) 16 | return b, nil 17 | } 18 | 19 | // UnmarshalBinary unmarshals the contents of a byte slice into a Packet. 20 | func (p *ServerTicket) UnmarshalBinary(b []byte) error { 21 | if len(b) < 4 { 22 | return errInvalidPacket 23 | } 24 | p.Result = ErrorCode(binary.LittleEndian.Uint32(b[0:4])) 25 | return p.validate() 26 | } 27 | 28 | // validate is used to validate the Packet. 29 | func (p *ServerTicket) validate() error { 30 | return nil 31 | } 32 | 33 | // finish is used to finish the Packet for sending. 34 | func (p *ServerTicket) finish() { 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (C) 2017 Jeroen Simonetti 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /red/minidataheader.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import "encoding/binary" 4 | 5 | // MiniDataHeader is a header to a data packet 6 | type MiniDataHeader struct { 7 | // MessageType is type of message 8 | MessageType uint16 9 | 10 | // Size in bytes following this field to the end of this message 11 | Size uint32 12 | } 13 | 14 | // MarshalBinary marshals an Packet into a byte slice. 15 | func (p *MiniDataHeader) MarshalBinary() ([]byte, error) { 16 | p.finish() 17 | b := make([]byte, 6) 18 | binary.LittleEndian.PutUint16(b[0:2], p.MessageType) 19 | binary.LittleEndian.PutUint32(b[2:6], p.Size) 20 | return b, nil 21 | } 22 | 23 | // UnmarshalBinary unmarshals the contents of a byte slice into a Packet. 24 | func (p *MiniDataHeader) UnmarshalBinary(b []byte) error { 25 | if len(b) < 6 { 26 | return errInvalidPacket 27 | } 28 | p.MessageType = binary.LittleEndian.Uint16(b[0:2]) 29 | p.Size = binary.LittleEndian.Uint32(b[2:6]) 30 | return p.validate() 31 | } 32 | 33 | // validate is used to validate the Packet. 34 | func (p *MiniDataHeader) validate() error { 35 | return nil 36 | } 37 | 38 | // finish is used to finish the Packet for sending. 39 | func (p *MiniDataHeader) finish() { 40 | } 41 | -------------------------------------------------------------------------------- /red/clientauthmethod.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import "encoding/binary" 4 | 5 | // ClientAuthMethod is a spice packet send by the client 6 | // to select the authentication method. 7 | type ClientAuthMethod struct { 8 | // Method is the authentication method selected by the client 9 | Method AuthMethod 10 | } 11 | 12 | // MarshalBinary marshals an Packet into a byte slice. 13 | func (p *ClientAuthMethod) MarshalBinary() ([]byte, error) { 14 | p.finish() 15 | b := make([]byte, 4) 16 | binary.LittleEndian.PutUint32(b[0:4], uint32(p.Method)) 17 | return b, nil 18 | } 19 | 20 | // UnmarshalBinary unmarshals the contents of a byte slice into a Packet. 21 | func (p *ClientAuthMethod) UnmarshalBinary(b []byte) error { 22 | if len(b) < 4 { 23 | return errInvalidPacket 24 | } 25 | p.Method = AuthMethod(binary.LittleEndian.Uint32(b[0:4])) 26 | return p.validate() 27 | } 28 | 29 | // validate is used to validate the Packet. 30 | func (p *ClientAuthMethod) validate() error { 31 | if p.Method != AuthMethodSpice && p.Method != AuthMethodSASL { 32 | return errInvalidPacket 33 | } 34 | return nil 35 | } 36 | 37 | // finish is used to finish the Packet for sending. 38 | func (p *ClientAuthMethod) finish() { 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-spice [![GoDoc](https://godoc.org/github.com/jsimonetti/go-spice?status.svg)](https://godoc.org/github.com/jsimonetti/go-spice) [![Go Report Card](https://goreportcard.com/badge/github.com/jsimonetti/go-spice)](https://goreportcard.com/report/github.com/jsimonetti/go-spice) 2 | [![Build Status](https://drone.simonetti.nl/api/badges/jsimonetti/go-spice/status.svg)](https://drone.simonetti.nl/jsimonetti/go-spice) 3 | ======= 4 | 5 | Package `spice` attempts to implement a SPICE proxy. 6 | It can be used to proxy virt-viewer/remote-viewer traffic to destination qemu instances. 7 | Using this proxy over a HTML5 based web viewer has many advantages. One being, the native remote-viewer 8 | client can be used through this proxy. This allows (for example) USB redirection, sound playback and recording 9 | and clipboard sharing to function. 10 | 11 | This package is mostly finished except for the below mentioned todo's. The API should be stable. 12 | Vendoring this package is still advised in any case. 13 | 14 | TODO: 15 | - implement proper auth capability handling 16 | - implement SASL authentication (Not planned, but nice to have) 17 | 18 | 19 | See [example](https://godoc.org/github.com/jsimonetti/go-spice#Proxy) for an example including an Authenticator 20 | -------------------------------------------------------------------------------- /red/clientticket.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | // ClientTicket is a spice packet send by the client 9 | // that contains a ticket 10 | type ClientTicket struct { 11 | // Ticket is the RSA encrypted ticket 12 | Ticket [ClientTicketBytes]byte 13 | } 14 | 15 | // MarshalBinary marshals a Packet into a byte slice. 16 | func (p *ClientTicket) MarshalBinary() ([]byte, error) { 17 | p.finish() 18 | 19 | var buf bytes.Buffer 20 | if err := binary.Write(&buf, binary.LittleEndian, p.Ticket); err != nil { 21 | return nil, err 22 | } 23 | 24 | return buf.Bytes()[0:ClientTicketBytes], nil 25 | } 26 | 27 | // UnmarshalBinary unmarshals the contents of a byte slice into a Packet. 28 | func (p *ClientTicket) UnmarshalBinary(b []byte) error { 29 | if len(b) != ClientTicketBytes { 30 | return errInvalidPacket 31 | } 32 | 33 | buf := bytes.NewReader(b[0:ClientTicketBytes]) 34 | if err := binary.Read(buf, binary.LittleEndian, p.Ticket[:]); err != nil { 35 | return err 36 | } 37 | 38 | return p.validate() 39 | } 40 | 41 | // validate is used to validate the Packet. 42 | func (p *ClientTicket) validate() error { 43 | return nil 44 | } 45 | 46 | // finish is used to finish the Packet for sending. 47 | func (p *ClientTicket) finish() { 48 | } 49 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package spice 2 | 3 | import "github.com/sirupsen/logrus" 4 | 5 | // Logger is a logging adapter interface 6 | type Logger interface { 7 | // Debug logs debugging messages. 8 | Debug(...interface{}) 9 | // Info logs informational messages. 10 | Info(...interface{}) 11 | // Error logs error messages. 12 | Error(...interface{}) 13 | // WithFields creates a new Logger with the fields embedded 14 | WithFields(keyvals ...interface{}) Logger 15 | // WithError creates a new Logger with the error embedded 16 | WithError(err error) Logger 17 | } 18 | 19 | // adapter is a thin wrapper around the logrus logger that adapts it to 20 | // the Logger interface. 21 | type adapter struct { 22 | *logrus.Entry 23 | } 24 | 25 | // Adapt creates a Logger backed from a logrus Entry. 26 | func Adapt(l *logrus.Entry) Logger { 27 | return &adapter{l} 28 | } 29 | 30 | func (a *adapter) WithFields(keyvals ...interface{}) Logger { 31 | fields := a.fields(keyvals...) 32 | return &adapter{a.Entry.WithFields(fields)} 33 | } 34 | 35 | func (a *adapter) WithError(err error) Logger { 36 | return &adapter{a.Entry.WithError(err)} 37 | } 38 | 39 | func (a *adapter) fields(keyvals ...interface{}) logrus.Fields { 40 | if len(keyvals)%2 != 0 { 41 | keyvals = append(keyvals, "MISSING") 42 | } 43 | 44 | fields := make(logrus.Fields) 45 | 46 | for i := 0; i < len(keyvals); i += 2 { 47 | k := keyvals[i].(string) 48 | v := keyvals[i+1] 49 | fields[k] = v 50 | } 51 | 52 | return fields 53 | } 54 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package spice 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/sha1" 8 | "io" 9 | "log" 10 | "testing" 11 | ) 12 | 13 | func TestAuthSpiceSave(t *testing.T) { 14 | auth := newAuthSpice(t) 15 | 16 | auth.SaveAddress("123456") 17 | if auth.LoadAddress() != "123456" { 18 | t.Errorf("address saved and loaded mismatch") 19 | } 20 | 21 | auth.SaveToken("123456") 22 | if auth.LoadToken() != "123456" { 23 | t.Errorf("tokens saved and loaded mismatch") 24 | } 25 | } 26 | func TestAuthSpiceToken(t *testing.T) { 27 | auth := newAuthSpice(t) 28 | 29 | password := "123456" 30 | 31 | // crypto/rand.Reader is a good source of entropy for randomizing the 32 | // encryption function. 33 | rng := rand.Reader 34 | 35 | pubkey := auth.privateKey.Public().(*rsa.PublicKey) 36 | 37 | ciphertext, err := rsa.EncryptOAEP(sha1.New(), rng, pubkey, []byte(password), []byte{}) 38 | if err != nil { 39 | panic(err) 40 | } 41 | auth.tenant.(io.Writer).Write(ciphertext) 42 | 43 | token, err := auth.Token() 44 | if err != nil { 45 | log.Fatalf("unexpected error %#v", err) 46 | } 47 | 48 | if token != password { 49 | log.Fatalf("wrong password received") 50 | } 51 | } 52 | 53 | func newAuthSpice(t *testing.T) *authSpice { 54 | t.Helper() 55 | key, err := rsa.GenerateKey(rand.Reader, 1024) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | a := &authSpice{ 61 | tenant: bytes.NewBuffer(make([]byte, 0, 0)), 62 | privateKey: key, 63 | } 64 | 65 | return a 66 | } 67 | -------------------------------------------------------------------------------- /sessions_test.go: -------------------------------------------------------------------------------- 1 | package spice 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEmptyTable(t *testing.T) { 8 | setupTable(t) 9 | 10 | if table.Lookup(1) != false { 11 | t.Fatal("lookup should not return for this key") 12 | } 13 | if table.OTP(1) != "" { 14 | t.Fatal("lookup should not return for this key") 15 | } 16 | if _, err := table.Connect(1); err == nil { 17 | t.Fatal("lookup should not return for this key") 18 | } 19 | } 20 | 21 | func TestTableAdd(t *testing.T) { 22 | setupTable(t) 23 | 24 | table.Add(1, "dst1", "otp1") 25 | 26 | if table.OTP(1) != "otp1" { 27 | t.Fatal("lookup should have returned this key") 28 | } 29 | 30 | var dst string 31 | var err error 32 | if dst, err = table.Connect(1); err != nil { 33 | t.Fatal("lookup should have returned this key") 34 | } 35 | 36 | if dst != "dst1" { 37 | t.Fatal("lookup should have returned this key") 38 | } 39 | } 40 | 41 | func TestTableConnect(t *testing.T) { 42 | setupTable(t) 43 | table.Add(1, "dst1", "otp1") 44 | table.Connect(1) 45 | table.Connect(1) 46 | if table.entries[1].usageCount != 3 { 47 | t.Fatal("should have 3 connections") 48 | } 49 | table.Disconnect(1) 50 | table.Disconnect(1) 51 | if table.entries[1].usageCount != 1 { 52 | t.Fatal("should have 1 connection") 53 | } 54 | table.Disconnect(1) 55 | if _, ok := table.entries[1]; ok { 56 | t.Fatal("should not have a connection") 57 | } 58 | } 59 | 60 | var table *sessionTable 61 | 62 | func setupTable(t *testing.T) { 63 | t.Helper() 64 | table = newSessionTable() 65 | } 66 | -------------------------------------------------------------------------------- /red/linkheader.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | // LinkHeader is a header to a client link message packet 9 | type LinkHeader struct { 10 | // Magic must be equal to Magic 11 | Magic [4]uint8 12 | 13 | // Major must be equal to RED_VERSION_MAJOR 14 | Major uint32 15 | 16 | // Minor must be equal to RED_VERSION_MINOR 17 | Minor uint32 18 | 19 | // Size in bytes following this field to the end of this message 20 | Size uint32 21 | } 22 | 23 | // MarshalBinary marshals a Packet into a byte slice. 24 | func (p *LinkHeader) MarshalBinary() ([]byte, error) { 25 | p.finish() 26 | b := make([]byte, 16) 27 | 28 | copy(b[0:4], p.Magic[0:4]) 29 | binary.LittleEndian.PutUint32(b[4:8], p.Major) 30 | binary.LittleEndian.PutUint32(b[8:12], p.Minor) 31 | binary.LittleEndian.PutUint32(b[12:16], p.Size) 32 | 33 | return b, nil 34 | } 35 | 36 | // UnmarshalBinary unmarshals the contents of a byte slice into a Packet. 37 | func (p *LinkHeader) UnmarshalBinary(b []byte) error { 38 | if len(b) < 16 { 39 | return errInvalidPacket 40 | } 41 | 42 | copy(p.Magic[0:4], b[0:4]) 43 | 44 | p.Major = binary.LittleEndian.Uint32(b[4:8]) 45 | p.Minor = binary.LittleEndian.Uint32(b[8:12]) 46 | p.Size = binary.LittleEndian.Uint32(b[12:16]) 47 | 48 | return p.validate() 49 | } 50 | 51 | // validate is used to validate the Packet. 52 | func (p *LinkHeader) validate() error { 53 | if !bytes.Equal(p.Magic[:], Magic[:]) { 54 | return errInvalidPacket 55 | } 56 | if p.Major != VersionMajor || p.Minor != VersionMinor { 57 | return errInvalidVersion 58 | } 59 | return nil 60 | } 61 | 62 | // finish is used to finish the Packet for sending. 63 | func (p *LinkHeader) finish() { 64 | p.Magic = Magic 65 | p.Major = VersionMajor 66 | p.Minor = VersionMinor 67 | } 68 | -------------------------------------------------------------------------------- /red/capability_test.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCapabilityNew(t *testing.T) { 8 | var v Capability 9 | if v.Test(0) { 10 | t.Errorf("Unable to make a capability and read its 0th value.") 11 | } 12 | } 13 | 14 | func TestCapabilityIsClear(t *testing.T) { 15 | var v Capability 16 | for i := uint32(0); i < 32; i++ { 17 | if v.Test(i) { 18 | t.Errorf("Bit %d is set, and it shouldn't be.", i) 19 | } 20 | } 21 | } 22 | 23 | func TestCapabilitySetTo(t *testing.T) { 24 | var v Capability 25 | v.SetTo(10, true) 26 | if !v.Test(10) { 27 | t.Errorf("Bit %d is clear, and it shouldn't be.", 10) 28 | } 29 | v.SetTo(10, false) 30 | if v.Test(10) { 31 | t.Errorf("Bit %d is set, and it shouldn't be.", 10) 32 | } 33 | } 34 | 35 | func TestCapabilitySetAndGet(t *testing.T) { 36 | var v Capability 37 | v.Set(10) 38 | if !v.Test(10) { 39 | t.Errorf("Bit %d is clear, and it shouldn't be.", 10) 40 | } 41 | } 42 | 43 | func TestCapabilityChain(t *testing.T) { 44 | var v Capability 45 | if !v.Set(10).Set(9).Clear(9).Test(10) { 46 | t.Errorf("Bit %d is clear, and it shouldn't be.", 10) 47 | } 48 | } 49 | 50 | func TestCapabilityFlip(t *testing.T) { 51 | var v Capability 52 | v.SetTo(10, false) 53 | v.Flip(10) 54 | if !v.Test(10) { 55 | t.Errorf("Bit %d is clear, and it shouldn't be.", 10) 56 | } 57 | v.SetTo(10, true) 58 | v.Flip(10) 59 | if v.Test(10) { 60 | t.Errorf("Bit %d is set, and it shouldn't be.", 10) 61 | } 62 | } 63 | 64 | func TestCommonCaps(t *testing.T) { 65 | var v Capability 66 | v.Set(CapabilityAuthSpice).Set(CapabilityAuthSelection).Set(CapabilityMiniHeader) 67 | if v != 0x0b { 68 | t.Errorf("wrong value") 69 | } 70 | } 71 | 72 | func TestChannelCaps(t *testing.T) { 73 | var v Capability 74 | v.Set(CapabilityMainSeamlessMigrate).Set(CapabilityMainSemiSeamlessMigrate) 75 | if v != 0x09 { 76 | t.Errorf("wrong value %x", v) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/acln0/zerocopy v0.0.0-20190410132315-ac749309e897 h1:YLKNvi62jcCA755rCWaeZkGOkz6x7ckZ2rCGlYRv1OY= 2 | github.com/acln0/zerocopy v0.0.0-20190410132315-ac749309e897/go.mod h1:6Y/vMw2GLvT76aiMGe6xQTsL2QSBKp3xe6j+I+YE4kE= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 7 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 11 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 14 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 15 | golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 17 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 21 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package spice 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func TestLoggerAdapt(t *testing.T) { 13 | adapter := cleanLogger(t) 14 | 15 | msg := "myInfoMessageString" 16 | adapter.Info(msg) 17 | 18 | if !strings.Contains(adapter.String(), "msg="+msg) || !strings.Contains(adapter.String(), "level=info") { 19 | t.Error("did not find string") 20 | } 21 | } 22 | 23 | func TestLoggerWithError(t *testing.T) { 24 | adapter := cleanLogger(t) 25 | 26 | msg := "myErrorMessageString" 27 | error := errors.New(msg) 28 | adapter.WithError(error).Info() 29 | 30 | if !strings.Contains(adapter.String(), "error="+msg) { 31 | t.Error("did not find short error message") 32 | } 33 | 34 | adapter = cleanLogger(t) 35 | msg = "myErrorMessageString longer" 36 | error = errors.New(msg) 37 | adapter.WithError(error).Info() 38 | 39 | if !strings.Contains(adapter.String(), "error=\""+msg+"\"") { 40 | t.Error("did not find long error message") 41 | } 42 | } 43 | 44 | func TestLoggerWithField(t *testing.T) { 45 | adapter := cleanLogger(t) 46 | 47 | field := "myField" 48 | msg := "myFieldMessageString" 49 | adapter.WithFields(field, msg).Info() 50 | 51 | if !strings.Contains(adapter.String(), field+"="+msg) { 52 | t.Error("did not find short field message") 53 | } 54 | 55 | adapter = cleanLogger(t) 56 | field = "myField" 57 | msg = "myFieldMessageString longer" 58 | adapter.WithFields(field, msg).Info() 59 | 60 | if !strings.Contains(adapter.String(), field+"=\""+msg+"\"") { 61 | t.Error("did not find long field message") 62 | } 63 | } 64 | 65 | func cleanLogger(t *testing.T) *clean { 66 | t.Helper() 67 | 68 | var buf bytes.Buffer 69 | logger := logrus.New() 70 | logger.Out = &buf 71 | 72 | adapter := &clean{ 73 | Adapt(logger.WithField("logger", "clean")), 74 | &buf, 75 | } 76 | return adapter 77 | } 78 | 79 | type clean struct { 80 | Logger 81 | buf *bytes.Buffer 82 | } 83 | 84 | func (c *clean) String() string { 85 | return c.buf.String() 86 | } 87 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package spice 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // Option is a functional option handler for Server. 11 | type Option func(*Proxy) error 12 | 13 | // SetOption runs a functional option against the server. 14 | func (p *Proxy) SetOption(option Option) error { 15 | return option(p) 16 | } 17 | 18 | // WithLogger can be used to provide a custom logger. 19 | // Defaults to a logrus implementation. 20 | func WithLogger(log Logger) Option { 21 | return func(p *Proxy) error { 22 | p.log = log 23 | return nil 24 | } 25 | } 26 | 27 | // WithAuthenticator can be provided to implement custom authentication 28 | // By default, "auth-less" no-op mode is enabled. 29 | func WithAuthenticator(a Authenticator) Option { 30 | return func(p *Proxy) error { 31 | if err := a.Init(); err != nil { 32 | return err 33 | } 34 | p.authenticator[a.Method()] = a 35 | return nil 36 | } 37 | } 38 | 39 | // WithDialer can be used to provide a custom dialer to reach compute nodes 40 | // the network is always of type 'tcp' and the computeAddress is the compute node 41 | // computeAddress that is return by an Authenticator. 42 | func WithDialer(dial func(ctx context.Context, network, addr string) (net.Conn, error)) Option { 43 | return func(p *Proxy) error { 44 | p.dial = dial 45 | return nil 46 | } 47 | } 48 | 49 | func defaultDialer() func(context.Context, string, string) (net.Conn, error) { 50 | dialer := &net.Dialer{} 51 | 52 | return dialer.DialContext 53 | } 54 | 55 | func defaultLogger() Logger { 56 | return Adapt(logrus.New().WithField("app", "spiceProxy")) 57 | } 58 | 59 | // WithConnectionCloseHandler is called when the main channel of a SPICE session is closed. 60 | // The "destination" parameter contains the compute node address returned by "resolveComputeAddress". 61 | // WithConnectionCloseHandler can be used to clean up after a SPICE connection was closed 62 | func WithConnectionCloseHandler(closeCallback func(destination string) error) Option { 63 | return func(p *Proxy) error { 64 | p.closeCallback = closeCallback 65 | return nil 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /sessions.go: -------------------------------------------------------------------------------- 1 | package spice 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // sessionTable holds a mapping of SessionID and destination node computeAddress 9 | // map[sessionid]computeAddress 10 | type sessionTable struct { 11 | lock sync.Mutex 12 | entries map[uint32]*sessionEntry 13 | } 14 | 15 | type sessionEntry struct { 16 | computeAddress string 17 | usageCount int 18 | authToken string 19 | } 20 | 21 | func newSessionTable() *sessionTable { 22 | return &sessionTable{ 23 | entries: make(map[uint32]*sessionEntry), 24 | } 25 | } 26 | 27 | func (s *sessionTable) Lookup(session uint32) bool { 28 | s.lock.Lock() 29 | defer s.lock.Unlock() 30 | _, ok := s.entries[session] 31 | return ok 32 | } 33 | 34 | func (s *sessionTable) OTP(session uint32) string { 35 | s.lock.Lock() 36 | defer s.lock.Unlock() 37 | if _, ok := s.entries[session]; ok { 38 | return s.entries[session].authToken 39 | } 40 | return "" 41 | } 42 | 43 | func (s *sessionTable) Compute(session uint32) string { 44 | s.lock.Lock() 45 | defer s.lock.Unlock() 46 | if _, ok := s.entries[session]; ok { 47 | return s.entries[session].computeAddress 48 | } 49 | return "" 50 | } 51 | 52 | func (s *sessionTable) Add(session uint32, destination string, otp string) { 53 | s.lock.Lock() 54 | defer s.lock.Unlock() 55 | if _, ok := s.entries[session]; !ok { 56 | s.entries[session] = &sessionEntry{computeAddress: destination, usageCount: 1, authToken: otp} 57 | } 58 | return 59 | } 60 | 61 | func (s *sessionTable) Connect(session uint32) (string, error) { 62 | s.lock.Lock() 63 | defer s.lock.Unlock() 64 | if _, ok := s.entries[session]; !ok { 65 | return "", fmt.Errorf("no such session in table") 66 | } 67 | s.entries[session].usageCount = s.entries[session].usageCount + 1 68 | return s.entries[session].computeAddress, nil 69 | } 70 | 71 | func (s *sessionTable) Disconnect(session uint32) { 72 | s.lock.Lock() 73 | defer s.lock.Unlock() 74 | if _, ok := s.entries[session]; !ok { 75 | return 76 | } 77 | s.entries[session].usageCount = s.entries[session].usageCount - 1 78 | if s.entries[session].usageCount < 1 { 79 | delete(s.entries, session) 80 | } 81 | return 82 | } 83 | -------------------------------------------------------------------------------- /red/serverticket_test.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestServerTicket_UnmarshalBinary(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | b []byte 12 | st ServerTicket 13 | err error 14 | }{ 15 | { 16 | name: "empty", 17 | err: errInvalidPacket, 18 | }, 19 | { 20 | name: "short", 21 | b: fromHex("00 00 00"), 22 | err: errInvalidPacket, 23 | }, 24 | { 25 | name: "ok", 26 | st: ServerTicket{Result: ErrorOk}, 27 | b: fromHex("00 00 00 00"), 28 | }, 29 | { 30 | name: "ok permission denied", 31 | st: ServerTicket{Result: ErrorPermissionDenied}, 32 | b: fromHex("07 00 00 00"), 33 | }, 34 | } 35 | 36 | for _, testCase := range tests { 37 | t.Run(testCase.name, func(t *testing.T) { 38 | var st ServerTicket 39 | err := (&st).UnmarshalBinary(testCase.b) 40 | 41 | if want, got := testCase.err, err; want != got { 42 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) 43 | } 44 | if err != nil { 45 | return 46 | } 47 | 48 | if want, got := testCase.st, st; !reflect.DeepEqual(want, got) { 49 | t.Fatalf("unexpected Message:\n- want: %#v\n- got: %#v", want, got) 50 | } 51 | }) 52 | } 53 | } 54 | 55 | /* 56 | func TestServerTicket_MarshalBinary(t *testing.T) { 57 | tests := []struct { 58 | name string 59 | b []byte 60 | cam ClientAuthMethod 61 | err error 62 | }{ 63 | { 64 | name: "ok spice", 65 | cam: ClientAuthMethod{Method: AuthMethodSpice}, 66 | b: fromHex("01 00 00 00"), 67 | }, 68 | { 69 | name: "ok sasl", 70 | cam: ClientAuthMethod{Method: AuthMethodSASL}, 71 | b: fromHex("02 00 00 00"), 72 | }, 73 | } 74 | 75 | for _, testCase := range tests { 76 | t.Run(testCase.name, func(t *testing.T) { 77 | b, err := testCase.cam.MarshalBinary() 78 | 79 | if want, got := testCase.err, err; want != got { 80 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) 81 | } 82 | if err != nil { 83 | return 84 | } 85 | 86 | if want, got := testCase.b, b; !bytes.Equal(want, got) { 87 | t.Fatalf("unexpected Message bytes:\n- want: [%# x]\n- got: [%# x]", want, got) 88 | } 89 | }) 90 | } 91 | } 92 | */ 93 | -------------------------------------------------------------------------------- /red/fuzz.go: -------------------------------------------------------------------------------- 1 | // +build gofuzz 2 | 3 | package red 4 | 5 | func Fuzz(data []byte) int { 6 | //return fuzzClientAuthMethod(data) 7 | //return fuzzClientLinkMessage(data) 8 | //return fuzzClientTicket(data) 9 | //return fuzzLinkHeader(data) 10 | //return fuzzMiniDataHeader(data) 11 | return fuzzServerLinkMessage(data) 12 | //return fuzzServerTicket(data) 13 | } 14 | 15 | func fuzzClientAuthMethod(data []byte) int { 16 | m := &ClientAuthMethod{} 17 | if err := (m).UnmarshalBinary(data); err != nil { 18 | return 0 19 | } 20 | 21 | if _, err := m.MarshalBinary(); err != nil { 22 | panic(err) 23 | } 24 | 25 | return 1 26 | } 27 | 28 | func fuzzClientLinkMessage(data []byte) int { 29 | m := &ClientLinkMessage{} 30 | if err := (m).UnmarshalBinary(data); err != nil { 31 | return 0 32 | } 33 | 34 | if _, err := m.MarshalBinary(); err != nil { 35 | panic(err) 36 | } 37 | 38 | return 1 39 | } 40 | 41 | func fuzzClientTicket(data []byte) int { 42 | m := &ClientTicket{} 43 | if err := (m).UnmarshalBinary(data); err != nil { 44 | return 0 45 | } 46 | 47 | if _, err := m.MarshalBinary(); err != nil { 48 | panic(err) 49 | } 50 | 51 | return 1 52 | } 53 | 54 | func fuzzLinkHeader(data []byte) int { 55 | m := &LinkHeader{} 56 | if err := (m).UnmarshalBinary(data); err != nil { 57 | return 0 58 | } 59 | 60 | if _, err := m.MarshalBinary(); err != nil { 61 | panic(err) 62 | } 63 | 64 | return 1 65 | } 66 | 67 | func fuzzMiniDataHeader(data []byte) int { 68 | m := &MiniDataHeader{} 69 | if err := (m).UnmarshalBinary(data); err != nil { 70 | return 0 71 | } 72 | 73 | if _, err := m.MarshalBinary(); err != nil { 74 | panic(err) 75 | } 76 | 77 | return 1 78 | } 79 | 80 | func fuzzServerLinkMessage(data []byte) int { 81 | m := &ServerLinkMessage{} 82 | if err := (m).UnmarshalBinary(data); err != nil { 83 | return 0 84 | } 85 | 86 | if _, err := m.MarshalBinary(); err != nil { 87 | panic(err) 88 | } 89 | 90 | return 1 91 | } 92 | 93 | func fuzzServerTicket(data []byte) int { 94 | m := &ServerTicket{} 95 | if err := (m).UnmarshalBinary(data); err != nil { 96 | return 0 97 | } 98 | 99 | if _, err := m.MarshalBinary(); err != nil { 100 | panic(err) 101 | } 102 | 103 | return 1 104 | } 105 | -------------------------------------------------------------------------------- /red/clientauthmethod_test.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestClientAuthMethodSelect_UnmarshalBinary(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | b []byte 13 | cam ClientAuthMethod 14 | err error 15 | }{ 16 | { 17 | name: "empty", 18 | err: errInvalidPacket, 19 | }, 20 | { 21 | name: "short", 22 | b: fromHex("00 00 00"), 23 | err: errInvalidPacket, 24 | }, 25 | { 26 | name: "bad method", 27 | b: fromHex("00 00 00 00"), 28 | err: errInvalidPacket, 29 | }, 30 | { 31 | name: "ok spice", 32 | cam: ClientAuthMethod{Method: AuthMethodSpice}, 33 | b: fromHex("01 00 00 00"), 34 | }, 35 | { 36 | name: "ok sasl", 37 | cam: ClientAuthMethod{Method: AuthMethodSASL}, 38 | b: fromHex("02 00 00 00"), 39 | }, 40 | } 41 | 42 | for _, testCase := range tests { 43 | t.Run(testCase.name, func(t *testing.T) { 44 | var cam ClientAuthMethod 45 | err := (&cam).UnmarshalBinary(testCase.b) 46 | 47 | if want, got := testCase.err, err; want != got { 48 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) 49 | } 50 | if err != nil { 51 | return 52 | } 53 | 54 | if want, got := testCase.cam, cam; !reflect.DeepEqual(want, got) { 55 | t.Fatalf("unexpected Message:\n- want: %#v\n- got: %#v", want, got) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestClientAuthMethodSelect_MarshalBinary(t *testing.T) { 62 | tests := []struct { 63 | name string 64 | b []byte 65 | cam ClientAuthMethod 66 | err error 67 | }{ 68 | { 69 | name: "ok spice", 70 | cam: ClientAuthMethod{Method: AuthMethodSpice}, 71 | b: fromHex("01 00 00 00"), 72 | }, 73 | { 74 | name: "ok sasl", 75 | cam: ClientAuthMethod{Method: AuthMethodSASL}, 76 | b: fromHex("02 00 00 00"), 77 | }, 78 | } 79 | 80 | for _, testCase := range tests { 81 | t.Run(testCase.name, func(t *testing.T) { 82 | b, err := testCase.cam.MarshalBinary() 83 | 84 | if want, got := testCase.err, err; want != got { 85 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) 86 | } 87 | if err != nil { 88 | return 89 | } 90 | 91 | if want, got := testCase.b, b; !bytes.Equal(want, got) { 92 | t.Fatalf("unexpected Message bytes:\n- want: [%# x]\n- got: [%# x]", want, got) 93 | } 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /red/clientticket_test.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestClientTicket_UnmarshalBinary(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | b []byte 12 | ct ClientTicket 13 | err error 14 | }{ 15 | { 16 | name: "empty", 17 | err: errInvalidPacket, 18 | }, 19 | { 20 | name: "short", 21 | b: make([]byte, 127), 22 | err: errInvalidPacket, 23 | }, 24 | /* 25 | { 26 | name: "ok", 27 | ct: ClientTicket{}, 28 | b: fromHex("61 9a 3d 63 6b e2 b2 b8 4e 94 cb 16 10 b8 ca e0 10 90 a3 01 98 3f b0 fe 8b 3f 6f ca 81 43 15 e8 5b 83 55 4d ae 51 5e ed d7 44 b8 e1 74 25 e6 f7 ba ff 8e fa f9 74 f1 76 a2 9b ea cc 1b 9d b4 3d d7 57 b5 79 11 41 7d fc f6 06 80 0c bb 2e 7f 98 22 46 59 b5 b2 df f8 b7 a3 ad 2a 5b 39 61 24 20 27 d6 17 f8 da a2 e7 f6 ab 1a bc 62 32 e4 ce 5f 91 1b f7 19 0a c1 b9 89 77 7f 01 f1 7d c1 88 54"), 29 | }, 30 | */ 31 | } 32 | 33 | for _, testCase := range tests { 34 | t.Run(testCase.name, func(t *testing.T) { 35 | var ct ClientTicket 36 | err := (&ct).UnmarshalBinary(testCase.b) 37 | 38 | if want, got := testCase.err, err; want != got { 39 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) 40 | } 41 | if err != nil { 42 | return 43 | } 44 | 45 | if want, got := testCase.ct, ct; !reflect.DeepEqual(want, got) { 46 | t.Fatalf("unexpected Message:\n- want: %#v\n- got: %#v", want, got) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | /* 53 | func TestClientTicket_MarshalBinary(t *testing.T) { 54 | tests := []struct { 55 | name string 56 | b []byte 57 | cam ClientAuthMethod 58 | err error 59 | }{ 60 | { 61 | name: "ok spice", 62 | cam: ClientAuthMethod{Method: AuthMethodSpice}, 63 | b: fromHex("01 00 00 00"), 64 | }, 65 | { 66 | name: "ok sasl", 67 | cam: ClientAuthMethod{Method: AuthMethodSASL}, 68 | b: fromHex("02 00 00 00"), 69 | }, 70 | } 71 | 72 | for _, testCase := range tests { 73 | t.Run(testCase.name, func(t *testing.T) { 74 | b, err := testCase.cam.MarshalBinary() 75 | 76 | if want, got := testCase.err, err; want != got { 77 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) 78 | } 79 | if err != nil { 80 | return 81 | } 82 | 83 | if want, got := testCase.b, b; !bytes.Equal(want, got) { 84 | t.Fatalf("unexpected Message bytes:\n- want: [%# x]\n- got: [%# x]", want, got) 85 | } 86 | }) 87 | } 88 | } 89 | */ 90 | -------------------------------------------------------------------------------- /red/capability.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | // Common capabilities 4 | const ( 5 | CapabilityAuthSelection uint32 = 0 6 | CapabilityAuthSpice uint32 = 1 7 | CapabilityAuthSASL uint32 = 2 8 | CapabilityMiniHeader uint32 = 3 9 | ) 10 | 11 | // Main Channel capabilities 12 | const ( 13 | CapabilityMainSemiSeamlessMigrate uint32 = 0 14 | CapabilityMainNameAndUUID uint32 = 1 15 | CapabilityMainAgentConnectedTokens uint32 = 2 16 | CapabilityMainSeamlessMigrate uint32 = 3 17 | ) 18 | 19 | // Playback Channel capabilities 20 | const ( 21 | CapabilityPlaybackCELT051 uint32 = 0 22 | CapabilityPlaybackVolume uint32 = 1 23 | CapabilityPlaybackLatency uint32 = 2 24 | CapabilityPlaybackOpus uint32 = 3 25 | ) 26 | 27 | // Record Channel capabilities 28 | const ( 29 | CapabilityRecordCELT051 uint32 = 0 30 | CapabilityRecordVolume uint32 = 1 31 | CapabilityRecordOpus uint32 = 2 32 | ) 33 | 34 | // Display Channel capabilities 35 | const ( 36 | CapabilityDisplaySizedStream uint32 = 0 37 | CapabilityDisplayMonitorsConfig uint32 = 1 38 | CapabilityDisplayComposite uint32 = 2 39 | CapabilityDisplayA8Surface uint32 = 3 40 | CapabilityDisplayStreamReport uint32 = 4 41 | CapabilityDisplayLZ4Compression uint32 = 5 42 | CapabilityDisplayPREFCompression uint32 = 6 43 | CapabilityDisplayGLScanout uint32 = 7 44 | CapabilityDisplayMMultiCodec uint32 = 8 45 | CapabilityDisplayCodecMJPEG uint32 = 9 46 | CapabilityDisplayCodecVP8 uint32 = 10 47 | CapabilityDisplayCodecH264 uint32 = 11 48 | ) 49 | 50 | // Input Channel capabilities 51 | const ( 52 | CapabilityInputKeyScancode uint32 = 0 53 | ) 54 | 55 | // Capability is a bitwise capability set 56 | type Capability uint32 57 | 58 | // Test whether bit i is set. 59 | func (c *Capability) Test(i uint32) bool { 60 | if i >= 32 { 61 | return false 62 | } 63 | return *c&(1< 0 64 | } 65 | 66 | // Set bit i to 1 67 | func (c *Capability) Set(i uint32) *Capability { 68 | if i >= 32 { 69 | return c 70 | } 71 | *c |= 1 << i 72 | return c 73 | } 74 | 75 | // Clear bit i to 0 76 | func (c *Capability) Clear(i uint32) *Capability { 77 | if i >= 32 { 78 | return c 79 | } 80 | *c &^= 1 << i 81 | return c 82 | } 83 | 84 | // SetTo sets bit i to value 85 | func (c *Capability) SetTo(i uint32, value bool) *Capability { 86 | if value { 87 | return c.Set(i) 88 | } 89 | return c.Clear(i) 90 | } 91 | 92 | // Flip bit at i 93 | func (c *Capability) Flip(i uint32) *Capability { 94 | if i >= 32 { 95 | return c 96 | } 97 | *c ^= 1 << i 98 | return c 99 | } 100 | -------------------------------------------------------------------------------- /red/red.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import ( 4 | "encoding" 5 | "errors" 6 | ) 7 | 8 | // Various errors which may occur when attempting to marshal or unmarshal 9 | // a SpicePacket to and from its binary form. 10 | var ( 11 | errInvalidPacket = errors.New("invalid Spice packet") 12 | errInvalidVersion = errors.New("invalid version") 13 | ) 14 | 15 | // SpicePacket is the interface used for passing around different kinds of packets. 16 | type SpicePacket interface { 17 | encoding.BinaryMarshaler 18 | encoding.BinaryUnmarshaler 19 | validate() error 20 | finish() 21 | } 22 | 23 | // Magic is the spice RED protocol magic bytes 24 | var Magic = [4]uint8{0x52, 0x45, 0x44, 0x51} 25 | 26 | const ( 27 | // VersionMajor is the major version of the supported protocol 28 | VersionMajor uint32 = 2 29 | // VersionMinor is the minor version of the supported protocol 30 | VersionMinor uint32 = 2 31 | ) 32 | 33 | //go:generate stringer -type=AuthMethod 34 | 35 | // AuthMethod is the method used for authentication 36 | type AuthMethod uint32 37 | 38 | const ( 39 | // AuthMethodSpice is the spice token based authentication method 40 | AuthMethodSpice AuthMethod = 1 41 | // AuthMethodSASL is the SASL authentication method 42 | AuthMethodSASL AuthMethod = 2 43 | ) 44 | 45 | //go:generate stringer -type=ChannelType 46 | 47 | // ChannelType is the packet channel type 48 | type ChannelType uint8 49 | 50 | // Channel types 51 | const ( 52 | ChannelMain ChannelType = 1 53 | ChannelDisplay ChannelType = 2 54 | ChannelInputs ChannelType = 3 55 | ChannelCursor ChannelType = 4 56 | ChannelPlayback ChannelType = 5 57 | ChannelRecord ChannelType = 6 58 | ChannelTunnel ChannelType = 7 59 | ChannelSmartcard ChannelType = 8 60 | ChannelUSBRedir ChannelType = 9 61 | ChannelPort ChannelType = 10 62 | ChannelWebdav ChannelType = 11 63 | ) 64 | 65 | //go:generate stringer -type=ErrorCode 66 | 67 | // ErrorCode return on error 68 | type ErrorCode uint32 69 | 70 | // Error codes 71 | const ( 72 | ErrorOk ErrorCode = 0 73 | ErrorError ErrorCode = 1 74 | ErrorInvalidMagic ErrorCode = 2 75 | ErrorInvalidData ErrorCode = 3 76 | ErrorVersionMismatch ErrorCode = 4 77 | ErrorNeedSecured ErrorCode = 5 78 | ErrorNeedUnsecured ErrorCode = 6 79 | ErrorPermissionDenied ErrorCode = 7 80 | ErrorBadConnectionID ErrorCode = 8 81 | ErrorChannelNotAvailable ErrorCode = 9 82 | ) 83 | 84 | // TicketPubkeyBytes is the length of a ticket RSA public key 85 | const TicketPubkeyBytes = 162 86 | 87 | // ClientTicketBytes is the length of an encrypted Spice token 88 | const ClientTicketBytes = 128 89 | 90 | // PubKey is a red ticket public key 91 | type PubKey [TicketPubkeyBytes]byte 92 | -------------------------------------------------------------------------------- /red/linkheader_test.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func fromHex(str string) []byte { 12 | var b []byte 13 | split := strings.Split(str, " ") 14 | for _, char := range split { 15 | c, _ := strconv.ParseUint(char, 16, 8) 16 | b = append(b, uint8(c)) 17 | } 18 | return b 19 | } 20 | 21 | func TestLinkHeader_MarshalBinary(t *testing.T) { 22 | tests := []struct { 23 | name string 24 | hdr LinkHeader 25 | b []byte 26 | err error 27 | }{ 28 | { 29 | name: "empty", 30 | hdr: LinkHeader{}, 31 | b: fromHex("52 45 44 51 02 00 00 00 02 00 00 00 00 00 00 00"), 32 | }, 33 | { 34 | name: "len 26", 35 | hdr: LinkHeader{Size: 26}, 36 | b: fromHex("52 45 44 51 02 00 00 00 02 00 00 00 1a 00 00 00"), 37 | }, 38 | } 39 | 40 | for _, testCase := range tests { 41 | t.Run(testCase.name, func(t *testing.T) { 42 | b, err := testCase.hdr.MarshalBinary() 43 | 44 | if want, got := testCase.err, err; want != got { 45 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) 46 | } 47 | if err != nil { 48 | return 49 | } 50 | 51 | if want, got := testCase.b, b; !bytes.Equal(want, got) { 52 | t.Fatalf("unexpected Message bytes:\n- want: [%# x]\n- got: [%# x]", want, got) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | func TestLinkHeader_UnmarshalBinary(t *testing.T) { 59 | tests := []struct { 60 | name string 61 | b []byte 62 | hdr LinkHeader 63 | err error 64 | }{ 65 | { 66 | name: "empty", 67 | err: errInvalidPacket, 68 | }, 69 | { 70 | name: "short", 71 | b: fromHex("52 45 44 51 00 00 00 00 02 00 00 00 1a 00 00"), 72 | err: errInvalidPacket, 73 | }, 74 | { 75 | name: "bad major version", 76 | err: errInvalidVersion, 77 | b: fromHex("52 45 44 51 00 00 00 00 02 00 00 00 1a 00 00 00"), 78 | }, 79 | { 80 | name: "bad minor version", 81 | err: errInvalidVersion, 82 | b: fromHex("52 45 44 51 02 00 00 00 01 00 00 00 1a 00 00 00"), 83 | }, 84 | { 85 | name: "size 26", 86 | hdr: LinkHeader{ 87 | Magic: Magic, 88 | Major: VersionMajor, 89 | Minor: VersionMinor, 90 | Size: 26, 91 | }, 92 | b: fromHex("52 45 44 51 02 00 00 00 02 00 00 00 1a 00 00 00"), 93 | }, 94 | } 95 | 96 | for _, testCase := range tests { 97 | t.Run(testCase.name, func(t *testing.T) { 98 | var hdr LinkHeader 99 | err := (&hdr).UnmarshalBinary(testCase.b) 100 | 101 | if want, got := testCase.err, err; want != got { 102 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) 103 | } 104 | if err != nil { 105 | return 106 | } 107 | 108 | if want, got := testCase.hdr, hdr; !reflect.DeepEqual(want, got) { 109 | t.Fatalf("unexpected Message:\n- want: %#v\n- got: %#v", want, got) 110 | } 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /red/clientlinkmessage_test.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestClientLinkMessage_UnmarshalBinary(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | b []byte 13 | clm ClientLinkMessage 14 | err error 15 | }{ 16 | { 17 | name: "no caps", 18 | clm: ClientLinkMessage{ 19 | SessionID: 0, 20 | ChannelType: 1, 21 | ChannelID: 0, 22 | CommonCaps: 0, 23 | ChannelCaps: 0, 24 | CapsOffset: 18, 25 | CommonCapabilities: nil, 26 | ChannelCapabilities: nil, 27 | }, 28 | b: fromHex("00 00 00 00 01 00 00 00 00 00 00 00 00 00 12 00 00 00"), 29 | }, 30 | { 31 | name: "ok", 32 | clm: ClientLinkMessage{ 33 | SessionID: 0, 34 | ChannelType: 1, 35 | ChannelID: 0, 36 | CommonCaps: 1, 37 | ChannelCaps: 1, 38 | CapsOffset: 18, 39 | CommonCapabilities: []Capability{0x0d}, 40 | ChannelCapabilities: []Capability{0x0f}, 41 | }, 42 | b: fromHex("00 00 00 00 01 00 01 00 00 00 01 00 00 00 12 00 00 00 0d 00 00 00 0f 00 00 00"), 43 | }, 44 | } 45 | 46 | for _, testCase := range tests { 47 | t.Run(testCase.name, func(t *testing.T) { 48 | var clm ClientLinkMessage 49 | err := (&clm).UnmarshalBinary(testCase.b) 50 | 51 | if want, got := testCase.err, err; want != got { 52 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) 53 | } 54 | if err != nil { 55 | return 56 | } 57 | 58 | if want, got := testCase.clm, clm; !reflect.DeepEqual(want, got) { 59 | t.Fatalf("unexpected Message:\n- want: %#v\n- got: %#v", want, got) 60 | } 61 | }) 62 | } 63 | } 64 | 65 | func TestClientLinkMessage_MarshalBinary(t *testing.T) { 66 | tests := []struct { 67 | name string 68 | b []byte 69 | clm ClientLinkMessage 70 | err error 71 | }{ 72 | { 73 | name: "no caps", 74 | clm: ClientLinkMessage{ 75 | SessionID: 0, 76 | ChannelType: 1, 77 | ChannelID: 0, 78 | CommonCaps: 0, 79 | ChannelCaps: 0, 80 | CapsOffset: 18, 81 | CommonCapabilities: nil, 82 | ChannelCapabilities: nil, 83 | }, 84 | b: fromHex("00 00 00 00 01 00 00 00 00 00 00 00 00 00 12 00 00 00"), 85 | }, 86 | { 87 | name: "ok", 88 | clm: ClientLinkMessage{ 89 | SessionID: 0, 90 | ChannelType: 1, 91 | ChannelID: 0, 92 | CommonCaps: 1, 93 | ChannelCaps: 1, 94 | CapsOffset: 18, 95 | CommonCapabilities: []Capability{0x0d}, 96 | ChannelCapabilities: []Capability{0x0f}, 97 | }, 98 | b: fromHex("00 00 00 00 01 00 01 00 00 00 01 00 00 00 12 00 00 00 0d 00 00 00 0f 00 00 00"), 99 | }, 100 | } 101 | 102 | for _, testCase := range tests { 103 | t.Run(testCase.name, func(t *testing.T) { 104 | b, err := testCase.clm.MarshalBinary() 105 | 106 | if want, got := testCase.err, err; want != got { 107 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) 108 | } 109 | if err != nil { 110 | return 111 | } 112 | 113 | if want, got := testCase.b, b; !bytes.Equal(want, got) { 114 | t.Fatalf("unexpected Message bytes:\n- want: [%# x]\n- got: [%# x]", want, got) 115 | } 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package spice 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/jsimonetti/go-spice/red" 9 | ) 10 | 11 | // Proxy is the server object for this spice proxy. 12 | type Proxy struct { 13 | // WithAuthenticator can be provided to implement custom authentication 14 | // By default, "auth-less" no-op mode is enabled. 15 | authenticator map[red.AuthMethod]Authenticator 16 | 17 | // WithLogger can be used to provide a custom logger. 18 | // Defaults to a logrus implementation. 19 | log Logger 20 | 21 | // WithDialer can be used to provide a custom dialer to reach compute nodes 22 | // the network is always of type 'tcp' and the computeAddress is the compute node 23 | // computeAddress that is return by an Authenticator. 24 | dial func(ctx context.Context, network, addr string) (net.Conn, error) 25 | 26 | // sessionTable holds all the sessions for this proxy 27 | sessionTable *sessionTable 28 | 29 | // optional function called when main channel is closed 30 | closeCallback func(destination string) error 31 | } 32 | 33 | // New returns a new *Proxy with the options applied 34 | func New(options ...Option) (*Proxy, error) { 35 | proxy := &Proxy{} 36 | proxy.authenticator = make(map[red.AuthMethod]Authenticator) 37 | 38 | for _, option := range options { 39 | if err := proxy.SetOption(option); err != nil { 40 | return nil, fmt.Errorf("could not set option: %v", err) 41 | } 42 | } 43 | 44 | if len(proxy.authenticator) < 1 { 45 | proxy.authenticator[red.AuthMethodSpice] = &noopAuth{} 46 | } 47 | 48 | if proxy.log == nil { 49 | proxy.log = defaultLogger() 50 | } 51 | 52 | if proxy.dial == nil { 53 | proxy.dial = defaultDialer() 54 | } 55 | 56 | proxy.sessionTable = newSessionTable() 57 | 58 | return proxy, nil 59 | } 60 | 61 | // ListenAndServe is used to create a listener and serve on it 62 | func (p *Proxy) ListenAndServe(network, addr string) error { 63 | l, err := net.Listen(network, addr) 64 | if err != nil { 65 | return err 66 | } 67 | p.log.Debug(fmt.Sprintf("listening on %s", l.Addr().String())) 68 | return p.Serve(l) 69 | } 70 | 71 | // Serve is used to serve connections from a listener 72 | func (p *Proxy) Serve(l net.Listener) error { 73 | for { 74 | tenant, err := l.Accept() 75 | if err != nil { 76 | return err 77 | } 78 | p.log.WithFields("tenant", tenant.RemoteAddr().String()).Debug("accepted connection") 79 | go p.ServeConn(tenant) 80 | } 81 | } 82 | 83 | // ServeConn is used to serve a single connection. 84 | func (p *Proxy) ServeConn(tenant net.Conn) error { 85 | defer tenant.Close() 86 | 87 | handShake, err := newTenantHandshake(p, p.log.WithFields("tenant", tenant.RemoteAddr().String())) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | var compute net.Conn 93 | 94 | handShake.log.Debug("starting handshake") 95 | for !handShake.Done() { 96 | if compute, err = handShake.clientLinkStage(tenant); err != nil { 97 | handShake.log.WithError(err).Info("handshake failed") 98 | return err 99 | } 100 | } 101 | 102 | handShake.log.Info("connection established") 103 | 104 | flow := newFlow(tenant, compute) 105 | if err := flow.Pipe(); err != nil { 106 | handShake.log.WithError(err).Error("close error") 107 | } 108 | 109 | // if connection was closed and it's the main channel, call the closeCallback 110 | if handShake.channelType == red.ChannelMain && p.closeCallback != nil { 111 | handShake.log.Info("clossing connection of main channel") 112 | if err := p.closeCallback(handShake.destination); err != nil { 113 | handShake.log.WithError(err).Error("error in connection closing callback") 114 | } 115 | } 116 | 117 | handShake.log.Info("connection closed") 118 | p.sessionTable.Disconnect(handShake.sessionID) 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /red/clientlinkmessage.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import "encoding/binary" 4 | 5 | // ClientLinkMessage is a spice packet send by the client 6 | // to start a connection. 7 | type ClientLinkMessage struct { 8 | // SessionID In case of a new session (i.e., channel type is 9 | // ChannelMain) this field is set to zero, and in response the server will 10 | // allocate session id and will send it via the RedLinkReply message. In case of all other 11 | // channel types, this field will be equal to the allocated session id. 12 | SessionID uint32 13 | 14 | // ChannelType is one of RED_CHANNEL_? 15 | ChannelType ChannelType 16 | 17 | // ChannelID to connect to. This enables having multiple channels of the same type 18 | ChannelID uint8 19 | 20 | // CommonCaps is the number of common client channel capabilities words 21 | CommonCaps uint32 22 | 23 | // ChannelCaps is the number of specific client channel capabilities words 24 | ChannelCaps uint32 25 | 26 | // CapsOffset is the location of the start of the capabilities vector given by the 27 | // bytes offset from the “ size” member (i.e., from the address of the “connection_id” 28 | // member). 29 | CapsOffset uint32 30 | 31 | // Capabilities hold the variable length capabilities 32 | CommonCapabilities []Capability 33 | ChannelCapabilities []Capability 34 | } 35 | 36 | // MarshalBinary marshals a Packet into a byte slice. 37 | func (p *ClientLinkMessage) MarshalBinary() ([]byte, error) { 38 | p.finish() 39 | 40 | b := make([]byte, int(p.CapsOffset)+4*len(p.CommonCapabilities)+4*len(p.ChannelCapabilities)) 41 | binary.LittleEndian.PutUint32(b[0:4], uint32(p.SessionID)) 42 | b[4] = uint8(p.ChannelType) 43 | b[5] = p.ChannelID 44 | binary.LittleEndian.PutUint32(b[6:10], p.CommonCaps) 45 | binary.LittleEndian.PutUint32(b[10:14], p.ChannelCaps) 46 | binary.LittleEndian.PutUint32(b[14:18], p.CapsOffset) 47 | 48 | offset := 18 49 | for i := 0; i < len(p.CommonCapabilities); i += 4 { 50 | binary.LittleEndian.PutUint32(b[i+offset:i+offset+4], uint32(p.CommonCapabilities[i])) 51 | } 52 | 53 | offset = 18 + 4*len(p.CommonCapabilities) 54 | for i := 0; i < len(p.ChannelCapabilities); i += 4 { 55 | binary.LittleEndian.PutUint32(b[i+offset:i+offset+4], uint32(p.ChannelCapabilities[i])) 56 | } 57 | 58 | return b, nil 59 | } 60 | 61 | // UnmarshalBinary unmarshals the contents of a byte slice into a Packet. 62 | func (p *ClientLinkMessage) UnmarshalBinary(b []byte) error { 63 | if len(b) < 18 { 64 | return errInvalidPacket 65 | } 66 | 67 | p.SessionID = binary.LittleEndian.Uint32(b[0:4]) 68 | p.ChannelType = ChannelType(b[4]) 69 | p.ChannelID = b[5] 70 | p.CommonCaps = binary.LittleEndian.Uint32(b[6:10]) 71 | p.ChannelCaps = binary.LittleEndian.Uint32(b[10:14]) 72 | p.CapsOffset = binary.LittleEndian.Uint32(b[14:18]) 73 | 74 | if len(b) < 18+int(p.CommonCaps)*4+int(p.ChannelCaps)*4 { 75 | return errInvalidPacket 76 | } 77 | 78 | for i := 18; i < 18+int(p.CommonCaps)*4; i += 4 { 79 | if len(b) < i+4 { 80 | return errInvalidPacket 81 | } 82 | p.CommonCapabilities = append(p.CommonCapabilities, Capability(binary.LittleEndian.Uint32(b[i:i+4]))) 83 | } 84 | 85 | for i := 18 + len(p.CommonCapabilities)*4; i < 18+int(p.CommonCaps)*4+int(p.ChannelCaps)*4; i += 4 { 86 | if len(b) < i+4 { 87 | return errInvalidPacket 88 | } 89 | p.ChannelCapabilities = append(p.ChannelCapabilities, Capability(binary.LittleEndian.Uint32(b[i:i+4]))) 90 | } 91 | 92 | return p.validate() 93 | } 94 | 95 | // validate is used to validate the Packet. 96 | func (p *ClientLinkMessage) validate() error { 97 | return nil 98 | } 99 | 100 | // finish is used to finish the Packet for sending. 101 | func (p *ClientLinkMessage) finish() { 102 | p.CapsOffset = 18 103 | p.CommonCaps = uint32(len(p.CommonCapabilities)) 104 | p.ChannelCaps = uint32(len(p.ChannelCapabilities)) 105 | } 106 | -------------------------------------------------------------------------------- /red/serverlinkmessage.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | // ServerLinkMessage is a spice packet send by the server in response to 9 | // a ClientLinkMessage 10 | type ServerLinkMessage struct { 11 | // Error codes (i.e., RED_ERROR_?) 12 | Error ErrorCode 13 | 14 | // PubKey is a 1024 bit RSA public key in X.509 SubjectPublicKeyInfo format 15 | PubKey [TicketPubkeyBytes]uint8 16 | 17 | // CommonCaps is the number of common client channel capabilities words 18 | CommonCaps uint32 19 | 20 | // ChannelCaps is the number of specific client channel capabilities words 21 | ChannelCaps uint32 22 | 23 | // CapsOffset is the location of the start of the capabilities vector given by the 24 | // bytes offset from the “ size” member (i.e., from the address of the “connection_id” 25 | // member). 26 | CapsOffset uint32 27 | 28 | // Capabilities hold the variable length capabilities 29 | CommonCapabilities []Capability 30 | ChannelCapabilities []Capability 31 | } 32 | 33 | // MarshalBinary marshals a Packet into a byte slice. 34 | func (p *ServerLinkMessage) MarshalBinary() ([]byte, error) { 35 | p.finish() 36 | 37 | b := make([]byte, int(p.CapsOffset)+4*len(p.CommonCapabilities)+4*len(p.ChannelCapabilities)) 38 | binary.LittleEndian.PutUint32(b[0:4], uint32(p.Error)) 39 | 40 | var buf bytes.Buffer 41 | if err := binary.Write(&buf, binary.LittleEndian, p.PubKey); err != nil { 42 | return nil, err 43 | } 44 | copy(b[4:4+TicketPubkeyBytes], buf.Bytes()) 45 | 46 | binary.LittleEndian.PutUint32(b[4+TicketPubkeyBytes:8+TicketPubkeyBytes], p.CommonCaps) 47 | binary.LittleEndian.PutUint32(b[8+TicketPubkeyBytes:12+TicketPubkeyBytes], p.ChannelCaps) 48 | binary.LittleEndian.PutUint32(b[12+TicketPubkeyBytes:16+TicketPubkeyBytes], p.CapsOffset) 49 | 50 | offset := 16 + TicketPubkeyBytes 51 | for i := 0; i < len(p.CommonCapabilities); i += 4 { 52 | binary.LittleEndian.PutUint32(b[i+offset:i+offset+4], uint32(p.CommonCapabilities[i])) 53 | } 54 | 55 | offset = 16 + TicketPubkeyBytes + 4*len(p.CommonCapabilities) 56 | for i := 0; i < len(p.ChannelCapabilities); i += 4 { 57 | binary.LittleEndian.PutUint32(b[i+offset:i+offset+4], uint32(p.ChannelCapabilities[i])) 58 | } 59 | 60 | return b, nil 61 | } 62 | 63 | // UnmarshalBinary unmarshals the contents of a byte slice into a Packet. 64 | func (p *ServerLinkMessage) UnmarshalBinary(b []byte) error { 65 | if len(b) < 178 { 66 | return errInvalidPacket 67 | } 68 | 69 | p.Error = ErrorCode(binary.LittleEndian.Uint32(b[0:4])) 70 | 71 | buf := bytes.NewReader(b[4 : 4+TicketPubkeyBytes]) 72 | if err := binary.Read(buf, binary.LittleEndian, p.PubKey[:]); err != nil { 73 | return err 74 | } 75 | 76 | p.CommonCaps = binary.LittleEndian.Uint32(b[4+TicketPubkeyBytes : 8+TicketPubkeyBytes]) 77 | p.ChannelCaps = binary.LittleEndian.Uint32(b[8+TicketPubkeyBytes : 12+TicketPubkeyBytes]) 78 | p.CapsOffset = binary.LittleEndian.Uint32(b[12+TicketPubkeyBytes : 16+TicketPubkeyBytes]) 79 | 80 | if len(b) < 178+int(p.CommonCaps)*4+int(p.ChannelCaps)*4 { 81 | return errInvalidPacket 82 | } 83 | 84 | for i := 178; i < 178+int(p.CommonCaps)*4; i += 4 { 85 | if len(b) < i+4 { 86 | return errInvalidPacket 87 | } 88 | p.CommonCapabilities = append(p.CommonCapabilities, Capability(binary.LittleEndian.Uint32(b[i:i+4]))) 89 | } 90 | 91 | for i := 178 + len(p.CommonCapabilities)*4; i < 178+int(p.CommonCaps)*4+int(p.ChannelCaps)*4; i += 4 { 92 | if len(b) < i+4 { 93 | return errInvalidPacket 94 | } 95 | p.ChannelCapabilities = append(p.ChannelCapabilities, Capability(binary.LittleEndian.Uint32(b[i:i+4]))) 96 | } 97 | 98 | return p.validate() 99 | } 100 | 101 | // validate is used to validate the Packet. 102 | func (p *ServerLinkMessage) validate() error { 103 | return nil 104 | } 105 | 106 | // finish is used to finish the Packet for sending. 107 | func (p *ServerLinkMessage) finish() { 108 | p.CapsOffset = 16 + TicketPubkeyBytes 109 | p.CommonCaps = uint32(len(p.CommonCapabilities)) 110 | p.ChannelCaps = uint32(len(p.ChannelCapabilities)) 111 | } 112 | -------------------------------------------------------------------------------- /example/proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | 8 | "github.com/jsimonetti/go-spice" 9 | "github.com/jsimonetti/go-spice/red" 10 | ) 11 | 12 | func main() { 13 | // create a new logger to be used for the proxy and the authenticator 14 | log := logrus.New() 15 | log.SetLevel(logrus.DebugLevel) 16 | 17 | // create a new instance of the sample authenticator 18 | authSpice := &AuthSpice{ 19 | log: log.WithField("component", "authenticator"), 20 | } 21 | 22 | // create the proxy using the logger and authenticator 23 | logger := spice.Adapt(log.WithField("component", "proxy")) 24 | proxy, err := spice.New(spice.WithLogger(logger), 25 | spice.WithAuthenticator(authSpice)) 26 | if err != nil { 27 | log.Fatalf("error: %s", err) 28 | } 29 | 30 | // start listening for tenant connections 31 | log.Fatal(proxy.ListenAndServe("tcp", "127.0.0.1:5900")) 32 | } 33 | 34 | // AuthSpice is an example implementation of a spice Authenticator 35 | type AuthSpice struct { 36 | log *logrus.Entry 37 | 38 | computeMap map[string]string 39 | } 40 | 41 | // Next will check the supplied token and return authorisation information 42 | func (a *AuthSpice) Next(c spice.AuthContext) (bool, string, error) { 43 | // convert the AuthContext into an AuthSpiceContext, since we do that 44 | var ctx spice.AuthSpiceContext 45 | var ok bool 46 | if ctx, ok = c.(spice.AuthSpiceContext); !ok { 47 | return false, "", fmt.Errorf("invalid auth method") 48 | } 49 | 50 | // retrieve the token sent by the tenant 51 | token, err := ctx.Token() 52 | if err != nil { 53 | return false, "", err 54 | } 55 | 56 | // is the previously saved token is set and matches the token 57 | // sent by the tenant we return the previously saved compute address 58 | if ctx.LoadToken() != "" && ctx.LoadToken() == token { 59 | a.log.Debug("LoadToken found and matches password") 60 | return true, ctx.LoadAddress(), nil 61 | } 62 | 63 | // find the compute node for this token 64 | if destination, ok := a.resolveComputeAddress(token); ok { 65 | a.log.Debugf("Ticket validated, compute node at %s", destination) 66 | // save the token and compute address into the context 67 | // so it can be saved into the session table by the proxy 68 | ctx.SaveToken(token) 69 | ctx.SaveAddress(destination) 70 | return true, ctx.LoadAddress(), nil 71 | } 72 | 73 | a.log.Warn("authentication failed") 74 | return false, "", nil 75 | } 76 | 77 | // Method returns the Spice auth method 78 | func (a *AuthSpice) Method() red.AuthMethod { 79 | return red.AuthMethodSpice 80 | } 81 | 82 | // resolveComputeAddress is a custom function that checks the token and returns 83 | // a compute node address 84 | func (a *AuthSpice) resolveComputeAddress(token string) (string, bool) { 85 | // this is just an example, lookup your token somewhere and resolve it 86 | // to a compute node. When creating your own authentication you should 87 | // probably use one-time tokens for the tenant authentication. 88 | // Using a method based on the below sequence of events: 89 | // 90 | // a) Tenant authenticates using token '123e4567:secretpw' 91 | // b) The Authenticator looks up the token '123e4567' in a shared store 92 | // (kv store or database) 93 | // c) The value of token 123e4567 is an encrypted compute 94 | // node computeAddress. 95 | // Attempt to decrypt the computeAddress using 'secretpw'. 96 | // If this results in a valid compute node computeAddress, 97 | // the user is granted access, and de compute destination 98 | // is set to the decrypted node computeAddress. In the same transaction, 99 | // a new token+secret should be generated, and the old one destroyed 100 | if compute, ok := a.computeMap[token]; ok { 101 | a.log.Warn("bogus token check and compute node") 102 | return compute, true 103 | } 104 | return "", false 105 | } 106 | 107 | // Init initialises this authenticator 108 | func (a *AuthSpice) Init() error { 109 | // fill in some compute nodes 110 | a.computeMap = map[string]string{ 111 | "test": "127.0.0.1:5901", 112 | "test2": "127.0.0.1:5902", 113 | } 114 | a.log.Debug("AuthSpice initialised") 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /proxy_test.go: -------------------------------------------------------------------------------- 1 | package spice_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | 8 | "github.com/jsimonetti/go-spice" 9 | "github.com/jsimonetti/go-spice/red" 10 | ) 11 | 12 | func ExampleProxy() { 13 | // create a new logger to be used for the proxy and the authenticator 14 | log := logrus.New() 15 | log.SetLevel(logrus.DebugLevel) 16 | 17 | // create a new instance of the sample authenticator 18 | authSpice := &AuthSpice{ 19 | log: log.WithField("component", "authenticator"), 20 | } 21 | 22 | // create the proxy using the logger and authenticator 23 | logger := spice.Adapt(log.WithField("component", "proxy")) 24 | proxy, err := spice.New(spice.WithLogger(logger), 25 | spice.WithAuthenticator(authSpice)) 26 | if err != nil { 27 | log.Fatalf("error: %s", err) 28 | } 29 | 30 | // start listening for tenant connections 31 | log.Fatal(proxy.ListenAndServe("tcp", "127.0.0.1:5900")) 32 | } 33 | 34 | // AuthSpice is an example implementation of a spice Authenticator 35 | type AuthSpice struct { 36 | log *logrus.Entry 37 | 38 | computeMap map[string]string 39 | } 40 | 41 | // Next will check the supplied token and return authorisation information 42 | func (a *AuthSpice) Next(c spice.AuthContext) (bool, string, error) { 43 | // convert the AuthContext into an AuthSpiceContext, since we do that 44 | var ctx spice.AuthSpiceContext 45 | var ok bool 46 | if ctx, ok = c.(spice.AuthSpiceContext); !ok { 47 | return false, "", fmt.Errorf("invalid auth method") 48 | } 49 | 50 | // retrieve the token sent by the tenant 51 | token, err := ctx.Token() 52 | if err != nil { 53 | return false, "", err 54 | } 55 | 56 | // is the previously saved token is set and matches the token 57 | // sent by the tenant we return the previously saved compute address 58 | if ctx.LoadToken() != "" && ctx.LoadToken() == token { 59 | a.log.Debug("LoadToken found and matches password") 60 | return true, ctx.LoadAddress(), nil 61 | } 62 | 63 | // find the compute node for this token 64 | if destination, ok := a.resolveComputeAddress(token); ok { 65 | a.log.Debugf("Ticket validated, compute node at %s", destination) 66 | // save the token and compute address into the context 67 | // so it can be saved into the session table by the proxy 68 | ctx.SaveToken(token) 69 | ctx.SaveAddress(destination) 70 | return true, ctx.LoadAddress(), nil 71 | } 72 | 73 | a.log.Warn("authentication failed") 74 | return false, "", nil 75 | } 76 | 77 | // Method returns the Spice auth method 78 | func (a *AuthSpice) Method() red.AuthMethod { 79 | return red.AuthMethodSpice 80 | } 81 | 82 | // resolveComputeAddress is a custom function that checks the token and returns 83 | // a compute node address 84 | func (a *AuthSpice) resolveComputeAddress(token string) (string, bool) { 85 | // this is just an example, lookup your token somewhere and resolve it 86 | // to a compute node. When creating your own authentication you should 87 | // probably use one-time tokens for the tenant authentication. 88 | // Using a method based on the below sequence of events: 89 | // 90 | // a) Tenant authenticates using token '123e4567:secretpw' 91 | // b) The Authenticator looks up the token '123e4567' in a shared store 92 | // (kv store or database) 93 | // c) The value of token 123e4567 is an encrypted compute 94 | // node computeAddress. 95 | // Attempt to decrypt the computeAddress using 'secretpw'. 96 | // If this results in a valid compute node computeAddress, 97 | // the user is granted access, and de compute destination 98 | // is set to the decrypted node computeAddress. In the same transaction, 99 | // a new token+secret should be generated, and the old one destroyed 100 | if compute, ok := a.computeMap[token]; ok { 101 | a.log.Warn("bogus token check and compute node") 102 | return compute, true 103 | } 104 | return "", false 105 | } 106 | 107 | // Init initialises this authenticator 108 | func (a *AuthSpice) Init() error { 109 | // fill in some compute nodes 110 | a.computeMap = map[string]string{ 111 | "test": "127.0.0.1:5901", 112 | "test2": "127.0.0.1:5902", 113 | } 114 | a.log.Debug("AuthSpice initialised") 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /tenant_test.go: -------------------------------------------------------------------------------- 1 | package spice 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "io" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/jsimonetti/go-spice/red" 11 | ) 12 | 13 | func Test_readLinkPacket(t *testing.T) { 14 | type args struct { 15 | conn io.Reader 16 | } 17 | tests := []struct { 18 | name string 19 | args args 20 | want []byte 21 | wantErr bool 22 | }{ 23 | { 24 | name: "empty", 25 | args: args{ 26 | bytes.NewBuffer(make([]byte, 0, 0)), 27 | }, 28 | wantErr: true, 29 | }, 30 | { 31 | name: "no packet", 32 | args: args{ 33 | bytes.NewBuffer([]byte{ 34 | 0x52, 0x45, 0x44, 0x51, 0x02, 0x00, 0x00, 0x00, 35 | 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 36 | }), 37 | }, 38 | wantErr: true, 39 | }, 40 | { 41 | name: "1 byte", 42 | args: args{ 43 | bytes.NewBuffer([]byte{ 44 | 0x52, 0x45, 0x44, 0x51, 0x02, 0x00, 0x00, 0x00, 45 | 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 46 | 0xaa, 47 | }), 48 | }, 49 | want: []byte{0xaa}, 50 | }, 51 | { 52 | name: "1 byte with extra", 53 | args: args{ 54 | bytes.NewBuffer([]byte{ 55 | 0x52, 0x45, 0x44, 0x51, 0x02, 0x00, 0x00, 0x00, 56 | 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 57 | 0xaa, 0xaa, 58 | }), 59 | }, 60 | want: []byte{0xaa}, 61 | }, 62 | { 63 | name: "8 bytes", 64 | args: args{ 65 | bytes.NewBuffer([]byte{ 66 | 0x52, 0x45, 0x44, 0x51, 0x02, 0x00, 0x00, 0x00, 67 | 0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 68 | 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 69 | }), 70 | }, 71 | want: []byte{0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa}, 72 | }, 73 | } 74 | 75 | for _, tt := range tests { 76 | t.Run(tt.name, func(t *testing.T) { 77 | got, err := readLinkPacket(tt.args.conn) 78 | if (err != nil) != tt.wantErr { 79 | t.Errorf("readLinkPacket() error = %v, wantErr %v", err, tt.wantErr) 80 | return 81 | } 82 | if !bytes.Equal(got, tt.want) { 83 | t.Errorf("readLinkPacket() = %v, want %v", got, tt.want) 84 | } 85 | }) 86 | } 87 | } 88 | 89 | func Test_sendServerTicket(t *testing.T) { 90 | type args struct { 91 | result red.ErrorCode 92 | } 93 | tests := []struct { 94 | name string 95 | args args 96 | wantWriter []byte 97 | wantErr bool 98 | }{ 99 | { 100 | name: "empty", 101 | args: args{}, 102 | wantWriter: []byte{0x00, 0x00, 0x00, 0x00}, 103 | }, 104 | { 105 | name: "ok", 106 | args: args{red.ErrorOk}, 107 | wantWriter: []byte{0x00, 0x00, 0x00, 0x00}, 108 | }, 109 | { 110 | name: "denied", 111 | args: args{red.ErrorPermissionDenied}, 112 | wantWriter: []byte{0x07, 0x00, 0x00, 0x00}, 113 | }, 114 | } 115 | for _, tt := range tests { 116 | t.Run(tt.name, func(t *testing.T) { 117 | writer := bytes.NewBuffer(make([]byte, 0, 0)) 118 | 119 | if err := sendServerTicket(tt.args.result, writer); (err != nil) != tt.wantErr { 120 | t.Errorf("sendServerTicket() error = %v, wantErr %v", err, tt.wantErr) 121 | return 122 | } 123 | if gotWriter := writer.Bytes(); !bytes.Equal(gotWriter, tt.wantWriter) { 124 | t.Errorf("sendServerTicket() = %+#v, want %+#v", gotWriter, tt.wantWriter) 125 | } 126 | }) 127 | } 128 | } 129 | 130 | func Test_sendServerLinkPacket(t *testing.T) { 131 | type args struct { 132 | key crypto.PublicKey 133 | } 134 | tests := []struct { 135 | name string 136 | args args 137 | wantWr string 138 | wantErr bool 139 | }{ 140 | // TODO: Add test cases. 141 | } 142 | for _, tt := range tests { 143 | t.Run(tt.name, func(t *testing.T) { 144 | wr := &bytes.Buffer{} 145 | if err := sendServerLinkPacket(wr, tt.args.key); (err != nil) != tt.wantErr { 146 | t.Errorf("sendServerLinkPacket() error = %v, wantErr %v", err, tt.wantErr) 147 | return 148 | } 149 | if gotWr := wr.String(); gotWr != tt.wantWr { 150 | t.Errorf("sendServerLinkPacket() = %v, want %v", gotWr, tt.wantWr) 151 | } 152 | }) 153 | } 154 | } 155 | 156 | func Test_redPubKey(t *testing.T) { 157 | type args struct { 158 | key crypto.PublicKey 159 | } 160 | tests := []struct { 161 | name string 162 | args args 163 | wantRet red.PubKey 164 | wantErr bool 165 | }{ 166 | // TODO: Add test cases. 167 | } 168 | for _, tt := range tests { 169 | t.Run(tt.name, func(t *testing.T) { 170 | gotRet, err := redPubKey(tt.args.key) 171 | if (err != nil) != tt.wantErr { 172 | t.Errorf("redPubKey() error = %v, wantErr %v", err, tt.wantErr) 173 | return 174 | } 175 | if !reflect.DeepEqual(gotRet, tt.wantRet) { 176 | t.Errorf("redPubKey() = %v, want %v", gotRet, tt.wantRet) 177 | } 178 | }) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /compute.go: -------------------------------------------------------------------------------- 1 | package spice 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/sha1" 9 | "crypto/x509" 10 | "encoding/binary" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "net" 15 | 16 | "github.com/jsimonetti/go-spice/red" 17 | ) 18 | 19 | type computeHandshake struct { 20 | proxy *Proxy 21 | 22 | done bool 23 | compute net.Conn 24 | tenant io.Writer 25 | 26 | channelID uint8 27 | channelType red.ChannelType 28 | sessionID uint32 29 | 30 | computePubKey red.PubKey 31 | log Logger 32 | } 33 | 34 | func (c *computeHandshake) Done() bool { 35 | return c.done 36 | } 37 | 38 | func (c *computeHandshake) clientLinkStage(destination string) error { 39 | var err error 40 | 41 | c.compute, err = c.proxy.dial(context.Background(), "tcp", destination) 42 | if err != nil { 43 | c.log.WithError(err).Error("dial compute error") 44 | return err 45 | } 46 | 47 | // handle send client LinkMessage 48 | if err := c.clientLinkMessage(c.compute); err != nil { 49 | return err 50 | } 51 | 52 | // handle send auth method 53 | if err := c.clientAuthMethod(c.compute); err != nil { 54 | return err 55 | } 56 | 57 | // Handle 3rd Client Ticket 58 | if err := c.clientTicket(c.compute); err != nil { 59 | return err 60 | } 61 | 62 | if c.channelType == red.ChannelMain { 63 | if err := c.readServerInit(c.compute, c.tenant); err != nil { 64 | return err 65 | } 66 | } 67 | 68 | c.done = true 69 | 70 | return nil 71 | } 72 | 73 | func (c *computeHandshake) readServerInit(in io.Reader, out io.Writer) error { 74 | var b []byte 75 | var mType uint16 76 | var err error 77 | 78 | if mType, b, err = readMiniHeaderPacket(in); err != nil { 79 | c.log.WithError(err).Error("read server Init") 80 | return err 81 | } 82 | 83 | if mType != 103 { // Server INIT 84 | err := errors.New("expected server INIT") 85 | c.log.WithError(err).Error("read server INIT") 86 | return err 87 | } 88 | 89 | c.sessionID = binary.LittleEndian.Uint32(b[6:10]) 90 | 91 | if _, err := out.Write(b); err != nil { 92 | c.log.WithError(err).Error("write server Init") 93 | return err 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (c *computeHandshake) clientTicket(rw io.ReadWriter) error { 100 | 101 | password := []byte{} // password for compute side 102 | 103 | // crypto/rand.Reader is a good source of entropy for randomizing the 104 | // encryption function. 105 | rng := rand.Reader 106 | 107 | key, err := x509.ParsePKIXPublicKey(c.computePubKey[:]) 108 | if err != nil { 109 | c.log.WithError(err).Error("Error parsing public key") 110 | return err 111 | } 112 | pubkey := key.(*rsa.PublicKey) 113 | 114 | ciphertext, err := rsa.EncryptOAEP(sha1.New(), rng, pubkey, password, []byte{}) 115 | if err != nil { 116 | c.log.WithError(err).Error("Error from encryption") 117 | return err 118 | } 119 | 120 | var ticket [128]byte 121 | copy(ticket[:], ciphertext[:]) 122 | 123 | myTicket := &red.ClientTicket{ 124 | Ticket: ticket, 125 | } 126 | 127 | mb, err := myTicket.MarshalBinary() 128 | if err != nil { 129 | c.log.WithError(err).Error("Error from marshalling ticket") 130 | return err 131 | } 132 | _, err = rw.Write(mb) 133 | if err != nil { 134 | c.log.WithError(err).Error("write ticket to compute error") 135 | return err 136 | } 137 | 138 | srvTicket := make([]byte, 4) 139 | _, err = rw.Read(srvTicket) 140 | if err != nil { 141 | c.log.WithError(err).Error("compute ticket read error") 142 | return err 143 | } 144 | 145 | if !bytes.Equal(srvTicket[:], []byte{0x00, 0x00, 0x00, 0x00}) { 146 | err := fmt.Errorf("compute ticket error %#v", srvTicket) 147 | c.log.WithError(err).Error("compute ticket error") 148 | return err 149 | } 150 | 151 | return nil 152 | } 153 | 154 | func (c *computeHandshake) clientAuthMethod(wr io.Writer) error { 155 | myAuthMethod := &red.ClientAuthMethod{ 156 | Method: red.AuthMethodSpice, 157 | } 158 | 159 | mb, err := myAuthMethod.MarshalBinary() 160 | if err != nil { 161 | return err 162 | } 163 | 164 | if _, err = wr.Write(mb); err != nil { 165 | c.log.WithError(err).Error("write link message to compute error") 166 | return err 167 | } 168 | 169 | return nil 170 | } 171 | 172 | func (c *computeHandshake) clientLinkMessage(rw io.ReadWriter) error { 173 | var channelCaps, commonCaps red.Capability 174 | commonCaps.Set(red.CapabilityAuthSpice).Set(red.CapabilityAuthSelection).Set(red.CapabilityMiniHeader) 175 | channelCaps.Set(red.CapabilityMainSeamlessMigrate).Set(red.CapabilityMainSemiSeamlessMigrate) 176 | 177 | myLink := &red.ClientLinkMessage{ 178 | ChannelID: c.channelID, 179 | ChannelType: c.channelType, 180 | SessionID: c.sessionID, 181 | CommonCaps: 1, 182 | ChannelCaps: 1, 183 | CapsOffset: 18, 184 | CommonCapabilities: []red.Capability{commonCaps}, 185 | ChannelCapabilities: []red.Capability{channelCaps}, 186 | } 187 | 188 | mb, err := myLink.MarshalBinary() 189 | if err != nil { 190 | return err 191 | } 192 | header := red.LinkHeader{ 193 | Size: myLink.CapsOffset + 8, 194 | } 195 | b2, err := header.MarshalBinary() 196 | if err != nil { 197 | return err 198 | } 199 | 200 | data := append(b2, mb...) 201 | 202 | if _, err = rw.Write(data); err != nil { 203 | c.log.WithError(err).Error("write link message to compute error") 204 | return err 205 | } 206 | 207 | var srvLmb []byte 208 | if srvLmb, err = readLinkPacket(rw); err != nil { 209 | c.log.WithError(err).Error("compute read serverLinkMessage error") 210 | } 211 | 212 | srvLm := &red.ServerLinkMessage{} 213 | if err := srvLm.UnmarshalBinary(srvLmb); err != nil { 214 | c.log.WithError(err).Error("serverlink unmarshal error") 215 | return err 216 | } 217 | if srvLm.Error != red.ErrorOk { 218 | err := fmt.Errorf("server connection error %#v", srvLm.Error) 219 | c.log.WithError(err).Error("server connection error") 220 | return err 221 | } 222 | 223 | c.computePubKey = srvLm.PubKey 224 | 225 | return nil 226 | } 227 | 228 | func readMiniHeaderPacket(conn io.Reader) (uint16, []byte, error) { 229 | headerBytes := make([]byte, 6) 230 | 231 | if _, err := conn.Read(headerBytes); err != nil { 232 | return 0, nil, err 233 | } 234 | 235 | header := &red.MiniDataHeader{} 236 | if err := header.UnmarshalBinary(headerBytes); err != nil { 237 | return 0, nil, err 238 | } 239 | 240 | var messageBytes []byte 241 | var n int 242 | var err error 243 | pending := int(header.Size) 244 | 245 | for pending > 0 { 246 | b := make([]byte, header.Size) 247 | if n, err = conn.Read(b); err != nil { 248 | return 0, nil, err 249 | } 250 | pending = pending - n 251 | messageBytes = append(messageBytes, b[:n]...) 252 | } 253 | 254 | totalBytes := append(headerBytes, messageBytes[:int(header.Size)]...) 255 | return header.MessageType, totalBytes, nil 256 | } 257 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package spice 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/sha1" 8 | "io" 9 | "net" 10 | "time" 11 | 12 | "github.com/jsimonetti/go-spice/red" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | // Authenticator is the interface used for creating a tenant authentication 17 | // It is used by the proxy to do two things: 18 | // 19 | // 1) authenticate the user 20 | // 2) return the compute node to forward the tenant user to 21 | // 22 | // When creating your own authentication you should probably use one-time tokens 23 | // for the tenant authentication. Using a method based on the below sequence of events: 24 | // 25 | // a) Tenant authenticates using token '123e4567:secretpw' 26 | // b) The Authenticator looks up the token '123e4567' in a shared store 27 | // (kv store or database) 28 | // c) The value of token 123e4567 is an encrypted compute node computeAddress. 29 | // Attempt to decrypt the computeAddress using 'secretpw'. If this results 30 | // in a valid compute node computeAddress, the user is granted access, and 31 | // de compute destination is set to the decrypted node computeAddress. 32 | // In the same transaction, a new token+secret should be generated, and the 33 | // old one destroyed 34 | type Authenticator interface { 35 | // Next starts the authentication procedure for the tenant connection 36 | // It should only ever return an error when there is a issue performing the 37 | // authentication. A non-existant user/token or a bad password/token is not 38 | // considered an error. 39 | // Errors result in a connection being dropped instantly, whereas 40 | // `accessGranted = false` results in the connection being dropped, after 41 | // an 'access denied' message is returned. `accessGranted = false` is also 42 | // not logged by the proxy, where an error is. 43 | Next(AuthContext) (accessGranted bool, computeDestination string, err error) 44 | 45 | // Method is used to retrieve the type of authentication this 46 | // Authenticator supports 47 | Method() red.AuthMethod 48 | 49 | // Init is called once during configuration and can be used to do any 50 | // initialisation this Authenticator might need. If an error is 51 | // returned, the Authenticator is not used. 52 | Init() error 53 | } 54 | 55 | var _ Authenticator = &noopAuth{} 56 | 57 | // noopAuth is a default no-op Authenticator that returns a static compute 58 | // entry and is always successful. 59 | type noopAuth struct{} 60 | 61 | // Next implements the Authenticator interface 62 | func (a *noopAuth) Next(ctx AuthContext) (bool, string, error) { 63 | var c AuthSpiceContext 64 | var ok bool 65 | if c, ok = ctx.(AuthSpiceContext); !ok { 66 | return false, "", errors.New("invalid auth method") 67 | } 68 | 69 | c.(*authSpice).readTicket() 70 | return true, "127.0.0.1:5900", nil 71 | } 72 | 73 | // Method implements the Authenticator interface 74 | func (a *noopAuth) Method() red.AuthMethod { 75 | return red.AuthMethodSpice 76 | } 77 | 78 | // Init implements the Authenticator interface 79 | func (a *noopAuth) Init() error { return nil } 80 | 81 | // AuthContext is used to pass either a spiceAuthContext or a saslAuthContext 82 | // to the Authenticator 83 | type AuthContext interface { 84 | LoadToken() string 85 | SaveToken(string) 86 | LoadAddress() string 87 | SaveAddress(string) 88 | } 89 | 90 | // AuthSASLContext is the interface for SASL authentication. 91 | // This is not yet implemented 92 | type AuthSASLContext interface { 93 | toBeImplemented() 94 | AuthContext 95 | } 96 | 97 | // AuthSpiceContext is the interface for token based (Spice) authentication. 98 | type AuthSpiceContext interface { 99 | Token() (string, error) 100 | AuthContext 101 | } 102 | 103 | // authSpice is a special context for the Authenticator 104 | // Is is used to pass information from the proxy to the Authenticator and 105 | // back again. 106 | type authSpice struct { 107 | tenant io.Reader 108 | ticketCrypted []byte 109 | ticketUncrypted []byte 110 | 111 | privateKey *rsa.PrivateKey // needed for Spice auth 112 | token string // previously authenticated ticket 113 | computeAddress string // destination compute node 114 | } 115 | 116 | // readTicket is a helper function to read the tenant ticket bytes 117 | func (a *authSpice) readTicket() ([]byte, error) { 118 | if a.ticketCrypted != nil { 119 | return a.ticketCrypted, nil 120 | } 121 | if _, ok := a.tenant.(net.Conn); ok { 122 | a.tenant.(net.Conn).SetReadDeadline(time.Now().Add(5 * time.Second)) 123 | defer a.tenant.(net.Conn).SetReadDeadline(time.Time{}) 124 | } 125 | 126 | a.ticketCrypted = make([]byte, red.ClientTicketBytes) 127 | if _, err := a.tenant.Read(a.ticketCrypted); err != nil { 128 | return nil, errors.Wrap(err, "read deadline reached") 129 | } 130 | return a.ticketCrypted, nil 131 | } 132 | 133 | // Token will return the unencrypted token string the tenant used 134 | // to authenticate this session after trimming trailing zero's. 135 | func (a *authSpice) Token() (string, error) { 136 | crypted, err := a.readTicket() 137 | if err != nil { 138 | return "", err 139 | } 140 | key := a.privateKey 141 | if key == nil { 142 | return "", errors.New("no private key") 143 | } 144 | 145 | rng := rand.Reader 146 | plaintext, err := rsa.DecryptOAEP(sha1.New(), rng, key, crypted, []byte{}) 147 | if err != nil { 148 | return "", errors.New("could not decrypt token") 149 | } 150 | 151 | // trim trailing nul 152 | a.ticketUncrypted = bytes.Trim(plaintext, "\x00") 153 | 154 | return string(a.ticketUncrypted), nil 155 | } 156 | 157 | // LoadToken return the token saved to this session. 158 | // If this connection belongs to a previously established session 159 | // (any channel after the first), this returns the token that was stored 160 | // in the session table when authenticating the first connection. 161 | // This allows for the use of One-Time-Passwords, but still allow multiple 162 | // connections belonging to the same session to be validated. 163 | // The exact method of validation is up to the implementor of an Authenticator. 164 | // See the example on how to use this. 165 | func (a *authSpice) LoadToken() string { 166 | return a.token 167 | } 168 | 169 | // SaveToken stores a token in the context. When the result of the authentication 170 | // is true (access is granted) this token is saved in the session table. Any subsequent 171 | // connections using the same session id, will have this token available in its auth, 172 | // and can be retrieved using LoadToken(). 173 | // See the example on how to use this. 174 | func (a *authSpice) SaveToken(token string) { 175 | a.token = token 176 | } 177 | 178 | // LoadAddress returns the compute node computeAddress saved to this session. 179 | // This is the same for LoadToken, only it is used to store the compute node computeAddress 180 | // for this session. 181 | // See the example on how to use this. 182 | func (a *authSpice) LoadAddress() string { 183 | return a.computeAddress 184 | } 185 | 186 | // SaveAddress saves the compute node computeAddress in the context. When the result of the authentication 187 | // is true (access is granted) this computeAddress is saved in the session table. 188 | // See the example on how to use this. 189 | func (a *authSpice) SaveAddress(address string) { 190 | a.computeAddress = address 191 | } 192 | -------------------------------------------------------------------------------- /tenant.go: -------------------------------------------------------------------------------- 1 | package spice 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/x509" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net" 12 | 13 | "github.com/jsimonetti/go-spice/red" 14 | ) 15 | 16 | type tenantHandshake struct { 17 | proxy *Proxy 18 | 19 | done bool 20 | 21 | tenantAuthMethod red.AuthMethod 22 | privateKey *rsa.PrivateKey 23 | 24 | channelID uint8 25 | channelType red.ChannelType 26 | sessionID uint32 27 | 28 | otp string // one time password 29 | destination string // compute computeAddress 30 | 31 | log Logger 32 | } 33 | 34 | func newTenantHandshake(p *Proxy, log Logger) (*tenantHandshake, error) { 35 | 36 | handShake := &tenantHandshake{ 37 | proxy: p, 38 | log: log, 39 | } 40 | 41 | rng := rand.Reader 42 | key, err := rsa.GenerateKey(rng, 1024) 43 | if err != nil { 44 | return nil, err 45 | } 46 | handShake.privateKey = key 47 | 48 | return handShake, nil 49 | } 50 | 51 | func (c *tenantHandshake) Done() bool { 52 | return c.done 53 | } 54 | 55 | func (c *tenantHandshake) clientLinkStage(tenant io.ReadWriter) (net.Conn, error) { 56 | // Handle first Tenant Link Message 57 | if err := c.clientLinkMessage(tenant); err != nil { 58 | return nil, err 59 | } 60 | 61 | c.otp = c.proxy.sessionTable.OTP(c.sessionID) 62 | c.destination = c.proxy.sessionTable.Compute(c.sessionID) 63 | 64 | // Handle 2nd Tenant auth method select 65 | if err := c.clientAuthMethod(tenant); err != nil { 66 | return nil, err 67 | } 68 | 69 | // Do compute handshake 70 | handShake := &computeHandshake{ 71 | proxy: c.proxy, 72 | channelType: c.channelType, 73 | channelID: c.channelID, 74 | sessionID: c.sessionID, 75 | tenant: tenant, 76 | log: c.log, 77 | } 78 | 79 | // Lookup destination in proxy.sessionTable 80 | if c.proxy.sessionTable.Lookup(c.sessionID) { 81 | var err error 82 | c.destination, err = c.proxy.sessionTable.Connect(c.sessionID) 83 | if err != nil { 84 | return nil, err 85 | } 86 | } 87 | 88 | handShake.log = c.log.WithFields("compute", c.destination) 89 | 90 | for !handShake.Done() { 91 | if err := handShake.clientLinkStage(c.destination); err != nil { 92 | handShake.log.WithError(err).Error("compute handshake error") 93 | return nil, err 94 | } 95 | } 96 | 97 | c.log = handShake.log 98 | 99 | c.sessionID = handShake.sessionID 100 | c.proxy.sessionTable.Add(c.sessionID, c.destination, c.otp) 101 | c.done = true 102 | 103 | return handShake.compute, nil 104 | } 105 | 106 | func (c *tenantHandshake) clientAuthMethod(tenant io.ReadWriter) error { 107 | var err error 108 | b := make([]byte, 4) 109 | 110 | if _, err = tenant.Read(b); err != nil { 111 | c.log.WithError(err).Error("error reading client AuthMethod") 112 | return err 113 | } 114 | 115 | c.log.Debug("received ClientAuthMethod") 116 | 117 | c.tenantAuthMethod = red.AuthMethod(b[0]) 118 | 119 | var auth Authenticator 120 | var ok bool 121 | 122 | if auth, ok = c.proxy.authenticator[c.tenantAuthMethod]; !ok { 123 | if err := sendServerTicket(red.ErrorPermissionDenied, tenant); err != nil { 124 | c.log.WithError(err).Error("send ticket") 125 | } 126 | return fmt.Errorf("unavailable auth method %s", c.tenantAuthMethod) 127 | } 128 | 129 | c.log = c.log.WithFields("authmethod", c.tenantAuthMethod) 130 | c.log.Debug("starting authentication") 131 | 132 | var authCtx AuthContext 133 | switch c.tenantAuthMethod { 134 | case red.AuthMethodSpice: 135 | authCtx = &authSpice{tenant: tenant, privateKey: c.privateKey, token: c.otp, computeAddress: c.destination} 136 | case red.AuthMethodSASL: 137 | return errors.New("SASL is not a supported authmethod") 138 | default: 139 | return errors.New("unsupported authmethod") 140 | } 141 | 142 | result, destination, err := auth.Next(authCtx) 143 | if err != nil { 144 | c.log.WithError(err).Error("authentication error") 145 | return err 146 | } 147 | 148 | c.otp = authCtx.LoadToken() 149 | c.destination = destination 150 | 151 | if !result { 152 | if err := sendServerTicket(red.ErrorPermissionDenied, tenant); err != nil { 153 | c.log.WithError(err).Error("send ticket") 154 | return err 155 | } 156 | return fmt.Errorf("authentication failed") 157 | } 158 | 159 | return sendServerTicket(red.ErrorOk, tenant) 160 | } 161 | 162 | func (c *tenantHandshake) clientLinkMessage(tenant io.ReadWriter) error { 163 | var err error 164 | var b []byte 165 | 166 | if b, err = readLinkPacket(tenant); err != nil { 167 | c.log.WithError(err).Error("error reading link packet") 168 | return err 169 | } 170 | 171 | c.log.Debug("received ClientLinkMessage") 172 | 173 | linkMessage := &red.ClientLinkMessage{} 174 | if err := linkMessage.UnmarshalBinary(b); err != nil { 175 | return err 176 | } 177 | 178 | c.channelType = linkMessage.ChannelType 179 | c.channelID = linkMessage.ChannelID 180 | c.sessionID = linkMessage.SessionID 181 | 182 | c.log = c.log.WithFields("channel", c.channelID, "type", c.channelType, "session", c.sessionID) 183 | 184 | return sendServerLinkPacket(tenant, c.privateKey.Public()) 185 | } 186 | 187 | func redPubKey(key crypto.PublicKey) (ret red.PubKey, err error) { 188 | cert, err := x509.MarshalPKIXPublicKey(key) 189 | if err != nil { 190 | return ret, err 191 | } 192 | 193 | copy(ret[:], cert[:]) 194 | return ret, nil 195 | } 196 | 197 | func sendServerLinkPacket(wr io.Writer, key crypto.PublicKey) error { 198 | pubkey, err := redPubKey(key) 199 | if err != nil { 200 | return err 201 | } 202 | 203 | var channelCaps, commonCaps red.Capability 204 | commonCaps.Set(red.CapabilityAuthSpice).Set(red.CapabilityAuthSelection).Set(red.CapabilityMiniHeader) 205 | channelCaps.Set(red.CapabilityMainSeamlessMigrate).Set(red.CapabilityMainSemiSeamlessMigrate) 206 | 207 | reply := red.ServerLinkMessage{ 208 | Error: red.ErrorOk, 209 | PubKey: pubkey, 210 | CommonCaps: 1, 211 | ChannelCaps: 1, 212 | CommonCapabilities: []red.Capability{commonCaps}, 213 | ChannelCapabilities: []red.Capability{channelCaps}, 214 | } 215 | 216 | b, err := reply.MarshalBinary() 217 | if err != nil { 218 | return err 219 | } 220 | 221 | header := red.LinkHeader{ 222 | Size: reply.CapsOffset + 8, 223 | } 224 | 225 | b2, err := header.MarshalBinary() 226 | if err != nil { 227 | return err 228 | } 229 | 230 | data := append(b2, b...) 231 | 232 | _, err = wr.Write(data) 233 | if err != nil { 234 | return err 235 | } 236 | 237 | return nil 238 | } 239 | 240 | func readLinkPacket(conn io.Reader) ([]byte, error) { 241 | headerBytes := make([]byte, 16) 242 | 243 | if _, err := conn.Read(headerBytes); err != nil { 244 | return nil, err 245 | } 246 | 247 | header := &red.LinkHeader{} 248 | if err := header.UnmarshalBinary(headerBytes); err != nil { 249 | return nil, err 250 | } 251 | 252 | var messageBytes []byte 253 | var n int 254 | var err error 255 | pending := int(header.Size) 256 | 257 | for pending > 0 { 258 | bytes := make([]byte, header.Size) 259 | if n, err = conn.Read(bytes); err != nil { 260 | return nil, err 261 | } 262 | pending = pending - n 263 | messageBytes = append(messageBytes, bytes[:n]...) 264 | } 265 | 266 | return messageBytes[:int(header.Size)], nil 267 | } 268 | 269 | func sendServerTicket(result red.ErrorCode, writer io.Writer) error { 270 | msg := red.ServerTicket{ 271 | Result: result, 272 | } 273 | 274 | b, err := msg.MarshalBinary() 275 | if err != nil { 276 | return err 277 | } 278 | 279 | if _, err := writer.Write(b); err != nil { 280 | return err 281 | } 282 | 283 | return nil 284 | } 285 | -------------------------------------------------------------------------------- /red/serverlinkmessage_test.go: -------------------------------------------------------------------------------- 1 | package red 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestServerLinkMessage_UnmarshalBinary(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | b []byte 13 | slm ServerLinkMessage 14 | err error 15 | }{ 16 | { 17 | name: "empty", 18 | err: errInvalidPacket, 19 | }, 20 | { 21 | name: "short", 22 | b: fromHex("00 00 00 00 30 81 9f 30 0d 06 09 2a 86 48 86 f7 0d 01 01 01 05 00 03 81 8d 00 30 81 89 02 81 81 00 bb 49 20 f1 9e 70 a3 07 32 ca a1 63 ce 8d 05 26 82 73 3a 74 59 9d cc c3 83 9c c8 59 60 7e 15 5b 62 8d 53 02 aa f4 81 bf e6 b5 bc 17 88 10 4c d6 dc 6c 83 b9 c2 05 4e ed 89 99 a7 a3 fd 2d 05 d3 0d 60 b3 de 6d 16 3c 9e c8 8c 33 38 b8 3d 39 c1 23 d7 c3 ae e0 59 b6 1a b1 87 d5 b5 30 dc 2b 04 c7 92 6d 92 c4 be bf 21 ae 8a 69 ff 53 1c 41 ff a7 1d 32 8d bb 86 aa c2 50 c4 da 53 f9 24 b0 99 02 03 01 00 01 01 00 00 00 01 00 00 00 b2 00 00"), 23 | err: errInvalidPacket, 24 | }, 25 | { 26 | name: "uneven caps", 27 | slm: ServerLinkMessage{}, 28 | b: fromHex("00 00 00 00 30 81 9f 30 0d 06 09 2a 86 48 86 f7 0d 01 01 01 05 00 03 81 8d 00 30 81 89 02 81 81 00 bb 49 20 f1 9e 70 a3 07 32 ca a1 63 ce 8d 05 26 82 73 3a 74 59 9d cc c3 83 9c c8 59 60 7e 15 5b 62 8d 53 02 aa f4 81 bf e6 b5 bc 17 88 10 4c d6 dc 6c 83 b9 c2 05 4e ed 89 99 a7 a3 fd 2d 05 d3 0d 60 b3 de 6d 16 3c 9e c8 8c 33 38 b8 3d 39 c1 23 d7 c3 ae e0 59 b6 1a b1 87 d5 b5 30 dc 2b 04 c7 92 6d 92 c4 be bf 21 ae 8a 69 ff 53 1c 41 ff a7 1d 32 8d bb 86 aa c2 50 c4 da 53 f9 24 b0 99 02 03 01 00 01 01 00 00 00 01 00 00 00 b2 00 00 00 0b 00 00"), 29 | err: errInvalidPacket, 30 | }, 31 | 32 | { 33 | name: "no caps", 34 | slm: ServerLinkMessage{ 35 | Error: 0x0, 36 | PubKey: [162]uint8{0x30, 0x81, 0x9f, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0x1, 0x5, 0x0, 0x3, 0x81, 0x8d, 0x0, 0x30, 0x81, 0x89, 0x2, 0x81, 0x81, 0x0, 0xbb, 0x49, 0x20, 0xf1, 0x9e, 0x70, 0xa3, 0x7, 0x32, 0xca, 0xa1, 0x63, 0xce, 0x8d, 0x5, 0x26, 0x82, 0x73, 0x3a, 0x74, 0x59, 0x9d, 0xcc, 0xc3, 0x83, 0x9c, 0xc8, 0x59, 0x60, 0x7e, 0x15, 0x5b, 0x62, 0x8d, 0x53, 0x2, 0xaa, 0xf4, 0x81, 0xbf, 0xe6, 0xb5, 0xbc, 0x17, 0x88, 0x10, 0x4c, 0xd6, 0xdc, 0x6c, 0x83, 0xb9, 0xc2, 0x5, 0x4e, 0xed, 0x89, 0x99, 0xa7, 0xa3, 0xfd, 0x2d, 0x5, 0xd3, 0xd, 0x60, 0xb3, 0xde, 0x6d, 0x16, 0x3c, 0x9e, 0xc8, 0x8c, 0x33, 0x38, 0xb8, 0x3d, 0x39, 0xc1, 0x23, 0xd7, 0xc3, 0xae, 0xe0, 0x59, 0xb6, 0x1a, 0xb1, 0x87, 0xd5, 0xb5, 0x30, 0xdc, 0x2b, 0x4, 0xc7, 0x92, 0x6d, 0x92, 0xc4, 0xbe, 0xbf, 0x21, 0xae, 0x8a, 0x69, 0xff, 0x53, 0x1c, 0x41, 0xff, 0xa7, 0x1d, 0x32, 0x8d, 0xbb, 0x86, 0xaa, 0xc2, 0x50, 0xc4, 0xda, 0x53, 0xf9, 0x24, 0xb0, 0x99, 0x2, 0x3, 0x1, 0x0, 0x1}, 37 | CommonCaps: 0x0, 38 | ChannelCaps: 0x0, 39 | CapsOffset: 0xb2, 40 | CommonCapabilities: nil, 41 | ChannelCapabilities: nil, 42 | }, 43 | b: fromHex("00 00 00 00 30 81 9f 30 0d 06 09 2a 86 48 86 f7 0d 01 01 01 05 00 03 81 8d 00 30 81 89 02 81 81 00 bb 49 20 f1 9e 70 a3 07 32 ca a1 63 ce 8d 05 26 82 73 3a 74 59 9d cc c3 83 9c c8 59 60 7e 15 5b 62 8d 53 02 aa f4 81 bf e6 b5 bc 17 88 10 4c d6 dc 6c 83 b9 c2 05 4e ed 89 99 a7 a3 fd 2d 05 d3 0d 60 b3 de 6d 16 3c 9e c8 8c 33 38 b8 3d 39 c1 23 d7 c3 ae e0 59 b6 1a b1 87 d5 b5 30 dc 2b 04 c7 92 6d 92 c4 be bf 21 ae 8a 69 ff 53 1c 41 ff a7 1d 32 8d bb 86 aa c2 50 c4 da 53 f9 24 b0 99 02 03 01 00 01 00 00 00 00 00 00 00 00 b2 00 00 00"), 44 | }, 45 | { 46 | name: "wrong caps", 47 | b: fromHex("00 00 00 00 30 81 9f 30 0d 06 09 2a 86 48 86 f7 0d 01 01 01 05 00 03 81 8d 00 30 81 89 02 81 81 00 bb 49 20 f1 9e 70 a3 07 32 ca a1 63 ce 8d 05 26 82 73 3a 74 59 9d cc c3 83 9c c8 59 60 7e 15 5b 62 8d 53 02 aa f4 81 bf e6 b5 bc 17 88 10 4c d6 dc 6c 83 b9 c2 05 4e ed 89 99 a7 a3 fd 2d 05 d3 0d 60 b3 de 6d 16 3c 9e c8 8c 33 38 b8 3d 39 c1 23 d7 c3 ae e0 59 b6 1a b1 87 d5 b5 30 dc 2b 04 c7 92 6d 92 c4 be bf 21 ae 8a 69 ff 53 1c 41 ff a7 1d 32 8d bb 86 aa c2 50 c4 da 53 f9 24 b0 99 02 03 01 00 01 01 00 00 00 01 00 00 00 b2 00 00 00"), 48 | err: errInvalidPacket, 49 | }, 50 | { 51 | name: "ok", 52 | slm: ServerLinkMessage{ 53 | Error: 0x0, 54 | PubKey: [162]uint8{0x30, 0x81, 0x9f, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0x1, 0x5, 0x0, 0x3, 0x81, 0x8d, 0x0, 0x30, 0x81, 0x89, 0x2, 0x81, 0x81, 0x0, 0xbb, 0x49, 0x20, 0xf1, 0x9e, 0x70, 0xa3, 0x7, 0x32, 0xca, 0xa1, 0x63, 0xce, 0x8d, 0x5, 0x26, 0x82, 0x73, 0x3a, 0x74, 0x59, 0x9d, 0xcc, 0xc3, 0x83, 0x9c, 0xc8, 0x59, 0x60, 0x7e, 0x15, 0x5b, 0x62, 0x8d, 0x53, 0x2, 0xaa, 0xf4, 0x81, 0xbf, 0xe6, 0xb5, 0xbc, 0x17, 0x88, 0x10, 0x4c, 0xd6, 0xdc, 0x6c, 0x83, 0xb9, 0xc2, 0x5, 0x4e, 0xed, 0x89, 0x99, 0xa7, 0xa3, 0xfd, 0x2d, 0x5, 0xd3, 0xd, 0x60, 0xb3, 0xde, 0x6d, 0x16, 0x3c, 0x9e, 0xc8, 0x8c, 0x33, 0x38, 0xb8, 0x3d, 0x39, 0xc1, 0x23, 0xd7, 0xc3, 0xae, 0xe0, 0x59, 0xb6, 0x1a, 0xb1, 0x87, 0xd5, 0xb5, 0x30, 0xdc, 0x2b, 0x4, 0xc7, 0x92, 0x6d, 0x92, 0xc4, 0xbe, 0xbf, 0x21, 0xae, 0x8a, 0x69, 0xff, 0x53, 0x1c, 0x41, 0xff, 0xa7, 0x1d, 0x32, 0x8d, 0xbb, 0x86, 0xaa, 0xc2, 0x50, 0xc4, 0xda, 0x53, 0xf9, 0x24, 0xb0, 0x99, 0x2, 0x3, 0x1, 0x0, 0x1}, 55 | CommonCaps: 0x1, 56 | ChannelCaps: 0x1, 57 | CapsOffset: 0xb2, 58 | CommonCapabilities: []Capability{0xb}, 59 | ChannelCapabilities: []Capability{0x9}, 60 | }, 61 | b: fromHex("00 00 00 00 30 81 9f 30 0d 06 09 2a 86 48 86 f7 0d 01 01 01 05 00 03 81 8d 00 30 81 89 02 81 81 00 bb 49 20 f1 9e 70 a3 07 32 ca a1 63 ce 8d 05 26 82 73 3a 74 59 9d cc c3 83 9c c8 59 60 7e 15 5b 62 8d 53 02 aa f4 81 bf e6 b5 bc 17 88 10 4c d6 dc 6c 83 b9 c2 05 4e ed 89 99 a7 a3 fd 2d 05 d3 0d 60 b3 de 6d 16 3c 9e c8 8c 33 38 b8 3d 39 c1 23 d7 c3 ae e0 59 b6 1a b1 87 d5 b5 30 dc 2b 04 c7 92 6d 92 c4 be bf 21 ae 8a 69 ff 53 1c 41 ff a7 1d 32 8d bb 86 aa c2 50 c4 da 53 f9 24 b0 99 02 03 01 00 01 01 00 00 00 01 00 00 00 b2 00 00 00 0b 00 00 00 09 00 00 00"), 62 | }, 63 | } 64 | 65 | for _, testCase := range tests { 66 | t.Run(testCase.name, func(t *testing.T) { 67 | var slm ServerLinkMessage 68 | err := (&slm).UnmarshalBinary(testCase.b) 69 | 70 | if want, got := testCase.err, err; want != got { 71 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) 72 | } 73 | if err != nil { 74 | return 75 | } 76 | 77 | if want, got := testCase.slm, slm; !reflect.DeepEqual(want, got) { 78 | t.Fatalf("unexpected Message:\n- want: %#v\n- got: %#v", want, got) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestServerLinkMessage_MarshalBinary(t *testing.T) { 85 | tests := []struct { 86 | name string 87 | b []byte 88 | slm ServerLinkMessage 89 | err error 90 | }{ 91 | { 92 | name: "ok", 93 | slm: ServerLinkMessage{ 94 | Error: 0x0, 95 | PubKey: [162]uint8{0x30, 0x81, 0x9f, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0x1, 0x5, 0x0, 0x3, 0x81, 0x8d, 0x0, 0x30, 0x81, 0x89, 0x2, 0x81, 0x81, 0x0, 0xbb, 0x49, 0x20, 0xf1, 0x9e, 0x70, 0xa3, 0x7, 0x32, 0xca, 0xa1, 0x63, 0xce, 0x8d, 0x5, 0x26, 0x82, 0x73, 0x3a, 0x74, 0x59, 0x9d, 0xcc, 0xc3, 0x83, 0x9c, 0xc8, 0x59, 0x60, 0x7e, 0x15, 0x5b, 0x62, 0x8d, 0x53, 0x2, 0xaa, 0xf4, 0x81, 0xbf, 0xe6, 0xb5, 0xbc, 0x17, 0x88, 0x10, 0x4c, 0xd6, 0xdc, 0x6c, 0x83, 0xb9, 0xc2, 0x5, 0x4e, 0xed, 0x89, 0x99, 0xa7, 0xa3, 0xfd, 0x2d, 0x5, 0xd3, 0xd, 0x60, 0xb3, 0xde, 0x6d, 0x16, 0x3c, 0x9e, 0xc8, 0x8c, 0x33, 0x38, 0xb8, 0x3d, 0x39, 0xc1, 0x23, 0xd7, 0xc3, 0xae, 0xe0, 0x59, 0xb6, 0x1a, 0xb1, 0x87, 0xd5, 0xb5, 0x30, 0xdc, 0x2b, 0x4, 0xc7, 0x92, 0x6d, 0x92, 0xc4, 0xbe, 0xbf, 0x21, 0xae, 0x8a, 0x69, 0xff, 0x53, 0x1c, 0x41, 0xff, 0xa7, 0x1d, 0x32, 0x8d, 0xbb, 0x86, 0xaa, 0xc2, 0x50, 0xc4, 0xda, 0x53, 0xf9, 0x24, 0xb0, 0x99, 0x2, 0x3, 0x1, 0x0, 0x1}, 96 | CommonCaps: 0x1, 97 | ChannelCaps: 0x1, 98 | CapsOffset: 0xb2, 99 | CommonCapabilities: []Capability{0xb}, 100 | ChannelCapabilities: []Capability{0x9}, 101 | }, 102 | b: fromHex("00 00 00 00 30 81 9f 30 0d 06 09 2a 86 48 86 f7 0d 01 01 01 05 00 03 81 8d 00 30 81 89 02 81 81 00 bb 49 20 f1 9e 70 a3 07 32 ca a1 63 ce 8d 05 26 82 73 3a 74 59 9d cc c3 83 9c c8 59 60 7e 15 5b 62 8d 53 02 aa f4 81 bf e6 b5 bc 17 88 10 4c d6 dc 6c 83 b9 c2 05 4e ed 89 99 a7 a3 fd 2d 05 d3 0d 60 b3 de 6d 16 3c 9e c8 8c 33 38 b8 3d 39 c1 23 d7 c3 ae e0 59 b6 1a b1 87 d5 b5 30 dc 2b 04 c7 92 6d 92 c4 be bf 21 ae 8a 69 ff 53 1c 41 ff a7 1d 32 8d bb 86 aa c2 50 c4 da 53 f9 24 b0 99 02 03 01 00 01 01 00 00 00 01 00 00 00 b2 00 00 00 0b 00 00 00 09 00 00 00"), 103 | }, 104 | } 105 | 106 | for _, testCase := range tests { 107 | t.Run(testCase.name, func(t *testing.T) { 108 | b, err := testCase.slm.MarshalBinary() 109 | 110 | if want, got := testCase.err, err; want != got { 111 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) 112 | } 113 | if err != nil { 114 | return 115 | } 116 | 117 | if want, got := testCase.b, b; !bytes.Equal(want, got) { 118 | t.Fatalf("unexpected Message bytes:\n- want: [%# x]\n- got: [%# x]", want, got) 119 | } 120 | }) 121 | } 122 | } 123 | --------------------------------------------------------------------------------