Skip to content
Permalink
main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
executable file 1113 lines (914 sloc) 30.3 KB
#!/usr/bin/env lua
--
-- public
-- netcat text://protocol client
--
-- # textprint() { public "$1" 2>/dev/null; }
-- # textsource() { public "$1" 2>/dev/null | tee; }
-- # textlog() { public "$1" 1>/dev/null ; }
--
local arg = arg
local assert = assert
local io = require( 'io' )
local ipairs = ipairs
local math = require( 'math' )
local os = require( 'os' )
local pcall = pcall
local print = print
local string = require( 'string' )
local table = require( 'table' )
local tonumber = tonumber
local tostring = tostring
_ENV = nil
local _url = nil
local _host = nil
local _port = nil
local _name = nil
local _extension = nil
local _size = 0
local _status = nil
local _address = nil
local function URL()
if not _url then
_url = assert( arg[ 1 ] )
if '-' == _url then
_url = assert( io.stdin:read() )
end
_url = _url:match( '^(%C+)' )
assert( 0 ~= _url:len() )
assert( 1024 >= _url:len() )
assert( not _url:find( '%s' ) )
assert( _url == _url:lower() )
assert( 'text://' == _url:sub( 1, 7 ) )
_host = assert( _url:match( '^text://([%w.-]+)' ) )
_port = tonumber( _url:match( ':([%d]+)/' ) )
_name, _extension = _url:match( '/([%w-]+)%.(%w+)$' )
if '/' == _url:sub( -1, -1 ) then _name = 'index' _extension = 'txt' end
assert( _name )
assert( 0 ~= _name:len() )
assert( 128 >= _name:len() )
assert( _extension )
assert( 0 ~= _extension:len() )
assert( 3 >= _extension:len() )
end
return _url, _host, _port, _name, _extension, _size, _status
end
local function PRINT( aHandle, ... )
assert( aHandle:write( ... ) )
assert( aHandle:write( '\n' ) )
end
local function PRINTERR( ... )
PRINT( io.stderr, ... )
end
local function PRINTOUT( ... )
PRINT( io.stdout, ... )
end
local _controls = { [ '\r' ] = '\\r', [ '\n' ] = '\\n' }
local function LOGCMD( aCommand )
PRINTERR( '# ', ( aCommand:gsub( '%c', _controls ) ) )
end
local function QUOTE( anArgument )
return ( "'%s'" ):format( tostring( anArgument or '' ):gsub( "'", "'\\''" ) )
end
local function CHECKWHICH( aProgram )
local aCommand =
( 'which %s 2>/dev/null' ):format
(
QUOTE( aProgram )
)
--LOGCMD( aCommand )
local aHandle = io.popen( aCommand )
local aPath = aHandle:read()
aHandle:close()
if aPath then
return QUOTE( aPath )
end
return false
end
local _which = {}
local function WHICH( aProgram )
if _which[ assert( aProgram ) ] == nil then _which[ aProgram ] = CHECKWHICH( aProgram ) end
return _which[ aProgram ]
end
local function LOCALE()
local aCommand =
( '%s charmap 2>/dev/null' ):format
(
assert( WHICH( 'locale' ) )
)
LOGCMD( aCommand )
local aHandle = io.popen( aCommand, 'r' )
local aCharmap = assert( aHandle:read() )
assert( aHandle:close() )
assert( aCharmap )
assert( 0 ~= aCharmap:len() )
assert( 16 >= aCharmap:len() )
aCharmap = aCharmap:upper()
return aCharmap
end
local function CHECKTTY()
local aCommand =
( '%s -t 1 2>/dev/null' ):format
(
assert( WHICH( 'test' ) )
)
LOGCMD( aCommand )
local ok, exit, signal = os.execute( aCommand )
return ( ok and exit == 'exit' ) and signal == 0 or false
end
local _istty = nil
local function ISTTY()
if _istty == nil then _istty = CHECKTTY() end
return _istty
end
local function LOG( aMark, aDescription )
if not ISTTY() then return end
assert( aMark )
assert( aDescription )
aDescription = tostring( aDescription )
assert( aDescription )
assert( 0 ~= aDescription:len() )
assert( 144 >= aDescription:len() )
assert( not aDescription:find( '%c' ) )
PRINTOUT( aMark, ' ', aDescription )
end
local function INFO( aDescription )
LOG( '\u{2139}', aDescription )
end
local function WARNING( aDescription )
LOG( '\u{26A0}', aDescription )
end
local function ERROR( aDescription )
LOG( '\u{2716}', aDescription )
end
local function LINE()
if not ISTTY() then return end
PRINTOUT( '\u{2702}', ( '\u{30FB}' ):rep( 10 ) )
end
local function CLEAR()
if not ISTTY() then return end
if not WHICH( 'clear' ) then return end
local aCommand =
( '%s 2>/dev/null' ):format
(
assert( WHICH( 'clear' ) )
)
LOGCMD( aCommand )
assert( os.execute( aCommand ) )
end
local function ADDRESS( aHost )
if not WHICH( 'dig' ) then return end
local aCommand =
( '%s +nocomments +nofail +ignore +short +retry=0 +notcp +time=1 +tries=1 $(%s %s) A 2>/dev/null' ):format
(
assert( WHICH( 'dig' ) ),
assert( WHICH( 'idn2' ) or WHICH( 'printf' ) ),
QUOTE( aHost )
)
LOGCMD( aCommand )
local aHandle = io.popen( aCommand, 'r' )
local anAddress = aHandle:read()
aHandle:close()
if anAddress and ';;' == anAddress:sub( 1, 2 ) then anAddress = nil end
return anAddress
end
local function LOCATION( anAddress )
if not anAddress then return end
if not ISTTY() then return end
if not WHICH( 'mmdbinspect' ) then return end
if not WHICH( 'gron' ) then return end
local aCommand =
( '%s -db %s -db %s -db %s %s 2>/dev/null | %s' ):format
(
assert( WHICH( 'mmdbinspect' ) ),
QUOTE( '/usr/local/var/GeoIP/GeoLite2-ASN.mmdb' ),
QUOTE( '/usr/local/var/GeoIP/GeoLite2-Country.mmdb' ),
QUOTE( '/usr/local/var/GeoIP/GeoLite2-City.mmdb' ),
QUOTE( anAddress ),
assert( WHICH( 'gron' ) )
)
LOGCMD( aCommand )
local aHandle = io.popen( aCommand, 'r' )
local anOrganization = nil
local aCity = nil
local aPostalCode = nil
local anArea = nil
local aCountry = nil
local aBuffer = {}
for aLine in aHandle:lines() do
anOrganization = anOrganization or aLine:match( 'autonomous_system_organization = "(.-)";$' )
aCity = aCity or aLine:match( 'city%.names%.en = "(.-)";$' )
aPostalCode = aPostalCode or aLine:match( 'postal.code = "(.-)";$' )
anArea = anArea or aLine:match( 'subdivisions%[0%]%.names%.en = "(.-)";$' )
aCountry = aCountry or aLine:match( 'country%.names%.en = "(.-)";$' )
end
aHandle:close()
if anArea and aPostalCode then anArea = ( '%s %s' ):format( anArea, aPostalCode ) end
if anOrganization then aBuffer[ #aBuffer + 1 ] = anOrganization:gsub( '%-ASN$', '' ) end
if aCity then aBuffer[ #aBuffer + 1 ] = aCity end
if anArea then aBuffer[ #aBuffer + 1 ] = anArea end
if aCountry then aBuffer[ #aBuffer + 1 ] = aCountry end
INFO( ( '\u{2105} %s' ):format( table.concat( aBuffer, ', ' ) ):upper() )
end
local function PING( aHost )
if not ISTTY() then return end
if not WHICH( 'ping' ) then return end
if not WHICH( 'tail' ) then return end
local aCommand =
( '%s -c1 -q %s 2>/dev/null | %s -1' ):format
(
assert( WHICH( 'ping' ) ),
QUOTE( aHost ),
assert( WHICH( 'tail' ) )
)
LOGCMD( aCommand )
local aHandle = io.popen( aCommand, 'r' )
local aLine = aHandle:read() or ''
aHandle:close()
local aLatency = math.floor( tonumber( aLine:match( ' = (%d+.%d+)/' ) ) or 0 )
local aDescription = ( '\u{29D7} %ims' ):format( aLatency )
if 0 ~= aLatency then INFO( aDescription ) end
end
local function TMPDIR()
local aCommand =
(
'%s -d -q 2>/dev/null'
):format
(
assert( WHICH( 'mktemp' ) )
)
LOGCMD( aCommand )
local aHandle = assert( io.popen( aCommand, 'r' ) )
local aDirectory = assert( aHandle:read() )
assert( aHandle:close() )
return assert( aDirectory )
end
local _directory = nil
local _sequence = 0
local function TMPNAME( aName, anExtension )
_directory = assert( _directory or TMPDIR() )
_sequence = _sequence + 1
assert( 99 > _sequence )
return ( '%s/%02i.%s.%s' ):format( _directory, _sequence, aName, anExtension )
end
local function MKFIFO( aPath )
local aCommand = ( '%s %s 2>/dev/null' ):format
(
WHICH( 'mkfifo' ),
QUOTE( aPath )
)
LOGCMD( aCommand )
assert( os.execute( aCommand ) )
return aPath
end
local function CHUNK( aHandle )
return function() return aHandle:read( 4096 ) end
end
local function TCPCONNECTION( aMultiAddr )
if not aMultiAddr.text:find( '/tcp/%d+$' ) then return end
if not WHICH( 'nc' ) then return end
local aConnection =
( '%s %s %i' ):format
(
assert( WHICH( 'nc' ) ),
QUOTE( aMultiAddr.address ),
aMultiAddr.port
)
return aConnection
end
local function TLSCONNECTION( aMultiAddr )
if not aMultiAddr.text:find( '/tcp/%d+/tls$' ) then return end
if not WHICH( 'openssl' ) then return end
local aConnection =
( '%s s_client -quiet -connect %s' ):format
(
assert( WHICH( 'openssl' ) ),
QUOTE( ( '%s:%i' ):format( aMultiAddr.address, aMultiAddr.port ) )
)
return aConnection
end
local function NOISECONNECTION( aMultiAddr )
if not aMultiAddr.text:find( '/tcp/%d+/noise$' ) then return end
if not WHICH( 'noisecat' ) then return end
local aConnection =
( '%s -v -proto Noise_XX_25519_ChaChaPoly_BLAKE2b %s %i' ):format
(
assert( WHICH( 'noisecat' ) ),
QUOTE( aMultiAddr.address ),
aMultiAddr.port
)
return aConnection
end
local _connectors = { NOISECONNECTION, TLSCONNECTION, TCPCONNECTION }
local function CONNECTION( aMultiAddr )
for _, aConnector in ipairs( _connectors ) do
local aConnection = aConnector( aMultiAddr )
if aConnection then return aConnection end
end
end
local function MULTIADDRBUILD( aText )
if '/ip4/' ~= aText:sub( 1, 5 ) then return end
local anAddress = aText:match( '^/ip4/(.-)/' )
local aPort = tonumber( aText:match( '/tcp/(%d+)' ) )
local aPriority = 0
aText = aText:gsub( ';(%d+)$', function( aValue ) aPriority = tonumber( aValue ) return '' end )
return { address = anAddress, port = aPort, priority = aPriority, text = aText }
end
local function MULTIADDRLOOKUP( aHost )
if not WHICH( 'dig' ) then return end
local aCommand =
( '%s +nocomments +nofail +ignore +short +retry=0 +notcp +time=1 +tries=1 $(%s %s) TXT 2>/dev/null' ):format
(
assert( WHICH( 'dig' ) ),
assert( WHICH( 'idn2' ) or WHICH( 'printf' ) ),
QUOTE( ( '_text._tcp.%s' ):format( aHost ) )
)
LOGCMD( aCommand )
local aHandle = io.popen( aCommand, 'r' )
local aList = {}
for aLine in aHandle:lines() do
local aText = aLine:match( '^"(.+)"$' )
local aMultiAddr = MULTIADDRBUILD( aText )
if aMultiAddr then
aList[ #aList + 1 ] = aMultiAddr
aList[ -aMultiAddr.port ] = aMultiAddr
end
end
table.sort( aList, function( aValue, anotherValue ) return aValue.priority < anotherValue.priority end )
aHandle:close()
return aList
end
local function MULTIADDRDISCOVER( aHost, aPort )
local aList = MULTIADDRLOOKUP( aHost )
if 0 == #aList then return end
if aList[ -( aPort or 0 ) ] then table.insert( aList, 1, aList[ -( aPort or 0 ) ] ) elseif aPort then assert( false ) end
for _, aMultiAddr in ipairs( aList ) do
aMultiAddr.connection = CONNECTION( aMultiAddr )
if aMultiAddr.connection then return aMultiAddr end
end
end
local _ports =
{
[ 1961 ] = '/ip4/{ip4}/tcp/{port}',
[ 1965 ] = '/ip4/{ip4}/tcp/{port}/tls',
[ 1968 ] = '/ip4/{ip4}/tcp/{port}/noise'
}
local function MULTIADDRDEFAULT( aHost, aPort )
local anAddress = ADDRESS( aHost ) or aHost
local aPort = aPort or 1961
local aText = _ports[ aPort ]
local aMultiAddr = {}
if not aText then aText = _ports[ 1 ] end
aText = aText:gsub( '{ip4}', anAddress )
aText = aText:gsub( '{port}', aPort )
aMultiAddr.address = anAddress
aMultiAddr.port = aPort
aMultiAddr.text = aText
aMultiAddr.connection = CONNECTION( aMultiAddr )
return aMultiAddr
end
local function MULTIADDR( aHost, aPort )
local aMultiAddress = MULTIADDRDISCOVER( aHost, aPort ) or MULTIADDRDEFAULT( aHost, aPort )
_address = assert( aMultiAddress.address )
assert( aMultiAddress.text )
assert( aMultiAddress.connection )
return aMultiAddress
end
local function REQUEST()
local aURL, aHost, aPort = URL()
INFO( ( '/dns/%s' ):format( aHost ):upper() )
local aMultiAddr = MULTIADDR( aHost, aPort )
INFO( aMultiAddr.text:upper() )
LOCATION( aMultiAddr.address )
PING( aMultiAddr.address )
local aResultName = MKFIFO( TMPNAME( 'result', 'fifo' ) )
local aCommand =
( '%s 0.5 %s 1>%s 2>/dev/null' ):format
(
assert( WHICH( 'timeout' ) ),
aMultiAddr.connection,
QUOTE( aResultName )
)
LOGCMD( aCommand )
local aHandle = assert( io.popen( aCommand, 'w' ) )
assert( aHandle:write( aURL, '\r\n' ) )
assert( aHandle:flush() )
local aResultHandle = assert( io.open( aResultName, 'r' ) )
local aName = TMPNAME( 'response', 'raw' )
local aFile = io.open( aName, 'wb' )
local aSize = 0
for aChunk in CHUNK( aResultHandle ) do
aSize = aSize + aChunk:len()
assert( aFile:write( aChunk ) )
assert( 1048576 > aSize )
end
assert( aFile:close() )
assert( aResultHandle:close() )
aHandle:close()
return aName
end
local BOLD = 1
local DIM = 2
local UNDERLINE = 4
local function ANSI( aCode, aValue )
local aPattern = string.char( 27 ) .. '[%dm'
assert( aCode == 1 or aCode == 2 or aCode == 4 )
assert( aValue )
return ( '%s%s%s' ):format( aPattern:format( aCode ), aValue, aPattern:format( 0 ) )
end
local function NOANSI( aValue )
return
(
assert( aValue )
:gsub( '\x1b%[%d+;%d+;%d+;%d+;%d+m', '' )
:gsub( '\x1b%[%d+;%d+;%d+;%d+m', '' )
:gsub( '\x1b%[%d+;%d+;%d+m', '' )
:gsub( '\x1b%[%d+;%d+m', '' )
:gsub( '\x1b%[%d+m', '' )
)
end
local function STATUS( aResponseName )
local aResponseFile = assert( io.open( aResponseName, 'r' ) )
local aLine = assert( aResponseFile:read():match( '^(%C+)' ) )
assert( 0 ~= aLine:len() )
assert( 1024 >= aLine:len() )
assert( aLine == NOANSI( aLine ) )
assert( aResponseFile:close() )
local aStatus = assert( tonumber( aLine:match( '^(%d+) ' ) ) )
local aName = TMPNAME( 'status', 'txt' )
local aFile = assert( io.open( aName, 'w' ) )
assert( aFile:write( aLine, '\n' ) )
assert( aFile:close() )
local aCommand =
( '%s --from-code=UTF-8 --to-code=UTF-8 < %s >/dev/null' ):format
(
assert( WHICH( 'iconv' ) ),
QUOTE( aName )
)
LOGCMD( aCommand )
assert( os.execute( aCommand ) )
return aName, aStatus
end
local DISPLAY20TEXTPLAIN = nil
local function COMMA( amount )
local formatted, k = amount
repeat
formatted, k = string.gsub( formatted, '^(-?%d+)(%d%d%d)' , '%1,%2' )
until k == 0
return formatted
end
local function PLURAL( aCount, aSingular, aPlural )
if 0 == aCount then return aSingular end
if 1 == aCount then return aSingular end
return aPlural or ( '%ss' ):format( aSingular )
end
local function DISPLAY20BINARY( aContentName, aType, aCharset )
if not ISTTY() then return false end
local aCommand =
( '%s < %s | %s -b -w64 2>/dev/null' ):format
(
assert( WHICH( 'base64' ) ),
QUOTE( aContentName ),
assert( WHICH( 'fold' ) )
)
LOGCMD( aCommand )
local aHandle = assert( io.popen( aCommand, 'r' ) )
PRINTOUT
(
ANSI( BOLD, '\u{2714} ' ),
ANSI( BOLD, aType:upper() ),
ANSI( DIM, ( ' \u{2014} %s %s ' ):format( COMMA( _size ), PLURAL( _size, 'byte' ) ) )
)
INFO( 'BASE64' )
PRINTOUT()
for aChunk in CHUNK( aHandle ) do
io.stdout:write( aChunk )
end
PRINTOUT()
assert( aHandle:close() )
return true
end
local function HANDLE20BINARY( aContentName, aType, aCharset )
if DISPLAY20BINARY( aContentName, aType, aCharset ) then return true end
local aContentFile = assert( io.open( aContentName, 'rb' ) )
PRINTOUT( ( '20 %s; charset=%s; content-length=%i' ):format( aType, aCharset, _size ) )
for aChunk in CHUNK( aContentFile ) do
assert( io.stdout:write( aChunk ) )
end
assert( aContentFile:close() )
return true
end
local function DISPLAY20APPLICATIONPDF( aContentName, aType, aCharset )
if not ISTTY() then return false end
if not WHICH( 'pdftotext' ) then return false end
local aTextName = TMPNAME( 'pdf', 'txt' )
local aCommand =
( '%s -enc UTF-8 -eol unix -layout -nopgbrk -q - - <%s 1>%s 2>/dev/null' ):format
(
assert( WHICH( 'pdftotext' ) ),
QUOTE( aContentName ),
QUOTE( aTextName )
)
LOGCMD( aCommand )
assert( os.execute( aCommand ) )
return DISPLAY20TEXTPLAIN( aTextName, aType, 'UTF-8' )
end
local function HANDLE20APPLICATIONPDF( aContentName, aType, aCharset )
if DISPLAY20APPLICATIONPDF( aContentName, aType, aCharset ) then return true end
return HANDLE20BINARY( aContentName, aType, aCharset )
end
local function DISPLAY20IMAGE( aContentName, aType, aCharset )
if not ISTTY() then return false end
if not WHICH( 'convert' ) then return false end
if not WHICH( 'jp2a' ) then return false end
local aCommand =
( '%s %s jpg:- 2>/dev/null | jp2a - 2>/dev/null' ):format
(
assert( WHICH( 'convert' ) ),
QUOTE( aContentName ),
assert( WHICH( 'jp2a' ) )
)
LOGCMD( aCommand )
local aHandle = assert( io.popen( aCommand, 'r' ) )
PRINTOUT
(
ANSI( BOLD, '\u{2714} ' ),
ANSI( BOLD, aType:upper() ),
ANSI( DIM, ( ' \u{2014} %s %s ' ):format( COMMA( _size ), PLURAL( _size, 'byte' ) ) )
)
INFO( 'ASCII' )
PRINTOUT()
for aLine in aHandle:lines() do
PRINTOUT( NOANSI( aLine ) )
end
assert( aHandle:close() )
PRINTOUT()
return true
end
local function HANDLE20IMAGE( aContentName, aType, aCharset )
if DISPLAY20IMAGE( aContentName, aType, aCharset ) then return true end
return HANDLE20BINARY( aContentName, aType, aCharset )
end
local function LANGUAGE( aFileName )
if not WHICH( 'franc' ) then return nil end
local aCommand =
( '%s --min-length 1024 < %s 2>/dev/null' ):format
(
assert( WHICH( 'franc' ) ),
QUOTE( aFileName )
)
LOGCMD( aCommand )
local aHandle = assert( io.popen( aCommand, 'r' ) )
local aLanguage = assert( aHandle:read() )
assert( aHandle:close() )
if aLanguage and 'und' == aLanguage then aLanguage = nil end
return aLanguage
end
local function DISPLAYIMAGEDATALINK( aScheme, aLink, aDescription )
if 'data:image/png;base64,' ~= aLink:sub( 1, 22 ) then return false end
if not WHICH( 'base64' ) then return false end
if not WHICH( 'convert' ) then return false end
if not WHICH( 'jp2a' ) then return false end
local aType = assert( aLink:match( '^data:(%w+/[%w.+-]+);' ) )
local aSubType = assert( aLink:match( '^data%:image%/([%w.+-]+)%;base64%,' ) )
local someData = assert( aLink:sub( 23 ) )
local aDataName = TMPNAME( 'data', aSubType )
local aCommand =
( '%s %s | %s --decode 2>/dev/null 1>%s' ):format
(
WHICH( 'printf' ),
QUOTE( someData ),
WHICH( 'base64' ),
QUOTE( aDataName )
)
LOGCMD( aCommand )
assert( os.execute( aCommand ) )
local aDataFile = assert( io.open( aDataName, 'rb' ) )
local aSize = assert( aDataFile:seek( 'end' ) )
assert( aDataFile:close() )
local aCommand =
( '%s %s jpg:- 2>/dev/null | jp2a --border --width=32 - 2>/dev/null' ):format
(
assert( WHICH( 'convert' ) ),
QUOTE( aDataName ),
assert( WHICH( 'jp2a' ) )
)
LOGCMD( aCommand )
local aHandle = assert( io.popen( aCommand, 'r' ) )
PRINTOUT( aDescription )
for aLine in aHandle:lines() do
PRINTOUT( NOANSI( aLine ) )
end
assert( aHandle:close() )
PRINTOUT
(
ANSI
(
DIM,
( '%s \u{2014} %s %s \u{2014} ascii' ):format
(
aType:lower(),
COMMA( aSize ),
PLURAL( aSize, 'byte' )
)
)
)
return true
end
local _datalinkhandles = { image = DISPLAYIMAGEDATALINK }
local function DISPLAYDATALINK( aScheme, aLink, aDescription )
local aType = assert( aLink:match( '^data:(%w+/[%w.+-]+);' ) )
local aMainType = aType:match( '^(%w+)/' )
local aHandler = _datalinkhandles[ aType ] or _datalinkhandles[ aMainType ]
if aHandler and aHandler( aScheme, aLink, aDescription ) then return true end
PRINTOUT
(
( '%s %s' ):format
(
ANSI( UNDERLINE, ( '%s:%s' ):format( aScheme, aType ) ),
aDescription
)
)
return true
end
local _linkhandlers = { data = DISPLAYDATALINK }
local function DISPLAYLINK( aLine )
if '=> ' ~= aLine:sub( 1, 3 ) then return false end
local aLine = aLine:sub( 4 )
local aLink = aLine:match( '^(%S+)' )
local aDescription = aLine:sub( aLink:len() + 2 )
local aScheme = aLink:match( '^([%w.+-]+):' )
local aHandler = _linkhandlers[ aScheme ]
if aHandler and aHandler( aScheme, aLink, aDescription ) then return true end
PRINTOUT( ( '%s %s' ):format( ANSI( UNDERLINE, aLink ), aDescription ) )
return true
end
local function DISPLAYLINE( aLine )
aLine = NOANSI( aLine )
if not DISPLAYLINK( aLine ) then PRINTOUT( aLine ) end
end
DISPLAY20TEXTPLAIN = function( aContentName, aType, aCharset )
if not ISTTY() then return false end
local aFileName = TMPNAME( 'content.utf', 'txt' )
local aCommand =
( '%s --from-code=%s --to-code=UTF-8 < %s 1> %s 2>/dev/null' ):format
(
assert( WHICH( 'iconv' ) ),
QUOTE( aCharset ),
QUOTE( aContentName ),
QUOTE( aFileName )
)
LOGCMD( aCommand )
assert( os.execute( aCommand ) )
local aCommand =
( '%s -l -w -m < %s 2>/dev/null' ):format
(
assert( WHICH( 'wc' ) ),
QUOTE( aFileName )
)
LOGCMD( aCommand )
local aHandle = assert( io.popen( aCommand, 'r' ) )
local aDescription = assert( aHandle:read() )
assert( aHandle:close() )
local someLines, someWords, someChars = aDescription:match( '^%s-(%d+)%s-(%d+)%s-(%d+)%s-$' )
someLines = tonumber( someLines )
someWords = tonumber( someWords )
someChars = tonumber( someChars )
local someBytes = ''
if _size ~= someChars then someBytes = ( ', %s %s ' ):format( COMMA( _size ), PLURAL( _size, 'byte' ) ) end
local aLabel =
( ' \u{2014} %s %s, %s %s, %s %s%s' ):format
(
COMMA( someLines ),
PLURAL( someLines, 'line' ),
COMMA( someWords ),
PLURAL( someLines, 'word' ),
COMMA( someChars ),
PLURAL( someLines, 'character' ),
someBytes
)
PRINTOUT( ANSI( BOLD, '\u{2714} ' ), ANSI( BOLD, aType:upper() ), ANSI( DIM, aLabel ) )
local aLanguage = ( LANGUAGE( aFileName ) or '' ):upper()
INFO( ( 'UTF-8 %s' ):format( aLanguage ) )
PRINTOUT()
for aLine in assert( io.open( aFileName, 'r' ) ):lines() do
DISPLAYLINE( aLine )
end
PRINTOUT()
return true
end
local function HANDLE20TEXTPLAIN( aContentName, aType, aCharset )
if DISPLAY20TEXTPLAIN( aContentName, aType, aCharset ) then return true end
local aContentFile = assert( io.open( aContentName, 'rb' ) )
PRINTOUT( ( '20 %s; charset=%s; content-length=%i' ):format( aType, aCharset, _size ) )
for aChunk in CHUNK( aContentFile ) do
assert( io.stdout:write( aChunk ) )
end
assert( aContentFile:close() )
return true
end
local _handlers20 =
{
[ 'application/pdf' ] = HANDLE20APPLICATIONPDF,
[ 'image' ] = HANDLE20IMAGE,
[ 'text/plain' ] = HANDLE20TEXTPLAIN
}
local function HANDLE20( aResponseName, aStatusName )
local aFile = assert( io.open( aStatusName, 'r' ) )
local aLine = assert( aFile:read() )
assert( aLine )
assert( aFile:close() )
local aResponseType = assert( aLine:match( '^20 (.+)$' ) )
assert( aResponseType )
assert( 0 ~= aResponseType:len() )
assert( 1024 >= aResponseType:len() )
assert( aResponseType == aResponseType:lower() )
local _, _, _, _, anExtension = URL()
local aContentName = TMPNAME( 'content', anExtension )
local aCommand =
( '%s 1d < %s 1>%s 2>/dev/null' ):format
(
assert( WHICH( 'sed' ) ),
QUOTE( aResponseName ),
QUOTE( aContentName )
)
LOGCMD( aCommand )
assert( os.execute( aCommand ) )
local aContentFile = assert( io.open( aContentName, 'rb' ) )
_size = assert( aContentFile:seek( 'end' ) )
assert( aContentFile:seek( 'set' ) )
assert( aContentFile:close() )
local aCommand =
( '%s --brief --mime-type --mime-encoding %s 2>/dev/null' ):format
(
assert( WHICH( 'file' ) ),
QUOTE( aContentName )
)
LOGCMD( aCommand )
local aHandle = assert( io.popen( aCommand, 'r' ) )
local aContentType = assert( aHandle:read() )
assert( aContentType )
assert( 0 ~= aContentType:len() )
assert( 1024 >= aContentType:len() )
assert( aHandle:close() )
local aType = aContentType:match( '^(%w+/[%w.+-]+); ' )
assert( aType )
assert( 0 ~= aType:len() )
assert( 32 >= aType:len() )
local aMainType = aType:match( '^(%w+)/' )
assert( aMainType )
assert( 0 ~= aMainType:len() )
assert( 16 >= aMainType:len() )
local aCharset = aContentType:match( 'charset=([%w-]+)' )
assert( aCharset )
assert( 0 ~= aCharset:len() )
assert( 16 >= aCharset:len() )
local aHandler = _handlers20[ aType ] or _handlers20[ aMainType ] or HANDLE20BINARY
if not aResponseType:find( aContentType, 1, true ) then WARNING( 'INCONSISTENT CONTENT' ) assert( false ) end
aHandler( aContentName, aType, aCharset )
return aContentName
end
local function DISPLAY30( aLink )
if not ISTTY() then return end
PRINTOUT( ANSI( BOLD, '\u{27A1} REDIRECT' ) )
PRINTOUT()
DISPLAYLINE( ( '=> %s' ):format( aLink ) )
PRINTOUT()
end
local function HANDLE30( aResponseName, aStatusName )
local aFile = assert( io.open( aStatusName, 'r' ) )
local aLine = assert( aFile:read() )
assert( aLine )
assert( aFile:close() )
local aLink = assert( aLine:match( '^30 (.+)$' ) )
assert( 0 ~= aLink:len() )
assert( 1024 >= aLink:len() )
assert( not aLink:find( '%s' ) )
assert( aLink == aLink:lower() )
DISPLAY30( aLink )
if ISTTY() then return end
PRINTOUT( aLine )
end
local function DISPLAY40( aDescription )
if not ISTTY() then return end
PRINTOUT( ANSI( BOLD, '\u{2716} ERROR' ) )
PRINTOUT()
PRINTOUT( aDescription )
PRINTOUT()
end
local function HANDLE40( aResponseName, aStatusName )
local aFile = assert( io.open( aStatusName, 'r' ) )
local aLine = assert( aFile:read() )
assert( aLine )
assert( aFile:close() )
local aDescription = assert( aLine:match( '^40 (.+)$' ) )
assert( aDescription )
assert( 0 ~= aDescription:len() )
assert( 1024 >= aDescription:len() )
DISPLAY40( aDescription )
if ISTTY() then return end
PRINTOUT( aLine )
end
local function TIMESTAMP()
if not ISTTY() then return end
if not WHICH( 'date' ) then return end
local aCommand =
( "%s -u +'%%Y-%%m-%%dT%%H:%%M:%%SZ' 2>/dev/null" ):format
(
assert( WHICH( 'date' ) )
)
LOGCMD( aCommand )
local aHandle = assert( io.popen( aCommand, 'r' ) )
local aTimestamp = assert( aHandle:read() )
assert( aHandle:close() )
if aTimestamp then INFO( aTimestamp ) end
end
local function FINGERPRINT( aContentName )
if not ISTTY() then return end
if not aContentName or not WHICH( 'md5sum' ) then return end
local aCommand =
( '%s --binary < %s 2>/dev/null' ):format
(
assert( WHICH( 'md5sum' ) ),
QUOTE( aContentName )
)
LOGCMD( aCommand )
local aHandle = assert( io.popen( aCommand, 'r' ) )
local aBuffer = { assert( aHandle:read() ):sub( 1, 16 ):upper():match( '^(%x%x%x%x)(%x%x%x%x)(%x%x%x%x)(%x%x%x%x)' ) }
assert( aHandle:close() )
if #aBuffer == 4 then INFO( table.concat( aBuffer, ' ' ) ) end
end
local base =
{
'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
}
local function tobase( aNumber, aBase )
assert( aNumber, 'bad argument #1 to \'tobase\' (nil number)' )
assert( aBase and aBase >= 2 and aBase <= #base, 'bad argument #2 to \'tobase\' (base out of range)' )
local abs = math.abs
local floor = math.floor
local aNumber = floor( abs( tonumber( aNumber ) ) )
local aBuffer = {}
repeat
aBuffer[ #aBuffer + 1 ] = base[ ( aNumber % aBase ) + 1 ]
aNumber = floor( aNumber / aBase )
until aNumber == 0
return table.concat( aBuffer ):reverse()
end
local function TAG( anAddress )
if not ISTTY() then return end
if not anAddress then return end
local ip2dec = function(ip) local i, dec=3,0; for d in ip:gmatch('%d+') do dec = dec+2^(8*i)*d; i=i-1 end; return dec end
INFO( tobase( ip2dec( anAddress ), 36 ) )
end
local function COMMONLOG()
local aBuffer = {}
aBuffer[ #aBuffer + 1 ] = _host or _address or '-' -- remotehost
aBuffer[ #aBuffer + 1 ] = '-' -- rfc931
aBuffer[ #aBuffer + 1 ] = '-' -- authuser
aBuffer[ #aBuffer + 1 ] = ( '[%s]' ):format( os.date( '!%Y-%m-%dT%TZ' ) ) -- [date]
aBuffer[ #aBuffer + 1 ] = ( '%q' ):format( _url or '' ) -- "request"
aBuffer[ #aBuffer + 1 ] = tostring( _status or '-' ) -- status
aBuffer[ #aBuffer + 1 ] = tostring( _size or '0' ) -- bytes
PRINTERR( table.concat( aBuffer, ' ' ) )
end
local function CLEANUP()
if not _directory then return end
assert( '.' ~= _directory )
assert( '..' ~= _directory )
assert( '~' ~= _directory )
assert( '/' ~= _directory )
local aCommand =
( '%s -r %s 2>/dev/null' ):format
(
assert( WHICH( 'rm' ) ),
QUOTE( _directory )
)
LOGCMD( aCommand )
assert( os.execute( aCommand ) )
end
local _handlers = { [ 20 ] = HANDLE20, [ 30 ] = HANDLE30, [ 40 ] = HANDLE40 }
local function PROCESS()
if ISTTY() then assert( 'UTF-8' == LOCALE() ) end
CLEAR()
LINE()
local aResponseName = REQUEST()
local aStatusName, aStatus = STATUS( aResponseName )
_status = aStatus
local aHandler = assert( _handlers[ aStatus ] )
LINE()
local aContentName = aHandler( aResponseName, aStatusName )
LINE()
TIMESTAMP()
FINGERPRINT( aContentName )
TAG( _address )
CLEANUP()
COMMONLOG()
end
if pcall( PROCESS ) then os.exit( true ) end
ERROR( 'FAILED' )
LINE()
os.exit( false )