Русский
Русский
English
Статистика
Реклама

Hook

Перевод Мифы о useEffect

07.10.2020 10:05:19 | Автор: admin


Доброго времени суток, друзья!

Представляю вашему вниманию перевод небольшой заметки Kent C. Dodds, в которой он делится своими соображениями относительно правильного использования хука useEffect.

Я обучил React тысячи разработчиков. Как до, так и после релиза
хуков. Одной из вещей, которые я заметил, является не очень четкое понимание
назначения и механизма работы хука useEffect. В этой статье я хочу немного об этом рассказать.

Никакого отношения к стадиям жизненного цикла синхронизация дополнительных эффектов


Разработчики, которые имеют опыт работы с классовыми компонентами и стадиями
жизненного цикла, такими как constructor, componentDidMount,
componentDidUpdate и componentWillUnmount, иногда пытаются реализовать
аналогичное поведение в функциональных компонентах с помощью хуков. Это большая
ошибка. Позвольте мне это продемонстрировать. Вот пример забавного
пользовательского интерфейса:



Вот реализация компонента DogInfo с помощью классов:

  class DogInfo extends React.Component {    controller = null;    state = { dog: null };    fetchDog() {      this.controller?.abort();      this.controller = new AbortController();      getDog(this.props.dogId, { signal: this.controller.signal }).then(        (dog) => {          this.setState({ dog });        },        (error) => {          // обработка ошибок        }      );    }    componentDidMount() {      this.fetchDog();    }    componentDidUpdate(prevProps) {      // обработка изменения dogId      if (prevProps.dogId !== this.props.dogId) {        this.fetchDog();      }    }    componentWillUnmount() {      // отмена запроса      this.controller?.abort();    }    render() {      return <div>{/* рендеринг информации о собаке */}</div>;    }  }

Это стандартный компонент для такого вида интеракции. В нем
используются стадии жизненного цикла constructor, componentDidMount,
componentDidUpdate и componentWillUnmount. Вот что получится, если мы
обернем эти стадии в хуки:

  function DogInfo({ dogId }) {    const controllerRef = React.useRef(null);    const [dog, setDog] = React.useState(null);    function fetchDog() {      controllerRef.current?.abort();      controllerRef.current = new AbortController();      getDog(dogId, { signal: controllerRef.current.signal }).then(        (d) => setDog(d),        (er) => {          // обработка ошибок        }      );    }    // didMount    React.useEffect(() => {      fetchDog();      // eslint-disable-next-line react-hooks/exhaustive-deps    }, []);    // didUpdate    const prevDogId = usePrevious(dogId);    useUpdate(() => {      if (prevDogId !== dogId) {        fetchDog();      }    });    // willUnmount    React.useEffect(() => {      return () => {        controllerRef.current?.abort();      };    }, []);    return <div>{/* рендеринг информации о собаке */}</div>;  }  function usePrevious(value) {    const ref = useRef();    useEffect(() => {      ref.current = value;    }, [value]);    return ref.current;  }

Здесь имеется некоторая несогласованность между хуками. Если бы они
предназначались для использования таким образом, я тоже был бы их противником.
Но правда в том, что useEffect это не стадия жизненного цикла. Это механизм
синхронизации дополнительных эффектов с состоянием приложения. В
приведенном примере наша задача состоит в том, чтобы запрашивать информацию о
собаке при изменении dogId. Учитывая это, useEffect становится намного проще:

  function DogInfo({ dogId }) {    const [dog, setDog] = useState(null);    useEffect(() => {      const controller = new AbortController();      getDog(dogId, { signal: controller.signal }).then(        (d) => setDog(d),        (er) => {          // обработка ошибок        }      );      return () => controller.abort();    }, [dogId]);    return <div>{/* рендеринг информации о собаке */}</div>;  }

Это выглядит намного лучше, не так ли? Когда команда React представила хуки, их
целью являлось не упрощение использования стадий жизненного цикла в
функциональных компонентах, а улучшение ментальной модели относительно дополнительных эффектов приложения. И они этого достигли.

Запомните высказывание Ryan Florence:
Вопрос не в том, когда запускать эффект, вопрос в том, с каким состоянием
он должен синхронизироваться

useEffect(fn) // все состояния
useEffect(fn, []) // отсутствие состояний
useEffect(fn, [these, states]) // указанные состояния

Могу я игнорировать eslint-plugin-react-hooks/exhaustive-deps?


Ну, технически да. И иногда для этого существуют хорошие причины. Однако, в
большинстве случаев это является плохой идеей, игнорирование этого правила почти наверняка приведет к ошибкам. Обычно, люди после этого говорят: Но я только
хотел, чтобы это запускалось после рендеринга!. Вот опять. Думать о хуках в
категориях жизненного цикла неправильно. Если ваш колбэк в useEffect имеет
зависимость, необходимо убедиться, что он выполняется при каждом изменении
этой зависимости. В противном случае, эффект не будет синхронизирован с
состоянием приложения. Короче говоря, у вас будут проблемы. Не игнорируйте
данное правило.

Один большой useEffect


Честно говоря, я давно такого не видел. Но такое порой все-таки случается. То,
что мне действительно нравится в useEffect, так это возможность разделения задачи на любое количество подзадач. Вот простой пример:



Вот некоторый всевдокод для этого демо:

  class ChatFeed extends React.Component {    componentDidMount() {      this.subscribeToFeed();      this.setDocumentTitle();      this.subscribeToOnlineStatus();      this.subscribeToGeoLocation();    }    componentWillUnmount() {      this.unsubscribeFromFeed();      this.restoreDocumentTitle();      this.unsubscribeFromOnlineStatus();      this.unsubscribeFromGeoLocation();    }    componentDidUpdate(prevProps, prevState) {      // ... сранение пропсов, повторной подписки и т.д.    }    render() {      return <div>{/* интерфейс чата */}</div>;    }  }

Видите эти 4 задачи? Они смешаны. Что, если вы захотите поделиться частью функционала? Я имею ввиду, что пропсы рендеринга отличная вещь, но хуки
все же лучше. Я видел, как некоторые люди создают огромный useEffect, отвечающий
за все:

  function ChatFeed() {    React.useEffect(() => {      // подписка на ленту      // установка заголовка документа      // перевод статуса в онлайн      // определения местоположения      return () => {        // отписка от ленты        // восстановление заголовка        // перевод статуса в офлайн        // отключение определения местоположения      };    });    return <div>{/* интерфейс чата */}</div>;  }

Но это делает колбэк очень сложным. Предлагаю другой подход. Не забывайте, что
вы можете разделять логику между разными хуками:

  function ChatFeed() {    React.useEffect(() => {      // подписка на ленту      return () => {        // отписка от ленты      };    });    React.useEffect(() => {      // установка заголовка документа      return () => {        // восстановление заголовка      };    });    React.useEffect(() => {      // перевод статуса в онлайн      return () => {        // перевод статуса в офлайн      };    });    React.useEffect(() => {      // определения местоположения      return () => {        // отключение определения местоположения      };    });    return <div>{/* интерфейс чата */}</div>;  }

Самодостаточность хуков дает массу преимуществ.

Внешние функции


Я видел такое несколько раз. Позвольте мне просто привести код до и после:

  // до. Не делайте так  function DogInfo({ dogId }) {    const [dog, setDog] = React.useState(null);    const controllerRef = React.useRef(null);    const fetchDog = React.useCallback((dogId) => {      controllerRef.current?.abort();      controllerRef.current = new AbortController();      return getDog(dogId, { signal: controller.signal }).then(        (d) => setDog(d),        (error) => {          // обработка ошибок        }      );    }, []);    React.useEffect(() => {      fetchDog(dogId);      return () => controller.current?.abort();    }, [dogId, fetchDog]);    return <div>{/* рендеринг информации о собаках */}</div>;  }

Мы уже видели, как можно упростить этот код, но давайте взглянем на него еще раз:

  function DogInfo({ dogId }) {    const [dog, setDog] = React.useState(null);    React.useEffect(() => {      const controller = new AbortController();      getDog(dogId, { signal: controller.signal }).then(        (d) => setDog(d),        (error) => {          // обработка ошибок        }      );      return () => controller.abort();    }, [dogId]);    return <div>{/* рендеринг информации о собаках */}</div>;  }

Я пытаюсь сказать, что определение функции за пределами колбэка useEffect это плохая идея. Поскольку такая функция является внешней по отношению к useEffect, нам приходится добавлять ее в список зависимостей. Также нам приходится кэшировать ее во избежание бесконечного цикла. Кроме того, мы вынуждены создавать ref для контроллера.

Запомните: при необходимости определения функции, вызываемой в эффекте, определяйте ее внутри колбэка данного эффекта, а не за его пределами.

Заключение


Когда Dan Abramov представил хуки, такие как useEffect, он сравнил компоненты с атомами, а хуки с электронами. Хуки представляют собой низкоуровневые примитивы, и именно это делает их такими мощными. Красота этих примитивов заключается в абстрагировании стадий жизненного цикла, с которыми мы имели дело раньше. С момента релиза хуков мы наблюдаем взрыв инноваций и появление хороших идей и библиотек на основе этих примитивов, что помогает нам, как разработчикам, создавать более качественные приложения.
Подробнее..

Эпическая сага про маленький custom hook для React (генераторы, sagas, rxjs) часть 3

11.12.2020 20:11:43 | Автор: admin

Часть 1. Кастомный хук

Часть 2. Генераторы

Redux-saga

Это middleware для управления сайд эффектами при работе с redux. В основе лежит механизм генераторов. Т.е. код ставится на паузу пока не будет выполнена определенная операция с эффектом - это объект с определенным типом и данными.

Можно представить себе redux-saga (middleware) как администратора камер хранения. В камеры хранения можно класть эффекты на неопределенный срок и забирать их оттуда, когда будет нужно. Есть такой посыльный put, который приходит к диспетчеру и просит положить в камеру хранения сообщение (эффект). Есть такой посыльный take, который приходит к диспетчеру и просит ему выдать сообщение с определенным типом (эффект). Диспетчер, по просьбе take, смотрит все камеры хранения и если этих данных нет, то take остаётся с диспетчером и ждёт, пока put не принесёт данные с нужным для take типом. Существуют разные виды таких посыльных (takeEvery и т.д.).

Основная идея камер хранения - это "развести" во времени отправителя и получателя (некий аналог асинхронных обработки).

Redux-saga - это просто инструмент, а вот главным тут является тот, кто посылает всех этих посыльных и обрабатывает данные, которые они приносят. Этим "кто-то" является функция-генератор (назову её пассажир), которая в справке называется saga и передаётся при запуске middleware. Запустить middleware можно двумя способами: с помощью middleware.run(saga, ...args) и runSaga(options, saga, ...args). Saga - это функция-генератор с логикой обработки эффектов.

Меня заинтересовала возможность использования redux-saga для обработки внешних событий без redux. Рассмотрю метод runSaga(...) подробнее:

runSaga(options, saga, ...args)

saga - это метод, в котором будет выполняться логика;

args - аргументы, которые будут переданы в saga;

options - объект, который "настраивает" работу redux-saga. Для данного хука использую всего три настройки:

channel - канал, из которого будут поступать внешние события;

dispatch - это метод, который при возникновении события, должен послать redux-saga эффект с помощью put.

getState - функция, которая используется для выборки данных из state, с которым используется redux-saga. В случае с хуком это будет локальный state.

Вариант 6. Redux-saga как канал обработки внешних сообщений

Логика работы хука с saga будет такова. Создаётся канал channel (камеры хранения) для хранения эффектов в процессе работы redux-saga. Создаётся канал, в который будут поступать внешние события от изображений - eventsChannel. Это два разных канала! Вернусь к аналогии с камерами хранения.

Создаются камеры хранения (channel), которым потом будет назначен администратор (redux-saga)

const sagaChannelRef = useRef(stdChannel());

При запуске runSaga() redux-saga назначается администратором созданных камер хранения.

runSaga(  {    channel: sagaChannelRef.current,    dispatch: () => {},    getState: () => {},  },  saga);

Камеры хранения созданы (channel), администратор назначен (redux-saga) и далее этим всем начинает пользоваться пассажир (происходит запуск функции-генератора saga)

Первым делом пассажир (функция-генератор saga) создает канал для внешних событий (конечно этих каналов может быть несколько от разных источников).

const eventsChannel = yield call(getImageLoadingSagas, imgArray);
function getImageLoadingSagas(imagesArray) {  return eventChannel((emit) => {    for (const img of imagesArray) {      const imageChecker = new Image();      imageChecker.addEventListener("load", () => {        emit(true);      });      imageChecker.addEventListener("error", () => {        emit(true);      });      imageChecker.src = img.url;    }    setTimeout(() => {      //закрытие канала по таймеру      emit(END);    }, 100000);    return () => {    };  }, buffers.expanding(10));}

Т.е. пассажир (функция-генератор saga) просит диспетчера (redux-saga) принимать сообщения не только от посыльного put, но и от другого источника (eventsChannel). Сообщения от этого источника (eventChannel) у диспетчера (redux-saga) будет забирать, специально выделенный для этого, посыльный take, который стоит рядом с диспетчером и ждёт от него сообщения.

yield take(eventsChannel);

Как только диспетчеру (redux-saga) приходит сообщение от eventChannel, он тут же отдает его take, который возвращает сообщение пассажиру (функции-генератору saga). Сам take остаётся рядом с пассажиром и ждёт от него указаний.

Пассажир (функция-генератор saga) отдает это сообщение на обработку другому пассажиру (функции-генератору putCounter) с помощью call(). Это означает, что пассажир saga (функция-генератор saga) будет ожидать, пока пассажир putCounter (функция-генератор putCounter) не освободится (т.е. saga блокируется, пока не отработает функция putCounter).

yield call(putCounter);
function* putCounter() {  dispatch({    type: ACTIONS.SET_COUNTER,    data: stateRef.current.counter + stateRef.current.counterStep,  });  yield take((action) => {    return action.type === "STATE_UPDATED";  });}

Чем занимается пассажир putCounter (функция-генератор putCounter). Диспатчит действие состояния хука и затем посылает посыльного take к диспетчеру (redux-saga) за сообщением с типом STATE_UPDATED и ждёт этого посыльного.

В этом месте остановимся и ещё раз опишем получившийся хоровод (сделаем срез на этот момент).

Посыльный take(eventChannel) стоит (ожидает пока не выполнится итерация цикла в функции-генераторе saga) рядом с пассажиром saga (функцией-генератором saga). Пассажир saga (функция-генератор saga) ожидает пока пассажир putCounter (функция-генератор putCounter) не освободится. Пассажир putCounter (функция-генератор putCounter), в свою очередь, ждёт посыльного take, который стоит рядом с диспетчером (redux-saga) и ждёт посыльного put, который должен принести сообщение с типом STATE_UPDATED. Короче "Дом, который построил Джек".

Таким образом весь хоровод "застыл" в ожидании одного-единственного сообщения STATE_UPDATED. Кстати, в канале eventChannel могут возникать события во время этого застывшего состояния. Если не использовать буфер с каналом eventChannel, то эти события останутся незамеченными для нашего диспетчера (redux-saga). Но буфер у нас есть, поэтому в это время в тамбуре перед камерами хранения (буфере) толпятся сообщения от eventChannel.

И этого посыльного put отправляет хук useEffect

useEffect(() => {...    sagaChannelRef.current.put({ type: "STATE_UPDATED" }); ...}, [state]);

Посыльный put приносит сообщение STATE_UPDATED диспетчеру (redux-saga).

Диспетчер (redux-saga) отдаёт его take, которого прислал пассажир putCounter.

Пассажир putCounter сообщает пассажиру saga, что он освободился.

Пассажир saga, отправляет посыльного take за следующим сообщением от eventChannel

Take забирает следующее сообщение у диспетчера, который взял его из буфера.

Круг замкнулся.

Исходный код хука с redux-saga в качестве канала обработки событий
import { useReducer, useEffect, useRef } from "react";import { reducer, initialState, ACTIONS } from "./state";import { runSaga, eventChannel, stdChannel, buffers, END } from "redux-saga";import { call, take } from "redux-saga/effects";const PRELOADER_SELECTOR = ".preloader__wrapper";const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";const usePreloader = () => {  const [state, dispatch] = useReducer(reducer, initialState);  const stateRef = useRef(state);  const sagaChannelRef = useRef(stdChannel());  const preloaderEl = document.querySelector(PRELOADER_SELECTOR);  const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);  useEffect(() => {    const imgArray = document.querySelectorAll("img");    if (imgArray.length > 0) {      dispatch({        type: ACTIONS.SET_COUNTER_STEP,        data: Math.floor(100 / imgArray.length) + 1,      });      function* putCounter() {        dispatch({          type: ACTIONS.SET_COUNTER,          data: stateRef.current.counter + stateRef.current.counterStep,        });        yield take((action) => {          return action.type === "STATE_UPDATED";        });      }      function* saga() {        const eventsChannel = yield call(getImageLoadingSagas, imgArray);        try {          while (true) {            yield take(eventsChannel);            yield call(putCounter);          }        } finally {          //channel closed        }      }      runSaga(        {          channel: sagaChannelRef.current,          dispatch: () => {},          getState: () => {},        },        saga      );    }  }, []);  useEffect(() => {    stateRef.current = state;    if (stateRef.current.counterStep != 0 && stateRef.current.counter != 0) {      sagaChannelRef.current.put({ type: "STATE_UPDATED" });    }    if (counterEl) {      stateRef.current.counter < 100        ? (counterEl.innerHTML = `${stateRef.current.counter}%`)        : hidePreloader(preloaderEl);    }  }, [state]);  return;};function getImageLoadingSagas(imagesArray) {  return eventChannel((emit) => {    for (const img of imagesArray) {      const imageChecker = new Image();      imageChecker.addEventListener("load", () => {        emit(true);      });      imageChecker.addEventListener("error", () => {        emit(true);      });      imageChecker.src = img.url;    }    setTimeout(() => {      //закрытие канала по таймеру      emit(END);    }, 100000);    return () => {          };  }, buffers.expanding(10));}const hidePreloader = (preloaderEl) => {  preloaderEl.remove();};export default usePreloader;

Я был трезвый, когда это писал. Чтобы прочитать, пить тоже не нужно.

Вариант 7. Redux-saga + useReducer = useReducerAndSaga

В варианте 6 сага использовалась исключительно как менеджер событий. Мне хотелось подключить его к управлению state хука. Погуглив нашёл такой вариант хука useReducerAndSaga

Описывать на примере камер хранения не буду, просто приведу исходный код

Исходный код useReducerAndSaga.js
import { useReducer, useEffect, useRef } from "react";import { runSaga, stdChannel, buffers } from "redux-saga";export function useReducerAndSaga(reducer, state0, saga, sagaOptions) {  const [state, reactDispatch] = useReducer(reducer, state0);  const sagaEnv = useRef({ state: state0, pendingActions: [] });  function dispatch(action) {    console.log("useReducerAndSaga: react dispatch", action);    reactDispatch(action);    console.log("useReducerAndSaga: post react dispatch", action);    // dispatch to sagas is done in the commit phase    sagaEnv.current.pendingActions.push(action);  }  useEffect(() => {    console.log("useReducerAndSaga: update saga state");    // sync with react state, *should* be safe since we're in commit phase    sagaEnv.current.state = state;    const pendingActions = sagaEnv.current.pendingActions;    // flush any pending actions, since we're in commit phase, reducer    // should've handled all those actions    if (pendingActions.length > 0) {      sagaEnv.current.pendingActions = [];      console.log("useReducerAndSaga: flush saga actions");      pendingActions.forEach((action) => sagaEnv.current.channel.put(action));      sagaEnv.current.channel.put({ type: "REACT_STATE_READY", state });    }  });  // This is a one-time effect that starts the root saga  useEffect(() => {    sagaEnv.current.channel = stdChannel();    const task = runSaga(      {        ...sagaOptions,        channel: sagaEnv.current.channel,        dispatch,        getState: () => {          return sagaEnv.current.state;        }      },      saga    );    return () => task.cancel();  }, []);  return [state, dispatch];}

Все генераторы были выдесены в отдельный файл sagas.js

Исходный код sagas.js
import { eventChannel, buffers } from "redux-saga";import { call, select, take, put } from "redux-saga/effects";import { ACTIONS, getCounterStep, getCounter, END } from "./state";export const getImageLoadingSagas = (imagesArray) => {  return eventChannel((emit) => {    for (const img of imagesArray) {      const imageChecker = new Image();            imageChecker.addEventListener("load", () => {        emit(true);      });      imageChecker.addEventListener("error", () => {        emit(true);      });      imageChecker.src = img.src;    }    setTimeout(() => {      //закрытие канала по таймеру      emit(END);    }, 100000);    return () => {};  }, buffers.fixed(20));};function* putCounter() {  const currentCounter = yield select(getCounter);  const counterStep = yield select(getCounterStep);  yield put({ type: ACTIONS.SET_COUNTER, data: currentCounter + counterStep });  yield take((action) => {    return action.type === "REACT_STATE_READY";  });}function* launchLoadingEvents(imgArray) {  const eventsChannel = yield call(getImageLoadingSagas, imgArray);  while (true) {    yield take(eventsChannel);    yield call(putCounter);  }}export function* saga() {  while (true) {    const { data } = yield take(ACTIONS.SET_IMAGES);    yield call(launchLoadingEvents, data);  }}

Немножко изменился state. Был добавлен action SET_IMAGES и селекторы для counter и counterStep

Исходный код state.js
const SET_COUNTER = "SET_COUNTER";const SET_COUNTER_STEP = "SET_COUNTER_STEP";const SET_IMAGES = "SET_IMAGES";export const initialState = {  counter: 0,  counterStep: 0,  images: [],};export const reducer = (state, action) => {  switch (action.type) {    case SET_IMAGES:      return { ...state, images: action.data };    case SET_COUNTER:      return { ...state, counter: action.data };    case SET_COUNTER_STEP:      return { ...state, counterStep: action.data };    default:      throw new Error("This action is not applicable to this component.");  }};export const ACTIONS = {  SET_COUNTER,  SET_COUNTER_STEP,  SET_IMAGES,};export const getCounterStep = (state) => state.counterStep;export const getCounter = (state) => state.counter;

Благодаря этому рефакторингу, код хука usePreloader стал компактным.

Исходный код usePreloader.js
import { useEffect } from "react";import { reducer, initialState, ACTIONS } from "./state";import { useReducerAndSaga } from "./useReducerAndSaga";import { saga } from "./sagas";const PRELOADER_SELECTOR = ".preloader__wrapper";const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";const usePreloader = () => {  const [state, dispatch] = useReducerAndSaga(reducer, initialState, saga);  const preloaderEl = document.querySelector(PRELOADER_SELECTOR);  const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);  useEffect(() => {    const imgArray = document.querySelectorAll("img");    if (imgArray.length > 0) {      dispatch({        type: ACTIONS.SET_COUNTER_STEP,        data: Math.floor(100 / imgArray.length) + 1,      });      dispatch({        type: ACTIONS.SET_IMAGES,        data: imgArray,      });    }  }, []);  useEffect(() => {    if (counterEl) {      state.counter < 100        ? (counterEl.innerHTML = `${state.counter}%`)        : hidePreloader(preloaderEl);    }  }, [state.counter]);  return;};const hidePreloader = (preloaderEl) => {  preloaderEl.remove();};export default usePreloader;

Итого

В этой части статьи показано:

  • что такое redux-saga

  • как использовать redux-saga без redux

  • как использовать redux-saga для управления состоянием хука

Ссылка напесочницу

Ссылка нарепозиторий

Продолжение следует... RxJS...

Подробнее..
Категории: Javascript , Reactjs , Hook , Redux-saga , Put , Take , Channel , Eventchannel

Что выбрать глобальные переменные или useThis?

16.02.2021 10:21:44 | Автор: admin

Привет Хабр!

Как вы знаете при переходе с компонентов классов на функциональные, у нас отняли такую полезную вещь как this, которая указывает на текущий экземпляр компонента. И конечно у нас возник вопрос: а где же тогда хранить timeoutId?. И я видел как люди по разному выкручивались из этой проблемы (Данная статья, является расшифровкой видео)

Например, если timeoutId используется только в рамках одного useEffect можно набросать следующий вариант:

useEffect(() => {  const timeout = setTimeout(() => {    // do some action  }, 3000);    return () => {    clearTimeout(timeout);  }}, [...]);

Данный подход работает, но когда появляется нужда очищать timeout по клику на кнопку, этот подход уже не работает.

Поэтому многие решили, просто создавать переменную вне компонента и хранить в ней id этого тайм-аута, чтобы например по клику иметь возможность его отменить:

let timeout;const Test = () => {  const onClick = () => clearTimeout(timeout);    useEffect(() => {    timeout = setTimeout(() => {      // do some action    }, 3000);  }, [...]);      return (...);}

И это работает в большинстве случаев без каких-либо проблем. Но как всегда есть НО.

Проблема глобальных переменных

Давайте рассмотрим пример. Допустим у нас есть компонент Counter, в котором есть локальный счетчик и глобальный счетчик определенный вне компонента:

let globalCounter = 0;const Counter = () => {  const [stateCounter, setStateCounter] = useState(0);    const onClick = () => {    globalCounter++;    setStateCounter((stateCounter) => stateCounter + 1);  };    return (    <div>      <p>global counter - <b>{globalCounter}</b></p>      <p>state counter - <b>{stateCounter}</b></p>      <button onClick={onClick}>increment</button>    </div>  );}

Компонент достаточно простой. Теперь добавим родительский компонент:

const App = () => {  const [countersNumber, setCountersNumber] = useState(0);    return (    <div>      <button onClick={setCountersNumber((count) => count + 1)}>        add      </button>      <button onClick={setCountersNumber((count) => count - 1)}>        removed      </button>      {[...Array.from(countersNumber).keys()].map((index) => (        <Counter key={index} />      ))}    </div>  );};

Здесь мы храним в state количество счетчиков, и ниже имеем 2 кнопки: для увеличения количества счетчиков и для уменьшения. И собственно вставляем сами счетчики в таком количество, как у нас указано в переменной countersNumber.

Смотрим результат

Перейдем в браузер и выполним следующие действия:

  • Добавим один счетчик;

  • Внутри появившегося счетчика, нажмем "increment" три раза;

  • Добавим второй счетчик.

Как вы видите у второго глобального каунтера значение три. И не важно сколько счетчиков вы создадите, они все замыкаются на одну и ту же глобальную переменную. И даже если вы удалите все счетчики, а потом добавите новый счетчик, конечно же глобальный каунтер все равно будет равен трем, так как глобальная переменная создается при запуске сайта и очищается при полном закрытии табы в браузере с вашим сайтом.

Таким образом, хранение timeoutId в такого рода глобальной переменной, может привести к странным багам. Да и если у вас в проекте огромное количество таких переменных, вы бесполезно засоряете память вашего компьютера, хоть это возможно и экономия на спичках.

Рассмотрим альтернативу

Решением данной проблемы является использование хука useRef(). Именно это и рекомендует React документация:

Они прямо упомянули, что useRef() нужно использовать как аналог this. И более того, для удобства добавили в useRef() возможность передачи начального значения. Поэтому вариант с timeout может выглядеть следующим образом:

const Test = () => {  const timeout = useRef();    const onClick = () => clearTimeout(timeout.current);    useEffect(() => {    timeout.current = setTimeout(() => {      // do some action    }, 3000);  }, [...]);    return (...);}

Возможно в этом решении вас смущает, то что в timeout начинает хранится свойство current, это действительно выглядит немного странно, но у этого есть разумное объяснение, о котором мы рассказывали в предыдущей статье createRef, setRef, useRef и зачем нужен current в ref.

prevProps не исчезли вместе с классами

Использование useRef() для хранения timeout это конечно же очень полезно. Но есть и более интересные способы использования. Например, в компонентах в виде классов есть удобный метод жизненного цикла componentDidUpdate. В качестве первого параметра нам предоставляют prevProps, т.е. props из предыдущей итерации. Это давало нам возможность, сравнивать props из текущей итерации с props из предыдущей. На основании этого выполнять какие-то действия.

componentDidUpdate(prevProps) {  if (this.props.id !== prevProps.id) {    // do some action  }}

Если вам кажется, что эту функциональность у вас отняли безвозвратно, тогда вы ошибаетесь.

Давайте напишем хук, который будет возвращать props из предыдущей итерации:

const useGetPrevValue = (value) => {  const prevValueRef = useRef();  useEffect(() => {    prevValueRef.current = value;  });    return prevValueRef.current;};

Здесь мы получаем value из текущей итерации, после создадим ref для хранения данных между итерациями. И в рамках текущей итерации мы вернем текущее значение current равное null. Но перед началом следующей итерации мы обновим current значение, таким образом в следующей итерации в ref у нас будет хранится значение из предыдущей.

И осталось только использовать этот хук:

const CounterView = ({ counter }) => {  const prevCount = useGetPrevValue(counter);    const classes = classNames({    [styles.greenCounter]: counter < prevCounter,    [styles.redCounter] counter > prevCounter,  });    ...}

В итоге, имея значение counter из предыдущей итерации и из текущей итерации, мы можем покрасить в зеленый цвет, если counter уменьшился, или в красный, если counter увеличился.

Расширяйте сознание

Мы упомянули лишь несколько базовых вариантов использования ref для решения нетипичных для ref задач. Но их гораздо больше. Например, если вы откроете телеграм десктоп версии, и будете скролить вверх, то у вас будет отображаться дата, а если вниз, она исчезнет. Мы можем хранить предыдущее значение скрола в ref, для того чтобы вычислить в какую же сторону скролит пользователь.

Как вы уже догадались, использование ref ограничено лишь вашей фантазией и вы найдете ему гораздо больше вариантов применения, чем мы упомянули в этой статье.

И если вы знаете еще какие-то интересные варианты использования ref обязательно пишите в комментариях

Подробнее..
Категории: Javascript , React , Reactjs , Hook , React hooks , Memoization

Блокируем заливку приватных ключей, архивов, больших файлов и не только в Gitlab CE

06.07.2020 10:05:56 | Автор: admin

Git hooks инструмент, помогающий держать в порядке ваш репозиторий. Можно настроить автоматические правила оформления ваших коммитов.


Все вы наверное знаете про pre-commit проверку вашего кода перед коммитом. Но ведь не все можно проверить перед коммитом. Некоторые ограничения хочется использоваться глобально на всем Gitlab.


Кто запутался в pre-commit и pre-receive хуках, в этом посте описываются различия между ними https://blog.gitguardian.com/git-hooks-automated-secrets-detection/ в абзаце "What are git hooks?".


Если у вас Gitlab Enterprise Edition, вы можете настроить хуки, которые описаны в посте через WEB интерфейс.


Но что делать, если у вас Gitlab Community Edition?


В этой статье будут описаны 5 pre-receive хуков, которые выполняются на сервере Gitlab Community Edition:


  • block_confidentials.sh Блокирование отправки приватных ключей и AWS токенов
  • block_file_extensions.sh Блокирование отправки архивов (Regex настраивается)
  • check-large-files.sh Блокирование отправки больших файлов (Размер настраивается)
  • reject-not-allowlist-email.sh Блокирование коммитов с email не из allow списка (Список email доменов настраивается)
  • require-issue.sh Блокирование коммитов без issue в названии (Список issue настраивается)

В основном хуки взяты из репозитория platform-samples в директории pre-receive-hooks (относится к GitHub Enterprise).


Весь исходный код серверных хуков вы можете посмотреть на Github https://github.com/patsevanton/git-server-pre-receive-hooks


Установка на Gitlab


  • Необходимо создать директорию /opt/gitlab/embedded/service/gitlab-shell/hooks/pre-receive.d/
  • Скопировать в эту директорию хуки
  • Не забыть выставить права запуска для хуков (chmod +x файл-хука)

Блокирование отправки приватных ключей и AWS токенов


В файле block_confidentials.sh настраиваем список regex_list, который описывает конфиденциальную информацию.


# Define list of REGEX to be searched and blockedregex_list=(  # block any private key file  '(\-){5}BEGIN\s?(RSA|OPENSSH|DSA|EC|PGP)?\s?PRIVATE KEY\s?(BLOCK)?(\-){5}.*'  # block AWS API Keys  'AKIA[0-9A-Z]{16}'  # block AWS Secret Access Key (TODO: adjust to not find validd Git SHA1s; false positives)  # '([^A-Za-z0-9/+=])?([A-Za-z0-9/+=]{40})([^A-Za-z0-9/+=])?'  # block confidential content  'CONFIDENTIAL')

Добавляем в репозиторий приватный ключ, делаем коммит и при git push получаем ошибку.



Блокирование отправки архивов


В файле block_file_extensions.sh настраиваем case *.zip|*.gz|*.tgz, в котором указываются расширения файлов, которые будут блокироваться.


Добавляем в репозиторий zip архив, делаем коммит и при git push получаем ошибку.



Блокирование отправки больших файлов


В файле check-large-files.sh настраиваем параметр maxsize, который указывает размер файла в мегабайтах, выше которого отправка будет блокироваться.


Добавляем в репозиторий файл больше 1 мегабайта, делаем коммит и при git push получаем ошибку.



Блокирование коммитов с email не из allow списка


В файле reject-not-allowlist-email.sh настраиваем список email-доменов, для которых разрешены коммиты.


declare -a DOMAIN_ARRAY=("group1.com" "group2.com")

Меняем почту в git на ту, которой нет в разрешенном списке.


git config user.email user1@group3.com

Добавляем в репозиторий любой файл, делаем коммит и при git push получаем ошибку.



Блокирование коммитов без issue в названии


Этот серверный хук был взят из блога Majilesh.


В файле require-issue.sh настраиваем список commit_format, для которых разрешены коммиты.


commit_format="(JIRA|PROJECTKEY|MULE|ECOM|SAP|XLR-[1-9]+Merge)"

Добавляем в репозиторий любой файл, делаем коммит, в названии которого нет слов из commit_format и при git push получаем ошибку.



Надеюсь что мой пост сподвигнет сообщество развивать направление серверных хуков.


Telegram чат по Gitlab https://t.me/ru_gitlab

Подробнее..

Перед коммитом

07.01.2021 10:12:31 | Автор: admin

Бывают определенные действия, которые надо обязательно выполнить до того как код попадет в коммит и репозиторий, чтобы, как минимум, не мучиться потом с rebase каждый раз. Решение, которое я опишу короткое и простое, но оно весьма упрощает жизнь разработчика.

Самое очевидное назначение - это выполнение тестов, но для меня это также и сборка бандла. Поскольку я не использую watch и регулярно забываю запускать команду билда руками, в коммит частенько заезжают неактуальные версии бандлов, а актуальные приходится коммитить следом. Выходит не очень опрятно. Для javascript нам поможет утилита pre-commit. Она использует hook гита, но дает возможность не лезть во все эти внутренности и управлять галактикой не выходя из npm.

Пойдем практическим путем: добавляем библиотеку в проект

npm i pre-commit --save-dev

В package.json в секцию pre-commit добавляем комманду build

"pre-commit": [ "build"],

Однако, собранные файлы бандла не попадут в коммит, т.к. гиту надо явно указывать измененные и добавленные файлы и добавлять все подряд выглядит довольно опасным. Чтобы все начало правильно работать определим команду add-bundles добавляющую файлы бандла.

"scripts": { "build": "npx rollup --config rollup.config.js", "add-bundles": "git add dist/*", "test": "echo \"Error: no test specified\" && exit 1"},

И саму команду добавляем в секцию pre-commit

"pre-commit": [ "build", "add-bundles"],

Теперь прямо перед коммитом сразу собирается бандл и добавляется в коммит. Если нужен полный работающий пример, он есть в репозитории этой библиотечки: https://github.com/syncro/key-toggler

Подробнее..

Перехват и обработка событий в файловой системе Linux

20.01.2021 10:13:24 | Автор: admin

Введение

В предыдущей статье мы рассмотрели сборку и установку пакета на Linux системах, в которой упомянули про Linux Kernel Module (LKM) и обещали раскрыть позднее подробности о пути к нему и его создании. Ну что ж, настало его время. LKM мы выбираем тебя.

Необходимость реализации

"Windows драйвер мы заменили на Linux Kernel Module LKM" итак, вернёмся мысленно к самому началу пути. Мы имеем Windows драйвер, который обеспечивает отслеживание и перехват событий обращения к файлу. Как его перенести или чем заменить в Linux системах? Покопавшись в архитектуре, почитав про перехват и реализацию подобных технологий в Linux мы поняли, что задача абсолютно нетривиальная, содержащая кучу подводных камней.

Inotify

Закинув удочки на пару форумов, посоветовавшись с коллегами, было принято решение копать в сторону Inotify. Inotify файловый монитор, который логирует события в системе уже после того, как они произошли. Но у него есть брат fanotify. В нём мы можем добавить ограничение доступности на события открытия, копирования файла. Но нам необходимо иметь такую же возможность и для событий удаления, переименования, перемещения, а, следовательно, fanotify нам в этом не поможет. Хочу заметить, что fanotify это userspace утилита, соответственно при её использовании нет проблем с платформопереносимостью.

Virtual File System

Следующим этапом изучения стала возможность реализации перехвата обращений при помощи VFS.

После анализа VFS на основе Dtrace, eBPF и bcc, стало понятно, что при использовании данной технологии возможно выполнять мониторинг событий, происходящих в системе. В данном случае, перехват осуществляется через LKM. В рамках изучения реализации различных модулей под разные ядра выявлено следующее: перехват не всегда позволяет отследить полный путь к файлу; при перехвате обращения к файлу через открытое приложение, а не из проводника, отсутствует путь к файлу в аргументах; для каждого ядра необходима своя реализация.

Janus, SElinux и AppArmor

В ходе исследования, была найдена статья по расширению функциональности системы безопасности ядра Linux. Отсюда следует, что на рынке существует достаточное количество решений. Самым легко реализуемым является Janus. Минусом решения выступает отсутствие поддержки свежих ядер и все вышеописанные проблемы LKM хука. Реализация SELinux и AppArmor представляет квинтэссенцию всего описанного и изученного ранее. Модуль SELinux включает в себя основные компоненты: сервер безопасности; кэш вектора доступа (англ. Access Vector Cache, AVC); таблицы сетевых интерфейсов; код сигнала сетевого уведомления; свою виртуальную файловую систему (selinuxfs) и реализацию функций-перехватчиков.

Долгожданное решение

После всех этих бесконечных но, на помощь нам пришёл Хабр! Наткнувшись на статью, стало ясно, что это наш случай.

Обработка перехвата

Изучив предложенные данные по ftrace и реализации из самой статьи, сделали аналогичный LKM модуль на базе ftrace. Данная утилита, в свою очередь, работает на базе файловой системы debugfs, которая в большинстве современных дистрибутивов Linux смонтирована по умолчанию. Hook'и добавили на события к уже имеющимся clone и open: openat, rename, unlink, unlinkat. Таким образом, удалось обработать открытие, переименование, перемещение, копирование, удаление файла.

Взаимодействие

Теперь нам нужно реализовать связь между модулем ядра и приложением userspace. Для решения данной задачи существуют разные подходы, но в основном выделяют два: socket между kernel и userspace; запись/чтение в системной директории в файл.

В итоге, мы выбрали netlink socket, так как в Windows мы используем аналогичный интерфейс - FltSendMessage. Можно было использовать inet socket, но это наименее защищённое решение. Также столкнулись с такой проблемой, что на .Net Core, на которой реализовано userspace приложение, отсутствует реализация netlink.

Поэтому пришлось реализовывать динамическую библиотеку с реализацией netlink и уже её подключать в проект.

int open_netlink_connection(void){    //initialize our variables    int sock;    struct sockaddr_nl addr;    int group = NETLINK_GROUP;    //open a new socket connection    sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_USERSOCK);    //if the socket failed to open,    if (sock < 0)     {        //inform the user        printf("Socket failed to initialize.\n");        //return the error value        return sock;    }    //initialize our addr structure by filling it with zeros    memset((void *) &addr, 0, sizeof(addr));    //specify the protocol family    addr.nl_family = AF_NETLINK;    //set the process id to the current process id    addr.nl_pid = getpid();    //bind the address to the socket created, and if it failed,    if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0)     {        //inform the user        printf("bind < 0.\n");        //return the function with a symbolic error code        return -1;    }    //set the option so that we can receive packets whose destination    //is the group address specified (so that we can receive the message broadcasted by the kernel)    if (setsockopt(sock, 270, NETLINK_ADD_MEMBERSHIP, &group, sizeof(group)) < 0)     {        //if it failed, inform the user        printf("setsockopt < 0\n");        //return the function with a symbolic error code        return -1;    }    //if we got thus far, then everything    //went fine. Return our socket.    return sock;}char* read_kernel_message(int sock){    //initialize the variables    //that we are going to need    struct sockaddr_nl nladdr;    struct msghdr msg;    struct iovec iov;    char* buffer[CHUNK_SIZE];    char* kernelMessage;    int ret;    memset(&msg, 0, CMSG_SPACE(MAX_PAYLOAD));    memset(&nladdr, 0, sizeof(nladdr));    memset(&iov, 0, sizeof(iov));    //specify the buffer to save the message    iov.iov_base = (void *) &buffer;    //specify the length of our buffer    iov.iov_len = sizeof(buffer);    //pass the pointer of our sockaddr structure    //that will save the source IP and port of the connection    msg.msg_name = (void *) &(dest_addr);    //give the size of our structure    msg.msg_namelen = sizeof(dest_addr);    //pass our scatter/gather I/O structure pointer    msg.msg_iov = &iov;    //we will pass only one buffer array,    //therefore we will specify that here    msg.msg_iovlen = 1;    //listen/wait for new data    ret = recvmsg(sock, &msg, 0);    //if message was received successfully,    if(ret >= 0)    {        //get the string data and save them to a local variable        char* buf = NLMSG_DATA((struct nlmsghdr *) &buffer);        //allocate memory for our kernel message        kernelMessage = (char*)malloc(CHUNK_SIZE);        //copy the kernel data to our allocated space        strcpy(kernelMessage, buf);        //return the pointer that points to the kernel data        return kernelMessage;    }        //if we got that far, reading the message failed,    //so we inform the user and return a NULL pointer    printf("Message could not received.\n");    return NULL;}int send_kernel_message(int sock, char* kernelMessage){    //initialize the variables    //that we are going to need    struct msghdr msg;    struct iovec iov;    char* buffer[CHUNK_SIZE];        int ret;    memset(&msg, 0, CMSG_SPACE(MAX_PAYLOAD));    memset(&iov, 0, sizeof(iov));    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));    memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);    nlh->nlmsg_pid = getpid();    nlh->nlmsg_flags = 0;    char buff[160];    snprintf(buff, sizeof(buff), "From:DSSAgent;Action:return;Message:%s;", kernelMessage);    strcpy(NLMSG_DATA(nlh), buff);    iov.iov_base = (void *)nlh;    iov.iov_len = nlh->nlmsg_len;    //pass the pointer of our sockaddr structure    //that will save the source IP and port of the connection    msg.msg_name = (void *) &(dest_addr);    //give the size of our structure    msg.msg_namelen = sizeof(dest_addr);    msg.msg_iov = &iov;    msg.msg_iovlen = 1;    printf("Sending message to kernel (%s)\n",(char *)NLMSG_DATA(nlh));    ret = sendmsg(sock, &msg, 0);    return ret;}int sock_netlink_connection(){sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);    if (sock_fd < 0)        return -1;    memset(&src_addr, 0, sizeof(src_addr));    src_addr.nl_family = AF_NETLINK;    src_addr.nl_pid = getpid(); /* self pid */    bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr));    memset(&dest_addr, 0, sizeof(dest_addr));    dest_addr.nl_family = AF_NETLINK;    dest_addr.nl_pid = 0; /* For Linux Kernel */    dest_addr.nl_groups = 0; /* unicast */    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));    memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);    nlh->nlmsg_pid = getpid();    nlh->nlmsg_flags = 0;    strcpy(NLMSG_DATA(nlh), "From:DSSAgent;Action:hello;");    iov.iov_base = (void *)nlh;    iov.iov_len = nlh->nlmsg_len;    msg.msg_name = (void *)&dest_addr;    msg.msg_namelen = sizeof(dest_addr);    msg.msg_iov = &iov;    msg.msg_iovlen = 1;    printf("Sending message to kernel\n");    sendmsg(sock_fd, &msg, 0);    printf("Waiting for message from kernel\n");    /* Read message from kernel */    recvmsg(sock_fd, &msg, 0);    printf("Received message payload: %s\n", (char *)NLMSG_DATA(nlh));return sock_fd;}void sock_netlink_disconnection(int sock){close(sock);    free(nlh);}

Также, в дальнейшем оказалось, что некоторые функции отсутствуют в Net.Core например поиск по pid процесса имени пользователя, которому принадлежит этот процесс. Примеров данной реализации оказалась масса, но, в рамках нашего приложения, не удалось их реализовать. Поэтому реализовали в той же библиотеке свою функцию нахождения uid пользователя, по которому используя системные функции можно найти имя.

char* get_username_by_pid(int pid){   register struct passwd *pw;  register uid_t uid;  int c;  FILE *fp;  char filename[255];  sprintf(filename, "/proc/%d/loginuid", pid);  char cc[8];    // чтение из файла  if((fp= fopen(filename, "r"))==NULL)    {        perror("Error occured while opening file");        return "";    }  // считываем, пока не дойдем до конца  while((fgets(cc, 8, fp))!=NULL) {}       fclose(fp);    uid = atoi(cc);  pw = getpwuid (uid);  if (pw)  {      return pw->pw_name;  }  else  {      return "";  }}

Доработка модуля

По итогу добавили соединение по netlink в инициализацию LKM.

static int fh_init(void){    int err;struct netlink_kernel_cfg cfg ={#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 6, 0).groups = 1,#endif.input = nl_recv_msg,};#if LINUX_VERSION_CODE > KERNEL_VERSION(2, 6, 36)nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg);#elif LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 32)nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, 0, nl_recv_msg, NULL, THIS_MODULE);#elsenl_sk = netlink_kernel_create(NETLINK_USER, 0, nl_recv_msg, THIS_MODULE);#endifif (!nl_sk){printk(KERN_ERR "%s Could not create netlink socket\n", __func__);return 1;}err = fh_install_hooks(hooks, ARRAY_SIZE(hooks));if (err)return err;p_list_hook_files = (tNode *)kmalloc(sizeof(tNode), GFP_KERNEL);p_list_hook_files->next = NULL;p_list_hook_files->value = 0;pr_info("module loaded\n");return 0;}module_init(fh_init);static void fh_exit(void){delete_list(p_list_hook_files);fh_remove_hooks(hooks, ARRAY_SIZE(hooks));netlink_kernel_release(nl_sk);pr_info("module unloaded\n");}module_exit(fh_exit);

Socket ожидает перехвата события обращения к файлу. Модуль, перехватывая событие, передаёт имя файла, pid и имя процесса. Userspace приложение, получая данную информацию, обрабатывает её и отвечает, что делать с файлом (блокировать или разрешать доступ). Впоследствии модуль возвращает соответствующий системный вызов.

static void send_msg_to_user(const char *msgText){int msgLen = strlen(msgText);struct sk_buff *skb = nlmsg_new(NLMSG_ALIGN(msgLen), GFP_KERNEL);if (!skb){printk(KERN_ERR "%s Allocation skb failure.\n", __func__);return;}struct nlmsghdr *nlh = nlmsg_put(skb, 0, 1, NLMSG_DONE, msgLen, 0);if (!nlh){printk(KERN_ERR "%s Create nlh failure.\n", __func__);nlmsg_free(skb);return;}NETLINK_CB(skb).dst_group = 0;strncpy(nlmsg_data(nlh), msgText, msgLen);int errorVal = nlmsg_unicast(nl_sk, skb, pid);if (errorVal < 0)printk(KERN_ERR "%s nlmsg_unicast() error: %d\n", __func__, errorVal);}static void return_msg_to_user(struct nlmsghdr *nlh){pid = nlh->nlmsg_pid;const char *msg = "Init socket from kernel";const int msg_size = strlen(msg);struct sk_buff *skb = nlmsg_new(msg_size, 0);if (!skb){printk(KERN_ERR "%s Failed to allocate new skb\n", __func__);return;}nlh = nlmsg_put(skb, 0, 0, NLMSG_DONE, msg_size, 0);NETLINK_CB(skb).dst_group = 0;strncpy(nlmsg_data(nlh), msg, msg_size);int res = nlmsg_unicast(nl_sk, skb, pid);if (res < 0)printk(KERN_ERR "%s Error while sending back to user (%i)\n", __func__, res);}

Позднее, для работы другого приложения в данный модуль, добавили возможность блокирования доступа к определённому файлу (по его полному пути) для всех процессов, кроме определённого (определяем по pid процесса).

static void parse_return_from_user(char *return_msg){char *msg = np_extract_value(return_msg, "Message", ';');const char *file_name = strsep(&msg, "|");printk(KERN_INFO "%s Name:(%s) Permiss:(%s)\n", __func__, file_name, msg);if (strstr(msg, "Deny"))reload_name_list(p_list_hook_files, file_name, Deny);elsereload_name_list(p_list_hook_files, file_name, Allow);}static void free_guards(void){// Possibly unpredictable behavior during cleaningmemset(&guards, 0, sizeof(struct process_guards));}static void change_guards(char *msg){char *path = np_extract_value(msg, "Path", ';');char *count_str = np_extract_value(msg, "Count", ';');if (path && strlen(path) && count_str && strlen(count_str)){int i, found = -1;for (i = 0; i < guards.count; ++i)if (guards.process[i].file_path && !strcmp(path, guards.process[i].file_path))found = i;guards.is_busy = 1;int count;kstrtoint(count_str, 10, &count);if (count > 0){if (found == -1){strcpy(guards.process[guards.count].file_path, path);found = guards.count;guards.count++;}for (i = 0; i < count; ++i){char buff[8];snprintf(buff, sizeof(buff), "Pid%d", i + 1);char *pid = np_extract_value(msg, buff, ';');if (pid && strlen(pid))kstrtoint(pid, 10, &guards.process[found].allow_pids[i]);elseguards.process[found].allow_pids[i] = 0;}guards.process[found].allow_pids[count] = 0;}else{if (found >= 0){for (i = found; i < guards.count - 1; ++i)guards.process[i] = guards.process[i + 1];guards.count--;}}guards.is_busy = 0;}}// Example message is "From:CryptoCli;Action:clear;" or "From:DSSAgent;Action:init;"static void nl_recv_msg(struct sk_buff *skb){printk(KERN_INFO "%s <--\n", __func__);struct nlmsghdr *nlh = (struct nlmsghdr *)skb->data;printk(KERN_INFO "%s Netlink received msg payload:%s\n", __func__, (char *)nlmsg_data(nlh));char *msg = (char *)nlmsg_data(nlh);if (msg && strlen(msg)){char *from = np_extract_value(msg, "From", ';');char *action = np_extract_value(msg, "Action", ';');if (from && strlen(from) && action && strlen(action)){if (!strcmp(from, "DSSAgent")){if (!strcmp(action, "init")){return_msg_to_user(nlh);}else if (!strcmp(action, "return")){parse_return_from_user(msg);}else{printk(KERN_ERR "%s Failed msg, \"From\" is %s and \"Action\" is %s\n", __func__, from, action);}}else if (!strcmp(from, "CryptoCli")){if (!strcmp(action, "clear")){free_guards();}else if (!strcmp(action, "change")){change_guards(msg);}else{printk(KERN_ERR "%s Failed msg, \"From\" is %s and \"Action\" is %s\n", __func__, from, action);}}else{printk(KERN_ERR "%s Failed msg, \"From\" is %s and \"Action\" is %s\n", __func__, from, action);}}else{printk(KERN_ERR "%s Failed parse msg, don`t found \"From\" and \"Action\" (%s)\n", __func__, msg);}}else{printk(KERN_ERR "%s Failed parse struct nlmsg_data, msg is empty\n", __func__);}printk(KERN_INFO "%s -->\n", __func__);}static bool check_file_access(char *fname, int processPid){if (fname && strlen(fname)){int i;for (i = 0; i < guards.count; ++i){if (!strcmp(fname, guards.process[i].file_path) && guards.process[i].allow_pids[0] != 0){int j;for (j = 0; guards.process[i].allow_pids[j] != 0; ++j)if (processPid == guards.process[i].allow_pids[j])return true;return false;}}// Not found filename in guardsif (strstr(fname, filetype)){char *processName = current->comm;printk(KERN_INFO "%s service pid = %d\n", __func__, pid);printk(KERN_INFO "%s file name = %s, process pid: %d, , process name = %s\n", __func__, fname, processPid, processName);if (processPid == pid){return true;}else{add_list(p_list_hook_files, processPid, fname, None);char *buffer = kmalloc(4096, GFP_KERNEL);sprintf(buffer, "%s|%s|%d", fname, processName, processPid);send_msg_to_user(buffer);kfree(buffer);ssleep(5);bool ret = true;if (find_list(p_list_hook_files, fname) == Deny)ret = false;delete_node(p_list_hook_files, fname);return ret;}}}return true;}

Интеграция в процесс установки

Так как первые два минуса LKM удалось преодолеть через реализацию ftrace, третий никто не отменял. Мало того, что под каждое ядро нужна сборка модуля, уже в процессе использования он может протухнуть. Было принято решение добавить его пересборку перед каждым запуском userspace приложения. В статье по сборке Linux пакетов было описано, что службу, для которой мы реализовываем обработку перехвата обращения к файлу, мы демонизировали путём добавления в system. Поэтому для демона.service добавляем два дополнительных пункта, помимо ExecStart и ExecStop будут:

ExecStartPre=/bin/sh /путь_до_расположения/prestart.shExecStopPost=/sbin/rmmod имя_модуля.ko

а в сам prestart.sh:

#!/bin/shMOD_VAL=$(lsmod | grep имя_модуля | wc -l)cd /путь_до_расположения_модуляmake cleanmake allif [ $MOD_VAL = 1 ]then    for proc in $(ps aux | grep DSS.Agent | awk '{print $2}'); do kill -9 $proc; doneelse    /sbin/insmod / путь_до_расположения_модуля/имя_модуля.kofi

Заключение

В завершение, хочется отметить: возможно, путь, по которому мы пошли, не самый красивый и элегантный, но, он содержит отработанную и проверенную логику работы на ОС Windows. Было бы полезно услышать в комментариях мнение читателей статьи. Возможно, есть более разумное решение задачи. Например, наш DevOps, в тот момент, когда мы автоматизировали сборку пакета Linux и обрабатывали/добавляли LKM, предложил реализовать логику с использованием Access Control List (ACL). Скорее всего, в дальнейшем мы займёмся переработкой нашего продукта под Linux. И, да, скоро будет новая статья, о том, как мы переносили MS Forms на Avalonia и его интеграции в Linux.

Ссылки которые нам помогли

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru