rage-iodine 1.8.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5b5db0d766696d96ae2730a5d954229249dbd982bceba8fb63f184a1da62beb
4
- data.tar.gz: 397f34465e987d8f63c253f61ee7e3efd098aa94af63a55eeff81c5c13f5b088
3
+ metadata.gz: 4221fdb6705dce60cb5e71da2644f639200571f690930fb9c428e29c4ae20032
4
+ data.tar.gz: c01222eb2a55634b414c456d46319e5d2072a2ed89b258e709ea1d236cf490b9
5
5
  SHA512:
6
- metadata.gz: 00a9d42202213c7c34292b5d70074d17300736004a5d14df21bf9f1f9b933d3172335e07dbb5617f38c278a1f5073333114622dacd69870f258c65181f638cd4
7
- data.tar.gz: bc5d84cf74cd4ee5078452fbd01ac67de8c0d2b7026d71af53136d47e862634fbf25d4910cb7e79280b77dab96bd91e76d92e1de41593e9ed2c11e0d0a2b27fe
6
+ metadata.gz: ad43ae3016c28c794926b71fd62b4f48c1362c7865f8c0118b8b5548ded1885b28c198a9d93f87a387873b4451b9f7310b545ba079cc26708521dc8c47497f7e
7
+ data.tar.gz: 0067566c6d7495bc2cc4652f49278d1eaa22f5afd9764cb84b654c44fd2483f7f14ccdcddd656b4ec529ad43c4d285ba8452976b075551c9683651ace0704a5a
data/ext/iodine/http.c CHANGED
@@ -19,6 +19,7 @@ Feel free to copy, use and enjoy according to the license provided.
19
19
  #include <unistd.h>
20
20
 
21
21
  #include <pthread.h>
22
+ #include <ruby/io.h>
22
23
 
23
24
  #ifndef HAVE_TM_TM_ZONE
24
25
  #if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || \
