Line data Source code
1 : import cockpit from "cockpit";
2 :
3 : import { debug } from "./util.js";
4 :
5 22 : function manage_error(reject, error, content) {
6 22 : let content_o = {};
7 22 : if (content) {
8 22 : try {
9 22 : content_o = JSON.parse(content);
10 3 : } catch {
11 3 : content_o.message = content;
12 3 : }
13 22 : }
14 22 : const c = { ...error, ...content_o };
15 22 : reject(c);
16 22 : }
17 :
18 : // calls are async, so keep track of a call counter to associate a result with a call
19 43 : let call_id = 0;
20 :
21 43 : const NL = '\n'.charCodeAt(0); // always 10, but avoid magic constant
22 43 : const CR = '\r'.charCodeAt(0); // always 13, but avoid magic constant
23 :
24 43 : const PODMAN_SYSTEM_ADDRESS = "/run/podman/podman.sock";
25 :
26 : /* uid: null for logged in session user, otherwise standard Unix user ID
27 : * Return { path, superuser } */
28 43 : function getAddress(uid) {
29 41 : if (uid === null) {
30 : // FIXME: make this async and call cockpit.user()
31 41 : const xrd = sessionStorage.getItem('XDG_RUNTIME_DIR');
32 41 : if (xrd)
33 41 : return { path: xrd + "/podman/podman.sock", superuser: null };
34 0 : console.warn("$XDG_RUNTIME_DIR is not present. Cannot use user service.");
35 0 : return { path: "", superuser: null };
36 0 : }
37 :
38 29 : if (uid === 0)
39 29 : return { path: PODMAN_SYSTEM_ADDRESS, superuser: "require" };
40 :
41 1 : if (Number.isInteger(uid))
42 1 : return { path: `/run/user/${uid}/podman/podman.sock`, superuser: "require" };
43 :
44 0 : throw new Error(`getAddress: uid ${uid} not supported`);
45 43 : }
46 :
47 : // split an Uint8Array at \r\n\r\n (separate headers from body)
48 43 : function splitAtNLNL(array) {
49 43 : for (let i = 0; i <= array.length - 4; i++) {
50 43 : if (array[i] === CR && array[i + 1] === NL && array[i + 2] === CR && array[i + 3] === NL) {
51 43 : return [array.subarray(0, i), array.subarray(i + 4)];
52 43 : }
53 43 : }
54 0 : console.error("did not find NLNL in array", array); // not-covered: if this happens, it's a podman bug
55 0 : return [array, null]; // not-covered: dito
56 43 : }
57 :
58 : /* uid: null for logged in session user; 0 for root; in the future we'll support other users
59 : * Returns a connection object with methods monitor(), call(), and close(), and an `uid` property.
60 : */
61 43 : function connect(uid) {
62 43 : const addr = getAddress(uid);
63 : /* This doesn't create a channel until a request */
64 : /* HACK: use binary channel to work around https://github.com/cockpit-project/cockpit/issues/19235 */
65 43 : const http = cockpit.http(addr.path, { superuser: addr.superuser, binary: true });
66 43 : const raw_channels = [];
67 43 : const connection = { uid };
68 43 : const decoder = new TextDecoder();
69 1 : const user_str = (uid === null) ? "user" : (uid === 0) ? "root" : `uid ${uid}`;
70 :
71 43 : connection.call = function (options) {
72 43 : const id = call_id++;
73 43 : debug(user_str, `call ${id}:`, JSON.stringify(options));
74 43 : return new Promise((resolve, reject) => {
75 0 : options = options || {};
76 43 : http.request(options)
77 43 : .then(result => {
78 43 : const text = decoder.decode(result);
79 43 : debug(user_str, `call ${id} result:`, text);
80 43 : resolve(text);
81 43 : })
82 21 : .catch((error, content) => {
83 21 : const text = decoder.decode(content);
84 21 : debug(user_str, `call ${id} error:`, JSON.stringify(error), "content", text);
85 21 : manage_error(reject, error, text);
86 21 : });
87 43 : });
88 43 : };
89 :
90 43 : connection.monitor = function(path, callback, return_raw = false) {
91 43 : return new Promise((resolve, reject) => {
92 43 : const ch = cockpit.channel({ unix: addr.path, superuser: addr.superuser, payload: "stream", binary: true });
93 43 : raw_channels.push(ch);
94 43 : let buffer = new Uint8Array();
95 :
96 7 : ch.addEventListener("close", () => {
97 7 : debug(user_str, "monitor", path, "closed");
98 7 : resolve();
99 7 : });
100 :
101 43 : const onHTTPMessage = message => {
102 43 : const [headers_bin, body] = splitAtNLNL(message.detail);
103 43 : const headers = decoder.decode(headers_bin);
104 43 : debug(user_str, "monitor", path, "HTTP response:", headers);
105 43 : if (headers.match(/^HTTP\/1.*\s+200\s/)) {
106 : // any further message is actual streaming data
107 43 : ch.removeEventListener("message", onHTTPMessage);
108 43 : ch.addEventListener("message", onDataMessage);
109 :
110 : // process the initial response data
111 43 : if (body)
112 43 : onDataMessage({ detail: body });
113 1 : } else {
114 1 : manage_error(reject, { reason: headers.split('\r\n')[0] }, body);
115 1 : }
116 43 : };
117 :
118 43 : const onDataMessage = message => {
119 5 : if (return_raw) {
120 : // debug(user_str, "monitor", path, "raw data:", message.detail);
121 5 : callback(message.detail);
122 5 : } else {
123 43 : buffer = new Uint8Array([...buffer, ...message.detail]);
124 :
125 : // split the buffer into lines on NL (this is safe with UTF-8)
126 43 : for (;;) {
127 43 : const idx = buffer.indexOf(NL);
128 43 : if (idx < 0)
129 43 : break;
130 :
131 43 : const line = buffer.slice(0, idx);
132 43 : buffer = buffer.slice(idx + 1);
133 :
134 43 : const line_str = decoder.decode(line);
135 43 : debug(user_str, "monitor", path, "data:", line_str);
136 43 : callback(JSON.parse(line_str));
137 43 : }
138 43 : }
139 43 : };
140 :
141 : // the initial message is the HTTP status response
142 43 : ch.addEventListener("message", onHTTPMessage);
143 :
144 43 : ch.send("GET " + path + " HTTP/1.0\r\nContent-Length: 0\r\n\r\n");
145 43 : });
146 43 : };
147 :
148 8 : connection.close = function () {
149 8 : http.close();
150 2 : raw_channels.forEach(ch => ch.close());
151 8 : };
152 :
153 43 : return connection;
154 43 : }
155 :
156 43 : export default {
157 43 : connect,
158 43 : getAddress,
159 43 : };
|