ed7d1a22bbcf5fda3975fabf7548bb57174ea909
[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 void    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         Authenticate(sock);
197         
198         if( i >= 0 )
199         {       
200                 // Dispense!
201                 sendf(sock, "DISPENSE %s\n", gaItems[i].Ident);
202                 
203                 len = recv(sock, buffer, BUFSIZ-1, 0);
204                 buffer[len] = '\0';
205                 trim(buffer);
206                 
207                 responseCode = atoi(buffer);
208                 switch( responseCode )
209                 {
210                 case 200:
211                         printf("Dispense OK\n");
212                         break;
213                 case 401:
214                         printf("Not authenticated\n");
215                         break;
216                 case 402:
217                         printf("Insufficient balance\n");
218                         break;
219                 case 406:
220                         printf("Bad item name, bug report\n");
221                         break;
222                 case 500:
223                         printf("Item failed to dispense, is the slot empty?\n");
224                         break;
225                 default:
226                         printf("Unknown response code %i ('%s')\n", responseCode, buffer);
227                         break;
228                 }
229         }
230
231         close(sock);
232
233         return 0;
234 }
235
236 void ShowItemAt(int Row, int Col, int Width, int Index)
237 {
238          int    _x, _y, times;
239         
240         move( Row, Col );
241         
242         if( Index < 0 || Index >= giNumItems ) {
243                 printw("%02i OOR", Index);
244                 return ;
245         }
246         printw("%02i %s", Index, gaItems[Index].Desc);
247         
248         getyx(stdscr, _y, _x);
249         // Assumes max 4 digit prices
250         times = Width - 4 - (_x - Col); // TODO: Better handling for large prices
251         while(times--)  addch(' ');
252         printw("%4i", gaItems[Index].Price);
253 }
254
255 /**
256  */
257 int ShowNCursesUI(void)
258 {
259         // TODO: ncurses interface (with separation between item classes)
260         // - Hmm... that would require standardising the item ID to be <class>:<index>
261         // Oh, why not :)
262          int    ch;
263          int    i, times;
264          int    xBase, yBase;
265         const int       displayMinWidth = 34;
266         const int       displayMinItems = 8;
267         char    *titleString = "Dispense";
268          int    itemCount = displayMinItems;
269          int    itemBase = 0;
270          
271          int    height = itemCount + 3;
272          int    width = displayMinWidth;
273          
274         // Enter curses mode
275         initscr();
276         raw(); noecho();
277         
278         xBase = COLS/2 - width/2;
279         yBase = LINES/2 - height/2;
280         
281         for( ;; )
282         {
283                 // Header
284                 PrintAlign(yBase, xBase, width, "/", '-', titleString, '-', "\\");
285                 
286                 // Items
287                 for( i = 0; i < itemCount; i ++ )
288                 {
289                         move( yBase + 1 + i, xBase );
290                         addch('|');
291                         addch(' ');
292                         
293                         // Check for ... row
294                         if( i == 0 && itemBase > 0 ) {
295                                 printw("   ...");
296                                 times = width - 1 - 8;
297                                 while(times--)  addch(' ');
298                         }
299                         else if( i == itemCount - 1 && itemBase < giNumItems - itemCount ) {
300                                 printw("   ...");
301                                 times = width - 1 - 8;
302                                 while(times--)  addch(' ');
303                         }
304                         // Show an item
305                         else {
306                                 ShowItemAt( yBase + 1 + i, xBase + 2, width - 4, itemBase + i);
307                                 addch(' ');
308                         }
309                         
310                         // Scrollbar (if needed)
311                         if( giNumItems > itemCount ) {
312                                 if( i == 0 ) {
313                                         addch('A');
314                                 }
315                                 else if( i == itemCount - 1 ) {
316                                         addch('V');
317                                 }
318                                 else {
319                                          int    percentage = itemBase * 100 / (giNumItems-itemCount);
320                                         if( i-1 == percentage*(itemCount-3)/100 ) {
321                                                 addch('#');
322                                         }
323                                         else {
324                                                 addch('|');
325                                         }
326                                 }
327                         }
328                         else {
329                                 addch('|');
330                         }
331                 }
332                 
333                 // Footer
334                 PrintAlign(yBase+height-2, xBase, width, "\\", '-', "", '-', "/");
335                 
336                 // Get input
337                 ch = getch();
338                 
339                 if( ch == '\x1B' ) {
340                         ch = getch();
341                         if( ch == '[' ) {
342                                 ch = getch();
343                                 
344                                 switch(ch)
345                                 {
346                                 case 'B':
347                                         if( itemBase < giNumItems - (itemCount) )
348                                                 itemBase ++;
349                                         break;
350                                 case 'A':
351                                         if( itemBase > 0 )
352                                                 itemBase --;
353                                         break;
354                                 }
355                         }
356                         else {
357                                 
358                         }
359                 }
360                 else {
361                         break;
362                 }
363                 
364         }
365         
366         
367         // Leave
368         endwin();
369         return -1;
370 }
371
372 void PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1, const char *Mid, char Pad2, const char *Right, ...)
373 {
374          int    lLen, mLen, rLen;
375          int    times;
376         
377         va_list args;
378         
379         // Get the length of the strings
380         va_start(args, Right);
381         lLen = vsnprintf(NULL, 0, Left, args);
382         mLen = vsnprintf(NULL, 0, Mid, args);
383         rLen = vsnprintf(NULL, 0, Right, args);
384         va_end(args);
385         
386         // Sanity check
387         if( lLen + mLen/2 > Width/2 || mLen/2 + rLen > Width/2 ) {
388                 return ;        // TODO: What to do?
389         }
390         
391         move(Row, Col);
392         
393         // Render strings
394         va_start(args, Right);
395         // - Left
396         {
397                 char    tmp[lLen+1];
398                 vsnprintf(tmp, lLen+1, Left, args);
399                 addstr(tmp);
400         }
401         // - Left padding
402         times = Width/2 - mLen/2 - lLen;
403         while(times--)  addch(Pad1);
404         // - Middle
405         {
406                 char    tmp[mLen+1];
407                 vsnprintf(tmp, mLen+1, Mid, args);
408                 addstr(tmp);
409         }
410         // - Right Padding
411         times = Width/2 - mLen/2 - rLen;
412         while(times--)  addch(Pad2);
413         // - Right
414         {
415                 char    tmp[rLen+1];
416                 vsnprintf(tmp, rLen+1, Right, args);
417                 addstr(tmp);
418         }
419 }
420
421 // === HELPERS ===
422 int sendf(int Socket, const char *Format, ...)
423 {
424         va_list args;
425          int    len;
426         
427         va_start(args, Format);
428         len = vsnprintf(NULL, 0, Format, args);
429         va_end(args);
430         
431         {
432                 char    buf[len+1];
433                 va_start(args, Format);
434                 vsnprintf(buf, len+1, Format, args);
435                 va_end(args);
436                 
437                 return send(Socket, buf, len, 0);
438         }
439 }
440
441 int OpenConnection(const char *Host, int Port)
442 {
443         struct hostent  *host;
444         struct sockaddr_in      serverAddr;
445          int    sock;
446         
447         host = gethostbyname(Host);
448         if( !host ) {
449                 fprintf(stderr, "Unable to look up '%s'\n", Host);
450                 return -1;
451         }
452         
453         memset(&serverAddr, 0, sizeof(serverAddr));
454         
455         serverAddr.sin_family = AF_INET;        // IPv4
456         // NOTE: I have a suspicion that IPv6 will play sillybuggers with this :)
457         serverAddr.sin_addr.s_addr = *((unsigned long *) host->h_addr_list[0]);
458         serverAddr.sin_port = htons(Port);
459         
460         sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
461         if( sock < 0 ) {
462                 fprintf(stderr, "Failed to create socket\n");
463                 return -1;
464         }
465         
466         #if USE_AUTOAUTH
467         {
468                 struct sockaddr_in      localAddr;
469                 memset(&localAddr, 0, sizeof(localAddr));
470                 localAddr.sin_family = AF_INET; // IPv4
471                 localAddr.sin_port = 1023;      // IPv4
472                 // Attempt to bind to low port for autoauth
473                 bind(sock, &localAddr, sizeof(localAddr));
474         }
475         #endif
476         
477         if( connect(sock, (struct sockaddr *) &serverAddr, sizeof(serverAddr)) < 0 ) {
478                 fprintf(stderr, "Failed to connect to server\n");
479                 return -1;
480         }
481         
482         return sock;
483 }
484
485 void Authenticate(int Socket)
486 {
487         struct passwd   *pwd;
488         char    buf[512];
489          int    responseCode;
490         char    salt[32];
491         regmatch_t      matches[4];
492         
493         // Get user name
494         pwd = getpwuid( getuid() );
495         
496         // Attempt automatic authentication
497         sendf(Socket, "AUTOAUTH %s\n", pwd->pw_name);
498         
499         // Check if it worked
500         recv(Socket, buf, 511, 0);
501         trim(buf);
502         
503         responseCode = atoi(buf);
504         switch( responseCode )
505         {
506         case 200:       // Authenticated, return :)
507                 return ;
508         case 401:       // Untrusted, attempt password authentication
509                 sendf(Socket, "USER %s\n", pwd->pw_name);
510                 printf("Using username %s\n", pwd->pw_name);
511                 
512                 recv(Socket, buf, 511, 0);
513                 trim(buf);
514                 // TODO: Get Salt
515                 // Expected format: 100 SALT <something> ...
516                 // OR             : 100 User Set
517                 RunRegex(&gSaltRegex, buf, 4, matches, "Malformed server response");
518                 if( atoi(buf) != 100 ) {
519                         exit(-1);       // ERROR
520                 }
521                 if( memcmp( buf+matches[2].rm_so, "SALT", matches[2].rm_eo - matches[2].rm_so) == 0) {
522                         // Set salt
523                         memcpy( salt, buf + matches[3].rm_so, matches[3].rm_eo - matches[3].rm_so );
524                         salt[ matches[3].rm_eo - matches[3].rm_so ] = 0;
525 //                      printf("Salt: '%s'\n", salt);
526                 }
527                 
528                 fflush(stdout);
529                 {
530                          int    ofs = strlen(pwd->pw_name)+strlen(salt);
531                         char    tmp[ofs+20];
532                         char    *pass = getpass("Password: ");
533                         uint8_t h[20];
534                         
535                         strcpy(tmp, pwd->pw_name);
536                         strcat(tmp, salt);
537                         SHA1( (unsigned char*)pass, strlen(pass), h );
538                         memcpy(tmp+ofs, h, 20);
539                         
540                         // Hash all that
541                         SHA1( (unsigned char*)tmp, ofs+20, h );
542                         sprintf(buf, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
543                                 h[ 0], h[ 1], h[ 2], h[ 3], h[ 4], h[ 5], h[ 6], h[ 7], h[ 8], h[ 9],
544                                 h[10], h[11], h[12], h[13], h[14], h[15], h[16], h[17], h[18], h[19]
545                                 );
546 //                      printf("Final hash: '%s'\n", buf);
547                         fflush(stdout); // Debug
548                 }
549                 
550                 sendf(Socket, "PASS %s\n", buf);
551                 recv(Socket, buf, 511, 0);
552                 break;
553         case 404:       // Bad Username
554                 fprintf(stderr, "Bad Username '%s'\n", pwd->pw_name);
555                 exit(-1);
556         default:
557                 fprintf(stderr, "Unkown response code %i from server\n", responseCode);
558                 printf("%s\n", buf);
559                 exit(-1);
560         }
561         
562         printf("%s\n", buf);
563 }
564
565 char *trim(char *string)
566 {
567          int    i;
568         
569         while( isspace(*string) )
570                 string ++;
571         
572         for( i = strlen(string); i--; )
573         {
574                 if( isspace(string[i]) )
575                         string[i] = '\0';
576                 else
577                         break;
578         }
579         
580         return string;
581 }
582
583 int RunRegex(regex_t *regex, const char *string, int nMatches, regmatch_t *matches, const char *errorMessage)
584 {
585          int    ret;
586         
587         ret = regexec(regex, string, nMatches, matches, 0);
588         if( ret ) {
589                 size_t  len = regerror(ret, regex, NULL, 0);
590                 char    errorStr[len];
591                 regerror(ret, regex, errorStr, len);
592                 printf("string = '%s'\n", string);
593                 fprintf(stderr, "%s\n%s", errorMessage, errorStr);
594                 exit(-1);
595         }
596         
597         return ret;
598 }
599
600 void CompileRegex(regex_t *regex, const char *pattern, int flags)
601 {
602          int    ret = regcomp(regex, pattern, flags);
603         if( ret ) {
604                 size_t  len = regerror(ret, regex, NULL, 0);
605                 char    errorStr[len];
606                 regerror(ret, regex, errorStr, len);
607                 fprintf(stderr, "Regex compilation failed - %s\n", errorStr);
608                 exit(-1);
609         }
610 }

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