/*Coco szerver; Skrabák Csaba*/
/*első implementáció TCP feletti kommunikációval, nem blokkolódó, egyszálú
  megoldás select rendszerhívás segítségével*/

/*-------szükséges header-fájlok beinklúdolása*/

#include <ctype.h>
#include <sys/time.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

/*-------állandók*/

/*továbbítható legnagyobb üzenet mérete+1, fogadó puffer mérete*/
#define MSG_BUF_SIZE 1024

/*kiszolgálható kliensek maximális száma*/
#define SESS_MAX 24

/*szerver port, amin a kliensek konnektálódását várja*/
#define SVR_PORT 7061

/*elfogadásra váró összeköttetések maximuma (a listen rendszerhíváshoz)*/
#define BACKLOG 8

/*ennek definiálva kéne lennie, de az én linuxomon pl. nincs*/
#ifndef UNIX_PATH_MAX
#define UNIX_PATH_MAX 108
#endif

/*-------objektumosztályok*/

/*egy-egy összerendelés-példány (session) feladata tárolni, ha egy adott
  felhasználó ennek a szervernek egy adott coco-oldalát figyeli*/
class Sess{
    //time_t lastActionTimestamp;
    //time_t lastDealyTime;

  public:
    char fileName[UNIX_PATH_MAX+1]; //mit figyel
    int fd; //a megfelelő klienssocket leírója
    
    /*konstruktor, amit új kapcsolat felépítésekor hívunk meg*/
    Sess(int svrFd){
#ifndef QUIET
      printf("accepting connection... ");
#endif
      fd=accept(svrFd,NULL,NULL); //elfogad
#ifndef QUIET
      printf("new client number is %d\n",fd);
#endif
      fcntl(fd,F_SETFL,fcntl(fd,F_GETFL)|O_NONBLOCK); //nem blokkol
      fileName[0]='\0'; //egyelőre nincs megadva a figyelt coco-oldal neve
    };
    
    /*destruktor, értelemszerűen*/
    ~Sess(){
      close(fd); //socket lezárása
    };
    
    /*WATCH parancsra hívjuk meg ezt itt*/
    void setFileName(char*p){
      /*buffer overloadra is vigyázva beírjuk a megadott fájlnevet*/
      strncpy(fileName,p,UNIX_PATH_MAX);
      fileName[UNIX_PATH_MAX]='\0';
    };
};

/*összerendelések listáját tárolja ez*/
class SessList{
    /*a lista, amelyet ritka tömb tárol: lehetnek benne NULL elemek*/
    Sess*sess[SESS_MAX];

    /*hol tart az utolsó lekérdezés; a fenti tömböt indexeli ciklikusan*/
    int tokenAt;
    
    /*privát; lépteti a tokent a következő elemre (nem feltétlenül eggyel:
      a legközelebbi értelmes bejegyzés indexére ugrik); ha a lista
      végére ér anélkül, hogy talált volna értelmes bejegyzést, ezt a
      visszatérési értékkel jelzi, és a token értéke érvénytelen lesz;
      érvénytelen token mellett hívva az első elemre próbál ugrani,
      egyébként ugyanúgy működik*/
    int passToken();
    
    /*a lista összerendeléseinek a fájlleírói közül a legnagyobbnak az értéke*/
    int maxFd;
    
    /*a legkisebb szám a világon; a maximumkeresést inicializáljuk ezzel*/
    int neg_inf;
    
  public:
    /*konstruktor*/
    SessList(int neg_inf){
      /*a lista kezdetben üres*/
      for(int i=0;i<SESS_MAX;i++)
        sess[i]=NULL;
      
      /*üres lista esetén érvénytelen a token értéke ("senkinél sincs")*/
      tokenAt=SESS_MAX;
      
      /*a paraméterben adott számnál kisebb sosem lesz a maxFd*/
      this->neg_inf=neg_inf;
      maxFd=neg_inf;
    };
    
    /*destruktor, takarít*/
    ~SessList(){
      for(int i=0;i<SESS_MAX;i++)
        if(sess[i]!=NULL)delete sess[i];
    };

