#include #include #include #include #include #include #include #include #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)); normalise_imei((char *)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)); normalise_imei((char *)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)); normalise_imei((char *)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; } }