@@ -61,6 +62,13 @@ fio_tls_alpn_add(void *tls, const char *protocol_name,
61
62
  #pragma weak fio_tls_alpn_add
62
63
  #endif
63
64
 
65
+ static VALUE cTempfile;
66
+ static VALUE tempfile_args;
67
+ static VALUE cUploadedFile;
68
+ static ID create_id;
69
+ static ID path_id;
70
+ static rb_encoding *IodineBinaryEncoding;
71
+
64
72
  /* *****************************************************************************
65
73
  Small Helpers
66
74
  ***************************************************************************** */
@@ -1797,105 +1805,262 @@ typedef struct {
1797
1805
  size_t pos;
1798
1806
  size_t partial_offset;
1799
1807
  size_t partial_length;
1800
- FIOBJ partial_name;
1808
+ VALUE partial_name;
1809
+ int partial_fd;
1810
+ VALUE partial_tempfile;
1811
+ VALUE params;
1801
1812
  } http_fio_mime_s;
1802
1813
 
1803
1814
  #define http_mime_parser2fio(parser) ((http_fio_mime_s *)(parser))
1804
1815
 
1816
+ /** Parse a parameter key and add it to the `params` hash. Check `parse_nested_query_internal` for reference. */
1817
+ static void add_to_params(VALUE params, char *key, size_t key_len, VALUE value) {
1818
+ char *pos = NULL;
1819
+ if (key_len > 1) {
1820
+ pos = memchr(key + 1, '[', key_len - 1);
1821
+ }
1822
+
1823
+ if (!pos) {
1824
+ VALUE k = ID2SYM(rb_intern2(key, key_len));
1825
+ rb_hash_aset(params, k, value);
1826
+ } else {
1827
+ VALUE arr = Qnil, hsh = Qnil, hsh_key = Qnil;
1828
+ VALUE k = ID2SYM(rb_intern2(key, pos - key));
1829
+ char *k_pos, *end = key + key_len;
1830
+ uint8_t depth = 0;
1831
+
1832
+ while (pos < end) {
1833
+ if (depth++ == PARAMS_MAX_DEPTH) {
1834
+ rb_raise(rb_eRuntimeError, "Params too deep");
1835
+ }
1836
+
1837
+ if (*pos == '[' && *(pos + 1) == ']') { // array
1838
+ if (arr != Qnil) {
1839
+ VALUE tmp = rb_ary_new();
1840
+ rb_ary_push(arr, tmp);
1841
+ arr = tmp;
1842
+ } else if (hsh != Qnil) {
1843
+ arr = rb_hash_aref(hsh, hsh_key);
1844
+ if (arr == Qnil) {
1845
+ arr = rb_ary_new();
1846
+ rb_hash_aset(hsh, hsh_key, arr);
1847
+ } else {
1848
+ Check_Type(arr, T_ARRAY);
1849
+ }
1850
+ hsh = hsh_key = Qnil;
1851
+ } else {
1852
+ VALUE tmp = rb_hash_aref(params, k);
1853
+ if (tmp != Qnil) {
1854
+ Check_Type(tmp, T_ARRAY);
1855
+ arr = tmp;
1856
+ } else {
1857
+ arr = rb_ary_new();
1858
+ rb_hash_aset(params, k, arr);
1859
+ }
1860
+ }
1861
+
1862
+ pos += 2;
1863
+
1864
+ } else if (*pos == '[' && *(pos + 1) != ']') { // hash
1865
+ if (pos + 2 < end) {
1866
+ k_pos = memchr(pos + 2, ']', end - pos - 2);
1867
+ } else {
1868
+ k_pos = NULL;
1869
+ }
1870
+
1871
+ if (!k_pos) {
1872
+ rb_raise(rb_eRuntimeError, "Bad params");
1873
+ }
1874
+
1875
+ VALUE prev_hsh_key = hsh_key;
1876
+ hsh_key = ID2SYM(rb_intern2(pos + 1, k_pos - pos - 1));
1877
+
1878
+ if (hsh != Qnil) {
1879
+ VALUE existing = rb_hash_aref(hsh, prev_hsh_key);
1880
+ if (existing != Qnil) {
1881
+ Check_Type(existing, T_HASH);
1882
+ hsh = existing;
1883
+ } else {
1884
+ VALUE tmp = rb_hash_new();
1885
+ rb_hash_aset(hsh, prev_hsh_key, tmp);
1886
+ hsh = tmp;
1887
+ }
1888
+ } else if (arr != Qnil) {
1889
+ VALUE nested_val = Qnil;
1890
+ if (RARRAY_LEN(arr) != 0) {
1891
+ VALUE tmp = rb_ary_entry(arr, -1);
1892
+ Check_Type(tmp, T_HASH);
1893
+ nested_val = rb_hash_aref(tmp, hsh_key);
1894
+ }
1895
+
1896
+ if (RB_TYPE_P(nested_val, T_HASH)) {
1897
+ char *n_pos_start = k_pos + 1, *n_pos_end = k_pos + 1;
1898
+
1899
+ while (nested_val != Qnil && *n_pos_start == '[' && *(n_pos_start + 1) != ']' && *n_pos_end != '&' && *n_pos_end != '=' && n_pos_end < end) {
1900
+ n_pos_end++;
1901
+
1902
+ if (*n_pos_end == ']') {
1903
+ VALUE nested_key = ID2SYM(rb_intern2(n_pos_start + 1, n_pos_end - n_pos_start - 1));
1904
+ Check_Type(nested_val, T_HASH);
1905
+ nested_val = rb_hash_aref(nested_val, nested_key);
1906
+ n_pos_start = ++n_pos_end;
1907
+ }
1908
+ }
1909
+ }
1910
+
1911
+ if (RARRAY_LEN(arr) == 0 || nested_val != Qnil) {
1912
+ hsh = rb_hash_new();
1913
+ rb_ary_push(arr, hsh);
1914
+ } else {
1915
+ hsh = rb_ary_entry(arr, -1);
1916
+ if (hsh != Qnil) {
1917
+ Check_Type(hsh, T_HASH);
1918
+ }
1919
+ }
1920
+
1921
+ arr = Qnil;
1922
+ } else {
1923
+ VALUE tmp = rb_hash_aref(params, k);
1924
+ if (tmp != Qnil) {
1925
+ Check_Type(tmp, T_HASH);
1926
+ hsh = tmp;
1927
+ } else {
1928
+ hsh = rb_hash_new();
1929
+ rb_hash_aset(params, k, hsh);
1930
+ }
1931
+ }
1932
+
1933
+ pos = k_pos + 1;
1934
+
1935
+ } else {
1936
+ pos++;
1937
+ }
1938
+ }
1939
+
1940
+ if (arr != Qnil) {
1941
+ rb_ary_push(arr, value);
1942
+ } else {
1943
+ rb_hash_aset(hsh, hsh_key, value);
1944
+ }
1945
+ }
1946
+ }
1947
+
1948
+ static inline void cleanup_temp_file(void *r_path) {
1949
+ char *path = StringValueCStr(r_path);
1950
+ IodineStore.remove((VALUE)r_path);
1951
+ unlink(path);
1952
+ }
1953
+
1954
+ static VALUE create_temp_file(http_mime_parser_s *parser, char **path) {
1955
+ VALUE tempfile = rb_funcallv_kw(cTempfile, create_id, 1, &tempfile_args, RB_PASS_KEYWORDS);
1956
+
1957
+ VALUE r_path = rb_funcall2(tempfile, path_id, 0, NULL);
1958
+ IodineStore.add(r_path);
1959
+ *path = StringValueCStr(r_path);
1960
+
1961
+ http_s *h = http_mime_parser2fio(parser)->h;
1962
+ http_fio_protocol_s *p = (http_fio_protocol_s *)h->private_data.flag;
1963
+ fio_uuid_link(p->uuid, (void *)r_path, cleanup_temp_file); // schedule file deletion
1964
+
1965
+ return tempfile;
1966
+ }
1967
+
1968
+ static VALUE build_file_value(VALUE file, char *filename, size_t filename_len, char *mimetype, size_t mimetype_len) {
1969
+ VALUE args[3] = { file, rb_enc_str_new(filename, filename_len, IodineBinaryEncoding), rb_enc_str_new(mimetype, mimetype_len, IodineBinaryEncoding) };
1970
+ VALUE uploaded_file = rb_class_new_instance(3, args, cUploadedFile);
1971
+
1972
+ return uploaded_file;
1973
+ }
1974
+
1805
1975
  /** Called when all the data is available at once. */
1806
1976
  static void http_mime_parser_on_data(http_mime_parser_s *parser, void *name,
1807
1977
  size_t name_len, void *filename,
1808
1978
  size_t filename_len, void *mimetype,
1809
1979
  size_t mimetype_len, void *value,
1810
1980
  size_t value_len) {
1981
+ // for regular values - just add them to params
1811
1982
  if (!filename_len) {
1812
- http_add2hash(http_mime_parser2fio(parser)->h->params, name, name_len,
1813
- value, value_len, 0);
1983
+ VALUE r_value = rb_enc_str_new(value, value_len, IodineBinaryEncoding);
1984
+ add_to_params(http_mime_parser2fio(parser)->params, name, name_len, r_value);
1814
1985
  return;
1815
1986
  }
1816
- FIOBJ n = fiobj_str_new(name, name_len);
1817
- fiobj_str_write(n, "[data]", 6);
1818
- fio_str_info_s tmp = fiobj_obj2cstr(n);
1819
- http_add2hash(http_mime_parser2fio(parser)->h->params, tmp.data, tmp.len,
1820
- value, value_len, 0);
1821
- fiobj_str_resize(n, name_len);
1822
- fiobj_str_write(n, "[name]", 6);
1823
- tmp = fiobj_obj2cstr(n);
1824
- http_add2hash(http_mime_parser2fio(parser)->h->params, tmp.data, tmp.len,
1825
- filename, filename_len, 0);
1826
- if (mimetype_len) {
1827
- fiobj_str_resize(n, name_len);
1828
- fiobj_str_write(n, "[type]", 6);
1829
- tmp = fiobj_obj2cstr(n);
1830
- http_add2hash(http_mime_parser2fio(parser)->h->params, tmp.data, tmp.len,
1831
- mimetype, mimetype_len, 0);
1832
- }
1833
- fiobj_free(n);
1987
+
1988
+ // write file data into a temporary file
1989
+ char* path;
1990
+ VALUE r_tempfile = create_temp_file(parser, &path);
1991
+
1992
+ int file = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0777);
1993
+ write(file, value, value_len);
1994
+ close(file);
1995
+
1996
+ VALUE r_value = build_file_value(r_tempfile, filename, filename_len, mimetype, mimetype_len);
1997
+ add_to_params(http_mime_parser2fio(parser)->params, name, name_len, r_value);
1834
1998
  }