    /*az adott összerendelés-példányt felveszi a listába, majd 1-gyel tér
      vissza, ha van hely; nem vesz fel semmit, és 0-val tér vissza, ha
      tele van; nem foglal le memóriát*/
    int add(Sess*sess);

    /*az adott bejegyzés eltávolítása a listáról; nem szabadít fel memóriát*/
    void remove(Sess*watcher);

    /*passzolja a tokent az adott coco-oldalt figyelő felhasználók listájának
      a következő (lekérdezésenként az első hívás esetén az első) eleméhez,
      paraméteren keresztül visszaadja az elemet, és visszatér 1-gyel
      általában; ha nem talált már ilyen elemet, és a token túlfutott a listán,
      akkor a paraméterben nem ad vissza semmit, és 0-val tér vissza*/
    int getNextWatcher(Sess*&watcher,char fileName[]);
    
    /*a teljes összerendelés-listából adja vissza a következő elemet, egyébként
      ugyanúgy, mint fent*/
    int getNextWatcher(Sess*&watcher);
    
    /*új lekérdezés indításakor kell meghívni*/
    void resetToken(){
      /*a token értéke érvénytelen, így a passzolás első hívása után az első
        elemmel kezdődik a lekérdezés*/
      tokenAt=SESS_MAX;
    };
    
    /*tokenérték elmentésére való, akkor kell hívi, ha felfüggesztünk egy
      lekérdezést, amíg egy másik lekérdezést hajtunk végre, de folytatni
      akarjuk utána*/
    int getToken(){
      return tokenAt;
    };
    
    /*elmentett tokenérték visszaállítására használható*/
    void setToken(int i){
      tokenAt=i;
    };
    
    /*baromállatság, a select rendszerhíváshoz kell*/
    int getMaxFd(){
      return maxFd;
    };
};


/*-------a főprogram*/

