#!/usr/local/bin/gawk -f #!/usr/bin/awk -f # @(#) httplog.gawk 3.1 97/04/30 # 94/07/18 John H. DuBois III (john@armory.com) (based on ftplog program) # 94/08 - 94/09 Added r option. # 94/09/23 Added Total line for processed reports, and adRnONq options. # 94/10/03 Added L option. # 94/11/02 Fixed d option. # 94/12/26 Do progress dots only if they will go to a tty. # 95/03/24 Added support for new httpd log format. # 95/04/15 Turn on -R automatically. Added yiTD options. # 95/07/19 Added S option. # 96/01/27 Added rcfile processing, f pseudo-option # 96/01/29 Create entire mail message so that Content-type header can be added. # 96/02/10 Added X-HTTPReport-Levels header and u option. # 96/05/09 Make mail subject be a single word (HTTP_Report) so that it can be # more easily selected from a .maildelivery file # 96/07/20 Added g option and tag processing. # 96/10/12 Make t option work again. # 97/02/04 Skip failed requests. Read standard input if logfiles named. # Added ABekKI options. Let record number be given with -x. # 97/02/11 Print full line num instead of last digit for progress indication. # 97/02/17 Report # of records skipped. Added "allhits" user report option. # Added C option. Convert control chars to symbolic form in lots of # stuff read from logfile & printed by debug code. # 97/02/28 Added U option. # 97/03/20 Added Y option. # 97/04/09 Fixed header printing. Do not convert + to space in URLs. # 97/04/20 Added E option. # 97/04/30 Fixed E option. BEGIN { Setup() # Need to allow >1 space for reprocessing output FS = "[ ]+" } # Global vars initialized in Setup(): # Name Name of this program. # Width Screen width. # Options[] Options set from command line. # Debug Whether debugging is turned on. # Truncate Whether to truncate objects to 1st path component. # MostAccessed Show most accessed page under each dir. # Reprocess Process output of previous httplog runs. # IGNORECASE Set to 1 so that case is ignored in pattern matches. # Progress Whether progress characters should be printed. # Subject Subject of mail sent to users (not yet used). # ObjPattern Pattern that objects must match. # HostPattern Pattern that host names must match. # NotObjPattern Pattern that objects must not match. # NotHostPattern Pattern that host names must not match. # TrackObjects Track object accesses. # TrackHosts Track hosts. Only one of TrackObjects or TrackHosts may be on. # HTMLout Produce HTML-formatted output. # Interval Counting interval. # Counting Doing access counting. # CountHosts Report the number of different hosts that accessed each object, # rather than the total number of accesses of each object. # UseSubmit Use submit to inject mail into the queue. # Setup() also calls MailRepSetup(), which sets additional vars. function Setup( RUsage,Usage,ConfigFile,rcFile,DefLevel,Tag) { Name = "httplog" rcFile = "." Name Width = 79 Sender = "www" Subject = "HTTP_Report" # Subject of mail sent to users RUsage = \ Name " -r [-dtPU] [-l] [-c] [-s]\n"\ " [-u] [-L] [-M] [-w] [-g]\n"\ " [-x[rec#:]level] [logfile ...]" Usage = "Usage:\n" \ Name " [-aAbCdDeIkKpqhRHyS] [-[oO]] [-[nN]]\n"\ " [-w] [-u] [-i] [-T] [-x[rec#:]level]\n"\ " [logfile ...]\n" RUsage # Can't use gawk flags fFmWv. f is a pseudo-opt for FILES. # iTD are the interval graphing options. # rPMstlLcY are the report-all options. # adpqyR are the regular-report-only options. # Unused option letters: GjJQVXzZ ARGC = \ Opts(Name,Usage,"aAbBc;dDeE;f;i;kKl:L;n;N;o;O;pPqs:ST)u;Uw: Process only objects that match (o) or do not match (O)\n"\ " pattern . (OBJECT, NOTOBJECT)\n"\ "-[nN]: Process only objects accessed from a host whose name\n"\ " matches (n) or does not match (N) . (HOSTPAT, NOTHOSTPAT)\n"\ " The patterns given with [oOnN] are not implicitely anchored at the\n"\ " beginning or end.\n"\ "-h: Print this help.\n"\ "-b: Do not print a header. (NOHEADER)\n"\ "-e: Count failed HTTP requests (those with HTTP response codes of 4xx and\n"\ " 5xx). Normally, they are skipped. (LOGFAILED)\n"\ "-R: Reprocess the output of previous runs of %s. In order for dates to be\n"\ " interpreted correctly, if more than one old run is processed, they\n"\ " should be given in order of the time periods they cover. %s\n"\ " will normally automatically recognize its output and turn on the -R\n"\ " option; an explicit -R is only needed if the output header has been\n"\ " modified or removed.\n"\ "-p: Do not print progress numbers when reading logfile. (NOPROGRESS)\n"\ "-S: Report the number of different hosts that accessed each object, or if\n"\ " -q is also given, the number different objects accessed by each host,\n"\ " rather than the total number of accesses of each object or host.\n"\ " (NUMHOST)\n"\ "-q: Track requesting hostnames rather than objects. (BYHOST)\n"\ "-k: Do not keep track of the time of last access and the last host that\n"\ " accessed an object (or, if -q is given, the last object accessed by a\n"\ " host). (NOTRACKLAST)\n"\ "-A: Generate a report that, for each object, includes a time and hostname\n"\ " for each access. (ALLACCESSES)\n"\ "-B: Record & display the total number of bytes transferred for each URL\n"\ " or, if -q is given, host. This option is only available if the\n"\ " neccessary informatio is recorded in the logfile. (LOGBYTES)\n"\ "-K: Sort output by bytes transferred instead of access count. (SORTBYTES)\n"\ "-w: Set the screen width to . The host field is truncated\n"\ " if neccessary to make the line or fewer characters long. Use\n"\ " 0 to prevent any truncation. The default is %d. (WIDTH)\n"\ "-r: Run reports for all users. Use -H for a description of this mode.\n"\ "-x<[record-number:]level>: Turn on debugging at level , which should\n"\ " be an integer greater than 0. If record-number is given, debugging at\n"\ " the given level is not turned on until record the given record number\n"\ " is reached. Example: -x59500:10\n"\ "-y: Produce HTML output. Items in the Object field get hrefs, etc. (HTML)\n"\ "-u: Set the hostname used in generating the URLs for the -y option, and\n"\ " for generating HTML reports requested by users when -r is used. By\n"\ " default, the network hostname of the local system is used. (HOSTNAME)\n"\ "\n"\ "The rest of the options are used for generating reports on the total\n"\ "number of accesses over various periods. The accesses counted may be\n"\ "restricted by the -[oOnN] options. The output of each is a tabular\n"\ "listing of times and counts. They may be used with each other, in which\n"\ "case multiple tables are produced. The only other options that they may\n"\ "be used with are oOnNbpx.\n"\ "-i: Report accesses for each period. is\n"\ " given as an integer followed by the letter d, h, or m to indicate\n"\ " days, hours, or minutes. Example: -i12h (INTERVAL)\n"\ "-T: Report accesses by time of day, for each fraction\n"\ " of a day. (PERIODS)\n"\ "-D: Report accesses by day of week. (BYDAY)\n", Name,Usage,rcFile,rcFile,rcFile,Name,Name,Width Err = "0" } if ("H" in Options) { printf \ "%s -r: mail reports of http accesses to users.\n"\ "Usage:\n"\ "%s\n"\ "%s -r examines the http access log and generates reports based on it,\n"\ "which are mailed to the users responsible for particular http-accessible\n"\ "files. The default is for no report to be sent. To get a report, users\n"\ "may put the following in a file named %s in their home directories\n"\ "(%s must be a real file; symbolic links are ignored):\n"\ "raw Raw http access log data, with one line for each access.\n"\ "standard Summary with one line for each file accessed, inc. access count.\n"\ "wide This gives the standard report without lines being truncated,\n"\ " as is usually done to make them fit an 80-column screen.\n"\ "allhits For each object, the number of accesses and the object name is\n"\ " printed, followed by a time and hostname for each access.\n"\ "html Like wide, but HTML-formatted (as in the -y option).\n"\ "Multiple report levels may be specified, spread over one or more lines.\n"\ "All of the requested reports will be sent.\n"\ "Report levels may be prefixed with a tag (separated from the level name by\n"\ "a colon), in which case a report at the given level will only be sent if\n"\ "the tag name is given with the -g option. If any level has a matching\n"\ "tag, then only the levels with matching tags will be set up. If no\n"\ "matching tags are found, then all of the levels without tags are set up. \n"\ "If -g is not given, then all of levels without tags are sent. Example: If\n"\ "-gmonthly is given and \"monthly:standard raw monthly:html\" appears in a\n"\ "user's configuration file, the user will get standard and html reports. \n"\ "For the same configuration file, if -gdaily is given, the user will get a\n"\ "raw report, since no daily tag appears in the configuration file.\n"\ " The operation of %s -r as a whole is determined by the master\n"\ "configuration file %s. This file maps patterns to user\n"\ "names and must exist. The master configuration file should contain lines\n"\ "of the form:\n"\ "Pattern Address[,...] [Report-level,...]\n"\ " Pattern is an egrep(C)-style regular expression to match the pathname\n"\ "of an object that may be accessed. It is not anchored; begin it with ^ to\n"\ "force a match to start at the beginning of the path.\n"\ " Address is the email address of the user that reports of accesses that\n"\ "match this pattern should be sent to. Accesses that match a pattern can\n"\ "be included in the reports sent to multiple users by giving a comma-\n"\ "separated list of email addresses for Address. If Pattern and Address\n"\ "both contain an embedded %%s, then the %%s in Pattern will be used to\n"\ "match anything up to the next '/', and the %%s in Address will be replaced\n"\ "with whatever was matched by it. This can be used to send reports to a\n"\ "set of users or aliases whose names will be embedded in URLs. If %%s\n"\ "substitution is used, the name of the local user whose %s file should\n"\ "be used to determine the levels for a report is taken to be the part that\n"\ "%%s matches, not the Address field after the %%s in it has been substituted\n"\ "for.\n"\ " Reports is a comma-separated list of report levels. If given, it\n"\ "overrides the default of %s. It in turn is overridden if a user requests\n"\ "particular report levels in a %s file.\n"\ " Reports are sent for all patterns that match, not just the first.\n"\ " Lines that begin with # are comments and are ignored.\n"\ " Example mapping file:\n"\ "######\n"\ "# report accesses of objects in users' home pages to the users\n"\ "^/~%%s/ %%s\n"\ "# Report cgi-bin & tests accesses to spcecdt\n"\ "^/(cgi-bin|tests)/ spcecdt\n"\ "# report all transfers other than user home pages to webmaster\n"\ "^/[^~] webmaster standard\n"\ "######\n"\ "The w option can be used to set the screen width for the 'standard'\n"\ "report level. The n option can be used but probably shouldn't be.\n"\ "Options specific to -r:\n"\ "-P: Print progress numbers. (PROGRESS)\n"\ "-M: Mail all reports to (for testing).\n"\ "-s: Set the subject used in mail. The default is '%s'.\n"\ " (SUBJECT)\n"\ "-E: Make report mail come from . The\n"\ " default is \"%s\". (MAILFROM)\n"\ "-Y: Use \"submit\" (the MMDF mail injection command) to submit messages\n"\ " to the mail queue. This allows special options to be given to the\n"\ " mail system; in particular, options are passed that prevent the mail\n"\ " from being returned if the local mail system cannot deliver it. If -Y\n"\ " is not used, mail is injected by invoking \"sendmail\". (USESUBMIT)\n"\ "-t: Test only; do not actually mail anything.\n"\ "-g: Set the report level tag.\n"\ "-l: Set the default report level to something other\n"\ " than %s. A comma-separated list of levels can be given. This is over-\n"\ " ridden by values given in the systemwide mapfile and in user config\n"\ " files. (DEFLEVEL)\n"\ "-L: Only generate reports at levels that are named in the comma-\n"\ " separated list. Requests for reports at other levels are ignored.\n"\ " ignored. This can be useful for e.g. monthly cumulative reports, for\n"\ " which raw reports might be unwanted or too large to process. (LEVELS)\n"\ "-U: If multiple users have the same home directory and there is a\n"\ " configuration file in the directory, all report data generated as\n"\ " a result of %%s substitution is sent only to the first user with that\n"\ " home directory listed in the password file. Example:\n"\ " Users spcecdt and john both have home directory /u/spcecdt, and there\n"\ " is a configuration file in that directory. spcecdt is listed first in\n"\ " /etc/passwd. A %%s line in the master configuration file causes a\n"\ " report on all accesses of objects that begin with /~username/ to be\n"\ " sent to username. In this case, accesses of /~john/ will be reported\n"\ " to spcecdt instead of john. (CONGLOMUSERS)\n"\ "-c: Use instead of %s. (MAPFILE)\n", Name,RUsage,Name,userMapfile,userMapfile,Name,ConfigFile, userMapfile,DefLevel,userMapfile,Subject,Sender,DefLevel,ConfigFile Err = "0" } if (Err != "") exit Err if ("x" in Options) { Debug = Options["x"] if (Debug ~ /^[1-9][0-9]*$/) printf "Debugging set to level %d\n",Debug > "/dev/stderr" else if (Debug ~ /^[1-9][0-9]*:[1-9][0-9]*$/) { split(Debug,elem,":") StartDebug = elem[1] DebugValue = elem[2] Debug = 0 printf "At record %d, debug level will be set to %d.\n", StartDebug,DebugValue > "/dev/stderr" } else { printf "%s: Bad value given with -x: %s\n", Name,Debug > "/dev/stderr" Err = 1 exit 1 } } CountOnly = "C" in Options noBad = !("e" in Options) if ("I" in Options) ARGV[ARGC++] = "/dev/stdin" else if (ARGC < 2) if ("f" in Options) { numFiles = split(Options["f"],Files,",") for (i = 1; i <= numFiles; i++) ARGV[i] = Files[i] ARGC = numFiles + 1 } else { printf "%s: No input files named.\n",Name > "/dev/stderr" Err = 1 exit 1 } if (Debug) { print "Files to be processed:" > "/dev/stderr" for (i = 1; i <= ARGC; i++) print ARGV[i] > "/dev/stderr" } if ("w" in Options) Width = Options["w"] Truncate = "d" in Options MostAccessed = "a" in Options UseSubmit = "Y" in Options if ("E" in Options) Sender = Options["E"] Reprocess = "R" in Options CountHosts = "S" in Options TrackObjects = !(TrackHosts = ("q" in Options)) DoHeader = !("b" in Options) LogBytes = "B" in Options || CountOnly SortBytes = "K" in Options AllAccesses = "A" in Options TrackLast = !("k" in Options || AllAccesses) if (CountOnly) TrackObjects = TrackHosts = TrackLast = DoHeader = 0 if (HTMLout = ("y" in Options)) Width = 0 if ("g" in Options) Tag = Options["g"] if ("r" in Options) { if (MailRepSetup(ConfigFile,userMapfile,DefLevel,Tag,"U" in Options)) { Err = 1 exit 1 } } else # Do progress numbers only if they would go to a tty Progress = !("p" in Options) && system("test -t 2") == 0 IGNORECASE = 1 if ("o" in Options || "O" in Options || "n" in Options || "N" in Options) { if ("o" in Options) ObjPattern = Options["o"] if ("n" in Options) HostPattern = Options["n"] if ("O" in Options) NotObjPattern = Options["O"] if ("N" in Options) NotHostPattern = Options["N"] PatternGiven = 1 } if ("y" in Options || "r" in Options) { if ("u" in Options) URLpref = Options["u"] else { CmdReadLine("hostname") URLpref = $0 } URLpref = "http://" URLpref if (Debug) print "URL prefix is " URLpref > "/dev/stderr" } if ("i" in Options) { Interval = Options["i"] IntervalUnits = substr(Interval,length(Interval),1) Interval += 0 if (IntervalUnits !~ /^[dhm]$/) { printf \ "Invalid unit '%s' given with -i option. Must be one of [dhm].\n", IntervalUnits > "/dev/stderr" Err = 1 exit(1) } if (Interval == 0) { printf "Invalid value '%s' given with -i option.\n", Interval > "/dev/stderr" Err = 1 exit(1) } if (IntervalUnits == "d") Interval *= 86400 else if (IntervalUnits == "h") Interval *= 3600 else if (IntervalUnits == "m") Interval *= 60 if (Debug) printf "Interval is %d seconds.\n",Interval > "/dev/stderr" } if ("T" in Options) { DayFracs = Options["T"] BucketSecs = 86400/DayFracs } DoDayOfWeek = "D" in Options if (Interval || DayFracs || DoDayOfWeek) { MkMonth2Num() MakeTZOffset() Counting = 1 } if (Debug) print "Done with setup." > "/dev/stderr" } # Setup actions needed only for allreports mode. # Global vars initialized here: # ReportAll Tells other functions that multiple users are being rept on. # Test If no mail should be sent. # Subject Subject of mail sent to users (not yet used). # LevelOrder Comma-sep list of valid report levels. # LevelSet[] Set of valid report levels. # ChkRaw If some raw report is requested. # AllAccesses If anyone requested an "allhits" report. # RawSet[] A set which contains only "raw", to be passed to Log() # See ReadConf() for a description of Users[] Patterns[] Prefixes[] # See LevelFixup() for a description of MaybeRaw[] MaybeNotRaw[] # See ReadUserConfigs() for a description of UserSpec[] UserLevelSet[] function MailRepSetup(ConfigFile,userMapfile,DefLevel,Tag,ConglomReports, i,NumRawUser,NumRawConf,NumPat,AllAddrs,LevelLists,LevelCount) { # NumRawUser is the number of users who requested a raw report, # passed to LevelFixup so that if it is nonzero, %s report lines can be # marked as possibly requiring a raw report. # NumRawConf is the number of patterns that may result in a raw report. # It and NumRawUser are used to determine whether any log lines should be # checked to determine if they should be attached to any raw reports. ReportAll = 1 # More than one user is being reported on # In allreport mode, print progress numbers only if P is given. Progress = ("P" in Options) Test = "t" in Options # Do not mail anything. if ("s" in Options) # Subject of mail sent to users. Subject = Options["s"] if ("c" in Options) # Systemwide config file ConfigFile = Options["c"] LevelOrder = "standard,wide,html,allhits,none,raw" MakeSet(LevelSet,LevelOrder,",") # Valid report levels if ("L" in Options) { if (CheckLevels(Options["L"],LevelSet,AcceptLevels) < 1) return 1 } else CopySet(LevelSet,AcceptLevels) # Always accept 'none', so that it can be used as the level for %s lines # that don't have any accepted level in the default. AcceptLevels["none"] if ("l" in Options) { # Change the default level DefLevel = Options["l"] if (CheckLevels(Options["L"],LevelSet) == -1) return 1 } # Read systemwide config file if ((NumPat = ReadConf(ConfigFile,DefLevel,Patterns,Users, Prefixes,LevelLists,AllAddrs)) < 0) return 1 # Read user config files beforehand to optimize data collection. # That way we do not need to keep allhits data if noone requests it, # and only need to keep raw report data around for users who request it. ReadUserConfigs(UserSpec,UserLevelSet,userMapfile,AcceptLevels, !IsEmpty(Prefixes),AllAddrs,Tag,LevelCount,ConglomReports,UserMap) NumRawUser = LevelCount["raw"] AllAccesses = "allhits" in LevelCount && LevelCount["allhits"] > 0 if ((NumRawConf = LevelFixup(UserSpec,NumRawUser,MaybeRaw,MaybeNotRaw, LevelSet,AcceptLevels,ConfigFile,Levels,NumPat,LevelLists)) == -1) return 1 if (NumRawConf > 0 || NumRawUser > 0) { ChkRaw = 1 if (Debug) print "Raw logging is on." > "/dev/stderr" } RawSet["raw"] if (Debug) { for (i = 0; i < NumPat; i++) { printf "Pattern %d: <%s> Levels: <%s>", i,Patterns[i],LevelLists[i] > "/dev/stderr" if (i in Users) printf " User: <%s>",Users[i] > "/dev/stderr" if (i in Prefixes) printf " Prefix: <%s>",Prefixes[i] > "/dev/stderr" if (i in MaybeRaw) printf " MaybeRaw" > "/dev/stderr" if (i in MaybeNotRaw) printf " MaybeNotRaw" > "/dev/stderr" print "" > "/dev/stderr" } } } function MkMonth2Num( Month) { split("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",Months,",") for (Month in Months) Month2Num[Months[Month]] = sprintf("%02d",Month) } # Sets global TZOffset to the number of seconds that need to be substracted # from the local date (without time of day) to give an epoch time. # TZOffset can also be added to systime() before doing %86400 to get the # current day number in the local timezone. # Note that TZOffset is only correct if the given date is in the same DST # phase as the current date. # 95/03/26 Calculate TZOffset more accurately. function MakeTZOffset( t) { t = systime() TZOffset = strftime("%H",t)*3600+strftime("%M",t)*60+strftime("%S",t) - \ t%86400 if (strftime("%j",0) != "001") TZOffset -= 24*3600 } # Convert a numeric timezone to a number of seconds. # Example: converts -0830 to -30600 function TZ2sec(NTimezone) { if (NTimezone < 0) { NTimezone = substr(NTimezone,2) Mult = -1 } else Mult = 1 return (substr(NTimezone,1,2)*3600+substr(NTimezone,3,2)*60)*Mult } # Converts comma-separated level list Levels to lower case, # splits it into LevelSet, and checks it against GoodLevels. # Returns -1 on success, else number of levels. # Filename is the file that Levels was read from, for error messages. # If not given, it is not included. function CheckLevels(Levels,GoodLevels,LevelSet,Filename, LevElem,Level,NumLevels,i,RealLev) { RealLev = 0 NumLevels = split(Levels,LevElem,",") split("",LevelSet) # clear LevelSet for (i = 1; i <= NumLevels; i++) { Level = LevElem[i] if (!(Level in GoodLevels)) { if (Filename == "") printf "Bad level '%s'. Exiting.\n",Level > "/dev/stderr" else printf \ "Error in config file '%s': bad level '%s'. Exiting.\n", Filename,Level > "/dev/stderr" return -1 } else if (!(Level in LevelSet)) { LevelSet[Level] RealLev++ } } return RealLev } # Input vars: # ConfigFile is the master configuration file to read. # The configuration file has this format: # pattern recipient[,...] [report-level,...] # DefLevel is a list of levels. Lines that have no report-levels specified # will default to these levels. # # Output vars: # AllAddrs[] is made the set of all addresses that reports may # be sent to (all recipient names found), for use by ReadUserConfigs() in # finding the configuration files for those recipients. # All of the rest of these arrays are indexed 0..(numPatterns-1): # Patterns[] contains the pattern that the data for each index applies to. # In the case of %s substitutions, Patterns[] will contain the given pattern # with the %s replaced by "[^/]*", since that is what the %s stands in for. # Users[] contains the email address of the user who should gets reports of # matches to this pattern. # Prefixes[] contains the part of a path that comes before the user name for # patterns that apply to more than one user (%s lines). # LevelLists[] contains the default report level for matches to this pattern. # # Return value: <0 for error. If no error, returns the number of entries # put in the arrays. function ReadConf(ConfigFile,DefLevel,Patterns,Users,Prefixes, LevelLists,AllAddrs, ret,Pattern,User,LineNum,UserElem,i,LevList) { NumPat = 0 while ((ret = (getline < ConfigFile)) == 1) { LineNum++ if ($0 ~ /^#/) # Comment continue if (NF < 2) { printf \ "Error on line %d of config file %s: Not enough fields:\n%s\n", LineNum,ConfigFile,$0 return -1 } if (Debug) printf "Config line: <%s>\n",$0 > "/dev/stderr" Pattern = $1 User = $2 # Generate a separate entry for each user. # This is faster than splitting later for each match because in # most cases there will only be one. split(User,UserElem,",") for (i in UserElem) { User = UserElem[i] if (Debug) printf " User: %s\n",User > "/dev/stderr" if (NF > 2) { $1 = $2 = "" LevList = $0 gsub("[ \t]+","",LevList) } else LevList = DefLevel # Convert %s line if (Pattern ~ /%s/ && User ~ /%s/) { Prefixes[NumPat] = Pattern sub(/%s.*/,"",Prefixes[NumPat]) sub("%s","[^/]*",Pattern) # Make User consisting solely of %s a special case # (by not recording in it Users[]), # for a little speedup when processing logfile. if (User != "%s") Users[NumPat] = User } else Users[NumPat] = User Patterns[NumPat] = Pattern LevelLists[NumPat] = LevList AllAddrs[User] NumPat++ } } if (ret) { printf "Error reading config file '%s'. Exiting.\n",ConfigFile > \ "/dev/stderr" return -1 } return NumPat } # Input variables: # UserSpec[] contains the list of report levels given by each user who # requests specific levels. # RawSpec is true if any users requested a raw report. # LevelSet[] is the set of valid level names. # AcceptLevels[] is the set of levels that reports may be requested at. # ConfigFile is the name of the config file that was read. # NumPat is the number of elements in Patterns[]. # LevelLists[] contains the default report level for matches to the pattern # with the same index in Patterns[]. # Global variables: # Users[] contains the email address of the user who should gets reports of # matches to this pattern. # Prefixes[] contains the part of a path that comes before the user name for # patterns that apply to more than one user. # Patterns[] contains the pattern that the data for each index applies to. # Output variables: # MaybeRaw[] contains the indexes of any patterns that may result in a raw # report line. # MaybeNotRaw[] contains the indexes of any patterns that may result in a # non-raw report line. # An index (i,Level) is made in Levels[] for each report level Level requested # for pattern i. # Return value: the number of patterns that result in a raw report. function LevelFixup(UserSpec,RawSpec,MaybeRaw,MaybeNotRaw,LevelSet, AcceptLevels,ConfigFile,Levels,NumPat,LevelLists, Level,LevList,PatLevels,NumRaw) { for (i = 0; i < NumPat; i++) { if (i in Users && Users[i] in UserSpec) { LevelLists[i] = LevList = UserSpec[Users[i]] if (Debug) printf "Changed report level for pattern %d to %s\n", i,LevList > "/dev/stderr" } else { LevList = LevelLists[i] if (Debug) printf "Kept report level for pattern %d at default of %s\n", i,LevList > "/dev/stderr" } if ((CheckLevels(LevList,LevelSet,PatLevels,ConfigFile)) == -1) return -1 # If this level uses %s substitution, and no levels in it are accepted, # then add "none" (which is always accepted) to its list of levels, # because some user may have specified a level that is accepted. if (i in Prefixes && Intersection(PatLevels,AcceptLevels) == 0) PatLevels["none"] for (Level in PatLevels) { if (!(Level in AcceptLevels)) continue # If this level is raw, or uses %s sustitution and any user # requested a raw report, then it may result in a raw report # line. if (Level == "raw" || RawSpec > 0 && (i in Prefixes)) { MaybeRaw[i] if (Level == "raw") NumRaw++ } # If this level isn't raw, or uses %s substitution, it may result # in a non-raw report line. if (Level != "raw" || (i in Prefixes)) MaybeNotRaw[i] Levels[i,Level] } } return NumRaw } # Find all user config files. # Any user with a config file is made an index of UserSpec, with the value # being the list of report levels requested. # Also, for each level a user requests a report at, (User,Level) is made # an index of UserLevelSet[]. # AllAddrs[] is the set of all addresses that reports may be sent to. # If no %s substitutions were called for in the master configuration file, # user configuration files are only searched for in the home directories of # the users given in AllAddrs[]. # Tag is a tag that a level may be prefixed with (separated from it by a # colon). If Tag is given and any levels prefixed by it are found in a user's # config file, only those levels are set up for the user. Otherwise, even if # Tag is given, all levels that do not have a tag are set up. # LevelCount[] is returned with the number of users who requested a report at # each level. E.g., LevelCount["raw"] is the number of users who requested a # raw report. # If ConglomReports is true, than the second & later instances of a particular # home directory found cause an entry added to UserMap, mapping the user whose # home directory it is to the first user who had that home directory. # Information for these users is still recorded in the rest of the arrays, # since ConglomReports applies only to implicit user names generated from %s # substitution. function ReadUserConfigs(UserSpec,UserLevelSet,userMapfile,GoodLevels,PctSUsed, AllAddrs,Tag,LevelCount,ConglomReports,UserMap, Cmd,User,Home,ConfFile,i,oIgnCase,Level,UserLevels,Elem,GotTag,Count) { oIgnCase = IGNORECASE IGNORECASE = 1 # Use sh to get config file list because it can check for symlinks & # non-regular files... a weak attempt at security if (PctSUsed) # If any %s lines were given, get config files for all users Cmd = \ "IFS=:\n"\ "while read user p u g n homedir s\n"\ "do\n"\ " UserConfigFile=$homedir/" userMapfile "\n"\ " if [ -f $UserConfigFile -a ! -L $UserConfigFile ]; then\n"\ " echo $user $homedir\n"\ " fi\n"\ "done < /etc/passwd" else { # If no %s lines were given, get config files only for users mentioned # in global config file; much faster on a system with many users. FS = ":" while ((getline < "/etc/passwd") == 1) if ($1 in AllAddrs) Cmd = Cmd " " $1 " " $6 close("/etc/passwd") FS = "[ \t]+" if (Debug) print "User/homedir list: " Cmd > "/dev/stderr" Cmd = \ "set -- " Cmd "\n"\ "while [ $# -ge 2 ]; do\n"\ " UserConfigFile=$2/" userMapfile "\n"\ " [ -f $UserConfigFile -a ! -L $UserConfigFile ] && echo $1 $2\n"\ " shift;shift\n"\ "done" } # Read files at the same time that the filelist is being generated so that # it will be harder to take advantage of the window while ((Cmd | getline) == 1) { User = $1 Home = $2 if (ConglomReports) if (Home in Home2User) { UserMap[User] = Home2User[Home] if (Debug) printf "Implicit %%s reports for %s go to %s\n",User, Home2User[Home] > "/dev/stderr" } else Home2User[Home] = User if (Debug) printf "%s has a config file.\n",User > "/dev/stderr" ConfFile = Home "/.httpreport" split("",UserLevels) # clear out the array. GotTag = Count = 0 while ((ret = (getline < ConfFile)) == 1) { for (i = 1; i <= NF; i++) { Level = tolower($i) if (split(Level,Elem,":") > 1) { if (Debug) printf "Tag is <%s>, level is <%s>\n",Elem[1], Elem[2] > "/dev/stderr" if (Tag == "" || Elem[1] != Tag) { if (Debug) printf "Skipping non-matching tag <%s>\n", Elem[1] > "/dev/stderr" continue } if (!GotTag) { # User gave a tag that matches the report being run, # so clear out anything else that has accumulated. if (Debug) printf "Removing %d untagged level(s)\n", Count > "/dev/stderr" split("",UserLevels) GotTag = 1 Count = 0 } Level = Elem[2] } # If user previously specified a tag that matches the report # being run, and this level has no tag, skip it. else if (GotTag) { if (Debug) printf "Skipping untagged level <%s>\n", Level > "/dev/stderr" continue } # Just ignore anything not in GoodLevels. Nothing obvious to # do about bad levels except mail a warning to users, which # is liable to just result in bloated mailboxes. Also, for # now the -L option causes the list of good levels passed # to this function to be reduced. if (Level in GoodLevels) { if (Debug) printf "Saving level <%s>\n", Level > "/dev/stderr" UserLevels[++Count] = Level } } } for (i = 1; i in UserLevels; i++) { Level = UserLevels[i] UserLevelSet[User,Level] if (User in UserSpec) UserSpec[User] = UserSpec[User] "," Level else UserSpec[User] = Level LevelCount[Level]++ if (Debug) printf "%s gets a report for level '%s'.\n", User,Level > "/dev/stderr" } if (Debug && ret) printf "Could not read config file '%s'.\n", ConfFile > "/dev/stderr" close(ConfFile) } close(Cmd) IGNORECASE = oIgnCase } # Returns 1 if Set is empty, 0 if not. function IsEmpty(Set, i) { for (i in Set) return 0 return 1 } # MakeSet: make a set from a list. # An index with the name of each element of the list # is created in the given array. # Input variables: # Elements is a string containing the list of elements. # Sep is the character that separates the elements of the list. # Output variables: # Set is the array. # Return value: the number of elements added to the set. function MakeSet(Set,Elements,Sep, i,Num,Names) { Num = split(Elements,Names,Sep) for (i = 1; i <= Num; i++) Set[Names[i]] return Num } ##### End of setup functions. ##### Start of logging functions. # YMD2day(year,month,day-of-month) returns the number of days that passed from # 1970 Jan 1 to the given date. # All parameters should be given in numeric form. # If year < 70, it is assumed to be part of the 2000 century # If year in (70..99), 1900. # Globals: sets and uses MDays[] function YMD2day(Year,Month,Day, LeapDays) { if ((Year+0) < 70) Year += 100 else if ((Year+0) >= 100) Year -= 1900 # Year is now the number of years since 1900. LeapDays = int((Year - 68) / 4) if (Month <= 2 && Year % 4 == 0) LeapDays -= 1 if (!MDays[2]) split("0 31 59 90 120 151 181 212 243 273 304 334 365",MDays) return (Year - 70) * 365 + MDays[Month + 0] + Day - 1 + LeapDays } # Strip args from cgi accesses # Globals: none. function CGIstrip(Object) { if (Object ~ "^/cgi-bin/") { Object = substr(Object,10) sub("[/?].*","",Object) Object = "/cgi-bin/" Object } return Object } # Input variables: # Object is the object to try matches against. # MaybeRept[] contains the indexes of each pattern that may result in a # report line (for optimization). # LevelsToLog[] is the set of levels to log. # Data is the data to be stored in Reports[] for matches. # # Output variables: # Report data are put in Reports[email-addr,Level,1..n] # The extra index of 1..n is used rather than just appending strings because # string concatenation becomes very expensive in awk. # Global UserLevelSet[email-addr,Level] contains the current highest highest # index used in Reports, for tracking the index. The indexes of ReptLevels # are also used to allow iteration over the indexes of Reports[]. # MailNames[email-addr] contains the set of all email-addrs that reports will # be sent to, for iterating over the contents of ReptLevels[]. # # Globals used: # Patterns[] contains the pattern that the data for each index applies to. # Users[] contains the email address of the user who should gets reports of # matches to this pattern. # Prefixes[] contains the part of a path that comes before the user name for # patterns that apply to more than one user. # UserSpec[] contains the names of users who have specified what report levels # they want. # UserLevelSet[] contains a User,Level index for each report level that a user # requested. # Levels[] contains the report levels for each pattern. # UserMap[addr] may remap certain implicit email addresses (produced by %s # substitution) to others. # For each index in MaybeRept[], the pattern with that index is checked for a # match against Object. If matched, the login name and user name are derived # from it using Users[] and Prefixes[]. # Then, for each level that is being logged at this stage, if the user # requested a report at that level, the data is added to the report for that # level for the user. function Log(Object,MaybeRept,Reports,LevelsToLog,Data, Level,User,Login,i,LEnd) { for (i in MaybeRept) { if (Object ~ Patterns[i]) { # Get login name and email address. if (i in Prefixes) { # If doing a %s substitution... # Get rid of the part that matches the prefix match(Object,Prefixes[i]) Login = substr(Object,RLENGTH+1) # Get rid of everything from the next / onward if (LEnd = index(Login,"/")) Login = substr(Login,1,LEnd-1) if (Login in UserMap) { if (Debug > 1) printf "Redirecting report for %s to %s\n", Login,UserMap[Login] > "/dev/stderr" Login = UserMap[Login] } # Login is now the user-name part of the object. if (i in Users) { # Subsitute what's left into the email address format # string contained in Users[] User = Users[i] sub("%s",Login,User) } else # The special case of no entry in Users[] indicates that # the email address is just the user-name. User = Login } else # If not doing a %s substitution... Login = User = Users[i] # Login is a local user name who might have an entry in UserSpec[]. # User is the email address that a report should be sent to. for (Level in LevelsToLog) { # If user requested specific levels & did not include this one, # or user didn't request specific levels and this level is not # in the default set of levels for this pattern, skip it. if (Login in UserSpec) { if (!((Login,Level) in UserLevelSet)) continue } else if (!((i,Level) in Levels)) continue Reports[User,Level,++UserLevelSet[User,Level]] = Data MailNames[User] if (Debug > 1) printf "Report on '%s' at level '%s' goes to '%s'.\n", Object,Level,User > "/dev/stderr" } } } } # Return date in the form: MMM dd hh:mm # Old format: # 3 4 5 6 # 1 2 3 # Feb 19 17:53:27 1995 # New format: # 4 5 # 2 3 4 5 6 7 8 # [15/Mar/1995:18:00:30 -0800] # [15/Mar/1995:18:00:30] # Globals: # NewFormat function Date( E) { if (Reprocess) return sprintf("%s %2d %5s",$2,$3,$4) else if (NewFormat) { split($4,E,"[][/: ]") return sprintf("%s %2d %2s:%2s",E[3],E[2],E[5],E[6]) } else return sprintf("%s %2d %5s",$3,$4,substr($5,1,5)) } # Returns the time the current entry was made, as though it occured in the # current timezone. This is done instead of adding the timezone offset # (which would make the returned value be a 'real' UNIX time) because we # want to dump entries into buckets/intervals based on the time in the local # timezone. function EntryTime( E,Time) { if (NewFormat) { split($4,E,"[][/: ]") Time = YMD2day(E[4],Month2Num[E[3]],E[2])*86400 + E[5]*3600 + E[6]*60 } else { split($5,E,":") Time = YMD2day($6,Month2Num[$3],$4)*86400 + E[1]*3600 + E[2]*60 } if (Debug > 5) printf "Entry time is %s for %s\n",strftime("%c",Time-TZOffset), Uncontrol($0) > "/dev/stderr" if (NR == 1) FirstTime = Time LastTime = Time return Time } # Do the work common to processing new data and reprocessing processed data. # Uses globals: # Object object logged # Truncate Whether to truncate objects to 1st path component. # TrackObjects Track object accesses. # TrackHosts Track hosts. # PatternGiven, ObjPattern, NotObjPattern, HostPatter, NotHostPattern # Progress Print progress numbers. # Sets globals: # LastRemote[] to what the last access of the object or host was # Index to the index that the info about this access should be logged under # Return value: 0 if there should be no further processing (used instead of # doing a 'next' here to avoid gawk memory leak). function Common( SStart) { if (PatternGiven && (ObjPattern != "" && Object !~ ObjPattern || NotObjPattern != "" && Object ~ NotObjPattern || NotHostPattern != "" && Host ~ NotHostPattern || HostPattern != "" && Host !~ HostPattern)) { if (Debug > 2) printf \ "Skipping record; object or host does not match pattern:\n"\ "%s\n",Uncontrol($0) > "/dev/stderr" NotMatch++ return 0 } if (Progress && ++pCount == 1000) { printf "\r%d",NR > "/dev/stderr" pCount = 0 } if (Counting) return 1 if (Truncate) { # Require object to have a / other than the initial one if (SStart = match(Object,"./")) # Truncate before the second / Object = substr(Object,1,SStart) else if (substr(Object,1,2) == "/~") Object = "/~" else Object = "/" } if (TrackObjects) { Index = Object if (TrackLast) LastRemote[Index] = Host } else if (TrackHosts) { Index = Host if (TrackLast) LastRemote[Index] = Object } else if (CountOnly) Index = "TOTAL" return 1 } NR == StartDebug { Debug = DebugValue } Debug > 9 { printf "Record %d: %s\n",NR,Uncontrol($0) > "/dev/stderr" } # Example lines from "old" httpd log: # 1 2 3 4 5 6 7 8 9 # Requesting-host Date Op URL Version # deeptht.armory.com [Sat Feb 19 17:53:27 1994] GET /~spcecdt/arm.html HTTP/1.0 # sgil301.cern.ch [Mon Jul 18 07:35:42 1994] POST /cgi-bin/purity-test/NumQuest=100/Name=Sex100 HTTP/1.0 # netcom5.netcom.com [Sun May 22 00:17:16 1994] get /u/css1217/index.html # pentlan.stir.ac.uk [Wed May 25 01:17:29 1994] HEAD /~zap/nc/nc.html HTTP/1.0 # New transfer log format: # (see http://hoohoo.ncsa.uiuc.edu/docs/setup/httpd/TransferLog.html) # host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "method resource protocol" status-code bytes-sent referer agent # rfc391, authuser, status-code, and bytes-sent are replaced with '-' if they # are not available. # resource is liable to consist of multiple words, and even a newline # occasionally, which makes part of it appear to be a new record. This is # dealt with by ignoring methods other than GET, POST, and HEAD (which also # skips uninteresting methods like OPTIONS). # Both resource and referrer are liable to contain embedded double quotes. # Spinner logs are liable to be completely missing the host field, thus # starting with the space that separates the first field from the second. # This is OK because FS is set to a pattern that causes the empty first field # to still count as a field. # Example: # 1 2 3 4 5 6 7 8+ 9+ 10+ 11+ 12+ # x.mv.com - - [15/Mar/1995:18:00:30 -0800] "GET / HTTP/1.0" 200 34110 ref agn # 1 2 3 4 5 6 7+ 8+ 9+ 11+ 12+ # x.mv.com blort - [15/Mar/1995:18:00:30] "GET / HTTP/1.0" 200 34110 ref agn # Header of processed output: # http accesses, Apr 13 01:18 to Apr 13 18:15. # Determine datafile type. This must come before the Reprocess block, since it # does reprocessing autorecognition. NR == 1 { # Use ~ for field 1 so it will match regardless of case; format changed if ($1 ~ "http" && $2 == "accesses,") { if (Debug) print "Datafile is old output." > "/dev/stderr" Reprocess = 1 if (Counting) { print "Cannot use old output file for interval counting." \ > "/dev/stderr" exit(1) } MinFields = 6 NotRecs++ } else { if (NewFormat = ($4 ~ /^\[/)) { if (Debug) print "Datafile is in the new format." > "/dev/stderr" if ($4 == "[") print "Warning: datafile has no dates?!" > "/dev/stderr" if ($4 ~ /\]$/) { if (Debug) print "Datafile has no timezone field." > "/dev/stderr" Op_i = 5 URL_i = 6 Bytes_i = 9 if (noBad) SC_i_start = 8 MinFields = 9 } else { if (Debug) print "Datafile has a timezone field." > "/dev/stderr" Op_i = 6 URL_i = 7 Bytes_i = 10 if (noBad) SC_i_start = 9 MinFields = 10 } MakeSet(Methods,"\"GET,\"POST,\"HEAD",",") # Codes actually used by spinner: # 200 Success # 302 Redirect (spinner sends this for both explicitly set # redirects and to redirect /~foo to /~foo/index.html) # 304 Not Modified # 404 Not Found # 403 Forbidden # 500 Internal Error MakeSet(StatusCodes, "200,201,202,203,204,400,401,402,403,404,500,501,502,503,301,302,303,304", ",") } else { if (Debug) print "Datafile is in the old format." > "/dev/stderr" Op_i = 7 URL_i = 8 MakeSet(Methods,"GET,POST,HEAD",",") LogBytes = 0 MinFields = 8 } StartDate = Date() } Host_i = 1 if (Debug) printf "Method index: %d\n",Op_i > "/dev/stderr" } Reprocess && $2 != "Total" { sub("^ +","") # Check for header, total line, etc. # Header line: http accesses, Sep 23 00:15 to Sep 23 08:06. # Log lines: # count mon d time host bytes? object # 1 2 3 4 5 6? 6/7 # 11 Feb 1 15:47 becnet51.becnet.com 99999 /robots.txt if (($1 + 0) == 0) { # Since dates are not in order in processed output, get them from # the old headers. if ($6 == "to") { if (Debug) printf "Got log interval line.\n" > "/dev/stderr" if (StartDate == "") StartDate = sprintf("%s %2d %5s",$3,$4,$5) EndDate = sprintf("%s %2d %5s",$7,$8,substr($9,1,5)) NotRecs++ next } else if ($1 == "Num" && $NF == "Object") { if (NF == 6 || NF == 7) GotLast = 1 else if (NF == 2 || NF == 3) TrackLast = 0 else { printf \ "%s: Unrecognized output-file format - %d fields in header. Exiting.\n", Name,NF > "/dev/stderr" exit 1 } if (!(NF == 3 || NF == 7)) LogBytes = 0 NotRecs++ next } if (Debug) printf "Skipping record: wrong number of fields (%d);"\ " presumably header/trailer line:\n%s\n", NF,Uncontrol($0) > "/dev/stderr" WrongNumFields++ next } # Set these for Common() Object = $NF if (GotLast) Host = $5 if (Common()) { if (LogBytes && $(NF-1) ~ /^[0-9]+$/) Bytes[Index] += $(NF - 1) Count[Index] += $1 if (TrackLast) LastXferDate[Index] = Date() } next } # For each access line, the count of the object accessed in incremented, and # the line is added to the raw logs of any users who have requested a raw log. NF >= MinFields && toupper($Op_i) in Methods { # Skip failed requests. # Search for response code starting at SC_i_start. If object contains # embedded spaces, response code will be shifted to a higher field. if (SC_i_start) { # Check up through field NF - 1; some logs do not contain referrer or agent for (SC_i = SC_i_start; SC_i < NF; SC_i++) { if (Debug > 6 && SC_i > SC_i_start) printf "Record %d: searching field %d for status...\n", NR,SC_i > "/dev/stderr" if ($SC_i ~ /^[0-9][0-9][0-9]$/) break } if (Debug) { if (Debug > 1 && SC_i > SC_i_start) printf "Record %d: found status (%s) in field %d, not %d\n", NR,$SC_i,SC_i,SC_i_start > "/dev/stderr" if (!($SC_i in StatusCodes)) printf "Record %d: Unknown status code: %s\n%s\n", NR,$SC_i,$0 > "/dev/stderr" } if (SC_i == NF || $SC_i ~ /^[45]/) { if (SC_i == NF) { if (Debug > 2) printf "Skipping record %d: couldn't find status:\n%s\n", NR,Uncontrol($0) > "/dev/stderr" NoStatus++ } else HTTPFailCount++ next } Bytes_i = SC_i+1 } Object = UnWeb(CGIstrip($URL_i),1) Host = $Host_i if (!Common()) next if (Counting) { ETime = EntryTime() if (DayFracs) DayFracLog[int(ETime%86400/BucketSecs)]++ if (DoDayOfWeek) # Jan 1 1969 was a Wednesday. Add 3 to make that be at index 3. DayOfWeekLog[(int(ETime/86400)+3)%7]++ if (Interval) { IntLog[int(ETime/Interval)]++ if (Debug) printf "Incrementing bucket %d\n",int(ETime/Interval) \ > "/dev/stderr" } next } if (!CountHosts) { if (Debug > 2 && !(Index in Count)) printf "New index (total now %d): %s\n", ++numInd,Uncontrol(Index) > "/dev/stderr" Count[Index]++ } else if (!((Object,Host) in TrackPairs)) { TrackPairs[Object,Host] Count[Index]++ } else if (Debug) printf "Already saw %s accessed from %s\n",Uncontrol(Object), Uncontrol(Host) > "/dev/stderr" EndDate = Date() if (AllAccesses) Accesses[Object] = Accesses[Object] "\n\t" EndDate " " Host if (TrackLast) LastXferDate[Index] = EndDate if (ChkRaw) Log(Object,MaybeRaw,Reports,RawSet,$0) if (LogBytes && $Bytes_i ~ /^[0-9]+$/) Bytes[Index] += $Bytes_i next } Debug { printf \ "Record %d skipped; not enough fields or wrong method.\n"\ "%d fields; method index = %d; method = <%s>. Line follows:\n%s\n", NR,NF,Op_i,Uncontrol($Op_i),Uncontrol($0) > "/dev/stderr" WrongNumFields++ } ##### End of logging functions. ##### Start of report generating functions. END { if (Err != "") exit Err if (Progress && NR >= 1000) printf "\r%d\n",NR > "/dev/stderr" if (Debug) { print "Finished reading input." > "/dev/stderr" printf \ "Records skipped:\n"\ "%6d header/trailer lines.\n"\ "%6d failed transfer records.\n"\ "%6d host/object pattern reject.\n"\ "%6d wrong number of fields or uncounted method field.\n"\ "%6d status field not found.\n", NotRecs,HTTPFailCount,NotMatch,WrongNumFields,NoStatus > "/dev/stderr" } if (Counting) { if (Interval) { PrintIntervalData() ReptPrinted = 1 } if (DayFracs) { if (ReptPrinted) print "" PrintBuckets(DayFracLog,DayFracs-1,BucketSecs,"time of day", FirstTime,LastTime) ReptPrinted = 1 } if (DoDayOfWeek) { if (ReptPrinted) print "" PrintBuckets(DayOfWeekLog,6,86400,"day of week",FirstTime,LastTime) } exit(0) } # Sort all objects by access count once, so that it doesn't have to be # done for each user. if (Debug) print "Sorting..." > "/dev/stderr" if (LogBytes && SortBytes) Num = qsortArbIndByValue(Bytes,k) else Num = qsortArbIndByValue(Count,k) if (Debug) printf "Sorted %d object(s).\n",Num > "/dev/stderr" if (ReportAll) { CopySet(LevelSet,NotRawSet) SubtractSet(NotRawSet,RawSet) delete NotRawSet["none"] if (Num > 0) { # Generate the non-raw reports to be sent to users. for (i = Num; i >= 1; i--) { Object = k[i] Log(Object,MaybeNotRaw,Reports,NotRawSet,i) if (LogBytes && (Object in Bytes) && Bytes[Object] > MaxBytes) MaxBytes = Bytes[Object] } # Mail reports to users. GenReports(UserLevelSet,MailNames,Count[k[Num]],MaxBytes,UseSubmit, Sender) } } else { if (Num > 0) { if (LogBytes) for (i = 1; i <= Num; i++) if (k[i] in Bytes && (Bytes[k[i]]+0) > MaxBytes) MaxBytes = b Header = MkHeader(StartDate,EndDate,Width,Count[k[Num]],MaxBytes, TrackLast) if (Debug) printf "Got header: %s\n",Header > "/dev/stderr" if (DoHeader) { # The ObjPattern stuff is here mainly so the feature wherein # the user's name is included in the title can be partially # tested without -r if (ObjPattern ~ "\\^/~[a-z][-a-z0-9]+/$") { User = substr(ObjPattern,4) sub("/","",User) } else User = "" print Title(StartDate,EndDate,HTMLout,User) print Header } Total = 0 if (MostAccessed) { for (i = Num; i >= 1; i--) { Object = k[i] if (SStart = match(Object,"./")) { # Truncate before the second / BaseDir = substr(Object,1,SStart) if (!(BaseDir in BaseDirs)) { BaseDirs[BaseDir] print FormatLine(i,HTMLout,AllAccesses,TrackLast) } } } } else if (CountOnly) for (i = Num; i >= 1; i--) FormatLine(i) else for (i = Num; i >= 1; i--) print FormatLine(i,HTMLout,AllAccesses,TrackLast) if (Debug) printf "Done with object records.\n" > "/dev/stderr" if (!("b" in Options)) print Trailer(CountLen,Total,NR - Total - NotRecs,TotalBytes, HTMLout) } else print "No http transfers." } } function PrintBuckets(Data,MaxBucket,BucketPeriod,PeriodName,FirstTime,LastTime, i) { printf "Accesses by %s, from %s to %s\n", PeriodName,strftime("%y/%m/%d %T",FirstTime-TZOffset), strftime("%y/%m/%d %T",LastTime-TZOffset) for (i = 0; i <= MaxBucket; i++) printf "%d %d\n",i,Data[i] } # Globals: Interval, FirstTime, LastTime, IntLog[] function PrintIntervalData( i,LTime) { printf "Accesses for each interval of %d seconds,\nfrom %s to %s\n", Interval,strftime("%y/%m/%d %T",FirstTime-TZOffset), strftime("%y/%m/%d %T",LastTime-TZOffset) LTime = int(LastTime/Interval) for (i = int(FirstTime/Interval); i <= LTime; i++) printf "%s %d\n",strftime("%y/%m/%d %T",i*Interval-TZOffset),IntLog[i] } # Format & return the object whose information is stored at index i # Also adds its count to Total & its byte count to TotalBytes # HTML: Format line as HTML # Globals used: # Format, TotWidth, Count[], LastXferDate[], LastRemote[], Total, # URLpref function FormatLine(i,HTML,AllAccesses,TrackLast, Object,s) { Object = k[i] if (Debug > 2) printf "Formatting line for object: %s\n", Uncontrol(Object) > "/dev/stderr" Total += Count[Object] if (LogBytes && Object in Bytes) TotalBytes += Bytes[Object] if (HTML) s = sprintf(Format,Count[Object], TrackLast ? LastXferDate[Object] : "", TrackLast ? LastRemote[Object] : "", LogBytes ? (Object in Bytes ? sprintf("%.18g",Bytes[Object]) : "-") \ : "", "" Object "") else s = substr(sprintf(Format,Count[Object], TrackLast ? LastXferDate[Object] : "", TrackLast ? LastRemote[Object] : "", LogBytes ? (Object in Bytes ? sprintf("%.18g",Bytes[Object]) : "-") \ : "",Object), 1,TotWidth) if (AllAccesses) s = s Accesses[Object] return s } # Variables: # StartDate, EndDate: start and end of log, in string form. # Width: line length. # MaxNum: Highest count for any object. # MaxBytes: Highest byte count for any object. # Return value: A format string for printing data lines. # Global variables: # TotWidth is set to the max line length. # CountLen is set to the width of the Count field. function MkHeader(StartDate,EndDate,Width,MaxNum,MaxBytes,TrackLast, BytesFmt,BytesLen,RemoteLen) { CountLen = max(length(sprintf("%s",MaxNum)),3) if (LogBytes) { BytesLen = max(length(sprintf("%.18g",MaxBytes)),5) BytesFmt = "%" BytesLen "s " BytesLen++ if (Debug) printf "Maximum byte value: %.18g\n",MaxBytes > "/dev/stderr" } else BytesFmt = "%.0s" if (Width == 0) RemoteLen = 24 else { RemoteLen = Width - 55 - BytesLen if (RemoteLen < 0) RemoteLen = 0 } if (TrackLast) Format = \ "%" CountLen "s %12s %-" RemoteLen "." RemoteLen "s " BytesFmt "%s" else Format = "%" CountLen "s%.0s%.0s " BytesFmt "%s" if (Width == 0) RemoteLen = TotWidth = 1000 else TotWidth = Width if (Debug) printf "Total width: %s\n",TotWidth > "/dev/stderr" if (TrackObjects) return sprintf(Format,"Num","Last Xfer","Last Host","Bytes","Object") else return sprintf(Format,"Num","Last Xfer","Host","Bytes","Last Object") } # Emit HTML ASCII title (not column headers). function Title(StartDate,EndDate,HTML,User, hdr,URL,ttl) { # This won't be correct in cases where a user is sent a report # that includes info for accesses outside their page, but it's # about the best that can be done. if (User !~ "^[a-z][-a-z0-9]+$") User = "" hdr = sprintf("HTTP accesses%s, %s to %s", (User != "") ? " for " User : "",StartDate,EndDate) if (HTML) { ttl = (User == "") ? hdr : \ sprintf("HTTP accesses for " \ User ", %s to %s",StartDate,EndDate) return "\n\n" hdr "\n\n\n"\ "

