puma 4.3.11-java → 4.3.12-java
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of puma might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/History.md +5 -0
- data/ext/puma_http11/extconf.rb +8 -0
- data/ext/puma_http11/mini_ssl.c +82 -47
- data/lib/puma/client.rb +54 -11
- data/lib/puma/const.rb +6 -4
- data/lib/puma/puma_http11.jar +0 -0
- data/lib/puma/server.rb +9 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a7d274f1762cbcb087c7b96ed35e87b0130141ec26f87d87b03a3d5ead5acf88
|
4
|
+
data.tar.gz: 3e1bb4c0b9905e847703c424d890ce3b7f60e5923b5fd332af6f0c7a2aa31915
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec28cbf74cfce5dbd955d1ed1da02ff28e05eecba4d8746b19c7c7d4248f46ae18c392d4616248e6f43d1344895636a5dc3f2328fc92e4411d274a3159e30676
|
7
|
+
data.tar.gz: d8809d43d2e39280d7627390a24281349f0f8dd8336fd612503ea89909fdfd1fe684918c7fc426af1484ba71f063363a414792e92fd4171b9e449548aa744365
|
data/History.md
CHANGED
data/ext/puma_http11/extconf.rb
CHANGED
@@ -22,6 +22,14 @@ unless ENV["DISABLE_SSL"]
|
|
22
22
|
# with versions after 1.1.1
|
23
23
|
have_func "TLS_server_method" , "openssl/ssl.h"
|
24
24
|
have_macro "SSL_CTX_set_min_proto_version", "openssl/ssl.h"
|
25
|
+
|
26
|
+
# Random.bytes available in Ruby 2.5 and later, Random::DEFAULT deprecated in 3.0
|
27
|
+
if Random.respond_to?(:bytes)
|
28
|
+
$defs.push("-DHAVE_RANDOM_BYTES")
|
29
|
+
puts "checking for Random.bytes... yes"
|
30
|
+
else
|
31
|
+
puts "checking for Random.bytes... no"
|
32
|
+
end
|
25
33
|
end
|
26
34
|
end
|
27
35
|
|
data/ext/puma_http11/mini_ssl.c
CHANGED
@@ -62,44 +62,65 @@ ms_conn* engine_alloc(VALUE klass, VALUE* obj) {
|
|
62
62
|
return conn;
|
63
63
|
}
|
64
64
|
|
65
|
-
DH *
|
66
|
-
/* `openssl dhparam
|
65
|
+
DH *get_dh2048(void) {
|
66
|
+
/* `openssl dhparam -C 2048`
|
67
67
|
* -----BEGIN DH PARAMETERS-----
|
68
|
-
*
|
69
|
-
*
|
70
|
-
*
|
68
|
+
* MIIBCAKCAQEAjmh1uQHdTfxOyxEbKAV30fUfzqMDF/ChPzjfyzl2jcrqQMhrk76o
|
69
|
+
* 2NPNXqxHwsddMZ1RzvU8/jl+uhRuPWjXCFZbhET4N1vrviZM3VJhV8PPHuiVOACO
|
70
|
+
* y32jFd+Szx4bo2cXSK83hJ6jRd+0asP1awWjz9/06dFkrILCXMIfQLo0D8rqmppn
|
71
|
+
* EfDDAwuudCpM9kcDmBRAm9JsKbQ6gzZWjkc5+QWSaQofojIHbjvj3xzguaCJn+oQ
|
72
|
+
* vHWM+hsAnaOgEwCyeZ3xqs+/5lwSbkE/tqJW98cEZGygBUVo9jxZRZx6KOfjpdrb
|
73
|
+
* yenO9LJr/qtyrZB31WJbqxI0m0AKTAO8UwIBAg==
|
71
74
|
* -----END DH PARAMETERS-----
|
72
75
|
*/
|
73
|
-
static unsigned char
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
76
|
+
static unsigned char dh2048_p[] = {
|
77
|
+
0x8E, 0x68, 0x75, 0xB9, 0x01, 0xDD, 0x4D, 0xFC, 0x4E, 0xCB,
|
78
|
+
0x11, 0x1B, 0x28, 0x05, 0x77, 0xD1, 0xF5, 0x1F, 0xCE, 0xA3,
|
79
|
+
0x03, 0x17, 0xF0, 0xA1, 0x3F, 0x38, 0xDF, 0xCB, 0x39, 0x76,
|
80
|
+
0x8D, 0xCA, 0xEA, 0x40, 0xC8, 0x6B, 0x93, 0xBE, 0xA8, 0xD8,
|
81
|
+
0xD3, 0xCD, 0x5E, 0xAC, 0x47, 0xC2, 0xC7, 0x5D, 0x31, 0x9D,
|
82
|
+
0x51, 0xCE, 0xF5, 0x3C, 0xFE, 0x39, 0x7E, 0xBA, 0x14, 0x6E,
|
83
|
+
0x3D, 0x68, 0xD7, 0x08, 0x56, 0x5B, 0x84, 0x44, 0xF8, 0x37,
|
84
|
+
0x5B, 0xEB, 0xBE, 0x26, 0x4C, 0xDD, 0x52, 0x61, 0x57, 0xC3,
|
85
|
+
0xCF, 0x1E, 0xE8, 0x95, 0x38, 0x00, 0x8E, 0xCB, 0x7D, 0xA3,
|
86
|
+
0x15, 0xDF, 0x92, 0xCF, 0x1E, 0x1B, 0xA3, 0x67, 0x17, 0x48,
|
87
|
+
0xAF, 0x37, 0x84, 0x9E, 0xA3, 0x45, 0xDF, 0xB4, 0x6A, 0xC3,
|
88
|
+
0xF5, 0x6B, 0x05, 0xA3, 0xCF, 0xDF, 0xF4, 0xE9, 0xD1, 0x64,
|
89
|
+
0xAC, 0x82, 0xC2, 0x5C, 0xC2, 0x1F, 0x40, 0xBA, 0x34, 0x0F,
|
90
|
+
0xCA, 0xEA, 0x9A, 0x9A, 0x67, 0x11, 0xF0, 0xC3, 0x03, 0x0B,
|
91
|
+
0xAE, 0x74, 0x2A, 0x4C, 0xF6, 0x47, 0x03, 0x98, 0x14, 0x40,
|
92
|
+
0x9B, 0xD2, 0x6C, 0x29, 0xB4, 0x3A, 0x83, 0x36, 0x56, 0x8E,
|
93
|
+
0x47, 0x39, 0xF9, 0x05, 0x92, 0x69, 0x0A, 0x1F, 0xA2, 0x32,
|
94
|
+
0x07, 0x6E, 0x3B, 0xE3, 0xDF, 0x1C, 0xE0, 0xB9, 0xA0, 0x89,
|
95
|
+
0x9F, 0xEA, 0x10, 0xBC, 0x75, 0x8C, 0xFA, 0x1B, 0x00, 0x9D,
|
96
|
+
0xA3, 0xA0, 0x13, 0x00, 0xB2, 0x79, 0x9D, 0xF1, 0xAA, 0xCF,
|
97
|
+
0xBF, 0xE6, 0x5C, 0x12, 0x6E, 0x41, 0x3F, 0xB6, 0xA2, 0x56,
|
98
|
+
0xF7, 0xC7, 0x04, 0x64, 0x6C, 0xA0, 0x05, 0x45, 0x68, 0xF6,
|
99
|
+
0x3C, 0x59, 0x45, 0x9C, 0x7A, 0x28, 0xE7, 0xE3, 0xA5, 0xDA,
|
100
|
+
0xDB, 0xC9, 0xE9, 0xCE, 0xF4, 0xB2, 0x6B, 0xFE, 0xAB, 0x72,
|
101
|
+
0xAD, 0x90, 0x77, 0xD5, 0x62, 0x5B, 0xAB, 0x12, 0x34, 0x9B,
|
102
|
+
0x40, 0x0A, 0x4C, 0x03, 0xBC, 0x53
|
85
103
|
};
|
86
|
-
static unsigned char
|
104
|
+
static unsigned char dh2048_g[] = { 0x02 };
|
87
105
|
|
88
106
|
DH *dh;
|
107
|
+
#if !(OPENSSL_VERSION_NUMBER < 0x10100005L || defined(LIBRESSL_VERSION_NUMBER))
|
108
|
+
BIGNUM *p, *g;
|
109
|
+
#endif
|
110
|
+
|
89
111
|
dh = DH_new();
|
90
112
|
|
91
113
|
#if OPENSSL_VERSION_NUMBER < 0x10100005L || defined(LIBRESSL_VERSION_NUMBER)
|
92
|
-
dh->p = BN_bin2bn(
|
93
|
-
dh->g = BN_bin2bn(
|
114
|
+
dh->p = BN_bin2bn(dh2048_p, sizeof(dh2048_p), NULL);
|
115
|
+
dh->g = BN_bin2bn(dh2048_g, sizeof(dh2048_g), NULL);
|
94
116
|
|
95
117
|
if ((dh->p == NULL) || (dh->g == NULL)) {
|
96
118
|
DH_free(dh);
|
97
119
|
return NULL;
|
98
120
|
}
|
99
121
|
#else
|
100
|
-
|
101
|
-
|
102
|
-
g = BN_bin2bn(dh1024_g, sizeof(dh1024_g), NULL);
|
122
|
+
p = BN_bin2bn(dh2048_p, sizeof(dh2048_p), NULL);
|
123
|
+
g = BN_bin2bn(dh2048_g, sizeof(dh2048_g), NULL);
|
103
124
|
|
104
125
|
if (p == NULL || g == NULL || !DH_set0_pqg(dh, p, NULL, g)) {
|
105
126
|
DH_free(dh);
|
@@ -139,7 +160,7 @@ static int engine_verify_callback(int preverify_ok, X509_STORE_CTX* ctx) {
|
|
139
160
|
}
|
140
161
|
|
141
162
|
VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
|
142
|
-
VALUE obj;
|
163
|
+
VALUE obj, session_id_bytes;
|
143
164
|
SSL_CTX* ctx;
|
144
165
|
SSL* ssl;
|
145
166
|
int min, ssl_options;
|
@@ -198,7 +219,7 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
|
|
198
219
|
else {
|
199
220
|
min = TLS1_VERSION;
|
200
221
|
}
|
201
|
-
|
222
|
+
|
202
223
|
SSL_CTX_set_min_proto_version(ctx, min);
|
203
224
|
|
204
225
|
SSL_CTX_set_options(ctx, ssl_options);
|
@@ -226,7 +247,21 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
|
|
226
247
|
SSL_CTX_set_cipher_list(ctx, "HIGH:!aNULL@STRENGTH");
|
227
248
|
}
|
228
249
|
|
229
|
-
|
250
|
+
// Random.bytes available in Ruby 2.5 and later, Random::DEFAULT deprecated in 3.0
|
251
|
+
session_id_bytes = rb_funcall(
|
252
|
+
#ifdef HAVE_RANDOM_BYTES
|
253
|
+
rb_cRandom,
|
254
|
+
#else
|
255
|
+
rb_const_get(rb_cRandom, rb_intern_const("DEFAULT")),
|
256
|
+
#endif
|
257
|
+
rb_intern_const("bytes"),
|
258
|
+
1, ULL2NUM(SSL_MAX_SSL_SESSION_ID_LENGTH));
|
259
|
+
|
260
|
+
SSL_CTX_set_session_id_context(ctx,
|
261
|
+
(unsigned char *) RSTRING_PTR(session_id_bytes),
|
262
|
+
SSL_MAX_SSL_SESSION_ID_LENGTH);
|
263
|
+
|
264
|
+
DH *dh = get_dh2048();
|
230
265
|
SSL_CTX_set_tmp_dh(ctx, dh);
|
231
266
|
|
232
267
|
#if OPENSSL_VERSION_NUMBER < 0x10002000L
|
@@ -493,27 +528,27 @@ void Init_mini_ssl(VALUE puma) {
|
|
493
528
|
#else
|
494
529
|
rb_define_const(mod, "OPENSSL_LIBRARY_VERSION", rb_str_new2(SSLeay_version(SSLEAY_VERSION)));
|
495
530
|
#endif
|
496
|
-
|
497
|
-
#if defined(OPENSSL_NO_SSL3) || defined(OPENSSL_NO_SSL3_METHOD)
|
498
|
-
/* True if SSL3 is not available */
|
499
|
-
rb_define_const(mod, "OPENSSL_NO_SSL3", Qtrue);
|
500
|
-
#else
|
501
|
-
rb_define_const(mod, "OPENSSL_NO_SSL3", Qfalse);
|
502
|
-
#endif
|
503
|
-
|
504
|
-
#if defined(OPENSSL_NO_TLS1) || defined(OPENSSL_NO_TLS1_METHOD)
|
505
|
-
/* True if TLS1 is not available */
|
506
|
-
rb_define_const(mod, "OPENSSL_NO_TLS1", Qtrue);
|
507
|
-
#else
|
508
|
-
rb_define_const(mod, "OPENSSL_NO_TLS1", Qfalse);
|
509
|
-
#endif
|
510
|
-
|
511
|
-
#if defined(OPENSSL_NO_TLS1_1) || defined(OPENSSL_NO_TLS1_1_METHOD)
|
512
|
-
/* True if TLS1_1 is not available */
|
513
|
-
rb_define_const(mod, "OPENSSL_NO_TLS1_1", Qtrue);
|
514
|
-
#else
|
515
|
-
rb_define_const(mod, "OPENSSL_NO_TLS1_1", Qfalse);
|
516
|
-
#endif
|
531
|
+
|
532
|
+
#if defined(OPENSSL_NO_SSL3) || defined(OPENSSL_NO_SSL3_METHOD)
|
533
|
+
/* True if SSL3 is not available */
|
534
|
+
rb_define_const(mod, "OPENSSL_NO_SSL3", Qtrue);
|
535
|
+
#else
|
536
|
+
rb_define_const(mod, "OPENSSL_NO_SSL3", Qfalse);
|
537
|
+
#endif
|
538
|
+
|
539
|
+
#if defined(OPENSSL_NO_TLS1) || defined(OPENSSL_NO_TLS1_METHOD)
|
540
|
+
/* True if TLS1 is not available */
|
541
|
+
rb_define_const(mod, "OPENSSL_NO_TLS1", Qtrue);
|
542
|
+
#else
|
543
|
+
rb_define_const(mod, "OPENSSL_NO_TLS1", Qfalse);
|
544
|
+
#endif
|
545
|
+
|
546
|
+
#if defined(OPENSSL_NO_TLS1_1) || defined(OPENSSL_NO_TLS1_1_METHOD)
|
547
|
+
/* True if TLS1_1 is not available */
|
548
|
+
rb_define_const(mod, "OPENSSL_NO_TLS1_1", Qtrue);
|
549
|
+
#else
|
550
|
+
rb_define_const(mod, "OPENSSL_NO_TLS1_1", Qfalse);
|
551
|
+
#endif
|
517
552
|
|
518
553
|
rb_define_singleton_method(mod, "check", noop, 0);
|
519
554
|
|
data/lib/puma/client.rb
CHANGED
@@ -23,6 +23,8 @@ module Puma
|
|
23
23
|
|
24
24
|
class ConnectionError < RuntimeError; end
|
25
25
|
|
26
|
+
class HttpParserError501 < IOError; end
|
27
|
+
|
26
28
|
# An instance of this class represents a unique request from a client.
|
27
29
|
# For example, this could be a web request from a browser or from CURL.
|
28
30
|
#
|
@@ -35,7 +37,21 @@ module Puma
|
|
35
37
|
# Instances of this class are responsible for knowing if
|
36
38
|
# the header and body are fully buffered via the `try_to_finish` method.
|
37
39
|
# They can be used to "time out" a response via the `timeout_at` reader.
|
40
|
+
#
|
38
41
|
class Client
|
42
|
+
|
43
|
+
# this tests all values but the last, which must be chunked
|
44
|
+
ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
|
45
|
+
|
46
|
+
# chunked body validation
|
47
|
+
CHUNK_SIZE_INVALID = /[^\h]/.freeze
|
48
|
+
CHUNK_VALID_ENDING = "\r\n".freeze
|
49
|
+
|
50
|
+
# Content-Length header value validation
|
51
|
+
CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
|
52
|
+
|
53
|
+
TE_ERR_MSG = 'Invalid Transfer-Encoding'
|
54
|
+
|
39
55
|
# The object used for a request with no body. All requests with
|
40
56
|
# no body share this one object since it has no state.
|
41
57
|
EmptyBody = NullIO.new
|
@@ -284,16 +300,27 @@ module Puma
|
|
284
300
|
body = @parser.body
|
285
301
|
|
286
302
|
te = @env[TRANSFER_ENCODING2]
|
287
|
-
|
288
303
|
if te
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
304
|
+
te_lwr = te.downcase
|
305
|
+
if te.include? ','
|
306
|
+
te_ary = te_lwr.split ','
|
307
|
+
te_count = te_ary.count CHUNKED
|
308
|
+
te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
|
309
|
+
if te_ary.last == CHUNKED && te_count == 1 && te_valid
|
310
|
+
@env.delete TRANSFER_ENCODING2
|
311
|
+
return setup_chunked_body body
|
312
|
+
elsif te_count >= 1
|
313
|
+
raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
|
314
|
+
elsif !te_valid
|
315
|
+
raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
294
316
|
end
|
295
|
-
elsif
|
296
|
-
|
317
|
+
elsif te_lwr == CHUNKED
|
318
|
+
@env.delete TRANSFER_ENCODING2
|
319
|
+
return setup_chunked_body body
|
320
|
+
elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
|
321
|
+
raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
|
322
|
+
else
|
323
|
+
raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
297
324
|
end
|
298
325
|
end
|
299
326
|
|
@@ -301,7 +328,12 @@ module Puma
|
|
301
328
|
|
302
329
|
cl = @env[CONTENT_LENGTH]
|
303
330
|
|
304
|
-
|
331
|
+
if cl
|
332
|
+
# cannot contain characters that are not \d
|
333
|
+
if cl =~ CONTENT_LENGTH_VALUE_INVALID
|
334
|
+
raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
|
335
|
+
end
|
336
|
+
else
|
305
337
|
@buffer = body.empty? ? nil : body
|
306
338
|
@body = EmptyBody
|
307
339
|
set_ready
|
@@ -450,7 +482,13 @@ module Puma
|
|
450
482
|
while !io.eof?
|
451
483
|
line = io.gets
|
452
484
|
if line.end_with?("\r\n")
|
453
|
-
|
485
|
+
# Puma doesn't process chunk extensions, but should parse if they're
|
486
|
+
# present, which is the reason for the semicolon regex
|
487
|
+
chunk_hex = line.strip[/\A[^;]+/]
|
488
|
+
if chunk_hex =~ CHUNK_SIZE_INVALID
|
489
|
+
raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
|
490
|
+
end
|
491
|
+
len = chunk_hex.to_i(16)
|
454
492
|
if len == 0
|
455
493
|
@in_last_chunk = true
|
456
494
|
@body.rewind
|
@@ -481,7 +519,12 @@ module Puma
|
|
481
519
|
|
482
520
|
case
|
483
521
|
when got == len
|
484
|
-
|
522
|
+
# proper chunked segment must end with "\r\n"
|
523
|
+
if part.end_with? CHUNK_VALID_ENDING
|
524
|
+
write_chunk(part[0..-3]) # to skip the ending \r\n
|
525
|
+
else
|
526
|
+
raise HttpParserError, "Chunk size mismatch"
|
527
|
+
end
|
485
528
|
when got <= len - 2
|
486
529
|
write_chunk(part)
|
487
530
|
@partial_part_left = len - part.size
|
data/lib/puma/const.rb
CHANGED
@@ -76,7 +76,7 @@ module Puma
|
|
76
76
|
508 => 'Loop Detected',
|
77
77
|
510 => 'Not Extended',
|
78
78
|
511 => 'Network Authentication Required'
|
79
|
-
}
|
79
|
+
}.freeze
|
80
80
|
|
81
81
|
# For some HTTP status codes the client only expects headers.
|
82
82
|
#
|
@@ -85,7 +85,7 @@ module Puma
|
|
85
85
|
204 => true,
|
86
86
|
205 => true,
|
87
87
|
304 => true
|
88
|
-
}
|
88
|
+
}.freeze
|
89
89
|
|
90
90
|
# Frequently used constants when constructing requests or responses. Many times
|
91
91
|
# the constant just refers to a string with the same contents. Using these constants
|
@@ -100,7 +100,7 @@ module Puma
|
|
100
100
|
# too taxing on performance.
|
101
101
|
module Const
|
102
102
|
|
103
|
-
PUMA_VERSION = VERSION = "4.3.
|
103
|
+
PUMA_VERSION = VERSION = "4.3.12".freeze
|
104
104
|
CODE_NAME = "Mysterious Traveller".freeze
|
105
105
|
PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
|
106
106
|
|
@@ -144,9 +144,11 @@ module Puma
|
|
144
144
|
408 => "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\n".freeze,
|
145
145
|
# Indicate that there was an internal error, obviously.
|
146
146
|
500 => "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze,
|
147
|
+
# Incorrect or invalid header value
|
148
|
+
501 => "HTTP/1.1 501 Not Implemented\r\n\r\n".freeze,
|
147
149
|
# A common header for indicating the server is too busy. Not used yet.
|
148
150
|
503 => "HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze
|
149
|
-
}
|
151
|
+
}.freeze
|
150
152
|
|
151
153
|
# The basic max request size we'll try to read.
|
152
154
|
CHUNK_SIZE = 16 * 1024
|
data/lib/puma/puma_http11.jar
CHANGED
Binary file
|
data/lib/puma/server.rb
CHANGED
@@ -320,6 +320,10 @@ module Puma
|
|
320
320
|
client.write_error(400)
|
321
321
|
client.close
|
322
322
|
|
323
|
+
@events.parse_error self, client.env, e
|
324
|
+
rescue HttpParserError501 => e
|
325
|
+
client.write_error(501)
|
326
|
+
client.close
|
323
327
|
@events.parse_error self, client.env, e
|
324
328
|
rescue ConnectionError, EOFError
|
325
329
|
client.close
|
@@ -530,7 +534,12 @@ module Puma
|
|
530
534
|
client.write_error(400)
|
531
535
|
|
532
536
|
@events.parse_error self, client.env, e
|
537
|
+
rescue HttpParserError501 => e
|
538
|
+
lowlevel_error(e, client.env)
|
533
539
|
|
540
|
+
client.write_error(501)
|
541
|
+
|
542
|
+
@events.parse_error self, client.env, e
|
534
543
|
# Server error
|
535
544
|
rescue StandardError => e
|
536
545
|
lowlevel_error(e, client.env)
|