int main(int argc,char*argv[]){
  /*a szerversocket leírója*/
  int svrFd;

  /*a továbbítandó üzenet*/
  char msgBuf[MSG_BUF_SIZE];
  
  /*idiótaságok a select rendszerhívás kedvéért*/
  fd_set toListenTo; //a Halmaz, aminek az értéke megmarad
  fd_set set; //amit a selectnek átadunk, hadd variálja
  int n; //the highest-numbered descriptor in any of the three sets, plus 1

  struct sockaddr_in svrAddr; //cím a szerversocketnek
  
  /*hányan vannak*/
  int readSocks;
  
  /*a szerversocket megnyitása*/
  svrFd=socket(AF_INET,SOCK_STREAM,0);

  if(svrFd<0){ //hiba
    perror("opening server socket");
    exit(1);
  }
  
  /*nem blokkolódóra történő állítás*/
  fcntl(svrFd,F_SETFL,fcntl(svrFd,F_GETFL)|O_NONBLOCK);
  
  /*címmezők kitöltése a szerversocketnek megfelelően*/
  memset((char*)&svrAddr,0,sizeof(svrAddr)); //először nullákkal
  svrAddr.sin_family=AF_INET; //aztán a család
  svrAddr.sin_addr.s_addr=htonl(INADDR_ANY); //fogadható ip-címek
  svrAddr.sin_port=htons(SVR_PORT); //portszám
  
  /*bájndolás következik*/
  if(bind(svrFd,(struct sockaddr*)&svrAddr,sizeof(svrAddr))<0){
    perror("binding server socket to port");
    exit(2);
  }
  
  /*listen rendszerhívás jön*/
  if(listen(svrFd,BACKLOG)<0){
    perror("calling listen");
    exit(3);
  }

  /*az elfogadott kapcsolatoknak megfelelő összerendelések listája*/
  SessList sessList(svrFd);
  
  /*a Halmaz inicializálása*/
  FD_ZERO(&toListenTo); //üres halmazból indul ki

  /*kezdetben benne van a szerversocket, hogy észrevegye a kliensek
    kapcsolódási szándékát*/
  FD_SET(svrFd,&toListenTo); 
  
  /*chroot-olas*/
  char cwd[UNIX_PATH_MAX];
  if(getcwd(cwd,UNIX_PATH_MAX)==NULL||chroot(cwd)<0){
    perror("setting root to working dir");
  }
  
  /*indul a főciklus*/
  while(1){
#ifdef DEBUG
    printf("\n-------\n\n[calling select]\n");
#endif
    /*a lényeg, a select rendszerhívás és előkészületei*/
    set=toListenTo;
    n=sessList.getMaxFd()+1;
    readSocks=select(n,&set,NULL,NULL,NULL);
    
    /*eredmény kiértékelése*/
    if(readSocks==-1){ //hiba van
      perror("calling select");
      exit(4);
    }else if(readSocks){
      /*amikor új kliens kapcsolódik*/
      if(FD_ISSET(svrFd,&set)){
        //létrehoz egy összerendelés-példányt, ezáltal egy klienssocketet
        Sess*ns=new Sess(svrFd); 
	
        if(sessList.add(ns)){ //az összerendelést listára veszi, ha van hely
          FD_SET(ns->fd,&toListenTo); //ezentúl erre is figyelni kell
	  char hello[23];
	  sprintf(hello,"HELLO %-16d\n",ns->fd);
          send(ns->fd,hello,23,0); //visszaigazolja a kapcsolatot
	}else{ //ha nincs hely, hibaüzenetet küld és bezárja
	  send(ns->fd,"FULL\n",5,0);
#ifndef QUIET
	  printf("client connection refused, server is full\n");
#endif
	  delete ns;
	}
      }
      
      /*jött-e üzenet valakitől: próbálgatós játék*/
      sessList.resetToken(); //lekérdezés inicializálása a bizt. kedv.
      Sess*s; //a küldő összerendelés-példánya lesz ebben, ha megtaláljuk
      
      /*egy nagy lekérdezéssel végigpróbálgatjuk a szerver összes kliensét,
        hátha épp az írta azt a titokzatos üzenetet (select rulzik)*/
#ifdef DEBUG
      printf("big query started...\n");
#endif
      while(sessList.getNextWatcher(s)){
#ifdef DEBUG
        printf("check whether client #%d instance of senders\n",s->fd);
#endif
        if(FD_ISSET(s->fd,&set)){ //megtaláltuk a küldőt
#ifdef DEBUG
          printf("client #%d found as sender\n",s->fd);
#endif
  
	  /*beolvasás illetve hibajelzés*/
	  int r=recv(s->fd,msgBuf,MSG_BUF_SIZE-1,0);
	  
	  if(r==0){ //hibajelzés van, a kapcsolat megszakadt
#ifndef QUIET
            printf("client #%d disconnected\n",s->fd);
#endif
	    sessList.remove(s);
	    FD_CLR(s->fd,&toListenTo);
	    delete s;
	  }else if(r>0){ //nem hibajelzéssel tért vissza a riszív, üzenet jött
	    //lezárjuk a socketet, ha ki akar lépni
	    if(!strncmp("LEAVE",msgBuf,5)){
#ifndef QUIET
              printf("*** client #%d leaving\n",s->fd);
#endif
  	      sessList.remove(s);
	      FD_CLR(s->fd,&toListenTo);
	      delete s;
	    }
	    
	    //beállítjuk a fájlnevet, ha WATCH kérés jött
	    else if(!strncmp("WATCH",msgBuf,5)){
  	      msgBuf[r-1]='\0';
  	      if(msgBuf[r-2]=='\r')msgBuf[r-2]='\0'; //soremelés helyett string vége
	      s->setFileName(msgBuf+6);
#ifndef QUIET
	      printf("*** client #%d watches %s\n",s->fd,s->fileName);
#endif
	    }
	    
	    //VERSION kérésre válaszol
	    else if(!strncmp("VERSION",msgBuf,7)){
  	      msgBuf[r-1]='\0';
  	      if(msgBuf[r-2]=='\r')msgBuf[r-2]='\0'; //soremelés helyett string vége
#ifndef QUIET
	      printf("*** client #%d asks version\n",s->fd);
#endif
	      
	      //ha a szerver tudna verziót kezelni, itt bonyolultabb lenne
	      send(s->fd,"VERSION 0\n",10,0);
	    }
	    
	    //DOWNLOAD kérésre letölt (háttérben) egy fájlt a szerverről
	    else if(!strncmp("DOWNLOAD",msgBuf,8)){
              msgBuf[8]='.';
  	      msgBuf[r-1]='\0';
  	      if(msgBuf[r-2]=='\r')msgBuf[r-2]='\0'; //soremelés helyett string vége
#ifndef QUIET
	      printf("*** client #%d downloads %s\n",s->fd,msgBuf+8);
#endif
	      int pid=fork();
	      if(pid<0){
	        perror("calling fork");
	      }else if(pid==0){ /*ez a gyerekfolyamat*/
		char buf[1024];
		int len;
		int localfd;
		struct stat localstatus;

		/*megnyitás*/
		if((localfd=open(msgBuf+8,O_RDONLY))<0){
		  perror("opening local file");
		  return 1; //gyerekfolyamat vége
		}

		/*méret lekérdezése*/
		if(fstat(localfd,&localstatus)<0){
		  perror("retrieving inode");
		  return 2; //gyerekfolyamat vége
		}

		/*fejléc átküldése*/
		sprintf(buf,"FILE %d\n",(int)localstatus.st_size);
		len=strlen(buf);
		if(write(s->fd,buf,len)!=len){
		  perror("sending file header");
		  return 3; //gyerekfolyamat vége
		}

		/*részleges olvasás és írás*/
		while((len=read(localfd,buf,sizeof(buf)))>0){
		  if(write(s->fd,buf,len)!=len){
		    perror("sending file");
		    return 4; //gyerekfolyamat vége
		  }
		}
		if(len<0){
		  perror("reading local file");
		  return 5; //gyerekfolyamat vége
		}

		/*soremelés átküldése a sorpufferelő kliensek kedvéért*/
		sprintf(buf,"\n");
		len=strlen(buf);

		/*zárás*/
		close(localfd);
		return 0; //gyerekfolyamat vége
	      }else{
	        /*ez a szülőfolyamat*/
	      }
	    }
	    
	    //elküldjük a megfelelő nézőknek, ha coco-parancsok jöttek
	    else if(!strncmp("REFRESH",msgBuf,7)){
#ifndef QUIET
	      printf("*** page %s has been changed by client #%d\n",s->fileName,s->fd);
#endif
	      int savedToken=sessList.getToken(); //token elmentése
#ifdef DEBUG_MORE
	      printf("<token saved>\n");
#endif
	      sessList.resetToken(); //belső kicsi lekérdezés indul
	      Sess*w; //ennek fogja küldeni

#ifdef DEBUG	      
	      printf("starting small query...\n");
#endif
	      /*az érintett nézők listáján megy végig ez a lekérdezés*/
	      while(sessList.getNextWatcher(w,s->fileName)){
	        /*mindnek elküldjük ugyanazt a parancsot: az egészet, ami
		  jött, úgy, ahogy van*/
#ifdef DEBUG
		printf("sending");
		printf(" to client #%d watching %s msg=\"%s\" len=%d\n",w->fd,w->fileName,msgBuf,r);
#endif
	        send(w->fd,msgBuf,r,0);
	      }
	      
	      /*token visszaállítása, mehet tovább a külső lekérdezés*/
	      sessList.setToken(savedToken);
#ifdef DEBUG_MORE
	      printf("<token retrieved>\n");
#endif
	    }
	    
	    //egyéb parancsokat ismeretlenként kezel
	    else{
#ifndef QUIET
	      printf("*** unknown message %s from client #%d\n",msgBuf,s->fd);
#endif

/*Na most! 7. szinten minden bezárva, ez elég brutál lesz!*/

	    } /*ismeretlen parancsot kezelő blokk vége*/
	  } /*üzenet érkezését kezelő blokk vége*/
	} /*amikor szerepel valamelyik néző a Halmazban, na, annak itt vége*/
      } /*a szerver minden nézőjét végigpróbáló lekérdező ciklus vége*/
    } /*annak az esetnek, amikor nincs hiba, lehet riszívni, annak a vége*/
  } /*a végtelen főciklus vége*/
} /*a főprogram vége; huh*/

