From 0b5755205d388b5704c040c21285c0b7ab58825e Mon Sep 17 00:00:00 2001
From: John Hodge <tpg@ucc.asn.au>
Date: Wed, 11 Jul 2012 18:51:55 +0800
Subject: [PATCH] Client - Broke the client out into separate files

- Added help to the ncurses display
---
 src/client/Makefile   |    2 +-
 src/client/common.h   |   92 +++
 src/client/main.c     | 1578 +----------------------------------------
 src/client/menu.c     |  407 +++++++++++
 src/client/protocol.c | 1161 ++++++++++++++++++++++++++++++
 5 files changed, 1664 insertions(+), 1576 deletions(-)
 create mode 100644 src/client/common.h
 create mode 100644 src/client/menu.c
 create mode 100644 src/client/protocol.c

diff --git a/src/client/Makefile b/src/client/Makefile
index e490549..8c1cda4 100644
--- a/src/client/Makefile
+++ b/src/client/Makefile
@@ -4,7 +4,7 @@ LDFLAGS := -g -lncurses
 # -lssl
 
 BIN := ../../dispense
-OBJ := main.o
+OBJ := main.o protocol.o menu.o
 
 DEPFILES := $(OBJ:%.o=%.d)
 
diff --git a/src/client/common.h b/src/client/common.h
new file mode 100644
index 0000000..4c592ff
--- /dev/null
+++ b/src/client/common.h
@@ -0,0 +1,92 @@
+/*
+ * OpenDispense 2 
+ * UCC (University [of WA] Computer Club) Electronic Accounting System
+ * - Dispense Client
+ *
+ * common.h
+ * - Shared definitions
+ *
+ * This file is licenced under the 3-clause BSD Licence. See the file
+ * COPYING for full details.
+ */
+#ifndef _CLIENT__COMMON_H_
+#define _CLIENT__COMMON_H_
+
+#include <regex.h>
+
+typedef struct sItem {
+	char	*Type;
+	 int	ID;
+	 int	Status;	// 0: Availiable, 1: Sold out, -1: Error
+	char	*Desc;
+	 int	Price;
+}	tItem;
+
+enum eUI_Modes
+{
+	UI_MODE_BASIC,	// Non-NCurses
+	UI_MODE_STANDARD,
+	UI_MODE_DRINKSONLY,
+	UI_MODE_ALL,
+	NUM_UI_MODES
+};
+
+enum eReturnValues
+{
+	RV_SUCCESS,
+	RV_BAD_ITEM,
+	RV_INVALID_USER,
+	RV_PERMISSIONS,
+	RV_ARGUMENTS,
+	RV_BALANCE,
+	RV_SERVER_ERROR,	// Generic for 5xx codes
+	RV_UNKNOWN_ERROR = -1,
+	RV_SOCKET_ERROR = -2,
+	RV_UNKNOWN_RESPONSE = -3,
+};
+
+extern regex_t	gArrayRegex;
+extern regex_t	gItemRegex;
+extern regex_t	gSaltRegex;
+extern regex_t	gUserInfoRegex;
+extern regex_t	gUserItemIdentRegex;
+
+extern int	gbDryRun;
+extern int	gbDisallowSelectWithoutBalance;
+extern int	giMinimumBalance;
+extern int	giMaximumBalance;
+extern enum eUI_Modes	giUIMode;
+
+extern int	gbIsAuthenticated;
+extern char	*gsEffectiveUser;
+extern char	*gsUserName;
+extern int	giUserBalance;
+extern char	*gsUserFlags;
+
+extern int	giNumItems;
+extern tItem	*gaItems;
+
+extern int	RunRegex(regex_t *regex, const char *str, int nMatches, regmatch_t *matches, const char *errmsg);
+
+extern int	ShowNCursesUI(void);
+
+extern int	OpenConnection(const char *Host, int Port);
+extern int	Authenticate(int Socket);
+extern int	GetUserBalance(int Socket);
+extern void	PopulateItemList(int Socket);
+extern int	Dispense_ItemInfo(int Socket, const char *Type, int ID);
+extern int	DispenseItem(int Socket, const char *Type, int ID);
+extern int	Dispense_AlterBalance(int Socket, const char *Username, int Ammount, const char *Reason);
+extern int	Dispense_SetBalance(int Socket, const char *Username, int Balance, const char *Reason);
+extern int	Dispense_Give(int Socket, const char *Username, int Ammount, const char *Reason);
+extern int	Dispense_Refund(int Socket, const char *Username, const char *Item, int PriceOverride);
+extern int	Dispense_Donate(int Socket, int Ammount, const char *Reason);
+extern int	Dispense_EnumUsers(int Socket);
+extern int	Dispense_ShowUser(int Socket, const char *Username);
+extern void	_PrintUserLine(const char *Line);
+extern int	Dispense_AddUser(int Socket, const char *Username);
+extern int	Dispense_SetUserType(int Socket, const char *Username, const char *TypeString, const char *Reason);
+extern int	Dispense_SetItem(int Socket, const char *Type, int ID, int NewPrice, const char *NewName);
+
+#endif
+
diff --git a/src/client/main.c b/src/client/main.c
index 708a5d2..444ab34 100644
--- a/src/client/main.c
+++ b/src/client/main.c
@@ -12,18 +12,9 @@
 #include <stdio.h>
 #include <string.h>
 #include <ctype.h>	// isspace
-#include <stdarg.h>
-#include <regex.h>
-#include <ncurses.h>
-#include <limits.h>
-
 #include <unistd.h>	// close
-#include <netdb.h>	// gethostbyname
-#include <pwd.h>	// getpwuids
-#include <sys/socket.h>
-#include <netinet/in.h>
-#include <arpa/inet.h>
-//#include <openssl/sha.h>	// SHA1
+#include <limits.h>	// INT_MIN/INT_MAX
+#include "common.h"
 
 #define	USE_NCURSES_INTERFACE	0
 #define DEBUG_TRACE_SERVER	0
@@ -32,66 +23,13 @@
 #define MAX_TXT_ARGS	5	// Maximum number of textual arguments (including command)
 #define DISPENSE_MULTIPLE_MAX	20	// Maximum argument to -c
 
-enum eUI_Modes
-{
-	UI_MODE_BASIC,	// Non-NCurses
-	UI_MODE_STANDARD,
-	UI_MODE_DRINKSONLY,
-	UI_MODE_ALL,
-	NUM_UI_MODES
-};
-
-enum eReturnValues
-{
-	RV_SUCCESS,
-	RV_BAD_ITEM,
-	RV_INVALID_USER,
-	RV_PERMISSIONS,
-	RV_ARGUMENTS,
-	RV_BALANCE,
-	RV_SERVER_ERROR,	// Generic for 5xx codes
-	RV_UNKNOWN_ERROR = -1,
-	RV_SOCKET_ERROR = -2,
-	RV_UNKNOWN_RESPONSE = -3,
-};
-
 // === TYPES ===
-typedef struct sItem {
-	char	*Type;
-	 int	ID;
-	 int	Status;	// 0: Availiable, 1: Sold out, -1: Error
-	char	*Desc;
-	 int	Price;
-}	tItem;
 
 // === PROTOTYPES ===
 void	ShowUsage(void);
  int	main(int argc, char *argv[]);
-// --- GUI ---
- int	ShowNCursesUI(void);
- int	ShowItemAt(int Row, int Col, int Width, int Index, int bHilighted);
-void	PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1, const char *Mid, char Pad2, const char *Right, ...);
 // --- Coke Server Communication ---
- int	OpenConnection(const char *Host, int Port);
- int	Authenticate(int Socket);
- int	GetUserBalance(int Socket);
-void	PopulateItemList(int Socket);
- int	Dispense_ItemInfo(int Socket, const char *Type, int ID);
- int	DispenseItem(int Socket, const char *Type, int ID);
- int	Dispense_AlterBalance(int Socket, const char *Username, int Ammount, const char *Reason);
- int	Dispense_SetBalance(int Socket, const char *Username, int Balance, const char *Reason);
- int	Dispense_Give(int Socket, const char *Username, int Ammount, const char *Reason);
- int	Dispense_Refund(int Socket, const char *Username, const char *Item, int PriceOverride);
- int	Dispense_Donate(int Socket, int Ammount, const char *Reason);
- int	Dispense_EnumUsers(int Socket);
- int	Dispense_ShowUser(int Socket, const char *Username);
-void	_PrintUserLine(const char *Line);
- int	Dispense_AddUser(int Socket, const char *Username);
- int	Dispense_SetUserType(int Socket, const char *Username, const char *TypeString, const char *Reason);
- int	Dispense_SetItem(int Socket, const char *Type, int ID, int NewPrice, const char *NewName);
 // --- Helpers ---
-char	*ReadLine(int Socket);
- int	sendf(int Socket, const char *Format, ...);
 char	*trim(char *string);
  int	RunRegex(regex_t *regex, const char *string, int nMatches, regmatch_t *matches, const char *errorMessage);
 void	CompileRegex(regex_t *regex, const char *pattern, int flags);
@@ -108,7 +46,7 @@ regex_t	gArrayRegex, gItemRegex, gSaltRegex, gUserInfoRegex, gUserItemIdentRegex
 char	*gsItemPattern;	//!< Item pattern
 char	*gsEffectiveUser;	//!< '-u' Dispense as another user
 
- int	giUIMode = UI_MODE_STANDARD;
+enum eUI_Modes	giUIMode = UI_MODE_STANDARD;
  int	gbDryRun = 0;	//!< '-n' Read-only
  int	gbDisallowSelectWithoutBalance = 1;	//!< Don't allow items to be hilighted if not affordable
 
@@ -882,1519 +820,9 @@ int main(int argc, char *argv[])
 	return ret;
 }
 
