util-common.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. /*
  2. * Common utilities for backend and frontend
  3. */
  4. import { Document } from "yaml";
  5. // Init dayjs
  6. import dayjs from "dayjs";
  7. import timezone from "dayjs/plugin/timezone";
  8. import utc from "dayjs/plugin/utc";
  9. import relativeTime from "dayjs/plugin/relativeTime";
  10. dayjs.extend(utc);
  11. dayjs.extend(timezone);
  12. dayjs.extend(relativeTime);
  13. let randomBytes : (numBytes: number) => Uint8Array;
  14. initRandomBytes();
  15. async function initRandomBytes() {
  16. if (typeof window !== "undefined" && window.crypto) {
  17. randomBytes = function randomBytes(numBytes: number) {
  18. const bytes = new Uint8Array(numBytes);
  19. for (let i = 0; i < numBytes; i += 65536) {
  20. window.crypto.getRandomValues(bytes.subarray(i, i + Math.min(numBytes - i, 65536)));
  21. }
  22. return bytes;
  23. };
  24. } else {
  25. randomBytes = (await import("node:crypto")).randomBytes;
  26. }
  27. }
  28. // Stack Status
  29. export const UNKNOWN = 0;
  30. export const CREATED_FILE = 1;
  31. export const CREATED_STACK = 2;
  32. export const RUNNING = 3;
  33. export const EXITED = 4;
  34. export function statusName(status : number) : string {
  35. switch (status) {
  36. case CREATED_FILE:
  37. return "draft";
  38. case CREATED_STACK:
  39. return "created_stack";
  40. case RUNNING:
  41. return "running";
  42. case EXITED:
  43. return "exited";
  44. default:
  45. return "unknown";
  46. }
  47. }
  48. export function statusNameShort(status : number) : string {
  49. switch (status) {
  50. case CREATED_FILE:
  51. return "inactive";
  52. case CREATED_STACK:
  53. return "inactive";
  54. case RUNNING:
  55. return "active";
  56. case EXITED:
  57. return "exited";
  58. default:
  59. return "?";
  60. }
  61. }
  62. export function statusColor(status : number) : string {
  63. switch (status) {
  64. case CREATED_FILE:
  65. return "dark";
  66. case CREATED_STACK:
  67. return "dark";
  68. case RUNNING:
  69. return "primary";
  70. case EXITED:
  71. return "danger";
  72. default:
  73. return "secondary";
  74. }
  75. }
  76. export const isDev = process.env.NODE_ENV === "development";
  77. export const TERMINAL_COLS = 105;
  78. export const TERMINAL_ROWS = 10;
  79. export const PROGRESS_TERMINAL_ROWS = 8;
  80. export const COMBINED_TERMINAL_COLS = 58;
  81. export const COMBINED_TERMINAL_ROWS = 20;
  82. export const ERROR_TYPE_VALIDATION = 1;
  83. export const allowedCommandList : string[] = [
  84. "docker",
  85. "ls",
  86. "cd",
  87. "dir",
  88. "clear",
  89. ];
  90. export const allowedRawKeys = [
  91. "\u0003", // Ctrl + C
  92. ];
  93. /**
  94. * Generate a decimal integer number from a string
  95. * @param str Input
  96. * @param length Default is 10 which means 0 - 9
  97. */
  98. export function intHash(str : string, length = 10) : number {
  99. // A simple hashing function (you can use more complex hash functions if needed)
  100. let hash = 0;
  101. for (let i = 0; i < str.length; i++) {
  102. hash += str.charCodeAt(i);
  103. }
  104. // Normalize the hash to the range [0, 10]
  105. return (hash % length + length) % length; // Ensure the result is non-negative
  106. }
  107. /**
  108. * Delays for specified number of seconds
  109. * @param ms Number of milliseconds to sleep for
  110. */
  111. export function sleep(ms: number) {
  112. return new Promise(resolve => setTimeout(resolve, ms));
  113. }
  114. /**
  115. * Generate a random alphanumeric string of fixed length
  116. * @param length Length of string to generate
  117. * @returns string
  118. */
  119. export function genSecret(length = 64) {
  120. let secret = "";
  121. const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  122. const charsLength = chars.length;
  123. for ( let i = 0; i < length; i++ ) {
  124. secret += chars.charAt(getCryptoRandomInt(0, charsLength - 1));
  125. }
  126. return secret;
  127. }
  128. /**
  129. * Get a random integer suitable for use in cryptography between upper
  130. * and lower bounds.
  131. * @param min Minimum value of integer
  132. * @param max Maximum value of integer
  133. * @returns Cryptographically suitable random integer
  134. */
  135. export function getCryptoRandomInt(min: number, max: number):number {
  136. // synchronous version of: https://github.com/joepie91/node-random-number-csprng
  137. const range = max - min;
  138. if (range >= Math.pow(2, 32)) {
  139. console.log("Warning! Range is too large.");
  140. }
  141. let tmpRange = range;
  142. let bitsNeeded = 0;
  143. let bytesNeeded = 0;
  144. let mask = 1;
  145. while (tmpRange > 0) {
  146. if (bitsNeeded % 8 === 0) {
  147. bytesNeeded += 1;
  148. }
  149. bitsNeeded += 1;
  150. mask = mask << 1 | 1;
  151. tmpRange = tmpRange >>> 1;
  152. }
  153. const bytes = randomBytes(bytesNeeded);
  154. let randomValue = 0;
  155. for (let i = 0; i < bytesNeeded; i++) {
  156. randomValue |= bytes[i] << 8 * i;
  157. }
  158. randomValue = randomValue & mask;
  159. if (randomValue <= range) {
  160. return min + randomValue;
  161. } else {
  162. return getCryptoRandomInt(min, max);
  163. }
  164. }
  165. export function getComposeTerminalName(stack : string) {
  166. return "compose-" + stack;
  167. }
  168. export function getCombinedTerminalName(stack : string) {
  169. return "combined-" + stack;
  170. }
  171. export function getContainerTerminalName(container : string) {
  172. return "container-" + container;
  173. }
  174. export function getContainerExecTerminalName(stackName : string, container : string, index : number) {
  175. return "container-exec-" + container + "-" + index;
  176. }
  177. export function copyYAMLComments(doc : Document, src : Document) {
  178. doc.comment = src.comment;
  179. doc.commentBefore = src.commentBefore;
  180. if (doc && doc.contents && src && src.contents) {
  181. // @ts-ignore
  182. copyYAMLCommentsItems(doc.contents.items, src.contents.items);
  183. }
  184. }
  185. /**
  186. * Copy yaml comments from srcItems to items
  187. * Typescript is super annoying here, so I have to use any here
  188. * TODO: Since comments are belong to the array index, the comments will be lost if the order of the items is changed or removed or added.
  189. */
  190. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  191. function copyYAMLCommentsItems(items : any, srcItems : any) {
  192. if (!items || !srcItems) {
  193. return;
  194. }
  195. for (let i = 0; i < items.length; i++) {
  196. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  197. const item : any = items[i];
  198. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  199. const srcItem : any = srcItems[i];
  200. if (!srcItem) {
  201. continue;
  202. }
  203. if (item.key && srcItem.key) {
  204. item.key.comment = srcItem.key.comment;
  205. item.key.commentBefore = srcItem.key.commentBefore;
  206. }
  207. if (srcItem.comment) {
  208. item.comment = srcItem.comment;
  209. }
  210. if (item.value && srcItem.value) {
  211. if (typeof item.value === "object" && typeof srcItem.value === "object") {
  212. item.value.comment = srcItem.value.comment;
  213. item.value.commentBefore = srcItem.value.commentBefore;
  214. if (item.value.items && srcItem.value.items) {
  215. copyYAMLCommentsItems(item.value.items, srcItem.value.items);
  216. }
  217. }
  218. }
  219. }
  220. }
  221. /**
  222. * Possible Inputs:
  223. * ports:
  224. * - "3000"
  225. * - "3000-3005"
  226. * - "8000:8000"
  227. * - "9090-9091:8080-8081"
  228. * - "49100:22"
  229. * - "8000-9000:80"
  230. * - "127.0.0.1:8001:8001"
  231. * - "127.0.0.1:5000-5010:5000-5010"
  232. * - "6060:6060/udp"
  233. * @param input
  234. * @param defaultHostname
  235. */
  236. export function parseDockerPort(input : string, defaultHostname : string = "localhost") {
  237. let hostname = defaultHostname;
  238. let port;
  239. let display;
  240. const parts = input.split("/");
  241. const part1 = parts[0];
  242. let protocol = parts[1] || "tcp";
  243. // Split the last ":"
  244. const lastColon = part1.lastIndexOf(":");
  245. if (lastColon === -1) {
  246. // No colon, so it's just a port or port range
  247. // Check if it's a port range
  248. const dash = part1.indexOf("-");
  249. if (dash === -1) {
  250. // No dash, so it's just a port
  251. port = part1;
  252. } else {
  253. // Has dash, so it's a port range, use the first port
  254. port = part1.substring(0, dash);
  255. }
  256. display = part1;
  257. } else {
  258. // Has colon, so it's a port mapping
  259. let hostPart = part1.substring(0, lastColon);
  260. display = hostPart;
  261. // Check if it's a port range
  262. const dash = part1.indexOf("-");
  263. if (dash !== -1) {
  264. // Has dash, so it's a port range, use the first port
  265. hostPart = part1.substring(0, dash);
  266. }
  267. // Check if it has a ip (ip:port)
  268. const colon = hostPart.indexOf(":");
  269. if (colon !== -1) {
  270. // Has colon, so it's a ip:port
  271. hostname = hostPart.substring(0, colon);
  272. port = hostPart.substring(colon + 1);
  273. } else {
  274. // No colon, so it's just a port
  275. port = hostPart;
  276. }
  277. }
  278. let portInt = parseInt(port);
  279. if (portInt == 443) {
  280. protocol = "https";
  281. } else if (protocol === "tcp") {
  282. protocol = "http";
  283. }
  284. return {
  285. url: protocol + "://" + hostname + ":" + portInt,
  286. display: display,
  287. };
  288. }