1835
1999
 
1836
2000
  /** Called when the data didn't fit in the buffer. Data will be streamed. */
1837
2001
  static void http_mime_parser_on_partial_start(
1838
2002
  http_mime_parser_s *parser, void *name, size_t name_len, void *filename,
1839
2003
  size_t filename_len, void *mimetype, size_t mimetype_len) {
1840
- http_mime_parser2fio(parser)->partial_length = 0;
1841
- http_mime_parser2fio(parser)->partial_offset = 0;
1842
- http_mime_parser2fio(parser)->partial_name = fiobj_str_new(name, name_len);
2004
+
2005
+ // store the parameter name
2006
+ http_mime_parser2fio(parser)->partial_name = rb_str_new(name, name_len);
1843
2007
 
1844
2008
  if (!filename)
1845
2009
  return;
1846
2010
 
1847
- fiobj_str_write(http_mime_parser2fio(parser)->partial_name, "[type]", 6);
1848
- fio_str_info_s tmp =
1849
- fiobj_obj2cstr(http_mime_parser2fio(parser)->partial_name);
1850
- http_add2hash(http_mime_parser2fio(parser)->h->params, tmp.data, tmp.len,
1851
- mimetype, mimetype_len, 0);
2011
+ // create and store the temporary file for the file data
2012
+ char* path;
2013
+ VALUE r_tempfile = create_temp_file(parser, &path);
1852
2014
 
1853
- fiobj_str_resize(http_mime_parser2fio(parser)->partial_name, name_len);
1854
- fiobj_str_write(http_mime_parser2fio(parser)->partial_name, "[name]", 6);
1855
- tmp = fiobj_obj2cstr(http_mime_parser2fio(parser)->partial_name);
1856
- http_add2hash(http_mime_parser2fio(parser)->h->params, tmp.data, tmp.len,
1857
- filename, filename_len, 0);
1858
-
1859
- fiobj_str_resize(http_mime_parser2fio(parser)->partial_name, name_len);
1860
- fiobj_str_write(http_mime_parser2fio(parser)->partial_name, "[data]", 6);
2015
+ http_mime_parser2fio(parser)->partial_fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0777);
2016
+
2017
+ VALUE r_value = build_file_value(r_tempfile, filename, filename_len, mimetype, mimetype_len);
2018
+ http_mime_parser2fio(parser)->partial_tempfile = r_value;
1861
2019
  }
1862
2020
 
1863
2021
  /** Called when partial data is available. */
1864
2022
  static void http_mime_parser_on_partial_data(http_mime_parser_s *parser,
1865
2023
  void *value, size_t value_len) {
1866
- if (!http_mime_parser2fio(parser)->partial_offset)
1867
- http_mime_parser2fio(parser)->partial_offset =
1868
- http_mime_parser2fio(parser)->pos +
1869
- ((uintptr_t)value -
1870
- (uintptr_t)http_mime_parser2fio(parser)->buffer.data);
1871
- http_mime_parser2fio(parser)->partial_length += value_len;
1872
- (void)value;
2024
+
2025
+ http_fio_mime_s *fio_parser = http_mime_parser2fio(parser);
2026
+
2027
+ // if this is a file - write into it rightaway
2028
+ if (fio_parser->partial_fd) {
2029
+ write(fio_parser->partial_fd, value, value_len);
2030
+ return;
2031
+ }
2032
+
2033
+ // if that's just a large piece of data - store the offset and length values
2034
+ if (!fio_parser->partial_offset) {
2035
+ fio_parser->partial_offset = fio_parser->pos + ((uintptr_t)value - (uintptr_t)fio_parser->buffer.data);
2036
+ }
2037
+ fio_parser->partial_length += value_len;
1873
2038
  }
