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

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