os.socket API proposal
This PR aim to start a discussion about the future socket API.
My goal is use cosmo+QuickJS/HTTP server to offer a portable cross-platform GUI.
I took inspiration from other std syscall wrapper to do this proposal
fd = std.socket(domain=AF_INET, type=SOCK_STREAM, protocol=0)err = std.bind(sock, {addr:XXX,port:XXX})err = std.listen(sock)fd = std.accept(sock)err = std.connect(sock, {addr:XXX,port:XXX})
I'm confident about the return value API, but I'm open to feedback about:
- allowing implicit args (
socket()currently return a TCP fd,listen()default to a backlog of 10) - shall we use a specific
SockAddr()constructor (with .parse() and .toString()) instead of the current plain{address:number,port:number}object used inbind()andconnect() - are socket more
osrelated thanstd - remove the
SO_REUSEADDRI added, and add a setsockopt wrapper (but just for this usecase ?)
Once agreed, I'll add required unit tests/docs ...
I am interested by the feature. Here are some suggestions:
- the functions should be in "os" instead of "std"
- no implicit args for socket()
- implicit arg for listen is acceptable
- use a plain object for sockaddr. The "family" field is optional (AF_INET by default). "address" should be renamed to "addr" and should be a string containing the IPv4 or IPv6 address. AF_INET6 should be supported too
- remove SO_REUSEADDR and add setsockopt() and getsockopt()
- For better portability it is useful to add recv, recvfrom, send, sendto and shutdown
- In your example, it is better to avoid using fdopen() on sockets ('FILE *' are designed to work on files)
- a wrapper to getaddrinfo() would be needed (take a hostname as input, returns a sockaddr object)
- for win32 compilation without cosmolibc some specific support might be needed if you want os.write/os.read/os.close to work with socket handles
Okay here is an update.
- socket are now part of
os socket()now use 2 args (family, type)backlog=10is kept as optional arg for listen- sockaddr now use
{ family: number, port: number, addr: string | null } - setsockopt + getsockopt now exist, see example/http_server.js
os.setsockopt(sock_srv, os.SO_REUSEADDR, new Uint32Array([1]).buffer)
- added following functions
ret = send (sockfd, buf, len?)
ret = sendto (sockfd, buf, len,saddr)
ret = recv (sockfd, buf, len?)
[ret,saddr] = recvfrom(sockfd, buf, len?)
- http_sever.js now only use
os.*api (no more std.fdopen). But I've seen cases on other project where fscanf/fprintf where used on socket (e.g. for an IRC client) So out of curiosity, would this usage fail on a non-UNIX kernel ? getaddrinfo(host,port)now exist, but an additionalhintargs might be needed since current implementation return too much results (See: example/http_client.js that need a .filter() to work)- I'll try on windows once I get back on my enterprise machine
In addition to the given http_server example, what client demo would be nice ? I've made an HTTP client, and could add a websocket server (to use it as REPL) but I could replace with an NTP or DNS or IRC client
Having a socket API for quickjs is great, however it won't be much useful (for me at least) if only blocking functions are available. I want to run other code in my app between accept() or connect() or read() and write() calls. The most useful socket api would be promised-based (accept/connect then read then write then close...) However, if you want to keep the socket api low-level, then it would be great to provide the options to make the sockets non-blocking:
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
And a wrapper for the select() function. With both of theses, it would be possible to design an asynchronous API on top of this one. Thanks
if you want to keep the socket api low-level
For now yes, because all os IO function are made that way (synchronous).
Note that existing os.setReadHandler(), os.setWriteHandler() offer an callback API for send/recv.
Adding *Async variant to all os IO function could be done in a separate PR.
then it would be great to provide the options to make the sockets non-blocking:
fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK);
agree, we could either go for:
- a plain API: add
fcntl()+EWOULDBLOCK/EAGAINsupport - a linux-ish API : allow
SOCK_NONBLOCKflag at socket() creation which will do thefcntl()under the hood to work on all OS.
I have a preference for the last one but I'll let @bellard have the last word.
I'll also add a poll() wrapper
poll({fd:number,events:number}[], timeoutMs=-1): number[] // returned events masks
SOCK_NONBLOCK http_client example
#!/usr/bin/env qjs
///@ts-check
/// <reference path="../doc/globals.d.ts" />
/// <reference path="../doc/os.d.ts" />
/// <reference path="../doc/std.d.ts" />
import * as os from "os";
import * as std from "std";
/** @template T @param {os.Result<T>} result @returns {T} */
function must(result) {
if (typeof result === "number" && result < 0) throw result;
return /** @type {T} */ (result)
}
/** @param {os.FileDescriptor} fd @param {string[]} lines */
function sendLines(fd, lines) {
const buf = Uint8Array.from(lines.join('\r\n'), c => c.charCodeAt(0));
const written = os.send(fd, buf.buffer, buf.byteLength);
if (written != buf.byteLength) throw `send:${written} : ${std.strerror(-written)}`;
}
const [host = "example.com", port = "80"] = scriptArgs.slice(1);
const ai = os.getaddrinfo(host, port).filter(ai => ai.family == os.AF_INET && ai.port); // TODO too much/invalid result
if (!ai.length) throw `Unable to getaddrinfo(${host}, ${port})`;
const sockfd = must(os.socket(os.AF_INET, os.SOCK_STREAM | os.SOCK_NONBLOCK));
must(os.connect(sockfd, ai[0]) == -std.Error.EINPROGRESS);
must(os.poll([{ fd: sockfd, events: os.POLLOUT }])?.[0] == os.POLLOUT);
sendLines(sockfd, ["GET / HTTP/1.0", `Host: ${host}`, "Connection: close", "", ""]);
const chunk = new Uint8Array(4096);
while (os.poll([{ fd: sockfd, events: os.POLLIN }])?.[0] == os.POLLIN) {
const got = os.recv(sockfd, chunk.buffer, chunk.byteLength);
if (got <= 0) break;
console.log(String.fromCharCode(...chunk));
}
os.close(sockfd);
if you want to keep the socket api low-level
For now yes, because all
osIO function are made that way (synchronous).
Kind of true, but filesystem functions are perfectly usable in a synchronous way, whereas a socket API with only synchronous functions is little more than a toy API.
Also look at os.sleepAsync()
Note that existing
os.setReadHandler(),os.setWriteHandler()offer an callback API for send/recv.
Oh, sorry, missed that. Well, then I think it covers all my use cases (together with your proposed SOCK_NONBLOCK).
If you can also use os.setReadHandler() after listen() and os.setWriteHandler() after a connect() call, then I think everything is covered. Have you tested that?
Adding *Async variant to all
osIO function could be done in a separate PR.
then it would be great to provide the options to make the sockets non-blocking:
fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK);agree, we could either go for:
- a plain API: add
fcntl()+EWOULDBLOCK/EAGAINsupport- a linux-ish API : allow
SOCK_NONBLOCKflag at socket() creation which will do thefcntl()under the hood to work on all OS.I have a preference for the last one but I'll let @bellard have the last word.
I also like SOCK_NONBLOCK fwiw. It seems supported by all the BSDs as well as Linux.
I'll also add a
poll()wrapper
I don't think it's the right approach, because once again your poll() function will be synchronous and block other events. It needs to be integrated in the main quicks_libc.c select() loop.
But in any case, this is moot because if SOCK_NONBLOCK can be set to a socket FD and os.setReadHandler() and os.setWriteHandler() can be attached to sockets FDs, then I believe everything can be built from these primitives.
A connectAsync() that returns a Promise like sleepAsync() would certainly be nice, but as you said, that can be added later. Same for acceptAsync() BTW.
If you can also use os.setReadHandler() after listen() and os.setWriteHandler() after a connect() call, then I think everything is covered. Have you tested that?
I tested it (both on sync/async socket), It kinda works, but for some reason it also call the handler when there is nothing to recv/send.
[...] poll() function will be synchronous and block other events. It needs to be integrated in the main quicks_libc.c select() loop.
Yes, we either
- move all socket API to async (I've looked at os.sleepAsync(), I understood the gist of it event loop registration but i'm still unsure about how to translate it to sockets)
- just move the poll() one to async.
If you can also use os.setReadHandler() after listen() and os.setWriteHandler() after a connect() call, then I think everything is covered. Have you tested that?
I tested it (both on sync/async socket), It kinda works, but for some reason it also call the handler when there is nothing to recv/send.
If the handler is called when there is "nothing to recv", then I don't understand, unless the socket is in error (if I remember correctly, a socket in error or has reached EOF will be reported as "readable" because a read() call will not block).
However, I don't know what you mean when you say the handler is called when there is nothing to send: a write handler should be called when there is room in the socket buffer for a write, in other words when a write() call will not block. so after a successful connect, it is normal that the write handler is called.
In the case of an async socket connection, the connect() call will return EINPROGRESS and operate in the background, then you can register the FD for select(write), and the write handler will be notified when the connection is done (or has failed) which is the time a write call to the socket will succeeds.
[...] poll() function will be synchronous and block other events. It needs to be integrated in the main quicks_libc.c select() loop.
Yes, we either
- move all socket API to async (I've looked at os.sleepAsync(), I understood the gist of it event loop registration but i'm still unsure about how to translate it to sockets)
It probably translate differently for the different functions, but the easiest and most important to start with are probably connect and accept. For connect, I guess we want to be able to write something like:
await os.connectAsync(sockfd, address)
To do that, you can follow the general sequence described in the first answer here:
https://stackoverflow.com/questions/17769964/linux-sockets-non-blocking-connect
So:
- make sure the socket is SOCK_NONBLOCK (or put it in that state temporarily with fcntl())
- call connect(fd, address), then there is 3 possible return values: 2.1) if 0 (which means the connect is already successful likely because it was done on the localhost) then call the promise success handler. 2.2) if value is an error other then EINPROGRESS then call the promise error handler, the connect() call has already failed. 2.3) if it is EINPROGRESS, it means the connection is in progress (and address is not in localhost). In this case, you need to register the socket for write (like os.setWriteHandler) on the quickjs event loop.
- when the quickjs event loop reports that the socket is signaled for write, call getsockopt(fd, SOL_SOCKET, SO_ERROR, ...) which will tell you if the connection succeeded or not. you can then call the promise success or error handler.
Does that make sense?
Similarly, I guess we want to be able to write something like
newSocket = await os.acceptAsync(sockfd);
or
os.acceptAsync(sockfd).then((newSocket) => {
// do something with newSocket
})
So we need something like:
- make sure the socket is SOCK_NONBLOCK (or put it in that state temporarily with fcntl())
- call accept(fd, address), then there is 3 possible return values: 2.1) if 0 or positive, a new socket is already available and the promise can be successfully resolved. 2.2) if value is an error other then EWOULDBLOCK/EAGAIN then call the promise error handler, the accept() call has already failed. 2.3) if it is EWOULDBLOCK/EAGAIN, it means we need to wait for a new connection to be accepted. In this case, you need to register the listening socket for read (like os.setReadHandler) on the quickjs event loop. when the quickjs event loop reports that the socket is signaled for read, call again accept() and call the promise success or error handler based on accept() return value.
Finally, for the send/recv calls, I think the os.setReadHandler and os.setWriteHandler functionality is enough for real world applications. Or maybe sendAsync() and recvAsync() could be added exactly like accept(): first trying a normal send / recv with the socket in NONBLOCK mode, and repeat in the quickjs event loop as long as EWOULDBLOCK/EAGAIN is received.
In any case I think a good high level test that the API is good enough would be to rewrite your two nice http client + server examples in such a way that they work both together in the same quickjs instance (start the server on localhost 8080, then connect to the same port with the client) :)
I will stop here but if you want I could try to write one of these functions.
Thanks you.
- I meant
it also call the handler when there is nothing to recv.=> indeed because the fd was closed. So a correct handler would need to unregister itself:
const chunk = new Uint8Array(16);
os.setReadHandler(sockfd, ()=> {
const ret = os.recv(sockfd, chunk.buffer);
if(!ret) os.setReadHandler(sockfd, null);
else console.log(String.fromCharCode(...chunk.slice(0, ret)));
})
All your JS async connect/listen/accept suggestions makes sens, but I'm just unsure of the intricacies with the event loop. For example what will happen with
acceptAsync(); // no await
await acceptAsync()
- The first
acceptAsyncwill do it job in background - before reaching the event loop select() we call acceptAsync() again
- event loop select() is called to wait for acceptAsync() completion => (it accept both ?)
- event loop select() is called to wait for acceptAsync() completion => will wait forever since all has been settled already
I gave you access to my branch if you want to push an eventloop-based accept/listen/connect prototype
Since my initial goal was to create an light and portable GUI-based JS webapp (cilent+server) I'll
- wait for feedback on the "sync" part of this socket API
- continue to play with it possibilities (e.g: using Worker, or
while(poll([fd],1))await os.sleepAsync()promise wrapper )
All your JS async
connect/listen/acceptsuggestions makes sens, but I'm just unsure of the intricacies with the event loop. For example what will happen withacceptAsync(); // no await await acceptAsync()
- The first
acceptAsyncwill do it job in background- before reaching the event loop select() we call acceptAsync() again
- event loop select() is called to wait for acceptAsync() completion => (it accept both ?)
- event loop select() is called to wait for acceptAsync() completion => will wait forever since all has been settled already
Good questions.
I gave you access to my branch if you want to push an eventloop-based accept/listen/connect prototype
Ok, thank you, I will try to see if I can do something. I hope I won't mess anything with the branch because I'm not very good with git...
Here some remarks:
- js_os_poll() should not be renamed.
- I don't think that adding poll() is relevant to this patch. setReadHandler() and setWriteHandler() should suffice.
- async functions can be implemented in Javascript with setReadHandler() and set setWriteHandler(). If not, then there is a problem in the API. You can provide examples in your HTTP client and server.
- If JS_IsException() returns TRUE, then the calling function must return an exception. This is not the case e.g. in JS_toSockaddrStruct. Moreover, all return values of JS API must be tested and handled.
- using SOCK_NONBLOCK in second argument of socket() is a good idea to set the O_NONBLOCK flag. Another possibility is to open the socket in non blocking mode by default and to add a SOCK_BLOCK flag to make it non blocking.
- only os.send() and os.recv() seem necessary with optional flags and sockaddr parameters.
- implementing IPv6 is necessary in order to have an example of another sockaddr type.
Here some remarks:
- async functions can be implemented in Javascript with setReadHandler() and set setWriteHandler(). If not, then there is a problem in the API. You can provide examples in your HTTP client and server.
@yne: if you want, after you provide the nonblocking sockets, I can write the async wrappers in Javascript, let me know.
- poll() API removed
- reverted js_os_poll to be the event loop pooling
- more JS_*() catched (sorry I'll do a global pass at the end)
- IPv6 shall now work thanks to
struct sockaddr_storage - socket is now non blocking mode by default, unless SOCK_BLOCK flag is given
- Fixed os.send() and os.recv() optional parameters
- async functions can be implemented in Javascript with setReadHandler()/setWriteHandler().
I'm now more confident with the internal poll loop of quickjs so I would prefer to offer native async socket API than forcing everybody to wrap with setReadHandler / setWriteHandler.
I'm planning on offering this API (os.connect is now async):
bind (sockfd: FileDescriptor, addr: SocketAddr ): Result<Success>;
listen (sockfd: FileDescriptor, backlog?: number ): Result<Success>;
shutdown(sockfd: FileDescriptor, type: SocketShutOpt): Result<Success>;
connect (sockfd: FileDescriptor, addr: SocketAddr ): Promise<Result<Success>>;
accept (sockfd: FileDescriptor ): Promise<[remotefd: FileDescriptor, remoteaddr: SocketAddr]>;
recv (sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number ): Promise<Result<number>>;
send (sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number ): Promise<Result<number>>;
recvfrom(sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number ): Promise<[total: Result<number>, from: SocketAddr]>;
sendto (sockfd: FileDescriptor, buffer: ArrayBuffer, length : number, addr: SocketAddr): Promise<Result<number>>;
//with type Result<T> = T | NegativeErrno;
The changes to provide a native async API (as I plan it):
- js_os_connect/accept/recv/send shall:
- if magic==connect: call connect() as it's the only one that need initiation call
- list_add_tail(os_sock_handlers) with magic, and sockfd put as readfds for accept/recv and writefds for connect/send
- return the JS_NewPromiseCapability to JS-land
- js_os_poll shall:
- include
os_sock_handlers'ssockfdin it select()readfds/writefds - if FD_ISSET(readfds|writefds) on a socket: handle_socket_message according to it magic:
- connect: if getsockopt() is SO_ERROR => reject(), else resolve()
- accept/recv/send: if accept/recv/send() is EAGAIN or EWOULDBLOCK => continue polling; else reject() if <0 else resolve()
- include
NB: each reject()/resolve() also list_del()
@ceedriic non-blocking socket (via O_NONBLOCK flag) are available since https://github.com/bellard/quickjs/commit/c0877189964447064df614aebffeb3105b747f30 if you want to try
bind (sockfd: FileDescriptor, addr: SocketAddr ): Result<Success>; listen (sockfd: FileDescriptor, backlog?: number ): Result<Success>; shutdown(sockfd: FileDescriptor, type: SocketShutOpt): Result<Success>; connect (sockfd: FileDescriptor, addr: SocketAddr ): Promise<Result<Success>>; accept (sockfd: FileDescriptor ): Promise<[remotefd: FileDescriptor, remoteaddr: SocketAddr]>; recv (sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number ): Promise<Result<number>>; send (sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number ): Promise<Result<number>>; recvfrom(sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number ): Promise<[total: Result<number>, from: SocketAddr]>; sendto (sockfd: FileDescriptor, buffer: ArrayBuffer, length : number, addr: SocketAddr): Promise<Result<number>>; //with type Result<T> = T | NegativeErrno;
As far as I'm concerned, an API like that would be fantastic.
* connect: if getsockopt() is SO_ERROR => reject(), else resolve() * accept/recv/send: if accept/recv/send() is EAGAIN or EWOULDBLOCK => continue polling; else reject() if <0 else resolve()
Great (you also need to poll for write for the connect+EAGAIN case, but I suppose it's your plan)
@ceedriic non-blocking socket (via O_NONBLOCK flag) are available since c087718 if you want to try
Well, If you can deliver the above API and it is accepted by the project, I don't think I need to spend any time on a javascript implementation, this will be better :smile:. When you've something ready to test, let me know and I'll try your code.
The only possible improvement I see (which again can - and should - be done later or in javascript) that would be nice to have would be to provide an optional fully boolean to ask the function to loop on short reads / short writes. Something like (not sure it's valid TypeScript):
recv (sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number, boolean?: fully): Promise<Result<number>>; send (sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number, boolean?: fully): Promise<Result<number>>;
Because sometimes you want short reads or short writes, but often (in the case of send for example) you would prefer to have all the bytes written on the sockets buffer before the promise is resolved (same for read if you're implementing a binary protocol and know in advance the exact number of bytes you want to read)
So it would simplify user code if you can write something as simple as:
const fdSa = await os.accept();
await os.send(fdSa[0], "Hello World\n", true)); /* fully = true */
os.close(fdSa[0]);
Without having to loop on short writes.
I've finished the api and it's a joy to play with so @ceedriic don't hésitante to play with
CHANGES:
- changed
sendtoarguments order to keep thelength?last and optional - added getsockname so I know which port got binded when port is 0
- bind/connect/sendto
addrargument rely onsockfdfamily if they addr.family is undefined
//sync calls
getaddrinfo(node: string, service: string ): Result<Array<SocketAddr>>;
socket (family: SocketFamily, type: SocketType ): Result<FileDescriptor>;
getsockname(sockfd: FileDescriptor ): Result<SocketAddr>;
getsockopt (sockfd: FileDescriptor, name: SocketOpt, data: ArrayBuffer): Result<Success>;
setsockopt (sockfd: FileDescriptor, name: SocketOpt, data: ArrayBuffer): Result<Success>;
bind (sockfd: FileDescriptor, addr: SocketAddr ): Result<Success>;
listen (sockfd: FileDescriptor, backlog?: number ): Result<Success>;
shutdown (sockfd: FileDescriptor, type: SocketShutOpt ): Result<Success>;
//async calls
accept (sockfd: FileDescriptor ): Promise<[remotefd: FileDescriptor, remoteaddr: SocketAddr]>;
connect (sockfd: FileDescriptor, addr: SocketAddr ): Promise<Result<Success>>;
recv (sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number): Promise<Result<number>>;
send (sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number): Promise<Result<number>>;
recvfrom (sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number): Promise<[total: Result<number>, from: SocketAddr]>;
sendto (sockfd: FileDescriptor, addr: SocketAddr, buffer: ArrayBuffer, length?: number): Promise<Result<number>>;
functional tests done on Cosmo+Linux env (TODO: w10):
- IPv4 TCP HTTP client
const sockfd = must(os.socket(os.AF_INET, os.SOCK_STREAM));
await os.connect(sockfd, { addr: "51.15.168.198", port: 80}); // bellard.org
const httpReq = ["GET / HTTP/1.0", "", ""].join('\r\n')
await os.send(sockfd, Uint8Array.from(httpReq, c => c.charCodeAt(0)).buffer);
const chunk = new Uint8Array(512);
const recvd = await os.recv(sockfd, chunk.buffer);
console.log([...chunk.slice(0,recvd)].map(c => String.fromCharCode(c)).join(''));
- IPv6 UDP NTP client
const sockfd = must(os.socket(os.AF_INET6, os.SOCK_DGRAM));
await os.sendto(sockfd, { addr: "2610:20:6f97:97::6", port: 37 }, new ArrayBuffer());
const u32 = new Uint32Array(1);
await os.recvfrom(sockfd, u32.buffer);
const seconds1900 = new DataView(u32.buffer).getUint32(0)
console.log(`${seconds1900} have passed since 1900 (${seconds1900/60/60/24/365} years)`);
What's bugging me:
- lot's of buf<->str back and forth needed (an
std.TextEncoder/std.TextDecoderwould be nice) - shall recv/send return the buffer.slice() sent/received (instead of just the length) so it can be chained ?
const str = (new std.TextDecoder()).decode // TODO in a later PR
console.log(str(await os.recv(sockfd, new ArrayBuffer(1024))))
- calling 2 times a recv/send without await may create endless select() for the second call => TODO: reproduce, and throw if a socket operation is already pending, what do you think @ceedriic ?
- (optional) quickjs could provide it typing definition (doc/os.d.ts /
std.d.ts) in addition to it pdf/html docs
to keep things simple I did not included the websocket handling in the HTTP server example.
const WSGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
await sendLines(sock_cli, [
"HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade",
`Sec-WebSocket-Accept: ${sha1base64(headers.get("Sec-WebSocket-Key") + WSGUID)}`, '', ''
]);
const sendStr = (str = "") => os.send(sock_cli, Uint8Array.from([0x81, str.length, ...str.split("").map(x => x.charCodeAt(0))]).buffer);
await sendStr("welcome to quickjs websocket REPL");
const hdr = new DataView(new Uint8Array(2 + 4).buffer);
const payload = new Uint8Array(127);
while (await os.recv(sock_cli, hdr.buffer) == hdr.byteLength) {
if ((hdr.getUint8(0) & 0x0F) == 8) break;
const got = await os.recv(sock_cli, payload.buffer, hdr.getUint8(1) & 0x7F);
const unmasked = String.fromCharCode(...payload.slice(0, got).map((b, i) => b ^ hdr.getUint8(2 + (i % 4))));
await sendStr(await (async (x = "") => `${eval(x)}`)(unmasked).catch(e => `${e}`)).catch(e => -1);
}
return os.close(sock_cli);
web client:
<form autocomplete=off>
<pre><output name=out></output></pre>
<input hidden name=q placeholder=$>
<input name=o type=button value=open onclick=document.body.onload()>
</form>
<script type=module>
let ws,form = document.forms[0];
form.o.onclick=function(){
ws = new WebSocket(location.href.replace(/^http/,'ws'));
ws.onerror = (error) => console.error('WebSocket error:', error);
ws.onopen = ws.onclose = (ev) => form.o.hidden=!(form.q.hidden=ws.readyState!=1)
ws.onmessage = function(ev) {
form.out.innerHTML+=ev.data+'\n'
window.scrollTo(0,document.body.scrollHeight)
}
}
form.onsubmit = function() {
form.out.innerHTML += '$ '+this.q.value+'\n';
ws.send(this.q.value);
this.q.value='';
return false;
}
</script>
@bellard : JS sockets are working with winsock2 API (tested HTTP client + HTTP server via wine/mingw32_64) I'm unsure about the os_poll part, so your mentoring would be appreciated.
In addition to my previous suggestions, I would like to point that getaddrinfo often return INET6 addr as [0]
const [ai] = os.getaddrinfo("bellard.org",'80');
while socket is on INET this make os.connect(sockv4, addrInfoV6) fail.
Proposed solutions:
- user
.findthe correct socket type (current situation)
const ai = os.getaddrinfo("bellard.org",'80').find(a => a.family == os.AF_INET);
getaddrinfocould receive a hints as 3rd argument
os.getaddrinfo("bellard.org",'80', { family: os.AF_INET } )
os.getaddrinfo("bellard.org",'80',os.AF_INET) // or maybe
connectcould receive a list of addr[]. and use appropriate one according to givensocketfdfamily
await os.connect(sockfd, os.getaddrinfo("bellard.org",'80'));
I would vote for the connect one
I've finished the api and it's a joy to play with so @ceedriic don't hésitante to play with
Great! I will try to test and play with your code and answer you this week (or next week-end if too busy).
@yne Adding a parameter to getaddrinfo() is the best solution. The "hints" parameter of getaddrinfo is made for that. It should be possible to specify at least hints.ai_family and hints.ai_socktype. Note that the "service" parameter is also optional and less important than the "hints" parameter. The API could be like:
getaddrinfo(host, { service: "80", family: os.AF_INET, socktype: os.SOCK_STREAM })
@bellard done :heavy_check_mark:
Also, returned addrinfo[] objects now also have an additional socktype property to be used to create sockets.
const [addr] = must(os.getaddrinfo("bellard.org", { service: 80 }));
const sockfd = must(os.socket(addr.family, addr.socktype));
await os.connect(sockfd, addr);
And here is the std.encode/decode draft (pretty sure it also add an UAF vulnerability, so I pushed it on a separate branch)
Next goal: qjs debugging
///@ts-check
/// <reference path="../doc/globals.d.ts" />
/// <reference path="../doc/os.d.ts" />
/// <reference path="../doc/std.d.ts" />
import * as os from "os";
import * as std from "std";
//import { sha1base64 } from "./sha1.js"
/** @template T @param {os.Result<T>} result @returns {T} */
function must(result) {
if (typeof result === "number" && result < 0) throw result;
return /** @type {T} */ (result)
}
const [addrinfo] = must(os.getaddrinfo("localhost", { service: 9229, socktype: os.SOCK_STREAM }));
const sock_srv = must(os.socket(addrinfo.family, addrinfo.socktype));
must(os.setsockopt(sock_srv, os.SO_REUSEADDR, new Uint32Array([1]).buffer));
must(os.bind(sock_srv, addrinfo))
must(os.listen(sock_srv))
const profileId = "98bf008b-13be-4d6d-b479-bc67bc20f729";
console.log(`Debugger listening on ws://${addrinfo.addr}:${addrinfo.port}/${profileId} ...`)
//https://learn.microsoft.com/en-us/microsoft-edge/devtools/protocol/
const GET = {
'^/$': () => ['HTTP/1.1 200 OK', 'Content-Type: text/html', '', `use a websocket profiler`],
'^/json/version$': () => ['HTTP/1.1 200 OK', 'Content-Type: application/json', '', JSON.stringify({})],
'^/json/list$': ({ addrinfo }) => ['HTTP/1.1 200 OK', 'Content-Type: application/json', '', JSON.stringify([{
"type": "node", "title": import.meta.url, "url": import.meta.url, "webSocketDebuggerUrl": `ws://${addrinfo.addr}:${addrinfo.port}/${profileId}`
}])]
}
/** @param {os.FileDescriptor} fd @param {string[]} lines */
function sendLines(fd, lines) {
const buf = Uint8Array.from(lines.join('\r\n'), c => c.charCodeAt(0));
return os.send(fd, buf.buffer);
}
/** @param {os.FileDescriptor} fd @param {number} len*/
async function recv(fd, len = 1) {
const buf = new Uint8Array(len);
must(await os.recv(fd, buf.buffer) == buf.byteLength);
return new DataView(buf.buffer);
}
while (true) {
const [sock_cli] = await os.accept(sock_srv); // we don't care about sockaddr
const sock_cli_r = std.fdopen(sock_cli, "r");
if (sock_cli_r == null) throw "";
const [method, path, http_ver] = sock_cli_r.getline()?.trimEnd().split(' ');
const headers = new Map();
for (let line; line = sock_cli_r.getline()?.trimEnd();) {
const index = line.indexOf(': ');
headers.set(line.slice(0, index), line.slice(index + 2));
}
// Sec-WebSocket-Key often set to "dGhlIHNhbXBsZSBub25jZQ==" => return Sec-WebSocket-Accept = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
// otherwise would require SHA1 compute: ${sha1base64(headers.get("Sec-WebSocket-Key") + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")}
if (headers.has("Sec-WebSocket-Key")) {
await sendLines(sock_cli, ["HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade", `Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=`, '', '']);
const sendStr = (str = "") => os.send(sock_cli, Uint8Array.from([0x81, str.length, ...str.split("").map(x => x.charCodeAt(0))]).buffer);
while (true) {
const fin1_res3_op4 = (await recv(sock_cli)).getUint8(0);
if (!fin1_res3_op4) { os.sleep(500); continue }
if ((fin1_res3_op4 & 0x0F) == 8) break;// only support TEXT opcode
const len7 = (await recv(sock_cli)).getUint8(0) & 0x7F;
const len16 = len7 == 126 ? (await recv(sock_cli, 2)).getUint16(0) : len7;
const mask32 = await recv(sock_cli, 4);
const payload = new Uint8Array(len16);
await os.recv(sock_cli, payload.buffer);
const jsonPayload = String.fromCharCode(...payload.map((b, i) => b ^ mask32.getUint8(i % 4)));
try { JSON.parse(jsonPayload) } catch (e) { console.log("invalidJSON:", len7, len16, jsonPayload); continue }
const { id, method, ...args } = JSON.parse(jsonPayload);
const debuggerId = "30.-19"
const replies = {
"Runtime.enable": () => [{ id, result: {} }, { method: "Runtime.executionContextCreated", params: { context: { id: 1, origin: "", name: "quickjs[65405]", uniqueId: debuggerId, auxData: { isDefault: true } } } }],
"Runtime.runIfWaitingForDebugger": () => [{ id, result: {} },{"method":"Debugger.scriptParsed","params":{"scriptId":"1","url":"node:internal/bootstrap/realm","startLine":0,"startColumn":0,"endLine":458,"endColumn":0,"executionContextId":1,"hash":"0bf9dbe21ca95845538b501033e894566bfbba1d","executionContextAuxData":{"isDefault":true},"isLiveEdit":false,"sourceMapURL":"","hasSourceURL":false,"isModule":false,"length":14583,"scriptLanguage":"JavaScript","embedderName":"node:internal/bootstrap/realm"}}],
"Debugger.enable": () => [{ id, result: { debuggerId } }],
"Debugger.setPauseOnExceptions": () => [{ id, result: { state: "all" } }],
"Debugger.setAsyncCallStackDepth": () => [{ id, result: {} }],
"Debugger.setBlackboxPatterns": () => [{ id, result: {} }],
"Debugger.stepOver": () => [{ method: "Debugger.resumed", params: {} }, { id, result: {} }],
"Debugger.getScriptSource": () => [{ id, result: { scriptSource: "this is a fake debug from QuickJS" } }],
"Profiler.enable": () => [{ id, result: {} }],
"Runtime.compileScript": () => [{ id, result: {} }],
"": () => [{ id, error: { code: -1, message: `Unsupported ${method}` } }]
}
const reply = (replies[method] || replies[''])();
console.log({ id, method, ...args }, reply);
for (const r of reply)
await sendStr(JSON.stringify(r));
}
os.close(sock_cli);
} else {
const sock_cli_w = std.fdopen(sock_cli, "w");
if (sock_cli_w == null) throw "";
console.log(method, path, http_ver);
const endpoint = Object.entries(GET).find(([re]) => path.match(new RegExp(re)))?.[1];
const notFound = (_) => ['HTTP/1.1 404', '', `No route to ${path}`];
const lines = (endpoint || notFound)({ addrinfo });
sock_cli_w.puts(lines.join('\r\n'));
sock_cli_w.close();
}
}