SIPREC module
Admin Guide
Section titled “Admin Guide”Overview
Section titled “Overview”This module provides the means to do calls recording using an external recorder - the entity that records the call is not in the media path between the caller and callee, but it is completely separate, thus it can not affect by any means the quality of the conversation. This is done in a standardized manner, using the SIPREC Protocol, thus it can be used by any recorder that implements this protocol.
Since an external server is used to record calls, there are no constraints regarding the location of the recorder, thus it can be placed arbitrary. This offers huge flexibility to your architecture configuration and various means for scaling.
The work for this module has been sponsored by the OrecX Company. This module is fully integrated with the OrecX Call Recording products.
How it works
Section titled “How it works”The full architecture of a SIP Media Recording platform is documented in RFC 7245. According to this architecture, this OpenSIPS module implements a SRC (Session Recording Client) that instructs a SRS (Session Recording Server) when new calls are started, the participants of the calls and their profiles. Based on this data, the SRS can decide whether the call should be recorded or not.
From SIP signalling perspective, the module does not change the call flow between the caller and callee. The call is established just as any other calls that are not recorded. But for each call that has SIPREC engaged, a completely separate SIP session is started by the SRC (OpenSIPS) towards the SRS, using the OpenSIPS Back-2-Back module. The INVITE message sent to the SRS contains a multi-part body consisting of two parts:
- Recording SDP - the SDP of the Media Server that will fork the RTP to the recorder.
- Participants Metadata - an XML-formatted document that contains information about the participants. The structure of the document is detailed in RFC 7865.
The SRS can respond with negative reply, indicating that the session does not need to be recorded, or with a positive reply (200 OK), indicating in the SDP body where the media RTP should be sent/forked. When the call ends, the SRC must send a BYE message to the SRS, indicating that the recording should be completed.
Full examples of call flows can be found in RFC 8068.
Media Handling
Section titled “Media Handling”Since OpenSIPS is a SIP Proxy, it does not have any Media Capabilities by itself. Thus we need to rely on a different Media Server to capture the RTP traffic and fork it to the SRS. The current implementation supports both the RTPProxy (through the RTPProxy module) and RTPEngine (through the RTEngine module) Media Servers.
SRS Failover
Section titled “SRS Failover”The siprec module supports failover between multiple SRS servers - when calling the siprec start recording function, one can provision multiple SRS URIs, separated by comma. In this case, OpenSIPS will try to use them in the same order specified, one by one, until either one of them responds with a positive reply (200 OK), or the response code is one of the codes matched by the skip failover codes regular expression. In the latter case the call is not recorded at all.
Limitations
Section titled “Limitations”This module only implements the SRC specifications of the SIPREC RFC. In order to have a full recording solution, you will also need a SRS solution such as Oreka - an open-source project provided by OrecX.
Although this module provides all the necessary tools to do calls recording, it does not fully implement the entire SIPREC SRC specifications. This list contains some of the module’s limitations:
- There is no Recording Indicator played to the callee - since OpenSIPS continues to act as a proxy, there is no way for us to postpone the media between the caller and callee to play a Recording Indicator message.
- Cannot handle Recording Sessions initiated by SRS - we do not support the scenario when an SRS suddenly decides to record a call in the middle of the dialog.
- OpenSIPS cannot be “queried” for ongoing recording sessions - this is scheduled to be implemented in further releases.
Dependencies
Section titled “Dependencies”OpenSIPS Modules
Section titled “OpenSIPS Modules”The following modules must be loaded before this module:
- TM - Transaction module.
- Dialog - Dialog module for keeping track of the call.
- RTP_Relay - RTP Relay module used for controlling the Media Servers that will fork the media.
- B2B_ENTITIES - Back-2-Back module used for communicating with the SRS.
External Libraries or Applications
Section titled “External Libraries or Applications”The following libraries or applications must be installed before running OpenSIPS with this module loaded:
- None.
Exported Parameters
Section titled “Exported Parameters”skip_failover_codes (string)
Section titled “skip_failover_codes (string)”A regular expression used to specify the codes that should prevent the module from failing over to a new SRS server.
By default any negative reply generates a failover.
...# do not failover on 408 reply codesmodparam("siprec", "skip_failover_codes", "408")
# do not failover on 408 or 487 reply codesmodparam("siprec", "skip_failover_codes", "408|487")
# do not failover on any 3xx or 4xx reply codemodparam("siprec", "skip_failover_codes", "[34][0-9][0-9]")...Exported Events
Section titled “Exported Events”E_SIPREC_START
Section titled “E_SIPREC_START”This event is raised when a SIPREC call is established and a call starts to be recorded.
Parameters:
- dlg_id - dialog id (“did”) of the call being recorded;
- dlg_callid - Call-Id of the call being recorded;
- callid - Call-Id (B2B id) of the SIPREC call;
- session_id - SIPREC UUID of the recording call;
- server - the SIPREC server handing this call;
- instance - the SIPREC instance this event is triggered for;
E_SIPREC_STOP
Section titled “E_SIPREC_STOP”This event is raised when a SIPREC call is terminated.
This event exposes the same parameters as the E SIPREC START event.
Exported Functions
Section titled “Exported Functions”siprec_start_recording(srs[, instance])
Section titled “siprec_start_recording(srs[, instance])”Calling this function on an initial INVITE engages call recording to SRS(s) for that call. Note that it does not necessary mean that the call will be recorded - it just means that OpenSIPS will query instruct the SRS that a new call has started, but the SRS might decide that the recording is disabled for those participants.
[!Note] If you only want to start recording when the call is established (200 OK is received), then you should call this function in the onreply route processing that 200 OK.
Parameters:
- srs (string) - a comma-separated list of SRS URIs. These URIs are used in the order specified. See siprec srs failover for more information.
- instance (string, optional) - used to start a particular SIPREC instance. When missing, the default instance is started.
The function returns false when an internal error is triggered and the call recording setup fails. Otherwise, if all the internal mechanisms are activated, it returns true.
This function can be used from REQUEST_ROUTE.
... if (!has_totag() && is_method("INVITE")) { $var(srs) = "sip:127.0.0.1"; xlog("Engage SIPREC call recording to $var(srs) for $ci\n"); siprec_start_recording($var(srs)); } ... ... if (!has_totag() && is_method("INVITE")) { $var(srs) = "sip:127.0.0.1, sip:127.0.0.1;transport=TCP"; xlog("Engage SIPREC call recording to servers $var(srs) for $ci in inbound group\n"); siprec_start_recording($var(srs), "inbound"); } ... ... $xml(caller_xml) = "<nameID></nameID>"; $xml(caller_xml/nameID.attr/aor) = "sip:6024151234@10.0.0.11:5090"; $xml(caller_xml/nameID) = "<name>test</name>"; $siprec(caller) = $xml(caller_xml/nameID); siprec_start_recording($var(srs)); ... ... $siprec(headers) = "X-MY-CUSTOM_HDR: 1\r\n"; siprec_start_recording($var(srs)); ... ... $var(temp) = "siprec_pause_recording([instance])
Section titled “siprec_pause_recording([instance])”Pauses the recording for the ongoing call. Should be called after the dialog has matched.
Parameters:
- instance (string, optional) - used to pause a particular SIPREC instance. When missing, the default instance is paused.
This function can be used from any route.
... if (has_totag() && is_method("INVITE")) { if (is_audio_on_hold()) siprec_pause_recording(); } ...siprec_resume_recording([instance])
Section titled “siprec_resume_recording([instance])”Resumes the recording for the ongoing call. Should be called after the dialog has matched.
Parameters:
- instance (string, optional) - used to resume a particular SIPREC instance. When missing, the default instance is resumed.
This function can be used from any route.
... if (has_totag() && is_method("INVITE")) { if (!is_audio_on_hold()) siprec_resume_recording(); } ...siprec_stop_recording([instance])
Section titled “siprec_stop_recording([instance])”Stops the recording for the ongoing call. Should be called for SIPREC sessions that have been previously started.
Parameters:
- instance (string, optional) - used to stop a particular SIPREC instance. When missing, the default instance is stopped.
This function can be used from any route.
... if (has_totag() && is_method("INVITE")) { if (is_audio_on_hold()) siprec_stop_recording(); } ...siprec_send_indialog([hdrs[, body]])
Section titled “siprec_send_indialog([hdrs[, body]])”Sends an arbitrary in-dialog request to the SRS.
This function can be used from any route.
Parameters:
- headers (string, optional) - a set of headers that will be added to the generated request.
- body (string, optional) - the body that will be added to the generated request.
- instance (string, optional) - used to send a request within a particular SIPREC instance. When missing, the request is sent in to the default instance.
... if (has_totag() && is_method("INFO")) { siprec_send_indialog("Content-Type: $hdr(Content-Type)\r\n", $rb); } ...Exported Pseudo-Variables
Section titled “Exported Pseudo-Variables”$siprec
Section titled “$siprec”Used to modify/describe different siprec sessions parameters that should be taken into account by the siprec start recording function.
The variable can be indexed with the instance the user wants to tune the variable for. If missing, the the default instance is being altered.
The context of this variable is only limited to the current message processed - it is not available at the transaction or dialog level.
Any of this setting is optional.
Settings that can be provisioned:
- group - an opaque value that will be inserted in the SIPREC body and represents the name of the group that can be used to classify calls in certain profiles. If missing, no group is added.
- caller - an XML block containing information about the caller. If absent, the From header of the initial dialog is used to build the value.
- callee - an XML block containing information about the callee. If absent, the To header of the initial dialog is used to build the value.
- media - the IP that RTPProxy will be streaming media from. If absent 127.0.0.1 will be used.
- headers - extra headers that are to be added in the initial request towards the SRS.
- socket - listening socket that the outgoing request towards SRS should be used.
- from_uri - the URI to appear in the From header of the dialog. Default value is the request URI.
- to_uri - the URI to appear in the To header of the dialog. Default value is the request URI. Note that this does not influence the callee information in the XML block, which is taken from the initial dialog.
- group_custom_extension - an optional XML block containing custom information to be added under the group tag.
- session_custom_extension - an optional XML block containing custom information to be added under the session tag.
Samples
Section titled “Samples”OpenSIPS Config Script - SIPREC Usage
Section titled “OpenSIPS Config Script - SIPREC Usage”## OpenSIPS residential configuration script# by OpenSIPS Solutions <team@opensips-solutions.com>## This is a basic residential configuration. Ready-to-use M4 templates are# available under examples/templates/.## Please refer to the OpenSIPS Manuals at:# https://opensips.org/Documentation/Manuals# for an explanation of available statements, functions and parameters.#
####### Global Parameters #########
/* uncomment the following lines to enable debugging */#debug_mode=yes
log_level=3xlog_level=3stderror_enabled=nosyslog_enabled=yessyslog_facility=LOG_LOCAL0
udp_workers=4
/* uncomment the next line to enable the auto temporary blacklisting of not available destinations (default disabled) */#disable_dns_blacklist=no
/* uncomment the next line to enable IPv6 lookup after IPv4 dns lookup failures (default disabled) */#dns_try_ipv6=yes
socket=udp:127.0.0.1:5060 # CUSTOMIZE ME
####### Modules Section ########
#set module pathmpath="/usr/local/lib/opensips/modules/"
#### SIGNALING moduleloadmodule "signaling.so"
#### StateLess moduleloadmodule "sl.so"
#### Transaction Moduleloadmodule "tm.so"modparam("tm", "fr_timeout", 5)modparam("tm", "fr_inv_timeout", 30)modparam("tm", "restart_fr_on_each_reply", 0)modparam("tm", "onreply_avp_mode", 1)
#### Record Route Moduleloadmodule "rr.so"/* do not append from tag to the RR (no need for this script) */modparam("rr", "append_fromtag", 0)
#### MAX ForWarD moduleloadmodule "maxfwd.so"
#### SIP MSG OPerationS moduleloadmodule "sipmsgops.so"
#### FIFO Management Interfaceloadmodule "mi_fifo.so"modparam("mi_fifo", "fifo_name", "/run/opensips/opensips_fifo")modparam("mi_fifo", "fifo_mode", 0666)
#### USeR LOCation moduleloadmodule "usrloc.so"modparam("usrloc", "nat_bflag", "NAT")modparam("usrloc", "working_mode_preset", "single-instance-no-db")
#### REGISTRAR moduleloadmodule "registrar.so"modparam("registrar", "tcp_persistent_flag", "TCP_PERSISTENT")/* uncomment the next line not to allow more than 10 contacts per AOR */#modparam("registrar", "max_contacts", 10)
#### ACCounting moduleloadmodule "acc.so"/* what special events should be accounted ? */modparam("acc", "early_media", 0)modparam("acc", "report_cancels", 0)/* by default we do not adjust the direct of the sequential requests. if you enable this parameter, be sure to enable "append_fromtag" in "rr" module */modparam("acc", "detect_direction", 0)
loadmodule "proto_udp.so"
loadmodule "dialog.so"loadmodule "b2b_entities.so"loadmodule "siprec.so"loadmodule "rtp_relay.so"loadmodule "rtpproxy.so"
modparam("rtpproxy", "rtpproxy_sock", "udp:127.0.0.1:7899")
####### Routing Logic ########
# main request routing logic
route{
if (!mf_process_maxfwd_header(10)) { send_reply(483,"Too Many Hops"); exit; }
if (has_totag()) {
# handle hop-by-hop ACK (no routing required) if ( is_method("ACK") && t_check_trans() ) { t_relay(); exit; }
# sequential request within a dialog should # take the path determined by record-routing if ( !loose_route() ) { # we do record-routing for all our traffic, so we should not # receive any sequential requests without Route hdr. send_reply(404,"Not here"); exit; }
if (is_method("BYE")) { # do accounting even if the transaction fails do_accounting("log","failed"); }
# route it out to whatever destination was set by loose_route() # in $du (destination URI). route(relay); exit; }
# CANCEL processing if (is_method("CANCEL")) { if (t_check_trans()) t_relay(); exit; }
# absorb retransmissions, but do not create transaction t_check_trans();
if ( !(is_method("REGISTER") ) ) {
if (is_myself("$fd")) {
} else { # if caller is not local, then called number must be local
if (!is_myself("$rd")) { send_reply(403,"Relay Forbidden"); exit; } }
}
# preloaded route checking if (loose_route()) { xlog("L_ERR", "Attempt to route with preloaded Route's [$fu/$tu/$ru/$ci]"); if (!is_method("ACK")) send_reply(403,"Preload Route denied"); exit; }
# record routing if (!is_method("REGISTER|MESSAGE")) record_route();
# account only INVITEs if (is_method("INVITE")) { create_dialog();
$rtp_relay = "co"; $rtp_relay_peer = "co"; rtp_relay_engage("rtpproxy"); siprec_start_recording("sip:127.0.0.1:5090");
do_accounting("log"); }
if (!is_myself("$rd")) { append_hf("P-hint: outbound\r\n");
route(relay); }
# requests for my domain
if (is_method("PUBLISH|SUBSCRIBE")) { send_reply(503, "Service Unavailable"); exit; }
if (is_method("REGISTER")) { # store the registration and generate a SIP reply if (!save("location")) xlog("failed to register AoR $tu\n");
exit; }
if ($rU==NULL) { # request with no Username in RURI send_reply(484,"Address Incomplete"); exit; }
# do lookup with method filtering if (!lookup("location","method-filtering")) { t_reply(404, "Not Found"); exit; }
# when routing via usrloc, log the missed calls also do_accounting("log","missed"); route(relay);}
route[relay] { # for INVITEs enable some additional helper routes if (is_method("INVITE")) { t_on_branch("per_branch_ops"); t_on_reply("handle_nat"); t_on_failure("missed_call"); }
if (!t_relay()) { send_reply(500,"Internal Error"); } exit;}
branch_route[per_branch_ops] { xlog("new branch at $ru\n");}
onreply_route[handle_nat] { xlog("incoming reply\n");}
failure_route[missed_call] { if (t_was_cancelled()) { exit; }
# uncomment the following lines if you want to block client # redirect based on 3xx replies. ##if (t_check_status("3[0-9][0-9]")) { ##t_reply(404,"Not found"); ## exit; ##}
}OpenSIPS Config Script - SIPREC With Dispatcher SRS Selection
Section titled “OpenSIPS Config Script - SIPREC With Dispatcher SRS Selection”## OpenSIPS SIPREC sample using dispatcher for SRS selection.## The SIPREC module is engaged with a local placeholder SRS URI:## sip:srs@siprec-dispatcher.invalid## The locally generated SIPREC INVITE is then caught in local_route,# where dispatcher selects the real SRS destination. Dispatcher set 1# is expected to contain the SIPREC Recording Servers, for example:## setid destination# 1 sip:127.0.0.1:5090# 1 sip:127.0.0.1:5091#
####### Global Parameters #########
#debug_mode=yes
log_level=3xlog_level=3stderror_enabled=nosyslog_enabled=yessyslog_facility=LOG_LOCAL0
udp_workers=4
socket=udp:127.0.0.1:5060 # CUSTOMIZE ME
####### Modules Section ########
mpath="/usr/local/lib/opensips/modules/"
loadmodule "signaling.so"loadmodule "sl.so"
loadmodule "tm.so"modparam("tm", "fr_timeout", 5)modparam("tm", "fr_inv_timeout", 30)modparam("tm", "restart_fr_on_each_reply", 0)modparam("tm", "onreply_avp_mode", 1)
loadmodule "rr.so"modparam("rr", "append_fromtag", 0)
loadmodule "maxfwd.so"loadmodule "sipmsgops.so"
loadmodule "mi_fifo.so"modparam("mi_fifo", "fifo_name", "/run/opensips/opensips_fifo")modparam("mi_fifo", "fifo_mode", 0666)
loadmodule "usrloc.so"modparam("usrloc", "nat_bflag", "NAT")modparam("usrloc", "working_mode_preset", "single-instance-no-db")
loadmodule "registrar.so"modparam("registrar", "tcp_persistent_flag", "TCP_PERSISTENT")
loadmodule "acc.so"modparam("acc", "early_media", 0)modparam("acc", "report_cancels", 0)modparam("acc", "detect_direction", 0)
loadmodule "proto_udp.so"
loadmodule "dialog.so"loadmodule "b2b_entities.so"loadmodule "siprec.so"loadmodule "rtp_relay.so"loadmodule "rtpproxy.so"loadmodule "dispatcher.so"
modparam("rtpproxy", "rtpproxy_sock", "udp:127.0.0.1:7899")
# Dispatcher must be provisioned with the SRS pool in set 1.# Uncomment the DB module/db_url below and replace the URL with your DB.# loadmodule "db_mysql.so"# modparam("dispatcher", "db_url", "mysql://opensips:opensipsrw@localhost/opensips")modparam("dispatcher", "ds_ping_interval", 30)modparam("dispatcher", "ds_probing_mode", 1)
####### Routing Logic ########
route { if (!mf_process_maxfwd_header(10)) { send_reply(483, "Too Many Hops"); exit; }
if (has_totag()) { if (is_method("ACK") && t_check_trans()) { t_relay(); exit; }
if (!loose_route()) { send_reply(404, "Not here"); exit; }
if (is_method("BYE")) do_accounting("log", "failed");
route(relay); exit; }
if (is_method("CANCEL")) { if (t_check_trans()) t_relay(); exit; }
t_check_trans();
if (!is_method("REGISTER")) { if (!is_myself("$fd") && !is_myself("$rd")) { send_reply(403, "Relay Forbidden"); exit; } }
if (loose_route()) { xlog("L_ERR", "Attempt to route with preloaded Route's [$fu/$tu/$ru/$ci]"); if (!is_method("ACK")) send_reply(403, "Preload Route denied"); exit; }
if (!is_method("REGISTER|MESSAGE")) record_route();
if (is_method("INVITE")) { create_dialog();
$rtp_relay = "co"; $rtp_relay_peer = "co"; rtp_relay_engage("rtpproxy");
# Use a stable internal placeholder. local_route below will replace # its domain with a dispatcher-selected SIPREC Recording Server. siprec_start_recording("sip:srs@siprec-dispatcher.invalid");
do_accounting("log"); }
if (!is_myself("$rd")) { append_hf("P-hint: outbound\r\n"); route(relay); }
if (is_method("PUBLISH|SUBSCRIBE")) { send_reply(503, "Service Unavailable"); exit; }
if (is_method("REGISTER")) { if (!save("location")) xlog("failed to register AoR $tu\n");
exit; }
if ($rU == NULL) { send_reply(484, "Address Incomplete"); exit; }
if (!lookup("location", "method-filtering")) { t_reply(404, "Not Found"); exit; }
do_accounting("log", "missed"); route(relay);}
route[relay] { if (is_method("INVITE")) { t_on_branch("per_branch_ops"); t_on_reply("handle_nat"); t_on_failure("missed_call"); }
if (!t_relay()) send_reply(500, "Internal Error"); exit;}
local_route { # Only touch the SIPREC INVITE generated by siprec_start_recording(). if (!is_method("INVITE") || $ru != "sip:srs@siprec-dispatcher.invalid") exit;
# set=1 is the SRS dispatcher set; alg=4 hashes over the Call-ID. # The "f" flag saves remaining destinations for ds_next_domain(). if (!ds_select_domain(1, 4, "f")) { xlog("L_ERR", "No active SIPREC SRS destination for $ci\n"); drop; }
xlog("L_INFO", "Selected SIPREC SRS $ru for $ci\n"); t_on_failure("siprec_srs_failure");}
failure_route[siprec_srs_failure] { if (t_was_cancelled()) exit;
if (t_check_status("5[0-9][0-9]") || (t_check_status("408") && t_local_replied("all"))) { xlog("L_WARN", "SIPREC SRS $du failed with code $T_reply_code for $ci\n");
# Put the failing SRS into probing state, then try the next server # saved by ds_select_domain(..., "f"). ds_mark_dst("p");
if (ds_next_domain()) { xlog("L_INFO", "Trying next SIPREC SRS $ru for $ci\n"); t_on_failure("siprec_srs_failure"); t_relay(); exit; }
xlog("L_ERR", "No more SIPREC SRS destinations for $ci\n"); }}
branch_route[per_branch_ops] { xlog("new branch at $ru\n");}
onreply_route[handle_nat] { xlog("incoming reply\n");}
failure_route[missed_call] { if (t_was_cancelled()) exit;}Contributors
Section titled “Contributors”By Commit Statistics
Section titled “By Commit Statistics”Top contributors by DevScore(1), authored commits(2) and lines added/removed(3)
| # | Name | DevScore | Commits | Lines++ | Lines— |
|---|---|---|---|---|---|
| 1. | Razvan Crainea (@razvancrainea) | 212 | 107 | 7281 | 2569 |
| 2. | Vlad Patrascu (@rvlad-patrascu) | 16 | 11 | 192 | 175 |
| 3. | Stefan Darius (@dariusstefan) | 14 | 5 | 674 | 168 |
| 4. | Liviu Chircu (@liviuchircu) | 9 | 7 | 35 | 53 |
| 5. | Maksym Sobolyev (@sobomax) | 6 | 4 | 12 | 11 |
| 6. | Jupiter Tang | 5 | 3 | 14 | 4 |
| 7. | Bogdan-Andrei Iancu (@bogdan-iancu) | 5 | 3 | 10 | 9 |
| 8. | Seyed Mehran Siadati | 5 | 2 | 132 | 23 |
| 9. | Norman Brandinger (@NormB) | 3 | 1 | 4 | 4 |
(1) DevScore = author_commits + author_lines_added / (project_lines_added / project_commits) + author_lines_deleted / (project_lines_deleted / project_commits)
(2) including any documentation-related commits, excluding merge commits
(3) ignoring whitespace edits, renamed files and auto-generated files
By Commit Activity
Section titled “By Commit Activity”| # | Name | Commit Activity |
|---|---|---|
| 1. | Razvan Crainea (@razvancrainea) | Jun 2017 - Jul 2026 |
| 2. | Stefan Darius (@dariusstefan) | Jun 2026 - Jul 2026 |
| 3. | Bogdan-Andrei Iancu (@bogdan-iancu) | Apr 2019 - Apr 2026 |
| 4. | Jupiter Tang | Oct 2025 - Dec 2025 |
| 5. | Maksym Sobolyev (@sobomax) | Feb 2023 - Nov 2023 |
| 6. | Seyed Mehran Siadati | Nov 2023 - Nov 2023 |
| 7. | Liviu Chircu (@liviuchircu) | Apr 2018 - Aug 2023 |
| 8. | Vlad Patrascu (@rvlad-patrascu) | Feb 2018 - Mar 2023 |
| 9. | Norman Brandinger (@NormB) | Aug 2021 - Aug 2021 |
(1) including any documentation-related commits, excluding merge commits
Documentation
Section titled “Documentation”Contributors
Section titled “Contributors”Last edited by: Razvan Crainea (@razvancrainea), Seyed Mehran Siadati, Norman Brandinger (@NormB), Vlad Patrascu (@rvlad-patrascu), Liviu Chircu (@liviuchircu).
License
Section titled “License”All documentation files (i.e. .md extension) are licensed under the Creative Common License 4.0