commit ca6a0833c2817ac4fcccd41edbb886a2b756d7aa
Author: Dominik Schmidt <dominik@schm1dt.ch>
Date: Sun, 21 Jul 2019 21:49:03 +0200
Initial commit
Diffstat:
Makefile | | | 13 | +++++++++++++ |
Readme.md | | | 49 | +++++++++++++++++++++++++++++++++++++++++++++++++ |
src/siproc.cpp | | | 441 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
3 files changed, 503 insertions(+), 0 deletions(-)
diff --git a/Makefile b/Makefile
@@ -0,0 +1,13 @@
+PJSIP_CFLAGS = `pkg-config --cflags libpjproject`
+PJSIP_LDFLAGS = `pkg-config --libs libpjproject`
+CPP = g++
+CFLAGS ?= -O5
+LDFLAGS ?= -s
+_CFLAGS = $(CFLAGS) $(PJSIP_CFLAGS)
+_LDFLAGS = $(LDFLAGS) $(PJSIP_LDFLAGS)
+
+siproc: src/siproc.o
+ $(CPP) $(_CFLAGS) $(_LDFLAGS) -o $@ $^
+
+%.o: %.cpp
+ $(CPP) $(_CFLAGS) -c -o $@ $^
diff --git a/Readme.md b/Readme.md
@@ -0,0 +1,49 @@
+SiProc
+======
+
+SiProc is a primitive SIP Client used for call automatization by spawning a given process for each call.
+It uses the [pjsip](http://www.pjsip.org/) library as a backend.
+
+Usage
+-----
+
+The main process can be started as follows:
+
+```
+$ export SIPROC_REGISTRAR_URI="sip:myserver.lan"
+$ export SIPROC_ID_URI="Full Name <Account@myserver.lan>"
+$ export SIPROC_USERNAME="Account"
+$ export SIPROC_PASSWORD="secret"
+$ ./siproc /path/to/executable args...
+```
+
+### Calls
+
+Whenever a call is made or received, `/path/to/executable` is executed, and is given the following environment variables
+
+* SIPROC_REMOTE_URI: The address of the caller
+* SIPROC_REMOTE_ID: Some pjsip interna, look up their documentation
+* SIPROC_REMOTE_CONTACT: Ditto
+
+The program can then perform actions via STDIO.
+Each command is separated by a newline ('\n').
+The following commands are supported:
+
+* `ANSWER`: Answer an incoming call
+* `HANGUP`: Hang up a call
+* `DTMF string`: Dial the DTMF tones of each character in `string`
+* `PLAY /path/to/file.wav`: Play a file
+
+The following lines can be received via stdin:
+
+* `DTMF c`: When the client receives the DTMF character `c`
+
+When the remote end hangs up, the process is killed via SIGTERM, so you might want to catch that signal for cleaning up.
+
+To-Do
+-----
+
+* [ ] Making calls
+* [ ] Recording audio
+* [ ] Expose more of pjsips features to the process!
+
diff --git a/src/siproc.cpp b/src/siproc.cpp
@@ -0,0 +1,441 @@
+#include <pjsua2.hpp>
+#include <stdlib.h>
+#include <stdio.h>
+#include <iostream>
+#include <unistd.h>
+#include <signal.h>
+#include <sys/wait.h>
+#include <sys/epoll.h>
+
+struct EV{
+ int efd;
+
+ EV(){
+ efd=epoll_create1(0);
+ if(efd<0){
+ throw "Could not create epoll fd";
+ }
+ }
+
+ ~EV(){
+ close(efd);
+ }
+
+ void ev_add(int fd, void *data){
+ struct epoll_event ev={0};
+ ev.events = EPOLLIN;
+ ev.data.ptr = data;
+ epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ev);
+ }
+
+ void ev_del(int fd){
+ epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL);
+ }
+
+ void* wait(int timeout){
+ struct epoll_event ev={0};
+ ev.data.u64=0;
+ int ret=epoll_wait(efd, &ev, 1, timeout);
+ if(ret<0){
+ throw "Could not wait on epollfd";
+ }
+ else if(ret == 0){
+ return NULL;
+ }
+ else{
+ return ev.data.ptr;
+ }
+ }
+
+ void* wait(){
+ return wait(-1);
+ }
+};
+
+ssize_t readall(int fd, char **buffer, size_t *buffer_length){
+ ssize_t ret=0;
+ size_t offset=0;
+ while((ret = read(fd, (*buffer+offset), *buffer_length-offset)) == *buffer_length-offset){
+ *buffer_length *= 2;
+ *buffer = (char*)realloc(*buffer, *buffer_length);
+ offset += ret;
+ }
+ if(ret == -1){
+ perror("read");
+ printf("ADDRESS: %p, %lu\n", *buffer, *buffer_length);
+ return -1;
+ }
+ return offset + ret;
+}
+
+EV ev_table=EV();
+
+using namespace pj;
+
+class MyAccount;
+
+class MyCall : public Call
+{
+
+ FILE *c_stdin;
+ int c_stdout;
+ MyAccount *myacc;
+ pid_t child;
+ char *line;
+ size_t line_length;
+ AudioMediaPlayer player;
+ AudioMediaRecorder recorder;
+public:
+ MyCall(Account &acc, int call_id = PJSUA_INVALID_ID) : Call(acc, call_id){
+ c_stdin = NULL;
+ c_stdout = -1;
+ child = -1;
+ line = (char*)malloc(256);
+ line_length = 256;
+ }
+
+ ~MyCall(){
+ if(line!= NULL){
+ free(line);
+ line=NULL;
+ line_length=0;
+ }
+ if (child != -1){
+ fork_off();
+ }
+ }
+
+ AudioMedia& playdev(){
+ //return Endpoint::instance().audDevManager().getPlaybackDevMedia();
+ CallInfo ci = getInfo();
+ AudioMedia *aud_med = NULL;
+
+ // Find out which media index is the audio
+ for (unsigned i=0; i<ci.media.size(); ++i) {
+ if (ci.media[i].type == PJMEDIA_TYPE_AUDIO) {
+ aud_med = (AudioMedia *)getMedia(i);
+ break;
+ }
+ }
+
+ return *aud_med;
+ }
+
+ void fork_off(){
+ if(child == -1){
+ return;
+ }
+ PJ_LOG(3, ("MyCall", "Shutting down child process"));
+ ev_table.ev_del(c_stdout);
+ fclose(c_stdin);
+ close(c_stdout);
+ c_stdin = NULL;
+ c_stdout = -1;
+ kill(child, SIGTERM);
+ int statloc;
+ waitpid(child, &statloc, 0);
+ child = -1;
+ }
+
+ void fork_on(char **args);
+
+ virtual void onCallState(OnCallStateParam &prm){
+ CallInfo ci = getInfo();
+ if (ci.state == PJSIP_INV_STATE_DISCONNECTED) {
+ fork_off();
+ }
+ }
+
+ virtual void onCallMediaState(OnCallMediaStateParam &prm){
+
+ }
+
+ int command(const char *cmd, ...){
+ if(c_stdin==NULL){
+ return -1;
+ }
+ int ret;
+ va_list ap;
+ va_start(ap, cmd);
+ ret = vfprintf(c_stdin, cmd, ap);
+ va_end(ap);
+ fflush(c_stdin);
+ return ret;
+ }
+
+ virtual void onDtmfDigit(OnDtmfDigitParam &prm){
+ command("DTMF %s\n", prm.digit.c_str());
+ }
+
+ virtual void onInstantMessage(OnInstantMessageParam &prm){
+ command("MESSAGE %s\n", prm.msgBody.c_str());
+ }
+
+ void cmd_hangup(){
+ // Just hangup for now
+ CallOpParam op;
+ op.statusCode = PJSIP_SC_DECLINE;
+ hangup(op);
+ }
+
+ void cmd_answer(){
+ CallOpParam prm;
+ prm.statusCode = PJSIP_SC_OK;
+ answer(prm);
+ }
+
+ void cmd_dtmf(char *args){
+ dialDtmf(args);
+ }
+
+ void cmd_message(char *msg){
+ SendInstantMessageParam prm=SendInstantMessageParam();
+ prm.content=std::string(msg);
+ sendInstantMessage(prm);
+ }
+
+ void cmd_play(char *path){
+ AudioMedia& play_dev_med=playdev();
+ try {
+ player.createPlayer(path, PJMEDIA_FILE_NO_LOOP);
+ player.startTransmit(play_dev_med);
+ }
+ catch(Error& err){
+ //std::cerr << err <<std::endl;
+ }
+ }
+
+ void cmd_record(char *path){
+ AudioMedia& play_dev_med=playdev();
+ try {
+ player.createPlayer(path, PJMEDIA_FILE_NO_LOOP);
+ player.startTransmit(play_dev_med);
+ }
+ catch(Error& err){
+ //std::cerr << err <<std::endl;
+ }
+ }
+
+ void cmd_stop(char *path){
+ AudioMedia& play_dev_med=playdev();
+ player.stopTransmit(play_dev_med);
+ }
+
+
+ void command_machine(char *command){
+ char *args = strstr(command, " ");
+ if(args != NULL){
+ *args++ = '\0';
+ }
+ if(strcmp(command, "HANGUP")==0){
+ cmd_hangup();
+ }
+ else if(strcmp(command, "ANSWER")==0){
+ cmd_answer();
+ }
+ else if(strcmp(command, "DTMF")==0){
+ cmd_dtmf(args);
+ }
+ else if(strcmp(command, "MESSAGE")==0){
+ cmd_message(args);
+ }
+ else if(strcmp(command, "PLAY")==0){
+ cmd_play(args);
+ }
+ else if(strcmp(command, "STOP")==0){
+ cmd_stop(args);
+ }
+ else if(strcmp(command, "RECORD")==0){
+ cmd_record(args);
+ }
+ }
+
+ void handle_line(){
+ PJ_LOG(3, ("MyCall", "Handling line"));
+ ssize_t ret=readall(c_stdout, &line, &line_length);
+ printf("Read %lu bytes\n", ret);
+ if(ret<0){
+ throw "Error reading from stdout";
+ }
+ else if(ret == 0){
+ PJ_LOG(3, ("MyCall", "Child closed stdout, killing it"));
+ fork_off();
+ }
+ else{
+ char *begin = line;
+ while(begin){
+ char *end=strstr(begin, "\n");
+ if(end != NULL){
+ *end='\0';
+ }
+ command_machine(begin);
+ begin=end;
+ }
+ }
+
+ }
+};
+
+class MyAccount : public Account
+{
+public:
+ char **args;
+ MyAccount(char **args) {
+ this->args=args;
+ }
+ ~MyAccount(){
+ }
+
+ virtual void onIncomingCall(OnIncomingCallParam &iprm){
+ MyCall *call = new MyCall(*this, iprm.callId);
+ call->fork_on(args);
+ }
+};
+
+void MyCall::fork_on(char **args){
+ PJ_LOG(4, ("MyCall", "Setting up Fork"));
+ int pipes_stdin[2],pipes_stdout[2];
+
+ if(pipe(pipes_stdin)!=0){
+ throw "Pipe creation failed";
+ }
+
+ if(pipe(pipes_stdout)!=0){
+ throw "Pipe creation failed";
+ }
+ c_stdin = fdopen(pipes_stdin[1], "w");
+ setvbuf(c_stdin, NULL, _IOLBF, 256);
+ c_stdout = pipes_stdout[0];
+
+ CallInfo ci = getInfo();
+ PJ_LOG(4, ("MyCall", "Calling fork"));
+ pid_t pid = fork();
+ if(pid == -1){
+ PJ_LOG(1, ("MyCall", "Fork failed"));
+ throw "Fork failed";
+ }
+ else if(pid == 0){
+ dup2(pipes_stdin[0], STDIN_FILENO);
+ dup2(pipes_stdout[1], STDOUT_FILENO);
+ close(pipes_stdin[1]);
+ close(pipes_stdout[0]);
+ setenv("SIPROC_REMOTE_ID", ci.callIdString.c_str(), 1);
+ setenv("SIPROC_REMOTE_URI", ci.remoteUri.c_str(), 1);
+ setenv("SIPROC_REMOTE_CONTACT", ci.remoteContact.c_str(), 1);
+ if(execv(args[0], &args[1]) < 0){
+ perror("exec");
+ }
+ exit(1);
+ }
+ else{
+ PJ_LOG(3, ("MyCall", "Fork successfull"));
+ child=pid;
+ close(pipes_stdin[0]);
+ close(pipes_stdout[1]);
+ ev_table.ev_add(pipes_stdout[0], this);
+ }
+}
+
+void usage(){
+ fprintf(stderr,
+"Usage: siproc <executable> [args...]\n\n"
+"Make sure to define the following environment variables:\n"
+"\t* SIPROC_USERNAME:\tThe username used for authentication, e.g. Foo\n"
+"\t* SIPROC_PASSWORD:\tThe password used for authentication, e.g. Bar\n"
+"\t* SIPROC_REGISTRAR_URI:\tThe server to connect to, e.g. \"sip:fritz.box\"\n"
+"\t* SIPROC_ID_URI:\tThe ID URI of your account, e.g. \"Foo Baz <sip:Foo@fritz.box>\"\n"
+"\nThanks for riding siproc!\n"
+);
+}
+
+int main(int argc, char **argv){
+
+ AccountConfig acfg;
+
+ char *user,*password;
+
+ if(!(user = getenv("SIPROC_USERNAME"))){
+ fprintf(stderr, "SIPROC_USERNAME not in environment variables\n\n");
+ usage();
+ return 1;
+ }
+ if(!(password = getenv("SIPROC_PASSWORD"))){
+ fprintf(stderr, "SIPROC_PASSWORD not in environment variables\n\n");
+ usage();
+ return 1;
+ }
+
+ if(char *reguri = getenv("SIPROC_REGISTRAR_URI")){
+ acfg.regConfig.registrarUri = reguri;
+ }
+ else{
+ fprintf(stderr, "SIPROC_REGISTRAR_URI not in environment variables\n\n");
+ usage();
+ return 1;
+ }
+
+ if(char *idUri = getenv("SIPROC_ID_URI")){
+ //acfg.idUri = "\"Testphone\" <sip:Dominik9@fritz.box>";
+ acfg.idUri = idUri;
+ }
+ else{
+ fprintf(stderr, "SIPROC_ID_URI not in environment variables\n\n");
+ usage();
+ return 1;
+ }
+
+ try{
+
+ Endpoint ep;
+
+ ep.libCreate();
+ EpConfig ep_cfg;
+ ep.libInit(ep_cfg);
+
+ TransportConfig tcfg;
+ tcfg.port = 5060;
+ try {
+ ep.transportCreate(PJSIP_TRANSPORT_UDP, tcfg);
+ } catch (Error &err) {
+ std::cerr << err.info() << std::endl;
+ return 1;
+ }
+
+ ep.libStart();
+
+
+ //acfg.regConfig.registrarUri = "sip:fritz.box";
+ //AuthCredInfo cred("digest", "*", "Dominik9", 0, "12345678");
+ AuthCredInfo cred("digest", "*", user, 0, password);
+ acfg.sipConfig.authCreds.push_back( cred );
+
+ MyAccount *acc = new MyAccount(&argv[1]);
+ acc->create(acfg);
+
+ char *line=NULL;
+ size_t line_length=0;
+ ev_table.ev_add(STDIN_FILENO, &line_length);
+ while(void *ptr=ev_table.wait()){
+ if(ptr == &line_length){
+ ssize_t bytes = getline(&line, &line_length, stdin);
+ if(bytes > 0){
+ printf("Got line with length %lu:\n", bytes);
+ fwrite(line, 1, bytes, stdout);
+ if(strncmp(line, "QUIT", line_length)==0){
+ break;
+ }
+ }
+ }
+ else{
+ MyCall* c=(MyCall*)ptr;
+ c->handle_line();
+ }
+ }
+ delete acc;
+ }
+ catch(char const* c){
+ printf("EXCEPTION: %s\n", c);
+ }
+
+ return 0;
+}