server.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. /**
  2. * СЕРВЕР ЭЛЕКТРОННОГО ЖУРНАЛА «ШКАЛА»
  3. * Copyright © 2024, А.М.Гольдин. Modified BSD License
  4. */
  5. "use strict";
  6. /* ПОДКЛЮЧЕНИЕ МОДУЛЕЙ И ОПРЕДЕЛЕНИЕ ГЛОБАЛЬНЫХ КОНСТАНТ
  7. * ----------------------------------------------------------------------- */
  8. const DOCROOT = __dirname + "/www",
  9. https = require("https"),
  10. fs = require("fs"),
  11. nedb = require("@yetzt/nedb"),
  12. {
  13. PORT, SERVER, ERR404, MIME, PWD, SALT, SALTPIN,
  14. ADMIN, KEYPATH, CERTPATH, CAPATH
  15. } = require("./config"),
  16. api = require("./api"),
  17. captGen = require("./api/captchaGen"),
  18. httpsOpt = {
  19. key: fs.readFileSync(__dirname + "/ssl/" + KEYPATH),
  20. cert: fs.readFileSync(__dirname + "/ssl/" + CERTPATH)
  21. };
  22. if (CAPATH) httpsOpt.ca = CAPATH;
  23. global.salt = SALT;
  24. global.saltpin = SALTPIN;
  25. global.admPwd = PWD;
  26. /* ИНИЦИАЛИЗАЦИЯ КОЛЛЕКЦИЙ БАЗЫ ДАННЫХ
  27. * ----------------------------------------------------------------------- */
  28. const dbTables = [
  29. "staff", "pupils", "curric", "distrib", "grades", "spravki", "topics",
  30. "authlog", "notes"
  31. ];
  32. global.db = {};
  33. for (let dbN of dbTables) db[dbN] =
  34. new nedb({filename: `${__dirname}/db/${dbN}.db`, autoload: true});
  35. /* ОПРЕДЕЛЕНИЯ ФУНКЦИЙ
  36. * ----------------------------------------------------------------------- */
  37. // Запись серверного лога
  38. const putlog = (ip, reqMeth, pathname, kodOtv, lengthOtv) => {
  39. let
  40. now = new Date(),
  41. y = now.getFullYear(),
  42. m = (now.getMonth() + 1).toString().padStart(2, '0'),
  43. d = now.getDate().toString().padStart(2, '0'),
  44. h = now.getHours().toString().padStart(2, '0'),
  45. i = now.getMinutes().toString().padStart(2, '0'),
  46. s = now. getSeconds().toString().padStart(2, '0'),
  47. dt = `${y}-${m}-${d}`,
  48. tm = `${h}:${i}:${s}`;
  49. // Пишем данные в серверный лог
  50. fs.appendFile(
  51. __dirname + `/logs/${dt}.log`,
  52. `${ip} [${tm}] ${reqMeth} ${pathname} ${kodOtv} ${lengthOtv}\n`,
  53. e => {}
  54. )
  55. // Пишем успешный запрос авторизации в коллекцию authlog
  56. if (pathname.includes(" login]") && lengthOtv > 5) {
  57. let loginArr = pathname.replace(/[\[\], ]/g, '').replace("login", '')
  58. . split('_'),
  59. login = loginArr[0],
  60. categ = loginArr[1] || "root";
  61. db.authlog.insert({d: `${dt} ${tm}`, l: login, c: categ, ip: ip});
  62. }
  63. };
  64. // Генерирование числового значения капчи по её Id
  65. // (используется также для генерирования родительских паролей из детских)
  66. global.captNumGen = str => {
  67. let captNum = '', s, h = 0;
  68. for (let j = 0; j < 6; j++) {
  69. s = global.salt + j + str;
  70. for (let i=0; i<s.length; i++) h = ((h << 5) - h) + s.charCodeAt(i);
  71. captNum += Math.abs(h) % 10;
  72. }
  73. return captNum;
  74. }
  75. // Промисификатор метода find() работы с базой db
  76. // Пример вызова: let res = await dbFind("curric", {type: "class"})
  77. global.dbFind = (collectionName, objFind) => {
  78. return new Promise((resolve, reject) => {
  79. db[collectionName].find(objFind, (err, docs) => {
  80. if (err) reject(err);
  81. else resolve(docs);
  82. })
  83. })
  84. };
  85. // Изготавление хэша длины 24 из строки str с солью slt
  86. global.hash = (str, slt) => {
  87. let
  88. alph = "0123456789AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz",
  89. char,
  90. strNew,
  91. h = 0,
  92. pass = '';
  93. for (let j = 0; j < 24; j++) {
  94. strNew = slt + j + str;
  95. for (let i = 0; i < strNew.length; i++) {
  96. char = strNew.charCodeAt(i);
  97. h = ((h << 5) - h) + char;
  98. }
  99. pass += alph[Math.abs(h) % alph.length];
  100. }
  101. return pass;
  102. }
  103. // Отправка ответа (kod - код состояния, contType - mime-тип, content - тело)
  104. const sendOtvet = (otvet, kod, contType, content) => {
  105. otvet.writeHead(kod, {
  106. "Content-Type": contType, "Server": SERVER,
  107. "Strict-Transport-Security": "max-age=32000000",
  108. "Access-Control-Allow-Origin": "*"
  109. });
  110. otvet.end(content);
  111. }
  112. /* ОБЪЕКТЫ, ОБСЛУЖИВАЮЩИЕ РАБОТУ С КАПЧЕЙ
  113. * ----------------------------------------------------------------------- */
  114. // Параметры отдаваемой капчи
  115. const captOpt = {
  116. bkR: 246, bkG: 243, bkB: 240, // фоновый цвет
  117. fnR: 214, fnG: 191, fnB: 168, // цвет шрифта
  118. }
  119. // Массив выданных сервером клиенту ID капчи и время жизни капчи в секундах
  120. // (те, что вернулись от клиента, а также старые удаляются)
  121. global.captchaIdArr = [];
  122. const CAPTDEATH = 180;
  123. /* СОБСТВЕННО ЦИКЛ ОБРАБОТКИ ЗАПРОСА
  124. * ----------------------------------------------------------------------- */
  125. https.createServer(httpsOpt, (zapros, otvet) => {
  126. // Получаем параметры запроса
  127. let url = new URL("http://host" + zapros.url),
  128. pathname = url.pathname;
  129. if (!pathname.includes(".")) pathname += "/index.html";
  130. pathname = pathname.replace("//", '/').replace(/\.\./g, '');
  131. let ADDR = (zapros.socket.remoteAddress || "unknown")
  132. . replace("::1", "127.0.0.1").replace(/\:.*\:/, '');
  133. // Если пришел запрос контактов администратора
  134. if (pathname == "/a.a") sendOtvet(otvet, 200, "text/plain", ADMIN);
  135. // Если пришел запрос капчи, отдаем ее вместе с ее Id (в заголовке X-Cpt)
  136. else if (pathname == "/cpt.a") {
  137. let tm = Date.now();
  138. // Удаляем все устаревшие Id капчи и кладем новый Id
  139. captchaIdArr = captchaIdArr.filter(
  140. x => Number(x) > Number(tm - CAPTDEATH * 1000));
  141. captchaIdArr.push(tm);
  142. otvet.writeHead(200, {
  143. "Content-Type": "image/png", "Server": SERVER, "X-Cpt": tm,
  144. "Access-Control-Allow-Origin": "*",
  145. "Access-Control-Expose-Headers": "X-Cpt"
  146. });
  147. otvet.end(captGen(captNumGen(tm), captOpt));
  148. }
  149. // Если метод GET, просто отдаем запрошенный статический файл
  150. else if (zapros.method == "GET")
  151. fs.readFile(DOCROOT + pathname, function(err, cont) {
  152. let mtip = MIME[pathname.split(".").pop()];
  153. if (!mtip || err) {
  154. sendOtvet(otvet, 404, "text/html", ERR404);
  155. putlog(ADDR, "GET", pathname, 404, ERR404.length);
  156. }
  157. else {
  158. sendOtvet(otvet, 200, mtip, cont);
  159. // Из успешных GET-запросов логируем только запрос главной
  160. if (pathname == "/index.html")
  161. putlog(ADDR, "GET", '/', 200, cont.length);
  162. }
  163. });
  164. // Если метод POST - это запрос к API
  165. else {
  166. let postData = '';
  167. zapros.on("data", dann => postData += dann.toString());
  168. zapros.on("end", async () => {
  169. let cont = await api(postData, ADDR);
  170. sendOtvet(otvet, 200, "text/plain", cont);
  171. // Определяем логин и запрашиваемую функцию API;
  172. // пишем логин и запрашиваемую функцию в серверный лог
  173. // (если функция содержится в списке логируемых функций),
  174. // а успешный запрос авторизации - еще и в базу (authlog)
  175. // с помощью функции putlog (определена выше)
  176. let logCont = '';
  177. let logFuncs = [
  178. "login", "classAdd", "classDel", "subjAdd", "subjEdit", "subjDel",
  179. "usAddEdit", "usImport", "usSetAdmin", "usBlock", "usChPwd",
  180. "tutorSet", "distrEdit", "topicEdit", "gradeAdd", "subgrEdit",
  181. "subgrPups", "sprAdd", "sprDel", "notesAdd", "notesDel",
  182. "interGroupEdit", "interGroupDel", "interGroupPup", "tabelGenAll",
  183. "permitAdd", "sprEdit"
  184. ];
  185. try {
  186. let postDataObj = JSON.parse(postData);
  187. let logLogin = postDataObj.l || "none";
  188. let logFunc = postDataObj.f || "none";
  189. let logRole = `_${postDataObj.t}` || "_none";
  190. if (logLogin == "admin") logRole = '';
  191. if (logFuncs.includes(logFunc))
  192. logCont = `[${logLogin}${logRole} ${logFunc}]`;
  193. }
  194. catch(e) {;}
  195. let codeOtv = (cont == "none") ? 403 : 200;
  196. if (logCont) putlog(ADDR, "POST", logCont, codeOtv, cont.length);
  197. });
  198. }
  199. }).listen(PORT);
  200. // Перенаправление с http на https
  201. const http = require("http");
  202. http.createServer((zapros, otvet) => {
  203. try {
  204. otvet.writeHead(
  205. 301, {"Location": "https://" + zapros.headers["host"] + zapros.url}
  206. ); otvet.end();
  207. }
  208. catch (e) {
  209. otvet.writeHead(404, {"Content-Type": "text/html", "Server": SERVER});
  210. otvet.end(ERR404);
  211. }
  212. }).listen(80);
  213. let now = (new Date()).toString().replace(/ \(.*\)/, '');
  214. console.info(`${now} ScoleServer стартовал на порту ${PORT}`);