Monday, November 08, 2010 at 12:06 AM.

system.verbs.builtins.tcp.httpClient

on httpClient (method="GET", server="", port=nil, path="", proxy=user.webBrowser.proxy.domain, proxyPort=user.webBrowser.proxy.port, proxyUserName=user.webBrowser.proxy.userName, proxyPassword=user.webBrowser.proxy.password, data="", datatype="", username="", password="", adrHdrTable=nil, cookiesOn=false, debug=false, timeOutTicks=60*30, flMessages=true, ctFollowRedirects=0, flJustHeaders=false, flAcceptOpml=false, flUseProxy=nil, adrRedirectInfo=nil) {
	<<Changes
		<<1/22/06; 7:33:35 PM by DW
			<<The code that checked to see if we're running on the "samemachine" wasn't checking if server was 127.0.0.1, and was only checking the port on user.inetd.config.http, when there may be more listeners running on different ports, as there are in the OPML Editor. It would only shortcut of it was localhost and if it was running on one of the daemons. 
		<<2/7/03; 3:49:12 PM by JES
			<<When testing for proxy-exempt servers, don't inclue the port number if specified in the server parameter.
		<<10/20/02; 2:32:23 AM by JES
			<<Respect user.webBrowser.proxy.exceptions.
		<<10/16/02; 6:55:56 PM by JES
			<<Removed debugging code.
		<<10/14/02; 11:10:16 AM by JES
			<<If server is 127.0.0.1 or "localhost", set flUseProxy to false.
		<<10/13/02; 6:23:28 PM by DW
			<<New optional param -- adrRedirectInfo. If it's non-nil (the default) and we're redirecting, we fill the table with info about the redirect. The RSS aggregator needs this information, and it may prove useful in other places. See xml.rss.readService for an example of its usage.
		<<12/19/01; 9:58:10 PM by JES
			<<Added a call to webBrowser.init, which initializes prefs at user.webBrowser.proxy. Added optional parameter, flUseProxy, which defaults to nil. If nil, then the value at user.webBrowser.proxy.enabled is used. Pass the value of flUseProxy when following redirects.
		<<8/14/01; 1:49:11 PM by JES
			<<When recursing to following redirects, pass in the flJustHeaders and flAcceptOpml parameters.
		<<8/9/01; 6:10:17 PM by JES
			<<If the stream is unexpectedly closed by the remote host, call tcp.abortStream, to avoid consuming the available connections.
		<<8/8/01; 3:21:27 PM by JES
			<<If no Accept header has been passed in, add an Accept header with the value: text/x-opml, */*, so that we get the response in opml instead of html.
		<<3/1/01; 1:57:33 PM by PBS
			<<Trim white space around the content-length header before coercing it to a number. If a server adds white space here, the coercion would fail, resulting in a connection counter leak. (History: this was discovered in Radio when reading the Red Herring channel.)
		<<1/10/01; 10:36:30 AM by DW
			<<Add an optional parameter, flJustHeaders, if true, we don't read the body, we just return the header portion of the response. This is useful if you just want to know how big a download is, and want to defer downloading big things until the middle of the night when it's quiet on the Internet.
		<<1/7/01; 9:44:18 PM by DW
			<<In Radio, set User-Agent to Radio UserLand.
			<<Changed the header so it doesn't use backslashes. The outliner now supports wrapping headlines.
		<<9/22/00; 11:27:50 AM by PBS
			<<Respect the user's offline settings.
		<<11/5/99; 3:00:39 AM by AR
			<<If the server doesn't send a Content-Length header, read until connection closes.
		<<10/21/99; 3:57:29 PM by AR
			<<Adapted to use new TCP verbs in 6.1.
		<<6/14/99; 5:23:37 AM by AR
			<<If flMessages is true, we now clean up the message area before we return.
		<<6/13/99; 9:20:04 AM by AR
			<<Both the read and write loop now yield time to other Frontier threads and to the system.
			<<The Host header is now generated correctly if the request goes thru a proxy server.
			<<Fixed a few inconsistencies in the cookie handling code. Thinks should now work correctly when connecting via a proxy server.
			<<Added an optional parameter, ctFollowRedirects, determining the number of times that HTTP redirects will be followed. In order to preserve previous behaviour, the default value is 0.
		<<3/4/99; 11:54:30 AM by PBS
			<<If adrHdrTable^ contains a "Host" sub-item, don't add a Host header item.
		<<3/2/99; 10:43:18 AM by DW
			<<if adrHdrTable^ contains a "User-Agent" sub-item, don't add a User-Agent header item
		<<10/25/98; 6:33:59 AM by DW
			<<allow the use of a colon in the server name to specify the port
				<<example: www.scripting.com:80
			<<if present, we use that port, not the one specified in the call
				<<the assumption is that most callers either specify 80 or don't specify the port at all
				<<for crawlers, for example, they definitely want to use the port specified in the server string
				<<some commands, esp in the webBrowser class, get the url from another piece of software
		<<9/28/98; 4:42:12 AM by DW
			<<if we're sending to the same machine, set the stream ID to 0
			<<user.webserver.responders.CGI.methods.any is copying it, hope no CGIs are using it!
		<<9/17/98; 7:11:56 AM by DW
			<<msgs now say that they're from tcp.httpClient.
			<<changed Connection to xxx timed out message to...
				<<Can't process the request because the connection to xxx timed out.
			<<allow server to be "localhost", if so, we totally optimize the functionality
		<<2/13/98; 8:57:10 AM by DW
			<<Initial code assembled from various sample scripts by Dave Winer.
			<<The goal is to have a single documented way to talk to HTTP servers.
			<<It's got a long parameter list, but requires no configuration, doesn't run off a user.xxx table.
			<<The target user of this script is someone who needs to understand how the code works.
			<<It will be distributed with Frontier on both platforms as soon as we agree that this is complete and works.
			<<When you call it, use parameter labels, don't depend on the order of these params.
			<<For examples, see tcp.examples.
	
	if tcp.isOffline () { //PBS 09/22/00: check offline status
		scriptError ("Can't send HTTP request because TCP is offline.")};
	
	webBrowser.init ();
	try { //set flUseProxy
		if (flUseProxy == nil) and (typeOf (flUseProxy) == unknownType) {
			flUseProxy = user.webBrowser.proxy.enabled};
		if (server == "127.0.0.1" or string.lower (server) == "localhost") { //JES 10/14/02: don't call the loopback address through the proxy
			flUseProxy = false};
		if flUseProxy { //check the exceptions table -- if we find a match then set flUseProxy to false
			on wildcardIpMatch (s, oneip) {
				local (i);
				if s == oneip { //optimization
					return (true)};
				for i = 1 to 4 {
					try {
						local (part = string.nthField (oneip, ".", i));
						local (pattern = string.nthField (s, ".", i));
						if pattern == "*" {
							continue};
						if pattern contains "-" {
							local (min = number (string.nthField (pattern, "-", 1)) );
							local (max = number (string.nthField (pattern, "-", 2)) );
							local (n = number (part));
							if n >= min and n <= max {
								continue}
							else {
								return (false)}};
						if pattern != part {
							return (false)}}
					else {
						return (false)}};
				return (true)};
			local (flIpAddress = false, hostToCheck = string.lower (string.nthField (server, ":", 1)) );
			try { //see if flIpAddress should be true
				local (serverWithoutDots = string.replaceAll (server, ".", ""));
				double (serverWithoutDots); //will error if non-numeric characters are present
				flIpAddress = true}
			else { //check domains, and then try a DNS lookup
				local (adr);
				local (lowerserver = string.lower (server));
				for adr in @user.webBrowser.proxy.exceptions {
					if string.wildcardMatch (hostToCheck, string.lower (nameOf (adr^)) ) {
						if adr^ {
							flUseProxy = false;
							break}}};
				if flUseProxy { //try a reverse lookup so we can check against IP address
					try {
						hostToCheck = tcp.dns.getDottedId (server);
						flIpAddress = true}}};
			if flUseProxy { //check to see if this specific IP is excepted
				if defined (user.webBrowser.proxy.exceptions.[hostToCheck]) {
					if user.webBrowser.proxy.exceptions.[hostToCheck] {
						flUseProxy = false}}};
			if flUseProxy { //check for IP-address exceptions which contain wildcard characters and ranges
				local (i);
				for i = 1 to sizeOf (user.webBrowser.proxy.exceptions) {
					if user.webBrowser.proxy.exceptions[i] {
						local (oneexception = nameOf (user.webBrowser.proxy.exceptions[i]));
						if wildcardIpMatch (oneexception, hostToCheck) {
							flUseProxy = false;
							break}}}}}};
	
	on displayMsg (s) {
		msg ("tcp.httpClient: " + s)};
	
	local (myAddress = tcp.myDottedID (), flSameMachine = false);
	bundle { //look for the port specified in the server string
		if server contains ":" {
			try {port = number (string.nthField (server, ":", 2))}; //it might not be a number
			server = string.nthField (server, ":", 1)};
		if port == nil { //no port was specified, either in the call or in the server string
			port = 80}};
	<<bundle //see if it's the same machine
		<<local (lowerserver = string.lower (server))
		<<if (lowerserver == "localhost") or (lowerserver == "127.0.0.1")
			<<flSameMachine = true
		<<else
			<<try {flSameMachine = tcp.equalNames (server, myAddress)} //DW 3/9/98
		<<
		<<bundle //check that we're running on the requested port, 1/22/06 by DW
			<<if flsamemachine //may turn false
				<<local (adrd, flfound = false)
				<<for adrd in @user.inetd.config
					<<if port == adrd^.port
						<<if inetd.isDaemonRunning (adrd)
							<<flfound = true
							<<break
				<<if not flfound
					<<flsamemachine = false
			<<bundle //old code
				<<PBS 9/18/98: check that the server on the requested port is Frontier.
				<<if port != user.inetd.config.http.port
					<<flSameMachine = false //it's the same machine, but it's not Frontier as a server
				<<else
					<<if not (inetd.isDaemonRunning (@user.inetd.config.http))
						<<flSameMachine = false //Frontier isn't running as a webserver, there must be another server on this machine
	
	if not (path beginsWith "/") {
		path = "/" + path};
	
	local (httpCommand = "", adrCookiesTable = @user.webBrowser.cookies);
	bundle { //build the HTTP request in "httpCommand"
		on add (s) {
			httpCommand = httpCommand + s + "\r\n"};
		local (uri = path);
		if flUseProxy and (sizeOf (proxy) > 0) { //the request is going thru a proxy server: set uri accordingly
			uri = "http://" + server;
			if port != 80 {
				uri = uri + ":" + port};
			uri = uri + path};
		add (method + " " + uri + " HTTP/1.0");
		if (adrHdrTable == nil) or (not defined (adrHdrTable^.["User-Agent"])) { //3/2/99; 10:43:10 AM by DW
			if system.environment.isPike {
				add ("User-Agent: Radio UserLand/" + Frontier.version () + " (" + sys.os () + ")")}
			else {
				add ("User-Agent: Frontier/" + Frontier.version () + " (" + sys.os () + ")")}};
		local (host = "Host: " + server);
		if port != 80 {
			host = host + ":" + port};
		if (adrHdrTable == nil) or (not defined (adrHdrTable^.["Host"])) { //3/4/99; 11:54:30 AM by PBS
			add (host)};
		if flAcceptOpml { //8/8/01; 3:19:50 PM by JES
			if (adrHdrTable == nil) {
				add ("Accept: text/x-opml, */*")}
			else { //headers were passed in
				if defined (adrHdrTable^.Accept) {
					local (acceptHeader = adrHdrTable^.Accept);
					if not (acceptHeader contains "text/x-opml") {
						if sizeOf (string.trimWhiteSpace (acceptHeader)) > 0 {
							acceptHeader = "text/x-opml, " + acceptHeader}
						else {
							acceptHeader = "text/x-opml"}};
					if not (acceptHeader contains "*/*") {
						acceptHeader = acceptHeader + ", */*"};
					add ("Accept: " + acceptHeader)}
				else {
					add ("Accept: text/x-opml, */*")}}};
		if sizeOf (data) > 0 {
			add ("Content-Type: " + datatype);
			add ("Content-length: " + sizeOf (data))};
		if (sizeOf (username) > 0) or (sizeOf (password) > 0) {
			add ("Authorization: Basic " + base64.encode (username + ":" + password, 0))};
		if flUseProxy and (sizeOf (proxy) > 0) { //the request is going thru a proxy server
			if (sizeOf (proxyUsername) > 0) or (sizeOf (proxyPassword) > 0) { //the proxy requires authentication
				add ("Proxy-Authorization: Basic " + base64.encode (proxyUsername + ":" + proxyPassword, 0))}};
		if adrHdrTable != nil { //added 3.27.98 by ASG to include header items
			local (i);
			for i = 1 to sizeOf (adrHdrTable^) {
				local (headerName = nameOf (adrHdrTable^[i]));
				if flAcceptOpml { //8/8/01; 3:19:50 PM by JES: Only add the Acept header once
					if string.lower (headerName) == "accept" {
						continue}};
				add (headerName + ": " + adrHdrTable^[i])}};
		
		if cookiesOn and defined (adrCookiesTable^) { //add cookies to the request header: ASG 4/9/98
			local (i, j, k, cookiePath = "/", cookiestring);
			if path != "/" {
				cookiePath = string.popSuffix (path, "/")};
			for i = 1 to sizeOf (adrCookiesTable^) { //loop through cookies table
				local (adrdomain = @adrCookiesTable^ [i]);
				if host endsWith nameOf (adrdomain^) {
					for j = 1 to sizeOf (adrdomain^) {
						local (adrcookie = @adrdomain^ [j]);
						if cookiePath contains nameOf (adrcookie^) {
							for k = 1 to sizeOf (adrcookie^) {
								if adrcookie^ [k].expires > clock.now () { //add this cookie
									cookiestring = cookiestring + nameOf (adrcookie^ [k]) + "=" + adrcookie^ [k].value + "; "}
								else {
									delete (@adrcookie^ [k])}}}}}}; //this cookie has expired so delete it
			if cookiestring != nil {
				cookiestring = string.delete (cookiestring, sizeof (cookiestring) - 1, 2); //delete trailing "; "
				add ("Cookie: " + cookiestring)}}; //add the cookies to the header
		
		add (""); //blank line
		if sizeOf (data) > 0 {
			httpCommand = httpCommand + data}};
	
	if debug { //save a copy of the HTTP request for debugging
		wp.newTextObject (httpCommand, @scratchpad.httpCommand)};
	
	local (httpResult = "");
	if flSameMachine { //shortcut the call, turn our worst case into our best case: 9/17/98; 7:21:13 AM by DW
		local (params);
		new (tabletype, @params);
		params.client = myAddress;
		params.port = port;
		params.stream = 0; //9/28/98; 4:42:45 AM by DW
		params.request = httpCommand;
		httpResult = webserver.server (@params, httpCommand)}
	else {
		local (serverAddress = server, serverPort = port);
		if flUseProxy and (sizeOf (proxy) > 0) { //the request is going thru a proxy server: switch server and port
			serverAddress = proxy;
			serverPort = proxyPort};
		
		if flMessages { //PBS 5/5/98
			displayMsg ("Connecting to " + serverAddress + ".")};
		
		local (stream = tcp.openStream (serverAddress, serverPort));
		local (chunksize = 5 * 1024);
		
		if flMessages {
			displayMsg ("Sending request to " + serverAddress + ".")};
		
		try {
			tcp.writeStringToStream (stream, httpCommand, chunkSize, timeOutTicks/60)} //write the command to the server
		else {
			tcp.abortStream (stream);
			scriptError (tryError)};
		
		if flMessages {
			displayMsg ("Waiting for data from " + serverAddress + ".")};
		
		local (pat = "\r\n\r\n");
		
		try {
			tcp.readStreamUntil (stream, pat, timeOutTicks/60, @httpResult)}
		else {
			tcp.abortStream (stream);
			scriptError (tryerror)};
		
		local (headerTable);
		webserver.util.parseHeaders (httpResult, @headerTable);
		
		if flJustHeaders { //the caller only wants the headers, not the body 1/10/01 by DW
			tcp.closeStream (stream)}
		else {
			if (string.upper (method) != "HEAD") and defined (headerTable.["Content-Length"]) { //we know there's content left to be read
				local (lenHeader = string.patternMatch (pat, httpResult) - 1);
				local (lenContent = number (string.trimWhiteSpace (headerTable.["Content-Length"]))); //PBS 03/01/01: trim white space before coercing
				local (lenTotal = lenHeader + string.length (pat) + lenContent);
				
				if flMessages {
					displayMsg ("Receiving response body  (" + string.kBytes (lenContent) + ") from " + serverAddress + ".")};
				try {
					tcp.readStreamBytes (stream, lenTotal, timeOutTicks/60, @httpResult)}
				else {
					tcp.abortStream (stream);
					scriptError (tryerror)}}
			else { //just read until connection closes
				if flMessages {
					displayMsg ("Receiving response from " + serverAddress + ".")};
				try {
					tcp.readStreamUntilClosed (stream, timeOutTicks/60, @httpResult)}
				else {
					tcp.abortStream (stream);
					scriptError (tryerror)}};
			
			tcp.closeStream (stream);
			if (httpResult != "") and flMessages {
				displayMsg ("Finished receiving " + string.kBytes (string.length (httpResult)) + " from " + serverAddress + ".")}}};
	
	if (httpResult != "") and cookiesOn { // find, parse, and store any cookies: ASG 4/9/98
		local (ixEndHeader, responseHeader, i, j);
		ixEndHeader = string.patternMatch ("\r\n\r\n", httpResult); //try to find the end of the response header
		if ixEndHeader > 0 { //there is a valid header, look for cookies
			responseHeader = string.mid (httpResult, 1, ixEndHeader - 1); //get the header
			if string.lower (responseHeader) contains "set-cookie:" { //the response header contains at least one cookie
				if not defined (adrCookiesTable^) {
					new (tableType, adrCookiesTable)};
				for i = 1 to string.countFields (responseHeader, "\r") { //find all the cookies
					local (onecookie = string.nthField (responseHeader, "\r", i)); //get the cookie
					onecookie = string.popLeading (onecookie, '\n');
					if string.lower (onecookie) beginsWith "set-cookie:" {
						local (flsecure = false); //not secure by default
						local (domain = server); //default domain to this server
						local (cookiePath = "/");
						local (expires = clock.now () + (24 * 60 * 60)); //default expires at "end of session"
						local (cookieName, cookieValue);
						if path != "/" {
							cookiePath = string.popSuffix (path, "/")};
						onecookie = string.popLeading (string.mid (onecookie, 12, infinity), ' ');
						for j = 1 to string.countFields (onecookie, ';') {
							local (cookiepart = string.nthField (onecookie, ';', j));
							cookiepart = string.popLeading (cookiepart, ' ');
							case string.lower (string.nthField (cookiepart, '=', 1)) { //take care of standard parts
								"domain" {
									domain = string.nthField (cookiepart, '=', 2)};
								"path" {
									cookiePath = string.nthField (cookiepart, '=', 2)};
								"expires" {
									on cookieDateToSystemDate (cookieDate) {
										try { //some cookie dates can be converted to dates directly
											local (systemDate = date (cookieDate));
											if systemDate < date ("Monday, 6-Feb-2040") {
												return (systemDate)}};
										try { //it may be a date in the 21st century
											local (lastpart = string.nthField (cookieDate, "-", 3));
											local (ixstart = string.patternMatch (lastpart, cookieDate));
											return (date (string.insert ("20", cookieDate, ixstart)))};
										return (false)};
									expires = cookieDateToSystemDate (string.nthField (cookiepart, '=', 2));
									if expires == false { //invalid cookie, ignore it
										break}};
								"secure" {
									flsecure = true}}
							else { //this is the actual cookie name/value
								cookieName = string.nthField (cookiepart, '=', 1);
								cookieValue = string.nthField (cookiepart, '=', 2)}};
						if clock.now () < expires { //store non-expired cookies only
							if not defined (adrCookiesTable^.[domain]) { //have we cookies already for this domain?
								new (tableType, @adrCookiesTable^.[domain])};
							if not defined (adrCookiesTable^.[domain].[cookiePath]) {
								new (tableType, @adrCookiesTable^.[domain].[cookiePath])};
							if not defined (adrCookiesTable^.[domain].[cookiePath].[cookieName]) {
								new (tableType, @adrCookiesTable^.[domain].[cookiePath].[cookieName])};
							adrCookiesTable^.[domain].[cookiePath].[cookieName].value = cookieValue;
							adrCookiesTable^.[domain].[cookiePath].[cookieName].expires = expires;
							adrCookiesTable^.[domain].[cookiePath].[cookieName].secure = flsecure}}}}}};
	
	if debug { //save a copy of the server's response for debugging
		wp.newTextObject (httpResult, @scratchpad.httpResult)};
	
	if (ctFollowRedirects > 0) { //check whether we are being redirected: AR 6/13/1999
		local (statusCode = string.nthField (httpResult, ' ', 2));
		if ({"301", "302", "303"} contains statusCode) { //response codes for Moved Permanently, Moved Temporarily, See Other
			local (flRedirect = false, redirectServer, redirectPath);
			local (t); new (tableType, @t);
			webserver.util.parseHeaders (httpResult, @t);
			if defined (t.location) { //the response has a Location: header field
				on isAbsoluteURL (s) {
					if not (s contains ":") {
						return (false)};
					local (scheme = string.lower (string.nthField (s, ":", 1)));
					if scheme == "" {
						return (false)};
					local (ix);
					for ix = 1 to sizeof (scheme) {
						if not string.isAlpha (scheme[ix]) {
							return (false)}};
					return (true)};
				on isAbsolutePath (s) {
					return (s beginsWith "/")};
				case true {
					isAbsoluteURL (t.location) {
						if string.lower (t.location) beginsWith "http://" { //it's an absolute HTTP URL
							local (s = string.delete (t.location, 1, string.length ("http://")));
							redirectServer = string.nthField (s, "/", 1);
							redirectPath = string.delete (s, 1, string.length (redirectServer) + 1);
							flRedirect = true}};
					isAbsolutePath (t.location) {
						redirectServer = server;
						redirectPath = t.location;
						flRedirect = true}}
				else { //assume it is a relative path
					redirectServer = server;
					redirectPath = string.popSuffix (path, "/") + "/" + t.location;
					flRedirect = true}};
			if flRedirect {
				if flMessages {
					displayMsg ("Redirected to " + t.Location + ".")};
				if statusCode == "303" { //use the GET method to retrieve the specified entity
					method = "GET"}; //see section 10.3.4 of RFC2068
				
				if adrRedirectInfo != nil { //10/13/02 by DW, accumulate info about the redirect
					local (adrsubtable = @adrRedirectInfo^.[string.padWithZeros (sizeof (adrRedirectInfo^) + 1, 5)]);
					new (tableType, adrsubtable);
					adrsubtable^.code = statusCode;
					adrsubtable^.server = redirectServer;
					adrsubtable^.port = port;
					adrsubtable^.path = redirectPath};
				
				httpResult = tcp.httpClient (method, redirectServer, port, redirectPath, proxy, proxyPort, proxyUserName, proxyPassword, data, datatype, username, password, adrHdrTable, cookiesOn, debug, timeOutTicks, flMessages, --ctFollowRedirects, flJustHeaders, flAcceptOpml, flUseProxy, adrRedirectInfo)}}};
	
	if flMessages { //clean up the message area: AR 6/14/1999
		msg ("")};
	
	return (httpResult)}



This listing is for code that runs in the OPML Editor environment. I created these listings because I wanted the search engines to index it, so that when I want to look up something in my codebase I don't have to use the much slower search functionality in my object database. Dave Winer.