/* * 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)); // explicitely zero out fields // ctx->riders = NULL; ctx->freelist = NULL; ctx->activelist = NULL; ctx->seq = 0; ctx->mmap_broken = 0; ctx->passes_fd = 0; if (ctx->logical_card_id_hash) { memset(ctx->logical_card_id_hash, 0, sizeof(rider_node *)*STORED_PASS_HASH); } if (ctx->rider_mag_hash) { memset(ctx->rider_mag_hash, 0, sizeof(rider_node *)*STORED_PASS_HASH); } if (ctx->rider_rf_hash) { memset(ctx->rider_rf_hash, 0, sizeof(rider_node *)*STORED_PASS_HASH); } 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; } */