Files
harbour-core/contrib/hbtip/httpcli.prg
Pritpal Bedi 208734b18e 2013-06-06 01:56 UTC-0800 Pritpal Bedi (bedipritpal@hotmail.com)
* contrib/hbtip/client.prg
! Fixed: to test if a connection is available actually or not
before sending other info. This fixes a RTE when a SSL over
HTTP request was made and internet access was not available.

* contrib/hbtip/httpcli.prg
+ Added: METHOD Head() for HEAD verb of HTTP interface.

METHOD SetConnectionPersistent()
This does not send "Connection: close" header entry.
Useful in cases where many requests are required to
be submitted under one session. :close() may be
called to close the connection explicitly.

METHOD IsConnectionAlive()
It allows the application to test connection's state
and facilitates to take alternate action.
2013-06-06 02:07:49 -07:00

639 lines
19 KiB
Plaintext

/*
* xHarbour Project source code:
* TIP Class oriented Internet protocol library (HTTP)
*
* Copyright 2003 Giancarlo Niccolai <gian@niccolai.ws>
* www - http://harbour-project.org
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software; see the file COPYING.txt. If not, write to
* the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
* Boston, MA 02111-1307 USA (or visit the web site http://www.gnu.org/).
*
* As a special exception, the Harbour Project gives permission for
* additional uses of the text contained in its release of Harbour.
*
* The exception is that, if you link the Harbour libraries with other
* files to produce an executable, this does not by itself cause the
* resulting executable to be covered by the GNU General Public License.
* Your use of that executable is in no way restricted on account of
* linking the Harbour library code into it.
*
* This exception does not however invalidate any other reasons why
* the executable file might be covered by the GNU General Public License.
*
* This exception applies only to the code released by the Harbour
* Project under the name Harbour. If you copy code from other
* Harbour Project or Free Software Foundation releases into a copy of
* Harbour, as the General Public License permits, the exception does
* not apply to the code that you add in this way. To avoid misleading
* anyone as to the status of such modified files, you must delete
* this exception notice from them.
*
* If you write modifications of your own for Harbour, it is your choice
* whether to permit this exception to apply to your modifications.
* If you do not wish that, delete this exception notice.
*
*/
#include "hbclass.ch"
#include "fileio.ch"
CREATE CLASS TIPClientHTTP FROM TIPClient
VAR cMethod
VAR nReplyCode
VAR cReplyDescr
VAR nVersion INIT 1
VAR nSubversion INIT 0
VAR bChunked
VAR hHeaders INIT { => }
VAR hCookies INIT { => }
VAR hFields INIT { => }
VAR cUserAgent INIT "Mozilla/3.0 compatible"
VAR cAuthMode INIT ""
VAR cBoundary
VAR aAttachments INIT {}
VAR lPersistent INIT .F.
METHOD New( oUrl, xTrace, oCredentials )
METHOD Get( cQuery )
METHOD Post( xPostData, cQuery )
METHOD Put( xPostData, cQuery )
METHOD Delete( xPostData, cQuery )
METHOD Head( xPostData, cQuery )
METHOD ReadHeaders( lClear )
METHOD Read( nLen )
METHOD UseBasicAuth() INLINE ::cAuthMode := "Basic"
METHOD ReadAll()
METHOD setCookie( cLine )
METHOD getcookies( cHost, cPath )
METHOD Boundary( nType )
METHOD Attach( cName, cFileName, cType )
METHOD PostMultiPart( xPostData, cQuery )
METHOD WriteAll( cFile )
METHOD StandardFields()
METHOD SetConnectionPersistent() INLINE ::lPersistent := .T.
METHOD IsConnectionAlive() INLINE ::inetErrorCode( ::SocketCon ) == 0
PROTECTED:
METHOD PostByVerb( xPostData, cQuery, cVerb )
ENDCLASS
METHOD New( oUrl, xTrace, oCredentials ) CLASS TIPClientHTTP
::super:new( oUrl, iif( HB_ISLOGICAL( xTrace ) .AND. xTrace, "http", xTrace ), oCredentials )
::nDefaultPort := iif( ::oUrl:cProto == "https", 443, 80 )
::nConnTimeout := 5000
::bChunked := .F.
hb_HCaseMatch( ::hHeaders, .F. )
RETURN Self
METHOD Get( cQuery ) CLASS TIPClientHTTP
IF ! HB_ISSTRING( cQuery )
cQuery := ::oUrl:BuildQuery()
ENDIF
::inetSendAll( ::SocketCon, "GET " + cQuery + " HTTP/1.1" + ::cCRLF )
::StandardFields()
::inetSendAll( ::SocketCon, ::cCRLF )
IF ::inetErrorCode( ::SocketCon ) == 0
RETURN ::ReadHeaders()
ENDIF
RETURN .F.
METHOD Post( xPostData, cQuery ) CLASS TIPClientHTTP
RETURN ::postByVerb( xPostData, cQuery, "POST" )
METHOD Put( xPostData, cQuery ) CLASS TIPClientHTTP
RETURN ::postByVerb( xPostData, cQuery, "PUT" )
METHOD Delete( xPostData, cQuery ) CLASS TIPClientHTTP
RETURN ::postByVerb( xPostData, cQuery, "DELETE" )
METHOD Head( xPostData, cQuery ) CLASS TIPClientHTTP
RETURN ::postByVerb( xPostData, cQuery, "HEAD" )
METHOD PostByVerb( xPostData, cQuery, cVerb ) CLASS TIPClientHTTP
LOCAL cData, nI, cTmp, y
hb_default( @cVerb, "POST" )
IF HB_ISHASH( xPostData )
cData := ""
y := Len( xPostData )
FOR nI := 1 TO y
cTmp := tip_URLEncode( AllTrim( hb_CStr( hb_HKeyAt( xPostData, nI ) ) ) )
cData += cTmp + "="
cTmp := tip_URLEncode( hb_CStr( hb_HValueAt( xPostData, nI ) ) )
cData += cTmp
IF nI != y
cData += "&"
ENDIF
NEXT
ELSEIF HB_ISARRAY( xPostData )
cData := ""
y := Len( xPostData )
FOR nI := 1 TO y
cTmp := tip_URLEncode( AllTrim( hb_CStr( xPostData[ nI, 1 ] ) ) )
cData += cTmp + "="
cTmp := tip_URLEncode( hb_CStr( xPostData[ nI, 2 ] ) )
cData += cTmp
IF nI != y
cData += "&"
ENDIF
NEXT
ELSEIF HB_ISSTRING( xPostData )
cData := xPostData
ELSE
RETURN .F.
ENDIF
IF ! HB_ISSTRING( cQuery )
cQuery := ::oUrl:BuildQuery()
ENDIF
::inetSendAll( ::SocketCon, cVerb + " " + cQuery + " HTTP/1.1" + ::cCRLF )
::StandardFields()
IF ! "Content-Type" $ ::hFields
::inetSendAll( ::SocketCon, e"Content-Type: application/x-www-form-urlencoded\r\n" )
ENDIF
::inetSendAll( ::SocketCon, "Content-Length: " + ;
hb_ntos( Len( cData ) ) + ::cCRLF )
// End of header
::inetSendAll( ::SocketCon, ::cCRLF )
IF ::inetErrorCode( ::SocketCon ) == 0
::inetSendAll( ::SocketCon, cData )
::bInitialized := .T.
RETURN ::ReadHeaders()
ENDIF
RETURN .F.
METHOD StandardFields() CLASS TIPClientHTTP
LOCAL iCount
LOCAL oEncoder, cCookies
::inetSendAll( ::SocketCon, "Host: " + ::oUrl:cServer + ::cCRLF )
::inetSendAll( ::SocketCon, "User-agent: " + ::cUserAgent + ::cCRLF )
IF ! ::lPersistent
::inetSendAll( ::SocketCon, "Connection: close" + ::cCRLF )
ENDIF
// Perform a basic authentication request
IF ::cAuthMode == "Basic" .AND. !( "Authorization" $ ::hFields )
oEncoder := TIPEncoderBase64():New()
oEncoder:bHttpExcept := .T.
::inetSendAll( ::SocketCon, "Authorization: Basic " + ;
oEncoder:Encode( ::oUrl:cUserID + ":" + ::oUrl:cPassword ) + ::cCRLF )
ENDIF
// send cookies
cCookies := ::getCookies()
IF ! Empty( cCookies )
::inetSendAll( ::SocketCon, "Cookie: " + cCookies + ::cCRLF )
ENDIF
// Send optional Fields
FOR iCount := 1 TO Len( ::hFields )
::inetSendAll( ::SocketCon, hb_HKeyAt( ::hFields, iCount ) + ;
": " + hb_HValueAt( ::hFields, iCount ) + ::cCRLF )
NEXT
RETURN .T.
METHOD ReadHeaders( lClear ) CLASS TIPClientHTTP
LOCAL cLine, nPos, aVersion
LOCAL aHead
// Now reads the fields and set the content lenght
cLine := ::inetRecvLine( ::SocketCon, @nPos, 500 )
IF Empty( cLine )
// In case of timeout or error on receiving
RETURN .F.
ENDIF
// Get Protocol version
aVersion := hb_regex( "^HTTP/(.)\.(.) ([0-9][0-9][0-9]) +(.*)$", cLine )
::cReply := cLine
IF Empty( aVersion )
::nVersion := 0
::nSubversion := 9
::nReplyCode := 0
::cReplyDescr := ""
ELSE
::nVersion := Val( aVersion[ 2 ] )
::nSubversion := Val( aVersion[ 3 ] )
::nReplyCode := Val( aVersion[ 4 ] )
::cReplyDescr := aVersion[ 5 ]
ENDIF
::nLength := -1
::bChunked := .F.
cLine := ::inetRecvLine( ::SocketCon, @nPos, 500 )
IF lClear != NIL .AND. lClear .AND. ! Empty( ::hHeaders )
::hHeaders := { => }
ENDIF
DO WHILE ::inetErrorCode( ::SocketCon ) == 0 .AND. ! Empty( cLine )
aHead := hb_regexSplit( ":", cLine,,, 1 )
IF aHead == NIL .OR. Len( aHead ) != 2
cLine := ::inetRecvLine( ::SocketCon, @nPos, 500 )
LOOP
ENDIF
::hHeaders[ aHead[ 1 ] ] := LTrim( aHead[ 2 ] )
DO CASE
// RFC 2068 forces to discard content length on chunked encoding
CASE Lower( aHead[ 1 ] ) == "content-length" .AND. ! ::bChunked
cLine := SubStr( cLine, 16 )
::nLength := Val( cLine )
// as above
CASE Lower( aHead[ 1 ] ) == "transfer-encoding"
IF At( "chunked", Lower( cLine ) ) > 0
::bChunked := .T.
::nLength := -1
ENDIF
CASE Lower( aHead[ 1 ] ) == "set-cookie"
::setCookie( aHead[ 2 ] )
ENDCASE
cLine := ::inetRecvLine( ::SocketCon, @nPos, 500 )
ENDDO
IF ::inetErrorCode( ::SocketCon ) != 0
RETURN .F.
ENDIF
RETURN .T.
METHOD Read( nLen ) CLASS TIPClientHTTP
LOCAL cData, nPos, cLine, aHead
IF ! ::bInitialized
::bInitialized := .T.
IF ! ::Get()
RETURN NIL
ENDIF
ENDIF
/* On HTTP/1.1 protocol, content lenght can be in hex format before each chunk.
The chunk header is read each time nLength is -1; While reading the chunk,
nLenght is set to nRead plus the expected chunk size. After reading the
chunk, the footer is discarded, and nLenght is reset to -1.
*/
IF ::nLength == -1 .AND. ::bChunked
cLine := ::inetRecvLine( ::SocketCon, @nPos, 1024 )
IF Empty( cLine )
RETURN NIL
ENDIF
// if this is the last chunk ...
IF cLine == "0"
// read the footers.
cLine := ::inetRecvLine( ::SocketCon, @nPos, 1024 )
DO WHILE ! Empty( cLine )
// add Headers to footers
aHead := hb_regexSplit( ":", cLine,,, 1 )
IF aHead != NIL
::hHeaders[ aHead[ 1 ] ] := LTrim( aHead[ 2 ] )
ENDIF
cLine := ::inetRecvLine( ::SocketCon, @nPos, 1024 )
ENDDO
// we are done
::bEof := .T.
RETURN NIL
ENDIF
// A normal chunk here
// Remove the extensions
nPos := At( ";", cLine )
IF nPos > 0
cLine := SubStr( cLine, 1, nPos - 1 )
ENDIF
// Convert to length
// Set length so that super::Read reads in at max cLine bytes.
::nLength := hb_HexToNum( cLine ) + ::nRead
ENDIF
// nLen is normalized by super:read()
cData := ::super:Read( nLen )
// If bEof is set with chunked encoding, this means that the whole chunk has been read;
IF ::bEof .AND. ::bChunked
::bEof := .F.
::nLength := -1
// chunked data is followed by a blank line
/* cLine := */ ::InetRecvLine( ::SocketCon, @nPos, 1024 )
ENDIF
RETURN cData
METHOD ReadAll() CLASS TIPClientHTTP
LOCAL cOut := "", cChunk
IF ! ::bInitialized
::bInitialized := .T.
IF ! ::Get()
RETURN NIL
ENDIF
ENDIF
IF ::bChunked
cChunk := ::read()
DO WHILE cChunk != NIL
cOut += cChunk
// ::nLength := -1
cChunk := ::read()
ENDDO
ELSE
RETURN ::read()
ENDIF
RETURN cOut
METHOD setCookie( cLine ) CLASS TIPClientHTTP
// docs from http://www.ietf.org/rfc/rfc2109.txt
LOCAL aParam
LOCAL cHost, cPath, cName, cValue, aElements, cElement
LOCAL cDefaultHost := ::oUrl:cServer, cDefaultPath := ::oUrl:cPath
LOCAL x, y
IF Empty( cDefaultPath )
cDefaultPath := "/"
ENDIF
// this function currently ignores expires, secure and other tags that may be in the cookie for now...
// ? "Setting COOKIE:", cLine
aParam := hb_regexSplit( ";", cLine )
cName := cValue := ""
cHost := cDefaultHost
cPath := cDefaultPath
y := Len( aParam )
FOR x := 1 TO y
aElements := hb_regexSplit( "=", aParam[ x ], 1 )
IF Len( aElements ) == 2
IF x == 1
cName := AllTrim( aElements[ 1 ] )
cValue := AllTrim( aElements[ 2 ] )
ELSE
cElement := Upper( AllTrim( aElements[ 1 ] ) )
DO CASE
#if 0
CASE cElement == "EXPIRES"
#endif
CASE cElement == "PATH"
cPath := AllTrim( aElements[ 2 ] )
CASE cElement == "DOMAIN"
cHost := AllTrim( aElements[ 2 ] )
ENDCASE
ENDIF
ENDIF
NEXT
IF ! Empty( cName )
// cookies are stored in hashes as host.path.name
// check if we have a host hash yet
IF ! hb_HHasKey( ::hCookies, cHost )
::hCookies[ cHost ] := { => }
ENDIF
IF ! hb_HHasKey( ::hCookies[ cHost ], cPath )
::hCookies[ cHost ][ cPath ] := { => }
ENDIF
::hCookies[ cHost ][ cPath ][ cName ] := cValue
ENDIF
RETURN NIL
METHOD getcookies( cHost, cPath ) CLASS TIPClientHTTP
LOCAL x, y, aDomKeys := {}, aKeys, z, cKey, aPathKeys, nPath
LOCAL a, b, cOut := "", c, d
hb_default( @cHost, ::oUrl:cServer )
IF cPath == NIL
cPath := ::oUrl:cPath
IF Empty( cPath )
cPath := "/"
ENDIF
ENDIF
IF Empty( cHost )
RETURN cOut
ENDIF
// tail matching the domain
aKeys := hb_HKeys( ::hCookies )
y := Len( aKeys )
z := Len( cHost )
cHost := Upper( cHost )
FOR x := 1 TO y
cKey := Upper( aKeys[ x ] )
IF Upper( Right( cKey, z ) ) == cHost .AND. ( Len( cKey ) == z .OR. SubStr( aKeys[ x ], 0 - z, 1 ) == "." )
AAdd( aDomKeys, aKeys[ x ] )
ENDIF
NEXT
// more specific paths should be sent before lesser generic paths.
ASort( aDomKeys,,, {| cX, cY | Len( cX ) > Len( cY ) } )
y := Len( aDomKeys )
// now that we have the domain matches we have to do path matchine
nPath := Len( cPath )
FOR x := 1 TO y
aKeys := hb_HKeys( ::hCookies[ aDomKeys[ x ] ] )
aPathKeys := {}
b := Len( aKeys )
FOR a := 1 TO b
cKey := aKeys[ a ]
z := Len( cKey )
IF cKey == "/" .OR. ( z <= nPath .AND. SubStr( cKey, 1, nPath ) == cKey )
AAdd( aPathKeys, aKeys[ a ] )
ENDIF
NEXT
ASort( aPathKeys,,, {| cX, cY | Len( cX ) > Len( cY ) } )
b := Len( aPathKeys )
FOR a := 1 TO b
aKeys := hb_HKeys( ::hCookies[ aDomKeys[ x ] ][ aPathKeys[ a ] ] )
d := Len( aKeys )
FOR c := 1 TO d
IF ! Empty( cOut )
cOut += "; "
ENDIF
cOut += aKeys[ c ] + "=" + ::hCookies[ aDomKeys[ x ] ][ aPathKeys[ a ] ][ aKeys[ c ] ]
NEXT
NEXT
NEXT
RETURN cOut
METHOD Boundary( nType ) CLASS TIPClientHTTP
/*
nType: 0=as found as the separator in the stdin stream
1=as found as the last one in the stdin stream
2=as found in the CGI enviroment
Examples:
-----------------------------41184676334 //in the body or stdin stream
-----------------------------41184676334-- //last one of the stdin stream
---------------------------41184676334 //in the header or CGI envirnment
*/
LOCAL cBound := ::cBoundary
LOCAL i
hb_default( @nType, 0 )
IF Empty( cBound )
cBound := Replicate( "-", 27 ) + Space( 11 )
FOR i := 28 TO 38
cBound := Stuff( cBound, i, 1, Str( Int( hb_Random( 0, 9 ) ), 1, 0 ) )
NEXT
::cBoundary := cBound
ENDIF
cBound := iif( nType < 2, "--", "" ) + cBound + iif( nType == 1, "--", "" )
RETURN cBound
METHOD Attach( cName, cFileName, cType ) CLASS TIPClientHTTP
AAdd( ::aAttachments, { cName, cFileName, cType } )
RETURN NIL
METHOD PostMultiPart( xPostData, cQuery ) CLASS TIPClientHTTP
LOCAL cData := "", nI, cTmp, y, cBound := ::boundary()
LOCAL cCrlf := ::cCRlf, oSub
LOCAL nPos
LOCAL cFilePath, cName, cFile, cType
LOCAL nFile, cBuf, nBuf, nRead
IF Empty( xPostData )
ELSEIF HB_ISHASH( xPostData )
y := Len( xPostData )
FOR nI := 1 TO y
cTmp := tip_URLEncode( AllTrim( hb_CStr( hb_HKeyAt( xPostData, nI ) ) ) )
cData += cBound + cCrlf + 'Content-Disposition: form-data; name="' + cTmp + '"' + cCrlf + cCrLf
cTmp := tip_URLEncode( AllTrim( hb_CStr( hb_HValueAt( xPostData, nI ) ) ) )
cData += cTmp + cCrLf
NEXT
ELSEIF HB_ISARRAY( xPostData )
y := Len( xPostData )
FOR nI := 1 TO y
cTmp := tip_URLEncode( AllTrim( hb_CStr( xPostData[ nI, 1 ] ) ) )
cData += cBound + cCrlf + 'Content-Disposition: form-data; name="' + cTmp + '"' + cCrlf + cCrLf
cTmp := tip_URLEncode( AllTrim( hb_CStr( xPostData[ nI, 2 ] ) ) )
cData += cTmp + cCrLf
NEXT
ELSEIF HB_ISSTRING( xPostData )
cData := xPostData
ENDIF
FOR EACH oSub IN ::aAttachments
cName := oSub[ 1 ]
cFile := oSub[ 2 ]
cType := oSub[ 3 ]
cTmp := StrTran( cFile, "/", "\" )
IF ( nPos := RAt( "\", cTmp ) ) != 0
cFilePath := Left( cTmp, nPos )
ELSEIF ( nPos := RAt( ":", cTmp ) ) != 0
cFilePath := Left( cTmp, nPos )
ELSE
cFilePath := ""
ENDIF
cTmp := SubStr( cFile, Len( cFilePath ) + 1 )
IF Empty( cType )
cType := "text/html"
ENDIF
cData += cBound + cCrlf + 'Content-Disposition: form-data; name="' + cName + '"; filename="' + cTmp + '"' + cCrlf + 'Content-Type: ' + cType + cCrLf + cCrLf
// hope this is not a big file....
nFile := FOpen( cFile )
/* TOFIX: Error checking on nFile. [vszakats] */
nbuf := 8192
nRead := nBuf
// cBuf := Space( nBuf )
DO WHILE nRead == nBuf
// nRead := FRead( nFile, @cBuf, nBuf )
cBuf := FReadStr( nFile, nBuf )
nRead := hb_BLen( cBuf )
#if 0
IF nRead < nBuf
cBuf := PadR( cBuf, nRead )
ENDIF
#endif
cData += cBuf
ENDDO
FClose( nFile )
cData += cCrlf
NEXT
cData += cBound + "--" + cCrlf
IF ! HB_ISSTRING( cQuery )
cQuery := ::oUrl:BuildQuery()
ENDIF
::inetSendAll( ::SocketCon, "POST " + cQuery + " HTTP/1.1" + ::cCRLF )
::StandardFields()
IF ! "Content-Type" $ ::hFields
::inetSendAll( ::SocketCon, e"Content-Type: multipart/form-data; boundary=" + ::boundary( 2 ) + ::cCrlf )
ENDIF
::inetSendAll( ::SocketCon, "Content-Length: " + hb_ntos( Len( cData ) ) + ::cCRLF )
// End of header
::inetSendAll( ::SocketCon, ::cCRLF )
IF ::inetErrorCode( ::SocketCon ) == 0
::inetSendAll( ::SocketCon, cData )
::bInitialized := .T.
RETURN ::ReadHeaders()
ENDIF
RETURN .F.
METHOD WriteAll( cFile ) CLASS TIPClientHTTP
LOCAL nFile
LOCAL lSuccess
LOCAL cStream
IF ( nFile := FCreate( cFile ) ) != F_ERROR
cStream := ::ReadAll()
lSuccess := ( FWrite( nFile, cStream ) == hb_BLen( cStream ) )
FClose( nFile )
ELSE
lSuccess := .F.
ENDIF
RETURN lSuccess