edgeway 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +602 -204
- data/js/cloudflare-kv.js +47 -0
- data/js/cloudflare-ratelimit.js +31 -0
- data/js/cloudflare-test-database.js +112 -0
- data/js/cloudflare-test-runner.js +76 -0
- data/js/cloudflare.js +25 -4
- data/js/fetch.js +113 -0
- data/js/jspi.js +32 -0
- data/js/jwt.js +5 -39
- data/js/runtime.js +42 -5
- data/lib/edgeway/application.rb +16 -7
- data/lib/edgeway/cache/limits.rb +8 -0
- data/lib/edgeway/cache/memory_store.rb +24 -0
- data/lib/edgeway/cache/null_store.rb +13 -0
- data/lib/edgeway/cache/store.rb +189 -0
- data/lib/edgeway/cache.rb +13 -0
- data/lib/edgeway/cli/build/cache_config.rb +108 -0
- data/lib/edgeway/cli/build/database_config.rb +4 -0
- data/lib/edgeway/cli/build/js_files.rb +30 -0
- data/lib/edgeway/cli/build.rb +17 -10
- data/lib/edgeway/cli/cloudflare_test/wasm_test_cli.rb +35 -0
- data/lib/edgeway/cli/cloudflare_test.rb +120 -0
- data/lib/edgeway/cli/test.rb +25 -9
- data/lib/edgeway/cli/toolchain.rb +5 -0
- data/lib/edgeway/cli.rb +2 -0
- data/lib/edgeway/controller/base.rb +22 -3
- data/lib/edgeway/controller/caching.rb +140 -0
- data/lib/edgeway/controller/callbacks.rb +5 -1
- data/lib/edgeway/controller/rate_limiting.rb +145 -0
- data/lib/edgeway/db/query_stats.rb +2 -6
- data/lib/edgeway/errors.rb +21 -0
- data/lib/edgeway/generators/app/templates/.github/workflows/ci.yml.tt +3 -3
- data/lib/edgeway/generators/app/templates/README.md.tt +3 -1
- data/lib/edgeway/generators/app/templates/bin/setup.tt +2 -3
- data/lib/edgeway/generators/app/templates/config/cache.yml +22 -0
- data/lib/edgeway/generators/app/templates/config/database.yml +8 -2
- data/lib/edgeway/generators/app/templates/public/.assetsignore +1 -0
- data/lib/edgeway/generators/app/templates/wrangler.toml.tt +34 -2
- data/lib/edgeway/http/client.rb +80 -0
- data/lib/edgeway/http/request.rb +5 -0
- data/lib/edgeway/http/stub_transport.rb +38 -0
- data/lib/edgeway/logger.rb +33 -5
- data/lib/edgeway/rate_limit/limiter.rb +9 -0
- data/lib/edgeway/rate_limit/null_limiter.rb +9 -0
- data/lib/edgeway/rate_limit/stub_limiter.rb +24 -0
- data/lib/edgeway/rate_limit.rb +2 -0
- data/lib/edgeway/server.rb +3 -2
- data/lib/edgeway/service/cloudflare/d1.rb +6 -21
- data/lib/edgeway/service/cloudflare/jwt.rb +3 -15
- data/lib/edgeway/service/cloudflare/kv.rb +45 -0
- data/lib/edgeway/service/cloudflare/kv_store.rb +58 -0
- data/lib/edgeway/service/cloudflare/rate_limit.rb +27 -0
- data/lib/edgeway/service/cloudflare/rate_limiter.rb +33 -0
- data/lib/edgeway/service/cloudflare/test_databases.rb +65 -0
- data/lib/edgeway/service/fetch.rb +26 -0
- data/lib/edgeway/service/import_result.rb +24 -0
- data/lib/edgeway/test/sqlite_databases.rb +18 -0
- data/lib/edgeway/test.rb +72 -33
- data/lib/edgeway/version.rb +1 -1
- data/lib/edgeway.rb +6 -0
- metadata +34 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Edgeway
|
|
2
|
+
module HTTP
|
|
3
|
+
DEFAULT_TIMEOUT = 10
|
|
4
|
+
MAX_TIMEOUT = 300
|
|
5
|
+
|
|
6
|
+
class ClientResponse
|
|
7
|
+
attr_reader :status, :headers, :body
|
|
8
|
+
|
|
9
|
+
def initialize(status, headers, body)
|
|
10
|
+
@status = status
|
|
11
|
+
@headers = headers
|
|
12
|
+
@body = body
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def success? = @status >= 200 && @status < 300
|
|
16
|
+
|
|
17
|
+
def json = JSON.parse(@body)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Transport
|
|
21
|
+
def request(payload)
|
|
22
|
+
JSON.generate({ "error" => "no HTTP transport is set" })
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@client = Transport.new
|
|
27
|
+
|
|
28
|
+
def self.client = @client
|
|
29
|
+
|
|
30
|
+
def self.client=(client)
|
|
31
|
+
@client = client
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.get(url, headers: {}, timeout: DEFAULT_TIMEOUT)
|
|
35
|
+
send_request("GET", url, "", headers, timeout)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.delete(url, headers: {}, timeout: DEFAULT_TIMEOUT)
|
|
39
|
+
send_request("DELETE", url, "", headers, timeout)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.post(url, body: "", headers: {}, timeout: DEFAULT_TIMEOUT)
|
|
43
|
+
send_request("POST", url, body, headers, timeout)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.put(url, body: "", headers: {}, timeout: DEFAULT_TIMEOUT)
|
|
47
|
+
send_request("PUT", url, body, headers, timeout)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.patch(url, body: "", headers: {}, timeout: DEFAULT_TIMEOUT)
|
|
51
|
+
send_request("PATCH", url, body, headers, timeout)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.send_request(verb, url, body, headers, timeout)
|
|
55
|
+
timeout_ms = timeout_milliseconds(timeout)
|
|
56
|
+
payload = JSON.generate({
|
|
57
|
+
"method" => verb, "url" => url, "headers" => headers, "body" => body,
|
|
58
|
+
"timeout_ms" => timeout_ms
|
|
59
|
+
})
|
|
60
|
+
parse_answer(client.request(payload))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.timeout_milliseconds(timeout)
|
|
64
|
+
milliseconds = timeout > MAX_TIMEOUT ? 0 : (timeout * 1000).to_i
|
|
65
|
+
raise ArgumentError, "timeout must be 0.001 to #{MAX_TIMEOUT} seconds" if milliseconds < 1
|
|
66
|
+
|
|
67
|
+
milliseconds
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.parse_answer(text)
|
|
71
|
+
answer = JSON.parse(text)
|
|
72
|
+
raise Timeout, answer["error"] if answer.key?("timeout")
|
|
73
|
+
raise Error, answer["error"] if answer.key?("error")
|
|
74
|
+
|
|
75
|
+
body = answer["body_base64"].unpack1("m0")
|
|
76
|
+
body.force_encoding("UTF-8")
|
|
77
|
+
ClientResponse.new(answer["status"], answer["headers"], body)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/edgeway/http/request.rb
CHANGED
|
@@ -25,6 +25,11 @@ module Edgeway
|
|
|
25
25
|
ENV["HTTP_" + name.upcase.gsub("-", "_")] || ""
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# The client address the platform reports, "" when unknown.
|
|
29
|
+
def remote_addr
|
|
30
|
+
ENV["REMOTE_ADDR"] || ""
|
|
31
|
+
end
|
|
32
|
+
|
|
28
33
|
def json?
|
|
29
34
|
@content_type.start_with?("application/json")
|
|
30
35
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require_relative "client"
|
|
2
|
+
|
|
3
|
+
module Edgeway
|
|
4
|
+
module HTTP
|
|
5
|
+
class UnstubbedRequest < Edgeway::Error; end
|
|
6
|
+
|
|
7
|
+
class StubTransport < Transport
|
|
8
|
+
attr_reader :requests
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@answers = {}
|
|
12
|
+
@requests = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def stub(verb, url, status, body, headers)
|
|
16
|
+
lowered = {}
|
|
17
|
+
headers.each { |name, value| lowered[name.to_s.downcase] = value.to_s }
|
|
18
|
+
@answers[key(verb, url)] = JSON.generate({ "status" => status, "headers" => lowered, "body_base64" => [body].pack("m0") })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def stub_error(verb, url, message, timeout)
|
|
22
|
+
answer = { "error" => message }
|
|
23
|
+
answer["timeout"] = true if timeout
|
|
24
|
+
@answers[key(verb, url)] = JSON.generate(answer)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def request(payload)
|
|
28
|
+
sent = JSON.parse(payload)
|
|
29
|
+
@requests << sent
|
|
30
|
+
@answers[key(sent["method"], sent["url"])] || raise(UnstubbedRequest, "no stub for #{sent["method"]} #{sent["url"]} (add stub_http to the test)")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def key(verb, url) = "#{verb.to_s.upcase} #{url}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/edgeway/logger.rb
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
module Edgeway
|
|
2
|
-
# Writes to stderr; the runtime sends it to console.error.
|
|
3
2
|
class Logger
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
LEVELS = ["debug", "info", "warn", "error"]
|
|
4
|
+
|
|
5
|
+
def initialize(output = STDERR)
|
|
6
|
+
@output = output
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def debug(message, fields = {})
|
|
10
|
+
write("debug", message, fields)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def info(message, fields = {})
|
|
14
|
+
write("info", message, fields)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def warn(message, fields = {})
|
|
18
|
+
write("warn", message, fields)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def error(message, fields = {})
|
|
22
|
+
write("error", message, fields)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def write(level, message, fields)
|
|
28
|
+
return if LEVELS.index(level) < threshold
|
|
29
|
+
entry = {}
|
|
30
|
+
fields.each { |name, value| entry[name.to_s] = value }
|
|
31
|
+
entry["level"] = level
|
|
32
|
+
entry["message"] = message
|
|
33
|
+
@output.puts JSON.generate(entry)
|
|
6
34
|
end
|
|
7
35
|
|
|
8
|
-
def
|
|
9
|
-
|
|
36
|
+
def threshold
|
|
37
|
+
LEVELS.index(Edgeway.config("LOG_LEVEL", "info")) || LEVELS.index("info")
|
|
10
38
|
end
|
|
11
39
|
end
|
|
12
40
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require_relative "limiter"
|
|
2
|
+
|
|
3
|
+
module Edgeway
|
|
4
|
+
module RateLimit
|
|
5
|
+
class StubLimiter < Limiter
|
|
6
|
+
attr_reader :checks
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@exceeded_bindings = []
|
|
10
|
+
@checks = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def stub(binding_name, exceeded)
|
|
14
|
+
@exceeded_bindings.delete(binding_name)
|
|
15
|
+
@exceeded_bindings << binding_name if exceeded
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def exceeded?(binding_name, key)
|
|
19
|
+
@checks << [binding_name, key]
|
|
20
|
+
@exceeded_bindings.include?(binding_name)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/edgeway/server.rb
CHANGED
|
@@ -17,8 +17,9 @@ module Edgeway
|
|
|
17
17
|
def self.log_database_stats(name)
|
|
18
18
|
stats = Model.query_stats(name)
|
|
19
19
|
return unless stats.queries > 0
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
fields = stats.log_fields
|
|
21
|
+
fields["database"] = name.to_s unless name == Model::PRIMARY
|
|
22
|
+
Edgeway.logger.info("query stats", fields)
|
|
22
23
|
end
|
|
23
24
|
end
|
|
24
25
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Spinel's, so CRuby can't read it (tests use edgeway/db/memory).
|
|
4
4
|
require "json"
|
|
5
5
|
require_relative "../../db/adapter"
|
|
6
|
+
require_relative "../import_result"
|
|
6
7
|
|
|
7
8
|
module Edgeway
|
|
8
9
|
module Cloudflare
|
|
@@ -34,8 +35,6 @@ module Edgeway
|
|
|
34
35
|
# suspended (JSPI), so they read like blocking calls here.
|
|
35
36
|
module D1Imports
|
|
36
37
|
ffi_source <<~C
|
|
37
|
-
#include <stdlib.h>
|
|
38
|
-
|
|
39
38
|
#define D1_IMPORT(name) __attribute__((import_module("edgeway_d1"), import_name(name)))
|
|
40
39
|
|
|
41
40
|
D1_IMPORT("query") int js_d1_query(const char *binding, const char *sql, const char *binds);
|
|
@@ -44,38 +43,24 @@ module Edgeway
|
|
|
44
43
|
D1_IMPORT("batch") int js_d1_batch(const char *binding, const char *statements);
|
|
45
44
|
D1_IMPORT("take") void js_d1_take(char *buf);
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
static char *result;
|
|
49
|
-
static int result_cap;
|
|
50
|
-
|
|
51
|
-
static const char *take_result(int length) {
|
|
52
|
-
if (length + 1 > result_cap) {
|
|
53
|
-
char *grown = realloc(result, length + 1);
|
|
54
|
-
if (!grown) abort();
|
|
55
|
-
result = grown;
|
|
56
|
-
result_cap = length + 1;
|
|
57
|
-
}
|
|
58
|
-
js_d1_take(result);
|
|
59
|
-
result[length] = 0;
|
|
60
|
-
return result;
|
|
61
|
-
}
|
|
46
|
+
static ImportResultBuffer d1_result;
|
|
62
47
|
|
|
63
48
|
/* What ffi_func binds. Its own extern declaration would lose the
|
|
64
49
|
import attributes, so the imports are wrapped. */
|
|
65
50
|
const char *d1_query(const char *binding, const char *sql, const char *binds) {
|
|
66
|
-
return
|
|
51
|
+
return take_import_result(&d1_result, js_d1_query(binding, sql, binds), js_d1_take);
|
|
67
52
|
}
|
|
68
53
|
|
|
69
54
|
const char *d1_execute(const char *binding, const char *sql, const char *binds) {
|
|
70
|
-
return
|
|
55
|
+
return take_import_result(&d1_result, js_d1_execute(binding, sql, binds), js_d1_take);
|
|
71
56
|
}
|
|
72
57
|
|
|
73
58
|
const char *d1_exec(const char *binding, const char *sql) {
|
|
74
|
-
return
|
|
59
|
+
return take_import_result(&d1_result, js_d1_exec(binding, sql), js_d1_take);
|
|
75
60
|
}
|
|
76
61
|
|
|
77
62
|
const char *d1_batch(const char *binding, const char *statements) {
|
|
78
|
-
return
|
|
63
|
+
return take_import_result(&d1_result, js_d1_batch(binding, statements), js_d1_take);
|
|
79
64
|
}
|
|
80
65
|
C
|
|
81
66
|
ffi_func :d1_query, [:str, :str, :str], :str
|
|
@@ -3,33 +3,21 @@
|
|
|
3
3
|
# (ffi_source is Spinel's; tests use edgeway/http/auth/stub).
|
|
4
4
|
require "json"
|
|
5
5
|
require_relative "../../http/auth/jwt"
|
|
6
|
+
require_relative "../import_result"
|
|
6
7
|
|
|
7
8
|
module Edgeway
|
|
8
9
|
module Cloudflare
|
|
9
10
|
module JWTImports
|
|
10
11
|
ffi_source <<~C
|
|
11
|
-
#include <stdlib.h>
|
|
12
|
-
|
|
13
12
|
#define JWT_IMPORT(name) __attribute__((import_module("edgeway_jwt"), import_name(name)))
|
|
14
13
|
|
|
15
14
|
JWT_IMPORT("verify") int js_jwt_verify(const char *token);
|
|
16
15
|
JWT_IMPORT("take") void js_jwt_take(char *buf);
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
static char *jwt_result;
|
|
20
|
-
static int jwt_result_cap;
|
|
17
|
+
static ImportResultBuffer jwt_result;
|
|
21
18
|
|
|
22
19
|
const char *jwt_verify(const char *token) {
|
|
23
|
-
|
|
24
|
-
if (length + 1 > jwt_result_cap) {
|
|
25
|
-
char *grown = realloc(jwt_result, length + 1);
|
|
26
|
-
if (!grown) abort();
|
|
27
|
-
jwt_result = grown;
|
|
28
|
-
jwt_result_cap = length + 1;
|
|
29
|
-
}
|
|
30
|
-
js_jwt_take(jwt_result);
|
|
31
|
-
jwt_result[length] = 0;
|
|
32
|
-
return jwt_result;
|
|
20
|
+
return take_import_result(&jwt_result, js_jwt_verify(token), js_jwt_take);
|
|
33
21
|
}
|
|
34
22
|
C
|
|
35
23
|
ffi_func :jwt_verify, [:str], :str
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "../import_result"
|
|
3
|
+
require_relative "kv_store"
|
|
4
|
+
|
|
5
|
+
module Edgeway
|
|
6
|
+
module Cloudflare
|
|
7
|
+
module KVImports
|
|
8
|
+
ffi_source <<~C
|
|
9
|
+
#define KV_IMPORT(name) __attribute__((import_module("edgeway_kv"), import_name(name)))
|
|
10
|
+
|
|
11
|
+
KV_IMPORT("get") int js_kv_get(const char *binding, const char *key);
|
|
12
|
+
KV_IMPORT("put") int js_kv_put(const char *binding, const char *key, const char *text, int expires_in);
|
|
13
|
+
KV_IMPORT("delete") int js_kv_delete(const char *binding, const char *key);
|
|
14
|
+
KV_IMPORT("take") void js_kv_take(char *buffer);
|
|
15
|
+
|
|
16
|
+
static ImportResultBuffer kv_result;
|
|
17
|
+
|
|
18
|
+
const char *kv_get(const char *binding, const char *key) {
|
|
19
|
+
return take_import_result(&kv_result, js_kv_get(binding, key), js_kv_take);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const char *kv_put(const char *binding, const char *key, const char *text, int expires_in) {
|
|
23
|
+
return take_import_result(&kv_result, js_kv_put(binding, key, text, expires_in), js_kv_take);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const char *kv_delete(const char *binding, const char *key) {
|
|
27
|
+
return take_import_result(&kv_result, js_kv_delete(binding, key), js_kv_take);
|
|
28
|
+
}
|
|
29
|
+
C
|
|
30
|
+
ffi_func :kv_get, [:str, :str], :str
|
|
31
|
+
ffi_func :kv_put, [:str, :str, :str, :int], :str
|
|
32
|
+
ffi_func :kv_delete, [:str, :str], :str
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class KVImportTransport < KVTransport
|
|
36
|
+
def get(binding_name, key) = KVImports.kv_get(binding_name, key)
|
|
37
|
+
|
|
38
|
+
def put(binding_name, key, text, expires_in_seconds)
|
|
39
|
+
KVImports.kv_put(binding_name, key, text, expires_in_seconds)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def delete(binding_name, key) = KVImports.kv_delete(binding_name, key)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "../../cache/store"
|
|
3
|
+
|
|
4
|
+
module Edgeway
|
|
5
|
+
module Cloudflare
|
|
6
|
+
class KVTransport
|
|
7
|
+
def get(binding_name, key)
|
|
8
|
+
raise Edgeway::Error, "#{self.class.name} does not get"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def put(binding_name, key, text, expires_in_seconds)
|
|
12
|
+
raise Edgeway::Error, "#{self.class.name} does not put"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def delete(binding_name, key)
|
|
16
|
+
raise Edgeway::Error, "#{self.class.name} does not delete"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class KVStore < Cache::Store
|
|
21
|
+
def initialize(binding:, namespace: nil, expires_in: nil, transport:)
|
|
22
|
+
super(namespace: namespace, expires_in: expires_in)
|
|
23
|
+
@binding_name = binding
|
|
24
|
+
@transport = transport
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def read_text(key)
|
|
28
|
+
check_key(key)
|
|
29
|
+
answer = parse_answer("get", @transport.get(@binding_name, key))
|
|
30
|
+
answer.nil? ? nil : answer["value"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def write_text(key, text, expires_in_seconds)
|
|
34
|
+
check_key(key)
|
|
35
|
+
!parse_answer("put", @transport.put(@binding_name, key, text, expires_in_seconds)).nil?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def delete_text(key)
|
|
39
|
+
check_key(key)
|
|
40
|
+
!parse_answer("delete", @transport.delete(@binding_name, key)).nil?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def check_key(key)
|
|
46
|
+
raise ArgumentError, "cache key can't be . or .." if key == "." || key == ".."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_answer(operation, json)
|
|
50
|
+
answer = JSON.parse(json)
|
|
51
|
+
return answer unless answer.key?("error")
|
|
52
|
+
|
|
53
|
+
Edgeway.logger.warn("cache operation failed", { "operation" => operation, "binding" => @binding_name, "error" => answer["error"] })
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "../import_result"
|
|
3
|
+
require_relative "rate_limiter"
|
|
4
|
+
|
|
5
|
+
module Edgeway
|
|
6
|
+
module Cloudflare
|
|
7
|
+
module RateLimitImports
|
|
8
|
+
ffi_source <<~C
|
|
9
|
+
#define RATE_LIMIT_IMPORT(name) __attribute__((import_module("edgeway_ratelimit"), import_name(name)))
|
|
10
|
+
|
|
11
|
+
RATE_LIMIT_IMPORT("check") int js_rate_limit_check(const char *binding, const char *key);
|
|
12
|
+
RATE_LIMIT_IMPORT("take") void js_rate_limit_take(char *buffer);
|
|
13
|
+
|
|
14
|
+
static ImportResultBuffer rate_limit_result;
|
|
15
|
+
|
|
16
|
+
const char *rate_limit_check(const char *binding, const char *key) {
|
|
17
|
+
return take_import_result(&rate_limit_result, js_rate_limit_check(binding, key), js_rate_limit_take);
|
|
18
|
+
}
|
|
19
|
+
C
|
|
20
|
+
ffi_func :rate_limit_check, [:str, :str], :str
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class RateLimitImportTransport < RateLimitTransport
|
|
24
|
+
def check(binding_name, key) = RateLimitImports.rate_limit_check(binding_name, key)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "../../rate_limit"
|
|
3
|
+
|
|
4
|
+
module Edgeway
|
|
5
|
+
module Cloudflare
|
|
6
|
+
class RateLimitTransport
|
|
7
|
+
def check(binding_name, key)
|
|
8
|
+
raise Edgeway::Error, "#{self.class.name} does not check"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class RateLimiter < RateLimit::Limiter
|
|
13
|
+
def initialize(transport:)
|
|
14
|
+
@transport = transport
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def exceeded?(binding_name, key)
|
|
18
|
+
answer = JSON.parse(@transport.check(binding_name, Digest::SHA256.hexdigest(key)))
|
|
19
|
+
raise ConfigurationError, "rate limit: #{answer["misconfigured"]}" if answer.key?("misconfigured")
|
|
20
|
+
return failed_open(binding_name, answer["error"]) if answer.key?("error")
|
|
21
|
+
|
|
22
|
+
answer["success"] == false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def failed_open(binding_name, message)
|
|
28
|
+
Edgeway.logger.warn("rate limit check failed", { "binding" => binding_name, "error" => message })
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require_relative "d1"
|
|
2
|
+
|
|
3
|
+
module Edgeway
|
|
4
|
+
module Cloudflare
|
|
5
|
+
module TestImports
|
|
6
|
+
ffi_source <<~C
|
|
7
|
+
__attribute__((import_module("edgeway_test"), import_name("reset"))) void js_test_reset(void);
|
|
8
|
+
|
|
9
|
+
int test_reset(void) {
|
|
10
|
+
js_test_reset();
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
C
|
|
14
|
+
ffi_func :test_reset, [], :int
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class TestD1Adapter < D1Adapter
|
|
18
|
+
attr_reader :log
|
|
19
|
+
|
|
20
|
+
def initialize(binding: "DB")
|
|
21
|
+
super(binding: binding)
|
|
22
|
+
@log = []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def select_all(sql, binds)
|
|
26
|
+
log_statement(sql, binds)
|
|
27
|
+
super(sql, binds)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def execute(sql, binds)
|
|
31
|
+
log_statement(sql, binds)
|
|
32
|
+
super(sql, binds)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def exec(sql)
|
|
36
|
+
log_statement(sql, [])
|
|
37
|
+
super(sql)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def batch(statements)
|
|
41
|
+
@log << "BATCH #{statements.size}"
|
|
42
|
+
super(statements)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def close
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def log_statement(sql, binds)
|
|
51
|
+
@log << "#{sql} | #{JSON.generate(binds)}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
module TestDatabases
|
|
57
|
+
def self.reset
|
|
58
|
+
Cloudflare::TestImports.test_reset
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.open(name)
|
|
62
|
+
Cloudflare::TestD1Adapter.new(binding: Edgeway::TEST_BINDINGS.fetch(name))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require_relative "../http/client"
|
|
2
|
+
require_relative "import_result"
|
|
3
|
+
|
|
4
|
+
module Edgeway
|
|
5
|
+
module FetchImports
|
|
6
|
+
ffi_source <<~C
|
|
7
|
+
#define FETCH_IMPORT(name) __attribute__((import_module("edgeway_fetch"), import_name(name)))
|
|
8
|
+
|
|
9
|
+
FETCH_IMPORT("request") int js_fetch_request(const char *payload);
|
|
10
|
+
FETCH_IMPORT("take") void js_fetch_take(char *buffer);
|
|
11
|
+
|
|
12
|
+
static ImportResultBuffer fetch_result;
|
|
13
|
+
|
|
14
|
+
const char *fetch_request(const char *payload) {
|
|
15
|
+
return take_import_result(&fetch_result, js_fetch_request(payload), js_fetch_take);
|
|
16
|
+
}
|
|
17
|
+
C
|
|
18
|
+
ffi_func :fetch_request, [:str], :str
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class FetchTransport < HTTP::Transport
|
|
22
|
+
def request(payload)
|
|
23
|
+
FetchImports.fetch_request(payload)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Edgeway
|
|
2
|
+
module ImportResult
|
|
3
|
+
ffi_source <<~C
|
|
4
|
+
#include <stdlib.h>
|
|
5
|
+
|
|
6
|
+
typedef struct {
|
|
7
|
+
char *text;
|
|
8
|
+
int capacity;
|
|
9
|
+
} ImportResultBuffer;
|
|
10
|
+
|
|
11
|
+
static const char *take_import_result(ImportResultBuffer *buffer, int length, void (*take)(char *)) {
|
|
12
|
+
if (length + 1 > buffer->capacity) {
|
|
13
|
+
char *grown = realloc(buffer->text, length + 1);
|
|
14
|
+
if (!grown) abort();
|
|
15
|
+
buffer->text = grown;
|
|
16
|
+
buffer->capacity = length + 1;
|
|
17
|
+
}
|
|
18
|
+
take(buffer->text);
|
|
19
|
+
buffer->text[length] = 0;
|
|
20
|
+
return buffer->text;
|
|
21
|
+
}
|
|
22
|
+
C
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Edgeway
|
|
2
|
+
module TestDatabases
|
|
3
|
+
# Called before each test.
|
|
4
|
+
def self.reset
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def self.open(name)
|
|
8
|
+
migrations = name == Edgeway::Model::PRIMARY ? "db/migrations" : "db/#{name}/migrations"
|
|
9
|
+
directory = File.join(Edgeway::TEST_APP_ROOT, migrations)
|
|
10
|
+
if name != Edgeway::Model::PRIMARY && !File.directory?(directory)
|
|
11
|
+
raise Edgeway::ConfigurationError, "database #{name} (config/database.yml) has no #{migrations} directory"
|
|
12
|
+
end
|
|
13
|
+
database = Edgeway::DB::SQLiteAdapter.new(":memory:")
|
|
14
|
+
database.apply_migrations(directory)
|
|
15
|
+
database
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|