/*
Basic HTTP/1.1 client request handler
*/
const int DEFAULT_SERVER_PORT = 80
const int DEFAULT_SECURE_PORT = 443
const char DEFAULT_RESOURCE[] = "/"
const char DEFAULT_PROTOCOL[] = "http"
data ResourceParts{
char protocol[]
char hostName[]
int hostPort
char resource[]
}
component provides HTTPRequest requires io.Output out, net.TCPSocket, net.TLS, net.TLSContext, net.dns.DNS dns, data.StringUtil stringUtil, data.IntUtil intUtil {
String[] splitString(char string[], char with[])
{
int ndx
if ((ndx = stringUtil.find(string, with)) != StringUtil.NOT_FOUND)
{
return new String[](new String(stringUtil.subString(string, 0, ndx)), new String(stringUtil.subString(string, ndx+1, string.arrayLength - (ndx+1))))
}
return new String[](new String(string))
}
char[] readHeaderLine(Stream socket)
{
char buf[]
char last2[] = new char[2]
while (last2 != "\r\n")
{
char b[] = socket.recv(1)
buf = new char[](buf, b)
last2[0] = last2[1]
last2[1] = b[0]
}
return buf
}
Header[] readHeaders(Stream socket)
{
Header headers[]
char buf[]
char last4[] = new char[4]
while (last4 != "\r\n\r\n")
{
char b[] = socket.recv(1)
if (b == null) throw new Exception("remote connection terminated before full response received")
buf = new char[](buf, b)
last4[0] = last4[1]
last4[1] = last4[2]
last4[2] = last4[3]
last4[3] = b[0]
}
String lines[] = stringUtil.explode(buf, "\r\n")
headers = new Header[](new Header("response", lines[0].string))
for (int i = 1; i < lines.arrayLength; i++)
{
int ndx = stringUtil.find(lines[i].string, ":") + 1
char key[] = stringUtil.subString(lines[i].string, 0, ndx - 1)
char value[] = stringUtil.trim(stringUtil.subString(lines[i].string, ndx, lines[i].string.arrayLength - ndx))
headers = new Header[](headers, new Header(stringUtil.lowercase(key), value))
}
return headers
}
char[] getHeaderValue(Header headers[], char key[])
{
for (int i = 0; i < headers.arrayLength; i++)
{
if (headers[i].key == key)
return headers[i].value
}
return null
}
int hexToInt(char hex[])
{
int result = 0
hex = stringUtil.lowercase(hex)
for (int i = 0; i < hex.arrayLength; i++)
{
result = result << 4
if (hex[i] == "0") result += 0
else if (hex[i] == "1") result += 1
else if (hex[i] == "2") result += 2
else if (hex[i] == "3") result += 3
else if (hex[i] == "4") result += 4
else if (hex[i] == "5") result += 5
else if (hex[i] == "6") result += 6
else if (hex[i] == "7") result += 7
else if (hex[i] == "8") result += 8
else if (hex[i] == "9") result += 9
else if (hex[i] == "a") result += 10
else if (hex[i] == "c") result += 12
else if (hex[i] == "b") result += 11
else if (hex[i] == "d") result += 13
else if (hex[i] == "e") result += 14
else if (hex[i] == "f") result += 15
}
return result
}
char[] readContent(Stream s, Header headers[])
{
char content[]
if (getHeaderValue(headers, "content-length") != null)
{
int size = intUtil.intFromString(getHeaderValue(headers, "content-length"))
content = s.recv(size)
}
else if (getHeaderValue(headers, "transfer-encoding") == "chunked")
{
//format: [;fields]\r\n\r\n
bool done = false
while (!done)
{
char hdr[] = stringUtil.trim(readHeaderLine(s))
String tokens[] = stringUtil.explode(hdr, ";")
int chunkSize = hexToInt(stringUtil.trim(tokens[0].string))
if (chunkSize == 0)
{
s.recv(2)
done = true
}
else
{
char chunk[] = s.recv(chunkSize)
content = new char[](content, chunk)
s.recv(2)
}
}
}
return content
}
ResourceParts parseResource(char url[], bool secure)
{
//parse a URL in the format [protocol://]hostname[:port][/path/to/resource]
ResourceParts result
if (!secure)
result = new ResourceParts(DEFAULT_PROTOCOL, "", DEFAULT_SERVER_PORT, DEFAULT_RESOURCE)
else
result = new ResourceParts(DEFAULT_PROTOCOL, "", DEFAULT_SECURE_PORT, DEFAULT_RESOURCE)
int ndx = 0
if ((ndx = stringUtil.find(url, "://")) != StringUtil.NOT_FOUND)
{
result.protocol = stringUtil.subString(url, 0, ndx)
url = stringUtil.subString(url, ndx + 3, url.arrayLength - (ndx+3))
}
if ((ndx = stringUtil.find(url, "/")) != StringUtil.NOT_FOUND)
{
result.hostName = stringUtil.subString(url, 0, ndx)
url = stringUtil.subString(url, ndx, url.arrayLength - ndx)
result.resource = url
}
else
{
result.hostName = url
}
//check for a port number, but make sure we're not matching against "::" in IPv6 addresses
if ((ndx = stringUtil.rfind(result.hostName, ":")) != StringUtil.NOT_FOUND && (ndx != 0 && result.hostName[ndx-1] != ":"))
{
char tmp[] = result.hostName
result.hostName = stringUtil.subString(tmp, 0, ndx)
result.hostPort = intUtil.intFromString(stringUtil.subString(tmp, ndx+1, tmp.arrayLength - (ndx+1)))
}
return result
}
HTTPResponse handleRequest(char url[], Header headers[], char method[], char postData[], bool secure) {
//look up host address of URL and extract the resource path being requested
ResourceParts parts = parseResource(url, secure)
char resource[] = parts.resource
char serverIP[] = dns.getHostIP(parts.hostName)
if (parts.protocol == "https") secure = true
//construct the request message, including a "host" field if none is provided
char request[] = "$method $resource HTTP/1.1\r\n"
bool hostIncluded = false
for (int i = 0; i < headers.arrayLength; i++) {
if (stringUtil.lowercase(headers[i].key) == "host") { hostIncluded = true }
request = new char[](request, headers[i].key, ":", headers[i].value, "\r\n")
}
if (method == "POST") {
request = new char[](request, "content-length", ":", intUtil.makeString(postData.arrayLength), "\r\n")
}
if (!hostIncluded) { request = new char[](request, "host:$(parts.hostName)\r\n") }
request = new char[](request, "\r\n")
if (method == "POST") {
request = new char[](request, postData)
}
//connect to server and send request
Stream stream
TLS tls
TLSContext ctx
TCPSocket s = new TCPSocket()
if (s.connect(serverIP, parts.hostPort)) {
stream = s
if (secure)
{
ctx = new TLSContext(TLSContext.CLIENT)
ctx.setCipherSet(TLSContext.CIPHER_DEFAULT)
tls = new TLS(ctx)
if (tls.connect(s) == false)
{
throw new Exception("Could not establish secure connection to host '$(serverIP)', port '$(parts.hostPort)'")
}
stream = tls
}
stream.send(request)
headers = readHeaders(stream)
char content[] = readContent(stream, headers)
s.disconnect()
//for convenience here we parse the response line into code/message, and we also extract the content type
char responseCode[] = ""
char responseMessage[] = ""
String split[]
if ((split = splitString(getHeaderValue(headers, "response"), " ")).arrayLength == 2
&& (split = splitString(split[1].string, " ")).arrayLength == 2) {
responseCode = split[0].string
responseMessage = split[1].string
}
return new HTTPResponse(url, responseCode, responseMessage, content,
getHeaderValue(headers, "content-type"), headers)
} else {
throw new Exception("Could not connect to host")
}
}
HTTPResponse HTTPRequest:get(char url[], opt Header headers[], bool secure) {
return handleRequest(url, headers, "GET", null, secure)
}
HTTPResponse HTTPRequest:post(char url[], Header headers[], char postData[], opt bool secure) {
return handleRequest(url, headers, "POST", postData, secure)
}
}