Implementing `dispense refund`, slight bugfixes, removed password auth
[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 #include <limits.h>
19
20 #include <unistd.h>     // close
21 #include <netdb.h>      // gethostbyname
22 #include <pwd.h>        // getpwuids
23 #include <sys/socket.h>
24 #include <netinet/in.h>
25 #include <arpa/inet.h>
26 #include <openssl/sha.h>        // SHA1
27
28 #define USE_NCURSES_INTERFACE   0
29 #define DEBUG_TRACE_SERVER      0
30 #define USE_AUTOAUTH    1
31
32 #define MAX_TXT_ARGS    5       // Maximum number of textual arguments (including command)
33 #define DISPENSE_MULTIPLE_MAX   20      // Maximum argument to -c
34
35 enum eUI_Modes
36 {
37         UI_MODE_BASIC,  // Non-NCurses
38         UI_MODE_STANDARD,
39         UI_MODE_DRINKSONLY,
40         UI_MODE_ALL,
41         NUM_UI_MODES
42 };
43
44 enum eReturnValues
45 {
46         RV_SUCCESS,
47         RV_BAD_ITEM,
48         RV_INVALID_USER,
49         RV_PERMISSIONS,
50         RV_ARGUMENTS,
51         RV_BALANCE,
52         RV_UNKNOWN_ERROR = -1,
53         RV_SOCKET_ERROR = -2
54 };
55
56 // === TYPES ===
57 typedef struct sItem {
58         char    *Type;
59          int    ID;
60          int    Status; // 0: Availiable, 1: Sold out, -1: Error
61         char    *Desc;
62          int    Price;
63 }       tItem;
64
65 // === PROTOTYPES ===
66  int    main(int argc, char *argv[]);
67 void    ShowUsage(void);
68 // --- GUI ---
69  int    ShowNCursesUI(void);
70  int    ShowItemAt(int Row, int Col, int Width, int Index, int bHilighted);
71 void    PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1, const char *Mid, char Pad2, const char *Right, ...);
72 // --- Coke Server Communication ---
73  int    OpenConnection(const char *Host, int Port);
74  int    Authenticate(int Socket);
75  int    GetUserBalance(int Socket);
76 void    PopulateItemList(int Socket);
77  int    Dispense_ItemInfo(int Socket, const char *Type, int ID);
78  int    DispenseItem(int Socket, const char *Type, int ID);
79  int    Dispense_AlterBalance(int Socket, const char *Username, int Ammount, const char *Reason);
80  int    Dispense_SetBalance(int Socket, const char *Username, int Balance, const char *Reason);
81  int    Dispense_Give(int Socket, const char *Username, int Ammount, const char *Reason);
82  int    Dispense_Refund(int Socket, const char *Username, const char *Item, int PriceOverride);
83  int    Dispense_Donate(int Socket, int Ammount, const char *Reason);
84  int    Dispense_EnumUsers(int Socket);
85  int    Dispense_ShowUser(int Socket, const char *Username);
86 void    _PrintUserLine(const char *Line);
87  int    Dispense_AddUser(int Socket, const char *Username);
88  int    Dispense_SetUserType(int Socket, const char *Username, const char *TypeString);
89 // --- Helpers ---
90 char    *ReadLine(int Socket);
91  int    sendf(int Socket, const char *Format, ...);
92 char    *trim(char *string);
93  int    RunRegex(regex_t *regex, const char *string, int nMatches, regmatch_t *matches, const char *errorMessage);
94 void    CompileRegex(regex_t *regex, const char *pattern, int flags);
95
96 // === GLOBALS ===
97 char    *gsDispenseServer = "heathred";
98  int    giDispensePort = 11020;
99
100 tItem   *gaItems;
101  int    giNumItems;
102 regex_t gArrayRegex, gItemRegex, gSaltRegex, gUserInfoRegex, gUserItemIdentRegex;
103  int    gbIsAuthenticated = 0;
104
105 char    *gsItemPattern; //!< Item pattern
106 char    *gsEffectiveUser;       //!< '-u' Dispense as another user
107  int    giUIMode = UI_MODE_STANDARD;
108  int    gbDryRun = 0;   //!< '-n' Read-only
109  int    giMinimumBalance = INT_MIN;     //!< '-m' Minumum balance for `dispense acct`
110  int    giMaximumBalance = INT_MAX;     //!< '-M' Maximum balance for `dispense acct`
111 char    *gsUserName;    //!< User that dispense will happen as
112 char    *gsUserFlags;   //!< User's flag set
113  int    giUserBalance=-1;       //!< User balance (set by Authenticate)
114  int    giDispenseCount = 1;    //!< Number of dispenses to do
115
116 // === CODE ===
117 int main(int argc, char *argv[])
118 {
119          int    sock;
120          int    i, ret = 0;
121         char    buffer[BUFSIZ];
122         char    *text_args[MAX_TXT_ARGS];       // Non-flag arguments
123          int    text_argc = 0;
124         
125         text_args[0] = "";
126
127         // -- Create regular expressions
128         // > Code Type Count ...
129         CompileRegex(&gArrayRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+([0-9]+)", REG_EXTENDED);     //
130         // > Code Type Ident Status Price Desc
131         CompileRegex(&gItemRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+([A-Za-z]+):([0-9]+)\\s+(avail|sold|error)\\s+([0-9]+)\\s+(.+)$", REG_EXTENDED);
132         // > Code 'SALT' salt
133         CompileRegex(&gSaltRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+(.+)$", REG_EXTENDED);
134         // > Code 'User' Username Balance Flags
135         CompileRegex(&gUserInfoRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+([^ ]+)\\s+(-?[0-9]+)\\s+(.+)$", REG_EXTENDED);
136         // > Item Ident
137         CompileRegex(&gUserItemIdentRegex, "^([A-Za-z]+):([0-9]+)$", REG_EXTENDED);
138
139         // Parse Arguments
140         for( i = 1; i < argc; i ++ )
141         {
142                 char    *arg = argv[i];
143                 
144                 if( arg[0] == '-' )
145                 {                       
146                         switch(arg[1])
147                         {
148                         case 'h':
149                         case '?':
150                                 ShowUsage();
151                                 return 0;
152                                         
153                         case 'c':
154                                 if( i + 1 >= argc ) {
155                                         fprintf(stderr, "%s: -c takes an argument\n", argv[0]);
156                                         ShowUsage();
157                                         return -1;
158                                 }
159                                 giDispenseCount = atoi(argv[++i]);
160                                 if( giDispenseCount < 1 || giDispenseCount > DISPENSE_MULTIPLE_MAX ) {
161                                         fprintf(stderr, "Sorry, only 1-20 can be passed to -c (safety)\n");
162                                         return -1;
163                                 }
164                                 
165                                 break ;
166         
167                         case 'm':       // Minimum balance
168                                 if( i + 1 >= argc ) {
169                                         fprintf(stderr, "%s: -m takes an argument\n", argv[0]);
170                                         ShowUsage();
171                                         return RV_ARGUMENTS;
172                                 }
173                                 giMinimumBalance = atoi(argv[++i]);
174                                 break;
175                         case 'M':       // Maximum balance
176                                 if( i + 1 >= argc ) {
177                                         fprintf(stderr, "%s: -M takes an argument\n", argv[0]);
178                                         ShowUsage();
179                                         return RV_ARGUMENTS;
180                                 }
181                                 giMaximumBalance = atoi(argv[++i]);
182                                 break;
183                         
184                         case 'u':       // Override User
185                                 if( i + 1 >= argc ) {
186                                         fprintf(stderr, "%s: -u takes an argument\n", argv[0]);
187                                         ShowUsage();
188                                         return RV_ARGUMENTS;
189                                 }
190                                 gsEffectiveUser = argv[++i];
191                                 break;
192                         
193                         case 'H':       // Override remote host
194                                 if( i + 1 >= argc ) {
195                                         fprintf(stderr, "%s: -H takes an argument\n", argv[0]);
196                                         ShowUsage();
197                                         return RV_ARGUMENTS;
198                                 }
199                                 gsDispenseServer = argv[++i];
200                                 break;
201                         case 'P':       // Override remote port
202                                 if( i + 1 >= argc ) {
203                                         fprintf(stderr, "%s: -P takes an argument\n", argv[0]);
204                                         ShowUsage();
205                                         return RV_ARGUMENTS;
206                                 }
207                                 giDispensePort = atoi(argv[++i]);
208                                 break;
209                         
210                         case 'G':       // Don't use GUI
211                                 giUIMode = UI_MODE_BASIC;
212                                 break;
213                         case 'D':       // Drinks only
214                                 giUIMode = UI_MODE_DRINKSONLY;
215                                 break;
216                         case 'n':       // Dry Run / read-only
217                                 gbDryRun = 1;
218                                 break;
219                         default:
220                                 if( text_argc + 1 ==  MAX_TXT_ARGS )
221                                 {
222                                         fprintf(stderr, "ERROR: Too many arguments\n");
223                                         return RV_ARGUMENTS;
224                                 }
225                                 text_args[text_argc++] = argv[i];
226                                 break;
227                         }
228
229                         continue;
230                 }
231
232                 if( text_argc + 1 == MAX_TXT_ARGS )
233                 {
234                         fprintf(stderr, "ERROR: Too many arguments\n");
235                         return RV_ARGUMENTS;
236                 }
237         
238                 text_args[text_argc++] = argv[i];
239         
240         }
241
242         //
243         // `dispense acct`
244         // - 
245         if( strcmp(text_args[0], "acct") == 0 )
246         {
247                 // Connect to server
248                 sock = OpenConnection(gsDispenseServer, giDispensePort);
249                 if( sock < 0 )  return RV_SOCKET_ERROR;
250                 // List accounts?
251                 if( text_argc == 1 ) {
252                         ret = Dispense_EnumUsers(sock);
253                         close(sock);
254                         return ret;
255                 }
256                         
257                 // text_args[1]: Username
258                 
259                 // Alter account?
260                 if( text_argc != 2 )
261                 {
262                         if( text_argc != 4 ) {
263                                 fprintf(stderr, "`dispense acct` requires a reason\n");
264                                 ShowUsage();
265                                 return RV_ARGUMENTS;
266                         }
267                         
268                         // Authentication required
269                         ret = Authenticate(sock);
270                         if(ret) return ret;
271                         
272                         // text_args[1]: Username
273                         // text_args[2]: Ammount
274                         // text_args[3]: Reason
275                         
276                         if( text_args[2][0] == '=' ) {
277                                 // Set balance
278                                 if( text_args[2][1] != '0' && atoi(text_args[2]+1) == 0 ) {
279                                         fprintf(stderr, "Error: Invalid balance to be set\n");
280                                         exit(1);
281                                 }
282                                 
283                                 ret = Dispense_SetBalance(sock, text_args[1], atoi(text_args[2]+1), text_args[3]);
284                         }
285                         else {
286                                 // Alter balance
287                                 ret = Dispense_AlterBalance(sock, text_args[1], atoi(text_args[2]), text_args[3]);
288                         }
289                 }
290                 // TODO: Preserve ret if non-zero
291                 
292                 // Show user information
293                 ret = Dispense_ShowUser(sock, text_args[1]);
294                 
295                 close(sock);
296                 return ret;
297         }
298         //
299         // `dispense give`
300         // - "Here, have some money."
301         if( strcmp(text_args[0], "give") == 0 )
302         {
303                 if( text_argc != 4 ) {
304                         fprintf(stderr, "`dispense give` takes three arguments\n");
305                         ShowUsage();
306                         return RV_ARGUMENTS;
307                 }
308                 
309                 // text_args[1]: Destination
310                 // text_args[2]: Ammount
311                 // text_args[3]: Reason
312                 
313                 // Connect to server
314                 sock = OpenConnection(gsDispenseServer, giDispensePort);
315                 if( sock < 0 )  return RV_SOCKET_ERROR;
316                 
317                 // Authenticate
318                 ret = Authenticate(sock);
319                 if(ret) return ret;
320                 
321                 ret = Dispense_Give(sock, text_args[1], atoi(text_args[2]), text_args[3]);
322
323                 close(sock);
324         
325                 return ret;
326         }
327         // 
328         // `dispense user`
329         // - User administration (Admin Only)
330         if( strcmp(text_args[0], "user") == 0 )
331         {
332                 // Check argument count
333                 if( text_argc == 1 ) {
334                         fprintf(stderr, "Error: `dispense user` requires arguments\n");
335                         ShowUsage();
336                         return RV_ARGUMENTS;
337                 }
338                 
339                 // Connect to server
340                 sock = OpenConnection(gsDispenseServer, giDispensePort);
341                 if( sock < 0 )  return RV_SOCKET_ERROR;
342                 
343                 // Attempt authentication
344                 ret = Authenticate(sock);
345                 if(ret) return ret;
346                 
347                 // Add new user?
348                 if( strcmp(text_args[1], "add") == 0 )
349                 {
350                         if( text_argc != 3 ) {
351                                 fprintf(stderr, "Error: `dispense user add` requires an argument\n");
352                                 ShowUsage();
353                                 return RV_ARGUMENTS;
354                         }
355                         
356                         ret = Dispense_AddUser(sock, text_args[2]);
357                 }
358                 // Update a user
359                 else if( strcmp(text_args[1], "type") == 0 || strcmp(text_args[1], "flags") == 0 )
360                 {
361                         if( text_argc != 4 ) {
362                                 fprintf(stderr, "Error: `dispense user flags` requires two arguments\n");
363                                 ShowUsage();
364                                 return RV_ARGUMENTS;
365                         }
366                         
367                         ret = Dispense_SetUserType(sock, text_args[2], text_args[3]);
368                 }
369                 else
370                 {
371                         fprintf(stderr, "Error: Unknown sub-command for `dispense user`\n");
372                         ShowUsage();
373                         return RV_ARGUMENTS;
374                 }
375                 close(sock);
376                 return ret;
377         }
378         // Donation!
379         else if( strcmp(text_args[0], "donate") == 0 )
380         {
381                 // Check argument count
382                 if( text_argc != 3 ) {
383                         fprintf(stderr, "Error: `dispense donate` requires two arguments\n");
384                         ShowUsage();
385                         return RV_ARGUMENTS;
386                 }
387                 
388                 // Connect to server
389                 sock = OpenConnection(gsDispenseServer, giDispensePort);
390                 if( sock < 0 )  return RV_SOCKET_ERROR;
391                 
392                 // Attempt authentication
393                 ret = Authenticate(sock);
394                 if(ret) return ret;
395                 
396                 // Do donation
397                 ret = Dispense_Donate(sock, atoi(text_args[1]), text_args[2]);
398                                 
399                 close(sock);
400
401                 return ret;
402         }
403         // Refund an item
404         else if( strcmp(text_args[0], "refund") == 0 )
405         {
406                  int     price = 0;
407                 // Check argument count
408                 if( text_argc != 3 && text_argc != 4 ) {
409                         fprintf(stderr, "Error: `dispense refund` takes 2 or 3 arguments\n");
410                         ShowUsage();
411                         return RV_ARGUMENTS;
412                 }
413         
414                 // Connect to server
415                 sock = OpenConnection(gsDispenseServer, giDispensePort);
416                 if(sock < 0)    return RV_SOCKET_ERROR; 
417
418                 // Attempt authentication
419                 ret = Authenticate(sock);
420                 if(ret) return ret;
421
422                 if( text_argc == 4 ) {
423                         price = atoi(text_args[3]);
424                         if( price <= 0 ) {
425                                 fprintf(stderr, "Error: Override price is invalid (should be > 0)\n");
426                                 return RV_ARGUMENTS;
427                         }
428                 }
429
430                 // Username, Item, cost
431                 ret = Dispense_Refund(sock, text_args[1], text_args[2], price);
432
433                 // TODO: More
434                 close(sock);
435                 return RV_UNKNOWN_ERROR;
436         }
437         // Query an item price
438         else if( strcmp(text_args[0], "iteminfo") == 0 )
439         {
440                 regmatch_t matches[3];
441                 char    *type;
442                  int    id;
443                 // Check argument count
444                 if( text_argc != 2 ) {
445                         fprintf(stderr, "Error: `dispense iteminfo` requires an argument\n");
446                         ShowUsage();
447                         return RV_ARGUMENTS;
448                 }
449                 // Parse item ID
450                 if( RunRegex(&gUserItemIdentRegex, text_args[1], 3, matches, NULL) != 0 ) {
451                         fprintf(stderr, "Error: Invalid item ID passed (<type>:<id> expected)\n");
452                         return RV_ARGUMENTS;
453                 }
454                 type = text_args[1] + matches[1].rm_so;
455                 text_args[1][ matches[1].rm_eo ] = '\0';
456                 id = atoi( text_args[1] + matches[2].rm_so );
457
458                 sock = OpenConnection(gsDispenseServer, giDispensePort);
459                 if( sock < 0 )  return RV_SOCKET_ERROR;
460                 
461                 ret = Dispense_ItemInfo(sock, type, id);
462                 close(sock);
463                 return ret;
464         }
465         // Item name / pattern
466         else {
467                 gsItemPattern = text_args[0];
468         }
469         
470         // Connect to server
471         sock = OpenConnection(gsDispenseServer, giDispensePort);
472         if( sock < 0 )  return RV_SOCKET_ERROR;
473
474         // Get the user's balance
475         ret = GetUserBalance(sock);
476         if(ret) return ret;
477
478         // Get items
479         PopulateItemList(sock);
480         
481         // Disconnect from server
482         close(sock);
483         
484         if( gsItemPattern && gsItemPattern[0] )
485         {
486                 regmatch_t matches[3];
487                 // Door (hard coded)
488                 if( strcmp(gsItemPattern, "door") == 0 )
489                 {
490                         // Connect, Authenticate, dispense and close
491                         sock = OpenConnection(gsDispenseServer, giDispensePort);
492                         if( sock < 0 )  return RV_SOCKET_ERROR;
493                         ret = Authenticate(sock);
494                         if(ret) return ret;
495                         ret = DispenseItem(sock, "door", 0);
496                         close(sock);
497                         return ret;
498                 }
499                 // Item id (<type>:<num>)
500                 else if( RunRegex(&gUserItemIdentRegex, gsItemPattern, 3, matches, NULL) == 0 )
501                 {
502                         char    *ident;
503                          int    id;
504                         
505                         // Get and finish ident
506                         ident = gsItemPattern + matches[1].rm_so;
507                         gsItemPattern[matches[1].rm_eo] = '\0';
508                         // Get ID
509                         id = atoi( gsItemPattern + matches[2].rm_so );
510                         
511                         // Connect, Authenticate, dispense and close
512                         sock = OpenConnection(gsDispenseServer, giDispensePort);
513                         if( sock < 0 )  return RV_SOCKET_ERROR;
514                         
515                         Dispense_ItemInfo(sock, ident, id);
516                         
517                         ret = Authenticate(sock);
518                         if(ret) return ret;
519                         ret = DispenseItem(sock, ident, id);
520                         close(sock);
521                         return ret;
522                 }
523                 // Item number (6 = coke)
524                 else if( strcmp(gsItemPattern, "0") == 0 || atoi(gsItemPattern) > 0 )
525                 {
526                         i = atoi(gsItemPattern);
527                 }
528                 // Item prefix
529                 else
530                 {
531                          int    j;
532                          int    best = -1;
533                         for( i = 0; i < giNumItems; i ++ )
534                         {
535                                 // Prefix match (with case-insensitive match)
536                                 for( j = 0; gsItemPattern[j]; j ++ )
537                                 {
538                                         if( gaItems[i].Desc[j] == gsItemPattern[j] )
539                                                 continue;
540                                         if( tolower(gaItems[i].Desc[j]) == tolower(gsItemPattern[j]) )
541                                                 continue;
542                                         break;
543                                 }
544                                 // Check if the prefix matched
545                                 if( gsItemPattern[j] != '\0' )
546                                         continue;
547                                 
548                                 // Prefect match
549                                 if( gaItems[i].Desc[j] == '\0' ) {
550                                         best = i;
551                                         break;
552                                 }
553                                 
554                                 // Only one match allowed
555                                 if( best == -1 ) {
556                                         best = i;
557                                 }
558                                 else {
559                                         // TODO: Allow ambiguous matches?
560                                         // or just print a wanrning
561                                         printf("Warning - Ambiguous pattern, stopping\n");
562                                         return RV_BAD_ITEM;
563                                 }
564                         }
565                         
566                         // Was a match found?
567                         if( best == -1 )
568                         {
569                                 fprintf(stderr, "No item matches the passed string\n");
570                                 return RV_BAD_ITEM;
571                         }
572                         
573                         i = best;
574                 }
575         }
576         else if( giUIMode != UI_MODE_BASIC )
577         {
578                 i = ShowNCursesUI();
579         }
580         else
581         {
582                 // Very basic dispense interface
583                 for( i = 0; i < giNumItems; i ++ ) {
584                         // Add a separator
585                         if( i && strcmp(gaItems[i].Type, gaItems[i-1].Type) != 0 )
586                                 printf("   ---\n");
587                         
588                         printf("%2i %s:%i\t%3i %s\n", i, gaItems[i].Type, gaItems[i].ID,
589                                 gaItems[i].Price, gaItems[i].Desc);
590                 }
591                 printf(" q Quit\n");
592                 for(;;)
593                 {
594                         char    *buf;
595                         
596                         i = -1;
597                         
598                         fgets(buffer, BUFSIZ, stdin);
599                         
600                         buf = trim(buffer);
601                         
602                         if( buf[0] == 'q' )     break;
603                         
604                         i = atoi(buf);
605                         
606                         if( i != 0 || buf[0] == '0' )
607                         {
608                                 if( i < 0 || i >= giNumItems ) {
609                                         printf("Bad item %i (should be between 0 and %i)\n", i, giNumItems);
610                                         continue;
611                                 }
612                                 break;
613                         }
614                 }
615         }
616         
617         
618         // Check for a valid item ID
619         if( i >= 0 )
620         {
621                  int j;
622                 // Connect, Authenticate, dispense and close
623                 sock = OpenConnection(gsDispenseServer, giDispensePort);
624                 if( sock < 0 )  return RV_SOCKET_ERROR;
625                         
626                 ret = Dispense_ItemInfo(sock, gaItems[i].Type, gaItems[i].ID);
627                 if(ret) return ret;
628                 
629                 ret = Authenticate(sock);
630                 if(ret) return ret;
631                 
632                 for( j = 0; j < giDispenseCount; j ++ ) {
633                         ret = DispenseItem(sock, gaItems[i].Type, gaItems[i].ID);
634                         if( ret )       break;
635                 }
636                 if( j > 1 ) {
637                         printf("%i items dispensed\n", j);
638                 }
639                 close(sock);
640         }
641
642         return ret;
643 }
644
645 void ShowUsage(void)
646 {
647         printf(
648                 "Usage:\n"
649                 "  == Everyone ==\n"
650                 "    dispense\n"
651                 "        Show interactive list\n"
652                 "    dispense <name>|<index>|<itemid>\n"
653                 "        Dispense named item (<name> matches if it is a unique prefix)\n"
654                 "    dispense give <user> <ammount> \"<reason>\"\n"
655                 "        Give money to another user\n"
656                 "    dispense donate <ammount> \"<reason>\"\n"
657                 "        Donate to the club\n"
658                 "    dispense iteminfo <type>:<id>\n"
659                 "        Get the name and price for an item\n"
660                 "  == Coke members == \n"
661                 "    dispense acct [<user>]\n"
662                 "        Show user balances\n"
663                 "    dispense acct <user> [+-]<ammount> \"<reason>\"\n"
664                 "        Alter a account value\n"
665                 "  == Dispense administrators ==\n"
666                 "    dispense acct <user> =<ammount> \"<reason>\"\n"
667                 "        Set an account balance\n"
668                 "    dispense user add <user>\n"
669                 "        Create new coke account (Admins only)\n"
670                 "    dispense user type <user> <flags>\n"
671                 "        Alter a user's flags\n"
672                 "        <flags> is a comma-separated list of user, coke, admin or disabled\n"
673                 "        Flags are removed by preceding the name with '-' or '!'\n"
674                 "\n"
675                 "General Options:\n"
676                 "    -c <count>\n"
677                 "        Dispense multiple times\n"
678                 "    -u <username>\n"
679                 "        Set a different user (Coke members only)\n"
680                 "    -h / -?\n"
681                 "        Show help text\n"
682                 "    -G\n"
683                 "        Use alternate GUI\n"
684                 "    -m <min balance>\n"
685                 "    -M <max balance>\n"
686                 "        Set the Maximum/Minimum balances shown in `dispense acct`\n"
687                 );
688 }
689
690 // -------------------
691 // --- NCurses GUI ---
692 // -------------------
693 /**
694  * \brief Render the NCurses UI
695  */
696 int ShowNCursesUI(void)
697 {
698          int    ch;
699          int    i, times;
700          int    xBase, yBase;
701         const int       displayMinWidth = 40;
702         char    *titleString = "Dispense";
703          int    itemCount;
704          int    maxItemIndex;
705          int    itemBase = 0;
706          int    currentItem;
707          int    ret = -2;       // -2: Used for marking "no return yet"
708         
709         char    balance_str[5+1+2+1];   // If $9999.99 is too little, something's wrong
710         char    *username;
711         struct passwd *pwd;
712          
713          int    height, width;
714          
715         // Get Username
716         if( gsEffectiveUser )
717                 username = gsEffectiveUser;
718         else {
719                 pwd = getpwuid( getuid() );
720                 username = pwd->pw_name;
721         }
722         // Get balance
723         snprintf(balance_str, sizeof balance_str, "$%i.%02i", giUserBalance/100, abs(giUserBalance)%100);
724         
725         // Enter curses mode
726         initscr();
727         cbreak(); noecho();
728         
729         // Get max index
730         maxItemIndex = ShowItemAt(0, 0, 0, -1, 0);
731         // Get item count per screen
732         // - 6: randomly chosen (Need at least 3)
733         itemCount = LINES - 6;
734         if( itemCount > maxItemIndex )
735                 itemCount = maxItemIndex;
736         // Get first index
737         currentItem = 0;
738         while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
739                 currentItem ++;
740         
741         
742         // Get dimensions
743         height = itemCount + 3;
744         width = displayMinWidth;
745         
746         // Get positions
747         xBase = COLS/2 - width/2;
748         yBase = LINES/2 - height/2;
749         
750         for( ;; )
751         {
752                 // Header
753                 PrintAlign(yBase, xBase, width, "/", '-', titleString, '-', "\\");
754                 
755                 // Items
756                 for( i = 0; i < itemCount; i ++ )
757                 {
758                          int    pos = 0;
759                         
760                         move( yBase + 1 + i, xBase );
761                         printw("| ");
762                         
763                         pos += 2;
764                         
765                         // Check for the '...' row
766                         // - Oh god, magic numbers!
767                         if( (i == 0 && itemBase > 0)
768                          || (i == itemCount - 1 && itemBase < maxItemIndex - itemCount) )
769                         {
770                                 printw("     ...");     pos += 8;
771                                 times = (width - pos) - 1;
772                                 while(times--)  addch(' ');
773                         }
774                         // Show an item
775                         else {
776                                 ShowItemAt(
777                                         yBase + 1 + i, xBase + pos,     // Position
778                                         (width - pos) - 3,      // Width
779                                         itemBase + i,   // Index
780                                         !!(currentItem == itemBase + i) // Hilighted
781                                         );
782                                 printw("  ");
783                         }
784                         
785                         // Scrollbar (if needed)
786                         if( maxItemIndex > itemCount ) {
787                                 if( i == 0 ) {
788                                         addch('A');
789                                 }
790                                 else if( i == itemCount - 1 ) {
791                                         addch('V');
792                                 }
793                                 else {
794                                          int    percentage = itemBase * 100 / (maxItemIndex-itemCount);
795                                         if( i-1 == percentage*(itemCount-3)/100 ) {
796                                                 addch('#');
797                                         }
798                                         else {
799                                                 addch('|');
800                                         }
801                                 }
802                         }
803                         else {
804                                 addch('|');
805                         }
806                 }
807                 
808                 // Footer
809                 PrintAlign(yBase+height-2, xBase, width, "\\", '-', "", '-', "/");
810                 
811                 // User line
812                 // - Username, balance, flags
813                 PrintAlign(yBase+height-1, xBase+1, width-2,
814                         username, ' ', balance_str, ' ', gsUserFlags);
815                 
816                 
817                 // Get input
818                 ch = getch();
819                 
820                 if( ch == '\x1B' ) {
821                         ch = getch();
822                         if( ch == '[' ) {
823                                 ch = getch();
824                                 
825                                 switch(ch)
826                                 {
827                                 case 'B':
828                                         currentItem ++;
829                                         // Skip over spacers
830                                         while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
831                                                 currentItem ++;
832                                         
833                                         if( currentItem >= maxItemIndex ) {
834                                                 currentItem = 0;
835                                                 // Skip over spacers
836                                                 while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
837                                                         currentItem ++;
838                                         }
839                                         break;
840                                 case 'A':
841                                         currentItem --;
842                                         // Skip over spacers
843                                         while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
844                                                 currentItem --;
845                                         
846                                         if( currentItem < 0 ) {
847                                                 currentItem = maxItemIndex - 1;
848                                                 // Skip over spacers
849                                                 while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
850                                                         currentItem --;
851                                         }
852                                         break;
853                                 }
854                         }
855                         else {
856                                 
857                         }
858                         
859                         if( itemCount > maxItemIndex && currentItem < itemBase + 2 && itemBase > 0 )
860                                 itemBase = currentItem - 2;
861                         if( itemCount > maxItemIndex && currentItem > itemBase + itemCount - 2 && itemBase < maxItemIndex-1 )
862                                 itemBase = currentItem - itemCount + 2;
863                 }
864                 else {
865                         switch(ch)
866                         {
867                         case '\n':
868                                 ret = ShowItemAt(0, 0, 0, currentItem, 0);
869                                 break;
870                         case 0x1b:      // Escape
871                         case 'q':
872                                 ret = -1;       // -1: Return with no dispense
873                                 break;
874                         }
875                         
876                         // Check if the return value was changed
877                         if( ret != -2 ) break;
878                 }
879                 
880         }
881         
882         
883         // Leave
884         endwin();
885         return ret;
886 }
887
888 /**
889  * \brief Show item \a Index at (\a Col, \a Row)
890  * \return Dispense index of item
891  * \note Part of the NCurses UI
892  */
893 int ShowItemAt(int Row, int Col, int Width, int Index, int bHilighted)
894 {
895          int    _x, _y, times;
896         char    *name = NULL;
897          int    price = 0;
898          int    status = -1;
899         
900         switch(giUIMode)
901         {
902         // Standard UI
903         // - This assumes that 
904         case UI_MODE_STANDARD:
905                 // Bounds check
906                 // Index = -1, request limit
907                 if( Index < 0 || Index >= giNumItems+2 )
908                         return giNumItems+2;
909                 // Drink label
910                 if( Index == 0 )
911                 {
912                         price = 0;
913                         name = "Coke Machine";
914                         Index = -1;     // -1 indicates a label
915                         break;
916                 }
917                 Index --;
918                 // Drinks 0 - 6
919                 if( Index <= 6 )
920                 {
921                         name = gaItems[Index].Desc;
922                         price = gaItems[Index].Price;
923                         status = gaItems[Index].Status;
924                         break;
925                 }
926                 Index -= 7;
927                 // EPS label
928                 if( Index == 0 )
929                 {
930                         price = 0;
931                         name = "Electronic Payment System";
932                         Index = -1;     // -1 indicates a label
933                         break;
934                 }
935                 Index --;
936                 Index += 7;
937                 name = gaItems[Index].Desc;
938                 price = gaItems[Index].Price;
939                 status = gaItems[Index].Status;
940                 break;
941         default:
942                 return -1;
943         }
944         
945         // Width = 0, don't print
946         if( Width > 0 )
947         {
948                 // 4 preceding, 5 price
949                 int nameWidth = Width - 4 - 5;
950                 move( Row, Col );
951                 
952                 if( Index >= 0 )
953                 {
954                         // Show hilight and status
955                         switch( status )
956                         {
957                         case 0:
958                                 if( bHilighted )
959                                         printw("->  ");
960                                 else
961                                         printw("    ");
962                                 break;
963                         case 1:
964                                 printw("SLD ");
965                                 break;
966                         
967                         default:
968                         case -1:
969                                 printw("ERR ");
970                                 break;
971                         }
972                         
973                         printw("%-*.*s", nameWidth, nameWidth, name);
974                 
975 //                      getyx(stdscr, _y, _x);
976                         // Assumes max 4 digit prices
977 //                      times = Width - 5 - (_x - Col); // TODO: Better handling for large prices
978 //                      while(times--)  addch(' ');
979                         
980                         printw(" %4i", price);
981                 }
982                 else
983                 {
984                         printw("-- %s", name);
985                         getyx(stdscr, _y, _x);
986                         times = Width - 4 - (_x - Col);
987                         while(times--)  addch(' ');
988                         printw("    ");
989                 }
990         }
991         
992         // If the item isn't availiable for sale, return -1 (so it's skipped)
993         if( status )
994                 Index = -1;
995         
996         return Index;
997 }
998
999 /**
1000  * \brief Print a three-part string at the specified position (formatted)
1001  * \note NCurses UI Helper
1002  * 
1003  * Prints \a Left on the left of the area, \a Right on the righthand side
1004  * and \a Mid in the middle of the area. These are padded with \a Pad1
1005  * between \a Left and \a Mid, and \a Pad2 between \a Mid and \a Right.
1006  * 
1007  * ::printf style format codes are allowed in \a Left, \a Mid and \a Right,
1008  * and the arguments to these are read in that order.
1009  */
1010 void PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1,
1011         const char *Mid, char Pad2, const char *Right, ...)
1012 {
1013          int    lLen, mLen, rLen;
1014          int    times;
1015         
1016         va_list args;
1017         
1018         // Get the length of the strings
1019         va_start(args, Right);
1020         lLen = vsnprintf(NULL, 0, Left, args);
1021         mLen = vsnprintf(NULL, 0, Mid, args);
1022         rLen = vsnprintf(NULL, 0, Right, args);
1023         va_end(args);
1024         
1025         // Sanity check
1026         if( lLen + mLen/2 > Width/2 || mLen/2 + rLen > Width/2 ) {
1027                 return ;        // TODO: What to do?
1028         }
1029         
1030         move(Row, Col);
1031         
1032         // Render strings
1033         va_start(args, Right);
1034         // - Left
1035         {
1036                 char    tmp[lLen+1];
1037                 vsnprintf(tmp, lLen+1, Left, args);
1038                 addstr(tmp);
1039         }
1040         // - Left padding
1041         times = (Width - mLen)/2 - lLen;
1042         while(times--)  addch(Pad1);
1043         // - Middle
1044         {
1045                 char    tmp[mLen+1];
1046                 vsnprintf(tmp, mLen+1, Mid, args);
1047                 addstr(tmp);
1048         }
1049         // - Right Padding
1050         times = (Width - mLen)/2 - rLen;
1051         if( (Width - mLen) % 2 )        times ++;
1052         while(times--)  addch(Pad2);
1053         // - Right
1054         {
1055                 char    tmp[rLen+1];
1056                 vsnprintf(tmp, rLen+1, Right, args);
1057                 addstr(tmp);
1058         }
1059 }
1060
1061 // ---------------------
1062 // --- Coke Protocol ---
1063 // ---------------------
1064 int OpenConnection(const char *Host, int Port)
1065 {
1066         struct hostent  *host;
1067         struct sockaddr_in      serverAddr;
1068          int    sock;
1069         
1070         host = gethostbyname(Host);
1071         if( !host ) {
1072                 fprintf(stderr, "Unable to look up '%s'\n", Host);
1073                 return -1;
1074         }
1075         
1076         memset(&serverAddr, 0, sizeof(serverAddr));
1077         
1078         serverAddr.sin_family = AF_INET;        // IPv4
1079         // NOTE: I have a suspicion that IPv6 will play sillybuggers with this :)
1080         serverAddr.sin_addr.s_addr = *((unsigned long *) host->h_addr_list[0]);
1081         serverAddr.sin_port = htons(Port);
1082         
1083         sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
1084         if( sock < 0 ) {
1085                 fprintf(stderr, "Failed to create socket\n");
1086                 return -1;
1087         }
1088
1089 //      printf("geteuid() = %i, getuid() = %i\n", geteuid(), getuid());
1090         
1091         if( geteuid() == 0 || getuid() == 0 )
1092         {
1093                  int    i;
1094                 struct sockaddr_in      localAddr;
1095                 memset(&localAddr, 0, sizeof(localAddr));
1096                 localAddr.sin_family = AF_INET; // IPv4
1097                 
1098                 // Loop through all the top ports until one is avaliable
1099                 for( i = 512; i < 1024; i ++)
1100                 {
1101                         localAddr.sin_port = htons(i);  // IPv4
1102                         // Attempt to bind to low port for autoauth
1103                         if( bind(sock, (struct sockaddr*)&localAddr, sizeof(localAddr)) == 0 )
1104                                 break;
1105                 }
1106                 if( i == 1024 )
1107                         printf("Warning: AUTOAUTH unavaliable\n");
1108 //              else
1109 //                      printf("Bound to 0.0.0.0:%i\n", i);
1110         }
1111         
1112         if( connect(sock, (struct sockaddr *) &serverAddr, sizeof(serverAddr)) < 0 ) {
1113                 fprintf(stderr, "Failed to connect to server\n");
1114                 return -1;
1115         }
1116
1117         // We're not authenticated if the connection has just opened
1118         gbIsAuthenticated = 0;
1119         
1120         return sock;
1121 }
1122
1123 /**
1124  * \brief Authenticate with the server
1125  * \return Boolean Failure
1126  */
1127 int Authenticate(int Socket)
1128 {
1129         struct passwd   *pwd;
1130         char    *buf;
1131          int    responseCode;
1132         #if ATTEMPT_PASSWORD_AUTH
1133         char    salt[32];
1134          int    i;
1135         regmatch_t      matches[4];
1136         #endif
1137         
1138         if( gbIsAuthenticated ) return 0;
1139         
1140         // Get user name
1141         pwd = getpwuid( getuid() );
1142         
1143         // Attempt automatic authentication
1144         sendf(Socket, "AUTOAUTH %s\n", pwd->pw_name);
1145         
1146         // Check if it worked
1147         buf = ReadLine(Socket);
1148         
1149         responseCode = atoi(buf);
1150         switch( responseCode )
1151         {
1152         case 200:       // Autoauth succeeded, return
1153                 free(buf);
1154                 break;
1155         
1156         case 401:       // Untrusted, attempt password authentication
1157                 free(buf);
1158
1159                 #if ATTEMPT_PASSWORD_AUTH       
1160                 sendf(Socket, "USER %s\n", pwd->pw_name);
1161                 printf("Using username %s\n", pwd->pw_name);
1162                 
1163                 buf = ReadLine(Socket);
1164                 
1165                 // TODO: Get Salt
1166                 // Expected format: 100 SALT <something> ...
1167                 // OR             : 100 User Set
1168                 RunRegex(&gSaltRegex, buf, 4, matches, "Malformed server response");
1169                 responseCode = atoi(buf);
1170                 if( responseCode != 100 ) {
1171                         fprintf(stderr, "Unknown repsonse code %i from server\n%s\n", responseCode, buf);
1172                         free(buf);
1173                         return RV_UNKNOWN_ERROR;        // ERROR
1174                 }
1175                 
1176                 // Check for salt
1177                 if( memcmp( buf+matches[2].rm_so, "SALT", matches[2].rm_eo - matches[2].rm_so) == 0) {
1178                         // Store it for later
1179                         memcpy( salt, buf + matches[3].rm_so, matches[3].rm_eo - matches[3].rm_so );
1180                         salt[ matches[3].rm_eo - matches[3].rm_so ] = 0;
1181                 }
1182                 free(buf);
1183                 
1184                 // Give three attempts
1185                 for( i = 0; i < 3; i ++ )
1186                 {
1187                          int    ofs = strlen(pwd->pw_name)+strlen(salt);
1188                         char    tmpBuf[42];
1189                         char    tmp[ofs+20];
1190                         char    *pass = getpass("Password: ");
1191                         uint8_t h[20];
1192                         
1193                         // Create hash string
1194                         // <username><salt><hash>
1195                         strcpy(tmp, pwd->pw_name);
1196                         strcat(tmp, salt);
1197                         SHA1( (unsigned char*)pass, strlen(pass), h );
1198                         memcpy(tmp+ofs, h, 20);
1199                         
1200                         // Hash all that
1201                         SHA1( (unsigned char*)tmp, ofs+20, h );
1202                         sprintf(tmpBuf, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
1203                                 h[ 0], h[ 1], h[ 2], h[ 3], h[ 4], h[ 5], h[ 6], h[ 7], h[ 8], h[ 9],
1204                                 h[10], h[11], h[12], h[13], h[14], h[15], h[16], h[17], h[18], h[19]
1205                                 );
1206                 
1207                         // Send password
1208                         sendf(Socket, "PASS %s\n", tmpBuf);
1209                         buf = ReadLine(Socket);
1210                 
1211                         responseCode = atoi(buf);
1212                         // Auth OK?
1213                         if( responseCode == 200 )       break;
1214                         // Bad username/password
1215                         if( responseCode == 401 )       continue;
1216                         
1217                         fprintf(stderr, "Unknown repsonse code %i from server\n%s\n", responseCode, buf);
1218                         free(buf);
1219                         return RV_UNKNOWN_ERROR;
1220                 }
1221                 free(buf);
1222                 if( i == 3 )
1223                         return RV_INVALID_USER; // 2 = Bad Password
1224                 
1225                 #else
1226                 fprintf(stderr, "Untrusted host, AUTOAUTH unavaliable\n");
1227                 return RV_INVALID_USER;
1228                 #endif
1229                 break;
1230         
1231         case 404:       // Bad Username
1232                 fprintf(stderr, "Bad Username '%s'\n", pwd->pw_name);
1233                 free(buf);
1234                 return RV_INVALID_USER;
1235         
1236         default:
1237                 fprintf(stderr, "Unkown response code %i from server\n", responseCode);
1238                 printf("%s\n", buf);
1239                 free(buf);
1240                 return RV_UNKNOWN_ERROR;
1241         }
1242         
1243         // Set effective user
1244         if( gsEffectiveUser ) {
1245                 sendf(Socket, "SETEUSER %s\n", gsEffectiveUser);
1246                 
1247                 buf = ReadLine(Socket);
1248                 responseCode = atoi(buf);
1249                 
1250                 switch(responseCode)
1251                 {
1252                 case 200:
1253                         printf("Running as '%s' by '%s'\n", gsEffectiveUser, pwd->pw_name);
1254                         break;
1255                 
1256                 case 403:
1257                         printf("Only coke members can use `dispense -u`\n");
1258                         free(buf);
1259                         return RV_PERMISSIONS;
1260                 
1261                 case 404:
1262                         printf("Invalid user selected\n");
1263                         free(buf);
1264                         return RV_INVALID_USER;
1265                 
1266                 default:
1267                         fprintf(stderr, "Unkown response code %i from server\n", responseCode);
1268                         printf("%s\n", buf);
1269                         free(buf);
1270                         return RV_UNKNOWN_ERROR;
1271                 }
1272                 
1273                 free(buf);
1274         }
1275         
1276         gbIsAuthenticated = 1;
1277         
1278         return 0;
1279 }
1280
1281 int GetUserBalance(int Socket)
1282 {
1283         regmatch_t      matches[6];
1284         struct passwd   *pwd;
1285         char    *buf;
1286          int    responseCode;
1287         
1288         if( !gsUserName )
1289         {
1290                 if( gsEffectiveUser ) {
1291                         gsUserName = gsEffectiveUser;
1292                 }
1293                 else {
1294                         pwd = getpwuid( getuid() );
1295                         gsUserName = strdup(pwd->pw_name);
1296                 }
1297         }
1298         
1299         sendf(Socket, "USER_INFO %s\n", gsUserName);
1300         buf = ReadLine(Socket);
1301         responseCode = atoi(buf);
1302         switch(responseCode)
1303         {
1304         case 202:       break;  // Ok
1305         
1306         case 404:
1307                 printf("Invalid user? (USER_INFO failed)\n");
1308                 free(buf);
1309                 return RV_INVALID_USER;
1310         
1311         default:
1312                 fprintf(stderr, "Unkown response code %i from server\n", responseCode);
1313                 printf("%s\n", buf);
1314                 free(buf);
1315                 return RV_UNKNOWN_ERROR;
1316         }
1317
1318         RunRegex(&gUserInfoRegex, buf, 6, matches, "Malformed server response");
1319         
1320         giUserBalance = atoi( buf + matches[4].rm_so );
1321         gsUserFlags = strdup( buf + matches[5].rm_so );
1322         
1323         free(buf);
1324         
1325         return 0;
1326 }
1327
1328 /**
1329  * \brief Read an item info response from the server
1330  * \param Dest  Destination for the read item (strings will be on the heap)
1331  */
1332 int ReadItemInfo(int Socket, tItem *Dest)
1333 {
1334         char    *buf;
1335          int    responseCode;
1336         
1337         regmatch_t      matches[8];
1338         char    *statusStr;
1339         
1340         // Get item info
1341         buf = ReadLine(Socket);
1342         responseCode = atoi(buf);
1343         
1344         switch(responseCode)
1345         {
1346         case 202:       break;
1347         
1348         case 406:
1349                 printf("Bad item name\n");
1350                 free(buf);
1351                 return RV_BAD_ITEM;
1352         
1353         default:
1354                 fprintf(stderr, "Unknown response from dispense server (Response Code %i)\n%s", responseCode, buf);
1355                 free(buf);
1356                 return RV_UNKNOWN_ERROR;
1357         }
1358         
1359         RunRegex(&gItemRegex, buf, 8, matches, "Malformed server response");
1360         
1361         buf[ matches[3].rm_eo ] = '\0';
1362         buf[ matches[5].rm_eo ] = '\0';
1363         buf[ matches[7].rm_eo ] = '\0';
1364         
1365         statusStr = &buf[ matches[5].rm_so ];
1366         
1367         Dest->ID = atoi( buf + matches[4].rm_so );
1368         
1369         if( strcmp(statusStr, "avail") == 0 )
1370                 Dest->Status = 0;
1371         else if( strcmp(statusStr, "sold") == 0 )
1372                 Dest->Status = 1;
1373         else if( strcmp(statusStr, "error") == 0 )
1374                 Dest->Status = -1;
1375         else {
1376                 fprintf(stderr, "Unknown response from dispense server (status '%s')\n",
1377                         statusStr);
1378                 return RV_UNKNOWN_ERROR;
1379         }
1380         Dest->Price = atoi( buf + matches[6].rm_so );
1381         
1382         // Hack a little to reduce heap fragmentation
1383         {
1384                 char    tmpType[strlen(buf + matches[3].rm_so) + 1];
1385                 char    tmpDesc[strlen(buf + matches[7].rm_so) + 1];
1386                 strcpy(tmpType, buf + matches[3].rm_so);
1387                 strcpy(tmpDesc, buf + matches[7].rm_so);
1388                 free(buf);
1389                 Dest->Type = strdup( tmpType );
1390                 Dest->Desc = strdup( tmpDesc );
1391         }
1392         
1393         return 0;
1394 }
1395
1396 /**
1397  * \brief Fill the item information structure
1398  * \return Boolean Failure
1399  */
1400 void PopulateItemList(int Socket)
1401 {
1402         char    *buf;
1403          int    responseCode;
1404         
1405         char    *itemType, *itemStart;
1406          int    count, i;
1407         regmatch_t      matches[4];
1408         
1409         // Ask server for stock list
1410         send(Socket, "ENUM_ITEMS\n", 11, 0);
1411         buf = ReadLine(Socket);
1412         
1413         //printf("Output: %s\n", buf);
1414         
1415         responseCode = atoi(buf);
1416         if( responseCode != 201 ) {
1417                 fprintf(stderr, "Unknown response from dispense server (Response Code %i)\n", responseCode);
1418                 exit(RV_UNKNOWN_ERROR);
1419         }
1420         
1421         // - Get item list -
1422         
1423         // Expected format:
1424         //  201 Items <count>
1425         //  202 Item <count>
1426         RunRegex(&gArrayRegex, buf, 4, matches, "Malformed server response");
1427                 
1428         itemType = &buf[ matches[2].rm_so ];    buf[ matches[2].rm_eo ] = '\0';
1429         count = atoi( &buf[ matches[3].rm_so ] );
1430                 
1431         // Check array type
1432         if( strcmp(itemType, "Items") != 0 ) {
1433                 // What the?!
1434                 fprintf(stderr, "Unexpected array type, expected 'Items', got '%s'\n",
1435                         itemType);
1436                 exit(RV_UNKNOWN_ERROR);
1437         }
1438                 
1439         itemStart = &buf[ matches[3].rm_eo ];
1440         
1441         free(buf);
1442         
1443         giNumItems = count;
1444         gaItems = malloc( giNumItems * sizeof(tItem) );
1445         
1446         // Fetch item information
1447         for( i = 0; i < giNumItems; i ++ )
1448         {
1449                 ReadItemInfo( Socket, &gaItems[i] );
1450         }
1451         
1452         // Read end of list
1453         buf = ReadLine(Socket);
1454         responseCode = atoi(buf);
1455                 
1456         if( responseCode != 200 ) {
1457                 fprintf(stderr, "Unknown response from dispense server %i\n'%s'",
1458                         responseCode, buf
1459                         );
1460                 exit(-1);
1461         }
1462         
1463         free(buf);
1464 }
1465
1466
1467 /**
1468  * \brief Get information on an item
1469  * \return Boolean Failure
1470  */
1471 int Dispense_ItemInfo(int Socket, const char *Type, int ID)
1472 {
1473         tItem   item;
1474          int    ret;
1475         
1476         // Query
1477         sendf(Socket, "ITEM_INFO %s:%i\n", Type, ID);
1478         
1479         ret = ReadItemInfo(Socket, &item);
1480         if(ret) return ret;
1481         
1482         printf("%8s:%-2i %2i.%02i %s\n",
1483                 item.Type, item.ID,
1484                 item.Price/100, item.Price%100,
1485                 item.Desc);
1486         
1487         free(item.Type);
1488         free(item.Desc);
1489         
1490         return 0;
1491 }
1492
1493 /**
1494  * \brief Dispense an item
1495  * \return Boolean Failure
1496  */
1497 int DispenseItem(int Socket, const char *Type, int ID)
1498 {
1499          int    ret, responseCode;
1500         char    *buf;
1501         
1502         // Check for a dry run
1503         if( gbDryRun ) {
1504                 printf("Dry Run - No action\n");
1505                 return 0;
1506         }
1507         
1508         // Dispense!
1509         sendf(Socket, "DISPENSE %s:%i\n", Type, ID);
1510         buf = ReadLine(Socket);
1511         
1512         responseCode = atoi(buf);
1513         switch( responseCode )
1514         {
1515         case 200:
1516                 printf("Dispense OK\n");
1517                 ret = 0;
1518                 break;
1519         case 401:
1520                 printf("Not authenticated\n");
1521                 ret = RV_PERMISSIONS;
1522                 break;
1523         case 402:
1524                 printf("Insufficient balance\n");
1525                 ret = RV_BALANCE;
1526                 break;
1527         case 406:
1528                 printf("Bad item name\n");
1529                 ret = RV_BAD_ITEM;
1530                 break;
1531         case 500:
1532                 printf("Item failed to dispense, is the slot empty?\n");
1533                 ret = 1;
1534                 break;
1535         case 501:
1536                 printf("Dispense not possible (slot empty/permissions)\n");
1537                 ret = 1;
1538                 break;
1539         default:
1540                 printf("Unknown response code %i ('%s')\n", responseCode, buf);
1541                 ret = RV_UNKNOWN_ERROR;
1542                 break;
1543         }
1544         
1545         free(buf);
1546         return ret;
1547 }
1548
1549 /**
1550  * \brief Alter a user's balance
1551  */
1552 int Dispense_AlterBalance(int Socket, const char *Username, int Ammount, const char *Reason)
1553 {
1554         char    *buf;
1555          int    responseCode;
1556         
1557         // Check for a dry run
1558         if( gbDryRun ) {
1559                 printf("Dry Run - No action\n");
1560                 return 0;
1561         }
1562
1563         // Sanity
1564         if( Ammount == 0 ) {
1565                 printf("An ammount would be nice\n");
1566                 return RV_ARGUMENTS;
1567         }
1568         
1569         sendf(Socket, "ADD %s %i %s\n", Username, Ammount, Reason);
1570         buf = ReadLine(Socket);
1571         
1572         responseCode = atoi(buf);
1573         free(buf);
1574         
1575         switch(responseCode)
1576         {
1577         case 200:       return 0;       // OK
1578         case 402:
1579                 fprintf(stderr, "Insufficient balance\n");
1580                 return 1;
1581         case 403:       // Not in coke
1582                 fprintf(stderr, "You are not in coke (sucker)\n");
1583                 return 1;
1584         case 404:       // Unknown user
1585                 fprintf(stderr, "Unknown user '%s'\n", Username);
1586                 return 2;
1587         default:
1588                 fprintf(stderr, "Unknown response code %i\n", responseCode);
1589                 return -1;
1590         }
1591         
1592         return -1;
1593 }
1594
1595 /**
1596  * \brief Set a user's balance
1597  * \note Only avaliable to dispense admins
1598  */
1599 int Dispense_SetBalance(int Socket, const char *Username, int Balance, const char *Reason)
1600 {
1601         char    *buf;
1602          int    responseCode;
1603         
1604         // Check for a dry run
1605         if( gbDryRun ) {
1606                 printf("Dry Run - No action\n");
1607                 return 0;
1608         }
1609         
1610         sendf(Socket, "SET %s %i %s\n", Username, Balance, Reason);
1611         buf = ReadLine(Socket);
1612         
1613         responseCode = atoi(buf);
1614         free(buf);
1615         
1616         switch(responseCode)
1617         {
1618         case 200:       return 0;       // OK
1619         case 403:       // Not in coke
1620                 fprintf(stderr, "You are not an admin\n");
1621                 return 1;
1622         case 404:       // Unknown user
1623                 fprintf(stderr, "Unknown user '%s'\n", Username);
1624                 return 2;
1625         default:
1626                 fprintf(stderr, "Unknown response code %i\n", responseCode);
1627                 return -1;
1628         }
1629         
1630         return -1;
1631 }
1632
1633 /**
1634  * \brief Give money to another user
1635  */
1636 int Dispense_Give(int Socket, const char *Username, int Ammount, const char *Reason)
1637 {
1638         char    *buf;
1639          int    responseCode;
1640         
1641         if( Ammount < 0 ) {
1642                 printf("Sorry, you can only give, you can't take.\n");
1643                 return 1;
1644         }
1645         
1646         // Fast return on zero
1647         if( Ammount == 0 ) {
1648                 printf("Are you actually going to give any?\n");
1649                 return 1;
1650         }
1651         
1652         // Check for a dry run
1653         if( gbDryRun ) {
1654                 printf("Dry Run - No action\n");
1655                 return 0;
1656         }
1657         
1658         sendf(Socket, "GIVE %s %i %s\n", Username, Ammount, Reason);
1659
1660         buf = ReadLine(Socket);
1661         responseCode = atoi(buf);
1662         free(buf);      
1663         switch(responseCode)
1664         {
1665         case 200:
1666                 printf("Give succeeded\n");
1667                 return 0;       // OK
1668         
1669         case 402:       
1670                 fprintf(stderr, "Insufficient balance\n");
1671                 return 1;
1672         
1673         case 404:       // Unknown user
1674                 fprintf(stderr, "Unknown user '%s'\n", Username);
1675                 return 2;
1676         
1677         default:
1678                 fprintf(stderr, "Unknown response code %i\n", responseCode);
1679                 return -1;
1680         }
1681         
1682         return -1;
1683 }
1684
1685 int Dispense_Refund(int Socket, const char *Username, const char *Item, int PriceOverride)
1686 {
1687         char    *buf;
1688          int    responseCode, ret = -1;
1689         
1690         // Check item id
1691         if( RunRegex(&gUserItemIdentRegex, Item, 0, NULL, NULL) != 0 )
1692         {
1693                 fprintf(stderr, "Error: Invalid item ID passed (should be <type>:<num>)\n");
1694                 return RV_ARGUMENTS;
1695         }
1696
1697         // Check username (quick)
1698         if( strchr(Username, ' ') || strchr(Username, '\n') )
1699         {
1700                 fprintf(stderr, "Error: Username is invalid (no spaces or newlines please)\n");
1701                 return RV_ARGUMENTS;
1702         }
1703
1704         // Send the query
1705         sendf(Socket, "REFUND %s %s %i", Username, Item, PriceOverride);
1706
1707         buf = ReadLine(Socket);
1708         responseCode = atoi(buf);
1709         switch(responseCode)
1710         {
1711         case 200:
1712                 Dispense_ShowUser(Socket, Username);    // Show destination account
1713                 ret = 0;
1714                 break;
1715         case 403:
1716                 fprintf(stderr, "Refund access is only avaliable to coke members\n");
1717                 ret = RV_PERMISSIONS;
1718                 break;
1719         case 404:
1720                 fprintf(stderr, "Unknown user '%s' passed\n", Username);
1721                 ret = RV_INVALID_USER;
1722                 break;
1723         case 406:
1724                 fprintf(stderr, "Invalid item '%s' passed\n", Item);
1725                 ret = RV_BAD_ITEM;
1726                 break;
1727         default:
1728                 fprintf(stderr, "Unknown response from server %i\n%s\n", responseCode, buf);
1729                 ret = -1;
1730                 break;
1731         }
1732         free(buf);
1733         return ret;
1734 }
1735
1736 /**
1737  * \brief Donate money to the club
1738  */
1739 int Dispense_Donate(int Socket, int Ammount, const char *Reason)
1740 {
1741         char    *buf;
1742          int    responseCode;
1743         
1744         if( Ammount < 0 ) {
1745                 printf("Sorry, you can only give, you can't take.\n");
1746                 return -1;
1747         }
1748         
1749         // Fast return on zero
1750         if( Ammount == 0 ) {
1751                 printf("Are you actually going to give any?\n");
1752                 return 1;
1753         }
1754         
1755         // Check for a dry run
1756         if( gbDryRun ) {
1757                 printf("Dry Run - No action\n");
1758                 return 0;
1759         }
1760         
1761         sendf(Socket, "DONATE %i %s\n", Ammount, Reason);
1762         buf = ReadLine(Socket);
1763         
1764         responseCode = atoi(buf);
1765         free(buf);
1766         
1767         switch(responseCode)
1768         {
1769         case 200:       return 0;       // OK
1770         
1771         case 402:       
1772                 fprintf(stderr, "Insufficient balance\n");
1773                 return 1;
1774         
1775         default:
1776                 fprintf(stderr, "Unknown response code %i\n", responseCode);
1777                 return -1;
1778         }
1779         
1780         return -1;
1781 }
1782
1783 /**
1784  * \brief Enumerate users
1785  */
1786 int Dispense_EnumUsers(int Socket)
1787 {
1788         char    *buf;
1789          int    responseCode;
1790          int    nUsers;
1791         regmatch_t      matches[4];
1792         
1793         if( giMinimumBalance != INT_MIN ) {
1794                 if( giMaximumBalance != INT_MAX ) {
1795                         sendf(Socket, "ENUM_USERS min_balance:%i max_balance:%i\n", giMinimumBalance, giMaximumBalance);
1796                 }
1797                 else {
1798                         sendf(Socket, "ENUM_USERS min_balance:%i\n", giMinimumBalance);
1799                 }
1800         }
1801         else {
1802                 if( giMaximumBalance != INT_MAX ) {
1803                         sendf(Socket, "ENUM_USERS max_balance:%i\n", giMaximumBalance);
1804                 }
1805                 else {
1806                         sendf(Socket, "ENUM_USERS\n");
1807                 }
1808         }
1809         buf = ReadLine(Socket);
1810         responseCode = atoi(buf);
1811         
1812         switch(responseCode)
1813         {
1814         case 201:       break;  // Ok, length follows
1815         
1816         default:
1817                 fprintf(stderr, "Unknown response code %i\n%s\n", responseCode, buf);
1818                 free(buf);
1819                 return -1;
1820         }
1821         
1822         // Get count (not actually used)
1823         RunRegex(&gArrayRegex, buf, 4, matches, "Malformed server response");
1824         nUsers = atoi( buf + matches[3].rm_so );
1825         printf("%i users returned\n", nUsers);
1826         
1827         // Free string
1828         free(buf);
1829         
1830         // Read returned users
1831         do {
1832                 buf = ReadLine(Socket);
1833                 responseCode = atoi(buf);
1834                 
1835                 if( responseCode != 202 )       break;
1836                 
1837                 _PrintUserLine(buf);
1838                 free(buf);
1839         } while(responseCode == 202);
1840         
1841         // Check final response
1842         if( responseCode != 200 ) {
1843                 fprintf(stderr, "Unknown response code %i\n%s\n", responseCode, buf);
1844                 free(buf);
1845                 return -1;
1846         }
1847         
1848         free(buf);
1849         
1850         return 0;
1851 }
1852
1853 int Dispense_ShowUser(int Socket, const char *Username)
1854 {
1855         char    *buf;
1856          int    responseCode, ret;
1857         
1858         sendf(Socket, "USER_INFO %s\n", Username);
1859         buf = ReadLine(Socket);
1860         
1861         responseCode = atoi(buf);
1862         
1863         switch(responseCode)
1864         {
1865         case 202:
1866                 _PrintUserLine(buf);
1867                 ret = 0;
1868                 break;
1869         
1870         case 404:
1871                 printf("Unknown user '%s'\n", Username);
1872                 ret = 1;
1873                 break;
1874         
1875         default:
1876                 fprintf(stderr, "Unknown response code %i '%s'\n", responseCode, buf);
1877                 ret = -1;
1878                 break;
1879         }
1880         
1881         free(buf);
1882         
1883         return ret;
1884 }
1885
1886 void _PrintUserLine(const char *Line)
1887 {
1888         regmatch_t      matches[6];
1889          int    bal;
1890         
1891         RunRegex(&gUserInfoRegex, Line, 6, matches, "Malformed server response");
1892         // 3: Username
1893         // 4: Balance
1894         // 5: Flags
1895         {
1896                  int    usernameLen = matches[3].rm_eo - matches[3].rm_so;
1897                 char    username[usernameLen + 1];
1898                  int    flagsLen = matches[5].rm_eo - matches[5].rm_so;
1899                 char    flags[flagsLen + 1];
1900                 
1901                 memcpy(username, Line + matches[3].rm_so, usernameLen);
1902                 username[usernameLen] = '\0';
1903                 memcpy(flags, Line + matches[5].rm_so, flagsLen);
1904                 flags[flagsLen] = '\0';
1905                 
1906                 bal = atoi(Line + matches[4].rm_so);
1907                 printf("%-15s: $%4i.%02i (%s)\n", username, bal/100, abs(bal)%100, flags);
1908         }
1909 }
1910
1911 int Dispense_AddUser(int Socket, const char *Username)
1912 {
1913         char    *buf;
1914          int    responseCode, ret;
1915         
1916         // Check for a dry run
1917         if( gbDryRun ) {
1918                 printf("Dry Run - No action\n");
1919                 return 0;
1920         }
1921         
1922         sendf(Socket, "USER_ADD %s\n", Username);
1923         
1924         buf = ReadLine(Socket);
1925         responseCode = atoi(buf);
1926         
1927         switch(responseCode)
1928         {
1929         case 200:
1930                 printf("User '%s' added\n", Username);
1931                 ret = 0;
1932                 break;
1933                 
1934         case 403:
1935                 printf("Only wheel can add users\n");
1936                 ret = 1;
1937                 break;
1938                 
1939         case 404:
1940                 printf("User '%s' already exists\n", Username);
1941                 ret = 0;
1942                 break;
1943         
1944         default:
1945                 fprintf(stderr, "Unknown response code %i '%s'\n", responseCode, buf);
1946                 ret = -1;
1947                 break;
1948         }
1949         
1950         free(buf);
1951         
1952         return ret;
1953 }
1954
1955 int Dispense_SetUserType(int Socket, const char *Username, const char *TypeString)
1956 {
1957         char    *buf;
1958          int    responseCode, ret;
1959         
1960         // Check for a dry run
1961         if( gbDryRun ) {
1962                 printf("Dry Run - No action\n");
1963                 return 0;
1964         }
1965         
1966         // TODO: Pre-validate the string
1967         
1968         sendf(Socket, "USER_FLAGS %s %s\n", Username, TypeString);
1969         
1970         buf = ReadLine(Socket);
1971         responseCode = atoi(buf);
1972         
1973         switch(responseCode)
1974         {
1975         case 200:
1976                 printf("User '%s' updated\n", Username);
1977                 ret = 0;
1978                 break;
1979                 
1980         case 403:
1981                 printf("Only wheel can modify users\n");
1982                 ret = 1;
1983                 break;
1984         
1985         case 404:
1986                 printf("User '%s' does not exist\n", Username);
1987                 ret = 0;
1988                 break;
1989         
1990         case 407:
1991                 printf("Flag string is invalid\n");
1992                 ret = 0;
1993                 break;
1994         
1995         default:
1996                 fprintf(stderr, "Unknown response code %i '%s'\n", responseCode, buf);
1997                 ret = -1;
1998                 break;
1999         }
2000         
2001         free(buf);
2002         
2003         return ret;
2004 }
2005
2006 // ---------------
2007 // --- Helpers ---
2008 // ---------------
2009 char *ReadLine(int Socket)
2010 {
2011         static char     buf[BUFSIZ];
2012         static int      bufPos = 0;
2013         static int      bufValid = 0;
2014          int    len;
2015         char    *newline = NULL;
2016          int    retLen = 0;
2017         char    *ret = malloc(10);
2018         
2019         #if DEBUG_TRACE_SERVER
2020         printf("ReadLine: ");
2021         #endif
2022         fflush(stdout);
2023         
2024         ret[0] = '\0';
2025         
2026         while( !newline )
2027         {
2028                 if( bufValid ) {
2029                         len = bufValid;
2030                 }
2031                 else {
2032                         len = recv(Socket, buf+bufPos, BUFSIZ-1-bufPos, 0);
2033                         buf[bufPos+len] = '\0';
2034                 }
2035                 
2036                 newline = strchr( buf+bufPos, '\n' );
2037                 if( newline ) {
2038                         *newline = '\0';
2039                 }
2040                 
2041                 retLen += strlen(buf+bufPos);
2042                 ret = realloc(ret, retLen + 1);
2043                 strcat( ret, buf+bufPos );
2044                 
2045                 if( newline ) {
2046                          int    newLen = newline - (buf+bufPos) + 1;
2047                         bufValid = len - newLen;
2048                         bufPos += newLen;
2049                 }
2050                 if( len + bufPos == BUFSIZ - 1 )        bufPos = 0;
2051         }
2052         
2053         #if DEBUG_TRACE_SERVER
2054         printf("%i '%s'\n", retLen, ret);
2055         #endif
2056         
2057         return ret;
2058 }
2059
2060 int sendf(int Socket, const char *Format, ...)
2061 {
2062         va_list args;
2063          int    len;
2064         
2065         va_start(args, Format);
2066         len = vsnprintf(NULL, 0, Format, args);
2067         va_end(args);
2068         
2069         {
2070                 char    buf[len+1];
2071                 va_start(args, Format);
2072                 vsnprintf(buf, len+1, Format, args);
2073                 va_end(args);
2074                 
2075                 #if DEBUG_TRACE_SERVER
2076                 printf("sendf: %s", buf);
2077                 #endif
2078                 
2079                 return send(Socket, buf, len, 0);
2080         }
2081 }
2082
2083 char *trim(char *string)
2084 {
2085          int    i;
2086         
2087         while( isspace(*string) )
2088                 string ++;
2089         
2090         for( i = strlen(string); i--; )
2091         {
2092                 if( isspace(string[i]) )
2093                         string[i] = '\0';
2094                 else
2095                         break;
2096         }
2097         
2098         return string;
2099 }
2100
2101 int RunRegex(regex_t *regex, const char *string, int nMatches, regmatch_t *matches, const char *errorMessage)
2102 {
2103          int    ret;
2104         
2105         ret = regexec(regex, string, nMatches, matches, 0);
2106         if( ret && errorMessage ) {
2107                 size_t  len = regerror(ret, regex, NULL, 0);
2108                 char    errorStr[len];
2109                 regerror(ret, regex, errorStr, len);
2110                 printf("string = '%s'\n", string);
2111                 fprintf(stderr, "%s\n%s", errorMessage, errorStr);
2112                 exit(-1);
2113         }
2114         
2115         return ret;
2116 }
2117
2118 void CompileRegex(regex_t *regex, const char *pattern, int flags)
2119 {
2120          int    ret = regcomp(regex, pattern, flags);
2121         if( ret ) {
2122                 size_t  len = regerror(ret, regex, NULL, 0);
2123                 char    errorStr[len];
2124                 regerror(ret, regex, errorStr, len);
2125                 fprintf(stderr, "Regex compilation failed - %s\n", errorStr);
2126                 exit(-1);
2127         }
2128 }

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