/*-------metódusok implementációja*/

int SessList::passToken(){
#ifdef DEBUG_MORE
  printf("token picked up from place %d\n",tokenAt);
#endif

  /*lépteti a tokent, amíg értelmes bejegyzést nem talál*/
  do{
    /*elölről kezdi, ha vége volt*/
    if(tokenAt>=SESS_MAX)tokenAt=0;
    else{
      tokenAt++;
    
      if(tokenAt>=SESS_MAX){ //a token értéke kifutott a világból
#ifdef DEBUG_MORE
        printf("token ran out of bounds\n");
#endif
        return 0;
      }	
    }
  }while(sess[tokenAt]==NULL);
  
#ifdef DEBUG_MORE
  printf("token put down at place %d, client #%d watching %s\n",tokenAt,sess[tokenAt]->fd,sess[tokenAt]->fileName);
#endif
  return 1;
}

int SessList::add(Sess*s){
  for(int i=0;i<SESS_MAX;i++){ //üres helyet keres
    if(sess[i]==NULL){ //talált egyet
      sess[i]=s; //beaddolja oda
      if(maxFd<s->fd)maxFd=s->fd; //maximum karbantartása
#ifdef DEBUG
      printf("client #%d added\n",s->fd);
#endif
      return 1; //örülünk
    }
  }
  
  /*ha nem talált helyet, sírunk*/
  return 0;
}
    
