mirror of
https://github.com/gravitl/netmaker.git
synced 2025-10-07 09:41:37 +08:00
finished initial crud for hosts, fixed stun server close bug
This commit is contained in:
@@ -27,6 +27,7 @@ var HttpHandlers = []interface{}{
|
|||||||
extClientHandlers,
|
extClientHandlers,
|
||||||
ipHandlers,
|
ipHandlers,
|
||||||
loggerHandlers,
|
loggerHandlers,
|
||||||
|
hostHandlers,
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleRESTRequests - handles the rest requests
|
// HandleRESTRequests - handles the rest requests
|
||||||
|
114
controllers/hosts.go
Normal file
114
controllers/hosts.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gravitl/netmaker/logger"
|
||||||
|
"github.com/gravitl/netmaker/logic"
|
||||||
|
"github.com/gravitl/netmaker/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func hostHandlers(r *mux.Router) {
|
||||||
|
r.HandleFunc("/api/hosts", logic.SecurityCheck(false, http.HandlerFunc(getHosts))).Methods("GET")
|
||||||
|
r.HandleFunc("/api/hosts", logic.SecurityCheck(true, http.HandlerFunc(updateHost))).Methods("PUT")
|
||||||
|
r.HandleFunc("/api/hosts/{hostid}", logic.SecurityCheck(true, http.HandlerFunc(deleteHost))).Methods("DELETE")
|
||||||
|
// r.HandleFunc("/api/hosts/{hostid}/{network}", logic.SecurityCheck(false, http.HandlerFunc(getHosts))).Methods("PUT")
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:route GET /api/hosts hosts getHosts
|
||||||
|
//
|
||||||
|
// Lists all hosts.
|
||||||
|
//
|
||||||
|
// Schemes: https
|
||||||
|
//
|
||||||
|
// Security:
|
||||||
|
// oauth
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: getHostsSliceResponse
|
||||||
|
func getHosts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
currentHosts, err := logic.GetAllHosts()
|
||||||
|
if err != nil {
|
||||||
|
logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error())
|
||||||
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Log(2, r.Header.Get("user"), "fetched all hosts")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(currentHosts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:route PUT /api/hosts/{hostid} hosts updateHost
|
||||||
|
//
|
||||||
|
// Updates a Netclient host on Netmaker server.
|
||||||
|
//
|
||||||
|
// Schemes: https
|
||||||
|
//
|
||||||
|
// Security:
|
||||||
|
// oauth
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: updateHostResponse
|
||||||
|
func updateHost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var newHostData models.Host
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&newHostData)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log(0, r.Header.Get("user"), "failed to update a host:", err.Error())
|
||||||
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// confirm host exists
|
||||||
|
currHost, err := logic.GetHost(newHostData.ID.String())
|
||||||
|
if err != nil {
|
||||||
|
logger.Log(0, r.Header.Get("user"), "failed to update a host:", err.Error())
|
||||||
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logic.UpdateHost(&newHostData, currHost) // update the in memory struct values
|
||||||
|
if err = logic.UpsertHost(&newHostData); err != nil {
|
||||||
|
logger.Log(0, r.Header.Get("user"), "failed to update a host:", err.Error())
|
||||||
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log(2, r.Header.Get("user"), "updated host", newHostData.ID.String())
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(newHostData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:route DELETE /api/hosts/{hostid} hosts deleteHost
|
||||||
|
//
|
||||||
|
// Deletes a Netclient host from Netmaker server.
|
||||||
|
//
|
||||||
|
// Schemes: https
|
||||||
|
//
|
||||||
|
// Security:
|
||||||
|
// oauth
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: deleteHostResponse
|
||||||
|
func deleteHost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var params = mux.Vars(r)
|
||||||
|
hostid := params["hostid"]
|
||||||
|
// confirm host exists
|
||||||
|
currHost, err := logic.GetHost(hostid)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log(0, r.Header.Get("user"), "failed to delete a host:", err.Error())
|
||||||
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = logic.RemoveHost(currHost); err != nil {
|
||||||
|
logger.Log(0, r.Header.Get("user"), "failed to delete a host:", err.Error())
|
||||||
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log(2, r.Header.Get("user"), "removed host", currHost.Name)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(currHost)
|
||||||
|
}
|
@@ -14,96 +14,73 @@ import (
|
|||||||
"golang.org/x/crypto/nacl/box"
|
"golang.org/x/crypto/nacl/box"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// == Table Names ==
|
||||||
// NETWORKS_TABLE_NAME - networks table
|
// NETWORKS_TABLE_NAME - networks table
|
||||||
const NETWORKS_TABLE_NAME = "networks"
|
NETWORKS_TABLE_NAME = "networks"
|
||||||
|
|
||||||
// NODES_TABLE_NAME - nodes table
|
// NODES_TABLE_NAME - nodes table
|
||||||
const NODES_TABLE_NAME = "nodes"
|
NODES_TABLE_NAME = "nodes"
|
||||||
|
|
||||||
// DELETED_NODES_TABLE_NAME - deleted nodes table
|
// DELETED_NODES_TABLE_NAME - deleted nodes table
|
||||||
const DELETED_NODES_TABLE_NAME = "deletednodes"
|
DELETED_NODES_TABLE_NAME = "deletednodes"
|
||||||
|
|
||||||
// USERS_TABLE_NAME - users table
|
// USERS_TABLE_NAME - users table
|
||||||
const USERS_TABLE_NAME = "users"
|
USERS_TABLE_NAME = "users"
|
||||||
|
|
||||||
// CERTS_TABLE_NAME - certificates table
|
// CERTS_TABLE_NAME - certificates table
|
||||||
const CERTS_TABLE_NAME = "certs"
|
CERTS_TABLE_NAME = "certs"
|
||||||
|
|
||||||
// DNS_TABLE_NAME - dns table
|
// DNS_TABLE_NAME - dns table
|
||||||
const DNS_TABLE_NAME = "dns"
|
DNS_TABLE_NAME = "dns"
|
||||||
|
|
||||||
// EXT_CLIENT_TABLE_NAME - ext client table
|
// EXT_CLIENT_TABLE_NAME - ext client table
|
||||||
const EXT_CLIENT_TABLE_NAME = "extclients"
|
EXT_CLIENT_TABLE_NAME = "extclients"
|
||||||
|
|
||||||
// PEERS_TABLE_NAME - peers table
|
// PEERS_TABLE_NAME - peers table
|
||||||
const PEERS_TABLE_NAME = "peers"
|
PEERS_TABLE_NAME = "peers"
|
||||||
|
|
||||||
// SERVERCONF_TABLE_NAME - stores server conf
|
// SERVERCONF_TABLE_NAME - stores server conf
|
||||||
const SERVERCONF_TABLE_NAME = "serverconf"
|
SERVERCONF_TABLE_NAME = "serverconf"
|
||||||
|
|
||||||
// SERVER_UUID_TABLE_NAME - stores unique netmaker server data
|
// SERVER_UUID_TABLE_NAME - stores unique netmaker server data
|
||||||
const SERVER_UUID_TABLE_NAME = "serveruuid"
|
SERVER_UUID_TABLE_NAME = "serveruuid"
|
||||||
|
|
||||||
// SERVER_UUID_RECORD_KEY - telemetry thing
|
// SERVER_UUID_RECORD_KEY - telemetry thing
|
||||||
const SERVER_UUID_RECORD_KEY = "serveruuid"
|
SERVER_UUID_RECORD_KEY = "serveruuid"
|
||||||
|
|
||||||
// DATABASE_FILENAME - database file name
|
// DATABASE_FILENAME - database file name
|
||||||
const DATABASE_FILENAME = "netmaker.db"
|
DATABASE_FILENAME = "netmaker.db"
|
||||||
|
|
||||||
// GENERATED_TABLE_NAME - stores server generated k/v
|
// GENERATED_TABLE_NAME - stores server generated k/v
|
||||||
const GENERATED_TABLE_NAME = "generated"
|
GENERATED_TABLE_NAME = "generated"
|
||||||
|
|
||||||
// NODE_ACLS_TABLE_NAME - stores the node ACL rules
|
// NODE_ACLS_TABLE_NAME - stores the node ACL rules
|
||||||
const NODE_ACLS_TABLE_NAME = "nodeacls"
|
NODE_ACLS_TABLE_NAME = "nodeacls"
|
||||||
|
|
||||||
// SSO_STATE_CACHE - holds sso session information for OAuth2 sign-ins
|
// SSO_STATE_CACHE - holds sso session information for OAuth2 sign-ins
|
||||||
const SSO_STATE_CACHE = "ssostatecache"
|
SSO_STATE_CACHE = "ssostatecache"
|
||||||
|
|
||||||
// METRICS_TABLE_NAME - stores network metrics
|
// METRICS_TABLE_NAME - stores network metrics
|
||||||
const METRICS_TABLE_NAME = "metrics"
|
METRICS_TABLE_NAME = "metrics"
|
||||||
|
|
||||||
// NETWORK_USER_TABLE_NAME - network user table tracks stats for a network user per network
|
// NETWORK_USER_TABLE_NAME - network user table tracks stats for a network user per network
|
||||||
const NETWORK_USER_TABLE_NAME = "networkusers"
|
NETWORK_USER_TABLE_NAME = "networkusers"
|
||||||
|
|
||||||
// USER_GROUPS_TABLE_NAME - table for storing usergroups
|
// USER_GROUPS_TABLE_NAME - table for storing usergroups
|
||||||
const USER_GROUPS_TABLE_NAME = "usergroups"
|
USER_GROUPS_TABLE_NAME = "usergroups"
|
||||||
|
|
||||||
// CACHE_TABLE_NAME - caching table
|
// CACHE_TABLE_NAME - caching table
|
||||||
const CACHE_TABLE_NAME = "cache"
|
CACHE_TABLE_NAME = "cache"
|
||||||
|
// HOSTS_TABLE_NAME - the table name for hosts
|
||||||
|
HOSTS_TABLE_NAME = "hosts"
|
||||||
|
|
||||||
// == ERROR CONSTS ==
|
// == ERROR CONSTS ==
|
||||||
|
|
||||||
// NO_RECORD - no singular result found
|
// NO_RECORD - no singular result found
|
||||||
const NO_RECORD = "no result found"
|
NO_RECORD = "no result found"
|
||||||
|
|
||||||
// NO_RECORDS - no results found
|
// NO_RECORDS - no results found
|
||||||
const NO_RECORDS = "could not find any records"
|
NO_RECORDS = "could not find any records"
|
||||||
|
|
||||||
// == Constants ==
|
|
||||||
|
|
||||||
|
// == DB Constants ==
|
||||||
// INIT_DB - initialize db
|
// INIT_DB - initialize db
|
||||||
const INIT_DB = "init"
|
INIT_DB = "init"
|
||||||
|
|
||||||
// CREATE_TABLE - create table const
|
// CREATE_TABLE - create table const
|
||||||
const CREATE_TABLE = "createtable"
|
CREATE_TABLE = "createtable"
|
||||||
|
|
||||||
// INSERT - insert into db const
|
// INSERT - insert into db const
|
||||||
const INSERT = "insert"
|
INSERT = "insert"
|
||||||
|
|
||||||
// INSERT_PEER - insert peer into db const
|
// INSERT_PEER - insert peer into db const
|
||||||
const INSERT_PEER = "insertpeer"
|
INSERT_PEER = "insertpeer"
|
||||||
|
|
||||||
// DELETE - delete db record const
|
// DELETE - delete db record const
|
||||||
const DELETE = "delete"
|
DELETE = "delete"
|
||||||
|
|
||||||
// DELETE_ALL - delete a table const
|
// DELETE_ALL - delete a table const
|
||||||
const DELETE_ALL = "deleteall"
|
DELETE_ALL = "deleteall"
|
||||||
|
|
||||||
// FETCH_ALL - fetch table contents const
|
// FETCH_ALL - fetch table contents const
|
||||||
const FETCH_ALL = "fetchall"
|
FETCH_ALL = "fetchall"
|
||||||
|
|
||||||
// CLOSE_DB - graceful close of db const
|
// CLOSE_DB - graceful close of db const
|
||||||
const CLOSE_DB = "closedb"
|
CLOSE_DB = "closedb"
|
||||||
|
)
|
||||||
|
|
||||||
func getCurrentDB() map[string]interface{} {
|
func getCurrentDB() map[string]interface{} {
|
||||||
switch servercfg.GetDB() {
|
switch servercfg.GetDB() {
|
||||||
@@ -155,6 +132,7 @@ func createTables() {
|
|||||||
createTable(NETWORK_USER_TABLE_NAME)
|
createTable(NETWORK_USER_TABLE_NAME)
|
||||||
createTable(USER_GROUPS_TABLE_NAME)
|
createTable(USER_GROUPS_TABLE_NAME)
|
||||||
createTable(CACHE_TABLE_NAME)
|
createTable(CACHE_TABLE_NAME)
|
||||||
|
createTable(HOSTS_TABLE_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTable(tableName string) error {
|
func createTable(tableName string) error {
|
||||||
|
139
logic/hosts.go
Normal file
139
logic/hosts.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gravitl/netmaker/database"
|
||||||
|
"github.com/gravitl/netmaker/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetAllHosts - returns all hosts in flat list or error
|
||||||
|
func GetAllHosts() ([]models.Host, error) {
|
||||||
|
currHostMap, err := GetHostsMap()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var currentHosts = []models.Host{}
|
||||||
|
for k := range currHostMap {
|
||||||
|
var h = *currHostMap[k]
|
||||||
|
currentHosts = append(currentHosts, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentHosts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHostsMap - gets all the current hosts on machine in a map
|
||||||
|
func GetHostsMap() (map[string]*models.Host, error) {
|
||||||
|
records, err := database.FetchRecords(database.HOSTS_TABLE_NAME)
|
||||||
|
if err != nil && !database.IsEmptyRecord(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
currHostMap := make(map[string]*models.Host)
|
||||||
|
for k := range records {
|
||||||
|
var h models.Host
|
||||||
|
err = json.Unmarshal([]byte(records[k]), &h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
currHostMap[h.ID.String()] = &h
|
||||||
|
}
|
||||||
|
|
||||||
|
return currHostMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHost - gets a host from db given id
|
||||||
|
func GetHost(hostid string) (*models.Host, error) {
|
||||||
|
record, err := database.FetchRecord(database.HOSTS_TABLE_NAME, hostid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var h models.Host
|
||||||
|
if err = json.Unmarshal([]byte(record), &h); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateHost - creates a host if not exist
|
||||||
|
func CreateHost(h *models.Host) error {
|
||||||
|
_, err := GetHost(h.ID.String())
|
||||||
|
if (err != nil && !database.IsEmptyRecord(err)) || (err == nil) {
|
||||||
|
return fmt.Errorf("host already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
return UpsertHost(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateHost - updates host data by field
|
||||||
|
func UpdateHost(newHost, currentHost *models.Host) {
|
||||||
|
// unchangeable fields via API here
|
||||||
|
newHost.DaemonInstalled = currentHost.DaemonInstalled
|
||||||
|
newHost.OS = currentHost.OS
|
||||||
|
newHost.IPForwarding = currentHost.IPForwarding
|
||||||
|
newHost.HostPass = currentHost.HostPass
|
||||||
|
newHost.NodePassword = currentHost.NodePassword
|
||||||
|
newHost.MacAddress = currentHost.MacAddress
|
||||||
|
newHost.Debug = currentHost.Debug
|
||||||
|
newHost.Nodes = currentHost.Nodes
|
||||||
|
newHost.PublicKey = currentHost.PublicKey
|
||||||
|
newHost.InternetGateway = currentHost.InternetGateway
|
||||||
|
newHost.TrafficKeyPublic = currentHost.TrafficKeyPublic
|
||||||
|
|
||||||
|
// changeable fields
|
||||||
|
if len(newHost.Version) == 0 {
|
||||||
|
newHost.Version = currentHost.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newHost.Name) == 0 {
|
||||||
|
newHost.Name = currentHost.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if newHost.LocalAddress.String() != currentHost.LocalAddress.String() {
|
||||||
|
newHost.LocalAddress = currentHost.LocalAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
if newHost.LocalRange.String() != currentHost.LocalRange.String() {
|
||||||
|
newHost.LocalRange = currentHost.LocalRange
|
||||||
|
}
|
||||||
|
|
||||||
|
if newHost.MTU == 0 {
|
||||||
|
newHost.MTU = currentHost.MTU
|
||||||
|
}
|
||||||
|
|
||||||
|
if newHost.ListenPort == 0 {
|
||||||
|
newHost.ListenPort = currentHost.ListenPort
|
||||||
|
}
|
||||||
|
|
||||||
|
if newHost.ProxyListenPort == 0 {
|
||||||
|
newHost.ProxyListenPort = currentHost.ProxyListenPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertHost - upserts into DB a given host model, does not check for existence*
|
||||||
|
func UpsertHost(h *models.Host) error {
|
||||||
|
data, err := json.Marshal(h)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.Insert(h.ID.String(), string(data), database.HOSTS_TABLE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveHost - removes a given host from server
|
||||||
|
func RemoveHost(h *models.Host) error {
|
||||||
|
if len(h.Nodes) > 0 {
|
||||||
|
for i := range h.Nodes {
|
||||||
|
id := h.Nodes[i]
|
||||||
|
n, err := GetNodeByID(id)
|
||||||
|
if err == nil {
|
||||||
|
if err = DeleteNodeByID(&n); err != nil {
|
||||||
|
return err // must remove associated nodes before removing a host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return database.DeleteRecord(database.HOSTS_TABLE_NAME, h.ID.String())
|
||||||
|
}
|
@@ -30,5 +30,5 @@ type Host struct {
|
|||||||
MacAddress net.HardwareAddr `json:"macaddress" yaml:"macaddress"`
|
MacAddress net.HardwareAddr `json:"macaddress" yaml:"macaddress"`
|
||||||
TrafficKeyPublic []byte `json:"traffickeypublic" yaml:"trafficekeypublic"`
|
TrafficKeyPublic []byte `json:"traffickeypublic" yaml:"trafficekeypublic"`
|
||||||
InternetGateway net.UDPAddr `json:"internetgateway" yaml:"internetgateway"`
|
InternetGateway net.UDPAddr `json:"internetgateway" yaml:"internetgateway"`
|
||||||
Nodes []Node `json:"nodes" yaml:"nodes"`
|
Nodes []string `json:"nodes" yaml:"nodes"`
|
||||||
}
|
}
|
||||||
|
@@ -144,20 +144,20 @@ func normalize(address string) string {
|
|||||||
return address
|
return address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start - starts the stun server
|
||||||
func Start(wg *sync.WaitGroup) {
|
func Start(wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
go func() {
|
go func(wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, syscall.SIGTERM, os.Interrupt)
|
signal.Notify(quit, syscall.SIGTERM, os.Interrupt)
|
||||||
<-quit
|
<-quit
|
||||||
cancel()
|
cancel()
|
||||||
}()
|
}(wg)
|
||||||
normalized := normalize(fmt.Sprintf("0.0.0.0:%d", servercfg.GetStunPort()))
|
normalized := normalize(fmt.Sprintf("0.0.0.0:%d", servercfg.GetStunPort()))
|
||||||
logger.Log(0, "netmaker-stun listening on", normalized, "via udp")
|
logger.Log(0, "netmaker-stun listening on", normalized, "via udp")
|
||||||
err := listenUDPAndServe(ctx, "udp", normalized)
|
err := listenUDPAndServe(ctx, "udp", normalized)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Log(0, "failed to start stun server: ", err.Error())
|
logger.Log(0, "failed to start stun server: ", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user