Skip to content

Commit 15a2ff3

Browse files
committed
Add socket API
1 parent f10ef29 commit 15a2ff3

File tree

3 files changed

+357
-4
lines changed

3 files changed

+357
-4
lines changed

examples/http_client.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env qjs
2+
///@ts-check
3+
/// <reference path="../doc/globals.d.ts" />
4+
/// <reference path="../doc/os.d.ts" />
5+
import * as os from "os";
6+
/** @template T @param {os.Result<T>} result @returns {T} */
7+
function must(result) {
8+
if (typeof result === "number" && result < 0) throw result;
9+
return /** @type {T} */ (result)
10+
}
11+
/** @param {os.FileDescriptor} fd @param {string[]} lines */
12+
function sendLines(fd, lines) {
13+
const buf = Uint8Array.from(lines.join('\r\n'), c => c.charCodeAt(0));
14+
os.write(fd, buf.buffer, 0, buf.byteLength);
15+
}
16+
const [host = "example.com", port = "80"] = scriptArgs.slice(1);
17+
const ai = os.getaddrinfo(host, port).filter(ai=>ai.family==os.AF_INET && ai.port); // TODO too much/invalid result
18+
if (!ai.length) throw `Unable to getaddrinfo(${host}, ${port})`;
19+
const sockfd = must(os.socket(os.AF_INET, os.SOCK_STREAM));
20+
must(os.connect(sockfd, ai[0]))
21+
sendLines(sockfd, ["GET / HTTP/1.0", `Host: ${host}`, "Connection: close", "",""]);
22+
23+
const chunk = new Uint8Array(4096);
24+
while (os.read(sockfd, chunk.buffer, 0, chunk.byteLength) > 0) {
25+
console.log(String.fromCharCode(...chunk));
26+
}

examples/http_server.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env qjs
2+
///@ts-check
3+
/// <reference path="../doc/globals.d.ts" />
4+
/// <reference path="../doc/os.d.ts" />
5+
import * as os from "os";
6+
7+
const MIMES = new Map([
8+
['html', 'text/html'],
9+
['txt', 'text/plain'],
10+
['css', 'text/css'],
11+
['c', 'text/plain'],
12+
['h', 'text/plain'],
13+
['json', 'application/json'],
14+
['mjs', 'application/javascript'],
15+
['js', 'application/javascript'],
16+
['', 'application/octet-stream'],
17+
]);
18+
/** @template T @param {os.Result<T>} result @returns {T} */
19+
function must(result) {
20+
if (typeof result === "number" && result < 0) throw result;
21+
return /** @type {T} */ (result)
22+
}
23+
/**@param {os.FileDescriptor} fd */
24+
function* recvLines(fd) {
25+
const chunk = new Uint8Array(1);
26+
let line = '';
27+
while (os.read(fd, chunk.buffer, 0, chunk.byteLength) > 0) {
28+
const char = String.fromCharCode(...chunk);
29+
if (char == '\n') {
30+
yield line;
31+
line = '';
32+
} else line += char;
33+
}
34+
if (line) yield line;
35+
}
36+
/** @param {os.FileDescriptor} fd @param {string[]} lines */
37+
function sendLines(fd, lines) {
38+
const buf = Uint8Array.from(lines.join('\r\n'), c => c.charCodeAt(0));
39+
os.write(fd, buf.buffer, 0, buf.byteLength);
40+
}
41+
//USAGE: qjs http_server.js [PORT=8080 [HOST=localhost]]
42+
const [port = "8080", host = "localhost"] = scriptArgs.slice(1);
43+
const ai = os.getaddrinfo(host, port);
44+
if (!ai.length) throw `Unable to getaddrinfo(${host}, ${port})`;
45+
46+
const sock_srv = must(os.socket(os.AF_INET, os.SOCK_STREAM));
47+
must(os.setsockopt(sock_srv, os.SO_REUSEADDR, new Uint32Array([1]).buffer));
48+
must(os.bind(sock_srv, ai[0]));
49+
must(os.listen(sock_srv));
50+
51+
console.log(`Listening on http://${ai[0].addr}:${ai[0].port} ...`);
52+
53+
while (true) { // TODO: break on SIG*
54+
const [sock_cli] = os.accept(sock_srv);
55+
56+
const lines = recvLines(sock_cli);
57+
const [method, path, http_ver] = (lines.next().value || '').split(' ');
58+
let safe_path = '.' + path.replaceAll(/\.+/g, '.'); // may += index.html later
59+
console.log(method, safe_path, http_ver);
60+
61+
const headers = new Map()
62+
for (const line of lines) {
63+
const header = line.trimEnd();
64+
if (!header) break;
65+
const sepIdx = header.indexOf(': ');
66+
headers.set(header.slice(0, sepIdx), header.slice(sepIdx + 2));
67+
}
68+
69+
let [obj, err] = os.stat(safe_path);
70+
if (obj?.mode & os.S_IFDIR && safe_path.endsWith('/') && os.stat(safe_path + 'index.html')[0]) {
71+
safe_path += 'index.html';
72+
[obj, err] = os.stat(safe_path);
73+
}
74+
if (err) {
75+
sendLines(sock_cli, ['HTTP/1.1 404', '', safe_path, 'errno:' + err])
76+
} else if (obj?.mode & os.S_IFDIR) {
77+
if (!safe_path.endsWith('/'))
78+
sendLines(sock_cli, ['HTTP/1.1 301', `Location: ${safe_path}/`, '']);
79+
else
80+
sendLines(sock_cli, ['HTTP/1.1 200', 'Content-Type: text/html', '',
81+
os.readdir(safe_path)[0]?.filter(e => e[0] != '.').map(e => `<li><a href="${e}">${e}</a></li>`).join('')
82+
]);
83+
} else {
84+
const mime = MIMES.get(safe_path.split('.').at(-1) || '') || MIMES.get('');
85+
sendLines(sock_cli, ['HTTP/1.1 200', `Content-Type: ${mime}`, '', '']);
86+
const fd = must(os.open(safe_path));
87+
const fbuf = new Uint8Array(4096);
88+
for (let got = 0; (got = os.read(fd, fbuf.buffer, 0, fbuf.byteLength)) > 0;) {
89+
os.write(sock_cli, fbuf.buffer, 0, got);
90+
}
91+
}
92+
93+
os.close(sock_cli);
94+
}
95+
96+
os.close(sock_srv);

0 commit comments

Comments
 (0)