Added files

This commit is contained in:
Holger Sielaff
2025-06-28 16:23:47 +02:00
parent 21a2714401
commit e2eeeaefcd
8 changed files with 328 additions and 0 deletions

7
.env Normal file
View File

@@ -0,0 +1,7 @@
APIHOST=www.example.com
APIURL=/track/api.php
# The APIKEY from the things network
APIKEY=xxx
DBNAME=track
DBUSER=track
DBPASS=track

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.idea
.env.*

34
api.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
const INCLUDER = true;
require_once __DIR__ . '/lib.php';
$cli = strtolower(php_sapi_name()) === 'cli';
$headers = getallheaders();
$raw = file_get_contents('php://input');
$apikey = Config::apiKey();
$allowed = $headers['X-Api-Key'] === $apikey || $_GET['X-Api-Key'] === $apikey || $_POST['X-Api-Key'] === $apikey;
if ($allowed) {
try {
if ($_GET['type'] == 'uplink' && preg_match("!thethings\.network$!i", $headers["X-Tts-Domain"])) {
$process = new UplinkData($raw, $headers);
$process->write();
Logging::info('uplink', $headers, $raw);
} else {
db_logging($_GET['type'] ?? 'error', $raw, $headers);
Logging::info($_GET['type'] || 'error', $headers);
}
} catch (Throwable $e) {
error_log($e->getMessage() . ' - see also /tmp/track-error.log');
Logging::info('error', $e->getMessage(), $headers, $_GET, $_POST, json_decode($raw));
}
} else {
Logging::info('error', "Not allowed", $headers, $_GET, $_POST, json_decode($raw));
}
header("Content-Type: application/json", true, 200);
echo "true";
exit(0);

51
bootstrap.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
ini_set('display_errors', false);
if ( ! file_exists(__DIR__ . '/.env.local') ){
copy(__DIR__ . '/.env', __DIR__ . '/.env.local');
}
$_allowed = range('A', 'Z');
foreach(file(__DIR__ . '/.env.local') as $line){
$line = trim($line);
if ( ! in_array($line[0], $_allowed) ) {
continue;
}
putenv($line);
}
class Config {
private static PDO $_pdo;
public static function dbName() {
return getenv('DBNAME') ?: 'track';
}
public static function dbUser() {
return getenv('DBUSER') ?: 'track';
}
public static function dbPass() {
return getenv('DBPASS') ?: 'track';
}
public static function apiHost() {
return getenv('APIHOST') ?: 'www.example.com';
}
public static function apiKey() {
return getenv('APIKEY') ?: 'xxx';
}
public static function isVerbose() {
return intval(getenv('VERBOSE')) ?: 0;
}
public static function dbConnection(): PDO {
if(!self::$_pdo) {
self::$_pdo = new PDO(
sprintf("pgsql:dbname=%s;host=127.0.0.1", Config::dbName()),
COnfig::dbUser(),
COnfig::dbPass(),
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]
);
}
return self::$_pdo;
}
}
$pdo = Config::dbConnection();

13
gpx.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
const INCLUDER = true;
require_once __DIR__ . '/lib.php';
if(!isset($_GET['json'])){
header('Content-Type: application/xml');
header('Access-Control-Allow-Origin: https://gpx.studio');
echo UplinkData::makeGPX($_GET['device'] ?? '', $_GET['from'] ?? '', $_GET['to'] ?? '');
}else{
header('Content-Type: application/json');
echo json_encode(UplinkData::makePositions($_GET['device'] ?? '', $_GET['from'] ?? '', $_GET['to'] ?? ''),128);
}
exit(0);

6
index.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
ini_set('display_errors', false);
if(strtolower($_SERVER['REQUEST_METHOD']) == 'post'){
error_log(json_encode($_POST));
}
exit(0);

213
lib.php Normal file
View File

