Cleaning up client, cleaning coke code
[tpg/opendispense2.git] / src / client / main.c
1 /*
2  * OpenDispense 2 
3  * UCC (University [of WA] Computer Club) Electronic Accounting System
4  * - Dispense Client
5  *
6  * main.c - Core and Initialisation
7  *
8  * This file is licenced under the 3-clause BSD Licence. See the file
9  * COPYING for full details.
10  */
11 #include <stdlib.h>
12 #include <stdio.h>
13 #include <string.h>
14 #include <ctype.h>      // isspace
15 #include <stdarg.h>
16 #include <regex.h>
17 #include <ncurses.h>
18
19 #include <unistd.h>     // close
20 #include <netdb.h>      // gethostbyname
21 #include <pwd.h>        // getpwuids
22 #include <sys/socket.h>
23 #include <netinet/in.h>
24 #include <arpa/inet.h>
25 #include <openssl/sha.h>        // SHA1
26
27 #define USE_NCURSES_INTERFACE   0
28
29 // === TYPES ===
30 typedef struct sItem {
31         char    *Ident;
32         char    *Desc;
33          int    Price;
34 }       tItem;
35
36 // === PROTOTYPES ===
37 // --- GUI ---
38  int    ShowNCursesUI(void);
39 void    ShowItemAt(int Row, int Col, int Width, int Index);
40 void    PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1, const char *Mid, char Pad2, const char *Right, ...);
41 // --- Coke Server Communication ---
42  int    OpenConnection(const char *Host, int Port);
43  int    Authenticate(int Socket);
44 void    PopulateItemList(int Socket);
45  int    DispenseItem(int Socket, int ItemID);
46 // --- Helpers ---
47  int    sendf(int Socket, const char *Format, ...);
48 char    *trim(char *string);
49  int    RunRegex(regex_t *regex, const char *string, int nMatches, regmatch_t *matches, const char *errorMessage);
50 void    CompileRegex(regex_t *regex, const char *pattern, int flags);
51
52 // === GLOBALS ===
53 char    *gsDispenseServer = "localhost";
54  int    giDispensePort = 11020;
55 tItem   *gaItems;
56  int    giNumItems;
57 regex_t gArrayRegex, gItemRegex, gSaltRegex;
58
59 char    *gsOverrideUser;        //!< '-u' argument (dispense as another user)
60  int    gbUseNCurses = 0;       //!< '-G' Use the NCurses GUI?
61
62 // === CODE ===
63 int main(int argc, char *argv[])
64 {
65          int    sock;
66          int    i;
67         char    buffer[BUFSIZ];
68         
69         // -- Create regular expressions
70         // > Code Type Count ...
71         CompileRegex(&gArrayRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+([0-9]+)", REG_EXTENDED);     //
72         // > Code Type Ident Price Desc
73         CompileRegex(&gItemRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+([A-Za-z0-9:]+?)\\s+([0-9]+)\\s+(.+)$", REG_EXTENDED);
74         // > Code 'SALT' salt
75         CompileRegex(&gSaltRegex, "^([0-9]{3})\\s+(.+)\\s+(.+)$", REG_EXTENDED);
76         
77         // Connect to server
78         sock = OpenConnection(gsDispenseServer, giDispensePort);
79         if( sock < 0 )  return -1;
80
81         // Authenticate
82         Authenticate(sock);
83
84         // Parse Arguments
85         for( i = 1; i < argc; i ++ )
86         {
87                 char    *arg = argv[i];
88                 
89                 if( arg[0] == '-' )
90                 {                       
91                         switch(arg[1])
92                         {
93                         case 'u':       // Override User
94                                 gsOverrideUser = argv[++i];
95                                 break;
96                         
97                         case 'G':       // Use GUI
98                                 gbUseNCurses = 1;
99                                 break;
100                         }
101
102                         continue;
103                 }
104                 
105                 if( strcmp(argv[1], "acct") == 0 ) {
106                         // Alter account
107                         // List accounts
108                         return 0;
109                 }
110                 else {
111                         // Item name / pattern
112                 }
113         }
114
115         // Get items
116         PopulateItemList(sock);
117         
118         if( gbUseNCurses )
119         {
120                 i = ShowNCursesUI();
121         }
122         else
123         {
124                 for( i = 0; i < giNumItems; i ++ ) {            
125                         printf("%2i %s\t%3i %s\n", i, gaItems[i].Ident, gaItems[i].Price, gaItems[i].Desc);
126                 }
127                 printf(" q Quit\n");
128                 for(;;)
129                 {
130                         char    *buf;
131                         
132                         i = -1;
133                         
134                         fgets(buffer, BUFSIZ, stdin);
135                         
136                         buf = trim(buffer);
137                         
138                         if( buf[0] == 'q' )     break;
139                         
140                         i = atoi(buf);
141                         
142                         if( i != 0 || buf[0] == '0' )
143                         {
144                                 if( i < 0 || i >= giNumItems ) {
145                                         printf("Bad item %i (should be between 0 and %i)\n", i, giNumItems);
146                                         continue;
147                                 }
148                                 break;
149                         }
150                 }
151         }
152         
153         // Check for a valid item ID
154         if( i >= 0 )
155                 DispenseItem(sock, i);
156
157         close(sock);
158
159         return 0;
160 }
161
162 // -------------------
163 // --- NCurses GUI ---
164 // -------------------
165 /**
166  * \brief Render the NCurses UI
167  */
168 int ShowNCursesUI(void)
169 {
170         // TODO: ncurses interface (with separation between item classes)
171         // - Hmm... that would require standardising the item ID to be <class>:<index>
172         // Oh, why not :)
173          int    ch;
174          int    i, times;
175          int    xBase, yBase;
176         const int       displayMinWidth = 40;
177         const int       displayMinItems = 8;
178         char    *titleString = "Dispense";
179          int    itemCount = displayMinItems;
180          int    itemBase = 0;
181          int    currentItem = 0;
182          int    ret = -2;       // -2: Used for marking "no return yet"
183          
184          int    height, width;
185          
186         // Enter curses mode
187         initscr();
188         raw(); noecho();
189         
190         // Get item count
191         // - 6: randomly chosen (Need at least 3)
192         itemCount = LINES - 6;
193         if( itemCount > giNumItems )
194                 itemCount = giNumItems;
195         
196         // Get dimensions
197         height = itemCount + 3;
198         width = displayMinWidth;
199         
200         // Get positions
201         xBase = COLS/2 - width/2;
202         yBase = LINES/2 - height/2;
203         
204         for( ;; )
205         {
206                 // Header
207                 PrintAlign(yBase, xBase, width, "/", '-', titleString, '-', "\\");
208                 
209                 // Items
210                 for( i = 0; i < itemCount; i ++ )
211                 {
212                         move( yBase + 1 + i, xBase );
213                         
214                         if( currentItem == itemBase + i ) {
215                                 printw("| -> ");
216                         }
217                         else {
218                                 printw("|    ");
219                         }
220                         
221                         // Check for ... row
222                         // - Oh god, magic numbers!
223                         if( i == 0 && itemBase > 0 ) {
224                                 printw("   ...");
225                                 times = width-1 - 8 - 3;
226                                 while(times--)  addch(' ');
227                         }
228                         else if( i == itemCount - 1 && itemBase < giNumItems - itemCount ) {
229                                 printw("   ...");
230                                 times = width-1 - 8 - 3;
231                                 while(times--)  addch(' ');
232                         }
233                         // Show an item
234                         else {
235                                 ShowItemAt( yBase + 1 + i, xBase + 5, width - 7, itemBase + i);
236                                 addch(' ');
237                         }
238                         
239                         // Scrollbar (if needed)
240                         if( giNumItems > itemCount ) {
241                                 if( i == 0 ) {
242                                         addch('A');
243                                 }
244                                 else if( i == itemCount - 1 ) {
245                                         addch('V');
246                                 }
247                                 else {
248                                          int    percentage = itemBase * 100 / (giNumItems-itemCount);
249                                         if( i-1 == percentage*(itemCount-3)/100 ) {
250                                                 addch('#');
251                                         }
252                                         else {
253                                                 addch('|');
254                                         }
255                                 }
256                         }
257                         else {
258                                 addch('|');
259                         }
260                 }
261                 
262                 // Footer
263                 PrintAlign(yBase+height-2, xBase, width, "\\", '-', "", '-', "/");
264                 
265                 // Get input
266                 ch = getch();
267                 
268                 if( ch == '\x1B' ) {
269                         ch = getch();
270                         if( ch == '[' ) {
271                                 ch = getch();
272                                 
273                                 switch(ch)
274                                 {
275                                 case 'B':
276                                         //if( itemBase < giNumItems - (itemCount) )
277                                         //      itemBase ++;
278                                         if( currentItem < giNumItems - 1 )
279                                                 currentItem ++;
280                                         if( itemBase + itemCount - 1 <= currentItem && itemBase + itemCount < giNumItems )
281                                                 itemBase ++;
282                                         break;
283                                 case 'A':
284                                         //if( itemBase > 0 )
285                                         //      itemBase --;
286                                         if( currentItem > 0 )
287                                                 currentItem --;
288                                         if( itemBase + 1 > currentItem && itemBase > 0 )
289                                                 itemBase --;
290                                         break;
291                                 }
292                         }
293                         else {
294                                 
295                         }
296                 }
297                 else {
298                         switch(ch)
299                         {
300                         case '\n':
301                                 ret = currentItem;
302                                 break;
303                         case 'q':
304                                 ret = -1;       // -1: Return with no dispense
305                                 break;
306                         }
307                         
308                         // Check if the return value was changed
309                         if( ret != -2 ) break;
310                 }
311                 
312         }
313         
314         
315         // Leave
316         endwin();
317         return ret;
318 }
319
320 /**
321  * \brief Show item \a Index at (\a Col, \a Row)
322  * \note Part of the NCurses UI
323  */
324 void ShowItemAt(int Row, int Col, int Width, int Index)
325 {
326          int    _x, _y, times;
327         char    *name;
328          int    price;
329         
330         move( Row, Col );
331         
332         if( Index < 0 || Index >= giNumItems ) {
333                 name = "OOR";
334                 price = 0;
335         }
336         else {
337                 name = gaItems[Index].Desc;
338                 price = gaItems[Index].Price;
339         }
340
341         printw("%02i %s", Index, name);
342         
343         getyx(stdscr, _y, _x);
344         // Assumes max 4 digit prices
345         times = Width - 4 - (_x - Col); // TODO: Better handling for large prices
346         while(times--)  addch(' ');
347         printw("%4i", price);
348 }
349
350 /**
351  * \brief Print a three-part string at the specified position (formatted)
352  * \note NCurses UI Helper
353  * 
354  * Prints \a Left on the left of the area, \a Right on the righthand side
355  * and \a Mid in the middle of the area. These are padded with \a Pad1
356  * between \a Left and \a Mid, and \a Pad2 between \a Mid and \a Right.
357  * 
358  * ::printf style format codes are allowed in \a Left, \a Mid and \a Right,
359  * and the arguments to these are read in that order.
360  */
361 void PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1,
362         const char *Mid, char Pad2, const char *Right, ...)
363 {
364          int    lLen, mLen, rLen;
365          int    times;
366         
367         va_list args;
368         
369         // Get the length of the strings
370         va_start(args, Right);
371         lLen = vsnprintf(NULL, 0, Left, args);
372         mLen = vsnprintf(NULL, 0, Mid, args);
373         rLen = vsnprintf(NULL, 0, Right, args);
374         va_end(args);
375         
376         // Sanity check
377         if( lLen + mLen/2 > Width/2 || mLen/2 + rLen > Width/2 ) {
378                 return ;        // TODO: What to do?
379         }
380         
381         move(Row, Col);
382         
383         // Render strings
384         va_start(args, Right);
385         // - Left
386         {
387                 char    tmp[lLen+1];
388                 vsnprintf(tmp, lLen+1, Left, args);
389                 addstr(tmp);
390         }
391         // - Left padding
392         times = Width/2 - mLen/2 - lLen;
393         while(times--)  addch(Pad1);
394         // - Middle
395         {
396                 char    tmp[mLen+1];
397                 vsnprintf(tmp, mLen+1, Mid, args);
398                 addstr(tmp);
399         }
400         // - Right Padding
401         times = Width/2 - mLen/2 - rLen;
402         while(times--)  addch(Pad2);
403         // - Right
404         {
405                 char    tmp[rLen+1];
406                 vsnprintf(tmp, rLen+1, Right, args);
407                 addstr(tmp);
408         }
409 }
410
411 // ---------------------
412 // --- Coke Protocol ---
413 // ---------------------
414 int OpenConnection(const char *Host, int Port)
415 {
416         struct hostent  *host;
417         struct sockaddr_in      serverAddr;
418          int    sock;
419         
420         host = gethostbyname(Host);
421         if( !host ) {
422                 fprintf(stderr, "Unable to look up '%s'\n", Host);
423                 return -1;
424         }
425         
426         memset(&serverAddr, 0, sizeof(serverAddr));
427         
428         serverAddr.sin_family = AF_INET;        // IPv4
429         // NOTE: I have a suspicion that IPv6 will play sillybuggers with this :)
430         serverAddr.sin_addr.s_addr = *((unsigned long *) host->h_addr_list[0]);
431         serverAddr.sin_port = htons(Port);
432         
433         sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
434         if( sock < 0 ) {
435                 fprintf(stderr, "Failed to create socket\n");
436                 return -1;
437         }
438         
439         #if USE_AUTOAUTH
440         {
441                 struct sockaddr_in      localAddr;
442                 memset(&localAddr, 0, sizeof(localAddr));
443                 localAddr.sin_family = AF_INET; // IPv4
444                 localAddr.sin_port = 1023;      // IPv4
445                 // Attempt to bind to low port for autoauth
446                 bind(sock, &localAddr, sizeof(localAddr));
447         }
448         #endif
449         
450         if( connect(sock, (struct sockaddr *) &serverAddr, sizeof(serverAddr)) < 0 ) {
451                 fprintf(stderr, "Failed to connect to server\n");
452                 return -1;
453         }
454         
455         return sock;
456 }
457
458 /**
459  * \brief Authenticate with the server
460  * \return Boolean Failure
461  */
462 int Authenticate(int Socket)
463 {
464         struct passwd   *pwd;
465         char    buf[512];
466          int    responseCode;
467         char    salt[32];
468          int    i;
469         regmatch_t      matches[4];
470         
471         // Get user name
472         pwd = getpwuid( getuid() );
473         
474         // Attempt automatic authentication
475         sendf(Socket, "AUTOAUTH %s\n", pwd->pw_name);
476         
477         // Check if it worked
478         recv(Socket, buf, 511, 0);
479         trim(buf);
480         
481         responseCode = atoi(buf);
482         switch( responseCode )
483         {
484         case 200:       // Authenticated, return :)
485                 return 0;
486         case 401:       // Untrusted, attempt password authentication
487                 sendf(Socket, "USER %s\n", pwd->pw_name);
488                 printf("Using username %s\n", pwd->pw_name);
489                 
490                 recv(Socket, buf, 511, 0);
491                 trim(buf);
492                 // TODO: Get Salt
493                 // Expected format: 100 SALT <something> ...
494                 // OR             : 100 User Set
495                 RunRegex(&gSaltRegex, buf, 4, matches, "Malformed server response");
496                 responseCode = atoi(buf);
497                 if( responseCode != 100 ) {
498                         fprintf(stderr, "Unknown repsonse code %i from server\n", responseCode);
499                         return -1;      // ERROR
500                 }
501                 
502                 // Check for salt
503                 if( memcmp( buf+matches[2].rm_so, "SALT", matches[2].rm_eo - matches[2].rm_so) == 0) {
504                         memcpy( salt, buf + matches[3].rm_so, matches[3].rm_eo - matches[3].rm_so );
505                         salt[ matches[3].rm_eo - matches[3].rm_so ] = 0;
506                 }
507                 
508                 // Get password
509                 fflush(stdout);
510                 
511                 // Give three attempts
512                 for( i = 0; i < 3; i ++ )
513                 {
514                          int    ofs = strlen(pwd->pw_name)+strlen(salt);
515                         char    tmp[ofs+20];
516                         char    *pass = getpass("Password: ");
517                         uint8_t h[20];
518                         
519                         // Create hash string
520                         // <username><salt><hash>
521                         strcpy(tmp, pwd->pw_name);
522                         strcat(tmp, salt);
523                         SHA1( (unsigned char*)pass, strlen(pass), h );
524                         memcpy(tmp+ofs, h, 20);
525                         
526                         // Hash all that
527                         SHA1( (unsigned char*)tmp, ofs+20, h );
528                         sprintf(buf, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
529                                 h[ 0], h[ 1], h[ 2], h[ 3], h[ 4], h[ 5], h[ 6], h[ 7], h[ 8], h[ 9],
530                                 h[10], h[11], h[12], h[13], h[14], h[15], h[16], h[17], h[18], h[19]
531                                 );
532                         fflush(stdout); // Debug
533                 
534                         // Send password
535                         sendf(Socket, "PASS %s\n", buf);
536                         recv(Socket, buf, 511, 0);
537                 
538                         responseCode = atoi(buf);
539                         // Auth OK?
540                         if( responseCode == 200 )       break;
541                         // Bad username/password
542                         if( responseCode == 401 )       continue;
543                         
544                         fprintf(stderr, "Unknown repsonse code %i from server\n", responseCode);
545                         return -1;
546                 }
547                 return 2;       // 2 = Bad Password
548         
549         case 404:       // Bad Username
550                 fprintf(stderr, "Bad Username '%s'\n", pwd->pw_name);
551                 return 1;
552         
553         default:
554                 fprintf(stderr, "Unkown response code %i from server\n", responseCode);
555                 printf("%s\n", buf);
556                 return -1;
557         }
558         
559         printf("%s\n", buf);
560         return 0;       // Seems OK
561 }
562
563 void PopulateItemList(int Socket)
564 {
565         char    buffer[BUFSIZ];
566          int    len;
567          int    responseCode;
568         
569         char    *itemType, *itemStart;
570          int    count, i;
571         regmatch_t      matches[4];
572         
573         // Ask server for stock list
574         send(Socket, "ENUM_ITEMS\n", 11, 0);
575         len = recv(Socket, buffer, BUFSIZ-1, 0);
576         buffer[len] = '\0';
577         
578         trim(buffer);
579         
580         //printf("Output: %s\n", buffer);
581         
582         responseCode = atoi(buffer);
583         if( responseCode != 201 ) {
584                 fprintf(stderr, "Unknown response from dispense server (Response Code %i)\n", responseCode);
585                 exit(-1);
586         }
587         
588         // - Get item list -
589         
590         // Expected format: 201 Items <count> <item1> <item2> ...
591         RunRegex(&gArrayRegex, buffer, 4, matches, "Malformed server response");
592                 
593         itemType = &buffer[ matches[2].rm_so ]; buffer[ matches[2].rm_eo ] = '\0';
594         count = atoi( &buffer[ matches[3].rm_so ] );
595                 
596         // Check array type
597         if( strcmp(itemType, "Items") != 0 ) {
598                 // What the?!
599                 fprintf(stderr, "Unexpected array type, expected 'Items', got '%s'\n",
600                         itemType);
601                 exit(-1);
602         }
603                 
604         itemStart = &buffer[ matches[3].rm_eo ];
605                 
606         gaItems = malloc( count * sizeof(tItem) );
607                 
608         for( giNumItems = 0; giNumItems < count && itemStart; giNumItems ++ )
609         {
610                 char    *next = strchr( ++itemStart, ' ' );
611                 if( next )      *next = '\0';
612                 gaItems[giNumItems].Ident = strdup(itemStart);
613                 itemStart = next;
614         }
615         
616         // Fetch item information
617         for( i = 0; i < giNumItems; i ++ )
618         {
619                 regmatch_t      matches[6];
620                 
621                 // Get item info
622                 sendf(Socket, "ITEM_INFO %s\n", gaItems[i].Ident);
623                 len = recv(Socket, buffer, BUFSIZ-1, 0);
624                 buffer[len] = '\0';
625                 trim(buffer);
626                 
627                 responseCode = atoi(buffer);
628                 if( responseCode != 202 ) {
629                         fprintf(stderr, "Unknown response from dispense server (Response Code %i)\n", responseCode);
630                         exit(-1);
631                 }
632                 
633                 RunRegex(&gItemRegex, buffer, 6, matches, "Malformed server response");
634                 
635                 buffer[ matches[3].rm_eo ] = '\0';
636                 
637                 gaItems[i].Price = atoi( buffer + matches[4].rm_so );
638                 gaItems[i].Desc = strdup( buffer + matches[5].rm_so );
639         }
640 }
641
642 int DispenseItem(int Socket, int ItemID)
643 {
644          int    len, responseCode;
645         char    buffer[BUFSIZ];
646         
647         if( ItemID < 0 || ItemID > giNumItems ) return -1;
648         
649         // Dispense!
650         sendf(Socket, "DISPENSE %s\n", gaItems[ItemID].Ident);
651         len = recv(Socket, buffer, BUFSIZ-1, 0);
652         buffer[len] = '\0';
653         trim(buffer);
654         
655         responseCode = atoi(buffer);
656         switch( responseCode )
657         {
658         case 200:
659                 printf("Dispense OK\n");
660                 return 0;
661         case 401:
662                 printf("Not authenticated\n");
663                 return 1;
664         case 402:
665                 printf("Insufficient balance\n");
666                 return 1;
667         case 406:
668                 printf("Bad item name, bug report\n");
669                 return 1;
670         case 500:
671                 printf("Item failed to dispense, is the slot empty?\n");
672                 return 1;
673         case 501:
674                 printf("Dispense not possible (slot empty/permissions)\n");
675                 return 1;
676         default:
677                 printf("Unknown response code %i ('%s')\n", responseCode, buffer);
678                 return -2;
679         }
680 }
681
682 // ---------------
683 // --- Helpers ---
684 // ---------------
685 int sendf(int Socket, const char *Format, ...)
686 {
687         va_list args;
688          int    len;
689         
690         va_start(args, Format);
691         len = vsnprintf(NULL, 0, Format, args);
692         va_end(args);
693         
694         {
695                 char    buf[len+1];
696                 va_start(args, Format);
697                 vsnprintf(buf, len+1, Format, args);
698                 va_end(args);
699                 
700                 return send(Socket, buf, len, 0);
701         }
702 }
703
704 char *trim(char *string)
705 {
706          int    i;
707         
708         while( isspace(*string) )
709                 string ++;
710         
711         for( i = strlen(string); i--; )
712         {
713                 if( isspace(string[i]) )
714                         string[i] = '\0';
715                 else
716                         break;
717         }
718         
719         return string;
720 }
721
722 int RunRegex(regex_t *regex, const char *string, int nMatches, regmatch_t *matches, const char *errorMessage)
723 {
724          int    ret;
725         
726         ret = regexec(regex, string, nMatches, matches, 0);
727         if( ret ) {
728                 size_t  len = regerror(ret, regex, NULL, 0);
729                 char    errorStr[len];
730                 regerror(ret, regex, errorStr, len);
731                 printf("string = '%s'\n", string);
732                 fprintf(stderr, "%s\n%s", errorMessage, errorStr);
733                 exit(-1);
734         }
735         
736         return ret;
737 }
738
739 void CompileRegex(regex_t *regex, const char *pattern, int flags)
740 {
741          int    ret = regcomp(regex, pattern, flags);
742         if( ret ) {
743                 size_t  len = regerror(ret, regex, NULL, 0);
744                 char    errorStr[len];
745                 regerror(ret, regex, errorStr, len);
746                 fprintf(stderr, "Regex compilation failed - %s\n", errorStr);
747                 exit(-1);
748         }
749 }

UCC git Repository :: git.ucc.asn.au