3 * UCC (University [of WA] Computer Club) Electronic Accounting System
6 * main.c - Core and Initialisation
8 * This file is licenced under the 3-clause BSD Licence. See the file
9 * COPYING for full details.
14 #include <ctype.h> // isspace
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
27 #define USE_NCURSES_INTERFACE 0
28 #define DEBUG_TRACE_SERVER 0
31 typedef struct sItem {
38 int main(int argc, char *argv[]);
41 int ShowNCursesUI(void);
42 void ShowItemAt(int Row, int Col, int Width, int Index);
43 void PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1, const char *Mid, char Pad2, const char *Right, ...);
44 // --- Coke Server Communication ---
45 int OpenConnection(const char *Host, int Port);
46 int Authenticate(int Socket);
47 void PopulateItemList(int Socket);
48 int DispenseItem(int Socket, int ItemID);
49 int Dispense_AlterBalance(int Socket, const char *Username, int Ammount, const char *Reason);
50 int Dispense_SetBalance(int Socket, const char *Username, int Ammount, const char *Reason);
51 int Dispense_EnumUsers(int Socket);
52 int Dispense_ShowUser(int Socket, const char *Username);
53 void _PrintUserLine(const char *Line);
55 char *ReadLine(int Socket);
56 int sendf(int Socket, const char *Format, ...);
57 char *trim(char *string);
58 int RunRegex(regex_t *regex, const char *string, int nMatches, regmatch_t *matches, const char *errorMessage);
59 void CompileRegex(regex_t *regex, const char *pattern, int flags);
62 char *gsDispenseServer = "localhost";
63 int giDispensePort = 11020;
67 regex_t gArrayRegex, gItemRegex, gSaltRegex, gUserInfoRegex;
68 int gbIsAuthenticated = 0;
70 char *gsOverrideUser; //!< '-u' argument (dispense as another user)
71 int gbUseNCurses = 0; //!< '-G' Use the NCurses GUI?
75 int main(int argc, char *argv[])
81 // -- Create regular expressions
82 // > Code Type Count ...
83 CompileRegex(&gArrayRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+([0-9]+)", REG_EXTENDED); //
84 // > Code Type Ident Price Desc
85 CompileRegex(&gItemRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+([A-Za-z0-9:]+?)\\s+([0-9]+)\\s+(.+)$", REG_EXTENDED);
87 CompileRegex(&gSaltRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+(.+)$", REG_EXTENDED);
88 // > Code 'User' Username Balance Flags
89 CompileRegex(&gUserInfoRegex, "^([0-9]{3})\\s+([A-Za-z]+)\\s+([^ ]+)\\s+(-?[0-9]+)\\s+(.+)$", REG_EXTENDED);
92 for( i = 1; i < argc; i ++ )
105 case 'u': // Override User
106 gsOverrideUser = argv[++i];
117 if( strcmp(arg, "acct") == 0 )
121 sock = OpenConnection(gsDispenseServer, giDispensePort);
122 if( sock < 0 ) return -1;
125 if( i + 1 == argc ) {
126 Dispense_EnumUsers(sock);
130 // argv[i+1]: Username
135 if( i + 3 >= argc ) {
136 fprintf(stderr, "Error: `dispense acct' needs a reason\n");
140 // Authentication required
143 // argv[i+1]: Username
144 // argv[i+2]: Ammount
147 if( argv[i+2][0] == '=' ) {
149 Dispense_SetBalance(sock, argv[i+1], atoi(argv[i+2] + 1), argv[i+3]);
153 Dispense_AlterBalance(sock, argv[i+1], atoi(argv[i+2]), argv[i+3]);
157 Dispense_ShowUser(sock, argv[i+1]);
163 // Item name / pattern
169 sock = OpenConnection(gsDispenseServer, giDispensePort);
170 if( sock < 0 ) return -1;
176 PopulateItemList(sock);
184 for( i = 0; i < giNumItems; i ++ ) {
185 printf("%2i %s\t%3i %s\n", i, gaItems[i].Ident, gaItems[i].Price, gaItems[i].Desc);
194 fgets(buffer, BUFSIZ, stdin);
198 if( buf[0] == 'q' ) break;
202 if( i != 0 || buf[0] == '0' )
204 if( i < 0 || i >= giNumItems ) {
205 printf("Bad item %i (should be between 0 and %i)\n", i, giNumItems);
213 // Check for a valid item ID
215 DispenseItem(sock, i);
227 "\t\tShow interactive list\n"
228 "\tdispense <item>\n"
229 "\t\tDispense named item\n"
230 "\tdispense give <user> <ammount> \"<reason>\"\n"
231 "\t\tGive some of your money away\n"
232 "\tdispense acct [<user>]\n"
233 "\t\tShow user balances\n"
234 "\tdispense acct <user> [+-=]<ammount> \"<reason>\"\n"
235 "\t\tAlter a account value (Coke members only)\n"
239 "\t\tSet a different user (Coke members only)\n"
241 "\t\tShow help text\n"
243 "\t\tUse alternate GUI\n"
247 // -------------------
248 // --- NCurses GUI ---
249 // -------------------
251 * \brief Render the NCurses UI
253 int ShowNCursesUI(void)
255 // TODO: ncurses interface (with separation between item classes)
256 // - Hmm... that would require standardising the item ID to be <class>:<index>
261 const int displayMinWidth = 40;
262 const int displayMinItems = 8;
263 char *titleString = "Dispense";
264 int itemCount = displayMinItems;
267 int ret = -2; // -2: Used for marking "no return yet"
276 // - 6: randomly chosen (Need at least 3)
277 itemCount = LINES - 6;
278 if( itemCount > giNumItems )
279 itemCount = giNumItems;
282 height = itemCount + 3;
283 width = displayMinWidth;
286 xBase = COLS/2 - width/2;
287 yBase = LINES/2 - height/2;
292 PrintAlign(yBase, xBase, width, "/", '-', titleString, '-', "\\");
295 for( i = 0; i < itemCount; i ++ )
297 move( yBase + 1 + i, xBase );
299 if( currentItem == itemBase + i ) {
307 // - Oh god, magic numbers!
308 if( i == 0 && itemBase > 0 ) {
310 times = width-1 - 8 - 3;
311 while(times--) addch(' ');
313 else if( i == itemCount - 1 && itemBase < giNumItems - itemCount ) {
315 times = width-1 - 8 - 3;
316 while(times--) addch(' ');
320 ShowItemAt( yBase + 1 + i, xBase + 5, width - 7, itemBase + i);
324 // Scrollbar (if needed)
325 if( giNumItems > itemCount ) {
329 else if( i == itemCount - 1 ) {
333 int percentage = itemBase * 100 / (giNumItems-itemCount);
334 if( i-1 == percentage*(itemCount-3)/100 ) {
348 PrintAlign(yBase+height-2, xBase, width, "\\", '-', "", '-', "/");
361 //if( itemBase < giNumItems - (itemCount) )
363 if( currentItem < giNumItems - 1 )
365 if( itemBase + itemCount - 1 <= currentItem && itemBase + itemCount < giNumItems )
371 if( currentItem > 0 )
373 if( itemBase + 1 > currentItem && itemBase > 0 )
389 ret = -1; // -1: Return with no dispense
393 // Check if the return value was changed
394 if( ret != -2 ) break;
406 * \brief Show item \a Index at (\a Col, \a Row)
407 * \note Part of the NCurses UI
409 void ShowItemAt(int Row, int Col, int Width, int Index)
417 if( Index < 0 || Index >= giNumItems ) {
422 name = gaItems[Index].Desc;
423 price = gaItems[Index].Price;
426 printw("%02i %s", Index, name);
428 getyx(stdscr, _y, _x);
429 // Assumes max 4 digit prices
430 times = Width - 4 - (_x - Col); // TODO: Better handling for large prices
431 while(times--) addch(' ');
432 printw("%4i", price);
436 * \brief Print a three-part string at the specified position (formatted)
437 * \note NCurses UI Helper
439 * Prints \a Left on the left of the area, \a Right on the righthand side
440 * and \a Mid in the middle of the area. These are padded with \a Pad1
441 * between \a Left and \a Mid, and \a Pad2 between \a Mid and \a Right.
443 * ::printf style format codes are allowed in \a Left, \a Mid and \a Right,
444 * and the arguments to these are read in that order.
446 void PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1,
447 const char *Mid, char Pad2, const char *Right, ...)
449 int lLen, mLen, rLen;
454 // Get the length of the strings
455 va_start(args, Right);
456 lLen = vsnprintf(NULL, 0, Left, args);
457 mLen = vsnprintf(NULL, 0, Mid, args);
458 rLen = vsnprintf(NULL, 0, Right, args);
462 if( lLen + mLen/2 > Width/2 || mLen/2 + rLen > Width/2 ) {
463 return ; // TODO: What to do?
469 va_start(args, Right);
473 vsnprintf(tmp, lLen+1, Left, args);
477 times = Width/2 - mLen/2 - lLen;
478 while(times--) addch(Pad1);
482 vsnprintf(tmp, mLen+1, Mid, args);
486 times = Width/2 - mLen/2 - rLen;
487 while(times--) addch(Pad2);
491 vsnprintf(tmp, rLen+1, Right, args);
496 // ---------------------
497 // --- Coke Protocol ---
498 // ---------------------
499 int OpenConnection(const char *Host, int Port)
501 struct hostent *host;
502 struct sockaddr_in serverAddr;
505 host = gethostbyname(Host);
507 fprintf(stderr, "Unable to look up '%s'\n", Host);
511 memset(&serverAddr, 0, sizeof(serverAddr));
513 serverAddr.sin_family = AF_INET; // IPv4
514 // NOTE: I have a suspicion that IPv6 will play sillybuggers with this :)
515 serverAddr.sin_addr.s_addr = *((unsigned long *) host->h_addr_list[0]);
516 serverAddr.sin_port = htons(Port);
518 sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
520 fprintf(stderr, "Failed to create socket\n");
526 struct sockaddr_in localAddr;
527 memset(&localAddr, 0, sizeof(localAddr));
528 localAddr.sin_family = AF_INET; // IPv4
529 localAddr.sin_port = 1023; // IPv4
530 // Attempt to bind to low port for autoauth
531 bind(sock, &localAddr, sizeof(localAddr));
535 if( connect(sock, (struct sockaddr *) &serverAddr, sizeof(serverAddr)) < 0 ) {
536 fprintf(stderr, "Failed to connect to server\n");
544 * \brief Authenticate with the server
545 * \return Boolean Failure
547 int Authenticate(int Socket)
554 regmatch_t matches[4];
556 if( gbIsAuthenticated ) return 0;
559 pwd = getpwuid( getuid() );
561 // Attempt automatic authentication
562 sendf(Socket, "AUTOAUTH %s\n", pwd->pw_name);
564 // Check if it worked
565 buf = ReadLine(Socket);
567 responseCode = atoi(buf);
568 switch( responseCode )
571 case 200: // Authenticated, return :)
572 gbIsAuthenticated = 1;
576 case 401: // Untrusted, attempt password authentication
579 sendf(Socket, "USER %s\n", pwd->pw_name);
580 printf("Using username %s\n", pwd->pw_name);
582 buf = ReadLine(Socket);
585 // Expected format: 100 SALT <something> ...
587 RunRegex(&gSaltRegex, buf, 4, matches, "Malformed server response");
588 responseCode = atoi(buf);
589 if( responseCode != 100 ) {
590 fprintf(stderr, "Unknown repsonse code %i from server\n%s\n", responseCode, buf);
596 if( memcmp( buf+matches[2].rm_so, "SALT", matches[2].rm_eo - matches[2].rm_so) == 0) {
597 // Store it for later
598 memcpy( salt, buf + matches[3].rm_so, matches[3].rm_eo - matches[3].rm_so );
599 salt[ matches[3].rm_eo - matches[3].rm_so ] = 0;
603 // Give three attempts
604 for( i = 0; i < 3; i ++ )
606 int ofs = strlen(pwd->pw_name)+strlen(salt);
609 char *pass = getpass("Password: ");
612 // Create hash string
613 // <username><salt><hash>
614 strcpy(tmp, pwd->pw_name);
616 SHA1( (unsigned char*)pass, strlen(pass), h );
617 memcpy(tmp+ofs, h, 20);
620 SHA1( (unsigned char*)tmp, ofs+20, h );
621 sprintf(tmpBuf, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
622 h[ 0], h[ 1], h[ 2], h[ 3], h[ 4], h[ 5], h[ 6], h[ 7], h[ 8], h[ 9],
623 h[10], h[11], h[12], h[13], h[14], h[15], h[16], h[17], h[18], h[19]
627 sendf(Socket, "PASS %s\n", tmpBuf);
628 buf = ReadLine(Socket);
630 responseCode = atoi(buf);
632 if( responseCode == 200 ) break;
633 // Bad username/password
634 if( responseCode == 401 ) continue;
636 fprintf(stderr, "Unknown repsonse code %i from server\n%s\n", responseCode, buf);
642 gbIsAuthenticated = 1;
646 return 2; // 2 = Bad Password
648 case 404: // Bad Username
649 fprintf(stderr, "Bad Username '%s'\n", pwd->pw_name);
654 fprintf(stderr, "Unkown response code %i from server\n", responseCode);
663 * \brief Fill the item information structure
664 * \return Boolean Failure
666 void PopulateItemList(int Socket)
671 char *itemType, *itemStart;
673 regmatch_t matches[4];
675 // Ask server for stock list
676 send(Socket, "ENUM_ITEMS\n", 11, 0);
677 buf = ReadLine(Socket);
679 //printf("Output: %s\n", buf);
681 responseCode = atoi(buf);
682 if( responseCode != 201 ) {
683 fprintf(stderr, "Unknown response from dispense server (Response Code %i)\n", responseCode);
692 RunRegex(&gArrayRegex, buf, 4, matches, "Malformed server response");
694 itemType = &buf[ matches[2].rm_so ]; buf[ matches[2].rm_eo ] = '\0';
695 count = atoi( &buf[ matches[3].rm_so ] );
698 if( strcmp(itemType, "Items") != 0 ) {
700 fprintf(stderr, "Unexpected array type, expected 'Items', got '%s'\n",
705 itemStart = &buf[ matches[3].rm_eo ];
710 gaItems = malloc( giNumItems * sizeof(tItem) );
712 // Fetch item information
713 for( i = 0; i < giNumItems; i ++ )
715 regmatch_t matches[6];
718 buf = ReadLine(Socket);
719 responseCode = atoi(buf);
721 if( responseCode != 202 ) {
722 fprintf(stderr, "Unknown response from dispense server (Response Code %i)\n", responseCode);
726 RunRegex(&gItemRegex, buf, 6, matches, "Malformed server response");
728 buf[ matches[3].rm_eo ] = '\0';
730 gaItems[i].Ident = strdup( buf + matches[3].rm_so );
731 gaItems[i].Price = atoi( buf + matches[4].rm_so );
732 gaItems[i].Desc = strdup( buf + matches[5].rm_so );
738 buf = ReadLine(Socket);
739 responseCode = atoi(buf);
741 if( responseCode != 200 ) {
742 fprintf(stderr, "Unknown response from dispense server %i\n'%s'",
752 * \brief Dispense an item
753 * \return Boolean Failure
755 int DispenseItem(int Socket, int ItemID)
757 int ret, responseCode;
760 if( ItemID < 0 || ItemID > giNumItems ) return -1;
763 sendf(Socket, "DISPENSE %s\n", gaItems[ItemID].Ident);
764 buf = ReadLine(Socket);
766 responseCode = atoi(buf);
767 switch( responseCode )
770 printf("Dispense OK\n");
774 printf("Not authenticated\n");
778 printf("Insufficient balance\n");
782 printf("Bad item name, bug report\n");
786 printf("Item failed to dispense, is the slot empty?\n");
790 printf("Dispense not possible (slot empty/permissions)\n");
794 printf("Unknown response code %i ('%s')\n", responseCode, buf);
804 * \brief Alter a user's balance
806 int Dispense_AlterBalance(int Socket, const char *Username, int Ammount, const char *Reason)
811 sendf(Socket, "ADD %s %i %s\n", Username, Ammount, Reason);
812 buf = ReadLine(Socket);
814 responseCode = atoi(buf);
819 case 200: return 0; // OK
820 case 403: // Not in coke
821 fprintf(stderr, "You are not in coke (sucker)\n");
823 case 404: // Unknown user
824 fprintf(stderr, "Unknown user '%s'\n", Username);
827 fprintf(stderr, "Unknown response code %i\n", responseCode);
835 * \brief Alter a user's balance
837 int Dispense_SetBalance(int Socket, const char *Username, int Ammount, const char *Reason)
842 sendf(Socket, "SET %s %i %s\n", Username, Ammount, Reason);
843 buf = ReadLine(Socket);
845 responseCode = atoi(buf);
850 case 200: return 0; // OK
851 case 403: // Not in coke
852 fprintf(stderr, "You are not in coke (sucker)\n");
854 case 404: // Unknown user
855 fprintf(stderr, "Unknown user '%s'\n", Username);
858 fprintf(stderr, "Unknown response code %i\n", responseCode);
865 int Dispense_EnumUsers(int Socket)
870 regmatch_t matches[4];
872 sendf(Socket, "ENUM_USERS\n");
873 buf = ReadLine(Socket);
874 responseCode = atoi(buf);
878 case 201: break; // Ok, length follows
881 fprintf(stderr, "Unknown response code %i\n%s\n", responseCode, buf);
886 // Get count (not actually used)
887 RunRegex(&gArrayRegex, buf, 4, matches, "Malformed server response");
888 nUsers = atoi( buf + matches[3].rm_so );
889 printf("%i users returned\n", nUsers);
894 // Read returned users
896 buf = ReadLine(Socket);
897 responseCode = atoi(buf);
899 if( responseCode != 202 ) break;
903 } while(responseCode == 202);
905 // Check final response
906 if( responseCode != 200 ) {
907 fprintf(stderr, "Unknown response code %i\n%s\n", responseCode, buf);
917 int Dispense_ShowUser(int Socket, const char *Username)
920 int responseCode, ret;
922 sendf(Socket, "USER_INFO %s\n", Username);
923 buf = ReadLine(Socket);
925 responseCode = atoi(buf);
935 printf("Unknown user '%s'\n", Username);
940 fprintf(stderr, "Unknown response code %i '%s'\n", responseCode, buf);
950 void _PrintUserLine(const char *Line)
952 regmatch_t matches[6];
955 RunRegex(&gUserInfoRegex, Line, 6, matches, "Malformed server response");
960 int usernameLen = matches[3].rm_eo - matches[3].rm_so;
961 char username[usernameLen + 1];
962 int flagsLen = matches[5].rm_eo - matches[5].rm_so;
963 char flags[flagsLen + 1];
965 memcpy(username, Line + matches[3].rm_so, usernameLen);
966 username[usernameLen] = '\0';
967 memcpy(flags, Line + matches[5].rm_so, flagsLen);
968 flags[flagsLen] = '\0';
970 bal = atoi(Line + matches[4].rm_so);
971 printf("%-15s: $%4i.%02i (%s)\n", username, bal/100, bal%100, flags);
978 char *ReadLine(int Socket)
980 static char buf[BUFSIZ];
981 static int bufPos = 0;
982 static int bufValid = 0;
984 char *newline = NULL;
986 char *ret = malloc(10);
988 #if DEBUG_TRACE_SERVER
989 printf("ReadLine: ");
1001 len = recv(Socket, buf+bufPos, BUFSIZ-1-bufPos, 0);
1002 buf[bufPos+len] = '\0';
1005 newline = strchr( buf+bufPos, '\n' );
1010 retLen += strlen(buf+bufPos);
1011 ret = realloc(ret, retLen + 1);
1012 strcat( ret, buf+bufPos );
1015 int newLen = newline - (buf+bufPos) + 1;
1016 bufValid = len - newLen;
1019 if( len + bufPos == BUFSIZ - 1 ) bufPos = 0;
1022 #if DEBUG_TRACE_SERVER
1023 printf("%i '%s'\n", retLen, ret);
1029 int sendf(int Socket, const char *Format, ...)
1034 va_start(args, Format);
1035 len = vsnprintf(NULL, 0, Format, args);
1040 va_start(args, Format);
1041 vsnprintf(buf, len+1, Format, args);
1044 #if DEBUG_TRACE_SERVER
1045 printf("sendf: %s", buf);
1048 return send(Socket, buf, len, 0);
1052 char *trim(char *string)
1056 while( isspace(*string) )
1059 for( i = strlen(string); i--; )
1061 if( isspace(string[i]) )
1070 int RunRegex(regex_t *regex, const char *string, int nMatches, regmatch_t *matches, const char *errorMessage)
1074 ret = regexec(regex, string, nMatches, matches, 0);
1076 size_t len = regerror(ret, regex, NULL, 0);
1078 regerror(ret, regex, errorStr, len);
1079 printf("string = '%s'\n", string);
1080 fprintf(stderr, "%s\n%s", errorMessage, errorStr);
1087 void CompileRegex(regex_t *regex, const char *pattern, int flags)
1089 int ret = regcomp(regex, pattern, flags);
1091 size_t len = regerror(ret, regex, NULL, 0);
1093 regerror(ret, regex, errorStr, len);
1094 fprintf(stderr, "Regex compilation failed - %s\n", errorStr);