Merge branch 'call-recording-rebased' of https://github.com/onsip/rtpengine into onsip-call-recording-rebased

pull/247/head
Richard Fuchs 10 years ago
commit 8b9fd27f92

@ -192,6 +192,7 @@ option and which are reproduced below:
--homer=IP46:PORT Address of Homer server for RTCP stats
--homer-protocol=udp|tcp Transport protocol for Homer (default udp)
--homer-id=INT 'Capture ID' to use within the HEP protocol
--recording-dir=FILE Spool directory where PCAP call recording data goes
Most of these options are indeed optional, with two exceptions. It's mandatory to specify at least one local
IP address through `--interface`, and at least one of the `--listen-...` options must be given.
@ -411,7 +412,7 @@ The options are described in more detail below.
* -k, --subscribe-keyspace
List of redis keyspaces to subscribe. If this is not present, no keyspaces are subscribed (default behaviour).
List of redis keyspaces to subscribe. If this is not present, no keyspaces are subscribed (default behaviour).
Further subscriptions could be added/removed via 'rtpengine-ctl ksadd/ksrm'.
This may lead to enabling/disabling of the redis keyspace notification feature.
@ -461,6 +462,48 @@ The options are described in more detail below.
The HEP protocol used by Homer contains a "capture ID" used to distinguish different sources
of capture data. This ID can be specified using this argument.
* --recording-dir
An optional argument to specify a path to a directory where PCAP recording
files and recording metadata files should be stored. If not specified, the the
rtpengine will default to placing recorded files in `/var/spool/rtpengine/` if
it exists. PCAP files will be stored within a "pcap" subdirectory and metadata
within a "metadata" subdirectory.
The format for a metadata file is (with a trailing newline):
/path/to/recording-pcap.pcap
SDP mode: offer
SDP before RTP packet: 1
first SDP
SDP mode: answer
SDP before RTP packet: 1
second SDP
...
SDP mode: answer
SDP before RTP packet: 100
n-th and final SDP
start timestamp (YYYY-MM-DDThh:mm:ss)
end timestamp (YYYY-MM-DDThh:mm:ss)
generic metadata
There are two empty lines between each logic block of metadata.
We write out all answer SDP, each separated from one another by one empty
line. The generic metadata at the end can be any length with any number of
lines. Metadata files will appear in the subdirectory when the call
completes. PCAP files will be written to the subdirectory as the call is
being recorded.
A typical command line (enabling both UDP and NG protocols) thus may look like:
@ -828,7 +871,7 @@ Optionally included keys are:
__run the above algorithm__!
Round robin for both legs of the stream:
{ ..., "direction": [ "round-robin-calls", "round-robin-calls" ], ... }
{ ..., "direction": [ "round-robin-calls", "round-robin-calls" ], ... }
Round robin for first leg and and select "pub" for the second leg of the stream:
{ ..., "direction": [ "round-robin-calls", "pub" ], ... }
@ -954,6 +997,32 @@ Optionally included keys are:
Negates the respective option. This is useful if one of the session parameters was offered by
an SDES endpoint, but it should not be offered on the far side if this endpoint also speaks SDES.
* `record-call`
Contains either the string "yes" or the string "no". This tells the rtpengine
whether or not to record the call to PCAP files. If the call is recorded, it
will generate PCAP files for each stream and a metadata file for each call.
Note that rtpengine *will not* force itself into the media path, and other
flags like `ICE=force` may be necessary to ensure the call is recorded.
See the `--recording-dir` option above.
Note that this is not a duplication of the `start_recording` message. If calls
are being kernelized, then they cannot be recorded. The `start_recording`
message does not have a way to prevent a call from being kernelized, so we need
to use this flag when we send an `offer` or `answer` message.
* `metadata`
This is a generic metadata string. The metadata will be written to the bottom of
metadata files within `/path/to/recording_dir/metadata/`. This can be used to
record additional information about recorded calls. `metadata` values passed in
through subsequent messages will overwrite previous metadata values.
See the `--recording-dir` option above.
An example of a complete `offer` request dictionary could be (SDP body abbreviated):
{ "command": "offer", "call-id": "cfBXzDSZqhYNcXM", "from-tag": "mS9rSAn0Cr",

@ -6,6 +6,7 @@ CFLAGS+= `pkg-config --cflags gthread-2.0`
CFLAGS+= `pkg-config --cflags zlib`
CFLAGS+= `pkg-config --cflags openssl`
CFLAGS+= `pkg-config --cflags libevent_pthreads`
CFLAGS+= "-lpcap"
CFLAGS+= `pcre-config --cflags`
CFLAGS+= -I../kernel-module/
CFLAGS+= -D_GNU_SOURCE
@ -50,6 +51,7 @@ LDFLAGS+= `pkg-config --libs libpcre`
LDFLAGS+= `pkg-config --libs libcrypto`
LDFLAGS+= `pkg-config --libs openssl`
LDFLAGS+= `pkg-config --libs libevent_pthreads`
LDFLAGS+= "-lpcap"
LDFLAGS+= `pcre-config --libs`
LDFLAGS+= `xmlrpc-c-config client --libs`
LDFLAGS+= -lhiredis
@ -67,7 +69,7 @@ endif
SRCS= main.c kernel.c poller.c aux.c control_tcp.c streambuf.c call.c control_udp.c redis.c \
bencode.c cookie_cache.c udp_listener.c control_ng.c sdp.c str.c stun.c rtcp.c \
crypto.c rtp.c call_interfaces.c dtls.c log.c cli.c graphite.c ice.c socket.c \
media_socket.c rtcp_xr.c homer.c
media_socket.c rtcp_xr.c homer.c recording.c
OBJS= $(SRCS:.c=.o)

@ -36,6 +36,7 @@
#include "ice.h"
#include "rtpengine_config.h"
#include "log_funcs.h"
#include "recording.h"
@ -131,7 +132,6 @@ const char * get_tag_type_text(enum tag_type t) {
static void __monologue_destroy(struct call_monologue *monologue);
static int monologue_destroy(struct call_monologue *ml);
/* called with call->master_lock held in R */
static int call_timer_delete_monologues(struct call *c) {
GList *i;
@ -1503,7 +1503,6 @@ int monologue_offer_answer(struct call_monologue *other_ml, GQueue *streams,
struct endpoint_map *em;
struct call *call;
call = monologue->call;
call->last_signal = poller_now;
@ -2210,11 +2209,17 @@ void call_destroy(struct call *c) {
obj_put(sfd);
}
if (c->recording != NULL) {
recording_finish_file(c->recording);
meta_finish_file(c);
g_slice_free1(sizeof(*(c->recording)), c->recording);
}
rwlock_unlock_w(&c->master_lock);
}
/* XXX move these */
int call_stream_address46(char *o, struct packet_stream *ps, enum stream_address_format format,
int *len, const struct local_intf *ifa)
@ -2306,6 +2311,7 @@ static struct call *call_create(const str *callid, struct callmaster *m) {
ilog(LOG_NOTICE, "Creating new call");
c = obj_alloc0("call", sizeof(*c), __call_free);
c->recording = NULL;
c->callmaster = m;
mutex_init(&c->buffer_lock);
call_buffer_init(&c->buffer);
@ -2316,6 +2322,7 @@ static struct call *call_create(const str *callid, struct callmaster *m) {
c->created = poller_now;
c->dtls_cert = dtls_cert();
c->tos = m->conf.default_tos;
return c;
}
@ -2401,6 +2408,7 @@ struct call_monologue *__monologue_create(struct call *call) {
ret->call = call;
ret->created = poller_now;
ret->other_tags = g_hash_table_new(str_hash, str_equal);
g_queue_init(&ret->medias);
gettimeofday(&ret->started, NULL);

@ -14,8 +14,10 @@
#include <pcre.h>
#include <openssl/x509.h>
#include <limits.h>
#include <pcap.h>
#include "compat.h"
#include "socket.h"
#include "media_socket.h"
#define UNDEFINED ((unsigned int) -1)
#define TRUNCATED " ... Output truncated. Increase Output Buffer ... \n"
@ -397,7 +399,6 @@ struct call_monologue {
enum termination_reason term_reason;
GHashTable *other_tags;
struct call_monologue *active_dialogue;
GQueue medias;
};
@ -413,14 +414,14 @@ struct call {
rwlock_t master_lock;
GQueue monologues;
GQueue medias;
GHashTable *tags;
GHashTable *tags;
GHashTable *viabranches;
GQueue streams;
GQueue stream_fds;
GQueue endpoint_maps;
struct dtls_cert *dtls_cert; /* for outgoing */
str callid;
str callid;
time_t created;
time_t last_signal;
time_t deleted;
@ -431,6 +432,9 @@ struct call {
unsigned int redis_hosted_db;
unsigned int foreign_call; // created_via_redis_notify call
int record_call;
struct recording *recording;
};
struct callmaster_config {

@ -18,6 +18,7 @@
#include "control_udp.h"
#include "rtp.h"
#include "ice.h"
#include "recording.h"
@ -25,8 +26,6 @@ int trust_address_def;
int dtls_passive_def;
static int call_stream_address_gstring(GString *o, struct packet_stream *ps, enum stream_address_format format) {
int len, ret;
char buf[64]; /* 64 bytes ought to be enough for anybody */
@ -354,7 +353,7 @@ str *call_lookup_tcp(char **out, struct callmaster *m) {
str *call_delete_udp(char **out, struct callmaster *m) {
str callid, branch, fromtag, totag;
__C_DBG("got delete for callid '%s' and viabranch '%s'",
__C_DBG("got delete for callid '%s' and viabranch '%s'",
out[RE_UDP_DQ_CALLID], out[RE_UDP_DQ_VIABRANCH]);
str_init(&callid, out[RE_UDP_DQ_CALLID]);
@ -647,7 +646,7 @@ static const char *call_offer_answer_ng(bencode_item_t *input, struct callmaster
bencode_item_t *output, enum call_opmode opmode, const char* addr,
const endpoint_t *sin)
{
str sdp, fromtag, totag = STR_NULL, callid, viabranch;
str sdp, fromtag, totag = STR_NULL, callid, viabranch, recordcall = STR_NULL, metadata = STR_NULL;
char *errstr;
GQueue parsed = G_QUEUE_INIT;
GQueue streams = G_QUEUE_INIT;
@ -682,6 +681,7 @@ static const char *call_offer_answer_ng(bencode_item_t *input, struct callmaster
goto out;
call = call_get_opmode(&callid, m, opmode);
errstr = "Unknown call-id";
if (!call)
goto out;
@ -710,6 +710,12 @@ static const char *call_offer_answer_ng(bencode_item_t *input, struct callmaster
chopper = sdp_chopper_new(&sdp);
bencode_buffer_destroy_add(output->buffer, (free_func_t) sdp_chopper_destroy, chopper);
bencode_dictionary_get_str(input, "record-call", &recordcall);
if (recordcall.s) {
detect_setup_recording(call, recordcall);
}
ret = monologue_offer_answer(monologue, &streams, &flags);
if (!ret)
ret = sdp_replace(chopper, &parsed, monologue->active_dialogue, &flags);
@ -734,6 +740,26 @@ static const char *call_offer_answer_ng(bencode_item_t *input, struct callmaster
chopper->iov_num, chopper->str_len);
bencode_dictionary_add_string(output, "result", "ok");
struct recording *recording = call->recording;
if (call->record_call && recording != NULL && recording->meta_fp != NULL) {
struct iovec *iov = &g_array_index(chopper->iov, struct iovec, 0);
int iovcnt = chopper->iov_num;
meta_write_sdp(recording->meta_fp, iov, iovcnt,
call->recording->packet_num, opmode);
}
bencode_dictionary_get_str(input, "metadata", &metadata);
if (metadata.len > 0 && call->recording != NULL) {
if (call->recording->metadata != NULL) {
free(call->recording->metadata);
}
call->recording->metadata = str_dup(&metadata);
}
bencode_item_t *recordings = bencode_dictionary_add_list(output, "recordings");
if (call->recording != NULL && call->recording->recording_path != NULL) {
char *recording_path = call->recording->recording_path->s;
bencode_list_add_string(recordings, recording_path);
}
errstr = NULL;
out:
sdp_free(&parsed);

@ -30,6 +30,7 @@
#include "socket.h"
#include "media_socket.h"
#include "homer.h"
#include "recording.h"
@ -86,6 +87,7 @@ static enum xmlrpc_format xmlrpc_fmt = XF_SEMS;
static int num_threads;
static int delete_delay = 30;
static int graphite_interval = 0;
static char *spooldir;
static void sighandler(gpointer x) {
sigset_t ss;
@ -108,7 +110,7 @@ static void sighandler(gpointer x) {
continue;
abort();
}
if (ret == SIGINT || ret == SIGTERM)
g_shutdown = 1;
else if (ret == SIGUSR1) {
@ -322,6 +324,7 @@ static void options(int *argc, char ***argv) {
{ "homer", 0, 0, G_OPTION_ARG_STRING, &homerp, "Address of Homer server for RTCP stats","IP46:PORT"},
{ "homer-protocol",0,0,G_OPTION_ARG_STRING, &homerproto, "Transport protocol for Homer (default udp)", "udp|tcp" },
{ "homer-id", 0, 0, G_OPTION_ARG_STRING, &homer_id, "'Capture ID' to use within the HEP protocol", "INT" },
{ "recording-dir", 0, 0, G_OPTION_ARG_STRING, &spooldir, "Directory for storing pcap and metadata files", "FILE" },
{ NULL, }
};
@ -518,6 +521,7 @@ static void init_everything() {
socket_init();
log_init();
recording_fs_init(spooldir);
clock_gettime(CLOCK_REALTIME, &ts);
srandom(ts.tv_sec ^ ts.tv_nsec);
SSL_library_init();
@ -686,6 +690,7 @@ no_kernel:
set_graphite_interval_tv(&tmp_tv);
}
int main(int argc, char **argv) {
struct main_context ctx;
int idx=0;

@ -3,6 +3,7 @@
#include <string.h>
#include <glib.h>
#include <errno.h>
#include <netinet/in.h>
#include "str.h"
#include "ice.h"
#include "socket.h"
@ -17,8 +18,7 @@
#include "aux.h"
#include "log_funcs.h"
#include "poller.h"
#include "recording.h"
#ifndef PORT_RANDOM_MIN
@ -237,7 +237,7 @@ static int has_free_ports_loc(struct local_intf *loc, unsigned int num_ports) {
ilog(LOG_ERR, "has_free_ports_loc - NULL local interface");
return 0;
}
if (num_ports > loc->spec->port_pool.free_ports) {
ilog(LOG_ERR, "Didn't found %d ports available for %.*s/%s",
num_ports, loc->logical->name.len, loc->logical->name.s,
@ -297,7 +297,7 @@ static int has_free_ports_log_all(struct logical_intf *log, unsigned int num_por
return 1;
}
/* run round-robin-calls algorithm */
/* run round-robin-calls algorithm */
static struct logical_intf* run_round_robin_calls(GQueue *q, unsigned int num_ports) {
struct logical_intf *log = NULL;
volatile unsigned int nr_tries = 0;
@ -340,7 +340,7 @@ select_log:
// 2 streams => 4 x get_logical_interface calls at offer
selection_count ++;
if (selection_count % (num_ports / 2) == 0) {
selection_count = 0;
selection_count = 0;
selection_index ++;
selection_index = selection_index % nr_logs;
}
@ -832,7 +832,7 @@ void kernelize(struct packet_stream *stream) {
struct packet_stream *sink = NULL;
const char *nk_warn_msg;
if (PS_ISSET(stream, KERNELIZED))
if (PS_ISSET(stream, KERNELIZED) || call->recording != NULL)
return;
if (cm->conf.kernelid < 0)
goto no_kernel;
@ -1016,6 +1016,26 @@ noop:
/* XXX split this function into pieces */
/* called lock-free */
static int stream_packet(struct stream_fd *sfd, str *s, const endpoint_t *fsin, const struct timeval *tv) {
/**
* Incoming packets:
* - sfd->socket.local: the local IP/port on which the packet arrived
* - sfd->stream->endpoint: adjusted/learned IP/port from where the packet
* was sent
* - sfd->stream->advertised_endpoint: the unadjusted IP/port from where the
* packet was sent. These are the values present in the SDP
*
* Outgoing packets:
* - sfd->stream->rtp_sink->endpoint: the destination IP/port
* - sfd->stream->selected_sfd->socket.local: the local source IP/port for the
* outgoing packet
*
* If the rtpengine runs behind a NAT and local addresses are configured with
* different advertised endpoints, the SDP would not contain the address from
* `...->socket.local`, but rather from `sfd->local_intf->spec->address.advertised`
* (of type `sockaddr_t`). The port will be the same.
*/
/* TODO move the above comments to the data structure definitions, if the above
* always holds true */
struct packet_stream *stream,
*sink = NULL,
*in_srtp, *out_srtp;
@ -1023,6 +1043,7 @@ static int stream_packet(struct stream_fd *sfd, str *s, const endpoint_t *fsin,
int ret = 0, update = 0, stun_ret = 0, handler_ret = 0, muxed_rtcp = 0, rtcp = 0,
unk = 0;
int i;
pcap_dumper_t *recording_pdumper;
struct call *call;
struct callmaster *cm;
/*unsigned char cc;*/
@ -1033,6 +1054,7 @@ static int stream_packet(struct stream_fd *sfd, str *s, const endpoint_t *fsin,
struct rtp_stats *rtp_s;
call = sfd->call;
recording_pdumper = call->recording != NULL ? call->recording->recording_pdumper : NULL;
cm = call->callmaster;
rwlock_lock_r(&call->master_lock);
@ -1142,7 +1164,6 @@ loop_ok:
}
}
/* do we have somewhere to forward it to? */
if (!sink || !sink->selected_sfd || !out_srtp->selected_sfd || !in_srtp->selected_sfd) {
@ -1172,8 +1193,18 @@ loop_ok:
/* return values are: 0 = forward packet, -1 = error/dont forward,
* 1 = forward and push update to redis */
if (rwf_in)
if (rwf_in) {
handler_ret = rwf_in(s, in_srtp);
}
// If recording pcap dumper is set, then we record the call.
if (recording_pdumper != NULL && call->record_call) {
mutex_lock(&call->recording->recording_lock);
stream_pcap_dump(recording_pdumper, stream, s);
call->recording->packet_num++;
mutex_unlock(&call->recording->recording_lock);
}
if (handler_ret >= 0) {
if (rtcp)
parse_and_log_rtcp_report(sfd, s, fsin, tv);
@ -1306,6 +1337,7 @@ forward:
|| stun_ret || handler_ret < 0)
goto drop;
// s is my packet?
ret = socket_sendto(&sink->selected_sfd->socket, s->s, s->len, &sink->endpoint);
__C_DBG("Forward to sink endpoint: %s:%d", sockaddr_print_buf(&sink->endpoint.address), sink->endpoint.port);
@ -1350,8 +1382,6 @@ unlock_out:
}
static void stream_fd_readable(int fd, void *p, uintptr_t u) {
struct stream_fd *sfd = p;
char buf[RTP_BUFFER_SIZE];

@ -0,0 +1,412 @@
#include "recording.h"
#include <stdio.h>
#include <stdlib.h>
#include <glib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <time.h>
#include <pcap.h>
#include <curl/curl.h>
#include <inttypes.h>
#include "call.h"
int maybe_create_spool_dir(char *dirpath);
int set_record_call(struct call *call, str recordcall);
str *init_write_pcap_file(struct call *call);
// Global file reference to the spool directory.
static char *spooldir = NULL;
// Used for URL encoding functions
CURL *curl;
/**
* Initialize RTP Engine filesystem settings and structure.
* Check for or create the RTP Engine spool directory.
*/
void recording_fs_init(char *spoolpath) {
curl = curl_easy_init();
// Whether or not to fail if the spool directory does not exist.
int dne_fail;
if (spoolpath == NULL || spoolpath[0] == '\0') {
spoolpath = "/var/spool/rtpengine";
dne_fail = FALSE;
} else {
dne_fail = TRUE;
int path_len = strlen(spoolpath);
// Get rid of trailing "/" if it exists. Other code adds that in when needed.
if (spoolpath[path_len-1] == '/') {
spoolpath[path_len-1] = '\0';
}
}
if (!maybe_create_spool_dir(spoolpath)) {
fprintf(stderr, "Error while setting up spool directory \"%s\".\n", spoolpath);
if (dne_fail) {
fprintf(stderr, "Please run `mkdir %s` and start rtpengine again.\n", spoolpath);
exit(-1);
}
} else {
spooldir = strdup(spoolpath);
}
}
/**
* Sets up the spool directory for RTP Engine.
* If the directory does not exist, return FALSE.
* If the directory exists, but "$spoolpath/metadata" or "$spoolpath/pcaps"
* exist as non-directory files, return FALSE.
* Otherwise, return TRUE.
*
* Create the "metadata" and "pcaps" directories if they are not there.
*/
int maybe_create_spool_dir(char *spoolpath) {
struct stat info;
int spool_good = TRUE;
if (stat(spoolpath, &info) != 0) {
fprintf(stderr, "Spool directory \"%s\" does not exist.\n", spoolpath);
spool_good = FALSE;
} else if (!S_ISDIR(info.st_mode)) {
fprintf(stderr, "Spool file exists, but \"%s\" is not a directory.\n", spoolpath);
spool_good = FALSE;
} else {
// Spool directory exists. Make sure it has inner directories.
int path_len = strlen(spoolpath);
char meta_path[path_len + 10];
char rec_path[path_len + 7];
snprintf(meta_path, path_len + 10, "%s/metadata", spoolpath);
snprintf(rec_path, path_len + 7, "%s/pcaps", spoolpath);
if (stat(meta_path, &info) != 0) {
fprintf(stdout, "Creating metadata directory \"%s\".\n", meta_path);
mkdir(meta_path, 0777);
} else if(!S_ISDIR(info.st_mode)) {
fprintf(stderr, "metadata file exists, but \"%s\" is not a directory.\n", meta_path);
spool_good = FALSE;
}
if (stat(rec_path, &info) != 0) {
fprintf(stdout, "Creating pcaps directory \"%s\".\n", rec_path);
mkdir(rec_path, 0777);
} else if(!S_ISDIR(info.st_mode)) {
fprintf(stderr, "pcaps file exists, but \"%s\" is not a directory.\n", rec_path);
spool_good = FALSE;
}
}
return spool_good;
}
/**
*
* Controls the setting of recording variables on a `struct call *`.
* Sets the `record_call` value on the `struct call`, initializing the
* recording struct if necessary.
* If we do not yet have a PCAP file associated with the call, create it
* and write its file URL to the metadata file.
*
* Returns a boolean for whether or not the call is being recorded.
*/
int detect_setup_recording(struct call *call, str recordcall) {
int is_recording = set_record_call(call, recordcall);
struct recording *recording = call->recording;
if (is_recording && recording != NULL && recording->recording_pdumper == NULL) {
// We haven't set up the PCAP file, so set it up and write the URL to metadata
init_write_pcap_file(call);
}
return is_recording;
}
/**
* Controls the setting of recording variables on a `struct call *`.
* Sets the `record_call` value on the `struct call`, initializing the
* recording struct if necessary.
*
* Returns a boolean for whether or not the call is being recorded.
*/
int set_record_call(struct call *call, str recordcall) {
if (!str_cmp(&recordcall, "yes")) {
if (call->record_call == FALSE) {
ilog(LOG_NOTICE, "Turning on call recording.");
}
call->record_call = TRUE;
if (call->recording == NULL) {
call->recording = g_slice_alloc0(sizeof(struct recording));
call->recording->recording_pd = NULL;
call->recording->recording_pdumper = NULL;
// Wireshark starts at packet index 1, so we start there, too
call->recording->packet_num = 1;
mutex_init(&call->recording->recording_lock);
meta_setup_file(call->recording, call->callid);
}
} else if (!str_cmp(&recordcall, "no")) {
if (call->record_call == TRUE) {
ilog(LOG_NOTICE, "Turning off call recording.");
}
call->record_call = FALSE;
} else {
ilog(LOG_INFO, "\"record-call\" flag %s is invalid flag.", recordcall.s);
}
return call->record_call;
}
/**
* Checks if we have a PCAP file for the call yet.
* If not, create it and write its location to the metadata file.
*/
str *init_write_pcap_file(struct call *call) {
str *pcap_path = recording_setup_file(call->recording, call->callid);
if (pcap_path != NULL && call->recording->recording_pdumper != NULL
&& call->recording->meta_fp) {
// Write the location of the PCAP file to the metadata file
fprintf(call->recording->meta_fp, "%s\n\n", pcap_path->s);
}
return pcap_path;
}
/**
* Create a call metadata file in a temporary location.
* Attaches the filepath and the file pointer to the call struct.
*/
str *meta_setup_file(struct recording *recording, str callid) {
if (spooldir == NULL) {
// No spool directory was created, so we cannot have metadata files.
return NULL;
}
else {
int rand_bytes = 8;
str *meta_filepath = malloc(sizeof(str));
// We don't want weird characters like ":" or "@" showing up in filenames
char *escaped_callid = curl_easy_escape(curl, callid.s, callid.len);
int escaped_callid_len = strlen(escaped_callid);
// Length for spool directory path + "/tmp/rtpengine-meta-${CALLID}-"
int mid_len = 20 + escaped_callid_len + 1 + 1;
char suffix_chars[mid_len];
snprintf(suffix_chars, mid_len, "/tmp/rtpengine-meta-%s-", escaped_callid);
curl_free(escaped_callid);
// Initially file extension is ".tmp". When call is over, it changes to ".txt".
char *path_chars = rand_affixed_str(suffix_chars, rand_bytes, ".tmp");
meta_filepath = str_init(meta_filepath, path_chars);
recording->meta_filepath = meta_filepath;
FILE *mfp = fopen(meta_filepath->s, "w");
chmod(meta_filepath->s, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
if (mfp == NULL) {
ilog(LOG_ERROR, "Could not open metadata file: %s", meta_filepath->s);
free(recording->meta_filepath->s);
free(recording->meta_filepath);
recording->meta_filepath = NULL;
}
recording->meta_fp = mfp;
ilog(LOG_DEBUG, "Wrote metadata file to temporary path: %s", meta_filepath->s);
return meta_filepath;
}
}
/**
* Write out a block of SDP to the metadata file.
*/
ssize_t meta_write_sdp(FILE *meta_fp, struct iovec *sdp_iov, int iovcnt,
uint64_t packet_num, enum call_opmode opmode) {
int meta_fd = fileno(meta_fp);
// File pointers buffer data, whereas direct writing using the file
// descriptor does not. Make sure to flush any unwritten contents
// so the file contents appear in order.
fprintf(meta_fp, "\nSDP mode: ");
if (opmode == OP_ANSWER) {
fprintf(meta_fp, "answer");
} else if (opmode == OP_OFFER) {
fprintf(meta_fp, "offer");
} else {
fprintf(meta_fp, "other");
}
fprintf(meta_fp, "\nSDP before RTP packet: %" PRIu64 "\n\n", packet_num);
fflush(meta_fp);
return writev(meta_fd, sdp_iov, iovcnt);
}
/**
* Writes metadata to metafile, closes file, and renames it to finished location.
* Returns non-zero for failure.
*/
int meta_finish_file(struct call *call) {
struct recording *recording = call->recording;
int return_code = 0;
if (recording != NULL && recording->meta_fp != NULL) {
// Print start timestamp and end timestamp
// YYYY-MM-DDThh:mm:ss
time_t start = call->created;
time_t end = g_now.tv_sec;
char timebuffer[20];
struct tm *timeinfo;
timeinfo = localtime(&start);
strftime(timebuffer, 20, "%FT%T", timeinfo);
fprintf(recording->meta_fp, "\n\ncall start time: %s\n", timebuffer);
timeinfo = localtime(&end);
strftime(timebuffer, 20, "%FT%T", timeinfo);
fprintf(recording->meta_fp, "call end time: %s\n", timebuffer);
// Print metadata
fprintf(recording->meta_fp, "\n\n%s\n", recording->metadata->s);
free(recording->metadata);
fclose(recording->meta_fp);
// Get the filename (in between its directory and the file extension)
// and move it to the finished file location.
// Rename extension to ".txt".
int fn_len;
char *meta_filename = strrchr(recording->meta_filepath->s, '/');
char *meta_ext = NULL;
if (meta_filename == NULL) {
meta_filename = recording->meta_filepath->s;
}
else {
meta_filename = meta_filename + 1;
}
// We can always expect a file extension
meta_ext = strrchr(meta_filename, '.');
fn_len = meta_ext - meta_filename;
int prefix_len = strlen(spooldir) + 10; // constant for "/metadata/" suffix
int ext_len = 4; // for ".txt"
char new_metapath[prefix_len + fn_len + ext_len + 1];
snprintf(new_metapath, prefix_len+fn_len+1, "%s/metadata/%s", spooldir, meta_filename);
snprintf(new_metapath + prefix_len+fn_len, ext_len+1, ".txt");
return_code = return_code || rename(recording->meta_filepath->s, new_metapath);
if (return_code != 0) {
ilog(LOG_ERROR, "Could not move metadata file \"%s\" to \"%s/metadata/\"",
recording->meta_filepath->s, spooldir);
} else {
ilog(LOG_INFO, "Moved metadata file \"%s\" to \"%s/metadata\"",
recording->meta_filepath->s, spooldir);
}
} else {
ilog(LOG_INFO, "Trying to clean up recording meta file without a file pointer opened.");
}
if (recording != NULL && recording->meta_filepath != NULL) {
free(recording->meta_filepath->s);
free(recording->meta_filepath);
}
mutex_destroy(&recording->recording_lock);
return return_code;
}
/**
* Generate a random PCAP filepath to write recorded RTP stream.
* Returns path to created file.
*/
str *recording_setup_file(struct recording *recording, str callid) {
str *recording_path = NULL;
if (spooldir != NULL
&& recording != NULL
&& recording->recording_pd == NULL && recording->recording_pdumper == NULL) {
int rand_bytes = 8;
// We don't want weird characters like ":" or "@" showing up in filenames
char *escaped_callid = curl_easy_escape(curl, callid.s, callid.len);
int escaped_callid_len = strlen(escaped_callid);
// Length for spool directory path + "/pcaps/${CALLID}-"
int rec_path_len = strlen(spooldir) + 7 + escaped_callid_len + 1 + 1;
char rec_path[rec_path_len];
snprintf(rec_path, rec_path_len, "%s/pcaps/%s-", spooldir, escaped_callid);
curl_free(escaped_callid);
char *path_chars = rand_affixed_str(rec_path, rand_bytes, ".pcap");
recording_path = malloc(sizeof(str));
recording_path = str_init(recording_path, path_chars);
recording->recording_path = recording_path;
recording->recording_pd = pcap_open_dead(DLT_RAW, 65535);
recording->recording_pdumper = pcap_dump_open(recording->recording_pd, path_chars);
if (recording->recording_pdumper == NULL) {
pcap_close(recording->recording_pd);
recording->recording_pd = NULL;
ilog(LOG_INFO, "Failed to write recording file: %s", recording_path->s);
} else {
ilog(LOG_INFO, "Writing recording file: %s", recording_path->s);
}
} else if (recording != NULL) {
recording->recording_path = NULL;
recording->recording_pd = NULL;
recording->recording_pdumper = NULL;
}
return recording_path;
}
/**
* Flushes PCAP file, closes the dumper and descriptors, and frees object memory.
*/
void recording_finish_file(struct recording *recording) {
if (recording->recording_pdumper != NULL) {
pcap_dump_flush(recording->recording_pdumper);
pcap_dump_close(recording->recording_pdumper);
free(recording->recording_path->s);
free(recording->recording_path);
}
if (recording->recording_pd != NULL) {
pcap_close(recording->recording_pd);
}
}
/**
* Write out a PCAP packet with payload string.
* A fair amount extraneous of packet data is spoofed.
*/
void stream_pcap_dump(pcap_dumper_t *pdumper, struct packet_stream *stream, str *s) {
endpoint_t src_endpoint = stream->advertised_endpoint;
endpoint_t dst_endpoint = stream->selected_sfd->socket.local;
// Wrap RTP in fake UDP packet header
// Right now, we spoof it all
u_int16_t udp_len = ((u_int16_t)s->len) + 8;
u_int16_t udp_header[4];
u_int16_t src_port = (u_int16_t) src_endpoint.port;
u_int16_t dst_port = (u_int16_t) dst_endpoint.port;
udp_header[0] = htons(src_port); // source port
udp_header[1] = htons(dst_port); // destination port
udp_header[2] = htons(udp_len); // packet length
udp_header[3] = 0; // checksum
// Wrap RTP in fake IP packet header
u_int8_t ip_header[20];
u_int16_t ip_total_length = udp_len + 20;
u_int16_t *ip_total_length_ptr = (u_int16_t*)(ip_header + 2);
u_int32_t *ip_src_addr = (u_int32_t*)(ip_header + 12);
u_int32_t *ip_dst_addr = (u_int32_t*)(ip_header + 16);
unsigned long src_ip = src_endpoint.address.u.ipv4.s_addr;
unsigned long dst_ip = dst_endpoint.address.u.ipv4.s_addr;
memset(ip_header, 0, 20);
ip_header[0] = 4 << 4; // IP version - 4 bits
ip_header[0] = ip_header[0] | 5; // Internet Header Length (IHL) - 4 bits
ip_header[1] = 0; // DSCP - 6 bits
ip_header[1] = 0; // ECN - 2 bits
*ip_total_length_ptr = htons(ip_total_length);
ip_header[4] = 0; ip_header[5] = 0 ; // Identification - 2 bytes
ip_header[6] = 0; // Flags - 3 bits
ip_header[7] = 0; // Fragment Offset - 13 bits
ip_header[8] = 64; // TTL - 1 byte
ip_header[9] = 17; // Protocol (defines protocol in data portion) - 1 byte
ip_header[10] = 0; ip_header[11] = 0; // Header Checksum - 2 bytes
*ip_src_addr = src_ip; // Source IP (set to localhost) - 4 bytes
*ip_dst_addr = dst_ip; // Destination IP (set to localhost) - 4 bytes
// Set up PCAP packet header
struct pcap_pkthdr header;
ZERO(header);
header.ts = g_now;
header.caplen = s->len + 28;
// This must be the same value we use in `pcap_open_dead`
header.len = s->len + 28;
// Copy all the headers and payload into a new string
unsigned char pkt_s[ip_total_length];
memcpy(pkt_s, ip_header, 20);
memcpy(pkt_s + 20, udp_header, 8);
memcpy(pkt_s + 28, s->s, s->len);
// Write the packet to the PCAP file
// Casting quiets compiler warning.
pcap_dump((unsigned char *)pdumper, &header, (unsigned char *)pkt_s);
}

@ -0,0 +1,116 @@
/**
* recording.h
*
* Handles call recording to PCAP files and recording metadata.
* Mostly filesystem operations
*/
#ifndef __RECORDING_H__
#define __RECORDING_H__
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include "call.h"
struct recording {
str *meta_filepath;
FILE *meta_fp;
str *metadata;
pcap_t *recording_pd;
pcap_dumper_t *recording_pdumper;
uint64_t packet_num;
str *recording_path;
mutex_t recording_lock;
};
/**
* Initialize RTP Engine filesystem settings and structure.
* Check for or create the RTP Engine spool directory.
*/
void recording_fs_init(char *spooldir);
/**
*
* Controls the setting of recording variables on a `struct call *`.
* Sets the `record_call` value on the `struct call`, initializing the
* recording struct if necessary.
* If we do not yet have a PCAP file associated with the call, create it
* and write its file URL to the metadata file.
*
* Returns a boolean for whether or not the call is being recorded.
*/
int detect_setup_recording(struct call *call, str recordcall);
/**
* Create a call metadata file in a temporary location.
* Attaches the filepath and the file pointer to the call struct.
* Returns path to created file.
*
* Metadata file format is (with trailing newline):
*
* /path/to/recording-pcap.pcap
*
*
* first SDP answer
*
* second SDP answer
*
* ...
*
* n-th and final SDP answer
*
*
* start timestamp (YYYY-MM-DDThh:mm:ss)
* end timestamp (YYYY-MM-DDThh:mm:ss)
*
*
* generic metadata
*
* There are two empty lines between each logic block of metadata.
* The generic metadata at the end can be any length with any number of lines.
* Temporary files go in /tmp/. They will end up in
* ${RECORDING_DIR}/metadata/. They are named like:
* ${CALL_ID}-${RAND-HEX}.pcap
*
*/
str *meta_setup_file(struct recording *recording, str callid);
/**
* Write out a block of SDP to the metadata file.
*/
ssize_t meta_write_sdp(FILE *meta_fp, struct iovec *sdp_iov, int iovcnt,
uint64_t packet_num, enum call_opmode opmode);
/**
* Writes metadata to metafile, closes file, and moves it to finished location.
* Returns non-zero for failure.
*
* Metadata files are moved to ${RECORDING_DIR}/metadata/
*/
int meta_finish_file(struct call *call);
/**
* Generate a random PCAP filepath to write recorded RTP stream.
* Returns path to created file.
*
* Files go in ${RECORDING_DIR}/pcaps, and are named like:
* ${CALL_ID}-${RAND-HEX}.pcap
*/
str *recording_setup_file(struct recording *recording, str callid);
/**
* Flushes PCAP file, closes the dumper and descriptors, and frees object memory.
*/
void recording_finish_file(struct recording *recording);
/**
* Write out a PCAP packet with payload string.
* A fair amount extraneous of packet data is spoofed.
*/
void stream_pcap_dump(pcap_dumper_t *pdumper, struct packet_stream *sink, str *s);
#endif

@ -33,3 +33,48 @@ str *__str_sprintf(const char *fmt, ...) {
void str_slice_free(void *p) {
g_slice_free1(sizeof(str), p);
}
/**
* Generates a random string sandwiched between affixes.
* Will create the char string for you. Don't forget to clean up!
*/
char *rand_affixed_str(char *prefix, int num_bytes, char *suffix) {
int rand_len = num_bytes*2 + 1;
char rand_affix[rand_len];
int prefix_len = strlen(prefix);
int suffix_len = strlen(suffix);
char *full_path = calloc(rand_len + prefix_len + suffix_len, sizeof(char));
rand_hex_str(rand_affix, num_bytes);
snprintf(full_path, rand_len+prefix_len, "%s%s", prefix, rand_affix);
snprintf(full_path + rand_len+prefix_len-1, suffix_len+1, "%s", suffix);
return full_path;
}
/**
* Generates a random hexadecimal string representing n random bytes.
* rand_str length must be 2*num_bytes + 1.
*/
char *rand_hex_str(char *rand_str, int num_bytes) {
char rand_tmp[3];
u_int8_t rand_byte;
int i, n;
// We might convert an int to a hex string shorter than 2 digits.
// This causes those strings to have leading '0' characters.
for (i=0; i<num_bytes*2 + 1; i++) {
rand_str[i] = '0';
}
for (i=0; i<num_bytes; i++) {
// Determine the length of the hex byte string.
// If less than two, offset by 2-len to pad with prefix zeroes.
rand_byte = (u_int8_t)rand();
snprintf(rand_tmp, 3, "%x", rand_byte);
n = strlen(rand_tmp);
snprintf(rand_str + i*2 + (2-n), 3, "%s", rand_tmp);
rand_str[i*2 + 2] = '0';
}
rand_str[num_bytes*2] = '\0';
return rand_str;
}

@ -292,4 +292,11 @@ INLINE int str_token(str *new_token, str *ori_and_remainder, int sep) {
return 0;
}
/* Generates a random string sandwiched between affixes. */
char *rand_affixed_str(char *prefix, int num_bytes, char *suffix);
/* Generates a hex string representing n random bytes. len(rand_str) = 2*num_bytes + 1 */
char *rand_hex_str(char *rand_str, int num_bytes);
#endif

1
debian/control vendored

@ -8,6 +8,7 @@ Build-Depends: debhelper (>= 5),
libevent-dev (>= 2.0),
libglib2.0-dev (>= 2.30),
libhiredis-dev,
libpcap-dev,
libpcre3-dev,
libssl-dev (>= 1.0.1),
libxmlrpc-c3-dev (>= 1.16.07) | libxmlrpc-core-c3-dev (>= 1.16.07),

Loading…
Cancel
Save