#!/usr/bin/env python3
"""
Termux SMS API Server
Bridges SMS Campaign Manager (Ubuntu) with Termux API (Android)
This server runs on the Android device in Termux and provides REST API
endpoints for the main SMS Campaign Manager to send messages using
native Android SMS capabilities instead of ADB automation.
"""
from flask import Flask, request, jsonify
import subprocess
import json
import time
import logging
import hmac
import hashlib
import os
from datetime import datetime
from typing import Dict, List, Optional, Any
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('/data/data/com.termux/files/home/logs/sms-api.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
# Configuration
CONFIG = {
'SECRET_KEY': os.environ.get('SMS_API_SECRET', 'termux-sms-campaign-2025'),
'MAX_MESSAGE_LENGTH': 160,
'RATE_LIMIT_DELAY': 2.0, # Seconds between messages
'ALLOWED_COMMANDS': [
'termux-sms-send',
'termux-sms-list',
'termux-battery-status',
'termux-location',
'termux-notification'
]
}
class SMSApiServer:
"""Main SMS API server class"""
def __init__(self):
self.last_send_time = 0
self.message_count = 0
self.start_time = time.time()
def authenticate_request(self, request_data: str, signature: str) -> bool:
"""Verify HMAC signature for request authentication"""
try:
expected_signature = hmac.new(
CONFIG['SECRET_KEY'].encode(),
request_data.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
except Exception as e:
logger.error(f"Authentication error: {e}")
return False
def execute_termux_command(self, command: List[str]) -> Dict[str, Any]:
"""Execute Termux API command with error handling"""
if not command or command[0] not in CONFIG['ALLOWED_COMMANDS']:
return {'success': False, 'error': 'Command not allowed'}
try:
logger.info(f"Executing: {' '.join(command)}")
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=30
)
return {
'success': result.returncode == 0,
'stdout': result.stdout.strip(),
'stderr': result.stderr.strip(),
'return_code': result.returncode
}
except subprocess.TimeoutExpired:
return {'success': False, 'error': 'Command timeout'}
except Exception as e:
return {'success': False, 'error': str(e)}
def rate_limit_check(self) -> bool:
"""Check if enough time has passed since last message"""
current_time = time.time()
if current_time - self.last_send_time < CONFIG['RATE_LIMIT_DELAY']:
return False
self.last_send_time = current_time
return True
def send_sms(self, phone: str, message: str) -> Dict[str, Any]:
"""Send SMS using Termux API"""
# Input validation
if not phone or not message:
return {'success': False, 'error': 'Phone and message required'}
if len(message) > CONFIG['MAX_MESSAGE_LENGTH']:
return {'success': False, 'error': f'Message too long (max {CONFIG["MAX_MESSAGE_LENGTH"]} chars)'}
# Rate limiting
if not self.rate_limit_check():
return {'success': False, 'error': 'Rate limit exceeded, please wait'}
# Execute SMS send command
command = ['termux-sms-send', '-n', phone, message]
result = self.execute_termux_command(command)
if result['success']:
self.message_count += 1
logger.info(f"SMS sent to {phone}: {message[:50]}...")
# Send confirmation notification
self.execute_termux_command([
'termux-notification',
'--title', 'SMS Sent',
'--content', f'Message sent to {phone}'
])
return {
'success': result['success'],
'error': result.get('error') or result.get('stderr'),
'timestamp': datetime.now().isoformat(),
'phone': phone,
'message_length': len(message),
'total_sent': self.message_count
}
# Global server instance
sms_server = SMSApiServer()
# API Endpoints
# Web interface route
@app.route("/")
def index():
"""Web interface for SMS API Server"""
from flask import render_template_string
return render_template_string("""
SMS API Server - Android
๐ Running on Android (Termux)
โ
Server Status: Operational
Device IP: {{ device_ip }}
Port: 5001
Environment: Termux on Android
๐ API Endpoints
๐ Health Check
GET /health
Returns server status, uptime, and message statistics
๐ฑ Send SMS
POST /api/sms/send
Send SMS messages with name substitution support
๐ Battery Status
GET /api/device/battery
Get real-time Android device battery information
๐ Location
GET /api/device/location
Get GPS coordinates (with permissions)
โน๏ธ Device Info
GET /api/device/info
System information and device details
""", device_ip="10.0.0.193", homelab_ip="10.0.0.190")
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
uptime = time.time() - sms_server.start_time
return jsonify({
'status': 'healthy',
'timestamp': datetime.now().isoformat(),
'uptime_seconds': int(uptime),
'messages_sent': sms_server.message_count,
'version': '1.0.0'
})
@app.route('/api/sms/send', methods=['POST'])
def send_sms():
"""Send SMS message via Termux API"""
try:
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': 'JSON data required'}), 400
# Extract parameters
phone = data.get('phone', '').strip()
message = data.get('message', '').strip()
name = data.get('name', '')
# Message template substitution (like existing ui.sh)
if name and '{name}' in message:
message = message.replace('{name}', name)
# Send SMS
result = sms_server.send_sms(phone, message)
status_code = 200 if result['success'] else 400
return jsonify(result), status_code
except Exception as e:
logger.error(f"SMS send error: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/sms/list', methods=['GET'])
def list_sms():
"""List SMS messages"""
try:
limit = request.args.get('limit', '10')
offset = request.args.get('offset', '0')
command = ['termux-sms-list', '-l', limit, '-o', offset]
result = sms_server.execute_termux_command(command)
if result['success']:
try:
sms_data = json.loads(result['stdout']) if result['stdout'] else []
return jsonify({
'success': True,
'messages': sms_data,
'count': len(sms_data)
})
except json.JSONDecodeError:
return jsonify({
'success': True,
'messages': result['stdout'],
'raw_output': True
})
else:
return jsonify({'success': False, 'error': result['error']}), 400
except Exception as e:
logger.error(f"SMS list error: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/device/battery', methods=['GET'])
def get_battery_status():
"""Get device battery status"""
try:
result = sms_server.execute_termux_command(['termux-battery-status'])
if result['success']:
battery_data = json.loads(result['stdout'])
return jsonify({
'success': True,
'battery': battery_data,
'timestamp': datetime.now().isoformat()
})
else:
return jsonify({'success': False, 'error': result['error']}), 400
except Exception as e:
logger.error(f"Battery status error: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/device/location', methods=['GET'])
def get_location():
"""Get device GPS location"""
try:
result = sms_server.execute_termux_command(['termux-location'])
if result['success'] and result['stdout']:
try:
location_data = json.loads(result['stdout'])
return jsonify({
'success': True,
'location': location_data,
'timestamp': datetime.now().isoformat()
})
except json.JSONDecodeError:
return jsonify({
'success': True,
'location': result['stdout'],
'raw_output': True
})
else:
return jsonify({'success': False, 'error': 'Location unavailable'}), 400
except Exception as e:
logger.error(f"Location error: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/device/info', methods=['GET'])
def get_device_info():
"""Get general device information"""
try:
# Get multiple device stats
battery_result = sms_server.execute_termux_command(['termux-battery-status'])
info = {
'timestamp': datetime.now().isoformat(),
'uptime': time.time() - sms_server.start_time,
'messages_sent': sms_server.message_count,
'api_version': '1.0.0',
'termux_api_available': True
}
if battery_result['success']:
try:
battery_data = json.loads(battery_result['stdout'])
info['battery'] = battery_data
except json.JSONDecodeError:
pass
return jsonify({'success': True, 'device_info': info})
except Exception as e:
logger.error(f"Device info error: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/campaign/notify', methods=['POST'])
def campaign_notification():
"""Send notification about campaign status"""
try:
data = request.get_json()
title = data.get('title', 'SMS Campaign')
message = data.get('message', 'Campaign update')
result = sms_server.execute_termux_command([
'termux-notification',
'--title', title,
'--content', message
])
return jsonify({
'success': result['success'],
'error': result.get('error')
})
except Exception as e:
logger.error(f"Notification error: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
if __name__ == '__main__':
# Create logs directory
os.makedirs('/data/data/com.termux/files/home/logs', exist_ok=True)
logger.info("Starting Termux SMS API Server...")
logger.info(f"Available commands: {CONFIG['ALLOWED_COMMANDS']}")
# Get local IP for display
try:
ip_result = subprocess.run([
'ifconfig', '2>/dev/null', '|', 'grep', '-A1', 'wlan0', '|',
'grep', 'inet', '|', 'awk', '{print $2}', '|', 'cut', '-d:', '-f2'
], shell=True, capture_output=True, text=True)
local_ip = ip_result.stdout.strip() or '10.0.0.193'
except:
local_ip = '10.0.0.193'
print(f"""
๐ Termux SMS API Server Starting
๐ฑ Device IP: {local_ip}
๐ API Base URL: http://{local_ip}:5001
๐ Health Check: http://{local_ip}:5001/health
๐ SMS Endpoint: http://{local_ip}:5001/api/sms/send
๐ Battery API: http://{local_ip}:5001/api/device/battery
Access from Ubuntu homelab:
curl http://{local_ip}:5001/health
""")
app.run(host='0.0.0.0', port=5001, debug=False)