[code] [blog] [Repos] [GPS Tracker] [Music] [VNC] [Restart VNC] [E-Mail me]

11.9 KB · raw

#include <time.h>
#include <stdio.h>
#include <string.h>
#include <memory.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdarg.h>
#include <math.h>

#include "../util.h"
#include "../config.h"
#include "../crc16.h"
#include "../string.h"
#include "../connection.h"
#include "../logfiles.h"
#include "../geofence.h"
#include "../commands.h"
#include "../wifi_lookup.h"
#include "basic_protocol.h"

#define BASIC_TIMEOUT 1200

bool basic_send_command( void * c, const char * cmd) {
    connection * conn = (connection *)c;
    size_t start = conn->send_count;
    send_string(conn, cmd);
    send_string(conn, "\n");
    log_command_bytes(conn, (const unsigned char *)conn->send_buffer, start, conn->send_count, false);

    return true;
}

char * unescape_str(unsigned char * str) {
    for (int i = 0; str[i] != 0; i++) {
        if (str[i] == 255) {
            str[i] = ';';
        }
    }

    return str;
}

void basic_process_message(connection * conn, char * string, size_t length) {
    unsigned char bufstr[BUF_SIZE];
    //zeroed: split_to only fills as many slots as the message had fields, and every other
    //file here relies on the rest reading back as NULL rather than as whatever the stack
    //happened to hold
    unsigned char * data_buffers[40] = {0};
    unsigned char imei[18] = {0};
    unsigned char * wifi_split[16];
    unsigned char * coord_split[2];
    time_t t = time(NULL);
    unsigned int num_sats = 0;
    unsigned int position_type = 0;
    float lat = 0;
    float lon = 0;
    //Never parsed on this protocol - there is no speed field - and passing 0 told
    //wifi_learn_position() the device was standing still, which is the one condition it
    //checks before recording an access point position. NAN says it is not known.
    double speed = NAN;
    double over_time;
    wifi_db_entry db_entry = {0};
    unsigned char * current_network[3];
    memset(imei, 0, sizeof(imei));
    memset(bufstr, 0, sizeof(bufstr));
    memset(data_buffers, 0, sizeof(data_buffers));
    string = strip_whitespace(string);
    memcpy(bufstr, string, min(strlen(string), sizeof(bufstr) - 1));
    log_line(conn, "message recieved: %s\n", bufstr);
    size_t str_count = split_to(';', bufstr, BUF_SIZE, data_buffers, 40);
    conn->timeout_time = time(0) + BASIC_TIMEOUT;
    log_line(conn, "  parsed message: ");

    for (size_t i = 0; i < str_count; i++) {
        logprintf(conn, "  [%zu]: %s ", i, data_buffers[i]);
    }

    logprintf(conn, "\n");

    if ( str_count < 3) {
        log_line(conn, "  invalid response length.\n");
        return;
    }

    if (strcmp(data_buffers[0], "CMDRESULT") == 0) {
        log_command_response(conn, unescape_str(data_buffers[1]));
        return;
    }

    /*
     * STAT;imei;name;value - a reading that is not a position. The watch has dedicated packets
     * for a pulse or a step count; a phone reports whatever sensors it happens to have through
     * this one message, and the value is written under the same stat name the rest of the system
     * already charts. The imei is needed here because a stat can be the first thing a phone sends
     * after connecting, before any position has identified it.
     */
    if (strcmp(data_buffers[0], "STAT") == 0 && str_count >= 4) {
        unsigned char stat_imei[18] = {0};
        memcpy(stat_imei, data_buffers[1], min(strlen(data_buffers[1]), 16));
        pad_imei(stat_imei);

        if (strlen(conn->imei) <= 1 || strcmp(conn->imei, stat_imei) != 0) {
            snprintf(conn->imei, sizeof(conn->imei), "%s", stat_imei);
            init_imei(conn);
        }

        //a phone reading is sent in real time and carries no timestamp of its own, so it is
        //stamped now rather than with a position clock that may lag it by a whole interval
        time_t when = time(0) > conn->device_time ? time(0) : conn->device_time;
        //the name is device text going into a comma separated file the page reads;
        //write_stat_at keeps only [A-Za-z0-9_.-] of it
        write_stat_at(conn, unescape_str(data_buffers[2]), parse_float(data_buffers[3]), when);
        conn->timeout_time = time(0) + BASIC_TIMEOUT;
        return;
    }

    /*
     * EVENT;imei;text - something the device wants in its event list, in its own words. A
     * controller or a phone has no alarm codes to map onto the watch's, and before this the
     * only way to get a line in front of the user was the server log.
     *
     * log_event stamps the line with conn->device_time, which on this protocol is the clock of
     * the last position - absent for a device that never sends one, and an interval stale for
     * one that does. Stamped now instead, the same as STAT, without disturbing the position
     * clock for anything else. The event file is comma separated with the text last, so a comma
     * in the text would cut it short on the page.
     */
    if (strcmp(data_buffers[0], "EVENT") == 0 && str_count >= 3) {
        unsigned char event_imei[18] = {0};
        memcpy(event_imei, data_buffers[1], min(strlen(data_buffers[1]), 16));
        pad_imei(event_imei);

        if (strlen(conn->imei) <= 1 || strcmp(conn->imei, event_imei) != 0) {
            snprintf(conn->imei, sizeof(conn->imei), "%s", event_imei);
            init_imei(conn);
        }

        unsigned char * text = unescape_str(data_buffers[2]);
        //The event file is one record per line, comma separated, with the text last. A comma
        //in the text would cut the record short on the page; a newline would end the record
        //altogether and make everything after it read as a second, forged event. Both are
        //attacker-controlled on this protocol - the message is terminated at '!', so a raw
        //newline before it travels intact - so both are blanked rather than only the comma.
        rep(text, ',', ' ', strlen(text));
        rep(text, '\n', ' ', strlen(text));
        rep(text, '\r', ' ', strlen(text));

        time_t position_time = conn->device_time;
        conn->device_time = time(0) > conn->device_time ? time(0) : conn->device_time;
        log_event(conn, text);
        conn->device_time = position_time;
        conn->timeout_time = time(0) + BASIC_TIMEOUT;
        return;
    }

    if ( str_count < 5) {
        log_line(conn, "  invalid location response length.\n");
        return;
    }

    memcpy(imei, data_buffers[1], min(strlen(data_buffers[1]), 16));
    pad_imei(imei);

    if (strlen(conn->imei) <= 1 || strcmp(conn->imei, imei) != 0) {
        snprintf(conn->imei, sizeof(conn->imei), "%s", imei);
        init_imei(conn);
    }

    if ( strlen(data_buffers[3]) > 4 ) {
        unsigned int coord_count = split_to(',', data_buffers[3], strlen(data_buffers[3]) + 1, coord_split, 2);

        if ( coord_count == 2 ) {
            lat = parse_float(coord_split[0]);
            lon = parse_float(coord_split[1]);
            num_sats = 1;

        } else {
            log_line(conn, "  missing lat or longitude.\n");
            num_sats = 0;
        }
    }

    unsigned int battery_level = parse_int( data_buffers[2], 3);

    //init_imei() above may have refused the IMEI (or none was ever established): then there
    //is nothing to record a position against and no lookup is made on its behalf
    if ( conn->can_log && strlen(data_buffers[4]) > 1 ) {
        size_t scanned = split_to('|', data_buffers[4], strlen(data_buffers[4]) + 1, wifi_split, 16);
        size_t usable = 0;

        //only addresses that parse, packed to the front
        for (size_t i = 0; i < scanned && usable < WIFI_LOOKUP_MAX; i++) {
            if (parse_mac(wifi_split[i], "%2x:%2x:%2x:%2x:%2x:%2x", db_entry.network_buffer[usable].mac_addr)) {
                usable++;
            }
        }

        db_entry.network_count = usable;

        if ( db_entry.network_count > 2) {

            db_entry.result =  wifi_lookup(db_entry.network_buffer,  db_entry.network_count);

            if (db_entry.result.valid) {
                position_type = 2;
                lat = db_entry.result.lat;
                lon = db_entry.result.lng;
                num_sats =  db_entry.network_count;

            } else {
                //teach the database where these networks are, if the fix is one worth
                //learning from - see wifi_learn_position()
                wifi_learn_position(&db_entry, lat, lon, speed, conn->established);
            }
        }
    }

    //test if GPS is equal to last GPS coordinates - if so don't update position

    if (num_sats > 0) {
        if (position_type != 0 || ((conn->last_gps_lat != lat || conn->last_gps_lon != lon) && (conn->last_gps_lat > -999)) ) {
            move_to(conn, t, position_type, lat, lon);
        }

        write_stat(conn, "battery_level", battery_level);
        set_status(conn, battery_level, 0, position_type, num_sats );
    }

    if (battery_level < 20 && (( time(0) - conn->since_battalm) > 600)) {
        log_event(conn, "low battery");
    }
}

