From 4bd6bbd788454367cc89d78543312f333051b840 Mon Sep 17 00:00:00 2001
From: "kali kaneko (leap communications)" <kali@leap.se>
Date: Wed, 2 Sep 2020 23:47:05 +0200
Subject: [PATCH] [feat] expose gateway selector in gui

---
 gui/gui.qrc                     |   3 +
 gui/handlers.cpp                |   5 +
 gui/handlers.h                  |   1 +
 gui/js/maps.js                  |  83 +++++++++++
 gui/qml/VpnState.qml            |  64 +++++++++
 gui/qml/main.qml                | 238 ++++++++++++++++++--------------
 pkg/backend/api.go              |   9 +-
 pkg/backend/status.go           |  36 +++--
 pkg/bitmask/bitmask.go          |   1 +
 pkg/vpn/bonafide/bonafide.go    |   6 +-
 pkg/vpn/bonafide/eip_service.go |  24 ++--
 pkg/vpn/bonafide/gateways.go    |  42 +++---
 pkg/vpn/openvpn.go              |   9 ++
 13 files changed, 373 insertions(+), 148 deletions(-)
 create mode 100644 gui/js/maps.js
 create mode 100644 gui/qml/VpnState.qml

diff --git a/gui/gui.qrc b/gui/gui.qrc
index 5e0d4aee..fdea1096 100644
--- a/gui/gui.qrc
+++ b/gui/gui.qrc
@@ -2,6 +2,7 @@
     <qresource prefix="/">
 
         <file>qml/main.qml</file>
+        <file>qml/VpnState.qml</file>
         <file>qml/AboutDialog.qml</file>
         <file>qml/DonateDialog.qml</file>
         <file>qml/LoginDialog.qml</file>
@@ -24,5 +25,7 @@
 
         <file alias="providers.json">providers/providers.json</file>
         <file>assets/svg/world.svg</file>
+        <file>js/maps.js</file>
+
     </qresource>
 </RCC>
diff --git a/gui/handlers.cpp b/gui/handlers.cpp
index 8f0e0d05..370c67cb 100644
--- a/gui/handlers.cpp
+++ b/gui/handlers.cpp
@@ -42,6 +42,11 @@ void Backend::donateSeen()
     DonateSeen();
 }
 
