Http.hx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. /*
  2. * Copyright (C)2005-2019 Haxe Foundation
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a
  5. * copy of this software and associated documentation files (the "Software"),
  6. * to deal in the Software without restriction, including without limitation
  7. * the rights to use, copy, modify, merge, publish, distribute, sublicense,
  8. * and/or sell copies of the Software, and to permit persons to whom the
  9. * Software is furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  19. * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  20. * DEALINGS IN THE SOFTWARE.
  21. */
  22. package sys;
  23. import haxe.io.BytesOutput;
  24. import haxe.io.Bytes;
  25. import haxe.io.Input;
  26. import sys.net.Host;
  27. import sys.net.Socket;
  28. class Http extends haxe.http.HttpBase {
  29. public var noShutdown:Bool;
  30. public var cnxTimeout:Float;
  31. public var responseHeaders:Map<String, String>;
  32. private var responseHeadersSameKey:Map<String, Array<String>>;
  33. var chunk_size:Null<Int>;
  34. var chunk_buf:haxe.io.Bytes;
  35. var file:{
  36. param:String,
  37. filename:String,
  38. io:haxe.io.Input,
  39. size:Int,
  40. mimeType:String
  41. };
  42. public static var PROXY:{host:String, port:Int, auth:{user:String, pass:String}} = null;
  43. public function new(url:String) {
  44. cnxTimeout = 10;
  45. #if php
  46. noShutdown = !php.Global.function_exists('stream_socket_shutdown');
  47. #end
  48. super(url);
  49. }
  50. public override function request(?post:Bool) {
  51. var output = new haxe.io.BytesOutput();
  52. var old = onError;
  53. var err = false;
  54. onError = function(e) {
  55. responseBytes = output.getBytes();
  56. err = true;
  57. // Resetting back onError before calling it allows for a second "retry" request to be sent without onError being wrapped twice
  58. onError = old;
  59. onError(e);
  60. }
  61. post = post || postBytes != null || postData != null;
  62. customRequest(post, output);
  63. if (!err) {
  64. success(output.getBytes());
  65. }
  66. }
  67. @:noCompletion
  68. @:deprecated("Use fileTransfer instead")
  69. inline public function fileTransfert(argname:String, filename:String, file:haxe.io.Input, size:Int, mimeType = "application/octet-stream") {
  70. fileTransfer(argname, filename, file, size, mimeType);
  71. }
  72. public function fileTransfer(argname:String, filename:String, file:haxe.io.Input, size:Int, mimeType = "application/octet-stream") {
  73. this.file = {
  74. param: argname,
  75. filename: filename,
  76. io: file,
  77. size: size,
  78. mimeType: mimeType
  79. };
  80. }
  81. public function customRequest(post:Bool, api:haxe.io.Output, ?sock:sys.net.Socket, ?method:String) {
  82. this.responseAsString = null;
  83. this.responseBytes = null;
  84. var url_regexp = ~/^(https?:\/\/)?([a-zA-Z\.0-9_-]+)(:[0-9]+)?(.*)$/;
  85. if (!url_regexp.match(url)) {
  86. onError("Invalid URL");
  87. return;
  88. }
  89. var secure = (url_regexp.matched(1) == "https://");
  90. if (sock == null) {
  91. if (secure) {
  92. #if php
  93. sock = new php.net.SslSocket();
  94. #elseif java
  95. sock = new java.net.SslSocket();
  96. #elseif python
  97. sock = new python.net.SslSocket();
  98. #elseif (!no_ssl && (hxssl || hl || cpp || (neko && !(macro || interp) || eval) || (lua && !lua_vanilla)))
  99. sock = new sys.ssl.Socket();
  100. #elseif (neko || cpp)
  101. throw "Https is only supported with -lib hxssl";
  102. #else
  103. throw new haxe.exceptions.NotImplementedException("Https support in haxe.Http is not implemented for this target");
  104. #end
  105. } else {
  106. sock = new Socket();
  107. }
  108. sock.setTimeout(cnxTimeout);
  109. }
  110. var host = url_regexp.matched(2);
  111. var portString = url_regexp.matched(3);
  112. var request = url_regexp.matched(4);
  113. // ensure path begins with a forward slash
  114. // this is required by original URL specifications and many servers have issues if it's not supplied
  115. // see https://stackoverflow.com/questions/1617058/ok-to-skip-slash-before-query-string
  116. if (request.charAt(0) != "/") {
  117. request = "/" + request;
  118. }
  119. var port = if (portString == null || portString == "") secure ? 443 : 80 else Std.parseInt(portString.substr(1, portString.length - 1));
  120. var multipart = (file != null);
  121. var boundary = null;
  122. var uri = null;
  123. if (multipart) {
  124. post = true;
  125. boundary = Std.string(Std.random(1000))
  126. + Std.string(Std.random(1000))
  127. + Std.string(Std.random(1000))
  128. + Std.string(Std.random(1000));
  129. while (boundary.length < 38)
  130. boundary = "-" + boundary;
  131. var b = new StringBuf();
  132. for (p in params) {
  133. b.add("--");
  134. b.add(boundary);
  135. b.add("\r\n");
  136. b.add('Content-Disposition: form-data; name="');
  137. b.add(p.name);
  138. b.add('"');
  139. b.add("\r\n");
  140. b.add("\r\n");
  141. b.add(p.value);
  142. b.add("\r\n");
  143. }
  144. b.add("--");
  145. b.add(boundary);
  146. b.add("\r\n");
  147. b.add('Content-Disposition: form-data; name="');
  148. b.add(file.param);
  149. b.add('"; filename="');
  150. b.add(file.filename);
  151. b.add('"');
  152. b.add("\r\n");
  153. b.add("Content-Type: " + file.mimeType + "\r\n" + "\r\n");
  154. uri = b.toString();
  155. } else {
  156. for (p in params) {
  157. if (uri == null)
  158. uri = "";
  159. else
  160. uri += "&";
  161. uri += StringTools.urlEncode(p.name) + "=" + StringTools.urlEncode('${p.value}');
  162. }
  163. }
  164. var b = new BytesOutput();
  165. if (method != null) {
  166. b.writeString(method);
  167. b.writeString(" ");
  168. } else if (post)
  169. b.writeString("POST ");
  170. else
  171. b.writeString("GET ");
  172. if (Http.PROXY != null) {
  173. b.writeString("http://");
  174. b.writeString(host);
  175. if (port != 80) {
  176. b.writeString(":");
  177. b.writeString('$port');
  178. }
  179. }
  180. b.writeString(request);
  181. if (!post && uri != null) {
  182. if (request.indexOf("?", 0) >= 0)
  183. b.writeString("&");
  184. else
  185. b.writeString("?");
  186. b.writeString(uri);
  187. }
  188. b.writeString(" HTTP/1.1\r\nHost: " + host + "\r\n");
  189. if (postData != null) {
  190. postBytes = Bytes.ofString(postData);
  191. postData = null;
  192. }
  193. if (postBytes != null)
  194. b.writeString("Content-Length: " + postBytes.length + "\r\n");
  195. else if (post && uri != null) {
  196. if (multipart || !Lambda.exists(headers, function(h) return h.name == "Content-Type")) {
  197. b.writeString("Content-Type: ");
  198. if (multipart) {
  199. b.writeString("multipart/form-data");
  200. b.writeString("; boundary=");
  201. b.writeString(boundary);
  202. } else
  203. b.writeString("application/x-www-form-urlencoded");
  204. b.writeString("\r\n");
  205. }
  206. if (multipart)
  207. b.writeString("Content-Length: " + (uri.length + file.size + boundary.length + 6) + "\r\n");
  208. else
  209. b.writeString("Content-Length: " + uri.length + "\r\n");
  210. }
  211. if( !Lambda.exists(headers, function(h) return h.name == "Connection") )
  212. b.writeString("Connection: close\r\n");
  213. for (h in headers) {
  214. b.writeString(h.name);
  215. b.writeString(": ");
  216. b.writeString(h.value);
  217. b.writeString("\r\n");
  218. }
  219. b.writeString("\r\n");
  220. if (postBytes != null)
  221. b.writeFullBytes(postBytes, 0, postBytes.length);
  222. else if (post && uri != null)
  223. b.writeString(uri);
  224. try {
  225. if (Http.PROXY != null)
  226. sock.connect(new Host(Http.PROXY.host), Http.PROXY.port);
  227. else
  228. sock.connect(new Host(host), port);
  229. if (multipart)
  230. writeBody(b, file.io, file.size, boundary, sock)
  231. else
  232. writeBody(b, null, 0, null, sock);
  233. readHttpResponse(api, sock);
  234. sock.close();
  235. } catch (e:Dynamic) {
  236. try
  237. sock.close()
  238. catch (e:Dynamic) {};
  239. onError(Std.string(e));
  240. }
  241. }
  242. /**
  243. Returns an array of values for a single response header or returns
  244. null if no such header exists.
  245. This method can be useful when you need to get a multiple headers with
  246. the same name (e.g. `Set-Cookie`), that are unreachable via the
  247. `responseHeaders` variable.
  248. **/
  249. public function getResponseHeaderValues(key:String):Null<Array<String>> {
  250. var array = responseHeadersSameKey.get(key);
  251. if (array == null) {
  252. var singleValue = responseHeaders.get(key);
  253. return (singleValue == null) ? null : [ singleValue ];
  254. } else {
  255. return array;
  256. }
  257. }
  258. function writeBody(body:Null<BytesOutput>, fileInput:Null<Input>, fileSize:Int, boundary:Null<String>, sock:Socket) {
  259. if (body != null) {
  260. var bytes = body.getBytes();
  261. sock.output.writeFullBytes(bytes, 0, bytes.length);
  262. }
  263. if (boundary != null) {
  264. var bufsize = 4096;
  265. var buf = haxe.io.Bytes.alloc(bufsize);
  266. while (fileSize > 0) {
  267. var size = if (fileSize > bufsize) bufsize else fileSize;
  268. var len = 0;
  269. try {
  270. len = fileInput.readBytes(buf, 0, size);
  271. } catch (e:haxe.io.Eof)
  272. break;
  273. sock.output.writeFullBytes(buf, 0, len);
  274. fileSize -= len;
  275. }
  276. sock.output.writeString("\r\n");
  277. sock.output.writeString("--");
  278. sock.output.writeString(boundary);
  279. sock.output.writeString("--");
  280. }
  281. }
  282. function readHttpResponse(api:haxe.io.Output, sock:sys.net.Socket) {
  283. // READ the HTTP header (until \r\n\r\n)
  284. var b = new haxe.io.BytesBuffer();
  285. var k = 4;
  286. var s = haxe.io.Bytes.alloc(4);
  287. sock.setTimeout(cnxTimeout);
  288. while (true) {
  289. var p = 0;
  290. while (p != k) {
  291. try {
  292. p += sock.input.readBytes(s, p, k - p);
  293. }
  294. catch (e:haxe.io.Eof) { }
  295. }
  296. b.addBytes(s, 0, k);
  297. switch (k) {
  298. case 1:
  299. var c = s.get(0);
  300. if (c == 10)
  301. break;
  302. if (c == 13)
  303. k = 3;
  304. else
  305. k = 4;
  306. case 2:
  307. var c = s.get(1);
  308. if (c == 10) {
  309. if (s.get(0) == 13)
  310. break;
  311. k = 4;
  312. } else if (c == 13)
  313. k = 3;
  314. else
  315. k = 4;
  316. case 3:
  317. var c = s.get(2);
  318. if (c == 10) {
  319. if (s.get(1) != 13)
  320. k = 4;
  321. else if (s.get(0) != 10)
  322. k = 2;
  323. else
  324. break;
  325. } else if (c == 13) {
  326. if (s.get(1) != 10 || s.get(0) != 13)
  327. k = 1;
  328. else
  329. k = 3;
  330. } else
  331. k = 4;
  332. case 4:
  333. var c = s.get(3);
  334. if (c == 10) {
  335. if (s.get(2) != 13)
  336. continue;
  337. else if (s.get(1) != 10 || s.get(0) != 13)
  338. k = 2;
  339. else
  340. break;
  341. } else if (c == 13) {
  342. if (s.get(2) != 10 || s.get(1) != 13)
  343. k = 3;
  344. else
  345. k = 1;
  346. }
  347. }
  348. }
  349. #if neko
  350. var headers = neko.Lib.stringReference(b.getBytes()).split("\r\n");
  351. #else
  352. var headers = b.getBytes().toString().split("\r\n");
  353. #end
  354. var response = headers.shift();
  355. var rp = response.split(" ");
  356. var status = Std.parseInt(rp[1]);
  357. if (status == 0 || status == null)
  358. throw "Response status error";
  359. // remove the two lasts \r\n\r\n
  360. headers.pop();
  361. headers.pop();
  362. responseHeaders = new haxe.ds.StringMap();
  363. var size = null;
  364. var chunked = false;
  365. for (hline in headers) {
  366. var a = hline.split(": ");
  367. var hname = a.shift();
  368. var hval = if (a.length == 1) a[0] else a.join(": ");
  369. hval = StringTools.ltrim(StringTools.rtrim(hval));
  370. {
  371. var previousValue = responseHeaders.get(hname);
  372. if (previousValue != null) {
  373. if (responseHeadersSameKey == null) {
  374. responseHeadersSameKey = new haxe.ds.Map<String, Array<String>>();
  375. }
  376. var array = responseHeadersSameKey.get(hname);
  377. if (array == null) {
  378. array = new Array<String>();
  379. array.push(previousValue);
  380. responseHeadersSameKey.set(hname, array);
  381. }
  382. array.push(hval);
  383. }
  384. }
  385. responseHeaders.set(hname, hval);
  386. switch (hname.toLowerCase()) {
  387. case "content-length":
  388. size = Std.parseInt(hval);
  389. case "transfer-encoding":
  390. chunked = (hval.toLowerCase() == "chunked");
  391. }
  392. }
  393. onStatus(status);
  394. var chunk_re = ~/^([0-9A-Fa-f]+)[ ]*\r\n/m;
  395. chunk_size = null;
  396. chunk_buf = null;
  397. var bufsize = 1024;
  398. var buf = haxe.io.Bytes.alloc(bufsize);
  399. if (chunked) {
  400. try {
  401. while (true) {
  402. var len = sock.input.readBytes(buf, 0, bufsize);
  403. if (!readChunk(chunk_re, api, buf, len))
  404. break;
  405. }
  406. } catch (e:haxe.io.Eof) {
  407. throw "Transfer aborted";
  408. }
  409. } else if (size == null) {
  410. if (!noShutdown)
  411. sock.shutdown(false, true);
  412. try {
  413. while (true) {
  414. var len = sock.input.readBytes(buf, 0, bufsize);
  415. if (len == 0)
  416. break;
  417. api.writeBytes(buf, 0, len);
  418. }
  419. } catch (e:haxe.io.Eof) {}
  420. } else {
  421. api.prepare(size);
  422. try {
  423. while (size > 0) {
  424. var len = sock.input.readBytes(buf, 0, if (size > bufsize) bufsize else size);
  425. api.writeBytes(buf, 0, len);
  426. size -= len;
  427. }
  428. } catch (e:haxe.io.Eof) {
  429. throw "Transfer aborted";
  430. }
  431. }
  432. if (chunked && (chunk_size != null || chunk_buf != null))
  433. throw "Invalid chunk";
  434. if (status < 200 || status >= 400)
  435. throw "Http Error #" + status;
  436. api.close();
  437. }
  438. function readChunk(chunk_re:EReg, api:haxe.io.Output, buf:haxe.io.Bytes, len) {
  439. if (chunk_size == null) {
  440. if (chunk_buf != null) {
  441. var b = new haxe.io.BytesBuffer();
  442. b.add(chunk_buf);
  443. b.addBytes(buf, 0, len);
  444. buf = b.getBytes();
  445. len += chunk_buf.length;
  446. chunk_buf = null;
  447. }
  448. #if neko
  449. if (chunk_re.match(neko.Lib.stringReference(buf))) {
  450. #else
  451. if (chunk_re.match(buf.toString())) {
  452. #end
  453. var p = chunk_re.matchedPos();
  454. if (p.len <= len) {
  455. var cstr = chunk_re.matched(1);
  456. chunk_size = Std.parseInt("0x" + cstr);
  457. if (chunk_size == 0) {
  458. chunk_size = null;
  459. chunk_buf = null;
  460. return false;
  461. }
  462. len -= p.len;
  463. return readChunk(chunk_re, api, buf.sub(p.len, len), len);
  464. }
  465. }
  466. // prevent buffer accumulation
  467. if (len > 10) {
  468. onError("Invalid chunk");
  469. return false;
  470. }
  471. chunk_buf = buf.sub(0, len);
  472. return true;
  473. }
  474. if (chunk_size > len) {
  475. chunk_size -= len;
  476. api.writeBytes(buf, 0, len);
  477. return true;
  478. }
  479. var end = chunk_size + 2;
  480. if (len >= end) {
  481. if (chunk_size > 0)
  482. api.writeBytes(buf, 0, chunk_size);
  483. len -= end;
  484. chunk_size = null;
  485. if (len == 0)
  486. return true;
  487. return readChunk(chunk_re, api, buf.sub(end, len), len);
  488. }
  489. if (chunk_size > 0)
  490. api.writeBytes(buf, 0, chunk_size);
  491. chunk_size -= len;
  492. return true;
  493. }
  494. /**
  495. Makes a synchronous request to `url`.
  496. This creates a new Http instance and makes a GET request by calling its
  497. `request(false)` method.
  498. If `url` is null, the result is unspecified.
  499. **/
  500. public static function requestUrl(url:String):String {
  501. var h = new Http(url);
  502. var r = null;
  503. h.onData = function(d) {
  504. r = d;
  505. }
  506. h.onError = function(e) {
  507. throw e;
  508. }
  509. h.request(false);
  510. return r;
  511. }
  512. }