root/src/kits/network/libnetservices/Geolocation.cpp
/*
 * Copyright 2014, Haiku, Inc. All Rights Reserved.
 * Copyright 2019, Adrien Destugues, pulkomandy@pulkomandy.tk
 * Distributed under the terms of the MIT License.
 */


#include <Geolocation.h>

#include <HttpRequest.h>
#include <Json.h>
#include <NetworkDevice.h>
#include <NetworkInterface.h>
#include <NetworkRoster.h>
#include <String.h>
#include <UrlProtocolRoster.h>
#include <UrlRequest.h>


namespace BPrivate {

namespace Network {

class GeolocationListener: public BUrlProtocolListener
{
        public:
                GeolocationListener()
                {
                        pthread_cond_init(&fCompletion, NULL);
                        pthread_mutex_init(&fLock, NULL);
                }

                virtual ~GeolocationListener() {
                        pthread_cond_destroy(&fCompletion);
                        pthread_mutex_destroy(&fLock);
                }

                void ConnectionOpened(BUrlRequest* caller)
                {
                        pthread_mutex_lock(&fLock);
                }

                void RequestCompleted(BUrlRequest* caller, bool success) {
                        pthread_cond_signal(&fCompletion);
                        pthread_mutex_unlock(&fLock);
                }

                pthread_cond_t fCompletion;
                pthread_mutex_t fLock;
};


BGeolocation::BGeolocation()
        : fGeolocationService(kDefaultGeolocationService, true),
        fGeocodingService(kDefaultGeocodingService, true)
{
}


BGeolocation::BGeolocation(const BUrl& geolocationService,
        const BUrl& geocodingService)
        : fGeolocationService(geolocationService),
        fGeocodingService(geocodingService)
{
        if (!fGeolocationService.IsValid())
                fGeolocationService.SetUrlString(kDefaultGeolocationService, true);
        if (!fGeocodingService.IsValid())
                fGeocodingService.SetUrlString(kDefaultGeocodingService, true);
}


status_t
BGeolocation::LocateSelf(float& latitude, float& longitude)
{
        // Enumerate wifi network and build JSON message
        BNetworkRoster& roster = BNetworkRoster::Default();
        uint32 interfaceCookie = 0;
        BNetworkInterface interface;

        BString query("{\n\t\"wifiAccessPoints\": [");
        int32 count = 0;

        while (roster.GetNextInterface(&interfaceCookie, interface) == B_OK) {
                BNetworkDevice device(interface.Name());
                        // TODO is that the correct way to enumerate devices?

                uint32 networksCount = 0;
                wireless_network* networks = NULL;
                device.GetNetworks(networks, networksCount);
                for (uint32 i = 0; i < networksCount; i++) {
                        const wireless_network& network = networks[i];
                        if (count != 0)
                                query += ',';

                        count++;

                        query += "\n\t\t{ \"macAddress\": \"";
                        query += network.address.ToString().ToUpper();
                        query += "\", \"signalStrength\": ";
                        query << (int)network.signal_strength;
                        query += ", \"signalToNoiseRatio\": ";
                        query << (int)network.noise_level;
                        query += " }";
                }
                delete[] networks;
        }

        query += "\n\t]\n}\n";

        // Check that we have enough data (we need at least 2 networks)
        if (count < 2)
                return B_DEVICE_NOT_FOUND;

        GeolocationListener listener;
        BMallocIO resultBuffer;

        // Send Request (POST JSON message)
        BUrlRequest* request = BUrlProtocolRoster::MakeRequest(fGeolocationService,
                &resultBuffer, &listener);
        if (request == NULL)
                return B_BAD_DATA;

        BHttpRequest* http = dynamic_cast<BHttpRequest*>(request);
        if (http == NULL) {
                delete request;
                return B_BAD_DATA;
        }

        // There are no API keys for BeaconDB, instead they ask to set a user agent identifying the
        // software. Let's also include a contact address in case something goes wrong.
        http->SetUserAgent("Haiku/R1 haiku-development@freelists.org");

        http->SetMethod(B_HTTP_POST);

        BHttpHeaders headers;
        headers.AddHeader("Content-Type", "application/json");
        http->SetHeaders(headers);

        BMemoryIO* io = new BMemoryIO(query.String(), query.Length());
        http->AdoptInputData(io, query.Length());

        status_t result = http->Run();
        if (result < 0) {
                delete http;
                return result;
        }

        pthread_mutex_lock(&listener.fLock);
        while (http->IsRunning())
                pthread_cond_wait(&listener.fCompletion, &listener.fLock);
        pthread_mutex_unlock(&listener.fLock);

        // Parse reply
        const BHttpResult& reply = (const BHttpResult&)http->Result();
        if (reply.StatusCode() != 200) {
                delete http;
                return B_ERROR;
        }

        BMessage data;
        result = BJson::Parse((char*)resultBuffer.Buffer(), data);
        delete http;
        if (result != B_OK) {
                return result;
        }

        BMessage location;
        result = data.FindMessage("location", &location);
        if (result != B_OK)
                return result;

        double lat, lon;
        result = location.FindDouble("lat", &lat);
        if (result != B_OK)
                return result;
        result = location.FindDouble("lng", &lon);
        if (result != B_OK)
                return result;

        latitude = lat;
        longitude = lon;

        return result;
}


status_t
BGeolocation::Country(const float latitude, const float longitude,
        BCountry& country)
{
        // Prepare the request URL
        BUrl url(fGeocodingService);
        BString requestString;
        requestString.SetToFormat("%s&lat=%f&lng=%f", url.Request().String(), latitude,
                longitude);
        url.SetPath("/countryCode");
        url.SetRequest(requestString);

        GeolocationListener listener;
        BMallocIO resultBuffer;
        BUrlRequest* request = BUrlProtocolRoster::MakeRequest(url,
                &resultBuffer, &listener);
        if (request == NULL)
                return B_BAD_DATA;

        BHttpRequest* http = dynamic_cast<BHttpRequest*>(request);
        if (http == NULL) {
                delete request;
                return B_BAD_DATA;
        }

        status_t result = http->Run();
        if (result < 0) {
                delete http;
                return result;
        }

        pthread_mutex_lock(&listener.fLock);
        while (http->IsRunning()) {
                pthread_cond_wait(&listener.fCompletion, &listener.fLock);
        }
        pthread_mutex_unlock(&listener.fLock);

        // Parse reply
        const BHttpResult& reply = (const BHttpResult&)http->Result();
        if (reply.StatusCode() != 200) {
                delete http;
                return B_ERROR;
        }

        off_t length = 0;
        resultBuffer.GetSize(&length);
        length -= 2; // Remove \r\n from response
        BString countryCode((char*)resultBuffer.Buffer(), (int32)length);
        return country.SetTo(countryCode);
}


const char* BGeolocation::kDefaultGeolocationService = "https://api.beacondb.net/v1/geolocate";

#ifdef HAVE_DEFAULT_GEOLOCATION_SERVICE_KEY

#include "DefaultGeolocationServiceKey.h"

const char* BGeolocation::kDefaultGeocodingService
        = "https://secure.geonames.org/?username="
                DEFAULT_GEOCODING_SERVICE_KEY;

#else

const char* BGeolocation::kDefaultGeocodingService = "";

#endif

}       // namespace Network

}       // namespace BPrivate