siproc

A primitive SIP client that spawns processes for each call
git clone git://xatko.vsos.ethz.ch/siproc.git
Log | Files | Refs

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; +}