Compare commits
2 commits
0125f4e3fe
...
c8e2d2a216
Author | SHA1 | Date | |
---|---|---|---|
c8e2d2a216 | |||
0428ad3ebc |
22 changed files with 1214 additions and 259 deletions
|
@ -1,7 +1,6 @@
|
||||||
linters:
|
linters:
|
||||||
presets:
|
presets:
|
||||||
- bugs
|
- bugs
|
||||||
- unused
|
|
||||||
- import
|
- import
|
||||||
- module
|
- module
|
||||||
|
|
||||||
|
@ -14,6 +13,7 @@ linters:
|
||||||
|
|
||||||
disable:
|
disable:
|
||||||
- scopelint
|
- scopelint
|
||||||
|
- noctx
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
lll:
|
lll:
|
||||||
|
@ -22,3 +22,7 @@ linters-settings:
|
||||||
min-complexity: 10
|
min-complexity: 10
|
||||||
nestif:
|
nestif:
|
||||||
min-complexity: 3
|
min-complexity: 3
|
||||||
|
errcheck:
|
||||||
|
exclude-functions:
|
||||||
|
- "(*github.com/gin-gonic/gin.Context).Error"
|
||||||
|
- "(*github.com/gin-gonic/gin.Context).AbortWithError"
|
||||||
|
|
10
go.mod
10
go.mod
|
@ -3,10 +3,14 @@ module code.thetadev.de/TSGRain/SEBRAUC
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
code.thetadev.de/TSGRain/ginzip v0.1.1
|
||||||
github.com/gofiber/fiber/v2 v2.21.0
|
github.com/fortytw2/leaktest v1.3.0
|
||||||
github.com/gofiber/websocket/v2 v2.0.12
|
github.com/gin-contrib/cors v1.3.1
|
||||||
|
github.com/gin-gonic/gin v1.7.7
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||||
)
|
)
|
||||||
|
|
97
go.sum
97
go.sum
|
@ -1,49 +1,92 @@
|
||||||
github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E=
|
code.thetadev.de/TSGRain/ginzip v0.1.1 h1:+X0L6qumEZiKYSLmM+Q0LqKVHsKvdcg4CVzsEpvM7fk=
|
||||||
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
code.thetadev.de/TSGRain/ginzip v0.1.1/go.mod h1:BH7VkvpP83vPRyMQ8rLIjKycQwGzF+/mFV0BKzg+BuA=
|
||||||
|
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||||
|
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fasthttp/websocket v1.4.3-rc.9 h1:CWJH0vONrOatdKXZgkgbFKWllijD9aY50C5KfbSDcWk=
|
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||||
github.com/fasthttp/websocket v1.4.3-rc.9/go.mod h1:eXL2zqDbexYJxaCw8/PQlm7VcMK6uoGvwbYbTdt4dFo=
|
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||||
github.com/gofiber/fiber/v2 v2.20.1/go.mod h1:/LdZHMUXZvTTo7gU4+b1hclqCAdoQphNQ9bi9gutPyI=
|
github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA=
|
||||||
github.com/gofiber/fiber/v2 v2.21.0 h1:tdRNrgqWqcHWBwE3o51oAleEVsil4Ro02zd2vMEuP4Q=
|
github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk=
|
||||||
github.com/gofiber/fiber/v2 v2.21.0/go.mod h1:MR1usVH3JHYRyQwMe2eZXRSZHRX38fkV+A7CPB+DlDQ=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gofiber/websocket/v2 v2.0.12 h1:jKwTrXiOut9UGOGEzFTAD6gq+/78mM3NcrI05VbxjAU=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gofiber/websocket/v2 v2.0.12/go.mod h1:lQRy0u5ACJfiez/e/bhGeYvM0/M940Y3NFw14U3/otI=
|
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
||||||
|
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||||
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s=
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||||
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4 h1:ocK/D6lCgLji37Z2so4xhMl46se1ntReQQCUIU4BWI8=
|
|
||||||
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
|
|
||||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
github.com/valyala/fasthttp v1.29.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
github.com/valyala/fasthttp v1.31.0 h1:lrauRLII19afgCs2fnWRJ4M5IkV0lo2FqA61uGkNBfE=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
|
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
|
||||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||||
|
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -18,7 +18,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Rauc struct {
|
type Rauc struct {
|
||||||
broadcast chan string
|
bc broadcaster
|
||||||
status RaucStatus
|
status RaucStatus
|
||||||
runningMtx sync.Mutex
|
runningMtx sync.Mutex
|
||||||
}
|
}
|
||||||
|
@ -31,19 +31,23 @@ type RaucStatus struct {
|
||||||
Log string `json:"log"`
|
Log string `json:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRauc(broadcast chan string) *Rauc {
|
type broadcaster interface {
|
||||||
|
Broadcast(msg []byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRauc(bc broadcaster) *Rauc {
|
||||||
r := &Rauc{
|
r := &Rauc{
|
||||||
broadcast: broadcast,
|
bc: bc,
|
||||||
}
|
}
|
||||||
|
|
||||||
r.broadcast <- r.GetStatusJson()
|
r.bc.Broadcast(r.GetStatusJson())
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rauc) completed(updateFile string) {
|
func (r *Rauc) completed(updateFile string) {
|
||||||
r.status.Installing = false
|
r.status.Installing = false
|
||||||
r.broadcast <- r.GetStatusJson()
|
r.bc.Broadcast(r.GetStatusJson())
|
||||||
|
|
||||||
_ = os.Remove(updateFile)
|
_ = os.Remove(updateFile)
|
||||||
}
|
}
|
||||||
|
@ -68,7 +72,7 @@ func (r *Rauc) RunRauc(updateFile string) error {
|
||||||
r.status = RaucStatus{
|
r.status = RaucStatus{
|
||||||
Installing: true,
|
Installing: true,
|
||||||
}
|
}
|
||||||
r.broadcast <- r.GetStatusJson()
|
r.bc.Broadcast(r.GetStatusJson())
|
||||||
|
|
||||||
cmd := util.CommandFromString(fmt.Sprintf("%s install %s", util.RaucCmd, updateFile))
|
cmd := util.CommandFromString(fmt.Sprintf("%s install %s", util.RaucCmd, updateFile))
|
||||||
|
|
||||||
|
@ -100,7 +104,7 @@ func (r *Rauc) RunRauc(updateFile string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasUpdate {
|
if hasUpdate {
|
||||||
r.broadcast <- r.GetStatusJson()
|
r.bc.Broadcast(r.GetStatusJson())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -126,7 +130,7 @@ func (r *Rauc) GetStatus() RaucStatus {
|
||||||
return r.status
|
return r.status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rauc) GetStatusJson() string {
|
func (r *Rauc) GetStatusJson() []byte {
|
||||||
statusJson, _ := json.Marshal(r.status)
|
statusJson, _ := json.Marshal(r.status)
|
||||||
return string(statusJson)
|
return statusJson
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,98 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/gofiber/websocket/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type hubClient struct{}
|
|
||||||
|
|
||||||
type MessageHub struct {
|
|
||||||
Broadcast chan string
|
|
||||||
|
|
||||||
clients map[*websocket.Conn]hubClient
|
|
||||||
register chan *websocket.Conn
|
|
||||||
unregister chan *websocket.Conn
|
|
||||||
lastMessage string
|
|
||||||
|
|
||||||
running bool
|
|
||||||
runningMtx sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHub() *MessageHub {
|
|
||||||
return &MessageHub{
|
|
||||||
clients: make(map[*websocket.Conn]hubClient),
|
|
||||||
register: make(chan *websocket.Conn),
|
|
||||||
Broadcast: make(chan string, 5),
|
|
||||||
unregister: make(chan *websocket.Conn),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hub *MessageHub) sendMessage(conn *websocket.Conn, message string) {
|
|
||||||
if err := conn.WriteMessage(
|
|
||||||
websocket.TextMessage, []byte(message)); err != nil {
|
|
||||||
log.Println("write error:", err)
|
|
||||||
|
|
||||||
_ = conn.WriteMessage(websocket.CloseMessage, []byte{})
|
|
||||||
_ = conn.Close()
|
|
||||||
delete(hub.clients, conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hub *MessageHub) Run() {
|
|
||||||
hub.runningMtx.Lock()
|
|
||||||
isRunning := hub.running
|
|
||||||
hub.running = true
|
|
||||||
hub.runningMtx.Unlock()
|
|
||||||
|
|
||||||
if isRunning {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case conn := <-hub.register:
|
|
||||||
hub.clients[conn] = hubClient{}
|
|
||||||
log.Println("connection registered")
|
|
||||||
|
|
||||||
case message := <-hub.Broadcast:
|
|
||||||
log.Println("message received:", message)
|
|
||||||
hub.lastMessage = message
|
|
||||||
|
|
||||||
// Send the message to all clients
|
|
||||||
for conn := range hub.clients {
|
|
||||||
hub.sendMessage(conn, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
case conn := <-hub.unregister:
|
|
||||||
// Remove the client from the hub
|
|
||||||
delete(hub.clients, conn)
|
|
||||||
|
|
||||||
log.Println("connection unregistered")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hub *MessageHub) Handler(conn *websocket.Conn) {
|
|
||||||
// When the function returns, unregister the client and close the connection
|
|
||||||
defer func() {
|
|
||||||
hub.unregister <- conn
|
|
||||||
conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Register the client
|
|
||||||
hub.register <- conn
|
|
||||||
|
|
||||||
if hub.lastMessage != "" {
|
|
||||||
hub.sendMessage(conn, hub.lastMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
_, _, err := conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
return // Calls the deferred function, i.e. closes the connection on error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,27 +3,23 @@ package server
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/rauc"
|
"code.thetadev.de/TSGRain/SEBRAUC/src/rauc"
|
||||||
|
"code.thetadev.de/TSGRain/SEBRAUC/src/server/stream"
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/sysinfo"
|
"code.thetadev.de/TSGRain/SEBRAUC/src/sysinfo"
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
||||||
"code.thetadev.de/TSGRain/SEBRAUC/ui"
|
"code.thetadev.de/TSGRain/SEBRAUC/ui"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gofiber/fiber/v2/middleware/compress"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
|
||||||
"github.com/gofiber/websocket/v2"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SEBRAUCServer struct {
|
type SEBRAUCServer struct {
|
||||||
address string
|
address string
|
||||||
raucUpdater *rauc.Rauc
|
raucUpdater *rauc.Rauc
|
||||||
hub *MessageHub
|
streamer *stream.API
|
||||||
tmpdir string
|
tmpdir string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,9 +29,9 @@ type statusMessage struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(address string) *SEBRAUCServer {
|
func NewServer(address string) *SEBRAUCServer {
|
||||||
hub := NewHub()
|
streamer := stream.New(10*time.Second, 1*time.Second, []string{})
|
||||||
|
|
||||||
raucUpdater := rauc.NewRauc(hub.Broadcast)
|
raucUpdater := rauc.NewRauc(streamer)
|
||||||
|
|
||||||
tmpdir, err := util.GetTmpdir()
|
tmpdir, err := util.GetTmpdir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -45,127 +41,100 @@ func NewServer(address string) *SEBRAUCServer {
|
||||||
return &SEBRAUCServer{
|
return &SEBRAUCServer{
|
||||||
address: address,
|
address: address,
|
||||||
raucUpdater: raucUpdater,
|
raucUpdater: raucUpdater,
|
||||||
hub: hub,
|
// hub: hub,
|
||||||
tmpdir: tmpdir,
|
streamer: streamer,
|
||||||
|
tmpdir: tmpdir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *SEBRAUCServer) Run() error {
|
func (srv *SEBRAUCServer) Run() error {
|
||||||
app := fiber.New(fiber.Config{
|
router := gin.Default()
|
||||||
AppName: "SEBRAUC",
|
|
||||||
BodyLimit: 1024 * 1024 * 1024,
|
|
||||||
ErrorHandler: errorHandler,
|
|
||||||
DisableStartupMessage: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Use(logger.New())
|
// only for testing
|
||||||
|
router.Use(cors.Default())
|
||||||
app.Use(compress.New(compress.Config{
|
|
||||||
Next: func(c *fiber.Ctx) bool {
|
|
||||||
return strings.HasPrefix(c.Path(), "/api")
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
// just for testing
|
|
||||||
app.Use("/api", cors.New())
|
|
||||||
|
|
||||||
app.Use("/api/ws", func(c *fiber.Ctx) error {
|
|
||||||
// IsWebSocketUpgrade returns true if the client
|
|
||||||
// requested upgrade to the WebSocket protocol.
|
|
||||||
if websocket.IsWebSocketUpgrade(c) {
|
|
||||||
c.Locals("allowed", true)
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
return fiber.ErrUpgradeRequired
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Use("/", filesystem.New(filesystem.Config{
|
|
||||||
Root: http.FS(ui.Assets),
|
|
||||||
PathPrefix: ui.AssetsDir,
|
|
||||||
MaxAge: 7200,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// ROUTES
|
// ROUTES
|
||||||
app.Get("/api/ws", websocket.New(srv.hub.Handler))
|
router.GET("/api/ws", srv.streamer.Handle)
|
||||||
app.Post("/api/update", srv.controllerUpdate)
|
router.GET("/api/status", srv.controllerStatus)
|
||||||
app.Get("/api/status", srv.controllerStatus)
|
router.GET("/api/info", srv.controllerInfo)
|
||||||
app.Get("/api/info", srv.controllerInfo)
|
|
||||||
app.Post("/api/reboot", srv.controllerReboot)
|
|
||||||
|
|
||||||
// Start messaging hub
|
router.POST("/api/update", srv.controllerUpdate)
|
||||||
go srv.hub.Run()
|
router.POST("/api/reboot", srv.controllerReboot)
|
||||||
|
|
||||||
return app.Listen(srv.address)
|
// router.StaticFS("/", ui.GetFS())
|
||||||
|
ui.Register(router)
|
||||||
|
|
||||||
|
return router.Run(srv.address)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *SEBRAUCServer) controllerUpdate(c *fiber.Ctx) error {
|
func (srv *SEBRAUCServer) controllerUpdate(c *gin.Context) {
|
||||||
file, err := c.FormFile("updateFile")
|
file, err := c.FormFile("updateFile")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
c.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uid, err := uuid.NewRandom()
|
uid, err := uuid.NewRandom()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
c.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFile := fmt.Sprintf("%s/update_%s.raucb", srv.tmpdir, uid.String())
|
updateFile := fmt.Sprintf("%s/update_%s.raucb", srv.tmpdir, uid.String())
|
||||||
|
|
||||||
err = c.SaveFile(file, updateFile)
|
err = c.SaveUploadedFile(file, updateFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
c.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = srv.raucUpdater.RunRauc(updateFile)
|
err = srv.raucUpdater.RunRauc(updateFile)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
writeStatus(c, true, "Update started")
|
writeStatus(c, true, "Update started")
|
||||||
} else if errors.Is(err, util.ErrAlreadyRunning) {
|
} else if errors.Is(err, util.ErrAlreadyRunning) {
|
||||||
return fiber.NewError(fiber.StatusConflict, "already running")
|
c.AbortWithError(409, errors.New("already running"))
|
||||||
} else {
|
} else {
|
||||||
return err
|
c.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *SEBRAUCServer) controllerStatus(c *fiber.Ctx) error {
|
func (srv *SEBRAUCServer) controllerStatus(c *gin.Context) {
|
||||||
c.Context().SetStatusCode(200)
|
c.JSON(200, srv.raucUpdater.GetStatus())
|
||||||
_ = c.JSON(srv.raucUpdater.GetStatus())
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *SEBRAUCServer) controllerInfo(c *fiber.Ctx) error {
|
func (srv *SEBRAUCServer) controllerInfo(c *gin.Context) {
|
||||||
info, err := sysinfo.GetSysinfo()
|
info, err := sysinfo.GetSysinfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
c.Error(err)
|
||||||
|
} else {
|
||||||
|
c.JSON(200, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Context().SetStatusCode(200)
|
|
||||||
_ = c.JSON(info)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *SEBRAUCServer) controllerReboot(c *fiber.Ctx) error {
|
func (srv *SEBRAUCServer) controllerReboot(c *gin.Context) {
|
||||||
go util.Reboot(5 * time.Second)
|
go util.Reboot(5 * time.Second)
|
||||||
|
|
||||||
writeStatus(c, true, "System is rebooting")
|
writeStatus(c, true, "System is rebooting")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorHandler(c *fiber.Ctx, err error) error {
|
func errorHandler(c *gin.Context, err error) error {
|
||||||
// API error handling
|
// API error handling
|
||||||
if strings.HasPrefix(c.Path(), "/api") {
|
if strings.HasPrefix(c.FullPath(), "/api") {
|
||||||
writeStatus(c, false, err.Error())
|
writeStatus(c, false, err.Error())
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeStatus(c *fiber.Ctx, success bool, msg string) {
|
func writeStatus(c *gin.Context, success bool, msg string) {
|
||||||
_ = c.JSON(statusMessage{
|
status := 200
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
status = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(status, statusMessage{
|
||||||
Success: success,
|
Success: success,
|
||||||
Msg: msg,
|
Msg: msg,
|
||||||
})
|
})
|
||||||
|
|
||||||
if success {
|
|
||||||
c.Context().SetStatusCode(200)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
119
src/server/stream/client.go
Normal file
119
src/server/stream/client.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package stream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
writeWait = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var ping = func(conn *websocket.Conn) error {
|
||||||
|
return conn.WriteMessage(websocket.PingMessage, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var writeBytes = func(conn *websocket.Conn, data []byte) error {
|
||||||
|
return conn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
type client struct {
|
||||||
|
conn *websocket.Conn
|
||||||
|
onClose func(*client)
|
||||||
|
write chan []byte
|
||||||
|
id uint
|
||||||
|
once once
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClient(conn *websocket.Conn, id uint, onClose func(*client)) *client {
|
||||||
|
return &client{
|
||||||
|
conn: conn,
|
||||||
|
write: make(chan []byte, 1),
|
||||||
|
id: id,
|
||||||
|
onClose: onClose,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the connection.
|
||||||
|
func (c *client) Close() {
|
||||||
|
c.once.Do(func() {
|
||||||
|
c.conn.Close()
|
||||||
|
close(c.write)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyClose closes the connection and notifies that the connection was closed.
|
||||||
|
func (c *client) NotifyClose() {
|
||||||
|
c.once.Do(func() {
|
||||||
|
c.conn.Close()
|
||||||
|
close(c.write)
|
||||||
|
c.onClose(c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// startWriteHandler starts listening on the client connection.
|
||||||
|
// As we do not need anything from the client,
|
||||||
|
// we ignore incoming messages. Leaves the loop on errors.
|
||||||
|
func (c *client) startReading(pongWait time.Duration) {
|
||||||
|
defer c.NotifyClose()
|
||||||
|
c.conn.SetReadLimit(64)
|
||||||
|
_ = c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||||
|
c.conn.SetPongHandler(func(appData string) error {
|
||||||
|
_ = c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
for {
|
||||||
|
if _, _, err := c.conn.NextReader(); err != nil {
|
||||||
|
printWebSocketError("ReadError", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startWriteHandler starts the write loop. The method has the following tasks:
|
||||||
|
// * ping the client in the interval provided as parameter
|
||||||
|
// * write messages send by the channel to the client
|
||||||
|
// * on errors exit the loop.
|
||||||
|
func (c *client) startWriteHandler(pingPeriod time.Duration) {
|
||||||
|
pingTicker := time.NewTicker(pingPeriod)
|
||||||
|
defer func() {
|
||||||
|
c.NotifyClose()
|
||||||
|
pingTicker.Stop()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message, ok := <-c.write:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
|
if err := writeBytes(c.conn, message); err != nil {
|
||||||
|
printWebSocketError("WriteError", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-pingTicker.C:
|
||||||
|
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
|
if err := ping(c.conn); err != nil {
|
||||||
|
printWebSocketError("PingError", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printWebSocketError(prefix string, err error) {
|
||||||
|
var closeError *websocket.CloseError
|
||||||
|
ok := errors.As(err, &closeError)
|
||||||
|
|
||||||
|
if ok && closeError != nil && (closeError.Code == 1000 || closeError.Code == 1001) {
|
||||||
|
// normal closure
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("WebSocket:", prefix, err)
|
||||||
|
}
|
1
src/server/stream/hub.go
Normal file
1
src/server/stream/hub.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package stream
|
1
src/server/stream/hub_test.go
Normal file
1
src/server/stream/hub_test.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package stream
|
38
src/server/stream/once.go
Normal file
38
src/server/stream/once.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package stream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Modified version of sync.Once
|
||||||
|
// (https://github.com/golang/go/blob/master/src/sync/once.go)
|
||||||
|
// This version unlocks the mutex early and therefore doesn't
|
||||||
|
// hold the lock while executing func f().
|
||||||
|
type once struct {
|
||||||
|
m sync.Mutex
|
||||||
|
done uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *once) Do(f func()) {
|
||||||
|
if atomic.LoadUint32(&o.done) == 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if o.mayExecute() {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *once) mayExecute() bool {
|
||||||
|
o.m.Lock()
|
||||||
|
defer o.m.Unlock()
|
||||||
|
if o.done == 0 {
|
||||||
|
atomic.StoreUint32(&o.done, 1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
43
src/server/stream/once_test.go
Normal file
43
src/server/stream/once_test.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package stream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Execute(t *testing.T) {
|
||||||
|
executeOnce := once{}
|
||||||
|
execution := make(chan struct{})
|
||||||
|
fExecute := func() {
|
||||||
|
execution <- struct{}{}
|
||||||
|
}
|
||||||
|
go executeOnce.Do(fExecute)
|
||||||
|
go executeOnce.Do(fExecute)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-execution:
|
||||||
|
// expected
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Fatal("fExecute should be executed once")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-execution:
|
||||||
|
t.Fatal("should only execute once")
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, executeOnce.mayExecute())
|
||||||
|
|
||||||
|
go executeOnce.Do(fExecute)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-execution:
|
||||||
|
t.Fatal("should only execute once")
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
}
|
187
src/server/stream/stream.go
Normal file
187
src/server/stream/stream.go
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
package stream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The API provides a handler for a WebSocket stream API.
|
||||||
|
type API struct {
|
||||||
|
clients map[uint]*client
|
||||||
|
lock sync.RWMutex
|
||||||
|
pingPeriod time.Duration
|
||||||
|
pongTimeout time.Duration
|
||||||
|
upgrader *websocket.Upgrader
|
||||||
|
counter *util.Counter
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new instance of API.
|
||||||
|
// pingPeriod: is the interval, in which is server sends the a ping to the client.
|
||||||
|
// pongTimeout: is the duration after the connection will be terminated,
|
||||||
|
// when the client does not respond with the pong command.
|
||||||
|
func New(pingPeriod, pongTimeout time.Duration, allowedWebSocketOrigins []string) *API {
|
||||||
|
return &API{
|
||||||
|
clients: make(map[uint]*client),
|
||||||
|
pingPeriod: pingPeriod,
|
||||||
|
pongTimeout: pingPeriod + pongTimeout,
|
||||||
|
upgrader: newUpgrader(allowedWebSocketOrigins),
|
||||||
|
counter: &util.Counter{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyDeletedUser closes existing connections for the given user.
|
||||||
|
func (a *API) NotifyDeletedClient(userID uint) error {
|
||||||
|
a.lock.Lock()
|
||||||
|
defer a.lock.Unlock()
|
||||||
|
if client, ok := a.clients[userID]; ok {
|
||||||
|
client.Close()
|
||||||
|
delete(a.clients, userID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify notifies the clients with the given userID that a new messages was created.
|
||||||
|
func (a *API) Notify(userID uint, msg []byte) {
|
||||||
|
a.lock.RLock()
|
||||||
|
defer a.lock.RUnlock()
|
||||||
|
if client, ok := a.clients[userID]; ok {
|
||||||
|
client.write <- msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *API) Broadcast(msg []byte) {
|
||||||
|
a.lock.RLock()
|
||||||
|
defer a.lock.RUnlock()
|
||||||
|
for _, client := range a.clients {
|
||||||
|
client.write <- msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *API) remove(remove *client) {
|
||||||
|
a.lock.Lock()
|
||||||
|
defer a.lock.Unlock()
|
||||||
|
delete(a.clients, remove.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *API) register(client *client) {
|
||||||
|
a.lock.Lock()
|
||||||
|
defer a.lock.Unlock()
|
||||||
|
a.clients[client.id] = client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle handles incoming requests.
|
||||||
|
// First it upgrades the protocol to the WebSocket protocol and then starts listening
|
||||||
|
// for read and writes.
|
||||||
|
// swagger:operation GET /stream message streamMessages
|
||||||
|
//
|
||||||
|
// Websocket, return newly created messages.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// schema: ws, wss
|
||||||
|
// produces: [application/json]
|
||||||
|
// security: [clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
|
||||||
|
// responses:
|
||||||
|
// 200:
|
||||||
|
// description: Ok
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Message"
|
||||||
|
// 400:
|
||||||
|
// description: Bad Request
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 401:
|
||||||
|
// description: Unauthorized
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 403:
|
||||||
|
// description: Forbidden
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
// 500:
|
||||||
|
// description: Server Error
|
||||||
|
// schema:
|
||||||
|
// $ref: "#/definitions/Error"
|
||||||
|
func (a *API) Handle(ctx *gin.Context) {
|
||||||
|
conn, err := a.upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := newClient(conn, a.counter.Increment(), a.remove)
|
||||||
|
a.register(client)
|
||||||
|
go client.startReading(a.pongTimeout)
|
||||||
|
go client.startWriteHandler(a.pingPeriod)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes all client connections and stops answering new connections.
|
||||||
|
func (a *API) Close() {
|
||||||
|
a.lock.Lock()
|
||||||
|
defer a.lock.Unlock()
|
||||||
|
|
||||||
|
for _, client := range a.clients {
|
||||||
|
client.Close()
|
||||||
|
}
|
||||||
|
for k := range a.clients {
|
||||||
|
delete(a.clients, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAllowedOrigin(r *http.Request, allowedOrigins []*regexp.Regexp) bool {
|
||||||
|
origin := r.Header.Get("origin")
|
||||||
|
if origin == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(origin)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(u.Host, r.Host) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, allowedOrigin := range allowedOrigins {
|
||||||
|
if allowedOrigin.Match([]byte(strings.ToLower(u.Hostname()))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpgrader(allowedWebSocketOrigins []string) *websocket.Upgrader {
|
||||||
|
// compiledAllowedOrigins := compileAllowedWebSocketOrigins(allowedWebSocketOrigins)
|
||||||
|
return &websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
/*
|
||||||
|
TODO: implement this
|
||||||
|
if mode.IsDev() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return isAllowedOrigin(r, compiledAllowedOrigins)
|
||||||
|
*/
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compileAllowedWebSocketOrigins(allowedOrigins []string) []*regexp.Regexp {
|
||||||
|
var compiledAllowedOrigins []*regexp.Regexp
|
||||||
|
for _, origin := range allowedOrigins {
|
||||||
|
compiledAllowedOrigins = append(compiledAllowedOrigins, regexp.MustCompile(origin))
|
||||||
|
}
|
||||||
|
|
||||||
|
return compiledAllowedOrigins
|
||||||
|
}
|
424
src/server/stream/stream_test.go
Normal file
424
src/server/stream/stream_test.go
Normal file
|
@ -0,0 +1,424 @@
|
||||||
|
package stream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fortytw2/leaktest"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFailureOnNormalHttpRequest(t *testing.T) {
|
||||||
|
// mode.Set(mode.TestDev)
|
||||||
|
|
||||||
|
defer leaktest.Check(t)()
|
||||||
|
|
||||||
|
server, api := bootTestServer()
|
||||||
|
defer server.Close()
|
||||||
|
defer api.Close()
|
||||||
|
|
||||||
|
resp, err := http.Get(server.URL)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteMessageFails(t *testing.T) {
|
||||||
|
// mode.Set(mode.TestDev)
|
||||||
|
oldWrite := writeBytes
|
||||||
|
// try emulate an write error, mostly this should kill the ReadMessage
|
||||||
|
// goroutine first but you'll never know.
|
||||||
|
writeBytes = func(conn *websocket.Conn, data []byte) error {
|
||||||
|
return errors.New("asd")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
writeBytes = oldWrite
|
||||||
|
}()
|
||||||
|
defer leaktest.Check(t)()
|
||||||
|
|
||||||
|
server, api := bootTestServer()
|
||||||
|
defer server.Close()
|
||||||
|
defer api.Close()
|
||||||
|
|
||||||
|
wsURL := wsURL(server.URL)
|
||||||
|
user := testClient(t, wsURL)
|
||||||
|
|
||||||
|
// the server may take some time to register the client
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
client := getClient(api, 1)
|
||||||
|
assert.NotNil(t, client)
|
||||||
|
|
||||||
|
api.Notify(1, []byte("HI"))
|
||||||
|
user.expectNoMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWritePingFails(t *testing.T) {
|
||||||
|
// mode.Set(mode.TestDev)
|
||||||
|
oldPing := ping
|
||||||
|
// try emulate an write error, mostly this should kill the ReadMessage
|
||||||
|
// gorouting first but you'll never know.
|
||||||
|
ping = func(conn *websocket.Conn) error {
|
||||||
|
return errors.New("asd")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
ping = oldPing
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer leaktest.CheckTimeout(t, 10*time.Second)()
|
||||||
|
|
||||||
|
server, api := bootTestServer()
|
||||||
|
defer api.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
wsURL := wsURL(server.URL)
|
||||||
|
user := testClient(t, wsURL)
|
||||||
|
defer user.conn.Close()
|
||||||
|
|
||||||
|
// the server may take some time to register the client
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
client := getClient(api, 1)
|
||||||
|
|
||||||
|
assert.NotNil(t, client)
|
||||||
|
|
||||||
|
time.Sleep(api.pingPeriod) // waiting for ping
|
||||||
|
|
||||||
|
api.Notify(1, []byte("HI"))
|
||||||
|
user.expectNoMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPing(t *testing.T) {
|
||||||
|
// mode.Set(mode.TestDev)
|
||||||
|
|
||||||
|
server, api := bootTestServer()
|
||||||
|
defer server.Close()
|
||||||
|
defer api.Close()
|
||||||
|
|
||||||
|
wsURL := wsURL(server.URL)
|
||||||
|
|
||||||
|
user := createClient(t, wsURL)
|
||||||
|
defer user.conn.Close()
|
||||||
|
|
||||||
|
ping := make(chan bool)
|
||||||
|
oldPingHandler := user.conn.PingHandler()
|
||||||
|
user.conn.SetPingHandler(func(appData string) error {
|
||||||
|
err := oldPingHandler(appData)
|
||||||
|
ping <- true
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
startReading(user)
|
||||||
|
|
||||||
|
expectNoMessage(user)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
assert.Fail(t, "Expected ping but there was one :(")
|
||||||
|
case <-ping:
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
expectNoMessage(user)
|
||||||
|
api.Notify(1, []byte("HI"))
|
||||||
|
user.expectMessage([]byte("HI"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloseClientOnNotReading(t *testing.T) {
|
||||||
|
// mode.Set(mode.TestDev)
|
||||||
|
|
||||||
|
server, api := bootTestServer()
|
||||||
|
defer server.Close()
|
||||||
|
defer api.Close()
|
||||||
|
|
||||||
|
wsURL := wsURL(server.URL)
|
||||||
|
|
||||||
|
ws, resp, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
defer ws.Close()
|
||||||
|
|
||||||
|
// the server may take some time to register the client
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
assert.NotNil(t, getClient(api, 1))
|
||||||
|
|
||||||
|
time.Sleep(api.pingPeriod + api.pongTimeout)
|
||||||
|
|
||||||
|
assert.Nil(t, getClient(api, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessageDirectlyAfterConnect(t *testing.T) {
|
||||||
|
// mode.Set(mode.Prod)
|
||||||
|
defer leaktest.Check(t)()
|
||||||
|
server, api := bootTestServer()
|
||||||
|
defer server.Close()
|
||||||
|
defer api.Close()
|
||||||
|
|
||||||
|
wsURL := wsURL(server.URL)
|
||||||
|
|
||||||
|
user := testClient(t, wsURL)
|
||||||
|
defer user.conn.Close()
|
||||||
|
// the server may take some time to register the client
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
api.Notify(1, []byte("msg"))
|
||||||
|
user.expectMessage([]byte("msg"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteClientShouldCloseConnection(t *testing.T) {
|
||||||
|
// mode.Set(mode.Prod)
|
||||||
|
defer leaktest.Check(t)()
|
||||||
|
server, api := bootTestServer()
|
||||||
|
defer server.Close()
|
||||||
|
defer api.Close()
|
||||||
|
|
||||||
|
wsURL := wsURL(server.URL)
|
||||||
|
|
||||||
|
user := testClient(t, wsURL)
|
||||||
|
defer user.conn.Close()
|
||||||
|
// the server may take some time to register the client
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
api.Notify(1, []byte("HI"))
|
||||||
|
user.expectMessage([]byte("HI"))
|
||||||
|
|
||||||
|
assert.Nil(t, api.NotifyDeletedClient(1))
|
||||||
|
|
||||||
|
api.Notify(1, []byte("HI"))
|
||||||
|
user.expectNoMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotify(t *testing.T) {
|
||||||
|
// mode.Set(mode.TestDev)
|
||||||
|
|
||||||
|
defer leaktest.Check(t)()
|
||||||
|
server, api := bootTestServer()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
wsURL := wsURL(server.URL)
|
||||||
|
|
||||||
|
client1 := testClient(t, wsURL)
|
||||||
|
defer client1.conn.Close()
|
||||||
|
|
||||||
|
client2 := testClient(t, wsURL)
|
||||||
|
defer client2.conn.Close()
|
||||||
|
|
||||||
|
client3 := testClient(t, wsURL)
|
||||||
|
defer client3.conn.Close()
|
||||||
|
|
||||||
|
// the server may take some time to register the client
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
api.Notify(1, []byte("msg"))
|
||||||
|
expectMessage([]byte("msg"), client1)
|
||||||
|
expectNoMessage(client2)
|
||||||
|
expectNoMessage(client3)
|
||||||
|
|
||||||
|
assert.Nil(t, api.NotifyDeletedClient(1))
|
||||||
|
|
||||||
|
api.Notify(1, []byte("msg"))
|
||||||
|
expectNoMessage(client1)
|
||||||
|
expectNoMessage(client2)
|
||||||
|
expectNoMessage(client3)
|
||||||
|
|
||||||
|
api.Notify(2, []byte("msg"))
|
||||||
|
expectNoMessage(client1)
|
||||||
|
expectMessage([]byte("msg"), client2)
|
||||||
|
expectNoMessage(client3)
|
||||||
|
|
||||||
|
api.Notify(3, []byte("msg"))
|
||||||
|
expectNoMessage(client1)
|
||||||
|
expectNoMessage(client2)
|
||||||
|
expectMessage([]byte("msg"), client3)
|
||||||
|
|
||||||
|
api.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBroadcast(t *testing.T) {
|
||||||
|
defer leaktest.Check(t)()
|
||||||
|
server, api := bootTestServer()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
wsURL := wsURL(server.URL)
|
||||||
|
|
||||||
|
client1 := testClient(t, wsURL)
|
||||||
|
defer client1.conn.Close()
|
||||||
|
|
||||||
|
client2 := testClient(t, wsURL)
|
||||||
|
defer client2.conn.Close()
|
||||||
|
|
||||||
|
client3 := testClient(t, wsURL)
|
||||||
|
defer client3.conn.Close()
|
||||||
|
|
||||||
|
// the server may take some time to register the client
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
testMsg1 := []byte("hello1")
|
||||||
|
api.Broadcast(testMsg1)
|
||||||
|
expectMessage(testMsg1, client1, client2, client3)
|
||||||
|
|
||||||
|
assert.Nil(t, api.NotifyDeletedClient(1))
|
||||||
|
|
||||||
|
testMsg2 := []byte("hello2")
|
||||||
|
api.Broadcast(testMsg2)
|
||||||
|
expectNoMessage(client1)
|
||||||
|
expectMessage(testMsg2, client2, client3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_sameOrigin_returnsTrue(t *testing.T) {
|
||||||
|
// mode.Set(mode.Prod)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/stream", nil)
|
||||||
|
req.Header.Set("Origin", "http://example.com")
|
||||||
|
actual := isAllowedOrigin(req, nil)
|
||||||
|
assert.True(t, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_sameOrigin_returnsTrue_withCustomPort(t *testing.T) {
|
||||||
|
// mode.Set(mode.Prod)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com:8080/stream", nil)
|
||||||
|
req.Header.Set("Origin", "http://example.com:8080")
|
||||||
|
actual := isAllowedOrigin(req, nil)
|
||||||
|
assert.True(t, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isAllowedOrigin_withoutAllowedOrigins_failsWhenNotSameOrigin(t *testing.T) {
|
||||||
|
// mode.Set(mode.Prod)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/stream", nil)
|
||||||
|
req.Header.Set("Origin", "http://gorify.example.com")
|
||||||
|
actual := isAllowedOrigin(req, nil)
|
||||||
|
assert.False(t, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isAllowedOriginMatching(t *testing.T) {
|
||||||
|
// mode.Set(mode.Prod)
|
||||||
|
compiledAllowedOrigins := compileAllowedWebSocketOrigins(
|
||||||
|
[]string{"go.{4}\\.example\\.com", "go\\.example\\.com"},
|
||||||
|
)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://example.me/stream", nil)
|
||||||
|
req.Header.Set("Origin", "http://gorify.example.com")
|
||||||
|
assert.True(t, isAllowedOrigin(req, compiledAllowedOrigins))
|
||||||
|
|
||||||
|
req.Header.Set("Origin", "http://go.example.com")
|
||||||
|
assert.True(t, isAllowedOrigin(req, compiledAllowedOrigins))
|
||||||
|
|
||||||
|
req.Header.Set("Origin", "http://hello.example.com")
|
||||||
|
assert.False(t, isAllowedOrigin(req, compiledAllowedOrigins))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_emptyOrigin_returnsTrue(t *testing.T) {
|
||||||
|
// mode.Set(mode.Prod)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/stream", nil)
|
||||||
|
actual := isAllowedOrigin(req, nil)
|
||||||
|
assert.True(t, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_otherOrigin_returnsFalse(t *testing.T) {
|
||||||
|
// mode.Set(mode.Prod)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/stream", nil)
|
||||||
|
req.Header.Set("Origin", "http://otherexample.de")
|
||||||
|
actual := isAllowedOrigin(req, nil)
|
||||||
|
assert.False(t, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_invalidOrigin_returnsFalse(t *testing.T) {
|
||||||
|
// mode.Set(mode.Prod)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/stream", nil)
|
||||||
|
req.Header.Set("Origin", "http\\://otherexample.de")
|
||||||
|
actual := isAllowedOrigin(req, nil)
|
||||||
|
assert.False(t, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_compileAllowedWebSocketOrigins(t *testing.T) {
|
||||||
|
assert.Equal(t, 0, len(compileAllowedWebSocketOrigins([]string{})))
|
||||||
|
assert.Equal(t, 3, len(compileAllowedWebSocketOrigins([]string{"^.*$", "", "abc"})))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClient(api *API, user uint) *client {
|
||||||
|
api.lock.RLock()
|
||||||
|
defer api.lock.RUnlock()
|
||||||
|
|
||||||
|
return api.clients[user]
|
||||||
|
}
|
||||||
|
|
||||||
|
func testClient(t *testing.T, url string) *testingClient {
|
||||||
|
client := createClient(t, url)
|
||||||
|
startReading(client)
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func startReading(client *testingClient) {
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
_, payload, err := client.conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client.readMessage <- payload
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createClient(t *testing.T, url string) *testingClient {
|
||||||
|
ws, resp, err := websocket.DefaultDialer.Dial(url, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
readMessages := make(chan []byte)
|
||||||
|
|
||||||
|
return &testingClient{conn: ws, readMessage: readMessages, t: t}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testingClient struct {
|
||||||
|
conn *websocket.Conn
|
||||||
|
readMessage chan []byte
|
||||||
|
t *testing.T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testingClient) expectMessage(expected []byte) {
|
||||||
|
select {
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
assert.Fail(c.t, "Expected message but none was send :(")
|
||||||
|
case actual := <-c.readMessage:
|
||||||
|
assert.Equal(c.t, expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectMessage(expected []byte, clients ...*testingClient) {
|
||||||
|
for _, client := range clients {
|
||||||
|
client.expectMessage(expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectNoMessage(clients ...*testingClient) {
|
||||||
|
for _, client := range clients {
|
||||||
|
client.expectNoMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testingClient) expectNoMessage() {
|
||||||
|
select {
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
// no message == as expected
|
||||||
|
case msg := <-c.readMessage:
|
||||||
|
assert.Fail(c.t, "Expected NO message but there was one :(", fmt.Sprint(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bootTestServer() (*httptest.Server, *API) {
|
||||||
|
r := gin.New()
|
||||||
|
// ping every 500 ms, and the client has 500 ms to respond
|
||||||
|
api := New(500*time.Millisecond, 500*time.Millisecond, []string{})
|
||||||
|
|
||||||
|
r.GET("/", api.Handle)
|
||||||
|
server := httptest.NewServer(r)
|
||||||
|
return server, api
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsURL(httpURL string) string {
|
||||||
|
return "ws" + strings.TrimPrefix(httpURL, "http")
|
||||||
|
}
|
30
src/util/counter.go
Normal file
30
src/util/counter.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
type Counter struct {
|
||||||
|
count uint
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Counter) Get() uint {
|
||||||
|
c.mutex.RLock()
|
||||||
|
defer c.mutex.RUnlock()
|
||||||
|
|
||||||
|
return c.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Counter) Reset() {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
c.count = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Counter) Increment() uint {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
c.count++
|
||||||
|
return c.count
|
||||||
|
}
|
30
src/util/counter_test.go
Normal file
30
src/util/counter_test.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCounter(t *testing.T) {
|
||||||
|
counter := Counter{}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
incrementer := func() {
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
counter.Increment()
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go incrementer()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
assert.EqualValues(t, 5000, counter.Get())
|
||||||
|
}
|
|
@ -7,7 +7,11 @@
|
||||||
<title>SEBRAUC</title>
|
<title>SEBRAUC</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<noscript>You have to enable JavaScript to use SEBRAUC.</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<script>
|
||||||
|
window.config = "%CONFIG%"
|
||||||
|
</script>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -7,8 +7,9 @@ import Icon from "../Icon/Icon"
|
||||||
import "./Updater.scss"
|
import "./Updater.scss"
|
||||||
import Alert from "./Alert"
|
import Alert from "./Alert"
|
||||||
import Reboot from "./Reboot"
|
import Reboot from "./Reboot"
|
||||||
import {sebraucApi, wsUrl} from "../../util/apiUrls"
|
import {sebraucApi} from "../../util/apiUrls"
|
||||||
import colors from "../../util/colors"
|
import colors from "../../util/colors"
|
||||||
|
import WebsocketClient from "../../util/websocket"
|
||||||
|
|
||||||
class UploadStatus {
|
class UploadStatus {
|
||||||
uploading = false
|
uploading = false
|
||||||
|
@ -52,19 +53,19 @@ type State = {
|
||||||
|
|
||||||
export default class UpdaterCard extends Component<Props, State> {
|
export default class UpdaterCard extends Component<Props, State> {
|
||||||
private dropzoneRef = createRef<Dropzone>()
|
private dropzoneRef = createRef<Dropzone>()
|
||||||
private conn: WebSocket | undefined
|
private ws: WebsocketClient
|
||||||
|
|
||||||
constructor(props?: Props | undefined, context?: any) {
|
constructor(props?: Props | undefined, context?: any) {
|
||||||
super(props, context)
|
super(props, context)
|
||||||
|
|
||||||
|
this.ws = new WebsocketClient(this.onWsStatusUpdate, this.onWsMessage)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
uploadStatus: new UploadStatus(false),
|
uploadStatus: new UploadStatus(false),
|
||||||
uploadFilename: "",
|
uploadFilename: "",
|
||||||
raucStatus: new RaucStatus(),
|
raucStatus: new RaucStatus(),
|
||||||
wsConnected: false,
|
wsConnected: this.ws.api().isConnected(),
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connectWebsocket()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private buttonClick = () => {
|
private buttonClick = () => {
|
||||||
|
@ -105,31 +106,16 @@ export default class UpdaterCard extends Component<Props, State> {
|
||||||
this.dropzoneRef.current?.reset()
|
this.dropzoneRef.current?.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
private connectWebsocket = () => {
|
private onWsStatusUpdate = (wsConnected: boolean) => {
|
||||||
if (window.WebSocket) {
|
this.setState({wsConnected: wsConnected})
|
||||||
this.conn = new WebSocket(wsUrl)
|
}
|
||||||
this.conn.onopen = () => {
|
|
||||||
this.setState({wsConnected: true})
|
private onWsMessage = (evt: MessageEvent) => {
|
||||||
console.log("WS connected")
|
var messages = evt.data.split("\n")
|
||||||
}
|
for (var i = 0; i < messages.length; i++) {
|
||||||
this.conn.onclose = () => {
|
this.setState({
|
||||||
this.setState({wsConnected: false})
|
raucStatus: Object.assign(new RaucStatus(), JSON.parse(messages[i])),
|
||||||
console.log("WS connection closed")
|
})
|
||||||
window.setTimeout(this.connectWebsocket, 3000)
|
|
||||||
}
|
|
||||||
this.conn.onmessage = (evt) => {
|
|
||||||
var messages = evt.data.split("\n")
|
|
||||||
for (var i = 0; i < messages.length; i++) {
|
|
||||||
this.setState({
|
|
||||||
raucStatus: Object.assign(
|
|
||||||
new RaucStatus(),
|
|
||||||
JSON.parse(messages[i])
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("Your browser does not support WebSockets")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,6 +153,10 @@ export default class UpdaterCard extends Component<Props, State> {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.ws.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const acceptUploads = this.acceptUploads()
|
const acceptUploads = this.acceptUploads()
|
||||||
const circleColor = this.circleColor()
|
const circleColor = this.circleColor()
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import {Component} from "preact"
|
import {Component} from "preact"
|
||||||
import UpdaterView from "./Updater/UpdaterView"
|
import UpdaterView from "./Updater/UpdaterView"
|
||||||
import logo from "../assets/logo.svg"
|
import logo from "../assets/logo.svg"
|
||||||
import {version} from "../util/version"
|
import {getConfig} from "../util/config"
|
||||||
|
|
||||||
export default class App extends Component {
|
export default class App extends Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<img src={logo} alt="SEBRAUC" height="64" />
|
<img src={logo} alt="SEBRAUC" height="64" />
|
||||||
{version}
|
{getConfig().version}
|
||||||
<UpdaterView />
|
<UpdaterView />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
23
ui/src/util/config.ts
Normal file
23
ui/src/util/config.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
export interface Config {
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
config?: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isConfig(object: any): object is Config {
|
||||||
|
return typeof object === "object" && "version" in object
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfig(): Config {
|
||||||
|
if (isConfig(window.config)) {
|
||||||
|
return window.config
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
version: "dev",
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
let version = import.meta.env.VITE_VERSION
|
|
||||||
|
|
||||||
if (version === undefined) {
|
|
||||||
version = "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
export {version}
|
|
92
ui/src/util/websocket.ts
Normal file
92
ui/src/util/websocket.ts
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import {wsUrl} from "./apiUrls"
|
||||||
|
|
||||||
|
class WebsocketAPI {
|
||||||
|
private static ws: WebsocketAPI | undefined
|
||||||
|
|
||||||
|
private conn: WebSocket | undefined
|
||||||
|
private wsConnected: boolean
|
||||||
|
|
||||||
|
private clients: Set<WebsocketClient>
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.clients = new Set()
|
||||||
|
this.wsConnected = false
|
||||||
|
|
||||||
|
if (window.WebSocket) {
|
||||||
|
this.connect()
|
||||||
|
} else {
|
||||||
|
console.log("Your browser does not support WebSockets")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setStatus(wsConnected: boolean) {
|
||||||
|
if (wsConnected !== this.wsConnected) {
|
||||||
|
this.wsConnected = wsConnected
|
||||||
|
this.clients.forEach((client) => {
|
||||||
|
client.statusCallback(this.wsConnected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private connect() {
|
||||||
|
this.conn = new WebSocket(wsUrl)
|
||||||
|
this.conn.onopen = () => {
|
||||||
|
this.setStatus(true)
|
||||||
|
console.log("WS connected")
|
||||||
|
}
|
||||||
|
this.conn.onclose = () => {
|
||||||
|
this.setStatus(false)
|
||||||
|
console.log("WS connection closed")
|
||||||
|
window.setTimeout(() => this.connect(), 3000)
|
||||||
|
}
|
||||||
|
this.conn.onmessage = (evt) => {
|
||||||
|
this.clients.forEach((client) => {
|
||||||
|
client.msgCallback(evt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Get(): WebsocketAPI {
|
||||||
|
if (this.ws === undefined) {
|
||||||
|
this.ws = new WebsocketAPI()
|
||||||
|
}
|
||||||
|
return this.ws
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected(): boolean {
|
||||||
|
return this.wsConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
addClient(client: WebsocketClient) {
|
||||||
|
console.log("added client", client)
|
||||||
|
this.clients.add(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeClient(client: WebsocketClient) {
|
||||||
|
console.log("removed client", client)
|
||||||
|
this.clients.delete(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class WebsocketClient {
|
||||||
|
statusCallback: (wsConnected: boolean) => void
|
||||||
|
msgCallback: (evt: MessageEvent) => void
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
statusCallback: (wsConnected: boolean) => void,
|
||||||
|
msgCallback: (evt: MessageEvent) => void
|
||||||
|
) {
|
||||||
|
this.statusCallback = statusCallback
|
||||||
|
this.msgCallback = msgCallback
|
||||||
|
|
||||||
|
this.api().addClient(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
api(): WebsocketAPI {
|
||||||
|
return WebsocketAPI.Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.api().removeClient(this)
|
||||||
|
}
|
||||||
|
}
|
58
ui/ui.go
58
ui/ui.go
|
@ -1,10 +1,64 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
|
||||||
|
"code.thetadev.de/TSGRain/ginzip"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
const AssetsDir = "dist"
|
const distDir = "dist"
|
||||||
|
|
||||||
//go:embed dist/**
|
//go:embed dist/**
|
||||||
var Assets embed.FS
|
var assets embed.FS
|
||||||
|
|
||||||
|
type uiConfig struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func subFS(fsys fs.FS, dir string) fs.FS {
|
||||||
|
sub, err := fs.Sub(fsys, dir)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func distFS() fs.FS {
|
||||||
|
return subFS(assets, distDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Register(r *gin.Engine) {
|
||||||
|
indexHandler := getIndexHandler()
|
||||||
|
|
||||||
|
ui := r.Group("/", ginzip.New(ginzip.DefaultOptions()))
|
||||||
|
|
||||||
|
ui.GET("/", indexHandler)
|
||||||
|
ui.GET("/index.html", indexHandler)
|
||||||
|
|
||||||
|
ui.StaticFS("/assets", http.FS(subFS(distFS(), "assets")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIndexHandler() gin.HandlerFunc {
|
||||||
|
content, err := fs.ReadFile(distFS(), "index.html")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uiConfigBytes, err := json.Marshal(uiConfig{
|
||||||
|
Version: util.Version(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
content = bytes.ReplaceAll(content, []byte("\"%CONFIG%\""), uiConfigBytes)
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Data(200, "text/html", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue