diff --git a/README.md b/README.md index 323154a..bdd4003 100644 --- a/README.md +++ b/README.md @@ -1,131 +1,1082 @@ +# 🌍 **Guía Completa para Mejorar el Sistema de Posicionamiento por Wi-Fi en Python** + +> *"Un proyecto paso a paso para hacer que el script funcione en Python 3, Windows, y sea más robusto y fácil de usar."* +> — **Para Luis Eduardo Ferrer Cruz** 👨‍💻 + +--- + +## 📌 **Índice** + +1. [Introducción y Objetivos](#-introducción-y-objetivos) +2. [Requisitos Previos](#-requisitos-previos) +3. [Mejoras Básicas (Para Principiantes)](#-mejoras-básicas-para-principiantes) + - [Actualizar el README.md](#1-actualizar-el-readmemd) + - [Reemplazar `commands` por `subprocess](#2-reemplazar-commands-por-subprocess)` + - [Reemplazar `urllib2` por `requests](#3-reemplazar-urllib2-por-requests)` +4. [Mejoras Intermedias](#-mejoras-intermedias) + - [Hacer el código compatible con Python 3](#4-hacer-el-código-compatible-con-python-3) + - [Manejo de errores para la API de Google](#5-manejo-de-errores-para-la-api-de-google) + - [Modo Demo con Datos de Ejemplo](#6-modo-demo-con-datos-de-ejemplo) + - [Mejorar la visualización del mapa con Folium](#7-mejorar-la-visualización-del-mapa-con-folium) +5. [Mejoras Avanzadas](#-mejoras-avanzadas) + - [Soporte para Windows](#8-soporte-para-windows) + - [Pruebas Unitarias](#9-pruebas-unitarias) + - [Soporte para Otras APIs de Geolocalización](#10-soporte-para-otras-apis-de-geolocalización) + - [Refactorizar el Código](#11-refactorizar-el-código) + - [Sistema de Logging](#12-sistema-de-logging) +6. [Pasos para Contribuir en GitHub](#-pasos-para-contribuir-en-github) +7. [Estructura Final del Proyecto](#-estructura-final-del-proyecto) +8. [Recursos Adicionales](#-recursos-adicionales) + +--- + +## 🌟 **Introducción y Objetivos** + +### **¿Qué es el Python-Wi-Fi-Positioning-System?** + +Es un script en Python que utiliza la **API de Geolocalización de Google** para determinar la ubicación de un dispositivo basado en las redes Wi-Fi cercanas. Originalmente, solo funcionaba en **GNU/Linux, OpenBSD y Mac OS X**, pero con esta guía, lo haremos compatible con **Windows** y lo mejoraremos para que sea más robusto y fácil de usar. + +### **Objetivos de esta Guía** + +1. **Hacer el script compatible con Python 3** (actualmente solo funciona en Python 2). +2. **Añadir soporte para Windows** (usando librerías como `pywifi` o `subprocess`). +3. **Mejorar la documentación** (README.md) para que sea más clara y útil. +4. **Reemplazar dependencias obsoletas** (`commands`, `urllib2`, `simplejson`). +5. **Añadir manejo de errores** para la API de Google y otros casos. +6. **Mejorar la visualización de resultados** (usando Folium en lugar de Google Maps API). +7. **Refactorizar el código** para que sea más legible y mantenible. + +--- + +## 🛠️ **Requisitos Previos** + +### **Herramientas Necesarias** + +1. **Python 3.8 o superior** (recomendado: [Python 3.10](https://www.python.org/downloads/)). +2. **Git** (para clonar el repositorio y hacer *pull requests*). Descárgalo [aquí](https://git-scm.com/downloads). +3. **Un editor de código** (recomendado: [VS Code](https://code.visualstudio.com/) o [PyCharm](https://www.jetbrains.com/pycharm/)). +4. **Una clave API de Google Geolocation** (gratis para pruebas). Obténla [aquí](https://developers.google.com/maps/documentation/geolocation/get-api-key). + +### **Librerías de Python a Instalar** + +Ejecuta el siguiente comando para instalar todas las dependencias necesarias: + +```bash +pip install requests folium pywifi subprocess32 +``` + +--- + +## 🟢 **Mejoras Básicas (Para Principiantes)** + +--- + +### **1. Actualizar el README.md** + +#### **¿Qué hacer?** + +Vamos a mejorar el `README.md` para que sea más claro y útil para nuevos usuarios. Incluiremos: + +- Instrucciones paso a paso para obtener la clave API de Google. +- Cómo instalar dependencias en diferentes sistemas operativos. +- Ejemplos de uso del script. + +#### **Código del Nuevo README.md** + +```markdown +# 🌍 Python Wi-Fi Positioning System + +> Un script en Python para determinar la ubicación de un dispositivo usando redes Wi-Fi cercanas y la API de Geolocalización de Google. + +--- + +## 📌 **Características** +- ✅ Funciona en **GNU/Linux, OpenBSD, Mac OS X y Windows**. +- ✅ Usa la **API de Geolocalización de Google** para obtener coordenadas. +- ✅ Genera un **mapa interactivo** con la ubicación estimada. +- ✅ Modo **demo** para probar sin escanear redes Wi-Fi. + +--- + +## 🛠️ **Requisitos** + +### **Sistemas Operativos Soportados** +| Sistema Operativo | Requisitos | Notas | +|--------------------|------------|-------| +| GNU/Linux | `iw` (para escanear redes Wi-Fi) | Instalar con `sudo apt-get install iw` (Ubuntu) | +| OpenBSD | `ifconfig` | Ya viene preinstalado | +| Mac OS X | `airport` | Ya viene preinstalado | +| Windows | `pywifi` | Instalar con `pip install pywifi` | + +### **Dependencias de Python** +```bash +pip install requests folium pywifi +``` + +### **Clave API de Google** + +1. Ve a [Google Cloud Console](https://console.cloud.google.com/). +2. Crea un nuevo proyecto. +3. Habilita la **API de Geolocation**. +4. Genera una clave API y guárdala en un lugar seguro. + +--- + +## 🚀 **Instalación y Uso** + +### **1. Clonar el Repositorio** + +```bash +git clone https://github.com/TU_USUARIO/Python-Wi-Fi-Positioning-System.git +cd Python-Wi-Fi-Positioning-System +``` + +### **2. Ejecutar el Script** + +#### **Modo Normal (Escanear Redes Wi-Fi)** + +- **En Linux/Mac/OpenBSD:** + ```bash + python wifi_positioning_system.py --api-key TU_CLAVE_API -i wlan0 + ``` +- **En Windows:** + ```bash + python wifi_positioning_system.py --api-key TU_CLAVE_API --windows + ``` + +#### **Modo Demo (Sin Escanear Redes)** + +```bash +python wifi_positioning_system.py --api-key TU_CLAVE_API --demo +``` + +#### **Opciones Adicionales** + + +| Argumento | Descripción | Ejemplo | +| ----------------- | -------------------------------------------------- | ------------------------ | +| `--api-key` | Clave API de Google (obligatorio) | `--api-key AIzaSyABC123` | +| `-i` | Interfaz Wi-Fi (Linux/Mac/OpenBSD) | `-i wlan0` | +| `--windows` | Usar modo Windows | `--windows` | +| `--demo` | Modo demo con datos de ejemplo | `--demo` | +| `--json-prettify` | Formatear la salida JSON | `--json-prettify` | +| `--with-overview` | Generar un mapa HTML | `--with-overview` | +| `--map-type` | Tipo de mapa (ROADMAP, SATELLITE, HYBRID, TERRAIN) | `--map-type HYBRID` | + + --- -[![GNU General Public License v3.0](https://raw.github.com/initbrain/btsmapper/master/btsmapper/images/logo_gpl_v3.png)](http://www.gnu.org/licenses/gpl-3.0.txt) -[GNU General Public License v3.0](http://www.gnu.org/licenses/gpl-3.0.txt) + +## 📂 **Estructura del Proyecto** + +``` +Python-Wi-Fi-Positioning-System/ +├── wifi_positioning_system.py # Script principal +├── demo_data.json # Datos de ejemplo para modo demo +├── README.md # Este archivo +└── requirements.txt # Dependencias de Python +``` --- -Python Wi-Fi Positioning System -=============================== +## 🤝 **Contribuir** + +1. Haz un *fork* del repositorio. +2. Crea una nueva rama (`git checkout -b mejora-readme`). +3. Haz *commit* de tus cambios (`git commit -m "Mejora el README"`). +4. Envía un *pull request*. -This script use the Google Geolocation API. -It was tested on GNU/Linux (require `iw`), OpenBSD (use `ifconfig`) and Mac OS X (require `airport`). -Special thanks go to contributors! +--- +## 📜 **Licencia** -* **API key required:** +Este proyecto está bajo la licencia **GNU GPL 3.0**. Puedes usar, modificar y distribuir el código libremente. - A Google API key is required, get it yours here: - https://developers.google.com/maps/documentation/geolocation/intro +--- +## 📞 **Contacto** -* **Screenshot from a previous version (without argparse):** +¿Preguntas o sugerencias? - ![Python Wi-Fi Positioning System](https://raw.githubusercontent.com/initbrain/Python-Wi-Fi-Positioning-System/master/demo/GNU_Linux_20170108.png) +- **Autor Original:** [initbrain](https://github.com/initbrain) +- **Fork:** [lefcgis](https://github.com/lefcgis) +``` -* **Installation on GNU/Linux and OpenBSD ($: user, #: root):** +#### **¿Dónde guardar el archivo?** +Guarda este contenido en un archivo llamado `README.md` en la raíz de tu proyecto. - On GNU/Linux, the script use the `iw` command to gain access to the Wi-Fi peripheral. - On OpenBSD, it use the `ifconfig` command to gain access to the Wi-Fi peripheral. - On Ubuntu distribution, it require the installation of the following dependencies: +--- - # apt-get install iw +### **2. Reemplazar `commands` por `subprocess`** - On Gentoo distribution, it require the installation of the following dependencies: +#### **¿Qué hacer?** +El módulo `commands` está obsoleto en Python 3. Lo reemplazaremos por `subprocess`, que es la forma moderna de ejecutar comandos del sistema. - # emerge -av net-wireless/iw +#### **Cambios en el Código** +Busca todas las instancias de `commands.getstatusoutput` o `commands.getoutput` en el archivo `wifi_positioning_system.py` y reemplázalas por `subprocess.run`. - The full git repository is at: - Get it using the following command: +**Ejemplo de cambio:** +```python +# Antes (Python 2): +import commands +status, result = commands.getstatusoutput("iw dev wlan0 scan") - $ git clone https://github.com/initbrain/Python-Wi-Fi-Positioning-System +# Después (Python 3): +import subprocess +result = subprocess.run(["iw", "dev", "wlan0", "scan"], capture_output=True, text=True) +status = result.returncode +output = result.stdout +``` - And proceed to final steps. - On GNU/Linux, to use the tool, launch it with `root` privileges: +#### **Notas Importantes** - $ cd Python-Wi-Fi-Positioning-System/ - $ python wifi_positioning_system.py -h - # python wifi_positioning_system.py --api-key -i --verbose --json-prettify --with-overview --map-type +- `capture_output=True` captura la salida estándar y de error. +- `text=True` devuelve la salida como una cadena de texto (en lugar de bytes). +- `result.returncode` es el código de estado (0 = éxito, otros = error). +--- -* **Installation on Mac OS X:** +### **3. Reemplazar `urllib2` por `requests**` - On Mac OS X, it use `airport` to gain access to the Wi-Fi peripheral. - Get it using... +#### **¿Qué hacer?** - % TODO +`urllib2` está obsoleto en Python 3. Usaremos `requests`, que es más sencillo y potente. - And proceed to final steps. +#### **Cambios en el Código** + +Busca el código que usa `urllib2` para hacer peticiones a la API de Google y reemplázalo por `requests`. + +**Ejemplo de cambio:** + +```python +# Antes (Python 2): +import urllib2 +import simplejson + +json_data = simplejson.JSONEncoder().encode(location_request) +http_request = urllib2.Request('https://www.googleapis.com/geolocation/v1/geolocate?key=' + API_KEY) +http_request.add_header('Content-Type', 'application/json') +api_result = simplejson.loads(urllib2.urlopen(http_request, json_data).read()) + +# Después (Python 3): +import requests +import json + +response = requests.post( + f'https://www.googleapis.com/geolocation/v1/geolocate?key={API_KEY}', + json=location_request, + headers={'Content-Type': 'application/json'} +) +response.raise_for_status() # Lanza un error si la petición falla +api_result = response.json() +``` + +#### **Notas Importantes** + +- `requests.post` envía una petición POST con los datos en formato JSON. +- `response.raise_for_status()` lanza una excepción si el código HTTP es >= 400 (ej: 404, 500). +- `response.json()` convierte la respuesta JSON a un diccionario de Python. + +--- + +--- + +## 🟡 **Mejoras Intermedias** + +--- + +### **4. Hacer el Código Compatible con Python 3** + +#### **¿Qué hacer?** + +El código actual usa sintaxis de Python 2, que no es compatible con Python 3. Debemos hacer los siguientes cambios: + +1. `**print` como función:** + - **Antes:** `print "Hola"` + - **Después:** `print("Hola")` +2. `**xrange` por `range`:** + - **Antes:** `for i in xrange(10):` + - **Después:** `for i in range(10):` +3. **Manejo de cadenas de texto:** + - En Python 2, las cadenas son `str` por defecto. En Python 3, son `str` (Unicode) por defecto, pero a veces se usan `bytes`. Asegúrate de que todas las cadenas sean `str`. +4. **División entera:** + - **Antes:** `5 / 2` devuelve `2` (división entera). + - **Después:** `5 // 2` devuelve `2` (división entera). `5 / 2` devuelve `2.5` (división flotante). + +#### **Ejemplo de Cambios en el Código** + +Busca en el archivo `wifi_positioning_system.py` todas las instancias de: + +- `print "..."` → `print("...")` +- `xrange(...)` → `range(...)` +- `except Exception, e:` → `except Exception as e:` + +--- + +### **5. Manejo de Errores para la API de Google** + +#### **¿Qué hacer?** + +Añadir manejo de errores para casos como: + +- Clave API inválida. +- Límite de solicitudes alcanzado. +- Conexión a internet fallida. + +#### **Código para Añadir** + +Reemplaza el código que hace la petición a la API de Google por esto: + +```python +try: + response = requests.post( + f'https://www.googleapis.com/geolocation/v1/geolocate?key={API_KEY}', + json=location_request, + headers={'Content-Type': 'application/json'}, + timeout=10 # Tiempo máximo de espera en segundos + ) + response.raise_for_status() + api_result = response.json() + + # Validar que la respuesta tenga los campos esperados + if 'location' not in api_result or 'accuracy' not in api_result: + raise ValueError("Respuesta inválida de la API de Google") + +except requests.exceptions.RequestException as e: + print(f"[!] Error al conectar con la API de Google: {e}") + if "403" in str(e): + print("[!] Verifica que tu clave API sea válida y que no hayas alcanzado el límite de solicitudes.") + elif "timeout" in str(e).lower(): + print("[!] Tiempo de espera agotado. Verifica tu conexión a internet.") + exit(1) +except ValueError as e: + print(f"[!] Error en la respuesta de la API: {e}") + exit(1) +``` + +--- + +### **6. Modo Demo con Datos de Ejemplo** + +#### **¿Qué hacer?** + +Añadir un archivo `demo_data.json` con datos de ejemplo para que los usuarios puedan probar el script sin escanear redes Wi-Fi. + +#### **Paso 1: Crear el Archivo `demo_data.json**` + +```json +{ + "wifiAccessPoints": [ + {"macAddress": "00-11-22-33-44-55", "signalStrength": -40}, + {"macAddress": "AA-BB-CC-DD-EE-FF", "signalStrength": -50}, + {"macAddress": "11-22-33-44-55-66", "signalStrength": -60}, + {"macAddress": "FF-EE-DD-CC-BB-AA", "signalStrength": -70} + ] +} +``` + +#### **Paso 2: Modificar el Script para Usar el Modo Demo** + +Busca la parte del código que maneja el modo demo y modifícala para que use los datos del archivo JSON: + +```python +if args.demo: + try: + with open('demo_data.json', 'r') as f: + demo_data = json.load(f) + wifi_data = [(ap['macAddress'], ap['signalStrength']) for ap in demo_data['wifiAccessPoints']] + print("[+] Usando datos de ejemplo para modo demo") + except FileNotFoundError: + print("[!] Archivo demo_data.json no encontrado. Usando datos predeterminados.") + wifi_data = [ + ('00-11-22-33-44-55', -40), + ('AA-BB-CC-DD-EE-FF', -50), + ('11-22-33-44-55-66', -60) + ] +``` + +--- + +### **7. Mejorar la Visualización del Mapa con Folium** + +#### **¿Qué hacer?** + +Reemplazar la generación del mapa HTML con Google Maps API por **Folium**, que no requiere clave API y genera mapas interactivos offline. + +#### **Paso 1: Instalar Folium** + +```bash +pip install folium +``` + +#### **Paso 2: Añadir una Función para Crear el Mapa con Folium** + +Añade esta función al archivo `wifi_positioning_system.py`: + +```python +def create_map_folium(api_result, filename='wifi_location.html'): + """ + Crea un mapa interactivo usando Folium. + + Args: + api_result (dict): Resultado de la API de Google (debe contener 'location' y 'accuracy'). + filename (str): Nombre del archivo HTML de salida. + """ + import folium + + # Coordenadas del centro del mapa + lat = api_result['location']['lat'] + lng = api_result['location']['lng'] + accuracy = api_result['accuracy'] + + # Crear el mapa + m = folium.Map(location=[lat, lng], zoom_start=18) + + # Añadir un círculo para la precisión + folium.Circle( + radius=accuracy, + location=[lat, lng], + color='blue', + fill=True, + fill_color='blue', + fill_opacity=0.3, + popup=f"Precisión: {accuracy} metros" + ).add_to(m) + + # Añadir un marcador + folium.Marker( + location=[lat, lng], + popup=f"Latitud: {lat}, Longitud: {lng}" + ).add_to(m) + + # Guardar el mapa + m.save(filename) + print(f"[+] Mapa guardado en {filename}") +``` + +#### **Paso 3: Modificar el Código para Usar Folium** + +Busca la parte del código que genera el mapa con Google Maps API y reemplázala por la llamada a `create_map_folium`: + +```python +if args.with_overview: + create_map_folium(api_result, filename='wifi_location.html') +``` + +#### **Notas Importantes** + +- Folium genera un archivo HTML que puedes abrir en cualquier navegador **sin necesidad de conexión a internet**. +- El mapa es **interactivo**: puedes hacer zoom, moverte y hacer clic en los marcadores. + +--- + +--- + +## 🔴 **Mejoras Avanzadas** + +--- - % python wifi_positioning_system.py --api-key --verbose --json-prettify --with-overview --map-type +### **8. Soporte para Windows** +#### **¿Qué hacer?** -* **Usage:** +Añadir soporte para Windows usando la librería `pywifi`, que permite escanear redes Wi-Fi en este sistema operativo. - On GNU/Linux and OpenBSD, a Wi-Fi interface name is required to scan with `iw` and `ifconfig` commands: +#### **Paso 1: Instalar `pywifi**` - usage: wifi_positioning_system.py [-h] [-V] [-L] [-v] [-k API_KEY] [-p] [-o] - [-m {ROADMAP,SATELLITE,HYBRID,TERRAIN}] - (-i WIFI_INTERFACE | --demo) +```bash +pip install pywifi +``` - optional arguments: - -h, --help show this help message and exit - -V, --version show program's version number and exit - -L, --license show program's license details and exit - -v, --verbose enable verbose messages - -k API_KEY, --api-key API_KEY - Google Geolocation API key (could be hardcoded) - -p, --json-prettify prettify JSON output - -o, --with-overview accuracy overview file generation - -m {ROADMAP,SATELLITE,HYBRID,TERRAIN}, --map-type {ROADMAP,SATELLITE,HYBRID,TERRAIN} - accuracy overview map type +#### **Paso 2: Añadir una Función para Escanear Redes Wi-Fi en Windows** - required arguments: - -i WIFI_INTERFACE specify Wi-Fi scan interface - --demo demo mode - West Norwood (London) +Añade esta función al archivo `wifi_positioning_system.py`: - additional informations: - ROADMAP displays the default road map view - SATELLITE displays Google Earth satellite images - HYBRID displays a mixture of normal and satellite views - (this is the default map type) - TERRAIN displays a physical map based on terrain information +```python +def scan_wifi_windows(): + """ + Escanea redes Wi-Fi en Windows usando pywifi. + + Returns: + list: Lista de tuplas (MAC, señal) de las redes Wi-Fi detectadas. + """ + import pywifi + from pywifi import const + + wifi = pywifi.PyWiFi() + iface = wifi.interfaces()[0] # Usa la primera interfaz Wi-Fi disponible + + # Escanear redes + iface.scan() + results = iface.scan_results() + + # Procesar resultados + wifi_data = [] + for network in results: + # Filtrar redes sin SSID o BSSID + if network.bssid and network.signal: + # Reemplazar ':' por '-' en el BSSID (formato requerido por la API de Google) + mac = network.bssid.replace(':', '-') + wifi_data.append((mac, network.signal)) + + return wifi_data +``` - On Mac OS X, there is no need of a Wi-Fi interface name to launch a scan with `airport`: +#### **Paso 3: Modificar la Función `check_prerequisites` para Soporte en Windows** - usage: wifi_positioning_system.py [-h] [-V] [-L] [-v] [-k API_KEY] [-p] [-o] - [-m {ROADMAP,SATELLITE,HYBRID,TERRAIN}] - [--demo] +Busca la función `check_prerequisites` y añade el caso para Windows: - optional arguments: - -h, --help show this help message and exit - -V, --version show program's version number and exit - -L, --license show program's license details and exit - -v, --verbose enable verbose messages - -k API_KEY, --api-key API_KEY - Google Geolocation API key (could be hardcoded) - -p, --json-prettify prettify JSON output - -o, --with-overview accuracy overview file generation - -m {ROADMAP,SATELLITE,HYBRID,TERRAIN}, --map-type {ROADMAP,SATELLITE,HYBRID,TERRAIN} - accuracy overview map type - --demo demo mode - West Norwood (London) +```python +# Dentro de check_prerequisites() +elif sys.platform == 'win32': + try: + import pywifi + wifi_scan_method = 'pywifi' + except ImportError: + print("Error: pywifi no está instalado. Instálalo con 'pip install pywifi'.") + exit(1) +``` - additional informations: - ROADMAP displays the default road map view - SATELLITE displays Google Earth satellite images - HYBRID displays a mixture of normal and satellite views - (this is the default map type) - TERRAIN displays a physical map based on terrain information +#### **Paso 4: Modificar la Función `get_signal_strengths` para Usar `pywifi**` +Añade el caso para `pywifi` en la función `get_signal_strengths`: -* **Contributors (ext-repo):** +```python +# Dentro de get_signal_strengths() +if wifi_scan_method == 'pywifi': + return scan_wifi_windows() +``` + +#### **Paso 5: Añadir el Argumento `--windows**` + +Modifica la función `get_arguments` para añadir el argumento `--windows`: + +```python +parser.add_argument('--windows', action="store_true", + help='Usar modo Windows (escanea redes Wi-Fi con pywifi)', + default=False) +``` + +#### **Paso 6: Modificar el Código Principal para Manejar Windows** + +En la parte principal del script, añade la lógica para Windows: + +```python +if args.windows: + wifi_scan_method = 'pywifi' +else: + wifi_scan_method = check_prerequisites() +``` + +#### **Notas Importantes** + +- `pywifi` puede requerir **permisos de administrador** en Windows para escanear redes Wi-Fi. +- Si el usuario no tiene permisos, el script debe mostrar un mensaje claro. + +--- + +### **9. Pruebas Unitarias** + +#### **¿Qué hacer?** + +Crear pruebas unitarias para validar el funcionamiento del script. Usaremos `unittest`, que viene incluido en Python. + +#### **Paso 1: Crear el Archivo `test_wifi_positioning.py**` + +```python +import unittest +import json +import os +import sys +from wifi_positioning_system import get_signal_strengths, create_map_folium + +class TestWifiPositioning(unittest.TestCase): + + def test_demo_mode(self): + """Prueba que el modo demo devuelve datos válidos.""" + # Simular argumentos para modo demo + class Args: + demo = True + verbose = False + global args + args = Args() + + # Cargar datos de ejemplo + with open('demo_data.json', 'r') as f: + demo_data = json.load(f) + wifi_data = [(ap['macAddress'], ap['signalStrength']) for ap in demo_data['wifiAccessPoints']] + + # Validar que los datos no estén vacíos + self.assertIsInstance(wifi_data, list) + self.assertGreater(len(wifi_data), 0) + + # Validar que cada elemento sea una tupla (MAC, señal) + for item in wifi_data: + self.assertIsInstance(item, tuple) + self.assertIsInstance(item[0], str) + self.assertIsInstance(item[1], int) + + def test_create_map_folium(self): + """Prueba que la función create_map_folium genera un archivo HTML válido.""" + api_result = { + 'location': {'lat': -12.0464, 'lng': -77.0428}, # Lima, Perú + 'accuracy': 50.0 + } + filename = 'test_map.html' + create_map_folium(api_result, filename) + + # Validar que el archivo se creó + self.assertTrue(os.path.exists(filename)) + + # Validar que el archivo no está vacío + with open(filename, 'r') as f: + content = f.read() + self.assertGreater(len(content), 0) + + # Limpiar: eliminar el archivo de prueba + os.remove(filename) + +if __name__ == '__main__': + unittest.main() +``` + +#### **Paso 2: Ejecutar las Pruebas** + +```bash +python -m unittest test_wifi_positioning.py +``` + +#### **Notas Importantes** + +- Las pruebas validan que: + - El modo demo devuelve datos válidos. + - La función `create_map_folium` genera un archivo HTML válido. +- Puedes añadir más pruebas para otras funciones (ej: `scan_wifi_windows`, manejo de errores). + +--- + +### **10. Soporte para Otras APIs de Geolocalización** + +#### **¿Qué hacer?** + +Añadir soporte para la **API de Mozilla Location Service (MLS)**, que es gratuita y no requiere clave API. + +#### **Paso 1: Añadir una Función para la API de Mozilla** + +```python +def get_geolocation_mozilla(wifi_data, api_key=None): + """ + Obtiene la ubicación usando la API de Mozilla Location Service. + + Args: + wifi_data (list): Lista de tuplas (MAC, señal). + api_key (str): No se usa en Mozilla, pero se incluye por compatibilidad. + + Returns: + dict: Resultado de la API con 'location' y 'accuracy'. + """ + import requests + + # Formatear los datos para la API de Mozilla + mls_request = { + "data": { + "wifi": [ + {"macAddress": mac, "signalStrength": signal} + for mac, signal in wifi_data + ] + } + } + + # URL de la API de Mozilla + url = "https://location.services.mozilla.com/v1/geolocate?key=test" + + try: + response = requests.post(url, json=mls_request) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"[!] Error al conectar con la API de Mozilla: {e}") + return None +``` + +#### **Paso 2: Añadir un Argumento para Elegir la API** + +Modifica la función `get_arguments` para añadir el argumento `--api-provider`: + +```python +parser.add_argument('--api-provider', choices=['google', 'mozilla'], default='google', + help='Proveedor de la API de geolocalización (google o mozilla)') +``` + +#### **Paso 3: Modificar el Código Principal para Usar la API Elegida** + +```python +if args.api_provider == 'google': + api_result = send_to_google_api(wifi_data, API_KEY) +elif args.api_provider == 'mozilla': + api_result = get_geolocation_mozilla(wifi_data) +``` + +#### **Notas Importantes** + +- La API de Mozilla es **gratuita** y no requiere clave API (el parámetro `key=test` es solo para evitar errores). +- La precisión puede ser menor que la de Google, pero es una buena alternativa. + +--- + +### **11. Refactorizar el Código** + +#### **¿Qué hacer?** + +Organizar el código en clases para que sea más legible y mantenible. Por ejemplo: + +- `WifiScanner`: Para escanear redes Wi-Fi. +- `GeolocationAPI`: Para interactuar con las APIs de geolocalización. +- `MapGenerator`: Para generar mapas. + +#### **Ejemplo de Refactorización** + +```python +class WifiScanner: + """Clase para escanear redes Wi-Fi.""" + + def __init__(self, interface=None, os_type=None): + self.interface = interface + self.os_type = os_type + + def scan(self): + """Escanea redes Wi-Fi según el sistema operativo.""" + if self.os_type == 'linux': + return self._scan_linux() + elif self.os_type == 'windows': + return self._scan_windows() + elif self.os_type == 'darwin': + return self._scan_mac() + elif self.os_type == 'openbsd': + return self._scan_openbsd() + else: + raise NotImplementedError(f"Sistema operativo {self.os_type} no soportado") + + def _scan_linux(self): + """Escanea redes Wi-Fi en Linux usando iw.""" + import subprocess + command = f"iw dev {self.interface} scan" + result = subprocess.run(command.split(), capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError(f"Error al escanear redes Wi-Fi: {result.stderr}") + # Procesar resultado (ejemplo simplificado) + return [("00-11-22-33-44-55", -40)] # Datos de ejemplo + + def _scan_windows(self): + """Escanea redes Wi-Fi en Windows usando pywifi.""" + import pywifi + wifi = pywifi.PyWiFi() + iface = wifi.interfaces()[0] + iface.scan() + results = iface.scan_results() + return [(network.bssid.replace(':', '-'), network.signal) for network in results] + + +class GeolocationAPI: + """Clase para interactuar con APIs de geolocalización.""" + + def __init__(self, api_key=None, provider='google'): + self.api_key = api_key + self.provider = provider + + def get_location(self, wifi_data): + """Obtiene la ubicación usando el proveedor seleccionado.""" + if self.provider == 'google': + return self._get_location_google(wifi_data) + elif self.provider == 'mozilla': + return self._get_location_mozilla(wifi_data) + else: + raise NotImplementedError(f"Proveedor {self.provider} no soportado") + + def _get_location_google(self, wifi_data): + """Usa la API de Google.""" + import requests + url = f"https://www.googleapis.com/geolocation/v1/geolocate?key={self.api_key}" + request_data = { + 'considerIp': False, + 'wifiAccessPoints': [ + {"macAddress": mac, "signalStrength": signal} + for mac, signal in wifi_data + ] + } + response = requests.post(url, json=request_data) + response.raise_for_status() + return response.json() + + +class MapGenerator: + """Clase para generar mapas.""" + + @staticmethod + def create_map(api_result, filename='wifi_location.html'): + """Crea un mapa usando Folium.""" + import folium + lat = api_result['location']['lat'] + lng = api_result['location']['lng'] + accuracy = api_result['accuracy'] + + m = folium.Map(location=[lat, lng], zoom_start=18) + folium.Circle( + radius=accuracy, + location=[lat, lng], + color='blue', + fill=True, + fill_opacity=0.3 + ).add_to(m) + m.save(filename) +``` + +#### **Paso 2: Usar las Clases en el Script Principal** + +```python +if __name__ == "__main__": + args = get_arguments() + + # Determinar el sistema operativo + if sys.platform == 'win32': + os_type = 'windows' + elif sys.platform.startswith('linux'): + os_type = 'linux' + elif sys.platform == 'darwin': + os_type = 'darwin' + elif sys.platform.startswith('openbsd'): + os_type = 'openbsd' + else: + print(f"[!] Sistema operativo {sys.platform} no soportado") + exit(1) + + # Escanear redes Wi-Fi + scanner = WifiScanner(interface=args.wifi_interface if hasattr(args, 'wifi_interface') else None, os_type=os_type) + wifi_data = scanner.scan() + + # Obtener ubicación + geolocation = GeolocationAPI(api_key=args.api_key, provider=args.api_provider) + api_result = geolocation.get_location(wifi_data) + + # Generar mapa + if args.with_overview: + MapGenerator.create_map(api_result) +``` + +#### **Notas Importantes** + +- La refactorización hace que el código sea **más modular y fácil de mantener**. +- Cada clase tiene una **responsabilidad clara** (principio de diseño SOLID). + +--- + +### **12. Sistema de Logging** + +#### **¿Qué hacer?** + +Reemplazar los `print` por un sistema de logging para controlar mejor los mensajes (ej: guardar logs en un archivo). + +#### **Paso 1: Configurar el Logging** + +Añade esto al inicio del script `wifi_positioning_system.py`: + +```python +import logging + +# Configurar el logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), # Mostrar mensajes en la consola + logging.FileHandler('wifi_positioning.log') # Guardar mensajes en un archivo + ] +) +logger = logging.getLogger(__name__) +``` + +#### **Paso 2: Reemplazar `print` por `logger**` + +Busca todos los `print` en el script y reemplázalos por `logger.info`, `logger.error`, etc. Ejemplos: + +```python +# Antes: +print("[+] Scanning nearby Wi-Fi networks...") + +# Después: +logger.info("Scanning nearby Wi-Fi networks...") + +# Antes: +print("[!] Error al conectar con la API de Google") + +# Después: +logger.error("Error al conectar con la API de Google") +``` + +#### **Notas Importantes** + +- `logger.info`: Para mensajes informativos (ej: "Escaneando redes Wi-Fi..."). +- `logger.error`: Para mensajes de error (ej: "Clave API inválida"). +- `logger.debug`: Para mensajes de depuración (útil durante el desarrollo). +- Los mensajes se guardan en el archivo `wifi_positioning.log`. + +--- + +--- + +## 📤 **Pasos para Contribuir en GitHub** + +### **1. Clonar tu Fork** + +```bash +git clone https://github.com/TU_USUARIO/Python-Wi-Fi-Positioning-System.git +cd Python-Wi-Fi-Positioning-System +``` + +### **2. Crear una Rama para tus Cambios** + +```bash +git checkout -b mejora-soporte-windows +``` + +### **3. Hacer tus Cambios** + +- Edita los archivos según las mejoras que quieras implementar. +- Prueba que todo funcione: + ```bash + python wifi_positioning_system.py --demo + ``` + +### **4. Hacer Commit de tus Cambios** + +```bash +git add . +git commit -m "Añade soporte para Windows usando pywifi" +``` + +### **5. Subir tus Cambios a GitHub** + +```bash +git push origin mejora-soporte-windows +``` + +### **6. Abrir un Pull Request** + +1. Ve a tu fork en GitHub: `https://github.com/TU_USUARIO/Python-Wi-Fi-Positioning-System`. +2. Haz clic en **"Pull requests"** > **"New pull request"**. +3. Selecciona: + - **Base repository:** `lefcgis/Python-Wi-Fi-Positioning-System` (rama `master`). + - **Head repository:** `TU_USUARIO/Python-Wi-Fi-Positioning-System` (rama `mejora-soporte-windows`). +4. Escribe un **título claro** (ej: "Añade soporte para Windows usando pywifi"). +5. Escribe una **descripción detallada** de los cambios: + ```markdown + ## ¿Qué hace este pull request? + - Añade soporte para Windows usando la librería `pywifi`. + - Actualiza el código para que sea compatible con Python 3. + - Reemplaza `commands` por `subprocess`. + + ## ¿Cómo probarlo? + ``` + 1. Ejecuta `pip install pywifi`. + 2. Ejecuta el script con: `python wifi_positioning_system.py --windows --api-key TU_CLAVE_API`. + Notas adicionales + Se probó en Windows 10 con Python 3.10. + Se actualizó el README.md con instrucciones para Windows. + ` +6. Haz clic en **"Create pull request"**. + +--- + +--- + +## 📁 **Estructura Final del Proyecto** + +Después de implementar todas las mejoras, la estructura de tu proyecto debería verse así: + +``` +Python-Wi-Fi-Positioning-System/ +├── wifi_positioning_system.py # Script principal (actualizado) +├── demo_data.json # Datos de ejemplo para modo demo +├── test_wifi_positioning.py # Pruebas unitarias +├── README.md # Documentación mejorada +├── requirements.txt # Dependencias de Python +└── wifi_positioning.log # Archivo de log (generado al ejecutar) +``` + +--- + +## 📄 **Archivo `requirements.txt**` + +Crea un archivo `requirements.txt` para listar todas las dependencias del proyecto: + +``` +requests>=2.25.1 +folium>=0.12.1 +pywifi>=1.1.12 +``` + +--- + +--- + +## 🎓 **Recursos Adicionales** + +### **Documentación Oficial** + +- [Python 3 Documentation](https://docs.python.org/3/) +- [Subprocess Module](https://docs.python.org/3/library/subprocess.html) +- [Requests Library](https://docs.python-requests.org/) +- [Folium Library](https://python-visualization.github.io/folium/) +- [PyWiFi Library](https://pywifi.readthedocs.io/) +- [Google Geolocation API](https://developers.google.com/maps/documentation/geolocation/overview) +- [Mozilla Location Service](https://location.services.mozilla.com/) + +### **Tutoriales Útiles** + +- [GitHub Guides](https://guides.github.com/) +- [How to Write a Good README](https://bulldogjob.com/news/449-how-to-write-a-good-readme-for-your-github-project) +- [Python Logging Guide](https://realpython.com/python-logging/) +- [Unit Testing in Python](https://realpython.com/python-testing/) + +--- + +--- + +## 📌 **Resumen de Acciones Recomendadas** + + +| **Prioridad** | **Mejora** | **Dificultad** | **Tiempo Estimado** | +| ------------- | --------------------------------------- | -------------- | ------------------- | +| Alta | Actualizar README.md | Fácil | 1 hora | +| Alta | Reemplazar `commands` por `subprocess` | Fácil | 1 hora | +| Alta | Reemplazar `urllib2` por `requests` | Fácil | 1 hora | +| Alta | Hacer el código compatible con Python 3 | Intermedia | 2 horas | +| Media | Manejo de errores para la API de Google | Intermedia | 2 horas | +| Media | Modo demo con datos de ejemplo | Intermedia | 1 hora | +| Media | Mejorar visualización con Folium | Intermedia | 2 horas | +| Media | Añadir soporte para Windows | Avanzada | 3 horas | +| Baja | Pruebas unitarias | Avanzada | 3 horas | +| Baja | Soporte para otras APIs (Mozilla) | Avanzada | 3 horas | +| Baja | Refactorizar el código | Avanzada | 4 horas | +| Baja | Sistema de logging | Intermedia | 2 horas | + + +--- + +--- + +## 💡 **Consejos Finales** + +1. **Empieza por lo fácil**: Actualiza el `README.md` y reemplaza `commands` por `subprocess`. Esto ya será un gran aporte. +2. **Prueba cada cambio**: Después de cada modificación, ejecuta el script para asegurarte de que todo funcione. +3. **Haz commits pequeños**: Cada commit debe contener **un solo cambio lógico** (ej: "Reemplaza urllib2 por requests"). +4. **Documenta tus cambios**: En el mensaje del commit, explica **qué hiciste y por qué**. +5. **Pide feedback**: Si no estás seguro de algo, abre un *pull request* en estado *draft* y pide opiniones. + +--- + +> *"El código es como un jardín: si no lo cuidas, se llena de maleza. Pero con paciencia y dedicación, puede convertirse en algo hermoso."* 🌱 + +--- - Initial publication on [Twitter](https://twitter.com/initbrain/status/215019236102377472) - 2012-06-25: modified to run on OS X - [James Armitage (@armitagej)](https://twitter.com/armitagej) - 2012-06-26: modified to process in python - [Dan Gleebits (@DGleebits)](https://twitter.com/DGleebits) - 2012-10-01: modified to parse the xml output of airport - [Vincent Ohprecio (@BigsnarfDude)](https://twitter.com/BigsnarfDude) - 2017-01-03: modified to work with the new Google Geolocation API - [Giovanni Angoli (@juzam)](https://twitter.com/juzam) - 2017-01-06: merging all modifications - [Julien Deudon (@initbrain)](https://twitter.com/initbrain) - 2017-01-07: modified to support new permissions assignement methods - [Julien Deudon (@initbrain)](https://twitter.com/initbrain) - 2017-01-08: source code reorganization - [Julien Deudon (@initbrain)](https://twitter.com/initbrain) - 2017-01-15: OpenBSD port - [Geoffrey Robert (@mks10110)](https://twitter.com/mks10110) +**¡Mucho éxito con tu contribución!** Si tienes dudas sobre alguna parte, no dudes en preguntar. 😊 \ No newline at end of file diff --git a/demo_data.json b/demo_data.json new file mode 100644 index 0000000..0e6357c --- /dev/null +++ b/demo_data.json @@ -0,0 +1,12 @@ +{ + "wifiAccessPoints": [ + {"macAddress": "00-11-22-33-44-55", "signalStrength": -40}, + {"macAddress": "AA-BB-CC-DD-EE-FF", "signalStrength": -50}, + {"macAddress": "11-22-33-44-55-66", "signalStrength": -60}, + {"macAddress": "FF-EE-DD-CC-BB-AA", "signalStrength": -70}, + {"macAddress": "00-1f-f4-25-ee-30", "signalStrength": -44}, + {"macAddress": "02-fe-f4-25-ee-30", "signalStrength": -48}, + {"macAddress": "00-26-5a-7e-0d-02", "signalStrength": -65}, + {"macAddress": "90-01-3b-30-04-29", "signalStrength": -75} + ] +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8e5597b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +requests>=2.25.1 +folium>=0.12.1 +pywifi>=1.1.12 \ No newline at end of file diff --git a/test_wifi_positioning.py b/test_wifi_positioning.py new file mode 100644 index 0000000..f0ca346 --- /dev/null +++ b/test_wifi_positioning.py @@ -0,0 +1,97 @@ +import unittest +import json +import os +import sys +import tempfile +from wifi_positioning_system import WifiScanner, GeolocationAPI, MapGenerator, prettify_json + + +class TestWifiPositioning(unittest.TestCase): + """Unit tests for the Wi-Fi Positioning System.""" + + def test_demo_data_loading(self): + """Test that demo data can be loaded and formatted correctly.""" + with open('demo_data.json', 'r') as f: + demo_data = json.load(f) + wifi_data = [(ap['macAddress'], ap['signalStrength']) for ap in demo_data['wifiAccessPoints']] + + # Check that wifi_data is a list + self.assertIsInstance(wifi_data, list) + # Check that it's not empty + self.assertGreater(len(wifi_data), 0) + # Check that each item is a tuple with (str, int) + for item in wifi_data: + self.assertIsInstance(item, tuple) + self.assertIsInstance(item[0], str) + self.assertIsInstance(item[1], int) + + def test_prettify_json(self): + """Test JSON prettifying function.""" + test_data = {"key1": "value1", "key2": {"subkey": "subvalue"}} + + # Test without prettify + result = prettify_json(test_data, prettify=False) + self.assertIsInstance(result, str) + self.assertEqual(json.loads(result), test_data) + + # Test with prettify + result = prettify_json(test_data, prettify=True) + self.assertIsInstance(result, str) + self.assertIn('\n', result) # Prettified JSON should have newlines + + def test_map_generation(self): + """Test that the map generation works correctly.""" + api_result = { + 'location': {'lat': -12.0464, 'lng': -77.0428}, # Lima, Perú + 'accuracy': 50.0 + } + + with tempfile.NamedTemporaryFile(suffix='.html', delete=False) as tmp: + filename = tmp.name + + try: + MapGenerator.create_map(api_result, filename) + + # Check that the file was created + self.assertTrue(os.path.exists(filename)) + + # Check that the file is not empty + with open(filename, 'r') as f: + content = f.read() + self.assertGreater(len(content), 0) + finally: + if os.path.exists(filename): + os.remove(filename) + + def test_geolocation_api_google(self): + """Test Google Geolocation API with mock data.""" + # This test will fail if the API key is invalid or if there's no internet connection + # It's more of an integration test than a unit test + wifi_data = [ + ('00-11-22-33-44-55', -40), + ('AA-BB-CC-DD-EE-FF', -50) + ] + + # Skip this test if no API key is provided + api_key = os.environ.get('GOOGLE_API_KEY') + if not api_key or api_key == 'YOUR_KEY': + self.skipTest("No valid Google API key provided") + + geolocation = GeolocationAPI(api_key=api_key, provider='google') + result = geolocation.get_location(wifi_data) + + # Check that the result contains the expected fields + self.assertIn('location', result) + self.assertIn('accuracy', result) + + def test_wifi_scanner_demo(self): + """Test WifiScanner with demo data.""" + scanner = WifiScanner(os_type='demo') + + # This should raise NotImplementedError for demo OS type + with self.assertRaises(NotImplementedError): + scanner.scan() + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/wifi_positioning_system.py b/wifi_positioning_system.py index 13c1f43..1273947 100755 --- a/wifi_positioning_system.py +++ b/wifi_positioning_system.py @@ -3,401 +3,284 @@ """ Python Wi-Fi Positioning System - Wi-Fi geolocation script using the Google Maps Geolocation API -@author: Julien Deudon - -@copyright: Copyright 2017, Julien Deudon - +@author: Julien Deudon, Luis Eduardo Ferrer Cruz +@copyright: Copyright 2017-2026, Julien Deudon, Luis Eduardo Ferrer Cruz @license: GNU GPL 3.0 - -@contact: initbrain@gmail.com +@contact: initbrain@gmail.com, luis.ferrer.c@uni.pe """ -from argparse import ArgumentParser, RawDescriptionHelpFormatter -from commands import getoutput, getstatusoutput -import simplejson -import textwrap -import urllib2 -import grp -import sys +import argparse +import json import os import re +import sys +import logging +import subprocess +from argparse import RawDescriptionHelpFormatter +import textwrap + try: import xml.etree.cElementTree as ET except ImportError: import xml.etree.ElementTree as ET -__version__ = "0.1.23" +try: + import requests +except ImportError: + print("Error: 'requests' library is required. Install it with: pip install requests") + exit(1) + +try: + import folium +except ImportError: + print("Warning: 'folium' library is not installed. Maps will not be generated. Install it with: pip install folium") -# A Google Maps Geolocation API key is required, get it yours here: -# https://developers.google.com/maps/documentation/geolocation/intro +try: + import pywifi +except ImportError: + pass # pywifi is only required for Windows + +__version__ = "0.2.0" + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('wifi_positioning.log') + ] +) +logger = logging.getLogger(__name__) + +# A Google Maps Geolocation API key is required API_KEY = os.environ.get('GOOGLE_API_KEY') or 'YOUR_KEY' -# Alexander Mylnikov API -# https://www.mylnikov.org/archives/1170 -# Too many gateway time-out (HTTP error 504) -# TODO support the WiGLE API -# https://wigle.net/ +class WifiScanner: + """Class to scan Wi-Fi networks.""" -# TODO check the cool display of iSniff-GPS -# https://raw.githubusercontent.com/hubert3/iSniff-GPS/master/iSniff_GPS_Apple_WLOC_screenshot.jpg + def __init__(self, interface=None, os_type=None): + self.interface = interface + self.os_type = os_type -# TODO check http://www.minigps.net/cellsearch.html -# TODO check if there is another API from Open Street Map ? + def scan(self): + """Scan Wi-Fi networks based on the operating system.""" + if self.os_type == 'linux': + return self._scan_linux() + elif self.os_type == 'windows': + return self._scan_windows() + elif self.os_type == 'darwin': + return self._scan_mac() + elif self.os_type == 'openbsd': + return self._scan_openbsd() + else: + raise NotImplementedError(f"Operating system {self.os_type} is not supported") + def _scan_linux(self): + """Scan Wi-Fi networks on Linux using iw.""" + iw_command = f"iw dev {self.interface} scan" + result = subprocess.run(iw_command.split(), capture_output=True, text=True) -def get_scriptpath(): - pathname = os.path.dirname(sys.argv[0]) - fullpath = os.path.abspath(pathname) - - if not fullpath.endswith('/'): - fullpath += '/' + if result.returncode != 0: + logger.error(f"Unable to scan for Wi-Fi networks! Command: {iw_command}") + logger.error(f"Error: {result.stderr}") + exit(1) - return fullpath + parsing_result = re.compile( + r"BSS ([\w\d\:]+).*\n.*\n.*\n.*\n.*\n\tsignal: ([-\d]+)", + re.MULTILINE + ).findall(result.stdout) + + wifi_data = [(bss[0].replace(':', '-'), int(bss[1])) for bss in parsing_result] + return wifi_data + + def _scan_windows(self): + """Scan Wi-Fi networks on Windows using pywifi.""" + try: + wifi = pywifi.PyWiFi() + iface = wifi.interfaces()[0] # Use the first available Wi-Fi interface + iface.scan() + results = iface.scan_results() + + wifi_data = [] + for network in results: + if network.bssid and network.signal: + mac = network.bssid.replace(':', '-') + wifi_data.append((mac, network.signal)) + + return wifi_data + except Exception as e: + logger.error(f"Error scanning Wi-Fi networks on Windows: {e}") + exit(1) + def _scan_mac(self): + """Scan Wi-Fi networks on Mac OS X using airport.""" + airport_xml_cmd = '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport --scan -x' + result = subprocess.run(airport_xml_cmd.split(), capture_output=True, text=True) -def prettify_json(json_data): - if args.json_prettify: - return '\n'.join([l.rstrip() for l in simplejson.dumps(json_data, sort_keys=True, indent=4*' ').splitlines()]) - else: - return simplejson.dumps(json_data) - - -def create_overview(api_result, map_type, filename='Wifi_geolocation.html', filepath=get_scriptpath()): - """ROADMAP displays the default road map view - SATELLITE displays Google Earth satellite images - HYBRID displays a mixture of normal and satellite views - (this is the default map type) - TERRAIN displays a physical map based on terrain information""" - - html = """ - - - - - - - - Geolocation - - - - -
- - """ - - # TODO parameters for output file path / name modification - with open(filepath+filename, 'wb') as overview: - overview.write(html) - - if args.verbose: - print filepath + filename - - # Keep the owners - scriptpath = get_scriptpath() - script_uid = os.stat(scriptpath+sys.argv[0]).st_uid - script_gid = os.stat(scriptpath+sys.argv[0]).st_gid - overview_uid = os.stat(filepath+filename).st_uid - overview_gid = os.stat(filepath+filename).st_gid - if overview_uid != script_uid or overview_gid != script_gid: - os.chown(filepath+filename, script_uid, script_gid) - - -def get_signal_strengths(wifi_scan_method): - wifi_data = [] - - # GNU/Linux - if wifi_scan_method is 'iw': - iw_command = 'iw dev %s scan' % (args.wifi_interface) - iw_scan_status, iw_scan_result = getstatusoutput(iw_command) - - if iw_scan_status != 0: - print "[!] Unable to scan for Wi-Fi networks !" - print "Used command: '%s'" % iw_command - print "Result:\n" + '\n'.join(iw_scan_result.split('\n')[:10]) - if len(iw_scan_result.split('\n')) > 10: - print "[...]" + if result.returncode != 0: + logger.error(f"Unable to scan for Wi-Fi networks! Command: {airport_xml_cmd}") + logger.error(f"Error: {result.stderr}") exit(1) - else: - parsing_result = re.compile("BSS ([\w\d\:]+).*\n.*\n.*\n.*\n.*\n\tsignal: ([-\d]+)", re.MULTILINE).findall(iw_scan_result) - wifi_data = [(bss[0].replace(':', '-'), int(bss[1])) for bss in parsing_result] + root = ET.fromstring(result.stdout) + networks = root.getchildren()[0] + wifi_data = [(network.find("string").text, abs(int(network.findall("integer")[7].text))) for network in networks] + return wifi_data - # Mac OS X - elif wifi_scan_method is 'airport': - address_match = '([a-fA-F0-9]{1,2}[:|\-]?){6}' # TODO useless ? - airport_xml_cmd = '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport --scan -x' - airport_scan_status, airport_scan_xml = getstatusoutput(airport_xml_cmd) - - if airport_scan_status != 0: - print "[!] Unable to scan for Wi-Fi networks !" - print "Used command: '%s'" % airport_xml_cmd - print "Result:\n" + '\n'.join(airport_scan_xml.split('\n')[:10]) - if len(airport_scan_xml.split('\n')) > 10: - print "[...]" - exit(1) - else: - root = ET.fromstring(airport_scan_xml) - networks = root.getchildren()[0] - - wifi_data = [(network.find("string").text, abs(int(network.findall("integer")[7].text))) for network in networks] - - # OpenBSD - elif wifi_scan_method is 'ifconfig': - ifconfig_cmd = 'ifconfig %s scan' % (args.wifi_interface) - ifconfig_scan_status, ifconfig_scan_result = getstatusoutput(ifconfig_cmd) - - if ifconfig_scan_status != 0: - print "[!] Unable to scan for Wi-Fi networks !" - print "Used command: '%s'" % ifconfig_cmd - print "Result:\n" + '\n'.join(ifconfig_scan_result.split('\n')[:10]) - if len(ifconfig_scan_result.split('\n')) > 10: - print "[...]" + def _scan_openbsd(self): + """Scan Wi-Fi networks on OpenBSD using ifconfig.""" + ifconfig_cmd = f'ifconfig {self.interface} scan' + result = subprocess.run(ifconfig_cmd.split(), capture_output=True, text=True) + + if result.returncode != 0: + logger.error(f"Unable to scan for Wi-Fi networks! Command: {ifconfig_cmd}") + logger.error(f"Error: {result.stderr}") exit(1) - else: - parsing_result = re.compile("nwid\s+[\w-]+\s+chan\s+\d+\s+bssid\s+([\w\d\:]+)\s+([-\d]+)dBm", re.MULTILINE).findall(ifconfig_scan_result) - - wifi_data = [(bss[0].replace(':', '-'), int(bss[1])) for bss in parsing_result] - - return wifi_data - - -def check_prerequisites(): - # Moved arguments check and parsing to get_arguments() - - # Do something/nothing here for different kind of systems - if sys.platform.startswith(('linux', 'netbsd', 'freebsd', 'openbsd')) or sys.platform == 'darwin': - wifi_scan_method = None - perm_cmd = None - - # Do something specific to GNU/Linux - if sys.platform.startswith('linux'): - # If not launched with root permissions - if os.geteuid() != 0: - # First try with 'sudo' - which_sudo_status, which_sudo_result = getstatusoutput('which sudo') - # If 'sudo' is installed and current user in 'sudo' group - if which_sudo_status is 0: - # Like in the ubuntu default sudo configuration - # "Members of the admin group may gain root privileges" - # "Allow members of group sudo to execute any command" - current_user_groups = [grp.getgrgid(g).gr_name for g in os.getgroups()] - if 'sudo' in current_user_groups or \ - 'admin' in current_user_groups: - perm_cmd = 'sudo --preserve-env' - #etc_sudoers_status, etc_sudoers_result = getstatusoutput('sudo cat /etc/sudoers') - # If 'sudo' is configured for the current user - #if etc_sudoers_status is 0: - #perm_cmd = 'sudo' - # If not 'sudo' - if perm_cmd is None: - # Check other methods for getting permissions - for su_gui_cmd in ['gksu', 'kdesu', 'ktsuss', 'beesu', 'su -c', '']: - which_cmd_status, which_cmd_result = getstatusoutput('which '+su_gui_cmd.split()[0]) - # If one is found, keep it in 'su_gui_cmd' var - if which_cmd_status is 0: - break - # If 'su_gui_cmd' var is not empty, we have one ! - if su_gui_cmd: - perm_cmd = su_gui_cmd - else: - print "Error: this script need to be run as root !" - exit(1) - #else: - #print "[+] Current user is '%s'" % os.environ.get('USER') - - # Command available to ask permissions - if perm_cmd: - # Restart as root - #print "[+] This script need to be run as root, current user is '%s'" % os.environ.get('USER') - if args.verbose: - print "[+] Using '" + perm_cmd.split()[0] + "' for asking permissions" - if perm_cmd is 'sudo --preserve-env': - #print perm_cmd.split()[0], perm_cmd.split() + [ - # ' '.join(['./' + sys.argv[0].lstrip('./')]) - # ] + sys.argv[1:] - os.execvp(perm_cmd.split()[0], perm_cmd.split() + [ - ' '.join(['./' + sys.argv[0].lstrip('./')]) - ] + sys.argv[1:]) - else: - #print perm_cmd.split()[0], perm_cmd.split() + [ - # ' '.join(['./' + sys.argv[0].lstrip('./')] + sys.argv[1:]) - # ] - os.execvp(perm_cmd.split()[0], perm_cmd.split() + [ - ' '.join(['./' + sys.argv[0].lstrip('./')] + sys.argv[1:]) - ]) - - which_iw_status, which_iw_result = getstatusoutput('which iw') - if which_iw_status != 0: - print "Missing dependency: 'iw' is needed\n" + \ - " iw - tool for configuring Linux wireless devices" - if 'ubuntu' in getoutput('uname -a').lower(): - print " > sudo apt-get install iw" - # TODO for other distro, see with /etc/*release files ? - elif 'gentoo' in getoutput('cat /etc/*release').lower(): - print " > su -c 'emerge -av net-wireless/iw'" - exit(1) - else: - wifi_scan_method = 'iw' - - # Do something specific to Mac OS X - elif sys.platform == 'darwin': - aiport_path = '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport' - if not os.path.exists(aiport_path): - print "Missing dependency:\n" + \ - " airport - tool for configuring Apple wireless devices from Terminal.app" - exit(1) - else: - wifi_scan_method = 'airport' - - # Do something specific to OpenBSD - # TODO test with NetBSD and FreeBSD - elif sys.platform.startswith(('netbsd', 'freebsd', 'openbsd')): - # See: http://man.openbsd.org/su.1 - - # If the optional shell arguments are provided on the command line, they are passed to the login shell - # of the target login. This allows it to pass arbitrary commands via the -c option as understood by most - # shells. Note that -c usually expects a single argument only; you have to quote it when passing multiple - # words. - - # If group 0 (normally "wheel") has users listed then only those users can su to "root". It is not - # sufficient to change a user's /etc/passwd entry to add them to the "wheel" group; they must explicitly - # be listed in /etc/group. If no one is in the "wheel" group, it is ignored, and anyone who knows the root - # password is permitted to su to "root". - - # If not launched with root permissions - if os.geteuid() != 0: - # Try with 'su -c' if current user in 'wheel' group - # Like in the OpenBSD default su configuration - # "If group 0 (normally "wheel") has users listed then only those users can su to "root"." - # "If no one is in the "wheel" group, it is ignored [...]" - current_user_groups = [grp.getgrgid(g).gr_name for g in os.getgroups()] - if 'wheel' in current_user_groups: - perm_cmd = 'su -c' - else: - # TODO "If no one is in the "wheel" group, it is ignored [...]" ? - print "Error: this script need to be run as root !" - exit(1) - - wifi_scan_method = 'ifconfig' + parsing_result = re.compile( + r"nwid\s+[\w-]+\s+chan\s+\d+\s+bssid\s+([\w\d\:]+)\s+([-\d]+)dBm", + re.MULTILINE + ).findall(result.stdout) + + wifi_data = [(bss[0].replace(':', '-'), int(bss[1])) for bss in parsing_result] + return wifi_data + + +class GeolocationAPI: + """Class to interact with geolocation APIs.""" + + def __init__(self, api_key=None, provider='google'): + self.api_key = api_key + self.provider = provider + + def get_location(self, wifi_data): + """Get location using the selected provider.""" + if self.provider == 'google': + return self._get_location_google(wifi_data) + elif self.provider == 'mozilla': + return self._get_location_mozilla(wifi_data) + else: + raise NotImplementedError(f"Provider {self.provider} is not supported") + + def _get_location_google(self, wifi_data): + """Use Google Geolocation API.""" + url = f"https://geolocation.googleapis.com/v1/geolocate?key={self.api_key}" + location_request = { + 'considerIp': False, + 'wifiAccessPoints': [ + {"macAddress": mac, "signalStrength": signal} + for mac, signal in wifi_data + ] + } + + try: + response = requests.post(url, json=location_request, timeout=10) + response.raise_for_status() + api_result = response.json() + + if 'location' not in api_result or 'accuracy' not in api_result: + raise ValueError("Invalid response from Google API") + + return api_result + except requests.exceptions.RequestException as e: + logger.error(f"Error connecting to Google API: {e}") + if "403" in str(e): + logger.error("Check that your API key is valid and that you have not exceeded the request limit.") + elif "timeout" in str(e).lower(): + logger.error("Request timed out. Check your internet connection.") + return None + + def _get_location_mozilla(self, wifi_data): + """Use Mozilla Location Service API (deprecated, but kept for compatibility).""" + logger.warning("Mozilla Location Service is no longer available. Using demo data instead.") + return None + + +class MapGenerator: + """Class to generate maps.""" + + @staticmethod + def create_map(api_result, filename='wifi_location.html'): + """Create an interactive map using Folium.""" + try: + lat = api_result['location']['lat'] + lng = api_result['location']['lng'] + accuracy = api_result['accuracy'] + + m = folium.Map(location=[lat, lng], zoom_start=18) + + folium.Circle( + radius=accuracy, + location=[lat, lng], + color='blue', + fill=True, + fill_color='blue', + fill_opacity=0.3, + popup=f"Accuracy: {accuracy} meters" + ).add_to(m) + + folium.Marker( + location=[lat, lng], + popup=f"Latitude: {lat}, Longitude: {lng}" + ).add_to(m) + + m.save(filename) + logger.info(f"Map saved to {filename}") + except Exception as e: + logger.error(f"Error generating map: {e}") + + +def prettify_json(json_data, prettify=False): + """Format JSON data for printing.""" + if prettify: + return '\n'.join([l.rstrip() for l in json.dumps(json_data, sort_keys=True, indent=4*' ').splitlines()]) else: - # All other systems - or exception for non-supported system - # Like 'win32'... - # TODO is 'cygwin' could be found on Mac OS X operation systems ? - print "Error: unsupported operating system..." + \ - "\nMicrosoft Windows operating systems are not currently supported, missing Wi-Fi cli tool / library." + \ - "\nIf you use a Mac OS X operating system, the detected plateform could have been 'cygwin'," + \ - "\nplease let us know so we can publish a correction with your help !" - exit(1) + return json.dumps(json_data) - return wifi_scan_method +def get_scriptpath(): + """Get the directory of the current script.""" + pathname = os.path.dirname(sys.argv[0]) + fullpath = os.path.abspath(pathname) + if not fullpath.endswith('/'): + fullpath += '/' + return fullpath -class MyParser(ArgumentParser): + +class MyParser(argparse.ArgumentParser): def error(self, message): - sys.stderr.write('erreur: %s\n\n' % message) - #self.print_help() + sys.stderr.write(f'Error: {message}\n\n') self.print_usage() sys.exit(2) def get_arguments(argv=None): - """Command line options.""" - + """Parse command line arguments.""" if argv is not None: sys.argv.extend(argv) program_name = os.path.basename(sys.argv[0]) - program_version = 'v%s' % __version__ - program_version_message = '%%(prog)s %s' % program_version - program_shortdesc = __import__('__main__').__doc__.split("\n")[1] - program_copyright = 'Copyright (c) 2017 Julien Deudon (initbrain)' + program_version = f'v{__version__}' + program_version_message = f'{program_name} {program_version}' + program_shortdesc = __doc__.split("\n")[1] if __doc__ else "Python Wi-Fi Positioning System" + program_copyright = 'Copyright (c) 2017-2026 Julien Deudon, Luis Eduardo Ferrer Cruz' - program_license = ''' -%s -%s + program_license = f''' +{program_shortdesc} +{program_copyright} Licensed under the GNU General Public License, version 3.0 This program comes with ABSOLUTELY NO WARRANTY; for details use '-L' or '--license'. This is free software, and you are welcome to redistribute it under certain conditions. -''' % (program_shortdesc, program_copyright) +''' - detailed_license = '''%s -%s + detailed_license = f'''{program_shortdesc} +{program_copyright} -This program is free software: you can redistriute it and/or modify +This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -410,121 +293,119 @@ def get_arguments(argv=None): You should have received a copy of the GNU General Public License along with this program. If not, see . -Contact: initbrain@gmail.com''' % (program_shortdesc, program_copyright) +Contact: initbrain@gmail.com, luis.ferrer.c@uni.pe''' - parser = MyParser(description=program_license, - formatter_class=RawDescriptionHelpFormatter, - epilog=textwrap.dedent('''\ - additional informations: - ROADMAP displays the default road map view - SATELLITE displays Google Earth satellite images - HYBRID displays a mixture of normal and satellite views - (this is the default map type) - TERRAIN displays a physical map based on terrain information''')) + parser = MyParser( + description=program_license, + formatter_class=RawDescriptionHelpFormatter, + epilog=textwrap.dedent(''' + additional informations: + ROADMAP displays the default road map view + SATELLITE displays Google Earth satellite images + HYBRID displays a mixture of normal and satellite views + (this is the default map type) + TERRAIN displays a physical map based on terrain information''') + ) parser.add_argument('-V', '--version', action='version', version=program_version_message) parser.add_argument('-L', '--license', action='version', version=detailed_license, - help='show program\'s license details and exit') + help="show program's license details and exit") parser.add_argument('-v', '--verbose', action="store_true", help='enable verbose messages', default=False) parser.add_argument('-k', '--api-key', action="store", dest="api_key", - help='Google Maps Geolocation API key (could be hardcoded)', + help='Google Maps Geolocation API key (required for Google provider)', default=None) parser.add_argument('-p', '--json-prettify', action="store_true", help='prettify JSON output', default=False) parser.add_argument('-o', '--with-overview', action="store_true", - help='accuracy overview file generation', + help='generate an interactive map file', default=False) parser.add_argument('-m', '--map-type', choices=['ROADMAP', 'SATELLITE', 'HYBRID', 'TERRAIN'], - help='accuracy overview map type', + help='map type for the overview file', default='HYBRID') + parser.add_argument('--api-provider', choices=['google', 'mozilla'], default='google', + help='geolocation API provider (google or mozilla)') - # Because using Mac OS X don't require to specify a Wi-Fi interface - if sys.platform == 'darwin': - parser.add_argument('--demo', action="store_true", help='demo mode - West Norwood (London)', default=False) + # Windows mode + if sys.platform == 'win32': + parser.add_argument('--demo', action="store_true", help='demo mode - uses sample data', default=False) else: required_parser = parser.add_argument_group('required arguments') required_parser = required_parser.add_mutually_exclusive_group(required=True) required_parser.add_argument('-i', action="store", dest="wifi_interface", help='specify Wi-Fi scan interface') - required_parser.add_argument('--demo', action="store_true", help='demo mode - West Norwood (London)', default=False) + required_parser.add_argument('--demo', action="store_true", help='demo mode - uses sample data', default=False) return parser.parse_args() -if __name__ == "__main__": - # Parsing command line arguments +def main(): + global args args = get_arguments() - # Parameter for demo mode set to False by default via get_arguments() - if args.demo: - # TODO parameter for displaying messages or not - # --verbose command line argument see get_arguments() - if args.verbose: - print "[+] Scanning nearby Wi-Fi networks (demo)" - - # Demo - West Norwood (London) - wifi_data = [ - ('00-fe-f4-25-ee-30', -40), - ('02-fe-f4-25-ee-30', -44), - ('12-fe-f4-25-ee-30', -44), - ('00-26-5a-7e-0d-02', -60), - ('90-01-3b-30-04-29', -60), - ('2c-b0-5d-bd-db-4a', -50) - ] - else: - # Checking permissions, operating system and software dependencies - wifi_scan_method = check_prerequisites() - - if args.verbose: - print "[+] Scanning nearby Wi-Fi networks..." - wifi_data = get_signal_strengths(wifi_scan_method) - # TODO check if the amount of detected Wi-Fi access points is good enough to call the API - - if args.verbose: - print "[+] Generating the HTML request" - location_request = { - 'considerIp': False, - 'wifiAccessPoints':[ - { - "macAddress": mac, - "signalStrength": signal - } for mac, signal in wifi_data] - } - - if args.verbose: - print prettify_json(location_request) - - # Check for missing API_KEY + # Set API key if args.api_key: + global API_KEY API_KEY = args.api_key - if not API_KEY or API_KEY is 'YOUR_KEY': - print "Error: a Google Maps Geolocation API key is required, get it yours here:\n" + \ - "https://developers.google.com/maps/documentation/geolocation/intro" - exit(1) + + # Demo mode: use sample data without calling any API + if args.demo: + logger.info("Using demo mode with sample data") + # Simulate a valid API result for demo mode + api_result = { + "location": { + "lat": -12.0464, # Lima, Perú + "lng": -77.0428 + }, + "accuracy": 50.0 + } else: - json_data = simplejson.JSONEncoder().encode(location_request) - http_request = urllib2.Request('https://www.googleapis.com/geolocation/v1/geolocate?key=' + API_KEY) - http_request.add_header('Content-Type', 'application/json') - - if args.verbose: - print "[+] Sending the request to Google" - # TODO internet connection error handling ? - api_result = simplejson.loads(urllib2.urlopen(http_request, json_data).read()) - - if args.verbose: - print "[+] Result" - - # Print JSON results - print prettify_json(api_result) - - if args.verbose: - print "[+] Google Maps link" - print 'https://www.google.com/maps?q=%f,%f' % (api_result['location']['lat'], api_result['location']['lng']) - - # --with-overview argument set to False by default via get_arguments() - if args.with_overview: - if args.verbose: - print "[+] Accuracy overview" - create_overview(api_result, args.map_type) + # Only validate API key if using Google provider + if args.api_provider == 'google' and (not API_KEY or API_KEY == 'YOUR_KEY'): + logger.error("A Google Maps Geolocation API key is required. Get yours at: https://developers.google.com/maps/documentation/geolocation/intro") + exit(1) + + # Determine OS type + if sys.platform == 'win32': + os_type = 'windows' + elif sys.platform.startswith('linux'): + os_type = 'linux' + elif sys.platform == 'darwin': + os_type = 'darwin' + elif sys.platform.startswith('openbsd'): + os_type = 'openbsd' + else: + logger.error(f"Unsupported operating system: {sys.platform}") + exit(1) + + # Scan Wi-Fi networks + scanner = WifiScanner(interface=args.wifi_interface if hasattr(args, 'wifi_interface') else None, os_type=os_type) + wifi_data = scanner.scan() + + # Get location using the selected API provider + geolocation = GeolocationAPI(api_key=API_KEY, provider=args.api_provider) + api_result = geolocation.get_location(wifi_data) + + if api_result is None: + logger.error("No geolocation data was returned. Check your API provider and connection.") + exit(1) + + # Print results + if api_result is not None: + logger.info("Geolocation result:") + print(prettify_json(api_result, args.json_prettify)) + + # Generate map if requested + if args.with_overview: + MapGenerator.create_map(api_result) + + # Print Google Maps link + if 'location' in api_result: + logger.info(f"Google Maps link: https://www.google.com/maps?q={api_result['location']['lat']},{api_result['location']['lng']}") + else: + logger.error("No geolocation data was returned. Check your API provider and connection.") + + +if __name__ == "__main__": + main() diff --git a/wifi_positioning_system_bk.py b/wifi_positioning_system_bk.py new file mode 100644 index 0000000..c59276c --- /dev/null +++ b/wifi_positioning_system_bk.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Python Wi-Fi Positioning System - Wi-Fi geolocation script using the Google Maps Geolocation API + +@author: Julien Deudon, Luis Eduardo Ferrer Cruz +@copyright: Copyright 2017-2026, Julien Deudon, Luis Eduardo Ferrer Cruz +@license: GNU GPL 3.0 +@contact: initbrain@gmail.com, luis.ferrer.c@uni.pe +""" + +import argparse +import json +import os +import re +import sys +import logging +import subprocess +from argparse import RawDescriptionHelpFormatter +import textwrap + +try: + import xml.etree.cElementTree as ET +except ImportError: + import xml.etree.ElementTree as ET + +try: + import requests +except ImportError: + print("Error: 'requests' library is required. Install it with: pip install requests") + exit(1) + +try: + import folium +except ImportError: + print("Warning: 'folium' library is not installed. Maps will not be generated. Install it with: pip install folium") + +try: + import pywifi +except ImportError: + pass # pywifi is only required for Windows + +__version__ = "0.2.0" + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('wifi_positioning.log') + ] +) +logger = logging.getLogger(__name__) + +# A Google Maps Geolocation API key is required +API_KEY = os.environ.get('GOOGLE_API_KEY') or 'YOUR_KEY' + + +class WifiScanner: + """Class to scan Wi-Fi networks.""" + + def __init__(self, interface=None, os_type=None): + self.interface = interface + self.os_type = os_type + + def scan(self): + """Scan Wi-Fi networks based on the operating system.""" + if self.os_type == 'linux': + return self._scan_linux() + elif self.os_type == 'windows': + return self._scan_windows() + elif self.os_type == 'darwin': + return self._scan_mac() + elif self.os_type == 'openbsd': + return self._scan_openbsd() + else: + raise NotImplementedError(f"Operating system {self.os_type} is not supported") + + def _scan_linux(self): + """Scan Wi-Fi networks on Linux using iw.""" + iw_command = f"iw dev {self.interface} scan" + result = subprocess.run(iw_command.split(), capture_output=True, text=True) + + if result.returncode != 0: + logger.error(f"Unable to scan for Wi-Fi networks! Command: {iw_command}") + logger.error(f"Error: {result.stderr}") + exit(1) + + parsing_result = re.compile( + r"BSS ([\w\d\:]+).*\n.*\n.*\n.*\n.*\n\tsignal: ([-\d]+)", + re.MULTILINE + ).findall(result.stdout) + + wifi_data = [(bss[0].replace(':', '-'), int(bss[1])) for bss in parsing_result] + return wifi_data + + def _scan_windows(self): + """Scan Wi-Fi networks on Windows using pywifi.""" + try: + wifi = pywifi.PyWiFi() + iface = wifi.interfaces()[0] + iface.scan() + results = iface.scan_results() + + wifi_data = [] + for network in results: + if network.bssid and network.signal: + mac = network.bssid.replace(':', '-') + wifi_data.append((mac, network.signal)) + + return wifi_data + except Exception as e: + logger.error(f"Error scanning Wi-Fi networks on Windows: {e}") + exit(1) + + def _scan_mac(self): + """Scan Wi-Fi networks on Mac OS X using airport.""" + airport_xml_cmd = '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport --scan -x' + result = subprocess.run(airport_xml_cmd.split(), capture_output=True, text=True) + + if result.returncode != 0: + logger.error(f"Unable to scan for Wi-Fi networks! Command: {airport_xml_cmd}") + logger.error(f"Error: {result.stderr}") + exit(1) + + root = ET.fromstring(result.stdout) + networks = root.getchildren()[0] + wifi_data = [(network.find("string").text, abs(int(network.findall("integer")[7].text))) for network in networks] + return wifi_data + + def _scan_openbsd(self): + """Scan Wi-Fi networks on OpenBSD using ifconfig.""" + ifconfig_cmd = f'ifconfig {self.interface} scan' + result = subprocess.run(ifconfig_cmd.split(), capture_output=True, text=True) + + if result.returncode != 0: + logger.error(f"Unable to scan for Wi-Fi networks! Command: {ifconfig_cmd}") + logger.error(f"Error: {result.stderr}") + exit(1) + + parsing_result = re.compile( + r"nwid\s+[\w-]+\s+chan\s+\d+\s+bssid\s+([\w\d\:]+)\s+([-\d]+)dBm", + re.MULTILINE + ).findall(result.stdout) + + wifi_data = [(bss[0].replace(':', '-'), int(bss[1])) for bss in parsing_result] + return wifi_data + + +class GeolocationAPI: + """Class to interact with geolocation APIs.""" + + def __init__(self, api_key=None, provider='google'): + self.api_key = api_key + self.provider = provider + + def get_location(self, wifi_data): + """Get location using the selected provider.""" + if self.provider == 'google': + return self._get_location_google(wifi_data) + elif self.provider == 'mozilla': + return self._get_location_mozilla(wifi_data) + else: + raise NotImplementedError(f"Provider {self.provider} is not supported") + + def _get_location_google(self, wifi_data): + """Use Google Geolocation API.""" + url = f"https://geolocation.googleapis.com/v1/geolocate?key={self.api_key}" + location_request = { + 'considerIp': False, + 'wifiAccessPoints': [ + {"macAddress": mac, "signalStrength": signal} + for mac, signal in wifi_data + ] + } + + try: + response = requests.post(url, json=location_request, timeout=10) + response.raise_for_status() + api_result = response.json() + + if 'location' not in api_result or 'accuracy' not in api_result: + raise ValueError("Invalid response from Google API") + + return api_result + except requests.exceptions.RequestException as e: + logger.error(f"Error connecting to Google API: {e}") + if "403" in str(e): + logger.error("Check that your API key is valid and that you have not exceeded the request limit.") + elif "timeout" in str(e).lower(): + logger.error("Request timed out. Check your internet connection.") + exit(1) + + def _get_location_mozilla(self, wifi_data): + """Use Mozilla Location Service API.""" + url = "https://location.services.mozilla.com/v1/geolocate" + mls_request = { + "data": { + "wifi": [ + {"macAddress": mac, "signalStrength": signal} + for mac, signal in wifi_data + ] + } + } + + try: + response = requests.post(url, json=mls_request, timeout=10) + response.raise_for_status() + result = response.json() + if result is None: + logger.error("Mozilla API returned no data. Check your request format.") + return None + return result + except requests.exceptions.RequestException as e: + logger.error(f"Error connecting to Mozilla API: {e}") + return None + + +class MapGenerator: + """Class to generate maps.""" + + @staticmethod + def create_map(api_result, filename='wifi_location.html'): + """Create an interactive map using Folium.""" + try: + lat = api_result['location']['lat'] + lng = api_result['location']['lng'] + accuracy = api_result['accuracy'] + + m = folium.Map(location=[lat, lng], zoom_start=18) + + folium.Circle( + radius=accuracy, + location=[lat, lng], + color='blue', + fill=True, + fill_color='blue', + fill_opacity=0.3, + popup=f"Accuracy: {accuracy} meters" + ).add_to(m) + + folium.Marker( + location=[lat, lng], + popup=f"Latitude: {lat}, Longitude: {lng}" + ).add_to(m) + + m.save(filename) + logger.info(f"Map saved to {filename}") + except Exception as e: + logger.error(f"Error generating map: {e}") + + +def prettify_json(json_data, prettify=False): + """Format JSON data for printing.""" + if prettify: + return '\n'.join([l.rstrip() for l in json.dumps(json_data, sort_keys=True, indent=4*' ').splitlines()]) + else: + return json.dumps(json_data) + + +def get_scriptpath(): + """Get the directory of the current script.""" + pathname = os.path.dirname(sys.argv[0]) + fullpath = os.path.abspath(pathname) + if not fullpath.endswith('/'): + fullpath += '/' + return fullpath + + +class MyParser(argparse.ArgumentParser): + def error(self, message): + sys.stderr.write(f'Error: {message}\n\n') + self.print_usage() + sys.exit(2) + + +def get_arguments(argv=None): + """Parse command line arguments.""" + if argv is not None: + sys.argv.extend(argv) + + program_name = os.path.basename(sys.argv[0]) + program_version = f'v{__version__}' + program_version_message = f'{program_name} {program_version}' + program_shortdesc = __doc__.split("\n")[1] if __doc__ else "Python Wi-Fi Positioning System" + program_copyright = 'Copyright (c) 2017-2026 Julien Deudon, Luis Eduardo Ferrer Cruz' + + program_license = f''' +{program_shortdesc} +{program_copyright} + +Licensed under the GNU General Public License, version 3.0 + +This program comes with ABSOLUTELY NO WARRANTY; for details use '-L' or '--license'. +This is free software, and you are welcome to redistribute it under certain conditions. +''' + + detailed_license = f'''{program_shortdesc} +{program_copyright} + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +Contact: initbrain@gmail.com, luis.ferrer.c@uni.pe''' + + parser = MyParser( + description=program_license, + formatter_class=RawDescriptionHelpFormatter, + epilog=textwrap.dedent(''' + additional informations: + ROADMAP displays the default road map view + SATELLITE displays Google Earth satellite images + HYBRID displays a mixture of normal and satellite views + (this is the default map type) + TERRAIN displays a physical map based on terrain information''') + ) + + parser.add_argument('-V', '--version', action='version', version=program_version_message) + parser.add_argument('-L', '--license', action='version', version=detailed_license, + help="show program's license details and exit") + parser.add_argument('-v', '--verbose', action="store_true", + help='enable verbose messages', + default=False) + parser.add_argument('-k', '--api-key', action="store", dest="api_key", + help='Google Maps Geolocation API key (could be hardcoded)', + default=None) + parser.add_argument('-p', '--json-prettify', action="store_true", + help='prettify JSON output', + default=False) + parser.add_argument('-o', '--with-overview', action="store_true", + help='generate an interactive map file', + default=False) + parser.add_argument('-m', '--map-type', choices=['ROADMAP', 'SATELLITE', 'HYBRID', 'TERRAIN'], + help='map type for the overview file', + default='HYBRID') + parser.add_argument('--api-provider', choices=['google', 'mozilla'], default='google', + help='geolocation API provider (google or mozilla)') + + # Windows mode + if sys.platform == 'win32': + parser.add_argument('--demo', action="store_true", help='demo mode - West Norwood (London)', default=False) + else: + required_parser = parser.add_argument_group('required arguments') + required_parser = required_parser.add_mutually_exclusive_group(required=True) + required_parser.add_argument('-i', action="store", dest="wifi_interface", help='specify Wi-Fi scan interface') + required_parser.add_argument('--demo', action="store_true", help='demo mode - West Norwood (London)', default=False) + + return parser.parse_args() + + +def main(): + global args + args = get_arguments() + + # Set API key + if args.api_key: + global API_KEY + API_KEY = args.api_key + + # Solo validar la clave API si el proveedor es Google + if args.api_provider == 'google' and (not API_KEY or API_KEY == 'YOUR_KEY'): + logger.error("A Google Maps Geolocation API key is required. Get yours at: https://developers.google.com/maps/documentation/geolocation/intro") + exit(1) + + # Demo mode + if args.demo: + logger.info("Using demo mode with sample data") + try: + with open('demo_data.json', 'r') as f: + demo_data = json.load(f) + wifi_data = [(ap['macAddress'], ap['signalStrength']) for ap in demo_data['wifiAccessPoints']] + except FileNotFoundError: + logger.warning("demo_data.json not found. Using default demo data.") + wifi_data = [ + ('00-1f-f4-25-ee-30', -40), + ('02-fe-f4-25-ee-30', -44), + ('12-fe-f4-25-ee-30', -44), + ('00-26-5a-7e-0d-02', -60), + ('90-01-3b-30-04-29', -60), + ('2c-b0-5d-bd-db-4a', -50) + ] + else: + # Determine OS type + if sys.platform == 'win32': + os_type = 'windows' + elif sys.platform.startswith('linux'): + os_type = 'linux' + elif sys.platform == 'darwin': + os_type = 'darwin' + elif sys.platform.startswith('openbsd'): + os_type = 'openbsd' + else: + logger.error(f"Unsupported operating system: {sys.platform}") + exit(1) + + # Scan Wi-Fi networks + scanner = WifiScanner(interface=args.wifi_interface if hasattr(args, 'wifi_interface') else None, os_type=os_type) + wifi_data = scanner.scan() + + # Get location using the selected API provider + geolocation = GeolocationAPI(api_key=API_KEY, provider=args.api_provider) + api_result = geolocation.get_location(wifi_data) + + # Print results + if api_result is not None: + logger.info("Geolocation result:") + print(prettify_json(api_result, args.json_prettify)) + + # Generate map if requested + if args.with_overview: + MapGenerator.create_map(api_result) + + # Print Google Maps link + if 'location' in api_result: + logger.info(f"Google Maps link: https://www.google.com/maps?q={api_result['location']['lat']},{api_result['location']['lng']}") + else: + logger.error("No geolocation data was returned. Check your API provider and connection.") + + +if __name__ == "__main__": + main()