1874
2039
 
1875
2040
  /** Called when the partial data is complete. */
1876
2041
  static void http_mime_parser_on_partial_end(http_mime_parser_s *parser) {
2042
+ http_fio_mime_s *fio_parser = http_mime_parser2fio(parser);
2043
+
2044
+ // for a file - add to params and close the fd
2045
+ if (fio_parser->partial_fd) {
2046
+ add_to_params(fio_parser->params, RSTRING_PTR(fio_parser->partial_name), RSTRING_LEN(fio_parser->partial_name), fio_parser->partial_tempfile);
2047
+ close(fio_parser->partial_fd);
2048
+
2049
+ fio_parser->partial_name = Qnil;
2050
+ fio_parser->partial_tempfile = Qnil;
2051
+ fio_parser->partial_fd = 0;
1877
2052
 
1878
- fio_str_info_s tmp =
1879
- fiobj_obj2cstr(http_mime_parser2fio(parser)->partial_name);
1880
- FIOBJ o = FIOBJ_INVALID;
1881
- if (!http_mime_parser2fio(parser)->partial_length)
1882
2053
  return;
1883
- if (http_mime_parser2fio(parser)->partial_length < 42) {
1884
- /* short data gets a new object */
1885
- o = fiobj_str_new(http_mime_parser2fio(parser)->buffer.data +
1886
- http_mime_parser2fio(parser)->partial_offset,
1887
- http_mime_parser2fio(parser)->partial_length);
1888
- } else {
1889
- /* longer data gets a reference object (memory collision concerns) */
1890
- o = fiobj_data_slice(http_mime_parser2fio(parser)->h->body,
1891
- http_mime_parser2fio(parser)->partial_offset,
1892
- http_mime_parser2fio(parser)->partial_length);
1893
2054
  }
1894
- http_add2hash2(http_mime_parser2fio(parser)->h->params, tmp.data, tmp.len, o,
1895
- 0);
1896
- fiobj_free(http_mime_parser2fio(parser)->partial_name);
1897
- http_mime_parser2fio(parser)->partial_name = FIOBJ_INVALID;
1898
- http_mime_parser2fio(parser)->partial_offset = 0;
2055
+
2056
+ FIOBJ slice = fiobj_data_slice(fio_parser->h->body, fio_parser->partial_offset, fio_parser->partial_length);
2057
+ fio_str_info_s s_slice = fiobj_obj2cstr(slice);
2058
+ VALUE r_value = rb_enc_str_new(s_slice.data, s_slice.len, IodineBinaryEncoding);
2059
+ add_to_params(fio_parser->params, RSTRING_PTR(fio_parser->partial_name), RSTRING_LEN(fio_parser->partial_name), r_value);
2060
+
2061
+ fio_parser->partial_name = Qnil;
2062
+ fio_parser->partial_length = 0;
2063
+ fio_parser->partial_offset = 0;
1899
2064
  }
1900
2065
 
1901
2066
  /**
@@ -1911,65 +2076,29 @@ static inline size_t http_mime_decode_url(char *dest, const char *encoded,
1911
2076
  }
1912
2077
 
1913
2078
  /**
1914
- * Attempts to decode the request's body.
1915
- *
1916
- * Supported Types include:
1917
- * * application/x-www-form-urlencoded
1918
- * * application/json
1919
- * * multipart/form-data
2079
+ * Attempts to decode a multipart/form-data encoded body.
1920
2080
  */
1921
- int http_parse_body(http_s *h) {
1922
- static uint64_t content_type_hash;
1923
- if (!h->body)
1924
- return -1;
1925
- if (!content_type_hash)
1926
- content_type_hash = fiobj_hash_string("content-type", 12);
1927
- FIOBJ ct = fiobj_hash_get2(h->headers, content_type_hash);
1928
- fio_str_info_s content_type = fiobj_obj2cstr(ct);
1929
- if (content_type.len < 16)
1930
- return -1;
1931
- if (content_type.len >= 33 &&
1932
- !strncasecmp("application/x-www-form-urlencoded", content_type.data,
1933
- 33)) {
1934
- if (!h->params)
1935
- h->params = fiobj_hash_new();
1936
- FIOBJ tmp = h->query;
1937
- h->query = h->body;
1938
- http_parse_query(h);
1939
- h->query = tmp;
1940
- return 0;
1941
- }
1942
- if (content_type.len >= 16 &&
1943
- !strncasecmp("application/json", content_type.data, 16)) {
1944
- content_type = fiobj_obj2cstr(h->body);
1945
- if (h->params)
1946
- return -1;
1947
- if (fiobj_json2obj(&h->params, content_type.data, content_type.len) == 0)
1948
- return -1;
1949
- if (FIOBJ_TYPE_IS(h->params, FIOBJ_T_HASH))
1950
- return 0;
1951
- FIOBJ tmp = h->params;
1952
- FIOBJ key = fiobj_str_new("JSON", 4);
1953
- h->params = fiobj_hash_new2(4);
1954
- fiobj_hash_set(h->params, key, tmp);
1955
- fiobj_free(key);
1956
- return 0;
2081
+ VALUE http_parse_multipart(http_s *h, char *content_type, size_t content_type_len) {
2082
+ static uint8_t http_initialized;
2083
+ if (!http_initialized) {
2084
+ http_initialized = 1;
2085
+ http_init();
1957
2086
  }
1958
2087
 
1959
- http_fio_mime_s p = {.h = h};
1960
- if (http_mime_parser_init(&p.p, content_type.data, content_type.len))
1961
- return -1;
1962
- if (!h->params)
1963
- h->params = fiobj_hash_new();
2088
+ VALUE params = rb_hash_new();
2089
+ http_fio_mime_s p = {.h = h, .params = params};
2090
+ if (http_mime_parser_init(&p.p, content_type, content_type_len))
2091
+ rb_raise(rb_eRuntimeError, "Malformed multipart request");
1964
2092
 
1965
2093
  do {
1966
2094
  size_t cons = http_mime_parse(&p.p, p.buffer.data, p.buffer.len);
1967
2095
  p.pos += cons;
1968
- p.buffer = fiobj_data_pread(h->body, p.pos, 4096);
2096
+ p.buffer = fiobj_data_pread(h->body, p.pos, 262144);
1969
2097
  } while (p.buffer.data && !p.p.done && !p.p.error);
1970
- fiobj_free(p.partial_name);
1971
- p.partial_name = FIOBJ_INVALID;
1972
- return 0;
2098
+
2099
+ p.params = Qnil;
2100
+
2101
+ return params;
1973
2102
  }
1974
2103
 
1975
2104
  /* *****************************************************************************
@@ -2489,6 +2618,24 @@ ssize_t http_decode_path_unsafe(char *dest, const char *url_data) {
2489
2618
  return pos - dest;
2490
2619
  }
2491
2620
 
2621
+ // init the http module to enable multipart/form-data parsing;
2622
+ // should be called lazily as it references `Rage`
2623
+ void http_init(void) {
2624
+ cTempfile = rb_const_get(rb_cObject, rb_intern("Tempfile"));
2625
+
2626
+ VALUE cRage = rb_const_get(rb_cObject, rb_intern("Rage"));
2627
+ cUploadedFile = rb_const_get(cRage, rb_intern("UploadedFile"));
2628
+
2629
+ tempfile_args = rb_hash_new();
2630
+ rb_hash_aset(tempfile_args, ID2SYM(rb_intern("binmode")), Qtrue);
2631
+ rb_global_variable(&tempfile_args);
2632
+
2633
+ create_id = rb_intern("create");
2634
+ path_id = rb_intern("path");
2635
+
2636
+ IodineBinaryEncoding = rb_enc_find("binary");
2637
+ }
2638
+
2492
2639
  /* *****************************************************************************
2493
2640
  Lookup Tables / functions
2494
2641
  ***************************************************************************** */
data/ext/iodine/http.h CHANGED
@@ -7,6 +7,8 @@ Feel free to copy, use and enjoy according to the license provided.
7
7
  */
8
8
  #define H_HTTP_H
9
9
 
10
+ #include "ruby.h"
11
+
10
12
  #include <fio.h>
11
13
 
12
14
  #include <fiobj.h>
@@ -58,6 +60,10 @@ Compile Time Settings
58
60
  #define FIO_HTTP_EXACT_LOGGING 0
59
61
  #endif
60
62
 
63
+ #ifndef PARAMS_MAX_DEPTH
64
+ #define PARAMS_MAX_DEPTH 5
65
+ #endif
66
+
61
67
  /** the `http_listen settings, see details in the struct definition. */
62
68
  typedef struct http_settings_s http_settings_s;
63
69
 
@@ -772,25 +778,9 @@ HTTP GET and POST parsing helpers
772
778
  ***************************************************************************** */
773
779
 
774
780
  /**
775
- * Attempts to decode the request's body.
776
- *
777
- * Supported Types include:
778
- * * application/x-www-form-urlencoded
779
- * * application/json
780
- * * multipart/form-data
781
- *
782
- * This should be called before `http_parse_query`, in order to support JSON
783
- * data.
784
- *
785
- * If the JSON data isn't an object, it will be saved under the key "JSON" in
786
- * the `params` hash.
787
- *
788
- * If the `multipart/form-data` type contains JSON files, they will NOT be
789
- * parsed (they will behave like any other file, with `data`, `type` and
790
- * `filename` keys assigned). This allows non-object JSON data (such as array)
791
- * to be handled by the app.
781
+ * Attempts to decode a multipart/form-data encoded body.
792
782
  */
793
- int http_parse_body(http_s *h);
783
+ VALUE http_parse_multipart(http_s *h, char *content_type, size_t content_type_len);
794
784
 
795
785
  /**
796
786
  * Parses the query part of an HTTP request/response. Uses `http_add2hash`.
@@ -1007,6 +997,10 @@ typedef fio_url_s http_url_s
1007
997
  */
1008
998
  #define http_url_parse(url, len) fio_url_parse((url), (len))
1009
999
 
1000
+ // init the http module to enable multipart/form-data parsing;
1001
+ // should be called lazily as it references `Rage`
1002
+ void http_init(void);
1003
+
1010
1004
  #if DEBUG
1011
1005
  void http_tests(void);
1012
1006
  #endif
@@ -209,6 +209,253 @@ static VALUE iodine_rfc2109(VALUE self, VALUE rtm) {
209
209
  (void)self;
210
210
  }
211
211
 
212
+ /**
213
+ Convert query string into a Ruby object in (almost always) one pass with no recursion.
214
+
215
+
216
+ Iodine::Rack::Utils.parse_nested_query("a=1&b[]=2&c[d]=3") # => { "a" => "1", "b" => ["2"], "c" => { "d" => "3" } }
217
+
218
+
219
+
220
+ query = "a=1&b[]=2&c[d]=3"
221
+ Benchmark.ips do |x|
222
+ x.report("Iodine") {
223
+ Iodine::Rack::Utils.parse_nested_query(query)
224
+ }
225
+
226
+ x.report("Rack") {
227
+ Rack::Utils.parse_nested_query(query)
228
+ }
229
+
230
+ x.compare!
231
+ end
232
+
233
+
234
+ Calculating -------------------------------------
235
+ Iodine 1.088M (~ 1.1%) i/s - 5.526M in 5.077759s
236
+ Rack 64.258k (~ 1.0%) i/s - 321.657k in 5.006187s
237
+
238
+ Comparison:
239
+ Iodine: 1088431.2 i/s
240
+ Rack: 64257.9 i/s - 16.94x slower
241
+
242
+ */
243
+ static VALUE parse_nested_query_internal(char *str, size_t len) {
244
+ uint8_t should_decode = 0;
245
+ VALUE params = rb_hash_new();
246
+
247
+ char *pos = str, *k_pos = str, *v_pos = str;
248
+ const char *end = str + len;
249
+
250
+ while (pos < end) {
251
+ k_pos++;
252
+
253
+ if (*k_pos == '=') { // plain param
254
+ v_pos = k_pos + 1;
255
+ while (*v_pos != '&' && v_pos < end) {
256
+ if (*v_pos == '%' || *v_pos == '+') {
257
+ should_decode = 1;
258
+ }
259
+ v_pos++;
260
+ }
261
+
262
+ VALUE k = ID2SYM(rb_intern2(pos, k_pos - pos)), v = rb_str_new(k_pos + 1, v_pos - k_pos - 1);
263
+ if (should_decode) {
264
+ rb_hash_aset(params, k, url_decode_inplace(Qnil, v));
265
+ should_decode = 0;
266
+ } else {
267
+ rb_hash_aset(params, k, v);
268
+ }
269
+ pos = k_pos = v_pos + 1;
270
+
271
+ } else if (*k_pos == '[') { // things are about to get rough now
272
+ VALUE arr = Qnil, hsh = Qnil, hsh_key = Qnil;
273
+ VALUE k = ID2SYM(rb_intern2(pos, k_pos - pos)), v;
274
+ pos = k_pos;
275
+ uint8_t depth = 0;
276
+
277
+ while (*pos != '=' && pos < end) {
278
+ if (depth++ == PARAMS_MAX_DEPTH) {
279
+ rb_raise(rb_eRuntimeError, "Params too deep");
280
+ }
281
+
282
+ if (*pos == '[' && *(pos + 1) == ']') { // array
283
+ if (arr != Qnil) {
284
+ VALUE tmp = rb_ary_new();
285
+ rb_ary_push(arr, tmp);
286
+ arr = tmp;
287
+ } else if (hsh != Qnil) {
288
+ arr = rb_hash_aref(hsh, hsh_key);
289
+ if (arr == Qnil) {
290
+ arr = rb_ary_new();
291
+ rb_hash_aset(hsh, hsh_key, arr);
292
+ } else {
293
+ Check_Type(arr, T_ARRAY);
294
+ }
295
+ hsh = hsh_key = Qnil;
296
+ } else {
297
+ VALUE tmp = rb_hash_aref(params, k);
298
+ if (tmp != Qnil) {
299
+ Check_Type(tmp, T_ARRAY);
300
+ arr = tmp;
301
+ } else {
302
+ arr = rb_ary_new();
303
+ rb_hash_aset(params, k, arr);
304
+ }
305
+ }
306
+
307
+ pos += 2;
308
+
309
+ } else if (*pos == '[' && *(pos + 1) != ']') { // hash
310
+ if (pos + 2 < end) {
311
+ k_pos = memchr(pos + 2, ']', end - pos - 2);
312
+ } else {
313
+ k_pos = NULL;
314
+ }
315
+
316
+ if (!k_pos) {
317
+ rb_raise(rb_eRuntimeError, "Bad params");
318
+ }
319
+
320
+ VALUE prev_hsh_key = hsh_key;
321
+ hsh_key = ID2SYM(rb_intern2(pos + 1, k_pos - pos - 1));
322
+
323
+ if (hsh != Qnil) {
324
+ VALUE existing = rb_hash_aref(hsh, prev_hsh_key);
325
+ if (existing != Qnil) {
326
+ Check_Type(existing, T_HASH);
327
+ hsh = existing;
328
+ } else {
329
+ VALUE tmp = rb_hash_new();
330
+ rb_hash_aset(hsh, prev_hsh_key, tmp);
331
+ hsh = tmp;
332
+ }
333
+ } else if (arr != Qnil) {
334
+ VALUE nested_val = Qnil;
335
+ if (RARRAY_LEN(arr) != 0) {
336
+ VALUE tmp = rb_ary_entry(arr, -1);
337
+ Check_Type(tmp, T_HASH);
338
+ nested_val = rb_hash_aref(tmp, hsh_key);
339
+ }
340
+
341
+ // handle Rails' smart key grouping;
342
+ // "users[][data][id]=11&users[][data][name]=ross&users[][data][id]=22&users[][data][name]=chandler"
343
+ // should result in:
344
+ // { users: [{ data: { id: "11", name: "ross" } }, { data: { id: "22", name: "chandler" } }] }
345
+ if (RB_TYPE_P(nested_val, T_HASH)) {
346
+ char *n_pos_start = k_pos + 1, *n_pos_end = k_pos + 1;
347
+
348
+ while (nested_val != Qnil && *n_pos_start == '[' && *(n_pos_start + 1) != ']' && *n_pos_end != '&' && *n_pos_end != '=' && n_pos_end < end) {
349
+ n_pos_end++;
350
+
351
+ if (*n_pos_end == ']') {
352
+ VALUE nested_key = ID2SYM(rb_intern2(n_pos_start + 1, n_pos_end - n_pos_start - 1));
353
+ Check_Type(nested_val, T_HASH);
354
+ nested_val = rb_hash_aref(nested_val, nested_key);
355
+ n_pos_start = ++n_pos_end;
356
+ }
357
+ }
358
+ }
359
+
360
+ // use a new hash if there's none under this key or the existing one already has such key
361
+ if (RARRAY_LEN(arr) == 0 || nested_val != Qnil) {
362
+ hsh = rb_hash_new();
363
+ rb_ary_push(arr, hsh);
364
+ } else {
365
+ hsh = rb_ary_entry(arr, -1);
366
+ if (hsh != Qnil) {
367
+ Check_Type(hsh, T_HASH);
368
+ }
369
+ }
370
+
371
+ arr = Qnil;
372
+ } else {
373
+ VALUE tmp = rb_hash_aref(params, k);
374
+ if (tmp != Qnil) {
375
+ Check_Type(tmp, T_HASH);
376
+ hsh = tmp;
377
+ } else {
378
+ hsh = rb_hash_new();
379
+ rb_hash_aset(params, k, hsh);
380
+ }
381
+ }
382
+
383
+ pos = k_pos + 1;
384
+
385
+ } else {
386
+ pos++;
387
+ }
388
+ }
389
+
390
+ //
391
+ // write the final object into `params`
392
+ //
393
+
394
+ for (v_pos = pos + 1; *v_pos != '&' && v_pos < end; v_pos++) {
395
+ if (*v_pos == '%' || *v_pos == '+') {
396
+ should_decode = 1;
397
+ }
398
+ }
399
+
400
+ v = rb_str_new(pos + 1, v_pos - pos - 1);
401
+ if (should_decode) {
402
+ url_decode_inplace(Qnil, v);
403
+ should_decode = 0;
404
+ }
405
+
406
+ if (arr != Qnil) {
407
+ rb_ary_push(arr, v);
408
+ } else {
409
+ rb_hash_aset(hsh, hsh_key, v);
410
+ }
411
+
412
+ pos = k_pos = v_pos + 1;
413
+
414
+ } else if (*k_pos == '&' || k_pos >= end) {
415
+ VALUE k = ID2SYM(rb_intern2(pos, k_pos - pos));
416
+ rb_hash_aset(params, k, Qnil);
417
+ pos = k_pos + 1;
418
+ }
419
+ }
420
+
421
+ return params;
422
+ }
423
+
424
+ /**
425
+ Convert query string into a Ruby object.
426
+ */
427
+ static VALUE parse_nested_query(VALUE self, VALUE r_str) {
428
+ return parse_nested_query_internal(RSTRING_PTR(r_str), RSTRING_LEN(r_str));
429
+ (void)self;
430
+ }
431
+
432
+ /**
433
+ Convert urlencoded body into a Ruby object.
434
+ */
435
+ static VALUE parse_urlencoded_nested_query(VALUE self, VALUE r_str) {
436
+ ssize_t len = http_decode_url(RSTRING_PTR(r_str), RSTRING_PTR(r_str), RSTRING_LEN(r_str));
437
+ if (len < 0) {
438
+ rb_raise(rb_eRuntimeError, "Invalid encoding");
439
+ }
440
+
441
+ return parse_nested_query_internal(RSTRING_PTR(r_str), len);
442
+ (void)self;
443
+ }
444
+
445
+ /**
446
+ Convert multipart/form-data into a Ruby object.
447
+ */
448
+ static VALUE parse_multipart(VALUE self, VALUE rack_io, VALUE content_type) {
449
+ http_s *h = IodineRackIO.get_handle(rack_io);
450
+
451
+ if (content_type == Qnil) {
452
+ rb_raise(rb_eRuntimeError, "Incorrect content type for multipart request");
453
+ }
454
+
455
+ return http_parse_multipart(h, RSTRING_PTR(content_type), RSTRING_LEN(content_type));
456
+ (void)self;
457
+ }
458
+
212
459
  /* *****************************************************************************
213
460
  Ruby Initialization
214
461
  ***************************************************************************** */
@@ -261,6 +508,9 @@ Results:
261
508
  rb_define_module_function(tmp, "time2str", date_str, -1);
262
509
  rb_define_module_function(tmp, "rfc2109", iodine_rfc2109, 1);
263
510
  rb_define_module_function(tmp, "rfc2822", iodine_rfc2822, 1);
511
+ rb_define_module_function(tmp, "parse_nested_query", parse_nested_query, 1);
512
+ rb_define_module_function(tmp, "parse_urlencoded_nested_query", parse_urlencoded_nested_query, 1);
513
+ rb_define_module_function(tmp, "parse_multipart", parse_multipart, 2);
264
514
 
265
515
  /*
266
516
  The monkey-patched methods are in this module, allowing Iodine::Rack::Utils to
@@ -106,6 +106,7 @@ rack_declare(XSENDFILE_TYPE); // for X-Sendfile support
106
106
  rack_declare(XSENDFILE_TYPE_HEADER); // for X-Sendfile support
107
107
  rack_declare(CONTENT_LENGTH_HEADER); // for X-Sendfile support
108
108
  rack_declare(IODINE_REQUEST_ID);
109
+ rack_declare(IODINE_HAS_BODY);
109
110
 
110
111
  /* used internally to handle requests */
111
112
  typedef struct {
@@ -341,6 +342,9 @@ static inline VALUE copy2env(iodine_http_request_handle_s *handle) {
341
342
  tmp = fiobj_obj2cstr(h->path);
342
343
  rb_hash_aset(env, PATH_INFO,
343
344
  rb_enc_str_new(tmp.data, tmp.len, IodineBinaryEncoding));
345
+
346
+ rb_hash_aset(env, IODINE_HAS_BODY, (h->body) ? Qtrue : Qfalse);
347
+
344
348
  if (h->query) {
345
349
  tmp = fiobj_obj2cstr(h->query);
346
350
  rb_hash_aset(env, QUERY_STRING,
@@ -976,6 +980,7 @@ static void initialize_env_template(void) {
976
980
  rb_hash_aset(env_template_no_upgrade, SERVER_NAME, QUERY_STRING);
977
981
  rb_hash_aset(env_template_no_upgrade, SERVER_PROTOCOL, QUERY_STRING);
978
982
  rb_hash_aset(env_template_no_upgrade, IODINE_REQUEST_ID, QUERY_STRING);
983
+ rb_hash_aset(env_template_no_upgrade, IODINE_HAS_BODY, QUERY_STRING);
979
984
 
980
985
  /* WebSocket upgrade support */
981
986
  env_template_websockets = rb_hash_dup(env_template_no_upgrade);
@@ -1235,6 +1240,7 @@ void iodine_init_http(void) {
1235
1240
  rack_autoset(HTTP_HOST);
1236
1241
 
1237
1242
  rack_autoset(IODINE_REQUEST_ID);
1243
+ rack_autoset(IODINE_HAS_BODY);
1238
1244
 
1239
1245
  rack_set(HTTP_SCHEME, "http");
1240
1246
  rack_set(HTTPS_SCHEME, "https");
@@ -270,4 +270,5 @@ struct IodineRackIO IodineRackIO = {
270
270
  .create = new_rack_io,
271
271
  .close = close_rack_io,
272
272
  .init = init_rack_io,
273
+ .get_handle = get_handle,
273
274
  };
@@ -15,6 +15,8 @@ extern struct IodineRackIO {
15
15
  VALUE (*create)(http_s *h, VALUE env);
16
16
  void (*close)(VALUE rack_io);
17
17
  void (*init)(void);
18
+ http_s * (*get_handle)(VALUE obj);
19
+
18
20
  } IodineRackIO;
19
21
 
20
22
  #endif /* RUBY_RACK_IO_H */
data/iodine.gemspec CHANGED
@@ -44,7 +44,4 @@ Gem::Specification.new do |spec|
44
44
  spec.add_development_dependency 'rspec', '>=3.9.0', '< 4.0'
45
45
  spec.add_development_dependency 'spec', '>=5.3.0', '< 6.0'
46
46
  spec.add_development_dependency 'rake-compiler', '>= 1', '< 2.0'
47
-
48
- spec.post_install_message = "Thank you for installing Iodine #{Iodine::VERSION}.\n" +
49
- "Remember: if iodine supports your business, it's only fair to give value back (code contributions / donations)."
50
47
  end
@@ -1,3 +1,3 @@
1
1
  module Iodine
2
- VERSION = '1.8.0'.freeze
2
+ VERSION = '2.0.0'.freeze
3
3
  end
data/lib/iodine.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'stringio' # Used internally as a default RackIO
2
2
  require 'socket' # TCPSocket is used internally for Hijack support
3
+ require 'tempfile' # Used to generate temporary files when parsing multipart/form-data
3
4
  # require 'openssl' # For SSL/TLS support using OpenSSL
4
5
 
5
6
  require_relative './iodine/version'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rage-iodine
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Boaz Segev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-25 00:00:00.000000000 Z
11
+ date: 2023-10-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -252,9 +252,7 @@ licenses:
252
252
  - MIT
253
253
  metadata:
254
254
  allowed_push_host: https://rubygems.org
255
- post_install_message: |-
256
- Thank you for installing Iodine 1.8.0.
257
- Remember: if iodine supports your business, it's only fair to give value back (code contributions / donations).
255
+ post_install_message:
258
256
  rdoc_options: []
259
257
  require_paths:
260
258
  - lib