void SessList::remove(Sess*watcher){
  /*az eddig talált legnagyobb fájlleíró*/
  int maxFd=neg_inf;
  
  /*robusztusság: végigkeresi az egészet, és az összes egyezőt kiirtja,
    ha több van; pedig elvileg soha nincs több egyezés; nagyságrendileg
    nem lassít, átlagosan kétszeres időigényű algoritmust eredményez;
    a hülye maxFd miatt az egészet végig kell nézni amúgy is*/
  for(int i=0;i<SESS_MAX;i++){
    Sess*s=sess[i];
    if(s!=NULL){
      if(s==watcher){
        sess[i]=NULL;
#ifdef DEBUG
	printf("client removed\n");
#endif
      }else if(maxFd<s->fd)maxFd=s->fd;
    }
  }
  this->maxFd=maxFd;
}
    
int SessList::getNextWatcher(Sess*&watcher,char fileName[]){
  /*passzolja a tokent az adott coco-oldalt figyelő felhasználók listájának
    a következő (lekérdezésenként az első hívás esetén az első) eleméhez*/
#ifdef DEBUG_MORE
  printf("passing over token to next watcher of %s...\n",fileName);
#endif
  do{
    /*első közelítésben passzol simán a következő bármilyen elemhez*/
#ifdef DEBUG_MORE
    printf("immediately passing token...\n");
#endif
    if(!passToken()) 
      /*nincs több megfelelő elem a tömbben (a tokentől balra nem számít,
        csak az az elem, amelyiknél a token van, és attól jobbra)*/
      return 0; 

  /*ha az adott fájlnévre nem illeszkedő elemnél van a token, akkor azok
    továbbpasszolják a tokent az első illeszkedőig: csak az adott coco-oldalt
    nézők listájában keresünk*/
  }while(strncmp(sess[tokenAt]->fileName,fileName,UNIX_PATH_MAX));

  /*na, most a token egy illeszkedő elemnél van: visszaadja paraméterben
    cím szerint ezt az elemet*/
  watcher=sess[tokenAt];
  
  /*ha idáig eljut, akkor 1-gyel tér vissza*/
  return 1;
}
    
int SessList::getNextWatcher(Sess*&watcher){
  /*fájlneveket nem vizsgáló változat*/
#ifdef DEBUG_MORE
  printf("passing token...\n");
#endif
  if(!passToken())return 0;
#ifdef DEBUG_MORE
  printf("returning watcher...\n");
#endif
  watcher=sess[tokenAt];
  return 1;
}