+void Backend::useGateway(QString label)
+{
+    UseGateway(toGoStr(label));
+}
+
 void Backend::login(QString username, QString password)
 {
     Login(toGoStr(username), toGoStr(password));
diff --git a/gui/handlers.h b/gui/handlers.h
index 8283645d..a7832072 100644
--- a/gui/handlers.h
+++ b/gui/handlers.h
@@ -36,6 +36,7 @@ public slots:
     void switchOff();
     void donateAccepted();
     void donateSeen();
+    void useGateway(QString username);
     void login(QString username, QString password);
     void resetError(QString errlabel);
     void resetNotification(QString label);
diff --git a/gui/js/maps.js b/gui/js/maps.js
new file mode 100644
index 00000000..c6f44d22
--- /dev/null
+++ b/gui/js/maps.js
@@ -0,0 +1,83 @@
+// Robinson projection calculation
+
+// Written by Niklas Bichinger (bichinger.de). This code is Public Domain - use as you like.
+
+// source of robinson numbers: https://simplemaps.com/static/img/flash/robinson_projection_table.jpg
+var robinsonAA = [
+    0.84870000,
+    0.84751182,
+    0.84479598,
+    0.84021300,
+    0.83359314,
+    0.82578510,
+    0.81475200,
+    0.80006949,
+    0.78216192,
+    0.76060494,
+    0.73658673,
+    0.70866450,
+    0.67777182,
+    0.64475739,
+    0.60987582,
+    0.57134484,
+    0.52729731,
+    0.48562614,
+    0.45167814
+];
+var robinsonBB = [
+    0.00000000,
+    0.08384260,
+    0.16768520,
+    0.25152780,
+    0.33537040,
+    0.41921300,
+    0.50305560,
+    0.58689820,
+    0.67047034,
+    0.75336633,
+    0.83518048,
+    0.91537187,
+    0.99339958,
+    1.06872269,
+    1.14066505,
+    1.20841528,
+    1.27035062,
+    1.31998003,
+    1.35230000
+];
+
+function project(latitude, longitude, mapWidth, heightFactor, mapOffsetX, mapOffsetY) {
+    if (typeof heightFactor === 'undefined') { heightFactor = 1; }
+    if (typeof mapOffsetX === 'undefined') { mapOffsetX = 0; }
+    if (typeof mapOffsetY === 'undefined') { mapOffsetY = 0; }
+
+    // Robinson's latitude interpolation points are in 5-degree-steps
+    var latitudeAbs = Math.abs(latitude);
+    var latitudeStepFloor = Math.floor(latitudeAbs / 5);
+    var latitudeStepCeil = Math.ceil(latitudeAbs / 5);
+    // calc interpolation factor (>=0 to <1) between two steps
+    var latitudeInterpolation = (latitudeAbs - latitudeStepFloor * 5) / 5;
+
+    // interpolate robinson table values
+    var AA = robinsonAA[latitudeStepFloor] + (robinsonAA[latitudeStepCeil] - robinsonAA[latitudeStepFloor]) * latitudeInterpolation;
+    var BB = robinsonBB[latitudeStepFloor] + (robinsonBB[latitudeStepCeil] - robinsonBB[latitudeStepFloor]) * latitudeInterpolation;
+
+    var robinsonWidth = 2 * Math.PI * robinsonAA[0];
+    var widthFactor = mapWidth / robinsonWidth;
+    var latitudeSign = Math.sign(latitude) || 1;
+    var x = (widthFactor * AA * longitude * Math.PI) / 180 + mapOffsetX;
+    var y = widthFactor * BB * latitudeSign * heightFactor + mapOffsetY;
+
+    return {x: x, y: y};
+}
+
+function projectAbsolute(latitude, longitude, mapWidth, heightFactor, mapOffsetX, mapOffsetY) {
+    if (typeof heightFactor === 'undefined') { heightFactor = 1; }
+
+    var relative = project(latitude, longitude, mapWidth, heightFactor, mapOffsetX, mapOffsetY);
+    var widthHeightRatio = Math.PI * robinsonAA[0] / robinsonBB[18];
+    var x = mapWidth / 2 + relative.x;
+    var y = mapWidth / widthHeightRatio * heightFactor / 2 - relative.y;
+
+    return {x: x, y: y};
+}
diff --git a/gui/qml/VpnState.qml b/gui/qml/VpnState.qml
new file mode 100644
index 00000000..ea2a3b14
--- /dev/null
+++ b/gui/qml/VpnState.qml
@@ -0,0 +1,64 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.4
+
+StateGroup {
+
+    state: ctx ? ctx.status : ""
+
+    states: [
+        State { name: "initializing" },
+        State {
+            name: "off"
+            PropertyChanges { target: systray; tooltip: toHuman("off"); icon.source: icons["off"] }
+            PropertyChanges { target: statusItem; text: toHuman("off") }
+            PropertyChanges { target: mainStatus; text: toHuman("off") }
+            PropertyChanges { target: mainCurrentGateway; text: "" }
+            PropertyChanges { target: mainOnBtn; visible: true }
+            PropertyChanges { target: mainOffBtn; visible: false }
+            PropertyChanges { target: gwMarker; color: "red"}
+        },
+        State {
+            name: "on"
+            StateChangeScript {
+                script: displayGatewayMarker()
+            }
+            PropertyChanges { target: systray; tooltip: toHuman("on"); icon.source: icons["on"] }
+            PropertyChanges { target: statusItem; text: toHumanWithLocation("on") }
+            PropertyChanges { target: mainStatus; text: toHuman("on") }
+            PropertyChanges { target: mainCurrentGateway; text: qsTr("Connected to ") + ctx.currentGateway }
+            PropertyChanges { target: mainOnBtn; visible: false }
+            PropertyChanges { target: mainOffBtn; visible: true }
+            PropertyChanges { target: gwMarker; color: "green"}
+        },
+        State {
+            name: "starting"
+            PropertyChanges { target: systray; tooltip: toHuman("connecting"); icon.source: icons["wait"] }
+            PropertyChanges { target: statusItem; text: toHumanWithLocation("connecting") }
+            PropertyChanges { target: mainStatus; text: qsTr("Connecting...") }
+            PropertyChanges { target: mainCurrentGateway; text: "" }
+            PropertyChanges { target: mainOnBtn; visible: false }
+            PropertyChanges { target: mainOffBtn; visible: true }
+            PropertyChanges { target: gwMarker; color: "orange"}
+        },
+        State {
+            name: "stopping"
+            PropertyChanges { target: systray; tooltip: toHuman("stopping"); icon.source: icons["wait"] }
+            PropertyChanges { target: statusItem; text: toHuman("stopping") }
+            PropertyChanges { target: mainStatus; text: toHuman("stopping") }
+            PropertyChanges { target: mainCurrentGateway; text: "" }
+            PropertyChanges { target: mainOnBtn; visible: true }
+            PropertyChanges { target: mainOffBtn; visible: false }
+            PropertyChanges { target: gwMarker; color: "orange"}
+        },
+        State {
+            name: "failed"
+            PropertyChanges { target: systray; tooltip: toHuman("failed"); icon.source: icons["wait"] }
+            PropertyChanges { target: statusItem; text: toHuman("failed") }
+            PropertyChanges { target: mainStatus; text: toHuman("failed") }
+            PropertyChanges { target: mainCurrentGateway; text: "" }
+            PropertyChanges { target: mainOnBtn; visible: true }
+            PropertyChanges { target: mainOffBtn; visible: false }
+            PropertyChanges { target: gwMarker; color: "red"}
+        }
+    ]
+}
diff --git a/gui/qml/main.qml b/gui/qml/main.qml
index 98a4445c..bbc3f696 100644
--- a/gui/qml/main.qml
+++ b/gui/qml/main.qml
@@ -6,12 +6,19 @@ import QtQuick.Extras 1.2
 
 import Qt.labs.platform 1.1 as LabsPlatform
 
+import "qrc:/js/maps.js" as Maps
+
 ApplicationWindow {
 
     id: app
     visible: true
-    width: 700
-    height: 700
+    width: 300
+    height: 600
+    maximumWidth: 300
+    minimumWidth: 300
+    maximumHeight: 600
+    minimumHeight: 600
+    // TODO get a nice background color
 
     flags: Qt.WindowsStaysOnTopHint | Qt.Popup
 
@@ -19,52 +26,121 @@ ApplicationWindow {
     property var loginDone
     property var allowEmptyPass
 
-    ColumnLayout{
-        anchors.centerIn: parent
-        width: parent.width
-        Layout.preferredHeight:  parent.height
+    onWidthChanged: displayGatewayMarker()
+    onHeightChanged: displayGatewayMarker()
+
+    GridLayout {
+
         visible: true
+        columns: 3
 
-        Text{
-            id: mainStatus
-            text: "Status: off"
-            font.pixelSize: 22
-            Layout.preferredWidth: parent.width
-            horizontalAlignment: Text.AlignHCenter
-        }
+        Item {
+            Layout.column: 2
+            Layout.topMargin: app.height * 0.15
+            Layout.leftMargin: app.width * 0.10
 
-        Label {
-            text: "gateway selection:"
-            font.pixelSize: 20
-        }
+            ColumnLayout {
+                Layout.alignment: Qt.AlignHCenter
+
+                Text{
+                    id: mainStatus
+                    text: "off"
+                    font.pixelSize: 26
+                    Layout.alignment: Text.AlignHCenter
+                }
+
+                Text{
+                    id: mainCurrentGateway
+                    text: ""
+                    font.pixelSize: 20
+                    Layout.alignment: Text.AlignHCenter
+                }
 
-        ComboBox {
-          id: comboGw
-          editable: false
-          model: [ qsTr("Automatic"), qsTr("Paris"), qsTr("Amsterdam") ]
-          onAccepted: {
-              if (combo.find(currentText) === -1) {
-                 currentIndex = combo.find(editText)
-              }
-          }
+                Button {
+                    id: mainOnBtn
+                    x: 80
+                    y: 200
+                    text: qsTr("on")
+                    visible: true
+                    onClicked: backend.switchOn()
+                }
+
+                Button {
+                    id: mainOffBtn
+                    x: 180
+                    y: 200
+                    text: qsTr("off")
+                    visible: false
+                    onClicked: backend.switchOff()
+                }
+
+                ComboBox {
+                    id: gwSelector
+                    editable: false
+                    model: [qsTr("Automatic")]
+                    onActivated: {
+                        console.debug("Selected gateway:", currentText);
+                        backend.useGateway(currentText.toString());
+                    }
+                }
+            }
         }
 
-        ColumnLayout{
-            width: parent.width
+        Item {
+            Layout.topMargin: app.height * 0.40
+            Layout.row: 3
+            Layout.column: 1
+            Layout.columnSpan: 3
 
             Image {
                 id: worldMap
+                width: app.width
                 source: "qrc:/assets/svg/world.svg"
-                fillMode: Image.PreserveAspectCrop
+                fillMode: Image.PreserveAspectFit
+                smooth: true
             }
-       }
+
+            Rectangle {
+                id: gwMarker
+                x: worldMap.width * 0.5
+                y: worldMap.height * 0.5
+                width: 10
+                height: 10
+                radius: 10
+                color: "red"
+                z: worldMap.z + 1
+            }
+
+        }
+    }
+
+
+    function displayGatewayMarker() {
+        let coords = {
+            'paris': {'x': 48, 'y': 2},
+            'miami': {'x': 25.7  , 'y': -80.2 },
+            'amsterdam': {'x': 52.4, 'y': 4.9 },
+            'montreal': {'x': 45.3, 'y': -73.4 },
+            'seattle': {'x': 47.4, 'y': -122.2 },
+        }
+        let city = ctx.currentGateway.split('-')[0]
+        let coord = coords[city]
+
+        // TODO the Robinson projection does not seem to fit super-nicely with
+        // our map, and this offset doesn't work with bigg-ish sizes. But good
+        // enough for a proof of concept - if we avoid resizing the window.
+        let xOffset = -1 * 0.10 * worldMap.width
+        let p = Maps.projectAbsolute(coord.x, coord.y, worldMap.width, 1, xOffset)
+        gwMarker.x = p.x
+        gwMarker.y = p.y
     }
 
 
     Connections {
         target: jsonModel
         onDataChanged: {
-            ctx = JSON.parse(jsonModel.getJson())
+            ctx = JSON.parse(jsonModel.getJson());
+            gwSelector.model = Object.keys(ctx.gateways)
 
             if (ctx.donateDialog == 'true') {
                 console.debug(jsonModel.getJson())
@@ -170,6 +246,28 @@ ApplicationWindow {
         }
     }
 
+    function toHumanWithLocation(st) {
+        switch(st) {
+            case "off":
+		//: %1 -> application name
+                return qsTr("%1 off").arg(ctx.appName);
+            case "on":
+		//: %1 -> application name
+                //: %2 -> current gateway
+                return qsTr("%1 on - %2").arg(ctx.appName).arg(ctx.currentGateway);
+            case "connecting":
+		//: %1 -> application name
+                //: %2 -> current gateway
+                return qsTr("Connecting to %1 - %2").arg(ctx.appName).arg(ctx.currentGateway);
+            case "stopping":
+		//: %1 -> application name
+                return qsTr("Stopping %1").arg(ctx.appName);
+            case "failed":
+		//: %1 -> application name
+                return qsTr("%1 blocking internet").arg(ctx.appName); // TODO failed is not handed yet
+        }
+    }
+
     property var icons: {
         "off": "qrc:/assets/icon/png/black/vpn_off.png",
         "on": "qrc:/assets/icon/png/black/vpn_on.png",
@@ -177,8 +275,11 @@ ApplicationWindow {
         "blocked": "qrc:/assets/icon/png/black/vpn_blocked.png"
     }
 
+    VpnState {
+        id: vpn
+    }
 
-    
+    SystemTrayIcon {
     LabsPlatform.SystemTrayIcon {
 
         id: systray
@@ -203,76 +304,6 @@ ApplicationWindow {
                 }
             }
 
-            StateGroup {
-                id: vpn
-                state: ctx ? ctx.status : ""
-
-                states: [
-                    State {
-                        name: "initializing"
-                    },
-                    State {
-                        name: "off"
-                        PropertyChanges {
-                            target: systray
-                            tooltip: toHuman("off")
-                            icon.source: icons["off"]
-                        }
-                        PropertyChanges {
-                            target: statusItem
-                            text: toHuman("off")
-                        }
-                    },
-                    State {
-                        name: "on"
-                        PropertyChanges {
-                            target: systray
-                            tooltip: toHuman("on")
-                            icon.source: icons["on"]
-                        }
-                        PropertyChanges {
-                            target: statusItem
-                            text: toHuman("on")
-                        }
-                    },
-                    State {
-                        name: "starting"
-                        PropertyChanges {
-                            target: systray
-                            tooltip: toHuman("connecting")
-                            icon.source: icons["wait"]
-                        }
-                        PropertyChanges {
-                            target: statusItem
-                            text: toHuman("connecting")
-                        }
-                    },
-                    State {
-                        name: "stopping"
-                        PropertyChanges {
-                            target: systray
-                            tooltip: toHuman("stopping")
-                            icon.source: icons["wait"]
-                        }
-                        PropertyChanges {
-                            target: statusItem
-                            text: toHuman("stopping")
-                        }
-                    },
-                    State {
-                        name: "failed"
-                        PropertyChanges {
-                            target: systray
-                            tooltip: toHuman("failed")
-                            icon.source: icons["blocked"]
-                        }
-                        PropertyChanges {
-                            target: statusItem
-                            text: toHuman("failed")
-                        }
-                    }
-                ]
-            }
 
             LabsPlatform.MenuItem {
                 id: statusItem
@@ -385,8 +416,6 @@ ApplicationWindow {
                 console.log("System doesn't support systray notifications")
             }
         }
-
-
     }
 
     DonateDialog {
@@ -432,5 +461,4 @@ ApplicationWindow {
         id: initFailure
         visible: false
     }
-
 }
diff --git a/pkg/backend/api.go b/pkg/backend/api.go
index 8d6d0496..761c03d2 100644
--- a/pkg/backend/api.go
+++ b/pkg/backend/api.go
@@ -7,6 +7,7 @@ import (
 	"encoding/json"
 	"log"
 	"strconv"
+	"time"
 	"unsafe"
 
 	"0xacab.org/leap/bitmask-vpn/pkg/bitmask"
@@ -54,10 +55,14 @@ func SwitchOff() {
 	go stopVPN()
 }
 
-// TODO implement Reconnect?
+// TODO implement Reconnect - do not tear whole fw down in between
 
 func UseGateway(label string) {
-	ctx.bm.UseGateway(label)
+	ctx.bm.UseGateway(string(label))
+	time.Sleep(200 * time.Millisecond)
+	SwitchOff()
+	time.Sleep(500 * time.Millisecond)
+	SwitchOn()
 }
 
 func UseTransport(label string) {
diff --git a/pkg/backend/status.go b/pkg/backend/status.go
index 16db2278..20128caa 100644
--- a/pkg/backend/status.go
+++ b/pkg/backend/status.go
@@ -8,6 +8,7 @@ import (
 
 	"0xacab.org/leap/bitmask-vpn/pkg/bitmask"
 	"0xacab.org/leap/bitmask-vpn/pkg/config"
+	"0xacab.org/leap/bitmask-vpn/pkg/vpn/bonafide"
 )
 
 const (
@@ -32,18 +33,20 @@ var updateMutex sync.Mutex
 // them.
 
 type connectionCtx struct {
-	AppName         string `json:"appName"`
-	Provider        string `json:"provider"`
-	TosURL          string `json:"tosURL"`
-	HelpURL         string `json:"helpURL"`
-	AskForDonations bool   `json:"askForDonations"`
-	DonateDialog    bool   `json:"donateDialog"`
-	DonateURL       string `json:"donateURL"`
-	LoginDialog     bool   `json:"loginDialog"`
-	LoginOk         bool   `json:"loginOk"`
-	Version         string `json:"version"`
-	Errors          string `json:"errors"`
-	Status          status `json:"status"`
+	AppName         string                      `json:"appName"`
+	Provider        string                      `json:"provider"`
+	TosURL          string                      `json:"tosURL"`
+	HelpURL         string                      `json:"helpURL"`
+	AskForDonations bool                        `json:"askForDonations"`
+	DonateDialog    bool                        `json:"donateDialog"`
+	DonateURL       string                      `json:"donateURL"`
+	LoginDialog     bool                        `json:"loginDialog"`
+	LoginOk         bool                        `json:"loginOk"`
+	Version         string                      `json:"version"`
+	Errors          string                      `json:"errors"`
+	Status          status                      `json:"status"`
+	Gateways        map[string]bonafide.Gateway `json:"gateways"`
+	CurrentGateway  string                      `json:"currentGateway"`
 	bm              bitmask.Bitmask
 	autostart       bitmask.Autostart
 	cfg             *config.Config
@@ -51,6 +54,15 @@ type connectionCtx struct {
 
 func (c connectionCtx) toJson() ([]byte, error) {
 	statusMutex.Lock()
+	if c.bm != nil {
+		c.Gateways = map[string]bonafide.Gateway{}
+		gateways, _ := c.bm.ListGateways("openvpn")
+		for _, label := range gateways {
+			gw, _ := c.bm.GetGatewayDetails(label)
+			c.Gateways[label] = gw.(bonafide.Gateway)
+		}
+		c.CurrentGateway = c.bm.GetCurrentGateway()
+	}
 	defer statusMutex.Unlock()
 	b, err := json.Marshal(c)
 	if err != nil {
diff --git a/pkg/bitmask/bitmask.go b/pkg/bitmask/bitmask.go
index 7ffe01ab..6d5fa33e 100644
--- a/pkg/bitmask/bitmask.go
+++ b/pkg/bitmask/bitmask.go
@@ -29,6 +29,7 @@ type Bitmask interface {
 	ListGateways(provider string) ([]string, error)
 	UseGateway(name string) error
 	GetCurrentGateway() string
+	GetGatewayDetails(label string) (interface{}, error)
 	UseTransport(transport string) error
 	NeedsCredentials() bool
 	DoLogin(username, password string) (bool, error)
diff --git a/pkg/vpn/bonafide/bonafide.go b/pkg/vpn/bonafide/bonafide.go
index 8b606413..561c2bbb 100644
--- a/pkg/vpn/bonafide/bonafide.go
+++ b/pkg/vpn/bonafide/bonafide.go
@@ -224,8 +224,12 @@ func (b *Bonafide) GetAllGateways(transport string) ([]Gateway, error) {
 	return gws, err
 }
 
+func (b *Bonafide) GetGatewayDetails(label string) (Gateway, error) {
+	return b.gateways.getGatewayByLabel(label)
+}
+
 func (b *Bonafide) SetManualGateway(label string) {
-	b.gateways.setUserChoice(label)
+	b.gateways.setUserChoice([]byte(label))
 }
 
 func (b *Bonafide) SetAutomaticGateway() {
diff --git a/pkg/vpn/bonafide/eip_service.go b/pkg/vpn/bonafide/eip_service.go
index 26a8f3c7..d5dd7510 100644
--- a/pkg/vpn/bonafide/eip_service.go
+++ b/pkg/vpn/bonafide/eip_service.go
@@ -14,7 +14,7 @@ import (
 type eipService struct {
 	Gateways             []gatewayV3
 	defaultGateway       string
-	Locations            map[string]location
+	Locations            map[string]Location
 	OpenvpnConfiguration openvpnConfig `json:"openvpn_configuration"`
 	auth                 string
 }
@@ -22,7 +22,7 @@ type eipService struct {
 type eipServiceV1 struct {
 	Gateways             []gatewayV1
 	defaultGateway       string
-	Locations            map[string]location
+	Locations            map[string]Location
 	OpenvpnConfiguration openvpnConfig `json:"openvpn_configuration"`
 }
 
@@ -45,8 +45,8 @@ type gatewayV3 struct {
 	Location  string
 }
 
-type location struct {
-	CountryCode string
+type Location struct {
+	CountryCode string `json:"country_code"`
 	Hemisphere  string
 	Name        string
 	Timezone    string
@@ -159,13 +159,15 @@ func (eip eipService) getGateways() []Gateway {
 	for _, g := range eip.Gateways {
 		for _, t := range g.Capabilities.Transport {
 			gateway := Gateway{
-				Host:      g.Host,
-				IPAddress: g.IPAddress,
-				Location:  g.Location,
-				Ports:     t.Ports,
-				Protocols: t.Protocols,
-				Options:   t.Options,
-				Transport: t.Type,
+				Host:         g.Host,
+				IPAddress:    g.IPAddress,
+				Location:     g.Location,
+				Ports:        t.Ports,
+				Protocols:    t.Protocols,
+				Options:      t.Options,
+				Transport:    t.Type,
+				LocationName: eip.Locations[g.Location].Name,
+				CountryCode:  eip.Locations[g.Location].CountryCode,
 			}
 			gws = append(gws, gateway)
 		}
diff --git a/pkg/vpn/bonafide/gateways.go b/pkg/vpn/bonafide/gateways.go
index d9735304..f454d3cc 100644
--- a/pkg/vpn/bonafide/gateways.go
+++ b/pkg/vpn/bonafide/gateways.go
@@ -16,14 +16,16 @@ const (
 // A Gateway is a representation of gateways that is independent of the api version.
 // If a given physical location offers different transports, they will appear as separate gateways.
 type Gateway struct {
-	Host      string
-	IPAddress string
-	Location  string
-	Ports     []string
-	Protocols []string
-	Options   map[string]string
-	Transport string
-	Label     string
+	Host         string
+	IPAddress    string
+	Location     string
+	LocationName string
+	CountryCode  string
+	Ports        []string
+	Protocols    []string
+	Options      map[string]string
+	Transport    string
+	Label        string
 }
 
 /* TODO add a String method with a human representation: Label (cc) */
@@ -35,18 +37,24 @@ type gatewayDistance struct {
 }
 
 type gatewayPool struct {
-	available []Gateway
+	available  []Gateway
+	userChoice []byte
 	/* ranked is, for now, just an array of hostnames (fetched from the
 	geoip service). it should be a map in the future, to keep track of
 	quantitative metrics */
-	ranked     []string
-	userChoice string
-	locations  map[string]location
+	ranked []string
+
+	/* TODO locations are just used to get the timezone for each gateway. I
+	* think it's easier to just merge that info into the version-agnostic
+	* Gateway, that is passed from the eipService, and do not worry with
+	* the location here */
+	locations map[string]Location
 }
 
 /* genLabels generates unique, human-readable labels for a gateway. It gives a serial
    number to each gateway in the same location (paris-1, paris-2,...). The
    current implementation will give a different label to each transport.
+   An alternative (to discuss) would be to give the same label to the same hostname.
 */
 func (p *gatewayPool) genLabels() {
 	acc := make(map[string]int)
@@ -59,7 +67,7 @@ func (p *gatewayPool) genLabels() {
 		gw.Label = gw.Location + "-" + strconv.Itoa(acc[gw.Location])
 		p.available[i] = gw
 	}
-	/* skip suffix if only one occurence */
+	/* skip suffix if only one occurrence */
 	for i, gw := range p.available {
 		if acc[gw.Location] == 1 {
 			gw.Label = gw.Location
@@ -102,11 +110,11 @@ func (p *gatewayPool) getGatewayByIP(ip string) (Gateway, error) {
 }
 
 func (p *gatewayPool) setAutomaticChoice() {
-	p.userChoice = ""
+	p.userChoice = []byte("")
 }
 
-func (p *gatewayPool) setUserChoice(label string) error {
-	if !p.isValidLabel(label) {
+func (p *gatewayPool) setUserChoice(label []byte) error {
+	if !p.isValidLabel(string(label)) {
 		return errors.New("bonafide: not a valid label for gateway choice")
 	}
 	p.userChoice = label
@@ -132,7 +140,7 @@ func (p *gatewayPool) setRanking(hostnames []string) {
 func (p *gatewayPool) getBest(transport string, tz, max int) ([]Gateway, error) {
 	gws := make([]Gateway, 0)
 	if len(p.userChoice) != 0 {
-		gw, err := p.getGatewayByLabel(p.userChoice)
+		gw, err := p.getGatewayByLabel(string(p.userChoice))
 		gws = append(gws, gw)
 		return gws, err
 	} else if len(p.ranked) != 0 {
diff --git a/pkg/vpn/openvpn.go b/pkg/vpn/openvpn.go
index 38a64a95..530f5673 100644
--- a/pkg/vpn/openvpn.go
+++ b/pkg/vpn/openvpn.go
@@ -25,6 +25,7 @@ import (
 	"strconv"
 	"strings"
 
+	"0xacab.org/leap/bitmask-vpn/pkg/vpn/bonafide"
 	"0xacab.org/leap/shapeshifter"
 )
 
@@ -244,6 +245,14 @@ func (b *Bitmask) ListGateways(provider string) ([]string, error) {
 	return gatewayNames, nil
 }
 
+func (b *Bitmask) GetGatewayDetails(label string) (interface{}, error) {
+	gw, err := b.bonafide.GetGatewayDetails(label)
+	if err != nil {
+		return bonafide.Gateway{}, err
+	}
+	return gw, nil
+}
+
 // UseGateway selects a gateway, by label, as the default gateway
 func (b *Bitmask) UseGateway(label string) error {
 	b.bonafide.SetManualGateway(label)
-- 
GitLab