315dbd12829e500b79b6cb7f934c45c6c515d0da
[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          
183          int    height, width;
184          
185         // Enter curses mode
186         initscr();
187         raw(); noecho();
188         
189         itemCount = LINES - 6;
190         if( itemCount > giNumItems )
191                 itemCount = giNumItems;
192         
193         // Get dimensions
194         height = itemCount + 3;
195         width = displayMinWidth;
196         
197         // Get positions
198         xBase = COLS/2 - width/2;
199         yBase = LINES/2 - height/2;
200         
201         for( ;; )
202         {
203                 // Header
204                 PrintAlign(yBase, xBase, width, "/", '-', titleString, '-', "\\");
205                 
206                 // Items
207                 for( i = 0; i < itemCount; i ++ )
208                 {
209                         move( yBase + 1 + i, xBase );
210                         
211                         if( currentItem == itemBase + i ) {
212                                 printw("| -> ");
213                         }
214                         else {
215                                 printw("|    ");
216                         }
217                         
218                         // Check for ... row
219                         // - Oh god, magic numbers!
220                         if( i == 0 && itemBase > 0 ) {
221                                 printw("   ...");
222                                 times = width-1 - 8 - 3;
223                                 while(times--)  addch(' ');
224                         }
225                         else if( i == itemCount - 1 && itemBase < giNumItems - itemCount ) {
226                                 printw("   ...");
227                                 times = width-1 - 8 - 3;
228                                 while(times--)  addch(' ');
229                         }
230                         // Show an item
231                         else {
232                                 ShowItemAt( yBase + 1 + i, xBase + 5, width - 7, itemBase + i);
233                                 addch(' ');
234                         }
235                         
236                         // Scrollbar (if needed)
237                         if( giNumItems > itemCount ) {
238                                 if( i == 0 ) {
239                                         addch('A');
240                                 }
241                                 else if( i == itemCount - 1 ) {
242                                         addch('V');
243                                 }
244                                 else {
245                                          int    percentage = itemBase * 100 / (giNumItems-itemCount);
246                                         if( i-1 == percentage*(itemCount-3)/100 ) {
247                                                 addch('#');
248                                         }
249                                         else {
250                                                 addch('|');
251                                         }
252                                 }
253                         }
254                         else {
255                                 addch('|');
256                         }
257                 }
258                 
259                 // Footer
260                 PrintAlign(yBase+height-2, xBase, width, "\\", '-', "", '-', "/");
261                 
262                 // Get input
263                 ch = getch();
264                 
265                 if( ch == '\x1B' ) {
266                         ch = getch();
267                         if( ch == '[' ) {
268                                 ch = getch();
269                                 
270                                 switch(ch)
271                                 {
272                                 case 'B':
273                                         //if( itemBase < giNumItems - (itemCount) )
274                                         //      itemBase ++;
275                                         if( currentItem < giNumItems - 1 )
276                                                 currentItem ++;
277                                         if( itemBase + itemCount - 1 <= currentItem && itemBase + itemCount < giNumItems )
278                                                 itemBase ++;
279                                         break;
280                                 case 'A':
281                                         //if( itemBase > 0 )
282                                         //      itemBase --;
283                                         if( currentItem > 0 )
284                                                 currentItem --;
285                                         if( itemBase + 1 > currentItem && itemBase > 0 )
286                                                 itemBase --;
287                                         break;
288                                 }
289                         }
290                         else {
291                                 
292                         }
293                 }
294                 else {
295                         break;
296                 }
297                 
298         }
299         
300         
301         // Leave
302         endwin();
303         return -1;
304 }
305
306 /**
307  * \brief Show item \a Index at (\a Col, \a Row)
308  * \note Part of the NCurses UI
309  */
310 void ShowItemAt(int Row, int Col, int Width, int Index)
311 {
312          int    _x, _y, times;
313         char    *name;
314          int    price;
315         
316         move( Row, Col );
317         
318         if( Index < 0 || Index >= giNumItems ) {
319                 name = "OOR";
320                 price = 0;
321         }
322         else {
323                 name = gaItems[Index].Desc;
324                 price = gaItems[Index].Price;
325         }
326
327         printw("%02i %s", Index, name);
328         
329         getyx(stdscr, _y, _x);
330         // Assumes max 4 digit prices
331         times = Width - 4 - (_x - Col); // TODO: Better handling for large prices
332         while(times--)  addch(' ');
333         printw("%4i", price);
334 }
335
336 /**
337  * \brief Print a three-part string at the specified position (formatted)
338  * \note NCurses UI Helper
339  * 
340  * Prints \a Left on the left of the area, \a Right on the righthand side
341  * and \a Mid in the middle of the area. These are padded with \a Pad1
342  * between \a Left and \a Mid, and \a Pad2 between \a Mid and \a Right.
343  * 
344  * ::printf style format codes are allowed in \a Left, \a Mid and \a Right,
345  * and the arguments to these are read in that order.
346  */
347 void PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1,
348         const char *Mid, char Pad2, const char *Right, ...)
349 {
350          int    lLen, mLen, rLen;
351          int    times;
352         
353         va_list args;
354         
355         // Get the length of the strings
356         va_start(args, Right);
357         lLen = vsnprintf(NULL, 0, Left, args);
358         mLen = vsnprintf(NULL, 0, Mid, args);
359         rLen = vsnprintf(NULL, 0, Right, args);
360         va_end(args);
361         
362         // Sanity check
363         if( lLen + mLen/2 > Width/2 || mLen/2 + rLen > Width/2 ) {
364                 return ;        // TODO: What to do?
365         }
366         
367         move(Row, Col);
368         
369         // Render strings
370         va_start(args, Right);
371         // - Left
372         {
373                 char    tmp[lLen+1];
374                 vsnprintf(tmp, lLen+1, Left, args);
375                 addstr(tmp);
376         }
377         // - Left padding
378         times = Width/2 - mLen/2 - lLen;
379         while(times--)  addch(Pad1);
380         // - Middle
381         {
382                 char    tmp[mLen+1];
383                 vsnprintf(tmp, mLen+1, Mid, args);
384                 addstr(tmp);
385         }
386         // - Right Padding
387         times = Width/2 - mLen/2 - rLen;
388         while(times--)  addch(Pad2);
389         // - Right
390         {
391                 char    tmp[rLen+1];
392                 vsnprintf(tmp, rLen+1, Right, args);
393                 addstr(tmp);
394         }
395 }
396
397 // ---------------------
398 // --- Coke Protocol ---
399 // ---------------------
400 int OpenConnection(const char *Host, int Port)
401 {
402         struct hostent  *host;
403         struct sockaddr_in      serverAddr;
404          int    sock;
405         
406         host = gethostbyname(Host);
407         if( !host ) {
408                 fprintf(stderr, "Unable to look up '%s'\n", Host);
409                 return -1;
410         }
411         
412         memset(&serverAddr, 0, sizeof(serverAddr));
413         
414         serverAddr.sin_family = AF_INET;        // IPv4
415         // NOTE: I have a suspicion that IPv6 will play sillybuggers with this :)
416         serverAddr.sin_addr.s_addr = *((unsigned long *) host->h_addr_list[0]);
417         serverAddr.sin_port = htons(Port);
418         
419         sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
420         if( sock < 0 ) {
421                 fprintf(stderr, "Failed to create socket\n");
422                 return -1;
423         }
424         
425         #if USE_AUTOAUTH
426         {
427                 struct sockaddr_in      localAddr;
428                 memset(&localAddr, 0, sizeof(localAddr));
429                 localAddr.sin_family = AF_INET; // IPv4
430                 localAddr.sin_port = 1023;      // IPv4
431                 // Attempt to bind to low port for autoauth
432                 bind(sock, &localAddr, sizeof(localAddr));
433         }
434         #endif
435         
436         if( connect(sock, (struct sockaddr *) &serverAddr, sizeof(serverAddr)) < 0 ) {
437                 fprintf(stderr, "Failed to connect to server\n");
438                 return -1;
439         }
440         
441         return sock;
442 }
443
444 /**
445  * \brief Authenticate with the server
446  * \return Boolean Failure
447  */
448 int Authenticate(int Socket)
449 {
450         struct passwd   *pwd;
451         char    buf[512];
452          int    responseCode;
453         char    salt[32];
454          int    i;
455         regmatch_t      matches[4];
456         
457         // Get user name
458         pwd = getpwuid( getuid() );
459         
460         // Attempt automatic authentication
461         sendf(Socket, "AUTOAUTH %s\n", pwd->pw_name);
462         
463         // Check if it worked
464         recv(Socket, buf, 511, 0);
465         trim(buf);
466         
467         responseCode = atoi(buf);
468         switch( responseCode )
469         {
470         case 200:       // Authenticated, return :)
471                 return 0;
472         case 401:       // Untrusted, attempt password authentication
473                 sendf(Socket, "USER %s\n", pwd->pw_name);
474                 printf("Using username %s\n", pwd->pw_name);
475                 
476                 recv(Socket, buf, 511, 0);
477                 trim(buf);
478                 // TODO: Get Salt
479                 // Expected format: 100 SALT <something> ...
480                 // OR             : 100 User Set
481                 RunRegex(&gSaltRegex, buf, 4, matches, "Malformed server response");
482                 responseCode = atoi(buf);
483                 if( responseCode != 100 ) {
484                         fprintf(stderr, "Unknown repsonse code %i from server\n", responseCode);
485                         return -1;      // ERROR
486                 }
487                 
488                 // Check for salt
489                 if( memcmp( buf+matches[2].rm_so, "SALT", matches[2].rm_eo - matches[2].rm_so) == 0) {
490                         memcpy( salt, buf + matches[3].rm_so, matches[3].rm_eo - matches[3].rm_so );
491                         salt[ matches[3].rm_eo - matches[3].rm_so ] = 0;
492                 }
493                 
494                 // Get password
495                 fflush(stdout);
496                 
497                 // Give three attempts
498                 for( i = 0; i < 3; i ++ )
499                 {
500                          int    ofs = strlen(pwd->pw_name)+strlen(salt);
501                         char    tmp[ofs+20];
502                         char    *pass = getpass("Password: ");
503                         uint8_t h[20];
504                         
505                         // Create hash string
506                         // <username><salt><hash>
507                         strcpy(tmp, pwd->pw_name);
508                         strcat(tmp, salt);
509                         SHA1( (unsigned char*)pass, strlen(pass), h );
510                         memcpy(tmp+ofs, h, 20);
511                         
512                         // Hash all that
513                         SHA1( (unsigned char*)tmp, ofs+20, h );
514                         sprintf(buf, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
515                                 h[ 0], h[ 1], h[ 2], h[ 3], h[ 4], h[ 5], h[ 6], h[ 7], h[ 8], h[ 9],
516                                 h[10], h[11], h[12], h[13], h[14], h[15], h[16], h[17], h[18], h[19]
517                                 );
518                         fflush(stdout); // Debug
519                 
520                         // Send password
521                         sendf(Socket, "PASS %s\n", buf);
522                         recv(Socket, buf, 511, 0);
523                 
524                         responseCode = atoi(buf);
525                         // Auth OK?
526                         if( responseCode == 200 )       break;
527                         // Bad username/password
528                         if( responseCode == 401 )       continue;
529                         
530                         fprintf(stderr, "Unknown repsonse code %i from server\n", responseCode);
531                         return -1;
532                 }
533                 return 2;       // 2 = Bad Password
534         
535         case 404:       // Bad Username
536                 fprintf(stderr, "Bad Username '%s'\n", pwd->pw_name);
537                 return 1;
538         
539         default:
540                 fprintf(stderr, "Unkown response code %i from server\n", responseCode);
541                 printf("%s\n", buf);
542                 return -1;
543         }
544         
545         printf("%s\n", buf);
546         return 0;       // Seems OK
547 }
548
549 void PopulateItemList(int Socket)
550 {
551         char    buffer[BUFSIZ];
552          int    len;
553          int    responseCode;
554         
555         char    *itemType, *itemStart;
556          int    count, i;
557         regmatch_t      matches[4];
558         
559         // Ask server for stock list
560         send(Socket, "ENUM_ITEMS\n", 11, 0);
561         len = recv(Socket, buffer, BUFSIZ-1, 0);
562         buffer[len] = '\0';
563         
564         trim(buffer);
565         
566         //printf("Output: %s\n", buffer);
567         
568         responseCode = atoi(buffer);
569         if( responseCode != 201 ) {
570                 fprintf(stderr, "Unknown response from dispense server (Response Code %i)\n", responseCode);
571                 exit(-1);
572         }
573         
574         // - Get item list -
575         
576         // Expected format: 201 Items <count> <item1> <item2> ...
577         RunRegex(&gArrayRegex, buffer, 4, matches, "Malformed server response");
578                 
579         itemType = &buffer[ matches[2].rm_so ]; buffer[ matches[2].rm_eo ] = '\0';
580         count = atoi( &buffer[ matches[3].rm_so ] );
581                 
582         // Check array type
583         if( strcmp(itemType, "Items") != 0 ) {
584                 // What the?!
585                 fprintf(stderr, "Unexpected array type, expected 'Items', got '%s'\n",
586                         itemType);
587                 exit(-1);
588         }
589                 
590         itemStart = &buffer[ matches[3].rm_eo ];
591                 
592         gaItems = malloc( count * sizeof(tItem) );
593                 
594         for( giNumItems = 0; giNumItems < count && itemStart; giNumItems ++ )
595         {
596                 char    *next = strchr( ++itemStart, ' ' );
597                 if( next )      *next = '\0';
598                 gaItems[giNumItems].Ident = strdup(itemStart);
599                 itemStart = next;
600         }
601         
602         // Fetch item information
603         for( i = 0; i < giNumItems; i ++ )
604         {
605                 regmatch_t      matches[6];
606                 
607                 // Get item info
608                 sendf(Socket, "ITEM_INFO %s\n", gaItems[i].Ident);
609                 len = recv(Socket, buffer, BUFSIZ-1, 0);
610                 buffer[len] = '\0';
611                 trim(buffer);
612                 
613                 responseCode = atoi(buffer);
614                 if( responseCode != 202 ) {
615                         fprintf(stderr, "Unknown response from dispense server (Response Code %i)\n", responseCode);
616                         exit(-1);
617                 }
618                 
619                 RunRegex(&gItemRegex, buffer, 6, matches, "Malformed server response");
620                 
621                 buffer[ matches[3].rm_eo ] = '\0';
622                 
623                 gaItems[i].Price = atoi( buffer + matches[4].rm_so );
624                 gaItems[i].Desc = strdup( buffer + matches[5].rm_so );
625         }
626 }
627
628 int DispenseItem(int Socket, int ItemID)
629 {
630          int    len, responseCode;
631         char    buffer[BUFSIZ];
632         
633         if( ItemID < 0 || ItemID > giNumItems ) return -1;
634         
635         // Dispense!
636         sendf(Socket, "DISPENSE %s\n", gaItems[ItemID].Ident);
637         len = recv(Socket, buffer, BUFSIZ-1, 0);
638         buffer[len] = '\0';
639         trim(buffer);
640         
641         responseCode = atoi(buffer);
642         switch( responseCode )
643         {
644         case 200:
645                 printf("Dispense OK\n");
646                 return 0;
647         case 401:
648                 printf("Not authenticated\n");
649                 return 1;
650         case 402:
651                 printf("Insufficient balance\n");
652                 return 1;
653         case 406:
654                 printf("Bad item name, bug report\n");
655                 return 1;
656         case 500:
657                 printf("Item failed to dispense, is the slot empty?\n");
658                 return 1;
659         case 501:
660                 printf("Dispense not possible (slot empty/permissions)\n");
661                 return 1;
662         default:
663                 printf("Unknown response code %i ('%s')\n", responseCode, buffer);
664                 return -2;
665         }
666 }
667
668 // ---------------
669 // --- Helpers ---
670 // ---------------
671 int sendf(int Socket, const char *Format, ...)
672 {
673         va_list args;
674          int    len;
675         
676         va_start(args, Format);
677         len = vsnprintf(NULL, 0, Format, args);
678         va_end(args);
679         
680         {
681                 char    buf[len+1];
682                 va_start(args, Format);
683                 vsnprintf(buf, len+1, Format, args);
684                 va_end(args);
685                 
686                 return send(Socket, buf, len, 0);
687         }
688 }
689
690 char *trim(char *string)
691 {
692          int    i;
693         
694         while( isspace(*string) )
695                 string ++;
696         
697         for( i = strlen(string); i--; )
698         {
699                 if( isspace(string[i]) )
700                         string[i] = '\0';
701                 else
702                         break;
703         }
704         
705         return string;
706 }
707
708 int RunRegex(regex_t *regex, const char *string, int nMatches, regmatch_t *matches, const char *errorMessage)
709 {
710          int    ret;
711         
712         ret = regexec(regex, string, nMatches, matches, 0);
713         if( ret ) {
714                 size_t  len = regerror(ret, regex, NULL, 0);
715                 char    errorStr[len];
716                 regerror(ret, regex, errorStr, len);
717                 printf("string = '%s'\n", string);
718                 fprintf(stderr, "%s\n%s", errorMessage, errorStr);
719                 exit(-1);
720         }
721         
722         return ret;
723 }
724
725 void CompileRegex(regex_t *regex, const char *pattern, int flags)
726 {
727          int    ret = regcomp(regex, pattern, flags);
728         if( ret ) {
729                 size_t  len = regerror(ret, regex, NULL, 0);
730                 char    errorStr[len];
731                 regerror(ret, regex, errorStr, len);
732                 fprintf(stderr, "Regex compilation failed - %s\n", errorStr);
733                 exit(-1);
734         }
735 }

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