Difference between revisions of "M4 Execute External Script"
| (25 intermediate revisions by the same user not shown) | |||
| Line 7: | Line 7: | ||
=Configuration= | =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). | To execute external script, in Connection Point Advanced settings enter path to the external script (either HTTP URL or direct path to local script) in '''Execute external script''' option. | ||
==HTTP URL== | ==HTTP URL== | ||
| Line 25: | Line 25: | ||
In the case of very light usage, an external script can be executed directly by providing path to local script, for example: | In the case of very light usage, an external script can be executed directly by providing path to local script, for example: | ||
/ | /usr/local/m4_external_script | ||
Data from M4 is passed to this script as single-line JSON string to STDIN. | Data from M4 is passed to this script as single-line JSON string to STDIN (more in [https://wiki.kolmisoft.com/index.php?title=M4_Execute_External_Script&action=submit#Variables_sent_by_M4 Variables sent by M4] section). | ||
M4 expects to receive '''only''' single-line JSON response to STDOUT. | M4 expects to receive '''only''' single-line JSON response to STDOUT (more in [https://wiki.kolmisoft.com/index.php?title=M4_Execute_External_Script&action=submit#Variables_received_by_M4 Variables received by M4] section). | ||
==Global configuration== | ==Global configuration== | ||
Global external script settings can be defined in /etc/m2/system.conf in Radius (core) server: | 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_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 | * '''external_script_reject_on_timeout''' - if value is set to 1, then the call will be rejected (with HGC 386) on external script execution timeout. Default value 0 (do not reject). | ||
* '''external_script_reject_on_failure''' - if value is set to 1, then the call will be rejected (with HGC 386) on external script execution failure (for example external script is not even running). Default value 0 (do not reject). | |||
<br/>Note: if timeout of more than 900ms is needed, then radius authentication timeout (default 1 second) should be increased as well. Edit '''/usr/local/etc/radiusclient/radiusclient.conf''', modify setting '''radius_auth_timeout'''. Kamailio restart is required. | |||
When settings are changed, core reload is required: | When settings are changed, core reload is required: | ||
| Line 53: | Line 56: | ||
For example: | For example: | ||
{"cp_id":2,"user_id": | {"cp_id":2,"user_id":4,"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. | For HTTP external scripts, JSON is sent in the body of POST method. For local scripts, JSON is sent to STDIN. | ||
| Line 63: | Line 66: | ||
* '''status_code''' - status code to indicate if script execution was successful. This variable is only for information purposes. Value is integer. | * '''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. | * '''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 | * '''reject_call''' - if the value is set to 1, then M4 will reject the call with HGC 386 . If executed on Termination Point, then the call will not be rejected, but Termination Point will be skipped. Any other value is ignored. Value is integer. | ||
* '''new_src''' - sets a new source number. Value is string. | * '''new_src''' - sets a new source number. Value is string. | ||
* '''src_prefix''' - adds prefix to source number. Value is string. | * '''src_prefix''' - adds prefix to source number. Value is string. | ||
| Line 75: | Line 78: | ||
{"status_code":0,"message":"success","new_src":"37011111111"} | {"status_code":0,"message":"success","new_src":"37011111111"} | ||
This response indicates that external script execution was successful and | This response indicates that external script execution was successful and tells to change source number. | ||
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). | 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). | ||
| Line 87: | Line 90: | ||
... | ... | ||
[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 | ||
| Line 99: | Line 101: | ||
As an example, let's an create an external script in Go (Golang) programming language. | 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 to get source number. | This script will execute API request to some external service provider to get source number. | ||
==External HTTP script== | ==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 | 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: | The flow of execution will be: | ||
| Line 111: | Line 115: | ||
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. | 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: | |||
<syntaxhighlight lang="go"> | <syntaxhighlight lang="go"> | ||
| Line 151: | Line 183: | ||
func main() { | func main() { | ||
// Set up logging to a file | // Set up logging to a file | ||
logFile, err := os.OpenFile("/ | 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 { | if err != nil { | ||
log.Fatalf("Failed to open log file: %v", err) | log.Fatalf("Failed to open log file: %v", err) | ||
| Line 161: | Line 193: | ||
log.SetOutput(logFile) | log.SetOutput(logFile) | ||
// And set flags like Ldate | Ltime for more info | // And set flags like Ldate | Ltime for more info | ||
log.SetFlags(log.Ldate | log.Ltime) | log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds) | ||
// Register the handler function | // Register the handler function | ||
| Line 274: | Line 306: | ||
// Check if we got a phone number from the API | // Check if we got a phone number from the API | ||
if dataAPI.Phone == "" { | if dataAPI.Phone == "" { | ||
responseData.Message = "not found" | responseData.Message = "number not found" | ||
return | return | ||
} | } | ||
| Line 288: | Line 320: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
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_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== | ==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 [https://wiki.kolmisoft.com/index.php/M4_Execute_External_Script#External_HTTP_script External HTTP script]: | |||
Follow steps '''1-7''' to prepare Go environment. | |||
In step '''8''', copy the following code to main.go: | |||
<syntaxhighlight lang="go"> | <syntaxhighlight lang="go"> | ||
| Line 331: | Line 466: | ||
func main() { | func main() { | ||
// Set up logging to a file | // Set up logging to a file | ||
logFile, err := os.OpenFile("/ | 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 { | if err != nil { | ||
log.Fatalf("Failed to open log file: %v", err) | log.Fatalf("Failed to open log file: %v", err) | ||
| Line 340: | Line 475: | ||
// Set the output of the standard logger to the file | // Set the output of the standard logger to the file | ||
log.SetOutput(logFile) | log.SetOutput(logFile) | ||
// And set flags like Ldate | Ltime | // And set flags like Ldate | Ltime for more info | ||
log.SetFlags(log.Ldate | log.Ltime | log. | log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds) | ||
// Check if we got JSON string as an argument | // Check if we got JSON string as an argument | ||
| Line 365: | Line 500: | ||
// Create a response variable to send data back to M4 | // Create a response variable to send data back to M4 | ||
var responseData Response | 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 | // Implement your business logic here | ||
| Line 428: | Line 569: | ||
// Check if we got a phone number from the API | // Check if we got a phone number from the API | ||
if dataAPI.Phone == "" { | if dataAPI.Phone == "" { | ||
responseData.Message = "not found" | responseData.Message = "number not found" | ||
return | return | ||
} | } | ||
| Line 445: | Line 586: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
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 | |||
Latest revision as of 15:13, 10 July 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) in Execute external script option.
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:
/usr/local/m4_external_script
Data from M4 is passed to this script as single-line JSON string to STDIN (more in Variables sent by M4 section).
M4 expects to receive only single-line JSON response to STDOUT (more in Variables received by M4 section).
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 386) on external script execution timeout. Default value 0 (do not reject).
- external_script_reject_on_failure - if value is set to 1, then the call will be rejected (with HGC 386) on external script execution failure (for example external script is not even running). Default value 0 (do not reject).
Note: if timeout of more than 900ms is needed, then radius authentication timeout (default 1 second) should be increased as well. Edit /usr/local/etc/radiusclient/radiusclient.conf, modify setting radius_auth_timeout. Kamailio restart is required.
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":4,"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 386 . If executed on Termination Point, then the call will not be rejected, but Termination Point will be skipped. 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 tells to change source number.
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 | log.Lmicroseconds)
// 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_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 | log.Lmicroseconds)
// 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