/*
webchat, an HTML5/websocket chat platform
Copyright (C) 2015
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "../libwebsocket/websock.h"
#include "users.h"
#include "channels.h"
#include "httpcam.h"
#include "db.h"
#define use_tls 1
int createserversock(int port)
{
int sock=socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in srvaddr;
srvaddr.sin_family=AF_INET;
srvaddr.sin_port=htons(port);
srvaddr.sin_addr.s_addr=0;
if(bind(sock, (struct sockaddr*)&srvaddr, sizeof(srvaddr)))
{
perror("bind");
printf("Retrying bind to port %i... ", port);
fflush(stdout);
while(bind(sock, (struct sockaddr*)&srvaddr, sizeof(srvaddr))){sleep(1);}
printf("done\n");
errno=0;
}
#define backlog 10 /* Todo: make this configurable */
if(listen(sock, backlog)){perror("listen");}
return sock;
}
void uridecode(char* string)
{
if(!string){return;}
char buf[3];
buf[2]=0;
while((string=strchr(string, '%')))
{
buf[0]=string[1];
buf[1]=string[2];
string[0]=strtol(buf, 0, 16);
string=&string[1];
memmove(string, &string[2], strlen(string)-1); // only -1 because we also want to move the null-terminator
}
}
char* channel=0;
char* channelpassword=0;
const char* websocket_requirements(const char* path, const char* host, char* protocol, const char* origin)
{
if(strncmp(path, "/chat/", 6)){return 0;}
/*
if(!host || strcasecmp(host, HOSTNAME)){return 0;}
if(!origin || strcasecmp(origin, "https://" HOSTNAME "/")){printf("Bad origin: %s\n", origin); return 0;}
*/
if(!protocol || strcmp(protocol, "chat-0.1")){return 0;}
free(channel);
channel=strdup(&path[6]);
channelpassword=0;
char* options=strchr(channel, '?');;
while(options)
{
options[0]=0;
options=&options[1];
// printf("Option: %s\n", options);
if(!strncmp(options, "password=", 9)){channelpassword=&options[9];}
options=strchr(options, '&');
}
return "chat-0.1";
}
int main()
{
signal(SIGPIPE, SIG_IGN);
int sock=createserversock(4000);
db_init();
struct sockaddr addr;
socklen_t socklen;
struct pollfd* fds=malloc(sizeof(struct pollfd));
fds[0].fd=sock;
fds[0].events=POLLIN;
fds[0].revents=0;
unsigned int i;
unsigned int j;
struct websock_head head;
while(1)
{
poll(fds, usercount+1, -1);
if(fds[0].revents)
{
printf("New connection\n");
fds[0].revents=0;
socklen=sizeof(addr);
int socket=accept(sock, &addr, &socklen);
// TODO: handle accept() returning -1
adduser(socket, &addr, socklen);
fds=realloc(fds, sizeof(struct pollfd)*(usercount+1));
fds[usercount].fd=socket;
fds[usercount].events=POLLIN;
fds[usercount].revents=0;
}
for(i=0; ichannel)
{
char quitmsg[strlen("quit:0")+strlen(users[i]->nickname)];
sprintf(quitmsg, "quit:%s", users[i]->nickname);
channel_write(users[i]->channel, quitmsg, strlen(quitmsg), WEBSOCK_TEXT, users[i]);
}
freeuser(users[i]);
--usercount;
memmove(&users[i], &users[i+1], sizeof(struct user*)*(usercount-i));
memmove(&fds[i+1], &fds[i+2], sizeof(struct pollfd)*(usercount-i));
break;
}
fds[i+1].revents=0;
if(!users[i]->handshake)
{
if(users[i]->httpcam) // httpcams shouldn't speak
{
char buf[128];
if(read(users[i]->rawsocket, buf, 128)<=0) // Read it and ignore it
{
freeuser(users[i]);
--usercount;
memmove(&users[i], &users[i+1], sizeof(struct user*)*(usercount-i));
memmove(&fds[i+1], &fds[i+2], sizeof(struct pollfd)*(usercount-i));
}
continue;
}
users[i]->handshake=websock_handshake_server(users[i]->socket, websocket_requirements, httpcam_cb, use_tls);
if(users[i]->handshake)
{
uridecode(channelpassword);
struct channel* chan=getchannel(channel);;
if(chan->password && chan->password[0] && (!channelpassword || strcmp(chan->password, channelpassword)))
{
//printf("Wrong/no password given ('%s' != '%s')\n", channelpassword, chan->password);
websock_write(users[i]->socket, "password", 8, WEBSOCK_TEXT, use_tls);
freeuser(users[i]);
--usercount;
memmove(&users[i], &users[i+1], sizeof(struct user*)*(usercount-i));
memmove(&fds[i+1], &fds[i+2], sizeof(struct pollfd)*(usercount-i));
break;
}
if(channel_checkban(chan, &users[i]->sockaddr))
{
websock_write(users[i]->socket, "banned", 6, WEBSOCK_TEXT, use_tls);
freeuser(users[i]);
--usercount;
memmove(&users[i], &users[i+1], sizeof(struct user*)*(usercount-i));
memmove(&fds[i+1], &fds[i+2], sizeof(struct pollfd)*(usercount-i));
break;
}
//printf("User %u finished handshake, and passed password/ban checks, joining channel '%s'\n", i, channel);
joinchannel(users[i], chan);
char join[strlen(users[i]->nickname)+11];
sprintf(join, "startnick:%s", users[i]->nickname);
websock_write(users[i]->socket, join, strlen(join), WEBSOCK_TEXT, use_tls);
sprintf(join, "join:%s", users[i]->nickname);
struct user** chanusers=users[i]->channel->users;
unsigned int chanusercount=users[i]->channel->usercount;
unsigned int j;
for(j=0; jhandshake){continue;}
websock_write(chanusers[j]->socket, join, strlen(join), WEBSOCK_TEXT, use_tls);
if(users[i]!=chanusers[j]) // Let user know about everyone already present, TODO: do this cleaner with maybe a 'names:' command instead
{
char join[strlen(chanusers[j]->nickname)+strlen("color::ffffff0")];
sprintf(join, "join:%s", chanusers[j]->nickname);
websock_write(users[i]->socket, join, strlen(join), WEBSOCK_TEXT, use_tls);
sprintf(join, "color:%s:%s", chanusers[j]->nickname, chanusers[j]->color);
websock_write(users[i]->socket, join, strlen(join), WEBSOCK_TEXT, use_tls);
if(chanusers[j]->broadcasting)
{
sprintf(join, "mediastart:%s", chanusers[j]->nickname);
websock_write(users[i]->socket, join, strlen(join), WEBSOCK_TEXT, use_tls);
}
}
}
}
else if(httpcam_cb_cam){httpcam_handle(users[i]);}
else{
printf("User %u failed handshake\n", i);
freeuser(users[i]);
--usercount;
memmove(&users[i], &users[i+1], sizeof(struct user*)*(usercount-i));
memmove(&fds[i+1], &fds[i+2], sizeof(struct pollfd)*(usercount-i));
}
continue;
}
char status;
if(!(status=websock_readhead(users[i]->socket, &head, use_tls)) || head.opcode==WEBSOCK_CLOSE) // Disconnect
{
if(status){websock_write(users[i]->socket, 0, 0, WEBSOCK_CLOSE, use_tls);}
printf("User %u disconnected\n", i);
if(users[i]->channel)
{
char quitmsg[strlen("quit:0")+strlen(users[i]->nickname)];
sprintf(quitmsg, "quit:%s", users[i]->nickname);
channel_write(users[i]->channel, quitmsg, strlen(quitmsg), WEBSOCK_TEXT, users[i]);
}
freeuser(users[i]); // also takes care of removing user from channel
--usercount;
memmove(&users[i], &users[i+1], sizeof(struct user*)*(usercount-i));
memmove(&fds[i+1], &fds[i+2], sizeof(struct pollfd)*(usercount-i));
break;
}
unsigned char data[head.length+1];
websock_readcontent(users[i]->socket, data, &head, use_tls);
data[head.length]=0;
// Handle commands
if(head.opcode==WEBSOCK_TEXT)
{
printf("Got data from user %u (length: %llu, opcode %u): '%s'\n", i, head.length, head.opcode, data);
if(!strncmp(data, "msg:", 4))
{
unsigned int msglen=head.length+strlen(users[i]->nickname)+1;
unsigned char msg[msglen];
sprintf(msg, "msg:%s:", users[i]->nickname);
memcpy(&msg[strlen(users[i]->nickname)+5], &data[4], head.length-4);
channel_write(users[i]->channel, msg, msglen, WEBSOCK_TEXT, users[i]);
}
else if(!strncmp(data, "pm:", 3))
{
char* to=&data[3];
char* text=strchr(to, ':');
if(!text){continue;}
text[0]=0;
text=&text[1];
unsigned int msglen=snprintf(0,0,"pm:%s:%s", users[i]->nickname, text);
unsigned char msg[msglen+1];
sprintf(msg, "pm:%s:%s", users[i]->nickname, text);
struct user* recipient=channel_finduser(users[i]->channel, to);
if(!recipient || !recipient->handshake)
{
websock_write(users[i]->socket, "err:User not found", 18, WEBSOCK_TEXT, use_tls);
continue;
}
websock_write(recipient->socket, msg, msglen, WEBSOCK_TEXT, use_tls);
}
else if(!strncmp(data, "nick:", 5))
{
if(strchr(&data[5], ':'))
{
websock_write(users[i]->socket, "err:Nicknames may not contain :", 31, WEBSOCK_TEXT, use_tls);
continue;
}
if(channel_finduser(users[i]->channel, &data[5]))
{
websock_write(users[i]->socket, "err:Nickname is already taken", 29, WEBSOCK_TEXT, use_tls);
continue;
}
unsigned int msglen=head.length+strlen(users[i]->nickname)+1;
unsigned char msg[msglen];
sprintf(msg, "nick:%s:", users[i]->nickname);
memcpy(&msg[strlen(users[i]->nickname)+6], &data[5], head.length-5);
free(users[i]->nickname);
users[i]->nickname=strdup(&data[5]);
channel_write(users[i]->channel, msg, msglen, WEBSOCK_TEXT, 0);
}
if(!strncmp(data, "color:", 6) && head.length==12)
{
strcpy(users[i]->color, &data[6]);
unsigned int msglen=head.length+strlen(users[i]->nickname)+1;
unsigned char msg[msglen+1];
sprintf(msg, "color:%s:%s", users[i]->nickname, &data[6]);
channel_write(users[i]->channel, msg, msglen, WEBSOCK_TEXT, users[i]);
}
else if(!strcmp(data, "mediastart"))
{
char msg[strlen("mediastart:0")+strlen(users[i]->nickname)];
sprintf(msg, "mediastart:%s", users[i]->nickname);
channel_write(users[i]->channel, msg, strlen(msg), WEBSOCK_TEXT, users[i]);
users[i]->broadcasting=1;
}
else if(!strcmp(data, "mediastop"))
{
char msg[strlen("mediastop:0")+strlen(users[i]->nickname)];
sprintf(msg, "mediastop:%s", users[i]->nickname);
channel_write(users[i]->channel, msg, strlen(msg), WEBSOCK_TEXT, users[i]);
users[i]->broadcasting=0;
free(users[i]->firstmedia);
users[i]->firstmedia=0;
users[i]->firstmedialength=0;
user_removerelations(users[i], relation_media);
}
else if(!strncmp(data, "mediasubscribe:", 15))
{
struct user* user=channel_finduser(users[i]->channel, &data[15]);
if(!user){continue;}
user_addrelation(user, users[i], relation_media);
if(user->firstmedia)
{
char msg[strlen("media:0")+strlen(user->nickname)];
sprintf(msg, "media:%s", user->nickname);
websock_write(users[i]->socket, msg, strlen(msg), WEBSOCK_TEXT, use_tls);
websock_write(users[i]->socket, user->firstmedia, user->firstmedialength, WEBSOCK_BINARY, use_tls);
}
}
else if(!strncmp(data, "mediaunsubscribe:", 17))
{
struct user* user=channel_finduser(users[i]->channel, &data[17]);
if(!user){continue;}
user_removerelation(user, users[i], relation_media);
}
else if(!strcmp(data, "media"))
{
users[i]->bincmd=bincmd_media;
}
else if(!strncmp(data, "httpcamkey:", 11))
{
struct user* user=channel_finduser(users[i]->channel, &data[11]);
if(!user){continue;}
const char* key=httpcam_getkey(user);
char msg[strlen("httpcamkey::0")+strlen(user->nickname)+strlen(key)];
sprintf(msg, "httpcamkey:%s:%s", user->nickname, key);
websock_write(users[i]->socket, msg, strlen(msg), WEBSOCK_TEXT, use_tls);
}
else if(!strncmp(data, "login:", 6))
{
char* username=&data[6];
char* password=strchr(username, ':');
if(!password){continue;}
password[0]=0;
password=&password[1];
if(user_login(users[i], username, password))
{
// TODO: what other info does the client need? does it need to know its username?
websock_write(users[i]->socket, "loggedin", 8, WEBSOCK_TEXT, use_tls);
if(users[i]->modprivileges)
{
user_sendmodmsg(users[i]);
}
}else{
websock_write(users[i]->socket, "err:Incorrect username and/or password", 38, WEBSOCK_TEXT, use_tls);
}
}
else if(!strcmp(data, "logout"))
{
if(!users[i]->account)
{
websock_write(users[i]->socket, "err:Cannot log out, you were not logged in", 42, WEBSOCK_TEXT, use_tls);
continue;
}
if(users[i]->modprivileges)
{
users[i]->modprivileges=0;
user_sendmodmsg(users[i]);
}
free(users[i]->account);
users[i]->account=0;
websock_write(users[i]->socket, "loggedout", 9, WEBSOCK_TEXT, use_tls);
}
else if(!strncmp(data, "createaccount:", 14))
{
char* username=&data[14];
char* password=strchr(username, ':');
if(!password){continue;}
password[0]=0;
password=&password[1];
if(!db_createuser(username, password))
{
websock_write(users[i]->socket, "err:Failed to create account, maybe the username is already taken", 65, WEBSOCK_TEXT, use_tls);
}else{
websock_write(users[i]->socket, "createdaccount", 14, WEBSOCK_TEXT, use_tls);
}
}
else if(!strcmp(data, "registerchannel"))
{
if(!users[i]->account)
{
websock_write(users[i]->socket, "err:You need an account to register a channel", 45, WEBSOCK_TEXT, use_tls);
continue;
}
if(users[i]->channel->id>-1)
{
websock_write(users[i]->socket, "err:Channel is already registered", 33, WEBSOCK_TEXT, use_tls);
continue;
}
if(!channel_register(users[i]->channel, users[i]))
{
websock_write(users[i]->socket, "err:Something unexpected went wrong while registering the channel", 65, WEBSOCK_TEXT, use_tls);
}
}
else if(!strcmp(data, "listmods")) // TODO: should this be restricted to SETMODS? not that non-mods don't deserve to know who the mods are, but it seems like it might be a little inefficient
{
if(!(users[i]->modprivileges&PRIV_SETMODS)){continue;}
if(users[i]->channel->id<0)
{
websock_write(users[i]->socket, "err:The channel is not registered, it has no mods", 49, WEBSOCK_TEXT, use_tls);
continue;
}
void listcallback(int userid, int privileges) // TODO: move to global scope for compatibility with non-gcc compilers?
{
char* name=db_getusername(userid);
char msg[snprintf(0,0, "listmods:%s:%i", name, privileges)+1];
sprintf(msg, "listmods:%s:%i", name, privileges);
free(name);
websock_write(users[i]->socket, msg, strlen(msg), WEBSOCK_TEXT, use_tls);
}
db_listmods(users[i]->channel->id, listcallback);
}
else if(!strncmp(data, "removemod:", 10))
{
if(!(users[i]->modprivileges&PRIV_SETMODS))
{
websock_write(users[i]->socket, "err:Insufficient moderator privileges", 37, WEBSOCK_TEXT, use_tls);
continue;
}
if(users[i]->account && !strcmp(users[i]->account, &data[10]))
{
websock_write(users[i]->socket, "err:To avoid accidentally losing control over your channel, removing yourself as a mod is not permitted", 103, WEBSOCK_TEXT, use_tls);
continue;
}
int userid;
db_finduser(&data[10], &userid, 0, 0);
if(db_removemod(users[i]->channel->id, userid))
{
websock_write(users[i]->socket, data, head.length, WEBSOCK_TEXT, use_tls);
// See if any of the currently online users are logged in with this account and demod them if they are
for(j=0; jchannel->usercount; ++j)
{
if(!users[i]->channel->users[j]->account){continue;}
if(strcmp(users[i]->channel->users[j]->account, &data[10])){continue;}
users[i]->channel->users[j]->modprivileges=0;
user_sendmodmsg(users[i]->channel->users[j]);
}
}else{
websock_write(users[i]->socket, "err:Failed to remove mod", 24, WEBSOCK_TEXT, use_tls);
}
}
else if(!strncmp(data, "addmod:", 7))
{
char* priv=strchr(&data[7], ':');
if(!priv){continue;}
unsigned int len=(void*)priv-(void*)&data[7];
char account[len+1];
memcpy(account, &data[7], len);
account[len]=0;
if(!(users[i]->modprivileges&PRIV_SETMODS))
{
websock_write(users[i]->socket, "err:Insufficient moderator privileges", 37, WEBSOCK_TEXT, use_tls);
continue;
}
int privileges=atoi(&priv[1]);
int userid;
if(!db_finduser(account, &userid, 0, 0))
{
websock_write(users[i]->socket, "err:Account not found", 21, WEBSOCK_TEXT, use_tls);
continue;
}
if(db_addmod(users[i]->channel->id, userid, privileges))
{
websock_write(users[i]->socket, data, head.length, WEBSOCK_TEXT, use_tls);
// See if any of the currently online users are logged in with this account and mod them if they are
for(j=0; jchannel->usercount; ++j)
{
if(!users[i]->channel->users[j]->account){continue;}
if(strcmp(users[i]->channel->users[j]->account, account)){continue;}
users[i]->channel->users[j]->modprivileges=privileges;
user_sendmodmsg(users[i]->channel->users[j]);
}
}else{
websock_write(users[i]->socket, "err:Failed to add mod", 21, WEBSOCK_TEXT, use_tls);
}
}
else if(!strncmp(data, "closemedia:", 11))
{
if(!(users[i]->modprivileges&PRIV_CLOSECAM))
{
websock_write(users[i]->socket, "err:Insufficient moderator privileges", 37, WEBSOCK_TEXT, use_tls);
continue;
}
struct user* user=channel_finduser(users[i]->channel, &data[11]);
if(!user)
{
websock_write(users[i]->socket, "err:User not found", 18, WEBSOCK_TEXT, use_tls);
continue;
}
channel_write(user->channel, data, head.length, WEBSOCK_TEXT, 0);
user->broadcasting=0;
free(user->firstmedia);
user->firstmedia=0;
user->firstmedialength=0;
user_removerelations(user, relation_media);
}
else if(!strncmp(data, "whois:", 6))
{
struct user* user=channel_finduser(users[i]->channel, &data[6]);
if(!user)
{
websock_write(users[i]->socket, "err:User not found", 18, WEBSOCK_TEXT, use_tls);
continue;
}
char msg[strlen("whois::0")+strlen(user->nickname)+(user->account?strlen(user->account):0)];
if(user->account)
{
sprintf(msg, "whois:%s:%s", user->nickname, user->account);
}else{
sprintf(msg, "whois:%s", user->nickname);
}
websock_write(users[i]->socket, msg, strlen(msg), WEBSOCK_TEXT, use_tls);
}
else if(!strncmp(data, "ban:", 4))
{
if(!(users[i]->modprivileges&PRIV_KICK))
{
websock_write(users[i]->socket, "err:Insufficient moderator privileges", 37, WEBSOCK_TEXT, use_tls);
continue;
}
struct user* user=channel_finduser(users[i]->channel, &data[4]);
if(!user)
{
websock_write(users[i]->socket, "err:User not found", 18, WEBSOCK_TEXT, use_tls);
continue;
}
channel_ban(user->channel, user);
char msg[strlen("ban::0")+strlen(user->nickname)+strlen(users[i]->nickname)];
sprintf(msg, "ban:%s:%s", user->nickname, users[i]->nickname);
channel_write(user->channel, msg, strlen(msg), WEBSOCK_TEXT, 0);
close(user->rawsocket);
}
else if(!strcmp(data, "listbans"))
{
if(!(users[i]->modprivileges&PRIV_KICK)) // TODO: is there any harm in letting non-mods see the banlist?
{
websock_write(users[i]->socket, "err:Insufficient moderator privileges", 37, WEBSOCK_TEXT, use_tls);
continue;
}
for(j=0; jchannel->bancount; ++j)
{
char msg[snprintf(0,0, "listbans:%s:%u", users[i]->channel->bans[j].nickname, users[i]->channel->bans[j].id)+1];
sprintf(msg, "listbans:%s:%u", users[i]->channel->bans[j].nickname, users[i]->channel->bans[j].id);
websock_write(users[i]->socket, msg, strlen(msg), WEBSOCK_TEXT, use_tls);
}
}
else if(!strncmp(data, "unban:", 6))
{
if(!(users[i]->modprivileges&PRIV_KICK))
{
websock_write(users[i]->socket, "err:Insufficient moderator privileges", 37, WEBSOCK_TEXT, use_tls);
continue;
}
if(channel_unban(users[i]->channel, strtoul(&data[6],0,0)))
{
websock_write(users[i]->socket, data, head.length, WEBSOCK_TEXT, use_tls);
}else{
websock_write(users[i]->socket, "err:Failed to unban", 19, WEBSOCK_TEXT, use_tls);
}
}
else if(!strncmp(data, "setchanpass:", 12))
{
if(!(users[i]->modprivileges&PRIV_PASSWORD))
{
websock_write(users[i]->socket, "err:Insufficient moderator privileges", 37, WEBSOCK_TEXT, use_tls);
continue;
}
free(users[i]->channel->password);
users[i]->channel->password=strdup(&data[12]);
if(!db_setchannelpassword(users[i]->channel->name, users[i]->channel->password))
{
websock_write(users[i]->socket, "err:Failed to save password, the change will be temporary", 57, WEBSOCK_TEXT, use_tls);
}
}
}
else if(head.opcode==WEBSOCK_BINARY)
{
printf("Got binary data from user %u (length: %llu, opcode %u)\n", i, head.length, head.opcode);
switch(users[i]->bincmd)
{
case bincmd_none:
printf("Got unexpected binary data! (->bincmd=bincmd_none)\n");
break;
case bincmd_media:
if(head.length==0){break;}
if(!users[i]->broadcasting){break;}
if(!users[i]->firstmedia)
{
printf("Saving firstmedia of %llu bytes\n", head.length);
users[i]->firstmedia=malloc(head.length);
users[i]->firstmedialength=head.length;
memcpy(users[i]->firstmedia, data, head.length);
}
printf("Got video packet\n");
char msg[strlen("media:0")+strlen(users[i]->nickname)];
sprintf(msg, "media:%s", users[i]->nickname);
for(j=0; jrelationcount; ++j)
{
if(users[i]->relations[j].type!=relation_media){continue;}
if(users[i]->relations[j].user->httpcam)
{
httpcam_sendchunk(users[i]->relations[j].user->socket, data, head.length);
continue;
}
websock_write(users[i]->relations[j].user->socket, msg, strlen(msg), WEBSOCK_TEXT, use_tls);
websock_write(users[i]->relations[j].user->socket, data, head.length, WEBSOCK_BINARY, use_tls);
}
}
users[i]->bincmd=bincmd_none;
}
}
}
return 0;
}