diff --git a/.env b/.env index c1aabe03015b67420d0c27a50e6da8a301078d79..7a97ff754791d4d7c5a6ab8ccde33a55be3e47c4 100644 --- a/.env +++ b/.env @@ -41,6 +41,6 @@ OAUTH2_REDIRECT_URL=https://wg-gen-web-demo.127-0-0-1.fr OAUTH2_PROVIDER_NAME=fake # https://github.com/jamescun/wg-api integration, user and password (basic auth) are optional -WG_STATS_API=https://wg.example.digital/wg-api +WG_STATS_API= WG_STATS_API_USER= WG_STATS_API_PASS= \ No newline at end of file diff --git a/api/v1/status/status.go b/api/v1/status/status.go index 7d3124cb2eb764116b9fd432b746ced830effbd8..f5a16de1b781abd7e27715d8d7e28378cb3b2306 100644 --- a/api/v1/status/status.go +++ b/api/v1/status/status.go @@ -23,7 +23,7 @@ func readInterfaceStatus(c *gin.Context) { log.WithFields(log.Fields{ "err": err, }).Error("failed to read interface status") - c.AbortWithStatus(http.StatusInternalServerError) + c.AbortWithStatusJSON(http.StatusInternalServerError, err.Error()) return } @@ -36,7 +36,7 @@ func readClientStatus(c *gin.Context) { log.WithFields(log.Fields{ "err": err, }).Error("failed to read client status") - c.AbortWithStatus(http.StatusInternalServerError) + c.AbortWithStatusJSON(http.StatusInternalServerError, err.Error()) return } diff --git a/core/status.go b/core/status.go index e2fc54e224a766067e17a4ce3a3f005bebe2b173..cbb4bfe1f1e79c5ea09ac72421e97e1b2f824eb3 100644 --- a/core/status.go +++ b/core/status.go @@ -3,6 +3,7 @@ package core import ( "bytes" "encoding/json" + "errors" "io/ioutil" "net/http" "os" @@ -34,6 +35,10 @@ type apiResponse struct { func fetchWireGuardAPI(reqData apiRequest) (*apiResponse, error) { apiUrl := os.Getenv("WG_STATS_API") + if apiUrl == "" { + return nil, errors.New("Status API integration not configured") + } + apiClient := http.Client{ Timeout: time.Second * 2, // Timeout after 2 seconds } @@ -135,19 +140,22 @@ func ReadClientStatus() ([]*model.ClientStatus, error) { for i, peerIP := range peerIPs { peerAddresses[i] = peerIP.(string) } + peerHandshakeRelative := time.Since(peerHandshake) + peerActive := peerHandshakeRelative.Minutes() < 3 // TODO: we need a better detection... ping for example? newClientStatus := &model.ClientStatus{ - PublicKey: peer["public_key"].(string), - HasPresharedKey: peer["has_preshared_key"].(bool), - ProtocolVersion: int(peer["protocol_version"].(float64)), - Name: "UNKNOWN", - Email: "UNKNOWN", - Connected: false, - AllowedIPs: peerAddresses, - Endpoint: peer["endpoint"].(string), - LastHandshake: peerHandshake, - ReceivedBytes: int(peer["receive_bytes"].(float64)), - TransmittedBytes: int(peer["transmit_bytes"].(float64)), + PublicKey: peer["public_key"].(string), + HasPresharedKey: peer["has_preshared_key"].(bool), + ProtocolVersion: int(peer["protocol_version"].(float64)), + Name: "UNKNOWN", + Email: "UNKNOWN", + Connected: peerActive, + AllowedIPs: peerAddresses, + Endpoint: peer["endpoint"].(string), + LastHandshake: peerHandshake, + LastHandshakeRelative: peerHandshakeRelative, + ReceivedBytes: int(peer["receive_bytes"].(float64)), + TransmittedBytes: int(peer["transmit_bytes"].(float64)), } if withClientDetails { diff --git a/model/status.go b/model/status.go index 0f63917717250214d75b0c444dbadf343757a6d7..1ac07dd223e426a5cc7d12503668dbba715e2522 100644 --- a/model/status.go +++ b/model/status.go @@ -1,22 +1,60 @@ package model import ( + "encoding/json" + "fmt" "time" ) // ClientStatus structure type ClientStatus struct { - PublicKey string `json:"publicKey"` - HasPresharedKey bool `json:"hasPresharedKey"` - ProtocolVersion int `json:"protocolVersion"` - Name string `json:"name"` - Email string `json:"email"` - Connected bool `json:"connected"` - AllowedIPs []string `json:"allowedIPs"` - Endpoint string `json:"endpoint"` - LastHandshake time.Time `json:"lastHandshake"` - ReceivedBytes int `json:"receivedBytes"` - TransmittedBytes int `json:"transmittedBytes"` + PublicKey string `json:"publicKey"` + HasPresharedKey bool `json:"hasPresharedKey"` + ProtocolVersion int `json:"protocolVersion"` + Name string `json:"name"` + Email string `json:"email"` + Connected bool `json:"connected"` + AllowedIPs []string `json:"allowedIPs"` + Endpoint string `json:"endpoint"` + LastHandshake time.Time `json:"lastHandshake"` + LastHandshakeRelative time.Duration `json:"lastHandshakeRelative"` + ReceivedBytes int `json:"receivedBytes"` + TransmittedBytes int `json:"transmittedBytes"` +} + +func (c *ClientStatus) MarshalJSON() ([]byte, error) { + + duration := fmt.Sprintf("%v ago", c.LastHandshakeRelative) + if c.LastHandshakeRelative.Hours() > 5208 { // 24*7*31 = approx one month + duration = "more than a month ago" + } + return json.Marshal(&struct { + PublicKey string `json:"publicKey"` + HasPresharedKey bool `json:"hasPresharedKey"` + ProtocolVersion int `json:"protocolVersion"` + Name string `json:"name"` + Email string `json:"email"` + Connected bool `json:"connected"` + AllowedIPs []string `json:"allowedIPs"` + Endpoint string `json:"endpoint"` + LastHandshake time.Time `json:"lastHandshake"` + LastHandshakeRelative string `json:"lastHandshakeRelative"` + ReceivedBytes int `json:"receivedBytes"` + TransmittedBytes int `json:"transmittedBytes"` + }{ + PublicKey: c.PublicKey, + HasPresharedKey: c.HasPresharedKey, + ProtocolVersion: c.ProtocolVersion, + Name: c.Name, + Email: c.Email, + Connected: c.Connected, + AllowedIPs: c.AllowedIPs, + Endpoint: c.Endpoint, + LastHandshake: c.LastHandshake, + LastHandshakeRelative: duration, + ReceivedBytes: c.ReceivedBytes, + TransmittedBytes: c.TransmittedBytes, + }) } // InterfaceStatus structure diff --git a/ui/src/components/Status.vue b/ui/src/components/Status.vue index 88633f02f4aaf46a122cfc1fa9f1b7f825ffe34e..eb6f9f8f34d0b7a94a2658c16ef1ae35371925dd 100644 --- a/ui/src/components/Status.vue +++ b/ui/src/components/Status.vue @@ -44,9 +44,19 @@ :items="clients" :search="search" > - <template v-slot:item.address="{ item }"> + <template v-slot:item.connected="{ item }"> + <v-icon left v-if="item.connected" color="success">mdi-lan-connect</v-icon> + <v-icon left v-else>mdi-lan-disconnect</v-icon> + </template> + <template v-slot:item.receivedBytes="{ item }"> + {{ humanFileSize(item.receivedBytes) }} + </template> + <template v-slot:item.transmittedBytes="{ item }"> + {{ humanFileSize(item.transmittedBytes) }} + </template> + <template v-slot:item.allowedIPs="{ item }"> <v-chip - v-for="(ip, i) in item.address" + v-for="(ip, i) in item.allowedIPs" :key="i" color="indigo" text-color="white" @@ -55,63 +65,11 @@ {{ ip }} </v-chip> </template> - <template v-slot:item.tags="{ item }"> - <v-chip - v-for="(tag, i) in item.tags" - :key="i" - color="blue-grey" - text-color="white" - > - <v-icon left>mdi-tag</v-icon> - {{ tag }} - </v-chip> - </template> - <template v-slot:item.created="{ item }"> - <v-row> - <p>At {{ item.created | formatDate }} by {{ item.createdBy }}</p> - </v-row> - </template> - <template v-slot:item.updated="{ item }"> + <template v-slot:item.lastHandshake="{ item }"> <v-row> - <p>At {{ item.updated | formatDate }} by {{ item.updatedBy }}</p> + <p>{{ item.lastHandshake | formatDate }} ({{ item.lastHandshakeRelative }})</p> </v-row> </template> - <template v-slot:item.action="{ item }"> - <v-row> - <v-icon - class="pr-1 pl-1" - @click.stop="startUpdate(item)" - > - mdi-square-edit-outline - </v-icon> - <v-icon - class="pr-1 pl-1" - @click.stop="forceFileDownload(item)" - > - mdi-cloud-download-outline - </v-icon> - <v-icon - class="pr-1 pl-1" - @click.stop="email(item)" - > - mdi-email-send-outline - </v-icon> - <v-icon - class="pr-1 pl-1" - @click="remove(item)" - > - mdi-trash-can-outline - </v-icon> - <v-switch - dark - class="pr-1 pl-1" - color="success" - v-model="item.enable" - v-on:change="update(item)" - /> - </v-row> - </template> - </v-data-table> </v-card> </v-col> @@ -170,6 +128,28 @@ reload() { this.readStatus() }, + + humanFileSize(bytes, si=false, dp=1) { + const thresh = si ? 1000 : 1024; + + if (Math.abs(bytes) < thresh) { + return bytes + ' B'; + } + + const units = si + ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + let u = -1; + const r = 10**dp; + + do { + bytes /= thresh; + ++u; + } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1); + + + return bytes.toFixed(dp) + ' ' + units[u]; + } } }; </script>