Browse Source

Proper CLI interface for iptraffic

* Wrapped in application object
 * Getting config file name from CLI args
 * Getting input and output filenams from CLI args
 * Reading and writing from STDIN & STDOUT
 * Send all non-data output to stderr
master
Jon Foster 5 months ago
parent
commit
d67dbc36c9
4 changed files with 424 additions and 89 deletions
  1. +6
    -3
      Makefile
  2. +74
    -0
      cli.cpp
  3. +179
    -0
      cli.h
  4. +165
    -86
      iptraffic.cpp

+ 6
- 3
Makefile View File

@@ -1,5 +1,5 @@
iptraffic: iptraffic.cpp strutil.o data.o config.o
g++ -o $@ $@.cpp strutil.o data.o config.o
iptraffic: iptraffic.cpp strutil.o data.o config.o cli.o
g++ -o $@ $@.cpp strutil.o data.o config.o cli.o

config.o: config.cpp config.h strutil.o data.o
g++ -c -o $@ config.cpp
@@ -7,6 +7,9 @@ config.o: config.cpp config.h strutil.o data.o
data.o: data.cpp data.h strutil.o
g++ -c -o $@ data.cpp

cli.o: cli.cpp cli.h
g++ -c -o $@ cli.cpp

strutil.o: strutil.cpp strutil.h
g++ -c -o $@ strutil.cpp

@@ -14,7 +17,7 @@ strutil.o: strutil.cpp strutil.h

.PHONY: run
run: iptraffic
./iptraffic 2> log # 2>&1 | head -n 20
./iptraffic





+ 74
- 0
cli.cpp View File

@@ -0,0 +1,74 @@
/*********************************************************************
* C++ Base Application Object
* Written by Jonathan A. Foster <jon@jfpossibilities.com
* Started August 31st, 2020
*********************************************************************/
//#include <ostream>
#include <iostream>
#include <cstdlib>
#include <sys/stat.h>
#include <sys/types.h>
#include "cli.h"



/*********************************************************************
* cBaseApp
*********************************************************************/
cBaseApp *baseapp = 0;



cBaseApp &cBaseApp::init(int argc, char **argv) {
command_argc = argc;
command_args = argv;
}



int cBaseApp::main() {
int i, ct;
char *p;
bool switches = true;

for(i=1; i<command_argc; i++) {
if(switches && *command_args[i]=='-') { // switches!
ct=0;
if(command_args[i][1]=='-') { // long switches
if(command_args[i][2]==0) { // --
switches=false;
continue;
}
p = command_args[i]+2;
ct = do_switch(p);

} else { // short switches
for(p=command_args[i]+1; *p; p++) {
if(ct) throw std::runtime_error(
"Only the last switch in stacked short switches can require an argument"
);
ct = do_switch(p);
}
p--;
}

// switch arguments
while(ct--) {
if(++i>=command_argc) throw std::runtime_error(
"Last switch requires more arguments"
);
do_switch_arg(p, command_args[i]);
}

} else // non-switch arguments
do_arg(command_args[i]);
}
return ExitCode;
}



int cBaseApp::crash(const std::exception &e) {
std::cerr << "Application crashed: " << e.what() << std::endl;
return 216; // just a weird number hopefully not conflicting with anything else.
}

+ 179
- 0
cli.h View File

@@ -0,0 +1,179 @@
//////////////////////////////////////////////////////////////////////
// C++ Base Application Object
// Written by Jonathan A. Foster <jon@jfpossibilities.com
// Started August 31st, 2020
//
// This is a base application object for traditional C/C++
// applications with a "int main(int argc, char//argv)" function.
//////////////////////////////////////////////////////////////////////
#ifndef __IDS_CLI_H__
#define __IDS_CLI_H__
#include <string>
#include <ostream>
#include <stdexcept>



//////////////////////////////////////////////////////////////////////
// Busy indicator aka. "Live Bug"
//
// This makes it stupid simple to step through a string that contains
// a single character per "frame". You can either call next() to get
// the next char in a sequence or use << with an ostream.
//////////////////////////////////////////////////////////////////////

