iodine 0.2.13 → 0.2.14
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of iodine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/bin/ws-shootout +2 -0
- data/exe/iodine +2 -2
- data/ext/iodine/http_response.h +1 -1
- data/ext/iodine/iodine_websocket.c +16 -5
- data/ext/iodine/libreact.h +1 -1
- data/ext/iodine/libserver.c +10 -7
- data/ext/iodine/libsock.c +3 -2
- data/ext/iodine/spnlock.h +4 -2
- data/ext/iodine/websockets.c +63 -30
- data/ext/iodine/websockets.h +33 -13
- data/lib/iodine/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ccbe7057ea01a3ced6291824c722160e8566200c
|
4
|
+
data.tar.gz: ef231d216e71b62b57331d34161858c4b3fb0284
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 00c4e4dfd7633df4fdf2f9ade456326e27671f8cb1f9dcdd0693ec6c2f1eb65a896f502c5e40065b155ee876b8f9e7be5446dccffa2b3d2cf5cf19ef52a888a4
|
7
|
+
data.tar.gz: 9153c0444bdb26d6f5d3864fb7986d9851e1793e61c53387fe8e2826e7de87d48aee369f37100ebf3b9fb92fede369e3f1e7293988c4606a358e7041d4822eb1
|
data/CHANGELOG.md
CHANGED
@@ -8,6 +8,16 @@ Please notice that this change log contains changes for upcoming releases as wel
|
|
8
8
|
|
9
9
|
***
|
10
10
|
|
11
|
+
Change log v.0.2.14
|
12
|
+
|
13
|
+
**Fix**: fixed the experimental `each_write`. An issue was found where passing a block might crash Iodine, since the block will be freed by the GC before Iodine was done with it. Now the block is correctly added to the object Registry, preventing premature memory deallocation.
|
14
|
+
|
15
|
+
**Fix**: fixed another issue with `each_write` where a race condition review was performed outside the protected critical section, in some cases this would caused memory to be freed twice and crash the server. This issue is now resolved.
|
16
|
+
|
17
|
+
**Deprecation**: In version 0.2.1 we have notified that the the websocket method `uuid` was deprecated in favor of `conn_id`, as suggested by the [Rack Websocket Draft](https://github.com/rack/rack/pull/1107). This deprecation is now enforced.
|
18
|
+
|
19
|
+
***
|
20
|
+
|
11
21
|
Change log v.0.2.13
|
12
22
|
|
13
23
|
**Fix**: Fixed an issue presented in the C layer, where big fragmented websocket messages sent by the client could cause parsing errors and potentially, in some cases, cause a server thread to spin in a loop (DoS). Credit to @Filly for exposing the issue in the [`facil.io`](https://github.com/boazsegev/facil.io) layer. It should be noted that Chrome is the only browser where this issue could be invoked for testing.
|
data/bin/ws-shootout
CHANGED
@@ -98,3 +98,5 @@ Iodine.start
|
|
98
98
|
# ws.onclose = function(e) {console.log("closed")};
|
99
99
|
# ws.onopen = function(e) {e.target.send("hi");};
|
100
100
|
# };
|
101
|
+
|
102
|
+
# sleep 10 while `websocket-bench broadcast ws://127.0.0.1:3000/ --concurrent 10 --sample-size 100 --step-size 1000 --limit-percentile 95 --limit-rtt 250ms --initial-clients 1000`.tap {|s| puts s; puts "zzz..."}
|
data/exe/iodine
CHANGED
@@ -16,8 +16,8 @@ Both <options> and <filename> are optional.
|
|
16
16
|
|
17
17
|
Available options:
|
18
18
|
-p Port number. Default: 3000.
|
19
|
-
-t Number of threads. Default:
|
20
|
-
-w Number of worker processes.
|
19
|
+
-t Number of threads. Default: half of the CPU core count.
|
20
|
+
-w Number of worker processes. half of the CPU core count.
|
21
21
|
-www Public folder for static file serving. Default: nil (none).
|
22
22
|
-v Log responses.
|
23
23
|
-q Never log responses.
|
data/ext/iodine/http_response.h
CHANGED
@@ -109,7 +109,7 @@ If this isn't set manually, the first call to
|
|
109
109
|
being written (which might be less then the total data sent, if the sending is
|
110
110
|
fragmented).
|
111
111
|
|
112
|
-
Set the value to -1 to force the HttpResponse not to write
|
112
|
+
Set the value to -1 to force the HttpResponse not to write the
|
113
113
|
`Content-Length` header.
|
114
114
|
*/
|
115
115
|
ssize_t content_length;
|
@@ -252,6 +252,12 @@ static uint8_t iodine_ws_if_callback(ws_s *ws, void *block) {
|
|
252
252
|
return ret && ret != Qnil && ret != Qfalse;
|
253
253
|
}
|
254
254
|
|
255
|
+
static void iodine_ws_write_each_complete(ws_s *ws, void *block) {
|
256
|
+
(void)ws;
|
257
|
+
if ((VALUE)block != Qnil)
|
258
|
+
Registry.remove((VALUE)block);
|
259
|
+
}
|
260
|
+
|
255
261
|
/**
|
256
262
|
* Writes data to all the Websocket connections sharing the same process
|
257
263
|
* (worker) except `self`.
|
@@ -276,10 +282,15 @@ static VALUE iodine_ws_multiwrite(VALUE self, VALUE data) {
|
|
276
282
|
VALUE block = Qnil;
|
277
283
|
if (rb_block_given_p())
|
278
284
|
block = rb_block_proc();
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
(
|
285
|
+
if (block != Qnil)
|
286
|
+
Registry.add(block);
|
287
|
+
websocket_write_each(.origin = ws, .data = RSTRING_PTR(data),
|
288
|
+
.length = RSTRING_LEN(data),
|
289
|
+
.is_text = (rb_enc_get(data) == UTF8Encoding),
|
290
|
+
.on_finished = iodine_ws_write_each_complete,
|
291
|
+
.filter =
|
292
|
+
((block == Qnil) ? NULL : iodine_ws_if_callback),
|
293
|
+
.arg = (void *)block);
|
283
294
|
return Qtrue;
|
284
295
|
}
|
285
296
|
|
@@ -519,7 +530,7 @@ void Init_iodine_websocket(void) {
|
|
519
530
|
rb_define_method(rWebsocket, "each_write", iodine_ws_multiwrite, 1);
|
520
531
|
rb_define_method(rWebsocket, "close", iodine_ws_close, 0);
|
521
532
|
|
522
|
-
rb_define_method(rWebsocket, "uuid", iodine_ws_uuid, 0);
|
533
|
+
// rb_define_method(rWebsocket, "uuid", iodine_ws_uuid, 0);
|
523
534
|
rb_define_method(rWebsocket, "conn_id", iodine_ws_uuid, 0);
|
524
535
|
rb_define_method(rWebsocket, "has_pending?", iodine_ws_has_pending, 0);
|
525
536
|
rb_define_method(rWebsocket, "defer", iodine_defer, -1);
|
data/ext/iodine/libreact.h
CHANGED
data/ext/iodine/libserver.c
CHANGED
@@ -647,8 +647,9 @@ Sets a connection's timeout.
|
|
647
647
|
Returns -1 on error (i.e. connection closed), otherwise returns 0.
|
648
648
|
*/
|
649
649
|
void server_set_timeout(intptr_t fd, uint8_t timeout) {
|
650
|
-
if (valid_uuid(fd) == 0)
|
650
|
+
if (valid_uuid(fd) == 0) {
|
651
651
|
return;
|
652
|
+
}
|
652
653
|
lock_uuid(fd);
|
653
654
|
uuid_data(fd).active = server_data.last_tick;
|
654
655
|
uuid_data(fd).timeout = timeout;
|
@@ -758,11 +759,11 @@ static inline srv_task_s *task_alloc(void) {
|
|
758
759
|
}
|
759
760
|
|
760
761
|
/* allows for later implementation of a task pool with minimal code updates. */
|
761
|
-
static inline void task_free(srv_task_s *task) {
|
762
|
+
static inline void task_free(srv_task_s *task) { free(task); }
|
762
763
|
|
763
764
|
/* performs a single connection task. */
|
764
765
|
static void perform_single_task(void *task) {
|
765
|
-
if (sock_isvalid(p2task(task).target) == 0) {
|
766
|
+
if (p2task(task).target < 0 || sock_isvalid(p2task(task).target) == 0) {
|
766
767
|
if (p2task(task).on_finish) // an invalid connection fallback
|
767
768
|
task2fallback(task)(p2task(task).origin, p2task(task).arg);
|
768
769
|
task_free(task);
|
@@ -771,7 +772,7 @@ static void perform_single_task(void *task) {
|
|
771
772
|
if (try_lock_uuid(p2task(task).target) == 0) {
|
772
773
|
// get protocol
|
773
774
|
protocol_s *protocol = protocol_uuid(p2task(task).target);
|
774
|
-
if (protocol_set_busy(protocol) == 0) {
|
775
|
+
if (protocol && protocol_set_busy(protocol) == 0) {
|
775
776
|
// clear the original busy flag
|
776
777
|
unlock_uuid(p2task(task).target);
|
777
778
|
p2task(task).task(p2task(task).target, protocol, p2task(task).arg);
|
@@ -871,7 +872,7 @@ void server_each(intptr_t origin_fd, const char *service,
|
|
871
872
|
*t = (srv_task_s){.service = service,
|
872
873
|
.origin = origin_fd,
|
873
874
|
.task = task,
|
874
|
-
.on_finish = on_finish,
|
875
|
+
.on_finish = (void *)on_finish,
|
875
876
|
.arg = arg};
|
876
877
|
if (async_run(perform_each_task, t))
|
877
878
|
goto error;
|
@@ -895,8 +896,10 @@ void server_task(intptr_t caller_fd,
|
|
895
896
|
t = task_alloc();
|
896
897
|
if (t == NULL)
|
897
898
|
goto error;
|
898
|
-
*t = (srv_task_s){
|
899
|
-
|
899
|
+
*t = (srv_task_s){.target = caller_fd,
|
900
|
+
.task = task,
|
901
|
+
.on_finish = (void *)fallback,
|
902
|
+
.arg = arg};
|
900
903
|
if (async_run(perform_single_task, t))
|
901
904
|
goto error;
|
902
905
|
return;
|
data/ext/iodine/libsock.c
CHANGED
@@ -194,12 +194,13 @@ static size_t fd_capacity = 0;
|
|
194
194
|
|
195
195
|
#define uuid2info(uuid) fd_info[sock_uuid2fd(uuid)]
|
196
196
|
#define is_valid(uuid) \
|
197
|
-
(
|
197
|
+
(sock_uuid2fd(uuid) >= 0 && sock_uuid2fd(uuid) <= (int)fd_capacity && \
|
198
|
+
fd_info[sock_uuid2fd(uuid)].fduuid.data.counter == \
|
198
199
|
((fduuid_u *)(&uuid))->data.counter && \
|
199
200
|
uuid2info(uuid).open)
|
200
201
|
|
201
202
|
static struct {
|
202
|
-
sock_packet_s *pool;
|
203
|
+
sock_packet_s *volatile pool;
|
203
204
|
sock_packet_s *allocated;
|
204
205
|
spn_lock_i lock;
|
205
206
|
} buffer_pool = {.lock = SPN_LOCK_INIT};
|
data/ext/iodine/spnlock.h
CHANGED
@@ -62,13 +62,13 @@ typedef atomic_bool spn_lock_i;
|
|
62
62
|
#define SPN_LOCK_INIT ATOMIC_VAR_INIT(0)
|
63
63
|
/** returns 1 if the lock was busy (TRUE == FAIL). */
|
64
64
|
static inline int spn_trylock(spn_lock_i *lock) {
|
65
|
-
|
65
|
+
__sync_synchronize();
|
66
66
|
return atomic_exchange(lock, 1);
|
67
67
|
}
|
68
68
|
/** Releases a lock. */
|
69
69
|
static inline void spn_unlock(spn_lock_i *lock) {
|
70
70
|
atomic_store(lock, 0);
|
71
|
-
|
71
|
+
__sync_synchronize();
|
72
72
|
}
|
73
73
|
/** returns a lock's state (non 0 == Busy). */
|
74
74
|
static inline int spn_is_locked(spn_lock_i *lock) { return atomic_load(lock); }
|
@@ -98,6 +98,7 @@ static inline int spn_trylock(spn_lock_i *lock) { return __sync_swap(lock, 1); }
|
|
98
98
|
typedef volatile uint8_t spn_lock_i;
|
99
99
|
/** returns 1 if the lock was busy (TRUE == FAIL). */
|
100
100
|
static inline int spn_trylock(spn_lock_i *lock) {
|
101
|
+
__sync_synchronize();
|
101
102
|
return __sync_fetch_and_or(lock, 1);
|
102
103
|
}
|
103
104
|
#define SPN_TMP_HAS_BUILTIN 1
|
@@ -116,6 +117,7 @@ typedef volatile uint8_t spn_lock_i;
|
|
116
117
|
/** returns 1 if the lock was busy (TRUE == FAIL). */
|
117
118
|
static inline int spn_trylock(spn_lock_i *lock) {
|
118
119
|
spn_lock_i tmp;
|
120
|
+
__asm__ volatile("mfence" ::: "memory");
|
119
121
|
__asm__ volatile("xchgb %0,%1" : "=r"(tmp), "=m"(*lock) : "0"(1) : "memory");
|
120
122
|
return tmp;
|
121
123
|
}
|
data/ext/iodine/websockets.c
CHANGED
@@ -520,11 +520,12 @@ static size_t websocket_encode(void *buff, void *data, size_t len, char text,
|
|
520
520
|
.size = len,
|
521
521
|
.masked = client};
|
522
522
|
memcpy(buff, &head, 2);
|
523
|
-
if (client)
|
523
|
+
if (client) {
|
524
524
|
websocket_mask((uint8_t *)buff + 2, data, len);
|
525
|
-
|
525
|
+
len += 4;
|
526
|
+
} else
|
526
527
|
memcpy((uint8_t *)buff + 2, data, len);
|
527
|
-
return len + 2
|
528
|
+
return len + 2;
|
528
529
|
} else if (len < (1UL << 16)) {
|
529
530
|
/* head is 4 bytes */
|
530
531
|
struct {
|
@@ -542,11 +543,12 @@ static size_t websocket_encode(void *buff, void *data, size_t len, char text,
|
|
542
543
|
.masked = client,
|
543
544
|
.length = htons(len)};
|
544
545
|
memcpy(buff, &head, 4);
|
545
|
-
if (client)
|
546
|
+
if (client) {
|
546
547
|
websocket_mask((uint8_t *)buff + 4, data, len);
|
547
|
-
|
548
|
+
len += 4;
|
549
|
+
} else
|
548
550
|
memcpy((uint8_t *)buff + 4, data, len);
|
549
|
-
return len + 4
|
551
|
+
return len + 4;
|
550
552
|
}
|
551
553
|
/* Really Long Message */
|
552
554
|
struct {
|
@@ -565,11 +567,12 @@ static size_t websocket_encode(void *buff, void *data, size_t len, char text,
|
|
565
567
|
};
|
566
568
|
memcpy(buff, &head, 2);
|
567
569
|
((size_t *)((uint8_t *)buff + 2))[0] = bswap64(len);
|
568
|
-
if (client)
|
570
|
+
if (client) {
|
569
571
|
websocket_mask((uint8_t *)buff + 10, data, len);
|
570
|
-
|
572
|
+
len += 4;
|
573
|
+
} else
|
571
574
|
memcpy((uint8_t *)buff + 10, data, len);
|
572
|
-
return len + 10
|
575
|
+
return len + 10;
|
573
576
|
}
|
574
577
|
|
575
578
|
static void websocket_write_impl(intptr_t fd, void *data, size_t len,
|
@@ -817,23 +820,45 @@ Multi-Write (direct broadcast) Implementation
|
|
817
820
|
*/
|
818
821
|
struct websocket_multi_write {
|
819
822
|
uint8_t (*if_callback)(ws_s *ws_to, void *arg);
|
823
|
+
void (*on_finished)(ws_s *ws_origin, void *arg);
|
824
|
+
intptr_t origin;
|
820
825
|
void *arg;
|
821
826
|
spn_lock_i lock;
|
827
|
+
/* ... we need to have padding for pointer arithmatics... */
|
828
|
+
uint8_t as_client;
|
829
|
+
/* ... we need to have padding for pointer arithmatics... */
|
822
830
|
size_t count;
|
823
831
|
size_t length;
|
824
|
-
uint8_t
|
825
|
-
uint8_t buffer[];
|
832
|
+
uint8_t buffer[]; /* starts on border alignment */
|
826
833
|
};
|
827
834
|
|
835
|
+
static void ws_mw_defered_on_finish_fb(intptr_t fd, void *arg) {
|
836
|
+
(void)(fd);
|
837
|
+
struct websocket_multi_write *fin = arg;
|
838
|
+
fin->on_finished(NULL, fin->arg);
|
839
|
+
free(fin);
|
840
|
+
}
|
841
|
+
static void ws_mw_defered_on_finish(intptr_t fd, protocol_s *ws, void *arg) {
|
842
|
+
(void)(fd);
|
843
|
+
struct websocket_multi_write *fin = arg;
|
844
|
+
fin->on_finished((ws->service == WEBSOCKET_ID_STR ? (ws_s *)ws : NULL),
|
845
|
+
fin->arg);
|
846
|
+
free(fin);
|
847
|
+
}
|
848
|
+
|
828
849
|
static void ws_reduce_or_free_multi_write(void *buff) {
|
829
|
-
struct websocket_multi_write *mw =
|
830
|
-
(void *)((uintptr_t)buff - sizeof(struct websocket_multi_write));
|
850
|
+
struct websocket_multi_write *mw = (void *)((uintptr_t)buff - sizeof(*mw));
|
831
851
|
spn_lock(&mw->lock);
|
832
852
|
mw->count -= 1;
|
833
|
-
spn_unlock(&mw->lock);
|
834
853
|
if (!mw->count) {
|
835
|
-
|
836
|
-
|
854
|
+
spn_unlock(&mw->lock);
|
855
|
+
if (mw->on_finished) {
|
856
|
+
server_task(mw->origin, ws_mw_defered_on_finish, mw,
|
857
|
+
ws_mw_defered_on_finish_fb);
|
858
|
+
} else
|
859
|
+
free(mw);
|
860
|
+
} else
|
861
|
+
spn_unlock(&mw->lock);
|
837
862
|
}
|
838
863
|
|
839
864
|
static void ws_finish_multi_write(intptr_t fd, protocol_s *_ws, void *arg) {
|
@@ -872,20 +897,28 @@ static void ws_check_multi_write(intptr_t fd, protocol_s *_ws, void *arg) {
|
|
872
897
|
ws_direct_multi_write(fd, _ws, arg);
|
873
898
|
}
|
874
899
|
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
900
|
+
#undef websocket_write_each
|
901
|
+
int websocket_write_each(struct websocket_write_each_args_s args) {
|
902
|
+
if (!args.data || !args.length)
|
903
|
+
return -1;
|
879
904
|
struct websocket_multi_write *multi =
|
880
|
-
malloc(
|
881
|
-
multi
|
882
|
-
|
883
|
-
multi
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
905
|
+
malloc(sizeof(*multi) + args.length + 16 /* max head size + 2 */);
|
906
|
+
if (!multi)
|
907
|
+
return -1;
|
908
|
+
*multi = (struct websocket_multi_write){
|
909
|
+
.length = websocket_encode(multi->buffer, args.data, args.length,
|
910
|
+
args.is_text, 1, 1, args.as_client),
|
911
|
+
.if_callback = args.filter,
|
912
|
+
.on_finished = args.on_finished,
|
913
|
+
.arg = args.arg,
|
914
|
+
.origin = (args.origin ? args.origin->fd : -1),
|
915
|
+
.as_client = args.as_client,
|
916
|
+
.lock = SPN_LOCK_INIT,
|
917
|
+
.count = 1,
|
918
|
+
};
|
919
|
+
|
920
|
+
server_each(multi->origin, WEBSOCKET_ID_STR,
|
921
|
+
(args.filter ? ws_check_multi_write : ws_direct_multi_write),
|
890
922
|
multi, ws_finish_multi_write);
|
923
|
+
return 0;
|
891
924
|
}
|
data/ext/iodine/websockets.h
CHANGED
@@ -128,24 +128,44 @@ Performs a task on each websocket connection that shares the same process
|
|
128
128
|
void websocket_each(ws_s *ws_originator,
|
129
129
|
void (*task)(ws_s *ws_target, void *arg), void *arg,
|
130
130
|
void (*on_finish)(ws_s *ws_originator, void *arg));
|
131
|
+
|
132
|
+
/**
|
133
|
+
The Arguments passed to the `websocket_write_each` function / macro are defined
|
134
|
+
here, for convinience of calling the function.
|
135
|
+
*/
|
136
|
+
struct websocket_write_each_args_s {
|
137
|
+
/** The originating websocket client will be excluded from the `write`.
|
138
|
+
* Can be NULL. */
|
139
|
+
ws_s *origin;
|
140
|
+
/** The data to be written to the websocket - required(!) */
|
141
|
+
void *data;
|
142
|
+
/** The length of the data to be written to the websocket - required(!) */
|
143
|
+
size_t length;
|
144
|
+
/** Text mode vs. binary mode. Defaults to binary mode. */
|
145
|
+
uint8_t is_text;
|
146
|
+
/** Set to 1 to send the data to websockets where this application is the
|
147
|
+
* client. Defaults to 0 (the data is sent to all clients where this
|
148
|
+
* application is the server). */
|
149
|
+
uint8_t as_client;
|
150
|
+
/** A filter callback, allowing us to exclude some clients.
|
151
|
+
* Should return 1 to send data and 0 to exclude. */
|
152
|
+
uint8_t (*filter)(ws_s *ws_to, void *arg);
|
153
|
+
/** A callback called once all the data was sent. */
|
154
|
+
void (*on_finished)(ws_s *ws_to, void *arg);
|
155
|
+
/** A user specified argumernt passed to each of the callbacks. */
|
156
|
+
void *arg;
|
157
|
+
};
|
131
158
|
/**
|
132
159
|
Writes data to each websocket connection that shares the same process
|
133
160
|
(except the originating `ws_s` connection which is allowed to be NULL).
|
134
161
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
The `as_client` is a boolean value indicating if the data should be masked (sent
|
139
|
-
to a server, in client mode) or not. The data will only be sent to the
|
140
|
-
connections matching the required state (i.e., if `as_client == 1`, the data
|
141
|
-
will only be sent to connections where this process behaves as a websocket
|
142
|
-
client). If some data should be sent in client mode and other in server mode,
|
143
|
-
than the function must be called twice.
|
162
|
+
Accepts a sing `struct websocket_write_each_args_s` argument. See the struct
|
163
|
+
details for possible arguments.
|
144
164
|
*/
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
165
|
+
int websocket_write_each(struct websocket_write_each_args_s args);
|
166
|
+
#define websocket_write_each(...) \
|
167
|
+
websocket_write_each((struct websocket_write_each_args_s){__VA_ARGS__})
|
168
|
+
/**
|
149
169
|
Counts the number of websocket connections.
|
150
170
|
*/
|
151
171
|
size_t websocket_count(ws_s *ws);
|
data/lib/iodine/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: iodine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Boaz Segev
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-03-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|