-// -------------------
-// --- NCurses GUI ---
-// -------------------
-/**
- * \brief Render the NCurses UI
- */
-int ShowNCursesUI(void)
-{
-	 int	ch;
-	 int	i, times;
-	 int	xBase, yBase;
-	const int	displayMinWidth = 50;
-	char	*titleString = "Dispense";
-	 int	items_in_view;
-	 int	maxItemIndex;
-	 int	itemBase = 0;
-	 int	currentItem;
-	 int	ret = -2;	// -2: Used for marking "no return yet"
-	
-	char	balance_str[5+1+2+1];	// If $9999.99 is too little, something's wrong
-	char	*username;
-	struct passwd *pwd;
-	 
-	 int	height, width;
-	
-	void _ItemDown(void)
-	{
-		currentItem ++;
-		// Skip over spacers
-		while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
-			currentItem ++;
-		
-		if( currentItem >= maxItemIndex ) {
-			currentItem = 0;
-			// Skip over spacers
-			while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
-				currentItem ++;
-		}
-	}
-	
-	void _ItemUp(void)
-	{
-		currentItem --;
-		// Skip over spacers
-		while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
-			currentItem --;
-		
-		if( currentItem < 0 ) {
-			currentItem = maxItemIndex - 1;
-			// Skip over spacers
-			while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
-				currentItem --;
-		}
-	}
-
-	// Get Username
-	if( gsEffectiveUser )
-		username = gsEffectiveUser;
-	else {
-		pwd = getpwuid( getuid() );
-		username = pwd->pw_name;
-	}
-	// Get balance
-	snprintf(balance_str, sizeof(balance_str), "$%i.%02i", giUserBalance/100, abs(giUserBalance)%100);
-	
-	// Enter curses mode
-	initscr();
-	cbreak(); noecho();
-	
-	// Get max index
-	maxItemIndex = ShowItemAt(0, 0, 0, -1, 0);
-	// Get item count per screen
-	// - 6: randomly chosen (Need at least 3)
-	items_in_view = LINES - 6;
-	if( items_in_view > maxItemIndex )
-		items_in_view = maxItemIndex;
-	// Get first index
-	currentItem = 0;
-	while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
-		currentItem ++;
-	
-	
-	// Get dimensions
-	height = items_in_view + 3;
-	width = displayMinWidth;
-	
-	// Get positions
-	xBase = COLS/2 - width/2;
-	yBase = LINES/2 - height/2;
-	
-	for( ;; )
-	{
-		// Header
-		PrintAlign(yBase, xBase, width, "/", '-', titleString, '-', "\\");
-		
-		// Items
-		for( i = 0; i < items_in_view; i ++ )
-		{
-			 int	pos = 0;
-			
-			move( yBase + 1 + i, xBase );
-			printw("| ");
-			
-			pos += 2;
-			
-			// Check for the '...' row
-			// - Oh god, magic numbers!
-			if( (i == 0 && itemBase > 0)
-			 || (i == items_in_view - 1 && itemBase < maxItemIndex - items_in_view) )
-			{
-				printw("     ...");	pos += 8;
-				times = (width - pos) - 1;
-				while(times--)	addch(' ');
-			}
-			// Show an item
-			else {
-				ShowItemAt(
-					yBase + 1 + i, xBase + pos,	// Position
-					(width - pos) - 3,	// Width
-					itemBase + i,	// Index
-					!!(currentItem == itemBase + i)	// Hilighted
-					);
-				printw("  ");
-			}
-			
-			// Scrollbar (if needed)
-			if( maxItemIndex > items_in_view ) {
-				if( i == 0 ) {
-					addch('A');
-				}
-				else if( i == items_in_view - 1 ) {
-					addch('V');
-				}
-				else {
-					 int	percentage = itemBase * 100 / (maxItemIndex-items_in_view);
-					if( i-1 == percentage*(items_in_view-3)/100 ) {
-						addch('#');
-					}
-					else {
-						addch('|');
-					}
-				}
-			}
-			else {
-				addch('|');
-			}
-		}
-		
-		// Footer
-		PrintAlign(yBase+height-2, xBase, width, "\\", '-', "", '-', "/");
-		
-		// User line
-		// - Username, balance, flags
-		PrintAlign(yBase+height-1, xBase+1, width-2,
-			username, ' ', balance_str, ' ', gsUserFlags);
-		
-		
-		// Get input
-		ch = getch();
-		
-		if( ch == '\x1B' ) {
-			ch = getch();
-			if( ch == '[' ) {
-				ch = getch();
-				
-				switch(ch)
-				{
-				case 'B':	_ItemDown();	break;
-				case 'A':	_ItemUp();	break;
-				}
-			}
-			else {
-				
-			}
-		}
-		else {
-			switch(ch)
-			{
-			case '\n':
-				ret = ShowItemAt(0, 0, 0, currentItem, 0);
-				break;
-			case 'h':	break;
-			case 'j':	_ItemDown();	break;
-			case 'k':	_ItemUp();	break;
-			case 'l':	break;
-			case 0x1b:	// Escape
-			case 'q':
-				ret = -1;	// -1: Return with no dispense
-				break;
-			}
-			
-			// Check if the return value was changed
-			if( ret != -2 )	break;
-		}
-		
-		// Scroll only if needed
-		if( items_in_view < maxItemIndex )
-		{
-			// - If the current item is above the second item shown, and we're not at the top
-			if( currentItem < itemBase + 2 && itemBase > 0 ) {
-				itemBase = currentItem - 2;
-				if(itemBase < 0)	itemBase = 0;
-			}
-			// - If the current item is below the second item show, and we're not at the bottom
-			if( currentItem > itemBase + items_in_view - 2 && itemBase + items_in_view < maxItemIndex ) {
-				itemBase = currentItem - items_in_view + 2;
-				if( itemBase > maxItemIndex - items_in_view )
-					itemBase = maxItemIndex - items_in_view;
-			}
-		}
-	}
-	
-	
-	// Leave
-	endwin();
-	return ret;
-}
-
-/**
- * \brief Show item \a Index at (\a Col, \a Row)
- * \return Dispense index of item
- * \note Part of the NCurses UI
- */
-int ShowItemAt(int Row, int Col, int Width, int Index, int bHilighted)
-{
-	char	*name = NULL;
-	 int	price = 0;
-	 int	status = -1;
-	
-	switch(giUIMode)
-	{
-	// Standard UI
-	// - This assumes that 
-	case UI_MODE_STANDARD:
-		// Bounds check
-		// Index = -1, request limit
-		if( Index < 0 || Index >= giNumItems+2 )
-			return giNumItems+2;
-		// Drink label
-		if( Index == 0 )
-		{
-			price = 0;
-			name = "Coke Machine";
-			Index = -1;	// -1 indicates a label
-			break;
-		}
-		Index --;
-		// Drinks 0 - 6
-		if( Index <= 6 )
-		{
-			name = gaItems[Index].Desc;
-			price = gaItems[Index].Price;
-			status = gaItems[Index].Status;
-			break;
-		}
-		Index -= 7;
-		// EPS label
-		if( Index == 0 )
-		{
-			price = 0;
-			name = "Electronic Payment System";
-			Index = -1;	// -1 indicates a label
-			break;
-		}
-		Index --;
-		Index += 7;
-		name = gaItems[Index].Desc;
-		price = gaItems[Index].Price;
-		status = gaItems[Index].Status;
-		break;
-	default:
-		return -1;
-	}
-	
-	// Width = 0, don't print
-	if( Width > 0 )
-	{
-		// 4 preceding, 5 price
-		int nameWidth = Width - 4 - snprintf(NULL, 0, " %4i", price);
-		move( Row, Col );
-		
-		if( Index >= 0 )
-		{
-			// Show hilight and status
-			switch( status )
-			{
-			case 0:
-				if( bHilighted )
-					printw("->  ");
-				else
-					printw("    ");
-				break;
-			case 1:
-				printw("SLD ");
-				break;
-			
-			default:
-			case -1:
-				printw("ERR ");
-				break;
-			}
-			
-			printw("%-*.*s", nameWidth, nameWidth, name);
-		
-			printw(" %4i", price);
-		}
-		else
-		{
-			printw("-- %-*.*s ", Width-4, Width-4, name);
-		}
-	}
-	
-	// If the item isn't availiable for sale, return -1 (so it's skipped)
-	if( status || (price > giUserBalance && gbDisallowSelectWithoutBalance) )
-		Index = -1;
-	
-	return Index;
-}
-
-/**
- * \brief Print a three-part string at the specified position (formatted)
- * \note NCurses UI Helper
- * 
- * Prints \a Left on the left of the area, \a Right on the righthand side
- * and \a Mid in the middle of the area. These are padded with \a Pad1
- * between \a Left and \a Mid, and \a Pad2 between \a Mid and \a Right.
- * 
- * ::printf style format codes are allowed in \a Left, \a Mid and \a Right,
- * and the arguments to these are read in that order.
- */
-void PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1,
-	const char *Mid, char Pad2, const char *Right, ...)
-{
-	 int	lLen, mLen, rLen;
-	 int	times;
-	
-	va_list	args;
-	
-	// Get the length of the strings
-	va_start(args, Right);
-	lLen = vsnprintf(NULL, 0, Left, args);
-	mLen = vsnprintf(NULL, 0, Mid, args);
-	rLen = vsnprintf(NULL, 0, Right, args);
-	va_end(args);
-	
-	// Sanity check
-	if( lLen + mLen/2 > Width/2 || mLen/2 + rLen > Width/2 ) {
-		return ;	// TODO: What to do?
-	}
-	
-	move(Row, Col);
-	
-	// Render strings
-	va_start(args, Right);
-	// - Left
-	{
-		char	tmp[lLen+1];
-		vsnprintf(tmp, lLen+1, Left, args);
-		addstr(tmp);
-	}
-	// - Left padding
-	times = (Width - mLen)/2 - lLen;
-	while(times--)	addch(Pad1);
-	// - Middle
-	{
-		char	tmp[mLen+1];
-		vsnprintf(tmp, mLen+1, Mid, args);
-		addstr(tmp);
-	}
-	// - Right Padding
-	times = (Width - mLen)/2 - rLen;
-	if( (Width - mLen) % 2 )	times ++;
-	while(times--)	addch(Pad2);
-	// - Right
-	{
-		char	tmp[rLen+1];
-		vsnprintf(tmp, rLen+1, Right, args);
-		addstr(tmp);
-	}
-}
-
-// ---------------------
-// --- Coke Protocol ---
-// ---------------------
-int OpenConnection(const char *Host, int Port)
-{
-	struct hostent	*host;
-	struct sockaddr_in	serverAddr;
-	 int	sock;
-	
-	host = gethostbyname(Host);
-	if( !host ) {
-		fprintf(stderr, "Unable to look up '%s'\n", Host);
-		return -1;
-	}
-	
-	memset(&serverAddr, 0, sizeof(serverAddr));
-	
-	serverAddr.sin_family = AF_INET;	// IPv4
-	// NOTE: I have a suspicion that IPv6 will play sillybuggers with this :)
-	serverAddr.sin_addr.s_addr = *((unsigned long *) host->h_addr_list[0]);
-	serverAddr.sin_port = htons(Port);
-	
-	sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
-	if( sock < 0 ) {
-		fprintf(stderr, "Failed to create socket\n");
-		return -1;
-	}
-
-//	printf("geteuid() = %i, getuid() = %i\n", geteuid(), getuid());
-	
-	if( geteuid() == 0 || getuid() == 0 )
-	{
-		 int	i;
-		struct sockaddr_in	localAddr;
-		memset(&localAddr, 0, sizeof(localAddr));
-		localAddr.sin_family = AF_INET;	// IPv4
-		
-		// Loop through all the top ports until one is avaliable
-		for( i = 512; i < 1024; i ++)
-		{
-			localAddr.sin_port = htons(i);	// IPv4
-			// Attempt to bind to low port for autoauth
-			if( bind(sock, (struct sockaddr*)&localAddr, sizeof(localAddr)) == 0 )
-				break;
-		}
-		if( i == 1024 )
-			printf("Warning: AUTOAUTH unavaliable\n");
-//		else
-//			printf("Bound to 0.0.0.0:%i\n", i);
-	}
-	
-	if( connect(sock, (struct sockaddr *) &serverAddr, sizeof(serverAddr)) < 0 ) {
-		fprintf(stderr, "Failed to connect to server\n");
-		return -1;
-	}
-
-	// We're not authenticated if the connection has just opened
-	gbIsAuthenticated = 0;
-	
-	return sock;
-}
-
-int Authenticate_AutoAuth(int Socket, const char *Username)
-{
-	char	*buf;
-	 int	responseCode;
-	 int	ret = -1;
-	
-	// Attempt automatic authentication
-	sendf(Socket, "AUTOAUTH %s\n", Username);
-	
-	// Check if it worked
-	buf = ReadLine(Socket);
-	
-	responseCode = atoi(buf);
-	switch( responseCode )
-	{
-	case 200:	// Autoauth succeeded, return
-		ret = 0;
-		break;
-	
-	case 401:	// Untrusted
-//		fprintf(stderr, "Untrusted host, AUTOAUTH unavaliable\n");
-		ret = RV_PERMISSIONS;
-		break;
-	case 404:	// Bad Username
-		fprintf(stderr, "Bad Username '%s'\n", Username);
-		ret = RV_INVALID_USER;
-		break;
-	
-	default:
-		fprintf(stderr, "Unkown response code %i from server\n", responseCode);
-		printf("%s\n", buf);
-		ret = RV_UNKNOWN_ERROR;
-		break;;
-	}
-	
-	free(buf);
-	return ret;
-}
-
-int Authenticate_AuthIdent(int Socket)
-{
-	char	*buf;
-	 int	responseCode;
-	 int	ret = -1;
-	
-	// Attempt automatic authentication
-	sendf(Socket, "AUTHIDENT\n");
-	
-	// Check if it worked
-	buf = ReadLine(Socket);
-	
-	responseCode = atoi(buf);
-	switch( responseCode )
-	{
-	case 200:	// Autoauth succeeded, return
-		ret = 0;
-		break;
-	
-	case 401:	// Untrusted
-//		fprintf(stderr, "Untrusted host, AUTHIDENT unavaliable\n");
-		ret = RV_PERMISSIONS;
-		break;
-	
-	default:
-		fprintf(stderr, "Unkown response code %i from server\n", responseCode);
-		printf("%s\n", buf);
-		ret = RV_UNKNOWN_RESPONSE;
-		break;
-	}
-	
-	free(buf);
-
-	return ret;
-}
-
-int Authenticate_Password(int Socket, const char *Username)
-{
-	#if USE_PASSWORD_AUTH
-	char	*buf;
-	 int	responseCode;	
-	char	salt[32];
-	 int	i;
-	regmatch_t	matches[4];
-
-	sendf(Socket, "USER %s\n", Username);
-	printf("Using username %s\n", Username);
-	
-	buf = ReadLine(Socket);
-	
-	// TODO: Get Salt
-	// Expected format: 100 SALT <something> ...
-	// OR             : 100 User Set
-	RunRegex(&gSaltRegex, buf, 4, matches, "Malformed server response");
-	responseCode = atoi(buf);
-	if( responseCode != 100 ) {
-		fprintf(stderr, "Unknown repsonse code %i from server\n%s\n", responseCode, buf);
-		free(buf);
-		return RV_UNKNOWN_ERROR;	// ERROR
-	}
-	
-	// Check for salt
-	if( memcmp( buf+matches[2].rm_so, "SALT", matches[2].rm_eo - matches[2].rm_so) == 0) {
-		// Store it for later
-		memcpy( salt, buf + matches[3].rm_so, matches[3].rm_eo - matches[3].rm_so );
-		salt[ matches[3].rm_eo - matches[3].rm_so ] = 0;
-	}
-	free(buf);
-	
-	// Give three attempts
-	for( i = 0; i < 3; i ++ )
-	{
-		 int	ofs = strlen(Username)+strlen(salt);
-		char	tmpBuf[42];
-		char	tmp[ofs+20];
-		char	*pass = getpass("Password: ");
-		uint8_t	h[20];
-		
-		// Create hash string
-		// <username><salt><hash>
-		strcpy(tmp, Username);
-		strcat(tmp, salt);
-		SHA1( (unsigned char*)pass, strlen(pass), h );
-		memcpy(tmp+ofs, h, 20);
-		
-		// Hash all that
-		SHA1( (unsigned char*)tmp, ofs+20, h );
-		sprintf(tmpBuf, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
-			h[ 0], h[ 1], h[ 2], h[ 3], h[ 4], h[ 5], h[ 6], h[ 7], h[ 8], h[ 9],
-			h[10], h[11], h[12], h[13], h[14], h[15], h[16], h[17], h[18], h[19]
-			);
-	
-		// Send password
-		sendf(Socket, "PASS %s\n", tmpBuf);
-		buf = ReadLine(Socket);
-	
-		responseCode = atoi(buf);
-		// Auth OK?
-		if( responseCode == 200 )	break;
-		// Bad username/password
-		if( responseCode == 401 )	continue;
-		
-		fprintf(stderr, "Unknown repsonse code %i from server\n%s\n", responseCode, buf);
-		free(buf);
-		return -1;
-	}
-	free(buf);
-	if( i == 3 )
-		return RV_INVALID_USER;	// 2 = Bad Password
-
-	return 0;
-	#else
-	return RV_INVALID_USER;
-	#endif
-}
-
-/**
- * \brief Authenticate with the server
- * \return Boolean Failure
- */
-int Authenticate(int Socket)
-{
-	struct passwd	*pwd;
-	
-	if( gbIsAuthenticated )	return 0;
-	
-	// Get user name
-	pwd = getpwuid( getuid() );
-
-	// Attempt AUTOAUTH
-	if( Authenticate_AutoAuth(Socket, pwd->pw_name) == 0 )
-		;
-	else if( Authenticate_AuthIdent(Socket) == 0 )
-		;
-	else if( Authenticate_Password(Socket, pwd->pw_name) == 0 )
-		return RV_INVALID_USER;
-
-	// Set effective user
-	if( gsEffectiveUser ) {
-		char	*buf;
-		 int	responseCode;
-		sendf(Socket, "SETEUSER %s\n", gsEffectiveUser);
-		
-		buf = ReadLine(Socket);
-		responseCode = atoi(buf);
-		
-		switch(responseCode)
-		{
-		case 200:
-			printf("Running as '%s' by '%s'\n", gsEffectiveUser, pwd->pw_name);
-			break;
-		
-		case 403:
-			printf("Only coke members can use `dispense -u`\n");
-			free(buf);
-			return RV_PERMISSIONS;
-		
-		case 404:
-			printf("Invalid user selected\n");
-			free(buf);
-			return RV_INVALID_USER;
-		
-		default:
-			fprintf(stderr, "Unkown response code %i from server\n", responseCode);
-			printf("%s\n", buf);
-			free(buf);
-			return RV_UNKNOWN_ERROR;
-		}
-		
-		free(buf);
-	}
-	
-	gbIsAuthenticated = 1;
-	
-	return 0;
-}
-
-int GetUserBalance(int Socket)
-{
-	regmatch_t	matches[6];
-	struct passwd	*pwd;
-	char	*buf;
-	 int	responseCode;
-	
-	if( !gsUserName )
-	{
-		if( gsEffectiveUser ) {
-			gsUserName = gsEffectiveUser;
-		}
-		else {
-			pwd = getpwuid( getuid() );
-			gsUserName = strdup(pwd->pw_name);
-		}
-	}
-	
-	sendf(Socket, "USER_INFO %s\n", gsUserName);
-	buf = ReadLine(Socket);
-	responseCode = atoi(buf);
-	switch(responseCode)
-	{
-	case 202:	break;	// Ok
-	
-	case 404:
-		printf("Invalid user? (USER_INFO failed)\n");
-		free(buf);
-		return RV_INVALID_USER;
-	
-	default:
-		fprintf(stderr, "Unkown response code %i from server\n", responseCode);
-		printf("%s\n", buf);
-		free(buf);
-		return RV_UNKNOWN_ERROR;
-	}
-
-	RunRegex(&gUserInfoRegex, buf, 6, matches, "Malformed server response");
-	
-	giUserBalance = atoi( buf + matches[4].rm_so );
-	gsUserFlags = strdup( buf + matches[5].rm_so );
-	
-	free(buf);
-	
-	return 0;
-}
-
-/**
- * \brief Read an item info response from the server
- * \param Dest	Destination for the read item (strings will be on the heap)
- */
-int ReadItemInfo(int Socket, tItem *Dest)
-{
-	char	*buf;
-	 int	responseCode;
-	
-	regmatch_t	matches[8];
-	char	*statusStr;
-	
-	// Get item info
-	buf = ReadLine(Socket);
-	responseCode = atoi(buf);
-	
-	switch(responseCode)
-	{
-	case 202:	break;
-	
-	case 406:
-		printf("Bad item name\n");
-		free(buf);
-		return RV_BAD_ITEM;
-	
-	default:
-		fprintf(stderr, "Unknown response from dispense server (Response Code %i)\n%s", responseCode, buf);
-		free(buf);
-		return RV_UNKNOWN_ERROR;
-	}
-	
-	RunRegex(&gItemRegex, buf, 8, matches, "Malformed server response");
-	
-	buf[ matches[3].rm_eo ] = '\0';
-	buf[ matches[5].rm_eo ] = '\0';
-	buf[ matches[7].rm_eo ] = '\0';
-	
-	statusStr = &buf[ matches[5].rm_so ];
-	
-	Dest->ID = atoi( buf + matches[4].rm_so );
-	
-	if( strcmp(statusStr, "avail") == 0 )
-		Dest->Status = 0;
-	else if( strcmp(statusStr, "sold") == 0 )
-		Dest->Status = 1;
-	else if( strcmp(statusStr, "error") == 0 )
-		Dest->Status = -1;
-	else {
-		fprintf(stderr, "Unknown response from dispense server (status '%s')\n",
-			statusStr);
-		return RV_UNKNOWN_ERROR;
-	}
-	Dest->Price = atoi( buf + matches[6].rm_so );
-	
-	// Hack a little to reduce heap fragmentation
-	{
-		char	tmpType[strlen(buf + matches[3].rm_so) + 1];
-		char	tmpDesc[strlen(buf + matches[7].rm_so) + 1];
-		strcpy(tmpType, buf + matches[3].rm_so);
-		strcpy(tmpDesc, buf + matches[7].rm_so);
-		free(buf);
-		Dest->Type = strdup( tmpType );
-		Dest->Desc = strdup( tmpDesc );
-	}
-	
-	return 0;
-}
-
-/**
- * \brief Fill the item information structure
- * \return Boolean Failure
- */
-void PopulateItemList(int Socket)
-{
-	char	*buf;
-	 int	responseCode;
-	
-	char	*arrayType;
-	 int	count, i;
-	regmatch_t	matches[4];
-	
-	// Ask server for stock list
-	send(Socket, "ENUM_ITEMS\n", 11, 0);
-	buf = ReadLine(Socket);
-	
-	//printf("Output: %s\n", buf);
-	
-	responseCode = atoi(buf);
-	if( responseCode != 201 ) {
-		fprintf(stderr, "Unknown response from dispense server (Response Code %i)\n", responseCode);
-		exit(RV_UNKNOWN_ERROR);
-	}
-	
-	// - Get item list -
-	
-	// Expected format:
-	//  201 Items <count>
-	//  202 Item <count>
-	RunRegex(&gArrayRegex, buf, 4, matches, "Malformed server response");
-		
-	arrayType = &buf[ matches[2].rm_so ];	buf[ matches[2].rm_eo ] = '\0';
-	count = atoi( &buf[ matches[3].rm_so ] );
-		
-	// Check array type
-	if( strcmp(arrayType, "Items") != 0 ) {
-		// What the?!
-		fprintf(stderr, "Unexpected array type, expected 'Items', got '%s'\n",
-			arrayType);
-		exit(RV_UNKNOWN_ERROR);
-	}
-	free(buf);
-	
-	giNumItems = count;
-	gaItems = malloc( giNumItems * sizeof(tItem) );
-	
-	// Fetch item information
-	for( i = 0; i < giNumItems; i ++ )
-	{
-		ReadItemInfo( Socket, &gaItems[i] );
-	}
-	
-	// Read end of list
-	buf = ReadLine(Socket);
-	responseCode = atoi(buf);
-		
-	if( responseCode != 200 ) {
-		fprintf(stderr, "Unknown response from dispense server %i\n'%s'",
-			responseCode, buf
-			);
-		exit(-1);
-	}
-	
-	free(buf);
-}
-
-
-/**
- * \brief Get information on an item
- * \return Boolean Failure
- */
-int Dispense_ItemInfo(int Socket, const char *Type, int ID)
-{
-	tItem	item;
-	 int	ret;
-	
-	// Query
-	sendf(Socket, "ITEM_INFO %s:%i\n", Type, ID);
-	
-	ret = ReadItemInfo(Socket, &item);
-	if(ret)	return ret;
-	
-	printf("%8s:%-2i %2i.%02i %s\n",
-		item.Type, item.ID,
-		item.Price/100, item.Price%100,
-		item.Desc);
-	
-	free(item.Type);
-	free(item.Desc);
-	
-	return 0;
-}
-
-/**
- * \brief Dispense an item
- * \return Boolean Failure
- */
-int DispenseItem(int Socket, const char *Type, int ID)
-{
-	 int	ret, responseCode;
-	char	*buf;
-	
-	// Check for a dry run
-	if( gbDryRun ) {
-		printf("Dry Run - No action\n");
-		return 0;
-	}
-	
-	// Dispense!
-	sendf(Socket, "DISPENSE %s:%i\n", Type, ID);
-	buf = ReadLine(Socket);
-	
-	responseCode = atoi(buf);
-	switch( responseCode )
-	{
-	case 200:
-		printf("Dispense OK\n");
-		ret = 0;
-		break;
-	case 401:
-		printf("Not authenticated\n");
-		ret = RV_PERMISSIONS;
-		break;
-	case 402:
-		printf("Insufficient balance\n");
-		ret = RV_BALANCE;
-		break;
-	case 406:
-		printf("Bad item name\n");
-		ret = RV_BAD_ITEM;
-		break;
-	case 500:
-		printf("Item failed to dispense, is the slot empty?\n");
-		ret = RV_SERVER_ERROR;
-		break;
-	case 501:
-		printf("Dispense not possible (slot empty/permissions)\n");
-		ret = RV_SERVER_ERROR;
-		break;
-	default:
-		printf("Unknown response code %i ('%s')\n", responseCode, buf);
-		ret = RV_UNKNOWN_ERROR;
-		break;
-	}
-	
-	free(buf);
-	return ret;
-}
-
-/**
- * \brief Alter a user's balance
- */
-int Dispense_AlterBalance(int Socket, const char *Username, int Ammount, const char *Reason)
-{
-	char	*buf;
-	 int	responseCode, rv = -1;
-	
-	// Check for a dry run
-	if( gbDryRun ) {
-		printf("Dry Run - No action\n");
-		return 0;
-	}
-
-	// Sanity
-	if( Ammount == 0 ) {
-		printf("An amount would be nice\n");
-		return RV_ARGUMENTS;
-	}
-	
-	sendf(Socket, "ADD %s %i %s\n", Username, Ammount, Reason);
-	buf = ReadLine(Socket);
-	
-	responseCode = atoi(buf);
-	
-	switch(responseCode)
-	{
-	case 200:
-		rv = 0;	// OK
-		break;
-	case 402:
-		fprintf(stderr, "Insufficient balance\n");
-		rv = RV_BAD_ITEM;
-		break;
-	case 403:	// Not in coke
-		fprintf(stderr, "You are not in coke (sucker)\n");
-		rv = RV_PERMISSIONS;
-		break;
-	case 404:	// Unknown user
-		fprintf(stderr, "Unknown user '%s'\n", Username);
-		rv = RV_INVALID_USER;
-		break;
-	default:
-		fprintf(stderr, "Unknown response code %i\n'%s'\n", responseCode, buf);
-		rv = RV_UNKNOWN_RESPONSE;
-		break;
-	}
-	free(buf);
-	
-	return rv;
-}
-
-/**
- * \brief Set a user's balance
- * \note Only avaliable to dispense admins
- */
-int Dispense_SetBalance(int Socket, const char *Username, int Balance, const char *Reason)
-{
-	char	*buf;
-	 int	responseCode;
-	
-	// Check for a dry run
-	if( gbDryRun ) {
-		printf("Dry Run - No action\n");
-		return 0;
-	}
-	
-	sendf(Socket, "SET %s %i %s\n", Username, Balance, Reason);
-	buf = ReadLine(Socket);
-	
-	responseCode = atoi(buf);
-	free(buf);
-	
-	switch(responseCode)
-	{
-	case 200:	return 0;	// OK
-	case 403:	// Not in coke
-		fprintf(stderr, "You are not an admin\n");
-		return RV_PERMISSIONS;
-	case 404:	// Unknown user
-		fprintf(stderr, "Unknown user '%s'\n", Username);
-		return RV_INVALID_USER;
-	default:
-		fprintf(stderr, "Unknown response code %i\n", responseCode);
-		return RV_UNKNOWN_RESPONSE;
-	}
-	
-	return -1;
-}
-
-/**
- * \brief Give money to another user
- */
-int Dispense_Give(int Socket, const char *Username, int Ammount, const char *Reason)
-{
-	char	*buf;
-	 int	responseCode;
-	
-	if( Ammount < 0 ) {
-		printf("Sorry, you can only give, you can't take.\n");
-		return RV_ARGUMENTS;
-	}
-	
-	// Fast return on zero
-	if( Ammount == 0 ) {
-		printf("Are you actually going to give any?\n");
-		return RV_ARGUMENTS;
-	}
-	
-	// Check for a dry run
-	if( gbDryRun ) {
-		printf("Dry Run - No action\n");
-		return 0;
-	}
-	
-	sendf(Socket, "GIVE %s %i %s\n", Username, Ammount, Reason);
-
-	buf = ReadLine(Socket);
-	responseCode = atoi(buf);
-	free(buf);	
-	switch(responseCode)
-	{
-	case 200:
-		printf("Give succeeded\n");
-		return RV_SUCCESS;	// OK
-	
-	case 402:	
-		fprintf(stderr, "Insufficient balance\n");
-		return RV_BALANCE;
-	
-	case 404:	// Unknown user
-		fprintf(stderr, "Unknown user '%s'\n", Username);
-		return RV_INVALID_USER;
-	
-	default:
-		fprintf(stderr, "Unknown response code %i\n", responseCode);
-		return RV_UNKNOWN_RESPONSE;
-	}
-	
-	return -1;
-}
-
-int Dispense_Refund(int Socket, const char *Username, const char *Item, int PriceOverride)
-{
-	char	*buf;
-	 int	responseCode, ret = -1;
-	
-	// Check item id
-	if( RunRegex(&gUserItemIdentRegex, Item, 0, NULL, NULL) != 0 )
-	{
-		fprintf(stderr, "Error: Invalid item ID passed (should be <type>:<num>)\n");
-		return RV_ARGUMENTS;
-	}
-
-	// Check username (quick)
-	if( strchr(Username, ' ') || strchr(Username, '\n') )
-	{
-		fprintf(stderr, "Error: Username is invalid (no spaces or newlines please)\n");
-		return RV_ARGUMENTS;
-	}
-
-	// Send the query
-	sendf(Socket, "REFUND %s %s %i\n", Username, Item, PriceOverride);
-
-	buf = ReadLine(Socket);
-	responseCode = atoi(buf);
-	switch(responseCode)
-	{
-	case 200:
-		Dispense_ShowUser(Socket, Username);	// Show destination account
-		ret = 0;
-		break;
-	case 403:
-		fprintf(stderr, "Refund access is only avaliable to coke members\n");
-		ret = RV_PERMISSIONS;
-		break;
-	case 404:
-		fprintf(stderr, "Unknown user '%s' passed\n", Username);
-		ret = RV_INVALID_USER;
-		break;
-	case 406:
-		fprintf(stderr, "Invalid item '%s' passed\n", Item);
-		ret = RV_BAD_ITEM;
-		break;
-	default:
-		fprintf(stderr, "Unknown response from server %i\n%s\n", responseCode, buf);
-		ret = -1;
-		break;
-	}
-	free(buf);
-	return ret;
-}
-
-/**
- * \brief Donate money to the club
- */
-int Dispense_Donate(int Socket, int Ammount, const char *Reason)
-{
-	char	*buf;
-	 int	responseCode;
-	
-	if( Ammount < 0 ) {
-		printf("Sorry, you can only give, you can't take.\n");
-		return -1;
-	}
-	
-	// Fast return on zero
-	if( Ammount == 0 ) {
-		printf("Are you actually going to give any?\n");
-		return 1;
-	}
-	
-	// Check for a dry run
-	if( gbDryRun ) {
-		printf("Dry Run - No action\n");
-		return 0;
-	}
-	
-	sendf(Socket, "DONATE %i %s\n", Ammount, Reason);
-	buf = ReadLine(Socket);
-	
-	responseCode = atoi(buf);
-	free(buf);
-	
-	switch(responseCode)
-	{
-	case 200:	return 0;	// OK
-	
-	case 402:	
-		fprintf(stderr, "Insufficient balance\n");
-		return 1;
-	
-	default:
-		fprintf(stderr, "Unknown response code %i\n", responseCode);
-		return -1;
-	}
-	
-	return -1;
-}
-
-/**
- * \brief Enumerate users
- */
-int Dispense_EnumUsers(int Socket)
-{
-	char	*buf;
-	 int	responseCode;
-	 int	nUsers;
-	regmatch_t	matches[4];
-	
-	if( giMinimumBalance != INT_MIN ) {
-		if( giMaximumBalance != INT_MAX ) {
-			sendf(Socket, "ENUM_USERS min_balance:%i max_balance:%i\n", giMinimumBalance, giMaximumBalance);
-		}
-		else {
-			sendf(Socket, "ENUM_USERS min_balance:%i\n", giMinimumBalance);
-		}
-	}
-	else {
-		if( giMaximumBalance != INT_MAX ) {
-			sendf(Socket, "ENUM_USERS max_balance:%i\n", giMaximumBalance);
-		}
-		else {
-			sendf(Socket, "ENUM_USERS\n");
-		}
-	}
-	buf = ReadLine(Socket);
-	responseCode = atoi(buf);
-	
-	switch(responseCode)
-	{
-	case 201:	break;	// Ok, length follows
-	
-	default:
-		fprintf(stderr, "Unknown response code %i\n%s\n", responseCode, buf);
-		free(buf);
-		return -1;
-	}
-	
-	// Get count (not actually used)
-	RunRegex(&gArrayRegex, buf, 4, matches, "Malformed server response");
-	nUsers = atoi( buf + matches[3].rm_so );
-	printf("%i users returned\n", nUsers);
-	
-	// Free string
-	free(buf);
-	
-	// Read returned users
-	do {
-		buf = ReadLine(Socket);
-		responseCode = atoi(buf);
-		
-		if( responseCode != 202 )	break;
-		
-		_PrintUserLine(buf);
-		free(buf);
-	} while(responseCode == 202);
-	
-	// Check final response
-	if( responseCode != 200 ) {
-		fprintf(stderr, "Unknown response code %i\n%s\n", responseCode, buf);
-		free(buf);
-		return -1;
-	}
-	
-	free(buf);
-	
-	return 0;
-}
-
-int Dispense_ShowUser(int Socket, const char *Username)
-{
-	char	*buf;
-	 int	responseCode, ret;
-	
-	sendf(Socket, "USER_INFO %s\n", Username);
-	buf = ReadLine(Socket);
-	
-	responseCode = atoi(buf);
-	
-	switch(responseCode)
-	{
-	case 202:
-		_PrintUserLine(buf);
-		ret = 0;
-		break;
-	
-	case 404:
-		printf("Unknown user '%s'\n", Username);
-		ret = 1;
-		break;
-	
-	default:
-		fprintf(stderr, "Unknown response code %i '%s'\n", responseCode, buf);
-		ret = -1;
-		break;
-	}
-	
-	free(buf);
-	
-	return ret;
-}
-
-void _PrintUserLine(const char *Line)
-{
-	regmatch_t	matches[6];
-	 int	bal;
-	
-	RunRegex(&gUserInfoRegex, Line, 6, matches, "Malformed server response");
-	// 3: Username
-	// 4: Balance
-	// 5: Flags
-	{
-		 int	usernameLen = matches[3].rm_eo - matches[3].rm_so;
-		char	username[usernameLen + 1];
-		 int	flagsLen = matches[5].rm_eo - matches[5].rm_so;
-		char	flags[flagsLen + 1];
-		
-		memcpy(username, Line + matches[3].rm_so, usernameLen);
-		username[usernameLen] = '\0';
-		memcpy(flags, Line + matches[5].rm_so, flagsLen);
-		flags[flagsLen] = '\0';
-		
-		bal = atoi(Line + matches[4].rm_so);
-		printf("%-15s: $%8.02f (%s)\n", username, ((float)bal)/100, flags);
-	}
-}
-
-int Dispense_AddUser(int Socket, const char *Username)
-{
-	char	*buf;
-	 int	responseCode, ret;
-	
-	// Check for a dry run
-	if( gbDryRun ) {
-		printf("Dry Run - No action\n");
-		return 0;
-	}
-	
-	sendf(Socket, "USER_ADD %s\n", Username);
-	
-	buf = ReadLine(Socket);
-	responseCode = atoi(buf);
-	
-	switch(responseCode)
-	{
-	case 200:
-		printf("User '%s' added\n", Username);
-		ret = 0;
-		break;
-		
-	case 403:
-		printf("Only wheel can add users\n");
-		ret = 1;
-		break;
-		
-	case 404:
-		printf("User '%s' already exists\n", Username);
-		ret = 0;
-		break;
-	
-	default:
-		fprintf(stderr, "Unknown response code %i '%s'\n", responseCode, buf);
-		ret = -1;
-		break;
-	}
-	
-	free(buf);
-	
-	return ret;
-}
-
-int Dispense_SetUserType(int Socket, const char *Username, const char *TypeString, const char *Reason)
-{
-	char	*buf;
-	 int	responseCode, ret;
-	
-	// Check for a dry run
-	if( gbDryRun ) {
-		printf("Dry Run - No action\n");
-		return 0;
-	}
-	
-	// TODO: Pre-validate the string
-	
-	sendf(Socket, "USER_FLAGS %s %s %s\n", Username, TypeString, Reason);
-	
-	buf = ReadLine(Socket);
-	responseCode = atoi(buf);
-	
-	switch(responseCode)
-	{
-	case 200:
-		printf("User '%s' updated\n", Username);
-		ret = 0;
-		break;
-		
-	case 403:
-		printf("Only dispense admins can modify users\n");
-		ret = RV_PERMISSIONS;
-		break;
-	
-	case 404:
-		printf("User '%s' does not exist\n", Username);
-		ret = RV_INVALID_USER;
-		break;
-	
-	case 407:
-		printf("Flag string is invalid\n");
-		ret = RV_ARGUMENTS;
-		break;
-	
-	default:
-		fprintf(stderr, "Unknown response code %i '%s'\n", responseCode, buf);
-		ret = RV_UNKNOWN_RESPONSE;
-		break;
-	}
-	
-	free(buf);
-	
-	return ret;
-}
-
-int Dispense_SetItem(int Socket, const char *Type, int ID, int NewPrice, const char *NewName)
-{
-	char	*buf;
-	 int	responseCode, ret;
-	
-	// Check for a dry run
-	if( gbDryRun ) {
-		printf("Dry Run - No action\n");
-		return 0;
-	}
-	
-	sendf(Socket, "UPDATE_ITEM %s:%i %i %s\n", Type, ID, NewPrice, NewName);
-	
-	buf = ReadLine(Socket);
-	responseCode = atoi(buf);
-	
-	switch(responseCode)
-	{
-	case 200:
-		printf("Item %s:%i updated\n", Type, ID);
-		ret = 0;
-		break;
-		
-	case 403:
-		printf("Only coke members can modify the slots\n");
-		ret = RV_PERMISSIONS;
-		break;
-	
-	case 406:
-		printf("Invalid item passed\n");
-		ret = RV_BAD_ITEM;
-		break;
-	
-	default:
-		fprintf(stderr, "Unknown response code %i '%s'\n", responseCode, buf);
-		ret = -1;
-		break;
-	}
-	
-	free(buf);
-	
-	return ret;
-}
-
 // ---------------
 // --- Helpers ---
 // ---------------
