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

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