/****************************************************************************************
* <b>LibHTTP</b>, small yet powerful library to simplify the use of HTTP in scripts.
* <p>
* Takes care of proxies, authentication, URL parsing, making the actual request, parsing
* headers, and following redirects (HTTP-redirects only). After this "boring" work is
* complete, LibHTTP transfers control back to the calling script, giving you full freedom
* to do whatever your need to do with the data. All headers will have been consumed at
* this point - the data you get from the socket with sockread is the document, without
* headers. LibHTTP accomplishes this by opening the socket with a different [temporary]
* name, and renaming it back to what you specified after it has finished its task.
* <p>
* Right click status window and select "Config HTTP Proxy" to do just that. The dialog
* should be clear enough for end users to understand but if you already have a config
* dialog in your script you may want to move the options there instead. Proxy support is
* automatic and transparent: making and handling the requests works exactly the same way
* whether or not a proxy is used - your script doesn't need to do anything special for
* proxies to work.
* <p>
* <code>/http_get SOCKETNAME %URL<br>
* on *:sockread:SOCKETNAME {<br>
* [read data and do things with it]<br>
* }</code>
* <p>
* <hr width="250" align="left">
* Errors are communicated with signal (replace "SocketName" in the signal name):<br>
* <code>on *:signal:HttpErr_SocketName: /echo -s HTTP connection error: $1-</code><br>
* <p>
* This signal is triggered if
* <ul>
* <li>LibHTTP is unable to connect to the server
* <li>the connection is lost while reading headers
* <li>server response code != 200 (200 means all went ok)
* <li>redirection limit is exceeded (limit is 5 redirections)
* </ul>
* The socket is automatically closed if any of these errors occur, meaning your
* custom sockread and sockclose event handlers are never executed. The reverse of this
* also guaranteed - if your sockread handler is executed you can 100% sure everything
* went ok. You don't <i>have to</i> catch and handle the error signal, but in most
* cases you <i>should</i> do it, at least to echo the message to status window.
* Without any kind of error message users will have no idea what is going wrong and
* how to fix it.
* <p>
* Of course you still need to test for $sockerr in the sockread and sockclose events,
* just like you have to with any other socket. LibHTTP detects errors only while
* connecting and reading headers - after this all control, including responsibilty
* to detect and handle errors, is yours.
* <hr width="250" align="left">
* <p>
* <b>Note:</b> You can not use <code>/sockmark</code> with connections opened
* by LibHTTP, the library uses the mark for storing various data.
* <p>
* <code>/set %libhttp.debug 1</code> - enables some debugging messages. Unset to disable.
*
*/
/****************************************************************************************
* Test whether Sockname is already in use. The normal test '<code>if ($sock(SOCKNAME) !=
* $null)</code>' is not enough because <code>/http_get</code> opens the sockets using
* temporary names (these are later renamed to the final form). $sockfree will also check
* the temporary names to see if there is a socket that will soon be renamed to Sockname.
* @param sockname The socket name to test
* @return boolean $true if Socketname is free, $false if not
*/
alias sockfree {
var %sockname = $1
if ($sock(%sockname)) return $false
var %i = 1
var %tempsock = $sock(LIBHTTP_TEMP_*,%i)
while (%tempsock != $null) {
if ($hget(%tempsock,_Final-Sockname) == %sockname) return $false
inc %i
var %tempsock = $sock(LIBHTTP_TEMP_*,%i)
}
return $true
}
/****************************************************************************************
* Request one file from remote server.
* @param Sockname Name for this connections
* @param URL Location of the file
* @return void
*/
alias http_get {
var %sockname = $1, %url = $2
if (!$sockfree(%sockname)) {
echo -sat HTTP socket error: socket name %sockname already in use
halt
}
lhttp_sockopen %sockname 0 $lhttp_parse_url(%url)
}
/****************************************************************************************
* Retrieve special information about Sockname. Uses same syntax as mIRC's
* <code>$sock(...).property</code>, meaning you can specify wildcard Sockname and select
* Nth match by passing a second param. This alias works only with sockets opened by
* <code>/http_get</code>, and even then only after the connection has been handed over
* to the calling script (IOW, when your on:sockread has run at least once).
* @param sockname Socket name or wildcard
* @optparam n Select Nth wildcard match
* @prop rcvd Number of bytes received, not counting HTTP-headers
* @prop redirects Number of redirects followed (usually 0)
* @prop percent Percent done, available only if .size is known
* @prop size Size of the file (value of the Content-Lenght header). Server does not know
* in advance the size for dynamic content so this works only with static files.
* @return int Numeric value, $null if the value isn't known (size/percent)
*/
alias httpsock {
var %sockname = $1, %n = $2
if (%n == $null) %n = 1
if ($sock(%sockname,%n) == $null) return
if ($prop == rcvd) return $calc($sock(%sockname,%n).rcvd - $gettok($sock(%sockname,%n).mark,1,32))
if ($prop == size) return $gettok($sock(%sockname,%n).mark,3,32)
if ($prop == redirects) return $gettok($sock(%sockname,%n).mark,2,32)
if ($prop == percent) {
if ($gettok($sock(%sockname,%n).mark,3,32) == $null) return $null
return $int($calc(($sock(%sockname,%n).rcvd - $gettok($sock(%sockname,%n).mark,1,32)) / $gettok($sock(%sockname,%n).mark,3,32) * 100))
}
}
;;;;;;;;;;;; Public API ends here. Rest is the internal workings of LibHTTP ;;;;;;;;;;;;
/****************************************************************************************
* Parses URL and returns a space separated list of URL-components, suitable for input
* to <code>/http_get</code>.
* @param URL The url to parse
* @return String Host Port Path [User] [Pass]
*/
alias -l lhttp_parse_url {
var %url = $1
var %port = 80
var %host = $gettok($remove(%url,http://),1,47)
var %path = / $+ $gettok($remove(%url,http://),2-,47)
if ($regex(%host,/^(.+):(.+)@(.+)$/)) {
var %user = $regml(1), %pass = $regml(2), %host = $regml(3)
}
if ($regex(%host,/^(.+):(\d+)$/)) {
var %host = $regml(1), %port = $regml(2)
}
return %host %port %path %user %pass
}
/****************************************************************************************
* This is what actually opens the connection. http_get is really just a wrapper, this
* where the magic happens. The point of having a wrapper is to be able to change this
* API without breaking existing programs. Programs use the public, very simple API of
* the wrapper that never changes, internally we have this more complex and freely
* changable thing.
* <p>
* The socket is opened with a temporary name, LIBTTP_TEMP_[number]. A hash table of the
* same name is also created, it is used for storing both request and response headers
* plus other misc data LibHTTP uses. The hashtable is automatically hfreed if an error
* occurs, or when control is handed over to user (socket is renamed).
*
* @param finalname The final name for this connections
* @param redirects Number of redirects followed so far
* @param host Hostname or IP-address of the server
* @param port Port to connect to (usually 80)
* @param path Path of the requested resource
* @optparam user Username (if the server requires one)
* @optparam pass Password (if the server requires one)
* @return void
*/
alias -l lhttp_sockopen {
var %finalname = $1, %redirects = $2, %host = $3, %port = $4, %path = $5, %user = $6, %pass = $7, %auth = none
if (%redirects > 5) {
.signal HttpErr_ $+ %finalname Redirect limit (5) exceeded - likely an infinite redirect loop.
return
}
var %i = 1
while ($sock(LIBHTTP_TEMP_ $+ %i) != $null) inc %i
var %tempname = LIBHTTP_TEMP_ $+ %i
hmake %tempname
if ((%libhttp.useproxy) && (%libhttp.proxy.host != $null) && (%libhttp.proxy.port != $null)) {
if (%libhttp.debug) echo -s *** sockopen %tempname %libhttp.proxy.host %libhttp.proxy.port
sockopen %tempname %libhttp.proxy.host %libhttp.proxy.port
hadd %tempname _Request-String GET http:// $+ %host $+ : $+ %port $+ %path HTTP/1.0
hadd %tempname _Connection-Host %libhttp.proxy.host
hadd %tempname _Connection-Port %libhttp.proxy.port
if ((%libhttp.proxy.user != $null) && (%libhttp.proxy.pass != $null)) {
hadd %tempname Proxy-Authorization: Basic $encode(%libhttp.proxy.user $+ : $+ %libhttp.proxy.pass,m)
}
hadd %tempname Proxy-Connection: close
}
else {
if (%libhttp.debug) echo -s *** sockopen %tempname %host %port
sockopen %tempname %host %port
hadd %tempname _Request-String GET %path HTTP/1.0
hadd %tempname _Connection-Host %host
hadd %tempname _Connection-Port %port
}
; Vars with leading underscore are for internal use of LibHTTP
hadd %tempname _Redirects-Followed %redirects
hadd %tempname _Final-Sockname %finalname
; Vars without leading underscore are headers
hadd %tempname Host: %host $+ : $+ %port
hadd %tempname Pragma: no-cache
hadd %tempname Cache-Control: no-cache
hadd %tempname Connection: close
hadd %tempname User-Agent: Mirc/ $+ $version (Windows $os $+ ) LibHTTP/1.0
if ((%user != $null) && (%pass != $null)) {
hadd %tempname Authorization: Basic $encode(%user $+ : $+ %pass,m)
}
}
/****************************************************************************************
* Writes the HTTP-request to socket. The request string and the headers were constructed
* in lhttp_sockopen, this just loops throught the headers and sends them over the socket.
* <p>
* It is very important that this event be "duplication tolerant", meaning it must still
* work if user has two copies of this script installed (happens if two independently
* distributed scripts bundle LibHTTP). First script to handle this sockopen event sets a
* hash table item "_Request-Sent" to $true - second script detects the presence of that
* flag and returns immediately. "if ($hget($sockname) != $null)" in the sockerr-branch
* of the code provides the same safeguard in error conditions.
*/
on *:sockopen:LIBHTTP_TEMP_*: {
if ($sockerr > 0) {
if ($hget($sockname) != $null) {
.signal HttpErr_ $+ $hget($sockname,_Final-Sockname) Unable to connect to $hget($sockname,_Connection-Host) $+ , port $hget($sockname,_Connection-Port)
hfree $sockname
}
return
}
if ($hget($sockname,_Request-Sent)) return
hadd $sockname _Request-Sent $true
if (%libhttp.debug) echo -s --> $hget($sockname,_Request-String)
sockwrite -n $sockname $hget($sockname,_Request-String)
var %hindex = $hget($sockname,0).item
while (%hindex > 0) {
var %key = $hget($sockname,%hindex).item
if (_* iswm %key) {
; Key is some internal var, not a header
}
else {
sockwrite -n $sockname %key $hget($sockname,%key)
if (%libhttp.debug) echo -s --> %key $hget($sockname,%key)
}
dec %hindex
}
sockwrite $sockname $crlf
.timer $+ $sockname $+ _TIMEOUT 1 30 lhttp_timeout $sockname
}
/****************************************************************************************
* Read and save headers. Once headers have been fully consumed, calls http_process_reply
* which determines what to do next.
* <p>
* Duplication tolerance is needed here also: Ordinarily getting $null from sockread
* indicates end of headers - with two scripts installed that may not always be correct.
* Since the sockread event now triggers twice whenever there is a CRLF terminated line
* in the buffer, it is possible that the first script consumes a normal headerline,
* leaving the buffer completely empty. When the second script triggers, sockread
* tries to read from empty buffer and returns $null. Thus it is crucial to test $sockbr
* to verify that we indeed got an "empty" line, instead of just hitting empty buffer.
*/
on *:sockread:LIBHTTP_TEMP_*: {
if ($sockerr > 0) {
if ($hget($sockname) != $null) {
.signal HttpErr_ $+ $hget($sockname,_Final-Sockname) Connection lost while reading headers (sockerr $sockerr $+ )
.timer $+ $sockname $+ _TIMEOUT off
hfree $sockname
}
return
}
var %sockinput
sockread %sockinput
if (%libhttp.debug) echo -s <-- %sockinput [ $+ $sockbr bytes]
if (%sockinput != $null) {
var %header = $gettok(%sockinput,1,32), %value = $gettok(%sockinput,2-,32)
if (HTTP/1.? iswm %header) {
hadd $sockname _Response-Code $gettok(%value,1,32)
}
else {
hadd $sockname _Response-Header_ $+ %header %value
}
.timer $+ $sockname $+ _TIMEOUT 1 30 lhttp_timeout $sockname
}
elseif ($sockbr > 0) {
lhttp_process_reply $sockname
}
}
/****************************************************************************************
* Headers have been read, determine the next step. If server replied with code 200,
* rename the socket and hand over control. If the server replied with code 301 or 302 AND
* sent a Location: header, follow the redirect and close current connection. If the server
* replied with any other code, signal an error. @return void
*/
alias -l lhttp_process_reply {
var %sockname = $1
var %respcode = $hget(%sockname,_Response-Code)
var %location = $hget(%sockname,_Response-Header_Location:)
var %filesize = $hget(%sockname,_Response-Header_Content-Length:)
var %finalname = $hget(%sockname,_Final-Sockname)
var %redirects = $hget(%sockname,_Redirects-Followed)
if (%respcode == 200) {
sockmark %sockname $sock(%sockname).rcvd %redirects %filesize
if (%libhttp.debug) echo -s *** sockrename %sockname %finalname
sockrename %sockname %finalname
}
elseif ((%respcode isnum 301-302) && (%location != $null)) {
lhttp_sockopen %finalname $calc(%redirects + 1) $lhttp_parse_url(%location)
if (%libhttp.debug) echo -s *** sockclose %sockname
sockclose %sockname
}
else {
.signal HttpErr_ $+ %finalname Server returned error %respcode
if (%libhttp.debug) echo -s *** sockclose %sockname
sockclose $sockname
}
hfree %sockname
.timer $+ %sockname $+ _TIMEOUT off
}
/****************************************************************************************
* 30 second timeout for the temp-socket. If server doesn't respond within 30 seconds of
* sending the request, or if the connection goes idle for more than 30 sec while reading
* headers, close the socket and signal an error. @return void
*/
alias -l lhttp_timeout {
var %sockname = $1
if ($hget(%sockname) != $null) {
.signal HttpErr_ $+ $hget(%sockname,_Final-Sockname) Connection timeout of 30 seconds reached while awaiting server responce
sockclose %sockname
hfree %sockname
}
}
/****************************************************************************************
* Connection closed while we were still reading headers
*/
on *:sockclose:LIBHTTP_TEMP_*: {
if ($hget($sockname) != $null) {
.signal HttpErr_ $+ $hget($sockname,_Final-Sockname) Connection lost while reading headers (sockerr: $sockerr $+ )
hfree $sockname
}
}
/****************************************************************************************
* Fancy dialog for proxy configuration
*/
menu status {
Config HTTP Proxy: dialog -md ProxyConfig ProxyConfig_Table
}
dialog ProxyConfig_Table {
size -1 -1 250 150
title "HTTP Proxy Configuration"
check "Use Proxy for HTTP connections", 1, 5 5 180 14
box "Server and Port" 10, 6 22 240 75
edit "" 11, 12 38 165 24, autohs disabled
text ":" 12, 179 41 3 14
edit "" 13, 183 38 55 24, autohs disabled
text "Status:" 14, 20 69 40 14
text "" 15, 60 69 170 14
box "Login and Password (optional)" 20, 6 89 240 55
edit "" 21, 12 110 110 24, autohs disabled
edit "" 22, 128 110 110 24, autohs pass disabled
}
/****************************************************************************************
* Proxy configuration dialog init
*/
on *:dialog:ProxyConfig:init:0: {
did -ra $dname 11 %libhttp.proxy.host
did -ra $dname 13 %libhttp.proxy.port
did -ra $dname 21 %libhttp.proxy.user
did -ra $dname 22 %libhttp.proxy.pass
if (%libhttp.useproxy) { did -c $dname 1 | did -e $dname 11,13,21,22 }
lhttp_proxytest
}
/****************************************************************************************
* User toggled the "Use Proxy" checkbox
*/
on *:dialog:ProxyConfig:sclick:1: {
if ($did($dname,$did).state == 1) { %libhttp.useproxy = $true | did -e $dname 11,13,21,22 }
if ($did($dname,$did).state == 0) { %libhttp.useproxy = $false | did -b $dname 11,13,21,22 }
lhttp_proxytest
}
/****************************************************************************************
* User edited one of the four text inputs
*/
on *:dialog:ProxyConfig:edit:*: {
%libhttp.proxy.host = $did($dname,11)
%libhttp.proxy.port = $did($dname,13)
%libhttp.proxy.user = $did($dname,21)
%libhttp.proxy.pass = $did($dname,22)
if ($did < 20) .timerTEST_PROXY 1 1 lhttp_proxytest
}
/****************************************************************************************
* Test proxy address by attempting to connect to it @param void @return void
*/
alias -l lhttp_proxytest {
if ($dialog(ProxyConfig) == $null) return
if ($sock(LIBHTTP_PROXYTEST) != $null) sockclose LIBHTTP_PROXYTEST
if ((%libhttp.proxy.host != $null) && (%libhttp.proxy.port != $null) && (%libhttp.useproxy)) {
sockopen LIBHTTP_PROXYTEST %libhttp.proxy.host %libhttp.proxy.port
did -ra ProxyConfig 15 Testing....
}
else did -r ProxyConfig 15
}
/****************************************************************************************
* Show result of the proxy test
*/
on *:sockopen:LIBHTTP_PROXYTEST: {
if ($dialog(ProxyConfig) != $null) did -ra ProxyConfig 15 $iif($sockerr, Unable to connect, OK)
sockclose $sockname
}
/****************************************************************************************
* Unset global variables used by libhttp
*/
on *:UNLOAD: {
unset %libhttp.*
}