/*
* Copyright (c) 2019 Clementine Computing LLC.
*
* This file is part of PopuFare.
*
* PopuFare is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PopuFare is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with PopuFare. If not, see .
*
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "../common/common_defs.h"
#include "passdb.h"
static void free_rider_node_list(rider_node *head)
{
rider_node *p = head;
rider_node *q;
while(p)
{
q = p;
p = p->next;
free(q);
}
}
#define FIND_IDX_IN_BUCKET(b, idx, p, q)\
{ \
p = b; \
q = NULL; \
\
while(p) \
{ \
if(p->idx == idx) \
{ \
break; \
} \
\
q = p; \
p = p->next; \
} \
} \
//---------------------------
#define ADD_TO_BUCKET(b, idx, p, q) \
{ \
p = (rider_node *) malloc( sizeof(rider_node) );\
\
if(p == NULL) return FAIL_MEM; \
\
p->next = NULL; \
p->idx = idx; \
\
if(q) \
{ \
q->next = p; \
} \
else \
{ \
b = p; \
} \
} \
//---------------------------
#define DEL_FROM_BUCKET(b, p, q) \
{ \
if(q) \
{ \
q->next = p->next; \
} \
else \
{ \
b = p->next; \
} \
\
free(p); \
} \
//---------------------------
int find_id_in_hash(passdb_context *ctx, logical_card_id_t id)
{
rider_node *p;
if(id <= 0)
return FAIL_PARAM;
p = ctx->logical_card_id_hash[id % STORED_PASS_HASH];
while(p)
{
if(ctx->riders[p->idx].id == id)
return p->idx;
p = p->next;
}
return WARN_NOTFOUND;
}
int add_to_id_hash(passdb_context *ctx, int idx)
{
rider_node *p, *q;
unsigned int bucket;
if(idx < 0) return FAIL_PARAM;
if(ctx->riders[idx].id == ID_INVALID) return FAIL_PARAM;
bucket = ctx->riders[idx].id % STORED_PASS_HASH;
FIND_IDX_IN_BUCKET( ctx->logical_card_id_hash[bucket], idx, p, q )
if(p)
{
return FAIL_DUPKEY; //already exists!
}
else
{
ADD_TO_BUCKET( ctx->logical_card_id_hash[bucket], idx, p, q );
return 0;
}
}
int delete_from_id_hash(passdb_context *ctx, int idx)
{
rider_node *p, *q;
unsigned int bucket;
if(idx < 0) return FAIL_PARAM;
bucket = ctx->riders[idx].id % STORED_PASS_HASH;
FIND_IDX_IN_BUCKET( ctx->logical_card_id_hash[bucket], idx, p, q)
if(p)
{
DEL_FROM_BUCKET( ctx->logical_card_id_hash[bucket], p, q )
return 0;
}
else
{
return WARN_NOTFOUND;
}
}
//##
int find_mag_in_hash(passdb_context *ctx, char *mag)
{
rider_node *p;
if(mag[0] == '\0') return FAIL_PARAM;
p = ctx->rider_mag_hash[stringhash(mag) % STORED_PASS_HASH];
while(p)
{
if(!strncmp(ctx->riders[p->idx].magstripe_value, mag, CREDENTIAL_LEN))
return p->idx;
p = p->next;
}
return WARN_NOTFOUND;
}
int add_to_mag_hash(passdb_context *ctx, int idx)
{
rider_node *p, *q;
unsigned int bucket;
if(idx < 0) return 0; //if we have a non index or a non-value, return silently
if(ctx->riders[idx].magstripe_value[0] == '\0') return 0;
bucket = stringhash(ctx->riders[idx].magstripe_value) % STORED_PASS_HASH;
FIND_IDX_IN_BUCKET( ctx->rider_mag_hash[bucket], idx, p, q )
#ifndef ALLOW_CREDENTIAL_COLLISIONS
//On allowing hash collisions among credentials see comment tagged **STUPID** later in this file.
if(p)
{
return FAIL_DUPKEY; //already exists!
}
else
#endif
{
ADD_TO_BUCKET( ctx->rider_mag_hash[bucket], idx, p, q );
return 0;
}
}
int delete_from_mag_hash(passdb_context *ctx, int idx)
{
rider_node *p, *q;
unsigned int bucket;
if(idx < 0) return FAIL_PARAM;
bucket = stringhash(ctx->riders[idx].magstripe_value) % STORED_PASS_HASH;
FIND_IDX_IN_BUCKET( ctx->rider_mag_hash[bucket], idx, p, q)
if(p)
{
DEL_FROM_BUCKET( ctx->rider_mag_hash[bucket], p, q )
return 0;
}
else
{
return WARN_NOTFOUND;
}
}
//##
int find_rf_in_hash(passdb_context *ctx, char *rfid)
{
rider_node *p;
if(rfid[0] == '\0') return WARN_NOTFOUND;
p = ctx->rider_rf_hash[stringhash(rfid)% STORED_PASS_HASH];
while(p)
{
if(!strncmp(ctx->riders[p->idx].rfid_value, rfid, CREDENTIAL_LEN))
return p->idx;
p = p->next;
}
return WARN_NOTFOUND;
}
int add_to_rf_hash(passdb_context *ctx, int idx)
{
rider_node *p, *q;
unsigned int bucket;
if(idx < 0) return 0; //if we have a non index or a non-value, return silently
if(ctx->riders[idx].rfid_value[0] == '\0') return 0;
bucket = stringhash(ctx->riders[idx].rfid_value) % STORED_PASS_HASH;
FIND_IDX_IN_BUCKET( ctx->rider_rf_hash[bucket], idx, p, q )
#ifndef ALLOW_CREDENTIAL_COLLISIONS
//On allowing hash collisions among credentials see comment tagged **STUPID** later in this file.
if(p)
{
return FAIL_DUPKEY; //already exists!
}
else
#endif
{
ADD_TO_BUCKET( ctx->rider_rf_hash[bucket], idx, p, q );
return 0;
}
}
int delete_from_rf_hash(passdb_context *ctx, int idx)
{
rider_node *p, *q;
unsigned int bucket;
if(idx < 0) return FAIL_PARAM;
bucket = stringhash(ctx->riders[idx].rfid_value) % STORED_PASS_HASH;
FIND_IDX_IN_BUCKET( ctx->rider_rf_hash[bucket], idx, p, q)
if(p)
{
DEL_FROM_BUCKET( ctx->rider_rf_hash[bucket], p, q )
return 0;
}
else
{
return WARN_NOTFOUND;
}
}
//##
int build_hashes(passdb_context *ctx)
{
rider_node *p = ctx->activelist;
int retval;
while(p)
{
retval = add_to_id_hash(ctx, p->idx);
if( !DB_OKAY(retval) )
{
fprintf(stderr, "Error (%d) indexing rider ID %llu at index %d!\n", retval, ctx->riders[p->idx].id, p->idx);
return retval;
}
retval = add_to_mag_hash(ctx, p->idx);
if( !DB_OKAY(retval) )
{
fprintf(stderr, "Error (%d) indexing magstripe %s at index %d!\n", retval, ctx->riders[p->idx].magstripe_value, p->idx);
return -1;
}
retval = add_to_rf_hash(ctx, p->idx);
if( !DB_OKAY(retval) )
{
fprintf(stderr, "Error (%d) indexing RFID %s at index %d!\n", retval, ctx->riders[p->idx].rfid_value, p->idx);
return -1;
}
p = p->next;
}
return 0;
}
int format_new_passdb()
{
char blank[MEMORY_PAGE_SIZE] = {0};
int i,n;
int fd;
fd = creat(PASSES_FILE, S_IRUSR | S_IWUSR);
if( fd < 0 )
{
fprintf(stderr, "Cannot create pass file '%s'!\n", PASSES_FILE);
return FAIL_DATABASE;
}
n = PASS_MAP_SIZE / MEMORY_PAGE_SIZE;
for(i = 0; i < n; i++)
{
if( write(fd, &blank, sizeof(blank)) != sizeof(blank) )
{
fprintf(stderr, "Cannot write blank data to passes file '%s'!\n", PASSES_FILE);
close(fd);
return FAIL_DATABASE;
}
}
close(fd);
return 0;
}
int detach_from_passdb(passdb_context *ctx)
{
int i;
if(!ctx)
return FAIL_PARAM;
free_rider_node_list(ctx->freelist);
free_rider_node_list(ctx->activelist);
for(i=0; i < STORED_PASS_HASH; i++)
{
free_rider_node_list(ctx->logical_card_id_hash[i]);
free_rider_node_list(ctx->rider_mag_hash[i]);
free_rider_node_list(ctx->rider_rf_hash[i]);
}
if(ctx->riders != NULL)
{
munmap(ctx->riders, PASS_MAP_SIZE);
}
if(ctx->mmap_broken)
{
close(ctx->passes_fd);
}
memset(ctx, 0, sizeof(passdb_context));
return 0;
}
int attach_to_passdb(passdb_context *ctx)
{
int n;
int retval;
struct stat st;
int fd;
rider_record *foo;
int mmap_broken;
seq_t maxseq = 0;
rider_node *freehead, *acthead, *q;
int numfree, numact;
int i;
//--------
if(!ctx) //fail if we get passed a null pointer
return FAIL_PARAM;
//We also want to fail if we get passed a pointer to an active/in-use context...
if(ctx->riders || ctx->activelist || ctx->freelist)
{
return FAIL_PARAM;
}
mmap_broken = 0;
//Go and stat the pass database file
retval = stat(PASSES_FILE, &st);
if(retval)
{
fprintf(stderr, "Cannot find passes file!\n");
return FAIL_DATABASE;
}
//Make sure it is the right size...
n = (st.st_size / sizeof(rider_record));
if(n != NUM_STORED_PASSES)
{
fprintf(stderr, "Passes file contains %d records, expecting %d!\n", n, NUM_STORED_PASSES);
return FAIL_DATABASE;
}
//open the file
fd = open(PASSES_FILE, O_RDWR | O_SYNC);
if(fd < 0)
{
fprintf(stderr, "Cannot open passes file '%s'!\n", PASSES_FILE);
return FAIL_DATABASE;
}
//mmap() the file into a pointer in our address space
foo = (rider_record *) mmap(NULL, PASS_MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if( (foo == NULL) || (foo == MAP_FAILED) ) //if the MAP_SHARED option fails...
{
//try again with MAP_PRIVATE and see if it works...
foo = (rider_record *) mmap(NULL, PASS_MAP_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if( (foo == NULL) || (foo == MAP_FAILED) )
{
close(fd);
fprintf(stderr, "Cannot mmap passes file! Try checking sysctl settings kernel.shmall and kernel.shmmax (return == %p errno == %d)\n",foo,errno);
return FAIL_MEM;
}
else
{
//set our mmap broken flag
printf("mmap seems to be broken... operating in braindead file IO mode...\n");
mmap_broken = 1;
}
}
else
{
//close the file (we no longer need it open once it is mmap()'d)
close(fd);
}
//------
freehead = acthead = q = NULL;
numfree = numact = 0;
maxseq = 0;
//For all records in our flat file
for(i=0; i < n; i++)
{
//check the sequence number and update our "latest" tally if it is newer.
if(foo[i].seq > maxseq)
{
maxseq = foo[i].seq;
}
//if the record is not in use
if(foo[i].id == ID_INVALID)
{
//add it to the freelist
q = (rider_node *) malloc( sizeof(rider_node) );
if(!q)
{
free_rider_node_list(freehead);
free_rider_node_list(acthead);
fprintf(stderr, "Malloc returned NULL loading riders!\n");
munmap(foo, n * sizeof(rider_record));
return FAIL_MEM;
}
else
{
numfree++;
q->next = freehead;
q->idx = i;
freehead = q;
}
}
else
{
//add it to the active list
q = (rider_node *) malloc( sizeof(rider_node) );
if(!q)
{
free_rider_node_list(freehead);
free_rider_node_list(acthead);
fprintf(stderr, "Malloc returned NULL loading riders!\n");
munmap(foo, n * sizeof(rider_record));
return FAIL_MEM;
}
else
{
numact++;
q->next = acthead;
q->idx = i;
acthead = q;
}
}
}
ctx->riders = foo;
ctx->freelist = freehead;
ctx->activelist = acthead;
ctx->seq = maxseq;
retval = build_hashes(ctx);
if( DB_FAIL(retval) )
{
fprintf(stderr, "Building hashes failed.\n");
detach_from_passdb(ctx);
return retval;
}
if(mmap_broken)
{
ctx->mmap_broken = 1;
ctx->passes_fd = fd;
}
else
{
ctx->mmap_broken = 0;
ctx->passes_fd = 0;
}
printf("Loaded and indexed %d records (%d used, %d free); Newest seq = %llu\n", n, numact, numfree, maxseq);
return n;
}
static int copy_rider(rider_record *dst, rider_record *src)
{
if(! (src && dst) )
return FAIL_PARAM;
memcpy(dst, src, sizeof(rider_record));
dst->rfid_value[CREDENTIAL_LEN - 1] = '\0';
dst->magstripe_value[CREDENTIAL_LEN - 1] = '\0';
dst->rule_name[RULENAME_LEN - 1] = '\0';
dst->rule_param[PARAM_LEN - 1] = '\0';
return 0;
}
static int alloc_rider(passdb_context *ctx)
{
rider_node *p;
p = ctx->freelist;
if(p)
{
ctx->freelist = ctx->freelist->next;
p->next = ctx->activelist;
ctx->activelist = p;
return p->idx;
}
else
{
return FAIL_FULL;
}
}
static int free_rider(passdb_context *ctx, int idx)
{
rider_node *p, *q;
q = NULL;
p = ctx->activelist;
while(p)
{
if( p->idx == idx )
break;
q = p;
p = p->next;
}
if(p)
{
if(q)
{
q->next = p->next;
}
else
{
ctx->activelist = p->next;
}
p->next = ctx->freelist;
ctx->freelist = p;
memset(&ctx->riders[idx], 0, sizeof(rider_record));
ctx->riders[idx].id = ID_INVALID;
return 0;
}
else
{
return WARN_NOTFOUND;
}
}
static void sync_rider_change(passdb_context *ctx, int idx)
{
int offset;
int retval;
if(idx < 0)
return;
if(idx >= NUM_STORED_PASSES)
return;
if(!ctx)
return;
if(ctx->mmap_broken)
{
offset = (idx * sizeof(rider_record)) / MEMORY_PAGE_SIZE; //calculate the beginning page number
offset *= MEMORY_PAGE_SIZE; //multiply by page size
retval = lseek(ctx->passes_fd, offset, SEEK_SET);
if(retval != offset)
{
fprintf(stderr, "lseek() failed in sync_rider_change(). errno = %d\n", errno);
return;
}
retval = write(ctx->passes_fd, ((void *)ctx->riders) + offset, MEMORY_PAGE_SIZE);
if(retval != MEMORY_PAGE_SIZE)
{
fprintf(stderr, "write() failed in sync_rider_change(). errno = %d\n", errno);
return;
}
}
else
{
retval = msync(ctx->riders, PASS_MAP_SIZE, MS_SYNC | MS_INVALIDATE);
if(retval < 0)
{
fprintf(stderr, "msync() failed in sync_rider_change(). errno = %d\n", errno);
return;
}
}
}
void sync_all_riders(passdb_context *ctx)
{
int retval;
if(!ctx)
return;
if(ctx->mmap_broken)
{
retval = lseek(ctx->passes_fd, 0, SEEK_SET);
if(retval != 0)
{
fprintf(stderr, "lseek() failed in sync_all_riders(). errno = %d\n", errno);
return;
}
retval = write(ctx->passes_fd, ctx->riders, PASS_MAP_SIZE);
if(retval != PASS_MAP_SIZE)
{
fprintf(stderr, "write() failed in sync_all_riders(). errno = %d\n", errno);
return;
}
}
else
{
retval = msync(ctx->riders, PASS_MAP_SIZE, MS_SYNC | MS_INVALIDATE);
if(retval < 0)
{
fprintf(stderr, "msync() failed in sync_all_riders(). errno = %d\n", errno);
return;
}
}
}
int delete_rider(passdb_context *ctx, rider_record *rec, int sync)
{
int id_idx;
if(!ctx)
{
return FAIL_PARAM;
}
if(!ctx->riders)
{
return FAIL_PARAM;
}
//If this record is older than out current database, ignore it as a duplicate.
if( ctx->seq >= rec->seq )
{
return 0;
}
//find the record to be deleted in our ID hash
id_idx = find_id_in_hash(ctx, rec->id);
//If we didn't find it, it must have already been deleted...
if(id_idx < 0)
{
return 0;
}
//delete it from all hashes
delete_from_id_hash(ctx, id_idx);
delete_from_mag_hash(ctx, id_idx);
delete_from_rf_hash(ctx, id_idx);
//free the record (this zeros out the entire block)
free_rider(ctx, id_idx);
//populate the seq number of this delete
ctx->riders[id_idx].seq = rec->seq;
//and sync our SHM
if(sync)
{
sync_rider_change(ctx, id_idx);
}
return 1;
}
int update_rider(passdb_context *ctx, rider_record *rec, int sync)
{
int id_idx;
int mag_idx;
int rf_idx;
int update_credentials = 0;
int update_id_hash = 0;
int retval;
if(!ctx)
{
return FAIL_MEM;
}
if(!ctx->riders)
{
return FAIL_MEM;
}
//If this record is older than out current database, ignore it as a duplicate.
if( ctx->seq >= rec->seq )
{
return 0;
}
id_idx = find_id_in_hash(ctx, rec->id);
mag_idx = find_mag_in_hash(ctx, rec->magstripe_value);
rf_idx = find_rf_in_hash(ctx, rec->rfid_value);
if ((mag_idx < 0) || (rf_idx < 0)) {
//pass
}
// We want to allow a short period of magstrip or RFID collision as the lesser of two evils vs. **STUPID**
// possibly losing a record due to a degenerately stupid administrator doing the following:
//
// 1) Create user 1 with magstripe '1:foo'
// 2) Delete user 1
// 3) Create user 2 with magstripe '1:foo'
// 4) Create user 1 with magstripe '1:bar' <---- THIS IS NOT ALLOWED (Creating user 1 is dissalowed after a delete of user 1. This card should be created as user 3).
//
// The issue here is that if the bus asks what's happensed since sequence number 1, it will get rows
// 3 and 4.
//
// In reality, we'd hope that each bus would complete a sync at least once on a shorter interval
// than the frequency at which credentials are recycled, but you never know... And if somebody manually
// fucks things up such that a user id (card id) is deleted, and then created again (this is a big no-no), we
// can recover by allowing a hash collision to exist in the meantime.
#ifndef ALLOW_CREDENTIAL_COLLISIONS
if( (mag_idx >= 0) && (mag_idx != id_idx) )
{
fprintf(stderr, "Refusing to accept change that would introduce duplicate magstripe \"%s\" for records %llu and %llu.\n", rec->magstripe_value, ctx->riders[mag_idx].id, rec->id);
return FAIL_DUPKEY;
}
if( (rf_idx >= 0) && (rf_idx != id_idx) )
{
fprintf(stderr, "Refusing to accept change that would introduce duplicate RFID \"%s\" for records %llu and %llu.\n", rec->rfid_value, ctx->riders[rf_idx].id, rec->id);
return FAIL_DUPKEY;
}
#endif
if(id_idx >= 0) //if rec->id already exists, we're updating an existing record...
{
//if EITHER the RFID or MAGSTRIPE values have changed...
if( strncmp(ctx->riders[id_idx].magstripe_value, rec->magstripe_value, CREDENTIAL_LEN) || strncmp(ctx->riders[id_idx].rfid_value, rec->rfid_value, CREDENTIAL_LEN) )
{
update_credentials = 1;
}
}
else //otherwise, we're creating a new record...
{
id_idx = alloc_rider(ctx);
if(DB_FAIL(id_idx))
{
fprintf(stderr, "Error (%d) trying to allocate rider\n", id_idx);
return id_idx;
}
update_credentials = update_id_hash = 1;
}
if(update_credentials)
{
delete_from_mag_hash(ctx, id_idx);
delete_from_rf_hash(ctx, id_idx);
}
if(update_id_hash)
{
delete_from_id_hash(ctx, id_idx);
}
copy_rider( &ctx->riders[id_idx], rec );
if(sync)
{
sync_rider_change(ctx, id_idx);
}
if(update_id_hash)
{
retval = add_to_id_hash(ctx, id_idx);
if( !DB_OKAY(retval) )
{
fprintf(stderr, "Error (%d) updating id hash for record seq = %llu\n", retval, (unsigned long long)rec->seq);
}
}
if(update_credentials)
{
retval = add_to_mag_hash(ctx, id_idx);
if( !DB_OKAY(retval) )
{
fprintf(stderr, "Error (%d) updating magstripe hash for record seq = %llu\n", retval, (unsigned long long)rec->seq);
}
retval = add_to_rf_hash(ctx, id_idx);
if( !DB_OKAY(retval) )
{
fprintf(stderr, "Error (%d) updating rf hash for record seq = %llu\n", retval, (unsigned long long)rec->seq);
}
}
ctx->seq = rec->seq;
return 1;
}
void dump_hashes(passdb_context *ctx)
{
int i;
rider_node *p;
if(!ctx)
{
printf("NULL Context!\n");
return;
}
if(!ctx->riders)
{
printf("NULL Riders, no database mmap()'d!\n");
return;
}
printf("ID HASH:\n");
for(i=0; i < STORED_PASS_HASH; i++)
{
if(!ctx->logical_card_id_hash[i]) continue;
printf("\t%d:", i);
p = ctx->logical_card_id_hash[i];
while(p)
{
printf("\t[%d] %llu", p->idx, ctx->riders[p->idx].id);
p = p -> next;
}
printf("\n");
}
printf("RFID HASH:\n");
for(i=0; i < STORED_PASS_HASH; i++)
{
if(!ctx->rider_rf_hash[i]) continue;
printf("\t%d:", i);
p = ctx->rider_rf_hash[i];
while(p)
{
printf("\t[%d] \"%s\"", p->idx, ctx->riders[p->idx].rfid_value);
p = p -> next;
}
printf("\n");
}
printf("MAGSTRIPE HASH:\n");
for(i=0; i < STORED_PASS_HASH; i++)
{
if(!ctx->rider_mag_hash[i]) continue;
printf("\t%d:", i);
p = ctx->rider_mag_hash[i];
while(p)
{
printf("\t[%d] \"%s\"", p->idx, ctx->riders[p->idx].magstripe_value);
p = p -> next;
}
printf("\n");
}
};
/*
int main(int argc, char **argv)
{
passdb_context ctx = {0};
int retval;
rider_record foo = {12362, 6, "SILLYRF", "FOOBAR", "STUPIDRULE", "STUPIDPARAM"};
rider_record bar = {12354, 7, "WAWAWA", "SILLYMAG", "STUPIDRULE", "STUPIDPARAM"};
rider_record baz = {12361, 8, "", "BARFOLA_MAG", "STUPIDRULE2", "STUPIDPARAM2"};
rider_record bat = {12363, 7, "", "", "", ""};
retval = attach_to_passdb(&ctx);
dump_hashes(&ctx);
retval = update_rider(&ctx, &foo);
retval = update_rider(&ctx, &bar);
retval = update_rider(&ctx, &baz);
retval = delete_rider(&ctx, &bat);
dump_hashes(&ctx);
return 0;
}
*/