├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md └── public /.gitattributes: -------------------------------------------------------------------------------- 1 | public linguist-documentation=false 2 | public linguist-language=Lua 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | public.txt 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 textprotocol 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PUBLIC 2 | [NETCAT](https://en.wikipedia.org/wiki/Netcat "NETCAT") [TEXT://PROTOCOL](https://textprotocol.org "TEXT://PROTOCOL") CLIENT 3 | 4 | ```bash 5 | -- 6 | -- public 7 | -- netcat text://protocol client 8 | -- 9 | -- # textprint() { public "$1" 2>/dev/null; } 10 | -- # textsource() { public "$1" 2>/dev/null | tee; } 11 | -- # textlog() { public "$1" 1>/dev/null ; } 12 | -- 13 | ``` 14 | 15 | ```bash 16 | # textprint text://txt.textprotocol.org/ 17 | ✂・・・・・・・・・・ 18 | ℹ /DNS/TXT.TEXTPROTOCOL.ORG 19 | ℹ /IP4/64.225.108.16/TCP/1968/NOISE 20 | ℹ ℅ DIGITALOCEAN, FRANKFURT AM MAIN, HESSE 60313, GERMANY 21 | ℹ ⧗ 18ms 22 | ✂・・・・・・・・・・ 23 | ✔ TEXT/PLAIN — 9 lines, 20 words, 251 characters, 262 bytes 24 | ℹ UTF-8 25 | 26 | TEXT://PROTOCOL 27 | 28 | geo:37.429167,-122.138056 PALO ALTO, CA 94301, USA 29 | tag:txt.textprotocol.org,2021-03-07:~lablyd-dolben rel=me 30 | text://txt.textprotocol.org/icon.png rel=icon 31 | text://txt.textprotocol.org/license.txt rel=license CC0-1.0 32 | 33 | — 34 | 🆃🆇🆃 35 | 36 | ✂・・・・・・・・・・ 37 | ℹ 2021-07-09T11:05:03Z 38 | ℹ 3DE5 488F 67AB F9CF 39 | ℹ I02NM8 40 | ``` 41 | 42 | ```bash 43 | # textsource text://txt.textprotocol.org/ 44 | 20 text/plain; charset=utf-8; content-length=262 45 | TEXT://PROTOCOL 46 | 47 | => geo:37.429167,-122.138056 PALO ALTO, CA 94301, USA 48 | => tag:txt.textprotocol.org,2021-03-07:~lablyd-dolben rel=me 49 | => text://txt.textprotocol.org/icon.png rel=icon 50 | => text://txt.textprotocol.org/license.txt rel=license CC0-1.0 51 | 52 | — 53 | 🆃🆇🆃 54 | ``` 55 | 56 | ```bash 57 | # textlog text://txt.textprotocol.org/ 58 | # '/bin/test' -t 1 2>/dev/null 59 | # '/usr/bin/dig' +nocomments +nofail +ignore +short +retry=0 +notcp +time=1 +tries=1 $('/usr/local/bin/idn2' '_text._tcp.txt.textprotocol.org') TXT 2>/dev/null 60 | # '/usr/bin/mktemp' -d -q 2>/dev/null 61 | # '/usr/bin/mkfifo' 'tmp.nfReqHV7/01.result.fifo' 2>/dev/null 62 | # '/usr/local/bin/timeout' 0.5 '/usr/local/bin/noisecat' -v -proto Noise_XX_25519_ChaChaPoly_BLAKE2b '64.225.108.16' 1968 1>'tmp.nfReqHV7/01.result.fifo' 2>/dev/null 63 | # '/usr/bin/iconv' --from-code=UTF-8 --to-code=UTF-8 < 'tmp.nfReqHV7/03.status.txt' >/dev/null 64 | # '/usr/bin/sed' 1d < 'tmp.nfReqHV7/02.response.raw' 1>'tmp.nfReqHV7/04.content.txt' 2>/dev/null 65 | # '/usr/bin/file' --brief --mime-type --mime-encoding 'tmp.nfReqHV7/04.content.txt' 2>/dev/null 66 | # '/bin/rm' -r 'tmp.nfReqHV7' 2>/dev/null 67 | txt.textprotocol.org - - [2021-07-09T11:05:42Z] "text://txt.textprotocol.org/" 20 262 68 | ``` 69 | 70 | ```bash 71 | # textprint text://txt.textprotocol.org/elos.pdf 72 | ✂・・・・・・・・・・ 73 | ℹ /DNS/TXT.TEXTPROTOCOL.ORG 74 | ℹ /IP4/64.225.108.16/TCP/1968/NOISE 75 | ℹ ℅ DIGITALOCEAN, FRANKFURT AM MAIN, HESSE 60313, GERMANY 76 | ℹ ⧗ 13ms 77 | ✂・・・・・・・・・・ 78 | ✔ APPLICATION/PDF — 1,868 lines, 11,119 words, 80,088 characters, 157,962 bytes 79 | ℹ UTF-8 ENG 80 | 81 | William Strunk, Jr. 82 | The Elements of Style 83 | 84 | … 85 | 86 | ``` 87 | 88 | ```bash 89 | # textprint text://txt.textprotocol.org/tatepon.png 90 | ✂・・・・・・・・・・ 91 | ℹ /DNS/TXT.TEXTPROTOCOL.ORG 92 | ℹ /IP4/64.225.108.16/TCP/1968/NOISE 93 | ℹ ℅ DIGITALOCEAN, FRANKFURT AM MAIN, HESSE 60313, GERMANY 94 | ℹ ⧗ 18ms 95 | ✂・・・・・・・・・・ 96 | ✔ IMAGE/PNG — 11,967 bytes 97 | ℹ ASCII 98 | 99 | MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 100 | MMMMMMMMMMMMMMMMMo'lNMMW0WMMMMMMMMMMMMMMMMMMMM 101 | MMMMMMMMMMMMMMMMM. .NX. ,MMMMMMMMMMMMMMMMMMMM 102 | MMMMMMMMMMMMN'.lX: :l XMMMMMMMMMMOx0WMMMMM 103 | MMMMMMMMMMMMW, d. : XMMMMMMMMMMX. ;KMMM 104 | MMMMMMNXKNMMMN: c ' .WMMMMMMMMMMM, :NM 105 | MMNd,',;;,',oNMx , .. .lcNMMMM0:lkc . :M 106 | Md.;KMMMMMMK:.kNd . :0WMM0 lWMMXxoM 107 | k dMMMMNWMMMMc c ..'.. .;kWK: 0MMMMMM 108 | :.WMMMO..OMMMK . ,kXk:,',;:; .0MNkl;kOKMMM 109 | l NMN: ,c .lK0 kM0. .k. xMMMx :MM 110 | K ;o..kWMWk:. dMM: l0 OMMX, ,,0X 111 | Mx kWMMMMMM0 0MMx ON cMMK.xMx,O 112 | MMO.:WMMMMMX. cMMM0:. .cXMx ;k:,OMMMMM 113 | MMMX'.0MMMW, , :NMMMMWNWMMWo cxXMMMMMMM 114 | MMMMWc oMMl dx: ;d0XXX0x; .OMMMMMMMMMM 115 | MMMMMMk.:x lMMMO. ;XMMMMMMMMMMM 116 | MMMMMMMK. .NMMMMWk:. .lKMMMMMMMMMMMMM 117 | MMMMMMMMN,0MMMMMMMMMXd :lc lONMMMMMMMMMMMMMMMM 118 | MMMMMMMMMMMMMMMMMMMMMO.WMW.kMMMMMMMMMMMMMMMMMM 119 | MMMMMMMMMMMMMMMMMMMMMk.MMW.xMMMMMMMMMMMMMMMMMM 120 | MMMMMMMMMMMMMMMMMM0cc:cMMW::ccxMMMMMMMMMMMMMMM 121 | MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 122 | 123 | ✂・・・・・・・・・・ 124 | ℹ 2021-07-09T11:07:34Z 125 | ℹ D562 D083 E7B9 6AEB 126 | ℹ I02NM8 127 | ``` 128 | 129 | ```bash 130 | # textprint text://txt.textprotocol.org/data.txt 131 | ✂・・・・・・・・・・ 132 | ℹ /DNS/TXT.TEXTPROTOCOL.ORG 133 | ℹ /IP4/64.225.108.16/TCP/1968/NOISE 134 | ℹ ℅ DIGITALOCEAN, FRANKFURT AM MAIN, HESSE 60313, GERMANY 135 | ℹ ⧗ 18ms 136 | ✂・・・・・・・・・・ 137 | ✔ TEXT/PLAIN — 6 lines, 9 words, 380 characters, 391 bytes 138 | ℹ UTF-8 139 | 140 | With embedded data: URI. 141 | 142 | rel=icon 143 | +--------------------------------+ 144 | | | 145 | | | 146 | | | 147 | | | 148 | | ........ . . ........ | 149 | | .::ckx::;..lc. :o..;::xkc::. | 150 | | .xo .oo,'ld. ox. | 151 | | .xo .dNXx. ox. | 152 | | .xo ;dood: ox. | 153 | | .xo .ld' .dl. ox. | 154 | | ,' .;. .;. '; | 155 | | | 156 | | | 157 | | | 158 | | | 159 | | | 160 | +--------------------------------+ 161 | image/png — 234 bytes — ascii 162 | 163 | — 164 | 🆃🆇🆃 165 | 166 | ✂・・・・・・・・・・ 167 | ℹ 2021-07-09T11:08:09Z 168 | ℹ F7B7 100E ABF4 0A2F 169 | ℹ I02NM8 170 | ``` 171 | 172 | __DEPENDENCIES__ 173 | 174 | [ℹ︎](https://linux.die.net/man/1/base64 "base64(1) - man page")`base64`\ 175 | [ℹ︎](https://linux.die.net/man/1/clear "clear(1) - man page")`clear` _optional_\ 176 | [ℹ︎](https://linux.die.net/man/1/convert "convert(1) - man page")`convert` _optional_\ 177 | [ℹ︎](https://linux.die.net/man/1/date "date(1) - man page")`date` _optional_\ 178 | [ℹ︎](https://linux.die.net/man/1/dig "dig(1) - man page")`dig` _optional_\ 179 | [ℹ︎](https://linux.die.net/man/1/file "file(1) - man page")`file`\ 180 | [ℹ︎](https://linux.die.net/man/1/fold "fold(1) - man page")`fold`\ 181 | [ℹ︎](https://github.com/wooorm/franc "language detection")`franc` _optional_\ 182 | [ℹ︎](https://github.com/tomnomnom/gron "make json greppable")`gron` _optional_\ 183 | [ℹ︎](https://linux.die.net/man/1/iconv "iconv(1) - man page")`iconv`\ 184 | [ℹ︎](https://linux.die.net/man/1/idn2 "idn2(1) - man page")`idn2` _optional_\ 185 | [ℹ︎](https://csl.name/jp2a/ "converts images to ascii")`jp2a` _optional_\ 186 | [ℹ︎](https://linux.die.net/man/1/locale "locale(1) - man page")`locale`\ 187 | [ℹ︎](https://linux.die.net/man/1/lua "lua(1) - man page")__`lua`__\ 188 | [ℹ︎](https://linux.die.net/man/1/md5sum "md5sum(1) - man page")`md5sum` _optional_\ 189 | [ℹ︎](https://linux.die.net/man/1/mkfifo "mkfifo(1) - man page")`mkfifo`\ 190 | [ℹ︎](https://linux.die.net/man/1/mktemp "mktemp(1) - man page")`mktemp`\ 191 | [ℹ︎](https://github.com/maxmind/mmdbinspect "maxmind geoip")`mmdbinspect` _optional_\ 192 | [ℹ︎](https://linux.die.net/man/1/nc "nc(1) - man page")`nc`\ 193 | [ℹ︎](https://github.com/gedigi/noisecat "noisecat")`noisecat` _optional_\ 194 | [ℹ︎](https://linux.die.net/man/1/openssl "openssl(1) - man page")`openssl` _optional_\ 195 | [ℹ︎](https://linux.die.net/man/1/pdftotext "pdftotext(1) - man page")`pdftotext` _optional_\ 196 | [ℹ︎](https://linux.die.net/man/8/ping "ping(8) - man page")`ping` _optional_\ 197 | [ℹ︎](https://linux.die.net/man/1/rm "rm(1) - man page")`rm`\ 198 | [ℹ︎](https://linux.die.net/man/1/sed "sed(1) - man page")`sed`\ 199 | [ℹ︎](https://linux.die.net/man/1/tail "tail(1) - man page")`tail` _optional_\ 200 | [ℹ︎](https://linux.die.net/man/1/test "test(1) - man page")`test`\ 201 | [ℹ︎](https://linux.die.net/man/1/timeout "timeout(1) - man page")`timeout`\ 202 | [ℹ︎](https://linux.die.net/man/1/wc "wc(1) - man page")`wc` 203 | 204 | -------------------------------------------------------------------------------- /public: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | -- 4 | -- public 5 | -- netcat text://protocol client 6 | -- 7 | -- # textprint() { public "$1" 2>/dev/null; } 8 | -- # textsource() { public "$1" 2>/dev/null | tee; } 9 | -- # textlog() { public "$1" 1>/dev/null ; } 10 | -- 11 | 12 | local arg = arg 13 | local assert = assert 14 | local io = require( 'io' ) 15 | local ipairs = ipairs 16 | local math = require( 'math' ) 17 | local os = require( 'os' ) 18 | local pcall = pcall 19 | local print = print 20 | local string = require( 'string' ) 21 | local table = require( 'table' ) 22 | local tonumber = tonumber 23 | local tostring = tostring 24 | 25 | _ENV = nil 26 | 27 | local _url = nil 28 | local _host = nil 29 | local _port = nil 30 | local _name = nil 31 | local _extension = nil 32 | local _size = 0 33 | local _status = nil 34 | local _address = nil 35 | 36 | local function URL() 37 | if not _url then 38 | _url = assert( arg[ 1 ] ) 39 | 40 | if '-' == _url then 41 | _url = assert( io.stdin:read() ) 42 | end 43 | 44 | _url = _url:match( '^(%C+)' ) 45 | 46 | assert( 0 ~= _url:len() ) 47 | assert( 1024 >= _url:len() ) 48 | assert( not _url:find( '%s' ) ) 49 | assert( _url == _url:lower() ) 50 | assert( 'text://' == _url:sub( 1, 7 ) ) 51 | 52 | _host = assert( _url:match( '^text://([%w.-]+)' ) ) 53 | _port = tonumber( _url:match( ':([%d]+)/' ) ) 54 | 55 | _name, _extension = _url:match( '/([%w-]+)%.(%w+)$' ) 56 | 57 | if '/' == _url:sub( -1, -1 ) then _name = 'index' _extension = 'txt' end 58 | 59 | assert( _name ) 60 | assert( 0 ~= _name:len() ) 61 | assert( 128 >= _name:len() ) 62 | assert( _extension ) 63 | assert( 0 ~= _extension:len() ) 64 | assert( 3 >= _extension:len() ) 65 | end 66 | 67 | return _url, _host, _port, _name, _extension, _size, _status 68 | end 69 | 70 | local function PRINT( aHandle, ... ) 71 | assert( aHandle:write( ... ) ) 72 | assert( aHandle:write( '\n' ) ) 73 | end 74 | 75 | local function PRINTERR( ... ) 76 | PRINT( io.stderr, ... ) 77 | end 78 | 79 | local function PRINTOUT( ... ) 80 | PRINT( io.stdout, ... ) 81 | end 82 | 83 | local _controls = { [ '\r' ] = '\\r', [ '\n' ] = '\\n' } 84 | 85 | local function LOGCMD( aCommand ) 86 | PRINTERR( '# ', ( aCommand:gsub( '%c', _controls ) ) ) 87 | end 88 | 89 | local function QUOTE( anArgument ) 90 | return ( "'%s'" ):format( tostring( anArgument or '' ):gsub( "'", "'\\''" ) ) 91 | end 92 | 93 | local function CHECKWHICH( aProgram ) 94 | local aCommand = 95 | ( 'which %s 2>/dev/null' ):format 96 | ( 97 | QUOTE( aProgram ) 98 | ) 99 | --LOGCMD( aCommand ) 100 | local aHandle = io.popen( aCommand ) 101 | local aPath = aHandle:read() 102 | 103 | aHandle:close() 104 | 105 | if aPath then 106 | return QUOTE( aPath ) 107 | end 108 | 109 | return false 110 | end 111 | 112 | local _which = {} 113 | 114 | local function WHICH( aProgram ) 115 | if _which[ assert( aProgram ) ] == nil then _which[ aProgram ] = CHECKWHICH( aProgram ) end 116 | 117 | return _which[ aProgram ] 118 | end 119 | 120 | local function LOCALE() 121 | local aCommand = 122 | ( '%s charmap 2>/dev/null' ):format 123 | ( 124 | assert( WHICH( 'locale' ) ) 125 | ) 126 | LOGCMD( aCommand ) 127 | local aHandle = io.popen( aCommand, 'r' ) 128 | local aCharmap = assert( aHandle:read() ) 129 | 130 | assert( aHandle:close() ) 131 | 132 | assert( aCharmap ) 133 | assert( 0 ~= aCharmap:len() ) 134 | assert( 16 >= aCharmap:len() ) 135 | aCharmap = aCharmap:upper() 136 | 137 | return aCharmap 138 | end 139 | 140 | local function CHECKTTY() 141 | local aCommand = 142 | ( '%s -t 1 2>/dev/null' ):format 143 | ( 144 | assert( WHICH( 'test' ) ) 145 | ) 146 | LOGCMD( aCommand ) 147 | local ok, exit, signal = os.execute( aCommand ) 148 | 149 | return ( ok and exit == 'exit' ) and signal == 0 or false 150 | end 151 | 152 | local _istty = nil 153 | 154 | local function ISTTY() 155 | if _istty == nil then _istty = CHECKTTY() end 156 | 157 | return _istty 158 | end 159 | 160 | local function LOG( aMark, aDescription ) 161 | if not ISTTY() then return end 162 | 163 | assert( aMark ) 164 | assert( aDescription ) 165 | aDescription = tostring( aDescription ) 166 | assert( aDescription ) 167 | assert( 0 ~= aDescription:len() ) 168 | assert( 144 >= aDescription:len() ) 169 | assert( not aDescription:find( '%c' ) ) 170 | 171 | PRINTOUT( aMark, ' ', aDescription ) 172 | end 173 | 174 | local function INFO( aDescription ) 175 | LOG( '\u{2139}', aDescription ) 176 | end 177 | 178 | local function WARNING( aDescription ) 179 | LOG( '\u{26A0}', aDescription ) 180 | end 181 | 182 | local function ERROR( aDescription ) 183 | LOG( '\u{2716}', aDescription ) 184 | end 185 | 186 | local function LINE() 187 | if not ISTTY() then return end 188 | 189 | PRINTOUT( '\u{2702}', ( '\u{30FB}' ):rep( 10 ) ) 190 | end 191 | 192 | local function CLEAR() 193 | if not ISTTY() then return end 194 | if not WHICH( 'clear' ) then return end 195 | local aCommand = 196 | ( '%s 2>/dev/null' ):format 197 | ( 198 | assert( WHICH( 'clear' ) ) 199 | ) 200 | LOGCMD( aCommand ) 201 | assert( os.execute( aCommand ) ) 202 | end 203 | 204 | local function ADDRESS( aHost ) 205 | if not WHICH( 'dig' ) then return end 206 | local aCommand = 207 | ( '%s +nocomments +nofail +ignore +short +retry=0 +notcp +time=1 +tries=1 $(%s %s) A 2>/dev/null' ):format 208 | ( 209 | assert( WHICH( 'dig' ) ), 210 | assert( WHICH( 'idn2' ) or WHICH( 'printf' ) ), 211 | QUOTE( aHost ) 212 | ) 213 | LOGCMD( aCommand ) 214 | local aHandle = io.popen( aCommand, 'r' ) 215 | local anAddress = aHandle:read() 216 | 217 | aHandle:close() 218 | 219 | if anAddress and ';;' == anAddress:sub( 1, 2 ) then anAddress = nil end 220 | 221 | return anAddress 222 | end 223 | 224 | local function LOCATION( anAddress ) 225 | if not anAddress then return end 226 | if not ISTTY() then return end 227 | if not WHICH( 'mmdbinspect' ) then return end 228 | if not WHICH( 'gron' ) then return end 229 | local aCommand = 230 | ( '%s -db %s -db %s -db %s %s 2>/dev/null | %s' ):format 231 | ( 232 | assert( WHICH( 'mmdbinspect' ) ), 233 | QUOTE( '/usr/local/var/GeoIP/GeoLite2-ASN.mmdb' ), 234 | QUOTE( '/usr/local/var/GeoIP/GeoLite2-Country.mmdb' ), 235 | QUOTE( '/usr/local/var/GeoIP/GeoLite2-City.mmdb' ), 236 | QUOTE( anAddress ), 237 | assert( WHICH( 'gron' ) ) 238 | ) 239 | LOGCMD( aCommand ) 240 | local aHandle = io.popen( aCommand, 'r' ) 241 | local anOrganization = nil 242 | local aCity = nil 243 | local aPostalCode = nil 244 | local anArea = nil 245 | local aCountry = nil 246 | local aBuffer = {} 247 | 248 | for aLine in aHandle:lines() do 249 | anOrganization = anOrganization or aLine:match( 'autonomous_system_organization = "(.-)";$' ) 250 | aCity = aCity or aLine:match( 'city%.names%.en = "(.-)";$' ) 251 | aPostalCode = aPostalCode or aLine:match( 'postal.code = "(.-)";$' ) 252 | anArea = anArea or aLine:match( 'subdivisions%[0%]%.names%.en = "(.-)";$' ) 253 | aCountry = aCountry or aLine:match( 'country%.names%.en = "(.-)";$' ) 254 | end 255 | 256 | aHandle:close() 257 | 258 | if anArea and aPostalCode then anArea = ( '%s %s' ):format( anArea, aPostalCode ) end 259 | 260 | if anOrganization then aBuffer[ #aBuffer + 1 ] = anOrganization:gsub( '%-ASN$', '' ) end 261 | if aCity then aBuffer[ #aBuffer + 1 ] = aCity end 262 | if anArea then aBuffer[ #aBuffer + 1 ] = anArea end 263 | if aCountry then aBuffer[ #aBuffer + 1 ] = aCountry end 264 | 265 | INFO( ( '\u{2105} %s' ):format( table.concat( aBuffer, ', ' ) ):upper() ) 266 | end 267 | 268 | local function PING( aHost ) 269 | if not ISTTY() then return end 270 | if not WHICH( 'ping' ) then return end 271 | if not WHICH( 'tail' ) then return end 272 | local aCommand = 273 | ( '%s -c1 -q %s 2>/dev/null | %s -1' ):format 274 | ( 275 | assert( WHICH( 'ping' ) ), 276 | QUOTE( aHost ), 277 | assert( WHICH( 'tail' ) ) 278 | ) 279 | LOGCMD( aCommand ) 280 | local aHandle = io.popen( aCommand, 'r' ) 281 | local aLine = aHandle:read() or '' 282 | aHandle:close() 283 | local aLatency = math.floor( tonumber( aLine:match( ' = (%d+.%d+)/' ) ) or 0 ) 284 | local aDescription = ( '\u{29D7} %ims' ):format( aLatency ) 285 | 286 | if 0 ~= aLatency then INFO( aDescription ) end 287 | end 288 | 289 | local function TMPDIR() 290 | local aCommand = 291 | ( 292 | '%s -d -q 2>/dev/null' 293 | ):format 294 | ( 295 | assert( WHICH( 'mktemp' ) ) 296 | ) 297 | LOGCMD( aCommand ) 298 | local aHandle = assert( io.popen( aCommand, 'r' ) ) 299 | local aDirectory = assert( aHandle:read() ) 300 | assert( aHandle:close() ) 301 | 302 | return assert( aDirectory ) 303 | end 304 | 305 | local _directory = nil 306 | local _sequence = 0 307 | 308 | local function TMPNAME( aName, anExtension ) 309 | _directory = assert( _directory or TMPDIR() ) 310 | _sequence = _sequence + 1 311 | assert( 99 > _sequence ) 312 | 313 | return ( '%s/%02i.%s.%s' ):format( _directory, _sequence, aName, anExtension ) 314 | end 315 | 316 | local function MKFIFO( aPath ) 317 | local aCommand = ( '%s %s 2>/dev/null' ):format 318 | ( 319 | WHICH( 'mkfifo' ), 320 | QUOTE( aPath ) 321 | ) 322 | LOGCMD( aCommand ) 323 | 324 | assert( os.execute( aCommand ) ) 325 | 326 | return aPath 327 | end 328 | 329 | local function CHUNK( aHandle ) 330 | return function() return aHandle:read( 4096 ) end 331 | end 332 | 333 | local function TCPCONNECTION( aMultiAddr ) 334 | if not aMultiAddr.text:find( '/tcp/%d+$' ) then return end 335 | if not WHICH( 'nc' ) then return end 336 | local aConnection = 337 | ( '%s %s %i' ):format 338 | ( 339 | assert( WHICH( 'nc' ) ), 340 | QUOTE( aMultiAddr.address ), 341 | aMultiAddr.port 342 | ) 343 | 344 | return aConnection 345 | end 346 | 347 | local function TLSCONNECTION( aMultiAddr ) 348 | if not aMultiAddr.text:find( '/tcp/%d+/tls$' ) then return end 349 | if not WHICH( 'openssl' ) then return end 350 | local aConnection = 351 | ( '%s s_client -quiet -connect %s' ):format 352 | ( 353 | assert( WHICH( 'openssl' ) ), 354 | QUOTE( ( '%s:%i' ):format( aMultiAddr.address, aMultiAddr.port ) ) 355 | ) 356 | 357 | return aConnection 358 | end 359 | 360 | local function NOISECONNECTION( aMultiAddr ) 361 | if not aMultiAddr.text:find( '/tcp/%d+/noise$' ) then return end 362 | if not WHICH( 'noisecat' ) then return end 363 | local aConnection = 364 | ( '%s -v -proto Noise_XX_25519_ChaChaPoly_BLAKE2b %s %i' ):format 365 | ( 366 | assert( WHICH( 'noisecat' ) ), 367 | QUOTE( aMultiAddr.address ), 368 | aMultiAddr.port 369 | ) 370 | 371 | return aConnection 372 | end 373 | 374 | local _connectors = { NOISECONNECTION, TLSCONNECTION, TCPCONNECTION } 375 | 376 | local function CONNECTION( aMultiAddr ) 377 | for _, aConnector in ipairs( _connectors ) do 378 | local aConnection = aConnector( aMultiAddr ) 379 | 380 | if aConnection then return aConnection end 381 | end 382 | end 383 | 384 | local function MULTIADDRBUILD( aText ) 385 | if '/ip4/' ~= aText:sub( 1, 5 ) then return end 386 | local anAddress = aText:match( '^/ip4/(.-)/' ) 387 | local aPort = tonumber( aText:match( '/tcp/(%d+)' ) ) 388 | local aPriority = 0 389 | aText = aText:gsub( ';(%d+)$', function( aValue ) aPriority = tonumber( aValue ) return '' end ) 390 | 391 | return { address = anAddress, port = aPort, priority = aPriority, text = aText } 392 | end 393 | 394 | local function MULTIADDRLOOKUP( aHost ) 395 | if not WHICH( 'dig' ) then return end 396 | local aCommand = 397 | ( '%s +nocomments +nofail +ignore +short +retry=0 +notcp +time=1 +tries=1 $(%s %s) TXT 2>/dev/null' ):format 398 | ( 399 | assert( WHICH( 'dig' ) ), 400 | assert( WHICH( 'idn2' ) or WHICH( 'printf' ) ), 401 | QUOTE( ( '_text._tcp.%s' ):format( aHost ) ) 402 | ) 403 | LOGCMD( aCommand ) 404 | local aHandle = io.popen( aCommand, 'r' ) 405 | local aList = {} 406 | 407 | for aLine in aHandle:lines() do 408 | local aText = aLine:match( '^"(.+)"$' ) 409 | local aMultiAddr = MULTIADDRBUILD( aText ) 410 | 411 | if aMultiAddr then 412 | aList[ #aList + 1 ] = aMultiAddr 413 | aList[ -aMultiAddr.port ] = aMultiAddr 414 | end 415 | end 416 | 417 | table.sort( aList, function( aValue, anotherValue ) return aValue.priority < anotherValue.priority end ) 418 | 419 | aHandle:close() 420 | 421 | return aList 422 | end 423 | 424 | local function MULTIADDRDISCOVER( aHost, aPort ) 425 | local aList = MULTIADDRLOOKUP( aHost ) 426 | 427 | if 0 == #aList then return end 428 | 429 | if aList[ -( aPort or 0 ) ] then table.insert( aList, 1, aList[ -( aPort or 0 ) ] ) elseif aPort then assert( false ) end 430 | 431 | for _, aMultiAddr in ipairs( aList ) do 432 | aMultiAddr.connection = CONNECTION( aMultiAddr ) 433 | 434 | if aMultiAddr.connection then return aMultiAddr end 435 | end 436 | end 437 | 438 | local _ports = 439 | { 440 | [ 1961 ] = '/ip4/{ip4}/tcp/{port}', 441 | [ 1965 ] = '/ip4/{ip4}/tcp/{port}/tls', 442 | [ 1968 ] = '/ip4/{ip4}/tcp/{port}/noise' 443 | } 444 | 445 | local function MULTIADDRDEFAULT( aHost, aPort ) 446 | local anAddress = ADDRESS( aHost ) or aHost 447 | local aPort = aPort or 1961 448 | local aText = _ports[ aPort ] 449 | local aMultiAddr = {} 450 | 451 | if not aText then aText = _ports[ 1 ] end 452 | aText = aText:gsub( '{ip4}', anAddress ) 453 | aText = aText:gsub( '{port}', aPort ) 454 | 455 | aMultiAddr.address = anAddress 456 | aMultiAddr.port = aPort 457 | aMultiAddr.text = aText 458 | aMultiAddr.connection = CONNECTION( aMultiAddr ) 459 | 460 | return aMultiAddr 461 | end 462 | 463 | local function MULTIADDR( aHost, aPort ) 464 | local aMultiAddress = MULTIADDRDISCOVER( aHost, aPort ) or MULTIADDRDEFAULT( aHost, aPort ) 465 | 466 | _address = assert( aMultiAddress.address ) 467 | assert( aMultiAddress.text ) 468 | assert( aMultiAddress.connection ) 469 | 470 | return aMultiAddress 471 | end 472 | 473 | local function REQUEST() 474 | local aURL, aHost, aPort = URL() 475 | INFO( ( '/dns/%s' ):format( aHost ):upper() ) 476 | local aMultiAddr = MULTIADDR( aHost, aPort ) 477 | INFO( aMultiAddr.text:upper() ) 478 | LOCATION( aMultiAddr.address ) 479 | PING( aMultiAddr.address ) 480 | local aResultName = MKFIFO( TMPNAME( 'result', 'fifo' ) ) 481 | local aCommand = 482 | ( '%s 0.5 %s 1>%s 2>/dev/null' ):format 483 | ( 484 | assert( WHICH( 'timeout' ) ), 485 | aMultiAddr.connection, 486 | QUOTE( aResultName ) 487 | ) 488 | LOGCMD( aCommand ) 489 | local aHandle = assert( io.popen( aCommand, 'w' ) ) 490 | assert( aHandle:write( aURL, '\r\n' ) ) 491 | assert( aHandle:flush() ) 492 | local aResultHandle = assert( io.open( aResultName, 'r' ) ) 493 | local aName = TMPNAME( 'response', 'raw' ) 494 | local aFile = io.open( aName, 'wb' ) 495 | local aSize = 0 496 | 497 | for aChunk in CHUNK( aResultHandle ) do 498 | aSize = aSize + aChunk:len() 499 | assert( aFile:write( aChunk ) ) 500 | assert( 1048576 > aSize ) 501 | end 502 | 503 | assert( aFile:close() ) 504 | assert( aResultHandle:close() ) 505 | aHandle:close() 506 | 507 | return aName 508 | end 509 | 510 | local BOLD = 1 511 | local DIM = 2 512 | local UNDERLINE = 4 513 | 514 | local function ANSI( aCode, aValue ) 515 | local aPattern = string.char( 27 ) .. '[%dm' 516 | 517 | assert( aCode == 1 or aCode == 2 or aCode == 4 ) 518 | assert( aValue ) 519 | 520 | return ( '%s%s%s' ):format( aPattern:format( aCode ), aValue, aPattern:format( 0 ) ) 521 | end 522 | 523 | local function NOANSI( aValue ) 524 | return 525 | ( 526 | assert( aValue ) 527 | :gsub( '\x1b%[%d+;%d+;%d+;%d+;%d+m', '' ) 528 | :gsub( '\x1b%[%d+;%d+;%d+;%d+m', '' ) 529 | :gsub( '\x1b%[%d+;%d+;%d+m', '' ) 530 | :gsub( '\x1b%[%d+;%d+m', '' ) 531 | :gsub( '\x1b%[%d+m', '' ) 532 | ) 533 | end 534 | 535 | local function STATUS( aResponseName ) 536 | local aResponseFile = assert( io.open( aResponseName, 'r' ) ) 537 | local aLine = assert( aResponseFile:read():match( '^(%C+)' ) ) 538 | assert( 0 ~= aLine:len() ) 539 | assert( 1024 >= aLine:len() ) 540 | assert( aLine == NOANSI( aLine ) ) 541 | assert( aResponseFile:close() ) 542 | local aStatus = assert( tonumber( aLine:match( '^(%d+) ' ) ) ) 543 | local aName = TMPNAME( 'status', 'txt' ) 544 | local aFile = assert( io.open( aName, 'w' ) ) 545 | assert( aFile:write( aLine, '\n' ) ) 546 | assert( aFile:close() ) 547 | local aCommand = 548 | ( '%s --from-code=UTF-8 --to-code=UTF-8 < %s >/dev/null' ):format 549 | ( 550 | assert( WHICH( 'iconv' ) ), 551 | QUOTE( aName ) 552 | ) 553 | LOGCMD( aCommand ) 554 | 555 | assert( os.execute( aCommand ) ) 556 | 557 | return aName, aStatus 558 | end 559 | 560 | local DISPLAY20TEXTPLAIN = nil 561 | 562 | local function COMMA( amount ) 563 | local formatted, k = amount 564 | 565 | repeat 566 | formatted, k = string.gsub( formatted, '^(-?%d+)(%d%d%d)' , '%1,%2' ) 567 | until k == 0 568 | 569 | return formatted 570 | end 571 | 572 | local function PLURAL( aCount, aSingular, aPlural ) 573 | if 0 == aCount then return aSingular end 574 | if 1 == aCount then return aSingular end 575 | 576 | return aPlural or ( '%ss' ):format( aSingular ) 577 | end 578 | 579 | local function DISPLAY20BINARY( aContentName, aType, aCharset ) 580 | if not ISTTY() then return false end 581 | local aCommand = 582 | ( '%s < %s | %s -b -w64 2>/dev/null' ):format 583 | ( 584 | assert( WHICH( 'base64' ) ), 585 | QUOTE( aContentName ), 586 | assert( WHICH( 'fold' ) ) 587 | ) 588 | LOGCMD( aCommand ) 589 | local aHandle = assert( io.popen( aCommand, 'r' ) ) 590 | 591 | PRINTOUT 592 | ( 593 | ANSI( BOLD, '\u{2714} ' ), 594 | ANSI( BOLD, aType:upper() ), 595 | ANSI( DIM, ( ' \u{2014} %s %s ' ):format( COMMA( _size ), PLURAL( _size, 'byte' ) ) ) 596 | ) 597 | INFO( 'BASE64' ) 598 | 599 | PRINTOUT() 600 | for aChunk in CHUNK( aHandle ) do 601 | io.stdout:write( aChunk ) 602 | end 603 | PRINTOUT() 604 | 605 | assert( aHandle:close() ) 606 | 607 | return true 608 | end 609 | 610 | local function HANDLE20BINARY( aContentName, aType, aCharset ) 611 | if DISPLAY20BINARY( aContentName, aType, aCharset ) then return true end 612 | local aContentFile = assert( io.open( aContentName, 'rb' ) ) 613 | 614 | PRINTOUT( ( '20 %s; charset=%s; content-length=%i' ):format( aType, aCharset, _size ) ) 615 | 616 | for aChunk in CHUNK( aContentFile ) do 617 | assert( io.stdout:write( aChunk ) ) 618 | end 619 | 620 | assert( aContentFile:close() ) 621 | 622 | return true 623 | end 624 | 625 | local function DISPLAY20APPLICATIONPDF( aContentName, aType, aCharset ) 626 | if not ISTTY() then return false end 627 | if not WHICH( 'pdftotext' ) then return false end 628 | local aTextName = TMPNAME( 'pdf', 'txt' ) 629 | local aCommand = 630 | ( '%s -enc UTF-8 -eol unix -layout -nopgbrk -q - - <%s 1>%s 2>/dev/null' ):format 631 | ( 632 | assert( WHICH( 'pdftotext' ) ), 633 | QUOTE( aContentName ), 634 | QUOTE( aTextName ) 635 | ) 636 | LOGCMD( aCommand ) 637 | assert( os.execute( aCommand ) ) 638 | 639 | return DISPLAY20TEXTPLAIN( aTextName, aType, 'UTF-8' ) 640 | end 641 | 642 | local function HANDLE20APPLICATIONPDF( aContentName, aType, aCharset ) 643 | if DISPLAY20APPLICATIONPDF( aContentName, aType, aCharset ) then return true end 644 | 645 | return HANDLE20BINARY( aContentName, aType, aCharset ) 646 | end 647 | 648 | local function DISPLAY20IMAGE( aContentName, aType, aCharset ) 649 | if not ISTTY() then return false end 650 | if not WHICH( 'convert' ) then return false end 651 | if not WHICH( 'jp2a' ) then return false end 652 | local aCommand = 653 | ( '%s %s jpg:- 2>/dev/null | jp2a - 2>/dev/null' ):format 654 | ( 655 | assert( WHICH( 'convert' ) ), 656 | QUOTE( aContentName ), 657 | assert( WHICH( 'jp2a' ) ) 658 | ) 659 | LOGCMD( aCommand ) 660 | local aHandle = assert( io.popen( aCommand, 'r' ) ) 661 | 662 | PRINTOUT 663 | ( 664 | ANSI( BOLD, '\u{2714} ' ), 665 | ANSI( BOLD, aType:upper() ), 666 | ANSI( DIM, ( ' \u{2014} %s %s ' ):format( COMMA( _size ), PLURAL( _size, 'byte' ) ) ) 667 | ) 668 | INFO( 'ASCII' ) 669 | PRINTOUT() 670 | 671 | for aLine in aHandle:lines() do 672 | PRINTOUT( NOANSI( aLine ) ) 673 | end 674 | 675 | assert( aHandle:close() ) 676 | 677 | PRINTOUT() 678 | 679 | return true 680 | end 681 | 682 | local function HANDLE20IMAGE( aContentName, aType, aCharset ) 683 | if DISPLAY20IMAGE( aContentName, aType, aCharset ) then return true end 684 | 685 | return HANDLE20BINARY( aContentName, aType, aCharset ) 686 | end 687 | 688 | local function LANGUAGE( aFileName ) 689 | if not WHICH( 'franc' ) then return nil end 690 | local aCommand = 691 | ( '%s --min-length 1024 < %s 2>/dev/null' ):format 692 | ( 693 | assert( WHICH( 'franc' ) ), 694 | QUOTE( aFileName ) 695 | ) 696 | LOGCMD( aCommand ) 697 | local aHandle = assert( io.popen( aCommand, 'r' ) ) 698 | local aLanguage = assert( aHandle:read() ) 699 | assert( aHandle:close() ) 700 | 701 | if aLanguage and 'und' == aLanguage then aLanguage = nil end 702 | 703 | return aLanguage 704 | end 705 | 706 | local function DISPLAYIMAGEDATALINK( aScheme, aLink, aDescription ) 707 | if 'data:image/png;base64,' ~= aLink:sub( 1, 22 ) then return false end 708 | if not WHICH( 'base64' ) then return false end 709 | if not WHICH( 'convert' ) then return false end 710 | if not WHICH( 'jp2a' ) then return false end 711 | local aType = assert( aLink:match( '^data:(%w+/[%w.+-]+);' ) ) 712 | local aSubType = assert( aLink:match( '^data%:image%/([%w.+-]+)%;base64%,' ) ) 713 | local someData = assert( aLink:sub( 23 ) ) 714 | local aDataName = TMPNAME( 'data', aSubType ) 715 | local aCommand = 716 | ( '%s %s | %s --decode 2>/dev/null 1>%s' ):format 717 | ( 718 | WHICH( 'printf' ), 719 | QUOTE( someData ), 720 | WHICH( 'base64' ), 721 | QUOTE( aDataName ) 722 | ) 723 | LOGCMD( aCommand ) 724 | assert( os.execute( aCommand ) ) 725 | local aDataFile = assert( io.open( aDataName, 'rb' ) ) 726 | local aSize = assert( aDataFile:seek( 'end' ) ) 727 | assert( aDataFile:close() ) 728 | local aCommand = 729 | ( '%s %s jpg:- 2>/dev/null | jp2a --border --width=32 - 2>/dev/null' ):format 730 | ( 731 | assert( WHICH( 'convert' ) ), 732 | QUOTE( aDataName ), 733 | assert( WHICH( 'jp2a' ) ) 734 | ) 735 | LOGCMD( aCommand ) 736 | local aHandle = assert( io.popen( aCommand, 'r' ) ) 737 | 738 | PRINTOUT( aDescription ) 739 | 740 | for aLine in aHandle:lines() do 741 | PRINTOUT( NOANSI( aLine ) ) 742 | end 743 | 744 | assert( aHandle:close() ) 745 | 746 | PRINTOUT 747 | ( 748 | ANSI 749 | ( 750 | DIM, 751 | ( '%s \u{2014} %s %s \u{2014} ascii' ):format 752 | ( 753 | aType:lower(), 754 | COMMA( aSize ), 755 | PLURAL( aSize, 'byte' ) 756 | ) 757 | ) 758 | ) 759 | 760 | return true 761 | end 762 | 763 | local _datalinkhandles = { image = DISPLAYIMAGEDATALINK } 764 | 765 | local function DISPLAYDATALINK( aScheme, aLink, aDescription ) 766 | local aType = assert( aLink:match( '^data:(%w+/[%w.+-]+);' ) ) 767 | local aMainType = aType:match( '^(%w+)/' ) 768 | local aHandler = _datalinkhandles[ aType ] or _datalinkhandles[ aMainType ] 769 | 770 | if aHandler and aHandler( aScheme, aLink, aDescription ) then return true end 771 | 772 | PRINTOUT 773 | ( 774 | ( '%s %s' ):format 775 | ( 776 | ANSI( UNDERLINE, ( '%s:%s' ):format( aScheme, aType ) ), 777 | aDescription 778 | ) 779 | ) 780 | 781 | return true 782 | end 783 | 784 | local _linkhandlers = { data = DISPLAYDATALINK } 785 | 786 | local function DISPLAYLINK( aLine ) 787 | if '=> ' ~= aLine:sub( 1, 3 ) then return false end 788 | local aLine = aLine:sub( 4 ) 789 | local aLink = aLine:match( '^(%S+)' ) 790 | local aDescription = aLine:sub( aLink:len() + 2 ) 791 | local aScheme = aLink:match( '^([%w.+-]+):' ) 792 | local aHandler = _linkhandlers[ aScheme ] 793 | 794 | if aHandler and aHandler( aScheme, aLink, aDescription ) then return true end 795 | 796 | PRINTOUT( ( '%s %s' ):format( ANSI( UNDERLINE, aLink ), aDescription ) ) 797 | 798 | return true 799 | end 800 | 801 | local function DISPLAYLINE( aLine ) 802 | aLine = NOANSI( aLine ) 803 | 804 | if not DISPLAYLINK( aLine ) then PRINTOUT( aLine ) end 805 | end 806 | 807 | DISPLAY20TEXTPLAIN = function( aContentName, aType, aCharset ) 808 | if not ISTTY() then return false end 809 | local aFileName = TMPNAME( 'content.utf', 'txt' ) 810 | local aCommand = 811 | ( '%s --from-code=%s --to-code=UTF-8 < %s 1> %s 2>/dev/null' ):format 812 | ( 813 | assert( WHICH( 'iconv' ) ), 814 | QUOTE( aCharset ), 815 | QUOTE( aContentName ), 816 | QUOTE( aFileName ) 817 | ) 818 | LOGCMD( aCommand ) 819 | assert( os.execute( aCommand ) ) 820 | local aCommand = 821 | ( '%s -l -w -m < %s 2>/dev/null' ):format 822 | ( 823 | assert( WHICH( 'wc' ) ), 824 | QUOTE( aFileName ) 825 | ) 826 | LOGCMD( aCommand ) 827 | local aHandle = assert( io.popen( aCommand, 'r' ) ) 828 | local aDescription = assert( aHandle:read() ) 829 | assert( aHandle:close() ) 830 | local someLines, someWords, someChars = aDescription:match( '^%s-(%d+)%s-(%d+)%s-(%d+)%s-$' ) 831 | someLines = tonumber( someLines ) 832 | someWords = tonumber( someWords ) 833 | someChars = tonumber( someChars ) 834 | local someBytes = '' 835 | if _size ~= someChars then someBytes = ( ', %s %s ' ):format( COMMA( _size ), PLURAL( _size, 'byte' ) ) end 836 | local aLabel = 837 | ( ' \u{2014} %s %s, %s %s, %s %s%s' ):format 838 | ( 839 | COMMA( someLines ), 840 | PLURAL( someLines, 'line' ), 841 | COMMA( someWords ), 842 | PLURAL( someLines, 'word' ), 843 | COMMA( someChars ), 844 | PLURAL( someLines, 'character' ), 845 | someBytes 846 | ) 847 | PRINTOUT( ANSI( BOLD, '\u{2714} ' ), ANSI( BOLD, aType:upper() ), ANSI( DIM, aLabel ) ) 848 | local aLanguage = ( LANGUAGE( aFileName ) or '' ):upper() 849 | 850 | INFO( ( 'UTF-8 %s' ):format( aLanguage ) ) 851 | 852 | PRINTOUT() 853 | for aLine in assert( io.open( aFileName, 'r' ) ):lines() do 854 | DISPLAYLINE( aLine ) 855 | end 856 | PRINTOUT() 857 | 858 | return true 859 | end 860 | 861 | local function HANDLE20TEXTPLAIN( aContentName, aType, aCharset ) 862 | if DISPLAY20TEXTPLAIN( aContentName, aType, aCharset ) then return true end 863 | local aContentFile = assert( io.open( aContentName, 'rb' ) ) 864 | 865 | PRINTOUT( ( '20 %s; charset=%s; content-length=%i' ):format( aType, aCharset, _size ) ) 866 | 867 | for aChunk in CHUNK( aContentFile ) do 868 | assert( io.stdout:write( aChunk ) ) 869 | end 870 | 871 | assert( aContentFile:close() ) 872 | 873 | return true 874 | end 875 | 876 | local _handlers20 = 877 | { 878 | [ 'application/pdf' ] = HANDLE20APPLICATIONPDF, 879 | [ 'image' ] = HANDLE20IMAGE, 880 | [ 'text/plain' ] = HANDLE20TEXTPLAIN 881 | } 882 | 883 | local function HANDLE20( aResponseName, aStatusName ) 884 | local aFile = assert( io.open( aStatusName, 'r' ) ) 885 | local aLine = assert( aFile:read() ) 886 | assert( aLine ) 887 | assert( aFile:close() ) 888 | local aResponseType = assert( aLine:match( '^20 (.+)$' ) ) 889 | assert( aResponseType ) 890 | assert( 0 ~= aResponseType:len() ) 891 | assert( 1024 >= aResponseType:len() ) 892 | assert( aResponseType == aResponseType:lower() ) 893 | local _, _, _, _, anExtension = URL() 894 | local aContentName = TMPNAME( 'content', anExtension ) 895 | local aCommand = 896 | ( '%s 1d < %s 1>%s 2>/dev/null' ):format 897 | ( 898 | assert( WHICH( 'sed' ) ), 899 | QUOTE( aResponseName ), 900 | QUOTE( aContentName ) 901 | ) 902 | LOGCMD( aCommand ) 903 | assert( os.execute( aCommand ) ) 904 | local aContentFile = assert( io.open( aContentName, 'rb' ) ) 905 | _size = assert( aContentFile:seek( 'end' ) ) 906 | assert( aContentFile:seek( 'set' ) ) 907 | assert( aContentFile:close() ) 908 | local aCommand = 909 | ( '%s --brief --mime-type --mime-encoding %s 2>/dev/null' ):format 910 | ( 911 | assert( WHICH( 'file' ) ), 912 | QUOTE( aContentName ) 913 | ) 914 | LOGCMD( aCommand ) 915 | local aHandle = assert( io.popen( aCommand, 'r' ) ) 916 | local aContentType = assert( aHandle:read() ) 917 | assert( aContentType ) 918 | assert( 0 ~= aContentType:len() ) 919 | assert( 1024 >= aContentType:len() ) 920 | assert( aHandle:close() ) 921 | local aType = aContentType:match( '^(%w+/[%w.+-]+); ' ) 922 | assert( aType ) 923 | assert( 0 ~= aType:len() ) 924 | assert( 32 >= aType:len() ) 925 | local aMainType = aType:match( '^(%w+)/' ) 926 | assert( aMainType ) 927 | assert( 0 ~= aMainType:len() ) 928 | assert( 16 >= aMainType:len() ) 929 | local aCharset = aContentType:match( 'charset=([%w-]+)' ) 930 | assert( aCharset ) 931 | assert( 0 ~= aCharset:len() ) 932 | assert( 16 >= aCharset:len() ) 933 | local aHandler = _handlers20[ aType ] or _handlers20[ aMainType ] or HANDLE20BINARY 934 | 935 | if not aResponseType:find( aContentType, 1, true ) then WARNING( 'INCONSISTENT CONTENT' ) assert( false ) end 936 | aHandler( aContentName, aType, aCharset ) 937 | 938 | return aContentName 939 | end 940 | 941 | local function DISPLAY30( aLink ) 942 | if not ISTTY() then return end 943 | 944 | PRINTOUT( ANSI( BOLD, '\u{27A1} REDIRECT' ) ) 945 | PRINTOUT() 946 | DISPLAYLINE( ( '=> %s' ):format( aLink ) ) 947 | PRINTOUT() 948 | end 949 | 950 | local function HANDLE30( aResponseName, aStatusName ) 951 | local aFile = assert( io.open( aStatusName, 'r' ) ) 952 | local aLine = assert( aFile:read() ) 953 | assert( aLine ) 954 | assert( aFile:close() ) 955 | local aLink = assert( aLine:match( '^30 (.+)$' ) ) 956 | assert( 0 ~= aLink:len() ) 957 | assert( 1024 >= aLink:len() ) 958 | assert( not aLink:find( '%s' ) ) 959 | assert( aLink == aLink:lower() ) 960 | 961 | DISPLAY30( aLink ) 962 | if ISTTY() then return end 963 | 964 | PRINTOUT( aLine ) 965 | end 966 | 967 | local function DISPLAY40( aDescription ) 968 | if not ISTTY() then return end 969 | 970 | PRINTOUT( ANSI( BOLD, '\u{2716} ERROR' ) ) 971 | PRINTOUT() 972 | PRINTOUT( aDescription ) 973 | PRINTOUT() 974 | end 975 | 976 | local function HANDLE40( aResponseName, aStatusName ) 977 | local aFile = assert( io.open( aStatusName, 'r' ) ) 978 | local aLine = assert( aFile:read() ) 979 | assert( aLine ) 980 | assert( aFile:close() ) 981 | local aDescription = assert( aLine:match( '^40 (.+)$' ) ) 982 | assert( aDescription ) 983 | assert( 0 ~= aDescription:len() ) 984 | assert( 1024 >= aDescription:len() ) 985 | 986 | DISPLAY40( aDescription ) 987 | if ISTTY() then return end 988 | 989 | PRINTOUT( aLine ) 990 | end 991 | 992 | local function TIMESTAMP() 993 | if not ISTTY() then return end 994 | if not WHICH( 'date' ) then return end 995 | local aCommand = 996 | ( "%s -u +'%%Y-%%m-%%dT%%H:%%M:%%SZ' 2>/dev/null" ):format 997 | ( 998 | assert( WHICH( 'date' ) ) 999 | ) 1000 | LOGCMD( aCommand ) 1001 | local aHandle = assert( io.popen( aCommand, 'r' ) ) 1002 | local aTimestamp = assert( aHandle:read() ) 1003 | assert( aHandle:close() ) 1004 | 1005 | if aTimestamp then INFO( aTimestamp ) end 1006 | end 1007 | 1008 | local function FINGERPRINT( aContentName ) 1009 | if not ISTTY() then return end 1010 | if not aContentName or not WHICH( 'md5sum' ) then return end 1011 | local aCommand = 1012 | ( '%s --binary < %s 2>/dev/null' ):format 1013 | ( 1014 | assert( WHICH( 'md5sum' ) ), 1015 | QUOTE( aContentName ) 1016 | ) 1017 | LOGCMD( aCommand ) 1018 | local aHandle = assert( io.popen( aCommand, 'r' ) ) 1019 | 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)' ) } 1020 | assert( aHandle:close() ) 1021 | 1022 | if #aBuffer == 4 then INFO( table.concat( aBuffer, ' ' ) ) end 1023 | end 1024 | 1025 | local base = 1026 | { 1027 | '0', '1', '2', '3', '4', '5', '6', '7', '8', 1028 | '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 1029 | 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 1030 | 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' 1031 | } 1032 | 1033 | local function tobase( aNumber, aBase ) 1034 | assert( aNumber, 'bad argument #1 to \'tobase\' (nil number)' ) 1035 | assert( aBase and aBase >= 2 and aBase <= #base, 'bad argument #2 to \'tobase\' (base out of range)' ) 1036 | 1037 | local abs = math.abs 1038 | local floor = math.floor 1039 | local aNumber = floor( abs( tonumber( aNumber ) ) ) 1040 | local aBuffer = {} 1041 | 1042 | repeat 1043 | aBuffer[ #aBuffer + 1 ] = base[ ( aNumber % aBase ) + 1 ] 1044 | aNumber = floor( aNumber / aBase ) 1045 | until aNumber == 0 1046 | 1047 | return table.concat( aBuffer ):reverse() 1048 | end 1049 | 1050 | local function TAG( anAddress ) 1051 | if not ISTTY() then return end 1052 | if not anAddress then return end 1053 | 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 1054 | 1055 | INFO( tobase( ip2dec( anAddress ), 36 ) ) 1056 | end 1057 | 1058 | local function COMMONLOG() 1059 | local aBuffer = {} 1060 | 1061 | aBuffer[ #aBuffer + 1 ] = _host or _address or '-' -- remotehost 1062 | aBuffer[ #aBuffer + 1 ] = '-' -- rfc931 1063 | aBuffer[ #aBuffer + 1 ] = '-' -- authuser 1064 | aBuffer[ #aBuffer + 1 ] = ( '[%s]' ):format( os.date( '!%Y-%m-%dT%TZ' ) ) -- [date] 1065 | aBuffer[ #aBuffer + 1 ] = ( '%q' ):format( _url or '' ) -- "request" 1066 | aBuffer[ #aBuffer + 1 ] = tostring( _status or '-' ) -- status 1067 | aBuffer[ #aBuffer + 1 ] = tostring( _size or '0' ) -- bytes 1068 | 1069 | PRINTERR( table.concat( aBuffer, ' ' ) ) 1070 | end 1071 | 1072 | local function CLEANUP() 1073 | if not _directory then return end 1074 | assert( '.' ~= _directory ) 1075 | assert( '..' ~= _directory ) 1076 | assert( '~' ~= _directory ) 1077 | assert( '/' ~= _directory ) 1078 | local aCommand = 1079 | ( '%s -r %s 2>/dev/null' ):format 1080 | ( 1081 | assert( WHICH( 'rm' ) ), 1082 | QUOTE( _directory ) 1083 | ) 1084 | LOGCMD( aCommand ) 1085 | assert( os.execute( aCommand ) ) 1086 | end 1087 | 1088 | local _handlers = { [ 20 ] = HANDLE20, [ 30 ] = HANDLE30, [ 40 ] = HANDLE40 } 1089 | 1090 | local function PROCESS() 1091 | if ISTTY() then assert( 'UTF-8' == LOCALE() ) end 1092 | CLEAR() 1093 | LINE() 1094 | local aResponseName = REQUEST() 1095 | local aStatusName, aStatus = STATUS( aResponseName ) 1096 | _status = aStatus 1097 | local aHandler = assert( _handlers[ aStatus ] ) 1098 | LINE() 1099 | local aContentName = aHandler( aResponseName, aStatusName ) 1100 | LINE() 1101 | 1102 | TIMESTAMP() 1103 | FINGERPRINT( aContentName ) 1104 | TAG( _address ) 1105 | CLEANUP() 1106 | COMMONLOG() 1107 | end 1108 | 1109 | if pcall( PROCESS ) then os.exit( true ) end 1110 | 1111 | ERROR( 'FAILED' ) 1112 | LINE() 1113 | os.exit( false ) 1114 | --------------------------------------------------------------------------------