Index: ipOps.lua =================================================================== --- ipOps.lua (revision 29646) +++ ipOps.lua (working copy) @@ -3,17 +3,17 @@ -- -- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html -local bin = require "bin" -local bit = require "bit" -local stdnse = require "stdnse" -local string = require "string" -local table = require "table" local type = type local table = table local string = string local ipairs = ipairs +local pairs = pairs local tonumber = tonumber +local tostring = tostring +local unpack = unpack +local stdnse = require "stdnse" +local bit = require "bit" _ENV = stdnse.module("ipOps", stdnse.seeall) @@ -42,11 +42,13 @@ -- String non-routable address containing the supplied IP address. isPrivate = function( ip ) local err + local sub = string.sub + local match = string.match ip, err = expand_ip( ip ) if err then return nil, err end - if ip:match( ":" ) then + if match(ip, ":") then local is_private local ipv6_private = { "::/127", "FC00::/7", "FE80::/10" } @@ -61,19 +63,19 @@ end end - elseif ip:sub(1,3) == '10.' then + elseif sub(ip,1,3) == '10.' then return true, '10/8' - elseif ip:sub(1,4) == '127.' then + elseif sub(ip,1,4) == '127.' then return true, '127/8' - elseif ip:sub(1,8) == '169.254.' then + elseif sub(ip,1,8) == '169.254.' then return true, '169.254/16' - elseif ip:sub(1,4) == '172.' then + elseif sub(ip,1,4) == '172.' then local p, e = ip_in_range(ip, '172.16/12') if p == true then @@ -82,33 +84,33 @@ return p, e end - elseif ip:sub(1,4) == '192.' then + elseif sub(ip,1,4) == '192.' then - if ip:sub(5,8) == '168.' then + if sub(ip,5,8) == '168.' then return true, '192.168/16' - elseif ip:match('^192%.[0][0]?[0]?%.[0][0]?[0]?%.') then + elseif match(ip, '^192%.[0][0]?[0]?%.[0][0]?[0]?%.') then return true, '192.0.0/24' - elseif ip:match('^192%.[0][0]?[0]?%.[0]?[0]?2') then + elseif match(ip, '^192%.[0][0]?[0]?%.[0]?[0]?2') then return true, '192.0.2/24' end - elseif ip:sub(1,4) == '198.' then + elseif sub(ip,1,4) == '198.' then - if ip:match('^198%.[0]?18%.') or ip:match('^198%.[0]?19%.') then + if match(ip,'^198%.[0]?18%.') or match(ip,'^198%.[0]?19%.') then return true, '198.18/15' - elseif ip:match('^198%.[0]?51%.100%.') then + elseif match(ip,'^198%.[0]?51%.100%.') then return true, '198.51.100/24' end - elseif ip:match('^203%.[0][0]?[0]?%.113%.') then + elseif match(ip,'^203%.[0][0]?[0]?%.113%.') then return true, '203.0.113/24' - elseif ip:match('^224%.[0][0]?[0]?%.[0][0]?[0]?%.') then + elseif match(ip,'^224%.[0][0]?[0]?%.[0][0]?[0]?%.') then return true, '224.0.0/24' - elseif ip:match('^24[0-9]%.') or ip:match('^25[0-5]%.') then + elseif match(ip,'^24[0-9]%.') or match(ip,'^25[0-5]%.') then return true, '240.0.0/4' @@ -127,7 +129,7 @@ -- -- Note: IPv6 addresses are not supported. Currently, numbers in NSE are -- limited to 10^14, and consequently not all IPv6 addresses can be --- represented. Consider using ip_to_str for IPv6 addresses. +-- represented. -- @param ip String representing an IPv4 address. Shortened notation is -- permitted. -- @usage @@ -152,12 +154,12 @@ end --- --- Converts the supplied IPv4 address from a DWORD value into a dotted string. +-- Converts the supplied IPv4 address from a DWORD value into a dotted string. -- --- For example, the address (((a*256+b)*256+c)*256+d) becomes a.b.c.d. +-- For example, the address (((a*256+b)*256+c)*256+d) becomes a.b.c.d. -- ---@param ip DWORD representing an IPv4 address. ---@return The string representing the address. +--@param ip DWORD representing an IPv4 address. +--@return The string representing the address. fromdword = function( ip ) if type( ip ) ~= "number" then stdnse.print_debug(1, "Error in ipOps.todword: Expected IPv4 address.") @@ -212,8 +214,7 @@ --- --- Compares two IP addresses. When comparing addresses from different families, --- IPv4 addresses will sort before IPv6 addresses. +-- Compares two IP addresses (from the same address family). -- @param left String representing an IPv4 or IPv6 address. Shortened -- notation is permitted. -- @param op A comparison operator which may be one of the following @@ -233,36 +234,47 @@ return nil, "Error in ipOps.compare_ip: Expected IP address as a string." end + if ( left:match( ":" ) and not right:match( ":" ) ) then right = expand_ip( right, 'inet6' ) + elseif ( not left:match( ":" ) and right:match( ":" ) ) then left = expand_ip( left, 'inet6' ) + end + + if op == "lt" or op == "le" then + left, right = right, left + elseif op ~= "eq" and op ~= "ge" and op ~= "gt" then + return nil, "Error in ipOps.compare_ip: Invalid Operator." + end + local err ={} - left, err[#err+1] = ip_to_str( left ) - right, err[#err+1] = ip_to_str( right ) + left, err[#err+1] = ip_to_bin( left ) + right, err[#err+1] = ip_to_bin( right ) if #err > 0 then return nil, table.concat( err, " " ) end - if #left > #right then - left = bin.pack( "CA", 0x06, left ) - right = bin.pack( "CA", 0x04, right ) - elseif #right > #left then - right = bin.pack( "CA", 0x06, right ) - left = bin.pack( "CA", 0x04, left ) + if string.len( left ) ~= string.len( right ) then + -- shouldn't happen... + return nil, "Error in ipOps.compare_ip: Binary IP addresses were of different lengths." end - if ( op == "eq" ) then - return ( left == right ) - elseif ( op == "ne" ) then - return ( left ~= right ) - elseif ( op == "le" ) then - return ( left <= right ) - elseif ( op == "ge" ) then - return ( left >= right ) - elseif ( op == "lt" ) then - return ( left < right ) - elseif ( op == "gt" ) then - return ( left > right ) + -- equal? + if ( op == "eq" or op == "le" or op == "ge" ) and left == right then + return true + elseif op == "eq" then + return false end - return nil, "Error in ipOps.compare_ip: Invalid Operator." + -- starting from the leftmost bit, subtract the bit in right from the bit in left + local compare + for i = 1, string.len( left ), 1 do + compare = tonumber( string.sub( left, i, i ) ) - tonumber( string.sub( right, i, i ) ) + if compare == 1 then + return true + elseif compare == -1 then + return false + end + end + return false + end @@ -288,8 +300,8 @@ if err then return nil, err end ip, err = expand_ip( ip ) if err then return nil, err end - if ( ip:match( ":" ) and not first:match( ":" ) ) or ( not ip:match( ":" ) and first:match( ":" ) ) then - return nil, "Error in ipOps.ip_in_range: IP address is of a different address family to Range." + if ( first:match( ":" ) and not ip:match( ":" ) ) then ip = expand_ip( ip, 'inet6' ) + elseif ( not first:match( ":" ) and ip:match( ":" ) ) then first = expand_ip( first, 'inet6' ) end err = {} @@ -318,15 +330,12 @@ -- the IPv4 portion is shortened and does not contain a dot, in which case the -- address will be treated as IPv6. -- @param ip String representing an IPv4 or IPv6 address in shortened or full notation. --- @param family String representing the address family to expand to. Only --- affects IPv4 addresses when "inet6" is provided, causing the function to --- return an IPv4-mapped IPv6 address. -- @usage -- local ip = ipOps.expand_ip( "2001::" ) -- @return String representing a fully expanded IPv4 or IPv6 address (or -- nil in case of an error). -- @return String error message in case of an error. -expand_ip = function( ip, family ) +expand_ip = function( ip ) local err if type( ip ) ~= "string" or ip == "" then @@ -341,7 +350,7 @@ return nil, err4 end local octets = {} - for octet in string.gmatch( ip, "%d+" ) do + for octet in ip:gmatch( "%d+" ) do if tonumber( octet, 10 ) > 255 then return nil, err4 end octets[#octets+1] = octet end @@ -349,20 +358,9 @@ while #octets < 4 do octets[#octets+1] = "0" end - if family == "inet6" then - return ( table.concat( { 0,0,0,0,0,"ffff", - stdnse.tohex( 256*octets[1]+octets[2] ), - stdnse.tohex( 256*octets[3]+octets[4] ) - }, ":" ) ) - else - return ( table.concat( octets, "." ) ) - end + return ( table.concat( octets, "." ) ) end - if family ~= nil and family ~= "inet6" then - return nil, "Error in ipOps.expand_ip: Cannot convert IPv6 address to IPv4" - end - if ip:match( "[^%.:%x]" ) then return nil, ( err4:gsub( "IPv4", "IPv6" ) ) end @@ -372,7 +370,7 @@ -- get a table of each hexadectet local hexadectets = {} - for hdt in string.gmatch( ip, "[%.z%x]+" ) do + for hdt in ip:gmatch( "[%.z%x]+" ) do hexadectets[#hexadectets+1] = hdt end @@ -445,6 +443,9 @@ first, prefix = range:match( "([%x%d:%.]+)/(%d+)" ) elseif range:match( "-" ) then first, last = range:match( "([%x%d:%.]+)%s*%-%s*([%x%d:%.]+)" ) + else + first = range + last = range end local err = {} @@ -460,8 +461,8 @@ end if first and last then - if ( first:match( ":" ) and not last:match( ":" ) ) or ( not first:match( ":" ) and last:match( ":" ) ) then - return nil, nil, "Error in ipOps.get_ips_from_range: First IP address is of a different address family to last IP address." + if ( first:match( ":" ) and not last:match( ":" ) ) then last = expand_ip( last, 'inet6' ) + elseif ( not first:match( ":" ) and last:match( ":" ) ) then first = expand_ip( first, 'inet6' ) end return first, last else @@ -473,6 +474,234 @@ --- +-- Converts a first and last address into a table of ranges. +-- @param first String representing a IPv4 or IPv6 address. +-- @param last String representing a IPv4 or IPv6 address. +-- @usage +-- ranges = ipOps.get_ranges_from_ips( "192.168.0.0", "192.168.255.255" ) +-- @return Table containing the list of ranges in CIDR format +-- (or nil in case of an error). +-- @return String error message in case of an error. +get_ranges_from_ips = function( first, last ) + + if first == last then return { first } end + + local err = {} + first, err[#err+1] = expand_ip( first ) + last, err[#err+1] = expand_ip( last ) + if #err > 0 then + return nil, table.concat( err, " " ) + end + + if first == last then return { first } end + + if compare_ip( first, "gt", last ) then first, last = last, first end + + if ( first:match( ":" ) and not last:match( ":" ) ) then last = expand_ip( last, 'inet6' ) + elseif ( not first:match( ":" ) and last:match( ":" ) ) then first = expand_ip( first, 'inet6' ) + end + + local maxPrefix = 32 + if first:match( ":" ) then maxPrefix = 128 end + + -- find the best net address + local netPrefix = maxPrefix + for cidr = maxPrefix-1, 1, -1 do + local netIP = get_net_ip(first, cidr) + if compare_ip( first, "eq", netIP ) then netPrefix = cidr else break end + end + + -- find the best prefix + local prefix = maxPrefix + local lastIP + for cidr = netPrefix, maxPrefix, 1 do + lastIP = get_last_ip(first, cidr) + if compare_ip( last, "ge", lastIP ) then prefix = cidr break end -- (no else) + end + + -- divide and conquer + local range = first .. '/' .. tostring(prefix) + if prefix == maxPrefix then range = first end + if compare_ip( last, "eq", lastIP ) then return { range } end + + local ip_blocks = {} + ip_blocks, err = get_ranges_from_ips( get_next_ip(lastIP), last ) -- re-run to get the next set of IP blocks + if ip_blocks then + table.insert(ip_blocks, 1, range) + return ip_blocks + else + return nil, err + end + +end + + + +--- +-- Returns a table (possibly combined) with both ranges A and B. +-- @param rangeA String representing a range of IPv4 or IPv6 addresses in either +-- CIDR or first-last notation. Can also be a single IP address. +-- @param rangeB String representing a range of IPv4 or IPv6 addresses in either +-- CIDR or first-last notation. Can also be a single IP address. +-- @usage +-- ranges = ipOps.add_range_to_range( "192.168.0.0/24", "192.168.1.0/24" ) +-- @return Table containing the list of ranges, which could be the exact +-- same addresses (if A is completely detached from B), or nil in +-- case of an error. +-- @return String error message in case of an error. +add_range_to_range = function( rangeA, rangeB ) + + -- ip to range shortcut + local isIPA, isIPB = not rangeA:match( "[/-]" ), not rangeB:match( "[/-]" ) + if isIPA and isIPB then return { rangeA, rangeB } -- (kind of absurd, but...) + elseif isIPA then + local inrange, err = ip_in_range( rangeA, rangeB ) + if err then return nil, err end + + if inrange then return { rangeB } + else return { rangeA, rangeB } + end + elseif isIPB then + local inrange, err = ip_in_range( rangeB, rangeA ) + if err then return nil, err end + + if inrange then return { rangeA } + else return { rangeA, rangeB } + end + end + + local firstA, lastA, firstB, lastB + local err = {} + firstA, lastA, err[#err+1] = get_ips_from_range( rangeA ) + firstB, lastB, err[#err+1] = get_ips_from_range( rangeB ) + if #err > 0 then + return nil, table.concat( err, " " ) + end + + local inrangeAF, inrangeAL, inrangeBF, inrangeBL, nextedgeA, nextedgeB + inrangeAF, err[#err+1] = ip_in_range( firstA, rangeB ) + inrangeAL, err[#err+1] = ip_in_range( lastA, rangeB ) + inrangeBF, err[#err+1] = ip_in_range( firstB, rangeA ) + inrangeBL, err[#err+1] = ip_in_range( lastB, rangeA ) + nextedgeA, err[#err+1] = compare_ip( get_next_ip(lastA), "eq", firstB ) + nextedgeB, err[#err+1] = compare_ip( get_next_ip(lastB), "eq", firstA ) + if #err > 0 then + return nil, table.concat( err, " " ) + end + + -- B fully contains A; B wins + if inrangeAF and inrangeAL then return { rangeB } + -- A fully contains B; A wins + elseif inrangeBF and inrangeBL then return { rangeA } + -- A/B don't even touch each other; separate ranges + elseif not inrangeBF and not inrangeBL then return { rangeA, rangeB } + -- A is on the left edge of B + elseif nextedgeA then return get_ranges_from_ips(firstA, lastB) + -- B is on the left edge of A + elseif nextedgeB then return get_ranges_from_ips(firstB, lastA) + -- Venn diagram; A on left side + elseif inrangeAL and inrangeBF then return get_ranges_from_ips(firstA, lastB) + -- Venn diagram; B on left side + elseif inrangeAF and inrangeBL then return get_ranges_from_ips(firstB, lastA) + end + + return nil, nil -- we should never get here... +end + + + +--- +-- Subtracts IPs in range B from range A (A minus B). +-- @param rangeA String representing a range of IPv4 or IPv6 addresses in either +-- CIDR or first-last notation. Can also be a single IP address. +-- @param rangeB String representing a range of IPv4 or IPv6 addresses in either +-- CIDR or first-last notation. Can also be a single IP address. +-- @usage +-- ranges = ipOps.subtract_range_from_range( "192.168.0.0/24", "192.168.128.0/22" ) +-- @return Table containing the list of ranges, which could be an empty +-- table (if B wholely contains A), or nil in case of an error. +-- @return String error message in case of an error. +subtract_range_from_range = function( rangeA, rangeB ) + + local firstA, lastA, firstB, lastB + local err = {} + firstA, lastA, err[#err+1] = get_ips_from_range( rangeA ) + firstB, lastB, err[#err+1] = get_ips_from_range( rangeB ) + if #err > 0 then + return nil, table.concat( err, " " ) + end + + local inrangeAF, inrangeAL, inrangeBF, inrangeBL, isedgeF, isedgeL + inrangeAF, err[#err+1] = ip_in_range( firstA, rangeB ) + inrangeAL, err[#err+1] = ip_in_range( lastA, rangeB ) + inrangeBF, err[#err+1] = ip_in_range( firstB, rangeA ) + inrangeBL, err[#err+1] = ip_in_range( lastB, rangeA ) + isedgeF, err[#err+1] = compare_ip( firstA, "eq", firstB ) + isedgeL, err[#err+1] = compare_ip( lastA, "eq", lastB ) + if #err > 0 then + return nil, table.concat( err, " " ) + end + + -- B fully contains A; B wins + if inrangeAF and inrangeAL then return { } + -- B isn't even in A; A wins + elseif not inrangeBF and not inrangeBL then return { rangeA } + -- Venn diagram; A on left side + elseif inrangeAL and inrangeBF then return get_ranges_from_ips(firstA, get_prev_ip(firstB)) + -- Venn diagram; B on left side + elseif inrangeAF and inrangeBL then return get_ranges_from_ips(get_next_ip(lastB), lastA) + -- A fully contains B; B is a hole within A + elseif inrangeBF and inrangeBL then + elseif inrangeBF and inrangeBL then + local blocksL, blocksR + if not isedgeF then blocksL, err[#err+1] = get_ranges_from_ips(firstA, get_prev_ip(firstB)) end + if not isedgeL then blocksR, err[#err+1] = get_ranges_from_ips(get_next_ip(lastB), lastA) end + if #err > 0 then + return nil, table.concat( err, " " ) + end + + if isedgeF then return blocksL end + if isedgeL then return blocksR end + return { unpack(blocksL), unpack(blocksR) } + end + + return nil, nil -- we should never get here... +end + +--- +-- Calculates the network IP address given an IP address in +-- the range and prefix length for that range. +-- @param ip String representing an IPv4 or IPv6 address. Shortened +-- notation is permitted. +-- @param prefix Number or a string representing a decimal number corresponding +-- to a prefix length. +-- @usage +-- net = ipOps.get_net_ip( "192.168.85.3", 26 ) +-- @return String representing the network IP address of the range denoted +-- by the supplied parameters (or nil in case of an error). +-- @return String error message in case of an error. +get_net_ip = function( ip, prefix ) + + local first, err = ip_to_bin( ip ) + if err then return nil, err end + + prefix = tonumber( prefix ) + if not prefix or ( prefix < 0 ) or ( prefix > # first ) then + return nil, "Error in ipOps.get_net_ip: Invalid prefix length." + end + + local netbits = first:sub(1, prefix) + local hostbits = ('0'):rep(#first - prefix) + + local net + net, err = bin_to_ip( netbits .. hostbits ) + if err then return nil, err end + return net +end + + + +--- -- Calculates the last IP address of a range of addresses given an IP address in -- the range and prefix length for that range. -- @param ip String representing an IPv4 or IPv6 address. Shortened @@ -490,54 +719,88 @@ if err then return nil, err end prefix = tonumber( prefix ) - if not prefix or ( prefix < 0 ) or ( prefix > # first ) then + if not prefix or ( prefix < 0 ) or ( prefix > #first ) then return nil, "Error in ipOps.get_last_ip: Invalid prefix length." end - local hostbits = string.sub( first, prefix + 1 ) - hostbits = string.gsub( hostbits, "0", "1" ) - local last = string.sub( first, 1, prefix ) .. hostbits + local hostbits = first:sub( prefix + 1 ) + hostbits = hostbits:gsub( "0", "1" ) + + local last = first:sub( 1, prefix ) .. hostbits last, err = bin_to_ip( last ) if err then return nil, err end return last end + + --- --- Converts an IP address into an opaque string. --- @param ip String representing an IPv4 or IPv6 address. --- @param family (optional) Address family to convert to. "ipv6" converts IPv4 --- addresses to IPv4-mapped IPv6. +-- Calculates the previous IP (IP-1) given an IP address. +-- @param ip String representing an IPv4 or IPv6 address. Shortened +-- notation is permitted. -- @usage --- opaque = ipOps.ip_to_str( "192.168.3.4" ) --- @return 4- or 16-byte string representing IP address (or nil +-- next = ipOps.get_prev_ip( "192.168.85.3" ) +-- @return String representing the previous IP address (or nil -- in case of an error). --- @return String error message in case of an error -ip_to_str = function( ip, family ) - local err +-- @return String error message in case of an error. +get_prev_ip = function( ip ) - ip, err = expand_ip( ip, family ) + local bin_ip, err = ip_to_bin( ip ) if err then return nil, err end - local t = {} + for p = #bin_ip, 1, -1 do + -- hit a one; bit can be subtracted and we can stop + if bin_ip:sub(p, p) == '1' then + bin_ip = bin_ip:replace_char('0', p) + break + -- hit a zero; bit will underflow to one and we need to continue + else + bin_ip = bin_ip:replace_char('1', p) + end + end - if not ip:match( ":" ) then - -- ipv4 string - for octet in string.gmatch( ip, "%d+" ) do - t[#t+1] = bin.pack( ">C", tonumber(octet) ) + local prevIP + prevIP, err = bin_to_ip( bin_ip ) + if err then return nil, err end + return prevIP +end + + + +--- +-- Calculates the next IP (IP+1) given an IP address. +-- @param ip String representing an IPv4 or IPv6 address. Shortened +-- notation is permitted. +-- @usage +-- next = ipOps.get_next_ip( "192.168.85.3" ) +-- @return String representing the next IP address (or nil +-- in case of an error). +-- @return String error message in case of an error. +get_next_ip = function( ip ) + + local bin_ip, err = ip_to_bin( ip ) + if err then return nil, err end + + for p = #bin_ip, 1, -1 do + -- hit a zero; will fill bit and we can stop + if bin_ip:sub(p, p) == '0' then + bin_ip = bin_ip:replace_char('1', p) + break + -- hit a one; bit will overflow to zero and we need to continue + else + bin_ip = bin_ip:replace_char('0', p) end - else - -- ipv6 string - for hdt in string.gmatch( ip, "%x+" ) do - t[#t+1] = bin.pack( ">S", tonumber(hdt, 16) ) - end end - - return table.concat( t ) + local nextIP + nextIP, err = bin_to_ip( bin_ip ) + if err then return nil, err end + return nextIP end + --- -- Converts an IP address into a string representing the address as binary -- digits. @@ -572,7 +835,7 @@ -- padding for i, v in ipairs( t ) do - t[i] = mask:sub( 1, # mask - # v ) .. v + t[i] = mask:sub( 1, #mask - #v ) .. v end return hex_to_bin( table.concat( t ) ) @@ -597,9 +860,9 @@ end local af - if # binstring == 32 then + if string.len( binstring ) == 32 then af = 4 - elseif # binstring == 128 then + elseif string.len( binstring ) == 128 then af = 6 else return nil, "Error in ipOps.bin_to_ip: Expected exactly 32 or 128 binary digits." @@ -647,10 +910,63 @@ local t, mask, binchar = {}, "0000" for hexchar in string.gmatch( hex, "%x" ) do binchar = stdnse.tobinary( tonumber( hexchar, 16 ) ) - t[#t+1] = mask:sub( 1, # mask - # binchar ) .. binchar + t[#t+1] = mask:sub( 1, #mask - #binchar ) .. binchar end return table.concat( t ) end +--- +-- Count the number of individual IPs in a set of ranges/IPs. +-- @param ... Any number of strings representing a range or IP address. +-- Non-IP/domain strings count as 1. Tables will be unpacked. Anything else +-- will simply be skipped over. +-- @usage +-- bin_string = ipOps.count_ips( "192.168.2.0/24", "4.4.4.4", ips, ipOps.get_ranges_from_ips(first, last) ) +-- @return Number representing the total count of (valid) IPs +count_ips = function( ... ) + local ips = {...} + + local count = 0 + for _,range in ipairs(ips) do + if type(range) == "string" then + local ip, prefix + if range:match( "/" ) then + ip, prefix = range:match( "([%x%d:\.]+)/(%d+)" ) + prefix = tonumber(prefix) + + local maxPrefix = 32 + if ip:match( ":" ) then maxPrefix = 128 end + + if prefix ~= nil then count = count + 2 ^ (maxPrefix-prefix) + else count = count+1 + end + elseif range:match( "-" ) then + -- let get_ranges_from_ips handle it + local first, last = range:match( "([%x%d:\.]+)%s*\-%s*([%x%d:\.]+)" ) + local ranges = get_ranges_from_ips(first, last) + + if ranges ~= nil then table.push( ips, ranges ) + else count = count+1 + end + + else + count = count+1 + end + elseif type(range) == "table" then + table.push( ips, range ) + end + end + + return count +end + +string.replace_char = function(str, r, pos) + return str:sub(1, pos-1) .. r .. str:sub(pos+1) +end + +table.push = function(tbl, itbl) + for _,i in pairs(itbl) do table.insert(tbl, i) end +end + return _ENV;