├── .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 | 
4 | [](https://goreportcard.com/report/github.com/genshen/wssocks)
5 | 
6 | 
7 | 
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 |
--------------------------------------------------------------------------------