Files
mtapi/Examples/MatLab/AdvancedExample/Slack/private/urlread2.m
T

372 lines
13 KiB
Matlab
Raw Normal View History

2020-10-24 10:14:58 +02:00
function [output,extras] = urlread2(urlChar,method,body,headersIn,varargin)
%urlread2 Makes HTTP requests and processes response
%
% [output,extras] = urlread2(urlChar, *method, *body, *headersIn, varargin)
%
% * indicates optional inputs that must be entered in place
%
% UNDOCUMENTED MATLAB VERSION
%
% EXAMPLE CALLING FORMS
% ... = urlread2(urlChar)
% ... = urlread2(urlChar,'GET','',[],prop1,value1,prop2,value2,etc)
% ... = urlread2(urlChar,'POST',body,headers)
%
% FEATURES
% =======================================================================
% 1) Allows specification of any HTTP method
% 2) Allows specification of any header. Very little is hard-coded
% in for header handling.
% 3) Returns response status and headers
% 4) Should handle unicode properly ...
%
% OUTPUTS
% =======================================================================
% output : body of the response, either text or binary depending upon
% CAST_OUTPUT property
% extras : (structure)
% .allHeaders - stucture, fields have cellstr values, HTTP headers may
% may be repeated but will have a single field entry, with each
% repeat's value another being another entry in the cellstr, for
% example:
% .Set_Cookie = {'first_value' 'second_value'}
% .firstHeaders - (structure), variable fields, contains the first
% string entry for each field in allHeaders, this
% structure can be used to avoid dereferencing a cell
% for fields you expect not to be repeated ...
% EXAMPLE:
% .Response : 'HTTP/1.1 200 OK'}
% .Server : 'nginx'
% .Date : 'Tue, 29 Nov 2011 02:23:16 GMT'
% .Content_Type : 'text/html; charset=UTF-8'
% .Content_Length : '109155'
% .Connection : 'keep-alive'
% .Vary : 'Accept-Encoding, User-Agent'
% .Cache_Control : 'max-age=60, private'
% .Set_Cookie : 'first_value'
% .status - (structure)
% .value : numeric value of status, ex. 200
% .msg : message that goes along with status, ex. 'OK'
% .url - eventual url that led to output, this can change from
% the input with redirects, see FOLLOW_REDIRECTS
% .isGood - (logical) I believe this is an indicator of the presence of 400
% or 500 status codes (see status.value) but more
% testing is needed. In other words, true if status.value < 400.
% In code, set true if the response was obtainable without
% resorting to checking the error stream.
%
% INPUTS
% =======================================================================
% urlChar : The full url, must include scheme (http, https)
% method : examples: 'GET' 'POST' etc
% body : (vector)(char, uint8 or int8) body to write, generally used
% with POST or PUT, use of uint8 or int8 ensures that the
% body input is not manipulated before sending, char is sent
% via unicode2native function with ENCODING input (see below)
% headersIn : (structure array), use empty [] or '' if no headers are needed
% but varargin property/value pairs are, multiple headers
% may be passed in as a structure array
% .name - (string), name of the header, a name property is used
% instead of a field because the name must match a valid
% header
% .value - (string), value to use
%
% OPTIONAL INPUTS (varargin, property/value pairs)
% =======================================================================
% CAST_OUTPUT : (default true) output is uint8, useful if the body
% of the response is not text
% ENCODING : (default ''), ENCODING input to function unicode2native
% FOLLOW_REDIRECTS : (default true), if false 3xx status codes will
% be returned and need to be handled by the user,
% note this does not handle javascript or meta tag
% redirects, just server based ones
% READ_TIMEOUT : (default 0), 0 means no timeout, value is in
% milliseconds
%
% EXAMPLES
% =======================================================================
% GET:
% --------------------------------------------
% url = 'http://www.mathworks.com/matlabcentral/fileexchange/';
% query = 'urlread2';
% params = {'term' query};
% queryString = http_paramsToString(params,1);
% url = [url '?' queryString];
% [output,extras] = urlread2(url);
%
% POST:
% --------------------------------------------
% url = 'http://posttestserver.com/post.php';
% params = {'testChars' char([2500 30000]) 'new code' '?'};
% [paramString,header] = http_paramsToString(params,1);
% [output,extras] = urlread2(url,'POST',paramString,header);
%
% From behind a firewall, use the Preferences to set your proxy server.
%
% See Also:
% http_paramsToString
% unicode2native
% native2unicode
%
% Subfunctions:
% fixHeaderCasing - small subfunction to fix case errors encountered in real
% world, requires updating when casing doesn't match expected form, like
% if someone sent the header content-Encoding instead of
% Content-Encoding
%
% Based on original urlread code by Matthew J. Simoneau
%
% VERSION = 1.1
in.CAST_OUTPUT = true;
in.FOLLOW_REDIRECTS = true;
in.READ_TIMEOUT = 0;
in.ENCODING = '';
%Input handling
%---------------------------------------
if ~isempty(varargin)
for i = 1:2:numel(varargin)
prop = upper(varargin{i});
value = varargin{i+1};
if isfield(in,prop)
in.(prop) = value;
else
error('Unrecognized input to function: %s',prop)
end
end
end
if ~exist('method','var') || isempty(method), method = 'GET'; end
if ~exist('body','var'), body = ''; end
if ~exist('headersIn','var'), headersIn = []; end
assert(usejava('jvm'),'Function requires Java')
import com.mathworks.mlwidgets.io.InterruptibleStreamCopier;
com.mathworks.mlwidgets.html.HTMLPrefs.setProxySettings %Proxy settings need to be set
%Create a urlConnection.
%-----------------------------------
urlConnection = getURLConnection(urlChar);
%For HTTP uses sun.net.www.protocol.http.HttpURLConnection
%Might use ice.net.HttpURLConnection but this has more overhead
%SETTING PROPERTIES
%-------------------------------------------------------
urlConnection.setRequestMethod(upper(method));
urlConnection.setFollowRedirects(in.FOLLOW_REDIRECTS);
urlConnection.setReadTimeout(in.READ_TIMEOUT);
for iHeader = 1:length(headersIn)
curHeader = headersIn(iHeader);
urlConnection.setRequestProperty(curHeader.name,curHeader.value);
end
if ~isempty(body)
%Ensure vector?
if size(body,1) > 1
if size(body,2) > 1
error('Input parameter to function: body, must be a vector')
else
body = body';
end
end
if ischar(body)
%NOTE: '' defaults to Matlab's default encoding scheme
body = unicode2native(body,in.ENCODING);
elseif ~(strcmp(class(body),'uint8') || strcmp(class(body),'int8'))
error('Function input: body, should be of class char, uint8, or int8, detected: %s',class(body))
end
urlConnection.setRequestProperty('Content-Length',int2str(length(body)));
urlConnection.setDoOutput(true);
outputStream = urlConnection.getOutputStream;
outputStream.write(body);
outputStream.close;
else
urlConnection.setRequestProperty('Content-Length','0');
end
%==========================================================================
% Read the data from the connection.
%==========================================================================
%This should be done first because it tells us if things are ok or not
%NOTE: If there is an error, functions below using urlConnection, notably
%getResponseCode, will fail as well
try
inputStream = urlConnection.getInputStream;
isGood = true;
catch ME
isGood = false;
%NOTE: HTTP error codes will throw an error here, we'll allow those for now
%We might also get another error in which case the inputStream will be
%undefined, those we will throw here
inputStream = urlConnection.getErrorStream;
if isempty(inputStream)
msg = ME.message;
I = strfind(msg,char([13 10 9])); %see example by setting timeout to 1
%Should remove the barf of the stack, at ... at ... at ... etc
%Likely that this could be improved ... (generate link with full msg)
if ~isempty(I)
msg = msg(1:I(1)-1);
end
fprintf(2,'Response stream is undefined\n below is a Java Error dump (truncated):\n');
error(msg)
end
end
%POPULATING HEADERS
%--------------------------------------------------------------------------
allHeaders = struct;
allHeaders.Response = {char(urlConnection.getHeaderField(0))};
done = false;
headerIndex = 0;
while ~done
headerIndex = headerIndex + 1;
headerValue = char(urlConnection.getHeaderField(headerIndex));
if ~isempty(headerValue)
headerName = char(urlConnection.getHeaderFieldKey(headerIndex));
headerName = fixHeaderCasing(headerName); %NOT YET FINISHED
%Important, for name safety all hyphens are replace with underscores
headerName(headerName == '-') = '_';
if isfield(allHeaders,headerName)
allHeaders.(headerName) = [allHeaders.(headerName) headerValue];
else
allHeaders.(headerName) = {headerValue};
end
else
done = true;
end
end
firstHeaders = struct;
fn = fieldnames(allHeaders);
for iHeader = 1:length(fn)
curField = fn{iHeader};
firstHeaders.(curField) = allHeaders.(curField){1};
end
status = struct(...
'value', urlConnection.getResponseCode(),...
'msg', char(urlConnection.getResponseMessage));
%PROCESSING OF OUTPUT
%----------------------------------------------------------
byteArrayOutputStream = java.io.ByteArrayOutputStream;
% This StreamCopier is unsupported and may change at any time. OH GREAT :/
isc = InterruptibleStreamCopier.getInterruptibleStreamCopier;
isc.copyStream(inputStream,byteArrayOutputStream);
inputStream.close;
byteArrayOutputStream.close;
if in.CAST_OUTPUT
charset = '';
%Extraction of character set from Content-Type header if possible
if isfield(firstHeaders,'Content_Type')
text = firstHeaders.Content_Type;
%Always open to regexp improvements
charset = regexp(text,'(?<=charset=)[^\s]*','match','once');
end
if ~isempty(charset)
output = native2unicode(typecast(byteArrayOutputStream.toByteArray','uint8'),charset);
else
output = char(typecast(byteArrayOutputStream.toByteArray','uint8'));
end
else
%uint8 is more useful for later charecter conversions
%uint8 or int8 is somewhat arbitary at this point
output = typecast(byteArrayOutputStream.toByteArray','uint8');
end
extras = struct;
extras.allHeaders = allHeaders;
extras.firstHeaders = firstHeaders;
extras.status = status;
%Gets eventual url even with redirection
extras.url = char(urlConnection.getURL);
extras.isGood = isGood;
end
function headerNameOut = fixHeaderCasing(headerName)
%fixHeaderCasing Forces standard casing of headers
%
% headerNameOut = fixHeaderCasing(headerName)
%
% This is important for field access in a structure which
% is case sensitive
%
% Not yet finished.
% I've been adding to this function as problems come along
switch lower(headerName)
case 'location'
headerNameOut = 'Location';
case 'content_type'
headerNameOut = 'Content_Type';
otherwise
headerNameOut = headerName;
end
end
%==========================================================================
%==========================================================================
%==========================================================================
function urlConnection = getURLConnection(urlChar)
%getURLConnection
%
% urlConnection = getURLConnection(urlChar)
% Determine the protocol (before the ":").
protocol = urlChar(1:find(urlChar==':',1)-1);
% Try to use the native handler, not the ice.* classes.
try
switch protocol
case 'http'
%http://www.docjar.com/docs/api/sun/net/www/protocol/http/HttpURLConnection.html
handler = sun.net.www.protocol.http.Handler;
case 'https'
handler = sun.net.www.protocol.https.Handler;
end
catch ME
handler = [];
end
% Create the URL object.
try
if isempty(handler)
url = java.net.URL(urlChar);
else
url = java.net.URL([],urlChar,handler);
end
catch ME
error('Failure to parse URL or protocol not supported for:\nURL: %s',urlChar);
end
% Get the proxy information using MathWorks facilities for unified proxy
% preference settings.
mwtcp = com.mathworks.net.transport.MWTransportClientPropertiesFactory.create();
proxy = mwtcp.getProxy();
% Open a connection to the URL.
if isempty(proxy)
urlConnection = url.openConnection;
else
urlConnection = url.openConnection(proxy);
end
end