Difference between revisions of "M4 Execute External Script"
(→Logs) |
|||
| Line 87: | Line 87: | ||
... | ... | ||
[NOTICE] Executing external script: http://127.0.0.1:8833 | [NOTICE] Executing external script: http://127.0.0.1:8833 | ||
[NOTICE] External script: received output from HTTP script: {"message":"success","new_src":"37011111111"} | |||
[NOTICE] External script executed in 0.135276 seconds | [NOTICE] External script executed in 0.135276 seconds | ||
[DEBUG] Message: success | [DEBUG] Message: success | ||
[DEBUG] New src: 37011111111 | [DEBUG] New src: 37011111111 | ||
Revision as of 06:43, 28 June 2025
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.
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 [NOTICE] External script: received output from HTTP script: {"message":"success","new_src":"37011111111"} [NOTICE] External script executed in 0.135276 seconds [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 an external script in Go (Golang) programming language.
This script will execute API request to some external service provider 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.
Here are the steps how to create this project:
1. Connect to Radius (core) server via ssh as root.
2. Download Go:
wget https://go.dev/dl/go1.24.0.linux-amd64.tar.gz -O /usr/src/go1.24.0.linux-amd64.tar.gz
3. Extract Go archive:
tar -C /usr/local -xzf /usr/src/go1.24.0.linux-amd64.tar.gz
4. Create directory for external script project:
mkdir -p /usr/local/m4_external_script
5. Switch to this directory:
cd /usr/local/m4_external_script
6. Initialize Go project:
/usr/local/go/bin/go mod init 127.0.0.1/m4_external_script
7. Create main source file:
touch main.go
8. Copy the source code bellow to main.go:
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("/usr/local/m4_external_script/m4_external_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 = "number 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)
}
9. Sort out dependencies:
/usr/local/go/bin/go mod tidy
10. Build script:
/usr/local/go/bin/go build
11. Create systemd service for this script to run it in background and start on system boot:
touch /etc/systemd/system/m4_external_script.service
12. Copy the following configuration to /etc/systemd/system/m4_external_script.service:
[Unit] Description=M4 external script Wants=network.target After=network.target [Service] Type=simple User=root Group=root WorkingDirectory=/usr/local/m4_external_script ExecStart=/usr/local/m4_external_script/m4_external_script ExecStop=/bin/killall m4_external_script Restart=always RestartSec=5 [Install] WantedBy=multi-user.target
13. Reload systemctl:
systemctl daemon-reload
14. Enable external script to start on system boot:
systemctl enable m4_external_script
15. Start external script (runs in background and listens for HTTP request on 127.0.0.1:8833):
systemctl start m4_external_script
16. Check if script is running:
systemctl status m4_external_script
You should see:
Active: active (running)
17. Check script log:
head /usr/local/m4/m4_external_script/m4_external_script.log
You should see something like:
2025/06/27 15:22:37 Starting server on 127.0.0.1:8833...
18. In GUI Connection Point Advanced settings, set URL to this script in Execute External Script option:
http://127.0.0.1:8833
19. Save Connection Point settings and try to make Call Tracing. In the Call Tracing output, you should see similar lines:
... 2025-06-28 06:01:57 [NOTICE] Executing external script: http://127.0.0.1:8833 2025-06-28 06:01:57 [NOTICE] External script: received output from HTTP: {`message`:`success`,`new_src`:`+492586276644`} 2025-06-28 06:01:57 [NOTICE] External script executed in 0.161371 seconds 2025-06-28 06:01:57 [NOTICE] External script: changing src from [37000000000] to [+492586276644] ...
20. Check script log at /usr/local/m4_external_script/m4_external_script.log, you should see:
2025/06/28 06:01:57 -- Received request from M4 --
2025/06/28 06:01:57 User ID: 2
2025/06/28 06:01:57 Connection Point ID: 2
2025/06/28 06:01:57 Source number: 37000000000
2025/06/28 06:01:57 Destination number: 37011111111
2025/06/28 06:01:57 API response: {Phone:+49 258-627-6644}
If all steps succeeded, you can start to modify main.go source file with your business logic and test it using Call Tracing until it is ready for live calls.
When main.go source is modified, you need to rebuild it and restart systemd service:
cd /usr/local/m4_external_script /usr/local/go/bin/go mod tidy /usr/local/go/bin/go build systemctl restart m4_external_script
Local script
Another way of executing external script is to call it directly by providing path to this script. This is less performant option and should be used only if there is no way to implement communication via HTTP. A lot of system resources may be used for creating a new process for each call. This can lead to system degradation.
The flow of execution will be:
M4 <-- system call --> External script <-- HTTP --> API Service provider
During the call (but before it is sent to the Termination Point), M4 core will execute external script directly by spawning child process. 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 (to STDOUT). This way, we can implement a middleware between M4 and API service provider to handle API construction and API response.
The steps to create this external script is similar to External HTTP script:
Follow steps 1-7 to prepare Go environment.
In step 8, copy the following code to main.go:
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("/usr/local/m4_external_script/m4_external_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)
// 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
// 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
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 = "number 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)
}
Follow steps 9 and 10 to build module.
Skip steps 11-17 as we don't need systemd service in this case.
In step 18, use the following path in Execute External Script option:
/usr/local/m4_external_script/m4_external_script
Follow steps 19 and 20.
If all steps succeeded, you can start to modify main.go source file with your business logic and test it using Call Tracing until it is ready for live calls.
When main.go source is modified, you need to rebuild it:
cd /usr/local/m4_external_script /usr/local/go/bin/go mod tidy /usr/local/go/bin/go build