Files
adif-api-reverse-engineering/docs/API_REQUEST_BODIES.md

480 lines
13 KiB
Markdown

# Request Body Analysis - ADIF API
> Reverse engineering of package `com.adif.elcanomovil.serviceNetworking`
> Date: 2025-12-04
## Table of Contents
- [1. Authentication Headers](#1-authentication-headers)
- [2. Request Bodies](#2-request-bodies)
- [3. Endpoints and Base URLs](#3-endpoints-and-base-urls)
- [4. Network Configuration](#4-network-configuration)
- [5. Authentication System](#5-authentication-system)
- [6. Code References](#6-code-references)
---
## 1. Authentication Headers
### 1.1 Static Headers
**File:** `ServicePaths.java:67-76`
#### Circulations
```
User-key: f4ce9fbfa9d721e39b8984805901b5df
Content-Type: application/json;charset=utf-8
```
#### Stations
```
User-key: 0d021447a2fd2ac64553674d5a0c1a6f
Content-Type: application/json;charset=utf-8
```
#### AVISA (Login/Refresh)
```
Authorization: Basic YXZpc3RhX2NsaWVudF9hbmRyb2lkOjh5WzZKNyFmSjwhXypmYXE1NyNnOSohNElwa2MjWC1BTg==
```
**Decoded (Base64):**
```
avista_client_android:8y[6J7!fJ<_*faq57#g9*!4Ipkc#X-AN
```
### 1.2 Dynamic Headers (Generated by ElcanoAuth)
**File:** `serviceNetworking/interceptors/auth/ElcanoAuth.java`
The interceptor adds the X-Elcano headers produced by the HMAC calculation:
```
X-Elcano-Host: <host>
X-Elcano-Client: AndroidElcanoApp
X-Elcano-Date: <timestamp>
X-Elcano-UserId: <uuid>
Authorization: HMAC-SHA256 Credential=... SignedHeaders=... Signature=...
```
**Generation algorithm (summary):**
- Canonical headers (order matters): `content-type`, `x-elcano-host`, `x-elcano-client`, `x-elcano-date`, `x-elcano-userid`
- Canonical request: method + path + query + canonical headers + SHA256(payload)
- String to sign: `HMAC-SHA256\n<timestamp>\n<date>/<client>/<userId>/elcano_request\n<hash>`
- Signature key: HMAC(secretKey, date) → HMAC(result, client) → HMAC(result, "elcano_request")
---
## 2. Request Bodies
### 2.1 Circulations - Departures/Arrivals/Between Stations
**Endpoints:**
- `/portroyalmanager/secure/circulationpaths/departures/traffictype/`
- `/portroyalmanager/secure/circulationpaths/arrivals/traffictype/`
- `/portroyalmanager/secure/circulationpaths/betweenstations/traffictype/`
**Model:** `TrafficCirculationPathRequest`
**File:** `circulations/model/request/TrafficCirculationPathRequest.java:10-212`
```json
{
"commercialService": "YES|NOT|BOTH",
"commercialStopType": "YES|NOT|BOTH",
"destinationStationCode": "string or null",
"originStationCode": "string or null",
"page": {
"pageNumber": 0
},
"stationCode": "string or null",
"trafficType": "CERCANIAS|AVLDMD|OTHERS|TRAVELERS|GOODS|ALL"
}
```
#### Real Example
```json
{
"commercialService": "BOTH",
"commercialStopType": "BOTH",
"page": {
"pageNumber": 0
},
"stationCode": "60000",
"trafficType": "ALL"
}
```
#### Allowed Values
**commercialService / commercialStopType** (`CirculationPathRequest.java:65-67`):
- `YES` - Only commercial services/stops
- `NOT` - Without commercial services/stops
- `BOTH` - All types
**trafficType** (`TrafficType.java:16-21`):
- `CERCANIAS` - Commuter trains
- `AVLDMD` - High speed long/medium distance
- `OTHERS` - Other types
- `TRAVELERS` - Passengers
- `GOODS` - Freight
- `ALL` - All types
---
### 2.2 Circulations - Specific Routes
**Endpoints:**
- `/portroyalmanager/secure/circulationpathdetails/onepaths/`
- `/portroyalmanager/secure/circulationpathdetails/severalpaths/`
**Model:** `OneOrSeveralPathsRequest`
**File:** `circulations/model/request/OneOrSeveralPathsRequest.java:11-140`
```json
{
"allControlPoints": true/false/null,
"commercialNumber": "string or null",
"destinationStationCode": "string or null",
"launchingDate": 1733356800000,
"originStationCode": "string or null"
}
```
#### Real Example
```json
{
"allControlPoints": true,
"commercialNumber": "04138",
"destinationStationCode": "60000",
"launchingDate": 1733356800000,
"originStationCode": "71801"
}
```
**Important notes:**
- `launchingDate` is a **millisecond** timestamp (Long in Java)
- `allControlPoints` indicates whether to include every control point in the route
- Optional fields should be omitted rather than set to `null`
---
### 2.3 Train Compositions
**Endpoint:** `/portroyalmanager/secure/circulationpaths/compositions/path/`
**Model:** `OneOrSeveralPathsRequest` (same as routes)
**File:** `compositions/CompositionsService.java:14-18`
```json
{
"allControlPoints": true/false/null,
"commercialNumber": "string or null",
"destinationStationCode": "string or null",
"launchingDate": 1733356800000,
"originStationCode": "string or null"
}
```
---
### 2.4 Stations - Single Station Details
**Endpoint:** `/portroyalmanager/secure/stations/onestation/`
**Model:** `OneStationRequest`
**File:** `stations/model/OneStationRequest.java:9-93`
```json
{
"detailedInfo": {
"extendedStationInfo": true,
"stationActivities": true,
"stationBanner": true,
"stationCommercialServices": true,
"stationInfo": true,
"stationServices": true,
"stationTransportServices": true
},
"stationCode": "60000",
"token": "string"
}
```
**Important notes:**
- The `detailedInfo` object controls what is returned
- All boolean fields default to `true` (see `DetailedInfoDTO.java:149`)
- `token` is required
#### DetailedInfo fields
**File:** `stations/model/DetailedInfoDTO.java:10-151`
| Field | Type | Description |
|-------|------|-------------|
| `extendedStationInfo` | boolean | Extended station information |
| `stationActivities` | boolean | Station activities |
| `stationBanner` | boolean | Banner/announcements |
| `stationCommercialServices` | boolean | Commercial services |
| `stationInfo` | boolean | Basic station information |
| `stationServices` | boolean | Available services |
| `stationTransportServices` | boolean | Transport services |
---
### 2.5 Station Observations
**Endpoint:** `/portroyalmanager/secure/stationsobservations/`
**Model:** `StationObservationsRequest`
**File:** `stationObservations/model/StationObservationsRequest.java:10-53`
```json
{
"stationCodes": ["60000", "71801"]
}
```
#### Real Example
```json
{
"stationCodes": ["60000", "71801", "79600"]
}
```
**Notes:**
- Array of station codes (strings)
- Required field
- Can contain multiple codes
---
## 3. Endpoints and Base URLs
### 3.1 Base URLs
**File:** `di/NetworkModule.java:73-159`
| Service | Base URL | Authentication |
|---------|----------|----------------|
| **Circulations** | `https://circulacion.api.adif.es` | Secured (with AuthHeaderInterceptor) |
| **Stations** | `https://estaciones.api.adif.es` | Secured (with AuthHeaderInterceptor) |
| **AVISA** | `https://avisa.adif.es` | Basic (without AuthHeaderInterceptor) |
| **Elcano Web** | `https://elcanoweb.adif.es/api/` | - |
### 3.2 Full Paths - Stations
**File:** `ServicePaths.java:106-112`
```
GET /portroyalmanager/secure/stations/allstations/reducedinfo/{token}/
POST /portroyalmanager/secure/stations/onestation/
POST /portroyalmanager/secure/stationsobservations/
```
### 3.3 Full Paths - Circulations
**File:** `ServicePaths.java:41-51`
```
POST /portroyalmanager/secure/circulationpaths/departures/traffictype/
POST /portroyalmanager/secure/circulationpaths/arrivals/traffictype/
POST /portroyalmanager/secure/circulationpaths/betweenstations/traffictype/
POST /portroyalmanager/secure/circulationpathdetails/onepaths/
POST /portroyalmanager/secure/circulationpathdetails/severalpaths/
```
### 3.4 Full Paths - Compositions
**File:** `ServicePaths.java:55-61`
```
POST /portroyalmanager/secure/circulationpaths/compositions/path/
```
### 3.5 Full Paths - AVISA
**Files:** `ServicePaths.java:82-92` and `ServicePaths.java:29-37`
```
POST /avisa-ws/api/token (login)
POST /avisa-ws/api/token (refresh)
POST /avisa-ws/api/v1/client (register)
GET /avisa-ws/api/v1/station (stations)
GET /avisa-ws/api/v1/category (categories)
GET /avisa-ws/api/v1/incidence (incidences list)
GET /avisa-ws/api/v1/incidence/{id} (incidence details)
POST /avisa-ws/api/v1/incidence (create incidence)
```
---
## 4. Network Configuration
### 4.1 OkHttpClient Configuration
**File:** `di/NetworkModule.java:100-132`
#### Basic client
```kotlin
OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build()
```
#### Secured client (with authentication)
```kotlin
OkHttpClient.Builder()
.addInterceptor(AuthHeaderInterceptor(userId))
.certificatePinner(certificatePinner)
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build()
```
**Timeouts:**
- Connect timeout: 60 seconds
- Read timeout: 60 seconds
### 4.2 Services using the secured client
**File:** `di/NetworkModule.java`
- `CirculationService` (line 73)
- `StationsService` (line 142)
- `StationObservationsService` (line 135)
- `CompositionsService` (line 156)
### 4.3 Services using the basic client
- `AvisaLoginService` (line 50)
- `AvisaStationsService` (line 57)
- `IncidenceService` (line 80)
- `SubscriptionsService` (line 149)
---
## 5. Authentication System
### 5.1 AuthHeaderInterceptor
**File:** `interceptors/AuthHeaderInterceptor.java:27-84`
This interceptor runs on **every** request for secured services.
#### Authentication process
1. **Generate persistent User ID**
- Uses `GeneratePersistentUserIdUseCase`
- The ID is stored and reused between sessions
2. **Build the token**
```java
ElcanoClientAuth.Builder()
.host(request.url().host())
.contentType("application/json;charset=utf-8")
.path(request.url().encodedPath())
.params(request.url().encodedQuery())
.xElcanoClient("AndroidElcanoApp")
.xElcanoUserId(userId)
.httpMethodName(request.method())
.payload(bodyJsonWithoutSpaces)
.build()
```
3. **Generate headers**
- The `ElcanoClientAuth` object generates authentication headers
- They are added automatically to the request
#### Generated headers
```
X-CanalMovil-Authentication: <calculated_token>
X-CanalMovil-deviceID: <device_id>
X-CanalMovil-pushID: <push_id>
```
### 5.2 GetKeysHelper class
**File:** `AuthHeaderInterceptor.java:44`
Provides keys for authentication:
- `getKeysHelper.a()` - First key
- `getKeysHelper.b()` - Second key
These keys are used by the signing/authentication algorithm.
### 5.3 Certificate Pinning
**File:** `di/NetworkModule.java:64-70`
The app uses **Certificate Pinning** to prevent MITM attacks:
- Expected SSL certificates are in `PinningRepository`
- Loaded asynchronously at startup
- All requests validate the server certificate
---
## 6. Code References
### 6.1 Key Files
| File | Location | Description |
|------|----------|-------------|
| `ServicePaths.java` | `serviceNetworking/` | Paths and static headers |
| `AuthHeaderInterceptor.java` | `serviceNetworking/interceptors/` | Auth header generation |
| `NetworkModule.java` | `serviceNetworking/di/` | Retrofit/OkHttp configuration |
| `CirculationService.java` | `serviceNetworking/circulations/` | Circulation API |
| `StationsService.java` | `serviceNetworking/stations/` | Stations API |
| `StationObservationsService.java` | `serviceNetworking/stationObservations/` | Observations API |
| `CompositionsService.java` | `serviceNetworking/compositions/` | Compositions API |
### 6.2 Request Models
| Model | File | Usage |
|-------|------|-------|
| `TrafficCirculationPathRequest` | `circulations/model/request/` | Departures, Arrivals, BetweenStations |
| `OneOrSeveralPathsRequest` | `circulations/model/request/` | OnePaths, SeveralPaths, Compositions |
| `OneStationRequest` | `stations/model/` | Station details |
| `DetailedInfoDTO` | `stations/model/` | Detailed info configuration |
| `StationObservationsRequest` | `stationObservations/model/` | Station observations |
### 6.3 Important Code Lines
- Static headers: `ServicePaths.java:67-76`
- Circulations user-key: `ServicePaths.java:67`
- Stations user-key: `ServicePaths.java:68`
- AVISA login token: `ServicePaths.java:70`
- Auth interceptor: `AuthHeaderInterceptor.java:38-83`
- Circulations base URL: `NetworkModule.java:76`
- Stations base URL: `NetworkModule.java:145`
- Enum TrafficType: `TrafficType.java:16-21`
- Enum State: `CirculationPathRequest.java:65-67`
---
## 7. Additional Notes
### 7.1 JSON Serialization
- **Library used:** Moshi (configured in `NetworkModule.java:87-96`)
- **Format:** JSON field names match Java property names exactly
- **Null handling:** Optional fields are **omitted** by the client; sending explicit `null` values leads to 401 responses
- **Date format:** Millisecond timestamps (Long)
### 7.2 Security Considerations
1. **Hardcoded user-keys:** API keys are in the code (easy to extract)
2. **Certificate Pinning:** Makes proxy interception harder
3. **Dynamic authentication:** X-Elcano headers require the HMAC algorithm to be reproduced
4. **AVISA token:** Base64 credentials in code (decodable)
### 7.3 Testing
Use the Python client (`adif_client.py`) or the signed test scripts in `/tests` to validate each endpoint. All endpoints are currently reproducible with the shipped keys and HMAC implementation.
---
**Last update:** 2025-12-05
**Source:** Decompiled ADIF El Cano Movil APK
**Tools:** JADX, Ghidra, manual Java code analysis