M4 Execute External Script
About
Execute external script feature allows to execute an external code before M4 sends the call to the Termination Point.
This is advanced feature, please do not use this it if you are not sure what you are doing! Incorrect usage may cause system instability.
Configuration
To execute external script, in Connection Point Advanced settings enter path to the external script (either HTTP URL or direct path to local script).
HTTP URL
M4 sends HTTP request to this URL. This is the preferred option. Even if the external script is located locally, it is recommended to connect to it via HTTP as performance is much better compared to executing local script directly.
HTTP URL must start with http:// or https://, for example:
http://127.0.0.1:8833
HTTP POST request to this URL is sent with JSON body containing variables from M4 (more in Variables sent by M4 section).
M4 expects to receive response with JSON body (more in Variables received by M4 section).
Local script path
In the case of very light usage, an external script can be executed directly by providing path to local script, for example:
/root/scripts/external_script
Data from M4 is passed to this script as single-line JSON string to STDIN.
M4 expects to receive only single-line JSON response to STDOUT.
There is no timeout, when executing local script directly.
Global configuration
Global external script settings can be defined in /etc/m2/system.conf in Radius (core) server:
- external_script_timeout_ms - timeout for external script execution (in milliseconds). Default value 900 ms.
- external_script_reject_on_timeout - if value is set to 1, then the call will be rejected (with HGC 384) on external script execution timeout. Default value 0 (do not reject).
When settings are changed, core reload is required:
m2 reload
Variables sent by M4
The following variables related to current call are sent to external script in JSON format.
- cp_id - ID of Connection Point. Value is integer.
- user_id - ID of User. Value is integer.
- src - localized source number. Value is string.
- dst - localized destination number. Value is string.
For example:
{"cp_id":2,"user_id":2,"src":"37000000000","dst":"37012345678"}
For HTTP external scripts, JSON is sent in the body of POST method. For local scripts, JSON is sent to STDIN.
Variables received by M4
M4 expects the following variables in JSON format.
- status_code - status code to indicate if script execution was successful. This variable is only for information purposes. Value is integer.
- message - short message to explain returned code. This variable is only for information purposes. Value is string.
- reject_call - if the value is set to 1, then M4 will reject the call with HGC 384. Any other value is ignored. Value is integer.
- new_src - sets a new source number. Value is string.
- src_prefix - adds prefix to source number. Value is string.
- src_suffix - adds suffix to source number. Value is string.
- new_dst - sets a new destination number. Value is string.
- dst_prefix - adds prefix to destination number. Value is string.
- dst_suffix - adds suffix to destination number. Value is string.
For example:
{"status_code":0,"message":"success","new_src":"37011111111"}
This response indicates that external script execution was successful and sets a new source number when dialing to Termination Point.
For HTTP external scripts, JSON must be sent in the response body. For local scripts, single-line JSON string must be sent to STDOUT (only JSON and nothing else should be sent to STDOUT).
All variables are optional.
Logs
Log /var/log/radius/radius.log can provide some information about external script execution:
... [NOTICE] Executing external script: http://127.0.0.1:8833 [DEBUG] External script: received output from HTTP script: {"message":"success","new_src":"37011111111"} [NOTICE] External script executed in 0.135276 seconds [DEBUG] Status code: 0 [DEBUG] Message: success [DEBUG] New src: 37011111111 [NOTICE] External script: changing src from [37000000000] to [37011111111] ...
Example script
As an example, let's an create external script in Go (golang) programming language.
This script will execute API request to some external service to get source number.
External HTTP script
The preferred way is to connect to external script via HTTP protocol. This example will demonstrate how to create Go script that listens for HTTP request on local network (127.0.0.1:8833) and implements business logic - retrieve new source number from API service provider.
The flow of execution will be:
M4 <-- HTTP --> External script (running locally) <-- HTTP --> API Service provider
During the call (but before it is sent to the Termination Point), M4 core will execute HTTP request to external script, which is running locally and listening for HTTP request on specific port. External script then executes its own HTTP request to some API service provider and gets data from it. Once the response is received, external script handles the response and constructs a response to M4 core. This way, we can implement a middleware between M4 and API service provider to handle API construction and API response.
TODO: how to download golang and create project
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"
)
// Request structure that M4 sends to this script
type Request struct {
ConnectionPointID int `json:"cp_id"` // Connection Point ID
UserID int `json:"user_id"` // User ID
Src string `json:"src"` // Source number
Dst string `json:"dst"` // Destination number
}
// Response structure that M4 expects to receive from this script
type Response struct {
StatusCode int `json:"status_code,omitempty"`
Message string `json:"message,omitempty"`
RejectCall int `json:"reject_call,omitempty"`
NewSrc string `json:"new_src,omitempty"`
SrcPrefix string `json:"src_prefix,omitempty"`
SrcSuffix string `json:"src_suffix,omitempty"`
NewDst string `json:"new_dst,omitempty"`
DstPrefix string `json:"dst_prefix,omitempty"`
DstSuffix string `json:"dst_suffix,omitempty"`
}
func main() {
// Set up logging to a file
logFile, err := os.OpenFile("/tmp/external_m4_script.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("Failed to open log file: %v", err)
}
// Ensure the log file is closed when main exits
defer logFile.Close()
// Set the output of the standard logger to the file
log.SetOutput(logFile)
// And set flags like Ldate | Ltime for more info
log.SetFlags(log.Ldate | log.Ltime)
// Register the handler function
http.HandleFunc("/", requestHandler)
// Define the address and port to listen on
addr := "127.0.0.1:8833"
log.Printf("Starting server on %s...", addr)
// Start the HTTP server
err = http.ListenAndServe(addr, nil)
if err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}
// Handle incoming requests from M4
func requestHandler(w http.ResponseWriter, r *http.Request) {
// Create a request variable to hold received data from M4
var requestData Request
// Create a response variable to send data back to M4
var responseData Response
// Get request data from M4 (in JSON format)
if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
defer r.Body.Close()
// Print request data
log.Printf("-- Received request from M4 --\n")
log.Printf("User ID: %d\n", requestData.UserID)
log.Printf("Connection Point ID: %d\n", requestData.ConnectionPointID)
log.Printf("Source number: %s\n", requestData.Src)
log.Printf("Destination number: %s\n", requestData.Dst)
// Defer will execute a function on return (in this case, HTTP response)
defer func() {
json.NewEncoder(w).Encode(responseData)
}()
// Set a response header JSON (this is the format M4 expects)
w.Header().Set("Content-Type", "application/json")
// Set default values for response
// In case of an error, we will return these values
// On success, we will overwrite these values with proper ones
responseData.StatusCode = 1
responseData.RejectCall = 1
// Implement your business logic here
// ...
// ...
// ...
// As an example, let's send an API request to a dummy service to get JSON response with new source number
apiURL := fmt.Sprintf("https://dummyjson.com/users/%d", requestData.UserID)
// Create HTTP client with a timeout of 1 second (we don't want to force SIP to wait too long)
client := &http.Client{
Timeout: 1 * time.Second,
}
// Execute API query
respAPI, err := client.Get(apiURL)
if err != nil {
// Handle timeout
if urlErr, ok := err.(*url.Error); ok {
if urlErr.Timeout() {
responseData.Message = "timeout"
return
}
}
responseData.Message = "http request error"
return
}
defer respAPI.Body.Close()
// Check response status code
if respAPI.StatusCode != http.StatusOK {
responseData.Message = "status code failed"
return
}
// Read the response body
body, err := io.ReadAll(respAPI.Body)
if err != nil {
responseData.Message = "read error"
return
}
// Define a struct to hold the API response
// Dummy JSON response contains many fields, but we only need the "phone" field
type DataAPI struct {
Phone string `json:"phone"`
}
// Variable to store JSON response
var dataAPI DataAPI
// Unmarshal the JSON response into variable
if err := json.Unmarshal(body, &dataAPI); err != nil {
responseData.Message = "json error"
return
}
// Log the API response
log.Printf("API response: %+v\n", dataAPI)
// Check if we got a phone number from the API
if dataAPI.Phone == "" {
responseData.Message = "not found"
return
}
// If we reached this point, it means we successfully got a phone number from the API
// Overwrite status code and message with success values
responseData.StatusCode = 0
responseData.Message = "success"
responseData.RejectCall = 0
// Set new source number (and filter out spaces and dashes from the number)
responseData.NewSrc = strings.NewReplacer(" ", "", "-", "").Replace(dataAPI.Phone)
}
Local script
WIP
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"
)
// Request structure that M4 sends to this script
type Request struct {
ConnectionPointID int `json:"cp_id"` // Connection Point ID
UserID int `json:"user_id"` // User ID
Src string `json:"src"` // Source number
Dst string `json:"dst"` // Destination number
}
// Response structure that M4 expects to receive from this script
type Response struct {
StatusCode int `json:"status_code,omitempty"`
Message string `json:"message,omitempty"`
RejectCall int `json:"reject_call,omitempty"`
NewSrc string `json:"new_src,omitempty"`
SrcPrefix string `json:"src_prefix,omitempty"`
SrcSuffix string `json:"src_suffix,omitempty"`
NewDst string `json:"new_dst,omitempty"`
DstPrefix string `json:"dst_prefix,omitempty"`
DstSuffix string `json:"dst_suffix,omitempty"`
}
func main() {
// Set up logging to a file
logFile, err := os.OpenFile("/tmp/external_m4_script.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("Failed to open log file: %v", err)
}
// Ensure the log file is closed when main exits
defer logFile.Close()
// Set the output of the standard logger to the file
log.SetOutput(logFile)
// And set flags like Ldate | Ltime | Lshortfile for more info
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
// Check if we got JSON string as an argument
if len(os.Args) != 2 {
log.Printf("Script expects JSON string as an argument\n")
return
}
// Unmarshal the JSON string into a Request struct
var requestData Request
if err := json.Unmarshal([]byte(os.Args[1]), &requestData); err != nil {
log.Printf("Failed to parse JSON: %v\n", err)
return
}
// Print the extracted arguments
log.Printf("-- Received request from M4 --\n")
log.Printf("Connection Point ID: %d\n", requestData.ConnectionPointID)
log.Printf("User ID: %d\n", requestData.UserID)
log.Printf("Source: %s\n", requestData.Src)
log.Printf("Destination: %s\n", requestData.Dst)
// Create a response variable to send data back to M4
var responseData Response
// Implement your business logic here
// ...
// ...
// ...
// As an example, let's send an API request to a dummy service to get JSON response
apiURL := fmt.Sprintf("https://dummyjson.com/users/%d", requestData.UserID)
// Create HTTP client with a timeout of 1 second (we don't want to force SIP to wait too long)
client := &http.Client{
Timeout: 1 * time.Second,
}
// Execute API query
respAPI, err := client.Get(apiURL)
if err != nil {
// Handle timeout
if urlErr, ok := err.(*url.Error); ok {
if urlErr.Timeout() {
responseData.Message = "timeout"
return
}
}
responseData.Message = "http request error"
return
}
defer respAPI.Body.Close()
// Check response status code
if respAPI.StatusCode != http.StatusOK {
responseData.Message = "status code failed"
return
}
// Read the response body
body, err := io.ReadAll(respAPI.Body)
if err != nil {
responseData.Message = "read error"
return
}
// Define a struct to hold the API response
// Dummy JSON response contains many fields, but we only need the "phone" field
type DataAPI struct {
Phone string `json:"phone"`
}
// Variable to store JSON response
var dataAPI DataAPI
// Unmarshal the JSON response into variable
if err := json.Unmarshal(body, &dataAPI); err != nil {
responseData.Message = "json error"
return
}
// Log the API response
log.Printf("API response: %+v\n", dataAPI)
// Check if we got a phone number from the API
if dataAPI.Phone == "" {
responseData.Message = "not found"
return
}
// If we reached this point, it means we successfully got a phone number from the API
// Overwrite status code and message with success values
responseData.StatusCode = 0
responseData.Message = "success"
responseData.RejectCall = 0
// Set new source number (and filter out spaces and dashes from the number)
responseData.NewSrc = strings.NewReplacer(" ", "", "-", "").Replace(dataAPI.Phone)
// Print JSON response using JSON encoder
json.NewEncoder(os.Stdout).Encode(responseData)
}