FSD登录验证过程简析

这两天在折腾国内某平台的管制员训练服务,其实就是一个用FSD搭建的服务器。不同于一般连飞服务器,训练用的服务端需要根据管制员训练的要求需要对登录验证做些修改。尽管一直不太愿意去看FSD的源码(实在是有点乱),但也一直很想弄清楚FSD的登录验证,于是我也抱着必胜的决心开始阅读源码(为什么要说也?)。

还好,在各种文件查找和0420的帮助下,这次总算是大概摸明白了登录验证的过程:

首先是客户端发起创建新对象(Pilot or ATC)的请求,通过抓包知道格式是#AP(Pilot, 如果是ATC则是#AA)开头的一串指令,服务端涉及的部分有:

//clinterface.cpp
void clinterface::sendpacket(client *dest, client *source, absuser *exclude,
   int broad, int range, int cmd, char *data)
{
   absuser *temp;
   if (dest) if (dest->location!=myserver) return;
   for (temp=rootuser;temp;temp=temp->next) if (!temp->killflag)
   {
      client *cl=((cluser*)temp)->thisclient;
      if (!cl) continue;
      if (exclude==temp) continue;
      if (dest&&cl!=dest) continue;
      if (!(cl->type&broad)) continue;
      if (source&&(range!=-1||cmd==CL_PILOTPOS||cmd==CL_ATCPOS))
      {
         int checkrange=calcrange(source, cl, cmd, range);
         double distance=cl->distance(source);
         if (distance==-1||distance>checkrange) continue;
      }
      temp->uslprintf("%s%s\r\n", cmd==CL_ATCPOS||cmd==CL_PILOTPOS,
         clcmdnames[cmd], data);
   }
}

//cluser.cpp
char *clcmdnames[]=
{
   "#AA",
   "#DA",
   "#AP",
   "#DP",
   "$HO",
   "#TM",
   "#RW",
   "@",
   "%",
   "$PI",
   "$PO",
   "$HA",
   "$FP",
   "#SB",
   "#PC",
   "#WX",
   "#CD",
   "#WD",
   "#TD",
   "$C?",
   "$CI",
   "$AX",
   "$AR",
   "$ER",
   "$CQ",
   "$CR",
   "$!!",
   "#DL",
   NULL
};


接着,FSD会分析开头的指令来确定要进行何种操作:



//cluser.cpp
void cluser::doparse(char *s)
{
   char cmd[4], *array[100];
   snappacket(s, cmd, 3);
   int index=getcomm(cmd), count;
   if (index==-1)
   {
      showerror(ERR_SYNTAX, "");
      return;
   }
   if (!thisclient&&index!=CL_ADDATC&&index!=CL_ADDPILOT) return;

   /* Just a hack to put the pointer on the first arg here */
   s+=strlen(clcmdnames[index]);
   count=breakpacket(s,array,100);
   switch (index)
   {
      case CL_ADDATC     : execaa(array,count);  break;
      case CL_ADDPILOT   : execap(array,count);  break;
      case CL_PLAN       : execfp(array,count); break;
      case CL_RMATC      : /* Handled like RMPILOT */
      case CL_RMPILOT    : execd(array,count); break;
      case CL_PILOTPOS   : execpilotpos(array,count); break;
      case CL_ATCPOS     : execatcpos(array,count); break;
      case CL_PONG       :
      case CL_PING       : execmulticast(array,count,index,0,1); break;
      case CL_MESSAGE    : execmulticast(array,count,index,1,1); break; 
      case CL_REQHANDOFF :
      case CL_ACHANDOFF  : execmulticast(array,count,index,1,0); break;
      case CL_SB         :
      case CL_PC         : execmulticast(array,count,index,0,0); break;
      case CL_WEATHER    : execweather(array, count); break;
      case CL_REQCOM     : execmulticast(array,count,index,0,0); break;
      case CL_REPCOM     : execmulticast(array,count,index,1,0); break;
      case CL_REQACARS   : execacars(array, count); break;
      case CL_CR         : execmulticast(array, count, index, 2, 0); break;
      case CL_CQ         : execcq(array, count); break;
      case CL_KILL       : execkill(array, count); break;
      default            : showerror(ERR_SYNTAX, ""); break;
   }
}