void basic_process(void * vp) {
    connection * conn = (connection *)vp;
    conn->current_packet_valid = false;

    //if we've at least got a header
    if (conn->read_count > 5) {
        rep(conn->recv_buffer, 0, ' ', conn->read_count); //remove all null characters up to read count
        long found = find_byte(conn->recv_buffer, conn->read_count, '!');

        //an empty frame is consumed, or it wedges everything behind it - see find_byte()
        if (found == 0) {
            memmove(conn->recv_buffer, conn->recv_buffer + 1, conn->read_count - 1);
            conn->read_count -= 1;
            return;
        }

        if (found > 0) {
            size_t index = (size_t)found;
            //terminate the message at its '!' before handing it on. basic_process_message reads
            //to the null rather than to the length it is given, so without this a second message
            //already sitting in the buffer behind this one bled into its last field - a burst of
            //reports that arrived in a single read fused into one.
            conn->recv_buffer[index] = 0;
            index++;
            basic_process_message(conn, conn->recv_buffer, index);
            memmove(conn->recv_buffer, conn->recv_buffer + index, conn->read_count - index);
            conn->read_count -= index;
            return;
        }
    }

    //if we're idle, and 5 minutes have passed we should get status from our device
    if (conn->read_count == 0 && ( time(0) - conn->since_last_status ) > 300) {
        conn->since_last_status = time(0);
    }
}

