@noita-ts/wininet 0.0.0-stage → 0.1.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.
@@ -0,0 +1,47 @@
1
+ -- AUTO-GENERATED by @noita-ts/nasm. Do not edit.
2
+ local M = {}
3
+ local band, rshift = bit.band, bit.rshift
4
+
5
+ local function add32(bytes, off, value)
6
+ value = value
7
+ + bytes[off + 1]
8
+ + bytes[off + 2] * 0x100
9
+ + bytes[off + 3] * 0x10000
10
+ + bytes[off + 4] * 0x1000000
11
+ bytes[off + 1] = band(value, 0xFF)
12
+ bytes[off + 2] = band(rshift(value, 8), 0xFF)
13
+ bytes[off + 3] = band(rshift(value, 16), 0xFF)
14
+ bytes[off + 4] = band(rshift(value, 24), 0xFF)
15
+ end
16
+
17
+ function M.link(patch, values)
18
+ local vars = patch.vars
19
+ if vars.BASE and not (values and values.BASE) then
20
+ return function(base)
21
+ local withBase = { BASE = base }
22
+ if values then
23
+ for name, value in pairs(values) do
24
+ withBase[name] = value
25
+ end
26
+ end
27
+ return M.link(patch, withBase)
28
+ end
29
+ end
30
+ local raw = patch.raw
31
+ local bytes = { entry = patch.labels.entry or 0 }
32
+ for i = 1, #raw do
33
+ bytes[i] = raw[i]
34
+ end
35
+ for name, offsets in pairs(vars) do
36
+ local value = values and values[name]
37
+ if value == nil then
38
+ error(string.format("asm: no value given for reloc '%s'", name), 2)
39
+ end
40
+ for i = 1, #offsets do
41
+ add32(bytes, offsets[i], value)
42
+ end
43
+ end
44
+ return bytes
45
+ end
46
+
47
+ return M
@@ -0,0 +1,100 @@
1
+ /** @noSelfInFile */
2
+ export declare class HttpResponse {
3
+ status: number;
4
+ headers: string;
5
+ body: string;
6
+ constructor(status: number, headers: string, body: string);
7
+ /**
8
+ * Parse the raw header block into a map.
9
+ * Header names seen more than once collapse into an array of their values, in
10
+ * order.
11
+ */
12
+ parseHeaders(): Record<string, string | string[]>;
13
+ }
14
+ /** The headers of a request: a map, or lines separated by `\n`. */
15
+ export type Headers = Record<string, string | string[]> | string;
16
+ /**
17
+ * Anything that looks, walks and quacks like a scheduler (to not depend on
18
+ * `@noita-ts/base` directly).
19
+ * Import and set up a `Scheduler` from `@noita-ts/base/async` for this.
20
+ */
21
+ interface Scheduler {
22
+ spawn<T>(body: () => T): Promise<T>;
23
+ }
24
+ export interface HttpClientOptions {
25
+ /** The `User-Agent` of the requests. Defaults to `noita-ts/wininet`. */
26
+ userAgent?: string;
27
+ /**
28
+ * The timeout of a connection, of a send and of a receive, in
29
+ * milliseconds. Defaults to 30 seconds.
30
+ */
31
+ timeoutMs?: number;
32
+ }
33
+ /**
34
+ * An async HTTP client built on top of a {@link Scheduler}.
35
+ *
36
+ * Requests run in coroutines and resolve to an {@link HttpResponse}. The
37
+ * blocking WinINet calls run on native threads, so the game runs while a
38
+ * request waits.
39
+ *
40
+ * A client keeps a WinINet session until the Lua state is closed, so make one
41
+ * client and keep it. When the state is closed, the client closes its handles
42
+ * and frees its memory; the thread of a request that still runs frees the
43
+ * memory of its call when the call ends.
44
+ */
45
+ export declare class HttpClient {
46
+ private scheduler;
47
+ private userAgent;
48
+ private timeoutMs;
49
+ private internet;
50
+ /** The open request and connection handles, with their kinds. */
51
+ private handles;
52
+ /** The memory that the threads of the requests use. */
53
+ private pool;
54
+ private released;
55
+ /** Removes the release of this client from the release of the state. */
56
+ private unregister;
57
+ constructor(scheduler: Scheduler, options?: HttpClientOptions);
58
+ /**
59
+ * Perform an HTTP GET.
60
+ */
61
+ get(url: string, headers?: Headers): Promise<HttpResponse>;
62
+ /**
63
+ * Perform an HTTP POST with an optional request `body`.
64
+ *
65
+ * Without `headers`, the request has
66
+ * `content-type: application/x-www-form-urlencoded`, as in pollnet.
67
+ */
68
+ post(url: string, headers?: Headers, body?: string): Promise<HttpResponse>;
69
+ /**
70
+ * Does now what closing the Lua state does: closes the WinINet handles of
71
+ * the client and frees its memory. A request that runs is abandoned: its
72
+ * promise rejects, and its thread frees the memory of its call when the call
73
+ * ends. The client takes no requests after that.
74
+ *
75
+ * Only for tests.
76
+ *
77
+ * @internal
78
+ */
79
+ _release(): void;
80
+ /** Remembers `handle` until `close`, or until the client is released. */
81
+ private track;
82
+ /** Closes a handle of `track`, unless the release closed it already. */
83
+ private close;
84
+ /**
85
+ * The WinINet session of this client, made on first use. It stays open
86
+ * until the client is released, so that WinINet can keep the connections
87
+ * alive.
88
+ */
89
+ private open;
90
+ /**
91
+ * Sets the timeouts of a handle. Wine takes the send and receive timeouts
92
+ * only from a connection handle, and waits forever without them, so each
93
+ * connection gets them too.
94
+ */
95
+ private setTimeouts;
96
+ private request;
97
+ /** Sends the request and reads the response. */
98
+ private exchange;
99
+ }
100
+ export {};
package/dist/index.lua ADDED
@@ -0,0 +1,457 @@
1
+ local ____lualib = require("lualib_bundle")
2
+ local __TS__Class = ____lualib.__TS__Class
3
+ local __TS__StringSlice = ____lualib.__TS__StringSlice
4
+ local __TS__ArrayIsArray = ____lualib.__TS__ArrayIsArray
5
+ local __TS__StringSplit = ____lualib.__TS__StringSplit
6
+ local Error = ____lualib.Error
7
+ local RangeError = ____lualib.RangeError
8
+ local ReferenceError = ____lualib.ReferenceError
9
+ local SyntaxError = ____lualib.SyntaxError
10
+ local TypeError = ____lualib.TypeError
11
+ local URIError = ____lualib.URIError
12
+ local __TS__New = ____lualib.__TS__New
13
+ local __TS__StringStartsWith = ____lualib.__TS__StringStartsWith
14
+ local __TS__ObjectKeys = ____lualib.__TS__ObjectKeys
15
+ local __TS__ArraySort = ____lualib.__TS__ArraySort
16
+ local __TS__ArrayFilter = ____lualib.__TS__ArrayFilter
17
+ local __TS__ArrayMap = ____lualib.__TS__ArrayMap
18
+ local __TS__ArraySlice = ____lualib.__TS__ArraySlice
19
+ local ____exports = {}
20
+ local ____ffi = require("@noita-ts/ffi")
21
+ local ffi = ____ffi.default
22
+ local ____thread = require("thread")
23
+ local Pool = ____thread.Pool
24
+ ffi.cdef("\n void* InternetOpenA(const char* agent, uint32_t access, const char* proxy, const char* bypass, uint32_t flags);\n void* InternetConnectA(void* internet, const char* server, uint16_t port, const char* user, const char* password, uint32_t service, uint32_t flags, uintptr_t context);\n void* HttpOpenRequestA(void* connect, const char* verb, const char* object, const char* version, const char* referrer, const char** accept, uint32_t flags, uintptr_t context);\n int HttpSendRequestA(void* request, const char* headers, uint32_t headers_length, void* optional, uint32_t optional_length);\n int InternetSetOptionA(void* internet, uint32_t option, void* buffer, uint32_t length);\n int InternetReadFile(void* file, void* buffer, uint32_t size, uint32_t* read);\n int HttpQueryInfoA(void* request, uint32_t info, void* buffer, uint32_t* length, uint32_t* index);\n int InternetCloseHandle(void* internet);\n uint32_t GetLastError();\n")
25
+ local wininet = ffi.load("wininet.dll")
26
+ local INTERNET_OPEN_TYPE_PRECONFIG = 0
27
+ local INTERNET_SERVICE_HTTP = 3
28
+ local INTERNET_OPTION_CONNECT_TIMEOUT = 2
29
+ local INTERNET_OPTION_SEND_TIMEOUT = 5
30
+ local INTERNET_OPTION_RECEIVE_TIMEOUT = 6
31
+ local INTERNET_FLAG_RELOAD = 2147483648
32
+ local INTERNET_FLAG_NO_CACHE_WRITE = 67108864
33
+ local INTERNET_FLAG_SECURE = 8388608
34
+ local INTERNET_FLAG_NO_COOKIES = 524288
35
+ local INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP = 32768
36
+ local INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS = 16384
37
+ local INTERNET_FLAG_NO_UI = 512
38
+ local HTTP_QUERY_STATUS_CODE = 19
39
+ local HTTP_QUERY_RAW_HEADERS_CRLF = 22
40
+ local HTTP_QUERY_FLAG_NUMBER = 536870912
41
+ --- The size of one read of the body.
42
+ local CHUNK = 256 * 1024
43
+ --- Names of the common WinINet errors, for the error messages.
44
+ local ERRORS = {
45
+ [12002] = "timeout",
46
+ [12005] = "invalid URL",
47
+ [12006] = "unrecognized scheme",
48
+ [12007] = "name not resolved",
49
+ [12029] = "cannot connect",
50
+ [12030] = "connection aborted",
51
+ [12031] = "connection reset",
52
+ [12037] = "certificate date invalid",
53
+ [12038] = "certificate name invalid",
54
+ [12045] = "invalid certificate authority",
55
+ [12057] = "certificate revocation check failed",
56
+ [12152] = "invalid server response",
57
+ [12157] = "security channel error",
58
+ [12169] = "invalid certificate",
59
+ [12170] = "certificate revoked",
60
+ [12175] = "decoding failed"
61
+ }
62
+ local function describe(____error)
63
+ local name = ERRORS[____error]
64
+ return name ~= nil and ((("WinINet error " .. tostring(____error)) .. " (") .. name) .. ")" or "WinINet error " .. tostring(____error)
65
+ end
66
+ ____exports.HttpResponse = __TS__Class()
67
+ local HttpResponse = ____exports.HttpResponse
68
+ HttpResponse.name = "HttpResponse"
69
+ function HttpResponse.prototype.____constructor(self, status, headers, body)
70
+ self.status = status
71
+ self.headers = headers
72
+ self.body = body
73
+ end
74
+ function HttpResponse.prototype.parseHeaders(self)
75
+ local headers = {}
76
+ for ____, line in ipairs(__TS__StringSplit(self.headers, "\n")) do
77
+ do
78
+ local sep = (string.find(line, ": ", nil, true) or 0) - 1
79
+ local key = __TS__StringSlice(line, 0, sep)
80
+ local value = __TS__StringSlice(line, sep + 2)
81
+ if sep <= 0 or not value then
82
+ goto __continue5
83
+ end
84
+ local prev = headers[key]
85
+ if not prev then
86
+ headers[key] = value
87
+ goto __continue5
88
+ end
89
+ if __TS__ArrayIsArray(prev) then
90
+ prev[#prev + 1] = value
91
+ else
92
+ headers[key] = {prev, value}
93
+ end
94
+ end
95
+ ::__continue5::
96
+ end
97
+ return headers
98
+ end
99
+ --- A URL, split into the parts that WinINet takes.
100
+ local function parseUrl(url)
101
+ local scheme, host, port, rest = string.match(url, "^(%a[%w+.-]*)://([^/:?#]+):?(%d*)(.*)$")
102
+ local ____opt_0 = scheme
103
+ local lower = ____opt_0 and string.lower(scheme)
104
+ if lower ~= "http" and lower ~= "https" then
105
+ error(
106
+ __TS__New(Error, "not an http or https URL: " .. url),
107
+ 0
108
+ )
109
+ end
110
+ local secure = lower == "https"
111
+ local path = (string.gsub(rest, "#.*$", ""))
112
+ if not __TS__StringStartsWith(path, "/") then
113
+ path = "/" .. path
114
+ end
115
+ return {
116
+ secure = secure,
117
+ host = host,
118
+ port = port ~= "" and tonumber(port) or (secure and 443 or 80),
119
+ path = path
120
+ }
121
+ end
122
+ --- Headers as WinINet takes them: `Key: value` lines, each ended by CRLF.
123
+ local function formatHeaders(headers)
124
+ local lines = {}
125
+ if type(headers) == "string" then
126
+ for ____, line in ipairs(__TS__StringSplit(headers, "\n")) do
127
+ lines[#lines + 1] = (string.gsub(line, "\r$", ""))
128
+ end
129
+ else
130
+ local keys = __TS__ObjectKeys(headers)
131
+ __TS__ArraySort(keys)
132
+ for ____, key in ipairs(keys) do
133
+ local value = headers[key]
134
+ for ____, v in ipairs(type(value) == "string" and ({value}) or value) do
135
+ lines[#lines + 1] = (key .. ": ") .. v
136
+ end
137
+ end
138
+ end
139
+ return table.concat(
140
+ __TS__ArrayMap(
141
+ __TS__ArrayFilter(
142
+ lines,
143
+ function(____, line) return line ~= "" end
144
+ ),
145
+ function(____, line) return line .. "\r\n" end
146
+ ),
147
+ ""
148
+ )
149
+ end
150
+ --- The response headers as `HttpResponse` of pollnet has them: `Key: value`
151
+ -- lines separated by `\n`, without the status line.
152
+ local function responseHeaders(raw)
153
+ local lines = __TS__StringSplit((string.gsub(raw, "\r\n", "\n")), "\n")
154
+ return table.concat(
155
+ __TS__ArrayFilter(
156
+ __TS__ArraySlice(lines, 1),
157
+ function(____, line) return line ~= "" end
158
+ ),
159
+ "\n"
160
+ )
161
+ end
162
+ --- The kinds of the WinINet handles of a request, children first.
163
+ local REQUEST = 1
164
+ local CONNECT = 2
165
+ --- An async HTTP client built on top of a {@link Scheduler}.
166
+ --
167
+ -- Requests run in coroutines and resolve to an {@link HttpResponse}. The
168
+ -- blocking WinINet calls run on native threads, so the game runs while a
169
+ -- request waits.
170
+ --
171
+ -- A client keeps a WinINet session until the Lua state is closed, so make one
172
+ -- client and keep it. When the state is closed, the client closes its handles
173
+ -- and frees its memory; the thread of a request that still runs frees the
174
+ -- memory of its call when the call ends.
175
+ ____exports.HttpClient = __TS__Class()
176
+ local HttpClient = ____exports.HttpClient
177
+ HttpClient.name = "HttpClient"
178
+ function HttpClient.prototype.____constructor(self, scheduler, options)
179
+ self.handles = {}
180
+ self.pool = __TS__New(Pool)
181
+ self.released = false
182
+ self.scheduler = scheduler
183
+ self.userAgent = options and options.userAgent or "noita-ts/wininet"
184
+ self.timeoutMs = options and options.timeoutMs or 30000
185
+ self.unregister = ffi.onRelease(function() return self:_release() end)
186
+ end
187
+ function HttpClient.prototype.get(self, url, headers)
188
+ return self:request("GET", url, headers or "", "")
189
+ end
190
+ function HttpClient.prototype.post(self, url, headers, body)
191
+ return self:request("POST", url, headers or ({["content-type"] = "application/x-www-form-urlencoded"}), body or "")
192
+ end
193
+ function HttpClient.prototype._release(self)
194
+ if self.released then
195
+ return
196
+ end
197
+ self.released = true
198
+ self.unregister()
199
+ for ____, kind in ipairs({REQUEST, CONNECT}) do
200
+ for handle, k in pairs(self.handles) do
201
+ if k == kind then
202
+ wininet.InternetCloseHandle(handle)
203
+ end
204
+ end
205
+ end
206
+ self.handles = {}
207
+ if self.internet ~= nil then
208
+ wininet.InternetCloseHandle(self.internet)
209
+ self.internet = nil
210
+ end
211
+ self.pool:release()
212
+ end
213
+ function HttpClient.prototype.track(self, handle, kind)
214
+ if handle ~= nil then
215
+ self.handles[handle] = kind
216
+ end
217
+ return handle
218
+ end
219
+ function HttpClient.prototype.close(self, handle)
220
+ if self.handles[handle] ~= nil then
221
+ self.handles[handle] = nil
222
+ wininet.InternetCloseHandle(handle)
223
+ end
224
+ end
225
+ function HttpClient.prototype.open(self)
226
+ if self.released then
227
+ error(
228
+ __TS__New(Error, "the client is released"),
229
+ 0
230
+ )
231
+ end
232
+ if self.internet == nil then
233
+ local internet = wininet.InternetOpenA(
234
+ self.userAgent,
235
+ INTERNET_OPEN_TYPE_PRECONFIG,
236
+ nil,
237
+ nil,
238
+ 0
239
+ )
240
+ if internet == nil then
241
+ error(
242
+ __TS__New(
243
+ Error,
244
+ "cannot open WinINet: " .. describe(ffi.C.GetLastError())
245
+ ),
246
+ 0
247
+ )
248
+ end
249
+ self:setTimeouts(internet)
250
+ self.internet = internet
251
+ end
252
+ return self.internet
253
+ end
254
+ function HttpClient.prototype.setTimeouts(self, handle)
255
+ local timeout = ffi.new("uint32_t[1]", self.timeoutMs)
256
+ for ____, option in ipairs({INTERNET_OPTION_CONNECT_TIMEOUT, INTERNET_OPTION_SEND_TIMEOUT, INTERNET_OPTION_RECEIVE_TIMEOUT}) do
257
+ wininet.InternetSetOptionA(handle, option, timeout, 4)
258
+ end
259
+ end
260
+ function HttpClient.prototype.request(self, method, url, headers, body)
261
+ return self.scheduler:spawn(function()
262
+ local ____parseUrl_result_6 = parseUrl(url)
263
+ local secure = ____parseUrl_result_6.secure
264
+ local host = ____parseUrl_result_6.host
265
+ local port = ____parseUrl_result_6.port
266
+ local path = ____parseUrl_result_6.path
267
+ local function fail(what, ____error)
268
+ error(
269
+ __TS__New(
270
+ Error,
271
+ (((((method .. " ") .. url) .. ": ") .. what) .. ": ") .. describe(____error)
272
+ ),
273
+ 0
274
+ )
275
+ end
276
+ local connect = self:track(
277
+ wininet.InternetConnectA(
278
+ self:open(),
279
+ host,
280
+ port,
281
+ nil,
282
+ nil,
283
+ INTERNET_SERVICE_HTTP,
284
+ 0,
285
+ 0
286
+ ),
287
+ CONNECT
288
+ )
289
+ if connect == nil then
290
+ fail(
291
+ "cannot connect",
292
+ ffi.C.GetLastError()
293
+ )
294
+ end
295
+ do
296
+ local ____try, ____hasReturned, ____returnValue = pcall(function()
297
+ self:setTimeouts(connect)
298
+ local flags = bit.bor(
299
+ bit.bor(
300
+ bit.bor(
301
+ bit.bor(
302
+ bit.bor(INTERNET_FLAG_RELOAD, INTERNET_FLAG_NO_CACHE_WRITE),
303
+ INTERNET_FLAG_NO_COOKIES
304
+ ),
305
+ INTERNET_FLAG_NO_UI
306
+ ),
307
+ INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP
308
+ ),
309
+ INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS
310
+ )
311
+ if secure then
312
+ flags = bit.bor(flags, INTERNET_FLAG_SECURE)
313
+ end
314
+ local request = self:track(
315
+ wininet.HttpOpenRequestA(
316
+ connect,
317
+ method,
318
+ path,
319
+ nil,
320
+ nil,
321
+ nil,
322
+ flags,
323
+ 0
324
+ ),
325
+ REQUEST
326
+ )
327
+ if request == nil then
328
+ fail(
329
+ "cannot open the request",
330
+ ffi.C.GetLastError()
331
+ )
332
+ end
333
+ do
334
+ local ____try, ____hasReturned, ____returnValue = pcall(function()
335
+ return true, self:exchange(request, headers, body, fail)
336
+ end)
337
+ do
338
+ self:close(request)
339
+ end
340
+ if not ____try then
341
+ error(____hasReturned, 0)
342
+ end
343
+ if ____try and ____hasReturned then
344
+ return true, ____returnValue
345
+ end
346
+ end
347
+ end)
348
+ do
349
+ self:close(connect)
350
+ end
351
+ if not ____try then
352
+ error(____hasReturned, 0)
353
+ end
354
+ if ____try and ____hasReturned then
355
+ return ____returnValue
356
+ end
357
+ end
358
+ end)
359
+ end
360
+ function HttpClient.prototype.exchange(self, request, headers, body, fail)
361
+ local formatted = formatHeaders(headers)
362
+ local sent, sendError = unpack(
363
+ self.pool:run(
364
+ wininet.HttpSendRequestA,
365
+ request,
366
+ formatted ~= "" and formatted or 0,
367
+ #formatted,
368
+ body ~= "" and body or 0,
369
+ #body
370
+ ),
371
+ 1,
372
+ 2
373
+ )
374
+ if sent == 0 then
375
+ fail("cannot send the request", sendError)
376
+ end
377
+ local status = ffi.new("uint32_t[1]")
378
+ local length = ffi.new("uint32_t[1]", 4)
379
+ if wininet.HttpQueryInfoA(
380
+ request,
381
+ bit.bor(HTTP_QUERY_STATUS_CODE, HTTP_QUERY_FLAG_NUMBER),
382
+ status,
383
+ length,
384
+ nil
385
+ ) == 0 then
386
+ fail(
387
+ "no status in the response",
388
+ ffi.C.GetLastError()
389
+ )
390
+ end
391
+ length[0] = 0
392
+ wininet.HttpQueryInfoA(
393
+ request,
394
+ HTTP_QUERY_RAW_HEADERS_CRLF,
395
+ nil,
396
+ length,
397
+ nil
398
+ )
399
+ local raw = ""
400
+ if length[0] > 0 then
401
+ local buffer = ffi.new("char[?]", length[0] + 1)
402
+ if wininet.HttpQueryInfoA(
403
+ request,
404
+ HTTP_QUERY_RAW_HEADERS_CRLF,
405
+ buffer,
406
+ length,
407
+ nil
408
+ ) == 0 then
409
+ fail(
410
+ "cannot read the response headers",
411
+ ffi.C.GetLastError()
412
+ )
413
+ end
414
+ raw = ffi.string(buffer, length[0])
415
+ end
416
+ local buffer = self.pool:alloc("uint8_t*", CHUNK)
417
+ local read = self.pool:alloc("uint32_t*", 4)
418
+ local parts = {}
419
+ do
420
+ local ____try, ____error = pcall(function()
421
+ while true do
422
+ local ok, ____error = unpack(
423
+ self.pool:run(
424
+ wininet.InternetReadFile,
425
+ request,
426
+ buffer,
427
+ CHUNK,
428
+ read
429
+ ),
430
+ 1,
431
+ 2
432
+ )
433
+ if ok == 0 then
434
+ fail("cannot read the response", ____error)
435
+ end
436
+ if read[0] == 0 then
437
+ break
438
+ end
439
+ parts[#parts + 1] = ffi.string(buffer, read[0])
440
+ end
441
+ end)
442
+ do
443
+ self.pool:free(buffer)
444
+ self.pool:free(read)
445
+ end
446
+ if not ____try then
447
+ error(____error, 0)
448
+ end
449
+ end
450
+ return __TS__New(
451
+ ____exports.HttpResponse,
452
+ status[0],
453
+ responseHeaders(raw),
454
+ table.concat(parts, "")
455
+ )
456
+ end
457
+ return ____exports