2024-09-02 18:43:49 +00:00
|
|
|
/*
|
|
|
|
Jilo Agent
|
|
|
|
|
|
|
|
Description: Remote agent for Jilo Web with http API
|
|
|
|
Author: Yasen Pramatarov
|
|
|
|
License: GPLv2
|
|
|
|
Project URL: https://lindeas.com/jilo
|
|
|
|
Year: 2024
|
|
|
|
Version: 0.1
|
|
|
|
*/
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-10-07 08:51:36 +00:00
|
|
|
"flag"
|
2024-09-02 18:43:49 +00:00
|
|
|
"fmt"
|
2024-09-03 10:57:17 +00:00
|
|
|
"encoding/json"
|
2024-09-03 07:23:44 +00:00
|
|
|
"gopkg.in/yaml.v2"
|
2024-09-03 10:57:17 +00:00
|
|
|
"github.com/dgrijalva/jwt-go"
|
2024-09-02 19:28:45 +00:00
|
|
|
"io/ioutil"
|
2024-09-02 18:43:49 +00:00
|
|
|
"log"
|
|
|
|
"net/http"
|
2024-09-02 19:28:45 +00:00
|
|
|
"os"
|
2024-09-02 18:43:49 +00:00
|
|
|
"os/exec"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
2024-09-02 19:28:45 +00:00
|
|
|
// Config holds the structure of the configuration file
|
|
|
|
type Config struct {
|
2024-09-03 07:23:44 +00:00
|
|
|
AgentPort int `yaml:"agent_port"`
|
2024-10-02 15:54:52 +00:00
|
|
|
SSLcert string `yaml:"ssl_cert"`
|
|
|
|
SSLkey string `yaml:"ssl_key"`
|
2024-09-03 10:57:17 +00:00
|
|
|
SecretKey string `yaml:"secret_key"`
|
2024-09-03 07:23:44 +00:00
|
|
|
NginxPort int `yaml:"nginx_port"`
|
|
|
|
ProsodyPort int `yaml:"prosody_port"`
|
|
|
|
JicofoStatsURL string `yaml:"jicofo_stats_url"`
|
|
|
|
JVBStatsURL string `yaml:"jvb_stats_url"`
|
2024-09-03 08:55:13 +00:00
|
|
|
JibriHealthURL string `yaml:"jibri_health_url"`
|
2024-09-02 19:28:45 +00:00
|
|
|
}
|
|
|
|
|
2024-09-03 10:57:17 +00:00
|
|
|
// Claims holds JWT access right
|
|
|
|
type Claims struct {
|
|
|
|
Username string `json:"sub"`
|
|
|
|
Role string `json:"role"`
|
|
|
|
jwt.StandardClaims
|
|
|
|
}
|
|
|
|
|
2024-09-02 18:43:49 +00:00
|
|
|
// NginxData holds the nginx data structure for the API response to /nginx
|
|
|
|
type NginxData struct {
|
2024-09-02 19:28:45 +00:00
|
|
|
NginxState string `json:"nginx_state"`
|
2024-09-02 18:43:49 +00:00
|
|
|
NginxConnections int `json:"nginx_connections"`
|
|
|
|
}
|
|
|
|
|
2024-09-02 21:04:13 +00:00
|
|
|
// ProsodyData holds the prosody data structure for the API response to /prosody
|
|
|
|
type ProsodyData struct {
|
|
|
|
ProsodyState string `json:"prosody_state"`
|
|
|
|
ProsodyConnections int `json:"prosody_connections"`
|
|
|
|
}
|
|
|
|
|
2024-09-02 19:28:45 +00:00
|
|
|
// JicofoData holds the Jicofo data structure for the API response to /jicofo
|
|
|
|
type JicofoData struct {
|
|
|
|
JicofoState string `json:"jicofo_state"`
|
|
|
|
JicofoAPIData map[string]interface{} `json:"jicofo_api_data"`
|
|
|
|
}
|
|
|
|
|
2024-09-02 21:04:13 +00:00
|
|
|
// JVBData holds the JVB data structure for the API response to /jvb
|
|
|
|
type JVBData struct {
|
|
|
|
JVBState string `json:"jvb_state"`
|
|
|
|
JVBAPIData map[string]interface{} `json:"jvb_api_data"`
|
|
|
|
}
|
|
|
|
|
2024-09-03 08:55:13 +00:00
|
|
|
// JibriData holds the Jibri data structure for the API response to /jibri
|
|
|
|
type JibriData struct {
|
|
|
|
JibriState string `json:"jibri_state"`
|
|
|
|
JibriHealthData map[string]interface{} `json:"jibri_health_data"`
|
|
|
|
}
|
|
|
|
|
2024-09-03 10:57:17 +00:00
|
|
|
var secretKey []byte
|
|
|
|
|
2024-09-02 20:01:15 +00:00
|
|
|
// getServiceState checks the status of the speciied service
|
|
|
|
func getServiceState(service string) string {
|
|
|
|
output, err := exec.Command("systemctl", "is-active", service).Output()
|
2024-09-02 18:43:49 +00:00
|
|
|
if err != nil {
|
2024-09-02 20:01:15 +00:00
|
|
|
log.Printf("Error checking the service \"%v\" state: %v", service, err)
|
2024-09-02 19:34:38 +00:00
|
|
|
return "error"
|
|
|
|
}
|
|
|
|
state := strings.TrimSpace(string(output))
|
|
|
|
if state == "active" {
|
|
|
|
return "running"
|
|
|
|
}
|
|
|
|
return "not running"
|
|
|
|
}
|
|
|
|
|
2024-09-02 21:04:13 +00:00
|
|
|
// getServiceConnections gets the number of active connections to the specified port
|
|
|
|
func getServiceConnections(service string, port int) int {
|
2024-09-02 20:01:15 +00:00
|
|
|
cmd := fmt.Sprintf("netstat -an | grep ':%d' | wc -l", port)
|
2024-09-02 19:28:45 +00:00
|
|
|
output, err := exec.Command("bash", "-c", cmd).Output()
|
2024-09-02 18:43:49 +00:00
|
|
|
if err != nil {
|
2024-09-02 21:04:13 +00:00
|
|
|
log.Printf("Error counting the \"%v\" connections: %v", service, err)
|
2024-09-02 18:43:49 +00:00
|
|
|
return -1
|
|
|
|
}
|
|
|
|
connections := strings.TrimSpace(string(output))
|
|
|
|
connectionsInt, err := strconv.Atoi(connections)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Error converting connections to integer number: %v", err)
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
return connectionsInt
|
|
|
|
}
|
|
|
|
|
2024-09-02 20:01:15 +00:00
|
|
|
// getJitsiAPIData gets the response from the specified Jitsi stats API
|
|
|
|
func getJitsiAPIData(service string, url string) map[string]interface{} {
|
|
|
|
cmd := fmt.Sprintf("curl -s %v", url)
|
|
|
|
output, err := exec.Command("bash", "-c", cmd).Output()
|
2024-09-02 19:28:45 +00:00
|
|
|
if err != nil {
|
2024-09-02 20:01:15 +00:00
|
|
|
log.Printf("Error getting the \"%v\" API stats: %v", service, err)
|
|
|
|
return map[string]interface{}{"error": "failed to get the Jitsi API stats"}
|
2024-09-02 19:28:45 +00:00
|
|
|
}
|
|
|
|
var result map[string]interface{}
|
|
|
|
if err := json.Unmarshal(output, &result); err != nil {
|
|
|
|
log.Printf("Error in parsing the JSON: %v", err)
|
|
|
|
return map[string]interface{}{"error": "invalid JSON format"}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2024-09-03 07:23:44 +00:00
|
|
|
// loadConfig loads the configuration from a YAML config file
|
2024-09-02 21:04:13 +00:00
|
|
|
func loadConfig(filename string) (Config) {
|
|
|
|
|
|
|
|
// default config values
|
|
|
|
config := Config {
|
|
|
|
AgentPort: 8081, // default Agent port (we avoid 80, 443, 8080 and 8888)
|
|
|
|
NginxPort: 80, // default nginx port
|
|
|
|
ProsodyPort: 5222, // default prosody port
|
|
|
|
JicofoStatsURL: "http://localhost:8888/stats", // default Jicofo stats URL
|
|
|
|
JVBStatsURL: "http://localhost:8080/colibri/stats", // default JVB stats URL
|
2024-09-03 08:55:13 +00:00
|
|
|
JibriHealthURL: "http://localhost:2222/jibri/api/v1.0/health", // default Jibri health URL
|
2024-09-02 21:04:13 +00:00
|
|
|
}
|
2024-09-02 19:28:45 +00:00
|
|
|
|
2024-09-02 21:04:13 +00:00
|
|
|
// we try to load the config file; use default values otherwise
|
2024-09-02 19:28:45 +00:00
|
|
|
file, err := os.Open(filename)
|
|
|
|
if err != nil {
|
2024-09-02 21:04:13 +00:00
|
|
|
log.Printf("Can't open the config file \"%v\". Using default values.", filename)
|
|
|
|
return config
|
2024-09-02 19:28:45 +00:00
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
bytes, err := ioutil.ReadAll(file)
|
|
|
|
if err != nil {
|
2024-09-02 21:04:13 +00:00
|
|
|
log.Printf("There was an error reading the config file. Using default values")
|
|
|
|
return config
|
2024-09-02 19:28:45 +00:00
|
|
|
}
|
|
|
|
|
2024-09-03 07:23:44 +00:00
|
|
|
if err := yaml.Unmarshal(bytes, &config); err != nil {
|
2024-09-02 21:04:13 +00:00
|
|
|
log.Printf("Error parsing the config file. Using default values.")
|
2024-09-02 19:28:45 +00:00
|
|
|
}
|
|
|
|
|
2024-09-02 21:04:13 +00:00
|
|
|
return config
|
2024-09-02 19:28:45 +00:00
|
|
|
}
|
|
|
|
|
2024-09-03 10:57:17 +00:00
|
|
|
// authenticationJWT handles the JWT auth
|
|
|
|
func authenticationJWT(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
tokenString := r.Header.Get("Authorization")
|
|
|
|
|
2024-10-02 13:40:57 +00:00
|
|
|
// DEBUG print the token for debugging (only for debug, remove in prod)
|
|
|
|
//log.Println("Received token:", tokenString)
|
|
|
|
|
2024-09-03 10:57:17 +00:00
|
|
|
// empty auth header
|
|
|
|
if tokenString == "" {
|
2024-10-02 13:40:57 +00:00
|
|
|
log.Println("No Authorization header received")
|
2024-09-03 10:57:17 +00:00
|
|
|
http.Error(w, "Auth header not received", http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove "Bearer "
|
|
|
|
if len(tokenString) > 7 && tokenString[:7] == "Bearer " {
|
|
|
|
tokenString = tokenString[7:]
|
2024-10-02 13:40:57 +00:00
|
|
|
} else {
|
|
|
|
log.Println("Bearer token missing")
|
|
|
|
http.Error(w, "Malformed Authorization header", http.StatusUnauthorized)
|
|
|
|
return
|
2024-09-03 10:57:17 +00:00
|
|
|
}
|
|
|
|
|
2024-10-02 13:40:57 +00:00
|
|
|
// DEBUG print out the token for debugging (remove in production!)
|
|
|
|
//log.Printf("Received JWT: %s", tokenString)
|
|
|
|
|
2024-09-03 10:57:17 +00:00
|
|
|
claims := &Claims{}
|
|
|
|
|
|
|
|
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
2024-10-02 13:40:57 +00:00
|
|
|
// DEBUG log secret key for debugging (remove from production!)
|
|
|
|
//log.Printf("Parsing JWT with secret key: %s", secretKey)
|
2024-09-03 10:57:17 +00:00
|
|
|
return secretKey, nil
|
|
|
|
})
|
|
|
|
|
2024-10-02 13:40:57 +00:00
|
|
|
// JWT errors and error logging
|
2024-09-03 10:57:17 +00:00
|
|
|
if err != nil {
|
2024-10-02 13:40:57 +00:00
|
|
|
// log the error message for debugging (not in prod!)
|
|
|
|
log.Printf("JWT parse error: %v", err)
|
2024-09-03 10:57:17 +00:00
|
|
|
if err == jwt.ErrSignatureInvalid {
|
|
|
|
http.Error(w, "Invalid JWT signature", http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
2024-10-02 13:40:57 +00:00
|
|
|
http.Error(w, "Error parsing JWT: "+err.Error(), http.StatusUnauthorized)
|
2024-09-03 10:57:17 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// JWT invalid
|
|
|
|
if !token.Valid {
|
2024-10-02 13:40:57 +00:00
|
|
|
log.Println("Invalid JWT token")
|
|
|
|
http.Error(w, "Invalid JWT token", http.StatusUnauthorized)
|
2024-09-03 10:57:17 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-09-02 18:43:49 +00:00
|
|
|
// nginxHandler handles the /nginx endpoint
|
2024-09-02 19:28:45 +00:00
|
|
|
func nginxHandler(config Config, w http.ResponseWriter, r *http.Request) {
|
2024-09-02 18:43:49 +00:00
|
|
|
data := NginxData {
|
2024-09-02 20:01:15 +00:00
|
|
|
NginxState: getServiceState("nginx"),
|
2024-09-02 21:04:13 +00:00
|
|
|
NginxConnections: getServiceConnections("nginx", config.NginxPort),
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
json.NewEncoder(w).Encode(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
// prosodyHandler handles the /prosody endpoint
|
|
|
|
func prosodyHandler(config Config, w http.ResponseWriter, r *http.Request) {
|
|
|
|
data := ProsodyData {
|
|
|
|
ProsodyState: getServiceState("prosody"),
|
|
|
|
ProsodyConnections: getServiceConnections("prosody", config.ProsodyPort),
|
2024-09-02 18:43:49 +00:00
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
json.NewEncoder(w).Encode(data)
|
|
|
|
}
|
|
|
|
|
2024-09-02 19:34:38 +00:00
|
|
|
// jicofoHandler handles the /jicofo endpoint
|
|
|
|
func jicofoHandler(config Config, w http.ResponseWriter, r *http.Request) {
|
|
|
|
data := JicofoData {
|
2024-09-02 20:01:15 +00:00
|
|
|
JicofoState: getServiceState("jicofo"),
|
|
|
|
JicofoAPIData: getJitsiAPIData("jicofo", config.JicofoStatsURL),
|
2024-09-02 19:34:38 +00:00
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
json.NewEncoder(w).Encode(data)
|
|
|
|
}
|
|
|
|
|
2024-09-02 21:04:13 +00:00
|
|
|
// jvbHandler handles the /jvb endpoint
|
|
|
|
func jvbHandler(config Config, w http.ResponseWriter, r *http.Request) {
|
|
|
|
data := JVBData {
|
|
|
|
JVBState: getServiceState("jitsi-videobridge2"),
|
|
|
|
JVBAPIData: getJitsiAPIData("jitsi-videobridge2", config.JVBStatsURL),
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
json.NewEncoder(w).Encode(data)
|
|
|
|
}
|
|
|
|
|
2024-09-03 08:55:13 +00:00
|
|
|
// jibriHandler handles the /jibri endpoint
|
|
|
|
func jibriHandler(config Config, w http.ResponseWriter, r *http.Request) {
|
|
|
|
data := JibriData {
|
|
|
|
JibriState: getServiceState("jibri"),
|
|
|
|
JibriHealthData: getJitsiAPIData("jibri", config.JibriHealthURL),
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
json.NewEncoder(w).Encode(data)
|
|
|
|
}
|
|
|
|
|
2024-09-02 19:34:38 +00:00
|
|
|
|
2024-10-02 13:40:57 +00:00
|
|
|
// CORS Middleware to handle CORS for all endpoints
|
|
|
|
func corsMiddleware(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// Set CORS headers
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") // Allow all origins or restrict to specific domain
|
|
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
|
|
|
|
|
|
// Handle preflight (OPTIONS) request
|
|
|
|
if r.Method == http.MethodOptions {
|
|
|
|
w.WriteHeader(http.StatusNoContent) // Respond with 204 No Content for preflight
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pass the request to the next handler
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-09-02 18:43:49 +00:00
|
|
|
// main sets up the http server and the routes
|
|
|
|
func main() {
|
2024-09-02 19:28:45 +00:00
|
|
|
|
2024-10-07 08:51:36 +00:00
|
|
|
// Define a flag for the config file
|
|
|
|
configFile := flag.String("c", "./jilo-agent.conf", "Specify the agent config file")
|
|
|
|
|
|
|
|
// Parse the flags
|
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
// Check if the file exists, fallback to default config file if not
|
|
|
|
if _, err := os.Stat(*configFile); os.IsNotExist(err) {
|
|
|
|
fmt.Println("Config file not found, using default values")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load the configuration from the specified file (option -c) or the default config file name
|
|
|
|
config := loadConfig(*configFile)
|
|
|
|
|
2024-09-03 10:57:17 +00:00
|
|
|
secretKey = []byte(config.SecretKey)
|
2024-09-02 19:28:45 +00:00
|
|
|
|
2024-10-02 13:40:57 +00:00
|
|
|
mux := http.NewServeMux()
|
|
|
|
|
2024-09-02 19:28:45 +00:00
|
|
|
// endpoints
|
2024-10-02 13:40:57 +00:00
|
|
|
mux.Handle("/nginx", authenticationJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2024-09-02 19:28:45 +00:00
|
|
|
nginxHandler(config, w, r)
|
2024-09-03 10:57:17 +00:00
|
|
|
})))
|
2024-10-02 13:40:57 +00:00
|
|
|
mux.Handle("/prosody", authenticationJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2024-09-02 21:04:13 +00:00
|
|
|
prosodyHandler(config, w, r)
|
2024-09-03 10:57:17 +00:00
|
|
|
})))
|
2024-10-02 13:40:57 +00:00
|
|
|
mux.Handle("/jicofo", authenticationJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2024-09-02 19:34:38 +00:00
|
|
|
jicofoHandler(config, w, r)
|
2024-09-03 10:57:17 +00:00
|
|
|
})))
|
2024-10-02 13:40:57 +00:00
|
|
|
mux.Handle("/jvb", authenticationJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2024-09-02 21:04:13 +00:00
|
|
|
jvbHandler(config, w, r)
|
2024-09-03 10:57:17 +00:00
|
|
|
})))
|
2024-10-02 13:40:57 +00:00
|
|
|
mux.Handle("/jibri", authenticationJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2024-09-03 08:55:13 +00:00
|
|
|
jibriHandler(config, w, r)
|
2024-09-03 10:57:17 +00:00
|
|
|
})))
|
2024-09-02 19:28:45 +00:00
|
|
|
|
2024-10-02 13:40:57 +00:00
|
|
|
// add the CORS headers to the mux
|
|
|
|
corsHandler := corsMiddleware(mux)
|
|
|
|
|
2024-09-02 19:28:45 +00:00
|
|
|
// start the http server
|
|
|
|
agentPortStr := fmt.Sprintf(":%d", config.AgentPort)
|
2024-09-03 08:55:13 +00:00
|
|
|
fmt.Printf("Starting Jilo agent on port %d.\n", config.AgentPort)
|
2024-10-02 15:54:52 +00:00
|
|
|
// if err := http.ListenAndServe(agentPortStr, corsHandler); err != nil {
|
|
|
|
if err := http.ListenAndServeTLS(agentPortStr, config.SSLcert, config.SSLkey, corsHandler); err != nil {
|
2024-09-03 08:55:13 +00:00
|
|
|
log.Fatalf("Could not start the agent: %v\n", err)
|
2024-09-02 18:43:49 +00:00
|
|
|
}
|
|
|
|
}
|