11 KiB
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:
# 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
-
Disables CSRF protection for Action Cable in development
- Allows WebSocket connections without origin validation
- Safe for development environment
- Should NOT be used in production
-
Allows specific origins:
localhostand127.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
- Restart Rails server:
# Stop current server (Ctrl+C or kill process)
lsof -ti:3000 | xargs kill -9
# Start server again
bin/dev
- Verify Redis is running:
redis-cli ping
# Should return: PONG
If Redis is not running:
# 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:
npm install -g wscat
Test connection:
# Replace with your actual API key
wscat -c "ws://localhost:3000/cable?api_key=gw_live_your_key_here"
Expected output:
{"type":"welcome"}
Using Android App
- Get API key from admin interface (when creating gateway)
- 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_...
- API Base URL:
- Connect in the app
- Check Rails logs for connection message:
Gateway device-001 connected
Finding Your Computer's IP Address
macOS:
ipconfig getifaddr en0 # WiFi
ipconfig getifaddr en1 # Ethernet
Linux:
hostname -I | awk '{print $1}'
Windows:
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
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:
{"type":"welcome"}
4. Gateway Subscribes
Android app subscribes to GatewayChannel:
{
"command": "subscribe",
"identifier": "{\"channel\":\"GatewayChannel\"}"
}
5. Server Confirms
{
"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:
# 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
- Enable SSL in Rails:
# config/environments/production.rb
config.force_ssl = true
- Configure Puma for SSL:
# config/puma.rb
ssl_bind '0.0.0.0', '3000', {
key: '/path/to/server.key',
cert: '/path/to/server.crt'
}
- 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:
- Rails server not running
- Redis not running
- Wrong port
- Firewall blocking
Solutions:
# 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:
- Wrong API key
- Gateway not active
- API key format incorrect
Solutions:
# Test API key in console
bin/rails console
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:
config.action_cable.allowed_request_origins = [
'https://yourdomain.com',
'https://api.yourdomain.com'
]
Connection Drops Frequently
Causes:
- Network instability
- Server restarting
- Redis connection issues
- Heartbeat timeout
Solutions:
- Implement reconnection in Android app
- Monitor Redis:
redis-cli
> CLIENT LIST
> MONITOR
- Check heartbeat interval: Gateway should send heartbeat every < 2 minutes
- Review server logs for errors
Monitoring WebSocket Connections
View Active Connections
In Rails console:
ActionCable.server.connections.size
Monitor Redis
redis-cli
> CLIENT LIST | grep cable
> SUBSCRIBE sms_gateway_development*
Check Gateway Status
# In Rails console
Gateway.online.each do |g|
puts "#{g.name}: Last heartbeat #{g.last_heartbeat_at}"
end
View Connection Logs
# 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
-
API Key Security:
- Never log API keys
- Use environment variables
- Rotate keys regularly
- Revoke compromised keys immediately
-
Connection Limits:
- Limit connections per gateway
- Implement backoff strategy
- Monitor connection attempts
- Alert on suspicious activity
-
Network Security:
- Use VPN for remote access
- Implement IP whitelisting
- Use SSL/TLS certificates
- Enable firewall rules
Example Android App Code
Connecting to WebSocket
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:
- Restart your Rails server
- Ensure Redis is running
- Try connecting from Android app
- 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! 🚀