updates
This commit is contained in:
parent
81fa6c983d
commit
2457662e12
@ -6,6 +6,9 @@ Bridges SMS Campaign Manager (Ubuntu) with Termux API (Android)
|
|||||||
This server runs on the Android device in Termux and provides REST API
|
This server runs on the Android device in Termux and provides REST API
|
||||||
endpoints for the main SMS Campaign Manager to send messages using
|
endpoints for the main SMS Campaign Manager to send messages using
|
||||||
native Android SMS capabilities instead of ADB automation.
|
native Android SMS capabilities instead of ADB automation.
|
||||||
|
|
||||||
|
All endpoints require API key authentication via X-API-Key header.
|
||||||
|
Localhost requests (127.0.0.1, ::1) are exempt for watchdog health checks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import Flask, request, jsonify
|
from flask import Flask, request, jsonify
|
||||||
@ -193,7 +196,7 @@ class SMSApiServer:
|
|||||||
|
|
||||||
if result['success']:
|
if result['success']:
|
||||||
self.message_count += 1
|
self.message_count += 1
|
||||||
logger.info(f"✅ SMS sent successfully to {phone}: {message[:50]}...")
|
logger.info(f"SMS sent successfully to {phone}: {message[:50]}...")
|
||||||
|
|
||||||
# Send confirmation notification
|
# Send confirmation notification
|
||||||
self.execute_termux_command([
|
self.execute_termux_command([
|
||||||
@ -202,7 +205,7 @@ class SMSApiServer:
|
|||||||
'--content', f'Message sent to {phone}'
|
'--content', f'Message sent to {phone}'
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
logger.error(f"❌ SMS send failed to {phone}: {result.get('error', 'Unknown error')}")
|
logger.error(f"SMS send failed to {phone}: {result.get('error', 'Unknown error')}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': result['success'],
|
'success': result['success'],
|
||||||
@ -222,130 +225,34 @@ def verify_api_key():
|
|||||||
expected_key = CONFIG['SECRET_KEY']
|
expected_key = CONFIG['SECRET_KEY']
|
||||||
|
|
||||||
if not api_key:
|
if not api_key:
|
||||||
logger.warning(f"⚠️ No API key provided for {request.path} from {request.remote_addr}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not hmac.compare_digest(api_key, expected_key):
|
if not hmac.compare_digest(api_key, expected_key):
|
||||||
logger.warning(f"⚠️ Invalid API key for {request.path} from {request.remote_addr}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.info(f"✅ Valid API key for {request.path}")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Global auth: every request from a remote client must provide a valid API key.
|
||||||
|
# Localhost (127.0.0.1 / ::1) is exempt so the watchdog health check works.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@app.before_request
|
||||||
|
def require_api_key():
|
||||||
|
"""Enforce API key authentication on all endpoints for remote clients."""
|
||||||
|
if request.remote_addr in ('127.0.0.1', '::1'):
|
||||||
|
return None # allow localhost (watchdog health checks)
|
||||||
|
|
||||||
|
if not verify_api_key():
|
||||||
|
logger.warning(f"Auth failed: {request.method} {request.path} from {request.remote_addr}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Authentication required',
|
||||||
|
'message': 'Please provide valid API key via X-API-Key header'
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
# API Endpoints
|
# 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("""
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>SMS API Server - Android</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<style>
|
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
||||||
margin: 0; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh; color: white; }
|
|
||||||
.container { max-width: 800px; margin: 0 auto; background: rgba(255,255,255,0.1);
|
|
||||||
padding: 30px; border-radius: 15px; backdrop-filter: blur(10px); box-shadow: 0 8px 32px rgba(0,0,0,0.1); }
|
|
||||||
h1 { text-align: center; margin-bottom: 30px; font-size: 2.5em; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); }
|
|
||||||
.status { background: rgba(0,255,0,0.2); padding: 20px; border-radius: 10px; margin: 20px 0;
|
|
||||||
border-left: 5px solid #00ff00; }
|
|
||||||
.endpoint { background: rgba(255,255,255,0.1); padding: 15px; margin: 10px 0; border-radius: 8px;
|
|
||||||
border-left: 3px solid #fff; }
|
|
||||||
.endpoint h3 { margin: 0 0 10px 0; color: #fff; }
|
|
||||||
.endpoint code { background: rgba(0,0,0,0.3); padding: 5px 10px; border-radius: 5px;
|
|
||||||
font-family: "Courier New", monospace; }
|
|
||||||
.endpoint p { margin: 5px 0; opacity: 0.9; }
|
|
||||||
.test-links { text-align: center; margin: 20px 0; }
|
|
||||||
.test-links a { display: inline-block; margin: 5px 10px; padding: 10px 20px;
|
|
||||||
background: rgba(255,255,255,0.2); color: white; text-decoration: none;
|
|
||||||
border-radius: 25px; transition: all 0.3s ease; }
|
|
||||||
.test-links a:hover { background: rgba(255,255,255,0.3); transform: translateY(-2px); }
|
|
||||||
.nav-links a:hover { background: rgba(255,255,255,0.3) !important; transform: translateY(-1px); }
|
|
||||||
.footer a:hover { opacity: 0.8; }
|
|
||||||
.footer { text-align: center; margin-top: 40px; opacity: 0.7; font-size: 0.9em; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header-nav" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.2);"><div><h1 style="margin: 0; font-size: 2.5em;">📱 SMS API Server</h1><p style="margin: 0; opacity: 0.8; font-size: 1.1em;">Android Termux Interface</p></div><div class="nav-links" style="display: flex; gap: 15px;"><a href="http://localhost:5000/" style="background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px; color: white; text-decoration: none; font-size: 0.9em; transition: all 0.3s ease;">🏠 Homelab</a><a href="http://10.0.0.193:5000" style="background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px; color: white; text-decoration: none; font-size: 0.9em; transition: all 0.3s ease;">📊 Monitor</a></div></div>
|
|
||||||
<h2>🚀 Running on Android (Termux)</h2>
|
|
||||||
|
|
||||||
<div class="status">
|
|
||||||
<h3>✅ Server Status: Operational</h3>
|
|
||||||
<p><strong>Device IP:</strong> {{ device_ip }}</p>
|
|
||||||
<p><strong>Port:</strong> 5001</p>
|
|
||||||
<p><strong>Environment:</strong> Termux on Android</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>🔗 API Endpoints</h3>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h3>📊 Health Check</h3>
|
|
||||||
<p><code>GET /health</code></p>
|
|
||||||
<p>Returns server status, uptime, and message statistics</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h3>📱 Send SMS</h3>
|
|
||||||
<p><code>POST /api/sms/send</code></p>
|
|
||||||
<p>Send SMS messages with name substitution support</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h3>🔋 Battery Status</h3>
|
|
||||||
<p><code>GET /api/device/battery</code></p>
|
|
||||||
<p>Get real-time Android device battery information</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h3>📍 Location</h3>
|
|
||||||
<p><code>GET /api/device/location</code></p>
|
|
||||||
<p>Get GPS coordinates (with permissions)</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h3>ℹ️ Device Info</h3>
|
|
||||||
<p><code>GET /api/device/info</code></p>
|
|
||||||
<p>System information and device details</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h3>📇 Contact List</h3>
|
|
||||||
<p><code>GET /api/contacts/list</code></p>
|
|
||||||
<p>Fetch all contacts from phone (with optional search)</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="endpoint">
|
|
||||||
<h3>🧪 Test Contact Structure</h3>
|
|
||||||
<p><code>GET /api/contacts/test</code></p>
|
|
||||||
<p>Analyze contact list JSON structure and fields</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="test-links">
|
|
||||||
<h3>🧪 Quick Tests</h3>
|
|
||||||
<a href="/health">📊 Health Check</a>
|
|
||||||
<a href="/api/device/battery">🔋 Battery</a>
|
|
||||||
<a href="/api/device/info">ℹ️ Device Info</a>
|
|
||||||
<a href="/api/contacts/test">📇 Test Contacts</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer" style="text-align: center; margin-top: 40px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.2); opacity: 0.7; font-size: 0.9em;">
|
|
||||||
<p>📱 SMS Campaign Manager • Android SMS API Service</p>
|
|
||||||
<div style="margin-top: 10px;">
|
|
||||||
<a href="http://localhost:5000/" style="color: #fff; margin: 0 10px; text-decoration: none;">🏠 Homelab Dashboard</a> |
|
|
||||||
<a href="http://10.0.0.193:5000" style="color: #fff; margin: 0 10px; text-decoration: none;">📊 Android Monitor</a> |
|
|
||||||
<span style="font-size: 0.8em;">© 2025 Campaign System</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
""", device_ip="10.0.0.193", homelab_ip="10.0.0.190")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/health', methods=['GET'])
|
@app.route('/health', methods=['GET'])
|
||||||
def health_check():
|
def health_check():
|
||||||
@ -363,14 +270,6 @@ def health_check():
|
|||||||
@app.route('/api/sms/send', methods=['POST'])
|
@app.route('/api/sms/send', methods=['POST'])
|
||||||
def send_sms():
|
def send_sms():
|
||||||
"""Send SMS message via Termux API"""
|
"""Send SMS message via Termux API"""
|
||||||
# Verify API key
|
|
||||||
if not verify_api_key():
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Authentication required',
|
|
||||||
'message': 'Please provide valid API key via X-API-Key header'
|
|
||||||
}), 401
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
@ -590,14 +489,6 @@ def get_contact_info(phone):
|
|||||||
@app.route('/api/sms/send-reply', methods=['POST'])
|
@app.route('/api/sms/send-reply', methods=['POST'])
|
||||||
def send_reply():
|
def send_reply():
|
||||||
"""Send a reply message with enhanced tracking"""
|
"""Send a reply message with enhanced tracking"""
|
||||||
# Verify API key
|
|
||||||
if not verify_api_key():
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Authentication required',
|
|
||||||
'message': 'Please provide valid API key via X-API-Key header'
|
|
||||||
}), 401
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|
||||||
@ -666,6 +557,45 @@ def campaign_notification():
|
|||||||
logger.error(f"Notification error: {e}")
|
logger.error(f"Notification error: {e}")
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/logs/tail', methods=['GET'])
|
||||||
|
def tail_logs():
|
||||||
|
"""Get last N lines from the SMS API log file"""
|
||||||
|
try:
|
||||||
|
lines_param = request.args.get('lines', '100', type=str)
|
||||||
|
try:
|
||||||
|
num_lines = min(500, max(1, int(lines_param)))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
num_lines = 100
|
||||||
|
|
||||||
|
log_path = os.path.expanduser('~/logs/sms-api.log')
|
||||||
|
|
||||||
|
if not os.path.isfile(log_path):
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'lines': [],
|
||||||
|
'total_lines': 0,
|
||||||
|
'file_size': 0
|
||||||
|
})
|
||||||
|
|
||||||
|
file_size = os.path.getsize(log_path)
|
||||||
|
|
||||||
|
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
all_lines = f.readlines()
|
||||||
|
|
||||||
|
total_lines = len(all_lines)
|
||||||
|
tail_lines = [line.rstrip('\n') for line in all_lines[-num_lines:]]
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'lines': tail_lines,
|
||||||
|
'total_lines': total_lines,
|
||||||
|
'file_size': file_size
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Log tail error: {e}")
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/contacts/test', methods=['GET'])
|
@app.route('/api/contacts/test', methods=['GET'])
|
||||||
def test_contacts():
|
def test_contacts():
|
||||||
"""Test endpoint to fetch and examine raw contact list structure"""
|
"""Test endpoint to fetch and examine raw contact list structure"""
|
||||||
@ -772,7 +702,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
# Validate required configuration
|
# Validate required configuration
|
||||||
if not CONFIG['SECRET_KEY']:
|
if not CONFIG['SECRET_KEY']:
|
||||||
logger.critical("❌ SECURITY ERROR: SMS_API_SECRET or TERMUX_API_KEY environment variable is required!")
|
logger.critical("SECURITY ERROR: SMS_API_SECRET or TERMUX_API_KEY environment variable is required!")
|
||||||
logger.critical("Set SMS_API_SECRET environment variable before starting the server")
|
logger.critical("Set SMS_API_SECRET environment variable before starting the server")
|
||||||
logger.critical("Generate a secure key with: python -c \"import secrets; print(secrets.token_hex(32))\"")
|
logger.critical("Generate a secure key with: python -c \"import secrets; print(secrets.token_hex(32))\"")
|
||||||
print("\n" + "="*80)
|
print("\n" + "="*80)
|
||||||
@ -788,7 +718,7 @@ if __name__ == '__main__':
|
|||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
logger.info("Starting Termux SMS API Server...")
|
logger.info("Starting Termux SMS API Server...")
|
||||||
logger.info(f"✅ API authentication configured")
|
logger.info("API authentication configured (all remote endpoints protected)")
|
||||||
logger.info(f"Available commands: {CONFIG['ALLOWED_COMMANDS']}")
|
logger.info(f"Available commands: {CONFIG['ALLOWED_COMMANDS']}")
|
||||||
|
|
||||||
# Get local IP for display (secure method without shell=True)
|
# Get local IP for display (secure method without shell=True)
|
||||||
@ -801,18 +731,16 @@ if __name__ == '__main__':
|
|||||||
s.close()
|
s.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Could not determine IP address: {e}")
|
logger.warning(f"Could not determine IP address: {e}")
|
||||||
local_ip = '10.0.0.193'
|
local_ip = '0.0.0.0'
|
||||||
|
|
||||||
print(f"""
|
print(f"""
|
||||||
🚀 Termux SMS API Server Starting
|
Termux SMS API Server Starting
|
||||||
📱 Device IP: {local_ip}
|
Device IP: {local_ip}
|
||||||
🌐 API Base URL: http://{local_ip}:5001
|
API Base URL: http://{local_ip}:5001
|
||||||
🔗 Health Check: http://{local_ip}:5001/health
|
All endpoints require X-API-Key header (except localhost)
|
||||||
📞 SMS Endpoint: http://{local_ip}:5001/api/sms/send
|
|
||||||
🔋 Battery API: http://{local_ip}:5001/api/device/battery
|
|
||||||
|
|
||||||
Access from Ubuntu homelab:
|
Access from server:
|
||||||
curl http://{local_ip}:5001/health
|
curl -H "X-API-Key: $SMS_API_SECRET" http://{local_ip}:5001/health
|
||||||
""")
|
""")
|
||||||
|
|
||||||
app.run(host='0.0.0.0', port=5001, debug=False)
|
app.run(host='0.0.0.0', port=5001, debug=False)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user