struct LiveBug {
std::string seq;
int p;
LiveBug(): seq("|/-\\"), p(0) {}
inline char next() { if(p>=seq.size()) p=0; return seq[p++]; }
};
inline std::ostream &operator<<(std::ostream &o, LiveBug &bug) {
return o << bug.next();
}



//////////////////////////////////////////////////////////////////////
// Base Unix Application
//
// Its apparent to me tha all applications should be objects. Therefor
// running an app should consist of setting up an object from a class,
// running something on it. And then tearing it back down. With that
// in mind the intent of this class is to provide the boiler plate
// needed for this model and the bridge from the typical C/C++ main().
//
// How to use:
// 1. Create a sublcass of this class with your app's functionality.
// 2. Override do_switch(), do_switch_arg() and do_arg() to process
// arguments passed from the OS. See main() for the built-in
// processing.
// 3. Override main to provide functionality not triggered by args,
// including setup, teardown or even to replace the argument
// processing. Don't call the inherited method if you don't need
// the argument handling.
// 4. override crash() if you want to do something more than dump
// the exception string to cerr.
// 5. Tell the compiler about your class with the MAIN(class)
// macro.
//
// I'm calling this a "Unix" application because its what developed
// the current main() interface concept. This is also emulated /
// provided on other platforms, but it started in Unix.
//////////////////////////////////////////////////////////////////////
// globally accessible pointer to allow polymorphism
struct cBaseApp; extern cBaseApp *baseapp;
struct cBaseApp {
int command_argc; // main(argc ...)
char **command_args; // main(... argv)
int ExitCode; // App's code returned to OS.

/// STARTUP ///

//cBaseApp();
cBaseApp():
command_argc(0),
command_args(0),
ExitCode(0)
{ if(!baseapp) baseapp = this; } // Only the first gets registered


/// Initialize ///
//
// This is called, prior to this->main(), with the argc & args from the C
// main() entry point. Override this to perform any initialization here that
// can't be done before the CLI args are known.
//
virtual cBaseApp &init(int argc, char **argv);

/// argument handlers ///
//
// Override these routines to implement argument processing. These are char//
// instead of string since this is how they come to us from the OS.
//

// how many args needed for val
virtual unsigned do_switch(const char *arg) { return 0; }
// proccess a val for switch
virtual void do_switch_arg(const char *sw, const std::string &val) { }
// process a non-switch arg.
virtual void do_arg(const char *arg) { }
// TODO: switch lookup methods ("has switch?", "what's switch val?", ...)

/// new main() - loop through args ///
//
// This implementation runs through the arguments passed in using the usual
// GNU semantics. It calls do_switch() and do_arg(), depending on the
// presence of a leading "-", passing the current argument (without leading
// dash(es)). if do_switch() returns > 0 then that many arguments are passed
// to do_switch_val() with the corresponding switch that triggered the
// calls. Typically do_switch() would only return 0 or 1.
//
// Switches are defined as arguments starting with one or two dashes. A
// single "--" switch will terminate switch processing, sending all
// remaining args to do_arg(). Switches with names longer than one char must
// be preceded with two dashes ("--" long switches).
//
// A switch starting with a single dash is a short switch, consisting of a
// single character. More than one character can follow a dash in which case
// they will be treated as a series of single char switches
// (Ex: -abc = -a -b -c). Only the last one is allowed to have an argument
// (do_switch()>0).
//
// This should be overridden to provide any functionality not directly
// triggered by switches or args and its functionality can be replaced
// completely if you don't need arguments.
//

virtual int main();

/// Catch exceptions ///
//
// This is called by the boiler plate main() (see bottom) when an excpetion
// falls out. This is meant to handle std::exception descendents. You can
// catch other odd bals in main() above and deal with then accordingly. But
// good style dictates all exceptions should descend from std::exception.
// The return value is the application's exit code.
//

virtual int crash(const std::exception &e);

/// virtualize destructor ///

virtual ~cBaseApp() { }

};



//////////////////////////////////////////////////////////////////////
// An exception class to signal CLI errors
//////////////////////////////////////////////////////////////////////

struct CLIerror: public std::runtime_error {
CLIerror(const std::string &s): runtime_error(s) {}
};



