diff --git a/contrib/deb/generate-gui.sh b/contrib/deb/generate-gui.sh index a262dbfd..ae90afd0 100755 --- a/contrib/deb/generate-gui.sh +++ b/contrib/deb/generate-gui.sh @@ -57,7 +57,7 @@ cat > /tmp/$PKGNAME/usr/share/applications/riv.desktop << EOF Name=RiV mesh GenericName=Mesh network Comment=RiV-mesh is an early-stage implementation of a fully end-to-end encrypted IPv6 network -Exec=sh -c "/usr/bin/mesh-ui http://localhost:19019" +Exec=sh -c "/usr/bin/mesh-ui" Terminal=false Type=Application Icon=riv diff --git a/contrib/macos/create-pkg-gui.sh b/contrib/macos/create-pkg-gui.sh index 10be6cef..c135ce24 100644 --- a/contrib/macos/create-pkg-gui.sh +++ b/contrib/macos/create-pkg-gui.sh @@ -54,7 +54,7 @@ cp contrib/macos/mesh.plist pkgbuild/root/Library/LaunchDaemons cat > pkgbuild/root/Applications/RiV-mesh.app/Contents/MacOS/open-mesh-ui << EOF #!/usr/bin/env bash -exec /Applications/RiV-mesh.app/Contents/MacOS/mesh-ui http://localhost:19019 1>/tmp/mesh-ui.stdout.log 2>/tmp/mesh-ui.stderr.log +exec /Applications/RiV-mesh.app/Contents/MacOS/mesh-ui 1>/tmp/mesh-ui.stdout.log 2>/tmp/mesh-ui.stderr.log EOF # Create the postinstall script diff --git a/contrib/msi/build-msi-gui.sh b/contrib/msi/build-msi-gui.sh index 66b9825d..b892e22f 100644 --- a/contrib/msi/build-msi-gui.sh +++ b/contrib/msi/build-msi-gui.sh @@ -272,7 +272,6 @@ cat > wix.xml << EOF @@ -291,7 +290,6 @@ cat > wix.xml << EOF Description="RiV-mesh is IoT E2E encrypted network" Directory="DesktopFolder" Target="[MeshInstallFolder]mesh-ui.exe" - Arguments="http://localhost:19019" WorkingDirectory="MeshInstallFolder"/> wix.xml << EOF Key="Software\Microsoft\Windows\CurrentVersion\Run" Name="RiV-mesh client" Type="string" - Value='"[MeshInstallFolder]mesh-ui.exe" "http://localhost:19019"' /> + Value='"[MeshInstallFolder]mesh-ui.exe"' /> ASSISTANCE_START_VIA_REGISTRY diff --git a/contrib/ui/mesh-ui/ui/assets/mesh-ui.js b/contrib/ui/mesh-ui/ui/assets/mesh-ui.js index cae90647..f0936cdb 100644 --- a/contrib/ui/mesh-ui/ui/assets/mesh-ui.js +++ b/contrib/ui/mesh-ui/ui/assets/mesh-ui.js @@ -120,13 +120,13 @@ function showWindow(text) { button_info_close.onclick = function () { message.value = ""; info.classList.add("is-hidden"); - //$("peer_list").remove(); + $("peer_list").remove(); }; var button_window_close = $("window_close"); button_window_close.onclick = function () { message.value = ""; info.classList.add("is-hidden"); - //$("peer_list").remove(); + $("peer_list").remove(); }; var button_window_save = $("window_save"); button_window_save.onclick = function () { @@ -142,8 +142,18 @@ function showWindow(text) { peer_list.push(peerURL); } } - savePeers(JSON.stringify(peer_list)); - //$("peer_list").remove(); + fetch('api/peers', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Riv-Save-Config': 'true', + }, + body: JSON.stringify({"peers": peer_list}), + }) + .catch((error) => { + console.error('Error:', error); + }); + $("peer_list").remove(); }; } @@ -249,32 +259,44 @@ ui.showAllPeers = () => .then((peerList) => { var peers = add_table(peerList); //start peers test - ping(JSON.stringify(peers)); + fetch('api/ping', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(peers) + }) + .catch((error) => { + console.error('Error:', error); + }); }).catch((error) => { console.error(error); }); ui.getConnectedPeers = () => - fetch('api/getpeers') + fetch('api/peers') .then((response) => response.json()) +ui.updateConnectedPeersHandler = (cont) => { + $("peers").innerText = ""; + const regexStrip = /%[^\]]*/gm; + const regexMulticast = /:\/\/\[fe80::/; + cont.peers.forEach(peer => { + let row = $("peers").appendChild(document.createElement('div')); + let flag = row.appendChild(document.createElement("span")); + if(peer["remote"].match(regexMulticast)) + flag.className = "fa fa-thin fa-share-nodes peer-connected-fl"; + else + flag.className = "fi fi-" + ui.lookupCountryCodeByAddress(peer["remote"]) + " peer-connected-fl"; + row.append(peer["remote"].replace(regexStrip, "")); + }); +} + ui.updateConnectedPeers = () => ui.getConnectedPeers() - .then((cont) => { - $("peers").innerText = ""; - const regexStrip = /%[^\]]*/gm; - const regexMulticast = /:\/\/\[fe80::/; - cont.peers.forEach(peer => { - let row = $("peers").appendChild(document.createElement('div')); - let flag = row.appendChild(document.createElement("span")); - if(peer["remote"].match(regexMulticast)) - flag.className = "fa fa-thin fa-share-nodes peer-connected-fl"; - else - flag.className = "fi fi-" + ui.lookupCountryCodeByAddress(peer["remote"]) + " peer-connected-fl"; - row.append(peer["remote"].replace(regexStrip, "")); - }); - }).catch((error) => { + .then(ui.updateConnectedPeersHandler) + .catch((error) => { $("peers").innerText = error.message; }); @@ -286,7 +308,7 @@ ui.lookupCountryCodeByAddress = (address) => { } ui.getSelfInfo = () => - fetch('api/getself') + fetch('api/self') .then((response) => response.json()) ui.updateSelfInfo = () => @@ -301,6 +323,7 @@ ui.updateSelfInfo = () => $("ipv6").innerText = error.message; }); +ui.sse = new EventSource('/api/sse'); function main() { @@ -315,7 +338,17 @@ function main() { setInterval(ui.updateConnectedPeers, 5000); ui.updateSelfInfo(); - setInterval(ui.updateSelfInfo, 5000); + //setInterval(ui.updateSelfInfo, 5000); + + ui.sse.addEventListener("ping", (e) => { + let data = JSON.parse(e.data); + setPingValue(data.peer, data.value); + }) + + ui.sse.addEventListener("peers", (e) => { + ui.updateConnectedPeersHandler(JSON.parse(e.data)); + }) + }); } diff --git a/contrib/ui/mesh-ui/webview.go b/contrib/ui/mesh-ui/webview.go index d83e8da9..cdb19afb 100644 --- a/contrib/ui/mesh-ui/webview.go +++ b/contrib/ui/mesh-ui/webview.go @@ -2,25 +2,14 @@ package main import ( "bytes" - "encoding/json" - "errors" - "fmt" - "io/ioutil" "log" - "net" - "net/url" "os" - "path/filepath" - "strconv" - "strings" - "time" + "github.com/RiV-chain/RiV-mesh/src/defaults" "github.com/hjson/hjson-go" "github.com/webview/webview" "golang.org/x/text/encoding/unicode" - "github.com/RiV-chain/RiV-mesh/src/admin" - "github.com/RiV-chain/RiV-mesh/src/defaults" "github.com/docopt/docopt-go" ) @@ -32,7 +21,7 @@ Usage: mesh-ui -v | --version Options: - Index file name [default: index.html]. + Index file name [default: http://localhost:19019]. -c --console Show debug console window. -h --help Show this screen. -v --version Show version.` @@ -55,224 +44,20 @@ func main() { defer w.Destroy() w.SetTitle("RiV-mesh") w.SetSize(690, 920, webview.HintFixed) - /*1. Create ~/.riv-mesh folder if not existing - *2. Create ~/.riv-mesh/mesh.conf if not existing - *3. If the file exists read Peers. - *3.1 Invoke add peers for each record - */ - mesh_folder := ".riv-mesh" - mesh_conf := "mesh.conf" - user_home := get_user_home_path() - mesh_settings_folder := filepath.Join(user_home, mesh_folder) - err := os.MkdirAll(mesh_settings_folder, os.ModePerm) - if err != nil { - fmt.Printf("Unable to create folder: %v", err) - } - mesh_settings_path := filepath.Join(user_home, mesh_folder, mesh_conf) - if _, err := os.Stat(mesh_settings_path); os.IsNotExist(err) { - err := ioutil.WriteFile(mesh_settings_path, []byte(""), 0750) - if err != nil { - fmt.Printf("Unable to write file: %v", err) - } - } else { - //read peers from mesh.conf - conf, _ := ioutil.ReadFile(mesh_settings_path) - var dat map[string]interface{} - if err := hjson.Unmarshal(conf, &dat); err != nil { - fmt.Printf("Unable to parse mesh.conf file: %v", err) - } else { - if dat["Peers"] != nil { - peers := dat["Peers"].([]interface{}) - remove_peers() - for _, u := range peers { - log.Printf("Unmarshaled: %v", u.(string)) - add_peers(u.(string)) - } - } else { - fmt.Printf("Warning: Peers array not loaded from mesh.conf file") - } - } - } if confui.IndexHtml == "" { - confui.IndexHtml = "index.html" + confui.IndexHtml = getEndpoint() } - //Check is it URL already - indexUrl, err := url.ParseRequestURI(confui.IndexHtml) - if err != nil || len(indexUrl.Scheme) < 2 { // handling no scheme at all and windows c:\ as scheme detection - confui.IndexHtml, err = filepath.Abs(confui.IndexHtml) - if err != nil { - panic(errors.New("Index file not found: " + err.Error())) - } - if stat, err := os.Stat(confui.IndexHtml); err != nil { - panic(errors.New(fmt.Sprintf("Index file %v not found or permissians denied: %v", confui.IndexHtml, err.Error()))) - } else if stat.IsDir() { - panic(errors.New(fmt.Sprintf("Index file %v not found", confui.IndexHtml))) - } - path_prefix := "" - if indexUrl != nil && len(indexUrl.Scheme) == 1 { - path_prefix = "/" - } - indexUrl, err = url.ParseRequestURI("file://" + path_prefix + strings.ReplaceAll(confui.IndexHtml, "\\", "/")) - if err != nil { - panic(errors.New("Index file URL parse error: " + err.Error())) - } + if confui.IndexHtml == "" { + confui.IndexHtml = "http://localhost:19019" } - w.Bind("savePeers", func(peer_list string) { - //log.Println("peers saved ", peer_list) - var peers []string - _ = json.Unmarshal([]byte(peer_list), &peers) - log.Printf("Unmarshaled: %v", peers) - remove_peers() - for _, u := range peers { - log.Printf("Unmarshaled: %v", u) - add_peers(u) - } - //add peers to ~/mesh.conf - dat := make(map[string]interface{}) - dat["Peers"] = peers - bs, _ := hjson.Marshal(dat) - e := ioutil.WriteFile(mesh_settings_path, bs, 0750) - if e != nil { - fmt.Printf("Unable to write file: %v", e) - } - }) - w.Bind("ping", func(peer_list string) { - go ping(w, peer_list) - }) - log.Printf("Opening: %v", indexUrl) - w.Navigate(indexUrl.String()) + log.Printf("Opening: %v", confui.IndexHtml) + w.Navigate(confui.IndexHtml) w.Run() } -func ping(w webview.WebView, peer_list string) { - var peers []string - _ = json.Unmarshal([]byte(peer_list), &peers) - log.Printf("Unmarshaled: %v", peers) - for _, u := range peers { - log.Printf("Unmarshaled: %v", u) - ping_time := check(u) - log.Printf("ping: %d", ping_time) - setPingValue(w, u, strconv.FormatInt(ping_time, 10)) - } -} - -func check(peer string) int64 { - u, e := url.Parse(peer) - if e != nil { - return -1 - } - t := time.Now() - _, err := net.DialTimeout("tcp", u.Host, 5*time.Second) - if err != nil { - return -1 - } - d := time.Since(t) - return d.Milliseconds() -} - -func add_peers(uri string) { - _, err := run_command_with_arg("addpeers", "uri="+uri) - if err != nil { - log.Println("Error in the add_peers() call:", err) - } -} - -func remove_peers() { - run_command("removepeers") -} - -func setPingValue(p webview.WebView, peer string, value string) { - p.Dispatch(func() { - p.Eval("setPingValue('" + peer + "','" + value + "');") - }) -} - -func run_command(command string) []byte { - data, err := run_command_with_arg(command, "") - if err != nil { - log.Println("Error in the "+command+" call:", err) - } - return data -} - -func run_command_with_arg(command string, arg string) ([]byte, error) { - logbuffer := &bytes.Buffer{} - logger := log.New(logbuffer, "", log.Flags()) - - wrapErr := func(err error) error { - logger.Println("Error:", err) - return errors.New(fmt.Sprintln(logbuffer)) - } - - endpoint := getEndpoint(logger) - - var conn net.Conn - u, err := url.Parse(endpoint) - d := net.Dialer{Timeout: 5000 * time.Millisecond} - if err == nil { - switch strings.ToLower(u.Scheme) { - case "unix": - logger.Println("Connecting to UNIX socket", endpoint[7:]) - conn, err = d.Dial("unix", endpoint[7:]) - case "tcp": - logger.Println("Connecting to TCP socket", u.Host) - conn, err = d.Dial("tcp", u.Host) - default: - logger.Println("Unknown protocol or malformed address - check your endpoint") - err = errors.New("protocol not supported") - } - } else { - logger.Println("Connecting to TCP socket", u.Host) - conn, err = d.Dial("tcp", endpoint) - } - if err != nil { - return nil, wrapErr(err) - } - - logger.Println("Connected") - defer conn.Close() - - decoder := json.NewDecoder(conn) - encoder := json.NewEncoder(conn) - send := &admin.AdminSocketRequest{} - recv := &admin.AdminSocketResponse{} - send.Name = command - args := map[string]string{} - switch { - case len(arg) > 0: - tokens := strings.SplitN(arg, "=", 2) - args[tokens[0]] = tokens[1] - default: - } - - if send.Arguments, err = json.Marshal(args); err != nil { - return nil, err - } - if err := encoder.Encode(&send); err != nil { - return nil, err - } - logger.Printf("Request sent") - //js, _ := json.Marshal(send) - //fmt.Println("sent:", string(js)) - if err := decoder.Decode(&recv); err != nil { - return nil, wrapErr(err) - } - if recv.Status == "error" { - if err := recv.Error; err != "" { - return nil, wrapErr(errors.New("Admin socket returned an error:" + err)) - } else { - return nil, wrapErr(errors.New("Admin socket returned an error but didn't specify any error text")) - } - } - if json, err := json.MarshalIndent(recv.Response, "", " "); err == nil { - return json, nil - } - return nil, wrapErr(err) -} - -func getEndpoint(logger *log.Logger) string { +func getEndpoint() string { if config, err := os.ReadFile(defaults.GetDefaults().DefaultConfigFile); err == nil { if bytes.Equal(config[0:2], []byte{0xFF, 0xFE}) || bytes.Equal(config[0:2], []byte{0xFE, 0xFF}) { @@ -280,25 +65,16 @@ func getEndpoint(logger *log.Logger) string { decoder := utf.NewDecoder() config, err = decoder.Bytes(config) if err != nil { - return defaults.GetDefaults().DefaultAdminListen + return "" } } var dat map[string]interface{} if err := hjson.Unmarshal(config, &dat); err != nil { - return defaults.GetDefaults().DefaultAdminListen + return "" } - if ep, ok := dat["AdminListen"].(string); ok && (ep != "none" && ep != "") { - logger.Println("Found platform default config file", defaults.GetDefaults().DefaultConfigFile) - logger.Println("Using endpoint", ep, "from AdminListen") + if ep, ok := dat["HttpAddress"].(string); ok && (ep != "none" && ep != "") { return ep - } else { - logger.Println("Configuration file doesn't contain appropriate AdminListen option") - logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen) - return defaults.GetDefaults().DefaultAdminListen } - } else { - logger.Println("Can't open config file from default location", defaults.GetDefaults().DefaultConfigFile) - logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen) - return defaults.GetDefaults().DefaultAdminListen } + return "" } diff --git a/contrib/ui/mesh-ui/webview_other.go b/contrib/ui/mesh-ui/webview_other.go index e4365ad5..313ad87f 100644 --- a/contrib/ui/mesh-ui/webview_other.go +++ b/contrib/ui/mesh-ui/webview_other.go @@ -3,16 +3,5 @@ package main -import "os" - -func get_user_home_path() string { - path, exists := os.LookupEnv("HOME") - if exists { - return path - } else { - return "" - } -} - func Console(show bool) { } diff --git a/contrib/ui/mesh-ui/webview_windows.go b/contrib/ui/mesh-ui/webview_windows.go index 872f3b63..556afedd 100755 --- a/contrib/ui/mesh-ui/webview_windows.go +++ b/contrib/ui/mesh-ui/webview_windows.go @@ -4,19 +4,9 @@ package main import ( - "os" "syscall" ) -func get_user_home_path() string { - path, exists := os.LookupEnv("USERPROFILE") - if exists { - return path - } else { - return "" - } -} - func Console(show bool) { var getWin = syscall.NewLazyDLL("kernel32.dll").NewProc("GetConsoleWindow") var showWin = syscall.NewLazyDLL("user32.dll").NewProc("ShowWindow") diff --git a/contrib/ui/nas-asustor/www/assets/properties.js b/contrib/ui/nas-asustor/www/assets/properties.js index d7af016c..95919118 100644 --- a/contrib/ui/nas-asustor/www/assets/properties.js +++ b/contrib/ui/nas-asustor/www/assets/properties.js @@ -13,7 +13,7 @@ var ed = { var d = new Date(); d.setTime(d.getTime() + (10 * 60 * 1000)); document.cookie = "access_key=" + btoa( "user=" + encodeURIComponent($('#nasInputUser').val()) + ";pwd=" + encodeURIComponent($('#nasInputPassword').val()))+ "; expires=" + d.toUTCString() + "; path=/"; - $.ajax({url: "api/getself"}).done(function () { + $.ajax({url: "api/self"}).done(function () { window.location.reload(); }).fail(function () { ed.nasLogoutCall(); diff --git a/src/admin/admin.go b/src/admin/admin.go index a7241b72..bcc20978 100644 --- a/src/admin/admin.go +++ b/src/admin/admin.go @@ -9,6 +9,7 @@ import ( "net/url" "os" "sort" + "strconv" "archive/zip" "strings" @@ -22,13 +23,20 @@ import ( // TODO: Add authentication +type ServerEvent struct { + event string + data string +} + type AdminSocket struct { - core *core.Core - log core.Logger - listener net.Listener - handlers map[string]handler - done chan struct{} - config struct { + core *core.Core + log core.Logger + listener net.Listener + handlers map[string]handler + done chan struct{} + serverEvents chan ServerEvent + serverEventNextId int + config struct { listenaddr ListenAddress } } @@ -103,6 +111,7 @@ func New(c *core.Core, log core.Logger, opts ...SetupOption) (*AdminSocket, erro return res, nil }) a.done = make(chan struct{}) + a.serverEvents = make(chan ServerEvent) go a.listen() return a, a.core.SetAdmin(a) } @@ -247,33 +256,139 @@ func (a *AdminSocket) StartHttpServer(nc *config.NodeConfig) { http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Following methods are allowed: getself, getpeers. litening"+u.Host) }) - http.HandleFunc("/api/getself", func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - req := &GetSelfRequest{} - res := &GetSelfResponse{} - if err := a.getSelfHandler(req, res); err != nil { - http.Error(w, err.Error(), 503) + http.HandleFunc("/api/self", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + w.Header().Add("Content-Type", "application/json") + req := &GetSelfRequest{} + res := &GetSelfResponse{} + if err := a.getSelfHandler(req, res); err != nil { + http.Error(w, err.Error(), 503) + } + b, err := json.Marshal(res) + if err != nil { + http.Error(w, err.Error(), 503) + } + fmt.Fprint(w, string(b[:])) + default: + http.Error(w, "Method Not Allowed", 405) } - b, err := json.Marshal(res) - if err != nil { - http.Error(w, err.Error(), 503) - } - fmt.Fprint(w, string(b[:])) }) - http.HandleFunc("/api/getpeers", func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - req := &GetPeersRequest{} - res := &GetPeersResponse{} + http.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + w.Header().Add("Content-Type", "application/json") + req := &GetPeersRequest{} + res := &GetPeersResponse{} - if err := a.getPeersHandler(req, res); err != nil { - http.Error(w, err.Error(), 503) + if err := a.getPeersHandler(req, res); err != nil { + http.Error(w, err.Error(), 503) + } + b, err := json.Marshal(res) + if err != nil { + http.Error(w, err.Error(), 503) + } + fmt.Fprint(w, string(b[:])) + case "POST": + req := &AddPeersRequest{} + res := &AddPeersResponse{} + + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := a.addPeersHandler(req, res); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + b, err := json.Marshal(res) + if err != nil { + http.Error(w, err.Error(), 503) + } + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, string(b[:])) + case "PUT": + err := a.core.RemovePeers() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + req := &AddPeersRequest{} + res := &AddPeersResponse{} + + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := a.addPeersHandler(req, res); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + b, err := json.Marshal(res) + if err != nil { + http.Error(w, err.Error(), 503) + } + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, string(b[:])) + //TODO save peers + // saveHeaders := r.Header["Riv-Save-Config"] + // if len(saveHeaders) > 0 && saveHeaders[0] == "true" { + // nc.Peers = + // } + case "DELETE": + err := a.core.RemovePeers() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + http.Error(w, "No content", http.StatusNoContent) + default: + http.Error(w, "Method Not Allowed", 405) } - b, err := json.Marshal(res) - if err != nil { - http.Error(w, err.Error(), 503) - } - fmt.Fprint(w, string(b[:])) }) + http.HandleFunc("/api/ping", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "POST": + peer_list := []string{} + + err := json.NewDecoder(r.Body).Decode(&peer_list) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + go a.ping(peer_list) + http.Error(w, "Accepted", http.StatusAccepted) + default: + http.Error(w, "Method Not Allowed", 405) + } + }) + + http.HandleFunc("/api/sse", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + w.Header().Add("Content-Type", "text/event-stream") + Loop: + for { + select { + case v := <-a.serverEvents: + fmt.Fprintln(w, "id:", a.serverEventNextId) + fmt.Fprintln(w, "event:", v.event) + fmt.Fprintln(w, "data:", v.data) + fmt.Fprintln(w) //end of event + a.serverEventNextId += 1 + default: + break Loop + } + } + default: + http.Error(w, "Method Not Allowed", 405) + } + }) + var docFs = "" pakReader, err := zip.OpenReader(nc.WwwRoot) if err == nil { @@ -300,6 +415,29 @@ func (a *AdminSocket) StartHttpServer(nc *config.NodeConfig) { } } +func (a *AdminSocket) ping(peers []string) { + for _, u := range peers { + go func(u string) { + data, _ := json.Marshal(map[string]string{"peer": u, "value": strconv.FormatInt(check(u), 10)}) + a.serverEvents <- ServerEvent{event: "ping", data: string(data)} + }(u) + } +} + +func check(peer string) int64 { + u, e := url.Parse(peer) + if e != nil { + return -1 + } + t := time.Now() + _, err := net.DialTimeout("tcp", u.Host, 5*time.Second) + if err != nil { + return -1 + } + d := time.Since(t) + return d.Milliseconds() +} + // IsStarted returns true if the module has been started. func (a *AdminSocket) IsStarted() bool { select {