-char *ReadLine(int Socket)
-{
-	static char	buf[BUFSIZ];
-	static int	bufPos = 0;
-	static int	bufValid = 0;
-	 int	len;
-	char	*newline = NULL;
-	 int	retLen = 0;
-	char	*ret = malloc(10);
-	
-	#if DEBUG_TRACE_SERVER
-	printf("ReadLine: ");
-	fflush(stdout);
-	#endif
-	
-	ret[0] = '\0';
-	
-	while( !newline )
-	{
-		if( bufValid ) {
-			len = bufValid;
-		}
-		else {
-			len = recv(Socket, buf+bufPos, BUFSIZ-1-bufPos, 0);
-			if( len <= 0 ) {
-				free(ret);
-				return strdup("599 Client Connection Error\n");
-			}
-		}
-		buf[bufPos+len] = '\0';
-		
-		newline = strchr( buf+bufPos, '\n' );
-		if( newline ) {
-			*newline = '\0';
-		}
-		
-		retLen += strlen(buf+bufPos);
-		ret = realloc(ret, retLen + 1);
-		strcat( ret, buf+bufPos );
-		
-		if( newline ) {
-			 int	newLen = newline - (buf+bufPos) + 1;
-			bufValid = len - newLen;
-			len = newLen;
-		}
-		if( len + bufPos == BUFSIZ - 1 )	bufPos = 0;
-		else	bufPos += len;
-	}
-	
-	#if DEBUG_TRACE_SERVER
-	printf("%i '%s'\n", retLen, ret);
-	#endif
-	
-	return ret;
-}
-
-int sendf(int Socket, const char *Format, ...)
-{
-	va_list	args;
-	 int	len;
-	
-	va_start(args, Format);
-	len = vsnprintf(NULL, 0, Format, args);
-	va_end(args);
-	
-	{
-		char	buf[len+1];
-		va_start(args, Format);
-		vsnprintf(buf, len+1, Format, args);
-		va_end(args);
-		
-		#if DEBUG_TRACE_SERVER
-		printf("sendf: %s", buf);
-		#endif
-		
-		return send(Socket, buf, len, 0);
-	}
-}
-
 char *trim(char *string)
 {
 	 int	i;
diff --git a/src/client/menu.c b/src/client/menu.c
new file mode 100644
index 0000000..ce3309f
--- /dev/null
+++ b/src/client/menu.c
@@ -0,0 +1,407 @@
+/*
+ * OpenDispense 2 
+ * UCC (University [of WA] Computer Club) Electronic Accounting System
+ * - Dispense Client
+ *
+ * menu.c - ncurses dispense menu
+ *
+ * This file is licenced under the 3-clause BSD Licence. See the file
+ * COPYING for full details.
+ */
+#include <stdlib.h>
+#include <ncurses.h>
+#include <pwd.h>	// getpwuids
+#include <unistd.h>	// getuid
+#include "common.h"
+
+// === PROTOTYPES ===
+ int	ShowItemAt(int Row, int Col, int Width, int Index, int bHilighted);
+void	PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1, const char *Mid, char Pad2, const char *Right, ...);
+
+// -------------------
+// --- NCurses GUI ---
+// -------------------
+/**
+ * \brief Render the NCurses UI
+ */
+int ShowNCursesUI(void)
+{
+	 int	ch;
+	 int	i, times;
+	 int	xBase, yBase;
+	const int	displayMinWidth = 50;
+	const char	*titleString = "Dispense";
+	 int	items_in_view;
+	 int	maxItemIndex;
+	 int	itemBase = 0;
+	 int	currentItem;
+	 int	ret = -2;	// -2: Used for marking "no return yet"
+	
+	char	balance_str[5+1+2+1];	// If $9999.99 is too little, something's wrong
+	char	*username;
+	struct passwd *pwd;
+	 
+	 int	height, width;
+	
+	void _ItemDown(void)
+	{
+		currentItem ++;
+		// Skip over spacers
+		while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
+			currentItem ++;
+		
+		if( currentItem >= maxItemIndex ) {
+			currentItem = 0;
+			// Skip over spacers
+			while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
+				currentItem ++;
+		}
+	}
+	
+	void _ItemUp(void)
+	{
+		currentItem --;
+		// Skip over spacers
+		while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
+			currentItem --;
+		
+		if( currentItem < 0 ) {
+			currentItem = maxItemIndex - 1;
+			// Skip over spacers
+			while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
+				currentItem --;
+		}
+	}
+
+	// Get Username
+	if( gsEffectiveUser )
+		username = gsEffectiveUser;
+	else {
+		pwd = getpwuid( getuid() );
+		username = pwd->pw_name;
+	}
+	// Get balance
+	snprintf(balance_str, sizeof(balance_str), "$%i.%02i", giUserBalance/100, abs(giUserBalance)%100);
+	
+	// Enter curses mode
+	initscr();
+	cbreak(); noecho();
+	
+	// Get max index
+	maxItemIndex = ShowItemAt(0, 0, 0, -1, 0);
+	// Get item count per screen
+	// - 6: randomly chosen (Need at least 3)
+	items_in_view = LINES - 6;
+	if( items_in_view > maxItemIndex )
+		items_in_view = maxItemIndex;
+	// Get first index
+	currentItem = 0;
+	while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
+		currentItem ++;
+	
+	
+	// Get dimensions
+	height = items_in_view + 3;
+	width = displayMinWidth;
+	
+	// Get positions
+	xBase = COLS/2 - width/2;
+	yBase = LINES/2 - height/2;
+	
+	for( ;; )
+	{
+		// Header
+		PrintAlign(yBase, xBase, width, "/", '-', titleString, '-', "\\");
+		
+		// Items
+		for( i = 0; i < items_in_view; i ++ )
+		{
+			 int	pos = 0;
+			
+			move( yBase + 1 + i, xBase );
+			printw("| ");
+			
+			pos += 2;
+			
+			// Check for the '...' row
+			// - Oh god, magic numbers!
+			if( (i == 0 && itemBase > 0)
+			 || (i == items_in_view - 1 && itemBase < maxItemIndex - items_in_view) )
+			{
+				printw("     ...");	pos += 8;
+				times = (width - pos) - 1;
+				while(times--)	addch(' ');
+			}
+			// Show an item
+			else {
+				ShowItemAt(
+					yBase + 1 + i, xBase + pos,	// Position
+					(width - pos) - 3,	// Width
+					itemBase + i,	// Index
+					!!(currentItem == itemBase + i)	// Hilighted
+					);
+				printw("  ");
+			}
+			
+			// Scrollbar (if needed)
+			if( maxItemIndex > items_in_view ) {
+				if( i == 0 ) {
+					addch('A');
+				}
+				else if( i == items_in_view - 1 ) {
+					addch('V');
+				}
+				else {
+					 int	percentage = itemBase * 100 / (maxItemIndex-items_in_view);
+					if( i-1 == percentage*(items_in_view-3)/100 ) {
+						addch('#');
+					}
+					else {
+						addch('|');
+					}
+				}
+			}
+			else {
+				addch('|');
+			}
+		}
+		
+		// Footer
+		PrintAlign(yBase+height-2, xBase, width, "\\", '-', "", '-', "/");
+		
+		// User line
+		// - Username, balance, flags
+		PrintAlign(yBase+height-1, xBase+1, width-2,
+			username, ' ', balance_str, ' ', gsUserFlags);
+		PrintAlign(yBase+height, xBase+1, width-2,
+			"q: Quit", ' ', "Arrow: Select", ' ', "Enter: Drop");
+		
+		
+		// Get input
+		ch = getch();
+		
+		if( ch == '\x1B' ) {
+			ch = getch();
+			if( ch == '[' ) {
+				ch = getch();
+				
+				switch(ch)
+				{
+				case 'B':	_ItemDown();	break;
+				case 'A':	_ItemUp();	break;
+				}
+			}
+			else if( ch == ERR || ch == '\x1b' ) {
+				ret = -1;
+				break;
+			}
+			else {
+				fprintf(stderr, "Unknown character 0x%x\n", ch);
+			}
+		}
+		else {
+			switch(ch)
+			{
+			case '\n':
+				ret = ShowItemAt(0, 0, 0, currentItem, 0);
+				break;
+			case 'h':	break;
+			case 'j':	_ItemDown();	break;
+			case 'k':	_ItemUp();	break;
+			case 'l':	break;
+			case 0x1b:	// Escape
+			case 'q':
+				ret = -1;	// -1: Return with no dispense
+				break;
+			}
+			
+			// Check if the return value was changed
+			if( ret != -2 )	break;
+		}
+		
+		// Scroll only if needed
+		if( items_in_view < maxItemIndex )
+		{
+			// - If the current item is above the second item shown, and we're not at the top
+			if( currentItem < itemBase + 2 && itemBase > 0 ) {
+				itemBase = currentItem - 2;
+				if(itemBase < 0)	itemBase = 0;
+			}
+			// - If the current item is below the second item show, and we're not at the bottom
+			if( currentItem > itemBase + items_in_view - 2 && itemBase + items_in_view < maxItemIndex ) {
+				itemBase = currentItem - items_in_view + 2;
+				if( itemBase > maxItemIndex - items_in_view )
+					itemBase = maxItemIndex - items_in_view;
+			}
+		}
+	}
+	
+	
+	// Leave
+	endwin();
+	return ret;
+}
+
+/**
+ * \brief Show item \a Index at (\a Col, \a Row)
+ * \return Dispense index of item
+ * \note Part of the NCurses UI
+ */
+int ShowItemAt(int Row, int Col, int Width, int Index, int bHilighted)
+{
+	char	*name = NULL;
+	 int	price = 0;
+	 int	status = -1;
+	
+	switch(giUIMode)
+	{
+	// Standard UI
+	// - This assumes that 
+	case UI_MODE_STANDARD:
+		// Bounds check
+		// Index = -1, request limit
+		if( Index < 0 || Index >= giNumItems+2 )
+			return giNumItems+2;
+		// Drink label
+		if( Index == 0 )
+		{
+			price = 0;
+			name = "Coke Machine";
+			Index = -1;	// -1 indicates a label
+			break;
+		}
+		Index --;
+		// Drinks 0 - 6
+		if( Index <= 6 )
+		{
+			name = gaItems[Index].Desc;
+			price = gaItems[Index].Price;
+			status = gaItems[Index].Status;
+			break;
+		}
+		Index -= 7;
+		// EPS label
+		if( Index == 0 )
+		{
+			price = 0;
+			name = "Electronic Payment System";
+			Index = -1;	// -1 indicates a label
+			break;
+		}
+		Index --;
+		Index += 7;
+		name = gaItems[Index].Desc;
+		price = gaItems[Index].Price;
+		status = gaItems[Index].Status;
+		break;
+	default:
+		return -1;
+	}
+	
+	// Width = 0, don't print
+	if( Width > 0 )
+	{
+		// 4 preceding, 5 price
+		int nameWidth = Width - 4 - snprintf(NULL, 0, " %4i", price);
+		move( Row, Col );
+		
+		if( Index >= 0 )
+		{
+			// Show hilight and status
+			switch( status )
+			{
+			case 0:
+				if( bHilighted )
+					printw("->  ");
+				else
+					printw("    ");
+				break;
+			case 1:
+				printw("SLD ");
+				break;
+			
+			default:
+			case -1:
+				printw("ERR ");
+				break;
+			}
+			
+			printw("%-*.*s", nameWidth, nameWidth, name);
+		
+			printw(" %4i", price);
+		}
+		else
+		{
+			printw("-- %-*.*s ", Width-4, Width-4, name);
+		}
+	}
+	
+	// If the item isn't availiable for sale, return -1 (so it's skipped)
+	if( status || (price > giUserBalance && gbDisallowSelectWithoutBalance) )
+		Index = -1;
+	
+	return Index;
+}
+
+/**
+ * \brief Print a three-part string at the specified position (formatted)
+ * \note NCurses UI Helper
+ * 
+ * Prints \a Left on the left of the area, \a Right on the righthand side
+ * and \a Mid in the middle of the area. These are padded with \a Pad1
+ * between \a Left and \a Mid, and \a Pad2 between \a Mid and \a Right.
+ * 
+ * ::printf style format codes are allowed in \a Left, \a Mid and \a Right,
+ * and the arguments to these are read in that order.
+ */
+void PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1,
+	const char *Mid, char Pad2, const char *Right, ...)
+{
+	 int	lLen, mLen, rLen;
+	 int	times;
+	
+	va_list	args;
+	
+	// Get the length of the strings
+	va_start(args, Right);
+	lLen = vsnprintf(NULL, 0, Left, args);
+	mLen = vsnprintf(NULL, 0, Mid, args);
+	rLen = vsnprintf(NULL, 0, Right, args);
+	va_end(args);
+	
+	// Sanity check
+	if( lLen + mLen/2 > Width/2 || mLen/2 + rLen > Width/2 ) {
+		return ;	// TODO: What to do?
+	}
+	
+	move(Row, Col);
+	
+	// Render strings
+	va_start(args, Right);
+	// - Left
+	{
+		char	tmp[lLen+1];
+		vsnprintf(tmp, lLen+1, Left, args);
+		addstr(tmp);
+	}
+	// - Left padding
+	times = (Width - mLen)/2 - lLen;
+	while(times--)	addch(Pad1);
+	// - Middle
+	{
+		char	tmp[mLen+1];
+		vsnprintf(tmp, mLen+1, Mid, args);
+		addstr(tmp);
+	}
+	// - Right Padding
+	times = (Width - mLen)/2 - rLen;
+	if( (Width - mLen) % 2 )	times ++;
+	while(times--)	addch(Pad2);
+	// - Right
+	{
+		char	tmp[rLen+1];
+		vsnprintf(tmp, rLen+1, Right, args);
+		addstr(tmp);
+	}
+}
+
diff --git a/src/client/protocol.c b/src/client/protocol.c
new file mode 100644
index 0000000..483ee57
--- /dev/null
+++ b/src/client/protocol.c
@@ -0,0 +1,1161 @@
+/*
+ * OpenDispense 2 
+ * UCC (University [of WA] Computer Club) Electronic Accounting System
+ * - Dispense Client
+ *
+ * protocol.c
+ * - Client/Server communication
+ *
+ * This file is licenced under the 3-clause BSD Licence. See the file
+ * COPYING for full details.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <netdb.h>	// gethostbyname
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+//#include <openssl/sha.h>	// SHA1
+#include <pwd.h>	// getpwuids
+#include <unistd.h>	// close/getuid
+#include <limits.h>	// INT_MIN/INT_MAX
+#include <stdarg.h>
+#include "common.h"
+
+// === PROTOTYPES ===
+char	*ReadLine(int Socket);
+ int	sendf(int Socket, const char *Format, ...);
+
+// ---------------------
+// --- Coke Protocol ---
+// ---------------------
+int OpenConnection(const char *Host, int Port)
+{
+	struct hostent	*host;
+	struct sockaddr_in	serverAddr;
+	 int	sock;
+	
+	host = gethostbyname(Host);
+	if( !host ) {
+		fprintf(stderr, "Unable to look up '%s'\n", Host);
+		return -1;
+	}
+	
+	memset(&serverAddr, 0, sizeof(serverAddr));
+	
+	serverAddr.sin_family = AF_INET;	// IPv4
+	// NOTE: I have a suspicion that IPv6 will play sillybuggers with this :)
+	serverAddr.sin_addr.s_addr = *((unsigned long *) host->h_addr_list[0]);
+	serverAddr.sin_port = htons(Port);
+	
+	sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
+	if( sock < 0 ) {
+		fprintf(stderr, "Failed to create socket\n");
+		return -1;
+	}
+
+//	printf("geteuid() = %i, getuid() = %i\n", geteuid(), getuid());
+	
+	if( geteuid() == 0 || getuid() == 0 )
+	{
+		 int	i;
+		struct sockaddr_in	localAddr;
+		memset(&localAddr, 0, sizeof(localAddr));
+		localAddr.sin_family = AF_INET;	// IPv4
+		
+		// Loop through all the top ports until one is avaliable
+		for( i = 512; i < 1024; i ++)
+		{
+			localAddr.sin_port = htons(i);	// IPv4
+			// Attempt to bind to low port for autoauth
+			if( bind(sock, (struct sockaddr*)&localAddr, sizeof(localAddr)) == 0 )
+				break;
+		}
+		if( i == 1024 )
+			printf("Warning: AUTOAUTH unavaliable\n");
+//		else
+//			printf("Bound to 0.0.0.0:%i\n", i);
+	}
+	
+	if( connect(sock, (struct sockaddr *) &serverAddr, sizeof(serverAddr)) < 0 ) {
+		fprintf(stderr, "Failed to connect to server\n");
+		return -1;
+	}
+
+	// We're not authenticated if the connection has just opened
+	gbIsAuthenticated = 0;
+	
+	return sock;
+}
+
+int Authenticate_AutoAuth(int Socket, const char *Username)
+{
+	char	*buf;
+	 int	responseCode;
+	 int	ret = -1;
+	
+	// Attempt automatic authentication
+	sendf(Socket, "AUTOAUTH %s\n", Username);
+	
+	// Check if it worked
+	buf = ReadLine(Socket);
+	
+	responseCode = atoi(buf);
+	switch( responseCode )
+	{
+	case 200:	// Autoauth succeeded, return
+		ret = 0;
+		break;
+	
+	case 401:	// Untrusted
+//		fprintf(stderr, "Untrusted host, AUTOAUTH unavaliable\n");
+		ret = RV_PERMISSIONS;
+		break;
+	case 404:	// Bad Username
+		fprintf(stderr, "Bad Username '%s'\n", Username);
+		ret = RV_INVALID_USER;
+		break;
+	
+	default:
+		fprintf(stderr, "Unkown response code %i from server\n", responseCode);
+		printf("%s\n", buf);
+		ret = RV_UNKNOWN_ERROR;
+		break;;
+	}
+	
+	free(buf);
+	return ret;
+}
+
+int Authenticate_AuthIdent(int Socket)
+{
+	char	*buf;
+	 int	responseCode;
+	 int	ret = -1;
+	
+	// Attempt automatic authentication
+	sendf(Socket, "AUTHIDENT\n");
+	
+	// Check if it worked
+	buf = ReadLine(Socket);
+	
+	responseCode = atoi(buf);
+	switch( responseCode )
+	{
+	case 200:	// Autoauth succeeded, return
+		ret = 0;
+		break;
+	
+	case 401:	// Untrusted
+//		fprintf(stderr, "Untrusted host, AUTHIDENT unavaliable\n");
+		ret = RV_PERMISSIONS;
+		break;
+	
+	default:
+		fprintf(stderr, "Unkown response code %i from server\n", responseCode);
+		printf("%s\n", buf);
+		ret = RV_UNKNOWN_RESPONSE;
+		break;
+	}
+	
+	free(buf);
+
+	return ret;
+}
+
+int Authenticate_Password(int Socket, const char *Username)
+{
+	#if USE_PASSWORD_AUTH
+	char	*buf;
+	 int	responseCode;	
+	char	salt[32];
+	 int	i;
+	regmatch_t	matches[4];
+
+	sendf(Socket, "USER %s\n", Username);
+	printf("Using username %s\n", Username);
+	
+	buf = ReadLine(Socket);
+	
+	// TODO: Get Salt
+	// Expected format: 100 SALT <something> ...
+	// OR             : 100 User Set
+	RunRegex(&gSaltRegex, buf, 4, matches, "Malformed server response");
+	responseCode = atoi(buf);
+	if( responseCode != 100 ) {
+		fprintf(stderr, "Unknown repsonse code %i from server\n%s\n", responseCode, buf);
+		free(buf);
+		return RV_UNKNOWN_ERROR;	// ERROR
+	}
+	
+	// Check for salt
+	if( memcmp( buf+matches[2].rm_so, "SALT", matches[2].rm_eo - matches[2].rm_so) == 0) {
+		// Store it for later
+		memcpy( salt, buf + matches[3].rm_so, matches[3].rm_eo - matches[3].rm_so );
+		salt[ matches[3].rm_eo - matches[3].rm_so ] = 0;
+	}
+	free(buf);
+	
+	// Give three attempts
+	for( i = 0; i < 3; i ++ )
+	{
+		 int	ofs = strlen(Username)+strlen(salt);
+		char	tmpBuf[42];
+		char	tmp[ofs+20];
+		char	*pass = getpass("Password: ");
+		uint8_t	h[20];
+		
+		// Create hash string
+		// <username><salt><hash>
+		strcpy(tmp, Username);
+		strcat(tmp, salt);
+		SHA1( (unsigned char*)pass, strlen(pass), h );
+		memcpy(tmp+ofs, h, 20);
+		
+		// Hash all that
+		SHA1( (unsigned char*)tmp, ofs+20, h );
+		sprintf(tmpBuf, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
+			h[ 0], h[ 1], h[ 2], h[ 3], h[ 4], h[ 5], h[ 6], h[ 7], h[ 8], h[ 9],
+			h[10], h[11], h[12], h[13], h[14], h[15], h[16], h[17], h[18], h[19]
+			);
+	
+		// Send password
+		sendf(Socket, "PASS %s\n", tmpBuf);
+		buf = ReadLine(Socket);
+	
+		responseCode = atoi(buf);
+		// Auth OK?
+		if( responseCode == 200 )	break;
+		// Bad username/password
+		if( responseCode == 401 )	continue;
+		
+		fprintf(stderr, "Unknown repsonse code %i from server\n%s\n", responseCode, buf);
+		free(buf);
+		return -1;
+	}
+	free(buf);
+	if( i == 3 )
+		return RV_INVALID_USER;	// 2 = Bad Password
+
+	return 0;
+	#else
+	return RV_INVALID_USER;
+	#endif
+}
+
+/**
+ * \brief Authenticate with the server
+ * \return Boolean Failure
+ */
+int Authenticate(int Socket)
+{
+	struct passwd	*pwd;
+	
+	if( gbIsAuthenticated )	return 0;
+	
+	// Get user name
+	pwd = getpwuid( getuid() );
+
+	// Attempt AUTOAUTH
+	if( Authenticate_AutoAuth(Socket, pwd->pw_name) == 0 )
+		;
+	else if( Authenticate_AuthIdent(Socket) == 0 )
+		;
+	else if( Authenticate_Password(Socket, pwd->pw_name) == 0 )
+		return RV_INVALID_USER;
+
+	// Set effective user
+	if( gsEffectiveUser ) {
+		char	*buf;
+		 int	responseCode;
+		sendf(Socket, "SETEUSER %s\n", gsEffectiveUser);
+		
+		buf = ReadLine(Socket);
+		responseCode = atoi(buf);
+		
+		switch(responseCode)
+		{
+		case 200:
+			printf("Running as '%s' by '%s'\n", gsEffectiveUser, pwd->pw_name);
+			break;
+		
+		case 403:
+			printf("Only coke members can use `dispense -u`\n");
+			free(buf);
+			return RV_PERMISSIONS;
+		
+		case 404:
+			printf("Invalid user selected\n");
+			free(buf);
+			return RV_INVALID_USER;
+		
+		default:
+			fprintf(stderr, "Unkown response code %i from server\n", responseCode);
+			printf("%s\n", buf);
+			free(buf);
+			return RV_UNKNOWN_ERROR;
+		}
+		
+		free(buf);
+	}
+	
+	gbIsAuthenticated = 1;
+	
+	return 0;
+}
+
+int GetUserBalance(int Socket)
+{
+	regmatch_t	matches[6];
+	struct passwd	*pwd;
+	char	*buf;
+	 int	responseCode;
+	
+	if( !gsUserName )
+	{
+		if( gsEffectiveUser ) {
+			gsUserName = gsEffectiveUser;
+		}
+		else {
+			pwd = getpwuid( getuid() );
+			gsUserName = strdup(pwd->pw_name);
+		}
+	}
+	
+	sendf(Socket, "USER_INFO %s\n", gsUserName);
+	buf = ReadLine(Socket);
+	responseCode = atoi(buf);
+	switch(responseCode)
+	{
+	case 202:	break;	// Ok
+	
+	case 404:
+		printf("Invalid user? (USER_INFO failed)\n");
+		free(buf);
+		return RV_INVALID_USER;
+	
+	default:
+		fprintf(stderr, "Unkown response code %i from server\n", responseCode);
+		printf("%s\n", buf);
+		free(buf);
+		return RV_UNKNOWN_ERROR;
+	}
+
+	RunRegex(&gUserInfoRegex, buf, 6, matches, "Malformed server response");
+	
+	giUserBalance = atoi( buf + matches[4].rm_so );
+	gsUserFlags = strdup( buf + matches[5].rm_so );
+	
+	free(buf);
+	
+	return 0;
+}
+
+/**
+ * \brief Read an item info response from the server
+ * \param Dest	Destination for the read item (strings will be on the heap)
+ */
+int ReadItemInfo(int Socket, tItem *Dest)
+{
+	char	*buf;
+	 int	responseCode;
+	
+	regmatch_t	matches[8];
+	char	*statusStr;
+	
+	// Get item info
+	buf = ReadLine(Socket);
+	responseCode = atoi(buf);
+	
+	switch(responseCode)
+	{
+	case 202:	break;
+	
+	case 406:
+		printf("Bad item name\n");
+		free(buf);
+		return RV_BAD_ITEM;
+	
+	default:
+		fprintf(stderr, "Unknown response from dispense server (Response Code %i)\n%s", responseCode, buf);
+		free(buf);
+		return RV_UNKNOWN_ERROR;
+	}
+	
+	RunRegex(&gItemRegex, buf, 8, matches, "Malformed server response");
+	
+	buf[ matches[3].rm_eo ] = '\0';
+	buf[ matches[5].rm_eo ] = '\0';
+	buf[ matches[7].rm_eo ] = '\0';
+	
+	statusStr = &buf[ matches[5].rm_so ];
+	
+	Dest->ID = atoi( buf + matches[4].rm_so );
+	
+	if( strcmp(statusStr, "avail") == 0 )
+		Dest->Status = 0;
+	else if( strcmp(statusStr, "sold") == 0 )
+		Dest->Status = 1;
+	else if( strcmp(statusStr, "error") == 0 )
+		Dest->Status = -1;
+	else {
+		fprintf(stderr, "Unknown response from dispense server (status '%s')\n",
+			statusStr);
+		return RV_UNKNOWN_ERROR;
+	}
+	Dest->Price = atoi( buf + matches[6].rm_so );
+	
+	// Hack a little to reduce heap fragmentation
+	{
+		char	tmpType[strlen(buf + matches[3].rm_so) + 1];
+		char	tmpDesc[strlen(buf + matches[7].rm_so) + 1];
+		strcpy(tmpType, buf + matches[3].rm_so);
+		strcpy(tmpDesc, buf + matches[7].rm_so);
+		free(buf);
+		Dest->Type = strdup( tmpType );
+		Dest->Desc = strdup( tmpDesc );
+	}
+	
+	return 0;
+}
+
+/**
+ * \brief Fill the item information structure
+ * \return Boolean Failure
+ */
+void PopulateItemList(int Socket)
+{
+	char	*buf;
+	 int	responseCode;
+	
+	char	*arrayType;
+	 int	count, i;
+	regmatch_t	matches[4];
+	
+	// Ask server for stock list
+	send(Socket, "ENUM_ITEMS\n", 11, 0);
+	buf = ReadLine(Socket);
+	
+	//printf("Output: %s\n", buf);
+	
+	responseCode = atoi(buf);
+	if( responseCode != 201 ) {
+		fprintf(stderr, "Unknown response from dispense server (Response Code %i)\n", responseCode);
+		exit(RV_UNKNOWN_ERROR);
+	}
+	
+	// - Get item list -
+	
+	// Expected format:
+	//  201 Items <count>
+	//  202 Item <count>
+	RunRegex(&gArrayRegex, buf, 4, matches, "Malformed server response");
+		
+	arrayType = &buf[ matches[2].rm_so ];	buf[ matches[2].rm_eo ] = '\0';
+	count = atoi( &buf[ matches[3].rm_so ] );
+		
+	// Check array type
+	if( strcmp(arrayType, "Items") != 0 ) {
+		// What the?!
+		fprintf(stderr, "Unexpected array type, expected 'Items', got '%s'\n",
+			arrayType);
+		exit(RV_UNKNOWN_ERROR);
+	}
+	free(buf);
+	
+	giNumItems = count;
+	gaItems = malloc( giNumItems * sizeof(tItem) );
+	
+	// Fetch item information
+	for( i = 0; i < giNumItems; i ++ )
+	{
+		ReadItemInfo( Socket, &gaItems[i] );
+	}
+	
+	// Read end of list
+	buf = ReadLine(Socket);
+	responseCode = atoi(buf);
+		
+	if( responseCode != 200 ) {
+		fprintf(stderr, "Unknown response from dispense server %i\n'%s'",
+			responseCode, buf
+			);
+		exit(-1);
+	}
+	
+	free(buf);
+}
+
+
+/**
+ * \brief Get information on an item
+ * \return Boolean Failure
+ */
+int Dispense_ItemInfo(int Socket, const char *Type, int ID)
+{
+	tItem	item;
+	 int	ret;
+	
+	// Query
+	sendf(Socket, "ITEM_INFO %s:%i\n", Type, ID);
+	
+	ret = ReadItemInfo(Socket, &item);
+	if(ret)	return ret;
+	
+	printf("%8s:%-2i %2i.%02i %s\n",
+		item.Type, item.ID,
+		item.Price/100, item.Price%100,
+		item.Desc);
+	
+	free(item.Type);
+	free(item.Desc);
+	
+	return 0;
+}
+
+/**
+ * \brief Dispense an item
+ * \return Boolean Failure
+ */
+int DispenseItem(int Socket, const char *Type, int ID)
+{
+	 int	ret, responseCode;
+	char	*buf;
+	
+	// Check for a dry run
+	if( gbDryRun ) {
+		printf("Dry Run - No action\n");
+		return 0;
+	}
+	
+	// Dispense!
+	sendf(Socket, "DISPENSE %s:%i\n", Type, ID);
+	buf = ReadLine(Socket);
+	
+	responseCode = atoi(buf);
+	switch( responseCode )
+	{
+	case 200:
+		printf("Dispense OK\n");
+		ret = 0;
+		break;
+	case 401:
+		printf("Not authenticated\n");
+		ret = RV_PERMISSIONS;
+		break;
+	case 402:
+		printf("Insufficient balance\n");
+		ret = RV_BALANCE;
+		break;
+	case 406:
+		printf("Bad item name\n");
+		ret = RV_BAD_ITEM;
+		break;
+	case 500:
+		printf("Item failed to dispense, is the slot empty?\n");
+		ret = RV_SERVER_ERROR;
+		break;
+	case 501:
+		printf("Dispense not possible (slot empty/permissions)\n");
+		ret = RV_SERVER_ERROR;
+		break;
+	default:
+		printf("Unknown response code %i ('%s')\n", responseCode, buf);
+		ret = RV_UNKNOWN_ERROR;
+		break;
+	}
+	
+	free(buf);
+	return ret;
+}
+
+/**
+ * \brief Alter a user's balance
+ */
+int Dispense_AlterBalance(int Socket, const char *Username, int Ammount, const char *Reason)
+{
+	char	*buf;
+	 int	responseCode, rv = -1;
+	
+	// Check for a dry run
+	if( gbDryRun ) {
+		printf("Dry Run - No action\n");
+		return 0;
+	}
+
+	// Sanity
+	if( Ammount == 0 ) {
+		printf("An amount would be nice\n");
+		return RV_ARGUMENTS;
+	}
+	
+	sendf(Socket, "ADD %s %i %s\n", Username, Ammount, Reason);
+	buf = ReadLine(Socket);
+	
+	responseCode = atoi(buf);
+	
+	switch(responseCode)
+	{
+	case 200:
+		rv = 0;	// OK
+		break;
+	case 402:
+		fprintf(stderr, "Insufficient balance\n");
+		rv = RV_BAD_ITEM;
+		break;
+	case 403:	// Not in coke
+		fprintf(stderr, "You are not in coke (sucker)\n");
+		rv = RV_PERMISSIONS;
+		break;
+	case 404:	// Unknown user
+		fprintf(stderr, "Unknown user '%s'\n", Username);
+		rv = RV_INVALID_USER;
+		break;
+	default:
+		fprintf(stderr, "Unknown response code %i\n'%s'\n", responseCode, buf);
+		rv = RV_UNKNOWN_RESPONSE;
+		break;
+	}
+	free(buf);
+	
+	return rv;
+}
+
+/**
+ * \brief Set a user's balance
+ * \note Only avaliable to dispense admins
+ */
+int Dispense_SetBalance(int Socket, const char *Username, int Balance, const char *Reason)
+{
+	char	*buf;
+	 int	responseCode;
+	
+	// Check for a dry run
+	if( gbDryRun ) {
+		printf("Dry Run - No action\n");
+		return 0;
+	}
+	
+	sendf(Socket, "SET %s %i %s\n", Username, Balance, Reason);
+	buf = ReadLine(Socket);
+	
+	responseCode = atoi(buf);
+	free(buf);
+	
+	switch(responseCode)
+	{
+	case 200:	return 0;	// OK
+	case 403:	// Not in coke
+		fprintf(stderr, "You are not an admin\n");
+		return RV_PERMISSIONS;
+	case 404:	// Unknown user
+		fprintf(stderr, "Unknown user '%s'\n", Username);
+		return RV_INVALID_USER;
+	default:
+		fprintf(stderr, "Unknown response code %i\n", responseCode);
+		return RV_UNKNOWN_RESPONSE;
+	}
+	
+	return -1;
+}
+
+/**
+ * \brief Give money to another user
+ */
+int Dispense_Give(int Socket, const char *Username, int Ammount, const char *Reason)
+{
+	char	*buf;
+	 int	responseCode;
+	
+	if( Ammount < 0 ) {
+		printf("Sorry, you can only give, you can't take.\n");
+		return RV_ARGUMENTS;
+	}
+	
+	// Fast return on zero
+	if( Ammount == 0 ) {
+		printf("Are you actually going to give any?\n");
+		return RV_ARGUMENTS;
+	}
+	
+	// Check for a dry run
+	if( gbDryRun ) {
+		printf("Dry Run - No action\n");
+		return 0;
+	}
+	
+	sendf(Socket, "GIVE %s %i %s\n", Username, Ammount, Reason);
+
+	buf = ReadLine(Socket);
+	responseCode = atoi(buf);
+	free(buf);	
+	switch(responseCode)
+	{
+	case 200:
+		printf("Give succeeded\n");
+		return RV_SUCCESS;	// OK
+	
+	case 402:	
+		fprintf(stderr, "Insufficient balance\n");
+		return RV_BALANCE;
+	
+	case 404:	// Unknown user
+		fprintf(stderr, "Unknown user '%s'\n", Username);
+		return RV_INVALID_USER;
+	
+	default:
+		fprintf(stderr, "Unknown response code %i\n", responseCode);
+		return RV_UNKNOWN_RESPONSE;
+	}
+	
+	return -1;
+}
+
+int Dispense_Refund(int Socket, const char *Username, const char *Item, int PriceOverride)
+{
+	char	*buf;
+	 int	responseCode, ret = -1;
+	
+	// Check item id
+	if( RunRegex(&gUserItemIdentRegex, Item, 0, NULL, NULL) != 0 )
+	{
+		fprintf(stderr, "Error: Invalid item ID passed (should be <type>:<num>)\n");
+		return RV_ARGUMENTS;
+	}
+
+	// Check username (quick)
+	if( strchr(Username, ' ') || strchr(Username, '\n') )
+	{
+		fprintf(stderr, "Error: Username is invalid (no spaces or newlines please)\n");
+		return RV_ARGUMENTS;
+	}
+
+	// Send the query
+	sendf(Socket, "REFUND %s %s %i\n", Username, Item, PriceOverride);
+
+	buf = ReadLine(Socket);
+	responseCode = atoi(buf);
+	switch(responseCode)
+	{
+	case 200:
+		Dispense_ShowUser(Socket, Username);	// Show destination account
+		ret = 0;
+		break;
+	case 403:
+		fprintf(stderr, "Refund access is only avaliable to coke members\n");
+		ret = RV_PERMISSIONS;
+		break;
+	case 404:
+		fprintf(stderr, "Unknown user '%s' passed\n", Username);
+		ret = RV_INVALID_USER;
+		break;
+	case 406:
+		fprintf(stderr, "Invalid item '%s' passed\n", Item);
+		ret = RV_BAD_ITEM;
+		break;
+	default:
+		fprintf(stderr, "Unknown response from server %i\n%s\n", responseCode, buf);
+		ret = -1;
+		break;
+	}
+	free(buf);
+	return ret;
+}
+
+/**
+ * \brief Donate money to the club
+ */
+int Dispense_Donate(int Socket, int Ammount, const char *Reason)
+{
+	char	*buf;
+	 int	responseCode;
+	
+	if( Ammount < 0 ) {
+		printf("Sorry, you can only give, you can't take.\n");
+		return -1;
+	}
+	
+	// Fast return on zero
+	if( Ammount == 0 ) {
+		printf("Are you actually going to give any?\n");
+		return 1;
+	}
+	
+	// Check for a dry run
+	if( gbDryRun ) {
+		printf("Dry Run - No action\n");
+		return 0;
+	}
+	
+	sendf(Socket, "DONATE %i %s\n", Ammount, Reason);
+	buf = ReadLine(Socket);
+	
+	responseCode = atoi(buf);
+	free(buf);
+	
+	switch(responseCode)
+	{
+	case 200:	return 0;	// OK
+	
+	case 402:	
+		fprintf(stderr, "Insufficient balance\n");
+		return 1;
+	
+	default:
+		fprintf(stderr, "Unknown response code %i\n", responseCode);
+		return -1;
+	}
+	
+	return -1;
+}
+
+/**
+ * \brief Enumerate users
+ */
+int Dispense_EnumUsers(int Socket)
+{
+	char	*buf;
+	 int	responseCode;
+	 int	nUsers;
+	regmatch_t	matches[4];
+	
+	if( giMinimumBalance != INT_MIN ) {
+		if( giMaximumBalance != INT_MAX ) {
+			sendf(Socket, "ENUM_USERS min_balance:%i max_balance:%i\n", giMinimumBalance, giMaximumBalance);
+		}
+		else {
+			sendf(Socket, "ENUM_USERS min_balance:%i\n", giMinimumBalance);
+		}
+	}
+	else {
+		if( giMaximumBalance != INT_MAX ) {
+			sendf(Socket, "ENUM_USERS max_balance:%i\n", giMaximumBalance);
+		}
+		else {
+			sendf(Socket, "ENUM_USERS\n");
+		}
+	}
+	buf = ReadLine(Socket);
+	responseCode = atoi(buf);
+	
+	switch(responseCode)
+	{
+	case 201:	break;	// Ok, length follows
+	
+	default:
+		fprintf(stderr, "Unknown response code %i\n%s\n", responseCode, buf);
+		free(buf);
+		return -1;
+	}
+	
+	// Get count (not actually used)
+	RunRegex(&gArrayRegex, buf, 4, matches, "Malformed server response");
+	nUsers = atoi( buf + matches[3].rm_so );
+	printf("%i users returned\n", nUsers);
+	
+	// Free string
+	free(buf);
+	
+	// Read returned users
+	do {
+		buf = ReadLine(Socket);
+		responseCode = atoi(buf);
+		
+		if( responseCode != 202 )	break;
+		
+		_PrintUserLine(buf);
+		free(buf);
+	} while(responseCode == 202);
+	
+	// Check final response
+	if( responseCode != 200 ) {
+		fprintf(stderr, "Unknown response code %i\n%s\n", responseCode, buf);
+		free(buf);
+		return -1;
+	}
+	
+	free(buf);
+	
+	return 0;
+}
+
+int Dispense_ShowUser(int Socket, const char *Username)
+{
+	char	*buf;
+	 int	responseCode, ret;
+	
+	sendf(Socket, "USER_INFO %s\n", Username);
+	buf = ReadLine(Socket);
+	
+	responseCode = atoi(buf);
+	
+	switch(responseCode)
+	{
+	case 202:
+		_PrintUserLine(buf);
+		ret = 0;
+		break;
+	
+	case 404:
+		printf("Unknown user '%s'\n", Username);
+		ret = 1;
+		break;
+	
+	default:
+		fprintf(stderr, "Unknown response code %i '%s'\n", responseCode, buf);
+		ret = -1;
+		break;
+	}
+	
+	free(buf);
+	
+	return ret;
+}
+
+void _PrintUserLine(const char *Line)
+{
+	regmatch_t	matches[6];
+	 int	bal;
+	
+	RunRegex(&gUserInfoRegex, Line, 6, matches, "Malformed server response");
+	// 3: Username
+	// 4: Balance
+	// 5: Flags
+	{
+		 int	usernameLen = matches[3].rm_eo - matches[3].rm_so;
+		char	username[usernameLen + 1];
+		 int	flagsLen = matches[5].rm_eo - matches[5].rm_so;
+		char	flags[flagsLen + 1];
+		
+		memcpy(username, Line + matches[3].rm_so, usernameLen);
+		username[usernameLen] = '\0';
+		memcpy(flags, Line + matches[5].rm_so, flagsLen);
+		flags[flagsLen] = '\0';
+		
+		bal = atoi(Line + matches[4].rm_so);
+		printf("%-15s: $%8.02f (%s)\n", username, ((float)bal)/100, flags);
+	}
+}
+
+int Dispense_AddUser(int Socket, const char *Username)
+{
+	char	*buf;
+	 int	responseCode, ret;
+	
+	// Check for a dry run
+	if( gbDryRun ) {
+		printf("Dry Run - No action\n");
+		return 0;
+	}
+	
+	sendf(Socket, "USER_ADD %s\n", Username);
+	
+	buf = ReadLine(Socket);
+	responseCode = atoi(buf);
+	
+	switch(responseCode)
+	{
+	case 200:
+		printf("User '%s' added\n", Username);
+		ret = 0;
+		break;
+		
+	case 403:
+		printf("Only wheel can add users\n");
+		ret = 1;
+		break;
+		
+	case 404:
+		printf("User '%s' already exists\n", Username);
+		ret = 0;
+		break;
+	
+	default:
+		fprintf(stderr, "Unknown response code %i '%s'\n", responseCode, buf);
+		ret = -1;
+		break;
+	}
+	
+	free(buf);
+	
+	return ret;
+}
+
+int Dispense_SetUserType(int Socket, const char *Username, const char *TypeString, const char *Reason)
+{
+	char	*buf;
+	 int	responseCode, ret;
+	
+	// Check for a dry run
+	if( gbDryRun ) {
+		printf("Dry Run - No action\n");
+		return 0;
+	}
+	
+	// TODO: Pre-validate the string
+	
+	sendf(Socket, "USER_FLAGS %s %s %s\n", Username, TypeString, Reason);
+	
+	buf = ReadLine(Socket);
+	responseCode = atoi(buf);
+	
+	switch(responseCode)
+	{
+	case 200:
+		printf("User '%s' updated\n", Username);
+		ret = 0;
+		break;
+		
+	case 403:
+		printf("Only dispense admins can modify users\n");
+		ret = RV_PERMISSIONS;
+		break;
+	
+	case 404:
+		printf("User '%s' does not exist\n", Username);
+		ret = RV_INVALID_USER;
+		break;
+	
+	case 407:
+		printf("Flag string is invalid\n");
+		ret = RV_ARGUMENTS;
+		break;
+	
+	default:
+		fprintf(stderr, "Unknown response code %i '%s'\n", responseCode, buf);
+		ret = RV_UNKNOWN_RESPONSE;
+		break;
+	}
+	
+	free(buf);
+	
+	return ret;
+}
+
+int Dispense_SetItem(int Socket, const char *Type, int ID, int NewPrice, const char *NewName)
+{
+	char	*buf;
+	 int	responseCode, ret;
+	
+	// Check for a dry run
+	if( gbDryRun ) {
+		printf("Dry Run - No action\n");
+		return 0;
+	}
+	
+	sendf(Socket, "UPDATE_ITEM %s:%i %i %s\n", Type, ID, NewPrice, NewName);
+	
+	buf = ReadLine(Socket);
+	responseCode = atoi(buf);
+	
+	switch(responseCode)
+	{
+	case 200:
+		printf("Item %s:%i updated\n", Type, ID);
+		ret = 0;
+		break;
+		
+	case 403:
+		printf("Only coke members can modify the slots\n");
+		ret = RV_PERMISSIONS;
+		break;
+	
+	case 406:
+		printf("Invalid item passed\n");
+		ret = RV_BAD_ITEM;
+		break;
+	
+	default:
+		fprintf(stderr, "Unknown response code %i '%s'\n", responseCode, buf);
+		ret = -1;
+		break;
+	}
+	
+	free(buf);
+	
+	return ret;
+}
+
+// ===
+// Helpers
+// ===
+char *ReadLine(int Socket)
+{
+	static char	buf[BUFSIZ];
+	static int	bufPos = 0;
+	static int	bufValid = 0;
+	 int	len;
+	char	*newline = NULL;
+	 int	retLen = 0;
+	char	*ret = malloc(10);
+	
+	#if DEBUG_TRACE_SERVER
+	printf("ReadLine: ");
+	fflush(stdout);
+	#endif
+	
+	ret[0] = '\0';
+	
+	while( !newline )
+	{
+		if( bufValid ) {
+			len = bufValid;
+		}
+		else {
+			len = recv(Socket, buf+bufPos, BUFSIZ-1-bufPos, 0);
+			if( len <= 0 ) {
+				free(ret);
+				return strdup("599 Client Connection Error\n");
+			}
+		}
+		buf[bufPos+len] = '\0';
+		
+		newline = strchr( buf+bufPos, '\n' );
+		if( newline ) {
+			*newline = '\0';
+		}
+		
+		retLen += strlen(buf+bufPos);
+		ret = realloc(ret, retLen + 1);
+		strcat( ret, buf+bufPos );
+		
+		if( newline ) {
+			 int	newLen = newline - (buf+bufPos) + 1;
+			bufValid = len - newLen;
+			len = newLen;
+		}
+		if( len + bufPos == BUFSIZ - 1 )	bufPos = 0;
+		else	bufPos += len;
+	}
+	
+	#if DEBUG_TRACE_SERVER
+	printf("%i '%s'\n", retLen, ret);
+	#endif
+	
+	return ret;
+}
+
+int sendf(int Socket, const char *Format, ...)
+{
+	va_list	args;
+	 int	len;
+	
+	va_start(args, Format);
+	len = vsnprintf(NULL, 0, Format, args);
+	va_end(args);
+	
+	{
+		char	buf[len+1];
+		va_start(args, Format);
+		vsnprintf(buf, len+1, Format, args);
+		va_end(args);
+		
+		#if DEBUG_TRACE_SERVER
+		printf("sendf: %s", buf);
+		#endif
+		
+		return send(Socket, buf, len, 0);
+	}
+}
+
-- 
GitLab