//////////////////////////////////////////////////////////////////////
// Macro to bridge the C/C++ main() to the app class' main()
//
// This sets up a global static variable named "app" that contains
// your application object. It then copies the CLI args into it and
// calls main. An std::exception handler is setup to pass exceptions
// to the "app" object for further handling prior to exiting the app.
// The app's exit code is returned from app.main() or app.crash().
//////////////////////////////////////////////////////////////////////

#define MAIN(__APPCLASS__) \
__APPCLASS__ app; \
int main(int argc, char **args) { \
try { \
return app.init(argc, args).main();\
} catch(const std::exception &e) { \
return app.crash(e); \
} \
}

#endif

+ 165
- 86
iptraffic.cpp View File

@@ -13,11 +13,19 @@
// order. This is the usual way things get logged. ;-)
//
// 2021-05-14 <ChipMaster@YeOlPiShack.net>
// Dumbed down for C++ < 11.
// Dumbed down for < C++11.
// Split into modules
//
// 2021-06-18 <ChipMaster@YeOlPiShack.net>
// Wrapped in application object and added command line handling.
// This includes:
// - Getting config file name from CLI args
// - Getting input and output filenams from CLI args
// - Reading and writing from STDIN & STDOUT
// - Send all non-data output to stderr
//////////////////////////////////////////////////////////////////////

// TODO: map names according to time and host. time is probably automatic
// TODO: map names according to time and requesting host. time is probably automatic

#include <string.h>
#include <string>
@@ -26,6 +34,7 @@
#include <stdexcept>
#include <vector>
#include <map>
#include "cli.h"
#include "strutil.h"
#include "data.h"
#include "config.h"
@@ -34,105 +43,175 @@ using namespace std;


//////////////////////////////////////////////////////////////////////
// Busy indicator aka. "Live Bug"
// Application class to process files.
//////////////////////////////////////////////////////////////////////
//#define DEBUG

