503 lines
11 KiB
Markdown
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! 🚀
|