diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b0a585a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,43 @@ +# Git files +.git +.gitignore +.gitattributes + +# Documentation +*.md +docs/ + +# Test files +test/ +*_test.go +coverage.out + +# Environment files +.env +.env.local +.env.*.local + +# Build artifacts +bin/ +tmp/ +*.exe +*.dll +*.so +*.dylib + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# CI/CD +.github/ + +# Makefile (optional - uncomment if you want to exclude it) +# Makefile + +# License (optional - uncomment if you want to exclude it) +# LICENSE diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f5afb69 --- /dev/null +++ b/.env.example @@ -0,0 +1,43 @@ +# GoChat Server Configuration +# Copy this file to .env and modify as needed + +# Server Configuration +# Port on which the server will listen (default: :8080) +# Format: :PORT or HOST:PORT +SERVER_PORT=:8080 + +# Allowed Origins for CORS (comma-separated list) +# Use "*" to allow all origins (NOT recommended for production) +# Examples: +# - Single origin: http://localhost:3000 +# - Multiple origins: http://localhost:3000,https://example.com,https://app.example.com +# - Allow all: * +ALLOWED_ORIGINS=http://localhost:8080,http://localhost:3000 + +# Maximum Message Size +# Maximum size in bytes for incoming WebSocket messages (default: 512) +# Helps prevent denial-of-service attacks from oversized messages +MAX_MESSAGE_SIZE=512 + +# Rate Limiting Configuration +# Maximum number of messages allowed in a burst (default: 5) +# Controls how many messages a client can send before being rate limited +RATE_LIMIT_BURST=5 + +# Rate limit refill interval in seconds (default: 1) +# How often the rate limit bucket refills +RATE_LIMIT_REFILL_INTERVAL=1 + +# Production Environment Example: +# SERVER_PORT=:8080 +# ALLOWED_ORIGINS=https://chat.example.com,https://app.example.com +# MAX_MESSAGE_SIZE=1024 +# RATE_LIMIT_BURST=10 +# RATE_LIMIT_REFILL_INTERVAL=2 + +# Development Environment Example: +# SERVER_PORT=:8080 +# ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080 +# MAX_MESSAGE_SIZE=512 +# RATE_LIMIT_BURST=5 +# RATE_LIMIT_REFILL_INTERVAL=1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e078f44 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,60 @@ +# Multi-stage Dockerfile for GoChat Server +# This creates a minimal, secure production image + +# Stage 1: Build stage +FROM golang:1.25.1-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache ca-certificates git tzdata + +# Set working directory +WORKDIR /build + +# Copy go mod files first for better caching +COPY go.mod go.sum ./ +RUN go mod download && go mod verify + +# Copy source code +COPY . . + +# Build the application with security optimizations +# -trimpath removes file system paths from the executable +# -ldflags="-s -w" strips debug information to reduce binary size +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -trimpath \ + -ldflags="-s -w -X main.version=${VERSION:-dev}" \ + -o gochat \ + ./cmd/server + +# Stage 2: Runtime stage +FROM alpine:3.20 + +# Install runtime dependencies and create non-root user +RUN apk add --no-cache ca-certificates tzdata && \ + addgroup -g 1000 gochat && \ + adduser -D -u 1000 -G gochat gochat + +# Set working directory +WORKDIR /app + +# Copy the binary from builder +COPY --from=builder /build/gochat . + +# Copy timezone data +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo + +# Change ownership to non-root user +RUN chown -R gochat:gochat /app + +# Switch to non-root user +USER gochat + +# Expose the default port (can be overridden with environment variable) +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1 + +# Run the application +ENTRYPOINT ["/app/gochat"] diff --git a/README.md b/README.md index 6769280..b5b443c 100644 --- a/README.md +++ b/README.md @@ -5,439 +5,146 @@ [![Go Version](https://img.shields.io/github/go-mod/go-version/Tyrowin/gochat)](https://golang.org/) [![Go Report Card](https://goreportcard.com/badge/github.com/Tyrowin/gochat)](https://goreportcard.com/report/github.com/Tyrowin/gochat) -High-performance, standalone, multi-client chat server built using Go and WebSockets. +A high-performance, production-ready WebSocket chat server built with Go. GoChat provides real-time multi-client communication with built-in security features, comprehensive testing, and cross-platform support. ## Features -- Real-time WebSocket-based chat communication -- Multi-client support with concurrent connections -- **Cross-platform development support** - Build and develop on Windows, macOS, or Linux -- **Cross-compilation** - Build binaries for any platform from any platform -- Built-in security and vulnerability scanning -- Comprehensive CI/CD pipeline with automated testing -- Static code analysis with golangci-lint -- Dependency vulnerability scanning with govulncheck - -## Prerequisites - -- Go 1.25.1 or later -- Git -- Make (optional but recommended for easier builds) - - **Windows**: Install via [Chocolatey](https://chocolatey.org/) (`choco install make`) - - **macOS**: Install Xcode Command Line Tools (`xcode-select --install`) - - **Linux**: Usually pre-installed (`apt install make` or `yum install make`) - -**Note**: If you don't have Make, you can use Go commands directly (see [Build Guide](docs/BUILD_GUIDE.md)). +- **Real-time Communication** - WebSocket-based instant messaging +- **Multi-client Support** - Handle thousands of concurrent connections +- **Built-in Security** - Origin validation, rate limiting, and message size limits +- **Production Ready** - Comprehensive testing, CI/CD pipeline, and deployment guides +- **Cross-platform** - Build and run on Windows, macOS, and Linux +- **Zero Dependencies** - Statically linked binaries with no external runtime dependencies +- **Docker Support** - Production-ready containerization with multi-stage builds +- **Environment Configuration** - Easy configuration via environment variables +- **Easy Deployment** - Simple binary or container deployment with reverse proxy support ## Quick Start -1. **Clone the repository** - - ```bash - git clone https://github.com/Tyrowin/gochat.git - cd gochat - ``` - -2. **Install development tools** - - ```bash - make install-tools - ``` - -3. **Build the application** - - **With Make:** - - ```bash - make build - ``` - - **Or with Go directly:** - - ```bash - go build -o bin/gochat ./cmd/server - ``` - -4. **Run the server** - - **On Windows:** - - ```powershell - .\bin\gochat.exe - ``` - - **On macOS/Linux:** - - ```bash - ./bin/gochat - ``` - - **Or using Make:** - - ```bash - make run - ``` - -The server will start on `http://localhost:8080` with the following endpoints: - -- `/` - Health check -- `/ws` - WebSocket connection endpoint -- `/test` - Test page for WebSocket functionality - -## Development - -### Prerequisites for Development - -Install the required development tools: - -```bash -make install-tools -``` - -This will install: - -- `golangci-lint` - Static code analysis -- `govulncheck` - Vulnerability scanner -- `gosec` - Security analyzer -- `goimports` - Import formatter -- `air` - Live reload for development - -### Available Make Targets - -Run `make help` to see all available targets: - -```bash -make help -``` - -### Common Development Commands - -- **Format and lint code**: `make fmt lint` -- **Run tests**: `make test` -- **Run tests with coverage**: `make test-coverage` -- **Security scanning**: `make security-scan` -- **Check dependencies**: `make deps-check` -- **Development mode with auto-reload**: `make dev` -- **Run full CI pipeline locally**: `make ci-local` - -### Code Quality and Security - -This project uses several tools to ensure code quality, security, and best practices: - -#### Static Analysis - -- **golangci-lint**: Comprehensive Go linter with multiple analyzers - ```bash - make lint - ``` - -#### Security Scanning - -- **govulncheck**: Official Go vulnerability scanner -- **gosec**: Security analyzer for Go code - ```bash - make security-scan - ``` - -#### Dependency Management - -- **Vulnerability checking**: Automated scanning of dependencies -- **License compliance**: Check licenses of all dependencies - ```bash - make deps-check - make license-check - ``` - -### Configuration Files - -- **`.golangci.yml`**: golangci-lint configuration with enabled linters and rules -- **`.github/workflows/ci.yml`**: GitHub Actions CI/CD pipeline -- **`Makefile`**: Development and build automation - -### CI/CD Pipeline - -The project includes a comprehensive GitHub Actions CI/CD pipeline that runs on every push and pull request: - -1. **Build and Test**: Compiles code and runs all tests with coverage -2. **Static Analysis**: Runs golangci-lint with comprehensive rule set -3. **Security Scan**: Vulnerability scanning with govulncheck and Nancy -4. **Dependency Check**: Validates dependencies and checks for updates -5. **Multi-version Build**: Tests against multiple Go versions -6. **Docker Security**: Scans Docker images with Trivy (if applicable) -7. **Quality Gate**: Ensures all checks pass before allowing merges - -### Running CI Pipeline Locally - -To run the same checks that run in CI: - -```bash -make ci-local -``` - -This will run: - -- Code formatting -- Linting -- Security scanning -- Tests with coverage -- Dependency checks -- Build verification - -### Performance and Benchmarking - -- **Run benchmarks**: `make bench` -- **Race condition detection**: `make race` -- **Generate dependency graph**: `make deps-graph` - -## Building and Deployment - -### Cross-Platform Development - -GoChat supports development and building on **Windows**, **macOS**, and **Linux** with full cross-compilation capabilities. You can build binaries for any platform from any platform. - -#### Using Make (Recommended) - -The Makefile works on all platforms (requires `make`): +### Local Build ```bash -# Build for current platform -make build-current - -# Build for specific platforms -make build-linux # Linux (amd64) -make build-linux-arm64 # Linux (arm64) -make build-darwin # macOS Intel -make build-darwin-arm64 # macOS Apple Silicon -make build-windows # Windows (amd64) - -# Build for all platforms -make build-all - -# Create optimized release builds -make release - -# List all supported platforms -make list-platforms -``` +# Clone the repository +git clone https://github.com/Tyrowin/gochat.git +cd gochat -#### Without Make (Direct Go Commands) - -If you don't have Make installed, you can use Go directly: - -**Windows (PowerShell):** - -```powershell -# Build for current platform -go build -o bin\gochat.exe .\cmd\server - -# Build for Linux -$env:GOOS="linux"; $env:GOARCH="amd64"; go build -o bin\gochat-linux-amd64 .\cmd\server +# Build the server +make build -# Build for macOS -$env:GOOS="darwin"; $env:GOARCH="arm64"; go build -o bin\gochat-darwin-arm64 .\cmd\server +# Run the server +./bin/gochat ``` -**macOS/Linux:** +### Docker ```bash -# Build for current platform -go build -o bin/gochat ./cmd/server - -# Build for Windows -GOOS=windows GOARCH=amd64 go build -o bin/gochat-windows-amd64.exe ./cmd/server +# Using Docker Compose (recommended) +docker-compose up -d -# Build for macOS Apple Silicon -GOOS=darwin GOARCH=arm64 go build -o bin/gochat-darwin-arm64 ./cmd/server +# Or build and run manually +docker build -t gochat:latest . +docker run -d -p 8080:8080 --name gochat gochat:latest ``` -#### Cross-Compilation Examples +### Configuration -Go's cross-compilation support means you can build for any platform from any platform: - -**From Windows → Build for Linux:** +GoChat can be configured using environment variables. Copy `.env.example` to `.env` and customize: ```bash -make build-linux +cp .env.example .env +# Edit .env with your settings ``` -**From macOS → Build for Windows:** +The server starts on `http://localhost:8080`. Visit `http://localhost:8080/test` to try the interactive test page. -```bash -make build-windows -``` +## Documentation -**From Linux → Build for macOS (Apple Silicon):** +### Getting Started -```bash -make build-darwin-arm64 -``` +- **[Getting Started Guide](docs/GETTING_STARTED.md)** - Installation, building, and running the server +- **[API Documentation](docs/API.md)** - WebSocket API reference and code examples -#### Understanding the Build Output +### Deployment -After building, binaries are organized in the `./bin` directory: +- **[Deployment Guide](docs/DEPLOYMENT.md)** - Production deployment with Nginx/Caddy, TLS/WSS setup, and process management +- **[Security Documentation](docs/SECURITY.md)** - Security features, configuration, and best practices -``` -bin/ -├── gochat # Current platform binary (from `make build`) -├── linux/ -│ ├── gochat-amd64 # Linux 64-bit -│ ├── gochat-arm64 # Linux ARM64 (Raspberry Pi, AWS Graviton) -│ └── checksums.txt # SHA256 checksums (from `make release`) -├── darwin/ -│ ├── gochat-amd64 # macOS Intel -│ ├── gochat-arm64 # macOS Apple Silicon (M1/M2/M3) -│ └── checksums.txt # SHA256 checksums (from `make release`) -└── windows/ - ├── gochat-amd64.exe # Windows 64-bit - └── checksums.txt # SHA256 checksums (from `make release`) -``` +### Development -**Note:** Platform-specific build targets create binaries in their respective subdirectories, making it easier to manage and distribute builds for different platforms. +- **[Development Guide](docs/DEVELOPMENT.md)** - Development setup, testing, and CI/CD +- **[Building Guide](docs/BUILDING.md)** - Build instructions and cross-compilation +- **[Contributing Guide](docs/CONTRIBUTING.md)** - How to contribute to the project -#### Platform-Specific Code +## Architecture -If you need to write platform-specific code, Go provides several approaches: +### Simple and Focused -**File Name Suffixes:** +GoChat follows a clean, modular architecture: ``` -config_windows.go # Only compiled on Windows -config_linux.go # Only compiled on Linux -config_darwin.go # Only compiled on macOS +Client (Browser/App) + ↓ WebSocket (ws:// or wss://) +Reverse Proxy (Nginx/Caddy) + ↓ HTTP +GoChat Server (Go) + ├── Hub (Message Broker) + ├── Clients (WebSocket Connections) + └── Security (Rate Limiting, Origin Validation) ``` -**Build Tags:** +### Key Components -```go -//go:build linux && amd64 +- **Hub** - Central message broker coordinating all connected clients +- **Client** - WebSocket connection handler with read/write pumps +- **Rate Limiter** - Token bucket per-connection rate limiting +- **Origin Validator** - CSWSH attack prevention +- **Handlers** - HTTP/WebSocket request handlers -package mypackage +See [Development Guide](docs/DEVELOPMENT.md#project-structure) for detailed architecture information. -// This code only compiles for 64-bit Linux -``` +## Technology Stack -**Runtime Detection:** +- **Language:** Go 1.25.1+ +- **WebSocket Library:** [gorilla/websocket](https://github.com/gorilla/websocket) +- **Testing:** Go standard library + custom test helpers +- **CI/CD:** GitHub Actions +- **Code Quality:** golangci-lint, gosec, govulncheck -```go -import "runtime" +## Project Status -if runtime.GOOS == "windows" { - // Windows-specific code -} else if runtime.GOOS == "darwin" { - // macOS-specific code -} -``` +GoChat is actively maintained and production-ready. We welcome contributions! -#### CGo Considerations +- **Stability:** Stable, used in production +- **Test Coverage:** 80%+ with unit and integration tests +- **Security:** Regular dependency scanning and security audits +- **Performance:** Handles thousands of concurrent connections -This project is built with `CGO_ENABLED=0` for maximum portability and easier cross-compilation. The binaries are completely self-contained with no external dependencies. +## Use Cases -If you need CGo for a specific feature, you'll need to set up cross-compilation toolchains for each target platform. +- **Chat Applications** - Real-time messaging systems +- **Live Notifications** - Push notifications to web clients +- **Collaborative Tools** - Real-time collaboration features +- **Gaming** - Multiplayer game communication +- **IoT** - Device-to-server real-time communication +- **Monitoring Dashboards** - Live data updates -### Local Build (Traditional) +## Community and Support -```bash -make build -``` +- **Issues:** [GitHub Issues](https://github.com/Tyrowin/gochat/issues) - Bug reports and feature requests +- **Discussions:** [GitHub Discussions](https://github.com/Tyrowin/gochat/discussions) - Questions and community chat +- **Contributing:** See [Contributing Guide](docs/CONTRIBUTING.md) +- **Security:** See [Security Policy](docs/SECURITY.md#reporting-security-issues) -### Release Build - -Create optimized builds for multiple platforms: - -```bash -make release -``` - -This creates production-ready binaries for: - -- Linux (amd64 and arm64) -- macOS (Intel and Apple Silicon) -- Windows (amd64) - -All binaries are: - -- Optimized with `-trimpath` for reproducible builds -- Built with `CGO_ENABLED=0` for static linking -- Accompanied by SHA256 checksums - -**For detailed cross-platform development instructions, see [Cross-Platform Development Guide](docs/CROSS_PLATFORM.md).** - -### Docker - -```bash -# Build Docker image -make docker-build - -# Run in Docker container -make docker-run -``` - -## Project Structure - -``` -gochat/ -├── cmd/ -│ └── server/ # Application entry point -│ └── main.go -├── internal/ # Private application code -│ └── server/ # Core HTTP/WebSocket server components -│ ├── client.go # WebSocket client connection management -│ ├── config.go # Runtime configuration and security controls -│ ├── handlers.go # HTTP and WebSocket request handlers -│ ├── hub.go # Client registry and broadcast coordination -│ ├── http_server.go # HTTP server setup helpers -│ ├── origin.go # Origin validation helpers -│ ├── rate_limiter.go # Per-connection rate limiting -│ ├── routes.go # Route registration -│ └── types.go # Shared message and utility types -├── .github/ -│ └── workflows/ -│ └── ci.yml # GitHub Actions CI pipeline -├── .golangci.yml # Linter configuration -├── Makefile # Build and development automation -├── go.mod # Go module definition -└── README.md # This file -``` - -## Code Quality Standards - -This project enforces the following standards: - -- **Go formatting**: Standard `gofmt` formatting -- **Import organization**: Organized with `goimports` -- **Linting**: Comprehensive linting with golangci-lint -- **Security**: Regular security scanning with multiple tools -- **Testing**: Minimum test coverage requirements -- **Documentation**: All exported functions and types must be documented - -## Contributing - -1. Fork the repository -2. Create a feature branch (`git checkout -b feature/amazing-feature`) -3. Run the full CI pipeline locally (`make ci-local`) -4. Commit your changes (`git commit -am 'Add amazing feature'`) -5. Push to the branch (`git push origin feature/amazing-feature`) -6. Open a Pull Request - -### Before Submitting a PR - -Ensure your code passes all quality checks: - -```bash -make ci-local -``` - -This will run all the same checks that run in the CI pipeline. +## License -## Security +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -- All dependencies are automatically scanned for vulnerabilities -- Security issues are tracked and remediated promptly -- Code is analyzed for security vulnerabilities using gosec -- Docker images (if used) are scanned with Trivy +## Acknowledgments -To report security vulnerabilities, please create a private issue or contact the maintainers directly. +- Built with [gorilla/websocket](https://github.com/gorilla/websocket) +- Inspired by the Go community's best practices +- Thanks to all contributors -## License +--- -This project is licensed under the terms specified in the [LICENSE](LICENSE) file. +**Ready to get started?** Check out the [Getting Started Guide](docs/GETTING_STARTED.md) or explore the [API Documentation](docs/API.md) to integrate GoChat into your application. diff --git a/bin/MacOS/gochat-amd64 b/bin/MacOS/gochat-amd64 index 5e2ec82..925ca88 100644 Binary files a/bin/MacOS/gochat-amd64 and b/bin/MacOS/gochat-amd64 differ diff --git a/bin/MacOS/gochat-arm64 b/bin/MacOS/gochat-arm64 index 8d1854b..b71a9fb 100644 Binary files a/bin/MacOS/gochat-arm64 and b/bin/MacOS/gochat-arm64 differ diff --git a/bin/gochat.exe b/bin/gochat.exe new file mode 100644 index 0000000..3716a8c Binary files /dev/null and b/bin/gochat.exe differ diff --git a/bin/linux/gochat-amd64 b/bin/linux/gochat-amd64 index 72e9834..2b29cea 100644 Binary files a/bin/linux/gochat-amd64 and b/bin/linux/gochat-amd64 differ diff --git a/bin/linux/gochat-arm64 b/bin/linux/gochat-arm64 index f96c6eb..5f99cf9 100644 Binary files a/bin/linux/gochat-arm64 and b/bin/linux/gochat-arm64 differ diff --git a/bin/windows/gochat-amd64.exe b/bin/windows/gochat-amd64.exe index 7fa03f4..8972a73 100644 Binary files a/bin/windows/gochat-amd64.exe and b/bin/windows/gochat-amd64.exe differ diff --git a/bin/windows/gochat-arm64.exe b/bin/windows/gochat-arm64.exe index 3aac235..2cacf95 100644 Binary files a/bin/windows/gochat-arm64.exe and b/bin/windows/gochat-arm64.exe differ diff --git a/cmd/server/main.go b/cmd/server/main.go index ad86e7c..0b90552 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -32,7 +32,7 @@ import ( func main() { fmt.Println("Starting GoChat server...") - config := server.NewConfig() + config := server.NewConfigFromEnv() server.SetConfig(config) server.StartHub() mux := server.SetupRoutes() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..407fc97 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.8' + +services: + gochat: + build: + context: . + dockerfile: Dockerfile + container_name: gochat-server + ports: + - "8080:8080" + environment: + - SERVER_PORT=:8080 + - ALLOWED_ORIGINS=http://localhost:8080,http://localhost:3000 + - MAX_MESSAGE_SIZE=512 + - RATE_LIMIT_BURST=5 + - RATE_LIMIT_REFILL_INTERVAL=1 + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/"] + interval: 30s + timeout: 3s + start_period: 5s + retries: 3 + # Uncomment to use .env file for configuration + # env_file: + # - .env + networks: + - gochat-network + +networks: + gochat-network: + driver: bridge diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..4d44a9c --- /dev/null +++ b/docs/API.md @@ -0,0 +1,369 @@ +# WebSocket API Documentation + +This document describes the GoChat WebSocket API for real-time chat communication. + +## Connection Endpoint + +**Endpoint:** `ws://localhost:8080/ws` (or `wss://yourdomain.com/ws` in production) + +**Method:** GET (WebSocket upgrade) + +**Required Headers:** + +- `Upgrade: websocket` +- `Connection: Upgrade` +- `Sec-WebSocket-Version: 13` +- `Sec-WebSocket-Key: ` +- `Origin: ` (must match server configuration) + +**Note:** Most WebSocket client libraries handle these headers automatically. + +## Message Protocol + +GoChat uses a simple JSON-based message protocol for all client-server communication. + +### Message Format + +```json +{ + "content": "Your message text here" +} +``` + +### Field Definitions + +| Field | Type | Required | Description | +| --------- | ------ | -------- | ------------------------------------------------------ | +| `content` | string | Yes | The message text to broadcast to all connected clients | + +### Constraints + +- **Maximum message size:** 512 bytes (configurable) +- **Format:** Must be valid JSON +- **Content field:** Required and must be a string + +### Message Flow + +1. Client connects to the WebSocket endpoint +2. Client sends JSON messages with the `content` field +3. Server validates the message format and size +4. Server broadcasts the message to all other connected clients (excluding the sender) +5. Each client receives messages from all other clients in real-time + +### Important Notes + +- Messages are **broadcast to all clients except the sender** +- **No message history** is stored - only real-time communication +- Invalid JSON or oversized messages will cause the connection to close +- Rate limiting applies per connection (see [Security](SECURITY.md)) + +## Code Examples + +### JavaScript (Browser) + +```javascript +// Connect to the WebSocket server +const ws = new WebSocket("ws://localhost:8080/ws"); + +// Connection opened +ws.addEventListener("open", (event) => { + console.log("Connected to GoChat server"); + + // Send a message + const message = { + content: "Hello from JavaScript!", + }; + ws.send(JSON.stringify(message)); +}); + +// Receive messages +ws.addEventListener("message", (event) => { + const message = JSON.parse(event.data); + console.log("Received:", message.content); +}); + +// Connection closed +ws.addEventListener("close", (event) => { + console.log("Disconnected from server"); +}); + +// Error handling +ws.addEventListener("error", (error) => { + console.error("WebSocket error:", error); +}); +``` + +### Python (with websockets library) + +```python +import asyncio +import websockets +import json + +async def chat(): + uri = "ws://localhost:8080/ws" + async with websockets.connect(uri) as websocket: + # Send a message + message = {"content": "Hello from Python!"} + await websocket.send(json.dumps(message)) + + # Receive messages + async for message in websocket: + data = json.loads(message) + print(f"Received: {data['content']}") + +asyncio.run(chat()) +``` + +### Go (with gorilla/websocket) + +```go +package main + +import ( + "encoding/json" + "log" + "github.com/gorilla/websocket" +) + +type Message struct { + Content string `json:"content"` +} + +func main() { + // Connect to server + conn, _, err := websocket.DefaultDialer.Dial("ws://localhost:8080/ws", nil) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + // Send message + msg := Message{Content: "Hello from Go!"} + if err := conn.WriteJSON(msg); err != nil { + log.Fatal(err) + } + + // Receive messages + for { + var received Message + if err := conn.ReadJSON(&received); err != nil { + log.Fatal(err) + } + log.Printf("Received: %s", received.Content) + } +} +``` + +### Node.js (with ws library) + +```javascript +const WebSocket = require("ws"); + +const ws = new WebSocket("ws://localhost:8080/ws"); + +ws.on("open", function open() { + console.log("Connected to GoChat server"); + + // Send a message + const message = { + content: "Hello from Node.js!", + }; + ws.send(JSON.stringify(message)); +}); + +ws.on("message", function incoming(data) { + const message = JSON.parse(data); + console.log("Received:", message.content); +}); + +ws.on("close", function close() { + console.log("Disconnected from server"); +}); + +ws.on("error", function error(err) { + console.error("WebSocket error:", err); +}); +``` + +### cURL (for testing) + +While cURL doesn't natively support WebSocket upgrades, you can use tools like `websocat` for command-line testing: + +```bash +# Install websocat +# On macOS: brew install websocat +# On Linux: cargo install websocat + +# Connect and send a message +echo '{"content":"Hello from command line!"}' | websocat ws://localhost:8080/ws +``` + +## Interactive Testing + +### Built-in Test Page + +Navigate to `http://localhost:8080/test` in your browser to access an interactive HTML test page. + +**Features:** + +- Connect/disconnect from the WebSocket server +- Send messages and see them broadcast to other connected clients +- View connection status in real-time +- See message history during the session +- Test multi-client chat by opening multiple browser windows + +### Testing Multi-Client Chat + +1. Open `http://localhost:8080/test` in two or more browser windows/tabs +2. Click "Connect" in each window +3. Send a message from one window +4. Observe the message appearing in all other windows (but not the sender's) + +## Error Handling + +### Connection Errors + +**Origin Not Allowed:** + +``` +WebSocket connection failed: Error during WebSocket handshake: Unexpected response code: 403 +``` + +- **Cause:** The Origin header doesn't match the allowed origins +- **Solution:** Add your origin to the allowed list in the server configuration +- **See:** [Security Documentation](SECURITY.md#origin-validation) + +**Connection Refused:** + +``` +WebSocket connection failed: Connection refused +``` + +- **Cause:** Server is not running or not accessible +- **Solution:** Verify the server is running and the URL is correct + +### Message Errors + +**Invalid JSON:** + +- Sending non-JSON data will cause the connection to close +- Always use `JSON.stringify()` or equivalent to serialize messages + +**Message Too Large:** + +- Messages exceeding 512 bytes (default) will close the connection +- Keep messages concise or adjust the server's `MaxMessageSize` configuration + +**Rate Limit Exceeded:** + +- Sending too many messages too quickly will close the connection +- Default limit: 5 messages per second with burst capacity of 5 +- See [Security Documentation](SECURITY.md#rate-limiting) for details + +## Production Considerations + +### Use WSS (WebSocket Secure) + +In production, always use WSS instead of WS: + +```javascript +const ws = new WebSocket("wss://chat.yourdomain.com/ws"); +``` + +**Why:** + +- Required for HTTPS websites (browsers block WS from HTTPS pages) +- Encrypts all traffic +- Prevents man-in-the-middle attacks + +See [Deployment Guide](DEPLOYMENT.md) for TLS setup instructions. + +### Origin Configuration + +Update the server's allowed origins to include your production domain: + +```go +AllowedOrigins: []string{ + "https://yourdomain.com", + "https://www.yourdomain.com", +} +``` + +### Connection Timeouts + +WebSocket connections are long-lived. Ensure your reverse proxy is configured with appropriate timeouts: + +**Nginx:** + +```nginx +proxy_read_timeout 86400; # 24 hours +proxy_send_timeout 86400; +``` + +**Caddy:** +Caddy handles WebSocket timeouts automatically. + +## Advanced Usage + +### Reconnection Logic + +Implement automatic reconnection in case of connection loss: + +```javascript +let ws; +let reconnectInterval = 1000; // Start with 1 second +const maxReconnectInterval = 30000; // Max 30 seconds + +function connect() { + ws = new WebSocket("ws://localhost:8080/ws"); + + ws.onopen = () => { + console.log("Connected"); + reconnectInterval = 1000; // Reset on successful connection + }; + + ws.onclose = () => { + console.log("Disconnected. Reconnecting..."); + setTimeout(() => { + reconnectInterval = Math.min(reconnectInterval * 2, maxReconnectInterval); + connect(); + }, reconnectInterval); + }; + + ws.onmessage = (event) => { + const message = JSON.parse(event.data); + console.log("Received:", message.content); + }; +} + +connect(); +``` + +### Heartbeat/Ping-Pong + +To detect dead connections, implement a heartbeat mechanism: + +```javascript +let heartbeatInterval; + +ws.onopen = () => { + // Send heartbeat every 30 seconds + heartbeatInterval = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ content: "ping" })); + } + }, 30000); +}; + +ws.onclose = () => { + clearInterval(heartbeatInterval); +}; +``` + +## Related Documentation + +- [Getting Started](GETTING_STARTED.md) - Installation and basic setup +- [Security](SECURITY.md) - Security features and configuration +- [Deployment](DEPLOYMENT.md) - Production deployment guide +- [Development](DEVELOPMENT.md) - Contributing and development guide diff --git a/docs/BUILDING.md b/docs/BUILDING.md new file mode 100644 index 0000000..dd6f8ea --- /dev/null +++ b/docs/BUILDING.md @@ -0,0 +1,518 @@ +# Building GoChat + +This guide covers building GoChat for development and production, including cross-compilation for multiple platforms. + +## Table of Contents + +- [Quick Build](#quick-build) +- [Build Options](#build-options) +- [Cross-Compilation](#cross-compilation) +- [Platform-Specific Builds](#platform-specific-builds) +- [Build Output](#build-output) +- [Advanced Build Options](#advanced-build-options) + +## Quick Build + +### Using Make (Recommended) + +**Build for current platform:** + +```bash +make build +``` + +**Build for all platforms:** + +```bash +make build-all +``` + +**Create production release builds:** + +```bash +make release +``` + +### Using Go Directly + +**On Windows (PowerShell):** + +```powershell +go build -o bin\gochat.exe .\cmd\server +``` + +**On macOS/Linux:** + +```bash +go build -o bin/gochat ./cmd/server +``` + +## Build Options + +### Development Build + +For local development and testing: + +```bash +make build +# or +go build -o bin/gochat ./cmd/server +``` + +**Characteristics:** + +- Fast compilation +- Includes debug symbols +- No optimization +- Larger binary size +- Good for debugging + +### Production Build + +For deployment: + +```bash +make release +# or +go build -ldflags="-s -w" -trimpath -o bin/gochat ./cmd/server +``` + +**Characteristics:** + +- Optimized for size and performance +- Debug symbols stripped (`-s -w`) +- Reproducible builds (`-trimpath`) +- Smaller binary size +- Faster execution + +### Build Flags Explained + +**`-ldflags="-s -w"`** + +- `-s`: Omit symbol table +- `-w`: Omit DWARF debugging information +- Result: Smaller binary size + +**`-trimpath`** + +- Removes absolute file paths from binary +- Makes builds reproducible across machines +- Enhances security by not exposing local paths + +**`CGO_ENABLED=0`** + +- Disables CGo +- Creates statically linked binary +- No external dependencies +- Easier cross-compilation + +## Cross-Compilation + +Go makes it easy to build for different platforms from any development machine. + +### How Cross-Compilation Works + +Set two environment variables: + +- **`GOOS`**: Target operating system (linux, darwin, windows) +- **`GOARCH`**: Target architecture (amd64, arm64, 386) + +### Cross-Compilation Examples + +**From Windows build for Linux:** + +```powershell +$env:GOOS="linux" +$env:GOARCH="amd64" +go build -o bin/gochat-linux-amd64 .\cmd\server +``` + +**From macOS build for Windows:** + +```bash +GOOS=windows GOARCH=amd64 go build -o bin/gochat-windows-amd64.exe ./cmd/server +``` + +**From Linux build for macOS (Apple Silicon):** + +```bash +GOOS=darwin GOARCH=arm64 go build -o bin/gochat-darwin-arm64 ./cmd/server +``` + +### Using Make for Cross-Compilation + +```bash +# Build for specific platform +make build-linux # Linux (amd64) +make build-linux-arm64 # Linux (arm64) +make build-darwin # macOS Intel +make build-darwin-arm64 # macOS Apple Silicon +make build-windows # Windows (amd64) + +# Build for all platforms +make build-all + +# List all supported platforms +make list-platforms +``` + +## Platform-Specific Builds + +### Linux Builds + +**AMD64 (x86_64):** + +```bash +GOOS=linux GOARCH=amd64 go build -o bin/gochat-linux-amd64 ./cmd/server +``` + +**ARM64 (Raspberry Pi, AWS Graviton):** + +```bash +GOOS=linux GOARCH=arm64 go build -o bin/gochat-linux-arm64 ./cmd/server +``` + +**ARM (32-bit):** + +```bash +GOOS=linux GOARCH=arm GOARM=7 go build -o bin/gochat-linux-arm ./cmd/server +``` + +### macOS Builds + +**Intel (x86_64):** + +```bash +GOOS=darwin GOARCH=amd64 go build -o bin/gochat-darwin-amd64 ./cmd/server +``` + +**Apple Silicon (M1/M2/M3/M4):** + +```bash +GOOS=darwin GOARCH=arm64 go build -o bin/gochat-darwin-arm64 ./cmd/server +``` + +### Windows Builds + +**AMD64 (x86_64):** + +```bash +GOOS=windows GOARCH=amd64 go build -o bin/gochat-windows-amd64.exe ./cmd/server +``` + +**ARM64:** + +```bash +GOOS=windows GOARCH=arm64 go build -o bin/gochat-windows-arm64.exe ./cmd/server +``` + +**32-bit (386):** + +```bash +GOOS=windows GOARCH=386 go build -o bin/gochat-windows-386.exe ./cmd/server +``` + +### FreeBSD Builds + +```bash +GOOS=freebsd GOARCH=amd64 go build -o bin/gochat-freebsd-amd64 ./cmd/server +``` + +### Other Platforms + +Go supports many platforms. View all: + +```bash +go tool dist list +``` + +## Build Output + +### Directory Structure + +After running `make build-all` or `make release`, binaries are organized: + +``` +bin/ +├── gochat # Current platform binary +├── linux/ +│ ├── gochat-amd64 # Linux x86_64 +│ ├── gochat-arm64 # Linux ARM64 +│ └── checksums.txt # SHA256 checksums +├── darwin/ +│ ├── gochat-amd64 # macOS Intel +│ ├── gochat-arm64 # macOS Apple Silicon +│ └── checksums.txt # SHA256 checksums +└── windows/ + ├── gochat-amd64.exe # Windows x86_64 + └── checksums.txt # SHA256 checksums +``` + +### Checksums + +Release builds include SHA256 checksums for verification: + +```bash +# Generate checksums +cd bin/linux +sha256sum gochat-* > checksums.txt + +# Verify integrity +sha256sum -c checksums.txt +``` + +### Binary Size + +Typical binary sizes: + +| Build Type | Size (approx) | +| ---------------------- | ------------- | +| Development | 8-12 MB | +| Production (optimized) | 6-8 MB | +| Compressed (UPX) | 3-4 MB | + +## Advanced Build Options + +### Version Information + +Embed version information at build time: + +```bash +VERSION=$(git describe --tags --always --dirty) +BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S') + +go build -ldflags="-X main.Version=$VERSION -X main.BuildTime=$BUILD_TIME" \ + -o bin/gochat ./cmd/server +``` + +In your code: + +```go +package main + +var ( + Version = "dev" + BuildTime = "unknown" +) + +func main() { + fmt.Printf("GoChat %s (built %s)\n", Version, BuildTime) + // ... +} +``` + +### Static Linking + +For maximum portability (no dynamic dependencies): + +```bash +CGO_ENABLED=0 go build -a -installsuffix cgo -o bin/gochat ./cmd/server +``` + +**Benefits:** + +- No external dependencies +- Works on any system (even minimal containers) +- Easier deployment + +**Limitations:** + +- Cannot use C libraries +- Larger binary size +- No dynamic linking benefits + +### Compression with UPX + +Further reduce binary size (optional): + +```bash +# Install UPX +# macOS: brew install upx +# Linux: apt install upx-ucl +# Windows: Download from https://upx.github.io/ + +# Compress binary +upx --best --lzma bin/gochat + +# Decompress if needed +upx -d bin/gochat +``` + +**Warning:** Some antivirus software flags UPX-compressed binaries. + +### Custom Build Tags + +Use build tags for conditional compilation: + +```go +//go:build debug + +package main + +func init() { + log.SetFlags(log.LstdFlags | log.Lshortfile) +} +``` + +Build with tag: + +```bash +go build -tags debug -o bin/gochat ./cmd/server +``` + +### Multi-Architecture Docker Builds + +Build Docker images for multiple architectures: + +```bash +docker buildx create --use +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t gochat:latest \ + --push \ + . +``` + +## Build Automation + +### Makefile Targets + +View all available build targets: + +```bash +make help +``` + +**Key targets:** + +| Target | Description | +| ------------------------- | -------------------------------- | +| `make build` | Build for current platform | +| `make build-current` | Same as `build` | +| `make build-all` | Build for all platforms | +| `make build-linux` | Build for Linux (amd64) | +| `make build-linux-arm64` | Build for Linux (arm64) | +| `make build-darwin` | Build for macOS Intel | +| `make build-darwin-arm64` | Build for macOS Apple Silicon | +| `make build-windows` | Build for Windows (amd64) | +| `make release` | Production builds with checksums | +| `make clean` | Remove all build artifacts | + +### CI/CD Builds + +GitHub Actions automatically builds on every commit: + +```yaml +- name: Build + run: make build-all +``` + +Artifacts are stored for 90 days. + +## Troubleshooting + +### Build Fails on Windows + +**Error:** `package github.com/gorilla/websocket: unrecognized import path` + +**Solution:** Ensure Go modules are enabled: + +```powershell +$env:GO111MODULE="on" +go build -o bin\gochat.exe .\cmd\server +``` + +### Cross-Compilation Fails + +**Error:** `undefined: syscall.SomeFunction` + +**Cause:** Platform-specific code not properly guarded + +**Solution:** Use build tags or runtime checks: + +```go +//go:build linux + +package server + +// Linux-specific code +``` + +### Binary Won't Run + +**Error (Linux):** `permission denied` + +**Solution:** Make binary executable: + +```bash +chmod +x bin/gochat +``` + +**Error (macOS):** `"gochat" cannot be opened because the developer cannot be verified` + +**Solution:** Allow in System Preferences > Security & Privacy, or: + +```bash +xattr -d com.apple.quarantine bin/gochat +``` + +### Large Binary Size + +**Problem:** Binary is larger than expected + +**Solutions:** + +1. Use production build flags: `-ldflags="-s -w"` +2. Enable trimpath: `-trimpath` +3. Strip additional symbols: `strip bin/gochat` +4. Consider UPX compression (optional) + +## Performance Considerations + +### Build Time Optimization + +**Use build cache:** + +```bash +# Cache is enabled by default +go env GOCACHE +``` + +**Parallel compilation:** + +```bash +# Set number of CPU cores +go build -p 8 -o bin/gochat ./cmd/server +``` + +**Incremental builds:** + +```bash +# Only rebuild changed packages +go build -i -o bin/gochat ./cmd/server +``` + +### Runtime Performance + +**Optimize for speed:** + +```bash +go build -ldflags="-s -w" -gcflags="-l=4" -o bin/gochat ./cmd/server +``` + +**Profile-guided optimization (PGO):** + +```bash +# Collect profile +go run ./cmd/server & +# Run load tests, then stop server +# Build with profile +go build -pgo=default.pgo -o bin/gochat ./cmd/server +``` + +## Related Documentation + +- [Getting Started](GETTING_STARTED.md) - Installation and setup +- [Development Guide](DEVELOPMENT.md) - Development workflow +- [Deployment Guide](DEPLOYMENT.md) - Production deployment +- [Contributing](CONTRIBUTING.md) - Contribution guidelines diff --git a/docs/BUILD_GUIDE.md b/docs/BUILD_GUIDE.md deleted file mode 100644 index b4df4d6..0000000 --- a/docs/BUILD_GUIDE.md +++ /dev/null @@ -1,124 +0,0 @@ -# GoChat Build Guide - Quick Reference - -## Quick Commands - -### Build for Current Platform - -**With Make:** - -```bash -make build -``` - -**With Go:** - -```powershell -# Windows -go build -o bin\gochat.exe .\cmd\server - -# macOS/Linux -go build -o bin/gochat ./cmd/server -``` - -### Cross-Compile - -**Windows → Linux:** - -```powershell -# With Make -make build-linux - -# With Go -$env:GOOS="linux"; $env:GOARCH="amd64"; go build -o bin\gochat-linux-amd64 .\cmd\server -``` - -**macOS/Linux → Windows:** - -```bash -# With Make -make build-windows - -# With Go -GOOS=windows GOARCH=amd64 go build -o bin/gochat-windows-amd64.exe ./cmd/server -``` - -### Build All Platforms - -```bash -make build-all -``` - -This creates organized binaries in platform-specific directories: - -**Linux binaries:** - -- `bin/linux/gochat-amd64` -- `bin/linux/gochat-arm64` - -**macOS binaries:** - -- `bin/darwin/gochat-amd64` (Intel) -- `bin/darwin/gochat-arm64` (Apple Silicon) - -**Windows binaries:** - -- `bin/windows/gochat-amd64.exe` - -### Create Release - -```bash -make release -``` - -Creates all platform binaries plus SHA256 checksums. - -## Development - -### Hot Reload - -```bash -make dev -# or -air -``` - -### Run Tests - -```bash -make test -``` - -### Lint & Format - -```bash -make fmt -make lint -``` - -### Full CI Check - -```bash -make ci-local -``` - -## VS Code - -Press **Ctrl+Shift+B** (Windows/Linux) or **Cmd+Shift+B** (macOS) to build. - -## Supported Platforms - -| Platform | GOOS | GOARCH | Make Target | -| ------------------- | ------- | ------ | -------------------- | -| Windows 64-bit | windows | amd64 | `build-windows` | -| Linux 64-bit | linux | amd64 | `build-linux` | -| Linux ARM64 | linux | arm64 | `build-linux-arm64` | -| macOS Intel | darwin | amd64 | `build-darwin` | -| macOS Apple Silicon | darwin | arm64 | `build-darwin-arm64` | - -View all: `make list-platforms` or `go tool dist list` - -## More Information - -- [Cross-Platform Development Guide](CROSS_PLATFORM.md) - Detailed guide -- [Main README](../README.md) - Project overview -- [Go Cross-Compilation](https://golang.org/doc/install/source#environment) - Official docs diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..cb2095a --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,506 @@ +# Contributing to GoChat + +Thank you for your interest in contributing to GoChat! This document provides guidelines and instructions for contributing. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [How to Contribute](#how-to-contribute) +- [Development Workflow](#development-workflow) +- [Code Standards](#code-standards) +- [Testing Requirements](#testing-requirements) +- [Pull Request Process](#pull-request-process) +- [Reporting Issues](#reporting-issues) + +## Code of Conduct + +### Our Standards + +- Be respectful and inclusive +- Welcome newcomers and help them learn +- Focus on what is best for the community +- Show empathy towards other community members +- Accept constructive criticism gracefully + +### Unacceptable Behavior + +- Harassment, discrimination, or offensive comments +- Trolling or insulting/derogatory comments +- Publishing others' private information +- Any other conduct inappropriate in a professional setting + +## Getting Started + +### Prerequisites + +Before contributing, ensure you have: + +- Go 1.25.1 or later installed +- Git configured with your name and email +- A GitHub account +- Familiarity with Go and WebSocket concepts + +### Fork and Clone + +1. Fork the repository on GitHub +2. Clone your fork locally: + +```bash +git clone https://github.com/YOUR_USERNAME/gochat.git +cd gochat +``` + +3. Add upstream remote: + +```bash +git remote add upstream https://github.com/Tyrowin/gochat.git +``` + +4. Install development tools: + +```bash +make install-tools +``` + +5. Verify your setup: + +```bash +make ci-local +``` + +## How to Contribute + +### Types of Contributions + +We welcome various types of contributions: + +**Code Contributions:** + +- Bug fixes +- New features +- Performance improvements +- Code refactoring + +**Documentation:** + +- Fix typos or improve clarity +- Add examples +- Translate documentation +- Write tutorials or guides + +**Testing:** + +- Add test cases +- Improve test coverage +- Report bugs with detailed reproduction steps + +**Other:** + +- Improve error messages +- Enhance logging +- Optimize build process +- Update dependencies + +## Development Workflow + +### 1. Create a Branch + +Create a descriptive branch name: + +```bash +git checkout -b feature/add-user-authentication +git checkout -b fix/websocket-memory-leak +git checkout -b docs/improve-api-examples +``` + +Branch naming conventions: + +- `feature/` - New features +- `fix/` - Bug fixes +- `docs/` - Documentation changes +- `refactor/` - Code refactoring +- `test/` - Test improvements + +### 2. Make Changes + +- Write clean, readable code +- Follow Go best practices +- Add comments for complex logic +- Update documentation as needed + +### 3. Write Tests + +- Add unit tests for new functions +- Add integration tests for new features +- Ensure all tests pass +- Maintain or improve code coverage + +### 4. Run Quality Checks + +Before committing, run: + +```bash +# Format code +make fmt + +# Run linter +make lint + +# Run tests +make test + +# Security scan +make security-scan + +# Or run everything +make ci-local +``` + +### 5. Commit Changes + +Write clear, descriptive commit messages: + +```bash +git add . +git commit -m "Add user authentication feature + +- Implement JWT-based authentication +- Add login/logout endpoints +- Update tests and documentation +- Closes #123" +``` + +**Commit message format:** + +- First line: Brief summary (50 chars or less) +- Blank line +- Detailed description (wrap at 72 chars) +- Reference related issues + +### 6. Keep Up to Date + +Regularly sync with upstream: + +```bash +git fetch upstream +git rebase upstream/main +``` + +### 7. Push and Create PR + +```bash +git push origin feature/add-user-authentication +``` + +Then create a Pull Request on GitHub. + +## Code Standards + +### Go Style Guide + +Follow the official [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments). + +**Key points:** + +- Use `gofmt` for formatting (automatic with `make fmt`) +- Use meaningful variable names +- Keep functions small and focused +- Document exported functions and types +- Handle errors explicitly +- Avoid global variables when possible + +### Documentation + +**Package documentation:** + +```go +// Package server provides WebSocket server functionality for real-time chat. +// +// The server handles WebSocket connections, message broadcasting, and +// client lifecycle management with built-in security features. +package server +``` + +**Function documentation:** + +```go +// NewClient creates a new client instance for a WebSocket connection. +// It initializes the send channel and associates the client with the hub. +// The client is ready to be registered with the hub and start message pumps. +func NewClient(conn *websocket.Conn, hub *Hub, remoteAddr string) *Client { + // ... +} +``` + +**Inline comments:** + +```go +// Check if the client has exceeded rate limits before processing +if !client.rateLimiter.allow() { + client.conn.Close() + return +} +``` + +### Error Handling + +**Always check errors:** + +```go +// Bad +data, _ := json.Marshal(message) + +// Good +data, err := json.Marshal(message) +if err != nil { + return fmt.Errorf("failed to marshal message: %w", err) +} +``` + +**Provide context:** + +```go +if err := client.conn.WriteMessage(messageType, data); err != nil { + return fmt.Errorf("failed to send message to client %s: %w", client.id, err) +} +``` + +### Code Organization + +**File structure:** + +- One main concept per file +- Group related functionality +- Keep files under 500 lines when possible + +**Package structure:** + +``` +internal/server/ +├── client.go # Client-specific code +├── hub.go # Hub-specific code +├── handlers.go # HTTP handlers +├── config.go # Configuration +└── types.go # Shared types +``` + +## Testing Requirements + +### Test Coverage + +- Aim for 80%+ test coverage +- All new code must include tests +- Tests must pass with `-race` flag + +### Writing Tests + +**Unit tests:** + +```go +func TestRateLimiter_Allow(t *testing.T) { + tests := []struct { + name string + burst int + calls int + want bool + }{ + {"within limit", 5, 3, true}, + {"exceeds limit", 5, 10, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rl := newRateLimiter(tt.burst, time.Second) + // Test implementation + }) + } +} +``` + +**Integration tests:** + +```go +func TestMultiClientChat(t *testing.T) { + server := testhelpers.StartTestServer(t) + defer server.Close() + + client1 := testhelpers.ConnectClient(t, server.URL) + client2 := testhelpers.ConnectClient(t, server.URL) + + // Test implementation +} +``` + +### Running Tests + +```bash +# All tests +make test + +# Specific package +go test -v -race ./internal/server + +# With coverage +make test-coverage +``` + +## Pull Request Process + +### Before Submitting + +Checklist: + +- [ ] Code follows style guidelines +- [ ] Tests pass locally (`make test`) +- [ ] Linting passes (`make lint`) +- [ ] Security scans pass (`make security-scan`) +- [ ] Documentation updated +- [ ] Commit messages are clear +- [ ] Branch is up to date with main + +### PR Description Template + +```markdown +## Description + +Brief description of changes + +## Type of Change + +- [ ] Bug fix +- [ ] New feature +- [ ] Breaking change +- [ ] Documentation update + +## How Has This Been Tested? + +Describe testing approach + +## Checklist + +- [ ] My code follows the project's style guidelines +- [ ] I have performed a self-review +- [ ] I have commented complex code +- [ ] I have updated documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix/feature works +- [ ] New and existing tests pass locally +- [ ] Any dependent changes have been merged + +## Related Issues + +Closes #(issue number) +``` + +### Review Process + +1. **Automated checks** - CI pipeline must pass +2. **Code review** - Maintainers review the code +3. **Feedback** - Address review comments +4. **Approval** - At least one maintainer approval required +5. **Merge** - Maintainers merge the PR + +### After Merge + +- Delete your feature branch +- Sync your fork with upstream +- Celebrate your contribution! + +## Reporting Issues + +### Bug Reports + +Include: + +- **Description:** Clear description of the bug +- **Steps to reproduce:** Detailed steps +- **Expected behavior:** What should happen +- **Actual behavior:** What actually happens +- **Environment:** + - OS: Windows/macOS/Linux + - Go version: `go version` + - GoChat version/commit +- **Logs:** Relevant error messages or logs +- **Screenshots:** If applicable + +**Example:** + +```markdown +## Bug: WebSocket connection fails on Windows + +### Description + +WebSocket connections fail immediately after upgrade on Windows 11. + +### Steps to Reproduce + +1. Start server with `.\bin\gochat.exe` +2. Open test page at http://localhost:8080/test +3. Click "Connect" + +### Expected Behavior + +WebSocket connection establishes successfully + +### Actual Behavior + +Connection fails with error: "WebSocket handshake failed" + +### Environment + +- OS: Windows 11 Pro +- Go version: go1.25.1 windows/amd64 +- GoChat version: commit abc123 + +### Logs +``` + +Error: WebSocket upgrade failed: ... + +``` + +``` + +### Feature Requests + +Include: + +- **Problem statement:** What problem does this solve? +- **Proposed solution:** How should it work? +- **Alternatives considered:** Other approaches +- **Use case:** Real-world scenario +- **Impact:** Who benefits from this? + +## Community + +### Getting Help + +- **Documentation:** Check [docs/](../docs) first +- **Issues:** Search existing issues +- **Discussions:** Use GitHub Discussions for questions +- **Pull Requests:** Reference related issues + +### Communication + +- Be patient - maintainers volunteer their time +- Be respectful and constructive +- Help others when you can +- Share your knowledge + +## Recognition + +Contributors are recognized in: + +- GitHub contributors page +- Release notes +- Project documentation + +Thank you for contributing to GoChat! + +## Related Documentation + +- [Getting Started](GETTING_STARTED.md) - Setup instructions +- [Development Guide](DEVELOPMENT.md) - Development workflow and tools +- [Building](BUILDING.md) - Build instructions +- [API Documentation](API.md) - WebSocket API reference diff --git a/docs/CROSS_PLATFORM.md b/docs/CROSS_PLATFORM.md deleted file mode 100644 index 5fcf81e..0000000 --- a/docs/CROSS_PLATFORM.md +++ /dev/null @@ -1,322 +0,0 @@ -# Cross-Platform Development Guide for GoChat - -Go's built-in cross-compilation support makes it easy to build GoChat for any platform from any platform. - -## Prerequisites - -- Go 1.25.1 or later -- Git -- Make (optional, but recommended) - - **Windows**: Install via [Chocolatey](https://chocolatey.org/) (`choco install make`) - - **macOS**: Included with Xcode Command Line Tools - - **Linux**: Usually pre-installed (`apt install make` / `yum install make`) - -## Building - -### With Make (Recommended) - -```bash -# Build for current platform -make build -# or -make build-current - -# Build for specific platforms -make build-linux # Linux (amd64) -make build-linux-arm64 # Linux (ARM64) -make build-darwin # macOS (Intel) -make build-darwin-arm64 # macOS (Apple Silicon) -make build-windows # Windows (amd64) - -# Build for all platforms -make build-all - -# Create optimized release builds -make release - -# List all supported platforms -make list-platforms -``` - -### Without Make (Direct Go Commands) - -**Windows (PowerShell):** - -```powershell -# Build for current platform -go build -o bin\gochat.exe .\cmd\server - -# Cross-compile for Linux -$env:GOOS="linux"; $env:GOARCH="amd64"; go build -o bin\gochat-linux-amd64 .\cmd\server - -# Cross-compile for macOS -$env:GOOS="darwin"; $env:GOARCH="arm64"; go build -o bin\gochat-darwin-arm64 .\cmd\server -``` - -**macOS/Linux:** - -```bash -# Build for current platform -go build -o bin/gochat ./cmd/server - -# Cross-compile for Windows -GOOS=windows GOARCH=amd64 go build -o bin/gochat-windows-amd64.exe ./cmd/server - -# Cross-compile for macOS Apple Silicon -GOOS=darwin GOARCH=arm64 go build -o bin/gochat-darwin-arm64 ./cmd/server -``` - -## Cross-Compilation - -Build for any platform from any platform: - -| From | To | Command | -| ------- | ------- | ------------------------- | -| Windows | Linux | `make build-linux` | -| Windows | macOS | `make build-darwin-arm64` | -| macOS | Windows | `make build-windows` | -| macOS | Linux | `make build-linux` | -| Linux | Windows | `make build-windows` | -| Linux | macOS | `make build-darwin-arm64` | - -## Build Output - -Binaries are organized in platform-specific directories within `bin/`: - -``` -bin/ -├── gochat # Current platform binary (from `make build`) -├── linux/ -│ ├── gochat-amd64 # Linux 64-bit -│ ├── gochat-arm64 # Linux ARM64 (Raspberry Pi, AWS Graviton) -│ └── checksums.txt # SHA256 checksums (from `make release`) -├── darwin/ -│ ├── gochat-amd64 # macOS Intel -│ ├── gochat-arm64 # macOS Apple Silicon (M1/M2/M3) -│ └── checksums.txt # SHA256 checksums (from `make release`) -└── windows/ - ├── gochat-amd64.exe # Windows 64-bit - └── checksums.txt # SHA256 checksums (from `make release`) -``` - -**Note:** - -- `make build` creates the binary for your current platform in `bin/` -- Platform-specific targets (`make build-linux`, `make build-windows`, etc.) create binaries in their respective directories -- `make build-all` builds for all platforms at once - -## Platform-Specific Code - -### File Name Suffixes - -Go automatically selects the right file based on the target platform: - -``` -internal/server/ -├── config.go # Shared code -├── config_windows.go # Windows-specific -├── config_darwin.go # macOS-specific -└── config_linux.go # Linux-specific -``` - -Example `config_windows.go`: - -```go -//go:build windows - -package server - -func getPlatformConfig() string { - return "Windows configuration" -} -``` - -### Build Tags - -For more complex conditions: - -```go -//go:build linux && amd64 - -package server - -// This code only compiles for 64-bit Linux -``` - -```go -//go:build darwin || linux - -package server - -// This code compiles on both macOS and Linux -``` - -```go -//go:build !windows - -package server - -// This code compiles on all platforms except Windows -``` - -### Runtime Detection - -Check the platform at runtime: - -```go -package server - -import ( - "runtime" - "path/filepath" -) - -func getPlatformPath() string { - switch runtime.GOOS { - case "windows": - return filepath.Join("C:", "Program Files", "gochat") - case "darwin": - return filepath.Join("/Applications", "gochat") - case "linux": - return filepath.Join("/usr", "local", "bin", "gochat") - default: - return "./gochat" - } -} -``` - -### Cross-Platform File Paths - -Always use the `path/filepath` package: - -```go -import "path/filepath" - -// GOOD - Works on all platforms -configPath := filepath.Join("config", "app.json") - -// BAD - Only works on Unix-like systems -configPath := "config/app.json" - -// BAD - Only works on Windows -configPath := "config\\app.json" -``` - -## Development - -### Hot Reload with Air - -```bash -# Install air (if not already installed) -go install github.com/air-verse/air@latest - -# Run with hot reload -make dev -# or -air -``` - -The `.air.toml` configuration works across all platforms. - -### VS Code Integration - -Press `Ctrl+Shift+B` (or `Cmd+Shift+B` on macOS) to access build tasks: - -- Build (Current Platform) -- Build All Platforms -- Run Server -- Dev (Hot Reload) -- Test All -- Create Release - -## Troubleshooting - -### Windows - -**"make: command not found"** - -```powershell -# Install Make via Chocolatey -choco install make - -# Or use Go directly -go build -o bin\gochat.exe .\cmd\server -``` - -**"Scripts are disabled on this system"** - -```powershell -Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -``` - -### macOS - -**"Permission denied"** - -```bash -chmod +x bin/gochat -``` - -**"Developer cannot be verified"** - -- Go to System Preferences > Security & Privacy and allow the binary -- Or remove quarantine: `xattr -d com.apple.quarantine bin/gochat` - -### Linux - -**"Go not found"** - -```bash -# Add Go to PATH -export PATH=$PATH:/usr/local/go/bin -echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc -``` - -### All Platforms - -**Build is slow** - -```bash -# Use cached builds -go build -i -o bin/gochat ./cmd/server - -# Or clean cache if corrupted -go clean -cache -modcache -i -r -``` - -## Best Practices - -1. ✅ Use `filepath.Join()` for all file paths -2. ✅ Set `CGO_ENABLED=0` for portable binaries (already configured) -3. ✅ Use build tags for platform-specific code -4. ✅ Test on multiple platforms when possible -5. ✅ Use the Makefile for consistent builds -6. ✅ Check the `.gitattributes` file handles line endings correctly - -## Resources - -- [Go Cross-Compilation](https://golang.org/doc/install/source#environment) -- [Build Constraints](https://pkg.go.dev/cmd/go#hdr-Build_constraints) -- [Main README](../README.md) - -## Summary - -**With Make:** - -```bash -make build # Current platform -make build-all # All platforms -make release # Release builds with checksums -``` - -**Without Make:** - -```bash -# Current platform -go build -o bin/gochat ./cmd/server - -# Cross-compile -GOOS=linux GOARCH=amd64 go build -o bin/gochat-linux-amd64 ./cmd/server -``` - -That's it! Go's cross-compilation makes building for any platform simple and straightforward. diff --git a/docs/CROSS_PLATFORM_GUIDE.md b/docs/CROSS_PLATFORM_GUIDE.md deleted file mode 100644 index 5984051..0000000 --- a/docs/CROSS_PLATFORM_GUIDE.md +++ /dev/null @@ -1,569 +0,0 @@ -# Cross-Platform Development Guide for GoChat - -This guide explains how to develop, build, and deploy GoChat across Windows, macOS, and Linux platforms. - -## Table of Contents - -- [Overview](#overview) -- [Development Setup](#development-setup) -- [Building](#building) -- [Cross-Compilation](#cross-compilation) -- [Platform-Specific Code](#platform-specific-code) -- [Troubleshooting](#troubleshooting) - -## Overview - -GoChat is designed to work seamlessly across all major operating systems. Thanks to Go's excellent cross-platform support, you can: - -- Develop on any platform (Windows, macOS, or Linux) -- Build binaries for any platform from any platform -- Use platform-specific build scripts or a unified Makefile -- Run the same tests and quality checks on all platforms - -## Development Setup - -### Windows - -1. **Install Go**: Download from [golang.org](https://golang.org/dl/) -2. **Install Git**: Download from [git-scm.com](https://git-scm.com/) -3. **Optional - Install Make**: - - Via Chocolatey: `choco install make` - - Or download GnuWin32 Make - -**PowerShell Setup:** - -```powershell -# Clone the repository -git clone https://github.com/Tyrowin/gochat.git -cd gochat - -# Install development tools -go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest -go install golang.org/x/vuln/cmd/govulncheck@latest -go install github.com/air-verse/air@latest - -# Build the project -.\build.ps1 current - -# Run the server -.\bin\gochat.exe -``` - -**Development with Hot Reload:** - -```powershell -# Install air if not already installed -go install github.com/air-verse/air@latest - -# Run with hot reload -air -``` - -### macOS - -1. **Install Go**: Download from [golang.org](https://golang.org/dl/) or use Homebrew: `brew install go` -2. **Install Git**: Comes with Xcode Command Line Tools: `xcode-select --install` -3. **Install Make**: Comes with Xcode Command Line Tools - -**Terminal Setup:** - -```bash -# Clone the repository -git clone https://github.com/Tyrowin/gochat.git -cd gochat - -# Make scripts executable -chmod +x scripts/build.sh quick-build.sh - -# Install development tools -make install-tools - -# Build the project -./build.sh current - -# Run the server -./bin/gochat -``` - -**Development with Hot Reload:** - -```bash -# Run with hot reload -make dev -# or -air -``` - -### Linux - -1. **Install Go**: Use your package manager or download from [golang.org](https://golang.org/dl/) - - Ubuntu/Debian: `sudo apt install golang-go` - - Fedora: `sudo dnf install golang` - - Arch: `sudo pacman -S go` -2. **Install Git**: Usually pre-installed, or `sudo apt install git` -3. **Install Make**: Usually pre-installed, or `sudo apt install make` - -**Terminal Setup:** - -```bash -# Clone the repository -git clone https://github.com/Tyrowin/gochat.git -cd gochat - -# Make scripts executable -chmod +x scripts/build.sh quick-build.sh - -# Install development tools -make install-tools - -# Build the project -./build.sh current - -# Run the server -./bin/gochat -``` - -**Development with Hot Reload:** - -```bash -# Run with hot reload -make dev -# or -air -``` - -## Building - -### Quick Build (Current Platform) - -The fastest way to build for your current platform: - -**Windows PowerShell:** - -```powershell -.\quick-build.ps1 -``` - -**macOS/Linux:** - -```bash -./quick-build.sh -``` - -### Platform-Specific Build Scripts - -#### Windows (build.ps1) - -```powershell -# Show help -.\build.ps1 -Help - -# Build for current platform -.\build.ps1 current - -# Build for specific platforms -.\build.ps1 windows -.\build.ps1 linux -.\build.ps1 darwin -.\build.ps1 darwin-arm64 - -# Build for all platforms -.\build.ps1 all - -# Create release builds -.\build.ps1 release - -# Clean before building -.\build.ps1 -Clean all - -# Custom output name -.\build.ps1 -Output myapp windows - -# Verbose output -.\build.ps1 -Verbose all -``` - -#### macOS/Linux (build.sh) - -```bash -# Show help -./build.sh --help - -# Build for current platform -./build.sh current - -# Build for specific platforms -./build.sh windows -./build.sh linux -./build.sh darwin -./build.sh darwin-arm64 - -# Build for all platforms -./build.sh all - -# Create release builds -./build.sh release - -# Clean before building -./build.sh --clean all - -# Custom output name -./build.sh -o myapp windows - -# Verbose output -./build.sh -v all -``` - -### Using Makefile (All Platforms) - -```bash -# Build for current platform -make build-current - -# Build for specific platforms -make build-windows -make build-linux -make build-linux-arm64 -make build-darwin -make build-darwin-arm64 - -# Build for all platforms -make build-all - -# Create optimized release builds -make release - -# Show all available targets -make help - -# List all supported platforms -make list-platforms -``` - -## Cross-Compilation - -One of Go's most powerful features is the ability to build binaries for different platforms without needing complex toolchains. - -### How Cross-Compilation Works - -Go uses two environment variables to control the target platform: - -- `GOOS`: Target operating system (e.g., `linux`, `windows`, `darwin`) -- `GOARCH`: Target architecture (e.g., `amd64`, `arm64`, `386`) - -### Examples - -**Build Windows executable from macOS:** - -```bash -./build.sh windows -# Creates: bin/gochat-windows-amd64.exe -``` - -**Build macOS binary from Windows:** - -```powershell -.\build.ps1 darwin-arm64 -# Creates: bin/gochat-darwin-arm64 -``` - -**Build Linux binary from any platform:** - -```bash -# Unix-like systems -./build.sh linux - -# Windows -.\build.ps1 linux -``` - -### Manual Cross-Compilation - -If you need to build manually: - -**Windows PowerShell:** - -```powershell -$env:CGO_ENABLED = "0" -$env:GOOS = "linux" -$env:GOARCH = "amd64" -go build -o bin/gochat-linux-amd64 ./cmd/server -``` - -**macOS/Linux:** - -```bash -CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/gochat-windows-amd64.exe ./cmd/server -``` - -### Supported Platforms - -View all supported platforms: - -```bash -go tool dist list -``` - -Common combinations: - -- `linux/amd64` - 64-bit Linux -- `linux/arm64` - ARM64 Linux (Raspberry Pi, AWS Graviton) -- `darwin/amd64` - macOS Intel -- `darwin/arm64` - macOS Apple Silicon (M1/M2/M3) -- `windows/amd64` - 64-bit Windows -- `windows/386` - 32-bit Windows - -## Platform-Specific Code - -Sometimes you need code that behaves differently on different platforms. - -### 1. File Name Suffixes - -Create different implementations for different platforms: - -``` -internal/server/ -├── config.go # Shared code -├── config_windows.go # Windows-specific -├── config_darwin.go # macOS-specific -└── config_linux.go # Linux-specific -``` - -Go automatically selects the right file based on the target platform. - -**Example - config_windows.go:** - -```go -//go:build windows - -package server - -func platformSpecificConfig() { - // Windows-specific configuration -} -``` - -**Example - config_linux.go:** - -```go -//go:build linux - -package server - -func platformSpecificConfig() { - // Linux-specific configuration -} -``` - -### 2. Build Tags - -Use build tags for more complex conditions: - -```go -//go:build linux && amd64 - -package server - -// This code only compiles for 64-bit Linux -``` - -```go -//go:build darwin || linux - -package server - -// This code compiles on both macOS and Linux -``` - -```go -//go:build !windows - -package server - -// This code compiles on all platforms except Windows -``` - -### 3. Runtime Detection - -Check the platform at runtime: - -```go -package server - -import ( - "runtime" - "path/filepath" -) - -func getPlatformSpecificPath() string { - switch runtime.GOOS { - case "windows": - return filepath.Join("C:", "Program Files", "gochat") - case "darwin": - return filepath.Join("/Applications", "gochat") - case "linux": - return filepath.Join("/usr", "local", "bin", "gochat") - default: - return "./gochat" - } -} -``` - -### 4. File Path Handling - -Use `filepath` package for cross-platform paths: - -```go -import "path/filepath" - -// GOOD - Works on all platforms -configPath := filepath.Join("config", "app.json") - -// BAD - Only works on Unix-like systems -configPath := "config/app.json" - -// BAD - Only works on Windows -configPath := "config\\app.json" -``` - -## Troubleshooting - -### Windows-Specific Issues - -**Issue: "Scripts are disabled on this system"** - -```powershell -# Solution: Set execution policy -Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -``` - -**Issue: "make: command not found"** - -```powershell -# Solution: Use PowerShell scripts instead of Make -.\build.ps1 current - -# Or install Make via Chocolatey -choco install make -``` - -**Issue: Line ending problems with Git** - -```powershell -# Configure Git to handle line endings correctly -git config --global core.autocrlf true -``` - -### macOS-Specific Issues - -**Issue: "Permission denied" when running scripts** - -```bash -# Solution: Make scripts executable -chmod +x scripts/build.sh quick-build.sh -``` - -**Issue: "Developer cannot be verified" when running binary** - -```bash -# Solution: Allow the binary in System Preferences > Security & Privacy -# Or remove quarantine attribute -xattr -d com.apple.quarantine bin/gochat -``` - -### Linux-Specific Issues - -**Issue: "Go not found" after installation** - -```bash -# Solution: Add Go to PATH -export PATH=$PATH:/usr/local/go/bin -echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc -``` - -**Issue: "Cannot execute binary file"** - -```bash -# Solution: Make sure you're running the correct architecture -file bin/gochat -# Should match your system architecture - -# Rebuild for your platform -./build.sh current -``` - -### Cross-Compilation Issues - -**Issue: "CGo is not enabled"** - -```bash -# Solution: This project doesn't need CGo. Make sure CGO_ENABLED=0 -# Our build scripts already set this correctly -``` - -**Issue: "No such file or directory" when running cross-compiled binary** - -```bash -# Make sure you're running the binary on the correct platform -# Linux binary won't run on Windows, etc. - -# Check the binary -file bin/gochat-linux-amd64 -# Output should match the target platform -``` - -### General Issues - -**Issue: Build is slow** - -```bash -# Solution: Use cached builds -go build -i -o bin/gochat ./cmd/server - -# Or clean Go cache if it's corrupted -go clean -cache -modcache -i -r -``` - -**Issue: Module download errors** - -```bash -# Solution: Clean and re-download modules -go clean -modcache -go mod download -``` - -**Issue: Version conflicts** - -```bash -# Solution: Tidy up dependencies -go mod tidy -``` - -## Best Practices - -1. **Use the provided build scripts** - They handle platform differences automatically -2. **Test on multiple platforms** - If possible, test your changes on Windows, macOS, and Linux -3. **Use `filepath` package** - Always use `filepath.Join()` for paths -4. **Set `CGO_ENABLED=0`** - For maximum portability (already configured) -5. **Use build tags** - When you need platform-specific code -6. **Check file permissions** - Remember that executable permissions work differently on Unix vs Windows -7. **Handle line endings** - Configure Git properly for your platform -8. **Use the `.air.toml` config** - For consistent hot-reload across platforms - -## Resources - -- [Go Official Documentation](https://golang.org/doc/) -- [Cross-Compilation Guide](https://golang.org/doc/install/source#environment) -- [Build Constraints](https://pkg.go.dev/cmd/go#hdr-Build_constraints) -- [Go on Windows](https://golang.org/doc/install/windows) -- [Go on macOS](https://golang.org/doc/install/darwin) -- [Go on Linux](https://golang.org/doc/install/linux) - -## Getting Help - -If you encounter issues not covered here: - -1. Check the [GitHub Issues](https://github.com/Tyrowin/gochat/issues) -2. Review the build script source code (`build.ps1` or `build.sh`) -3. Run with verbose output (`-Verbose` or `-v` flag) -4. Open a new issue with details about your platform and the error diff --git a/docs/CROSS_PLATFORM_SETUP.md b/docs/CROSS_PLATFORM_SETUP.md deleted file mode 100644 index fa2889f..0000000 --- a/docs/CROSS_PLATFORM_SETUP.md +++ /dev/null @@ -1,441 +0,0 @@ -# Cross-Platform Development Setup Summary - -This document summarizes the cross-platform development capabilities of the GoChat project. - -## Overview - -GoChat supports seamless development, building, and deployment across **Windows**, **macOS**, and **Linux**. You can develop on any platform and build binaries for any platform using Make or direct Go commands. - -## What's Included - -### 1. Enhanced Makefile - -Comprehensive cross-platform build targets: - -- `build-current` - Build for current platform -- `build-linux` - Build for Linux (amd64) -- `build-linux-arm64` - Build for Linux (ARM64) -- `build-darwin` - Build for macOS (Intel) -- `build-darwin-arm64` - Build for macOS (Apple Silicon) -- `build-windows` - Build for Windows (amd64) -- `build-all` - Build for all platforms -- `release` - Create optimized release builds for all platforms -- `list-platforms` - Show all supported GOOS/GOARCH combinations - -### 2. Development Configuration - -#### Air Configuration (`.air.toml`) - -- Hot reload configuration for development -- Works consistently across all platforms -- Excludes test files and build directories -- Configurable build commands and delays - -#### VS Code Integration - -**Tasks (`.vscode/tasks.json`)** - -- Build (Current Platform) - Default build task -- Build All Platforms -- Run Server -- Dev (Hot Reload) -- Test All - Default test task -- Test with Coverage -- Lint -- Security Scan -- Full CI Check -- Clean Build Artifacts -- Create Release - -All tasks work on Windows, macOS, and Linux with platform-specific command adjustments. - -**Launch Configurations (`.vscode/launch.json`)** - -- Launch Server (with pre-build) -- Launch Server (no build) -- Debug Tests (Current File) -- Debug Tests (All) -- Attach to Process - -**Extensions (`.vscode/extensions.json`)** -Recommended extensions for cross-platform Go development: - -- Go -- YAML -- Makefile Tools -- PowerShell (for Windows users) -- GitLens -- Markdown Lint -- EditorConfig -- Code Spell Checker -- Docker -- GitHub Actions - -### 3. Configuration Files - -#### `.gitattributes` - -Ensures consistent line ending handling across platforms: - -- Shell scripts (`.sh`) always use LF -- PowerShell scripts (`.ps1`) use CRLF on Windows -- Go source files use LF -- Binary files properly marked - -#### `.editorconfig` - -Ensures consistent coding style across all editors and platforms: - -- Go files: tabs, size 4 -- YAML/JSON: spaces, size 2 -- Makefiles: tabs (required) -- Shell scripts: spaces, LF line endings -- PowerShell: spaces, CRLF line endings - -### 4. Documentation - -#### Comprehensive Guide (`docs/CROSS_PLATFORM_GUIDE.md`) - -Complete guide covering: - -- Development setup for each platform -- Building and cross-compilation -- Platform-specific code techniques -- Troubleshooting for each platform -- Best practices -- Resources and help - -#### Quick Reference (`docs/QUICK_REFERENCE.md`) - -One-page reference card with: - -- All build commands -- Development workflow -- Testing commands -- Cross-compilation examples -- Troubleshooting quick fixes -- File locations - -#### Updated README - -Main README now includes: - -- Cross-platform features highlighted -- Platform-specific prerequisites -- Quick build instructions for all platforms -- Comprehensive cross-platform build section -- Links to detailed guides - -## File Structure - -``` -gochat/ -├── .editorconfig # Editor configuration (all platforms) -├── .gitattributes # Git line ending configuration -├── .air.toml # Hot reload configuration -├── Makefile # Cross-platform build targets -├── .vscode/ -│ ├── tasks.json # VS Code tasks (all platforms) -│ ├── launch.json # Debug configurations -│ └── extensions.json # Recommended extensions -├── docs/ -│ ├── CROSS_PLATFORM_GUIDE.md # Comprehensive guide -│ └── QUICK_REFERENCE.md # Quick reference card -└── bin/ # Build output directory - ├── gochat-linux-amd64 - ├── gochat-linux-arm64 - ├── gochat-darwin-amd64 - ├── gochat-darwin-arm64 - ├── gochat-windows-amd64.exe - └── checksums.txt # SHA256 checksums -``` - -## Supported Platforms - -### Development Platforms (where you code) - -- ✅ Windows 10/11 (PowerShell 5.1+ or 7+) -- ✅ macOS (Intel and Apple Silicon) -- ✅ Linux (any distribution with Bash) - -### Target Platforms (what you can build for) - -- ✅ Windows (amd64) -- ✅ Linux (amd64, arm64) -- ✅ macOS (Intel amd64, Apple Silicon arm64) -- ✅ And many more via `go tool dist list` - -## Key Features - -### 1. True Cross-Compilation - -Build binaries for **any** platform from **any** platform: - -- Build Windows .exe from macOS -- Build macOS binary from Windows -- Build Linux binary from Windows -- No complex toolchains needed - -### 2. Platform-Native Scripts - -Choose your preferred workflow: - -- **Windows users**: Use PowerShell scripts (`.ps1`) -- **macOS/Linux users**: Use Bash scripts (`.sh`) -- **Everyone**: Use Make targets (if Make is installed) - -### 3. Consistent Development Experience - -Same workflow on all platforms: - -```bash -# Clone -git clone -cd gochat - -# Build -./quick-build.sh # or .\quick-build.ps1 - -# Run -./bin/gochat # or .\bin\gochat.exe - -# Develop with hot reload -air -``` - -### 4. VS Code Integration - -Press `Ctrl+Shift+B` (or `Cmd+Shift+B` on macOS) to: - -- Build for current platform -- Build for all platforms -- Run tests -- Start dev server -- Create release builds - -All tasks work on all platforms with no configuration changes. - -### 5. Proper Line Ending Handling - -- Shell scripts always use LF (required for Unix) -- PowerShell scripts use CRLF on Windows -- Go source files consistent across platforms -- No more "bad interpreter" errors - -### 6. Consistent Code Formatting - -EditorConfig ensures: - -- Tabs vs spaces handled correctly -- Consistent indentation -- Proper line endings -- Works with any editor (VS Code, Vim, IntelliJ, etc.) - -## Quick Start Guide - -### Windows - -```powershell -# First time setup -git clone https://github.com/Tyrowin/gochat.git -cd gochat - -# Build -.\quick-build.ps1 - -# Run -.\bin\gochat.exe - -# Develop with hot reload -air -``` - -### macOS/Linux - -```bash -# First time setup -git clone https://github.com/Tyrowin/gochat.git -cd gochat -chmod +x scripts/*.sh - -# Build -./quick-build.sh - -# Run -./bin/gochat - -# Develop with hot reload -air -``` - -## Common Tasks - -### Build for Current Platform - -```bash -# Windows -.\quick-build.ps1 - -# macOS/Linux -./quick-build.sh - -# With Make -make build-current -``` - -### Build for All Platforms - -```bash -# Windows -.\build.ps1 all - -# macOS/Linux -./build.sh all - -# With Make -make build-all -``` - -### Create Release - -```bash -# Windows -.\build.ps1 release - -# macOS/Linux -./build.sh release - -# With Make -make release -``` - -### Development with Hot Reload - -```bash -# All platforms -air - -# Or with Make -make dev -``` - -## Testing the Setup - -1. **Test quick build:** - - ```bash - # Your platform's quick build command - ./quick-build.sh # or .\quick-build.ps1 - ``` - -2. **Test cross-compilation:** - - ```bash - # Build for a different platform - ./build.sh windows # or .\build.ps1 linux - ``` - -3. **Test hot reload:** - - ```bash - air - # Make a change to a .go file - # Should automatically rebuild and restart - ``` - -4. **Test VS Code tasks:** - - Open VS Code - - Press `Ctrl+Shift+B` / `Cmd+Shift+B` - - Select "Build (Current Platform)" - - Should build successfully - -## Benefits - -### For Developers - -- Work on your preferred platform -- No platform lock-in -- Consistent experience everywhere -- Easy onboarding for new contributors -- Professional tooling support - -### For Users - -- Get binaries for their platform easily -- No need to build from source -- Checksums for security verification -- Optimized, self-contained binaries - -### For Contributors - -- Clear documentation -- Easy setup process -- Automated builds and tests -- Platform-specific help available - -## Maintenance - -### Keeping Scripts in Sync - -When adding new build features: - -1. Update `build.sh` -2. Update `build.ps1` (equivalent functionality) -3. Update `Makefile` targets -4. Update documentation -5. Test on at least two platforms - -### Adding New Platforms - -To add support for a new platform (e.g., FreeBSD): - -1. Add build target to Makefile -2. Add case to build scripts -3. Update documentation -4. Test cross-compilation works - -### Updating Dependencies - -```bash -# All platforms -go get -u ./... -go mod tidy -``` - -## Troubleshooting - -See the comprehensive troubleshooting sections in: - -- [Cross-Platform Development Guide](./CROSS_PLATFORM_GUIDE.md#troubleshooting) -- [Quick Reference](./QUICK_REFERENCE.md#troubleshooting) - -## Resources - -- **Full Documentation**: [CROSS_PLATFORM_GUIDE.md](./CROSS_PLATFORM_GUIDE.md) -- **Quick Reference**: [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) -- **Main README**: [../README.md](../README.md) -- **Go Cross-Compilation**: https://golang.org/doc/install/source#environment -- **EditorConfig**: https://editorconfig.org/ -- **Air (Hot Reload)**: https://github.com/air-verse/air - -## Contributing - -When contributing to this project: - -1. ✅ Test changes on multiple platforms if possible -2. ✅ Update both `.sh` and `.ps1` scripts if modifying builds -3. ✅ Use `filepath` package for cross-platform paths -4. ✅ Check line endings are correct (`.gitattributes` helps) -5. ✅ Update documentation if adding new features -6. ✅ Run `make ci-local` before submitting PR - -## Next Steps - -1. **Read the guides**: Start with [CROSS_PLATFORM_GUIDE.md](./CROSS_PLATFORM_GUIDE.md) -2. **Try the tools**: Test building for different platforms -3. **Set up VS Code**: Install recommended extensions -4. **Join development**: See CONTRIBUTING.md - ---- - -**Happy cross-platform development! 🚀** diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..5e7c57b --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,826 @@ +# Deployment Guide + +This guide covers deploying GoChat to production environments with proper security, scalability, and reliability. + +## Table of Contents + +- [Production Deployment Overview](#production-deployment-overview) +- [Reverse Proxy Configuration](#reverse-proxy-configuration) +- [TLS/WSS Setup](#tlswss-setup) +- [Process Management](#process-management) +- [Docker Deployment](#docker-deployment) +- [Monitoring and Logging](#monitoring-and-logging) +- [Performance Tuning](#performance-tuning) + +## Production Deployment Overview + +### Architecture + +GoChat should always run behind a reverse proxy in production: + +``` +Internet → Reverse Proxy (Nginx/Caddy) → GoChat Server + (TLS termination) (localhost:8080) +``` + +### Why a Reverse Proxy? + +- **TLS/SSL termination** - Handle HTTPS/WSS encryption +- **Load balancing** - Distribute traffic across multiple instances +- **Security** - Additional protection layer +- **Static files** - Serve static assets efficiently +- **Request filtering** - Block malicious requests +- **Logging** - Centralized access logs + +### Deployment Checklist + +- [ ] Build optimized binary (`make release`) +- [ ] Configure allowed origins for your domain +- [ ] Set up reverse proxy (Nginx or Caddy) +- [ ] Configure TLS certificate (Let's Encrypt recommended) +- [ ] Set up process management (systemd, Docker, or supervisor) +- [ ] Configure firewall rules +- [ ] Set up monitoring and logging +- [ ] Test failover and restart scenarios +- [ ] Document rollback procedures + +## Reverse Proxy Configuration + +### Nginx Configuration + +**File:** `/etc/nginx/sites-available/gochat` + +```nginx +upstream gochat_backend { + server 127.0.0.1:8080; + # For multiple instances (load balancing): + # server 127.0.0.1:8080; + # server 127.0.0.1:8081; + # server 127.0.0.1:8082; +} + +server { + listen 443 ssl http2; + server_name chat.yourdomain.com; + + # TLS Configuration + ssl_certificate /etc/letsencrypt/live/chat.yourdomain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/chat.yourdomain.com/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + # SSL session cache for performance + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # Security Headers + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # WebSocket Endpoint + location /ws { + proxy_pass http://gochat_backend; + proxy_http_version 1.1; + + # WebSocket upgrade headers + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Standard proxy headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket timeouts (24 hours) + proxy_read_timeout 86400; + proxy_send_timeout 86400; + + # Disable buffering for real-time communication + proxy_buffering off; + } + + # Health Check and Test Page + location / { + proxy_pass http://gochat_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Access and error logs + access_log /var/log/nginx/gochat-access.log; + error_log /var/log/nginx/gochat-error.log; +} + +# Redirect HTTP to HTTPS +server { + listen 80; + server_name chat.yourdomain.com; + return 301 https://$server_name$request_uri; +} +``` + +**Enable the configuration:** + +```bash +# Create symbolic link +sudo ln -s /etc/nginx/sites-available/gochat /etc/nginx/sites-enabled/ + +# Test configuration +sudo nginx -t + +# Reload Nginx +sudo systemctl reload nginx +``` + +### Caddy Configuration + +Caddy automatically handles TLS certificates via Let's Encrypt. + +**File:** `Caddyfile` + +```caddy +chat.yourdomain.com { + # Automatic HTTPS via Let's Encrypt + + # Security Headers + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Frame-Options "DENY" + X-Content-Type-Options "nosniff" + X-XSS-Protection "1; mode=block" + Referrer-Policy "strict-origin-when-cross-origin" + } + + # WebSocket Endpoint + @websocket { + path /ws + } + handle @websocket { + reverse_proxy localhost:8080 { + # Preserve client IP + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} + } + } + + # Health Check and Test Page + handle { + reverse_proxy localhost:8080 + } + + # Logging + log { + output file /var/log/caddy/gochat.log + format json + } +} +``` + +**Run Caddy:** + +```bash +# Run directly +caddy run + +# Or as a service +sudo systemctl enable caddy +sudo systemctl start caddy +sudo systemctl status caddy +``` + +## TLS/WSS Setup + +### Why TLS is Required + +- **Browser security:** Modern browsers require WSS for HTTPS pages +- **Data encryption:** Protects messages from eavesdropping +- **MITM prevention:** Prevents man-in-the-middle attacks +- **Production requirement:** Always use TLS in production + +### Using Let's Encrypt (Free) + +#### With Nginx + +**Install Certbot:** + +```bash +# Ubuntu/Debian +sudo apt update +sudo apt install certbot python3-certbot-nginx + +# RHEL/CentOS +sudo yum install certbot python3-certbot-nginx +``` + +**Obtain Certificate:** + +```bash +sudo certbot --nginx -d chat.yourdomain.com +``` + +**Auto-renewal:** + +Certbot automatically sets up a cron job or systemd timer. Test renewal: + +```bash +sudo certbot renew --dry-run +``` + +#### With Caddy + +Caddy automatically obtains and renews TLS certificates from Let's Encrypt. No additional configuration needed! + +### Custom TLS Certificates + +If using a commercial certificate: + +**Nginx:** + +```nginx +ssl_certificate /path/to/your/fullchain.pem; +ssl_certificate_key /path/to/your/privkey.pem; +``` + +**Caddy:** + +```caddy +chat.yourdomain.com { + tls /path/to/cert.pem /path/to/key.pem +} +``` + +### Client Configuration + +After setting up TLS, update clients to use WSS: + +```javascript +const ws = new WebSocket("wss://chat.yourdomain.com/ws"); +``` + +### Server Configuration + +Update allowed origins in `internal/server/config.go`: + +```go +AllowedOrigins: []string{ + "https://chat.yourdomain.com", + "https://www.yourdomain.com", +} +``` + +## Process Management + +### Systemd (Linux) + +**File:** `/etc/systemd/system/gochat.service` + +```ini +[Unit] +Description=GoChat WebSocket Server +After=network.target +Documentation=https://github.com/Tyrowin/gochat + +[Service] +Type=simple +User=gochat +Group=gochat +WorkingDirectory=/opt/gochat +ExecStart=/opt/gochat/bin/gochat +Restart=always +RestartSec=10 + +# Security hardening +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/opt/gochat + +# Resource limits +LimitNOFILE=65536 + +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=gochat + +[Install] +WantedBy=multi-user.target +``` + +**Setup:** + +```bash +# Create user +sudo useradd -r -s /bin/false gochat + +# Create directory and set ownership +sudo mkdir -p /opt/gochat/bin +sudo chown -R gochat:gochat /opt/gochat + +# Copy binary +sudo cp bin/gochat /opt/gochat/bin/ + +# Enable and start service +sudo systemctl enable gochat +sudo systemctl start gochat + +# Check status +sudo systemctl status gochat + +# View logs +sudo journalctl -u gochat -f +``` + +**Management Commands:** + +```bash +# Start +sudo systemctl start gochat + +# Stop +sudo systemctl stop gochat + +# Restart +sudo systemctl restart gochat + +# Reload (if supporting graceful reload) +sudo systemctl reload gochat + +# Check status +sudo systemctl status gochat + +# View logs +sudo journalctl -u gochat -n 100 -f +``` + +### Supervisor (Alternative) + +**File:** `/etc/supervisor/conf.d/gochat.conf` + +```ini +[program:gochat] +command=/opt/gochat/bin/gochat +directory=/opt/gochat +user=gochat +autostart=true +autorestart=true +redirect_stderr=true +stdout_logfile=/var/log/gochat/gochat.log +``` + +## Docker Deployment + +GoChat includes production-ready Docker support with a multi-stage build process, minimal image size, and security best practices. + +### Quick Start with Docker + +**1. Copy and configure environment file:** + +```bash +cp .env.example .env +# Edit .env with your configuration +``` + +**2. Build and run with Docker Compose:** + +```bash +docker-compose up -d +``` + +**3. Check logs:** + +```bash +docker-compose logs -f gochat +``` + +### Environment Configuration + +GoChat can be configured using environment variables. See `.env.example` for all available options: + +```bash +# Server Configuration +SERVER_PORT=:8080 + +# Allowed Origins for CORS (comma-separated) +ALLOWED_ORIGINS=http://localhost:8080,https://chat.example.com + +# Maximum Message Size (bytes) +MAX_MESSAGE_SIZE=512 + +# Rate Limiting +RATE_LIMIT_BURST=5 +RATE_LIMIT_REFILL_INTERVAL=1 +``` + +### Docker Compose Configuration + +The included `docker-compose.yml` provides: + +- Container health checks +- Automatic restarts +- Network isolation +- Easy environment configuration + +**File:** `docker-compose.yml` + +```yaml +version: "3.8" + +services: + gochat: + build: + context: . + dockerfile: Dockerfile + container_name: gochat-server + ports: + - "8080:8080" + environment: + - SERVER_PORT=:8080 + - ALLOWED_ORIGINS=http://localhost:8080,https://chat.example.com + - MAX_MESSAGE_SIZE=512 + - RATE_LIMIT_BURST=5 + - RATE_LIMIT_REFILL_INTERVAL=1 + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:8080/", + ] + interval: 30s + timeout: 3s + start_period: 5s + retries: 3 + networks: + - gochat-network + +networks: + gochat-network: + driver: bridge +``` + +### Manual Docker Build and Run + +**Build the image:** + +```bash +docker build -t gochat:latest . +``` + +**Run the container:** + +```bash +docker run -d \ + --name gochat \ + -p 8080:8080 \ + -e SERVER_PORT=:8080 \ + -e ALLOWED_ORIGINS="https://chat.example.com" \ + -e MAX_MESSAGE_SIZE=512 \ + -e RATE_LIMIT_BURST=10 \ + -e RATE_LIMIT_REFILL_INTERVAL=2 \ + --restart unless-stopped \ + gochat:latest +``` + +### Dockerfile Details + +The Dockerfile uses a multi-stage build process: + +**Stage 1: Build** + +- Based on `golang:1.25.1-alpine` +- Compiles the application with optimizations +- Strips debug information to reduce binary size + +**Stage 2: Runtime** + +- Based on `alpine:3.20` (minimal size) +- Runs as non-root user (security) +- Includes health check +- Only contains the compiled binary and essential dependencies + +### Production Docker Deployment + +**1. Build for production:** + +```bash +docker build --no-cache -t gochat:1.0.0 . +``` + +**2. Tag for registry:** + +```bash +docker tag gochat:1.0.0 your-registry.com/gochat:1.0.0 +docker tag gochat:1.0.0 your-registry.com/gochat:latest +``` + +**3. Push to registry:** + +```bash +docker push your-registry.com/gochat:1.0.0 +docker push your-registry.com/gochat:latest +``` + +**4. Deploy on server:** + +```bash +# Pull the image +docker pull your-registry.com/gochat:1.0.0 + +# Run with production config +docker run -d \ + --name gochat \ + -p 8080:8080 \ + --env-file .env \ + --restart always \ + your-registry.com/gochat:1.0.0 +``` + +### Docker Behind Reverse Proxy + +When running Docker behind Nginx or Caddy: + +**docker-compose.yml:** + +```yaml +services: + gochat: + build: . + expose: + - "8080" + environment: + - SERVER_PORT=:8080 + - ALLOWED_ORIGINS=https://chat.example.com + networks: + - web + +networks: + web: + external: true +``` + +**Nginx configuration:** + +```nginx +upstream gochat { + server gochat:8080; +} + +server { + listen 443 ssl http2; + server_name chat.example.com; + + location / { + proxy_pass http://gochat; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +### Docker Management Commands + +```bash +# View logs +docker-compose logs -f gochat + +# Restart container +docker-compose restart gochat + +# Stop and remove +docker-compose down + +# Update and redeploy +docker-compose pull +docker-compose up -d + +# Check health status +docker inspect --format='{{.State.Health.Status}}' gochat-server + +# Execute command in container +docker exec -it gochat-server /bin/sh +``` + +### Security Best Practices + +The Dockerfile includes several security features: + +1. **Non-root user:** Application runs as user `gochat` (UID 1000) +2. **Minimal base image:** Alpine Linux reduces attack surface +3. **No unnecessary tools:** Only essential runtime dependencies +4. **Read-only filesystem:** Can be enforced with `--read-only` flag +5. **Health checks:** Automated container health monitoring + +### Docker Performance Optimization + +**Multi-stage build benefits:** + +- Final image size: ~15MB (vs ~1GB with full Go image) +- Faster deployment and startup +- Reduced network transfer time +- Lower storage costs + +**Build cache optimization:** + +- Dependencies downloaded separately for better caching +- Source code copied last to maximize cache hits + + "http://localhost:8080/", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +networks: +gochat-network: +driver: bridge + +```` + +### Build and Run + +```bash +# Build image +docker build -t gochat:latest . + +# Run container +docker run -d \ + --name gochat \ + --restart unless-stopped \ + -p 8080:8080 \ + gochat:latest + +# Using Docker Compose +docker-compose up -d + +# View logs +docker logs -f gochat + +# Stop +docker stop gochat + +# Remove +docker rm gochat +```` + +## Monitoring and Logging + +### Logging Best Practices + +1. **Structured logging** - Use JSON format for easier parsing +2. **Log levels** - Implement INFO, WARN, ERROR levels +3. **Log rotation** - Prevent disk space issues +4. **Centralized logging** - Use ELK stack, Splunk, or similar + +### Health Checks + +```bash +# Simple health check +curl http://localhost:8080/ + +# With reverse proxy +curl https://chat.yourdomain.com/ +``` + +### Monitoring Metrics + +Consider monitoring: + +- Active WebSocket connections +- Messages per second +- Connection errors +- Rate limit violations +- Memory usage +- CPU usage +- Network I/O + +### Tools + +- **Prometheus** - Metrics collection +- **Grafana** - Visualization +- **AlertManager** - Alerting +- **ELK Stack** - Log aggregation + +## Performance Tuning + +### System Limits + +**File:** `/etc/security/limits.conf` + +``` +gochat soft nofile 65536 +gochat hard nofile 65536 +``` + +### Kernel Parameters + +**File:** `/etc/sysctl.conf` + +``` +# Increase TCP buffer sizes +net.core.rmem_max = 16777216 +net.core.wmem_max = 16777216 +net.ipv4.tcp_rmem = 4096 87380 16777216 +net.ipv4.tcp_wmem = 4096 65536 16777216 + +# Increase connection backlog +net.core.somaxconn = 4096 +net.ipv4.tcp_max_syn_backlog = 4096 + +# Enable TCP Fast Open +net.ipv4.tcp_fastopen = 3 +``` + +Apply changes: + +```bash +sudo sysctl -p +``` + +### Go Runtime + +Set GOMAXPROCS to match CPU cores (usually automatic). + +### Load Balancing + +For high traffic, run multiple instances: + +```nginx +upstream gochat_backend { + least_conn; # Load balancing algorithm + server 127.0.0.1:8080; + server 127.0.0.1:8081; + server 127.0.0.1:8082; +} +``` + +## Firewall Configuration + +### UFW (Ubuntu) + +```bash +# Allow SSH +sudo ufw allow 22/tcp + +# Allow HTTP and HTTPS +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp + +# Block direct access to GoChat port +sudo ufw deny 8080/tcp + +# Enable firewall +sudo ufw enable +``` + +### iptables + +```bash +# Allow HTTP/HTTPS +iptables -A INPUT -p tcp --dport 80 -j ACCEPT +iptables -A INPUT -p tcp --dport 443 -j ACCEPT + +# Block external access to port 8080 +iptables -A INPUT -p tcp --dport 8080 -i eth0 -j DROP +iptables -A INPUT -p tcp --dport 8080 -i lo -j ACCEPT +``` + +## Backup and Recovery + +### What to Backup + +- Configuration files +- TLS certificates (if not using Let's Encrypt) +- Custom code modifications +- Deployment scripts + +### Rollback Plan + +1. Keep previous binary versions +2. Document configuration changes +3. Test rollback procedure +4. Have a tested recovery process + +## Related Documentation + +- [Getting Started](GETTING_STARTED.md) - Initial setup +- [API Documentation](API.md) - WebSocket API +- [Security](SECURITY.md) - Security features and best practices +- [Building](BUILDING.md) - Build and compilation instructions diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..5953900 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,647 @@ +# Development Guide + +This guide covers setting up a development environment, running tests, and contributing to GoChat. + +## Table of Contents + +- [Development Setup](#development-setup) +- [Development Tools](#development-tools) +- [Hot Reload Development](#hot-reload-development) +- [Code Quality](#code-quality) +- [Testing](#testing) +- [CI/CD Pipeline](#cicd-pipeline) +- [Project Structure](#project-structure) + +## Development Setup + +### Prerequisites + +- Go 1.25.1 or later +- Git +- Make (optional but recommended) + +### Clone and Setup + +```bash +# Clone repository +git clone https://github.com/Tyrowin/gochat.git +cd gochat + +# Install development tools +make install-tools + +# Verify setup +go version +make help +``` + +## Development Tools + +### Install All Tools + +```bash +make install-tools +``` + +This installs: + +- **golangci-lint** - Static code analysis and linting +- **govulncheck** - Go vulnerability database scanner +- **gosec** - Security-focused static analyzer +- **goimports** - Import organization and formatting +- **air** - Live reload for rapid development + +### Individual Tool Installation + +**golangci-lint:** + +```bash +go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest +``` + +**govulncheck:** + +```bash +go install golang.org/x/vuln/cmd/govulncheck@latest +``` + +**gosec:** + +```bash +go install github.com/securego/gosec/v2/cmd/gosec@latest +``` + +**goimports:** + +```bash +go install golang.org/x/tools/cmd/goimports@latest +``` + +**air:** + +```bash +go install github.com/cosmtrek/air@latest +``` + +## Hot Reload Development + +For rapid development with automatic reloading on file changes: + +```bash +make dev +``` + +This uses [Air](https://github.com/cosmtrek/air) to: + +- Watch for file changes +- Automatically rebuild the application +- Restart the server +- Preserve the terminal output + +**Manual air configuration** (`.air.toml`): + +```toml +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ./cmd/server" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata", "bin"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true +``` + +## Code Quality + +### Available Make Targets + +View all available commands: + +```bash +make help +``` + +### Common Development Tasks + +**Code Formatting:** + +```bash +make fmt +``` + +- Runs `gofmt` on all Go files +- Organizes imports with `goimports` +- Ensures consistent code style + +**Linting:** + +```bash +make lint +``` + +- Runs `golangci-lint` with comprehensive rule set +- Checks code quality, style, and potential bugs +- Configuration in `.golangci.yml` + +**Security Scanning:** + +```bash +make security-scan +``` + +- Runs `govulncheck` for dependency vulnerabilities +- Runs `gosec` for security issues in code + +**Dependency Management:** + +```bash +# Check for dependency issues +make deps-check + +# Update dependencies +make deps-update + +# Verify licenses +make license-check +``` + +### Code Quality Standards + +**Formatting Rules:** + +- Standard Go formatting (`gofmt`) +- Organized imports (stdlib, external, internal) +- Maximum line length: 120 characters (recommended) + +**Documentation:** + +- All exported functions must have doc comments +- Package-level documentation in `doc.go` files +- Comments should explain "why", not "what" + +**Error Handling:** + +- Always check errors +- Provide context with error messages +- Use `fmt.Errorf` with `%w` for error wrapping + +**Testing:** + +- Unit tests for all new functionality +- Table-driven tests where applicable +- Test edge cases and error conditions +- Maintain or improve code coverage + +## Testing + +### Run All Tests + +```bash +make test +``` + +This runs: + +- All unit tests +- All integration tests +- Race condition detection enabled +- Verbose output + +### Test with Coverage + +```bash +make test-coverage +``` + +This generates: + +- Coverage report in `coverage.out` +- HTML coverage visualization (opens in browser) + +**View coverage manually:** + +```bash +go test -coverprofile=coverage.out ./... +go tool cover -html=coverage.out +``` + +### Run Specific Tests + +**Unit tests only:** + +```bash +go test -v -race ./test/unit +``` + +**Integration tests only:** + +```bash +go test -v -race ./test/integration +``` + +**Specific test file:** + +```bash +go test -v -race ./test/unit/handlers_test.go +``` + +**Specific test function:** + +```bash +go test -v -race -run TestWebSocketHandler ./test/unit +``` + +### Test Structure + +``` +test/ +├── integration/ # Integration tests +│ ├── multiclient_test.go +│ ├── security_test.go +│ ├── server_test.go +│ ├── shutdown_test.go +│ └── websocket_test.go +├── unit/ # Unit tests +│ ├── error_handling_test.go +│ ├── handlers_test.go +│ ├── hub_test.go +│ └── websocket_test.go +└── testhelpers/ # Shared test utilities + └── helpers.go +``` + +### Writing Tests + +**Example unit test:** + +```go +func TestNewClient(t *testing.T) { + hub := NewHub() + conn := &mockWebSocketConn{} + + client := NewClient(conn, hub, "127.0.0.1:1234") + + if client == nil { + t.Fatal("Expected client to be created") + } + if client.conn != conn { + t.Error("Client connection not set correctly") + } +} +``` + +**Example integration test:** + +```go +func TestMultiClientBroadcast(t *testing.T) { + server := testhelpers.StartTestServer(t) + defer server.Close() + + // Connect multiple clients + client1 := testhelpers.ConnectClient(t, server.URL) + client2 := testhelpers.ConnectClient(t, server.URL) + + // Send message from client1 + msg := Message{Content: "Hello"} + client1.WriteJSON(msg) + + // Verify client2 receives it + var received Message + if err := client2.ReadJSON(&received); err != nil { + t.Fatalf("Failed to receive message: %v", err) + } + + if received.Content != msg.Content { + t.Errorf("Expected %q, got %q", msg.Content, received.Content) + } +} +``` + +### Benchmarks + +Run benchmarks to measure performance: + +```bash +make bench +``` + +**Write benchmarks:** + +```go +func BenchmarkMessageBroadcast(b *testing.B) { + hub := NewHub() + // Setup... + + b.ResetTimer() + for i := 0; i < b.N; i++ { + hub.broadcast <- message + } +} +``` + +### Race Detection + +Always run tests with race detection: + +```bash +go test -race ./... +``` + +The CI pipeline automatically runs tests with `-race` flag. + +## CI/CD Pipeline + +### GitHub Actions + +The project uses GitHub Actions for continuous integration. Pipeline runs on: + +- Every push to any branch +- Every pull request + +### Pipeline Stages + +1. **Code Formatting** - Verify `gofmt` compliance +2. **Static Analysis** - Run `golangci-lint` +3. **Security Scanning** - Run `govulncheck` and `gosec` +4. **Unit Tests** - Run all tests with race detection +5. **Integration Tests** - Run integration test suite +6. **Coverage Check** - Ensure minimum coverage +7. **Dependency Check** - Verify dependencies are up to date +8. **Build Verification** - Build for multiple platforms + +### Run CI Locally + +Before pushing code, run the same checks locally: + +```bash +make ci-local +``` + +This runs: + +- Code formatting check +- Linting +- Security scans +- All tests with race detection +- Dependency checks +- Build verification + +**Individual CI steps:** + +```bash +# Format check +make fmt-check + +# Lint +make lint + +# Security +make security-scan + +# Tests +make test + +# Dependencies +make deps-check + +# Build +make build +``` + +### CI Configuration + +**File:** `.github/workflows/ci.yml` + +Key features: + +- Runs on Ubuntu latest +- Tests against Go 1.25.1 +- Caches dependencies for speed +- Uploads test coverage +- Builds for multiple platforms + +## Project Structure + +``` +gochat/ +├── cmd/ +│ └── server/ # Application entry point +│ └── main.go # Server initialization and graceful shutdown +├── internal/ +│ └── server/ # Core server implementation +│ ├── client.go # WebSocket client lifecycle +│ ├── config.go # Server configuration +│ ├── handlers.go # HTTP/WebSocket handlers +│ ├── hub.go # Client registry and broadcasting +│ ├── http_server.go # HTTP server setup +│ ├── origin.go # Origin validation +│ ├── rate_limiter.go # Rate limiting +│ ├── routes.go # Route registration +│ └── types.go # Shared types +├── test/ +│ ├── integration/ # Integration tests +│ ├── unit/ # Unit tests +│ └── testhelpers/ # Test utilities +├── docs/ # Documentation +├── .github/ +│ └── workflows/ +│ └── ci.yml # CI/CD pipeline +├── .golangci.yml # Linter configuration +├── Makefile # Build automation +├── go.mod # Go module definition +├── go.sum # Dependency checksums +└── README.md # Project overview +``` + +### Key Components + +**client.go:** + +- Manages individual WebSocket connections +- Implements read/write pumps +- Handles message validation +- Rate limiting per connection + +**hub.go:** + +- Central message broker +- Client registry +- Broadcast coordination +- Connection lifecycle management + +**handlers.go:** + +- HTTP request handlers +- WebSocket upgrade logic +- Health check endpoint +- Test page serving + +**rate_limiter.go:** + +- Token bucket implementation +- Per-connection rate limiting +- Configurable limits + +**origin.go:** + +- Origin header validation +- CSWSH attack prevention +- Configurable allowed origins + +## Development Workflow + +### Standard Workflow + +1. **Create a branch** + + ```bash + git checkout -b feature/your-feature-name + ``` + +2. **Make changes** + + - Write code + - Add tests + - Update documentation + +3. **Test locally** + + ```bash + make ci-local + ``` + +4. **Commit changes** + + ```bash + git add . + git commit -m "Add feature: description" + ``` + +5. **Push to GitHub** + + ```bash + git push origin feature/your-feature-name + ``` + +6. **Open Pull Request** + - Describe changes + - Link related issues + - Wait for CI to pass + +### Pre-commit Checklist + +- [ ] Code is formatted (`make fmt`) +- [ ] Linting passes (`make lint`) +- [ ] Tests pass (`make test`) +- [ ] Security scans pass (`make security-scan`) +- [ ] Documentation updated +- [ ] No debug code left in +- [ ] Commit message is clear + +### Debugging + +**Enable verbose logging:** + +```go +log.SetFlags(log.LstdFlags | log.Lshortfile) +``` + +**Use delve debugger:** + +```bash +go install github.com/go-delve/delve/cmd/dlv@latest +dlv debug ./cmd/server +``` + +**VS Code debugging:** + +`.vscode/launch.json`: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Server", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/cmd/server" + } + ] +} +``` + +## Performance Profiling + +### CPU Profiling + +```bash +go test -cpuprofile=cpu.prof -bench=. +go tool pprof cpu.prof +``` + +### Memory Profiling + +```bash +go test -memprofile=mem.prof -bench=. +go tool pprof mem.prof +``` + +### Runtime Profiling + +Add to your code: + +```go +import _ "net/http/pprof" + +go func() { + log.Println(http.ListenAndServe("localhost:6060", nil)) +}() +``` + +Access profiles at `http://localhost:6060/debug/pprof/` + +## Related Documentation + +- [Getting Started](GETTING_STARTED.md) - Installation and setup +- [Building](BUILDING.md) - Build and cross-compilation +- [Contributing](CONTRIBUTING.md) - Contribution guidelines +- [API Documentation](API.md) - WebSocket API +- [Security](SECURITY.md) - Security features diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 0000000..10b6880 --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,169 @@ +# Getting Started with GoChat + +This guide will help you get GoChat up and running on your system. + +## Prerequisites + +- Go 1.25.1 or later +- Git +- Make (optional but recommended) + - **Windows**: Install via [Chocolatey](https://chocolatey.org/) - `choco install make` + - **macOS**: Install Xcode Command Line Tools - `xcode-select --install` + - **Linux**: Usually pre-installed (`apt install make` or `yum install make`) + +**Note**: If you don't have Make, you can use Go commands directly (see examples below). + +## Installation + +1. Clone the repository: + + ```bash + git clone https://github.com/Tyrowin/gochat.git + cd gochat + ``` + +2. Verify Go installation: + + ```bash + go version + # Should show Go 1.25.1 or later + ``` + +## Building the Server + +### Using Make (recommended) + +```bash +make build +``` + +### Using Go directly + +**On Windows (PowerShell):** + +```powershell +go build -o bin\gochat.exe .\cmd\server +``` + +**On macOS/Linux:** + +```bash +go build -o bin/gochat ./cmd/server +``` + +The compiled binary will be placed in the `bin/` directory. + +## Running the Server + +### Start the Server + +**On Windows:** + +```powershell +.\bin\gochat.exe +``` + +**On macOS/Linux:** + +```bash +./bin/gochat +``` + +**Using Make:** + +```bash +make run +``` + +### Expected Output + +The server will start on `http://localhost:8080` and display: + +``` +Starting GoChat server... +Server starting on port :8080 +Hub started - ready to accept connections +``` + +## Available Endpoints + +Once the server is running, you can access: + +- **`GET /`** - Health check endpoint + + - Returns: "GoChat server is running!" + - Use this to verify the server is operational + +- **`GET /ws`** - WebSocket connection endpoint + + - This is where clients connect for real-time chat + - See [API Documentation](API.md) for details + +- **`GET /test`** - Built-in test page + - Interactive HTML page for testing WebSocket functionality + - Navigate to `http://localhost:8080/test` in your browser + - Allows you to connect, send messages, and see real-time chat + +## Quick Test + +1. Start the server (see above) +2. Open your browser and navigate to `http://localhost:8080/test` +3. Click "Connect" to establish a WebSocket connection +4. Type a message and click "Send" +5. Open another browser window/tab to the same URL to test multi-client chat + +## Next Steps + +- Learn about the [WebSocket API](API.md) to integrate GoChat into your application +- Review [Security features](SECURITY.md) to understand built-in protections +- See [Deployment Guide](DEPLOYMENT.md) for production deployment +- Check [Development Guide](DEVELOPMENT.md) to contribute or customize + +## Troubleshooting + +### Port Already in Use + +If you see an error like "address already in use", another application is using port 8080: + +**Windows:** + +```powershell +# Find what's using port 8080 +netstat -ano | findstr :8080 +# Kill the process (replace PID with the actual process ID) +taskkill /PID /F +``` + +**macOS/Linux:** + +```bash +# Find what's using port 8080 +lsof -i :8080 +# Kill the process +kill -9 +``` + +### Permission Denied + +If you get a "permission denied" error on macOS/Linux, make the binary executable: + +```bash +chmod +x bin/gochat +``` + +### Connection Refused + +If you can't connect to the server: + +1. Verify the server is running and shows "Server starting on port :8080" +2. Check your firewall settings +3. Ensure you're using the correct URL: `http://localhost:8080` +4. Try `http://127.0.0.1:8080` instead + +### Origin Not Allowed + +If WebSocket connections are rejected: + +- By default, only `http://localhost:8080` is allowed as an origin +- To allow other origins, modify the configuration in `internal/server/config.go` +- See [Security Documentation](SECURITY.md) for details on origin validation diff --git a/docs/GRACEFUL_SHUTDOWN.md b/docs/GRACEFUL_SHUTDOWN.md deleted file mode 100644 index 5cb9fb4..0000000 --- a/docs/GRACEFUL_SHUTDOWN.md +++ /dev/null @@ -1,221 +0,0 @@ -# Graceful Shutdown & Error Handling Implementation Summary - -## Overview - -This document summarizes the graceful shutdown and robust error handling features implemented for the GoChat WebSocket server. - -## Features Implemented - -### 1. Graceful Shutdown Mechanism - -#### Hub Shutdown (`internal/server/hub.go`) - -- **Context-Based Shutdown**: Added `context.Context` and `context.CancelFunc` to Hub struct for coordinated shutdown -- **Goroutine Tracking**: Implemented `sync.WaitGroup` to track all client read/write goroutines -- **Shutdown Method**: `Hub.Shutdown(timeout time.Duration)` method that: - - Signals all goroutines to stop via context cancellation - - Closes all active client WebSocket connections - - Waits for all goroutines to terminate (with timeout) - - Returns error if timeout is exceeded - -#### HTTP Server Shutdown (`internal/server/http_server.go`) - -- **ShutdownServer Function**: Gracefully shuts down the HTTP server - - Uses Go's built-in `http.Server.Shutdown()` for graceful connection draining - - Accepts configurable timeout - - Logs shutdown progress and completion - -#### Main Application Shutdown (`cmd/server/main.go`) - -- **Signal Handling**: Listens for OS interrupt signals (SIGINT, SIGTERM) -- **Orderly Shutdown Sequence**: - 1. Receives shutdown signal - 2. Stops accepting new HTTP connections - 3. Gracefully closes all WebSocket connections via Hub shutdown - 4. Waits for all goroutines to complete (with 30s total timeout) -- **Error Handling**: Proper error logging and exit codes - -### 2. Robust Error Handling for I/O Operations - -#### Enhanced Read Operations (`internal/server/client.go`) - -- **Comprehensive Error Categorization**: - - - `websocket.ErrReadLimit`: Message size limit violations - - `io.EOF`: Normal connection closure - - `websocket.CloseError`: Graceful close scenarios (normal, going away, abnormal) - - Unexpected close errors: Logged with full context - - Generic errors: Logged with descriptive messages - -- **Error Context**: All error messages now include client address for better debugging - -#### Enhanced Write Operations (`internal/server/client.go`) - -- **Write Deadline Errors**: Logged with client context -- **Writer Creation Errors**: Properly handled and logged -- **Message Content Errors**: Detailed error logging for write failures -- **Queued Message Errors**: Individual error handling for each queued message -- **Writer Close Errors**: Logged when writer fails to close properly -- **Ping Errors**: Specific error handling for ping message failures - -#### Connection Management - -- **Setup Errors**: Read deadline configuration errors are logged -- **Pong Handler Errors**: Errors in keepalive mechanism are logged -- **Close Errors**: Expected vs unexpected close errors are differentiated - -### 3. Testing - -#### Integration Tests (`test/integration/shutdown_test.go`) - -- **TestGracefulShutdown**: Basic hub shutdown without clients -- **TestGracefulShutdownWithClients**: Shutdown with multiple active connections -- **TestShutdownWithActiveMessages**: Verifies message handling during shutdown -- **TestShutdownTimeout**: Validates timeout behavior -- **TestConcurrentShutdown**: Tests multiple simultaneous shutdown calls -- **TestNoClientsShutdown**: Shutdown with no active connections - -#### Unit Tests (`test/unit/error_handling_test.go`) - -- **TestClientErrorHandling**: Error categorization verification -- **TestHubShutdownContext**: Hub respects shutdown context -- **TestHubShutdownTimeout**: Timeout enforcement -- **TestRecoveryFromPanic**: Panic recovery in send operations - -#### Test Helpers (`test/testhelpers/helpers.go`) - -- WebSocket connection helpers -- Message sending/receiving utilities -- Proper origin header configuration - -## Key Benefits - -### 1. No Data Loss - -- Graceful shutdown ensures in-flight messages are processed -- Clients receive close notifications before server terminates - -### 2. Clean Resource Cleanup - -- All goroutines properly terminate -- No goroutine leaks -- WebSocket connections cleanly closed - -### 3. Production Ready - -- Signal handling for container environments (Docker, Kubernetes) -- Configurable timeouts prevent indefinite hangs -- Comprehensive error logging for debugging - -### 4. Better Debugging - -- All error messages include client address -- Error categorization makes diagnosis easier -- Separate logging for expected vs unexpected errors - -## Usage - -### Running the Server - -```bash -./gochat -``` - -### Graceful Shutdown - -Send `SIGINT` (Ctrl+C) or `SIGTERM`: - -```bash -kill -TERM -``` - -The server will: - -1. Log "Received shutdown signal" -2. Stop accepting new connections -3. Close all WebSocket connections -4. Wait for goroutines to finish (max 30s) -5. Log "Server stopped gracefully" - -### Configuration - -Shutdown timeouts can be adjusted in `cmd/server/main.go`: - -```go -const shutdownTimeout = 30 * time.Second // Total timeout -httpServer.Shutdown(15*time.Second) // HTTP shutdown -hub.Shutdown(15*time.Second) // Hub shutdown -``` - -## Error Handling Examples - -### Read Errors - -``` -Message from 127.0.0.1:59593 exceeded maximum size of 64 bytes -Client 127.0.0.1:59593 disconnected: websocket: close 1000 (normal) -WebSocket read error from 127.0.0.1:59593: read tcp: connection reset -``` - -### Write Errors - -``` -Error setting write deadline for 127.0.0.1:59593: use of closed connection -Error creating writer for 127.0.0.1:59593: websocket: close sent -Error writing message to 127.0.0.1:59593: broken pipe -``` - -### Shutdown Logs - -``` -Received shutdown signal: interrupt -Step 1: Stopping HTTP server... -Shutting down HTTP server... -HTTP server shutdown completed -Step 2: Shutting down WebSocket hub... -Initiating hub shutdown... -Shutting down all client connections... -Closed 5 client connections -Hub shutdown completed successfully -Server stopped gracefully -``` - -## Testing - -Run all tests: - -```bash -go test -v ./test/... -``` - -Run shutdown tests specifically: - -```bash -go test -v ./test/integration/shutdown_test.go -``` - -Run with race detector: - -```bash -go test -v -race ./test/... -``` - -## Future Enhancements - -Potential improvements: - -1. Metrics for shutdown duration -2. Configurable shutdown behavior per environment -3. Graceful degradation under load -4. Connection draining strategies -5. Shutdown hooks for custom cleanup logic - -## Compliance - -This implementation follows Go best practices: - -- Uses `context.Context` for cancellation -- Implements `sync.WaitGroup` for goroutine coordination -- Leverages standard library `signal` package -- Proper error wrapping with `fmt.Errorf` -- Thread-safe operations with mutex protection diff --git a/docs/QUICK_REFERENCE.md b/docs/QUICK_REFERENCE.md deleted file mode 100644 index 975d007..0000000 --- a/docs/QUICK_REFERENCE.md +++ /dev/null @@ -1,242 +0,0 @@ -# GoChat Cross-Platform Quick Reference - -## Quick Build Commands - -### Windows (PowerShell) - -```powershell -.\scripts\quick-build.ps1 # Quick build for current platform -.\scripts\build.ps1 current # Build for Windows -.\scripts\build.ps1 linux # Build for Linux -.\scripts\build.ps1 darwin-arm64 # Build for macOS (Apple Silicon) -.\scripts\build.ps1 all # Build for all platforms -.\scripts\build.ps1 release # Create release builds -.\scripts\build.ps1 -Clean all # Clean and build all -``` - -### macOS/Linux (Bash) - -```bash -./scripts/quick-build.sh # Quick build for current platform -./scripts/build.sh current # Build for current platform -./scripts/build.sh windows # Build for Windows -./scripts/build.sh darwin-arm64 # Build for macOS (Apple Silicon) -./scripts/build.sh all # Build for all platforms -./scripts/build.sh release # Create release builds -./scripts/build.sh --clean all # Clean and build all -``` - -### Make (All Platforms) - -```bash -make build-current # Build for current platform -make build-windows # Build for Windows -make build-linux # Build for Linux -make build-darwin-arm64 # Build for macOS (Apple Silicon) -make build-all # Build for all platforms -make release # Create release builds -make list-platforms # List all supported platforms -``` - -## Development Commands - -### Run Server - -```powershell -# Windows -.\bin\gochat.exe - -# macOS/Linux -./bin/gochat - -# With Make -make run -``` - -### Hot Reload Development - -```bash -make dev # All platforms (requires air) -air # Direct command (requires air) -``` - -### Install Development Tools - -```bash -make install-tools # Installs golangci-lint, govulncheck, air, etc. -``` - -## Testing & Quality - -```bash -make test # Run all tests -make test-unit # Run unit tests only -make test-integration # Run integration tests only -make test-coverage # Run tests with coverage report -make lint # Run linters -make lint-fix # Run linters with auto-fix -make security-scan # Run security scans -make ci-local # Run full CI pipeline locally -``` - -## Cross-Compilation Environment Variables - -### PowerShell - -```powershell -$env:CGO_ENABLED = "0" -$env:GOOS = "linux" -$env:GOARCH = "amd64" -go build -o bin/gochat-linux-amd64 ./cmd/server -``` - -### Bash - -```bash -CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/gochat.exe ./cmd/server -``` - -## Platform Targets - -| Platform | GOOS | GOARCH | Output | -| ------------------- | ------- | ------ | ------------------------ | -| Windows 64-bit | windows | amd64 | gochat-windows-amd64.exe | -| Linux 64-bit | linux | amd64 | gochat-linux-amd64 | -| Linux ARM64 | linux | arm64 | gochat-linux-arm64 | -| macOS Intel | darwin | amd64 | gochat-darwin-amd64 | -| macOS Apple Silicon | darwin | arm64 | gochat-darwin-arm64 | - -## Common Tasks - -### First Time Setup - -```bash -# Clone -git clone https://github.com/Tyrowin/gochat.git -cd gochat - -# Make scripts executable (macOS/Linux only) -chmod +x scripts/build.sh scripts/quick-build.sh - -# Install tools -make install-tools - -# Build -./scripts/quick-build.sh # or .\scripts\quick-build.ps1 on Windows -``` - -### Development Workflow - -```bash -# 1. Make changes to code -# 2. Run with hot reload -make dev - -# OR build and run manually -make build && make run - -# 3. Run tests -make test - -# 4. Check code quality -make lint - -# 5. Full CI check before commit -make ci-local -``` - -### Release Workflow - -```bash -# 1. Run full quality checks -make ci-local - -# 2. Create release builds -make release - -# 3. Check output -ls -la bin/ - -# 4. Verify checksums -cat bin/checksums.txt -``` - -## Troubleshooting - -### Windows - -```powershell -# Enable script execution -Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser - -# If Make not found, use PowerShell scripts -.\scripts\build.ps1 current -``` - -### macOS - -```bash -# Make scripts executable -chmod +x scripts/*.sh - -# Remove quarantine from downloaded binaries -xattr -d com.apple.quarantine bin/gochat -``` - -### Linux - -```bash -# Add Go to PATH if needed -export PATH=$PATH:/usr/local/go/bin - -# Make sure binary is executable -chmod +x bin/gochat -``` - -## File Locations - -``` -gochat/ -├── scripts/ -│ ├── build.ps1 # Windows build script -│ ├── build.sh # Unix build script -│ ├── quick-build.ps1 # Windows quick build -│ └── quick-build.sh # Unix quick build -├── Makefile # Make targets (all platforms) -├── .air.toml # Hot reload config -├── .gitattributes # Line ending config -├── bin/ # Build output directory -│ ├── gochat-linux-amd64 -│ ├── gochat-darwin-arm64 -│ ├── gochat-windows-amd64.exe -│ └── checksums.txt -└── docs/ - └── CROSS_PLATFORM_GUIDE.md # Detailed guide -``` - -## Environment Setup - -### Windows - -- Install Go from golang.org -- Install Git from git-scm.com -- Optional: Install Make via Chocolatey - -### macOS - -- Install Xcode Command Line Tools -- Install Go via Homebrew or golang.org -- Make and Git included with Xcode - -### Linux - -- Install Go via package manager or golang.org -- Install Make and Git via package manager -- Usually pre-installed on most distributions - -## Resources - -- Full Guide: [docs/CROSS_PLATFORM_GUIDE.md](CROSS_PLATFORM_GUIDE.md) -- Main README: [README.md](../README.md) -- Go Documentation: https://golang.org/doc/ -- Cross-Compilation: https://golang.org/doc/install/source#environment diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..fb60876 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,397 @@ +# Security Documentation + +GoChat implements multiple layers of security to protect against common WebSocket vulnerabilities and abuse. + +## Table of Contents + +- [Origin Validation](#origin-validation) +- [Rate Limiting](#rate-limiting) +- [Message Size Limits](#message-size-limits) +- [Security Scanning](#security-scanning) +- [Security Best Practices](#security-best-practices) +- [Reporting Security Issues](#reporting-security-issues) + +## Origin Validation + +The server validates the `Origin` header of all WebSocket connection requests to prevent Cross-Site WebSocket Hijacking (CSWSH) attacks. + +### How It Works + +- Every WebSocket upgrade request must include a valid `Origin` header +- The origin is normalized (scheme and host are lowercased) +- Only origins in the allowed list can establish connections +- Requests from disallowed origins are rejected with a 403 Forbidden response +- The server logs all blocked connection attempts + +### Default Configuration + +```go +AllowedOrigins: []string{ + "http://localhost:8080", +} +``` + +### Customizing Allowed Origins + +Modify the allowed origins in `internal/server/config.go`: + +```go +AllowedOrigins: []string{ + "http://localhost:8080", + "https://yourdomain.com", + "https://www.yourdomain.com", +} +``` + +### Allow All Origins (Development Only) + +**WARNING:** Never use this in production! + +```go +AllowedOrigins: []string{"*"} +``` + +### Security Implications + +- **Prevents CSWSH attacks:** Malicious websites cannot connect to your chat server +- **Whitelist approach:** Only explicitly allowed origins can connect +- **Production requirement:** Always configure actual domain names, never use `*` +- **Multiple domains:** Include all legitimate frontend domains (www, subdomains, etc.) + +### Testing Origin Validation + +```bash +# This should succeed (if localhost:8080 is allowed) +websocat ws://localhost:8080/ws -H "Origin: http://localhost:8080" + +# This should fail (origin not in allowed list) +websocat ws://localhost:8080/ws -H "Origin: http://malicious-site.com" +``` + +## Rate Limiting + +GoChat implements per-connection token bucket rate limiting to prevent message flooding and abuse. + +### How It Works + +- Each client connection has its own rate limiter +- The limiter starts with a burst of tokens +- Each message sent consumes 1 token +- Tokens refill at a configured rate +- When a client runs out of tokens, the connection is closed + +### Default Configuration + +```go +RateLimit: RateLimitConfig{ + Burst: 5, // Allow 5 messages immediately + RefillInterval: time.Second, // Refill 5 tokens per second +} +``` + +This allows: + +- **Burst:** Up to 5 messages instantly +- **Sustained rate:** 5 messages per second +- **Recovery:** Full burst capacity restored every second + +### Example Scenarios + +**Normal Usage:** + +- Client sends 3 messages per second +- Result: No issues, well within limits + +**Burst Traffic:** + +- Client sends 5 messages instantly +- Result: Allowed (uses burst capacity) +- Tokens refill over the next second + +**Abuse Attempt:** + +- Client attempts to send 100 messages instantly +- Result: First 5 succeed, connection closed after token exhaustion + +**Sustained Flooding:** + +- Client sends 10 messages per second continuously +- Result: First 5 succeed, then connection closed + +### Customizing Rate Limits + +Modify the configuration in `internal/server/config.go`: + +```go +RateLimit: RateLimitConfig{ + Burst: 10, // Higher burst for occasional spikes + RefillInterval: 500 * time.Millisecond, // Faster refill (10 tokens/sec) +} +``` + +**Recommendations:** + +- **Chat applications:** 5-10 messages/second is typically sufficient +- **High-frequency trading:** May need higher limits (50-100/sec) +- **Public servers:** Keep limits conservative to prevent abuse +- **Private networks:** Can be more lenient + +### Benefits + +- **DoS prevention:** Stops malicious clients from overwhelming the server +- **Resource protection:** Prevents excessive CPU and bandwidth usage +- **Fair sharing:** One abusive client cannot affect others +- **Legitimate bursts:** Allows normal usage patterns (quick replies, etc.) + +### Rate Limit Headers + +The server does not currently expose rate limit information in headers, but clients are disconnected if limits are exceeded. Consider implementing exponential backoff in your client code. + +## Message Size Limits + +Message size limits prevent memory exhaustion attacks and reduce bandwidth consumption. + +### Default Limits + +- **Maximum message size:** 512 bytes +- **Read buffer size:** 1024 bytes +- **Write buffer size:** 1024 bytes + +### How It Works + +- WebSocket upgrader sets buffer sizes +- Configuration enforces maximum message size +- Messages exceeding the limit cause the connection to close +- No warning is sent - connection is terminated immediately + +### Customizing Message Size + +In `internal/server/config.go`: + +```go +MaxMessageSize: 1024, // Allow larger messages (in bytes) +``` + +**Note:** Also update the WebSocket upgrader buffer sizes in `internal/server/handlers.go` if needed: + +```go +var upgrader = websocket.Upgrader{ + ReadBufferSize: 2048, // Increase if needed + WriteBufferSize: 2048, // Increase if needed + CheckOrigin: checkOrigin, +} +``` + +### Size Recommendations + +| Use Case | Recommended Size | +| --------------------- | ---------------- | +| Short chat messages | 256-512 bytes | +| Regular chat messages | 512-1024 bytes | +| Rich text/links | 1024-2048 bytes | +| JSON with metadata | 2048-4096 bytes | + +### Calculating Message Size + +JSON overhead adds to your message size: + +```json +{ "content": "Hello" } +``` + +- Content: 5 bytes +- JSON structure: 15 bytes +- Total: 20 bytes + +A 512-byte limit allows approximately 500 characters of message content. + +## Security Scanning + +GoChat uses automated security scanning tools in the CI/CD pipeline and for local development. + +### Vulnerability Scanning + +**govulncheck** - Official Go vulnerability database scanner + +- Scans all dependencies for known CVEs +- Runs on every commit and pull request +- Checks against the Go vulnerability database +- Zero-configuration security monitoring + +```bash +# Run locally +govulncheck ./... +``` + +### Static Security Analysis + +**gosec** - Security-focused Go static analyzer + +- Checks for common security issues: + - SQL injection vulnerabilities + - Command injection risks + - Unsafe use of cryptography + - File permission issues + - Hardcoded credentials + - And more... + +```bash +# Run locally +gosec ./... +``` + +### Running All Security Scans + +```bash +# Run comprehensive security scan +make security-scan + +# This runs: +# - govulncheck for dependency vulnerabilities +# - gosec for code security issues +``` + +### Dependency Auditing + +- **Regular updates:** Dependencies are updated frequently +- **Security patches:** Critical vulnerabilities are patched immediately +- **License compliance:** All dependencies use permissive licenses +- **Minimal dependencies:** Only essential packages are used + +### CI/CD Security Pipeline + +Every commit and pull request runs: + +1. **govulncheck** - Dependency vulnerability scan +2. **gosec** - Static security analysis +3. **Dependency check** - License and update verification +4. **Test suite** - Including security-focused tests + +## Security Best Practices + +### Production Deployment + +1. **Always use TLS/WSS** + + - Never use plain WS in production + - Encrypt all traffic between clients and server + - See [Deployment Guide](DEPLOYMENT.md) + +2. **Run behind a reverse proxy** + + - Add additional security layers + - Implement rate limiting at proxy level + - Filter malicious requests + +3. **Configure allowed origins** + + - Never use `*` (allow all) in production + - List all legitimate frontend domains + - Include all subdomains and variants + +4. **Monitor and log** + + - Track failed connection attempts + - Monitor rate limit violations + - Set up alerts for unusual patterns + +5. **Keep dependencies updated** + - Run `govulncheck` regularly + - Update Go to the latest version + - Patch vulnerabilities promptly + +### Network Security + +1. **Firewall configuration** + + - Only expose necessary ports (443 for WSS) + - Block direct access to the Go server + - Use reverse proxy as the public endpoint + +2. **DDoS protection** + + - Use a CDN or DDoS protection service + - Implement connection limits + - Rate limit at multiple layers + +3. **IP whitelisting** (if applicable) + - Restrict access to known IP ranges + - Implement at firewall or reverse proxy level + +### Application Security + +1. **Input validation** + + - Validate all message content + - Sanitize data before processing + - Reject malformed JSON + +2. **Error handling** + + - Don't expose internal errors to clients + - Log errors securely + - Avoid information leakage + +3. **Authentication** (future consideration) + - Currently, GoChat has no built-in authentication + - Implement authentication at the application level + - Consider JWT tokens or session-based auth + +## Reporting Security Issues + +If you discover a security vulnerability in GoChat, please follow responsible disclosure practices: + +### Do Not + +- Open a public GitHub issue +- Disclose the vulnerability publicly +- Exploit the vulnerability + +### Do + +1. **Contact maintainers directly** + + - Email: (Add contact email) + - Private message on GitHub + +2. **Provide details** + + - Description of the vulnerability + - Steps to reproduce + - Potential impact + - Suggested fix (if known) + +3. **Allow time for a fix** + - Give maintainers reasonable time to address the issue + - Coordinate disclosure timeline + - Receive credit in security advisory (if desired) + +### Security Response + +- **Acknowledgment:** Within 48 hours +- **Initial assessment:** Within 7 days +- **Fix timeline:** Depends on severity + - Critical: 1-7 days + - High: 1-2 weeks + - Medium: 2-4 weeks + - Low: Next release cycle + +## Security Checklist for Production + +- [ ] TLS/WSS enabled with valid certificate +- [ ] Allowed origins configured (no `*`) +- [ ] Running behind reverse proxy +- [ ] Rate limits configured appropriately +- [ ] Message size limits set +- [ ] Security scanning in CI/CD +- [ ] Dependencies up to date +- [ ] Firewall rules configured +- [ ] Monitoring and logging enabled +- [ ] Incident response plan in place + +## Related Documentation + +- [Getting Started](GETTING_STARTED.md) - Installation and setup +- [API Documentation](API.md) - WebSocket API details +- [Deployment Guide](DEPLOYMENT.md) - Production deployment with TLS +- [Development Guide](DEVELOPMENT.md) - Security testing and development diff --git a/internal/server/client.go b/internal/server/client.go index 202017e..b50e356 100644 --- a/internal/server/client.go +++ b/internal/server/client.go @@ -56,6 +56,9 @@ func (c *Client) GetSendChan() <-chan []byte { // setupReadConnection configures read deadlines and pong handler for the WebSocket connection func (c *Client) setupReadConnection() { + if c.conn == nil { + return + } if err := c.conn.SetReadDeadline(time.Now().Add(60 * time.Second)); err != nil { log.Printf("Error setting initial read deadline for %s: %v", c.addr, err) } @@ -139,31 +142,43 @@ func (c *Client) processMessage(rawMessage []byte) bool { return true } -func (c *Client) readPump() { - defer func() { - c.hub.unregister <- c +// cleanupReadPump handles cleanup tasks when readPump exits +func (c *Client) cleanupReadPump() { + c.hub.unregister <- c + if c.conn != nil { if err := c.conn.Close(); err != nil { if !isExpectedCloseError(err) { log.Printf("Error closing connection in readPump: %v", err) } } - }() + } +} - c.setupReadConnection() +// handleReadMessage processes a single message read from the WebSocket +func (c *Client) handleReadMessage() bool { + _, rawMessage, err := c.conn.ReadMessage() + if err != nil { + return c.handleReadError(err) + } - for { - _, rawMessage, err := c.conn.ReadMessage() - if err != nil { - if c.handleReadError(err) { - break - } - } + if !c.checkRateLimit() { + return false + } - if !c.checkRateLimit() { - continue - } + c.processMessage(rawMessage) + return false +} - c.processMessage(rawMessage) +func (c *Client) readPump() { + defer c.cleanupReadPump() + + if c.conn == nil { + return + } + + c.setupReadConnection() + + for !c.handleReadMessage() { //nolint:revive // empty body is intentional } } @@ -191,6 +206,9 @@ func (c *Client) processWriteEvent(ticker *time.Ticker) bool { // closeConnection safely closes the WebSocket connection with proper error handling func (c *Client) closeConnection() { + if c.conn == nil { + return + } if err := c.conn.Close(); err != nil { // Only log unexpected connection close errors if !isExpectedCloseError(err) { @@ -201,6 +219,9 @@ func (c *Client) closeConnection() { // handleMessage processes outgoing messages and returns false if the connection should be closed func (c *Client) handleMessage(message []byte, ok bool) bool { + if c.conn == nil { + return false + } if err := c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { log.Printf("Error setting write deadline for %s: %v", c.addr, err) return false @@ -215,6 +236,9 @@ func (c *Client) handleMessage(message []byte, ok bool) bool { // writeCloseMessage sends a close message to the client func (c *Client) writeCloseMessage() bool { + if c.conn == nil { + return false + } if err := c.conn.WriteMessage(websocket.CloseMessage, []byte{}); err != nil { if !isExpectedCloseError(err) { log.Printf("Error writing close message to %s: %v", c.addr, err) @@ -225,6 +249,9 @@ func (c *Client) writeCloseMessage() bool { // writeTextMessage writes a text message and any queued messages func (c *Client) writeTextMessage(message []byte) bool { + if c.conn == nil { + return false + } w, err := c.conn.NextWriter(websocket.TextMessage) if err != nil { log.Printf("Error creating writer for %s: %v", c.addr, err) @@ -286,6 +313,9 @@ func (c *Client) closeWriter(w io.WriteCloser) bool { // handlePing sends a ping message to keep the connection alive func (c *Client) handlePing() bool { + if c.conn == nil { + return false + } if err := c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { log.Printf("Error setting write deadline for ping to %s: %v", c.addr, err) return false diff --git a/internal/server/config.go b/internal/server/config.go index be59c74..69fba1c 100644 --- a/internal/server/config.go +++ b/internal/server/config.go @@ -3,6 +3,9 @@ package server import ( + "os" + "strconv" + "strings" "sync" "time" ) @@ -113,3 +116,65 @@ func NewConfig() *Config { cfg := defaultConfig() return &cfg } + +// NewConfigFromEnv creates a Config instance from environment variables. +// Falls back to default values if environment variables are not set. +func NewConfigFromEnv() *Config { + cfg := defaultConfig() + + // Load SERVER_PORT + if port := os.Getenv("SERVER_PORT"); port != "" { + cfg.Port = port + } + + // Load ALLOWED_ORIGINS + if origins := os.Getenv("ALLOWED_ORIGINS"); origins != "" { + cfg.AllowedOrigins = parseOrigins(origins) + } + + // Load MAX_MESSAGE_SIZE + if maxSize := os.Getenv("MAX_MESSAGE_SIZE"); maxSize != "" { + cfg.MaxMessageSize = parseMaxMessageSize(maxSize, cfg.MaxMessageSize) + } + + // Load RATE_LIMIT_BURST + if burst := os.Getenv("RATE_LIMIT_BURST"); burst != "" { + cfg.RateLimit.Burst = parseIntValue(burst, cfg.RateLimit.Burst) + } + + // Load RATE_LIMIT_REFILL_INTERVAL + if interval := os.Getenv("RATE_LIMIT_REFILL_INTERVAL"); interval != "" { + cfg.RateLimit.RefillInterval = parseRefillInterval(interval, cfg.RateLimit.RefillInterval) + } + + return &cfg +} + +func parseOrigins(origins string) []string { + parts := strings.Split(origins, ",") + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + return parts +} + +func parseMaxMessageSize(value string, defaultValue int64) int64 { + if size, err := strconv.ParseInt(value, 10, 64); err == nil && size > 0 { + return size + } + return defaultValue +} + +func parseIntValue(value string, defaultValue int) int { + if parsed, err := strconv.Atoi(value); err == nil && parsed > 0 { + return parsed + } + return defaultValue +} + +func parseRefillInterval(value string, defaultValue time.Duration) time.Duration { + if seconds, err := strconv.Atoi(value); err == nil && seconds > 0 { + return time.Duration(seconds) * time.Second + } + return defaultValue +} diff --git a/internal/server/hub.go b/internal/server/hub.go index 4d3a5cf..bfa3949 100644 --- a/internal/server/hub.go +++ b/internal/server/hub.go @@ -228,9 +228,11 @@ func (h *Hub) shutdownClients() { // Close all client connections for _, client := range clients { - if err := client.conn.Close(); err != nil { - if !isExpectedCloseError(err) { - log.Printf("Error closing client connection from %s: %v", client.addr, err) + if client.conn != nil { + if err := client.conn.Close(); err != nil { + if !isExpectedCloseError(err) { + log.Printf("Error closing client connection from %s: %v", client.addr, err) + } } } } diff --git a/test/integration/multiclient_test.go b/test/integration/multiclient_test.go new file mode 100644 index 0000000..93365c1 --- /dev/null +++ b/test/integration/multiclient_test.go @@ -0,0 +1,661 @@ +// Package integration contains integration tests for multi-client scenarios. +// +// These tests verify the system behavior when multiple clients connect +// simultaneously, send messages, and interact with each other through +// the hub's broadcast system. +package integration + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/Tyrowin/gochat/internal/server" + "github.com/gorilla/websocket" +) + +const ( + msgAfterNewClientJoined = "After new client joined" + msgFromClientTemplate = "Message from client %d" + msgInitial = "Initial message" +) + +// TestMultipleClientsMessageExchange tests complex message exchange scenarios +// between multiple clients connected to the hub. +func TestMultipleClientsMessageExchange(t *testing.T) { + server.StartHub() + + mux := server.SetupRoutes() + testServer := httptest.NewServer(mux) + defer testServer.Close() + configureServerForTest(t, testServer.URL, nil) + + wsURL := buildWebSocketURL(t, testServer.URL) + + t.Run("Five clients sending and receiving messages", func(t *testing.T) { + testFiveClientsSendingAndReceiving(t, wsURL, testServer.URL) + }) + + t.Run("Clients joining and leaving dynamically", func(t *testing.T) { + testDynamicJoiningAndLeaving(t, wsURL, testServer.URL) + }) + + t.Run("Rapid message exchange between clients", func(t *testing.T) { + testRapidMessageExchange(t, wsURL, testServer.URL) + }) +} + +// TestMultipleClientsConcurrentOperations tests concurrent operations with multiple clients. +func TestMultipleClientsConcurrentOperations(t *testing.T) { + server.StartHub() + + mux := server.SetupRoutes() + testServer := httptest.NewServer(mux) + defer testServer.Close() + configureServerForTest(t, testServer.URL, nil) + + wsURL := buildWebSocketURL(t, testServer.URL) + + t.Run("Concurrent client connections and disconnections", func(t *testing.T) { + testConcurrentConnectionsAndDisconnections(t, wsURL, testServer.URL) + }) + + t.Run("Concurrent message sending from multiple clients", func(t *testing.T) { + testConcurrentMessageSending(t, wsURL, testServer.URL) + }) +} + +// TestMultipleClientsEdgeCases tests edge cases with multiple clients. +func TestMultipleClientsEdgeCases(t *testing.T) { + server.StartHub() + + mux := server.SetupRoutes() + testServer := httptest.NewServer(mux) + defer testServer.Close() + configureServerForTest(t, testServer.URL, nil) + + wsURL := buildWebSocketURL(t, testServer.URL) + + t.Run("Single client broadcasting to itself", func(t *testing.T) { + connections := connectMultipleClients(t, wsURL, testServer.URL, 1) + defer closeAllConnections(t, connections) + time.Sleep(50 * time.Millisecond) + + // Send a message (should not receive it back) + sendMessageFromClient(t, connections[0], "Self message") + expectNoMessage(t, connections[0], 300*time.Millisecond) + }) + + t.Run("All clients disconnecting simultaneously", func(t *testing.T) { + const numClients = 5 + connections := connectMultipleClients(t, wsURL, testServer.URL, numClients) + time.Sleep(50 * time.Millisecond) + + var wg sync.WaitGroup + wg.Add(numClients) + + for i := 0; i < numClients; i++ { + go func(clientID int) { + defer wg.Done() + if err := connections[clientID].Close(); err != nil { + t.Logf("Client %d close error: %v", clientID, err) + } + }(i) + } + + wg.Wait() + time.Sleep(100 * time.Millisecond) + }) + + t.Run("Client sending empty content messages", func(t *testing.T) { + connections := connectMultipleClients(t, wsURL, testServer.URL, 2) + defer closeAllConnections(t, connections) + time.Sleep(50 * time.Millisecond) + + // Send message with empty content + sendMessageFromClient(t, connections[0], "") + + // Client 1 should receive it + verifyClientReceivesMessage(t, connections[1], "", 1) + expectNoMessage(t, connections[0], 150*time.Millisecond) + }) + + t.Run("Clients sending very long content", func(t *testing.T) { + connections := connectMultipleClients(t, wsURL, testServer.URL, 2) + defer closeAllConnections(t, connections) + time.Sleep(50 * time.Millisecond) + + // Send a long message (but within size limit) + longContent := "" + for i := 0; i < 50; i++ { + longContent += "X" + } + + sendMessageFromClient(t, connections[0], longContent) + verifyClientReceivesMessage(t, connections[1], longContent, 1) + expectNoMessage(t, connections[0], 150*time.Millisecond) + }) +} + +// drainMessages reads and discards all available messages from a connection +func drainMessages(conn *websocket.Conn, timeout time.Duration) { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if err := conn.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil { + break + } + _, _, err := conn.ReadMessage() + if err != nil { + break + } + } +} + +// testFiveClientsSendingAndReceiving tests that five clients can send messages +// and all other clients receive them correctly. +func testFiveClientsSendingAndReceiving(t *testing.T, wsURL, serverURL string) { + const numClients = 5 + connections := connectMultipleClients(t, wsURL, serverURL, numClients) + defer closeAllConnections(t, connections) + + // Give clients time to register and start their read/write pumps + time.Sleep(200 * time.Millisecond) + + // Each client sends a unique message + sendMessagesFromAllClients(t, connections, numClients) + + // Wait for all messages to be delivered + time.Sleep(200 * time.Millisecond) + + // Verify each client received all messages except their own + verifyAllClientsReceivedMessages(t, connections, numClients) +} + +// sendMessagesFromAllClients sends one message from each client +func sendMessagesFromAllClients(t *testing.T, connections []*websocket.Conn, numClients int) { + for i := 0; i < numClients; i++ { + messageContent := fmt.Sprintf(msgFromClientTemplate, i) + sendMessageFromClient(t, connections[i], messageContent) + time.Sleep(100 * time.Millisecond) + } +} + +// verifyAllClientsReceivedMessages verifies each client received expected messages +func verifyAllClientsReceivedMessages(t *testing.T, connections []*websocket.Conn, numClients int) { + expectedMessagesPerClient := numClients - 1 + + for i := 0; i < numClients; i++ { + messagesReceived := readAllMessagesFromClient(t, connections[i], expectedMessagesPerClient, i) + verifyReceivedMessageCount(t, messagesReceived, expectedMessagesPerClient, i) + verifyDidNotReceiveOwnMessage(t, messagesReceived, i) + } +} + +// readAllMessagesFromClient reads all available messages for a client +func readAllMessagesFromClient(t *testing.T, conn *websocket.Conn, expectedCount, clientIndex int) map[string]bool { + messagesReceived := make(map[string]bool) + deadline := time.Now().Add(2 * time.Second) + + for len(messagesReceived) < expectedCount && time.Now().Before(deadline) { + messages := readSingleWebSocketMessage(t, conn, clientIndex) + if messages == nil { + break + } + for _, content := range messages { + messagesReceived[content] = true + } + } + + return messagesReceived +} + +// readSingleWebSocketMessage reads one WebSocket message and returns all contained messages +func readSingleWebSocketMessage(t *testing.T, conn *websocket.Conn, clientIndex int) []string { + if err := conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond)); err != nil { + t.Errorf("Client %d: Failed to set read deadline: %v", clientIndex, err) + return nil + } + + messageType, message, err := conn.ReadMessage() + if err != nil { + return nil + } + + if messageType != websocket.TextMessage { + return nil + } + + return parseMessageContent(message) +} + +// parseMessageContent parses batched messages separated by newlines +func parseMessageContent(message []byte) []string { + var contents []string + parts := bytes.Split(message, []byte("\n")) + + for _, part := range parts { + if len(part) == 0 { + continue + } + var msg server.Message + if err := json.Unmarshal(part, &msg); err == nil { + contents = append(contents, msg.Content) + } + } + + return contents +} + +// verifyReceivedMessageCount checks if the client received the expected number of messages +func verifyReceivedMessageCount(t *testing.T, messagesReceived map[string]bool, expected, clientIndex int) { + if len(messagesReceived) != expected { + t.Errorf("Client %d: Expected %d messages, got %d", clientIndex, expected, len(messagesReceived)) + } +} + +// verifyDidNotReceiveOwnMessage checks that a client didn't receive its own message +func verifyDidNotReceiveOwnMessage(t *testing.T, messagesReceived map[string]bool, clientIndex int) { + ownMessage := fmt.Sprintf(msgFromClientTemplate, clientIndex) + if messagesReceived[ownMessage] { + t.Errorf("Client %d received its own message", clientIndex) + } +} + +// testDynamicJoiningAndLeaving tests clients connecting and disconnecting +// dynamically while messages are being sent. +func testDynamicJoiningAndLeaving(t *testing.T, wsURL, serverURL string) { + // Start with 3 clients + connections := connectMultipleClients(t, wsURL, serverURL, 3) + time.Sleep(200 * time.Millisecond) // Wait for registration and pump startup + + // Client 0 sends a message + sendMessageFromClient(t, connections[0], msgInitial) + time.Sleep(150 * time.Millisecond) // Wait for broadcast + + // Verify clients 1 and 2 received the message + verifyClientReceivesMessage(t, connections[1], msgInitial, 1) + verifyClientReceivesMessage(t, connections[2], msgInitial, 2) + + // Client 1 disconnects + closeClientConnection(t, connections, 1) + time.Sleep(150 * time.Millisecond) // Wait for unregistration + + // Client 0 sends another message (only client 2 should receive) + sendMessageFromClient(t, connections[0], "After client 1 left") + time.Sleep(150 * time.Millisecond) // Wait for broadcast + + verifyClientReceivesMessage(t, connections[2], "After client 1 left", 2) + + // New client joins + newClient := connectNewClient(t, wsURL, serverURL) + defer func() { _ = newClient.Close() }() + time.Sleep(200 * time.Millisecond) // Wait for registration and pump startup + + // Client 2 sends a message (both client 0 and new client should receive) + sendMessageFromClient(t, connections[2], msgAfterNewClientJoined) + time.Sleep(300 * time.Millisecond) // Wait longer for broadcast + + // Use a more flexible verification that handles batched messages and retries + verifyClientReceivesMessageFlexible(t, connections[0], msgAfterNewClientJoined, 0) + verifyClientReceivesMessage(t, newClient, msgAfterNewClientJoined, 3) + expectNoMessage(t, connections[2], 200*time.Millisecond) + + // Clean up remaining connections + closeRemainingConnections(t, connections) +} + +// verifyClientReceivesMessageFlexible is a more flexible version that handles +// potential timing issues and batched messages +func verifyClientReceivesMessageFlexible(t *testing.T, conn *websocket.Conn, expectedContent string, clientIndex int) { + t.Helper() + + deadline := time.Now().Add(3 * time.Second) + + defer handlePanicDuringMessageRead(t, clientIndex) + + found := searchForMessageWithRetry(t, conn, expectedContent, clientIndex, deadline) + + if !found { + t.Errorf("Client %d: Expected content %q not found after 3 seconds", clientIndex, expectedContent) + } +} + +// handlePanicDuringMessageRead recovers from panics during WebSocket reads +func handlePanicDuringMessageRead(t *testing.T, clientIndex int) { + if r := recover(); r != nil { + t.Errorf("Client %d: Panic while reading message: %v", clientIndex, r) + } +} + +// searchForMessageWithRetry searches for expected message content with retry logic +func searchForMessageWithRetry(t *testing.T, conn *websocket.Conn, expectedContent string, clientIndex int, deadline time.Time) bool { + for time.Now().Before(deadline) { + message, err := readWebSocketMessageWithTimeout(t, conn, clientIndex) + if err != nil { + if isFatalWebSocketError(err) { + t.Errorf("Client %d: Connection closed while waiting for message: %v", clientIndex, err) + return false + } + // Timeout is OK, we'll try again + continue + } + + if message == nil { + continue + } + + if messageContainsExpectedContent(message, expectedContent) { + return true + } + } + return false +} + +// readWebSocketMessageWithTimeout reads a WebSocket message with a timeout +func readWebSocketMessageWithTimeout(t *testing.T, conn *websocket.Conn, clientIndex int) ([]byte, error) { + if err := conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond)); err != nil { + t.Errorf("Client %d: Failed to set read deadline: %v", clientIndex, err) + return nil, err + } + + messageType, message, err := conn.ReadMessage() + if err != nil { + return nil, err + } + + if messageType != websocket.TextMessage { + return nil, nil + } + + return message, nil +} + +// isFatalWebSocketError checks if the error is a fatal WebSocket connection error +func isFatalWebSocketError(err error) bool { + return websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) || + websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) +} + +// messageContainsExpectedContent checks if batched message contains expected content +func messageContainsExpectedContent(message []byte, expectedContent string) bool { + parts := bytes.Split(message, []byte("\n")) + + for _, part := range parts { + if len(part) == 0 { + continue + } + + var received server.Message + if err := json.Unmarshal(part, &received); err != nil { + continue + } + + if received.Content == expectedContent { + return true + } + } + + return false +} + +// testRapidMessageExchange tests multiple clients sending messages rapidly +// and verifies all messages are received correctly. +func testRapidMessageExchange(t *testing.T, wsURL, serverURL string) { + const numClients = 3 + connections := connectMultipleClients(t, wsURL, serverURL, numClients) + defer closeAllConnections(t, connections) + time.Sleep(200 * time.Millisecond) // Wait for registration and pump startup + + // Send multiple messages rapidly from each client + const messagesPerClient = 5 + sendRapidMessages(t, connections, messagesPerClient) + + // Give time for all broadcasts to complete + // With 3 clients and 5 messages each, we have 15 messages total + // Each message needs to be broadcast to 2 other clients + // Wait longer to ensure all messages are processed + time.Sleep(1500 * time.Millisecond) + + // Verify all clients received the expected number of messages (allow some tolerance for timing) + expectedMessagesPerClient := messagesPerClient * (numClients - 1) + + for clientID := 0; clientID < numClients; clientID++ { + receivedCount := countReceivedMessages(t, connections[clientID], expectedMessagesPerClient) + + // Allow a small tolerance (e.g., at least 80% of messages should be received) + minExpected := int(float64(expectedMessagesPerClient) * 0.8) + + if receivedCount < minExpected { + t.Errorf("Client %d: expected at least %d messages (80%% of %d), got %d", + clientID, minExpected, expectedMessagesPerClient, receivedCount) + } else if receivedCount != expectedMessagesPerClient { + t.Logf("Client %d: received %d/%d messages (%.0f%%)", + clientID, receivedCount, expectedMessagesPerClient, + float64(receivedCount)/float64(expectedMessagesPerClient)*100) + } + } +} + +// sendRapidMessages sends multiple messages rapidly from each client. +func sendRapidMessages(t *testing.T, connections []*websocket.Conn, messagesPerClient int) { + numClients := len(connections) + for round := 0; round < messagesPerClient; round++ { + for clientID := 0; clientID < numClients; clientID++ { + content := fmt.Sprintf("Round %d from client %d", round, clientID) + sendMessageFromClient(t, connections[clientID], content) + } + // Delay between rounds to prevent overwhelming the hub + time.Sleep(50 * time.Millisecond) + } +} + +// countReceivedMessages counts how many valid messages a client receives +// within a timeout period. Handles batched messages separated by newlines. +func countReceivedMessages(t *testing.T, conn *websocket.Conn, maxExpected int) int { + receivedCount := 0 + deadline := time.Now().Add(5 * time.Second) + + for receivedCount < maxExpected && time.Now().Before(deadline) { + message, err := readSingleMessageWithDeadline(t, conn) + if err != nil { + break + } + + if message != nil { + receivedCount += countMessagesInBatch(message) + } + } + + return receivedCount +} + +// readSingleMessageWithDeadline reads a single WebSocket message with a deadline +func readSingleMessageWithDeadline(t *testing.T, conn *websocket.Conn) ([]byte, error) { + if err := conn.SetReadDeadline(time.Now().Add(1 * time.Second)); err != nil { + t.Logf("Failed to set read deadline: %v", err) + return nil, err + } + + messageType, message, err := conn.ReadMessage() + if err != nil { + return nil, err + } + + if messageType != websocket.TextMessage { + return nil, nil + } + + return message, nil +} + +// countMessagesInBatch counts valid messages in a batched message payload +func countMessagesInBatch(message []byte) int { + count := 0 + parts := bytes.Split(message, []byte("\n")) + + for _, part := range parts { + if len(part) == 0 { + continue + } + + var msg server.Message + if err := json.Unmarshal(part, &msg); err == nil { + count++ + } + } + + return count +} + +// closeClientConnection safely closes a client connection at the given index. +func closeClientConnection(t *testing.T, connections []*websocket.Conn, index int) { + if err := connections[index].Close(); err != nil { + t.Errorf("Failed to close client %d: %v", index, err) + } + connections[index] = nil +} + +// closeRemainingConnections closes all non-nil connections in the slice. +func closeRemainingConnections(t *testing.T, connections []*websocket.Conn) { + for i, conn := range connections { + if conn != nil { + if err := conn.Close(); err != nil { + t.Logf("Failed to close connection %d: %v", i, err) + } + } + } +} + +// connectNewClient establishes a new WebSocket connection and returns it. +func connectNewClient(t *testing.T, wsURL, serverURL string) *websocket.Conn { + newClient, resp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + t.Fatalf("Failed to connect new client: %v", err) + } + _ = resp.Body.Close() + return newClient +} + +// testConcurrentConnectionsAndDisconnections tests multiple clients connecting +// and disconnecting concurrently. +func testConcurrentConnectionsAndDisconnections(t *testing.T, wsURL, serverURL string) { + const numClients = 10 + var wg sync.WaitGroup + errors := make(chan error, numClients) + + wg.Add(numClients) + for i := 0; i < numClients; i++ { + go runSingleConcurrentClient(t, wsURL, serverURL, i, &wg, errors) + } + + wg.Wait() + close(errors) + + reportErrors(t, errors) +} + +// runSingleConcurrentClient connects a single client, sends a message, reads responses, +// and disconnects. +func runSingleConcurrentClient(t *testing.T, wsURL, serverURL string, clientID int, wg *sync.WaitGroup, errors chan<- error) { + defer wg.Done() + + conn, resp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + errors <- fmt.Errorf("client %d: connection failed: %w", clientID, err) + return + } + defer func() { _ = conn.Close() }() + defer func() { _ = resp.Body.Close() }() + + // Send a message + content := fmt.Sprintf(msgFromClientTemplate, clientID) + if err := conn.WriteMessage(websocket.TextMessage, mustMarshalMessage(t, content)); err != nil { + errors <- fmt.Errorf("client %d: send failed: %w", clientID, err) + return + } + + // Try to read some messages (may or may not receive) + attemptToReadMessages(conn, 500*time.Millisecond) +} + +// attemptToReadMessages attempts to read messages from a connection +// within the specified timeout period. +func attemptToReadMessages(conn *websocket.Conn, timeout time.Duration) { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if err := conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil { + break + } + _, _, err := conn.ReadMessage() + if err != nil { + break + } + } +} + +// testConcurrentMessageSending tests multiple clients sending messages concurrently. +func testConcurrentMessageSending(t *testing.T, wsURL, serverURL string) { + const numClients = 5 + connections := connectMultipleClients(t, wsURL, serverURL, numClients) + defer closeAllConnections(t, connections) + time.Sleep(100 * time.Millisecond) + + errors := sendMessagesFromAllClientsConcurrently(t, connections) + reportErrors(t, errors) + + // Drain messages from all clients + drainAllClientMessages(connections) +} + +// sendMessagesFromAllClientsConcurrently sends multiple messages from each client +// concurrently and returns any errors that occurred. +func sendMessagesFromAllClientsConcurrently(t *testing.T, connections []*websocket.Conn) chan error { + const messagesPerClient = 10 + numClients := len(connections) + + var wg sync.WaitGroup + errors := make(chan error, numClients*messagesPerClient) + + // Each client sends 10 messages concurrently + for i := 0; i < numClients; i++ { + wg.Add(1) + go sendMultipleMessagesFromClient(t, connections[i], i, messagesPerClient, &wg, errors) + } + + wg.Wait() + close(errors) + + return errors +} + +// sendMultipleMessagesFromClient sends multiple messages from a single client. +func sendMultipleMessagesFromClient(t *testing.T, conn *websocket.Conn, clientID, numMessages int, wg *sync.WaitGroup, errors chan<- error) { + defer wg.Done() + + for msgNum := 0; msgNum < numMessages; msgNum++ { + content := fmt.Sprintf("Client %d message %d", clientID, msgNum) + if err := conn.WriteMessage(websocket.TextMessage, mustMarshalMessage(t, content)); err != nil { + errors <- fmt.Errorf("client %d msg %d: send failed: %w", clientID, msgNum, err) + } + time.Sleep(10 * time.Millisecond) // Small delay between messages + } +} + +// drainAllClientMessages drains messages from all client connections. +func drainAllClientMessages(connections []*websocket.Conn) { + time.Sleep(500 * time.Millisecond) + for i := 0; i < len(connections); i++ { + drainMessages(connections[i], 1*time.Second) + } +} + +// reportErrors reports all errors from the error channel to the test. +func reportErrors(t *testing.T, errors <-chan error) { + for err := range errors { + t.Error(err) + } +} diff --git a/test/integration/security_test.go b/test/integration/security_test.go new file mode 100644 index 0000000..0edb2c6 --- /dev/null +++ b/test/integration/security_test.go @@ -0,0 +1,605 @@ +// Package integration contains security-focused integration tests. +// +// These tests verify that the security constraints are properly enforced, +// including origin validation, message size limits, and rate limiting. +package integration + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/Tyrowin/gochat/internal/server" + "github.com/gorilla/websocket" +) + +const ( + exampleOriginHTTP = "http://example.com" + + // Error message constants + errFailedConnectSender = "Failed to connect sender: %v" + errFailedConnectReceiver = "Failed to connect receiver: %v" + errFailedSetReadDeadline = "Failed to set read deadline: %v" + errExpectedTextMessage = "Expected text message, got type %d" + errFailedUnmarshal = "Failed to unmarshal message: %v" + errFailedSendMessage = "Failed to send message %d: %v" + errFailedReceiveMessage = "Failed to receive message %d: %v" +) + +// Helper function to assert connection should fail with forbidden status +func assertConnectionFails(t *testing.T, wsURL string, header http.Header, errorMsg string) { + t.Helper() + conn, resp, err := websocket.DefaultDialer.Dial(wsURL, header) + if err == nil { + _ = conn.Close() + _ = resp.Body.Close() + t.Fatal(errorMsg) + } + if resp != nil { + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusForbidden { + t.Errorf("Expected status %d, got %d", http.StatusForbidden, resp.StatusCode) + } + } +} + +// Helper function to assert connection succeeds +func assertConnectionSucceeds(t *testing.T, wsURL string, header http.Header, origin string) { + t.Helper() + conn, resp, err := websocket.DefaultDialer.Dial(wsURL, header) + if err != nil { + t.Errorf("Expected origin %q to be allowed: %v", origin, err) + return + } + _ = conn.Close() + if resp != nil { + _ = resp.Body.Close() + } +} + +// Helper function to test missing origin header +func testMissingOriginHeader(t *testing.T, wsURL, serverURL string) { + t.Helper() + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.AllowedOrigins = []string{serverURL} + }) + + header := http.Header{} + assertConnectionFails(t, wsURL, header, "Expected connection to fail with missing origin") +} + +// Helper function to test empty origin header +func testEmptyOriginHeader(t *testing.T, wsURL, serverURL string) { + t.Helper() + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.AllowedOrigins = []string{serverURL} + }) + + header := http.Header{} + header.Set("Origin", "") + assertConnectionFails(t, wsURL, header, "Expected connection to fail with empty origin") +} + +// Helper function to test malformed origins +func testMalformedOrigins(t *testing.T, wsURL, serverURL string) { + t.Helper() + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.AllowedOrigins = []string{serverURL} + }) + + malformedOrigins := []string{ + "not-a-url", + "://missing-scheme", + "http://", + "ftp://unsupported-scheme.com", + "javascript:alert(1)", + } + + for _, origin := range malformedOrigins { + header := http.Header{} + header.Set("Origin", origin) + conn, resp, err := websocket.DefaultDialer.Dial(wsURL, header) + if err == nil { + _ = conn.Close() + _ = resp.Body.Close() + t.Errorf("Expected connection to fail with malformed origin %q", origin) + } + if resp != nil { + _ = resp.Body.Close() + } + } +} + +// Helper function to test case sensitivity +func testCaseSensitivity(t *testing.T, wsURL, serverURL string) { + t.Helper() + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.AllowedOrigins = []string{exampleOriginHTTP} + }) + + caseVariations := []string{ + "http://EXAMPLE.COM", + "http://Example.Com", + "HTTP://example.com", + } + + for _, origin := range caseVariations { + header := http.Header{} + header.Set("Origin", origin) + assertConnectionSucceeds(t, wsURL, header, origin) + } +} + +// Helper function to test wildcard origin +func testWildcardOrigin(t *testing.T, wsURL, serverURL string) { + t.Helper() + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.AllowedOrigins = []string{"*"} + }) + + testOrigins := []string{ + exampleOriginHTTP, + "https://another.com", + "http://localhost:3000", + } + + for _, origin := range testOrigins { + header := http.Header{} + header.Set("Origin", origin) + assertConnectionSucceeds(t, wsURL, header, origin) + } +} + +// Helper function to test different port rejection +func testDifferentPort(t *testing.T, wsURL, serverURL string) { + t.Helper() + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.AllowedOrigins = []string{"http://localhost:8080"} + }) + + header := http.Header{} + header.Set("Origin", "http://localhost:9090") + assertConnectionFails(t, wsURL, header, "Expected connection to fail with different port") +} + +// Helper function to test path component handling +func testPathComponentIgnored(t *testing.T, wsURL, serverURL string) { + t.Helper() + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.AllowedOrigins = []string{exampleOriginHTTP} + }) + + header := http.Header{} + header.Set("Origin", "http://example.com/some/path") + conn, resp, err := websocket.DefaultDialer.Dial(wsURL, header) + if err != nil { + t.Errorf("Expected origin with path to be allowed: %v", err) + return + } + _ = conn.Close() + if resp != nil { + _ = resp.Body.Close() + } +} + +// Helper function to test HTTP vs HTTPS scheme difference +func testSchemeDifference(t *testing.T, wsURL, serverURL string) { + t.Helper() + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.AllowedOrigins = []string{exampleOriginHTTP} + }) + + header := http.Header{} + header.Set("Origin", "https://example.com") + assertConnectionFails(t, wsURL, header, "Expected HTTPS origin to be rejected when only HTTP is allowed") +} + +// TestOriginValidationEdgeCases tests various edge cases for origin validation. +func TestOriginValidationEdgeCases(t *testing.T) { + server.StartHub() + + mux := server.SetupRoutes() + testServer := httptest.NewServer(mux) + defer testServer.Close() + + wsURL := buildWebSocketURL(t, testServer.URL) + + t.Run("Missing Origin header", func(t *testing.T) { + testMissingOriginHeader(t, wsURL, testServer.URL) + }) + + t.Run("Empty Origin header", func(t *testing.T) { + testEmptyOriginHeader(t, wsURL, testServer.URL) + }) + + t.Run("Malformed Origin URL", func(t *testing.T) { + testMalformedOrigins(t, wsURL, testServer.URL) + }) + + t.Run("Case sensitivity in origin matching", func(t *testing.T) { + testCaseSensitivity(t, wsURL, testServer.URL) + }) + + t.Run("Wildcard origin configuration", func(t *testing.T) { + testWildcardOrigin(t, wsURL, testServer.URL) + }) + + t.Run("Origin with different port", func(t *testing.T) { + testDifferentPort(t, wsURL, testServer.URL) + }) + + t.Run("Origin with path component ignored", func(t *testing.T) { + testPathComponentIgnored(t, wsURL, testServer.URL) + }) + + t.Run("HTTP vs HTTPS scheme difference", func(t *testing.T) { + testSchemeDifference(t, wsURL, testServer.URL) + }) +} + +// Helper function to test message exactly at size limit +func testMessageAtSizeLimit(t *testing.T, wsURL, serverURL string) { + t.Helper() + const limit int64 = 100 + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.MaxMessageSize = limit + }) + + sender, senderResp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + t.Fatalf(errFailedConnectSender, err) + } + defer func() { _ = sender.Close() }() + defer func() { _ = senderResp.Body.Close() }() + + receiver, receiverResp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + t.Fatalf(errFailedConnectReceiver, err) + } + defer func() { _ = receiver.Close() }() + defer func() { _ = receiverResp.Body.Close() }() + + time.Sleep(50 * time.Millisecond) + + // Create a message that's exactly at the limit + // JSON overhead: {"content":""} = 14 bytes, so content needs to be limit - 14 + contentSize := int(limit) - 14 + if contentSize <= 0 { + t.Skip("Limit too small for test") + } + + content := strings.Repeat("A", contentSize) + payload := mustMarshalMessage(t, content) + + if int64(len(payload)) > limit { + t.Logf("Payload size %d exceeds limit %d, adjusting", len(payload), limit) + // Adjust content size + contentSize = int(limit) - len(payload) + len(content) + if contentSize <= 0 { + t.Skip("Cannot create exact-size message") + } + content = strings.Repeat("A", contentSize) + payload = mustMarshalMessage(t, content) + } + + if err := sender.WriteMessage(websocket.TextMessage, payload); err != nil { + t.Fatalf("Failed to send at-limit message: %v", err) + } + + // Receiver should get the message + if err := receiver.SetReadDeadline(time.Now().Add(time.Second)); err != nil { + t.Fatalf(errFailedSetReadDeadline, err) + } + + messageType, message, err := receiver.ReadMessage() + if err != nil { + t.Fatalf("Expected to receive at-limit message: %v", err) + } + + if messageType != websocket.TextMessage { + t.Errorf(errExpectedTextMessage, messageType) + } + + var received server.Message + if err := json.Unmarshal(message, &received); err != nil { + t.Errorf(errFailedUnmarshal, err) + } +} + +// Helper function to test message one byte over limit +func testMessageOneByteOverLimit(t *testing.T, wsURL, serverURL string) { + t.Helper() + const limit int64 = 100 + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.MaxMessageSize = limit + }) + + sender, senderResp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + t.Fatalf(errFailedConnectSender, err) + } + defer func() { _ = sender.Close() }() + defer func() { _ = senderResp.Body.Close() }() + + receiver, receiverResp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + t.Fatalf(errFailedConnectReceiver, err) + } + defer func() { _ = receiver.Close() }() + defer func() { _ = receiverResp.Body.Close() }() + + time.Sleep(50 * time.Millisecond) + + // Create message that exceeds limit by 1 byte + oversizedContent := strings.Repeat("A", int(limit)+1) + oversizedPayload := mustMarshalMessage(t, oversizedContent) + + if err := sender.WriteMessage(websocket.TextMessage, oversizedPayload); err != nil && !websocket.IsCloseError(err, websocket.CloseMessageTooBig) { + t.Logf("Send error (expected): %v", err) + } + + expectNoMessage(t, receiver, 300*time.Millisecond) +} + +// Helper function to test very large message well over limit +func testVeryLargeMessage(t *testing.T, wsURL, serverURL string) { + t.Helper() + const limit int64 = 64 + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.MaxMessageSize = limit + }) + + sender, senderResp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + t.Fatalf(errFailedConnectSender, err) + } + defer func() { _ = sender.Close() }() + defer func() { _ = senderResp.Body.Close() }() + + receiver, receiverResp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + t.Fatalf(errFailedConnectReceiver, err) + } + defer func() { _ = receiver.Close() }() + defer func() { _ = receiverResp.Body.Close() }() + + time.Sleep(50 * time.Millisecond) + + // Create a very large message + hugeContent := strings.Repeat("X", int(limit)*10) + hugePayload := mustMarshalMessage(t, hugeContent) + + if err := sender.WriteMessage(websocket.TextMessage, hugePayload); err != nil { + t.Logf("Expected error sending huge message: %v", err) + } + + expectNoMessage(t, receiver, 300*time.Millisecond) + + // Verify sender connection is closed + if err := sender.SetReadDeadline(time.Now().Add(300 * time.Millisecond)); err != nil { + t.Logf("Set deadline error: %v", err) + } + if _, _, readErr := sender.ReadMessage(); readErr == nil { + t.Error("Expected sender connection to be closed") + } +} + +// Helper function to test multiple small messages within limit +func testMultipleSmallMessages(t *testing.T, wsURL, serverURL string) { + t.Helper() + const limit int64 = 200 + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.MaxMessageSize = limit + }) + + sender, senderResp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + t.Fatalf(errFailedConnectSender, err) + } + defer func() { _ = sender.Close() }() + defer func() { _ = senderResp.Body.Close() }() + + receiver, receiverResp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + t.Fatalf(errFailedConnectReceiver, err) + } + defer func() { _ = receiver.Close() }() + defer func() { _ = receiverResp.Body.Close() }() + + time.Sleep(50 * time.Millisecond) + + // Send multiple small messages + for i := 0; i < 5; i++ { + content := strings.Repeat("A", 20) + if err := sender.WriteMessage(websocket.TextMessage, mustMarshalMessage(t, content)); err != nil { + t.Errorf(errFailedSendMessage, i, err) + } + + // Verify receiver gets it + if err := receiver.SetReadDeadline(time.Now().Add(time.Second)); err != nil { + t.Fatalf(errFailedSetReadDeadline, err) + } + + if _, _, err := receiver.ReadMessage(); err != nil { + t.Errorf(errFailedReceiveMessage, i, err) + } + } +} + +// Helper function to test zero-length message +func testZeroLengthMessage(t *testing.T, wsURL, serverURL string) { + t.Helper() + const limit int64 = 100 + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.MaxMessageSize = limit + }) + + sender, senderResp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + t.Fatalf(errFailedConnectSender, err) + } + defer func() { _ = sender.Close() }() + defer func() { _ = senderResp.Body.Close() }() + + receiver, receiverResp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + t.Fatalf(errFailedConnectReceiver, err) + } + defer func() { _ = receiver.Close() }() + defer func() { _ = receiverResp.Body.Close() }() + + time.Sleep(50 * time.Millisecond) + + // Send message with empty content + if err := sender.WriteMessage(websocket.TextMessage, mustMarshalMessage(t, "")); err != nil { + t.Errorf("Failed to send zero-length message: %v", err) + } + + // Receiver should get it + if err := receiver.SetReadDeadline(time.Now().Add(time.Second)); err != nil { + t.Fatalf(errFailedSetReadDeadline, err) + } + + messageType, message, err := receiver.ReadMessage() + if err != nil { + t.Errorf("Failed to receive zero-length message: %v", err) + } + + if messageType != websocket.TextMessage { + t.Errorf(errExpectedTextMessage, messageType) + } + + var received server.Message + if err := json.Unmarshal(message, &received); err != nil { + t.Errorf(errFailedUnmarshal, err) + } + + if received.Content != "" { + t.Errorf("Expected empty content, got %q", received.Content) + } +} + +// TestMessageSizeLimitEdgeCases tests various edge cases for message size validation. +func TestMessageSizeLimitEdgeCases(t *testing.T) { + server.StartHub() + + mux := server.SetupRoutes() + testServer := httptest.NewServer(mux) + defer testServer.Close() + + wsURL := buildWebSocketURL(t, testServer.URL) + + t.Run("Message exactly at size limit", func(t *testing.T) { + testMessageAtSizeLimit(t, wsURL, testServer.URL) + }) + + t.Run("Message one byte over limit", func(t *testing.T) { + testMessageOneByteOverLimit(t, wsURL, testServer.URL) + }) + + t.Run("Very large message well over limit", func(t *testing.T) { + testVeryLargeMessage(t, wsURL, testServer.URL) + }) + + t.Run("Multiple small messages within limit", func(t *testing.T) { + testMultipleSmallMessages(t, wsURL, testServer.URL) + }) + + t.Run("Zero-length message", func(t *testing.T) { + testZeroLengthMessage(t, wsURL, testServer.URL) + }) +} + +// Helper function to test invalid origin with oversized message +func testInvalidOriginWithOversizedMessage(t *testing.T, wsURL, serverURL string) { + t.Helper() + const limit int64 = 64 + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.AllowedOrigins = []string{"http://allowed.com"} + cfg.MaxMessageSize = limit + }) + + header := http.Header{} + header.Set("Origin", "http://blocked.com") + conn, resp, err := websocket.DefaultDialer.Dial(wsURL, header) + if err == nil { + _ = conn.Close() + _ = resp.Body.Close() + t.Fatal("Expected connection to fail with invalid origin") + } + if resp != nil { + _ = resp.Body.Close() + } +} + +// Helper function to test valid origin with message size and rate limits +func testValidOriginWithSizeAndRateLimits(t *testing.T, wsURL, serverURL string) { + t.Helper() + configureServerForTest(t, serverURL, func(cfg *server.Config) { + cfg.AllowedOrigins = []string{serverURL} + cfg.MaxMessageSize = 100 + cfg.RateLimit = server.RateLimitConfig{ + Burst: 3, + RefillInterval: 500 * time.Millisecond, + } + }) + + sender, senderResp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + t.Fatalf(errFailedConnectSender, err) + } + defer func() { _ = sender.Close() }() + defer func() { _ = senderResp.Body.Close() }() + + receiver, receiverResp, err := websocket.DefaultDialer.Dial(wsURL, newOriginHeader(serverURL)) + if err != nil { + t.Fatalf(errFailedConnectReceiver, err) + } + defer func() { _ = receiver.Close() }() + defer func() { _ = receiverResp.Body.Close() }() + + time.Sleep(50 * time.Millisecond) + + // Send messages up to rate limit + for i := 0; i < 3; i++ { + if err := sender.WriteMessage(websocket.TextMessage, mustMarshalMessage(t, "msg")); err != nil { + t.Errorf(errFailedSendMessage, i, err) + } + + if err := receiver.SetReadDeadline(time.Now().Add(time.Second)); err != nil { + t.Fatalf(errFailedSetReadDeadline, err) + } + + if _, _, err := receiver.ReadMessage(); err != nil { + t.Errorf(errFailedReceiveMessage, i, err) + } + } + + // Next message should be rate limited + if err := sender.WriteMessage(websocket.TextMessage, mustMarshalMessage(t, "over")); err != nil { + t.Logf("Send error: %v", err) + } + expectNoMessage(t, receiver, 200*time.Millisecond) +} + +// TestSecurityConstraintsCombined tests combinations of security constraints. +func TestSecurityConstraintsCombined(t *testing.T) { + server.StartHub() + + mux := server.SetupRoutes() + testServer := httptest.NewServer(mux) + defer testServer.Close() + + wsURL := buildWebSocketURL(t, testServer.URL) + + t.Run("Invalid origin with oversized message", func(t *testing.T) { + testInvalidOriginWithOversizedMessage(t, wsURL, testServer.URL) + }) + + t.Run("Valid origin with message size and rate limits", func(t *testing.T) { + testValidOriginWithSizeAndRateLimits(t, wsURL, testServer.URL) + }) +} diff --git a/test/integration/shutdown_test.go b/test/integration/shutdown_test.go index 21485c4..ae86583 100644 --- a/test/integration/shutdown_test.go +++ b/test/integration/shutdown_test.go @@ -224,7 +224,12 @@ func runMessageExchange(_ *testing.T, client1, client2 *websocket.Conn) (int, in time.Sleep(200 * time.Millisecond) close(stopReceiving) - return messagesSent, messagesReceived + // Read messagesReceived with mutex protection to avoid race condition + receiveMutex.Lock() + finalMessagesReceived := messagesReceived + receiveMutex.Unlock() + + return messagesSent, finalMessagesReceived } // receiveMessages continuously receives messages on a WebSocket connection diff --git a/test/integration/websocket_test.go b/test/integration/websocket_test.go index c57e8ae..88602b6 100644 --- a/test/integration/websocket_test.go +++ b/test/integration/websocket_test.go @@ -7,6 +7,7 @@ package integration import ( + "bytes" "context" "encoding/json" "fmt" @@ -242,7 +243,8 @@ func verifyMessageReceivedByOtherClients(t *testing.T, connections []*websocket. } } -// verifyClientReceivesMessage verifies a single client receives the expected message +// verifyClientReceivesMessage verifies a single client receives the expected message. +// Handles batched messages separated by newlines. func verifyClientReceivesMessage(t *testing.T, conn *websocket.Conn, expectedContent string, clientIndex int) { if err := conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil { t.Errorf("Failed to set read deadline for client %d: %v", clientIndex, err) @@ -257,16 +259,32 @@ func verifyClientReceivesMessage(t *testing.T, conn *websocket.Conn, expectedCon if messageType != websocket.TextMessage { t.Errorf("Client %d: Expected text message, got type %d", clientIndex, messageType) + return } - var received server.Message - if err := json.Unmarshal(message, &received); err != nil { - t.Errorf("Client %d: Failed to unmarshal message: %v", clientIndex, err) - return + // Handle batched messages - split by newline and check each part + parts := bytes.Split(message, []byte("\n")) + found := false + + for _, part := range parts { + if len(part) == 0 { + continue + } + + var received server.Message + if err := json.Unmarshal(part, &received); err != nil { + t.Errorf("Client %d: Failed to unmarshal message part: %v", clientIndex, err) + continue + } + + if received.Content == expectedContent { + found = true + break + } } - if received.Content != expectedContent { - t.Errorf("Client %d: Expected content %q, got %q", clientIndex, expectedContent, received.Content) + if !found { + t.Errorf("Client %d: Expected content %q not found in received message(s)", clientIndex, expectedContent) } } diff --git a/test/unit/hub_test.go b/test/unit/hub_test.go index 64b7054..b3f3da4 100644 --- a/test/unit/hub_test.go +++ b/test/unit/hub_test.go @@ -6,12 +6,15 @@ package unit import ( + "strconv" "testing" "time" "github.com/Tyrowin/gochat/internal/server" ) +const shutdownErrorMsg = "Failed to shutdown hub: %v" + // TestNewHub tests the hub creation function. // It verifies that NewHub returns a properly initialized Hub // with all necessary channels and data structures. @@ -94,13 +97,15 @@ func TestHubBroadcastChannel(t *testing.T) { time.Sleep(10 * time.Millisecond) } +const testClientAddr = "127.0.0.1:12345" + // TestNewClient tests the client creation function. // It verifies that NewClient returns a properly initialized Client // with all necessary fields and channels set up correctly. func TestNewClient(t *testing.T) { hub := server.NewHub() - client := server.NewClient(nil, hub, "127.0.0.1:12345") + client := server.NewClient(nil, hub, testClientAddr) if client == nil { t.Fatal("NewClient() returned nil") @@ -117,7 +122,7 @@ func TestNewClient(t *testing.T) { // and accessible through the GetSendChan method. func TestClientSendChannel(t *testing.T) { hub := server.NewHub() - client := server.NewClient(nil, hub, "127.0.0.1:12345") + client := server.NewClient(nil, hub, testClientAddr) sendChan := client.GetSendChan() @@ -164,3 +169,281 @@ func TestConcurrentHubOperations(t *testing.T) { } } } + +// TestHubClientRegistrationChannel tests the hub's client registration channel. +// It verifies that the registration channel accepts clients without blocking. +func TestHubClientRegistrationChannel(t *testing.T) { + hub := server.NewHub() + go hub.Run() + defer func() { + if err := hub.Shutdown(time.Second); err != nil { + t.Errorf(shutdownErrorMsg, err) + } + }() + time.Sleep(10 * time.Millisecond) + + t.Run("Nil client registration is ignored", func(t *testing.T) { + select { + case hub.GetRegisterChan() <- nil: + case <-time.After(100 * time.Millisecond): + t.Fatal("Failed to send nil client") + } + + // Should not panic or cause issues + time.Sleep(20 * time.Millisecond) + }) + + t.Run("Registration channel is non-blocking", func(t *testing.T) { + // Test that we can send to the registration channel + done := make(chan bool, 1) + + go func() { + // This would block if the hub isn't running + hub.GetRegisterChan() <- nil + done <- true + }() + + select { + case <-done: + // Success - channel accepted the value + case <-time.After(100 * time.Millisecond): + t.Error("Registration channel blocked") + } + + time.Sleep(20 * time.Millisecond) + }) +} + +// TestHubClientUnregistration tests the hub's client unregistration functionality. +// It verifies that unregistration requests are properly handled by the hub. +func TestHubClientUnregistration(t *testing.T) { + hub := server.NewHub() + go hub.Run() + defer func() { + if err := hub.Shutdown(time.Second); err != nil { + t.Errorf(shutdownErrorMsg, err) + } + }() + time.Sleep(10 * time.Millisecond) + + t.Run("Unregister channel is non-blocking", func(t *testing.T) { + done := make(chan bool, 1) + + go func() { + // Send a nil client (hub should handle it gracefully) + hub.GetUnregisterChan() <- nil + done <- true + }() + + select { + case <-done: + // Success + case <-time.After(100 * time.Millisecond): + t.Error("Unregistration channel blocked") + } + + time.Sleep(20 * time.Millisecond) + }) + + t.Run("Multiple concurrent unregistration requests", func(t *testing.T) { + done := make(chan bool, 5) + + for i := 0; i < 5; i++ { + go func(id int) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Unregistration goroutine %d panicked: %v", id, r) + } + done <- true + }() + + // Send nil - hub should handle gracefully + select { + case hub.GetUnregisterChan() <- nil: + case <-time.After(100 * time.Millisecond): + t.Errorf("Failed to send unregister request %d", id) + } + }(i) + } + + for i := 0; i < 5; i++ { + select { + case <-done: + case <-time.After(200 * time.Millisecond): + t.Error("Concurrent unregistration test timed out") + return + } + } + + time.Sleep(50 * time.Millisecond) + }) +} + +// TestHubBroadcastMessage tests the hub's broadcast functionality. +// It verifies that messages are properly broadcast to all clients except the sender. +func TestHubBroadcastMessage(t *testing.T) { + hub := server.NewHub() + go hub.Run() + defer func() { + if err := hub.Shutdown(time.Second); err != nil { + t.Errorf(shutdownErrorMsg, err) + } + }() + time.Sleep(10 * time.Millisecond) + + t.Run("Broadcast with nil sender", func(t *testing.T) { + testMsg := []byte(`{"content":"broadcast test"}`) + + select { + case hub.GetBroadcastChan() <- server.BroadcastMessage{Sender: nil, Payload: testMsg}: + case <-time.After(100 * time.Millisecond): + t.Fatal("Failed to send broadcast message") + } + + time.Sleep(20 * time.Millisecond) + }) + + t.Run("Broadcast with sender", func(t *testing.T) { + sender := server.NewClient(nil, hub, "127.0.0.1:12345") + testMsg := []byte(`{"content":"message from sender"}`) + + select { + case hub.GetBroadcastChan() <- server.BroadcastMessage{Sender: sender, Payload: testMsg}: + case <-time.After(100 * time.Millisecond): + t.Fatal("Failed to send broadcast message") + } + + time.Sleep(20 * time.Millisecond) + }) + + t.Run("Multiple concurrent broadcasts", func(t *testing.T) { + done := make(chan bool, 10) + + for i := 0; i < 10; i++ { + go func(id int) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Broadcast goroutine %d panicked: %v", id, r) + } + done <- true + }() + + msg := []byte(`{"content":"concurrent broadcast"}`) + select { + case hub.GetBroadcastChan() <- server.BroadcastMessage{Payload: msg}: + case <-time.After(100 * time.Millisecond): + t.Errorf("Failed to broadcast message %d", id) + } + }(i) + } + + for i := 0; i < 10; i++ { + select { + case <-done: + case <-time.After(200 * time.Millisecond): + t.Error("Concurrent broadcast test timed out") + return + } + } + + time.Sleep(50 * time.Millisecond) + }) + + t.Run("Broadcast empty message", func(t *testing.T) { + emptyMsg := []byte("") + + select { + case hub.GetBroadcastChan() <- server.BroadcastMessage{Payload: emptyMsg}: + case <-time.After(100 * time.Millisecond): + t.Fatal("Failed to send empty broadcast message") + } + + time.Sleep(20 * time.Millisecond) + }) +} + +// TestHubShutdown tests the hub's graceful shutdown functionality. +// It verifies that the hub can be shut down properly and all resources are cleaned up. +func TestHubShutdown(t *testing.T) { + t.Run("Shutdown empty hub", func(t *testing.T) { + hub := server.NewHub() + go hub.Run() + time.Sleep(10 * time.Millisecond) + + err := hub.Shutdown(time.Second) + if err != nil { + t.Errorf("Expected successful shutdown, got error: %v", err) + } + }) + + t.Run("Shutdown hub with clients", func(t *testing.T) { + hub := server.NewHub() + go hub.Run() + time.Sleep(10 * time.Millisecond) + + // Register some clients + for i := 0; i < 3; i++ { + client := server.NewClient(nil, hub, "127.0.0.1:"+strconv.Itoa(12340+i)) + select { + case hub.GetRegisterChan() <- client: + case <-time.After(100 * time.Millisecond): + t.Fatalf("Failed to register client %d", i) + } + } + time.Sleep(50 * time.Millisecond) + + err := hub.Shutdown(2 * time.Second) + if err != nil { + t.Errorf("Expected successful shutdown with clients, got error: %v", err) + } + }) +} + +// TestHubChannelsCommunication tests that hub channels can communicate properly. +// It verifies that messages can be sent through broadcast, register, and unregister channels. +func TestHubChannelsCommunication(t *testing.T) { + hub := server.NewHub() + go hub.Run() + defer func() { + if err := hub.Shutdown(time.Second); err != nil { + t.Errorf(shutdownErrorMsg, err) + } + }() + time.Sleep(10 * time.Millisecond) + + t.Run("Broadcast channel accepts messages", func(t *testing.T) { + for i := 0; i < 3; i++ { + msg := []byte(`{"content":"test"}`) + select { + case hub.GetBroadcastChan() <- server.BroadcastMessage{Payload: msg}: + // Success + case <-time.After(100 * time.Millisecond): + t.Fatalf("Iteration %d: Failed to send broadcast message", i) + } + time.Sleep(10 * time.Millisecond) + } + }) + + t.Run("All channels remain responsive", func(t *testing.T) { + // Send to all channels to ensure they're all working + select { + case hub.GetBroadcastChan() <- server.BroadcastMessage{Payload: []byte(`{"content":"test"}`)}: + case <-time.After(50 * time.Millisecond): + t.Error("Broadcast channel not responsive") + } + + select { + case hub.GetRegisterChan() <- nil: + case <-time.After(50 * time.Millisecond): + t.Error("Register channel not responsive") + } + + select { + case hub.GetUnregisterChan() <- nil: + case <-time.After(50 * time.Millisecond): + t.Error("Unregister channel not responsive") + } + + time.Sleep(20 * time.Millisecond) + }) +}