diff --git a/Dockerfile b/Dockerfile index fb015cb..495699e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,4 @@ FROM php:apache -COPY backend-php/ /var/www/html/ -COPY frontend/ /var/www/html/ -COPY docker/start.sh . RUN apt-get update && \ apt-get install -y memcached libmemcached-dev zlib1g-dev libldap2-dev && \ @@ -12,7 +9,11 @@ RUN apt-get update && \ EXPOSE 80/tcp VOLUME /etc/hauk - STOPSIGNAL SIGINT + +COPY docker/start.sh . RUN chmod +x ./start.sh +COPY backend-php/ /var/www/html/ +COPY frontend/ /var/www/html/ + CMD ["./start.sh"] diff --git a/android/app/src/main/java/info/varden/hauk/Constants.java b/android/app/src/main/java/info/varden/hauk/Constants.java index e75524b..401e900 100644 --- a/android/app/src/main/java/info/varden/hauk/Constants.java +++ b/android/app/src/main/java/info/varden/hauk/Constants.java @@ -123,6 +123,7 @@ public enum Constants { public static final String PACKET_PARAM_SPEED = "spd"; public static final String PACKET_PARAM_TIMESTAMP = "time"; public static final String PACKET_PARAM_USERNAME = "usr"; + public static final String PACKET_PARAM_ALTITUDE = "alt"; // Packet OK response header. All valid packets start with this line. public static final String PACKET_RESPONSE_OK = "OK"; diff --git a/android/app/src/main/java/info/varden/hauk/http/LocationUpdatePacket.java b/android/app/src/main/java/info/varden/hauk/http/LocationUpdatePacket.java index 4c571fa..21a86f7 100644 --- a/android/app/src/main/java/info/varden/hauk/http/LocationUpdatePacket.java +++ b/android/app/src/main/java/info/varden/hauk/http/LocationUpdatePacket.java @@ -5,9 +5,15 @@ import android.util.Base64; import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import javax.crypto.BadPaddingException; import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; import info.varden.hauk.Constants; import info.varden.hauk.R; @@ -48,36 +54,17 @@ protected LocationUpdatePacket(Context ctx, Session session, Location location, super(ctx, session.getServerURL(), session.getConnectionParameters(), Constants.URL_PATH_POST_LOCATION); setParameter(Constants.PACKET_PARAM_SESSION_ID, session.getID()); - if (session.getDerivableE2EKey() == null) { - // If not using end-to-end encryption, send parameters in plain text. - setParameter(Constants.PACKET_PARAM_LATITUDE, String.valueOf(location.getLatitude())); - setParameter(Constants.PACKET_PARAM_LONGITUDE, String.valueOf(location.getLongitude())); - setParameter(Constants.PACKET_PARAM_PROVIDER_ACCURACY, String.valueOf(accuracy.getMode())); - setParameter(Constants.PACKET_PARAM_TIMESTAMP, String.valueOf(System.currentTimeMillis() / (double) TimeUtils.MILLIS_PER_SECOND)); + Cipher cipher = initCipher(session); - // Not all devices provide these parameters: - if (location.hasSpeed()) setParameter(Constants.PACKET_PARAM_SPEED, String.valueOf(location.getSpeed())); - if (location.hasAccuracy()) setParameter(Constants.PACKET_PARAM_ACCURACY, String.valueOf(location.getAccuracy())); - } else { - // We're using end-to-end encryption - generate an IV and encrypt all parameters. - try { - Cipher cipher = Cipher.getInstance(Constants.E2E_TRANSFORMATION); - cipher.init(Cipher.ENCRYPT_MODE, session.getDerivableE2EKey().deriveSpec(), new SecureRandom()); - byte[] iv = cipher.getIV(); - setParameter(Constants.PACKET_PARAM_INIT_VECTOR, Base64.encodeToString(iv, Base64.DEFAULT)); + encryptAndSetParameter(Constants.PACKET_PARAM_LATITUDE, location.getLatitude(), cipher); + encryptAndSetParameter(Constants.PACKET_PARAM_LONGITUDE, location.getLongitude(), cipher); + encryptAndSetParameter(Constants.PACKET_PARAM_PROVIDER_ACCURACY, accuracy.getMode(), cipher); + encryptAndSetParameter(Constants.PACKET_PARAM_TIMESTAMP, System.currentTimeMillis() / (double) TimeUtils.MILLIS_PER_SECOND, cipher); - setParameter(Constants.PACKET_PARAM_LATITUDE, Base64.encodeToString(cipher.doFinal(String.valueOf(location.getLatitude()).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT)); - setParameter(Constants.PACKET_PARAM_LONGITUDE, Base64.encodeToString(cipher.doFinal(String.valueOf(location.getLongitude()).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT)); - setParameter(Constants.PACKET_PARAM_PROVIDER_ACCURACY, Base64.encodeToString(cipher.doFinal(String.valueOf(accuracy.getMode()).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT)); - setParameter(Constants.PACKET_PARAM_TIMESTAMP, Base64.encodeToString(cipher.doFinal(String.valueOf(System.currentTimeMillis() / (double) TimeUtils.MILLIS_PER_SECOND).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT)); - - // Not all devices provide these parameters: - if (location.hasSpeed()) setParameter(Constants.PACKET_PARAM_SPEED, Base64.encodeToString(cipher.doFinal(String.valueOf(location.getSpeed()).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT)); - if (location.hasAccuracy()) setParameter(Constants.PACKET_PARAM_ACCURACY, Base64.encodeToString(cipher.doFinal(String.valueOf(location.getAccuracy()).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT)); - } catch (Exception e) { - Log.e("Error was thrown when encrypting location data", e); //NON-NLS - } - } + // Not all devices provide these parameters: + if (location.hasSpeed()) encryptAndSetParameter(Constants.PACKET_PARAM_SPEED, location.getSpeed(), cipher); + if (location.hasAccuracy()) encryptAndSetParameter(Constants.PACKET_PARAM_ACCURACY, location.getAccuracy(), cipher); + if (location.hasAltitude()) encryptAndSetParameter(Constants.PACKET_PARAM_ALTITUDE, location.getAltitude(), cipher); } @SuppressWarnings("DesignForExtension") @@ -113,4 +100,33 @@ protected void onSuccess(String[] data, Version backendVersion) throws ServerExc throw new ServerException(err.toString()); } } + + private Cipher initCipher(Session session) { + Cipher cipher = null; + if (session.getDerivableE2EKey() != null) { + try { + cipher = Cipher.getInstance(Constants.E2E_TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, session.getDerivableE2EKey().deriveSpec(), new SecureRandom()); + byte[] iv = cipher.getIV(); + setParameter(Constants.PACKET_PARAM_INIT_VECTOR, Base64.encodeToString(iv, Base64.DEFAULT)); + } catch (NoSuchAlgorithmException | InvalidKeyException | InvalidKeySpecException | NoSuchPaddingException exception) { + Log.e("Error was thrown while initializing E2E encryption", exception); //NON-NLS + } + } + return cipher; + } + + private void encryptAndSetParameter(String key, V value, Cipher cipher) { + if (cipher != null) { + // We're using end-to-end encryption - generate an IV and encrypt all parameters. + try { + setParameter(key, Base64.encodeToString(cipher.doFinal(String.valueOf(value).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT)); + } catch (BadPaddingException | IllegalBlockSizeException exception) { + Log.e("Error was thrown while encrypting location data", exception); //NON-NLS + } + } else { + // If not using end-to-end encryption, send parameters in plain text. + setParameter(key, String.valueOf(value)); + } + } } diff --git a/backend-php/api/post.php b/backend-php/api/post.php index 05048c1..562e6e9 100644 --- a/backend-php/api/post.php +++ b/backend-php/api/post.php @@ -21,45 +21,37 @@ $session = new Client($memcache, $sid); if (!$session->exists()) die($LANG['session_expired']."\n"); -if (!$session->isEncrypted()) { - // Perform input validation. - $lat = floatval($_POST["lat"]); - $lon = floatval($_POST["lon"]); - $time = floatval($_POST["time"]); - if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) die($LANG['location_invalid']."\n"); - - // Not all devices report speed and accuracy, but if available, report them - // too. - $speed = isset($_POST["spd"]) ? floatval($_POST["spd"]) : null; - $accuracy = isset($_POST["acc"]) ? floatval($_POST["acc"]) : null; - $provider = isset($_POST["prv"]) && $_POST["prv"] == "1" ? 1 : 0; - - // The location data object contains the sharing interval (i), duration (d) - // and a location list (l). Each entry in the location list contains a - // latitude, longitude, timestamp, provider, accuracy and speed, in that - // order, as an array. - $session->addPoint([$lat, $lon, $time, $provider, $accuracy, $speed])->save(); - -} else { - // Input validation cannot be performed for end-to-end encrypted data. - $lat = $_POST["lat"]; - $lon = $_POST["lon"]; - $time = $_POST["time"]; - $speed = isset($_POST["spd"]) ? $_POST["spd"] : null; - $accuracy = isset($_POST["acc"]) ? $_POST["acc"] : null; - $provider = isset($_POST["prv"]) ? $_POST["prv"] : null; - +$lat = $_POST["lat"]; +$lon = $_POST["lon"]; +$time = $_POST["time"]; +$speed = isset($_POST["spd"]) ? $_POST["spd"] : null; +$altitude = isset($_POST["alt"]) ? $_POST["alt"] : null; +$accuracy = isset($_POST["acc"]) ? $_POST["acc"] : null; +$provider = isset($_POST["prv"]) ? $_POST["prv"] : null; + +// The location data object contains the sharing interval (i), duration (d) +// and a location list (l). Each entry in the location list contains a +// latitude, longitude, timestamp, provider, accuracy and speed, in that +// order, as an array. +$point = [$lat, $lon, $time, $provider, $accuracy, $speed, $altitude]; + +if ($session->isEncrypted()) { // End-to-end encrypted connections also have an IV field used to decrypt // the data fields. requirePOST("iv"); $iv = $_POST["iv"]; // The IV field is prepended to the array to send to the client. - $session->addPoint([$iv, $lat, $lon, $time, $provider, $accuracy, $speed])->save(); + array_unshift($point , $iv); +} else { + // Perform input validation + if (floatval($lat) < -90 || floatval($lat) > 90 || floatval($lon) < -180 || floatval($lon) > 180) die($LANG['location_invalid']."\n"); } +$session->addPoint($point)->save(); + if ($session->hasExpired()) { echo $LANG['session_expired']."\n"; } else { echo "OK\n".getConfig("public_url")."?%s\n".implode(",", $session->getTargetIDs())."\n"; -} +} \ No newline at end of file diff --git a/backend-php/dynamic.js.php b/backend-php/dynamic.js.php index 325bead..47e2867 100644 --- a/backend-php/dynamic.js.php +++ b/backend-php/dynamic.js.php @@ -15,5 +15,8 @@ var VELOCITY_DELTA_TIME = ; var TRAIL_COLOR = ; var VELOCITY_UNIT = ; +var ALTITUDE_UNIT = ; +var SHOW_VELOCITY = ; +var SHOW_ALTITUDE_AMSL = ; var OFFLINE_TIMEOUT = ; var REQUEST_TIMEOUT = ; diff --git a/backend-php/include/config-sample.php b/backend-php/include/config-sample.php index 9b9d07d..9186369 100644 --- a/backend-php/include/config-sample.php +++ b/backend-php/include/config-sample.php @@ -274,6 +274,16 @@ // KILOMETERS_PER_HOUR, MILES_PER_HOUR, METERS_PER_SECOND "velocity_unit" => KILOMETERS_PER_HOUR, +// The unit of measurement of altitude. Valid are: +// METERS, FEET +"altitude_unit" => METERS, + +// Display velocity below marker +"show_velocity" => true, + +// Display altitude AMSL below marker +"show_altitude_amsl"=> true, + // The publicly accessible URL to reach Hauk, with trailing slash. "public_url" => 'https://example.com/' diff --git a/backend-php/include/inc.php b/backend-php/include/inc.php index 64b6d16..318a455 100644 --- a/backend-php/include/inc.php +++ b/backend-php/include/inc.php @@ -59,6 +59,16 @@ "mpsMultiplier" => 1, "unit" => "m/s" ); +const METERS = array( + // Absolute distance in meters + "metersMultiplier" => 1, + "unit" => "m" +); +const FEET = array( + // Absolute distance in feet + "metersMultiplier" => 3.280839895, + "unit" => "ft" +); // Load fallback language. include(__DIR__."/lang/en/texts.php"); @@ -149,6 +159,9 @@ "v_data_points" => 2, "trail_color" => '#d80037', "velocity_unit" => KILOMETERS_PER_HOUR, + "altitude_unit" => METERS, + "show_velocity" => true, + "show_altitude_amsl" => true, "public_url" => 'https://example.com/' ); diff --git a/frontend/main.js b/frontend/main.js index 14581d2..5bc0b3a 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -700,12 +700,13 @@ function processUpdate(data, init) { var lastPoint = shares[user].points.length > 0 ? shares[user].points[shares[user].points.length - 1] : null; for (var i = 0; i < users[user].length; i++) { - var lat = users[user][i][0]; - var lon = users[user][i][1]; - var time = users[user][i][2]; - var prov = users[user][i][3]; - var acc = users[user][i][4]; - var spd = users[user][i][5]; + var lat = getValueOrNull(users[user][i],0); + var lon = getValueOrNull(users[user][i],1); + var time = getValueOrNull(users[user][i],2); + var prov = getValueOrNull(users[user][i],3); + var acc = getValueOrNull(users[user][i],4); + var spd = getValueOrNull(users[user][i],5); + var alt = getValueOrNull(users[user][i],6); // Default to "Fine" provider for older clients. if (prov === null) prov = LOC_PROVIDER_FINE; @@ -726,14 +727,23 @@ function processUpdate(data, init) { '
' + '

' + '' + - '' + - '0.0 ' + - VELOCITY_UNIT.unit + - '' + + '' + + 'vel:' + + '0.0 ' + + VELOCITY_UNIT.unit + '' + + '
' + + '
' + + '' + + 'alt:' + + '0.0 ' + + ALTITUDE_UNIT.unit + '' + + '' + + '' + '' + '

' + '', - iconAnchor: [33, 18] + // FIXME: hard-coded and dependend on style.css .marker + iconAnchor: [48, 18] }); shares[user].marker = L.marker([lat, lon], {icon: shares[user].icon}).on("click", function() { follow(this.haukUser); @@ -754,7 +764,7 @@ function processUpdate(data, init) { shares[user].circle.setLatLng([lat, lon]); if (acc !== null) shares[user].circle.setRadius(acc); } - shares[user].points.push({lat: lat, lon: lon, line: line, time: time, spd: spd, acc: acc}); + shares[user].points.push({lat: lat, lon: lon, line: line, time: time, spd: spd, acc: acc, alt: alt}); lastPoint = shares[user].points[shares[user].points.length - 1]; } } @@ -797,6 +807,20 @@ function processUpdate(data, init) { vel = velocity(dist, time); eVelocity.textContent = vel.toFixed(1);; } + + // Altitude (If available) + var eAltitude = document.getElementById("altitude-" + shares[user].id); + var alt = 0; + var eAltitudeContainer = document.getElementById("altitude-container-" + shares[user].id); + if (lastPoint !== null && lastPoint.alt !== null && eAltitude !== null) { + alt = lastPoint.alt * ALTITUDE_UNIT.metersMultiplier; + eAltitude.textContent = alt.toFixed(1); + if (SHOW_ALTITUDE_AMSL) { + eAltitudeContainer.classList.remove("hidden"); + } + } else { + eAltitudeContainer.classList.add("hidden"); + } // Flag that the first location has been received, for map centering. if (lastPoint !== null && !hasReceivedFirst) { @@ -906,6 +930,14 @@ function processUpdate(data, init) { } } +function getValueOrNull(points,idx) { + var value = null; + if (idx < points.length) { + value = points[idx]; + } + return value; +} + // Calculates the distance between two points on a sphere using the Haversine // algorithm. function distance(from, to) { diff --git a/frontend/style.css b/frontend/style.css index 4967b12..4fb6db8 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -264,7 +264,7 @@ a:last-child > .store-icon { /* The outer marker div. */ .marker { - width: 66px; + width: 96px; height: 62px; } @@ -312,8 +312,8 @@ a:last-child > .store-icon { width: 100%; border-radius: 15px; text-align: center; - padding: 2px 0; - line-height: 100%; + padding: 5px 0 5px; + line-height: 125%; font-family: sans-serif; overflow: hidden; white-space: nowrap; @@ -334,7 +334,7 @@ a:last-child > .store-icon { background-color: rgba(165,0,42,0.5); } -.marker p.dead > span.velocity { +.marker p.dead > span.metric { display: none; } @@ -343,3 +343,20 @@ a:last-child > .store-icon { background: none; border: none; } + +.metric-label { + display:inline-block; + width: 30%; + text-align:right; + vertical-align: bottom; + padding-right: 0.5em; +} + +.metric-value { + display:inline-block; + width: 70%; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; +}