diff --git a/.gitignore b/.gitignore index 80e8170..f95e2da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /config/* +/stats/* /templates/* /rest-server /o-rest-server diff --git a/pkg/rest/stats.go b/pkg/rest/stats.go index ba19f3a..057ef89 100644 --- a/pkg/rest/stats.go +++ b/pkg/rest/stats.go @@ -51,9 +51,11 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { // calculate stats var userStatsResponse UserStatsResponse statsFiles := []string{ - c.Storage.Client.ConfigPath(path.Join(wireguard.VPN_STATS_DIR, "user-"+date.AddDate(0, 0, -1).Format("2006-01-02")+".log")), - c.Storage.Client.ConfigPath(path.Join(wireguard.VPN_STATS_DIR, "user-"+date.Format("2006-01-02")+".log")), - c.Storage.Client.ConfigPath(path.Join(wireguard.VPN_STATS_DIR, "user-"+date.AddDate(0, 0, 1).Format("2006-01-02")+".log")), + path.Join(wireguard.VPN_STATS_DIR, "user-"+date.AddDate(0, 0, -1).Format("2006-01-02")+".log"), + path.Join(wireguard.VPN_STATS_DIR, "user-"+date.Format("2006-01-02")+".log"), + } + if !dateEqual(time.Now(), date) { + statsFiles = append(statsFiles, path.Join(wireguard.VPN_STATS_DIR, "user-"+date.AddDate(0, 0, 1).Format("2006-01-02")+".log")) } logData := bytes.NewBuffer([]byte{}) for _, statsFile := range statsFiles { @@ -73,6 +75,8 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { transmitBytesLast := make(map[string]int64) receiveBytesData := make(map[string][]UserStatsDataPoint) transmitBytesData := make(map[string][]UserStatsDataPoint) + handshakeLast := make(map[string]time.Time) + handshakeData := make(map[string][]UserStatsDataPoint) for scanner.Scan() { // all other entries inputSplit := strings.Split(scanner.Text(), ",") userID := inputSplit[1] @@ -92,6 +96,9 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { transmitBytesLast[userID] = 0 } } + if _, ok := handshakeLast[userID]; !ok { + handshakeLast[userID] = time.Time{} + } receiveBytes, err := strconv.ParseInt(inputSplit[3], 10, 64) if err == nil { if _, ok := receiveBytesData[userID]; !ok { @@ -120,8 +127,19 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { } } } + handshake, err := time.Parse(wireguard.TIMESTAMP_FORMAT, inputSplit[5]) + if err == nil { + if _, ok := handshakeData[userID]; !ok { + handshakeData[userID] = []UserStatsDataPoint{} + } + handshake = handshake.Add(time.Duration(offset) * time.Minute) + if dateEqual(handshake, date) && !handshake.Equal(handshakeLast[userID]) { + handshakeData[userID] = append(handshakeData[userID], UserStatsDataPoint{X: handshake.Format(wireguard.TIMESTAMP_FORMAT), Y: 1}) + } + } receiveBytesLast[userID] = receiveBytes transmitBytesLast[userID] = transmitBytes + handshakeLast[userID] = handshake } if err := scanner.Err(); err != nil { @@ -134,6 +152,9 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { userStatsResponse.TransmitBytes = UserStatsData{ Datasets: []UserStatsDataset{}, } + userStatsResponse.Handshakes = UserStatsData{ + Datasets: []UserStatsDataset{}, + } for userID, data := range receiveBytesData { login, ok := userMap[userID] if !ok { @@ -145,6 +166,7 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { Label: login, Data: data, Tension: 0.1, + ShowLine: true, }) } for userID, data := range transmitBytesData { @@ -158,11 +180,27 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { Label: login, Data: data, Tension: 0.1, + ShowLine: true, + }) + } + for userID, data := range handshakeData { + login, ok := userMap[userID] + if !ok { + login = "unknown" + } + userStatsResponse.Handshakes.Datasets = append(userStatsResponse.Handshakes.Datasets, UserStatsDataset{ + BorderColor: getColor(len(userStatsResponse.Handshakes.Datasets)), + BackgroundColor: getColor(len(userStatsResponse.Handshakes.Datasets)), + Label: login, + Data: data, + Tension: 0.1, + ShowLine: false, }) } sort.Sort(userStatsResponse.ReceiveBytes.Datasets) sort.Sort(userStatsResponse.TransmitBytes.Datasets) + sort.Sort(userStatsResponse.Handshakes.Datasets) out, err := json.Marshal(userStatsResponse) if err != nil { @@ -179,6 +217,21 @@ func getColor(i int) string { "#5FB49C", "#414288", "#682D63", + "#b45f5f", + "#b49f5f", + "#8ab45f", + "#5fb475", + "#5f8ab4", + "#755fb4", + "#b45fb4", + "#b45f75", + "#b45f5f", + "#0066cc", + "#cc0000", + "#33cc00", + "#00cc99", + "#cc00cc", + "#00cc99", } return colors[i%len(colors)] } diff --git a/pkg/rest/stats_test.go b/pkg/rest/stats_test.go index ea5b1f3..1778b33 100644 --- a/pkg/rest/stats_test.go +++ b/pkg/rest/stats_test.go @@ -2,8 +2,10 @@ package rest import ( "encoding/json" + "fmt" "net/http/httptest" "path" + "strings" "testing" "time" @@ -30,12 +32,13 @@ func TestUserStatsHandler(t *testing.T) { 2024-08-23T20:09:03,3df97301-5f73-407a-a26b-91829f1e7f48,1,39928520,85171728,2024-08-23T20:08:54` statsFile := c.Storage.Client.ConfigPath(path.Join(wireguard.VPN_STATS_DIR, "user-"+time.Now().Format("2006-01-02")) + ".log") - err = c.Storage.Client.WriteFile(statsFile, []byte(testData)) + err = c.Storage.Client.WriteFile(statsFile, []byte(strings.ReplaceAll(testData, "2024-08-23", time.Now().Format("2006-01-02")))) if err != nil { t.Fatalf("Cannot write test file") } req := httptest.NewRequest("GET", "http://example.com/stats/user", nil) + req.SetPathValue("date", time.Now().Format("2006-01-02")) w := httptest.NewRecorder() c.userStatsHandler(w, req) @@ -55,10 +58,10 @@ func TestUserStatsHandler(t *testing.T) { } if userStatsResponse.ReceiveBytes.Datasets[0].Data[1].Y != 662580 { - t.Fatalf("unexpected data: %d", userStatsResponse.ReceiveBytes.Datasets[0].Data[1].Y) + t.Fatalf("unexpected data: %f", userStatsResponse.ReceiveBytes.Datasets[0].Data[1].Y) } if userStatsResponse.TransmitBytes.Datasets[0].Data[1].Y != 813588 { - t.Fatalf("unexpected data: %d", userStatsResponse.TransmitBytes.Datasets[0].Data[1].Y) + t.Fatalf("unexpected data: %f", userStatsResponse.TransmitBytes.Datasets[0].Data[1].Y) } - + fmt.Printf("%+v\n", userStatsResponse.Handshakes) } diff --git a/pkg/rest/types.go b/pkg/rest/types.go index e3443a3..f06b11f 100644 --- a/pkg/rest/types.go +++ b/pkg/rest/types.go @@ -178,6 +178,7 @@ type SAMLSetup struct { type UserStatsResponse struct { ReceiveBytes UserStatsData `json:"receivedBytes"` TransmitBytes UserStatsData `json:"transmitBytes"` + Handshakes UserStatsData `json:"handshakes"` } type UserStatsData struct { Datasets UserStatsDatasets `json:"datasets"` @@ -190,6 +191,7 @@ type UserStatsDataset struct { BorderColor string `json:"borderColor"` BackgroundColor string `json:"backgroundColor"` Tension float64 `json:"tension"` + ShowLine bool `json:"showLine"` } type UserStatsDataPoint struct { diff --git a/pkg/wireguard/stats_linux.go b/pkg/wireguard/stats_linux.go index e82495f..e4c0bfc 100644 --- a/pkg/wireguard/stats_linux.go +++ b/pkg/wireguard/stats_linux.go @@ -19,12 +19,12 @@ import ( const RUN_STATS_INTERVAL = 5 func RunStats(storage storage.Iface) { - err := storage.EnsurePath(storage.ConfigPath(VPN_STATS_DIR)) + err := storage.EnsurePath(VPN_STATS_DIR) if err != nil { logging.ErrorLog(fmt.Errorf("could not create stats path: %s. Stats disabled", err)) return } - err = storage.EnsureOwnership(storage.ConfigPath(VPN_STATS_DIR), "vpn") + err = storage.EnsureOwnership(VPN_STATS_DIR, "vpn") if err != nil { logging.ErrorLog(fmt.Errorf("could not ensure ownership of stats path: %s. Stats disabled", err)) return @@ -67,7 +67,7 @@ func runStats(storage storage.Iface) error { if len(statsEntries) > 0 { statsCsv := statsToCsv(statsEntries) - statsPath := storage.ConfigPath(path.Join(VPN_STATS_DIR, "user-"+time.Now().Format("2006-01-02")) + ".log") + statsPath := path.Join(VPN_STATS_DIR, "user-"+time.Now().Format("2006-01-02")) + ".log" err = storage.AppendFile(statsPath, statsCsv) if err != nil { return fmt.Errorf("could not append stats to file (%s): %s", statsPath, err) diff --git a/webapp/src/Routes/Home/UserStats.tsx b/webapp/src/Routes/Home/UserStats.tsx index c7431fd..a9aa5f7 100644 --- a/webapp/src/Routes/Home/UserStats.tsx +++ b/webapp/src/Routes/Home/UserStats.tsx @@ -6,11 +6,11 @@ import { AppSettings } from '../../Constants/Constants'; import { format } from "date-fns"; import { Chart } from 'react-chartjs-2'; import 'chartjs-adapter-date-fns'; -import { Chart as ChartJS, LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale, ChartOptions, Legend } from 'chart.js'; +import { Chart as ChartJS, LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale, ChartOptions, Legend, Tooltip } from 'chart.js'; import { useState } from "react"; export function UserStats() { - ChartJS.register(LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale, Legend); + ChartJS.register(LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale, Legend, Tooltip); const timezoneOffset = new Date().getTimezoneOffset() * -1 const {authInfo} = useAuthContext() const [statsDate, setStatsDate] = useState(new Date()); @@ -38,6 +38,12 @@ export function UserStats() { position: 'right' as const, display: true, }, + tooltip: { + callbacks: { + //title: (xDatapoint) => {return "this is the data: " + xDatapoint.}, + label: (yDatapoint) => {return " "+yDatapoint.formattedValue + " " + unit}, + } + } }, scales: { x: { @@ -46,7 +52,12 @@ export function UserStats() { y: { min: 0 } - } + }, + + hover: { + mode: 'index', + intersect: false + } } if (isPending) return '' @@ -58,6 +69,9 @@ export function UserStats() { if(data.transmitBytes.datasets === null) { data.transmitBytes.datasets = [{ data: [0], label: "no data"}] } + if(data.handshakes.datasets === null) { + data.handshakes.datasets = [{ data: [0], label: "no data"}] + } return ( <> @@ -97,6 +111,12 @@ export function UserStats() { + +
+ User Handshakes +
+ {return " "+yDatapoint.formattedValue }} }} }} /> +
) } \ No newline at end of file