@@ -0,0 +1,213 @@
<?php
if ( ! defined('INCLUDER') { exit(0); }
if(getenv('DBNAME') === false){
require_once __DIR__ . '/bootstrap.php';
}
function db_logging(string $type, string $raw_data, array $headers)
{
$pdo = Config::dbConnection();
$pdo->exec("CREATE TABLE IF NOT EXISTS logging(
id SERIAL PRIMARY KEY,
request_type VARCHAR,
inserted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
raw JSON,
headers JSON,
_get JSON,
_post JSON
)");
$stmt = $pdo->prepare("INSERT INTO logging(raw, request_type, headers, _get, _post) VALUES(?, ?, ?, ?, ?)");
$stmt->execute([$raw_data, $_GET['type'] ?? 'NN', json_encode($headers), json_encode($_GET), json_encode($_POST)]);
}
class UplinkData
{
public $raw;
public $altitude;
public $longitude;
public $latitude;
public $uplink_token;
public $request_type = 'uplink';
public $_get;
public $_post;
public $_headers;
public $time;
public $device_id;
public function __construct(string $raw, array $headers)
{
$this->raw = $raw;
$data = json_decode($raw);
$this->device_id = $data->end_device_ids->device_id;
$this->time = $data->received_at;
$this->_headers = json_encode($headers);
$this->_post = json_encode($_POST);
$this->_get = json_encode($_GET);
$this->uplink_token = $data->uplink_message->rx_metadata[0]->uplink_token;
$this->latitude = $data->uplink_message->rx_metadata[0]->location->latitude;
$this->longitude = $data->uplink_message->rx_metadata[0]->location->longitude;
$this->altitude = $data->uplink_message->rx_metadata[0]->location->altitude;
#$this->latitude = $data->uplink_message->decoded_payload->latitude;
#$this->altitude = $data->uplink_message->decoded_payload->altitude;
#$this->longitude = $data->uplink_message->decoded_payload->longitude;
}
public function write()
{
$pdo = Config::dbConnection();
$stmt = $pdo->prepare("INSERT INTO tracks
(
raw, device_id, _get, _post, _headers, latitude,
longitude, altitude, time, request_type, uplink_token
)
VALUES(
?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?
)");
$args = [
$this->raw,
$this->device_id,
$this->_get,
$this->_post,
$this->_headers,
$this->latitude,
$this->longitude,
$this->altitude,
$this->time,
$this->request_type,
$this->uplink_token,
];
$stmt->execute($args);
}
public static function get(string $device_id = '', string $from = '', string $to = ''): array
{
$pdo = Config::dbConnection();
$from = $from ?: $_GET['from'] ?: date('Y-m-d H:i:s', time() - 3600 * 24);
$to = $to ?: date('Y-m-d H:i:s');
$sql = "SELECT * FROM tracks WHERE request_type = 'uplink' AND time BETWEEN ? AND ?";
$args = [$from, $to];
if ($device_id) {
$sql .= " AND device_id = ?";
$args[] = $device_id;
}
$stmt = $pdo->prepare($sql);
$res = $stmt->execute($args);
$ret = [];
if ($res) {
$ret = [];
foreach ($stmt->fetchAll(PDO::FETCH_OBJ) as $_) {
if(!$_->device_id){
continue;
}
if ( ! isset($ret[$_->device_id])) {
$ret[$_->device_id] = [];
}
$ret[$_->device_id][] = $_;
}
}
return $ret;
}
public static function makePositions(string $device_id = '',string $from = '', string $to='')
{
$data = self::get($device_id, $from, $to);
$tracks = [];
if ($data) {
foreach ($data as $device_id => $resultset) {
if(!isset($tracks[$device_id])){$tracks[$device_id] = [];}
foreach ($resultset as $res) {
$raw = json_decode($res->raw);
list($lat,$lon) = static::decodePayload($raw, $human);
$tracks[$device_id][] = ['received_at' => $raw->received_at, 'lat' => $lat, 'lon' => $lon,];
}
}
}
return [$tracks, $human];
}
public static function makeGPX(string $device_id = '', string $from = '', string $to = ''): string
{
$root = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>'
. '<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1" creator="Holguin" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
. 'xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
. '<metadata><name>Track - %s bis %s</name><desc>Track halt</desc><author><name>Holguin</name></author></metadata>'
. '%s'
. '</gpx>';
$track = '<trk><name>Trackname %s</name><desc>Trackbeschreibung %s</desc>'
. '<trkseg>'
. '%s'
. '</trkseg>'
. '</trk>';
$data = self::get($device_id, $from, $to);
$tracks = [];
if ($data) {
foreach ($data as $device_id => $resultset) {
$inner = [];
foreach ($resultset as $res) {
$raw = json_decode($res->raw);
list($lat,$lon) = static::decodePayload($raw);
# $inner[] = sprintf('<trkpt lat="%s" lon="%s"><ele>%s</ele><time>%s</time></trkpt>', $res->latitude, $res->longitude, $res->altitude, $res->time);
$inner[] = sprintf('<trkpt lat="%s" lon="%s"><ele>%s</ele><time>%s</time></trkpt>', $lat, $lon, 0, $raw->received_at);
}
$tracks[] = sprintf($track, $device_id, $device_id, join("\n", $inner));
}
return sprintf($root,$from, $to, join("\n", $tracks));
}
return '';
}
public static function decodePayload($raw, &$human=[])
{
$payload=$raw->uplink_message->frm_payload;
$human = file_get_contents('https://1m2m.eu/services/GETPAYLOAD?Human=0&PL='.bin2hex(base64_decode($payload)));
$human = json_decode($human);
if(isset($human->Lat) && isset($human->Lon)){
return [str_replace(',','.',$human->Lat),str_replace(',','.',$human->Lon)];
}
return [0,0];
}
}
class Logging
{
public static function info($type, ...$_)
{
global $verbose;
if ($verbose || $type == 'error') {
file_put_contents("/tmp/track-$type.log", json_encode($_) . "\n", FILE_APPEND);
}
}
}
if ( ! function_exists('getallheaders')) {
function getallheaders(): array
{
$headers = [
'Content-type' => 'application/json',
"X-Tts-Domain" => '',
];
foreach ($_SERVER as $name => $value) {
if ($name != 'HTTP_MOD_REWRITE' && (substr($name, 0, 5) == 'HTTP_' || $name == 'CONTENT_LENGTH' || $name == 'CONTENT_TYPE')) {
$name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', str_replace('HTTP_', '', $name)))));
if ($name == 'Content-Type') {
$name = 'Content-type';
}
$headers[$name] = $value;
}
}
return $headers;
}
}

2
utils Executable file
View File

@@ -0,0 +1,2 @@
psq="sudo -upostgres psql track -c"
$psq "select count(*) from tracks WHERE request_type = 'uplink'"