001/** 002 * 003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved. 004 * 005 * This library is free software; you can redistribute it and/or 006 * modify it under the terms of the GNU Lesser General Public 007 * License as published by the Free Software Foundation; either 008 * version 2.1 of the License, or (at your option) any later version. 009 * 010 * This library is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013 * Lesser General Public License for more details. 014 * 015 * You should have received a copy of the GNU Lesser General Public 016 * License along with this library. If not, see <http://www.gnu.org/licenses/>. 017 * 018 **/ 019package lucee.runtime.net.http; 020 021import java.io.IOException; 022import java.io.InputStream; 023import java.net.MalformedURLException; 024import java.net.URL; 025import java.nio.charset.Charset; 026import java.util.HashMap; 027import java.util.Iterator; 028import java.util.Map; 029import java.util.Map.Entry; 030 031import lucee.commons.io.IOUtil; 032import lucee.commons.lang.ExceptionUtil; 033import lucee.commons.lang.StringUtil; 034import lucee.commons.lang.mimetype.MimeType; 035import lucee.commons.net.HTTPUtil; 036import lucee.commons.net.http.HTTPEngine; 037import lucee.commons.net.http.HTTPResponse; 038import lucee.commons.net.http.Header; 039import lucee.runtime.ComponentPage; 040import lucee.runtime.Info; 041import lucee.runtime.PageContext; 042import lucee.runtime.PageContextImpl; 043import lucee.runtime.converter.ConverterException; 044import lucee.runtime.converter.JSONConverter; 045import lucee.runtime.converter.ScriptConverter; 046import lucee.runtime.dump.DumpData; 047import lucee.runtime.dump.DumpProperties; 048import lucee.runtime.dump.DumpTable; 049import lucee.runtime.dump.SimpleDumpData; 050import lucee.runtime.engine.ThreadLocalPageContext; 051import lucee.runtime.exp.ApplicationException; 052import lucee.runtime.exp.ExpressionException; 053import lucee.runtime.exp.PageException; 054import lucee.runtime.exp.PageRuntimeException; 055import lucee.runtime.net.proxy.ProxyData; 056import lucee.runtime.net.rpc.RPCException; 057import lucee.runtime.op.Caster; 058import lucee.runtime.type.Array; 059import lucee.runtime.type.ArrayImpl; 060import lucee.runtime.type.Collection; 061import lucee.runtime.type.Collection.Key; 062import lucee.runtime.type.Iteratorable; 063import lucee.runtime.type.KeyImpl; 064import lucee.runtime.type.Objects; 065import lucee.runtime.type.Struct; 066import lucee.runtime.type.StructImpl; 067import lucee.runtime.type.UDF; 068import lucee.runtime.type.dt.DateTime; 069import lucee.runtime.type.it.KeyAsStringIterator; 070import lucee.runtime.type.it.KeyIterator; 071import lucee.runtime.type.it.ObjectsEntryIterator; 072import lucee.runtime.type.it.ObjectsIterator; 073import lucee.runtime.type.util.ArrayUtil; 074import lucee.runtime.type.util.KeyConstants; 075import lucee.runtime.type.util.ListUtil; 076import lucee.runtime.type.util.UDFUtil; 077 078/** 079 * Client to implement http based webservice 080 */ 081public class HTTPClient implements Objects, Iteratorable { 082 083 private static final long serialVersionUID = -7920478535030737537L; 084 085 private static final String USER_AGENT = "Lucee "+Info.getFullVersionInfo(); 086 087 088 private URL metaURL; 089 private String username; 090 private String password; 091 private ProxyData proxyData; 092 private URL url; 093 private Struct meta; 094 095 private int argumentsCollectionFormat=-1; 096 097 public HTTPClient(String httpUrl, String username, String password, ProxyData proxyData) throws PageException { 098 try { 099 url=HTTPUtil.toURL(httpUrl,true); 100 101 if(!StringUtil.isEmpty(this.url.getQuery())) throw new ApplicationException("invalid url, query string is not allowed as part of the call"); 102 metaURL=HTTPUtil.toURL(url.toExternalForm()+"?cfml",true); 103 } 104 catch (MalformedURLException e) { 105 throw Caster.toPageException(e); 106 } 107 108 this.username=username; 109 this.password=password; 110 this.proxyData=proxyData; 111 112 } 113 114 @Override 115 public DumpData toDumpData(PageContext pageContext, int maxlevel, DumpProperties dp) { 116 try { 117 Array args; 118 Struct sct = getMetaData(pageContext),val,a; 119 DumpTable cfc = new DumpTable("udf","#66ccff","#ccffff","#000000"),udf,arg; 120 cfc.setTitle("Web Service (HTTP)"); 121 if(dp.getMetainfo())cfc.setComment(url.toExternalForm()); 122 Iterator<Entry<Key, Object>> it = sct.entryIterator(); 123 Entry<Key, Object> e; 124 // Loop UDFs 125 while(it.hasNext()){ 126 e = it.next(); 127 val=Caster.toStruct(e.getValue()); 128 129 // udf name 130 udf = new DumpTable("udf","#66ccff","#ccffff","#000000"); 131 arg = new DumpTable("udf","#66ccff","#ccffff","#000000"); 132 133 cfc.appendRow(1, new SimpleDumpData(e.getKey().getString()),udf); 134 135 // args 136 args = Caster.toArray(val.get(KeyConstants._arguments)); 137 udf.appendRow(1,new SimpleDumpData("arguments"),arg); 138 arg.appendRow(7,new SimpleDumpData("name"),new SimpleDumpData("required"),new SimpleDumpData("type")); 139 Iterator<Object> ait = args.valueIterator(); 140 while(ait.hasNext()){ 141 a=Caster.toStruct(ait.next()); 142 arg.appendRow(0, 143 new SimpleDumpData(Caster.toString(a.get(KeyConstants._name))), 144 new SimpleDumpData(Caster.toString(a.get(KeyConstants._required))), 145 new SimpleDumpData(Caster.toString(a.get(KeyConstants._type)))); 146 147 } 148 149 // return type 150 udf.appendRow(1,new SimpleDumpData("return type"),new SimpleDumpData(Caster.toString(val.get(KeyConstants._returntype)))); 151 152 153 /* 154 cfc.appendRow(new DumpRow(0,new DumpData[]{ 155 new SimpleDumpData(arg.getDisplayName()), 156 new SimpleDumpData(e.getKey().getString()), 157 new SimpleDumpData(arg.isRequired()), 158 new SimpleDumpData(arg.getTypeAsString()), 159 def, 160 new SimpleDumpData(arg.getHint())}));*/ 161 162 } 163 return cfc; 164 165 } 166 catch (Throwable t) { 167 ExceptionUtil.rethrowIfNecessary(t); 168 throw new PageRuntimeException(Caster.toPageException(t)); 169 } 170 } 171 172 private Struct getMetaData(PageContext pc) { 173 if(meta==null) { 174 pc=ThreadLocalPageContext.get(pc); 175 InputStream is=null; 176 177 try{ 178 HTTPResponse rsp = HTTPEngine.get(metaURL, username, password, -1, 0, "UTF-8", USER_AGENT, proxyData, null); 179 MimeType mt = getMimeType(rsp,null); 180 int format = MimeType.toFormat(mt, -1); 181 if(format==-1) throw new ApplicationException("cannot convert response with mime type ["+mt+"] to a CFML Object"); 182 is = rsp.getContentAsStream(); 183 Struct data = Caster.toStruct(ReqRspUtil.toObject(pc,IOUtil.toBytes(is,false),format,mt.getCharset(),null)); 184 Object oUDF=data.get(KeyConstants._functions,null); 185 Object oAACF=data.get(ComponentPage.ACCEPT_ARG_COLL_FORMATS,null); 186 187 if(oUDF!=null && oAACF!=null) { 188 meta=Caster.toStruct(oUDF); 189 String[] strFormats = ListUtil.listToStringArray(Caster.toString(oAACF),','); 190 argumentsCollectionFormat=UDFUtil.toReturnFormat(strFormats,UDF.RETURN_FORMAT_JSON); 191 } 192 else { 193 meta=data; 194 } 195 196 197 } 198 catch(Throwable t) { 199 ExceptionUtil.rethrowIfNecessary(t); 200 throw new PageRuntimeException(Caster.toPageException(t)); 201 } 202 finally { 203 IOUtil.closeEL(is); 204 } 205 } 206 return meta; 207 } 208 209 @Override 210 public Iterator<Key> keyIterator() { 211 try { 212 return getMetaData(null).keyIterator(); 213 } 214 catch (Exception e) { 215 return new KeyIterator(new Collection.Key[0]); 216 } 217 } 218 219 @Override 220 public Object call(PageContext pc, Key methodName, Object[] arguments) throws PageException { 221 checkFunctionExistence(pc,methodName,false); 222 223 if(arguments.length==0) return _callWithNamedValues(pc, methodName, new StructImpl()); 224 Struct m = checkFunctionExistence(pc,methodName,true); 225 226 227 Array args = Caster.toArray(m.get(KeyConstants._arguments,null),null); 228 if(args==null) args=new ArrayImpl(); 229 Struct sct=new StructImpl(),el; 230 String name; 231 for(int i=0;i<arguments.length;i++){ 232 if(args.size()>i) { 233 el=Caster.toStruct(args.get(i+1, null),null); 234 if(el!=null) { 235 name=Caster.toString(el.get(KeyConstants._name,null),null); 236 if(!StringUtil.isEmpty(name)) { 237 sct.set(name, arguments[i]); 238 continue; 239 } 240 } 241 } 242 sct.set("arg"+(i+1), arguments[i]); 243 } 244 245 246 return _callWithNamedValues(pc, methodName, sct); 247 } 248 249 @Override 250 251 public Object callWithNamedValues(PageContext pc, Key methodName, Struct args) throws PageException { 252 checkFunctionExistence(pc,methodName,false); 253 return _callWithNamedValues(pc, methodName, args); 254 255 } 256 257 private Object _callWithNamedValues(PageContext pc, Key methodName, Struct args) throws PageException { 258 259 // prepare request 260 Map<String,String> formfields=new HashMap<String, String>(); 261 formfields.put("method", methodName.getString()); 262 formfields.put("returnformat", "cfml"); 263 264 265 String str; 266 try { 267 if(UDF.RETURN_FORMAT_JSON==argumentsCollectionFormat) { 268 Charset cs = ((PageContextImpl)pc).getWebCharset(); 269 str = new JSONConverter(true,cs).serialize(pc,args,false); 270 formfields.put("argumentCollectionFormat", "json"); 271 } 272 else if(UDF.RETURN_FORMAT_SERIALIZE==argumentsCollectionFormat) { 273 str = new ScriptConverter().serialize(args); 274 formfields.put("argumentCollectionFormat", "cfml"); 275 } 276 else { 277 str = new ScriptConverter().serialize(args); // Json interpreter in Lucee also accepts cfscript 278 } 279 } 280 catch (ConverterException e) { 281 throw Caster.toPageException(e); 282 } 283 284 285 // add aparams to request 286 formfields.put("argumentCollection", str); 287 /* 288 Iterator<Entry<Key, Object>> it = args.entryIterator(); 289 Entry<Key, Object> e; 290 while(it.hasNext()){ 291 e = it.next(); 292 formfields.put(e.getKey().getString(), Caster.toString(e.getValue())); 293 }*/ 294 295 Map<String,String> headers=new HashMap<String, String>(); 296 headers.put("accept", "application/cfml,application/json"); // application/java disabled for the moment, it is not working when we have different lucee versions 297 298 InputStream is=null; 299 try { 300 // call remote cfc 301 HTTPResponse rsp = HTTPEngine.post(url, username, password, -1, 0, "UTF-8", USER_AGENT, proxyData,headers, formfields); 302 303 // read result 304 Header[] rspHeaders = rsp.getAllHeaders(); 305 MimeType mt = getMimeType(rspHeaders,null); 306 int format = MimeType.toFormat(mt, -1); 307 if(format==-1) { 308 if(rsp.getStatusCode()!=200) { 309 boolean hasMsg=false; 310 String msg=rsp.getStatusText(); 311 for(int i=0;i<rspHeaders.length;i++){ 312 if(rspHeaders[i].getName().equalsIgnoreCase("exception-message")){ 313 msg=rspHeaders[i].getValue(); 314 hasMsg=true; 315 } 316 } 317 is = rsp.getContentAsStream(); 318 ApplicationException ae = new ApplicationException("remote component throws the following error:"+msg); 319 if(!hasMsg)ae.setAdditional(KeyImpl.init("respone-body"),IOUtil.toString(is, mt.getCharset())); 320 321 throw ae; 322 } 323 throw new ApplicationException("cannot convert response with mime type ["+mt+"] to a CFML Object"); 324 } 325 is = rsp.getContentAsStream(); 326 return ReqRspUtil.toObject(pc,IOUtil.toBytes(is,false),format,mt.getCharset(),null); 327 } 328 catch (IOException ioe) { 329 throw Caster.toPageException(ioe); 330 } 331 finally { 332 IOUtil.closeEL(is); 333 } 334 } 335 336 private Struct checkFunctionExistence(PageContext pc,Key methodName, boolean getDataFromRemoteIfNecessary) throws ApplicationException { 337 if(getDataFromRemoteIfNecessary) getMetaData(pc); 338 if(meta==null) return null; 339 Struct m = Caster.toStruct(meta.get(methodName,null),null); 340 if(m==null) throw new ApplicationException( 341 "the remote component has no function with name ["+methodName+"]", 342 ExceptionUtil.createSoundexDetail(methodName.getString(),meta.keysAsStringIterator(),"functions")); 343 return m; 344 } 345 346 347 private MimeType getMimeType(HTTPResponse rsp, MimeType defaultValue) { 348 return getMimeType(rsp.getAllHeaders(), defaultValue); 349 } 350 private MimeType getMimeType(Header[] headers, MimeType defaultValue) { 351 String returnFormat=null,contentType=null; 352 for(int i=0;i<headers.length;i++){ 353 if(headers[i].getName().equalsIgnoreCase("Return-Format"))returnFormat=headers[i].getValue(); 354 else if(headers[i].getName().equalsIgnoreCase("Content-Type"))contentType=headers[i].getValue(); 355 } 356 MimeType rf=null,ct=null; 357 358 // return format 359 if(!StringUtil.isEmpty(returnFormat)) { 360 int format=UDFUtil.toReturnFormat(returnFormat,-1); 361 rf=MimeType.toMimetype(format, null); 362 } 363 // ContentType 364 if(!StringUtil.isEmpty(contentType)) { 365 ct= MimeType.getInstance(contentType); 366 } 367 if(rf!=null && ct!=null) { 368 if(rf.same(ct)) return ct; // because this has perhaps a charset definition 369 return rf; 370 } 371 if(rf!=null) return rf; 372 if(ct!=null) return ct; 373 374 375 return defaultValue; 376 } 377 378 @Override 379 public Object get(PageContext pc, Collection.Key key) throws PageException { 380 return call(pc,KeyImpl.init("get"+key.getString()), ArrayUtil.OBJECT_EMPTY); 381 } 382 383 @Override 384 public Object get(PageContext pc, Collection.Key key, Object defaultValue) { 385 try { 386 return call(pc,KeyImpl.init("get"+StringUtil.ucFirst(key.getString())), ArrayUtil.OBJECT_EMPTY); 387 } catch (PageException e) { 388 return defaultValue; 389 } 390 } 391 392 @Override 393 public Object set(PageContext pc, Collection.Key propertyName, Object value) throws PageException { 394 return call(pc,KeyImpl.init("set"+propertyName.getString()), new Object[]{value}); 395 } 396 397 @Override 398 public Object setEL(PageContext pc, Collection.Key propertyName, Object value) { 399 try { 400 return call(pc,KeyImpl.init("set"+propertyName.getString()), new Object[]{value}); 401 } catch (PageException e) { 402 return null; 403 } 404 } 405 406 @Override 407 public Iterator<String> keysAsStringIterator() { 408 return new KeyAsStringIterator(keyIterator()); 409 } 410 411 @Override 412 public Iterator<Object> valueIterator() { 413 return new ObjectsIterator(keyIterator(),this); 414 } 415 416 @Override 417 public Iterator<Entry<Key, Object>> entryIterator() { 418 return new ObjectsEntryIterator(keyIterator(), this); 419 } 420 421 @Override 422 public String castToString() throws ExpressionException { 423 throw new RPCException("can't cast Webservice to a string"); 424 } 425 426 @Override 427 public String castToString(String defaultValue) { 428 return defaultValue; 429 } 430 431 @Override 432 public boolean castToBooleanValue() throws ExpressionException { 433 throw new RPCException("can't cast Webservice to a boolean"); 434 } 435 436 @Override 437 public Boolean castToBoolean(Boolean defaultValue) { 438 return defaultValue; 439 } 440 441 @Override 442 public double castToDoubleValue() throws ExpressionException { 443 throw new RPCException("can't cast Webservice to a number"); 444 } 445 446 @Override 447 public double castToDoubleValue(double defaultValue) { 448 return defaultValue; 449 } 450 451 @Override 452 public DateTime castToDateTime() throws RPCException { 453 throw new RPCException("can't cast Webservice to a Date Object"); 454 } 455 456 @Override 457 public DateTime castToDateTime(DateTime defaultValue) { 458 return defaultValue; 459 } 460 461 @Override 462 public int compareTo(boolean b) throws ExpressionException { 463 throw new ExpressionException("can't compare Webservice Object with a boolean value"); 464 } 465 466 @Override 467 public int compareTo(DateTime dt) throws PageException { 468 throw new ExpressionException("can't compare Webservice Object with a DateTime Object"); 469 } 470 471 @Override 472 public int compareTo(double d) throws PageException { 473 throw new ExpressionException("can't compare Webservice Object with a numeric value"); 474 } 475 476 @Override 477 public int compareTo(String str) throws PageException { 478 throw new ExpressionException("can't compare Webservice Object with a String"); 479 } 480}