#!/usr/bin/python ################################################################################ # ____ _ _ # # | _ \ _ __ ___ | |_ ___ ___ ___ | | # # | |_) | '__/ _ \| __/ _ \ / __/ _ \| | # # | __/| | | (_) | || (_) | (_| (_) | | # # |_| |_| \___/ \__\___/ \___\___/|_| # # # # == A Simple ASCII Header Generator for Network Protocols == # # # ################################################################################ # # # Written by: # # # # Luis MartinGarcia. # # -> E-Mail: luis.mgarc@gmail.com # # -> WWWW: http://www.luismg.com # # -> GitHub: https://github.com/luismartingarcia # # # ################################################################################ # # # This file is part of Protocol. # # # # Copyright (C) 2014 Luis MartinGarcia (luis.mgarc@gmail.com) # # # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation, either version 3 of the License, or # # (at your option) any later version. # # # # This program 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 General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program. If not, see . # # # # Please check file LICENSE.txt for the complete version of the license, # # as this disclaimer does not contain the full information. Also, note # # that although Protocol is licensed under the GNU GPL v3 license, it may # # be possible to obtain copies of it under different, less restrictive, # # alternative licenses. Requests will be studied on a case by case basis. # # If you wish to obtain Protocol under a different license, please contact # # the email address mentioned above. # # # ################################################################################ # # # Description: # # # # Protocol is a command-line tool that provides quick access to the most # # common network protocol headers in ASCII (RFC-like) format. It also has the # # ability to create ASCII headers for custom protocols defined by the user # # through a very simple syntax. # # # ################################################################################ # STANDARD LIBRARY IMPORTS import sys from datetime import date # INTERNAL IMPORTS from constants import * import specs # CLASS DEFINITIONS class ProtocolException(Exception): """ This class represents exceptions raised by the Protocol class """ def __init__(self, errmsg): self.errmsg=errmsg def __str__(self): return str(self.errmsg) class Protocol(): """ This class represents a network protocol header. Objects are constructed by passing a textual protocol specification. Once that is done, instances can be printed by converting them to a str type. """ def __init__(self, spec): """ Class constructor. @param spec is the textual specification that describes the protocol. """ self.hdr_char_start="+" # Character for start of the border line self.hdr_char_end="+" # Character for end of the border line self.hdr_char_fill_odd="+" # Fill character for border odd positions self.hdr_char_fill_even="-" # Fill character for border even positions self.hdr_char_sep="|" # Field separator character self.bits_per_line=32 # Number of bits per line self.do_print_top_tens=True # True: print top numbers for bit tens self.do_print_top_units=True # True: print top numbers for bit units self.field_list=[] # Header fields to be printed out self.parse_spec(spec) # Parse the received spec and populate self.field_list def parse_spec(self, spec): """ Parses a textual protocol spec and stores the relevant internal state so such spec can be later converted to a nice ASCII diagram. @return the list of protocol fields, as a dictionary containing keys 'len' and 'text'. The list is returned for completeness but no caller is expected to store or use such list. @raise ProtocolException in case the supplied spec is not valid """ if "?" in spec: parts=spec.split("?") fields=parts[0] opts=parts[1] if spec.count("?")>1: raise ProtocolException("FATAL: Character '?' may only be used as an option separator.") else: fields=spec opts=None # Parse field spec items=fields.split(",") for item in items: try: text, bits = item.split(":") bits=int(bits) if bits<=0: raise ProtocolException("FATAL: Fields must be at least one bit long (%s)" %spec) except ProtocolException: raise except: raise ProtocolException("FATAL: Invalid field_list specification (%s)" %spec) self.field_list.append({"text":text, "len":bits}) # Parse options if opts is not None: opts=opts.split(",") for opt in opts: try: var, value = opt.split("=") if var.lower()=="bits": self.bits_per_line=int(value) if self.bits_per_line<=0: raise ProtocolException("FATAL: Invalid value for 'bits' option (%s)" % value) elif var.lower()=="numbers": if value.lower() in ["0", "n", "no", "none", "false"]: self.do_print_top_tens=False self.do_print_top_units=False elif value.lower() in ["1", "y", "yes", "none", "true"]: self.do_print_top_tens=True self.do_print_top_units=True else: raise ProtocolException("FATAL: Invalid value for 'numbers' option (%s)" % value) elif var.lower() in ["oddchar", "evenchar", "startchar", "endchar", "sepchar"]: if len(value)>1 or len(value)<=0: raise ProtocolException("FATAL: Invalid value for '%s' option (%s)" % (var, value)) else: if var.lower()=="oddchar": self.hdr_char_fill_odd=value elif var.lower()=="evenchar": self.hdr_char_fill_even=value elif var.lower()=="startchar": self.hdr_char_start=value elif var.lower()=="endchar": self.hdr_char_end=value elif var.lower()=="sepchar": self.hdr_char_sep=value except ProtocolException: raise except: raise ProtocolException("FATAL: Invalid options specification (%s)" % opt) return self.field_list def _get_top_numbers(self): """ @return a string representing the bit units and bit tens on top of the protocol header. Note that a proper string is only returned if one or both self.do_print_top_tens and self.do_print_top_units is True. The returned string is not \n terminated, but it may contain a newline character in the middle. """ lines=["", ""] if self.do_print_top_tens is True: for i in range(0, self.bits_per_line): if str(i)[-1:]=="0": lines[0]+=" %s" % str(i)[0] else: lines[0]+=" " lines[0]+="\n" if self.do_print_top_units is True: for i in range(0, self.bits_per_line): lines[1]+=" %s" % str(i)[-1:] #lines[1]+="\n" result = "".join(lines) return result if len(result)>0 else None def _get_horizontal(self, width=None): """ @return the horizontal border line that separates field rows. @param width controls how many field bits the line should cover. By default, if no width is supplied, the line covers the hole length of the header. """ if width is None: width=self.bits_per_line if width<=0: return "" else: a="%s" % self.hdr_char_start b=(self.hdr_char_fill_even+self.hdr_char_fill_odd)*(width-1) c="%s%s" % (self.hdr_char_fill_even, self.hdr_char_end) return a+b+c def _get_separator(self, line_end=""): """ @return a string containing a protocol field separator. Returned string is a single character and matches whatever is stored in self.hdr_char_sep """ return self.hdr_char_sep def _process_field_list(self): """ Processes the list of protocol fields that we got from the spec and turns it into something that we can print easily (useful for cases when we have protocol fields that span more than one line). This is just a helper function to make __str__()'s life easier. """ new_fields=[] bits_in_line=0 i=0 while i < len(self.field_list): # Extract all the info we need about the field field=self.field_list[i] field_text= field['text'] field_len= field['len'] field['MF']=False available_in_line = self.bits_per_line - bits_in_line # If we have enough space on this line to include the current field # then just keep it as it is. if available_in_line >= field_len: new_fields.append(field) bits_in_line+=field_len i+=1 if bits_in_line==self.bits_per_line: bits_in_line=0 # Otherwise, split the field into two parts, one blank and one with # the actual field text else: # Case 1: We have a field that is perfectly aligned and it # has a length that is multiple of our line length if bits_in_line==0 and field_len%self.bits_per_line==0: new_fields.append(field) i+=1 bits_in_line=0 # Case 2: We weren't that lucky and the field is either not # aligned or we can't print it using an exact number of full # lines else: # If we have more space in the current line than in the next, # then put the field text in this one if available_in_line >= field_len-available_in_line: new_field = {'text':field_text, 'len':available_in_line, "MF":True} new_fields.append(new_field) field['text']="" field['len']=field_len-available_in_line field['MF']=False else: new_field = {'text':"", 'len':available_in_line, "MF":True} new_fields.append(new_field) field['text']=field_text field['len']=field_len-available_in_line field['MF']=False bits_in_line=0 continue return new_fields # Convert to string def __str__(self): """ Converts the protocol specification stored in the object to a nice ASCII diagram like the ones that appear in RFCs. Conversion supports fields of any length, and supports field that span more than one line in the diagram. @return a string containing the ASCII representation of the protocol header. """ # First of all, process our field list. This does some magic to make # the algorithm work for fields that span more than one line proto_fields = self._process_field_list() lines=[] numbers=self._get_top_numbers() if numbers is not None: lines.append(numbers) lines.append(self._get_horizontal()) # Print all protocol fields bits_in_line=0 current_line="" fields_done=0 p=-1 while p < len(proto_fields)-1: p+=1 # Extract all the info we need about the field field = proto_fields[p] field_text= field['text'] field_len= field['len'] field_mf = field['MF'] is True # Field has more fragments # If the field text is too long, we truncate it, and add a dot # at the end. if len(field_text) > (field_len*2)-1: field_text=field_text[0:(field_len*2)-1] if len(field_text)>1: field_text=field_text[0:-1]+"." # If we have space for the whole field in the current line, go # ahead and add it if self.bits_per_line-bits_in_line >= field_len: # If this is the first thing we print on a line, add the # starting character if bits_in_line==0: current_line+=self._get_separator() # Add the whole field current_line+=str.center(field_text, (field_len*2)-1) # Update counters bits_in_line+=field_len fields_done+=1 # If this is the last character in the line, store the line if bits_in_line==self.bits_per_line: current_line+=self._get_separator() lines.append(current_line) current_line="" bits_in_line=0 # When we have a fragmented field, we may need to suppress # the floor of the field, so the current line connects # with the one that follows. E.g: # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ # | field16 | | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + # | field | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ if field_mf is True: if proto_fields[p+1]['len'] > self.bits_per_line - field_len: # Print some +-+-+ to cover the previous field line_left=self._get_horizontal(self.bits_per_line - field_len) if len(line_left)==0: line_left=self.hdr_char_start # Now print some empty space to cover the part that # we can join with the field below. # Case 1: If the next field reaches the end of its # line, then we need to print whitespace until the # end our line if proto_fields[p+1]['len'] >= self.bits_per_line: line_center=" "* ((2*(field_len)-1)) line_right=self.hdr_char_end # Case 2: the field in the next row is not big enough # to cover all the space we'd like to join, so we # just print whitespace to cover as much as we can else: line_center=" "* ((2*((proto_fields[p+1]['len']-(self.bits_per_line-field_len))))-1) line_right=self._get_horizontal(self.bits_per_line-proto_fields[p+1]['len']) lines.append(line_left+line_center+line_right) else: lines.append(self._get_horizontal()) else: lines.append(self._get_horizontal()) # If this is not the last character of the line but we have no # more fields to print, wrap up elif fields_done==len(proto_fields): current_line+=self._get_separator() lines.append(current_line) lines.append(self._get_horizontal(bits_in_line)) else: # Add the separator character current_line+=self.hdr_char_sep # We don't have enough space for the field on this line. else: # Case 1: We are at the beginning of a new line and we need to # span more than one line if bits_in_line==0: # Case 1a: We have a multiple of the number of bits per line if field_len%self.bits_per_line==0: # Compute how many lines in total we need to print for this # big field. lines_to_print = int(((field_len/self.bits_per_line)*2)-1) # We print the field text in the central line central_line=int(lines_to_print/2) # Print all those lines for i in range(0, lines_to_print): # Let's figure out which character we need to use # to start and end the current line if i%2==1: start_line=self.hdr_char_start end_line=self.hdr_char_end else: start_line=self.hdr_char_sep end_line=self.hdr_char_sep # This is the line where we need to print the field # text. if i == central_line: lines.append(start_line + str.center(field_text, (self.bits_per_line*2)-1) + end_line) # This is a line we need to leave blank else: lines.append(start_line + (" " * ((self.bits_per_line*2)-1)) + end_line) # If we just added the last line, add a horizontal separator if i==lines_to_print-1: lines.append(self._get_horizontal()) # Case 2: We are not at the beginning of the line and we need # to print something that does not fit in the current line else: # This should never happen, since our _process_field_list() # divides fields in chunks so we never have the case of # something spanning lines in a weird manner assert(False) result= "\n".join(lines) return result class Main(): """ This class does all the boring task of a command-line application. It parses user input, displays usage, parses input files, etc. """ def __init__(self): """ Class constructor. Nothing fancy. """ self.cmd_line_args=None # Copy of the user argv self.protocols=[] # List of protocols to print out self.bits_per_line=None # Number of bits per line to print self.skip_numbers=None # True to avoid printing bit units and tens self.hdr_char_start=None # Character for start of the border line self.hdr_char_end=None # Character for end of the border line self.hdr_char_fill_odd=None # Fill character for border odd positions self.hdr_char_fill_even=None # Fill character for border even positions self.hdr_char_sep=None # Field separator character def display_help(self): """ Displays command-line usage help to standard output. """ print("") print("%s v%s" % (APPLICATION_NAME, APPLICATION_VERSION)) print("Copyright (C) %i, %s (%s)." % (max(2014, date.today().year), APPLICATION_AUTHOR, APPLICATION_AUTHOR_EMAIL)) print("This software comes with ABSOLUTELY NO WARRANTY.") print("") self.display_usage() print("PARAMETERS:") print(" : Name of an existing protocol") print(" : Field by field specification of non-existing protocol") print("OPTIONS:") print(" -b, --bits : Number of bits per line") print(" -f, --file : Read specs from a text file") print(" -h, --help : Displays this help information") print(" -n, --no-numbers : Do not print bit numbers on top of the header") print(" -V, --version : Displays current version") print(" --evenchar : Character for the even positions of horizontal table borders") print(" --oddchar : Character for the odd positions of horizontal table borders") print(" --startchar : Character that starts horizontal table borders") print(" --endchar : Character that ends horizontal table borders") print(" --sepchar : Character that separates protocol fields") def get_usage(self): """ @return a string containing application usage information """ return "Usage: %s { or } [OPTIONS]" % self.cmd_line_args[0] def display_usage(self): """ Prints usage information to standard output """ print(self.get_usage()) def parse_config_file(self, filename): """ This method parses the supplied configuration file and adds any protocols to our list of protocols to print. @return The number of protocols parsed """ i=0 # Read the contents of the whole file try: f = open(filename) lines = f.readlines() f.close() except: print("Error while reading file %s. Please make sure it exists and it's readable." % filename) sys.exit(1) # Parse protocol specs, line by line for line in lines: # Sanitize the line line=line.strip() # If it starts with #, or is an empty line ignore it if line.startswith("#") or len(line)==0: continue # If we have something else, treat it as a protocol spec proto=Protocol(line) self.protocols.append(proto) i+=1 return i def parse_cmd_line_args(self, argv, is_config_file=False): """ Parses command-line arguments and stores any relevant information internally """ # Store a reference to command line args for later use. if is_config_file==False: self.cmd_line_args = argv # Check we have received enough command-line parameters if len(argv) == 1 and is_config_file is False: print(self.get_usage()) sys.exit(1) else: skip_arg=False for i in range(1, len(argv)): # Useful for args like -c . This avoids parsing the # filename as it if was a command-line flag. if skip_arg==True: skip_arg=False continue # Spec file if argv[i]=="-f" or argv[i]=="--file": # Make sure we have an actual parameter after the flag if (i+1)>=len(argv): return OP_FAILURE, "Expected parameter after %s\n%s" % (argv[i], self.get_usage()) skip_arg=True # Parse the config file protos = self.parse_config_file(argv[i+1]) if protos <= 0: return OP_FAILURE, "No protocol specifications found in the supplied file (%s)" % argv[i+1] # Bits per line elif argv[i]=="-b" or argv[i]=="--bits": # Make sure we have an actual parameter after the flag if (i+1)>=len(argv): return OP_FAILURE, "Expected parameter after %s\n%s" % (argv[i], self.get_usage()) skip_arg=True # Parse the config file try: self.bits_per_line=int(argv[i+1]) if self.bits_per_line<=0: return OP_FAILURE, "Invalid number of bits per line supplied (%s)" % argv[i+1] except: return OP_FAILURE, "Invalid number of bits per line supplied (%s)" % argv[i+1] # Avoid displaying numbers on top of the header elif argv[i]=="-n" or argv[i]=="--no-numbers": self.skip_numbers=True # Character variations elif argv[i] in ["--oddchar", "--evenchar", "--startchar", "--endchar", "--sepchar"]: # Make sure we have an actual parameter after the flag if (i+1)>=len(argv): return OP_FAILURE, "Expected parameter after %s\n%s" % (argv[i], self.get_usage()) skip_arg=True # Make sure we got a single character, not more if len(argv[i+1])!=1: return OP_FAILURE, "A single character is expected after %s\n%s" % (argv[i], self.get_usage()) # Now let's store whatever character spec we got if argv[i]=="--oddchar": self.hdr_char_fill_odd=argv[i+1] elif argv[i]=="--evenchar": self.hdr_char_fill_even=argv[i+1] elif argv[i]=="--startchar": self.hdr_char_start=argv[i+1] elif argv[i]=="--endchar": self.hdr_char_end=argv[i+1] elif argv[i]=="--sepchar": self.hdr_char_sep=argv[i+1] # Display help elif argv[i]=="-h" or argv[i]=="--help": self.display_help() sys.exit(0) # Display version elif argv[i]=="-V" or argv[i]=="--version": print("%s v%s" % (APPLICATION_NAME, APPLICATION_VERSION)) sys.exit(0) # Incorrect option supplied elif argv[i].startswith("-"): print("ERROR: Invalid option supplied (%s)" % argv[i]) sys.exit(1) # Protocol name or protocol spec else: # If it contains ":" characters, we have a protocol spec if argv[i].count(":")>0: spec = argv[i] # Otherwise, the user meant to display an existing protocol else: # If we got an exact match, end of story if argv[i] in specs.protocols: spec = specs.protocols[argv[i]] # Otherwise, we may have received a partial match so # we need to figure out which protocol the user meant. # If the specification is ambiguous, we will error else: start_with_the_same=[] for spec in specs.protocols: if spec.startswith(argv[i]): start_with_the_same.append(spec) # If we only have one entry, it means we got some # shortened version of the protocol name but no # ambiguity. In that case, we will use the match. if len(start_with_the_same)==1: spec=specs.protocols[start_with_the_same[0]] elif len(start_with_the_same)==0: print("ERROR: supplied protocol '%s' does not exist." % argv[i]); sys.exit(1) else: print("Ambiguous protocol specifier '%s'. Did you mean any of these?" % argv[i]) for spec in start_with_the_same: print(" %s" % spec) sys.exit(1) # Finally, based on the spec, instance an actual protocol. # Note that if the spec is incorect, the Protocol() consutrctor # will call sys.exit() itself, so there is no need to do # error checking here. try: proto = Protocol(spec) self.protocols.append(proto) except ProtocolException as e: print("ERROR: %s" % str(e)) sys.exit(1) if len(self.protocols)==0: print("ERROR: Missing protocol") sys.exit(1) return OP_SUCCESS, None def run(self): """ This is Protocol's 'core' method: parses command line argument and prints any necessary protocol to standard output """ # Parse command-line arguments code, err = self.parse_cmd_line_args(sys.argv) if code!=OP_SUCCESS: print("ERROR: %s" % err) sys.exit(1) # Print the appropriate protocol headers for i in range(0, len(self.protocols)): # Modify the properties of the object if the user passed any # options that require it if self.bits_per_line is not None: self.protocols[i].bits_per_line = self.bits_per_line if self.skip_numbers is not None: if self.skip_numbers is True: self.protocols[i].do_print_top_tens=False self.protocols[i].do_print_top_units=False else: self.protocols[i].do_print_top_tens=True self.protocols[i].do_print_top_units=True if self.hdr_char_end is not None: self.protocols[i].hdr_char_end=self.hdr_char_end if self.hdr_char_start is not None: self.protocols[i].hdr_char_start=self.hdr_char_start if self.hdr_char_fill_even is not None: self.protocols[i].hdr_char_fill_even=self.hdr_char_fill_even if self.hdr_char_fill_odd is not None: self.protocols[i].hdr_char_fill_odd=self.hdr_char_fill_odd if self.hdr_char_sep is not None: self.protocols[i].hdr_char_sep=self.hdr_char_sep print(self.protocols[i]) if len(self.protocols)>1 and i!=len(self.protocols)-1: print("") # Main function def main(): """ Main function. Runs the Protocol program. """ # Instance our core class program = Main() # Do our magic program.run() # THIS IS THE START OF THE EXECUTION if __name__ == "__main__": main()