Init
This commit is contained in:
commit
e4523df602
42
.gitea/workflows/ci.yml
Normal file
42
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: make build
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
28
.gitea/workflows/release.yml
Normal file
28
.gitea/workflows/release.yml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
|
||||||
|
- name: Build release binaries
|
||||||
|
run: VERSION=${{ github.ref_name }} make release
|
||||||
|
|
||||||
|
- name: Upload Release Assets
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: websocket-relay
|
||||||
|
path: build/*
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Binaries
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
websocket-relay
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# SSL certificates
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.crt
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Other
|
||||||
|
config.yaml
|
||||||
25
Makefile
Normal file
25
Makefile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
.PHONY: build test release clean
|
||||||
|
|
||||||
|
BINARY_NAME=websocket-relay
|
||||||
|
VERSION?=1.0.0
|
||||||
|
|
||||||
|
build:
|
||||||
|
mkdir -p build
|
||||||
|
go build -o build/$(BINARY_NAME) .
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
release:
|
||||||
|
mkdir -p build
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s -X main.version=$(VERSION)" -o build/$(BINARY_NAME)-linux-amd64 .
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-w -s -X main.version=$(VERSION)" -o build/$(BINARY_NAME)-darwin-arm64 .
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf build/$(BINARY_NAME)*
|
||||||
|
|
||||||
|
run:
|
||||||
|
go run .
|
||||||
|
|
||||||
|
deps:
|
||||||
|
go mod tidy
|
||||||
33
README.md
Normal file
33
README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# WebSocket Relay Server
|
||||||
|
|
||||||
|
A minimal Go WebSocket relay server with SSL support for P2P connections.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod tidy
|
||||||
|
# Configure via config.yaml (see config.yaml for options)
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Edit `config.yaml` to configure:
|
||||||
|
- **Server port and TLS settings**
|
||||||
|
- **SSL certificate paths**
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- WebSocket endpoint: `/ws`
|
||||||
|
- All WebSocket messages are relayed to all connected clients
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// For TLS enabled (default config)
|
||||||
|
const ws = new WebSocket('wss://localhost:8443/ws');
|
||||||
|
// For HTTP only
|
||||||
|
// const ws = new WebSocket('ws://localhost:8443/ws');
|
||||||
|
ws.onmessage = (event) => console.log('Received:', event.data);
|
||||||
|
ws.send('Hello from client!');
|
||||||
|
```
|
||||||
6
config.example.yaml
Normal file
6
config.example.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
server:
|
||||||
|
port: 8443
|
||||||
|
tls:
|
||||||
|
enabled: true
|
||||||
|
cert_file: cert.pem
|
||||||
|
key_file: key.pem
|
||||||
86
example/index.html
Normal file
86
example/index.html
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Websocket relay example</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<style>
|
||||||
|
.chat {
|
||||||
|
width: 600px;
|
||||||
|
height: 500px;
|
||||||
|
border: 1px solid black;
|
||||||
|
overflow-y: scroll;
|
||||||
|
background-color: beige;
|
||||||
|
padding: 15px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat .bubble {
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: bisque;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat .error {
|
||||||
|
color: red;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div>
|
||||||
|
<h1>P2P Chat</h1>
|
||||||
|
<div class="chat" id="box"></div>
|
||||||
|
<textarea id="message"></textarea>
|
||||||
|
<br />
|
||||||
|
<button id="send" disabled>Send</button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const chat = document.getElementById("box");
|
||||||
|
const message = document.getElementById("message");
|
||||||
|
const btn = document.getElementById("send");
|
||||||
|
const name = Date.now().toString(36);
|
||||||
|
let retry = 1000;
|
||||||
|
let ws;
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
ws = new WebSocket('ws://localhost:8000/');
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
console.log('Received:', event.data);
|
||||||
|
chat.innerHTML += `<div class="bubble">${event.data}</div>`;
|
||||||
|
};
|
||||||
|
ws.onopen = () => {
|
||||||
|
btn.removeAttribute("disabled");
|
||||||
|
ws.send(`${name} joined the chat`);
|
||||||
|
};
|
||||||
|
ws.onerror = (ev) => {
|
||||||
|
btn.setAttribute("disabled", "disabled");
|
||||||
|
chat.innerHTML += `<div class="error">Failed to connect to websocket, retrying...</div>`;
|
||||||
|
console.error(ev);
|
||||||
|
delete ws.onmessage;
|
||||||
|
delete ws.onopen;
|
||||||
|
setTimeout(connect, retry);
|
||||||
|
retry *= 2;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
btn.addEventListener("click", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const msg = message.value.trim();
|
||||||
|
if (!msg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = `${name}<br>${msg}`;
|
||||||
|
ws.send(data);
|
||||||
|
message.value = "";
|
||||||
|
});
|
||||||
|
connect();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
23
go.mod
Normal file
23
go.mod
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
module websocket-relay
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/websocket v1.5.1
|
||||||
|
github.com/prometheus/client_golang v1.17.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
|
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
||||||
|
github.com/prometheus/common v0.44.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.11.1 // indirect
|
||||||
|
golang.org/x/net v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
|
)
|
||||||
47
go.sum
Normal file
47
go.sum
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
|
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||||
|
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||||
|
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
|
||||||
|
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||||
|
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||||
|
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||||
|
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||||
|
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
32
internal/config/config.go
Normal file
32
internal/config/config.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server struct {
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
TLS struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
CertFile string `yaml:"cert_file"`
|
||||||
|
KeyFile string `yaml:"key_file"`
|
||||||
|
} `yaml:"tls"`
|
||||||
|
} `yaml:"server"`
|
||||||
|
Metrics struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
} `yaml:"metrics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(filename string) (*Config, error) {
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var config Config
|
||||||
|
err = yaml.Unmarshal(data, &config)
|
||||||
|
return &config, err
|
||||||
|
}
|
||||||
45
internal/config/config_test.go
Normal file
45
internal/config/config_test.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoad(t *testing.T) {
|
||||||
|
testConfig := `server:
|
||||||
|
port: 9000
|
||||||
|
tls:
|
||||||
|
enabled: false
|
||||||
|
cert_file: test.pem
|
||||||
|
key_file: test.key`
|
||||||
|
|
||||||
|
tmpFile, err := os.CreateTemp("", "config_test_*.yaml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpFile.Name())
|
||||||
|
|
||||||
|
if _, err := tmpFile.WriteString(testConfig); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tmpFile.Close()
|
||||||
|
|
||||||
|
config, err := Load(tmpFile.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Server.Port != 9000 {
|
||||||
|
t.Errorf("Expected port 9000, got %d", config.Server.Port)
|
||||||
|
}
|
||||||
|
if config.Server.TLS.Enabled != false {
|
||||||
|
t.Errorf("Expected TLS disabled, got %v", config.Server.TLS.Enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFileNotFound(t *testing.T) {
|
||||||
|
_, err := Load("nonexistent.yaml")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for nonexistent file")
|
||||||
|
}
|
||||||
|
}
|
||||||
97
internal/hub/hub.go
Normal file
97
internal/hub/hub.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package hub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"websocket-relay/internal/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
CheckOrigin: func(r *http.Request) bool { return true },
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hub struct {
|
||||||
|
clients map[*websocket.Conn]bool
|
||||||
|
broadcast chan []byte
|
||||||
|
register chan *websocket.Conn
|
||||||
|
unregister chan *websocket.Conn
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Hub {
|
||||||
|
return &Hub{
|
||||||
|
clients: make(map[*websocket.Conn]bool),
|
||||||
|
broadcast: make(chan []byte),
|
||||||
|
register: make(chan *websocket.Conn),
|
||||||
|
unregister: make(chan *websocket.Conn),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) Run() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case conn := <-h.register:
|
||||||
|
h.mu.Lock()
|
||||||
|
h.clients[conn] = true
|
||||||
|
h.mu.Unlock()
|
||||||
|
metrics.ConnectedClients.Set(float64(len(h.clients)))
|
||||||
|
metrics.ConnectionsTotal.Inc()
|
||||||
|
log.Printf("Client connected. Total: %d", len(h.clients))
|
||||||
|
|
||||||
|
case conn := <-h.unregister:
|
||||||
|
h.mu.Lock()
|
||||||
|
if _, ok := h.clients[conn]; ok {
|
||||||
|
delete(h.clients, conn)
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
metrics.ConnectedClients.Set(float64(len(h.clients)))
|
||||||
|
metrics.DisconnectionsTotal.Inc()
|
||||||
|
log.Printf("Client disconnected. Total: %d", len(h.clients))
|
||||||
|
|
||||||
|
case message := <-h.broadcast:
|
||||||
|
metrics.MessagesTotal.Inc()
|
||||||
|
h.mu.RLock()
|
||||||
|
for conn := range h.clients {
|
||||||
|
if err := conn.WriteMessage(websocket.TextMessage, message); err != nil {
|
||||||
|
delete(h.clients, conn)
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.mu.RUnlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WebSocket upgrade error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.register <- conn
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
h.unregister <- conn
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, message, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
h.broadcast <- message
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) ClientCount() int {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
return len(h.clients)
|
||||||
|
}
|
||||||
40
internal/hub/hub_test.go
Normal file
40
internal/hub/hub_test.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package hub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
h := New()
|
||||||
|
if h == nil {
|
||||||
|
t.Fatal("New returned nil")
|
||||||
|
}
|
||||||
|
if h.clients == nil {
|
||||||
|
t.Error("clients map not initialized")
|
||||||
|
}
|
||||||
|
if h.broadcast == nil {
|
||||||
|
t.Error("broadcast channel not initialized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientCount(t *testing.T) {
|
||||||
|
h := New()
|
||||||
|
go h.Run()
|
||||||
|
|
||||||
|
if count := h.ClientCount(); count != 0 {
|
||||||
|
t.Errorf("Expected 0 clients, got %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBroadcastChannel(t *testing.T) {
|
||||||
|
h := New()
|
||||||
|
go h.Run()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case h.broadcast <- []byte("test"):
|
||||||
|
// Channel is working
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Error("broadcast channel blocked")
|
||||||
|
}
|
||||||
|
}
|
||||||
28
internal/metrics/metrics.go
Normal file
28
internal/metrics/metrics.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ConnectedClients = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Name: "websocket_connected_clients",
|
||||||
|
Help: "Number of currently connected WebSocket clients",
|
||||||
|
})
|
||||||
|
|
||||||
|
MessagesTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||||
|
Name: "websocket_messages_total",
|
||||||
|
Help: "Total number of WebSocket messages processed",
|
||||||
|
})
|
||||||
|
|
||||||
|
ConnectionsTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||||
|
Name: "websocket_connections_total",
|
||||||
|
Help: "Total number of WebSocket connections established",
|
||||||
|
})
|
||||||
|
|
||||||
|
DisconnectionsTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||||
|
Name: "websocket_disconnections_total",
|
||||||
|
Help: "Total number of WebSocket disconnections",
|
||||||
|
})
|
||||||
|
)
|
||||||
43
main.go
Normal file
43
main.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"websocket-relay/internal/config"
|
||||||
|
"websocket-relay/internal/hub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg, err := config.Load("config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to load config:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := hub.New()
|
||||||
|
go h.Run()
|
||||||
|
|
||||||
|
// Start metrics server if enabled
|
||||||
|
if cfg.Metrics.Enabled {
|
||||||
|
go func() {
|
||||||
|
metricsAddr := fmt.Sprintf(":%d", cfg.Metrics.Port)
|
||||||
|
log.Printf("Metrics server starting on %s", metricsAddr)
|
||||||
|
http.Handle("/metrics", promhttp.Handler())
|
||||||
|
log.Fatal(http.ListenAndServe(metricsAddr, nil))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/ws", h.HandleWebSocket)
|
||||||
|
|
||||||
|
addr := fmt.Sprintf(":%d", cfg.Server.Port)
|
||||||
|
if cfg.Server.TLS.Enabled {
|
||||||
|
log.Printf("WebSocket relay server starting on %s (TLS)", addr)
|
||||||
|
log.Fatal(http.ListenAndServeTLS(addr, cfg.Server.TLS.CertFile, cfg.Server.TLS.KeyFile, mux))
|
||||||
|
} else {
|
||||||
|
log.Printf("WebSocket relay server starting on %s (HTTP)", addr)
|
||||||
|
log.Fatal(http.ListenAndServe(addr, mux))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user