" ttl "

"
    }
    else
	return hdr
}

function Trailer(CountLen,TotalAcc,Ignored,TotalBytes,HTML,  Bytes,iInfo) {
    if (Debug && LogBytes)
	printf "%.18g bytes xferred.  Generating trailer...\n",
	TotalBytes > "/dev/stderr"
    if (LogBytes)
	Bytes = sprintf(" (%sB transferred)",i2emet(TotalBytes,6,1,0,1))
    if (Ignored >= 0)
	iInfo = sprintf("; %d records skipped.",Ignored)
    if (HTML)
	return \
	sprintf("
\n

Total Accesses: %d%s%s

\n\n", TotalAcc,Bytes,iInfo) else return sprintf("%*d Total Accesses%s%s",CountLen,TotalAcc,Bytes,iInfo) } # Input variables: # Levs[1..n]: The report levels to check whether a user wants. # Addr: The name of the user to generate a level list for. # ReptLevels[] contains a User,Level index for each report level that a user # requested. # Output variables: # LevSet[]: The set of levels requested by the user. # Return value: space-separated list of report levels requested by user. function MakeLevList(Levs,LevSet,Addr,ReptLevels, LevNum,LevList,Level) { for (LevNum = 1; LevNum in Levs; LevNum++) { Level = Levs[LevNum] if (Level != "none" && (Addr,Level) in ReptLevels) { LevList = LevList " " Level LevSet[Level] } } return substr(LevList,2) } function InitReportMail(recipient,subject,Fields,UseSubmit,Sender, To,Cc,Bcc,Order,SubmitOpts) { # return sprintf("mail -s '%s' %s",subject,recipient) To[1] = recipient Fields["From"] = Sender " (HTTP Report Daemon)" SubmitOpts["t"] # trust author specification SubmitOpts["q"] # do not return undelivered mail SubmitOpts["z"] # do not warn about undelivered mail return InitMail(To,Cc,Bcc,Fields,Order,subject,Sender,UseSubmit,SubmitOpts) } # Globals used: # Width, LevelOrder, StartDate, EndDate, Width, Reports[], Test, Debug, # Options, Subject, Total, TotalBytes function GenReports(ReptLevels,MailNames,MaxNum,MaxBytes,UseSubmit,Sender, i,Level,NumLines,Addr,Levs,LevNum,Header,Line,Cmd,AccLine,testMailAddr,HTMLout, DidTitle,LevSet,ExtraHeaders,LevList,AllAcc,HeaderOrder) { split(LevelOrder,Levs,",") testMailAddr = "M" in Options # For each user who is to get a report for (Addr in MailNames) { # If html is the only level requested, add appropriate headers. if ((LevList = MakeLevList(Levs,LevSet,Addr,ReptLevels)) == "html") { ExtraHeaders["MIME-Version"] = "1.0" ExtraHeaders["Content-Type"] = "text/html; charset=us-ascii" HeaderOrder[1] = "MIME-Version" HeaderOrder[2] = "Content-Type" } else split("",ExtraHeaders) ExtraHeaders["X-HTTPReport-Levels"] = LevList if (!Test) { Cmd = InitReportMail(testMailAddr ? Options["M"] : Addr, Subject (testMailAddr ? " [for " Addr "]" : ""),ExtraHeaders, UseSubmit,Sender) } DidTitle = 0 # For each level that can be requested for (LevNum = 1; LevNum in Levs; LevNum++) { if ((Level = Levs[LevNum]) == "none") continue HTMLout = (Level == "html") # If the user is to get a report at this level... if ((Addr,Level) in ReptLevels) { AllAcc = (Level == "allhits") # "b" option means to not print a header. # We want a header to be printed at the start of the reports, # and also want an HTML format header if an HTML report was # requested. if ((!DidTitle || HTMLout) && !Test && !("b" in Options)) { AccLine = Title(StartDate,EndDate,HTMLout,Addr) if (Debug) print AccLine > "/dev/stderr" print AccLine | Cmd DidTitle = 1 } if (Debug) printf "%s report for %s:\n",Level,Addr > "/dev/stderr" NumLines = ReptLevels[Addr,Level] if (Level == "raw") for (i = 1; i <= NumLines; i++) { Line = Reports[Addr,Level,i] if (Debug) print Line > "/dev/stderr" if (!Test) print Line | Cmd } else { Total = 0 TotalBytes = 0 Header = MkHeader(StartDate,EndDate, (Level == "wide" || HTMLout) ? 0 : Width,MaxNum,MaxBytes, !AllAcc) if (!Test && !("b" in Options)) print Header | Cmd if (Debug) print Header > "/dev/stderr" for (i = 1; i <= NumLines; i++) { Line = FormatLine(Reports[Addr,Level,i],HTMLout,AllAcc, !AllAcc) if (Debug) print Line > "/dev/stderr" if (!Test) print Line | Cmd } if (!Test && !("b" in Options)) print Trailer(CountLen,Total,-1,TotalBytes,HTMLout) | \ Cmd } if (Debug) print "" > "/dev/stderr" if (!Test) print "" | Cmd } else if (Debug) printf "%s report for %s: none.\n",Level,Addr > "/dev/stderr" } if (!Test) close(Cmd) } } function max(a,b) { if (a > b) return a else return b } # Deletes any elements that are in both Minuend and Subtrahend from Minuend. function SubtractSet(Minuend,Subtrahend, Elem) { for (Elem in Subtrahend) delete Minuend[Elem] } function CopySet(From,To, Elem) { for (Elem in From) To[Elem] } function Intersection(A,B,Inter, Elem,Count) { for (Elem in A) if (Elem in B) { Inter[Elem] Count++ } return Count } # @(#) CmdReadLine 95/09/04 # Run Command, read a single line of output from it, then close it. # If Verbose is true, a complaint is issued if the read fails. # Output is returned in $* # The return value from getline is returned. It will be 1 on a successful # read; 0 if no lines were read due because the command produced no output # or could not be run. ERRNO is never set since pipes are run by a shell. function CmdReadLine(Command,Verbose, ret) { if (Debug) { print "* Issuing command: " Command "\n"\ "* Waiting for single line of output..." > "/dev/stderr" } ret = Command | getline if (Verbose && ret != 1) printf "Read from pipe '%s' failed\n",Command # close doesn't return a value under awk, only gawk close(Command) if (Debug) print "* Output: " $0 > "/dev/stderr" return ret } ### Begin utty,id routines # utty: find ttys a user is logged in on. # For each tty User is logged in on, an element is created in TTYs[]. # The index is the name of the tty, with a leading "/dev/". # The value is set to 1 if the user is writable on that tty, 0 if not. # The number of ttys the user is logged in on is returned. function utty(User,TTYs, Cmd,Count) { Cmd = "exec who -T" Count = 0 while ((Cmd | getline) == 1) if ($1 == User) { if ($2 == "+") TTYs[$3] = 1 else TTYs[$3] = 0 Count++ } close(Cmd) return Count } # id returns the user name of the user who owns the current process. # In the array IDs, elements are set as follows: # uid: numeric user id # gid: numeric group id # group: group name, if any # user: user name, if any function id(IDs, Cmd,line,elem) { Cmd = "exec id" Cmd | getline line split(line,elem,"[()=]") close(Cmd) IDs["user"] = elem[3] IDs["gid"] = elem[5] IDs["group"] = elem[6] return IDs["uid"] = elem[2] } ### End utty,id routines ### Start of ProcArgs library # @(#) ProcArgs 1.11 96/12/08 # 92/02/29 john h. dubois iii (john@armory.com) # 93/07/18 Added "#" arg type # 93/09/26 Do not count -h against MinArgs # 94/01/01 Stop scanning at first non-option arg. Added ">" option type. # Removed meaning of "+" or "-" by itself. # 94/03/08 Added & option and *()< option types. # 94/04/02 Added NoRCopt to Opts() # 94/06/11 Mark numeric variables as such. # 94/07/08 Opts(): Do not require any args if h option is given. # 95/01/22 Record options given more than once. Record option num in argv. # 95/06/08 Added ExclusiveOptions(). # 96/01/20 Let rcfiles be a colon-separated list of filenames. # Expand $VARNAME at the start of its filenames. # Let varname=0 and -option- turn off an option. # 96/05/05 Changed meaning of 7th arg to Opts; now can specify exactly how many # of the vars should be searched for in the environment. # Check for duplicate rcfiles. # 96/05/13 Return more specific error values. Note: ProcArgs() and InitOpts() # now return various negatives values on error, not just -1, and # Opts() may set Err to various positive values, not just 1. # Added AllowUnrecOpt. # 96/05/23 Check type given for & option # 96/06/15 Re-port to awk # 96/10/01 Moved file-reading code into ReadConfFile(), so that it can be # used by other functions. # 96/10/15 Added OptChars # 96/11/01 Added exOpts arg to Opts() # 96/11/16 Added ; type # 96/12/08 Added Opt2Set() & Opt2Sets() # 96/12/27 Added CmdLineOpt() # optlist is a string which contains all of the possible command line options. # A character followed by certain characters indicates that the option takes # an argument, with type as follows: # : String argument # ; Non-empty string argument # * Floating point argument # ( Non-negative floating point argument # ) Positive floating point argument # # Integer argument # < Non-negative integer argument # > Positive integer argument # The only difference the type of argument makes is in the runtime argument # error checking that is done. # The & option is a special case used to get numeric options without the # user having to give an option character. It is shorthand for [-+.0-9]. # If & is included in optlist and an option string that begins with one of # these characters is seen, the value given to "&" will include the first # char of the option. & must be followed by a type character other than ":" # or ";". # Note that if e.g. &> is given, an option of -.5 will produce an error. # Strings in argv[] which begin with "-" or "+" are taken to be # strings of options, except that a string which consists solely of "-" # or "+" is taken to be a non-option string; like other non-option strings, # it stops the scanning of argv and is left in argv[]. # An argument of "--" or "++" also stops the scanning of argv[] but is removed. # If an option takes an argument, the argument may either immediately # follow it or be given separately. # "-" and "+" options are treated the same. "+" is allowed because most awks # take any -options to be arguments to themselves. gawk 2.15 was enhanced to # stop scanning when it encounters an unrecognized option, though until 2.15.5 # this feature had a flaw that caused problems in some cases. See the OptChars # parameter to explicitly set the option-specifier characters. # If an option that does not take an argument is given, # an index with its name is created in Options and its value is set to the # number of times it occurs in argv[]. # If an option that does take an argument is given, an index with its name is # created in Options and its value is set to the value of the argument given # for it, and Options[option-name,"count"] is (initially) set to the 1. # If an option that takes an argument is given more than once, # Options[option-name,"count"] is incremented, and the value is assigned to # the index (option-name,instance) where instance is 2 for the second occurance # of the option, etc. # In other words, the first time an option with a value is encountered, the # value is assigned to an index consisting only of its name; for any further # occurances of the option, the value index has an extra (count) dimension. # The sequence number for each option found in argv[] is stored in # Options[option-name,"num",instance], where instance is 1 for the first # occurance of the option, etc. The sequence number starts at 1 and is # incremented for each option, both those that have a value and those that # do not. Options set from a config file have a value of 0 assigned to this. # Options and their arguments are deleted from argv. # Note that this means that there may be gaps left in the indices of argv[]. # If compress is nonzero, argv[] is packed by moving its elements so that # they have contiguous integer indices starting with 0. # Option processing will stop with the first unrecognized option, just as # though -- was given except that unlike -- the unrecognized option will not be # removed from ARGV[]. Normally, an error value is returned in this case. # If AllowUnrecOpt is true, it is not an error for an unrecognized option to # be found, so the number of remaining arguments is returned instead. # If OptChars is not a null string, it is the set of characters that indicate # that an argument is an option string if the string begins with one of the # characters. A string consisting solely of two of the same option-indicator # characters stops the scanning of argv[]. The default is "-+". # argv[0] is not examined. # The number of arguments left in argc is returned. # If an error occurs, the global string OptErr is set to an error message # and a negative value is returned. # Current error values: # -1: option that required an argument did not get it. # -2: argument of incorrect type supplied for an option. # -3: unrecognized (invalid) option. function ProcArgs(argc,argv,OptList,Options,compress,AllowUnrecOpt,OptChars, ArgNum,ArgsLeft,Arg,ArgLen,ArgInd,Option,Pos,NumOpt,Value,HadValue,specGiven, NeedNextOpt,GotValue,OptionNum,Escape,dest,src,count,c,OptTerm,OptCharSet) { # ArgNum is the index of the argument being processed. # ArgsLeft is the number of arguments left in argv. # Arg is the argument being processed. # ArgLen is the length of the argument being processed. # ArgInd is the position of the character in Arg being processed. # Option is the character in Arg being processed. # Pos is the position in OptList of the option being processed. # NumOpt is true if a numeric option may be given. ArgsLeft = argc NumOpt = index(OptList,"&") OptionNum = 0 if (OptChars == "") OptChars = "-+" while (OptChars != "") { c = substr(OptChars,1,1) OptChars = substr(OptChars,2) OptCharSet[c] OptTerm[c c] } for (ArgNum = 1; ArgNum < argc; ArgNum++) { Arg = argv[ArgNum] if (length(Arg) < 2 || !((specGiven = substr(Arg,1,1)) in OptCharSet)) break # Not an option; quit if (Arg in OptTerm) { delete argv[ArgNum] ArgsLeft-- break } ArgLen = length(Arg) for (ArgInd = 2; ArgInd <= ArgLen; ArgInd++) { Option = substr(Arg,ArgInd,1) if (NumOpt && Option ~ /[-+.0-9]/) { # If this option is a numeric option, make its flag be & and # its option string flag position be the position of & in # the option string. Option = "&" Pos = NumOpt # Prefix Arg with a char so that ArgInd will point to the # first char of the numeric option. Arg = "&" Arg ArgLen++ } # Find position of flag in option string, to get its type (if any). # Disallow & as literal flag. else if (!(Pos = index(OptList,Option)) || Option == "&") { if (AllowUnrecOpt) { Escape = 1 break } else { OptErr = "Invalid option: " specGiven Option return -3 } } # Find what the value of the option will be if it takes one. # NeedNextOpt is true if the option specifier is the last char of # this arg, which means that if the option requires a value it is # the next arg. if (NeedNextOpt = (ArgInd >= ArgLen)) { # Value is the next arg if (GotValue = ArgNum + 1 < argc) Value = argv[ArgNum+1] } else { # Value is included with option Value = substr(Arg,ArgInd + 1) GotValue = 1 } if (HadValue = AssignVal(Option,Value,Options, substr(OptList,Pos + 1,1),GotValue,"",++OptionNum,!NeedNextOpt, specGiven)) { if (HadValue < 0) # error occured return HadValue if (HadValue == 2) ArgInd++ # Account for the single-char value we used. else { if (NeedNextOpt) { # option took next arg as value delete argv[++ArgNum] ArgsLeft-- } break # This option has been used up } } } if (Escape) break # Do not delete arg until after processing of it, so that if it is not # recognized it can be left in ARGV[]. delete argv[ArgNum] ArgsLeft-- } if (compress != 0) { dest = 1 src = argc - ArgsLeft + 1 for (count = ArgsLeft - 1; count; count--) { ARGV[dest] = ARGV[src] dest++ src++ } } return ArgsLeft } # Assignment to values in Options[] occurs only in this function. # Option: Option specifier character. # Value: Value to be assigned to option, if it takes a value. # Options[]: Options array to return values in. # ArgType: Argument type specifier character. # GotValue: Whether any value is available to be assigned to this option. # Name: Name of option being processed. # OptionNum: Number of this option (starting with 1) if set in argv[], # or 0 if it was given in a config file or in the environment. # SingleOpt: true if the value (if any) that is available for this option was # given as part of the same command line arg as the option. Used only for # options from the command line. # specGiven is the option specifier character use, if any (e.g. - or +), # for use in error messages. # Global variables: OptErr # Return value: negative value on error, 0 if option did not require an # argument, 1 if it did & used the whole arg, 2 if it required just one char of # the arg. # Current error values: # -1: Option that required an argument did not get it. # -2: Value of incorrect type supplied for option. # -3: Bad type given for option & function AssignVal(Option,Value,Options,ArgType,GotValue,Name,OptionNum, SingleOpt,specGiven, UsedValue,Err,NumTypes) { # If option takes a value... [ NumTypes = "*()#<>]" if (Option == "&" && ArgType !~ "[" NumTypes) { # ] OptErr = "Bad type given for & option" return -3 } if (UsedValue = (ArgType ~ "[:;" NumTypes)) { # ] if (!GotValue) { if (Name != "") OptErr = "Variable requires a value -- " Name else OptErr = "option requires an argument -- " Option return -1 } if ((Err = CheckType(ArgType,Value,Option,Name,specGiven)) != "") { OptErr = Err return -2 } # Mark this as a numeric variable; will be propogated to Options[] val. if (ArgType != ":" && ArgType != ";") Value += 0 if ((Instance = ++Options[Option,"count"]) > 1) Options[Option,Instance] = Value else Options[Option] = Value } # If this is an environ or rcfile assignment & it was given a value... else if (!OptionNum && Value != "") { UsedValue = 1 # If the value is "0" or "-" and this is the first instance of it, # do not set Options[Option]; this allows an assignment in an rcfile to # turn off an option (for the simple "Option in Options" test) in such # a way that it cannot be turned on in a later file. if (!(Option in Options) && (Value == "0" || Value == "-")) Instance = 1 else Instance = ++Options[Option] # Save the value even though this is a flag Options[Option,Instance] = Value } # If this is a command line flag and has a - following it in the same arg, # it is being turned off. else if (OptionNum && SingleOpt && substr(Value,1,1) == "-") { UsedValue = 2 if (Option in Options) Instance = ++Options[Option] else Instance = 1 Options[Option,Instance] } # If this is a flag assignment without a value, increment the count for the # flag unless it was turned off. The indicator for a flag being turned off # is that the flag index has not been set in Options[] but it has an # instance count. else if (Option in Options || !((Option,1) in Options)) # Increment number of times this flag seen; will inc null value to 1 Instance = ++Options[Option] Options[Option,"num",Instance] = OptionNum return UsedValue } # Option is the option letter # Value is the value being assigned # Name is the var name of the option, if any # ArgType is one of: # : String argument # ; Non-null string argument # * Floating point argument # ( Non-negative floating point argument # ) Positive floating point argument # # Integer argument # < Non-negative integer argument # > Positive integer argument # specGiven is the option specifier character use, if any (e.g. - or +), # for use in error messages. # Returns null on success, err string on error function CheckType(ArgType,Value,Option,Name,specGiven, Err,ErrStr) { if (ArgType == ":") return "" if (ArgType == ";") { if (Value == "") Err = "must be a non-empty string" } # A number begins with optional + or -, and is followed by a string of # digits or a decimal with digits before it, after it, or both else if (Value !~ /^[-+]?([0-9]+|[0-9]*\.[0-9]+|[0-9]+\.)$/) Err = "must be a number" else if (ArgType ~ "[#<>]" && Value ~ /\./) Err = "may not include a fraction" else if (ArgType ~ "[()<>]" && Value < 0) Err = "may not be negative" # ( else if (ArgType ~ "[)>]" && Value == 0) Err = "must be a positive number" if (Err != "") { ErrStr = "Bad value \"" Value "\". Value assigned to " if (Name != "") return ErrStr "variable " substr(Name,1,1) " " Err else { if (Option == "&") Option = Value return ErrStr "option " specGiven substr(Option,1,1) " " Err } } else return "" } # Note: only the above functions are needed by ProcArgs. # The rest of these functions call ProcArgs() and also do other # option-processing stuff. # Opts: Process command line arguments. # Opts processes command line arguments using ProcArgs() # and checks for errors. If an error occurs, a message is printed # and the program is exited. # # Input variables: # Name is the name of the program, for error messages. # Usage is a usage message, for error messages. # OptList the option description string, as used by ProcArgs(). # MinArgs is the minimum number of non-option arguments that this # program should have, non including ARGV[0] and +h. # If the program does not require any non-option arguments, # MinArgs should be omitted or given as 0. # rcFiles, if given, is a colon-seprated list of filenames to read for # variable initialization. If a filename begins with ~/, the ~ is replaced # by the value of the environment variable HOME. If a filename begins with # $, the part from the character after the $ up until (but not including) # the first character not in [a-zA-Z0-9_] will be searched for in the # environment; if found its value will be substituted, if not the filename will # be discarded. # rcfiles are read in the order given. # Values given in them will not override values given on the command line, # and values given in later files will not override those set in earlier # files, because AssignVal() will store each with a different instance index. # The first instance of each variable, either on the command line or in an # rcfile, will be stored with no instance index, and this is the value # normally used by programs that call this function. # VarNames is a comma-separated list of variable names to map to options, # in the same order as the options are given in OptList. # If EnvSearch is given and nonzero, the first EnvSearch variables will also be # searched for in the environment. If set to -1, all values will be searched # for in the environment. Values given in the environment will override # those given in the rcfiles but not those given on the command line. # NoRCopt, if given, is an additional letter option that if given on the # command line prevents the rcfiles from being read. # See ProcArgs() for a description of AllowUnRecOpt and optChars, and # ExclusiveOptions() for a description of exOpts. # Special options: # If x is made an option and is given, some debugging info is output. # h is assumed to be the help option. # Global variables: # The command line arguments are taken from ARGV[]. # The arguments that are option specifiers and values are removed from # ARGV[], leaving only ARGV[0] and the non-option arguments. # The number of elements in ARGV[] should be in ARGC. # After processing, ARGC is set to the number of elements left in ARGV[]. # The option values are put in Options[]. # On error, Err is set to a positive integer value so it can be checked for in # an END block. # Return value: The number of elements left in ARGV is returned. # Must keep OptErr global since it may be set by InitOpts(). function Opts(Name,Usage,OptList,MinArgs,rcFiles,VarNames,EnvSearch,NoRCopt, AllowUnrecOpt,optChars,exOpts, ArgsLeft,e) { if (MinArgs == "") MinArgs = 0 ArgsLeft = ProcArgs(ARGC,ARGV,OptList NoRCopt,Options,1,AllowUnrecOpt, optChars) if (ArgsLeft < (MinArgs+1) && !("h" in Options)) { if (ArgsLeft >= 0) { OptErr = "Not enough arguments" Err = 4 } else Err = -ArgsLeft printf "%s: %s.\nUse -h for help.\n%s\n", Name,OptErr,Usage > "/dev/stderr" exit 1 } if (rcFiles != "" && (NoRCopt == "" || !(NoRCopt in Options)) && (e = InitOpts(rcFiles,Options,OptList,VarNames,EnvSearch)) < 0) { print Name ": " OptErr ".\nUse -h for help." > "/dev/stderr" Err = -e exit 1 } if ((exOpts != "") && ((OptErr = ExclusiveOptions(exOpts,Options)) != "")) { printf "%s: Error: %s\n",Name,OptErr > "/dev/stderr" Err = 1 exit 1 } return ArgsLeft } # ReadConfFile(): Read a file containing var/value assignments, in the form # . # Whitespace (spaces and tabs) around a variable (leading whitespace on the # line and whitespace between the variable name and the assignment character) # is stripped. Lines that do not contain an assignment operator or which # contain a null variable name are ignored, other than possibly being noted in # the return value. If more than one assignment is made to a variable, the # first assignment is used. # Input variables: # File is the file to read. # Comment is the line-comment character. If it is found as the first non- # whitespace character on a line, the line is ignored. # Assign is the assignment string. The first instance of Assign on a line # separates the variable name from its value. # If StripWhite is true, whitespace around the value (whitespace between the # assignment char and trailing whitespace on the line) is stripped. # VarPat is a pattern that variable names must match. # Example: "^[a-zA-Z][a-zA-Z0-9]+$" # If FlagsOK is true, variables are allowed to be "set" by being put alone on # a line; no assignment operator is needed. These variables are set in # the output array with a null value. Lines containing nothing but # whitespace are still ignored. # Output variables: # Values[] contains the assignments, with the indexes being the variable names # and the values being the assigned values. # Lines[] contains the line number that each variable occured on. A flag set # is record by giving it an index in Lines[] but not in Values[]. # Return value: # If any errors occur, a string consisting of descriptions of the errors # separated by newlines is returned. In no case will the string start with a # numeric value. If no errors occur, the number of lines read is returned. function ReadConfigFile(Values,Lines,File,Comment,Assign,StripWhite,VarPat, FlagsOK, Line,Status,Errs,AssignLen,LineNum,Var,Val) { if (Comment != "") Comment = "^" Comment AssignLen = length(Assign) if (VarPat == "") VarPat = "." # null varname not allowed while ((Status = (getline Line < File)) == 1) { LineNum++ sub("^[ \t]+","",Line) if (Line == "") # blank line continue if (Comment != "" && Line ~ Comment) continue if (Pos = index(Line,Assign)) { Var = substr(Line,1,Pos-1) Val = substr(Line,Pos+AssignLen) if (StripWhite) { sub("^[ \t]+","",Val) sub("[ \t]+$","",Val) } } else { Var = Line # If no value, var is entire line Val = "" } if (!FlagsOK && Val == "") { Errs = Errs \ sprintf("\nBad assignment on line %d of file %s: %s", LineNum,File,Line) continue } sub("[ \t]+$","",Var) if (Var !~ VarPat) { Errs = Errs sprintf("\nBad variable name on line %d of file %s: %s", LineNum,File,Var) continue } if (!(Var in Lines)) { Lines[Var] = LineNum if (Pos) Values[Var] = Val } } if (Status) Errs = Errs "\nCould not read file " File close(File) return Errs == "" ? LineNum : substr(Errs,2) # Skip first newline } # Variables: # Data is stored in Options[]. # rcFiles, OptList, VarNames, and EnvSearch are as as described for Opts(). # Global vars: # Sets OptErr. Uses ENVIRON[]. # If anything is read from any of the rcfiles, sets READ_RCFILE to 1. function InitOpts(rcFiles,Options,OptList,VarNames,EnvSearch, Line,Var,Pos,Vars,Map,CharOpt,NumVars,TypesInd,Types,Type,Ret,i,rcFile, fNames,numrcFiles,filesRead,Err,Values,retStr) { split("",filesRead,"") # make awk know this is an array NumVars = split(VarNames,Vars,",") TypesInd = Ret = 0 if (EnvSearch == -1) EnvSearch = NumVars for (i = 1; i <= NumVars; i++) { Var = Vars[i] CharOpt = substr(OptList,++TypesInd,1) if (CharOpt ~ "^[:;*()#<>&]$") CharOpt = substr(OptList,++TypesInd,1) Map[Var] = CharOpt Types[Var] = Type = substr(OptList,TypesInd+1,1) # Do not overwrite entries from environment if (i <= EnvSearch && Var in ENVIRON && (Err = AssignVal(CharOpt,ENVIRON[Var],Options,Type,1,Var,0)) < 0) return Err } numrcFiles = split(rcFiles,fNames,":") for (i = 1; i <= numrcFiles; i++) { rcFile = fNames[i] if (rcFile ~ "^~/") rcFile = ENVIRON["HOME"] substr(rcFile,2) else if (rcFile ~ /^\$/) { rcFile = substr(rcFile,2) match(rcFile,"^[a-zA-Z0-9_]*") envvar = substr(rcFile,1,RLENGTH) if (envvar in ENVIRON) rcFile = ENVIRON[envvar] substr(rcFile,RLENGTH+1) else continue } if (rcFile in filesRead) continue # rcfiles are liable to be given more than once, e.g. UHOME and HOME # may be the same filesRead[rcFile] if ("x" in Options) printf "Reading configuration file %s\n",rcFile > "/dev/stderr" retStr = ReadConfigFile(Values,Lines,rcFile,"#","=",0,"",1) if (retStr > 0) READ_RCFILE = 1 else if (ret != "") { OptErr = retStr Ret = -1 } for (Var in Lines) if (Var in Map) { if ((Err = AssignVal(Map[Var], Var in Values ? Values[Var] : "",Options,Types[Var], Var in Values,Var,0)) < 0) return Err } else { OptErr = sprintf(\ "Unknown var \"%s\" assigned to on line %d\nof file %s",Var, Lines[Var],rcFile) Ret = -1 } } if ("x" in Options) for (Var in Map) if (Map[Var] in Options) printf "(%s) %s=%s\n",Map[Var],Var,Options[Map[Var]] > \ "/dev/stderr" else printf "(%s) %s not set\n",Map[Var],Var > "/dev/stderr" return Ret } # OptSets is a semicolon-separated list of sets of option sets. # Within a list of option sets, the option sets are separated by commas. For # each set of sets, if any option in one of the sets is in Options[] AND any # option in one of the other sets is in Options[], an error string is returned. # If no conflicts are found, nothing is returned. # Example: if OptSets = "ab,def,g;i,j", an error will be returned due to # the exclusions presented by the first set of sets (ab,def,g) if: # (a or b is in Options[]) AND (d, e, or f is in Options[]) OR # (a or b is in Options[]) AND (g is in Options) OR # (d, e, or f is in Options[]) AND (g is in Options) # An error will be returned due to the exclusions presented by the second set # of sets (i,j) if: (i is in Options[]) AND (j is in Options[]). # todo: make options given on command line unset options given in config file # todo: that they conflict with. function ExclusiveOptions(OptSets,Options, Sets,SetSet,NumSets,Pos1,Pos2,Len,s1,s2,c1,c2,ErrStr,L1,L2,SetSets,NumSetSets, SetNum,OSetNum) { NumSetSets = split(OptSets,SetSets,";") # For each set of sets... for (SetSet = 1; SetSet <= NumSetSets; SetSet++) { # NumSets is the number of sets in this set of sets. NumSets = split(SetSets[SetSet],Sets,",") # For each set in a set of sets except the last... for (SetNum = 1; SetNum < NumSets; SetNum++) { s1 = Sets[SetNum] L1 = length(s1) for (Pos1 = 1; Pos1 <= L1; Pos1++) # If any of the options in this set was given, check whether # any of the options in the other sets was given. Only check # later sets since earlier sets will have already been checked # against this set. if ((c1 = substr(s1,Pos1,1)) in Options) for (OSetNum = SetNum+1; OSetNum <= NumSets; OSetNum++) { s2 = Sets[OSetNum] L2 = length(s2) for (Pos2 = 1; Pos2 <= L2; Pos2++) if ((c2 = substr(s2,Pos2,1)) in Options) ErrStr = ErrStr "\n"\ sprintf("Cannot give both %s and %s options.", c1,c2) } } } if (ErrStr != "") return substr(ErrStr,2) return "" } # The value of each instance of option Opt that occurs in Options[] is made an # index of Set[]. # The return value is the number of instances of Opt in Options. function Opt2Set(Options,Opt,Set, count) { if (!(Opt in Options)) return 0 Set[Options[Opt]] count = Options[Opt,"count"] for (; count > 1; count--) Set[Options[Opt,count]] return count } # The value of each instance of option Opt that occurs in Options[] that # begins with "!" is made an index of nSet[] (with the ! stripped from it). # Other values are made indexes of Set[]. # The return value is the number of instances of Opt in Options. function Opt2Sets(Options,Opt,Set,nSet, count,aSet,ret) { ret = Opt2Set(Options,Opt,aSet) for (value in aSet) if (substr(value,1,1) == "!") nSet[substr(value,2)] else Set[value] return ret } # Returns true if option Opt was given on the command line. function CmdLineOpt(Options,Opt, i) { for (i = 1; (Opt,"num",i) in Options; i++) if (Options[Opt,"num",i] != 0) return 1 return 0 } ### End of ProcArgs library # @(#) i2met.awk 1.0 96/02/13 # jhdiii 96/01/14 # Convert positive integer value Value to a string at most MaxLen characters # long. This is done by converting the integer to a string of the form n*m, # where m is a metric suffix from: K M G # If Pow2 is true, then each factor of 1K is taken to be 1024; if it is false, # it is taken to be 1000. # MaxLen must be between 4 and 9. # Value may be any integer from 0..maxint # If Units is given, it is the units that Value is passed in, where # Units=1 means Value is in K; Units=2 means Value is in M, etc. function i2met(Value,MaxLen,Pow2,Units, Len,Div) { if (Value == 0) return "0" if (!(1 in Suf)) split("K,M,G,T,P,E,Z,Y",Suf,",") # In both awk & gawk, integer values that can be represented as # machine integers will be printed as integers. # If value can be printed without modification, return it as it, # but with a multiplier suffix if reqd. if ((Len = length(Value Suf[Units])) <= MaxLen) return Value Suf[Units] MaxLen -= 1 # Leave space for suffix Div = Pow2 ? 1024 : 1000 for (Units += 1; Units in Suf; Units++) if (length(int(Value /= Div)) <= MaxLen) break Value = substr(sprintf("%." MaxLen "f",Value),1,MaxLen) if (substr(Value,MaxLen,1) == ".") Value = substr(Value,1,MaxLen-1) return Value Suf[Units] } # @(#) i2emet.awk 1.0 96/02/13 # jhdiii 96/01/27 # Convert numeric value Value to one with the decimal point set according to # engineering convention. In this convention, there is always between 1 and # 3 digits before the decimal point. A metric suffix is attached to retain # the original value. # If Pow2 is true, then each factor of 1K is taken to be 1024; if it is false, # it is taken to be 1000. # If Pow2 is true, MaxLen must be >= 5; if Pow2 is false, MaxLen must be >= 4. # If Units is given, it is the units that Value is passed in, where # Units=0 means Value is in base units; Units=-1 means Value is in milliunits, # Units=1 means Value is in kilounits, etc. # If NoZeroes is true, trailing zeroes in the fractional part are removed. function i2emet(Value,MaxLen,Pow2,Units,NoZeroes, Len,Factor,i,suf2) { if (Value == 0) return "0" if (!(1 in _Suf)) { _MaxUnit = split("K,M,G,T,P,E,Z,Y",_Suf,",") split("m,u,n,p,f,a,z,y",suf2,",") for (i = 1; i in suf2; i++) _Suf[-i] = suf2[i] } # Make sure awk treats all of these as numbers Factor = (Pow2 ? 1024 : 1000)+0 Units += 0 Value += 0 if (Value < 1) for (; Value < 1 && Units > -_MaxUnit; Value *= Factor) Units-- else for (; Value >= Factor && Units < _MaxUnit; Value /= Factor) Units++ if (Units) MaxLen -= 1 # Leave space for suffix # Round reasonably carefully fDig = MaxLen-length(int(Value))-1 if (fDig > 0) Value = sprintf("%." fDig "f",Value)+0 # Turn it back into a number! else Value = int(Value+0.5) # Rounding may have caused rollover of leading digit, making the result # exceed the allowed range (e.g. 999.6 -> 1000) if (Value >= Factor) { Value /= Factor Units++ } if (substr(Value,MaxLen,1) == ".") Value = substr(Value,1,MaxLen-1) # Get rid of trailing "." else Value = substr(Value,1,MaxLen) if (NoZeroes && Value ~ /\..*0$/) sub(/\.?0+$/,"",Value) return Value _Suf[Units] } ### Begin qsort routines # Arr[] is an array of values with arbitrary indices. # k[] is returned with numeric indices 1..n. # The values in k[] are the indices of Arr[], # ordered so that if Arr[] is stepped through # in the order Arr[k[1]] .. Arr[k[n]], it will be stepped # through in order of the values of its elements. # The return value is the number of elements in the arrays (n). function qsortArbIndByValue(Arr,k, ArrInd,ElNum) { ElNum = 0 for (ArrInd in Arr) k[++ElNum] = ArrInd qsortSegment(Arr,k,1,ElNum) return ElNum } # Sort a segment of an array. # Arr[] contains data with arbitrary indices. # k[] has indices 1..nelem, with the indices of arr[] as values. # This function sorts the elements of arr that are pointed to by # k[start..end], swapping the values of elements of k[] so that # when this function returns arr[k[start..end]] will be in order. function qsortSegment(Arr,k,start,end, left,right,sepval,tmp,tmpe,tmps) { # handle two-element case explicitly for a tiny speedup if ((end - start) == 1) { if (Arr[tmps = k[start]] > Arr[tmpe = k[end]]) { k[start] = tmpe k[end] = tmps } return } # Make sure comparisons act on these as numbers left = start+0 right = end+0 sepval = Arr[k[int((left + right) / 2)]] # Make every element <= sepval be to the left of every element > sepval while (left < right) { while (Arr[k[left]] < sepval) left++ while (Arr[k[right]] > sepval) right-- if (left < right) { tmp = k[left] k[left++] = k[right] k[right--] = tmp } } if (left == right) if (Arr[k[left]] < sepval) left++ else right-- if (start < right) qsortSegment(Arr,k,start,right) if (left < end) qsortSegment(Arr,k,left,end) } # Arr[] is an array of values with arbitrary indices. # k[] is returned with numeric indices 1..n. # The values in k are the indices of Arr[], # ordered so that if Arr[] is stepped through # in the order Arr[k[1]] .. Arr[k[n]], it will be stepped # through in order of the values of its indices. # The return value is the number of elements in the arrays (n). # If the indexes are numeric, Numeric should be true, so that they can be # compared as such rather than as strings. Numeric indexes do not have to be # contiguous. function qsortByArbIndex(Arr,k,Numeric, ArrInd,ElNum) { ElNum = 0 if (Numeric) # Indexes do not preserve numeric type, so must be forced for (ArrInd in Arr) k[++ElNum] = ArrInd+0 else for (ArrInd in Arr) k[++ElNum] = ArrInd qsortNumIndByValue(k,1,ElNum) return ElNum } # Arr is an array of elements with contiguous numeric indexes to be sorted # by value. # start and end are the starting and ending indexes of the range to be sorted. function qsortNumIndByValue(Arr,start,end, left,right,sepval,tmp,tmpe,tmps) { # handle two-element case explicitly for a tiny speedup if ((start - end) == 1) { if ((tmps = Arr[start]) > (tmpe = Arr[end])) { Arr[start] = tmpe Arr[end] = tmps } return } left = start+0 right = end+0 sepval = Arr[int((left + right) / 2)] while (left < right) { while (Arr[left] < sepval) left++ while (Arr[right] > sepval) right-- if (left <= right) { tmp = Arr[left] Arr[left++] = Arr[right] Arr[right--] = tmp } } if (start < right) qsortNumIndByValue(Arr,start,right) if (left < end) qsortNumIndByValue(Arr,left,end) } ### End qsort routines ### Begin UnControl routines # @(#) uncontrol.awk 1.1 96/05/29 # 92/11/09 john h. dubois iii (john@armory.com) # 96/05/29 Added octal-only conversion. # Uncontrol(S): Convert control characters in S to symbolic form. # Characters in S with values < 32 and with value 127 are converted to the form # ^X. Characters with value >= 128 are converted to the octal form \0nnn, # where nnn is the octal value of the character. # The resulting string is returned. # If OctalOnly is true, octal numbers are used for all symbolic values instead # of ^X. # Global variables: UncTable[] and char2octal[]. function Uncontrol(S,OctalOnly, i,len,Output) { len = length(S) Output = "" if (!("a" in UncTable)) MakeUncontrolTable() for (i = 1; i <= len; i++) Output = Output \ (OctalOnly ? char2octal[substr(S,i,1)] : UncTable[substr(S,i,1)]) return Output } # MakeUncontrolTable: Make tables for use by Uncontrol(). # Global variables: # UncTable[] is made into a character -> symbolic character lookup table # with characters with values < 32 and with value 127 converted to the form # ^X, and characters with value >= 128 are converted to the octal form \0nnn. # char2octal[] is made into a similar table but with all non-printing chars # in the form \0nnn. function MakeUncontrolTable( i,c) { for (i = 0; i < 32; i++) { UncTable[c = sprintf("%c",i)] = "^" sprintf("%c",i + 64) char2octal[c] = "\\" sprintf("%03o",i) } for (i = 32; i < 127; i++) { c = sprintf("%c",i) char2octal[c] = UncTable[c] = sprintf("%c",i) } UncTable[c = sprintf("%c",127)] = "^?" char2octal[c] = "\\0177" for (i = 128; i < 256; i++) { UncTable[c = sprintf("%c",i)] = "\\" sprintf("%03o",i) char2octal[c] = "\\" sprintf("%03o",i) } } ### End UnControl routines function UnWeb(Value,HexOnly, UnWebbed) { # Do + first, since % conversion may yield plusses if (!HexOnly) gsub(/\+/," ",Value) while (match(Value,/%[a-fA-F0-9][a-fA-F0-9]/)) { UnWebbed = UnWebbed substr(Value,1,RSTART-1) \ sprintf("%c",strtoi(substr(Value,RSTART+1,2),16)) Value = substr(Value,RSTART+3) } UnWebbed = UnWebbed Value return UnWebbed } # @(#) strtol 1.0 96/03/01 # 96/03/01 john h. dubois iii (john@armory.com) # Convert a value in base Base to an integer. function strtoi(S,Base, ret,len,i,conv,digit) { if (Base < 2 || Base > 36) return "" S = tolower(S) len = length(S) conv = substr("0123456789abcdefghijklmnopqrstuvwxyz",1,Base) for (i = 1; i <= len; i++) { if (!(digit = index(conv,substr(S,i,1)))) return "" ret = ret * Base + digit - 1 } return ret } # If Base is 1-36, S is taken to be a number in base Base. # If Base is 16, an initial 0x or 0X is ignored. # If Base is 0, an initial 0x or 0X causes Base to be set to 16; otherwise # Base is set to 10. # If S is empty or contains any characters not appropriate to a number in # base Base, a null string is returned. On success, an integer value is # returned. function strtol(S,Base) { Base += 0 # yes, this is neccessary if (Base < 0 || Base > 36) return "" if (Base == 0) if (S ~ /^0[xX]/) { Base = 16 S = substr(S,3) } else Base = 10 else if (Base == 16 && S ~ /^0[xX]/) S = substr(S,3) return strtoi(S,Base) } ### Start of mail sending routines. # @(#) mail-send.gawk 2.0 97/02/22 # 96/01/29 john h. dubois iii (john@armory.com) # 97/02/15 Rewritten. # # Returns name of cmd to pipe into, ready for body of message. # To[], Cc[], Fields[], and Order[] are as described for header822() # Bcc[] is an additional list of recipients who should not be mentioned in # headers. # If a non-null Subject is passed, as a convenience it is added to Fields[] # before it is passed to header822(). # If a non-null From is passed, it is used as the return address. It does # not affect the headers unless the MTA records it in them. function InitMail(To,Cc,Bcc,Fields,Order,Subject,From,UseSubmit,SubmitOpts, Debug, Recips,i,j,Cmd) { for (i = 1; i in To; i++) Recips[++j] = To[i] for (i = 1; i in Cc; i++) Recips[++j] = Cc[i] for (i = 1; i in Bcc; i++) Recips[++j] = Bcc[i] if (UseSubmit) Cmd = InitSubmit(Recips,From,SubmitOpts,Debug) else Cmd = InitSendmail(Recips,From) if (Cmd ~ "^!") return Cmd if (Subject != "") Fields["Subject"] = Subject printf "%s\n",header822(Fields,To,Cc,Order) | Cmd return Cmd } # Sets up globals _ParamSOpts[] and _NoParamSOpts[] for use as sets of # submit options to do and do not take values. function SubmitParam(Opt,Val, i,c) { if (!("r" in _NoParamSOpts)) { for (i = 1; (c = substr("Wcdhjlmnqrstuvwz",i,1)) != ""; i++) _NoParamSOpts[c] for (i = 1; (c = substr("LUVfghikx",i,1)) != ""; i++) _ParamSOpts[c] } if (Opt in _ParamSOpts) return Opt Val "*" else if (Opt in _NoParamSOpts) return Opt else return "" } # Return value: Command to pipe into. If an invalid submit option is passed, # a null string is returned. # Submit options are: # i* Source channel # h* Source host # t Trust Sender/From line (root/mmdf only) # u Don't trust Sender/From line (add Source-Info line to header) # f* Don't trust Sender/From line; add given text # x* Extract recipient list from named fields (comma-separated list) # RecipList[] will not be used if this option is given. # g* Extract recipient list from named fields and use explicit list # v Report validity of each address given, rather than aborting on any bad # m Deliver to mailbox (default; there used to be a tty option as well). # m is always turned on for the sake of old versions. # l Deliver local mail immediately; overrides mod=reg in MMDF config # n Deliver netmail immediately; overrides mod=reg in MMDF config # w Watch immediate delivery attempts # r Return undelivered mail to submitter when it expires # s Return undelivered mail to address given by 'Sender:' when it expires # q Do not return undelivered mail (discard it when it expires). # Remote systems may still return mail. Use q with null From parameter # for no returns at all. # c If mail is returned, include only a citation of the contents # z Do not send warnings re undelivered (but not expired) mail # d Don't use delay channel. If 1st nameserver use fails, mail is returned # j Used by the delay channel to indicate that submission is by it # k* Specify nameserver timeout. Parameter is in seconds. # W Watch submission. Output is sent to fd 2 # L* Specify logfile (root/mmdf only) # V* Specify logging level (root/mmdf only). FAT TMP GEN BST FST PTR BTR FTR # U* Specify invoker's UID (root only) function InitSubmit(RecipList,From,SubmitOpts,Debug, Cmd,i,c,Opt) { # If not special return handling requested and no return address given, # send returns to submitter if (From == "" && !("s" in SubmitOpts || "q" in SubmitOpts)) SubmitOpts["r"] SubmitOpts["m"] for (Opt in SubmitOpts) if ((c = SubmitParam(Opt,SubmitOpts[Opt])) == "") return "!Error initializing submit: Bad option '" Opt "'." else Cmd = Cmd c if (Cmd != "") Cmd = " -" Cmd Cmd = "exec /usr/mmdf/bin/submit" Cmd if (Debug) { Cmd = "exec tee /dev/tty | " Cmd print "mail submission command: " Cmd > "/dev/stderr" } if (!("r" in SubmitOpts || "s" in SubmitOpts)) print From | Cmd # Explicit return address - maybe empty if (!("x" in SubmitOpts)) { # If explicit addresses may be given for (i = 1; i in RecipList; i++) print RecipList[i] | Cmd print "!" | Cmd # terminate recipient list } return Cmd } function InitSendmail(RecipList,From, ToList,i) { if (From != "") From = " -f '" From "'" for (i = 1; i in RecipList; i++) ToList = ToList " " RecipList[i] return "exec /usr/lib/sendmail" From ToList } # @(#) GetMailHostName 97/02/12 # 91/03/13 jhdiii # 97/02/12 Use hostname if unable to get name from mmdftailor. # Returns the name of the local host that should be used for mail purposes. # If mmdftailor is readable and both MLNAME and MLDOMAIN can be found, uses # MLNAME.MLDOMAIN. If not, uses 'hostname'. # The name is stored in the global _MailHostName for reuse by this function. function GetMailHostName( mlname,mldomain,proc,tailor,oFS,hostname) { if (_MailHostName != "") return _MailHostName tailor = "/usr/mmdf/mmdftailor" oFS = FS FS = " " # normal awk field splitting while ((getline < tailor) == 1) { if ($1 == "MLNAME") mlname = $2 else if ($1 == "MLDOMAIN") mldomain = $2 else continue if (mlname != "" && mldomain != "") { hostname = mlname "." mldomain gsub("\"","",hostname) # in case values are quoted break } } close(tailor) if (hostname == "") { proc = "/usr/bin/hostname" proc | getline hostname close(proc) } FS = oFS _MailHostName = hostname return hostname } # Returns an RFC822 recipient field, wrapped as neccessary, ending with a # newline. function WrapField822(FieldName,Values, Field,Line,len,w,i,val,indentLen,indentStr) { Line = FieldName ":" len = length(Line) indentLen = len+1 for (i = 1; i <= indentLen; i++) indentStr = indentStr " " for (i = 1; i in Values; i++) { val = Values[i] if ((i+1) in Values) val = val "," len += w = length(val)+1 if (len > 79) { Field = Field Line "\n" Line = indentStr val len = w + indentLen - 1 } else Line = Line " " val } return Field Line "\n" } # Create an RFC822-compliant mail header. A blank line is *not* appended; # the returned value ends with a single trailing newline. # Fields[] contains field values, indexed by name. The name is given without # a trailing :. To[] and Cc[] are the recipient lists. # The first line of the header is the Date: field. If there is no Date index # in Fields[], the current date & time in an RFC822-compliant format is used. # The second line of the header is the From: field. If there is no From index # in Fields[], the From: field is made "user@host (name)", where user is the # value of the USER environment variable, or the name from 'id' if it is not # set; host is as described for GetMailHostName(), and name is from the NAME # environment variable. If NAME is not set, (name) is not included in the # From: field. # The next lines of the header give the To: and (optionally) Cc: fields. # To[] and Cc[] should contain values indexed by integers starting with 1. # The fields are built by concatenating these values in the order of their # indices. Header extensions are used as neccessary to keep the length of # each physical line below 80 characters if possible. # After these are added, any other fields are added. # The order they are added in may be specified by assigning the header names to # consecutive integer indexes in Order[], starting with 1. # Any field named in Order[] that does not exist in Fields[] will not be added. # Date, From, To, and Cc should not be given in Order. # After all fields named in Order[] are added, any remaining fields are added. # Field wrapping is not done to any values in Fields. # All elements are removed from Fields[]. # Minimal RFC822 message has From, Date, and either To or Bcc line. function header822(Fields,To,Cc,Order, header,i,field) { header = "Date: " ( ("Date" in Fields) ? Fields["Date"] : strftime("%a, %d %h %Y %T %Z") ) "\nFrom: " delete Fields["Date"] if ("From" in Fields) { header = header Fields["From"] delete Fields["From"] } else { header = header WhoAmI() header = header "@" GetMailHostName() if ("NAME" in ENVIRON) header = header " (" ENVIRON["NAME"] ")" } header = header "\n" WrapField822("To",To) if (1 in Cc) header = header WrapField822("Cc",Cc) for (i = 1; i in Order; i++) if ((field = Order[i]) in Fields) { header = header field ": " Fields[field] "\n" delete Fields[field] } for (field in Fields) { header = header field ": " Fields[field] "\n" delete Fields[field] } return header } ### End of mail sending routines. # WhoAmI 1.0 97/02/14 # 97/02/14 john h. dubois iii (john@armory.com) # WhoAmI: return best attempt at determining what user owns this process. # First, get USER from environment. If that fails, try logname; it gives a # better indication of who the user is than the uid does, since multiple login # names may have the same uid. But, check that the name returned by logname # maps to the process' uid, as utmp may have bogus data or the user may have # su'd. If it doesn't, or logname fails, use the user name returned by id. # For efficiency in multiple invokations, the user name is stored in # _WhoAmI_user for reuse. function WhoAmI( Cmd,line,elem,logname,uiduser,uid,oFS) { if (_WhoAmI_user != "") return _WhoAmI_user if ("USER" in ENVIRON && ENVIRON["USER"] != "") return _WhoAmI_user = ENVIRON["USER"] Cmd = "exec /usr/bin/logname 2>/dev/null" Cmd | getline logname close(Cmd) Cmd = "exec /usr/bin/id" Cmd | getline line close(Cmd) split(line,elem,"[()=]") uiduser = elem[3] if (logname == uiduser) return _WhoAmI_user = logname uid = elem[2] oFS = FS FS = ":" while ((getline < "/etc/passwd") == 1) if ($1 == logname) { if ($3 == uid) return _WhoAmI_user = logname break } return _WhoAmI_user = uiduser }