completed SMS gateway project
This commit is contained in:
502
WEBSOCKET_SETUP.md
Normal file
502
WEBSOCKET_SETUP.md
Normal file
@@ -0,0 +1,502 @@
|
||||
# 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! 🚀
|
||||
Reference in New Issue
Block a user