/*
 * The phone speaks the command channel this protocol already has: a command is a newline
 * terminated string, and the app knows WARN, WARNAUDIO and QUIET. These were empty, so a
 * geofence crossing raised on a phone connection did nothing at all. They send the command the
 * app is waiting for, with the reason passed through so the notification can say what happened.
 */
void basic_warn(void * vp, const char * reason ) {
    connection * conn = (connection *)vp;
    char buffer[BUF_SIZE] = {0};
    snprintf(buffer, sizeof(buffer) - 1, "WARN;%s", reason ? reason : "");
    conn->COMMAND_FUNCTION(conn, buffer);
}

void basic_warn_audio(void * vp, const char * reason) {
    connection * conn = (connection *)vp;
    char buffer[BUF_SIZE] = {0};
    snprintf(buffer, sizeof(buffer) - 1, "WARNAUDIO;%s", reason ? reason : "");
    conn->COMMAND_FUNCTION(conn, buffer);
}

void basic_identify(void * vp) {
    connection * conn = (connection *)vp;
    const uint8_t basic_start_contains[] = "BASIC;";
    uint8_t first_bytes[8];
    memset(first_bytes, 0, sizeof(first_bytes));
    memcpy(first_bytes, conn->recv_buffer, 7);

    if (strstr(first_bytes, basic_start_contains) != 0) {
        fprintf(stdout, "  device type is phone\n");
        conn->PROCESS_FUNCTION = basic_process;
        conn->COMMAND_FUNCTION = basic_send_command;
        conn->WARNING_FUNCTION = basic_warn;
        conn->AUDIO_WARNING_FUNCTION = basic_warn_audio;
        conn->MOTOR_WARNING_FUNCTION = basic_warn;
        conn->timeout_time = time(0) + BASIC_TIMEOUT;
    }
}