# 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! 🚀