这里的case中的值是定义在protocol.h中的,这里不再列出,接下来我们以ADDPILOT为例说明验证的过程。当index的值与CL_ADDPILOT相等时将执行execap()函数,这里传入两个参数,第一个参数(即上方的array)是含有客户端传送的CID、呼号、密码以及请求等级(后面会提到有什么作用)等信息的数组,第二个参数是客户端传送的信息数量,这里要说明一下客户端传送的格式类似于FSD生成的whazzup信息,都是以英文冒号隔开的,因此FSD还有一个专门分割信息的函数breakpacket()。然后我们来看验证的主角——execap()函数:



void cluser::execap(char **s, int count)
{
   if (thisclient)
   {
      showerror(ERR_REGISTERED, "");
      return;
   }
   if (count<8)
   {
      showerror(ERR_SYNTAX, "");
      return;
   }
   int err=callsignok(s[0]);
   if (err)
   {
      showerror(err, "");
      kill(KILL_COMMAND);
      return;
   }
   if (atoi(s[5])!=NEEDREVISION)
   {
      showerror(ERR_REVISION, "");
      kill(KILL_PROTOCOL);
      return;
   }
   int req=atoi(s[4]);
   if (req<0) req=0;
   int level=checklogin(s[2], s[3], req);
   if (level<0)
   {
      kill(KILL_COMMAND);
      return;
   }
   else if (level==0)
   {
      showerror(ERR_CSSUSPEND, "");
      kill(KILL_COMMAND);
      return;
   }
   if (level<req)
   {
      showerror(ERR_LEVEL, s[4]);
      kill(KILL_COMMAND);
      return;
   }
   thisclient=new client(s[2], myserver, s[0], CLIENT_PILOT, level, s[4], s[7],
      atoi(s[6]));
   serverinterface->sendaddclient("*",thisclient, NULL, this, 0);
   readmotd();
}

我们从第13行开始看,这里声明了一个变量err,并将callsignok()函数的返回值赋给它,从函数名上不难看出这是用来检查呼号的,个人认为某平台的各种呼号格式要求就是在这里做的,有兴趣的可以研究一下,也在cluser.cpp里。


然后我们跳到int req=atoi(s[4]);这里,这个就是前文标注的请求级别,当客户端请求建立新PILOT对象时,它的值为1(即OBS级别),而ATC则根据Rating选择不同而有差异。随后调用了checklogin()函数,并把返回值赋给level,level是什么?别急,我们先来看看checklogin()和它牵扯到的其他函数:


//cluser.cpp
int cluser::checklogin(char *id, char *pwd, int req)
{
   if (id[0]=='\0') return -2;
   int max, ok=maxlevel(id, pwd, &max);
   if (!ok)
   {
      showerror(ERR_CIDINVALID, id);
      return -1;
   }
   return req>max?max:req;
}

//certificate.cpp
int maxlevel(char *id, char *p, int *max)
{
   certificate *temp=getcert(id);
   if (!temp)
   {
      *max=LEV_OBSPILOT;
      return 0;
   }
   if (!STRCASECMP(temp->password,p))
   {
      *max=temp->level;
      temp->prevvisit=time(NULL);
      return 1;
   }
   *max=LEV_OBSPILOT;
   return 0;
}

checklogin会首先检查CID是否合法,为空则返回-2,随后调用了maxlevel函数,这个函数会到cert文件(默认是cert.txt)中读取该CID的信息(密码和等级),如果密码错误会返回0,密码正确则将checklogin中的max变量的值修改为等级,同时返回1,完成后maxlevel返回的是请求等级和CID具有等级中的较小者,没错,这个返回值就是本次登录该CID在服务器上具有的等级,这决定了该CID拥有的权限,了解了这些,继续看完execap应该没有太大问题了。


添加新评论