#!/usr/bin/rexx
/* --------------------------------------------------------------------------------------- */
/* No freakin' copyright, no stinkin' license, no guarantees or warrantees                 */
/* (implied, explicit or whatever). Usage is totally and completely at your own risk.      */
/* Please keep this comment block as is if modifying this code. Thanks in advance,         */
/*   Ruurd Idenburg                                                                        */
/* --------------------------------------------------------------------------------------- */
::class xhr public
-- using attributes without‘expose’, so that methods are free in choosing variable names
::attribute host get
::attribute host set private
::attribute port get
::attribute port set private
::attribute socket get
::attribute socket set private
::attribute reqHeaders get
::attribute reqHeaders set private
::attribute hostname get
::attribute hostname set private
::attribute rspHeaders get
::attribute rspHeaders set private
::attribute response get
::attribute response set private

::method init
	use strict arg host, port=80
	self~hostname = host
	self~port = port
	self~host = .inetaddress~new(host,port)
	self~rspHeaders = .array~new
exit 

::method doHeaders private
	self~reqHeaders = .array~new
	self~addHeader('Host:' self~hostname)
	self~addHeader('User-Agent: Not My Browser 0.9')
	self~addHeader('Accept: */*')
exit

::method get -- HTTP/1.1 GET with support for Content-Length: or Transfer-Encoding: chunked 
	use strict arg query
	self~socket = .socket~new
	if self~socket~connect(self~host)<0 then do
		raise syntax 48.900 array('Socket connection to' self~hostname 'on port' self~port 'failed')
	end
	self~doHeaders
	request = 'GET' query 'HTTP/1.1' || '0d0a'x || self~reqHeaders~makeString(,'0d0a'x) || '0d0a0d0a'x
	if self~socket~send(request)<0 then do
		raise syntax 48.900 array('Sending request to' self~hostname 'on port' self~port 'failed')
	end
	recvLength = 1024
	response = self~socket~recv(recvLength)
	if response==.nil then do
		if self~socket~errno<0 then do
			raise syntax 48.900 array('Receiving response from' self~hostname 'on port' self~port 'failed')
		end
		else do
			raise syntax 48.900 array('Socket on' self~hostname 'on port' self~port 'already closed')
		end
	end
	parse var response headers '0d0a0d0a'x data
	parse var headers . 'Content-Length:' length '0d0a'x .
	if length=='' then do
		parse var headers . 'Transfer-Encoding:' how '0d0a'x .
		if how='chunked' then do
			data = self~getChunked(data)
		end
		else do
			raise syntax 48.900 array('Unsupported Transfer-Encoding:' how)
		end
	end
	else do (length>data~length) -- if we need to receive more content
		data = self~getLength(data,length) 
	end
	self~rspHeaders = headers~makeArray('0d0a'x)
	self~response = data
	if self~socket~close<0 then do
		raise syntax 48.900 array('Closing socket for' self~hostname 'on port' self~port 'failed')
	end
exit
	
::method post -- HTTP/1.1 POST, for now copied mostly from GET; will do finetuning/combining later on 
	use strict arg target,content
	self~socket = .socket~new
	if self~socket~connect(self~host)<0 then do
		raise syntax 48.900 array('Socket connection to' self~hostname 'on port' self~port 'failed')
	end
	self~doHeaders
	self~addHeader('Content-Type: application/x-www-form-urlencoded')
	self~addHeader('Content-Length:' content~length)
	request = 'POST' target 'HTTP/1.1' || '0d0a'x || self~reqHeaders~makeString(,'0d0a'x) || '0d0a0d0a'x || content
	if self~socket~send(request)<0 then do
		raise syntax 48.900 array('Sending request to' self~hostname 'on port' self~port 'failed')
	end
	recvLength = 1024
	response = self~socket~recv(recvLength)
	if response==.nil then do
		if self~socket~errno<0 then do
			raise syntax 48.900 array('Receiving response from' self~hostname 'on port' self~port 'failed')
		end
		else do
			raise syntax 48.900 array('Socket on' self~hostname 'on port' self~port 'already closed')
		end
	end
	parse var response headers '0d0a0d0a'x data
	parse var headers . 'Content-Length:' length '0d0a'x .
	if length=='' then do
		parse var headers . 'Transfer-Encoding:' how '0d0a'x .
		if how='chunked' then do
			data = self~getChunked(data)
		end
		else do
			raise syntax 48.900 array('Unsupported Transfer-Encoding:' how)
		end
	end
	else do (length>data~length) -- if we need to receive more content
		data = self~getLength(data,length) 
	end
	self~rspHeaders = headers~makeArray('0d0a'x)
	self~response = data
	if self~socket~close<0 then do
		raise syntax 48.900 array('Closing socket for' self~hostname 'on port' self~port 'failed')
	end
exit
	
::method addHeader	-- Allow for user supplied request headers
	use strict arg header
	self~reqHeaders~append(header)
exit
	
::method getChunked private
	use strict arg chunks
	-- The end of content in chunked mode is identified by the last chunk ('0' || '0d0a0d0a'x )
  lastChunk = .false
  if chunks<>"" then do
    lastChunk = (chunks~substr(chunks~length-4)==(0 || '0d0a0d0a'x))
  end
	do while \lastChunk
		nextChunk = self~socket~recv(1024)
		if nextChunk==.nil then do
			if self~socket~errno<0 then do
				raise syntax 48.900 array('Receiving response chunk from' self~hostname 'on port' self~port 'failed')
			end
			else do
				raise syntax 48.900 array('Socket on' self~hostname 'on port' self~port 'already closed')
			end
		end
		chunks ||= nextChunk
		lastChunk = (chunks~substr(chunks~length-4)==(0 || '0d0a0d0a'x))
	end
	data = ''
	do until chunks==(0 || '0d0a0d0a'x)
		parse var chunks header '0d0a'x chunk '0d0a'x chunks
		data ||= chunk
	end
	return data
exit
	
::method getLength private
	use strict arg content,length
	recvLength = length-content~length -- already received part of the content
	do while recvLength>0
		nextContent = self~socket~recv(recvLength)
		if nextContent==.nil then do
			if self~socket~errno<0 then do
				raise syntax 48.900 array('Receiving response chunk from' self~hostname 'on port' self~port 'failed')
			end
			else do
				raise syntax 48.900 array('Socket on' self~hostname 'on port' self~port 'already closed')
			end
		end
		content ||= nextContent
		recvLength -= nextContent~length
	end
	return content 
exit
	
::requires 'socket.cls' 