struct LiveBug {
string seq;
char pre;
int p;
LiveBug(): seq("-\\|/"), pre('\r'), p(0) {}
inline char next() { if(p>=seq.size()) p=0; return seq[p++]; }
};
ostream &operator<<(ostream &o, LiveBug &bug) {
return o << bug.pre << bug.next();
}
struct IPtraffic: public cBaseApp {
Config config;
StringList dns_ignore, dns_del;
NameVal rdns; // Reverse DNS lookup cache
istream *log;
ostream *out;
LiveBug bug;
int line_no;



//////////////////////////////////////////////////////////////////////
// Roll through file
//////////////////////////////////////////////////////////////////////
//#define DEBUG
IPtraffic(): out(&cout), log(0)
{ // I'd rather this initialization be static...
dns_ignore.push_back("v=spf1");
dns_ignore.push_back("https:");
dns_del.push_back("NODATA-");
dns_del.push_back("NXDOMAIN-");
}
~IPtraffic() { if(log && log!=&cin) delete(log); }



// (char *) to silence warnings...
const char *dns_ignore[] = { (char *)"v=spf1", (char *)"https:", 0 };
const char *dns_del[] = { (char *)"NODATA-", (char *)"NXDOMAIN-", 0 };
#define PATH "/srv/backups/iptraffic"
ifstream log(PATH "/test.log");
ofstream out(PATH "/processed.log");
Splits ln;
int lnno = 0, ict = 0;
LiveBug bug;
NameVal rdns;
NameVal::iterator nvp;
string name, address, s;
Conn conn;
bool match;
Config config;



void dlog(const string msg) {
void dlog(const string msg) {
#ifdef DEBUG
cerr << "\r" << lnno << ": " << msg << endl;
cerr << line_no << ": " << msg << endl;
#endif
}
}


// TODO: elaborate
void help() {
cerr <<
"\n"
"iptraffic -c {config file} [-o {output file}] [{input file} [...]]\n";
ExitCode = 1;
}
unsigned do_switch(const char *sw) {
if((sw[0]=='c' || sw[0]=='o') && sw[1]==0) return 1;
throw CLIerror("Unrecognized Switch");
}
void do_switch_arg(const char *sw, const std::string &val) {
switch(*sw) {
case 'c':
config.load(val);
if(!config.us.size()) throw CLIerror(
"The configuration files MUST contain an [us] section with "
"appropriate values"
);
break;
case 'o':
if(out!=&cout)
throw CLIerror("Output file has already been specified");
out = new ofstream(val.c_str()); // c_str(), really?!?!
}
}
void do_arg(const char *fname) {
if(log && log!=&cin) delete(log);
log = 0;
log = new ifstream(fname);
ExitCode = do_log();
}
// NOTE: the return values isn't really used yet but the channel is here if
// it can be of use.
int do_log() {
if(!config.us.size()) throw CLIerror(
"A configuration file must be specified before input files."
);

Splits ln;
int ict=0;
NameVal::iterator nvp;
string name, address, s;
Conn conn;
bool match;

/// parse log file ///

line_no=0;
while((*log >> ln)) {
line_no++;
cerr << bug << ' ' << line_no << '\r' << flush;

/// DNS query result ///

// TODO: need to get more specific on tying us + them + time to DNS
if(ln.count>8 && strncmp(ln.fields[4], "dnsmasq[", 8)==0) {
if(ln[5]=="reply") {
name = ln[6];
address = ln[8];
// Hmm... is this reply an address?
if(pre_match(dns_ignore, address)) continue; // nope
if(pre_match(dns_del, address)) continue; // does not exist reply
if((nvp=rdns.find(address))!=rdns.end()) {
if(nvp->second==name) continue;
dlog("WARN: DNS address overlap "+address+": "+nvp->second+" : "+name);
}
rdns[address] = name;
dlog("Added "+address+" = "+name);
continue;
}
}

int main(int argc, char **argv) {
/// process connections ///

if(ln.count>5
&& ln[4]=="kernel:"
&& ln[5]=="ACCEPT"
) {
conn = ln;
if(!pre_match(config.us, conn.us)) conn.swap();
if((nvp=rdns.find(conn.them))!=rdns.end())
conn.name = nvp->second;
if(config.ignores.find(conn)<0)
*out << ln[0] << " " << ln[1] << " " << ln[2] << " " << conn << "\n";
else
ict++;
}
}
*out << flush; // make sure all data gets written.
cerr << "\nIgnored: " << ict << endl;
cerr << "Total rDNS: " << rdns.size() << "\n";
return 0;
}

/// Load config ///

config.load(PATH "/iptraffic.conf");

/// parse log file ///
int main() {
try {
cBaseApp::main();
if(!log) {
// no inputs were specified run stdin.
log = &cin;
ExitCode = do_log();
}
} catch(const CLIerror &e) {
cerr << "ERROR: " << e.what() << "\n";
help();
}
return ExitCode;
}
};

while((log >> ln)) {
lnno++;
cout << bug << " " << lnno << flush;

/// DNS query result ///

// TODO: need to get more specific on tying us + them + time to DNS
if(ln.count>8 && strncmp(ln.fields[4], "dnsmasq[", 8)==0) {
if(ln[5]=="reply") {
name = ln[6];
address = ln[8];
// Hmm... is this reply an address?
if(pre_match(dns_ignore, address)) continue; // nope
if(pre_match(dns_del, address)) continue; // does not exist reply
if((nvp=rdns.find(address))!=rdns.end()) {
if(nvp->second==name) continue;
dlog("WARN: DNS address overlap "+address+": "+nvp->second+" : "+name);
}
rdns[address] = name;
dlog("Added "+address+" = "+name);
#ifdef DEBUG
cout << '\r' << lnno << ": " << name << endl;
#endif
continue;
}
}
//////////////////////////////////////////////////////////////////////
// Run it
//////////////////////////////////////////////////////////////////////

/// process connections ///

if(ln.count>5
&& ln[4]=="kernel:"
&& ln[5]=="ACCEPT"
) {
conn = ln;
if(!pre_match(config.us, conn.us)) conn.swap();
if((nvp=rdns.find(conn.them))!=rdns.end())
conn.name = nvp->second;
if(config.ignores.find(conn)<0)
out << ln[0] << " " << ln[1] << " " << ln[2] << " " << conn << "\n";
else
ict++;
}
}
cout << "\nIgnored: " << ict << endl;
cout << "Total rDNS: " << rdns.size() << "\n";
return 0;
}
MAIN(IPtraffic)

Loading…
Cancel
Save