Files
MySMSAPio/WEBSOCKET_SETUP.md
2025-10-22 17:22:17 +08:00

503 lines
11 KiB
Markdown

# WebSocket Connection Setup
## Issue Fixed
**Problem**: Android gateway device couldn't connect to WebSocket server
**Error**:
```
Request origin not allowed
Failed to upgrade to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
```
**Root Cause**: Action Cable was blocking WebSocket connections from devices on the local network (192.168.x.x) due to origin restrictions.
## Solution Applied
### Configuration Changes
**File**: `config/environments/development.rb`
Added the following configuration:
```ruby
# Allow Action Cable connections from any origin in development
config.action_cable.disable_request_forgery_protection = true
# Allow WebSocket connections from local network
config.action_cable.allowed_request_origins = [
/http:\/\/localhost.*/,
/http:\/\/127\.0\.0\.1.*/,
/http:\/\/192\.168\..*/, # Local network (192.168.0.0/16)
/http:\/\/10\..*/, # Local network (10.0.0.0/8)
/http:\/\/172\.(1[6-9]|2[0-9]|3[0-1])\..*/ # Local network (172.16.0.0/12)
]
```
### What This Does
1. **Disables CSRF protection** for Action Cable in development
- Allows WebSocket connections without origin validation
- Safe for development environment
- **Should NOT be used in production**
2. **Allows specific origins**:
- `localhost` and `127.0.0.1` (local machine)
- `192.168.x.x` (most common home/office networks)
- `10.x.x.x` (enterprise networks, Docker)
- `172.16-31.x.x` (Docker bridge networks)
## How to Apply
1. **Restart Rails server**:
```bash
# Stop current server (Ctrl+C or kill process)
lsof -ti:3000 | xargs kill -9
# Start server again
bin/dev
```
2. **Verify Redis is running**:
```bash
redis-cli ping
# Should return: PONG
```
If Redis is not running:
```bash
# macOS (with Homebrew)
brew services start redis
# Linux
sudo systemctl start redis
# Or run manually
redis-server
```
## Testing WebSocket Connection
### Using wscat (Command Line)
Install wscat:
```bash
npm install -g wscat
```
Test connection:
```bash
# Replace with your actual API key
wscat -c "ws://localhost:3000/cable?api_key=gw_live_your_key_here"
```
Expected output:
```json
{"type":"welcome"}
```
### Using Android App
1. **Get API key** from admin interface (when creating gateway)
2. **Configure in Android app**:
- API Base URL: `http://192.168.x.x:3000` (your computer's IP)
- WebSocket URL: `ws://192.168.x.x:3000/cable`
- API Key: `gw_live_...`
3. **Connect** in the app
4. **Check Rails logs** for connection message:
```
Gateway device-001 connected
```
### Finding Your Computer's IP Address
**macOS**:
```bash
ipconfig getifaddr en0 # WiFi
ipconfig getifaddr en1 # Ethernet
```
**Linux**:
```bash
hostname -I | awk '{print $1}'
```
**Windows**:
```cmd
ipconfig | findstr IPv4
```
## Connection Flow
### 1. Client Connects
Android app initiates WebSocket connection:
```
ws://192.168.x.x:3000/cable?api_key=gw_live_abc123...
```
### 2. Server Authenticates
**File**: `app/channels/application_cable/connection.rb`
```ruby
def connect
self.current_gateway = find_verified_gateway
logger.info "Gateway #{current_gateway.device_id} connected"
end
private
def find_verified_gateway
api_key = request.params[:api_key]
return reject_unauthorized_connection if api_key.blank?
api_key_digest = Digest::SHA256.hexdigest(api_key)
gateway = Gateway.find_by(api_key_digest: api_key_digest, active: true)
gateway || reject_unauthorized_connection
end
```
### 3. Welcome Message
Server sends welcome:
```json
{"type":"welcome"}
```
### 4. Gateway Subscribes
Android app subscribes to GatewayChannel:
```json
{
"command": "subscribe",
"identifier": "{\"channel\":\"GatewayChannel\"}"
}
```
### 5. Server Confirms
```json
{
"identifier": "{\"channel\":\"GatewayChannel\"}",
"type": "confirm_subscription"
}
```
### 6. Communication Begins
Gateway can now:
- Send heartbeats
- Report received SMS
- Report delivery status
- Receive SMS to send
## Production Configuration
### Important: Different Settings for Production
**File**: `config/environments/production.rb`
**DO NOT** disable CSRF protection in production. Instead, specify exact allowed origins:
```ruby
# Production settings (recommended)
config.action_cable.allowed_request_origins = [
'https://yourdomain.com',
'https://www.yourdomain.com'
]
# Or allow all HTTPS origins (less secure)
config.action_cable.allowed_request_origins = /https:.*/
# NEVER do this in production:
# config.action_cable.disable_request_forgery_protection = true
```
### Production WebSocket URL
Use `wss://` (WebSocket Secure) instead of `ws://`:
```
wss://api.yourdomain.com/cable
```
### SSL/TLS Requirements
1. **Enable SSL** in Rails:
```ruby
# config/environments/production.rb
config.force_ssl = true
```
2. **Configure Puma** for SSL:
```ruby
# config/puma.rb
ssl_bind '0.0.0.0', '3000', {
key: '/path/to/server.key',
cert: '/path/to/server.crt'
}
```
3. **Or use reverse proxy** (recommended):
- Nginx or Apache with SSL
- Cloudflare
- AWS Application Load Balancer
- Heroku (automatic SSL)
## Troubleshooting
### Connection Refused
**Error**: `Failed to upgrade to WebSocket`
**Causes**:
1. Rails server not running
2. Redis not running
3. Wrong port
4. Firewall blocking
**Solutions**:
```bash
# Check if server is running
lsof -i:3000
# Check if Redis is running
redis-cli ping
# Check Rails logs
tail -f log/development.log
# Restart server
bin/dev
```
### Authentication Failed
**Error**: `Unauthorized`
**Causes**:
1. Wrong API key
2. Gateway not active
3. API key format incorrect
**Solutions**:
```bash
# Test API key in console
bin/rails console
```
```ruby
api_key = "gw_live_abc123..."
digest = Digest::SHA256.hexdigest(api_key)
gateway = Gateway.find_by(api_key_digest: digest)
puts gateway&.device_id || "NOT FOUND"
puts "Active: #{gateway&.active}"
```
### Origin Not Allowed (in production)
**Error**: `Request origin not allowed`
**Cause**: Origin not in `allowed_request_origins`
**Solution**: Add your domain to allowed origins:
```ruby
config.action_cable.allowed_request_origins = [
'https://yourdomain.com',
'https://api.yourdomain.com'
]
```
### Connection Drops Frequently
**Causes**:
1. Network instability
2. Server restarting
3. Redis connection issues
4. Heartbeat timeout
**Solutions**:
1. **Implement reconnection** in Android app
2. **Monitor Redis**:
```bash
redis-cli
> CLIENT LIST
> MONITOR
```
3. **Check heartbeat interval**: Gateway should send heartbeat every < 2 minutes
4. **Review server logs** for errors
## Monitoring WebSocket Connections
### View Active Connections
**In Rails console**:
```ruby
ActionCable.server.connections.size
```
### Monitor Redis
```bash
redis-cli
> CLIENT LIST | grep cable
> SUBSCRIBE sms_gateway_development*
```
### Check Gateway Status
```ruby
# In Rails console
Gateway.online.each do |g|
puts "#{g.name}: Last heartbeat #{g.last_heartbeat_at}"
end
```
### View Connection Logs
```bash
# Development logs
tail -f log/development.log | grep -i "cable\|gateway\|websocket"
# Production logs
tail -f log/production.log | grep -i "cable\|gateway\|websocket"
```
## Security Considerations
### Development (Current Setup)
**Acceptable**:
- CSRF protection disabled
- All origins allowed
- HTTP connections
- Local network only
⚠️ **Not for production**
### Production Requirements
🔒 **Must Have**:
- HTTPS/WSS only (`config.force_ssl = true`)
- Specific allowed origins
- CSRF protection enabled
- Rate limiting on connections
- Authentication required
- Connection monitoring
- Audit logging
### Best Practices
1. **API Key Security**:
- Never log API keys
- Use environment variables
- Rotate keys regularly
- Revoke compromised keys immediately
2. **Connection Limits**:
- Limit connections per gateway
- Implement backoff strategy
- Monitor connection attempts
- Alert on suspicious activity
3. **Network Security**:
- Use VPN for remote access
- Implement IP whitelisting
- Use SSL/TLS certificates
- Enable firewall rules
## Example Android App Code
### Connecting to WebSocket
```kotlin
import okhttp3.*
import java.util.concurrent.TimeUnit
class GatewayWebSocket(
private val apiKey: String,
private val websocketUrl: String
) {
private var webSocket: WebSocket? = null
private val client = OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.pingInterval(30, TimeUnit.SECONDS)
.build()
fun connect() {
val request = Request.Builder()
.url("$websocketUrl?api_key=$apiKey")
.build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d("WebSocket", "Connected!")
subscribe()
}
override fun onMessage(webSocket: WebSocket, text: String) {
Log.d("WebSocket", "Received: $text")
handleMessage(text)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e("WebSocket", "Connection failed: ${t.message}")
reconnect()
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d("WebSocket", "Closed: $reason")
reconnect()
}
})
}
private fun subscribe() {
val subscribeMessage = """
{
"command": "subscribe",
"identifier": "{\"channel\":\"GatewayChannel\"}"
}
""".trimIndent()
webSocket?.send(subscribeMessage)
}
private fun handleMessage(json: String) {
val message = JSONObject(json)
when (message.optString("type")) {
"welcome" -> Log.d("WebSocket", "Welcome received")
"ping" -> Log.d("WebSocket", "Ping received")
"confirm_subscription" -> Log.d("WebSocket", "Subscribed to GatewayChannel")
}
}
fun disconnect() {
webSocket?.close(1000, "Normal closure")
}
private fun reconnect() {
Handler(Looper.getMainLooper()).postDelayed({
connect()
}, 5000) // Reconnect after 5 seconds
}
}
```
## Summary
**Fixed**: WebSocket connection now works from local network
**Configuration**: Action Cable allows connections from 192.168.x.x, 10.x.x.x, etc.
**Security**: CSRF protection disabled for development only
**Production**: Different, more secure settings required
**Next Steps**:
1. Restart your Rails server
2. Ensure Redis is running
3. Try connecting from Android app
4. Check Rails logs for "Gateway XXX connected" message
The WebSocket server is now ready to accept connections from your Android gateway devices on the local network! 🚀