├── .chglog ├── CHANGELOG.tpl.md └── config.yml ├── .github └── workflows │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── client ├── client.go └── client_plugin.go ├── cmd ├── client │ └── client.go └── server │ ├── server.go │ └── statik │ └── keep.go ├── go.mod ├── go.sum ├── main.go ├── status-web ├── .gitignore ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── InfoBoard.tsx │ ├── MoreVersionInfo.tsx │ ├── StatisticsBoard.tsx │ ├── Status.ts │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── service-worker.ts │ ├── serviceWorkerRegistration.ts │ └── setupTests.ts ├── tsconfig.json └── yarn.lock ├── version └── version.go └── wss ├── buffered_reader_writer.go ├── buffered_reader_writer_test.go ├── concurrent_websocket.go ├── conn_records.go ├── heart_beat.go ├── http_utils.go ├── hub.go ├── hub_collection.go ├── proxy_client.go ├── proxy_client_http.go ├── proxy_client_https.go ├── proxy_client_interface.go ├── proxy_client_socks5.go ├── proxy_server.go ├── status └── status.go ├── term_view ├── clear_lines_unix.go ├── clear_lines_windows.go ├── log.go └── writer.go ├── version.go ├── websocket_client.go ├── ws_datatypes.go ├── wssocks_client.go └── wssocks_server.go /.chglog/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | {{ range .Versions }} 2 | 3 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} 4 | 5 | > {{ datetime "2006-01-02" .Tag.Date }} 6 | 7 | {{ range .CommitGroups -}} 8 | ### {{ .Title }} 9 | 10 | {{ range .Commits -}} 11 | * {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 12 | {{ end }} 13 | {{ end -}} 14 | 15 | {{- if .RevertCommits -}} 16 | ### Reverts 17 | 18 | {{ range .RevertCommits -}} 19 | * {{ .Revert.Header }} 20 | {{ end }} 21 | {{ end -}} 22 | 23 | {{- if .MergeCommits -}} 24 | ### Pull Requests 25 | 26 | {{ range .MergeCommits -}} 27 | * {{ .Header }} 28 | {{ end }} 29 | {{ end -}} 30 | 31 | {{- if .NoteGroups -}} 32 | {{ range .NoteGroups -}} 33 | ### {{ .Title }} 34 | 35 | {{ range .Notes }} 36 | {{ .Body }} 37 | {{ end }} 38 | {{ end -}} 39 | {{ end -}} 40 | {{ end -}} -------------------------------------------------------------------------------- /.chglog/config.yml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: https://github.com/genshen/wssocks 6 | options: 7 | commits: 8 | # filters: 9 | # Type: 10 | # - feat 11 | # - fix 12 | # - perf 13 | # - refactor 14 | commit_groups: 15 | # title_maps: 16 | # feat: Features 17 | # fix: Bug Fixes 18 | # perf: Performance Improvements 19 | # refactor: Code Refactoring 20 | header: 21 | pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$" 22 | pattern_maps: 23 | - Type 24 | - Scope 25 | - Subject 26 | notes: 27 | keywords: 28 | - BREAKING CHANGE -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: ubuntu-22.04 8 | steps: 9 | 10 | - name: Setup Node.js 11 | uses: actions/setup-node@v3 12 | with: 13 | node-version: '18.17.0' 14 | 15 | - name: Set up Go 1.20 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: '1.20' 19 | id: go 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v3 23 | 24 | - name: Build static status page 25 | run: cd status-web && yarn && yarn build && cd ../ 26 | 27 | - name: Get dependencies 28 | run: go mod download && go install github.com/rakyll/statik 29 | 30 | - name: Static->GO generation 31 | run: cd cmd/server && statik --src=../../status-web/build/ && cd ../../ 32 | 33 | - name: Build 34 | run: make 35 | - uses: actions/upload-artifact@v3 36 | with: 37 | name: build-artifact 38 | path: wssocks-* 39 | 40 | release: 41 | name: On Release 42 | needs: build 43 | runs-on: ubuntu-22.04 44 | steps: 45 | - uses: actions/download-artifact@v3 46 | with: 47 | name: build-artifact 48 | # - run: ls -R 49 | 50 | - name: Release 51 | uses: softprops/action-gh-release@v1 52 | if: startsWith(github.ref, 'refs/tags/') 53 | with: 54 | files: | 55 | wssocks-linux-amd64 56 | wssocks-linux-arm64 57 | wssocks-darwin-amd64 58 | wssocks-darwin-arm64 59 | wssocks-windows-amd64.exe 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmd/server/statik 2 | 3 | # Gradle 4 | .idea/ 5 | 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## [v0.6.0](https://github.com/genshen/wssocks/compare/v0.5.0...v0.6.0) 4 | 5 | > 2023-07-31 6 | 7 | ### Build 8 | 9 | * **gomodule:** bump go packages to latest 10 | * **gomodule:** bump package golang.org/x/crypto and golang.org/x/sync to fix building error 11 | * **gomodule:** update dependencies version 12 | * **makefile:** add darwin-arm64 building in Makefile 13 | * **npm:** bump react-scripts to v5 to fix "error:0308010C:digital envelope routines::unsupported" 14 | * **status:** bump dependencies version in yarn.lock 15 | * **status:** bump evergreen-ui to 6.5.1 16 | 17 | ### Ci 18 | 19 | * **github-action:** bump gh-actions version: go, node, actions of setup-go, setup-node and checkout 20 | * **github-action:** create Release via github action 21 | * **github-action:** bump node to 16.x and go to 1.18 in github action 22 | 23 | ### Docs 24 | 25 | * **changelog:** add changelog for version 0.6.0 26 | 27 | ### Feat 28 | 29 | * **client:** remove the unused parameter `*sync.Once` in `Handles.Wait` of client 30 | * **client:** add ability to catch error from any tasks in main goroutine 31 | * **version:** bump version to v0.6.0 32 | 33 | ### Fix 34 | 35 | * **client:** fix the incorrect error passing in client side data transmission 36 | * **server:** fix bug of server side crashing when using http/https proxy 37 | 38 | ### Merge 39 | 40 | * **client:** Merge pull request [#69](https://github.com/genshen/wssocks/issues/69) from genshen/remove-client-Wait-parameter 41 | * **client:** Merge pull request [#68](https://github.com/genshen/wssocks/issues/68) from genshen/fix-error-passing-in-client 42 | * **client:** Merge pull request [#54](https://github.com/genshen/wssocks/issues/54) from genshen/feature-client-wait-with-error 43 | * **github-action:** Merge pull request [#67](https://github.com/genshen/wssocks/issues/67) from genshen/ci-create-release 44 | * **server:** Merge pull request [#57](https://github.com/genshen/wssocks/issues/57) from genshen/fix-http-proxy-panic 45 | * **status:** Merge pull request [#45](https://github.com/genshen/wssocks/issues/45) from genshen/bump-status-web-dependencies 46 | 47 | 48 | 49 | ## [v0.5.0](https://github.com/genshen/wssocks/compare/v0.5.0-rc.3...v0.5.0) 50 | 51 | > 2021-01-25 52 | 53 | ### Build 54 | 55 | * **docker:** upgrade go/node/alpine versions in Dockerfile for docker building 56 | * **npm:** bump react dependency of status web page to v17 via cra, and enable pwa 57 | 58 | ### Ci 59 | 60 | * **gh-action:** upgrade go and node versions in github action building 61 | 62 | ### Docs 63 | 64 | * **changelog:** add changelog for version 0.5.0 65 | 66 | ### Feat 67 | 68 | * **version:** bump version to v0.5.0 69 | 70 | 71 | 72 | ## [v0.5.0-rc.3](https://github.com/genshen/wssocks/compare/v0.5.0-rc.2...v0.5.0-rc.3) 73 | 74 | > 2021-01-08 75 | 76 | ### Chore 77 | 78 | * **logs:** add description to func ProgressLog.SetLogBuffer 79 | 80 | ### Docs 81 | 82 | * **changelog:** add changelog for version 0.5.0-rc.3 83 | 84 | ### Feat 85 | 86 | * **client:** better error when there is error in Client.ListenAndServe (in wss/wssocks_client.go) 87 | * **plugin:** add "connection option" plugin interface 88 | * **version:** bump version to v0.5.0-rc.3 89 | 90 | ### Refactor 91 | 92 | * **plugin:** rename struct type "Plugin" to "Plugins" 93 | 94 | ### Revert 95 | 96 | * **version:** set protocol version back to 0x004, because it has been set in v0.5.0-beta 97 | 98 | 99 | 100 | ## [v0.5.0-rc.2](https://github.com/genshen/wssocks/compare/v0.5.0-rc.1...v0.5.0-rc.2) 101 | 102 | > 2021-01-02 103 | 104 | ### Build 105 | 106 | * **docker:** fix docker build error of "../web-build: no such file or directory" 107 | 108 | ### Docs 109 | 110 | * **changelog:** add changelog for version 0.5.0-rc.2 111 | 112 | ### Feat 113 | 114 | * **plugin:** return error, instead of calling log.Fatal, when the adding plugin exists 115 | * **version:** bump version to v0.5.0-rc.2 and increase version code 116 | 117 | ### Fix 118 | 119 | * **server:** fix compiling error of no package "github.com/genshen/wssocks/cmd/server/statik" 120 | 121 | ### Refactor 122 | 123 | * **plugin:** rename plugin api: AddPluginRedirect -> AddPluginRequest 124 | 125 | 126 | 127 | ## [v0.5.0-rc.1](https://github.com/genshen/wssocks/compare/v0.5.0-beta.3...v0.5.0-rc.1) 128 | 129 | > 2021-01-02 130 | 131 | ### Build 132 | 133 | * **npm:** upgrade npm dependencies 134 | * **status:** update dependencies (axios and evergreen-ui) for status page 135 | 136 | ### Ci 137 | 138 | * **action:** fix building error in github action while performing static files to go code generation 139 | 140 | ### Docs 141 | 142 | * **changelog:** add changelog for version 0.5.0-rc.1 143 | 144 | ### Feat 145 | 146 | * **client:** use http.DefaultTransport based Transport in http client for http dialing 147 | * **plugin:** we can change value of http transport (used for websocket dialing) in request plugin 148 | * **version:** bump version to v0.5.0-rc.1 149 | 150 | ### Fix 151 | 152 | * **client:** fix unexpected closing of client by using lock to Write and context canceling 153 | * **status:** fix building error of "data Object is possibly 'undefined'." 154 | 155 | ### Refactor 156 | 157 | * **cli:** move/split cli implementation of client and server to cmd directory 158 | * **client:** use more semantic variable names in client Options 159 | * **client:** move client connections closing to func `NotifyClose` in struct Handles 160 | * **client:** move sync.WaiitGroup passed to StartClient and Wait as a field of type Handles 161 | * **client:** split client setting-up func StartClient into multiple function calls 162 | * **plugin:** rename plugin ServerRedirect to RequestPlugin 163 | 164 | 165 | 166 | ## [v0.5.0-beta.3](https://github.com/genshen/wssocks/compare/v0.5.0-beta.2...v0.5.0-beta.3) 167 | 168 | > 2020-10-03 169 | 170 | ### Build 171 | 172 | * **docker:** update go version in docker building, and specific alpine version 173 | * **gomodule:** update dependencies version 174 | 175 | ### Chore 176 | 177 | * add go code report badge 178 | 179 | ### Docs 180 | 181 | * **changelog:** add changelog for version 0.5.0-beta.3 182 | 183 | ### Feat 184 | 185 | * **version:** bump version to v0.5.0-beta.3 186 | 187 | ### Fix 188 | 189 | * **server:** increase server read limit to 8 MiB to fix client exit with error "StatusMessageTooBig" 190 | 191 | ### Style 192 | 193 | * format project code: use tab as indent 194 | 195 | 196 | 197 | ## [v0.5.0-beta.2](https://github.com/genshen/wssocks/compare/v0.5.0-beta...v0.5.0-beta.2) 198 | 199 | > 2020-08-23 200 | 201 | ### Build 202 | 203 | * **status:** update dependencies in status page: evergreen-ui to 5.x and typescript to 3.9 204 | 205 | ### Chore 206 | 207 | * fix typo of UI (one for status page and one for connections table of client) 208 | 209 | ### Docs 210 | 211 | * **changelog:** add changelog for version 0.5.0-beta.2 212 | * **changelog:** add changelog generated by git-chglog tool 213 | * **readme:** add badges of docker image size, version and pulls 214 | * **readme:** add document of "connection key", "TSL/SSL support" and "server status" 215 | 216 | ### Feat 217 | 218 | * **client:** add flag for passing user defined http headers to websocket request and send to remote 219 | * **server:** add flags to server sub-command to support HTTPS/tls: -tls -tls-cert-file -tls-key-file 220 | * **server:** add ability of setting websocket serving path in server cli 221 | * **status:** show correct server address (including proctocol and base path) in status page 222 | * **version:** update version to v0.5.0-beta.2 223 | 224 | ### Fix 225 | 226 | * **server:** remove channel usage in server side to avoid panic "send on closed channel" 227 | 228 | ### Merge 229 | 230 | * Merge pull request [#25](https://github.com/genshen/wssocks/issues/25) from genshen/feature-ssl-tsl-support 231 | 232 | ### Pull Requests 233 | 234 | * Merge pull request [#27](https://github.com/genshen/wssocks/issues/27) from genshen/fix-server-crashed-if-client-killed 235 | 236 | 237 | 238 | ## [v0.5.0-beta](https://github.com/genshen/wssocks/compare/v0.4.1...v0.5.0-beta) 239 | 240 | > 2020-08-06 241 | 242 | ### Build 243 | 244 | * **docker:** update Dockerfile to inject git revision, build data, go ver into output of 'version' 245 | * **docker:** update Dockerfile to build static files of status-web and embed them into go binary 246 | * **gomodule:** update package github.com/genshen/cmds to avoid to call exist(2) in subcommand help 247 | * **gomodule:** update dependencies version 248 | * **makefile:** make target `clean` and `all` as '.PHONY' 249 | 250 | ### Chore 251 | 252 | * **status:** remove unused import in tsx files of 'status-web' for passing CI job 253 | 254 | ### Ci 255 | 256 | * **action:** update github action config to build 'status-web' and generate build files to go file 257 | 258 | ### Docs 259 | 260 | * **readme:** update building document in README.md 261 | * **readme:** add github action badge to README.md file 262 | 263 | ### Feat 264 | 265 | * use nhooyr.io/websocket as websocket lib to replace gorilla/websocket lib 266 | * **client:** set timeout for sending heartbeat to wssocks server 267 | * **client:** add skip-tls-verify to client to ignore tsl verification if server ssl/tls is enabled 268 | * **client:** use more specified context in establish and http proxy client to send data to server 269 | * **client:** use timeout context or cancel context to writing data to server 270 | * **logs:** log only proxy connecting and closing message when a TTY is not attached on client side 271 | * **server:** user can provide a user-specified connection key for authentication if auth is enabled 272 | * **server:** add hub collection to store all hubs and websocket connections created at server side 273 | * **server:** add --status flage to server cli to enable/disable status page 274 | * **server:** let func DefaultProxyEst.establish return ConnCloseByClient err it is closed by client 275 | * **status:** add static page for showing service status (not req data from server) 276 | * **status:** fetch server info (not include statistics data) from server and show in info board 277 | * **status:** show real server address and can copy address in status page 278 | * **status:** fetch statistics data (e.g. uptime, clients) from server and shown in status page 279 | * **status:** serve statis files of status page at server side 280 | * **status:** fxi theme color and github libk stype of status page 281 | * **status:** add logo files for status page 282 | * **version:** add ability to show git commit hash in version sub-command 283 | * **version:** write buildTime and go build version to version info when building via makefile 284 | * **version:** update version to v0.5.0-beta 285 | 286 | ### Fix 287 | 288 | * fix bug of `client close due to message too big` by increasing message read limit 289 | * **logs:** dont show proxy connections dashboard/table when a TTY is not attached on client side 290 | * **server:** add missing return statement in ServeHTTP after func websocker.Accept returns an error 291 | * **server:** fix bug of infinite run loop after closing websocket/hub 292 | * **server:** also perform tellClose if establish function return a non-ConnCloseByClient error 293 | * **server:** fix "send on closed channel" error by removing tellClose with channel 294 | * **server:** close proxy instances when closing hub (hub is closed when websocket is finished) 295 | * **status:** git axios url of requesting status data of server, it should be '/api/status' 296 | 297 | ### Improvement 298 | 299 | * **server:** generate random connection key in server side if connection key is enabled 300 | 301 | ### Merge 302 | 303 | * Merge pull request [#18](https://github.com/genshen/wssocks/issues/18) from genshen/feature-more-specified-context 304 | * Merge pull request [#15](https://github.com/genshen/wssocks/issues/15) from genshen/feature-web-status 305 | * Merge pull request [#13](https://github.com/genshen/wssocks/issues/13) from genshen/fix-send-on-closed-channel 306 | 307 | ### Refactor 308 | 309 | * rename file wss/client.go to wss/proxy_client_interface.go, move proxy client of https to new file 310 | * **client:** call Client.Reply without onDial function as callback, move onDial out of Reply 311 | * **client:** refactor func NewWebSocketClient: create WebSocketClient at the last line of func 312 | * **client:** declare *net.TCPConn forward and pass to client.Reply in incoming socks5 handle 313 | * **client:** use context to close heartbeat loop and client websocket income message listening 314 | * **client:** move inlined onDial callback in Client.ListenAndServe in wssocks_client.go as func 315 | * **logs:** refactor clear lines: move check IsTerminal to Writer from OS-platform's clearLines 316 | * **server:** rm ctx passed to establish in ProxyEstablish, use timeout ctx in establish instead 317 | * **server:** implement ServeHTTP interface for websocket serving to replace func handle 318 | * **server:** implement BufferedWR using more elegant approach 319 | * **server:** extract interface of establishing connections for socks5 and http proxy 320 | * **server:** move proxy connections maintaining in server side to hub 321 | 322 | ### Pull Requests 323 | 324 | * Merge pull request [#10](https://github.com/genshen/wssocks/issues/10) from genshen/websocket-lib-nhooyr.io/websocket 325 | * Merge pull request [#8](https://github.com/genshen/wssocks/issues/8) from DefinitlyEvil/master 326 | 327 | ### BREAKING CHANGE 328 | 329 | 330 | change first parameter in func BeforeRequest of ServerRedirect interface: from 331 | gorilla websocket dialer to pointer of http.Client. 332 | 333 | We also update the minimal go building version to 1.13 due to the usage of feature `errors.Is` to 334 | handle results of command line parsing. 335 | go version less then 1.12 (including 1.12) is not supported from this commit. 336 | 337 | 338 | 339 | ## [v0.4.1](https://github.com/genshen/wssocks/compare/v0.4.0...v0.4.1) 340 | 341 | > 2020-02-24 342 | 343 | ### Build 344 | 345 | * **docker:** update version of golang docker image to 1.13.8 346 | * **gomodule:** update go.sum file 347 | 348 | ### Fix 349 | 350 | * **client:** log error, instead of fatal, when httpserver listener returns error 351 | * **version:** fix core version not update, now update it to 0.4.1 352 | 353 | 354 | 355 | ## [v0.4.0](https://github.com/genshen/wssocks/compare/v0.3.0...v0.4.0) 356 | 357 | > 2020-02-11 358 | 359 | ### Build 360 | 361 | * **gomodule:** update dependencies: go-isatty and crypto 362 | 363 | ### Feat 364 | 365 | * **client:** handel kill signal in client closing 366 | * **client:** stop all connections or tasks and exit if one of tasks is finished 367 | * **client:** we can close client listener and heartbeat loop 368 | 369 | ### Refactor 370 | 371 | * **logs:** split connection reecords updating and log writing 372 | 373 | ### Style 374 | 375 | * **server:** code formating, change to use error wrapping 376 | 377 | 378 | 379 | ## [v0.3.0](https://github.com/genshen/wssocks/compare/v0.3.0-alpha.2...v0.3.0) 380 | 381 | > 2019-09-01 382 | 383 | ### Build 384 | 385 | * **gomodule:** update dependenies. 386 | 387 | ### Docs 388 | 389 | * update readme document and help messages. 390 | 391 | ### Feat 392 | 393 | * **client:** add basic feature of http proxy by http protocol. 394 | * **http:** add Hijacker http Proxy. 395 | * **log:** better log to show http proxy size and welcome messages. 396 | * **version:** add version negotiation plugin. 397 | * **version:** send and check compatible version code in version negotiation. 398 | * **version:** update version to 0.3.0 and protocol version to 0x003. 399 | 400 | ### Refactor 401 | 402 | * rename ServerData.Type to ServerData.Tag and rename unused param conn in WebSocketClient.NewProxy. 403 | * replace net.TCPConn with ReadWriteCloser in proxy connections container. 404 | * **client:** use callback(not channel) to receive data from server. 405 | * **client:** use channel to handle data received from proxy server. 406 | * **server:** remove server channel, use callback instead. 407 | * **server:** use channel to handle data received from proxy client. 408 | 409 | 410 | 411 | ## [v0.3.0-alpha.2](https://github.com/genshen/wssocks/compare/v0.3.0-alpha...v0.3.0-alpha.2) 412 | 413 | > 2019-08-27 414 | 415 | ### Docs 416 | 417 | * **readme:** update README, add document of http proxy. 418 | 419 | ### Feat 420 | 421 | * **logs:** better logs for version negotiation. 422 | * **version:** update version to 0.3.0-alpha.2 423 | 424 | ### Refactor 425 | 426 | * **logs:** better server logs of connections size. 427 | 428 | 429 | 430 | ## [v0.3.0-alpha](https://github.com/genshen/wssocks/compare/v0.2.1...v0.3.0-alpha) 431 | 432 | > 2019-08-26 433 | 434 | ### Build 435 | 436 | * add github workflows for building go. 437 | 438 | ### Feat 439 | 440 | * **http:** add http proxy support. 441 | * **http:** add https proxy support. 442 | * **version:** update version to 0.3.0-alpha. 443 | 444 | ### Merge 445 | 446 | * Merge pull request [#7](https://github.com/genshen/wssocks/issues/7) from genshen/feature-http-proxy 447 | * **ticker:** Merge pull request [#6](https://github.com/genshen/wssocks/issues/6) from genshen/remove-ticker 448 | 449 | ### Perf 450 | 451 | * **ticker:** remove ticker support in both client and server. 452 | 453 | ### Refactor 454 | 455 | * **client:** reorganize data types and functions in client implementation. 456 | * **client:** add interface for different proxy(socks5, http, https) in client. 457 | 458 | 459 | 460 | ## [v0.2.1](https://github.com/genshen/wssocks/compare/v0.2.0...v0.2.1) 461 | 462 | > 2019-06-16 463 | 464 | ### Build 465 | 466 | * **makefile:** add linux arm64 building in makefile. 467 | 468 | ### Feat 469 | 470 | * **docker:** add Dockerfile. 471 | * **logs:** better view for proxy connections dashboard. 472 | * **logs:** adapte client normal running logs to progress logs. 473 | * **logs:** add feature of progress logs of proxy connections. 474 | * **logs:** add logrus lib as log lib. 475 | * **version:** update version to 0.2.1 476 | 477 | ### Fix 478 | 479 | * **windows:** fix windows building problems. 480 | 481 | ### Merge 482 | 483 | * **logs:** Merge pull request [#5](https://github.com/genshen/wssocks/issues/5) from genshen/dev 484 | 485 | ### Refactor 486 | 487 | * **log:** use ssh/terminal pacakge to get terminal size. 488 | 489 | 490 | 491 | ## [v0.2.0](https://github.com/genshen/wssocks/compare/v0.1.0...v0.2.0) 492 | 493 | > 2019-04-11 494 | 495 | ### Build 496 | 497 | * **makefile:** add PACKAGE option to makefile and go build command. 498 | 499 | ### Docs 500 | 501 | * **license:** add MIT license. 502 | 503 | ### Feat 504 | 505 | * **plugin:** add *websocket.Dialer as func param in client plugin interface ServerRedirect. 506 | * **version:** change version name to v0.2.0 507 | 508 | ### Fix 509 | 510 | * **version:** client also sends its version information to server now. 511 | 512 | ### Refactor 513 | 514 | * **client:** refactor client code: move socks listerning code to file wss/wssocks.go 515 | 516 | ### Pull Requests 517 | 518 | * Merge pull request [#2](https://github.com/genshen/wssocks/issues/2) from genshen/dev 519 | 520 | 521 | 522 | ## [v0.1.0](https://github.com/genshen/wssocks/compare/v0.1.0-vpn...v0.1.0) 523 | 524 | > 2019-03-03 525 | 526 | ### Chore 527 | 528 | * **socks5:** add more legality checking for socks5 server side. 529 | 530 | ### Docs 531 | 532 | * **readme:** add README 533 | 534 | ### Feat 535 | 536 | * **heartbeat:** add feature of websocket heart beat. 537 | * **log:** add log for parsing error of cli. 538 | * **plugin:** add client Plugin interface. 539 | * **protocol:** add check of protocol version incompatibility. 540 | 541 | ### Fix 542 | 543 | * **close:** add more connection close calling. 544 | * **server:** fix connection lose error(read connection EOF) 545 | 546 | ### Merge 547 | 548 | * Merge pull request [#1](https://github.com/genshen/wssocks/issues/1) from genshen/dev 549 | 550 | ### Refactor 551 | 552 | * **datatypes:** rename WriteProxyMessage func in ConcurrentWebSocket and move type Base64WSBuff 553 | * **datatypes:** combine data typeRequestMessage anf ProxyData into one type. 554 | * **server:** return error in dispatchMessage not nil and move mutex forward in Flush. 555 | * **server:** remove unnercessary terms in ServerWS.map[]Connector 556 | * **server:** add function NewServerWS and rename NewConn to AddConn. 557 | 558 | ### Style 559 | 560 | * reorganize the code structural. 561 | * move code position like println and comments. 562 | * move directory from ws-socks to wssocks. 563 | 564 | 565 | 566 | ## v0.1.0-vpn 567 | 568 | > 2019-02-08 569 | 570 | ### Feat 571 | 572 | * **cmd:** add user command line option to set ticker. 573 | * **server:** add ticker and non-ticker option implementation for proxy server. 574 | 575 | ### Fix 576 | 577 | * fixed known bugs 578 | 579 | ### Perf 580 | 581 | * **websocket:** use one websocket for all connections, instead of one websocket for one socks. 582 | 583 | ### Refactor 584 | 585 | * rename and remove variables 586 | * **client:** rename client command variable, and rename ws-socks/client.go -> ws-socks/socks5_server.go 587 | 588 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build method: just run `docker build --rm -t genshen/wssocks .` 2 | 3 | # build frontend code 4 | FROM node:14.15.4-alpine3.12 AS web-builder 5 | 6 | COPY status-web web/ 7 | 8 | RUN cd web \ 9 | && yarn install \ 10 | && yarn build 11 | 12 | ## build go binary 13 | FROM golang:1.15.7-alpine3.13 AS builder 14 | 15 | ARG PACKAGE=github.com/genshen/wssocks 16 | ARG BUILD_FLAG="-X 'github.com/genshen/wssocks/version.buildHash=`git rev-parse HEAD`' \ 17 | -X 'github.com/genshen/wssocks/version.buildTime=`date`' \ 18 | -X 'github.com/genshen/wssocks/version.buildGoVersion=`go version | cut -f 3,4 -d\" \"`'" 19 | 20 | RUN apk add --no-cache git \ 21 | && go get -u github.com/rakyll/statik 22 | 23 | COPY ./ /go/src/${PACKAGE}/ 24 | COPY --from=web-builder web/build /go/src/github.com/genshen/wssocks/web-build/ 25 | 26 | RUN cd ./src/${PACKAGE} \ 27 | && cd cmd/server \ 28 | && statik -src=../../web-build \ 29 | && cd ../../ \ 30 | && go build -ldflags "${BUILD_FLAG}" -o wssocks ${PACKAGE} \ 31 | && go install 32 | 33 | ## copy binary 34 | FROM alpine:3.13.0 35 | 36 | ARG HOME="/home/wssocks" 37 | 38 | RUN adduser -D wssocks -h ${HOME} 39 | 40 | COPY --from=builder --chown=wssocks /go/bin/wssocks ${HOME}/wssocks 41 | 42 | WORKDIR ${HOME} 43 | USER wssocks 44 | 45 | ENTRYPOINT ["./wssocks"] 46 | CMD ["--help"] 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 genshen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGE=github.com/genshen/wssocks 2 | 3 | .PHONY: clean all 4 | 5 | 6 | LDFLAGS= -v -ldflags "-X 'github.com/genshen/wssocks/version.buildHash=`git rev-parse HEAD`' \ 7 | -X 'github.com/genshen/wssocks/version.buildTime=`date`' \ 8 | -X 'github.com/genshen/wssocks/version.buildGoVersion=`go version | cut -f 3,4 -d" "`'" 9 | 10 | all: wssocks-linux-amd64 wssocks-linux-arm64 wssocks-darwin-amd64 wssocks-darwin-arm64 wssocks-windows-amd64.exe 11 | 12 | wssocks-linux-amd64: 13 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ${LDFLAGS} -o wssocks-linux-amd64 ${PACKAGE} 14 | 15 | wssocks-linux-arm64: 16 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ${LDFLAGS} -o wssocks-linux-arm64 ${PACKAGE} 17 | 18 | wssocks-darwin-arm64: 19 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build ${LDFLAGS} -o wssocks-darwin-arm64 ${PACKAGE} 20 | wssocks-darwin-amd64: 21 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build ${LDFLAGS} -o wssocks-darwin-amd64 ${PACKAGE} 22 | 23 | wssocks-windows-amd64.exe: 24 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build ${LDFLAGS} -o wssocks-windows-amd64.exe ${PACKAGE} 25 | 26 | wssocks : 27 | go build -o wssocks 28 | 29 | clean: 30 | rm -f wssocks-linux-amd64 wssocks-linux-arm64 wssocks-darwin-arm64 wssocks-darwin-amd64 wssocks-windows-amd64.exe 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wssocks 2 | 3 | ![build](https://github.com/genshen/wssocks/workflows/Go/badge.svg) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/genshen/wssocks)](https://goreportcard.com/report/github.com/genshen/wssocks) 5 | ![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/genshen/wssocks?logo=docker&sort=date) 6 | ![Docker Image Version (latest semver)](https://img.shields.io/docker/v/genshen/wssocks?sort=semver&logo=docker) 7 | ![Docker Pulls](https://img.shields.io/docker/pulls/genshen/wssocks?logo=docker) 8 | 9 | > socks5 over websocket. 10 | 11 | wssocks can proxy TCP and UDP(not implemented currently) connections via socks5. But the socks5 data is wrapped in websockets and then sent to server. 12 | 13 | ## Features 14 | - **Transfer data through firewalls** 15 | In some network environment, due to the restricts of firewalls, only http(s)/websocket is allowed. wssocks is mainly useful for passing through firewalls. We can access the inner netwrok (such as ssh) behind the firewalls via socks protocol wrapped in websockets. 16 | - **High performance** 17 | wssocks only create one TCP connection (websocket) per client to handle multiple socks5 connections, which achieves much higher performance. 18 | - **Easy to use** 19 | No configures, no dependences, just a single executable including client and server. 20 | 21 | ## Build and install 22 | ```bash 23 | cd status-web; yarn install; yarn build; cd ../ 24 | go get -u github.com/rakyll/statik 25 | cd cmd/server; statik --src=../../status-web/build/; cd ../../ 26 | go build 27 | go install 28 | ``` 29 | You can also download it from [release](https://github.com/genshen/wssocks/releases) page. 30 | 31 | ## Quick start 32 | 33 | ### server side 34 | ```bash 35 | wssocks server --addr :1088 36 | ``` 37 | ### client side 38 | ```bash 39 | wssocks client --addr :1080 --remote ws://example.com:1088 40 | # using ssh to connect to example.com which may be behind firewalls. 41 | ssh -o ProxyCommand='nc -x 127.0.0.1:1080 %h %p' user@example.com 42 | ``` 43 | 44 | And set your socks5 server address as `:1080` in your socks5 client (such as [proxifier](https://www.proxifier.com/) or proxy setting in mac's network preferences) if you need to use socks5 proxy in more situations, not only `ssh` in terminal. 45 | 46 | ## Advanced usage 47 | ### enable http and https proxy 48 | You can also enable http and https proxy by `--http` option(in client side) 49 | if http(s) proxy in server side is enabled: 50 | 51 | ```bash 52 | # client siede 53 | wssocks client --addr :1080 --remote ws://example.com:1088 --http 54 | ``` 55 | The http proxy listen address is specified by `--http-addr` in client side (default value is `:1086`), 56 | and https proxy listen address is the same as socks5 proxy listen address(specified by `--addr` option). 57 | 58 | Then you can set server address of http and https proxy as `:1080` 59 | in your http(s) proxy client (e.g. mac's network preferences). 60 | 61 | note: http(s) proxy is enabled by default in server side, you can disable it in server side 62 | by `wssocks server --addr :1088 --http=false` . 63 | 64 | ### Connection key 65 | In some cases, you don't want anyone to connect to your wssocks server. 66 | You can use connection key to prevent the clients who don't have correct connection authentication. 67 | At server side, just enable flag `--auth`, e.g.: 68 | ```bash 69 | wssocks server --addr :1088 --auth 70 | ``` 71 | Then it will generate a random connection key. 72 | You can also specific a customized connection key via flag `--auth_key`. 73 | At client side, connect to wssocks server via the connection key: 74 | ```bash 75 | wssocks client --remote ws://example.com:1088 --key YOUR_CONNECTION_KEY 76 | ``` 77 | 78 | ### TSL/SSL support 79 | Method 1: 80 | In version 0.5.0, transfering data between wssocks client and wssocks server under TSL/SSL protocol is supported. 81 | 82 | At server side, use `--tsl` flag to enable TSL/SSL support, 83 | and specific path of certificate via `--tls-cert-file` and `--tls-key-file`. 84 | e.g. 85 | ```bash 86 | wssocks server --addr :1088 --tsl --tls-cert-file /path/of/certificate-file --tls-key-file /path/of/certificate-key-file 87 | ``` 88 | At client side, we can then use `wss://example.com:1088` as remote address, for instance. 89 | 90 | Method 2: 91 | Use nginx reverse proxy, enable ssl and specific certificate file and certificate key file in nginx config. 92 | For more information, see issue [#11](https://github.com/genshen/wssocks/issues/11#issuecomment-669324542)). 93 | 94 | ### Server status 95 | In version 0.5.0, we can enable statue page of server by passing `--status` flag at server side (status page is disabled by default). 96 | Then, you can get server status in your browser of client side, by visiting http://example.com:1088/status (where example.com:1088 is the address of wssocks server). 97 | 98 | ### Help 99 | ``` 100 | wssocks --help 101 | wssocks client --help 102 | wssocks server --help 103 | ``` 104 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "github.com/genshen/wssocks/wss" 9 | "github.com/genshen/wssocks/wss/term_view" 10 | log "github.com/sirupsen/logrus" 11 | "golang.org/x/crypto/ssh/terminal" 12 | "golang.org/x/sync/errgroup" 13 | "net" 14 | "net/http" 15 | "net/url" 16 | "os" 17 | "os/signal" 18 | "sync" 19 | "time" 20 | ) 21 | 22 | func NewHttpClient() (*http.Client, *http.Transport) { 23 | // set to use default Http Transport 24 | tr := http.Transport{ 25 | Proxy: http.ProxyFromEnvironment, 26 | DialContext: (&net.Dialer{ 27 | Timeout: 30 * time.Second, 28 | KeepAlive: 30 * time.Second, 29 | DualStack: true, 30 | }).DialContext, 31 | ForceAttemptHTTP2: true, 32 | MaxIdleConns: 100, 33 | IdleConnTimeout: 90 * time.Second, 34 | TLSHandshakeTimeout: 10 * time.Second, 35 | ExpectContinueTimeout: 1 * time.Second, 36 | } 37 | 38 | httpClient := http.Client{ 39 | Transport: &tr, 40 | } 41 | return &httpClient, &tr 42 | } 43 | 44 | type Options struct { 45 | LocalSocks5Addr string // local listening address 46 | HttpEnabled bool // enable http and https proxy 47 | LocalHttpAddr string // listen address of http and https(if it is enabled) 48 | RemoteUrl *url.URL // url of server 49 | RemoteHeaders http.Header // parsed websocket headers (not presented in flag). 50 | ConnectionKey string // connection key for authentication 51 | SkipTLSVerify bool // skip TSL verify 52 | } 53 | 54 | type Handles struct { 55 | wsc *wss.WebSocketClient 56 | hb *wss.HeartBeat 57 | httpServer *http.Server 58 | cl *wss.Client 59 | closed bool 60 | eg *errgroup.Group 61 | } 62 | 63 | func NewClientHandles() *Handles { 64 | eg, _ := errgroup.WithContext(context.Background()) 65 | return &Handles{closed: true, eg: eg} 66 | } 67 | 68 | // NotifyClose send closing message to all running tasks 69 | func (hdl *Handles) NotifyClose(once *sync.Once, wait bool) { 70 | if hdl.closed { 71 | return 72 | } 73 | hdl.closed = true 74 | 75 | // stop tasks in signal 76 | once.Do(func() { 77 | if hdl.cl != nil { 78 | hdl.cl.Close(wait) 79 | } 80 | if hdl.httpServer != nil { 81 | hdl.httpServer.Shutdown(context.TODO()) 82 | } 83 | if hdl.hb != nil { 84 | hdl.hb.Close() 85 | } 86 | if hdl.wsc != nil { 87 | hdl.wsc.Close() 88 | } 89 | }) 90 | } 91 | 92 | // CreateServerConn create a server websocket connection based on user options. 93 | func (hdl *Handles) CreateServerConn(c *Options, ctx context.Context) (*wss.WebSocketClient, error) { 94 | if c.ConnectionKey != "" { 95 | c.RemoteHeaders.Set("Key", c.ConnectionKey) 96 | } 97 | 98 | httpClient, transport := NewHttpClient() 99 | 100 | if c.RemoteUrl.Scheme == "wss" && c.SkipTLSVerify { 101 | // ignore insecure verify 102 | transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 103 | log.Warnln("Warning: you have skipped verification of the server's certificate chain and host name. " + 104 | "Then client will accepts any certificate presented by the server and any host name in that certificate. " + 105 | "In this mode, TLS is susceptible to man-in-the-middle attacks.") 106 | } 107 | 108 | // load and use option plugin 109 | if clientPlugin.HasOptionPlugin() { 110 | if err := clientPlugin.OptionPlugin.OnOptionSet(*c); err != nil { 111 | return nil, err 112 | } 113 | } 114 | 115 | // loading and execute plugin 116 | if clientPlugin.HasRequestPlugin() { 117 | // in the plugin, we may add http header/dialer and modify remote address. 118 | if err := clientPlugin.RequestPlugin.BeforeRequest(httpClient, transport, c.RemoteUrl, &c.RemoteHeaders); err != nil { 119 | return nil, err 120 | } 121 | } 122 | 123 | // start websocket connection (to remote server). 124 | wsc, err := wss.NewWebSocketClient(ctx, c.RemoteUrl.String(), httpClient, c.RemoteHeaders) 125 | if err != nil { 126 | return nil, fmt.Errorf("establishing connection error: %w", err) 127 | } 128 | // todo chan for wsc and tcp accept 129 | hdl.wsc = wsc 130 | return wsc, nil 131 | } 132 | 133 | func (hdl *Handles) NegotiateVersion(ctx context.Context, remoteUrl string) error { 134 | // negotiate version 135 | if version, err := wss.ExchangeVersion(ctx, hdl.wsc.WsConn); err != nil { 136 | return err 137 | } else { 138 | if clientPlugin.HasVersionPlugin() { 139 | if err := clientPlugin.VersionPlugin.OnServerVersion(version); err != nil { 140 | return err 141 | } 142 | } else { 143 | log.WithFields(log.Fields{ 144 | "compatible version code": version.CompVersion, 145 | "version code": version.VersionCode, 146 | "version number": version.Version, 147 | }).Info("server version") 148 | 149 | // client protocol version must eq or smaller than server version (newer client is not allowed) 150 | // And, compatible version is the lowest version for client. 151 | if version.CompVersion > wss.VersionCode || wss.VersionCode > version.VersionCode { 152 | return errors.New("incompatible protocol version of client and server") 153 | } 154 | if version.Version != wss.CoreVersion { 155 | log.WithFields(log.Fields{ 156 | "client wssocks version": wss.CoreVersion, 157 | "server wssocks version": version.Version, 158 | }).Warning("different version of client and server wssocks") 159 | } 160 | if version.EnableStatusPage { 161 | if endpoint, err := url.Parse(remoteUrl + "/status"); err != nil { 162 | return err 163 | } else { 164 | endpoint.Scheme = "http" 165 | log.WithFields(log.Fields{ 166 | "endpoint": endpoint.String(), 167 | }).Infoln("server status is available, you can visit the endpoint to get status.") 168 | } 169 | } 170 | } 171 | } 172 | return nil 173 | } 174 | 175 | func (hdl *Handles) StartClient(c *Options, once *sync.Once) { 176 | // stop all connections or tasks, if one of tasks is finished. 177 | closeAll := func() { 178 | if hdl.cl != nil { 179 | hdl.cl.Close(false) 180 | } 181 | if hdl.httpServer != nil { 182 | hdl.httpServer.Shutdown(context.TODO()) 183 | } 184 | if hdl.hb != nil { 185 | hdl.hb.Close() 186 | } 187 | if hdl.wsc != nil { 188 | hdl.wsc.Close() 189 | } 190 | } 191 | 192 | // start websocket message listen. 193 | hdl.eg.Go(func() error { 194 | defer once.Do(closeAll) 195 | if err := hdl.wsc.ListenIncomeMsg(1 << 29); err != nil { 196 | return fmt.Errorf("error websocket read %w", err) 197 | } 198 | return nil 199 | }) 200 | // send heart beats. 201 | heartbeat, hbCtx := wss.NewHeartBeat(hdl.wsc) 202 | hdl.hb = heartbeat 203 | hdl.eg.Go(func() error { 204 | defer once.Do(closeAll) 205 | if err := hdl.hb.Start(hbCtx, time.Minute); err != nil { 206 | return fmt.Errorf("heartbeat ending %w", err) 207 | } 208 | return nil 209 | }) 210 | 211 | record := wss.NewConnRecord() 212 | if terminal.IsTerminal(int(os.Stdout.Fd())) { 213 | // if it is tty, use term_view as output, and set onChange function to update output 214 | plog := term_view.NewPLog(record) 215 | log.SetOutput(plog) // change log stdout to plog 216 | 217 | record.OnChange = func(wss.ConnStatus) { 218 | // update log 219 | plog.SetLogBuffer(record) // call Writer.Write() to set log data into buffer 220 | plog.Writer.Flush(nil) // flush buffer 221 | } 222 | } else { 223 | record.OnChange = func(status wss.ConnStatus) { 224 | if status.IsNew { 225 | log.WithField("address", status.Address).Traceln("new proxy connection") 226 | } else { 227 | log.WithField("address", status.Address).Traceln("close proxy connection") 228 | } 229 | } 230 | } 231 | 232 | // http listening 233 | if c.HttpEnabled { 234 | log.WithField("http listen address", c.LocalHttpAddr). 235 | Info("listening on local address for incoming proxy requests.") 236 | hdl.eg.Go(func() error { 237 | defer once.Do(closeAll) 238 | handle := wss.NewHttpProxy(hdl.wsc, record) 239 | hdl.httpServer = &http.Server{Addr: c.LocalHttpAddr, Handler: &handle} 240 | if err := hdl.httpServer.ListenAndServe(); err != nil { 241 | return err 242 | } 243 | return nil 244 | }) 245 | } 246 | 247 | // start listen for socks5 and https connection. 248 | hdl.cl = wss.NewClient() 249 | hdl.eg.Go(func() error { 250 | defer once.Do(closeAll) 251 | if err := hdl.cl.ListenAndServe(record, hdl.wsc, c.LocalSocks5Addr, c.HttpEnabled, func() { 252 | if c.HttpEnabled { 253 | log.WithField("socks5 listen address", c.LocalSocks5Addr). 254 | WithField("https listen address", c.LocalSocks5Addr). 255 | Info("listening on local address for incoming proxy requests.") 256 | } else { 257 | log.WithField("socks5 listen address", c.LocalSocks5Addr). 258 | Info("listening on local address for incoming proxy requests.") 259 | } 260 | }); err != nil { 261 | return fmt.Errorf("start client error %w", err) 262 | } 263 | return nil 264 | }) 265 | 266 | hdl.closed = false 267 | } 268 | 269 | 270 | // Wait waits an error in client connection. 271 | // If the connection lost or any other connection error happens, Wait will return an error. 272 | func (hdl *Handles) Wait() error { 273 | return hdl.eg.Wait() 274 | } 275 | 276 | // CliWait can be used in cli env to wait service to finish. 277 | // similar to Wait, but CliWait is usually used in cli. 278 | func (hdl *Handles) CliWait(once *sync.Once) { 279 | go func() { 280 | firstInterrupt := true 281 | c := make(chan os.Signal, 1) 282 | signal.Notify(c, os.Interrupt) 283 | for { // accept multiple signal 284 | select { 285 | case <-c: 286 | if firstInterrupt { 287 | log.Println("press CTRL+C to force exit") 288 | firstInterrupt = false 289 | hdl.NotifyClose(once, true) 290 | } else { 291 | os.Exit(0) 292 | } 293 | } 294 | } 295 | }() 296 | 297 | // wait all tasks finished 298 | if err := hdl.eg.Wait(); err != nil { 299 | log.Errorln(err) 300 | } 301 | 302 | // about exit: 1. press ctrl+c, it will wait active connection to finish. 303 | // 2. press twice, force exit. 304 | // 3. one of tasks error, exit immediately. 305 | // 4. close server, then client exit (the same as 3). 306 | } 307 | -------------------------------------------------------------------------------- /client/client_plugin.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "github.com/genshen/wssocks/wss" 6 | "net/http" 7 | "net/url" 8 | ) 9 | 10 | // pass read-only connection option to OnOptionSet when options are set. 11 | // we can check connection options by returning an error and may set RemoteUrl here. 12 | type OptionPlugin interface { 13 | OnOptionSet(options Options) error 14 | } 15 | 16 | // in the plugin, we may add http header and modify remote address. 17 | type RequestPlugin interface { 18 | BeforeRequest(hc *http.Client, transport *http.Transport, url *url.URL, header *http.Header) error 19 | } 20 | 21 | type VersionPlugin interface { 22 | OnServerVersion(ver wss.VersionNeg) error 23 | } 24 | 25 | // Plugins is a collection of all possible plugins on client 26 | type Plugins struct { 27 | OptionPlugin OptionPlugin 28 | RequestPlugin RequestPlugin 29 | VersionPlugin VersionPlugin 30 | } 31 | 32 | var ErrPluginOccupied = errors.New("the plugin is occupied by another plugin") 33 | 34 | // check whether the option plugin has been added. 35 | // this plugin can only be at most one instance. 36 | func (plugin *Plugins) HasOptionPlugin() bool { 37 | return plugin.OptionPlugin != nil 38 | } 39 | 40 | // check whether the request plugin has been added. 41 | // this plugin can only be at most one instance. 42 | func (plugin *Plugins) HasRequestPlugin() bool { 43 | return plugin.RequestPlugin != nil 44 | } 45 | 46 | func (plugin *Plugins) HasVersionPlugin() bool { 47 | return plugin.VersionPlugin != nil 48 | } 49 | 50 | var clientPlugin Plugins 51 | 52 | // add an option plugin 53 | func AddPluginOption(opPlugin OptionPlugin) error { 54 | if clientPlugin.OptionPlugin != nil { 55 | return ErrPluginOccupied 56 | } 57 | clientPlugin.OptionPlugin = opPlugin 58 | return nil 59 | } 60 | 61 | // add a request plugin 62 | func AddPluginRequest(redirect RequestPlugin) error { 63 | if clientPlugin.RequestPlugin != nil { 64 | return ErrPluginOccupied 65 | } 66 | clientPlugin.RequestPlugin = redirect 67 | return nil 68 | } 69 | 70 | // add a version plugin 71 | func AddPluginVersion(verPlugin VersionPlugin) error { 72 | if clientPlugin.VersionPlugin != nil { 73 | return ErrPluginOccupied 74 | } 75 | clientPlugin.VersionPlugin = verPlugin 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /cmd/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/genshen/cmds" 15 | cl "github.com/genshen/wssocks/client" 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | const CommandNameClient = "client" 20 | 21 | var clientCommand = &cmds.Command{ 22 | Name: CommandNameClient, 23 | Summary: "run as client mode", 24 | Description: "run as client program.", 25 | CustomFlags: false, 26 | HasOptions: true, 27 | } 28 | 29 | type listFlags []string 30 | 31 | func (l *listFlags) String() string { 32 | return "my string representation" 33 | } 34 | 35 | func (l *listFlags) Set(value string) error { 36 | *l = append(*l, value) 37 | return nil 38 | } 39 | 40 | func init() { 41 | var client client 42 | fs := flag.NewFlagSet(CommandNameClient, flag.ContinueOnError) 43 | clientCommand.FlagSet = fs 44 | clientCommand.FlagSet.StringVar(&client.address, "addr", ":1080", `listen address of socks5 proxy.`) 45 | clientCommand.FlagSet.BoolVar(&client.http, "http", false, `enable http and https proxy.`) 46 | clientCommand.FlagSet.StringVar(&client.httpAddr, "http-addr", ":1086", `listen address of http proxy (if enabled).`) 47 | clientCommand.FlagSet.StringVar(&client.remote, "remote", "", `server address and port(e.g: ws://example.com:1088).`) 48 | clientCommand.FlagSet.StringVar(&client.key, "key", "", `connection key.`) 49 | clientCommand.FlagSet.Var(&client.headers, "ws-header", `list of user defined http headers in websocket request. 50 | (e.g: --ws-header "X-Custom-Header=some-value" --ws-header "X-Second-Header=another-value")`) 51 | clientCommand.FlagSet.BoolVar(&client.skipTLSVerify, "skip-tls-verify", false, `skip verification of the server's certificate chain and host name.`) 52 | 53 | clientCommand.FlagSet.Usage = clientCommand.Usage // use default usage provided by cmds.Command. 54 | clientCommand.Runner = &client 55 | 56 | cmds.AllCommands = append(cmds.AllCommands, clientCommand) 57 | } 58 | 59 | type client struct { 60 | address string // local listening address 61 | http bool // enable http and https proxy 62 | httpAddr string // listen address of http and https(if it is enabled) 63 | remote string // string usr of server 64 | remoteUrl *url.URL // url of server 65 | headers listFlags // websocket headers passed from user. 66 | remoteHeaders http.Header // parsed websocket headers (not presented in flag). 67 | key string 68 | skipTLSVerify bool 69 | } 70 | 71 | func (c *client) PreRun() error { 72 | // check remote address 73 | if c.remote == "" { 74 | return errors.New("empty remote address") 75 | } 76 | if u, err := url.Parse(c.remote); err != nil { 77 | return err 78 | } else { 79 | c.remoteUrl = u 80 | } 81 | 82 | if c.http { 83 | log.Info("http(s) proxy is enabled.") 84 | } else { 85 | log.Info("http(s) proxy is disabled.") 86 | } 87 | 88 | // check header format. 89 | c.remoteHeaders = make(http.Header) 90 | for _, header := range c.headers { 91 | index := strings.IndexByte(header, '=') 92 | if index == -1 || index+1 == len(header) { 93 | return fmt.Errorf("bad http header in websocket request: %s", header) 94 | } 95 | hKey := ([]byte(header))[:index] 96 | hValue := ([]byte(header))[index+1:] 97 | c.remoteHeaders.Add(string(hKey), string(hValue)) 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func (c *client) Run() error { 104 | log.WithFields(log.Fields{ 105 | "remote": c.remoteUrl.String(), 106 | }).Info("connecting to wssocks server.") 107 | 108 | options := cl.Options{ 109 | LocalSocks5Addr: c.address, 110 | HttpEnabled: c.http, 111 | LocalHttpAddr: c.httpAddr, 112 | RemoteUrl: c.remoteUrl, 113 | RemoteHeaders: c.remoteHeaders, 114 | ConnectionKey: c.key, 115 | SkipTLSVerify: c.skipTLSVerify, 116 | } 117 | hdl := cl.NewClientHandles() 118 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) // fixme 119 | defer cancel() 120 | 121 | wsc, err := hdl.CreateServerConn(&options, ctx) 122 | if err != nil { 123 | return err 124 | } 125 | // server connect successfully 126 | log.WithFields(log.Fields{ 127 | "remote": c.remoteUrl.String(), 128 | }).Info("connected to wssocks server.") 129 | defer wsc.WSClose() 130 | 131 | if err := hdl.NegotiateVersion(ctx, c.remote); err != nil { 132 | return err 133 | } 134 | 135 | var once sync.Once 136 | hdl.StartClient(&options, &once) 137 | hdl.CliWait(&once) 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /cmd/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "flag" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/genshen/cmds" 11 | _ "github.com/genshen/wssocks/cmd/server/statik" 12 | "github.com/genshen/wssocks/wss" 13 | "github.com/genshen/wssocks/wss/status" 14 | "github.com/rakyll/statik/fs" 15 | log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | var serverCommand = &cmds.Command{ 19 | Name: "server", 20 | Summary: "run as server mode", 21 | Description: "run as server program.", 22 | CustomFlags: false, 23 | HasOptions: true, 24 | } 25 | 26 | func init() { 27 | var s server 28 | fs := flag.NewFlagSet("server", flag.ContinueOnError) 29 | serverCommand.FlagSet = fs 30 | serverCommand.FlagSet.StringVar(&s.address, "addr", ":1088", `listen address.`) 31 | serverCommand.FlagSet.StringVar(&s.wsBasePath, "ws_base_path", "/", "base path for serving websocket.") 32 | serverCommand.FlagSet.BoolVar(&s.http, "http", true, `enable http and https proxy.`) 33 | serverCommand.FlagSet.BoolVar(&s.authEnable, "auth", false, `enable/disable connection authentication.`) 34 | serverCommand.FlagSet.StringVar(&s.authKey, "auth_key", "", "connection key for authentication. \nIf not provided, it will generate one randomly.") 35 | serverCommand.FlagSet.BoolVar(&s.tls, "tls", false, "enable/disable HTTPS/TLS support of server.") 36 | serverCommand.FlagSet.StringVar(&s.tlsCertFile, "tls-cert-file", "", "path of certificate file if HTTPS/tls is enabled.") 37 | serverCommand.FlagSet.StringVar(&s.tlsKeyFile, "tls-key-file", "", "path of private key file if HTTPS/tls is enabled.") 38 | serverCommand.FlagSet.BoolVar(&s.status, "status", false, `enable/disable service status page.`) 39 | serverCommand.FlagSet.Usage = serverCommand.Usage // use default usage provided by cmds.Command. 40 | 41 | serverCommand.Runner = &s 42 | cmds.AllCommands = append(cmds.AllCommands, serverCommand) 43 | } 44 | 45 | type server struct { 46 | address string 47 | wsBasePath string // base path for serving websocket and status page 48 | http bool // enable http and https proxy 49 | authEnable bool // enable authentication connection key 50 | authKey string // the connection key if authentication is enabled 51 | tls bool // enable/disable HTTPS/tls support of server. 52 | tlsCertFile string // path of certificate file if HTTPS/tls is enabled. 53 | tlsKeyFile string // path of private key file if HTTPS/tls is enabled. 54 | status bool // enable service status page 55 | } 56 | 57 | func genRandBytes(n int) ([]byte, error) { 58 | b := make([]byte, n) 59 | _, err := rand.Read(b) 60 | // Note that err == nil only if we read len(b) bytes. 61 | if err != nil { 62 | return nil, err 63 | } 64 | return b, nil 65 | } 66 | 67 | func (s *server) PreRun() error { 68 | if s.authEnable && s.authKey == "" { 69 | log.Trace("empty authentication key provided, now it will generate a random authentication key.") 70 | b, err := genRandBytes(12) 71 | if err != nil { 72 | return err 73 | } 74 | s.authKey = strings.ToUpper(hex.EncodeToString(b)) 75 | } 76 | // set base url 77 | if s.wsBasePath == "" { 78 | s.wsBasePath = "/" 79 | } 80 | // complete prefix and suffix 81 | if !strings.HasPrefix(s.wsBasePath, "/") { 82 | s.wsBasePath = "/" + s.wsBasePath 83 | } 84 | if !strings.HasSuffix(s.wsBasePath, "/") { 85 | s.wsBasePath = s.wsBasePath + "/" 86 | } 87 | return nil 88 | } 89 | 90 | func (s *server) Run() error { 91 | config := wss.WebsocksServerConfig{EnableHttp: s.http, EnableConnKey: s.authEnable, ConnKey: s.authKey, EnableStatusPage: s.status} 92 | hc := wss.NewHubCollection() 93 | 94 | http.Handle(s.wsBasePath, wss.NewServeWS(hc, config)) 95 | if s.status { 96 | statikFS, err := fs.New() 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | http.Handle("/status/", http.StripPrefix("/status", http.FileServer(statikFS))) 101 | http.Handle("/api/status/", status.NewStatusHandle(hc, s.http, s.authEnable, s.wsBasePath)) 102 | } 103 | 104 | if s.authEnable { 105 | log.Info("connection authentication key: ", s.authKey) 106 | } 107 | if s.status { 108 | log.Info("service status page is enabled at `/status` endpoint") 109 | } 110 | 111 | listenAddrToLog := s.address + s.wsBasePath 112 | if s.wsBasePath == "/" { 113 | listenAddrToLog = s.address 114 | } 115 | log.WithFields(log.Fields{ 116 | "listen address": listenAddrToLog, 117 | }).Info("listening for incoming messages.") 118 | 119 | if s.tls { 120 | log.Fatal(http.ListenAndServeTLS(s.address, s.tlsCertFile, s.tlsKeyFile, nil)) 121 | } else { 122 | log.Fatal(http.ListenAndServe(s.address, nil)) 123 | } 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /cmd/server/statik/keep.go: -------------------------------------------------------------------------------- 1 | package statik 2 | 3 | // This empty file is localed here to prevent compiling errors. 4 | // Otherwise, this package is imported by other package but it is empty. 5 | func init() {} 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/genshen/wssocks 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/genshen/cmds v0.0.0-20200505065256-d4c52690e15b 7 | github.com/mattn/go-isatty v0.0.19 8 | github.com/rakyll/statik v0.1.7 9 | github.com/segmentio/ksuid v1.0.4 10 | github.com/sirupsen/logrus v1.9.3 11 | golang.org/x/crypto v0.11.0 12 | golang.org/x/sync v0.3.0 13 | nhooyr.io/websocket v1.8.7 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/genshen/cmds v0.0.0-20200505065256-d4c52690e15b h1:Y9zJTS5q74DL49zeEED9FBI5Q+x46WKAlsiFckMM270= 5 | github.com/genshen/cmds v0.0.0-20200505065256-d4c52690e15b/go.mod h1:aYnllk8ZNn1U/vr+oRh5EYtGqO3tmLQw1Hj3XesFx68= 6 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 7 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 8 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 9 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 10 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 11 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 12 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 13 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 14 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 15 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 16 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 17 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 18 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= 19 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 20 | github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= 21 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 22 | github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= 23 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 24 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 25 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= 26 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 27 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 28 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 29 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 30 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 31 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 32 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 33 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 34 | github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= 35 | github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 36 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 37 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 38 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 39 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 40 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 42 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 43 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 44 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 45 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 46 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 47 | github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= 48 | github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= 49 | github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= 50 | github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= 51 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 52 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 53 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 54 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 55 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 56 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 57 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 58 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 59 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 60 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 61 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 62 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 63 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 64 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 65 | golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= 66 | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 67 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 68 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 69 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 70 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 71 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 72 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 73 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 74 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 75 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 76 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 77 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 78 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 79 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 80 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 82 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 83 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 84 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 85 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 86 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 87 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 88 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 89 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 90 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 91 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 92 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 93 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 94 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 95 | golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= 96 | golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= 97 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 98 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 99 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 100 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 101 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 102 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 103 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 104 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 105 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 106 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 107 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 108 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 109 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 110 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 111 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 112 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 113 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 114 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 115 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 116 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 117 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 118 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 119 | nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= 120 | nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= 121 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "github.com/genshen/cmds" 7 | _ "github.com/genshen/wssocks/cmd/client" 8 | _ "github.com/genshen/wssocks/cmd/server" 9 | _ "github.com/genshen/wssocks/version" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func init() { 14 | log.SetLevel(log.TraceLevel) 15 | } 16 | 17 | func main() { 18 | cmds.SetProgramName("wssocks") 19 | if err := cmds.Parse(); err != nil { 20 | if !errors.Is(err, flag.ErrHelp) && !errors.Is(err, &cmds.SubCommandParseError{}) { 21 | log.Fatal(err) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /status-web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # production 12 | build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /status-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "status-web", 3 | "version": "0.1.0", 4 | "homepage": "/status", 5 | "private": false, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.11.4", 8 | "@testing-library/react": "^11.1.0", 9 | "@testing-library/user-event": "^12.1.10", 10 | "@types/jest": "^26.0.15", 11 | "@types/node": "^12.0.0", 12 | "@types/react": "^16.9.53", 13 | "@types/react-dom": "^16.9.8", 14 | "axios": "^0.21.1", 15 | "axios-hooks": "^2.3.0", 16 | "evergreen-ui": "^6.5.1", 17 | "react": "^17.0.1", 18 | "react-copy-to-clipboard": "^5.0.3", 19 | "react-dom": "^17.0.1", 20 | "react-grid-system": "^7.1.1", 21 | "react-scripts": "5.0.1", 22 | "typescript": "^4.0.3", 23 | "web-vitals": "^0.2.4", 24 | "workbox-background-sync": "^5.1.3", 25 | "workbox-broadcast-update": "^5.1.3", 26 | "workbox-cacheable-response": "^5.1.3", 27 | "workbox-core": "^5.1.3", 28 | "workbox-expiration": "^5.1.3", 29 | "workbox-google-analytics": "^5.1.3", 30 | "workbox-navigation-preload": "^5.1.3", 31 | "workbox-precaching": "^5.1.3", 32 | "workbox-range-requests": "^5.1.3", 33 | "workbox-routing": "^5.1.3", 34 | "workbox-strategies": "^5.1.3", 35 | "workbox-streams": "^5.1.3" 36 | }, 37 | "scripts": { 38 | "start": "react-scripts start", 39 | "build": "react-scripts build", 40 | "test": "react-scripts test", 41 | "eject": "react-scripts eject" 42 | }, 43 | "eslintConfig": { 44 | "extends": [ 45 | "react-app", 46 | "react-app/jest" 47 | ] 48 | }, 49 | "browserslist": { 50 | "production": [ 51 | ">0.2%", 52 | "not dead", 53 | "not op_mini all" 54 | ], 55 | "development": [ 56 | "last 1 chrome version", 57 | "last 1 firefox version", 58 | "last 1 safari version" 59 | ] 60 | }, 61 | "devDependencies": { 62 | "@types/react-copy-to-clipboard": "^5.0.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /status-web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genshen/wssocks/0794f544ced44878d94ff115b11631e2534f0aa1/status-web/public/favicon.ico -------------------------------------------------------------------------------- /status-web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Status | wssocks 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /status-web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genshen/wssocks/0794f544ced44878d94ff115b11631e2534f0aa1/status-web/public/logo192.png -------------------------------------------------------------------------------- /status-web/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genshen/wssocks/0794f544ced44878d94ff115b11631e2534f0aa1/status-web/public/logo512.png -------------------------------------------------------------------------------- /status-web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "wssocks status", 3 | "name": "Status of wssocks", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#47B881", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /status-web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /status-web/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | /* text-align: center; */ 3 | } 4 | 5 | .App-header { 6 | background-color: #282c34; 7 | min-height: 100vh; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | font-size: calc(10px + 2vmin); 13 | color: white; 14 | } 15 | 16 | .github-link { 17 | text-decoration: none !important; 18 | } 19 | -------------------------------------------------------------------------------- /status-web/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /status-web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pane, Text, Heading, Button, GitRepoIcon, Link, Alert, Spinner } from 'evergreen-ui' 3 | import { Container, Row, Col } from 'react-grid-system' 4 | import useAxios from 'axios-hooks' 5 | 6 | import './App.css'; 7 | import StatisticsBoard from './StatisticsBoard' 8 | import InfoBoard from './InfoBoard' 9 | import { WssosksStatus } from './Status' 10 | 11 | function App() { 12 | const [{ data, loading, error }, ] = useAxios( 13 | window.location.protocol + "//" + window.location.host + "/api/status" 14 | ) 15 | 16 | let content = null 17 | if (loading) { 18 | content = ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | } else if (error || !data) { 28 | content = ( 29 | 30 | 31 | 35 | 36 | 37 | ) 38 | } else { 39 | content = ( 40 | <> 41 | 42 | Information 43 | 44 | 45 | 46 | Statistics 47 | 48 | 49 | 50 | ) 51 | } 52 | 53 | return ( 54 | 55 | 56 | 57 | wssocks status 58 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | {content} 70 | 71 | 72 | ); 73 | } 74 | 75 | export default App; 76 | -------------------------------------------------------------------------------- /status-web/src/InfoBoard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Text, Pane, Popover, Position, toaster, IconProps } from 'evergreen-ui' 3 | import { DuplicateIcon, IconButton, KeyIcon, MoreIcon, LockIcon, DisableIcon, InfoSignIcon, TickCircleIcon } from 'evergreen-ui' 4 | import { Row, Col } from 'react-grid-system' 5 | import CopyToClipboard from 'react-copy-to-clipboard' 6 | 7 | import MoreVersionInfo from './MoreVersionInfo' 8 | import { Info } from './Status' 9 | 10 | type IconComponent = React.ForwardRefExoticComponent> & React.RefAttributes> 11 | 12 | interface InfoBoardProps { 13 | data: Info 14 | } 15 | 16 | interface StatusItemProps { 17 | title: string 18 | enable: boolean 19 | enableReason?: string 20 | disableReason: string 21 | enableIconColor?: string 22 | disableIconColor?: string 23 | enableTextColor?: string 24 | disableTextColor?: string 25 | enableIcon?: IconComponent 26 | disableIcon?: IconComponent 27 | } 28 | 29 | 30 | function StatusItem({ 31 | title, 32 | enable, 33 | enableReason = "enabled", 34 | disableReason, 35 | enableIconColor = "success", 36 | disableIconColor = "disabled", 37 | enableTextColor = "success", 38 | disableTextColor = "muted", 39 | }: StatusItemProps) { 40 | if (enable) { 41 | return ( 42 | <> 43 | {title} 44 | 45 | {enableReason} 46 | 47 | ) 48 | } else { 49 | return ( 50 | <> 51 | {title} 52 | 53 | {disableReason} 54 | 55 | ) 56 | } 57 | } 58 | 59 | function InfoBoard(props: InfoBoardProps) { 60 | const onCopyAddr = (addr: string) => { 61 | toaster.success( 62 | 'Remote server address copied', 63 | { 64 | description: 'address: ' + addr 65 | } 66 | ) 67 | } 68 | 69 | const server_proctocol = props.data.ssl_enabled ? 'wss' : (window.location.protocol === 'https' ? 'wss' : 'ws') 70 | const server_path = props.data.server_base_url === '/' ? '' : props.data.server_base_url 71 | const server_addr = server_proctocol + '://' + window.location.host + server_path 72 | return ( 73 | 74 | {/* flex={1} display="flex" justifyContent="space-between" */} 75 | 76 | 77 | 78 | Server Version 79 | {props.data.version.version_str} 80 | }> 85 | 86 | 87 | 88 | 89 | 90 | 91 | Address 92 | {server_addr} 93 | onCopyAddr(server_addr)}> 94 | Minimal 95 | 96 | 97 | 98 | 99 | 100 | 105 | 106 | 107 | 108 | 109 | 114 | 115 | 116 | 117 | 118 | Connection Key 119 | 131 | 132 | You can get connection key from admin of server 133 | 134 | } 135 | > 136 | 137 | 138 | {props.data.conn_key_enable && (<> 139 | enabled 140 | )} 141 | {!props.data.conn_key_enable && (<> 142 | {props.data.conn_key_disabled_reason} 143 | )} 144 | 145 | 146 | 147 | 148 | SSL/TLS 149 | {props.data.ssl_enabled && (<> 150 | 151 | enabled 152 | )} 153 | {!props.data.ssl_enabled && (<> 154 | 155 | {props.data.ssl_disabled_reason} 156 | )} 157 | 158 | 159 | 160 | 161 | ) 162 | } 163 | 164 | export default InfoBoard 165 | -------------------------------------------------------------------------------- /status-web/src/MoreVersionInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Text, Pane, UnorderedList, ListItem, PropertyIcon, Strong, Small, DotIcon } from 'evergreen-ui' 4 | 5 | interface VersionProps { 6 | version_code: number 7 | compatible_version: number 8 | } 9 | 10 | function MoreVersionInfo(props: VersionProps) { 11 | return ( 12 | 20 | 21 | 22 | Protocol Version:  23 | {props.version_code} 24 | 25 | 27 | Compatible Protocol Version:  28 | {props.compatible_version} 29 | 30 | 31 | Compatible Protocol Version is the lowest protocal version allowed for client. 32 | 33 | 34 | 35 | ) 36 | } 37 | 38 | export default MoreVersionInfo 39 | -------------------------------------------------------------------------------- /status-web/src/StatisticsBoard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Pane, Strong, UnorderedList, ListItem, Badge, Text, SwapHorizontalIcon } from 'evergreen-ui' 3 | import { ChangesIcon, ApplicationsIcon, TickCircleIcon } from 'evergreen-ui' 4 | import { Statistics } from './Status'; 5 | 6 | interface InfoBoardProps { 7 | data: Statistics 8 | } 9 | 10 | function upTimeString(t: number) { 11 | const seconds = t % 60 12 | t = Math.floor(t / 60) // left minutes 13 | const minutes = t % 60 14 | t = Math.floor(t / 60) // left hours 15 | const hours = t % 24 16 | const days = Math.floor(t / 24) // left days 17 | 18 | let re = seconds + ' second(s)' 19 | if (minutes === 0) { 20 | return re 21 | } 22 | re = minutes + ' minute(s) ' + re 23 | if (hours === 0) { 24 | return re 25 | } 26 | re = hours + ' hour(s) ' + re 27 | if (days === 0) { 28 | return re 29 | } 30 | return days + ' day(s) ' + re 31 | } 32 | 33 | function StatisticsBoard(props: InfoBoardProps) { 34 | const list_title_size = "150px" 35 | let [uptime, setUptime] = useState(props.data.up_time) 36 | useEffect(() => { 37 | const timer = setInterval(() => { 38 | setUptime((uptime) => uptime + 1) 39 | }, 1000) 40 | return () => clearInterval(timer) 41 | }, []) 42 | 43 | return ( 44 | 45 | 46 | 47 | 48 | Status 49 | 50 | 51 | In service 52 | 53 | 54 | 55 | 56 | 57 | 58 | Up Time 59 | 60 | 61 | {upTimeString(uptime)} 62 | 63 | 64 | 65 | 66 | 67 | 68 | Clients 69 | 70 | 71 | {props.data.clients} 72 | 73 | 74 | 75 | 76 | 77 | 78 | Proxy Connections 79 | 80 | 81 | {props.data.proxies} 82 | 83 | 84 | 85 | 86 | 87 | 88 | Data Trans 89 | 90 | 91 | unknown TiB 92 | 93 | 94 | 95 | 96 | ) 97 | } 98 | 99 | export default StatisticsBoard 100 | -------------------------------------------------------------------------------- /status-web/src/Status.ts: -------------------------------------------------------------------------------- 1 | export interface Version { 2 | version_str: string 3 | version_code: number 4 | compatible_version: number 5 | } 6 | 7 | export interface Info { 8 | version: Version 9 | server_base_url: string 10 | socks5_enabled: boolean 11 | socks5_disabled_reason: string 12 | http_enabled: boolean 13 | http_disabled_reason: string 14 | ssl_enabled: boolean 15 | ssl_disabled_reason: string 16 | conn_key_enable: boolean 17 | conn_key_disabled_reason: string 18 | } 19 | 20 | export interface Statistics { 21 | up_time: number 22 | clients: number 23 | proxies: number 24 | } 25 | 26 | export interface WssosksStatus { 27 | info: Info 28 | statistics: Statistics 29 | } 30 | -------------------------------------------------------------------------------- /status-web/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /status-web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorkerRegistration from './serviceWorkerRegistration'; 6 | import reportWebVitals from './reportWebVitals'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: https://cra.link/PWA 18 | serviceWorkerRegistration.register(); 19 | 20 | // If you want to start measuring performance in your app, pass a function 21 | // to log results (for example: reportWebVitals(console.log)) 22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 23 | reportWebVitals(); 24 | -------------------------------------------------------------------------------- /status-web/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /status-web/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /status-web/src/service-worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /* eslint-disable no-restricted-globals */ 3 | 4 | // This service worker can be customized! 5 | // See https://developers.google.com/web/tools/workbox/modules 6 | // for the list of available Workbox modules, or add any other 7 | // code you'd like. 8 | // You can also remove this file if you'd prefer not to use a 9 | // service worker, and the Workbox build step will be skipped. 10 | 11 | import { clientsClaim } from 'workbox-core'; 12 | import { ExpirationPlugin } from 'workbox-expiration'; 13 | import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'; 14 | import { registerRoute } from 'workbox-routing'; 15 | import { StaleWhileRevalidate } from 'workbox-strategies'; 16 | 17 | declare const self: ServiceWorkerGlobalScope; 18 | 19 | clientsClaim(); 20 | 21 | // Precache all of the assets generated by your build process. 22 | // Their URLs are injected into the manifest variable below. 23 | // This variable must be present somewhere in your service worker file, 24 | // even if you decide not to use precaching. See https://cra.link/PWA 25 | precacheAndRoute(self.__WB_MANIFEST); 26 | 27 | // Set up App Shell-style routing, so that all navigation requests 28 | // are fulfilled with your index.html shell. Learn more at 29 | // https://developers.google.com/web/fundamentals/architecture/app-shell 30 | const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$'); 31 | registerRoute( 32 | // Return false to exempt requests from being fulfilled by index.html. 33 | ({ request, url }: { request: Request; url: URL }) => { 34 | // If this isn't a navigation, skip. 35 | if (request.mode !== 'navigate') { 36 | return false; 37 | } 38 | 39 | // If this is a URL that starts with /_, skip. 40 | if (url.pathname.startsWith('/_')) { 41 | return false; 42 | } 43 | 44 | // If this looks like a URL for a resource, because it contains 45 | // a file extension, skip. 46 | if (url.pathname.match(fileExtensionRegexp)) { 47 | return false; 48 | } 49 | 50 | // Return true to signal that we want to use the handler. 51 | return true; 52 | }, 53 | createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') 54 | ); 55 | 56 | // An example runtime caching route for requests that aren't handled by the 57 | // precache, in this case same-origin .png requests like those from in public/ 58 | registerRoute( 59 | // Add in any other file extensions or routing criteria as needed. 60 | ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), 61 | // Customize this strategy as needed, e.g., by changing to CacheFirst. 62 | new StaleWhileRevalidate({ 63 | cacheName: 'images', 64 | plugins: [ 65 | // Ensure that once this runtime cache reaches a maximum size the 66 | // least-recently used images are removed. 67 | new ExpirationPlugin({ maxEntries: 50 }), 68 | ], 69 | }) 70 | ); 71 | 72 | // This allows the web app to trigger skipWaiting via 73 | // registration.waiting.postMessage({type: 'SKIP_WAITING'}) 74 | self.addEventListener('message', (event) => { 75 | if (event.data && event.data.type === 'SKIP_WAITING') { 76 | self.skipWaiting(); 77 | } 78 | }); 79 | 80 | // Any other custom service worker logic can go here. 81 | -------------------------------------------------------------------------------- /status-web/src/serviceWorkerRegistration.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://cra.link/PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) 19 | ); 20 | 21 | type Config = { 22 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 23 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 24 | }; 25 | 26 | export function register(config?: Config) { 27 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 28 | // The URL constructor is available in all browsers that support SW. 29 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 30 | if (publicUrl.origin !== window.location.origin) { 31 | // Our service worker won't work if PUBLIC_URL is on a different origin 32 | // from what our page is served on. This might happen if a CDN is used to 33 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 34 | return; 35 | } 36 | 37 | window.addEventListener('load', () => { 38 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 39 | 40 | if (isLocalhost) { 41 | // This is running on localhost. Let's check if a service worker still exists or not. 42 | checkValidServiceWorker(swUrl, config); 43 | 44 | // Add some additional logging to localhost, pointing developers to the 45 | // service worker/PWA documentation. 46 | navigator.serviceWorker.ready.then(() => { 47 | console.log( 48 | 'This web app is being served cache-first by a service ' + 49 | 'worker. To learn more, visit https://cra.link/PWA' 50 | ); 51 | }); 52 | } else { 53 | // Is not localhost. Just register service worker 54 | registerValidSW(swUrl, config); 55 | } 56 | }); 57 | } 58 | } 59 | 60 | function registerValidSW(swUrl: string, config?: Config) { 61 | navigator.serviceWorker 62 | .register(swUrl) 63 | .then((registration) => { 64 | registration.onupdatefound = () => { 65 | const installingWorker = registration.installing; 66 | if (installingWorker == null) { 67 | return; 68 | } 69 | installingWorker.onstatechange = () => { 70 | if (installingWorker.state === 'installed') { 71 | if (navigator.serviceWorker.controller) { 72 | // At this point, the updated precached content has been fetched, 73 | // but the previous service worker will still serve the older 74 | // content until all client tabs are closed. 75 | console.log( 76 | 'New content is available and will be used when all ' + 77 | 'tabs for this page are closed. See https://cra.link/PWA.' 78 | ); 79 | 80 | // Execute callback 81 | if (config && config.onUpdate) { 82 | config.onUpdate(registration); 83 | } 84 | } else { 85 | // At this point, everything has been precached. 86 | // It's the perfect time to display a 87 | // "Content is cached for offline use." message. 88 | console.log('Content is cached for offline use.'); 89 | 90 | // Execute callback 91 | if (config && config.onSuccess) { 92 | config.onSuccess(registration); 93 | } 94 | } 95 | } 96 | }; 97 | }; 98 | }) 99 | .catch((error) => { 100 | console.error('Error during service worker registration:', error); 101 | }); 102 | } 103 | 104 | function checkValidServiceWorker(swUrl: string, config?: Config) { 105 | // Check if the service worker can be found. If it can't reload the page. 106 | fetch(swUrl, { 107 | headers: { 'Service-Worker': 'script' }, 108 | }) 109 | .then((response) => { 110 | // Ensure service worker exists, and that we really are getting a JS file. 111 | const contentType = response.headers.get('content-type'); 112 | if ( 113 | response.status === 404 || 114 | (contentType != null && contentType.indexOf('javascript') === -1) 115 | ) { 116 | // No service worker found. Probably a different app. Reload the page. 117 | navigator.serviceWorker.ready.then((registration) => { 118 | registration.unregister().then(() => { 119 | window.location.reload(); 120 | }); 121 | }); 122 | } else { 123 | // Service worker found. Proceed as normal. 124 | registerValidSW(swUrl, config); 125 | } 126 | }) 127 | .catch(() => { 128 | console.log('No internet connection found. App is running in offline mode.'); 129 | }); 130 | } 131 | 132 | export function unregister() { 133 | if ('serviceWorker' in navigator) { 134 | navigator.serviceWorker.ready 135 | .then((registration) => { 136 | registration.unregister(); 137 | }) 138 | .catch((error) => { 139 | console.error(error.message); 140 | }); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /status-web/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /status-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/genshen/cmds" 7 | "github.com/genshen/wssocks/wss" 8 | ) 9 | 10 | const VERSION = wss.CoreVersion 11 | 12 | var buildHash = "none" 13 | var buildTime = "none" 14 | var buildGoVersion = "none" 15 | 16 | var versionCommand = &cmds.Command{ 17 | Name: "version", 18 | Summary: "show version", 19 | Description: "print current version.", 20 | CustomFlags: false, 21 | HasOptions: false, 22 | } 23 | 24 | func init() { 25 | versionCommand.Runner = &version{} 26 | fs := flag.NewFlagSet("version", flag.ContinueOnError) 27 | versionCommand.FlagSet = fs 28 | versionCommand.FlagSet.Usage = versionCommand.Usage // use default usage provided by cmds.Command. 29 | cmds.AllCommands = append(cmds.AllCommands, versionCommand) 30 | } 31 | 32 | type version struct{} 33 | 34 | func (v *version) PreRun() error { 35 | return nil 36 | } 37 | 38 | func (v *version) Run() error { 39 | fmt.Printf("version: %s.\n", VERSION) 40 | fmt.Printf("protocol version: %d\n", wss.VersionCode) 41 | fmt.Printf("commit: %s\n", buildHash) 42 | fmt.Printf("build time: %s\n", buildTime) 43 | fmt.Printf("build by: %s\n", buildGoVersion) 44 | fmt.Println("Author: genshen") 45 | fmt.Println("github: https://github.com/genshen/wssocks") 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /wss/buffered_reader_writer.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "sync" 8 | ) 9 | 10 | type BufferedWR struct { 11 | buffer bytes.Buffer 12 | done bool 13 | //closeCh bool 14 | update chan struct{} 15 | mu sync.Mutex 16 | } 17 | 18 | func (h *BufferedWR) Close() error { 19 | h.mu.Lock() 20 | defer h.mu.Unlock() 21 | if h.done { 22 | return nil 23 | } 24 | h.done = true 25 | close(h.update) 26 | return nil 27 | } 28 | 29 | func (h *BufferedWR) isClosed() bool { 30 | return h.done 31 | } 32 | 33 | // implement Write interface to write bytes from ssh server into bytes.Buffer. 34 | func (h *BufferedWR) Write(p []byte) (int, error) { 35 | h.mu.Lock() 36 | defer h.mu.Unlock() 37 | if h.done { 38 | return 0, errors.New("write after buffer closed") 39 | } 40 | // make sure it indeed has data in buffer when noticing wait 41 | if len(p) == 0 { 42 | return 0, nil 43 | } 44 | if h.buffer.Len() == 0 { 45 | h.update <- struct{}{} 46 | } 47 | return h.buffer.Write(p) 48 | } 49 | 50 | // read data from buffer 51 | // make sure there is no more one goroutine reading 52 | func (h *BufferedWR) Read(p []byte) (int, error) { 53 | // wait to make sure there is data in buffer 54 | if h.buffer.Len() == 0 { 55 | select { 56 | case _, ok := <-h.update: // data received from client 57 | if !ok { 58 | return 0, io.EOF 59 | } 60 | } 61 | } 62 | 63 | h.mu.Lock() 64 | defer h.mu.Unlock() 65 | if h.done { 66 | return 0, io.EOF 67 | } 68 | return h.buffer.Read(p) 69 | } 70 | 71 | func NewBufferWR() *BufferedWR { 72 | update := make(chan struct{}, 1) 73 | body := BufferedWR{done: false, update: update} 74 | return &body 75 | } 76 | -------------------------------------------------------------------------------- /wss/buffered_reader_writer_test.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestBufferWRClose(t *testing.T) { 10 | done := make(chan struct{}, 1) 11 | bwr := NewBufferWR() 12 | go func() { 13 | var d1 [1024]byte 14 | for { 15 | // with close, read can return 16 | if _, err := bwr.Read(d1[:]); err == io.EOF { 17 | break 18 | } 19 | } 20 | println("read passed") 21 | done <- struct{}{} 22 | }() 23 | 24 | _ = bwr.Close() 25 | 26 | <-done 27 | } 28 | 29 | func TestBufferWRWrite(t *testing.T) { 30 | bwr := NewBufferWR() 31 | 32 | var d2 [1024]byte 33 | 34 | _, _ = bwr.Write(d2[:]) 35 | _, _ = bwr.Write(d2[:]) 36 | _, _ = bwr.Write(d2[:]) 37 | } 38 | 39 | func TestBufferWR(t *testing.T) { 40 | done := make(chan struct{}, 1) 41 | bwr := NewBufferWR() 42 | 43 | go func() { 44 | var d1 [3 * 1024]byte 45 | for i := 0; i < 1; i++ { 46 | // read all data at once 47 | if n, err := bwr.Read(d1[:]); err == io.EOF { 48 | break 49 | } else { 50 | if n != 3*1024 { 51 | t.Error("read data length not match") 52 | } 53 | } 54 | } 55 | done <- struct{}{} 56 | }() 57 | 58 | var d2 [1024]byte 59 | _, _ = bwr.Write(d2[:]) // 3 writes, but only one read 60 | _, _ = bwr.Write(d2[:]) 61 | _, _ = bwr.Write(d2[:]) 62 | 63 | <-done 64 | } 65 | 66 | func TestBufferWR2(t *testing.T) { 67 | done := make(chan struct{}, 1) 68 | bwr := NewBufferWR() 69 | 70 | go func() { 71 | var d1 [1024]byte 72 | for i := 0; i < 3; i++ { 73 | if n, err := bwr.Read(d1[:]); err == io.EOF { 74 | break 75 | } else { 76 | if n != 1024 { 77 | t.Error("read data length not match") 78 | } 79 | } 80 | } 81 | done <- struct{}{} 82 | }() 83 | 84 | var d2 [1024]byte 85 | _, _ = bwr.Write(d2[:]) // 3 writes, with 3 reads 86 | time.Sleep(1 * time.Second) 87 | _, _ = bwr.Write(d2[:]) 88 | time.Sleep(1 * time.Second) 89 | _, _ = bwr.Write(d2[:]) 90 | 91 | <-done 92 | } 93 | 94 | func TestBufferWR3(t *testing.T) { 95 | done := make(chan struct{}, 1) 96 | bwr := NewBufferWR() 97 | 98 | go func() { 99 | var d1 [1024]byte 100 | for i := 0; i < 3; i++ { 101 | if n, err := bwr.Read(d1[:]); err == io.EOF { 102 | break 103 | } else { 104 | if n != 1024 { 105 | t.Error("read data length not match") 106 | } 107 | } 108 | } 109 | done <- struct{}{} 110 | }() 111 | 112 | var d2 [3 * 1024]byte 113 | _, _ = bwr.Write(d2[:]) // 1 writes, but 3 reads, due to the small read buffer 114 | 115 | <-done 116 | } 117 | -------------------------------------------------------------------------------- /wss/concurrent_websocket.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "github.com/segmentio/ksuid" 7 | "nhooyr.io/websocket" 8 | "nhooyr.io/websocket/wsjson" 9 | "sync" 10 | ) 11 | 12 | type ConcurrentWebSocketInterface interface { 13 | WSClose() error 14 | WriteWSJSON(data interface{}) error 15 | } 16 | 17 | // add lock to websocket connection to make sure only one goroutine can write this websocket. 18 | type ConcurrentWebSocket struct { 19 | WsConn *websocket.Conn 20 | } 21 | 22 | // close websocket connection 23 | func (wsc *ConcurrentWebSocket) WSClose() error { 24 | return wsc.WsConn.Close(websocket.StatusNormalClosure, "") 25 | } 26 | 27 | // write message to websocket, the data is fixed format @ProxyData 28 | // id: connection id 29 | // data: data to be written 30 | func (wsc *ConcurrentWebSocket) WriteProxyMessage(ctx context.Context, id ksuid.KSUID, tag int, data []byte) error { 31 | dataBase64 := base64.StdEncoding.EncodeToString(data) 32 | jsonData := WebSocketMessage{ 33 | Id: id.String(), 34 | Type: WsTpData, 35 | Data: ProxyData{Tag: tag, DataBase64: dataBase64}, 36 | } 37 | return wsjson.Write(ctx, wsc.WsConn, &jsonData) 38 | } 39 | 40 | type webSocketWriter struct { 41 | WSC *ConcurrentWebSocket 42 | Id ksuid.KSUID // connection id. 43 | Ctx context.Context 44 | Type int // type of trans data. 45 | Mu *sync.Mutex 46 | } 47 | 48 | func NewWebSocketWriter(wsc *ConcurrentWebSocket, id ksuid.KSUID, ctx context.Context) *webSocketWriter { 49 | return &webSocketWriter{WSC: wsc, Id: id, Ctx: ctx} 50 | } 51 | 52 | func NewWebSocketWriterWithMutex(wsc *ConcurrentWebSocket, id ksuid.KSUID, ctx context.Context) *webSocketWriter { 53 | return &webSocketWriter{WSC: wsc, Id: id, Ctx: ctx, Mu: &sync.Mutex{}} 54 | } 55 | 56 | func (writer *webSocketWriter) CloseWsWriter(cancel context.CancelFunc) { 57 | if writer.Mu != nil { 58 | writer.Mu.Lock() 59 | defer writer.Mu.Unlock() 60 | } 61 | cancel() 62 | } 63 | 64 | func (writer *webSocketWriter) Write(buffer []byte) (n int, err error) { 65 | if writer.Mu != nil { 66 | writer.Mu.Lock() 67 | defer writer.Mu.Unlock() 68 | } 69 | // make sure context is not Canceled/DeadlineExceeded before Write. 70 | if writer.Ctx.Err() != nil { 71 | return 0, writer.Ctx.Err() 72 | } 73 | if err := writer.WSC.WriteProxyMessage(writer.Ctx, writer.Id, TagData, buffer); err != nil { 74 | return 0, err 75 | } else { 76 | return len(buffer), nil 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /wss/conn_records.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "io" 6 | "sync" 7 | ) 8 | 9 | // struct record the connection size of each target host 10 | type ConnRecord struct { 11 | ConnSize uint // total size of current connections 12 | Addresses map[string]uint // current connections as well as its count 13 | Writer *io.Writer // terminal writer todo defer Flush 14 | OnChange func(status ConnStatus) 15 | Mutex *sync.Mutex 16 | } 17 | 18 | // connection status when a connection is added or removed. 19 | type ConnStatus struct { 20 | Address string 21 | IsNew bool 22 | Type int 23 | } 24 | 25 | func NewConnRecord() *ConnRecord { 26 | cr := ConnRecord{ConnSize: 0, OnChange: nil} 27 | cr.Addresses = make(map[string]uint) 28 | cr.Mutex = &sync.Mutex{} 29 | return &cr 30 | } 31 | 32 | func (cr *ConnRecord) Update(status ConnStatus) { 33 | cr.Mutex.Lock() 34 | defer cr.Mutex.Unlock() 35 | if status.IsNew { 36 | cr.ConnSize++ 37 | if size, ok := cr.Addresses[status.Address]; ok { 38 | cr.Addresses[status.Address] = size + 1 39 | } else { 40 | cr.Addresses[status.Address] = 1 41 | } 42 | } else { 43 | cr.ConnSize-- 44 | if size, ok := cr.Addresses[status.Address]; ok && size > 0 { 45 | if size-1 == 0 { 46 | delete(cr.Addresses, status.Address) 47 | } else { 48 | cr.Addresses[status.Address] = size - 1 49 | } 50 | } else { 51 | logrus.Fatal("bad connection size") 52 | } 53 | } 54 | // update log 55 | if cr.OnChange != nil { 56 | cr.OnChange(status) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /wss/heart_beat.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "context" 5 | "github.com/segmentio/ksuid" 6 | "nhooyr.io/websocket/wsjson" 7 | "time" 8 | ) 9 | 10 | type HeartBeat struct { 11 | wsc *WebSocketClient 12 | cancel context.CancelFunc 13 | isClosed bool 14 | } 15 | 16 | func NewHeartBeat(wsc *WebSocketClient) (*HeartBeat, context.Context) { 17 | hb := HeartBeat{wsc: wsc, isClosed: false} 18 | ctx, can := context.WithCancel(context.Background()) 19 | 20 | hb.cancel = can 21 | return &hb, ctx 22 | } 23 | 24 | // close heartbeat sending 25 | func (hb *HeartBeat) Close() { 26 | if hb.isClosed { 27 | return 28 | } 29 | hb.isClosed = true 30 | hb.cancel() 31 | } 32 | 33 | // start sending heart beat to server. 34 | func (hb *HeartBeat) Start(ctx context.Context, writeTimeout time.Duration) error { 35 | t := time.NewTicker(time.Second * 15) 36 | defer t.Stop() 37 | for { 38 | select { 39 | case <-ctx.Done(): 40 | return nil 41 | case <-t.C: 42 | heartBeats := WebSocketMessage{ 43 | Id: ksuid.KSUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}.String(), 44 | Type: WsTpBeats, 45 | Data: nil, 46 | } 47 | writeCtx, _ := context.WithTimeout(ctx, writeTimeout) 48 | if err := wsjson.Write(writeCtx, hb.wsc.WsConn, heartBeats); err != nil { 49 | return err 50 | } 51 | } 52 | } 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /wss/http_utils.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | func HttpRequestHeader(buffer *bytes.Buffer, req *http.Request) { 11 | buffer.WriteString(fmt.Sprintf("%s %s %s\r\n", req.Method, req.URL.String(), req.Proto)) 12 | // req.Header.Add("Connection", "close") 13 | for name, headers := range req.Header { 14 | name = strings.ToLower(name) 15 | for _, h := range headers { 16 | buffer.WriteString(fmt.Sprintf("%v: %v\r\n", name, h)) 17 | } 18 | } 19 | buffer.WriteString("\r\n") 20 | } 21 | 22 | func HttpRespHeader(buffer *bytes.Buffer, resp *http.Response) { 23 | buffer.Write([]byte(fmt.Sprintf("%s %s\r\n", resp.Proto, resp.Status))) 24 | //req.Header.Add("Connection", "close") 25 | for name, headers := range resp.Header { 26 | for _, h := range headers { 27 | buffer.Write([]byte(fmt.Sprintf("%s: %s\r\n", name, h))) 28 | } 29 | } 30 | buffer.Write([]byte("\r\n")) 31 | } 32 | -------------------------------------------------------------------------------- /wss/hub.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "context" 5 | "github.com/segmentio/ksuid" 6 | "nhooyr.io/websocket/wsjson" 7 | "sync" 8 | ) 9 | 10 | type ProxyServer struct { 11 | Id ksuid.KSUID // id of proxy connection 12 | ProxyIns ProxyEstablish 13 | } 14 | 15 | // Hub maintains the set of active proxy clients in server side for a user 16 | type Hub struct { 17 | id ksuid.KSUID 18 | ConcurrentWebSocket 19 | // Registered proxy connections. 20 | connPool map[ksuid.KSUID]*ProxyServer 21 | 22 | mu sync.RWMutex 23 | } 24 | 25 | type ProxyRegister struct { 26 | id ksuid.KSUID 27 | _type int 28 | addr string 29 | withData []byte 30 | } 31 | 32 | func (h *Hub) Close() { 33 | // if there are connections, close them. 34 | h.mu.Lock() 35 | defer h.mu.Unlock() 36 | for id, proxy := range h.connPool { 37 | proxy.ProxyIns.Close(false) 38 | delete(h.connPool, id) 39 | } 40 | } 41 | 42 | // add a tcp connection to connection pool. 43 | func (h *Hub) addNewProxy(proxy *ProxyServer) { 44 | h.mu.Lock() 45 | defer h.mu.Unlock() 46 | h.connPool[proxy.Id] = proxy 47 | } 48 | 49 | func (h *Hub) GetProxyById(id ksuid.KSUID) *ProxyServer { 50 | h.mu.RLock() 51 | defer h.mu.RUnlock() 52 | if proxy, ok := h.connPool[id]; ok { 53 | return proxy 54 | } 55 | return nil 56 | } 57 | 58 | // return the proxies handled by this hub/websocket connetion 59 | func (h *Hub) GetConnectorSize() int { 60 | // h.mu.RLock() 61 | // defer h.mu.RUnlock() 62 | return len(h.connPool) 63 | } 64 | 65 | // Close proxy connection with remote host. 66 | // It can be called when receiving tell close message from client 67 | func (h *Hub) CloseProxyConn(id ksuid.KSUID) error { 68 | if proxy := h.GetProxyById(id); proxy != nil { 69 | return proxy.ProxyIns.Close(false) // todo remove proxy here 70 | } 71 | return nil 72 | } 73 | 74 | func (h *Hub) RemoveProxy(id ksuid.KSUID) { 75 | h.mu.Lock() 76 | defer h.mu.Unlock() 77 | if _, ok := h.connPool[id]; ok { 78 | delete(h.connPool, id) 79 | } 80 | } 81 | 82 | // tell the client the connection has been closed 83 | func (h *Hub) tellClosed(id ksuid.KSUID) error { 84 | // send finish flag to client 85 | finish := WebSocketMessage{ 86 | Id: id.String(), 87 | Type: WsTpClose, 88 | Data: nil, 89 | } 90 | // fixme lock or NextWriter 91 | if err := wsjson.Write(context.TODO(), h.WsConn, &finish); err != nil { 92 | return err 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /wss/hub_collection.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "github.com/segmentio/ksuid" 5 | "nhooyr.io/websocket" 6 | "sync" 7 | ) 8 | 9 | // HubCollection is a set of hubs. It handle several hubs. 10 | // Each hub can map to a websocket connection, 11 | // which also handle several proxies instance. 12 | type HubCollection struct { 13 | hubs map[ksuid.KSUID]*Hub 14 | 15 | mutex sync.RWMutex 16 | } 17 | 18 | func NewHubCollection() *HubCollection { 19 | hc := HubCollection{} 20 | hc.hubs = make(map[ksuid.KSUID]*Hub) 21 | return &hc 22 | } 23 | 24 | // create a hub and add it to hub collection 25 | func (hc *HubCollection) NewHub(conn *websocket.Conn) *Hub { 26 | hc.mutex.Lock() 27 | defer hc.mutex.Unlock() 28 | 29 | hub := Hub{ 30 | id: ksuid.New(), 31 | ConcurrentWebSocket: ConcurrentWebSocket{WsConn: conn}, 32 | connPool: make(map[ksuid.KSUID]*ProxyServer), 33 | } 34 | 35 | hc.hubs[hub.id] = &hub 36 | return &hub 37 | } 38 | 39 | // count the client size and proxy connection size. 40 | func (hc *HubCollection) GetConnCount() (int, int) { 41 | hc.mutex.Lock() 42 | defer hc.mutex.Unlock() 43 | clients := len(hc.hubs) 44 | 45 | connections := 0 46 | for _, h := range hc.hubs { 47 | connections += h.GetConnectorSize() 48 | } 49 | return clients, connections 50 | } 51 | 52 | // remove a hub specified by its id. 53 | func (hc *HubCollection) RemoveProxy(id ksuid.KSUID) { 54 | hc.mutex.Lock() 55 | defer hc.mutex.Unlock() 56 | if _, ok := hc.hubs[id]; ok { 57 | delete(hc.hubs, id) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /wss/proxy_client.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "time" 7 | 8 | "github.com/segmentio/ksuid" 9 | log "github.com/sirupsen/logrus" 10 | "nhooyr.io/websocket/wsjson" 11 | ) 12 | 13 | const ( 14 | TagData = iota 15 | TagEstOk 16 | TagEstErr 17 | TagNoMore 18 | ) 19 | 20 | // proxy client handle one connection, send data to proxy server vai websocket. 21 | type ProxyClient struct { 22 | Id ksuid.KSUID 23 | onData func(ksuid.KSUID, ServerData) // data from server todo data with type 24 | onClosed func(ksuid.KSUID, bool) // close connection, param bool: do tellClose if true 25 | onError func(ksuid.KSUID, error) // if there are error messages 26 | } 27 | 28 | type ServerData struct { 29 | Tag int 30 | Data []byte 31 | } 32 | 33 | // tell wssocks proxy server to establish a proxy connection by sending server 34 | // proxy address, type, initial data. 35 | func (p *ProxyClient) Establish(wsc *WebSocketClient, firstSendData []byte, proxyType int, addr string) error { 36 | estMsg := ProxyEstMessage{ 37 | Type: proxyType, 38 | Addr: addr, 39 | WithData: false, 40 | } 41 | if firstSendData != nil { 42 | estMsg.WithData = true 43 | estMsg.DataBase64 = base64.StdEncoding.EncodeToString(firstSendData) 44 | } 45 | 46 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 47 | defer cancel() 48 | if err := wsjson.Write(ctx, wsc.WsConn, &WebSocketMessage{ 49 | Type: WsTpEst, 50 | Id: p.Id.String(), 51 | Data: estMsg, 52 | }); err != nil { 53 | log.Error("json error:", err) 54 | return err 55 | } 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /wss/proxy_client_http.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | 11 | "github.com/segmentio/ksuid" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type HttpClient struct { 16 | wsc *WebSocketClient 17 | record *ConnRecord 18 | } 19 | 20 | func NewHttpProxy(wsc *WebSocketClient, cr *ConnRecord) HttpClient { 21 | return HttpClient{wsc: wsc, record: cr} 22 | } 23 | 24 | func (client *HttpClient) ServeHTTP(w http.ResponseWriter, req *http.Request) { 25 | type Done struct { 26 | tell bool 27 | err error 28 | } 29 | done := make(chan Done, 2) 30 | continued := make(chan int) 31 | defer close(continued) 32 | //defer close(done) 33 | 34 | hj, _ := w.(http.Hijacker) 35 | conn, jack, _ := hj.Hijack() 36 | defer conn.Close() 37 | defer jack.Flush() 38 | 39 | proxy := client.wsc.NewProxy(nil, nil, nil) 40 | proxy.onData = func(id ksuid.KSUID, data ServerData) { 41 | if data.Tag == TagEstOk || data.Tag == TagEstErr { 42 | continued <- data.Tag 43 | return 44 | } 45 | if _, err := jack.Write(data.Data); err != nil { 46 | done <- Done{true, err} 47 | } 48 | } 49 | proxy.onClosed = func(id ksuid.KSUID, tell bool) { 50 | done <- Done{tell, nil} 51 | } 52 | proxy.onError = func(ksuids ksuid.KSUID, err error) { 53 | done <- Done{true, err} 54 | } 55 | 56 | // establish with header fixme record 57 | if !req.URL.IsAbs() { 58 | client.wsc.RemoveProxy(proxy.Id) 59 | w.WriteHeader(403) 60 | _, _ = w.Write([]byte("This is a proxy server. Does not respond to non-proxy requests.")) 61 | return 62 | } 63 | 64 | client.record.Update(ConnStatus{IsNew: true, Address: req.URL.Host, Type: ProxyTypeHttp}) 65 | defer client.record.Update(ConnStatus{IsNew: false, Address: req.URL.Host, Type: ProxyTypeHttp}) 66 | 67 | var headerBuffer bytes.Buffer 68 | host, _ := client.parseUrl(req.Method, req.Proto, req.URL) 69 | HttpRequestHeader(&headerBuffer, req) 70 | 71 | if err := proxy.Establish(client.wsc, headerBuffer.Bytes(), ProxyTypeHttp, host); err != nil { // fixme default port 72 | log.Error("write header error:", err) 73 | client.wsc.RemoveProxy(proxy.Id) 74 | if err := client.wsc.TellClose(proxy.Id); err != nil { 75 | log.Error("close error", err) 76 | } 77 | return 78 | } 79 | 80 | // fixme add timeout 81 | // wait receiving "established connection" from server 82 | if tag := <-continued; tag == TagEstErr { 83 | return 84 | } 85 | 86 | // copy request body data 87 | writer := NewWebSocketWriter(&client.wsc.ConcurrentWebSocket, proxy.Id, context.Background()) 88 | if _, err := io.Copy(writer, req.Body); err != nil { 89 | log.Error("write body error:", err) 90 | client.wsc.RemoveProxy(proxy.Id) 91 | if err := client.wsc.TellClose(proxy.Id); err != nil { 92 | log.Error("close error", err) 93 | } 94 | return 95 | } 96 | 97 | ctx, cancel := context.WithCancel(context.Background()) 98 | defer cancel() 99 | if err := client.wsc.WriteProxyMessage(ctx, proxy.Id, TagNoMore, nil); err != nil { 100 | log.Error("write body error:", err) 101 | client.wsc.RemoveProxy(proxy.Id) 102 | if err := client.wsc.TellClose(proxy.Id); err != nil { 103 | log.Error("close error", err) 104 | } 105 | return 106 | } 107 | 108 | // finished 109 | d := <-done // fixme add timeout 110 | client.wsc.RemoveProxy(proxy.Id) 111 | if d.tell { 112 | if err := client.wsc.TellClose(proxy.Id); err != nil { 113 | log.Error(err) 114 | } 115 | } 116 | if d.err != nil { 117 | log.Error(d.err) 118 | } 119 | } 120 | 121 | func copyHeaders(dst, src http.Header) { 122 | for k, vs := range src { 123 | for _, v := range vs { 124 | dst.Add(k, v) 125 | } 126 | } 127 | } 128 | 129 | func (client *HttpClient) ProxyType() int { 130 | return ProxyTypeHttp 131 | } 132 | 133 | // parse first line of http header, returning method, address, http version and the bytes of first line. 134 | func (client *HttpClient) parseUrl(method, ver string, u *url.URL) (string, string) { 135 | var host string 136 | // parsing port and host 137 | if u.Opaque == "80" { // https 138 | host = u.Scheme + ":80" 139 | } else { // http 140 | if u.Port() == "" { 141 | host = net.JoinHostPort(u.Hostname(), "80") 142 | } else { 143 | host = net.JoinHostPort(u.Hostname(), u.Port()) 144 | } 145 | } 146 | // get path?query#fragment 147 | //u.Host = "" 148 | //u.Scheme = "" 149 | return host, u.String() 150 | } 151 | -------------------------------------------------------------------------------- /wss/proxy_client_https.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "net" 8 | "net/url" 9 | ) 10 | 11 | type HttpsClient struct { 12 | } 13 | 14 | func (client *HttpsClient) ProxyType() int { 15 | return ProxyTypeHttps 16 | } 17 | 18 | func (client *HttpsClient) Trigger(data []byte) bool { 19 | return len(data) > len("CONNECT") && string(data[:len("CONNECT")]) == "CONNECT" 20 | } 21 | 22 | func (client *HttpsClient) EstablishData(origin []byte) ([]byte, error) { 23 | return nil, nil 24 | } 25 | 26 | // parsing https header, and return address and parsing error 27 | func (client *HttpsClient) ParseHeader(conn net.Conn, header []byte) (string, error) { 28 | buff := bytes.NewBuffer(header) 29 | if line, _, err := bufio.NewReader(buff).ReadLine(); err != nil { 30 | return "", err 31 | } else { 32 | var method, address, httpVer string 33 | if _, err := fmt.Sscanf(string(line), "%s %s %s", &method, &address, &httpVer); err != nil { 34 | return "", err 35 | } else { 36 | if u, err := url.Parse(address); err != nil { 37 | return "", err 38 | } else { 39 | var host string 40 | // parsing port and host 41 | if u.Opaque == "443" { // https 42 | host = u.Scheme + ":443" 43 | } else { // https 44 | if u.Port() == "" { 45 | host = net.JoinHostPort(u.Host, "443") 46 | } else { 47 | host = net.JoinHostPort(u.Host, u.Port()) 48 | } 49 | } 50 | return host, nil 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /wss/proxy_client_interface.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | const ( 8 | ProxyTypeSocks5 = iota 9 | ProxyTypeHttp 10 | ProxyTypeHttps 11 | ) 12 | 13 | func ProxyTypeStr(tp int) string { 14 | switch tp { 15 | case ProxyTypeHttp: 16 | return "http" 17 | case ProxyTypeHttps: 18 | return "https" 19 | case ProxyTypeSocks5: 20 | return "socks5" 21 | } 22 | return "unknown" 23 | } 24 | 25 | // interface of proxy client, supported types: http/https/socks5 26 | type ProxyInterface interface { 27 | ProxyType() int 28 | // return a bool value to indicate whether it is the matched protocol. 29 | Trigger(data []byte) bool 30 | // parse protocol header bytes, return target host. 31 | ParseHeader(conn net.Conn, header []byte) (string, error) 32 | // return data transformed in connection establishing step. 33 | EstablishData(origin []byte) ([]byte, error) 34 | } 35 | -------------------------------------------------------------------------------- /wss/proxy_client_socks5.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "io" 8 | "net" 9 | "strconv" 10 | ) 11 | 12 | type Socks5Client struct { 13 | } 14 | 15 | func (client *Socks5Client) ProxyType() int { 16 | return ProxyTypeSocks5 17 | } 18 | 19 | func (client *Socks5Client) Trigger(data []byte) bool { 20 | return len(data) >= 2 && data[0] == 0x05 21 | } 22 | 23 | func (client *Socks5Client) EstablishData(origin []byte) ([]byte, error) { 24 | return nil, nil 25 | } 26 | 27 | // parsing socks5 header, and return address and parsing error 28 | func (client *Socks5Client) ParseHeader(conn net.Conn, header []byte) (string, error) { 29 | // response to socks5 client 30 | // see rfc 1982 for more details (https://tools.ietf.org/html/rfc1928) 31 | n, err := conn.Write([]byte{0x05, 0x00}) // version and no authentication required 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | // step2: process client Requests and does Reply 37 | /** 38 | +----+-----+-------+------+----------+----------+ 39 | |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | 40 | +----+-----+-------+------+----------+----------+ 41 | | 1 | 1 | X'00' | 1 | Variable | 2 | 42 | +----+-----+-------+------+----------+----------+ 43 | */ 44 | var buffer [1024]byte 45 | 46 | n, err = conn.Read(buffer[:]) 47 | if err != nil { 48 | return "", err 49 | } 50 | if n < 6 { 51 | return "", errors.New("not a socks protocol") 52 | } 53 | 54 | var host string 55 | switch buffer[3] { 56 | case 0x01: 57 | // ipv4 address 58 | ipv4 := make([]byte, 4) 59 | if _, err := io.ReadAtLeast(bytes.NewReader(buffer[4:]), ipv4, len(ipv4)); err != nil { 60 | return "", err 61 | } 62 | host = net.IP(ipv4).String() 63 | case 0x04: 64 | // ipv6 65 | ipv6 := make([]byte, 16) 66 | if _, err := io.ReadAtLeast(bytes.NewReader(buffer[4:]), ipv6, len(ipv6)); err != nil { 67 | return "", err 68 | } 69 | host = net.IP(ipv6).String() 70 | case 0x03: 71 | // domain 72 | addrLen := int(buffer[4]) 73 | domain := make([]byte, addrLen) 74 | if _, err := io.ReadAtLeast(bytes.NewReader(buffer[5:]), domain, addrLen); err != nil { 75 | return "", err 76 | } 77 | host = string(domain) 78 | } 79 | 80 | port := make([]byte, 2) 81 | err = binary.Read(bytes.NewReader(buffer[n-2:n]), binary.BigEndian, &port) 82 | if err != nil { 83 | return "", err 84 | } 85 | 86 | return net.JoinHostPort(host, strconv.Itoa((int(port[0])<<8)|int(port[1]))), nil 87 | } 88 | -------------------------------------------------------------------------------- /wss/proxy_server.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/base64" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "github.com/segmentio/ksuid" 12 | log "github.com/sirupsen/logrus" 13 | "io" 14 | "net" 15 | "net/http" 16 | "nhooyr.io/websocket" 17 | "time" 18 | ) 19 | 20 | type Connector struct { 21 | Conn io.ReadWriteCloser 22 | } 23 | 24 | // interface of establishing proxy connection with target 25 | type ProxyEstablish interface { 26 | establish(hub *Hub, id ksuid.KSUID, proxyType int, addr string, data []byte) error 27 | 28 | // data from client todo data with type 29 | onData(data ClientData) error 30 | 31 | // close connection 32 | // tell: whether to send close message to proxy client 33 | Close(tell bool) error 34 | } 35 | 36 | type ClientData ServerData 37 | 38 | var ConnCloseByClient = errors.New("conn closed by client") 39 | 40 | func dispatchMessage(hub *Hub, msgType websocket.MessageType, data []byte, config WebsocksServerConfig) error { 41 | if msgType == websocket.MessageText { 42 | return dispatchDataMessage(hub, data, config) 43 | } 44 | return nil 45 | } 46 | 47 | func dispatchDataMessage(hub *Hub, data []byte, config WebsocksServerConfig) error { 48 | var socketData json.RawMessage 49 | socketStream := WebSocketMessage{ 50 | Data: &socketData, 51 | } 52 | if err := json.Unmarshal(data, &socketStream); err != nil { 53 | return err 54 | } 55 | 56 | // parsing id 57 | id, err := ksuid.Parse(socketStream.Id) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | switch socketStream.Type { 63 | case WsTpBeats: // heart beats 64 | case WsTpClose: // closed by client 65 | return hub.CloseProxyConn(id) 66 | case WsTpEst: // establish 67 | var proxyEstMsg ProxyEstMessage 68 | if err := json.Unmarshal(socketData, &proxyEstMsg); err != nil { 69 | return err 70 | } 71 | // check proxy type support. 72 | if (proxyEstMsg.Type == ProxyTypeHttp || proxyEstMsg.Type == ProxyTypeHttps) && !config.EnableHttp { 73 | hub.tellClosed(id) // tell client to close connection. 74 | return errors.New("http(s) proxy is not support in server side") 75 | } 76 | 77 | var estData []byte = nil 78 | if proxyEstMsg.WithData { 79 | if decodedBytes, err := base64.StdEncoding.DecodeString(proxyEstMsg.DataBase64); err != nil { 80 | log.Error("base64 decode error,", err) 81 | return err 82 | } else { 83 | estData = decodedBytes 84 | } 85 | } 86 | go establishProxy(hub, ProxyRegister{id, proxyEstMsg.Type, proxyEstMsg.Addr, estData}) 87 | case WsTpData: 88 | var requestMsg ProxyData 89 | if err := json.Unmarshal(socketData, &requestMsg); err != nil { 90 | return err 91 | } 92 | 93 | if proxy := hub.GetProxyById(id); proxy != nil { 94 | // write income data from websocket to TCP connection 95 | if decodeBytes, err := base64.StdEncoding.DecodeString(requestMsg.DataBase64); err != nil { 96 | log.Error("base64 decode error,", err) 97 | return err 98 | } else { 99 | return proxy.ProxyIns.onData(ClientData{Tag: requestMsg.Tag, Data: decodeBytes}) 100 | } 101 | } 102 | return nil 103 | } 104 | return nil 105 | } 106 | 107 | func establishProxy(hub *Hub, proxyMeta ProxyRegister) { 108 | var e ProxyEstablish 109 | if proxyMeta._type == ProxyTypeHttp { 110 | e = makeHttpProxyInstance() 111 | } else { 112 | e = &DefaultProxyEst{} 113 | } 114 | 115 | err := e.establish(hub, proxyMeta.id, proxyMeta._type, proxyMeta.addr, proxyMeta.withData) 116 | if err == nil { 117 | hub.tellClosed(proxyMeta.id) // tell client to close connection. 118 | } else if err != ConnCloseByClient { 119 | log.Error(err) // todo error handle better way 120 | hub.tellClosed(proxyMeta.id) 121 | } 122 | return 123 | // log.WithField("size", s.GetConnectorSize()).Trace("connection size changed.") 124 | } 125 | 126 | // data type used in DefaultProxyEst to pass data to channel 127 | type ChanDone struct { 128 | tell bool 129 | err error 130 | } 131 | 132 | // interface implementation for socks5 and https proxy. 133 | type DefaultProxyEst struct { 134 | done chan ChanDone 135 | tcpConn net.Conn 136 | } 137 | 138 | func (e *DefaultProxyEst) onData(data ClientData) error { 139 | if _, err := e.tcpConn.Write(data.Data); err != nil { 140 | e.done <- ChanDone{true, err} 141 | } 142 | return nil 143 | } 144 | 145 | func (e *DefaultProxyEst) Close(tell bool) error { 146 | e.done <- ChanDone{tell, ConnCloseByClient} 147 | return nil // todo error 148 | } 149 | 150 | // data: data send in establish step (can be nil). 151 | func (e *DefaultProxyEst) establish(hub *Hub, id ksuid.KSUID, proxyType int, addr string, data []byte) error { 152 | conn, err := net.DialTimeout("tcp", addr, time.Second*8) // todo config timeout 153 | if err != nil { 154 | return err 155 | } 156 | e.tcpConn = conn 157 | defer conn.Close() 158 | 159 | e.done = make(chan ChanDone, 2) 160 | //defer close(done) 161 | 162 | // todo check exists 163 | hub.addNewProxy(&ProxyServer{Id: id, ProxyIns: e}) 164 | defer hub.RemoveProxy(id) 165 | 166 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 167 | defer cancel() 168 | switch proxyType { 169 | case ProxyTypeSocks5: 170 | if err := hub.WriteProxyMessage(ctx, id, TagData, []byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); err != nil { 171 | return err 172 | } 173 | case ProxyTypeHttps: 174 | if err := hub.WriteProxyMessage(ctx, id, TagData, []byte("HTTP/1.0 200 Connection Established\r\nProxy-agent: wssocks\r\n\r\n")); err != nil { 175 | return err 176 | } 177 | } 178 | 179 | go func() { 180 | writer := NewWebSocketWriter(&hub.ConcurrentWebSocket, id, context.Background()) 181 | if _, err := io.Copy(writer, conn); err != nil { 182 | log.Error("copy error,", err) 183 | e.done <- ChanDone{true, err} 184 | } 185 | e.done <- ChanDone{true, nil} 186 | }() 187 | 188 | d := <-e.done 189 | // s.RemoveProxy(proxy.Id) 190 | // tellClosed is called outside this func. 191 | return d.err 192 | } 193 | 194 | type HttpProxyEst struct { 195 | bodyReadCloser *BufferedWR 196 | } 197 | 198 | func makeHttpProxyInstance() *HttpProxyEst { 199 | buf := NewBufferWR() 200 | return &HttpProxyEst{bodyReadCloser: buf} 201 | } 202 | 203 | func (h *HttpProxyEst) onData(data ClientData) error { 204 | if data.Tag == TagNoMore { 205 | return h.bodyReadCloser.Close() // close due to no more data. 206 | } 207 | if _, err := h.bodyReadCloser.Write(data.Data); err != nil { 208 | return err 209 | } 210 | return nil 211 | } 212 | 213 | func (h *HttpProxyEst) Close(tell bool) error { 214 | return h.bodyReadCloser.Close() // close from client 215 | } 216 | 217 | func (h *HttpProxyEst) establish(hub *Hub, id ksuid.KSUID, proxyType int, addr string, header []byte) error { 218 | if header == nil { 219 | hub.tellClosed(id) 220 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 221 | defer cancel() 222 | _ = hub.WriteProxyMessage(ctx, id, TagEstErr, nil) 223 | return errors.New("http header empty") 224 | } 225 | 226 | closed := make(chan bool) 227 | client := make(chan ClientData, 2) // for http at most 2 data buffers are needed(http body, TagNoMore tag). 228 | defer close(closed) 229 | defer close(client) 230 | 231 | hub.addNewProxy(&ProxyServer{Id: id, ProxyIns: h}) 232 | defer hub.RemoveProxy(id) 233 | defer func() { 234 | if !h.bodyReadCloser.isClosed() { // if it is not closed by client. 235 | hub.tellClosed(id) // todo 236 | } 237 | }() 238 | 239 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 240 | defer cancel() 241 | if err := hub.ConcurrentWebSocket.WriteProxyMessage(ctx, id, TagEstOk, nil); err != nil { 242 | return err 243 | } 244 | 245 | // get http request by header bytes. 246 | bufferHeader := bufio.NewReader(bytes.NewBuffer(header)) 247 | req, err := http.ReadRequest(bufferHeader) 248 | if err != nil { 249 | return err 250 | } 251 | req.Body = h.bodyReadCloser 252 | 253 | // read request and copy response back 254 | resp, err := http.DefaultTransport.RoundTrip(req) 255 | if err != nil { 256 | return fmt.Errorf("transport error: %w", err) 257 | } 258 | defer resp.Body.Close() 259 | 260 | writer := NewWebSocketWriter(&hub.ConcurrentWebSocket, id, context.Background()) 261 | var headerBuffer bytes.Buffer 262 | HttpRespHeader(&headerBuffer, resp) 263 | writer.Write(headerBuffer.Bytes()) 264 | if _, err := io.Copy(writer, resp.Body); err != nil { 265 | return fmt.Errorf("http body copy error: %w", err) 266 | } 267 | return nil 268 | } 269 | -------------------------------------------------------------------------------- /wss/status/status.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/genshen/wssocks/wss" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type Version struct { 11 | VersionStr string `json:"version_str"` 12 | VersionCode int `json:"version_code"` 13 | ComVersion int `json:"compatible_version"` 14 | } 15 | 16 | type Info struct { 17 | Version Version `json:"version"` 18 | ServerBaseUrl string `json:"server_base_url"` 19 | Socks5Enable bool `json:"socks5_enabled"` 20 | Socks5DisableReason string `json:"socks5_disabled_reason"` 21 | HttpsEnable bool `json:"http_enabled"` 22 | HttpsDisableReason string `json:"http_disabled_reason"` 23 | SSLEnable bool `json:"ssl_enabled"` 24 | SSLDisableReason string `json:"ssl_disabled_reason"` 25 | ConnKeyEnable bool `json:"conn_key_enable"` 26 | ConnKeyDisableReason string `json:"conn_key_disabled_reason"` 27 | } 28 | 29 | type Statistics struct { 30 | UpTime float64 `json:"up_time"` 31 | Clients int `json:"clients"` 32 | Proxies int `json:"proxies"` 33 | } 34 | 35 | type Status struct { 36 | Info Info `json:"info"` 37 | Statistics Statistics `json:"statistics"` 38 | } 39 | 40 | type handleStatus struct { 41 | enableHttp bool 42 | enableConnKey bool 43 | hc *wss.HubCollection 44 | setupTime time.Time 45 | serverBaseUrl string // base url of websocket 46 | } 47 | 48 | // create a http handle for handling service status 49 | func NewStatusHandle(hc *wss.HubCollection, enableHttp bool, enableConnKey bool, wsBaseUrl string) *handleStatus { 50 | return &handleStatus{ 51 | hc: hc, 52 | enableHttp: enableHttp, 53 | enableConnKey: enableConnKey, 54 | setupTime: time.Now(), 55 | serverBaseUrl: wsBaseUrl, 56 | } 57 | } 58 | 59 | func (s *handleStatus) ServeHTTP(w http.ResponseWriter, req *http.Request) { 60 | w.Header().Set("Access-Control-Allow-Origin", "*") // todo: remove in production env 61 | w.Header().Add("Access-Control-Allow-Headers", "Content-Type") 62 | w.Header().Set("Content-Type", "application/json") 63 | 64 | clients, proxies := s.hc.GetConnCount() 65 | duration := time.Now().Sub(s.setupTime).Truncate(time.Second) 66 | 67 | status := Status{ 68 | Info: Info{ 69 | Version: Version{ 70 | VersionStr: wss.CoreVersion, 71 | VersionCode: wss.VersionCode, 72 | ComVersion: wss.CompVersion, 73 | }, 74 | ServerBaseUrl: s.serverBaseUrl, 75 | Socks5Enable: true, 76 | Socks5DisableReason: "", 77 | HttpsEnable: s.enableHttp, 78 | ConnKeyEnable: s.enableConnKey, 79 | SSLEnable: false, 80 | SSLDisableReason: "not support", // todo ssl support 81 | }, 82 | Statistics: Statistics{ 83 | UpTime: duration.Seconds(), 84 | Clients: clients, 85 | Proxies: proxies, 86 | }, 87 | } 88 | 89 | if !status.Info.HttpsEnable { 90 | status.Info.HttpsDisableReason = "disabled" 91 | } 92 | if !status.Info.ConnKeyEnable { 93 | status.Info.ConnKeyDisableReason = "disabled" 94 | } 95 | 96 | if err := json.NewEncoder(w).Encode(status); err != nil { 97 | w.WriteHeader(http.StatusInternalServerError) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /wss/term_view/clear_lines_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package term_view 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | // ESC is the ASCII code for escape character 11 | const ESC = 27 12 | 13 | // clear the line and move the cursor up 14 | var clear = fmt.Sprintf("%c[%dA%c[2K", ESC, 1, ESC) 15 | 16 | func clearLines(outDev FdWriter, lines int) { 17 | _, _ = fmt.Fprint(outDev, strings.Repeat(clear, lines)) 18 | } 19 | -------------------------------------------------------------------------------- /wss/term_view/clear_lines_windows.go: -------------------------------------------------------------------------------- 1 | // file is copied from: https://github.com/gosuri/uilive/blob/master/writer_windows.go 2 | 3 | // +build windows 4 | 5 | package term_view 6 | 7 | import ( 8 | "fmt" 9 | "syscall" 10 | "unsafe" 11 | ) 12 | 13 | const ESC = 27 14 | 15 | var kernel32 = syscall.NewLazyDLL("kernel32.dll") 16 | 17 | var ( 18 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") 19 | procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") 20 | procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") 21 | ) 22 | 23 | // clear the line and move the cursor up 24 | var clear = fmt.Sprintf("%c[%dA%c[2K\r", ESC, 0, ESC) 25 | 26 | type short int16 27 | type dword uint32 28 | type word uint16 29 | 30 | type coord struct { 31 | x short 32 | y short 33 | } 34 | 35 | type smallRect struct { 36 | left short 37 | top short 38 | right short 39 | bottom short 40 | } 41 | 42 | type consoleScreenBufferInfo struct { 43 | size coord 44 | cursorPosition coord 45 | attributes word 46 | window smallRect 47 | maximumWindowSize coord 48 | } 49 | 50 | func clearLines(f FdWriter, lines int) { 51 | fd := f.Fd() 52 | var csbi consoleScreenBufferInfo 53 | _, _, _ = procGetConsoleScreenBufferInfo.Call(fd, uintptr(unsafe.Pointer(&csbi))) 54 | 55 | for i := 0; i < lines; i++ { 56 | // move the cursor up 57 | csbi.cursorPosition.y-- 58 | _, _, _ = procSetConsoleCursorPosition.Call(fd, uintptr(*(*int32)(unsafe.Pointer(&csbi.cursorPosition)))) 59 | // clear the line 60 | cursor := coord{ 61 | x: csbi.window.left, 62 | y: csbi.window.top + csbi.cursorPosition.y, 63 | } 64 | var count, w dword 65 | count = dword(csbi.size.x) 66 | _, _, _ = procFillConsoleOutputCharacter.Call(fd, uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w))) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /wss/term_view/log.go: -------------------------------------------------------------------------------- 1 | package term_view 2 | 3 | import ( 4 | "fmt" 5 | "github.com/genshen/wssocks/wss" 6 | "github.com/sirupsen/logrus" 7 | "golang.org/x/crypto/ssh/terminal" 8 | "os" 9 | "text/tabwriter" 10 | ) 11 | 12 | type ProgressLog struct { 13 | Writer *Writer // terminal writer todo defer Flush 14 | record *wss.ConnRecord 15 | } 16 | 17 | func NewPLog(cr *wss.ConnRecord) *ProgressLog { 18 | plog := ProgressLog{ 19 | record: cr, 20 | } 21 | plog.Writer = NewWriter() 22 | return &plog 23 | } 24 | 25 | // set progress log(connection table), 26 | // the connection table is write into p.Write 27 | // (p.Write is a bytes buffer, only really output to screen when calling Flush). 28 | func (p *ProgressLog) SetLogBuffer(r *wss.ConnRecord) { 29 | _, terminalRows, err := terminal.GetSize(int(os.Stdout.Fd())) 30 | if err != nil { 31 | logrus.Error(err) 32 | return 33 | } 34 | // log size is ok for terminal (at least one row) 35 | w := new(tabwriter.Writer) 36 | 37 | w.Init(p.Writer, 0, 0, 5, ' ', 0) 38 | defer w.Flush() 39 | 40 | _, _ = fmt.Fprintf(w, "TARGETs\tCONNECTIONs\t\n") 41 | terminalRows-- 42 | 43 | var recordsHiden = len(r.Addresses) 44 | if terminalRows >= 2 { // at least 2 lines left: one for show more records and one for new line(\n). 45 | // have rows left 46 | for addr, size := range r.Addresses { 47 | if terminalRows <= 2 { 48 | // hide left records 49 | break 50 | } else { 51 | _, _ = fmt.Fprintf(w, "%s\t%d\t\n", addr, size) 52 | terminalRows-- 53 | recordsHiden-- 54 | } 55 | } 56 | // log total connection size. 57 | if recordsHiden == 0 { 58 | _, _ = fmt.Fprintf(w, "TOTAL\t%d\t\n", r.ConnSize) 59 | } else { 60 | _, _ = w.Write([]byte(fmt.Sprintf("TOTAL\t%d\t(%d record(s) hidden)\t\n", 61 | r.ConnSize, recordsHiden))) 62 | } 63 | } 64 | } 65 | 66 | // write interface: write buffer data directly to stdout. 67 | func (p *ProgressLog) Write(buf []byte) (int, error) { 68 | p.record.Mutex.Lock() 69 | defer p.record.Mutex.Unlock() 70 | p.SetLogBuffer(p.record) // call Writer.Write() to set log data into buffer 71 | err := p.Writer.Flush(func() error { // flush buffer 72 | if _, err := p.Writer.OutDev.Write(buf); err != nil { // just write buff to stdout, and keep progress log. 73 | return err 74 | } 75 | return nil 76 | }) 77 | return len(buf), err 78 | } 79 | -------------------------------------------------------------------------------- /wss/term_view/writer.go: -------------------------------------------------------------------------------- 1 | // most code in this sub-package is copy or modified from https://github.com/gosuri/uilive 2 | 3 | package term_view 4 | 5 | import ( 6 | "bytes" 7 | "github.com/mattn/go-isatty" 8 | "io" 9 | "os" 10 | "sync" 11 | ) 12 | 13 | // FdWriter is a writer with a file descriptor. 14 | type FdWriter interface { 15 | io.Writer 16 | Fd() uintptr 17 | } 18 | 19 | // Writer will updates the terminal when flush is called. 20 | type Writer struct { 21 | OutDev io.Writer 22 | buf bytes.Buffer 23 | mtx *sync.Mutex 24 | lineCount int // lines of table that have wrote to terminal. 25 | } 26 | 27 | // NewWriter returns a new Writer with default values 28 | func NewWriter() *Writer { 29 | return &Writer{ 30 | OutDev: os.Stdout, 31 | mtx: &sync.Mutex{}, 32 | lineCount: 0, 33 | } 34 | } 35 | 36 | // wrapper function to call clearLines on different platform 37 | func (w *Writer) ClearLines() { 38 | f, ok := w.OutDev.(FdWriter) 39 | if ok && !isatty.IsTerminal(f.Fd()) { 40 | ok = false 41 | } 42 | if !ok { 43 | // dont clear lines if it is not a tty (e.g. io redirect to a file). 44 | return 45 | } 46 | clearLines(f, w.lineCount) 47 | } 48 | 49 | // Write write contents to the writer's io writer. 50 | func (w *Writer) NormalWrite(buf []byte) (n int, err error) { 51 | w.mtx.Lock() 52 | w.ClearLines() // clean progress lines first 53 | defer w.mtx.Unlock() 54 | return w.OutDev.Write(buf) 55 | } 56 | 57 | func (w *Writer) Write(buf []byte) (n int, err error) { 58 | w.mtx.Lock() 59 | defer w.mtx.Unlock() 60 | return w.buf.Write(buf) 61 | } 62 | 63 | // Flush writes to the out and resets the buffer. 64 | func (w *Writer) Flush(onLinesCleared func() error) error { 65 | w.mtx.Lock() 66 | defer w.mtx.Unlock() 67 | 68 | if len(w.buf.Bytes()) == 0 { 69 | return nil 70 | } 71 | w.ClearLines() 72 | if onLinesCleared != nil { 73 | if err := onLinesCleared(); err != nil { // callback if lines is cleared. 74 | return err 75 | } 76 | } 77 | 78 | lines := 0 79 | var currentLine bytes.Buffer 80 | for _, b := range w.buf.Bytes() { 81 | if b == '\n' { 82 | lines++ 83 | currentLine.Reset() 84 | } else { 85 | currentLine.Write([]byte{b}) 86 | // todo windows overflow 87 | //if overFlowHandled && currentLine.Len() > termWidth { 88 | // lines++ 89 | // currentLine.Reset() 90 | //} 91 | } 92 | } 93 | w.lineCount = lines 94 | _, err := w.OutDev.Write(w.buf.Bytes()) 95 | w.buf.Reset() 96 | return err 97 | } 98 | -------------------------------------------------------------------------------- /wss/version.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "context" 5 | "nhooyr.io/websocket" 6 | "nhooyr.io/websocket/wsjson" 7 | ) 8 | 9 | // version of protocol. 10 | const VersionCode = 0x004 11 | const CompVersion = 0x003 12 | const CoreVersion = "0.6.0" 13 | 14 | type VersionNeg struct { 15 | Version string `json:"version"` 16 | CompVersion uint `json:"comp_version"` // Compatible version code 17 | VersionCode uint `json:"version_code"` 18 | EnableStatusPage bool `json:"status_page"` 19 | } 20 | 21 | // negotiate client and server version 22 | // after websocket connection is established, 23 | // client can receive a message from server with server version number. 24 | func ExchangeVersion(ctx context.Context, wsConn *websocket.Conn) (VersionNeg, error) { 25 | var versionRec VersionNeg 26 | versionServer := VersionNeg{Version: CoreVersion, VersionCode: VersionCode} 27 | if err := wsjson.Write(ctx, wsConn, &versionServer); err != nil { 28 | return versionRec, err 29 | } 30 | if err := wsjson.Read(ctx, wsConn, &versionRec); err != nil { 31 | return versionRec, err 32 | } 33 | return versionRec, nil 34 | } 35 | 36 | // send version information to client from server 37 | func NegVersionServer(ctx context.Context, wsConn *websocket.Conn, enableStatusPage bool) error { 38 | // read from client 39 | var versionClient VersionNeg 40 | if err := wsjson.Read(ctx, wsConn, &versionClient); err != nil { 41 | return err 42 | } 43 | // send to client 44 | versionServer := VersionNeg{ 45 | Version: CoreVersion, 46 | CompVersion: CompVersion, 47 | VersionCode: VersionCode, 48 | EnableStatusPage: enableStatusPage, 49 | } // todo more information 50 | return wsjson.Write(ctx, wsConn, &versionServer) 51 | } 52 | -------------------------------------------------------------------------------- /wss/websocket_client.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "encoding/json" 7 | "github.com/segmentio/ksuid" 8 | "net/http" 9 | "nhooyr.io/websocket" 10 | "nhooyr.io/websocket/wsjson" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | // WebSocketClient is a collection of proxy clients. 16 | // It can add/remove proxy clients from this collection, 17 | // and dispatch web socket message to a specific proxy client. 18 | type WebSocketClient struct { 19 | ConcurrentWebSocket 20 | proxies map[ksuid.KSUID]*ProxyClient // all proxies on this websocket. 21 | proxyMu sync.RWMutex // mutex to operate proxies map. 22 | cancel context.CancelFunc 23 | } 24 | 25 | // get the connection size 26 | func (wsc *WebSocketClient) ConnSize() int { 27 | wsc.proxyMu.RLock() 28 | defer wsc.proxyMu.RUnlock() 29 | return len(wsc.proxies) 30 | } 31 | 32 | // Establish websocket connection. 33 | // And initialize proxies container. 34 | func NewWebSocketClient(ctx context.Context, addr string, hc *http.Client, header http.Header) (*WebSocketClient, error) { 35 | ws, _, err := websocket.Dial(ctx, addr, &websocket.DialOptions{HTTPClient: hc, HTTPHeader: header}) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return &WebSocketClient{ 40 | ConcurrentWebSocket: ConcurrentWebSocket{ 41 | WsConn: ws, 42 | }, 43 | cancel: nil, 44 | proxies: make(map[ksuid.KSUID]*ProxyClient), 45 | }, nil 46 | } 47 | 48 | // create a new proxy with unique id 49 | func (wsc *WebSocketClient) NewProxy(onData func(ksuid.KSUID, ServerData), 50 | onClosed func(ksuid.KSUID, bool), onError func(ksuid.KSUID, error)) *ProxyClient { 51 | id := ksuid.New() 52 | proxy := ProxyClient{Id: id, onData: onData, onClosed: onClosed, onError: onError} 53 | 54 | wsc.proxyMu.Lock() 55 | defer wsc.proxyMu.Unlock() 56 | 57 | wsc.proxies[id] = &proxy 58 | return &proxy 59 | } 60 | 61 | func (wsc *WebSocketClient) GetProxyById(id ksuid.KSUID) *ProxyClient { 62 | wsc.proxyMu.RLock() 63 | defer wsc.proxyMu.RUnlock() 64 | if proxy, ok := wsc.proxies[id]; ok { 65 | return proxy 66 | } 67 | return nil 68 | } 69 | 70 | // tell the remote proxy server to close this connection. 71 | func (wsc *WebSocketClient) TellClose(id ksuid.KSUID) error { 72 | // send finish flag to client 73 | finish := WebSocketMessage{ 74 | Id: id.String(), 75 | Type: WsTpClose, 76 | Data: nil, 77 | } 78 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 79 | defer cancel() 80 | if err := wsjson.Write(ctx, wsc.WsConn, &finish); err != nil { 81 | return err 82 | } 83 | return nil 84 | } 85 | 86 | // remove current proxy by id 87 | func (wsc *WebSocketClient) RemoveProxy(id ksuid.KSUID) { 88 | wsc.proxyMu.Lock() 89 | defer wsc.proxyMu.Unlock() 90 | if _, ok := wsc.proxies[id]; ok { 91 | delete(wsc.proxies, id) 92 | } 93 | } 94 | 95 | // listen income websocket messages and dispatch to different proxies. 96 | func (wsc *WebSocketClient) ListenIncomeMsg(readLimit int64) error { 97 | ctx, can := context.WithCancel(context.Background()) 98 | wsc.cancel = can 99 | wsc.WsConn.SetReadLimit(readLimit) 100 | 101 | for { 102 | // check stop first 103 | select { 104 | case <-ctx.Done(): 105 | return StoppedError 106 | default: 107 | // if the channel is still open, continue as normal 108 | } 109 | 110 | _, data, err := wsc.WsConn.Read(ctx) 111 | if err != nil { 112 | // todo close all 113 | return err // todo close websocket 114 | } 115 | 116 | var socketData json.RawMessage 117 | socketStream := WebSocketMessage{ 118 | Data: &socketData, 119 | } 120 | if err := json.Unmarshal(data, &socketStream); err != nil { 121 | continue // todo log 122 | } 123 | // find proxy by id 124 | if ksid, err := ksuid.Parse(socketStream.Id); err != nil { 125 | continue 126 | } else { 127 | if proxy := wsc.GetProxyById(ksid); proxy != nil { 128 | // now, we known the id and type of incoming data 129 | switch socketStream.Type { 130 | case WsTpClose: // remove proxy 131 | proxy.onClosed(ksid, false) 132 | case WsTpData: 133 | var proxyData ProxyData 134 | if err := json.Unmarshal(socketData, &proxyData); err != nil { 135 | proxy.onError(ksid, err) 136 | continue 137 | } 138 | if decodeBytes, err := base64.StdEncoding.DecodeString(proxyData.DataBase64); err != nil { 139 | proxy.onError(ksid, err) 140 | continue 141 | } else { 142 | // just write data back 143 | proxy.onData(ksid, ServerData{Tag: proxyData.Tag, Data: decodeBytes}) 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | 151 | func (wsc *WebSocketClient) Close() error { 152 | if wsc.cancel != nil { 153 | wsc.cancel() 154 | } 155 | if err := wsc.WSClose(); err != nil { 156 | return err 157 | } 158 | return nil 159 | } 160 | -------------------------------------------------------------------------------- /wss/ws_datatypes.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "github.com/segmentio/ksuid" 7 | "sync" 8 | ) 9 | 10 | const ( 11 | WsTpVer = "version" 12 | WsTpBeats = "heart_beat" 13 | WsTpClose = "finish" 14 | WsTpData = "data" 15 | WsTpEst = "est" // establish 16 | ) 17 | 18 | // write data to WebSocket server or client 19 | type Base64WSBufferWriter struct { 20 | buffer bytes.Buffer 21 | mu sync.Mutex 22 | } 23 | 24 | // implement Write interface to write bytes from ssh server into bytes.Buffer. 25 | func (b *Base64WSBufferWriter) Write(p []byte) (int, error) { 26 | b.mu.Lock() 27 | defer b.mu.Unlock() 28 | return b.buffer.Write(p) 29 | } 30 | 31 | // flush all data in this buff into WebSocket. 32 | func (b *Base64WSBufferWriter) Flush(messageType int, id ksuid.KSUID, cws ConcurrentWebSocketInterface) (int, error) { 33 | b.mu.Lock() 34 | defer b.mu.Unlock() 35 | 36 | length := b.buffer.Len() 37 | if length != 0 { 38 | dataBase64 := base64.StdEncoding.EncodeToString(b.buffer.Bytes()) 39 | jsonData := WebSocketMessage{ 40 | Id: id.String(), 41 | Type: WsTpData, 42 | Data: ProxyData{DataBase64: dataBase64}, 43 | } 44 | if err := cws.WriteWSJSON(&jsonData); err != nil { 45 | return 0, err 46 | } 47 | b.buffer.Reset() 48 | return length, nil 49 | } 50 | return 0, nil 51 | } 52 | 53 | type WebSocketMessage struct { 54 | Id string `json:"id"` 55 | Type string `json:"type"` 56 | Data interface{} `json:"data"` // json.RawMessage 57 | } 58 | 59 | // Proxy data (from server to client or from client to server) 60 | type ProxyData struct { 61 | Tag int `json:"tag"` 62 | DataBase64 string `json:"base64"` 63 | } 64 | 65 | // proxy data from client to server 66 | // type ProxyServerData ProxyData 67 | 68 | // Proxy message for establishing connection 69 | type ProxyEstMessage struct { 70 | Type int `json:"proxy_type"` 71 | Addr string `json:"addr"` 72 | WithData bool `json:"with_data"` 73 | DataBase64 string `json:"base64"` // establish with initialized data. 74 | } 75 | -------------------------------------------------------------------------------- /wss/wssocks_client.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/segmentio/ksuid" 8 | log "github.com/sirupsen/logrus" 9 | "io" 10 | "net" 11 | "sync" 12 | ) 13 | 14 | var StoppedError = errors.New("listener stopped") 15 | 16 | // client part of wssocks 17 | type Client struct { 18 | tcpl *net.TCPListener 19 | stop chan interface{} 20 | closed bool 21 | wgClose sync.WaitGroup // wait for closing 22 | } 23 | 24 | func NewClient() *Client { 25 | var client Client 26 | client.closed = false 27 | client.stop = make(chan interface{}) 28 | return &client 29 | } 30 | 31 | // parse target address and proxy type, and response to socks5/https client 32 | func (client *Client) Reply(conn net.Conn, enableHttp bool) ([]byte, int, string, error) { 33 | var buffer [1024]byte 34 | var addr string 35 | var proxyType int 36 | 37 | n, err := conn.Read(buffer[:]) 38 | if err != nil { 39 | return nil, 0, "", err 40 | } 41 | 42 | // select a matched proxy type 43 | instances := []ProxyInterface{&Socks5Client{}} 44 | if enableHttp { // if http and https proxy is enabled. 45 | instances = append(instances, &HttpsClient{}) 46 | } 47 | var matchedInstance ProxyInterface = nil 48 | for _, proxyInstance := range instances { 49 | if proxyInstance.Trigger(buffer[:n]) { 50 | matchedInstance = proxyInstance 51 | break 52 | } 53 | } 54 | 55 | if matchedInstance == nil { 56 | return nil, 0, "", errors.New("only socks5 or http(s) proxy") 57 | } 58 | 59 | // set address and type 60 | if proxyAddr, err := matchedInstance.ParseHeader(conn, buffer[:n]); err != nil { 61 | return nil, 0, "", err 62 | } else { 63 | proxyType = matchedInstance.ProxyType() 64 | addr = proxyAddr 65 | } 66 | // set data sent in establish step. 67 | if firstSendData, err := matchedInstance.EstablishData(buffer[:n]); err != nil { 68 | return nil, 0, "", err 69 | } else { 70 | // firstSendData can be nil, which means there is no data to be send during connection establishing. 71 | return firstSendData, proxyType, addr, nil 72 | } 73 | } 74 | 75 | // listen on local address:port and forward socks5 requests to wssocks server. 76 | func (client *Client) ListenAndServe(record *ConnRecord, wsc *WebSocketClient, address string, enableHttp bool, onConnected func()) error { 77 | netListener, err := net.Listen("tcp", address) 78 | if err != nil { 79 | return err 80 | } 81 | tcpl, ok := (netListener).(*net.TCPListener) 82 | if !ok { 83 | return errors.New("not a tcp listener") 84 | } 85 | client.tcpl = tcpl 86 | 87 | onConnected() 88 | for { 89 | // check stop first 90 | select { 91 | case <-client.stop: 92 | return StoppedError 93 | default: 94 | // if the channel is still open, continue as normal 95 | } 96 | 97 | c, err := tcpl.Accept() 98 | if err != nil { 99 | return fmt.Errorf("tcp accept error: %w", err) 100 | } 101 | 102 | go func() { 103 | conn := c.(*net.TCPConn) 104 | // defer c.Close() 105 | defer conn.Close() 106 | // In reply, we can get proxy type, target address and first send data. 107 | firstSendData, proxyType, addr, err := client.Reply(conn, enableHttp) 108 | if err != nil { 109 | log.Error("reply error: ", err) 110 | } 111 | client.wgClose.Add(1) 112 | defer client.wgClose.Done() 113 | 114 | // update connection record 115 | record.Update(ConnStatus{IsNew: true, Address: addr, Type: proxyType}) 116 | defer record.Update(ConnStatus{IsNew: false, Address: addr, Type: proxyType}) 117 | 118 | // on connection established, copy data now. 119 | if err := client.transData(wsc, conn, firstSendData, proxyType, addr); err != nil { 120 | log.Error("trans error: ", err) 121 | } 122 | }() 123 | } 124 | } 125 | 126 | func (client *Client) transData(wsc *WebSocketClient, conn *net.TCPConn, firstSendData []byte, proxyType int, addr string) error { 127 | type Done struct { 128 | tell bool 129 | err error 130 | } 131 | done := make(chan Done, 2) 132 | // defer close(done) 133 | 134 | // create a with proxy with callback func 135 | proxy := wsc.NewProxy(func(id ksuid.KSUID, data ServerData) { 136 | if _, err := conn.Write(data.Data); err != nil { 137 | done <- Done{true, err} 138 | } 139 | }, func(id ksuid.KSUID, tell bool) { 140 | done <- Done{tell, nil} 141 | }, func(id ksuid.KSUID, err error) { 142 | if err != nil { 143 | done <- Done{true, err} 144 | } 145 | }) 146 | 147 | // tell server to establish connection 148 | if err := proxy.Establish(wsc, firstSendData, proxyType, addr); err != nil { 149 | wsc.RemoveProxy(proxy.Id) 150 | err := wsc.TellClose(proxy.Id) 151 | if err != nil { 152 | log.Error("close error", err) 153 | } 154 | return err 155 | } 156 | 157 | // trans incoming data from proxy client application. 158 | ctx, cancel := context.WithCancel(context.Background()) 159 | writer := NewWebSocketWriterWithMutex(&wsc.ConcurrentWebSocket, proxy.Id, ctx) 160 | go func() { 161 | _, err := io.Copy(writer, conn) 162 | if err != nil { 163 | log.Error("write error: ", err) 164 | } 165 | done <- Done{true, err} 166 | }() 167 | defer writer.CloseWsWriter(cancel) // cancel data writing 168 | 169 | d := <-done 170 | wsc.RemoveProxy(proxy.Id) 171 | if d.tell { 172 | if err := wsc.TellClose(proxy.Id); err != nil { 173 | return err 174 | } 175 | } 176 | if d.err != nil { 177 | return d.err 178 | } 179 | return nil 180 | } 181 | 182 | // Close stops listening on the TCP address, 183 | // But the active links are not closed and wait them to finish. 184 | func (client *Client) Close(wait bool) error { 185 | if client.closed { 186 | return nil 187 | } 188 | close(client.stop) 189 | client.closed = true 190 | err := client.tcpl.Close() 191 | if wait { 192 | client.wgClose.Wait() // wait the active connection to finish 193 | } 194 | return err 195 | } 196 | -------------------------------------------------------------------------------- /wss/wssocks_server.go: -------------------------------------------------------------------------------- 1 | package wss 2 | 3 | import ( 4 | "context" 5 | log "github.com/sirupsen/logrus" 6 | "io" 7 | "net/http" 8 | "nhooyr.io/websocket" 9 | ) 10 | 11 | type WebsocksServerConfig struct { 12 | EnableHttp bool 13 | EnableConnKey bool // bale connection key 14 | ConnKey string // connection key 15 | EnableStatusPage bool // enable/disable status page 16 | } 17 | 18 | type ServerWS struct { 19 | config WebsocksServerConfig 20 | hc *HubCollection 21 | } 22 | 23 | // return a a function handling websocket requests from the peer. 24 | func NewServeWS(hc *HubCollection, config WebsocksServerConfig) *ServerWS { 25 | return &ServerWS{config: config, hc: hc} 26 | } 27 | 28 | func (s *ServerWS) ServeHTTP(w http.ResponseWriter, r *http.Request) { 29 | // check connection key 30 | if s.config.EnableConnKey && r.Header.Get("Key") != s.config.ConnKey { 31 | w.WriteHeader(401) 32 | w.Write([]byte("Access denied!\n")) 33 | return 34 | } 35 | 36 | wc, err := websocket.Accept(w, r, nil) 37 | if err != nil { 38 | log.Error(err) 39 | return 40 | } 41 | defer wc.Close(websocket.StatusNormalClosure, "the sky is falling") 42 | 43 | ctx, cancel := context.WithCancel(r.Context()) 44 | defer cancel() 45 | 46 | // negotiate version with client. 47 | if err := NegVersionServer(ctx, wc, s.config.EnableStatusPage); err != nil { 48 | return 49 | } 50 | 51 | hub := s.hc.NewHub(wc) 52 | defer s.hc.RemoveProxy(hub.id) 53 | defer hub.Close() 54 | // read messages from webSocket 55 | wc.SetReadLimit(1 << 23) // 8 MiB 56 | for { 57 | msgType, p, err := wc.Read(ctx) // fixme context 58 | // if WebSocket is closed by some reason, then this func will return, 59 | // and 'done' channel will be set, the outer func will reach to the end. 60 | if err != nil && err != io.EOF { 61 | log.Error("error reading webSocket message:", err) 62 | break 63 | } 64 | if err = dispatchMessage(hub, msgType, p, s.config); err != nil { 65 | log.Error("error proxy:", err) 66 | // break skip error 67 | } 68 | } 69 | } 70 | --------------------------------------------------------------------------------