diff --git a/.gitignore b/.gitignore index 78b2af6..4221f2b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,12 +19,13 @@ /frontend/dist/ /frontend/dist/** /backend/modules/cppqueue +/backend/binsrc/cppqueue /backend/modules/proxy docker-compose.yml firegex-compose.yml firegex-compose-tmp-file.yml firegex.py - +/tests/benchmark.csv # misc **/.DS_Store **/.env.local @@ -35,13 +36,3 @@ firegex.py **/npm-debug.log* **/yarn-debug.log* **/yarn-error.log* - -#rust -/backend/binsrc/nfqueue_regex/debug/ -/backend/binsrc/nfqueue_regex/target/ - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -**/*.pdb diff --git a/Dockerfile b/Dockerfile index c5b567f..74a1d6d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,25 +7,18 @@ FROM --platform=$BUILDPLATFORM oven/bun AS frontend WORKDIR /app ADD ./frontend/package.json . -ADD ./frontend/bun.lockb . +ADD ./frontend/bun.lock . RUN bun i COPY ./frontend/ . RUN bun run build #Building main conteiner -FROM --platform=$TARGETARCH debian:stable-slim AS base +FROM --platform=$TARGETARCH debian:trixie-slim AS base RUN apt-get update -qq && apt-get upgrade -qq && \ apt-get install -qq python3-pip build-essential \ - git libpcre2-dev libnetfilter-queue-dev libssl-dev \ - libnfnetlink-dev libmnl-dev libcap2-bin make cmake \ - nftables libboost-all-dev autoconf automake cargo \ - libffi-dev libvectorscan-dev libtins-dev python3-nftables - -WORKDIR /tmp/ -RUN git clone --single-branch --branch release https://github.com/jpcre2/jpcre2 -WORKDIR /tmp/jpcre2 -RUN ./configure; make -j`nproc`; make install + libnetfilter-queue-dev libnfnetlink-dev libmnl-dev libcap2-bin\ + nftables libvectorscan-dev libtins-dev python3-nftables RUN mkdir -p /execute/modules WORKDIR /execute @@ -34,8 +27,7 @@ ADD ./backend/requirements.txt /execute/requirements.txt RUN pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt --no-warn-script-location COPY ./backend/binsrc /execute/binsrc -RUN g++ binsrc/nfqueue.cpp -o modules/cppqueue -O3 -lnetfilter_queue -pthread -lpcre2-8 -ltins -lmnl -lnfnetlink -RUN g++ binsrc/proxy.cpp -o modules/proxy -O3 -pthread -lboost_system -lboost_thread -lpcre2-8 +RUN g++ binsrc/nfqueue.cpp -o modules/cppqueue -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl) COPY ./backend/ /execute/ COPY --from=frontend /app/dist/ ./frontend/ diff --git a/README.md b/README.md index 80f8109..aa4bc3a 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,6 @@ All the configuration at the startup is customizable in [firegex.py](./start.py) - Create basic firewall rules to allow and deny specific traffic, like ufw or iptables but using firegex graphic interface (by using [nftable](https://netfilter.org/projects/nftables/)) - Port Hijacking allows you to redirect the traffic on a specific port to another port. Thanks to this you can start your own proxy, connecting to the real service using the loopback interface. Firegex will be resposable about the routing of the packets using internally [nftables](https://netfilter.org/projects/nftables/) -DEPRECATED: -- TCP Proxy regex filter, create a proxy tunnel from the service internal port to a public port published by the proxy. Internally the c++ proxy filter the request with PCRE2 regexes. For mantaining the same public port you will need to open only in localhost the real services. (Available only on TCP/IPv4) - ## Documentation Find the documentation of the backend and of the frontend in the related README files @@ -57,8 +54,7 @@ This means that firegex is projected to avoid any possibility to have the servic ## Why "Firegex"? Initiially the project was based only on regex filters, and also now the main function uses regexes, but firegex have and will have also other filtering tools. -# Credits -- Copyright (c) 2007 Arash Partow (http://www.partow.net) for the base of our proxy implementation +# Credits - Copyright (c) 2022 Pwnzer0tt1 ## Star History diff --git a/backend/app.py b/backend/app.py index d4379ef..2f6f7e1 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,6 +1,10 @@ -import uvicorn, secrets, utils -import os, asyncio, logging -from fastapi import FastAPI, HTTPException, Depends, APIRouter, Request +import uvicorn +import secrets +import utils +import os +import asyncio +import logging +from fastapi import FastAPI, HTTPException, Depends, APIRouter from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import jwt from passlib.context import CryptContext @@ -30,7 +34,14 @@ async def lifespan(app): yield await shutdown_main() -app = FastAPI(debug=DEBUG, redoc_url=None, lifespan=lifespan) +app = FastAPI( + debug=DEBUG, + redoc_url=None, + lifespan=lifespan, + docs_url="/api/docs", + title="Firegex API", + version=API_VERSION, +) utils.socketio = SocketManager(app, "/sock", socketio_path="") if DEBUG: @@ -94,7 +105,8 @@ async def get_app_status(auth: bool = Depends(check_login)): @app.post("/api/login") async def login_api(form: OAuth2PasswordRequestForm = Depends()): """Get a login token to use the firegex api""" - if APP_STATUS() != "run": raise HTTPException(status_code=400) + if APP_STATUS() != "run": + raise HTTPException(status_code=400) if form.password == "": return {"status":"Cannot insert an empty password!"} await asyncio.sleep(0.3) # No bruteforce :) @@ -105,7 +117,8 @@ async def login_api(form: OAuth2PasswordRequestForm = Depends()): @app.post('/api/set-password', response_model=ChangePasswordModel) async def set_password(form: PasswordForm): """Set the password of firegex""" - if APP_STATUS() != "init": raise HTTPException(status_code=400) + if APP_STATUS() != "init": + raise HTTPException(status_code=400) if form.password == "": return {"status":"Cannot insert an empty password!"} set_psw(form.password) @@ -115,7 +128,8 @@ async def set_password(form: PasswordForm): @api.post('/change-password', response_model=ChangePasswordModel) async def change_password(form: PasswordChangeForm): """Change the password of firegex""" - if APP_STATUS() != "run": raise HTTPException(status_code=400) + if APP_STATUS() != "run": + raise HTTPException(status_code=400) if form.password == "": return {"status":"Cannot insert an empty password!"} @@ -144,7 +158,8 @@ async def startup_main(): except Exception as e: logging.error(f"Error setting sysctls: {e}") await startup() - if not JWT_SECRET(): db.put("secret", secrets.token_hex(32)) + if not JWT_SECRET(): + db.put("secret", secrets.token_hex(32)) await refresh_frontend() async def shutdown_main(): @@ -175,9 +190,9 @@ if __name__ == '__main__': os.chdir(os.path.dirname(os.path.realpath(__file__))) uvicorn.run( "app:app", - host="::" if DEBUG else None, + host=None, #"::" if DEBUG else None, port=FIREGEX_PORT, - reload=DEBUG, + reload=False,#DEBUG, access_log=True, workers=1, # Multiple workers will cause a crash due to the creation # of multiple processes with separated memory diff --git a/backend/binsrc/classes/netfilter.cpp b/backend/binsrc/classes/netfilter.cpp new file mode 100644 index 0000000..4ab759b --- /dev/null +++ b/backend/binsrc/classes/netfilter.cpp @@ -0,0 +1,530 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Tins::TCPIP::Stream; +using Tins::TCPIP::StreamFollower; +using namespace std; + +#ifndef NETFILTER_CLASSES_HPP +#define NETFILTER_CLASSES_HPP +typedef Tins::TCPIP::StreamIdentifier stream_id; +typedef map matching_map; + +/* Considering to use unorder_map using this hash of stream_id + +namespace std { + template<> + struct hash { + size_t operator()(const stream_id& sid) const + { + return std::hash()(sid.max_address[0] + sid.max_address[1] + sid.max_address[2] + sid.max_address[3] + sid.max_address_port + sid.min_address[0] + sid.min_address[1] + sid.min_address[2] + sid.min_address[3] + sid.min_address_port); + } + }; +} + +*/ + +#ifdef DEBUG +ostream& operator<<(ostream& os, const Tins::TCPIP::StreamIdentifier::address_type &sid){ + bool first_print = false; + for (auto ele: sid){ + if (first_print || ele){ + first_print = true; + os << (int)ele << "."; + } + } + return os; +} + +ostream& operator<<(ostream& os, const stream_id &sid){ + os << sid.max_address << ":" << sid.max_address_port << " -> " << sid.min_address << ":" << sid.min_address_port; + return os; +} +#endif + +struct packet_info; + +struct tcp_stream_tmp { + bool matching_has_been_called = false; + bool result; + packet_info *pkt_info; +}; + +struct stream_ctx { + matching_map in_hs_streams; + matching_map out_hs_streams; + hs_scratch_t* in_scratch = nullptr; + hs_scratch_t* out_scratch = nullptr; + u_int16_t latest_config_ver = 0; + StreamFollower follower; + mnl_socket* nl; + tcp_stream_tmp tcp_match_util; + + void clean_scratches(){ + if (out_scratch != nullptr){ + hs_free_scratch(out_scratch); + out_scratch = nullptr; + } + if (in_scratch != nullptr){ + hs_free_scratch(in_scratch); + in_scratch = nullptr; + } + } + + void clean_stream_by_id(stream_id sid){ + #ifdef DEBUG + cerr << "[DEBUG] [NetfilterQueue.clean_stream_by_id] Cleaning stream context of " << sid << endl; + #endif + auto stream_search = in_hs_streams.find(sid); + hs_stream_t* stream_match; + if (stream_search != in_hs_streams.end()){ + stream_match = stream_search->second; + if (hs_close_stream(stream_match, in_scratch, nullptr, nullptr) != HS_SUCCESS) { + cerr << "[error] [NetfilterQueue.clean_stream_by_id] Error closing the stream matcher (hs)" << endl; + throw invalid_argument("Cannot close stream match on hyperscan"); + } + in_hs_streams.erase(stream_search); + } + + stream_search = out_hs_streams.find(sid); + if (stream_search != out_hs_streams.end()){ + stream_match = stream_search->second; + if (hs_close_stream(stream_match, out_scratch, nullptr, nullptr) != HS_SUCCESS) { + cerr << "[error] [NetfilterQueue.clean_stream_by_id] Error closing the stream matcher (hs)" << endl; + throw invalid_argument("Cannot close stream match on hyperscan"); + } + out_hs_streams.erase(stream_search); + } + } + + void clean(){ + + #ifdef DEBUG + cerr << "[DEBUG] [NetfilterQueue.clean] Cleaning stream context" << endl; + #endif + + if (in_scratch){ + for(auto ele: in_hs_streams){ + if (hs_close_stream(ele.second, in_scratch, nullptr, nullptr) != HS_SUCCESS) { + cerr << "[error] [NetfilterQueue.clean_stream_by_id] Error closing the stream matcher (hs)" << endl; + throw invalid_argument("Cannot close stream match on hyperscan"); + } + } + in_hs_streams.clear(); + } + + if (out_scratch){ + for(auto ele: out_hs_streams){ + if (hs_close_stream(ele.second, out_scratch, nullptr, nullptr) != HS_SUCCESS) { + cerr << "[error] [NetfilterQueue.clean_stream_by_id] Error closing the stream matcher (hs)" << endl; + throw invalid_argument("Cannot close stream match on hyperscan"); + } + } + out_hs_streams.clear(); + } + clean_scratches(); + } +}; + +struct packet_info { + string packet; + string payload; + stream_id sid; + bool is_input; + bool is_tcp; + stream_ctx* sctx; +}; + +typedef bool NetFilterQueueCallback(packet_info &); + +template +class NetfilterQueue { + public: + + size_t BUF_SIZE = 0xffff + (MNL_SOCKET_BUFFER_SIZE/2); + char *buf = nullptr; + unsigned int portid; + u_int16_t queue_num; + stream_ctx sctx; + + NetfilterQueue(u_int16_t queue_num): queue_num(queue_num) { + sctx.nl = mnl_socket_open(NETLINK_NETFILTER); + + if (sctx.nl == nullptr) { throw runtime_error( "mnl_socket_open" );} + + if (mnl_socket_bind(sctx.nl, 0, MNL_SOCKET_AUTOPID) < 0) { + mnl_socket_close(sctx.nl); + throw runtime_error( "mnl_socket_bind" ); + } + portid = mnl_socket_get_portid(sctx.nl); + + buf = (char*) malloc(BUF_SIZE); + + if (!buf) { + mnl_socket_close(sctx.nl); + throw runtime_error( "allocate receive buffer" ); + } + + if (send_config_cmd(NFQNL_CFG_CMD_BIND) < 0) { + _clear(); + throw runtime_error( "mnl_socket_send" ); + } + //TEST if BIND was successful + if (send_config_cmd(NFQNL_CFG_CMD_NONE) < 0) { // SEND A NONE cmmand to generate an error meessage + _clear(); + throw runtime_error( "mnl_socket_send" ); + } + if (recv_packet() == -1) { //RECV the error message + _clear(); + throw runtime_error( "mnl_socket_recvfrom" ); + } + + struct nlmsghdr *nlh = (struct nlmsghdr *) buf; + + if (nlh->nlmsg_type != NLMSG_ERROR) { + _clear(); + throw runtime_error( "unexpected packet from kernel (expected NLMSG_ERROR packet)" ); + } + //nfqnl_msg_config_cmd + nlmsgerr* error_msg = (nlmsgerr *)mnl_nlmsg_get_payload(nlh); + + // error code taken from the linux kernel: + // https://elixir.bootlin.com/linux/v5.18.12/source/include/linux/errno.h#L27 + #define ENOTSUPP 524 /* Operation is not supported */ + + if (error_msg->error != -ENOTSUPP) { + _clear(); + throw invalid_argument( "queueid is already busy" ); + } + + //END TESTING BIND + nlh = nfq_nlmsg_put(buf, NFQNL_MSG_CONFIG, queue_num); + nfq_nlmsg_cfg_put_params(nlh, NFQNL_COPY_PACKET, 0xffff); + + mnl_attr_put_u32(nlh, NFQA_CFG_FLAGS, htonl(NFQA_CFG_F_GSO)); + mnl_attr_put_u32(nlh, NFQA_CFG_MASK, htonl(NFQA_CFG_F_GSO)); + + if (mnl_socket_sendto(sctx.nl, nlh, nlh->nlmsg_len) < 0) { + _clear(); + throw runtime_error( "mnl_socket_send" ); + } + + } + + static void on_data_recv(Stream& stream, stream_ctx* sctx, string data) { + #ifdef DEBUG + cerr << "[DEBUG] [NetfilterQueue.on_data_recv] data: " << data << endl; + #endif + sctx->tcp_match_util.matching_has_been_called = true; + bool result = callback_func(*sctx->tcp_match_util.pkt_info); + #ifdef DEBUG + cerr << "[DEBUG] [NetfilterQueue.on_data_recv] result: " << result << endl; + #endif + if (!result){ + #ifdef DEBUG + cerr << "[DEBUG] [NetfilterQueue.on_data_recv] Stream matched, removing all data about it" << endl; + #endif + sctx->clean_stream_by_id(sctx->tcp_match_util.pkt_info->sid); + stream.ignore_client_data(); + stream.ignore_server_data(); + } + sctx->tcp_match_util.result = result; + } + + //Input data filtering + static void on_client_data(Stream& stream, stream_ctx* sctx) { + on_data_recv(stream, sctx, string(stream.client_payload().begin(), stream.client_payload().end())); + } + + //Server data filtering + static void on_server_data(Stream& stream, stream_ctx* sctx) { + on_data_recv(stream, sctx, string(stream.server_payload().begin(), stream.server_payload().end())); + } + + static void on_new_stream(Stream& stream, stream_ctx* sctx) { + #ifdef DEBUG + cerr << "[DEBUG] [NetfilterQueue.on_new_stream] New stream detected" << endl; + #endif + if (stream.is_partial_stream()) { + #ifdef DEBUG + cerr << "[DEBUG] [NetfilterQueue.on_new_stream] Partial stream detected, skipping" << endl; + #endif + return; + } + stream.auto_cleanup_payloads(true); + stream.client_data_callback(bind(on_client_data, placeholders::_1, sctx)); + stream.server_data_callback(bind(on_server_data, placeholders::_1, sctx)); + stream.stream_closed_callback(bind(on_stream_close, placeholders::_1, sctx)); + } + + // A stream was terminated. The second argument is the reason why it was terminated + static void on_stream_close(Stream& stream, stream_ctx* sctx) { + stream_id stream_id = stream_id::make_identifier(stream); + #ifdef DEBUG + cerr << "[DEBUG] [NetfilterQueue.on_stream_close] Stream terminated, deleting all data" << endl; + #endif + sctx->clean_stream_by_id(stream_id); + } + + + void run(){ + /* + * ENOBUFS is signalled to userspace when packets were lost + * on kernel side. In most cases, userspace isn't interested + * in this information, so turn it off. + */ + int ret = 1; + mnl_socket_setsockopt(sctx.nl, NETLINK_NO_ENOBUFS, &ret, sizeof(int)); + + sctx.follower.new_stream_callback(bind(on_new_stream, placeholders::_1, &sctx)); + sctx.follower.stream_termination_callback(bind(on_stream_close, placeholders::_1, &sctx)); + + for (;;) { + ret = recv_packet(); + if (ret == -1) { + throw runtime_error( "mnl_socket_recvfrom" ); + } + + ret = mnl_cb_run(buf, ret, 0, portid, queue_cb, &sctx); + if (ret < 0){ + throw runtime_error( "mnl_cb_run" ); + } + } + } + + + ~NetfilterQueue() { + #ifdef DEBUG + cerr << "[DEBUG] [NetfilterQueue.~NetfilterQueue] Destructor called" << endl; + #endif + send_config_cmd(NFQNL_CFG_CMD_UNBIND); + _clear(); + } + private: + + ssize_t send_config_cmd(nfqnl_msg_config_cmds cmd){ + struct nlmsghdr *nlh = nfq_nlmsg_put(buf, NFQNL_MSG_CONFIG, queue_num); + nfq_nlmsg_cfg_put_cmd(nlh, AF_INET, cmd); + return mnl_socket_sendto(sctx.nl, nlh, nlh->nlmsg_len); + } + + ssize_t recv_packet(){ + return mnl_socket_recvfrom(sctx.nl, buf, BUF_SIZE); + } + + void _clear(){ + if (buf != nullptr) { + free(buf); + buf = nullptr; + } + mnl_socket_close(sctx.nl); + sctx.nl = nullptr; + sctx.clean(); + } + + template + static void build_verdict(T packet, uint8_t *payload, uint16_t plen, nlmsghdr *nlh_verdict, nfqnl_msg_packet_hdr *ph, stream_ctx* sctx, bool is_input){ + Tins::TCP* tcp = packet.template find_pdu(); + + if (tcp){ + Tins::PDU* application_layer = tcp->inner_pdu(); + u_int16_t payload_size = 0; + if (application_layer != nullptr){ + payload_size = application_layer->size(); + } + packet_info pktinfo{ + packet: string(payload, payload+plen), + payload: string(payload+plen - payload_size, payload+plen), + sid: stream_id::make_identifier(packet), + is_input: is_input, + is_tcp: true, + sctx: sctx, + }; + sctx->tcp_match_util.matching_has_been_called = false; + sctx->tcp_match_util.pkt_info = &pktinfo; + #ifdef DEBUG + cerr << "[DEBUG] [NetfilterQueue.build_verdict] TCP Packet received " << packet.src_addr() << ":" << tcp->sport() << " -> " << packet.dst_addr() << ":" << tcp->dport() << ", sending to libtins StreamFollower" << endl; + #endif + sctx->follower.process_packet(packet); + #ifdef DEBUG + if (sctx->tcp_match_util.matching_has_been_called){ + cerr << "[DEBUG] [NetfilterQueue.build_verdict] StreamFollower has called matching functions" << endl; + }else{ + cerr << "[DEBUG] [NetfilterQueue.build_verdict] StreamFollower has NOT called matching functions" << endl; + } + #endif + if (sctx->tcp_match_util.matching_has_been_called && !sctx->tcp_match_util.result){ + Tins::PDU* data_layer = tcp->release_inner_pdu(); + if (data_layer != nullptr){ + delete data_layer; + } + tcp->set_flag(Tins::TCP::FIN,1); + tcp->set_flag(Tins::TCP::ACK,1); + tcp->set_flag(Tins::TCP::SYN,0); + nfq_nlmsg_verdict_put_pkt(nlh_verdict, packet.serialize().data(), packet.size()); + } + nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT ); + }else{ + Tins::UDP* udp = packet.template find_pdu(); + if (!udp){ + throw invalid_argument("Only TCP and UDP are supported"); + } + Tins::PDU* application_layer = udp->inner_pdu(); + u_int16_t payload_size = 0; + if (application_layer != nullptr){ + payload_size = application_layer->size(); + } + if((udp->inner_pdu() == nullptr)){ + nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT ); + } + packet_info pktinfo{ + packet: string(payload, payload+plen), + payload: string(payload+plen - payload_size, payload+plen), + sid: stream_id::make_identifier(packet), + is_input: is_input, + is_tcp: false, + sctx: sctx, + }; + if (callback_func(pktinfo)){ + nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT ); + }else{ + nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_DROP ); + } + } + } + + static int queue_cb(const nlmsghdr *nlh, void *data_ptr) + { + stream_ctx* sctx = (stream_ctx*)data_ptr; + + //Extract attributes from the nlmsghdr + nlattr *attr[NFQA_MAX+1] = {}; + + if (nfq_nlmsg_parse(nlh, attr) < 0) { + perror("problems parsing"); + return MNL_CB_ERROR; + } + if (attr[NFQA_PACKET_HDR] == nullptr) { + fputs("metaheader not set\n", stderr); + return MNL_CB_ERROR; + } + if (attr[NFQA_MARK] == nullptr) { + fputs("mark not set\n", stderr); + return MNL_CB_ERROR; + } + //Get Payload + uint16_t plen = mnl_attr_get_payload_len(attr[NFQA_PAYLOAD]); + uint8_t *payload = (uint8_t *)mnl_attr_get_payload(attr[NFQA_PAYLOAD]); + + //Return result to the kernel + struct nfqnl_msg_packet_hdr *ph = (nfqnl_msg_packet_hdr*) mnl_attr_get_payload(attr[NFQA_PACKET_HDR]); + struct nfgenmsg *nfg = (nfgenmsg *)mnl_nlmsg_get_payload(nlh); + char buf[MNL_SOCKET_BUFFER_SIZE]; + struct nlmsghdr *nlh_verdict; + struct nlattr *nest; + + nlh_verdict = nfq_nlmsg_put(buf, NFQNL_MSG_VERDICT, ntohs(nfg->res_id)); + + bool is_input = ntohl(mnl_attr_get_u32(attr[NFQA_MARK])) & 0x1; // == 0x1337 that is odd + #ifdef DEBUG + cerr << "[DEBUG] [NetfilterQueue.queue_cb] Packet received" << endl; + cerr << "[DEBUG] [NetfilterQueue.queue_cb] Packet ID: " << ntohl(ph->packet_id) << endl; + cerr << "[DEBUG] [NetfilterQueue.queue_cb] Payload size: " << plen << endl; + cerr << "[DEBUG] [NetfilterQueue.queue_cb] Is input: " << is_input << endl; + #endif + + // Check IP protocol version + if ( (payload[0] & 0xf0) == 0x40 ){ + build_verdict(Tins::IP(payload, plen), payload, plen, nlh_verdict, ph, sctx, is_input); + }else{ + build_verdict(Tins::IPv6(payload, plen), payload, plen, nlh_verdict, ph, sctx, is_input); + } + + nest = mnl_attr_nest_start(nlh_verdict, NFQA_CT); + mnl_attr_put_u32(nlh_verdict, CTA_MARK, htonl(42)); + mnl_attr_nest_end(nlh_verdict, nest); + + if (mnl_socket_sendto(sctx->nl, nlh_verdict, nlh_verdict->nlmsg_len) < 0) { + throw runtime_error( "mnl_socket_send" ); + } + + return MNL_CB_OK; + } + +}; + +template +class NFQueueSequence{ + private: + vector *> nfq; + uint16_t _init; + uint16_t _end; + vector threads; + public: + static const int QUEUE_BASE_NUM = 1000; + + NFQueueSequence(uint16_t seq_len){ + if (seq_len <= 0) throw invalid_argument("seq_len <= 0"); + nfq = vector*>(seq_len); + _init = QUEUE_BASE_NUM; + while(nfq[0] == nullptr){ + if (_init+seq_len-1 >= 65536){ + throw runtime_error("NFQueueSequence: too many queues!"); + } + for (int i=0;i(_init+i); + }catch(const invalid_argument e){ + for(int j = 0; j < i; j++) { + delete nfq[j]; + nfq[j] = nullptr; + } + _init += seq_len - i; + break; + } + } + } + _end = _init + seq_len - 1; + } + + void start(){ + if (threads.size() != 0) throw runtime_error("NFQueueSequence: already started!"); + for (int i=0;i::run, nfq[i])); + } + } + + void join(){ + for (int i=0;i -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef NETFILTER_CLASSES_HPP -#define NETFILTER_CLASSES_HPP - -typedef bool NetFilterQueueCallback(const uint8_t*,uint32_t); - -Tins::PDU * find_transport_layer(Tins::PDU* pkt){ - while(pkt != NULL){ - if (pkt->pdu_type() == Tins::PDU::TCP || pkt->pdu_type() == Tins::PDU::UDP) { - return pkt; - } - pkt = pkt->inner_pdu(); - } - return pkt; -} - -template -class NetfilterQueue { - public: - size_t BUF_SIZE = 0xffff + (MNL_SOCKET_BUFFER_SIZE/2); - char *buf = NULL; - unsigned int portid; - u_int16_t queue_num; - struct mnl_socket* nl = NULL; - - NetfilterQueue(u_int16_t queue_num): queue_num(queue_num) { - - nl = mnl_socket_open(NETLINK_NETFILTER); - - if (nl == NULL) { throw std::runtime_error( "mnl_socket_open" );} - - if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) { - mnl_socket_close(nl); - throw std::runtime_error( "mnl_socket_bind" ); - } - portid = mnl_socket_get_portid(nl); - - buf = (char*) malloc(BUF_SIZE); - - if (!buf) { - mnl_socket_close(nl); - throw std::runtime_error( "allocate receive buffer" ); - } - - if (send_config_cmd(NFQNL_CFG_CMD_BIND) < 0) { - _clear(); - throw std::runtime_error( "mnl_socket_send" ); - } - //TEST if BIND was successful - if (send_config_cmd(NFQNL_CFG_CMD_NONE) < 0) { // SEND A NONE cmmand to generate an error meessage - _clear(); - throw std::runtime_error( "mnl_socket_send" ); - } - if (recv_packet() == -1) { //RECV the error message - _clear(); - throw std::runtime_error( "mnl_socket_recvfrom" ); - } - - struct nlmsghdr *nlh = (struct nlmsghdr *) buf; - - if (nlh->nlmsg_type != NLMSG_ERROR) { - _clear(); - throw std::runtime_error( "unexpected packet from kernel (expected NLMSG_ERROR packet)" ); - } - //nfqnl_msg_config_cmd - nlmsgerr* error_msg = (nlmsgerr *)mnl_nlmsg_get_payload(nlh); - - // error code taken from the linux kernel: - // https://elixir.bootlin.com/linux/v5.18.12/source/include/linux/errno.h#L27 - #define ENOTSUPP 524 /* Operation is not supported */ - - if (error_msg->error != -ENOTSUPP) { - _clear(); - throw std::invalid_argument( "queueid is already busy" ); - } - - //END TESTING BIND - nlh = nfq_nlmsg_put(buf, NFQNL_MSG_CONFIG, queue_num); - nfq_nlmsg_cfg_put_params(nlh, NFQNL_COPY_PACKET, 0xffff); - - - mnl_attr_put_u32(nlh, NFQA_CFG_FLAGS, htonl(NFQA_CFG_F_GSO)); - mnl_attr_put_u32(nlh, NFQA_CFG_MASK, htonl(NFQA_CFG_F_GSO)); - - if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) { - _clear(); - throw std::runtime_error( "mnl_socket_send" ); - } - - } - - - - void run(){ - /* - * ENOBUFS is signalled to userspace when packets were lost - * on kernel side. In most cases, userspace isn't interested - * in this information, so turn it off. - */ - int ret = 1; - mnl_socket_setsockopt(nl, NETLINK_NO_ENOBUFS, &ret, sizeof(int)); - - for (;;) { - ret = recv_packet(); - if (ret == -1) { - throw std::runtime_error( "mnl_socket_recvfrom" ); - } - - ret = mnl_cb_run(buf, ret, 0, portid, queue_cb, nl); - if (ret < 0){ - throw std::runtime_error( "mnl_cb_run" ); - } - } - } - - ~NetfilterQueue() { - send_config_cmd(NFQNL_CFG_CMD_UNBIND); - _clear(); - } - private: - - ssize_t send_config_cmd(nfqnl_msg_config_cmds cmd){ - struct nlmsghdr *nlh = nfq_nlmsg_put(buf, NFQNL_MSG_CONFIG, queue_num); - nfq_nlmsg_cfg_put_cmd(nlh, AF_INET, cmd); - return mnl_socket_sendto(nl, nlh, nlh->nlmsg_len); - } - - ssize_t recv_packet(){ - return mnl_socket_recvfrom(nl, buf, BUF_SIZE); - } - - void _clear(){ - if (buf != NULL) { - free(buf); - buf = NULL; - } - mnl_socket_close(nl); - } - - static int queue_cb(const struct nlmsghdr *nlh, void *data) - { - struct mnl_socket* nl = (struct mnl_socket*)data; - //Extract attributes from the nlmsghdr - struct nlattr *attr[NFQA_MAX+1] = {}; - - if (nfq_nlmsg_parse(nlh, attr) < 0) { - perror("problems parsing"); - return MNL_CB_ERROR; - } - if (attr[NFQA_PACKET_HDR] == NULL) { - fputs("metaheader not set\n", stderr); - return MNL_CB_ERROR; - } - //Get Payload - uint16_t plen = mnl_attr_get_payload_len(attr[NFQA_PAYLOAD]); - void *payload = mnl_attr_get_payload(attr[NFQA_PAYLOAD]); - - //Return result to the kernel - struct nfqnl_msg_packet_hdr *ph = (nfqnl_msg_packet_hdr*) mnl_attr_get_payload(attr[NFQA_PACKET_HDR]); - struct nfgenmsg *nfg = (nfgenmsg *)mnl_nlmsg_get_payload(nlh); - char buf[MNL_SOCKET_BUFFER_SIZE]; - struct nlmsghdr *nlh_verdict; - struct nlattr *nest; - - nlh_verdict = nfq_nlmsg_put(buf, NFQNL_MSG_VERDICT, ntohs(nfg->res_id)); - - /* - This define allow to avoid to allocate new heap memory for each packet. - The code under this comment is replicated for ipv6 and ip - Better solutions are welcome. :) - */ - #define PKT_HANDLE \ - Tins::PDU *transport_layer = find_transport_layer(&packet); \ - if(transport_layer->inner_pdu() == nullptr || transport_layer == nullptr){ \ - nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT ); \ - }else{ \ - int size = transport_layer->inner_pdu()->size(); \ - if(callback_func((const uint8_t*)payload+plen - size, size)){ \ - nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT ); \ - } else{ \ - if (transport_layer->pdu_type() == Tins::PDU::TCP){ \ - ((Tins::TCP *)transport_layer)->release_inner_pdu(); \ - ((Tins::TCP *)transport_layer)->set_flag(Tins::TCP::FIN,1); \ - ((Tins::TCP *)transport_layer)->set_flag(Tins::TCP::ACK,1); \ - ((Tins::TCP *)transport_layer)->set_flag(Tins::TCP::SYN,0); \ - nfq_nlmsg_verdict_put_pkt(nlh_verdict, packet.serialize().data(), packet.size()); \ - nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_ACCEPT ); \ - }else{ \ - nfq_nlmsg_verdict_put(nlh_verdict, ntohl(ph->packet_id), NF_DROP ); \ - } \ - } \ - } - - // Check IP protocol version - if ( (((uint8_t*)payload)[0] & 0xf0) == 0x40 ){ - Tins::IP packet = Tins::IP((uint8_t*)payload,plen); - PKT_HANDLE - }else{ - Tins::IPv6 packet = Tins::IPv6((uint8_t*)payload,plen); - PKT_HANDLE - } - - /* example to set the connmark. First, start NFQA_CT section: */ - nest = mnl_attr_nest_start(nlh_verdict, NFQA_CT); - - /* then, add the connmark attribute: */ - mnl_attr_put_u32(nlh_verdict, CTA_MARK, htonl(42)); - /* more conntrack attributes, e.g. CTA_LABELS could be set here */ - - /* end conntrack section */ - mnl_attr_nest_end(nlh_verdict, nest); - - if (mnl_socket_sendto(nl, nlh_verdict, nlh_verdict->nlmsg_len) < 0) { - throw std::runtime_error( "mnl_socket_send" ); - } - - return MNL_CB_OK; - } - -}; - -template -class NFQueueSequence{ - private: - std::vector *> nfq; - uint16_t _init; - uint16_t _end; - std::vector threads; - public: - static const int QUEUE_BASE_NUM = 1000; - - NFQueueSequence(uint16_t seq_len){ - if (seq_len <= 0) throw std::invalid_argument("seq_len <= 0"); - nfq = std::vector*>(seq_len); - _init = QUEUE_BASE_NUM; - while(nfq[0] == NULL){ - if (_init+seq_len-1 >= 65536){ - throw std::runtime_error("NFQueueSequence: too many queues!"); - } - for (int i=0;i(_init+i); - }catch(const std::invalid_argument e){ - for(int j = 0; j < i; j++) { - delete nfq[j]; - nfq[j] = nullptr; - } - _init += seq_len - i; - break; - } - } - } - _end = _init + seq_len - 1; - } - - void start(){ - if (threads.size() != 0) throw std::runtime_error("NFQueueSequence: already started!"); - for (int i=0;i::run, nfq[i])); - } - } - - void join(){ - for (int i=0;i -#include -#include -#include -#include "../utils.hpp" - - -#ifndef REGEX_FILTER_HPP -#define REGEX_FILTER_HPP - -typedef jpcre2::select jp; -typedef std::pair regex_rule_pair; -typedef std::vector regex_rule_vector; -struct regex_rules{ - regex_rule_vector output_whitelist, input_whitelist, output_blacklist, input_blacklist; - - regex_rule_vector* getByCode(char code){ - switch(code){ - case 'C': // Client to server Blacklist - return &input_blacklist; break; - case 'c': // Client to server Whitelist - return &input_whitelist; break; - case 'S': // Server to client Blacklist - return &output_blacklist; break; - case 's': // Server to client Whitelist - return &output_whitelist; break; - } - throw std::invalid_argument( "Expected 'C' 'c' 'S' or 's'" ); - } - - int add(const char* arg){ - //Integrity checks - size_t arg_len = strlen(arg); - if (arg_len < 2 || arg_len%2 != 0){ - std::cerr << "[warning] [regex_rules.add] invalid arg passed (" << arg << "), skipping..." << std::endl; - return -1; - } - if (arg[0] != '0' && arg[0] != '1'){ - std::cerr << "[warning] [regex_rules.add] invalid is_case_sensitive (" << arg[0] << ") in '" << arg << "', must be '1' or '0', skipping..." << std::endl; - return -1; - } - if (arg[1] != 'C' && arg[1] != 'c' && arg[1] != 'S' && arg[1] != 's'){ - std::cerr << "[warning] [regex_rules.add] invalid filter_type (" << arg[1] << ") in '" << arg << "', must be 'C', 'c', 'S' or 's', skipping..." << std::endl; - return -1; - } - std::string hex(arg+2), expr; - if (!unhexlify(hex, expr)){ - std::cerr << "[warning] [regex_rules.add] invalid hex regex value (" << hex << "), skipping..." << std::endl; - return -1; - } - //Push regex - jp::Regex regex(expr,arg[0] == '1'?"gS":"giS"); - if (regex){ - std::cerr << "[info] [regex_rules.add] adding new regex filter: '" << expr << "'" << std::endl; - getByCode(arg[1])->push_back(std::make_pair(std::string(arg), regex)); - } else { - std::cerr << "[warning] [regex_rules.add] compiling of '" << expr << "' regex failed, skipping..." << std::endl; - return -1; - } - return 0; - } - - bool check(unsigned char* data, const size_t& bytes_transferred, const bool in_input){ - std::string str_data((char *) data, bytes_transferred); - for (regex_rule_pair ele:(in_input?input_blacklist:output_blacklist)){ - try{ - if(ele.second.match(str_data)){ - std::stringstream msg; - msg << "BLOCKED " << ele.first << "\n"; - std::cout << msg.str() << std::flush; - return false; - } - } catch(...){ - std::cerr << "[info] [regex_rules.check] Error while matching blacklist regex: " << ele.first << std::endl; - } - } - for (regex_rule_pair ele:(in_input?input_whitelist:output_whitelist)){ - try{ - std::cerr << "[debug] [regex_rules.check] regex whitelist match " << ele.second.getPattern() << std::endl; - if(!ele.second.match(str_data)){ - std::stringstream msg; - msg << "BLOCKED " << ele.first << "\n"; - std::cout << msg.str() << std::flush; - return false; - } - } catch(...){ - std::cerr << "[info] [regex_rules.check] Error while matching whitelist regex: " << ele.first << std::endl; - } - } - return true; - } - -}; - -#endif // REGEX_FILTER_HPP \ No newline at end of file diff --git a/backend/binsrc/classes/regex_rules.cpp b/backend/binsrc/classes/regex_rules.cpp new file mode 100644 index 0000000..c01b2a2 --- /dev/null +++ b/backend/binsrc/classes/regex_rules.cpp @@ -0,0 +1,174 @@ +#include +#include +#include +#include "../utils.hpp" +#include +#include + +using namespace std; + +#ifndef REGEX_FILTER_HPP +#define REGEX_FILTER_HPP + +enum FilterDirection{ CTOS, STOC }; + +struct decoded_regex { + string regex; + FilterDirection direction; + bool is_case_sensitive; +}; + +struct regex_ruleset { + hs_database_t* hs_db = nullptr; + vector regexes; +}; + +decoded_regex decode_regex(string regex){ + + size_t arg_len = regex.size(); + if (arg_len < 2 || arg_len%2 != 0){ + cerr << "[warning] [decode_regex] invalid arg passed (" << regex << "), skipping..." << endl; + throw runtime_error( "Invalid expression len (too small)" ); + } + if (regex[0] != '0' && regex[0] != '1'){ + cerr << "[warning] [decode_regex] invalid is_case_sensitive (" << regex[0] << ") in '" << regex << "', must be '1' or '0', skipping..." << endl; + throw runtime_error( "Invalid is_case_sensitive" ); + } + if (regex[1] != 'C' && regex[1] != 'S'){ + cerr << "[warning] [decode_regex] invalid filter_direction (" << regex[1] << ") in '" << regex << "', must be 'C', 'S', skipping..." << endl; + throw runtime_error( "Invalid filter_direction" ); + } + string hex(regex.c_str()+2), expr; + if (!unhexlify(hex, expr)){ + cerr << "[warning] [decode_regex] invalid hex regex value (" << hex << "), skipping..." << endl; + throw runtime_error( "Invalid hex regex encoded value" ); + } + decoded_regex ruleset{ + regex: expr, + direction: regex[1] == 'C' ? CTOS : STOC, + is_case_sensitive: regex[0] == '1' + }; + return ruleset; +} + +class RegexRules{ + public: + regex_ruleset output_ruleset, input_ruleset; + + private: + static inline u_int16_t glob_seq = 0; + u_int16_t version; + vector> decoded_input_rules; + vector> decoded_output_rules; + bool is_stream = true; + + void free_dbs(){ + if (output_ruleset.hs_db != nullptr){ + hs_free_database(output_ruleset.hs_db); + output_ruleset.hs_db = nullptr; + } + if (input_ruleset.hs_db != nullptr){ + hs_free_database(input_ruleset.hs_db); + input_ruleset.hs_db = nullptr; + } + } + + void fill_ruleset(vector> & decoded, regex_ruleset & ruleset){ + size_t n_of_regex = decoded.size(); + if (n_of_regex == 0){ + return; + } + vector regex_match_rules(n_of_regex); + vector regex_array_ids(n_of_regex); + vector regex_flags(n_of_regex); + for(int i = 0; i < n_of_regex; i++){ + regex_match_rules[i] = decoded[i].second.regex.c_str(); + regex_array_ids[i] = i; + regex_flags[i] = HS_FLAG_SINGLEMATCH | HS_FLAG_ALLOWEMPTY; + if (!decoded[i].second.is_case_sensitive){ + regex_flags[i] |= HS_FLAG_CASELESS; + } + } + #ifdef DEBUG + cerr << "[DEBUG] [RegexRules.fill_ruleset] compiling " << n_of_regex << " regexes..." << endl; + for (int i = 0; i < n_of_regex; i++){ + cerr << "[DEBUG] [RegexRules.fill_ruleset] regex[" << i << "]: " << decoded[i].first << " " << decoded[i].second.regex << endl; + cerr << "[DEBUG] [RegexRules.fill_ruleset] regex_match_rules[" << i << "]: " << regex_match_rules[i] << endl; + cerr << "[DEBUG] [RegexRules.fill_ruleset] regex_flags[" << i << "]: " << regex_flags[i] << endl; + cerr << "[DEBUG] [RegexRules.fill_ruleset] regex_array_ids[" << i << "]: " << regex_array_ids[i] << endl; + } + #endif + hs_database_t* rebuilt_db = nullptr; + hs_compile_error_t *compile_err = nullptr; + if ( + hs_compile_multi( + regex_match_rules.data(), + regex_flags.data(), + regex_array_ids.data(), + n_of_regex, + is_stream?HS_MODE_STREAM:HS_MODE_BLOCK, + nullptr, &rebuilt_db, &compile_err + ) != HS_SUCCESS + ) { + cerr << "[warning] [RegexRules.fill_ruleset] hs_db failed to compile: '" << compile_err->message << "' skipping..." << endl; + hs_free_compile_error(compile_err); + throw runtime_error( "Failed to compile hyperscan db" ); + } + ruleset.hs_db = rebuilt_db; + ruleset.regexes = vector(n_of_regex); + for(int i = 0; i < n_of_regex; i++){ + ruleset.regexes[i] = decoded[i].first; + } + } + + public: + RegexRules(vector raw_rules, bool is_stream){ + this->is_stream = is_stream; + this->version = ++glob_seq; // 0 version is a invalid version (useful for some logics) + for(string ele : raw_rules){ + try{ + decoded_regex rule = decode_regex(ele); + if (rule.direction == FilterDirection::CTOS){ + decoded_input_rules.push_back(make_pair(ele, rule)); + }else{ + decoded_output_rules.push_back(make_pair(ele, rule)); + } + }catch(...){ + throw current_exception(); + } + } + fill_ruleset(decoded_input_rules, input_ruleset); + try{ + fill_ruleset(decoded_output_rules, output_ruleset); + }catch(...){ + free_dbs(); + throw current_exception(); + } + } + + u_int16_t ver(){ + return version; + } + + RegexRules(bool is_stream){ + vector no_rules; + RegexRules(no_rules, is_stream); + } + + bool stream_mode(){ + return is_stream; + } + + + + RegexRules(){ + RegexRules(true); + } + + ~RegexRules(){ + free_dbs(); + } +}; + +#endif // REGEX_FILTER_HPP + diff --git a/backend/binsrc/nfqueue.cpp b/backend/binsrc/nfqueue.cpp index 63dee11..6538661 100644 --- a/backend/binsrc/nfqueue.cpp +++ b/backend/binsrc/nfqueue.cpp @@ -1,11 +1,11 @@ -#include "classes/regex_filter.hpp" -#include "classes/netfilter.hpp" +#include "classes/regex_rules.cpp" +#include "classes/netfilter.cpp" #include "utils.hpp" #include using namespace std; -shared_ptr regex_config; +shared_ptr regex_config; void config_updater (){ string line; @@ -21,44 +21,158 @@ void config_updater (){ } cerr << "[info] [updater] Updating configuration with line " << line << endl; istringstream config_stream(line); - regex_rules *regex_new_config = new regex_rules(); + vector raw_rules; + while(!config_stream.eof()){ string data; config_stream >> data; if (data != "" && data != "\n"){ - regex_new_config->add(data.c_str()); + raw_rules.push_back(data); } } - regex_config.reset(regex_new_config); - cerr << "[info] [updater] Config update done" << endl; - + try{ + regex_config.reset(new RegexRules(raw_rules, regex_config->stream_mode())); + cerr << "[info] [updater] Config update done to ver "<< regex_config->ver() << endl; + cout << "ACK OK" << endl; + }catch(const std::exception& e){ + cerr << "[error] [updater] Failed to build new configuration!" << endl; + cout << "ACK FAIL " << e.what() << endl; + } } } -template -bool filter_callback(const uint8_t *data, uint32_t len){ - shared_ptr current_config = regex_config; - return current_config->check((unsigned char *)data, len, is_input); +void inline scratch_setup(regex_ruleset &conf, hs_scratch_t* & scratch){ + if (scratch == nullptr && conf.hs_db != nullptr){ + if (hs_alloc_scratch(conf.hs_db, &scratch) != HS_SUCCESS) { + throw invalid_argument("Cannot alloc scratch"); + } + } } -int main(int argc, char *argv[]) -{ +struct matched_data{ + unsigned int matched = 0; + bool has_matched = false; +}; + + +bool filter_callback(packet_info& info){ + shared_ptr conf = regex_config; + auto current_version = conf->ver(); + if (current_version != info.sctx->latest_config_ver){ + #ifdef DEBUG + cerr << "[DEBUG] [filter_callback] Configuration has changed (" << current_version << "!=" << info.sctx->latest_config_ver << "), cleaning scratch spaces" << endl; + #endif + info.sctx->clean(); + info.sctx->latest_config_ver = current_version; + } + scratch_setup(conf->input_ruleset, info.sctx->in_scratch); + scratch_setup(conf->output_ruleset, info.sctx->out_scratch); + + hs_database_t* regex_matcher = info.is_input ? conf->input_ruleset.hs_db : conf->output_ruleset.hs_db; + if (regex_matcher == nullptr){ + return true; + } + + #ifdef DEBUG + cerr << "[DEBUG] [filter_callback] Matching packet with " << (info.is_input ? "input" : "output") << " ruleset" << endl; + if (info.payload.size() <= 30){ + cerr << "[DEBUG] [filter_callback] Packet: " << info.payload << endl; + } + #endif + + matched_data match_res; + hs_error_t err; + hs_scratch_t* scratch_space = info.is_input ? info.sctx->in_scratch: info.sctx->out_scratch; + auto match_func = [](unsigned int id, auto from, auto to, auto flags, auto ctx){ + auto res = (matched_data*)ctx; + res->has_matched = true; + res->matched = id; + return -1; // Stop matching + }; + hs_stream_t* stream_match; + if (conf->stream_mode()){ + matching_map* match_map = info.is_input ? &info.sctx->in_hs_streams : &info.sctx->out_hs_streams; + #ifdef DEBUG + cerr << "[DEBUG] [filter_callback] Dumping match_map " << match_map << endl; + for (auto ele: *match_map){ + cerr << "[DEBUG] [filter_callback] " << ele.first << " -> " << ele.second << endl; + } + cerr << "[DEBUG] [filter_callback] End of match_map" << endl; + #endif + auto stream_search = match_map->find(info.sid); + + if (stream_search == match_map->end()){ + + #ifdef DEBUG + cerr << "[DEBUG] [filter_callback] Creating new stream matcher for " << info.sid << endl; + #endif + if (hs_open_stream(regex_matcher, 0, &stream_match) != HS_SUCCESS) { + cerr << "[error] [filter_callback] Error opening the stream matcher (hs)" << endl; + throw invalid_argument("Cannot open stream match on hyperscan"); + } + if (info.is_tcp){ + match_map->insert_or_assign(info.sid, stream_match); + } + }else{ + stream_match = stream_search->second; + } + #ifdef DEBUG + cerr << "[DEBUG] [filter_callback] Matching as a stream" << endl; + #endif + err = hs_scan_stream( + stream_match,info.payload.c_str(), info.payload.length(), + 0, scratch_space, match_func, &match_res + ); + }else{ + #ifdef DEBUG + cerr << "[DEBUG] [filter_callback] Matching as a block" << endl; + #endif + err = hs_scan( + regex_matcher,info.payload.c_str(), info.payload.length(), + 0, scratch_space, match_func, &match_res + ); + } + if ( + !info.is_tcp && conf->stream_mode() && + hs_close_stream(stream_match, scratch_space, nullptr, nullptr) != HS_SUCCESS + ){ + cerr << "[error] [filter_callback] Error closing the stream matcher (hs)" << endl; + throw invalid_argument("Cannot close stream match on hyperscan"); + } + if (err != HS_SUCCESS && err != HS_SCAN_TERMINATED) { + cerr << "[error] [filter_callback] Error while matching the stream (hs)" << endl; + throw invalid_argument("Error while matching the stream with hyperscan"); + } + if (match_res.has_matched){ + auto rules_vector = info.is_input ? conf->input_ruleset.regexes : conf->output_ruleset.regexes; + stringstream msg; + msg << "BLOCKED " << rules_vector[match_res.matched] << "\n"; + cout << msg.str() << flush; + return false; + } + return true; +} + +int main(int argc, char *argv[]){ int n_of_threads = 1; char * n_threads_str = getenv("NTHREADS"); - if (n_threads_str != NULL) n_of_threads = ::atoi(n_threads_str); + if (n_threads_str != nullptr) n_of_threads = ::atoi(n_threads_str); if(n_of_threads <= 0) n_of_threads = 1; - if (n_of_threads % 2 != 0 ) n_of_threads++; - cerr << "[info] [main] Using " << n_of_threads << " threads" << endl; - regex_config.reset(new regex_rules()); - NFQueueSequence> input_queues(n_of_threads/2); - input_queues.start(); - NFQueueSequence> output_queues(n_of_threads/2); - output_queues.start(); - cout << "QUEUES INPUT " << input_queues.init() << " " << input_queues.end() << " OUTPUT " << output_queues.init() << " " << output_queues.end() << endl; - cerr << "[info] [main] Input queues: " << input_queues.init() << ":" << input_queues.end() << " threads assigned: " << n_of_threads/2 << endl; - cerr << "[info] [main] Output queues: " << output_queues.init() << ":" << output_queues.end() << " threads assigned: " << n_of_threads/2 << endl; + char * matchmode = getenv("MATCH_MODE"); + bool stream_mode = true; + if (matchmode != nullptr && strcmp(matchmode, "block") == 0){ + stream_mode = false; + } + + regex_config.reset(new RegexRules(stream_mode)); + + NFQueueSequence queues(n_of_threads); + queues.start(); + + cout << "QUEUES " << queues.init() << " " << queues.end() << endl; + cerr << "[info] [main] Queues: " << queues.init() << ":" << queues.end() << " threads assigned: " << n_of_threads << " stream mode: " << stream_mode << endl; config_updater(); } diff --git a/backend/binsrc/nfqueue_regex/Cargo.lock b/backend/binsrc/nfqueue_regex/Cargo.lock deleted file mode 100644 index 11e8d94..0000000 --- a/backend/binsrc/nfqueue_regex/Cargo.lock +++ /dev/null @@ -1,32 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "atomic_refcell" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" - -[[package]] -name = "libc" -version = "0.2.153" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" - -[[package]] -name = "nfq" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c8f4c88952507d9df9400a6a2e48640fb460e21dcb2b4716eb3ff156d6db9e" -dependencies = [ - "libc", -] - -[[package]] -name = "nfqueue_regex" -version = "0.1.0" -dependencies = [ - "atomic_refcell", - "nfq", -] diff --git a/backend/binsrc/nfqueue_regex/Cargo.toml b/backend/binsrc/nfqueue_regex/Cargo.toml deleted file mode 100644 index b9b0e0a..0000000 --- a/backend/binsrc/nfqueue_regex/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "nfqueue_regex" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -atomic_refcell = "0.1.13" -nfq = "0.2.5" -#hyperscan = "0.3.2" diff --git a/backend/binsrc/nfqueue_regex/src/main.rs b/backend/binsrc/nfqueue_regex/src/main.rs deleted file mode 100644 index 639dfd5..0000000 --- a/backend/binsrc/nfqueue_regex/src/main.rs +++ /dev/null @@ -1,150 +0,0 @@ -use atomic_refcell::AtomicRefCell; -use nfq::{Queue, Verdict}; -use std::cell::{Cell, RefCell}; -use std::env; -use std::pin::Pin; -use std::rc::Rc; -use std::sync::atomic::{AtomicPtr, AtomicU32}; -use std::sync::mpsc::{self, Receiver, Sender}; -use std::sync::Arc; -use std::thread::{self, sleep, sleep_ms, JoinHandle}; - -enum WorkerMessage { - Error(String), - Dropped(usize), -} - -impl ToString for WorkerMessage { - fn to_string(&self) -> String { - match self { - WorkerMessage::Error(e) => format!("E{}", e), - WorkerMessage::Dropped(d) => format!("D{}", d), - } - } -} -struct Pool { - _workers: Vec, - pub start: u16, - pub end: u16, -} - -const QUEUE_BASE_NUM: u16 = 1000; -impl Pool { - fn new(threads: u16, tx: Sender, db: RefCell<&str>) -> Self { - // Find free queues - let mut start = QUEUE_BASE_NUM; - let mut queues: Vec<(Queue, u16)> = vec![]; - while queues.len() != threads.into() { - for queue_num in - (start..start.checked_add(threads + 1).expect("No more queues left")).rev() - { - let mut queue = Queue::open().unwrap(); - if queue.bind(queue_num).is_err() { - start = queue_num; - while let Some((mut q, num)) = queues.pop() { - let _ = q.unbind(num); - } - break; - }; - queues.push((queue, queue_num)); - } - } - - Pool { - _workers: queues - .into_iter() - .map(|(queue, queue_num)| Worker::new(queue, queue_num, tx.clone())) - .collect(), - start, - end: (start + threads), - } - } - - // fn join(self) { - // for worker in self._workers { - // let _ = worker.join(); - // } - // } -} - -struct Worker { - _inner: JoinHandle<()>, -} - -impl Worker { - fn new(mut queue: Queue, _queue_num: u16, tx: Sender) -> Self { - Worker { - _inner: thread::spawn(move || loop { - let mut msg = queue.recv().unwrap_or_else(|_| { - let _ = tx.send(WorkerMessage::Error("Fuck".to_string())); - panic!(""); - }); - - msg.set_verdict(Verdict::Accept); - queue.verdict(msg).unwrap(); - }), - } - } -} -struct InputOuputPools { - pub output_queue: Pool, - pub input_queue: Pool, - rx: Receiver, -} -impl InputOuputPools { - fn new(threads: u16) -> InputOuputPools { - let (tx, rx) = mpsc::channel(); - InputOuputPools { - output_queue: Pool::new(threads / 2, tx.clone(), RefCell::new("ciao")), - input_queue: Pool::new(threads / 2, tx, RefCell::new("miao")), - rx, - } - } - - fn poll_events(&self) { - loop { - let event = self.rx.recv().expect("Channel has hung up"); - println!("{}", event.to_string()); - } - } -} - -static mut DB: AtomicPtr> = AtomicPtr::new(std::ptr::null_mut() as *mut Arc); - -fn main() -> std::io::Result<()> { - let mut my_x: Arc = Arc::new(0); - let my_x_ptr: *mut Arc = std::ptr::addr_of_mut!(my_x); - - unsafe { DB.store(my_x_ptr, std::sync::atomic::Ordering::SeqCst) }; - - thread::spawn(|| loop { - let x_ptr = unsafe { DB.load(std::sync::atomic::Ordering::SeqCst) }; - let x = unsafe { (*x_ptr).clone() }; - dbg!(x); - //sleep_ms(1000); - }); - - for i in 0..1000000000 { - let mut my_x: Arc = Arc::new(i); - let my_x_ptr: *mut Arc = std::ptr::addr_of_mut!(my_x); - unsafe { DB.store(my_x_ptr, std::sync::atomic::Ordering::SeqCst) }; - //sleep_ms(100); - } - - let mut threads = env::var("NPROCS").unwrap_or_default().parse().unwrap_or(2); - if threads % 2 != 0 { - threads += 1; - } - - let in_out_pools = InputOuputPools::new(threads); - eprintln!( - "[info] [main] Input queues: {}:{}", - in_out_pools.input_queue.start, in_out_pools.input_queue.end - ); - eprintln!( - "[info] [main] Output queues: {}:{}", - in_out_pools.output_queue.start, in_out_pools.output_queue.end - ); - in_out_pools.poll_events(); - Ok(()) -} diff --git a/backend/binsrc/nfqueue_regex/src/regex_rules.rs b/backend/binsrc/nfqueue_regex/src/regex_rules.rs deleted file mode 100644 index 8b13789..0000000 --- a/backend/binsrc/nfqueue_regex/src/regex_rules.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/backend/binsrc/proxy.cpp b/backend/binsrc/proxy.cpp deleted file mode 100644 index d572667..0000000 --- a/backend/binsrc/proxy.cpp +++ /dev/null @@ -1,493 +0,0 @@ -/* - Copyright (c) 2007 Arash Partow (http://www.partow.net) - URL: http://www.partow.net/programming/tcpproxy/index.html - Modified and adapted by Pwnzer0tt1 -*/ -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -typedef jpcre2::select jp; -using namespace std; - -bool unhexlify(string const &hex, string &newString) { - try{ - int len = hex.length(); - for(int i=0; i< len; i+=2) - { - std::string byte = hex.substr(i,2); - char chr = (char) (int)strtol(byte.c_str(), NULL, 16); - newString.push_back(chr); - } - return true; - } - catch (...){ - return false; - } -} - -typedef pair regex_rule_pair; -typedef vector regex_rule_vector; -struct regex_rules{ - regex_rule_vector regex_s_c_w, regex_c_s_w, regex_s_c_b, regex_c_s_b; - - regex_rule_vector* getByCode(char code){ - switch(code){ - case 'C': // Client to server Blacklist - return ®ex_c_s_b; break; - case 'c': // Client to server Whitelist - return ®ex_c_s_w; break; - case 'S': // Server to client Blacklist - return ®ex_s_c_b; break; - case 's': // Server to client Whitelist - return ®ex_s_c_w; break; - } - throw invalid_argument( "Expected 'C' 'c' 'S' or 's'" ); - } - - void add(const char* arg){ - - //Integrity checks - size_t arg_len = strlen(arg); - if (arg_len < 2 || arg_len%2 != 0) return; - if (arg[0] != '0' && arg[0] != '1') return; - if (arg[1] != 'C' && arg[1] != 'c' && arg[1] != 'S' && arg[1] != 's') return; - string hex(arg+2), expr; - if (!unhexlify(hex, expr)) return; - //Push regex - jp::Regex regex(expr,arg[0] == '1'?"gS":"giS"); - if (regex){ - #ifdef DEBUG - cerr << "Added regex " << expr << " " << arg << endl; - #endif - getByCode(arg[1])->push_back(make_pair(string(arg), regex)); - } else { - cerr << "Regex " << arg << " was not compiled successfully" << endl; - } - } - -}; -shared_ptr regex_config; - -mutex update_mutex; - -bool filter_data(unsigned char* data, const size_t& bytes_transferred, regex_rule_vector const &blacklist, regex_rule_vector const &whitelist){ - #ifdef DEBUG_PACKET - cerr << "---------------- Packet ----------------" << endl; - for(int i=0;i - { - public: - - typedef ip::tcp::socket socket_type; - typedef boost::shared_ptr ptr_type; - - bridge(boost::asio::io_context& ios) - : downstream_socket_(ios), - upstream_socket_ (ios), - thread_safety(ios) - {} - - socket_type& downstream_socket() - { - // Client socket - return downstream_socket_; - } - - socket_type& upstream_socket() - { - // Remote server socket - return upstream_socket_; - } - - void start(const string& upstream_host, unsigned short upstream_port) - { - // Attempt connection to remote server (upstream side) - upstream_socket_.async_connect( - ip::tcp::endpoint( - boost::asio::ip::address::from_string(upstream_host), - upstream_port), - boost::asio::bind_executor(thread_safety, - boost::bind( - &bridge::handle_upstream_connect, - shared_from_this(), - boost::asio::placeholders::error))); - } - - void handle_upstream_connect(const boost::system::error_code& error) - { - if (!error) - { - // Setup async read from remote server (upstream) - - upstream_socket_.async_read_some( - boost::asio::buffer(upstream_data_,max_data_length), - boost::asio::bind_executor(thread_safety, - boost::bind(&bridge::handle_upstream_read, - shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred))); - - // Setup async read from client (downstream) - downstream_socket_.async_read_some( - boost::asio::buffer(downstream_data_,max_data_length), - boost::asio::bind_executor(thread_safety, - boost::bind(&bridge::handle_downstream_read, - shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred))); - } - else - close(); - } - - private: - - /* - Section A: Remote Server --> Proxy --> Client - Process data recieved from remote sever then send to client. - */ - - // Read from remote server complete, now send data to client - void handle_upstream_read(const boost::system::error_code& error, - const size_t& bytes_transferred) // Da Server a Client - { - if (!error) - { - shared_ptr regex_old_config = regex_config; - if (filter_data(upstream_data_, bytes_transferred, regex_old_config->regex_s_c_b, regex_old_config->regex_s_c_w)){ - async_write(downstream_socket_, - boost::asio::buffer(upstream_data_,bytes_transferred), - boost::asio::bind_executor(thread_safety, - boost::bind(&bridge::handle_downstream_write, - shared_from_this(), - boost::asio::placeholders::error))); - }else{ - close(); - } - } - else - close(); - } - - // Write to client complete, Async read from remote server - void handle_downstream_write(const boost::system::error_code& error) - { - if (!error) - { - - upstream_socket_.async_read_some( - boost::asio::buffer(upstream_data_,max_data_length), - boost::asio::bind_executor(thread_safety, - boost::bind(&bridge::handle_upstream_read, - shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred))); - } - else - close(); - } - // *** End Of Section A *** - - - /* - Section B: Client --> Proxy --> Remove Server - Process data recieved from client then write to remove server. - */ - - // Read from client complete, now send data to remote server - void handle_downstream_read(const boost::system::error_code& error, - const size_t& bytes_transferred) // Da Client a Server - { - if (!error) - { - shared_ptr regex_old_config = regex_config; - if (filter_data(downstream_data_, bytes_transferred, regex_old_config->regex_c_s_b, regex_old_config->regex_c_s_w)){ - async_write(upstream_socket_, - boost::asio::buffer(downstream_data_,bytes_transferred), - boost::asio::bind_executor(thread_safety, - boost::bind(&bridge::handle_upstream_write, - shared_from_this(), - boost::asio::placeholders::error))); - }else{ - close(); - } - } - else - close(); - } - - // Write to remote server complete, Async read from client - void handle_upstream_write(const boost::system::error_code& error) - { - if (!error) - { - downstream_socket_.async_read_some( - boost::asio::buffer(downstream_data_,max_data_length), - boost::asio::bind_executor(thread_safety, - boost::bind(&bridge::handle_downstream_read, - shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred))); - } - else - close(); - } - // *** End Of Section B *** - - void close() - { - boost::mutex::scoped_lock lock(mutex_); - - if (downstream_socket_.is_open()) - { - downstream_socket_.close(); - } - - if (upstream_socket_.is_open()) - { - upstream_socket_.close(); - } - } - - socket_type downstream_socket_; - socket_type upstream_socket_; - - enum { max_data_length = 8192 }; //8KB - unsigned char downstream_data_[max_data_length]; - unsigned char upstream_data_ [max_data_length]; - boost::asio::io_context::strand thread_safety; - boost::mutex mutex_; - public: - - class acceptor - { - public: - - acceptor(boost::asio::io_context& io_context, - const string& local_host, unsigned short local_port, - const string& upstream_host, unsigned short upstream_port) - : io_context_(io_context), - localhost_address(boost::asio::ip::address_v4::from_string(local_host)), - acceptor_(io_context_,ip::tcp::endpoint(localhost_address,local_port)), - upstream_port_(upstream_port), - upstream_host_(upstream_host) - {} - - bool accept_connections() - { - try - { - session_ = boost::shared_ptr(new bridge(io_context_)); - - acceptor_.async_accept(session_->downstream_socket(), - boost::asio::bind_executor(session_->thread_safety, - boost::bind(&acceptor::handle_accept, - this, - boost::asio::placeholders::error))); - } - catch(exception& e) - { - cerr << "acceptor exception: " << e.what() << endl; - return false; - } - - return true; - } - - private: - - void handle_accept(const boost::system::error_code& error) - { - if (!error) - { - session_->start(upstream_host_,upstream_port_); - - if (!accept_connections()) - { - cerr << "Failure during call to accept." << endl; - } - } - else - { - cerr << "Error: " << error.message() << endl; - } - } - - boost::asio::io_context& io_context_; - ip::address_v4 localhost_address; - ip::tcp::acceptor acceptor_; - ptr_type session_; - unsigned short upstream_port_; - string upstream_host_; - }; - - }; -} - -void update_config (boost::asio::streambuf &input_buffer){ - #ifdef DEBUG - cerr << "Updating configuration" << endl; - #endif - std::istream config_stream(&input_buffer); - std::unique_lock lck(update_mutex); - regex_rules *regex_new_config = new regex_rules(); - string data; - while(true){ - config_stream >> data; - if (config_stream.eof()) break; - regex_new_config->add(data.c_str()); - } - regex_config.reset(regex_new_config); -} - -class async_updater -{ -public: - async_updater(boost::asio::io_context& io_context) : input_(io_context, ::dup(STDIN_FILENO)), thread_safety(io_context) - { - - boost::asio::async_read_until(input_, input_buffer_, '\n', - boost::asio::bind_executor(thread_safety, - boost::bind(&async_updater::on_update, this, - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred))); - } - - void on_update(const boost::system::error_code& error, std::size_t length) - { - if (!error) - { - update_config(input_buffer_); - boost::asio::async_read_until(input_, input_buffer_, '\n', - boost::asio::bind_executor(thread_safety, - boost::bind(&async_updater::on_update, this, - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred))); - } - else - { - close(); - } - } - - void close() - { - input_.close(); - } - -private: - boost::asio::posix::stream_descriptor input_; - boost::asio::io_context::strand thread_safety; - boost::asio::streambuf input_buffer_; -}; - - -int main(int argc, char* argv[]) -{ - if (argc < 5) - { - cerr << "usage: tcpproxy_server " << endl; - return 1; - } - - const unsigned short local_port = static_cast(::atoi(argv[2])); - const unsigned short forward_port = static_cast(::atoi(argv[4])); - const string local_host = argv[1]; - const string forward_host = argv[3]; - - int threads = 1; - char * n_threads_str = getenv("NTHREADS"); - if (n_threads_str != NULL) threads = ::atoi(n_threads_str); - - boost::asio::io_context ios; - - boost::asio::streambuf buf; - boost::asio::posix::stream_descriptor cin_in(ios, ::dup(STDIN_FILENO)); - boost::asio::read_until(cin_in, buf,'\n'); - update_config(buf); - - async_updater updater(ios); - - #ifdef DEBUG - cerr << "Starting Proxy" << endl; - #endif - try - { - tcp_proxy::bridge::acceptor acceptor(ios, - local_host, local_port, - forward_host, forward_port); - - acceptor.accept_connections(); - - if (threads > 1){ - boost::thread_group tg; - for (unsigned i = 0; i < threads; ++i) - tg.create_thread(boost::bind(&boost::asio::io_context::run, &ios)); - - tg.join_all(); - }else{ - ios.run(); - } - } - catch(exception& e) - { - cerr << "Error: " << e.what() << endl; - return 1; - } - #ifdef DEBUG - cerr << "Proxy stopped!" << endl; - #endif - - return 0; -} diff --git a/backend/binsrc/utils.hpp b/backend/binsrc/utils.hpp index 9d40366..b61ef22 100644 --- a/backend/binsrc/utils.hpp +++ b/backend/binsrc/utils.hpp @@ -10,7 +10,7 @@ bool unhexlify(std::string const &hex, std::string &newString) { for(int i=0; i< len; i+=2) { std::string byte = hex.substr(i,2); - char chr = (char) (int)strtol(byte.c_str(), NULL, 16); + char chr = (char) (int)strtol(byte.c_str(), nullptr, 16); newString.push_back(chr); } return true; diff --git a/backend/modules/firewall/firewall.py b/backend/modules/firewall/firewall.py index 13c0122..b5bb292 100644 --- a/backend/modules/firewall/firewall.py +++ b/backend/modules/firewall/firewall.py @@ -1,6 +1,6 @@ import asyncio from modules.firewall.nftables import FiregexTables -from modules.firewall.models import * +from modules.firewall.models import Rule, FirewallSettings from utils.sqlite import SQLite from modules.firewall.models import Action @@ -131,5 +131,5 @@ class FirewallManager: return self.db.get("allow_dhcp", "1") == "1" @drop_invalid.setter - def allow_dhcp(self, value): + def allow_dhcp_set(self, value): self.db.set("allow_dhcp", "1" if value else "0") diff --git a/backend/modules/nfregex/firegex.py b/backend/modules/nfregex/firegex.py index 8d57d5d..71afcf8 100644 --- a/backend/modules/nfregex/firegex.py +++ b/backend/modules/nfregex/firegex.py @@ -1,8 +1,12 @@ from modules.nfregex.nftables import FiregexTables -from utils import ip_parse, run_func +from utils import run_func from modules.nfregex.models import Service, Regex -import re, os, asyncio +import re +import os +import asyncio import traceback +from utils import DEBUG +from fastapi import HTTPException nft = FiregexTables() @@ -10,7 +14,6 @@ class RegexFilter: def __init__( self, regex, is_case_sensitive=True, - is_blacklist=True, input_mode=False, output_mode=False, blocked_packets=0, @@ -19,8 +22,8 @@ class RegexFilter: ): self.regex = regex self.is_case_sensitive = is_case_sensitive - self.is_blacklist = is_blacklist - if input_mode == output_mode: input_mode = output_mode = True # (False, False) == (True, True) + if input_mode == output_mode: + input_mode = output_mode = True # (False, False) == (True, True) self.input_mode = input_mode self.output_mode = output_mode self.blocked = blocked_packets @@ -32,19 +35,21 @@ class RegexFilter: def from_regex(cls, regex:Regex, update_func = None): return cls( id=regex.id, regex=regex.regex, is_case_sensitive=regex.is_case_sensitive, - is_blacklist=regex.is_blacklist, blocked_packets=regex.blocked_packets, + blocked_packets=regex.blocked_packets, input_mode = regex.mode in ["C","B"], output_mode=regex.mode in ["S","B"], update_func = update_func ) def compile(self): - if isinstance(self.regex, str): self.regex = self.regex.encode() - if not isinstance(self.regex, bytes): raise Exception("Invalid Regex Paramether") + if isinstance(self.regex, str): + self.regex = self.regex.encode() + if not isinstance(self.regex, bytes): + raise Exception("Invalid Regex Paramether") re.compile(self.regex) # raise re.error if it's invalid! case_sensitive = "1" if self.is_case_sensitive else "0" if self.input_mode: - yield case_sensitive + "C" + self.regex.hex() if self.is_blacklist else case_sensitive + "c"+ self.regex.hex() + yield case_sensitive + "C" + self.regex.hex() if self.output_mode: - yield case_sensitive + "S" + self.regex.hex() if self.is_blacklist else case_sensitive + "s"+ self.regex.hex() + yield case_sensitive + "S" + self.regex.hex() async def update(self): if self.update_func: @@ -60,6 +65,10 @@ class FiregexInterceptor: self.update_config_lock:asyncio.Lock self.process:asyncio.subprocess.Process self.update_task: asyncio.Task + self.ack_arrived = False + self.ack_status = None + self.ack_fail_what = "" + self.ack_lock = asyncio.Lock() @classmethod async def start(cls, srv: Service): @@ -67,16 +76,19 @@ class FiregexInterceptor: self.srv = srv self.filter_map_lock = asyncio.Lock() self.update_config_lock = asyncio.Lock() - input_range, output_range = await self._start_binary() + queue_range = await self._start_binary() self.update_task = asyncio.create_task(self.update_blocked()) - nft.add(self.srv, input_range, output_range) + nft.add(self.srv, queue_range) + if not self.ack_lock.locked(): + await self.ack_lock.acquire() return self async def _start_binary(self): proxy_binary_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),"../cppqueue") self.process = await asyncio.create_subprocess_exec( proxy_binary_path, - stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE + stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE, + env={"MATCH_MODE": "stream" if self.srv.proto == "tcp" else "block", "NTHREADS": os.getenv("NTHREADS","1")}, ) line_fut = self.process.stdout.readuntil() try: @@ -87,7 +99,7 @@ class FiregexInterceptor: line = line_fut.decode() if line.startswith("QUEUES "): params = line.split() - return (int(params[2]), int(params[3])), (int(params[5]), int(params[6])) + return (int(params[1]), int(params[2])) else: self.process.kill() raise Exception("Invalid binary output") @@ -96,14 +108,24 @@ class FiregexInterceptor: try: while True: line = (await self.process.stdout.readuntil()).decode() - if line.startswith("BLOCKED"): + if DEBUG: + print(line) + if line.startswith("BLOCKED "): regex_id = line.split()[1] async with self.filter_map_lock: if regex_id in self.filter_map: self.filter_map[regex_id].blocked+=1 await self.filter_map[regex_id].update() - except asyncio.CancelledError: pass - except asyncio.IncompleteReadError: pass + if line.startswith("ACK "): + self.ack_arrived = True + self.ack_status = line.split()[1].upper() == "OK" + if not self.ack_status: + self.ack_fail_what = " ".join(line.split()[2:]) + self.ack_lock.release() + except asyncio.CancelledError: + pass + except asyncio.IncompleteReadError: + pass except Exception: traceback.print_exc() @@ -116,6 +138,14 @@ class FiregexInterceptor: async with self.update_config_lock: self.process.stdin.write((" ".join(filters_codes)+"\n").encode()) await self.process.stdin.drain() + try: + async with asyncio.timeout(3): + await self.ack_lock.acquire() + except TimeoutError: + pass + if not self.ack_arrived or not self.ack_status: + raise HTTPException(status_code=500, detail=f"NFQ error: {self.ack_fail_what}") + async def reload(self, filters:list[RegexFilter]): async with self.filter_map_lock: @@ -135,6 +165,7 @@ class FiregexInterceptor: raw_filters = filter_obj.compile() for filter in raw_filters: res[filter] = filter_obj - except Exception: pass + except Exception: + pass return res diff --git a/backend/modules/nfregex/firewall.py b/backend/modules/nfregex/firewall.py index 9516f63..d0d5479 100644 --- a/backend/modules/nfregex/firewall.py +++ b/backend/modules/nfregex/firewall.py @@ -30,14 +30,15 @@ class ServiceManager: new_filters = set([f.id for f in regexes]) #remove old filters for f in old_filters: - if not f in new_filters: + if f not in new_filters: del self.filters[f] #add new filters for f in new_filters: - if not f in old_filters: + if f not in old_filters: filter = [ele for ele in regexes if ele.id == f][0] self.filters[f] = RegexFilter.from_regex(filter, self._stats_updater) - if self.interceptor: await self.interceptor.reload(self.filters.values()) + if self.interceptor: + await self.interceptor.reload(self.filters.values()) def __update_status_db(self, status): self.db.query("UPDATE services SET status = ? WHERE service_id = ?;", status, self.srv.id) @@ -114,4 +115,5 @@ class FirewallManager: else: raise ServiceNotFoundException() -class ServiceNotFoundException(Exception): pass +class ServiceNotFoundException(Exception): + pass diff --git a/backend/modules/nfregex/models.py b/backend/modules/nfregex/models.py index b81365a..0c36890 100644 --- a/backend/modules/nfregex/models.py +++ b/backend/modules/nfregex/models.py @@ -15,11 +15,10 @@ class Service: class Regex: - def __init__(self, regex_id: int, regex: bytes, mode: str, service_id: str, is_blacklist: bool, blocked_packets: int, is_case_sensitive: bool, active: bool, **other): + def __init__(self, regex_id: int, regex: bytes, mode: str, service_id: str, blocked_packets: int, is_case_sensitive: bool, active: bool, **other): self.regex = regex self.mode = mode self.service_id = service_id - self.is_blacklist = is_blacklist self.blocked_packets = blocked_packets self.id = regex_id self.is_case_sensitive = is_case_sensitive diff --git a/backend/modules/nfregex/nftables.py b/backend/modules/nfregex/nftables.py index a0bc917..ce0088a 100644 --- a/backend/modules/nfregex/nftables.py +++ b/backend/modules/nfregex/nftables.py @@ -45,36 +45,37 @@ class FiregexTables(NFTableManager): {"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}}, ]) - def add(self, srv:Service, queue_range_input, queue_range_output): + def add(self, srv:Service, queue_range): for ele in self.get(): if ele.__eq__(srv): return - init, end = queue_range_output + init, end = queue_range if init > end: init, end = end, init - self.cmd({ "insert":{ "rule": { - "family": "inet", - "table": self.table_name, - "chain": self.output_chain, - "expr": [ - {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, - {'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "sport"}}, "op": "==", "right": int(srv.port)}}, - {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} + self.cmd( + { "insert":{ "rule": { + "family": "inet", + "table": self.table_name, + "chain": self.output_chain, + "expr": [ + {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, + {'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "sport"}}, "op": "==", "right": int(srv.port)}}, + {"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1338}}, + {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} ] - }}}) - - init, end = queue_range_input - if init > end: init, end = end, init - self.cmd({"insert":{"rule":{ - "family": "inet", - "table": self.table_name, - "chain": self.input_chain, - "expr": [ - {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, - {'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "dport"}}, "op": "==", "right": int(srv.port)}}, - {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} - ] - }}}) + }}}, + {"insert":{"rule":{ + "family": "inet", + "table": self.table_name, + "chain": self.input_chain, + "expr": [ + {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, + {'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "dport"}}, "op": "==", "right": int(srv.port)}}, + {"mangle": {"key": {"meta": {"key": "mark"}},"value": 0x1337}}, + {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} + ] + }}} + ) def get(self) -> list[FiregexFilter]: diff --git a/backend/modules/porthijack/firewall.py b/backend/modules/porthijack/firewall.py index 29e2f06..03004b2 100644 --- a/backend/modules/porthijack/firewall.py +++ b/backend/modules/porthijack/firewall.py @@ -5,7 +5,8 @@ from utils.sqlite import SQLite nft = FiregexTables() -class ServiceNotFoundException(Exception): pass +class ServiceNotFoundException(Exception): + pass class ServiceManager: def __init__(self, srv: Service, db): @@ -29,7 +30,8 @@ class ServiceManager: async def refresh(self, srv:Service): self.srv = srv - if self.active: await self.restart() + if self.active: + await self.restart() def _set_status(self,active): self.active = active diff --git a/backend/modules/porthijack/nftables.py b/backend/modules/porthijack/nftables.py index 34e0669..1d8dcde 100644 --- a/backend/modules/porthijack/nftables.py +++ b/backend/modules/porthijack/nftables.py @@ -50,7 +50,8 @@ class FiregexTables(NFTableManager): def add(self, srv:Service): for ele in self.get(): - if ele.__eq__(srv): return + if ele.__eq__(srv): + return self.cmd({ "insert":{ "rule": { "family": "inet", diff --git a/backend/modules/regexproxy/proxy.py b/backend/modules/regexproxy/proxy.py deleted file mode 100644 index efaa090..0000000 --- a/backend/modules/regexproxy/proxy.py +++ /dev/null @@ -1,116 +0,0 @@ -import re, os, asyncio - -class Filter: - def __init__(self, regex, is_case_sensitive=True, is_blacklist=True, c_to_s=False, s_to_c=False, blocked_packets=0, code=None): - self.regex = regex - self.is_case_sensitive = is_case_sensitive - self.is_blacklist = is_blacklist - if c_to_s == s_to_c: c_to_s = s_to_c = True # (False, False) == (True, True) - self.c_to_s = c_to_s - self.s_to_c = s_to_c - self.blocked = blocked_packets - self.code = code - - def compile(self): - if isinstance(self.regex, str): self.regex = self.regex.encode() - if not isinstance(self.regex, bytes): raise Exception("Invalid Regex Paramether") - re.compile(self.regex) # raise re.error if is invalid! - case_sensitive = "1" if self.is_case_sensitive else "0" - if self.c_to_s: - yield case_sensitive + "C" + self.regex.hex() if self.is_blacklist else case_sensitive + "c"+ self.regex.hex() - if self.s_to_c: - yield case_sensitive + "S" + self.regex.hex() if self.is_blacklist else case_sensitive + "s"+ self.regex.hex() - -class Proxy: - def __init__(self, internal_port=0, public_port=0, callback_blocked_update=None, filters=None, public_host="0.0.0.0", internal_host="127.0.0.1"): - self.filter_map = {} - self.filter_map_lock = asyncio.Lock() - self.update_config_lock = asyncio.Lock() - self.status_change = asyncio.Lock() - self.public_host = public_host - self.public_port = public_port - self.internal_host = internal_host - self.internal_port = internal_port - self.filters = set(filters) if filters else set([]) - self.process = None - self.callback_blocked_update = callback_blocked_update - - async def start(self, in_pause=False): - await self.status_change.acquire() - if not self.isactive(): - try: - self.filter_map = self.compile_filters() - filters_codes = self.get_filter_codes() if not in_pause else [] - proxy_binary_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),"../proxy") - - self.process = await asyncio.create_subprocess_exec( - proxy_binary_path, str(self.public_host), str(self.public_port), str(self.internal_host), str(self.internal_port), - stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE - ) - await self.update_config(filters_codes) - finally: - self.status_change.release() - try: - while True: - buff = await self.process.stdout.readuntil() - stdout_line = buff.decode() - if stdout_line.startswith("BLOCKED"): - regex_id = stdout_line.split()[1] - async with self.filter_map_lock: - if regex_id in self.filter_map: - self.filter_map[regex_id].blocked+=1 - if self.callback_blocked_update: self.callback_blocked_update(self.filter_map[regex_id]) - except Exception: - return await self.process.wait() - else: - self.status_change.release() - - - async def stop(self): - async with self.status_change: - if self.isactive(): - self.process.kill() - return False - return True - - async def restart(self, in_pause=False): - status = await self.stop() - await self.start(in_pause=in_pause) - return status - - async def update_config(self, filters_codes): - async with self.update_config_lock: - if (self.isactive()): - self.process.stdin.write((" ".join(filters_codes)+"\n").encode()) - await self.process.stdin.drain() - - async def reload(self): - if self.isactive(): - async with self.filter_map_lock: - self.filter_map = self.compile_filters() - filters_codes = self.get_filter_codes() - await self.update_config(filters_codes) - - def get_filter_codes(self): - filters_codes = list(self.filter_map.keys()) - filters_codes.sort(key=lambda a: self.filter_map[a].blocked, reverse=True) - return filters_codes - - def isactive(self): - return self.process and self.process.returncode is None - - async def pause(self): - if self.isactive(): - await self.update_config([]) - else: - await self.start(in_pause=True) - - def compile_filters(self): - res = {} - for filter_obj in self.filters: - try: - raw_filters = filter_obj.compile() - for filter in raw_filters: - res[filter] = filter_obj - except Exception: pass - return res diff --git a/backend/modules/regexproxy/utils.py b/backend/modules/regexproxy/utils.py deleted file mode 100644 index 2695b87..0000000 --- a/backend/modules/regexproxy/utils.py +++ /dev/null @@ -1,199 +0,0 @@ -import secrets -from modules.regexproxy.proxy import Filter, Proxy -import random, socket, asyncio -from base64 import b64decode -from utils.sqlite import SQLite -from utils import socketio_emit - -class STATUS: - WAIT = "wait" - STOP = "stop" - PAUSE = "pause" - ACTIVE = "active" - -class ServiceNotFoundException(Exception): pass - -class ServiceManager: - def __init__(self, id, db): - self.id = id - self.db = db - self.proxy = Proxy( - internal_host="127.0.0.1", - callback_blocked_update=self._stats_updater - ) - self.status = STATUS.STOP - self.wanted_status = STATUS.STOP - self.filters = {} - self._update_port_from_db() - self._update_filters_from_db() - self.lock = asyncio.Lock() - self.starter = None - - def _update_port_from_db(self): - res = self.db.query(""" - SELECT - public_port, - internal_port - FROM services WHERE service_id = ?; - """, self.id) - if len(res) == 0: raise ServiceNotFoundException() - self.proxy.internal_port = res[0]["internal_port"] - self.proxy.public_port = res[0]["public_port"] - - def _update_filters_from_db(self): - res = self.db.query(""" - SELECT - regex, mode, regex_id `id`, is_blacklist, - blocked_packets n_packets, is_case_sensitive - FROM regexes WHERE service_id = ? AND active=1; - """, self.id) - - #Filter check - old_filters = set(self.filters.keys()) - new_filters = set([f["id"] for f in res]) - - #remove old filters - for f in old_filters: - if not f in new_filters: - del self.filters[f] - - for f in new_filters: - if not f in old_filters: - filter_info = [ele for ele in res if ele["id"] == f][0] - self.filters[f] = Filter( - is_case_sensitive=filter_info["is_case_sensitive"], - c_to_s=filter_info["mode"] in ["C","B"], - s_to_c=filter_info["mode"] in ["S","B"], - is_blacklist=filter_info["is_blacklist"], - regex=b64decode(filter_info["regex"]), - blocked_packets=filter_info["n_packets"], - code=f - ) - self.proxy.filters = list(self.filters.values()) - - def __update_status_db(self, status): - self.db.query("UPDATE services SET status = ? WHERE service_id = ?;", status, self.id) - - async def next(self,to): - async with self.lock: - return await self._next(to) - - async def _next(self, to): - if self.status != to: - # ACTIVE -> PAUSE or PAUSE -> ACTIVE - if (self.status, to) in [(STATUS.ACTIVE, STATUS.PAUSE)]: - await self.proxy.pause() - self._set_status(to) - - elif (self.status, to) in [(STATUS.PAUSE, STATUS.ACTIVE)]: - await self.proxy.reload() - self._set_status(to) - - # ACTIVE -> STOP - elif (self.status,to) in [(STATUS.ACTIVE, STATUS.STOP), (STATUS.WAIT, STATUS.STOP), (STATUS.PAUSE, STATUS.STOP)]: #Stop proxy - if self.starter: self.starter.cancel() - await self.proxy.stop() - self._set_status(to) - - # STOP -> ACTIVE or STOP -> PAUSE - elif (self.status, to) in [(STATUS.STOP, STATUS.ACTIVE), (STATUS.STOP, STATUS.PAUSE)]: - self.wanted_status = to - self._set_status(STATUS.WAIT) - self.__proxy_starter(to) - - - def _stats_updater(self,filter:Filter): - self.db.query("UPDATE regexes SET blocked_packets = ? WHERE regex_id = ?;", filter.blocked, filter.code) - - async def update_port(self): - async with self.lock: - self._update_port_from_db() - if self.status in [STATUS.PAUSE, STATUS.ACTIVE]: - next_status = self.status if self.status != STATUS.WAIT else self.wanted_status - await self._next(STATUS.STOP) - await self._next(next_status) - - def _set_status(self,status): - self.status = status - self.__update_status_db(status) - - - async def update_filters(self): - async with self.lock: - self._update_filters_from_db() - if self.status in [STATUS.PAUSE, STATUS.ACTIVE]: - await self.proxy.reload() - - def __proxy_starter(self,to): - async def func(): - try: - while True: - if check_port_is_open(self.proxy.public_port): - self._set_status(to) - await socketio_emit(["regexproxy"]) - await self.proxy.start(in_pause=(to==STATUS.PAUSE)) - self._set_status(STATUS.STOP) - await socketio_emit(["regexproxy"]) - return - else: - await asyncio.sleep(.5) - except asyncio.CancelledError: - self._set_status(STATUS.STOP) - await self.proxy.stop() - self.starter = asyncio.create_task(func()) - -class ProxyManager: - def __init__(self, db:SQLite): - self.db = db - self.proxy_table: dict[str, ServiceManager] = {} - self.lock = asyncio.Lock() - - async def close(self): - for key in list(self.proxy_table.keys()): - await self.remove(key) - - async def remove(self,id): - async with self.lock: - if id in self.proxy_table: - await self.proxy_table[id].next(STATUS.STOP) - del self.proxy_table[id] - - async def reload(self): - async with self.lock: - for srv in self.db.query('SELECT service_id, status FROM services;'): - srv_id, req_status = srv["service_id"], srv["status"] - if srv_id in self.proxy_table: - continue - - self.proxy_table[srv_id] = ServiceManager(srv_id,self.db) - await self.proxy_table[srv_id].next(req_status) - - def get(self,id) -> ServiceManager: - if id in self.proxy_table: - return self.proxy_table[id] - else: - raise ServiceNotFoundException() - -def check_port_is_open(port): - try: - sock = socket.socket() - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind(('0.0.0.0',port)) - sock.close() - return True - except Exception: - return False - -def gen_service_id(db): - while True: - res = secrets.token_hex(8) - if len(db.query('SELECT 1 FROM services WHERE service_id = ?;', res)) == 0: - break - return res - -def gen_internal_port(db): - while True: - res = random.randint(30000, 45000) - if len(db.query('SELECT 1 FROM services WHERE internal_port = ?;', res)) == 0: - break - return res \ No newline at end of file diff --git a/backend/routers/firewall.py b/backend/routers/firewall.py index 9f3d311..8801db9 100644 --- a/backend/routers/firewall.py +++ b/backend/routers/firewall.py @@ -5,7 +5,7 @@ from utils import ip_parse, ip_family, socketio_emit from utils.models import ResetRequest, StatusMessageModel from modules.firewall.nftables import FiregexTables from modules.firewall.firewall import FirewallManager -from modules.firewall.models import * +from modules.firewall.models import FirewallSettings, RuleInfo, RuleModel, RuleFormAdd, Mode, Table db = SQLite('db/firewall-rules.db', { 'rules': { diff --git a/backend/routers/nfregex.py b/backend/routers/nfregex.py index b50dc34..f5de063 100644 --- a/backend/routers/nfregex.py +++ b/backend/routers/nfregex.py @@ -28,7 +28,6 @@ class RegexModel(BaseModel): mode:str id:int service_id:str - is_blacklist: bool n_packets:int is_case_sensitive:bool active:bool @@ -38,7 +37,6 @@ class RegexAddForm(BaseModel): regex: str mode: str active: bool|None = None - is_blacklist: bool is_case_sensitive: bool class ServiceAddForm(BaseModel): @@ -66,7 +64,6 @@ db = SQLite('db/nft-regex.db', { 'regex': 'TEXT NOT NULL', 'mode': 'VARCHAR(1) NOT NULL CHECK (mode IN ("C", "S", "B"))', # C = to the client, S = to the server, B = both 'service_id': 'VARCHAR(100) NOT NULL', - 'is_blacklist': 'BOOLEAN NOT NULL CHECK (is_blacklist IN (0, 1))', 'blocked_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0', 'regex_id': 'INTEGER PRIMARY KEY', 'is_case_sensitive' : 'BOOLEAN NOT NULL CHECK (is_case_sensitive IN (0, 1))', @@ -75,7 +72,7 @@ db = SQLite('db/nft-regex.db', { }, 'QUERY':[ "CREATE UNIQUE INDEX IF NOT EXISTS unique_services ON services (port, ip_int, proto);", - "CREATE UNIQUE INDEX IF NOT EXISTS unique_regex_service ON regexes (regex,service_id,is_blacklist,mode,is_case_sensitive);" + "CREATE UNIQUE INDEX IF NOT EXISTS unique_regex_service ON regexes (regex,service_id,mode,is_case_sensitive);" ] }) @@ -92,12 +89,18 @@ async def reset(params: ResetRequest): db.init() else: db.restore() - await firewall.init() + try: + await firewall.init() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) async def startup(): db.init() - await firewall.init() + try: + await firewall.init() + except Exception as e: + print("WARNING cannot start firewall:", e) async def shutdown(): db.backup() @@ -147,7 +150,8 @@ async def get_service_by_id(service_id: str): FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id WHERE s.service_id = ? GROUP BY s.service_id; """, service_id) - if len(res) == 0: raise HTTPException(status_code=400, detail="This service does not exists!") + if len(res) == 0: + raise HTTPException(status_code=400, detail="This service does not exists!") return res[0] @app.get('/service/{service_id}/stop', response_model=StatusMessageModel) @@ -177,7 +181,8 @@ async def service_delete(service_id: str): async def service_rename(service_id: str, form: RenameForm): """Request to change the name of a specific service""" form.name = refactor_name(form.name) - if not form.name: raise HTTPException(status_code=400, detail="The name cannot be empty!") + if not form.name: + raise HTTPException(status_code=400, detail="The name cannot be empty!") try: db.query('UPDATE services SET name=? WHERE service_id = ?;', form.name, service_id) except sqlite3.IntegrityError: @@ -188,10 +193,11 @@ async def service_rename(service_id: str, form: RenameForm): @app.get('/service/{service_id}/regexes', response_model=list[RegexModel]) async def get_service_regexe_list(service_id: str): """Get the list of the regexes of a service""" - if not db.query("SELECT 1 FROM services s WHERE s.service_id = ?;", service_id): raise HTTPException(status_code=400, detail="This service does not exists!") + if not db.query("SELECT 1 FROM services s WHERE s.service_id = ?;", service_id): + raise HTTPException(status_code=400, detail="This service does not exists!") return db.query(""" SELECT - regex, mode, regex_id `id`, service_id, is_blacklist, + regex, mode, regex_id `id`, service_id, blocked_packets n_packets, is_case_sensitive, active FROM regexes WHERE service_id = ?; """, service_id) @@ -201,11 +207,12 @@ async def get_regex_by_id(regex_id: int): """Get regex info using his id""" res = db.query(""" SELECT - regex, mode, regex_id `id`, service_id, is_blacklist, + regex, mode, regex_id `id`, service_id, blocked_packets n_packets, is_case_sensitive, active FROM regexes WHERE `id` = ?; """, regex_id) - if len(res) == 0: raise HTTPException(status_code=400, detail="This regex does not exists!") + if len(res) == 0: + raise HTTPException(status_code=400, detail="This regex does not exists!") return res[0] @app.get('/regex/{regex_id}/delete', response_model=StatusMessageModel) @@ -247,8 +254,8 @@ async def add_new_regex(form: RegexAddForm): except Exception: raise HTTPException(status_code=400, detail="Invalid regex") try: - db.query("INSERT INTO regexes (service_id, regex, is_blacklist, mode, is_case_sensitive, active ) VALUES (?, ?, ?, ?, ?, ?);", - form.service_id, form.regex, form.is_blacklist, form.mode, form.is_case_sensitive, True if form.active is None else form.active ) + db.query("INSERT INTO regexes (service_id, regex, mode, is_case_sensitive, active ) VALUES (?, ?, ?, ?, ?);", + form.service_id, form.regex, form.mode, form.is_case_sensitive, True if form.active is None else form.active ) except sqlite3.IntegrityError: raise HTTPException(status_code=400, detail="An identical regex already exists") diff --git a/backend/routers/porthijack.py b/backend/routers/porthijack.py index 6bf8603..8fd3c54 100644 --- a/backend/routers/porthijack.py +++ b/backend/routers/porthijack.py @@ -96,7 +96,8 @@ async def get_service_list(): async def get_service_by_id(service_id: str): """Get info about a specific service using his id""" res = db.query("SELECT service_id, active, public_port, proxy_port, name, proto, ip_src, ip_dst FROM services WHERE service_id = ?;", service_id) - if len(res) == 0: raise HTTPException(status_code=400, detail="This service does not exists!") + if len(res) == 0: + raise HTTPException(status_code=400, detail="This service does not exists!") return res[0] @app.get('/service/{service_id}/stop', response_model=StatusMessageModel) @@ -125,7 +126,8 @@ async def service_delete(service_id: str): async def service_rename(service_id: str, form: RenameForm): """Request to change the name of a specific service""" form.name = refactor_name(form.name) - if not form.name: raise HTTPException(status_code=400, detail="The name cannot be empty!") + if not form.name: + raise HTTPException(status_code=400, detail="The name cannot be empty!") try: db.query('UPDATE services SET name=? WHERE service_id = ?;', form.name, service_id) except sqlite3.IntegrityError: diff --git a/backend/routers/regexproxy.py b/backend/routers/regexproxy.py deleted file mode 100644 index 5360592..0000000 --- a/backend/routers/regexproxy.py +++ /dev/null @@ -1,311 +0,0 @@ -from base64 import b64decode -import sqlite3, re -from fastapi import APIRouter, HTTPException -from pydantic import BaseModel -from modules.regexproxy.utils import STATUS, ProxyManager, gen_internal_port, gen_service_id -from utils.sqlite import SQLite -from utils.models import ResetRequest, StatusMessageModel -from utils import refactor_name, socketio_emit, PortType - -app = APIRouter() -db = SQLite("db/regextcpproxy.db",{ - 'services': { - 'status': 'VARCHAR(100) NOT NULL', - 'service_id': 'VARCHAR(100) PRIMARY KEY', - 'internal_port': 'INT NOT NULL CHECK(internal_port > 0 and internal_port < 65536)', - 'public_port': 'INT NOT NULL CHECK(internal_port > 0 and internal_port < 65536) UNIQUE', - 'name': 'VARCHAR(100) NOT NULL UNIQUE' - }, - 'regexes': { - 'regex': 'TEXT NOT NULL', - 'mode': 'VARCHAR(1) NOT NULL', - 'service_id': 'VARCHAR(100) NOT NULL', - 'is_blacklist': 'BOOLEAN NOT NULL CHECK (is_blacklist IN (0, 1))', - 'blocked_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0', - 'regex_id': 'INTEGER PRIMARY KEY', - 'is_case_sensitive' : 'BOOLEAN NOT NULL CHECK (is_case_sensitive IN (0, 1))', - 'active' : 'BOOLEAN NOT NULL CHECK (is_case_sensitive IN (0, 1)) DEFAULT 1', - 'FOREIGN KEY (service_id)':'REFERENCES services (service_id)', - }, - 'QUERY':[ - "CREATE UNIQUE INDEX IF NOT EXISTS unique_regex_service ON regexes (regex,service_id,is_blacklist,mode,is_case_sensitive);" - ] -}) - -firewall = ProxyManager(db) - -async def reset(params: ResetRequest): - if not params.delete: - db.backup() - await firewall.close() - if params.delete: - db.delete() - db.init() - else: - db.restore() - await firewall.reload() - - -async def startup(): - db.init() - await firewall.reload() - -async def shutdown(): - db.backup() - await firewall.close() - db.disconnect() - db.restore() - -async def refresh_frontend(additional:list[str]=[]): - await socketio_emit(["regexproxy"]+additional) - -class GeneralStatModel(BaseModel): - closed:int - regexes: int - services: int - -@app.get('/stats', response_model=GeneralStatModel) -async def get_general_stats(): - """Get firegex general status about services""" - return db.query(""" - SELECT - (SELECT COALESCE(SUM(blocked_packets),0) FROM regexes) closed, - (SELECT COUNT(*) FROM regexes) regexes, - (SELECT COUNT(*) FROM services) services - """)[0] - -class ServiceModel(BaseModel): - id:str - status: str - public_port: PortType - internal_port: PortType - name: str - n_regex: int - n_packets: int - -@app.get('/services', response_model=list[ServiceModel]) -async def get_service_list(): - """Get the list of existent firegex services""" - return db.query(""" - SELECT - s.service_id `id`, - s.status status, - s.public_port public_port, - s.internal_port internal_port, - s.name name, - COUNT(r.regex_id) n_regex, - COALESCE(SUM(r.blocked_packets),0) n_packets - FROM services s LEFT JOIN regexes r ON r.service_id = s.service_id - GROUP BY s.service_id; - """) - -@app.get('/service/{service_id}', response_model=ServiceModel) -async def get_service_by_id(service_id: str): - """Get info about a specific service using his id""" - res = db.query(""" - SELECT - s.service_id `id`, - s.status status, - s.public_port public_port, - s.internal_port internal_port, - s.name name, - COUNT(r.regex_id) n_regex, - COALESCE(SUM(r.blocked_packets),0) n_packets - FROM services s LEFT JOIN regexes r ON r.service_id = s.service_id WHERE s.service_id = ? - GROUP BY s.service_id; - """, service_id) - if len(res) == 0: raise HTTPException(status_code=400, detail="This service does not exists!") - return res[0] - -@app.get('/service/{service_id}/stop', response_model=StatusMessageModel) -async def service_stop(service_id: str): - """Request the stop of a specific service""" - await firewall.get(service_id).next(STATUS.STOP) - await refresh_frontend() - return {'status': 'ok'} - -@app.get('/service/{service_id}/pause', response_model=StatusMessageModel) -async def service_pause(service_id: str): - """Request the pause of a specific service""" - await firewall.get(service_id).next(STATUS.PAUSE) - await refresh_frontend() - return {'status': 'ok'} - -@app.get('/service/{service_id}/start', response_model=StatusMessageModel) -async def service_start(service_id: str): - """Request the start of a specific service""" - await firewall.get(service_id).next(STATUS.ACTIVE) - await refresh_frontend() - return {'status': 'ok'} - -@app.get('/service/{service_id}/delete', response_model=StatusMessageModel) -async def service_delete(service_id: str): - """Request the deletion of a specific service""" - db.query('DELETE FROM services WHERE service_id = ?;', service_id) - db.query('DELETE FROM regexes WHERE service_id = ?;', service_id) - await firewall.remove(service_id) - await refresh_frontend() - return {'status': 'ok'} - - -@app.get('/service/{service_id}/regen-port', response_model=StatusMessageModel) -async def regen_service_port(service_id: str): - """Request the regeneration of a the internal proxy port of a specific service""" - db.query('UPDATE services SET internal_port = ? WHERE service_id = ?;', gen_internal_port(db), service_id) - await firewall.get(service_id).update_port() - await refresh_frontend() - return {'status': 'ok'} - -class ChangePortForm(BaseModel): - port: int|None = None - internalPort: int|None = None - -@app.post('/service/{service_id}/change-ports', response_model=StatusMessageModel) -async def change_service_ports(service_id: str, change_port:ChangePortForm): - """Choose and change the ports of the service""" - if change_port.port is None and change_port.internalPort is None: - raise HTTPException(status_code=400, detail="Invalid Request!") - try: - sql_inj = "" - query:list[str|int] = [] - if not change_port.port is None: - sql_inj+=" public_port = ? " - query.append(change_port.port) - if not change_port.port is None and not change_port.internalPort is None: - sql_inj += "," - if not change_port.internalPort is None: - sql_inj+=" internal_port = ? " - query.append(change_port.internalPort) - query.append(service_id) - db.query(f'UPDATE services SET {sql_inj} WHERE service_id = ?;', *query) - except sqlite3.IntegrityError: - raise HTTPException(status_code=400, detail="Port of the service has been already assigned to another service") - await firewall.get(service_id).update_port() - await refresh_frontend() - return {'status': 'ok'} - -class RegexModel(BaseModel): - regex:str - mode:str - id:int - service_id:str - is_blacklist: bool - n_packets:int - is_case_sensitive:bool - active:bool - -@app.get('/service/{service_id}/regexes', response_model=list[RegexModel]) -async def get_service_regexe_list(service_id: str): - """Get the list of the regexes of a service""" - if not db.query("SELECT 1 FROM services s WHERE s.service_id = ?;", service_id): raise HTTPException(status_code=400, detail="This service does not exists!") - return db.query(""" - SELECT - regex, mode, regex_id `id`, service_id, is_blacklist, - blocked_packets n_packets, is_case_sensitive, active - FROM regexes WHERE service_id = ?; - """, service_id) - -@app.get('/regex/{regex_id}', response_model=RegexModel) -async def get_regex_by_id(regex_id: int): - """Get regex info using his id""" - res = db.query(""" - SELECT - regex, mode, regex_id `id`, service_id, is_blacklist, - blocked_packets n_packets, is_case_sensitive, active - FROM regexes WHERE `id` = ?; - """, regex_id) - if len(res) == 0: raise HTTPException(status_code=400, detail="This regex does not exists!") - return res[0] - -@app.get('/regex/{regex_id}/delete', response_model=StatusMessageModel) -async def regex_delete(regex_id: int): - """Delete a regex using his id""" - res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) - if len(res) != 0: - db.query('DELETE FROM regexes WHERE regex_id = ?;', regex_id) - await firewall.get(res[0]["service_id"]).update_filters() - await refresh_frontend() - return {'status': 'ok'} - -@app.get('/regex/{regex_id}/enable', response_model=StatusMessageModel) -async def regex_enable(regex_id: int): - """Request the enabling of a regex""" - res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) - if len(res) != 0: - db.query('UPDATE regexes SET active=1 WHERE regex_id = ?;', regex_id) - await firewall.get(res[0]["service_id"]).update_filters() - await refresh_frontend() - return {'status': 'ok'} - -@app.get('/regex/{regex_id}/disable', response_model=StatusMessageModel) -async def regex_disable(regex_id: int): - """Request the deactivation of a regex""" - res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) - if len(res) != 0: - db.query('UPDATE regexes SET active=0 WHERE regex_id = ?;', regex_id) - await firewall.get(res[0]["service_id"]).update_filters() - await refresh_frontend() - return {'status': 'ok'} - -class RegexAddForm(BaseModel): - service_id: str - regex: str - mode: str - active: bool|None = None - is_blacklist: bool - is_case_sensitive: bool - -@app.post('/regexes/add', response_model=StatusMessageModel) -async def add_new_regex(form: RegexAddForm): - """Add a new regex""" - try: - re.compile(b64decode(form.regex)) - except Exception: - raise HTTPException(status_code=400, detail="Invalid regex") - try: - db.query("INSERT INTO regexes (service_id, regex, is_blacklist, mode, is_case_sensitive, active ) VALUES (?, ?, ?, ?, ?, ?);", - form.service_id, form.regex, form.is_blacklist, form.mode, form.is_case_sensitive, True if form.active is None else form.active ) - except sqlite3.IntegrityError: - raise HTTPException(status_code=400, detail="An identical regex already exists") - await firewall.get(form.service_id).update_filters() - await refresh_frontend() - return {'status': 'ok'} - -class ServiceAddForm(BaseModel): - name: str - port: PortType - internalPort: int|None = None - -class ServiceAddStatus(BaseModel): - status:str - id: str|None = None - -class RenameForm(BaseModel): - name:str - -@app.post('/service/{service_id}/rename', response_model=StatusMessageModel) -async def service_rename(service_id: str, form: RenameForm): - """Request to change the name of a specific service""" - form.name = refactor_name(form.name) - if not form.name: raise HTTPException(status_code=400, detail="The name cannot be empty!") - try: - db.query('UPDATE services SET name=? WHERE service_id = ?;', form.name, service_id) - except sqlite3.IntegrityError: - raise HTTPException(status_code=400, detail="The name is already used!") - await refresh_frontend() - return {'status': 'ok'} - -@app.post('/services/add', response_model=ServiceAddStatus) -async def add_new_service(form: ServiceAddForm): - """Add a new service""" - serv_id = gen_service_id(db) - form.name = refactor_name(form.name) - try: - internal_port = form.internalPort if form.internalPort else gen_internal_port(db) - db.query("INSERT INTO services (name, service_id, internal_port, public_port, status) VALUES (?, ?, ?, ?, ?)", - form.name, serv_id, internal_port, form.port, 'stop') - except sqlite3.IntegrityError: - raise HTTPException(status_code=400, detail="Name or/and ports of the service has been already assigned to another service") - await firewall.reload() - await refresh_frontend() - return {'status': 'ok', "id": serv_id } \ No newline at end of file diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py index fa3fe4f..52e753d 100644 --- a/backend/utils/__init__.py +++ b/backend/utils/__init__.py @@ -1,10 +1,13 @@ import asyncio from ipaddress import ip_address, ip_interface -import os, socket, psutil, sys, nftables +import os +import socket +import psutil +import sys +import nftables from fastapi_socketio import SocketManager from fastapi import Path from typing import Annotated -import json LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1")) @@ -16,7 +19,7 @@ ON_DOCKER = "DOCKER" in sys.argv DEBUG = "DEBUG" in sys.argv FIREGEX_PORT = int(os.getenv("PORT","4444")) JWT_ALGORITHM: str = "HS256" -API_VERSION = "2.2.0" +API_VERSION = "3.0.0" PortType = Annotated[int, Path(gt=0, lt=65536)] @@ -31,7 +34,8 @@ async def socketio_emit(elements:list[str]): def refactor_name(name:str): name = name.strip() - while " " in name: name = name.replace(" "," ") + while " " in name: + name = name.replace(" "," ") return name class SysctlManager: @@ -125,8 +129,10 @@ class NFTableManager(Singleton): def cmd(self, *cmds): code, out, err = self.raw_cmd(*cmds) - if code == 0: return out - else: raise Exception(err) + if code == 0: + return out + else: + raise Exception(err) def init(self): self.reset() @@ -138,8 +144,10 @@ class NFTableManager(Singleton): def list_rules(self, tables = None, chains = None): for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]: - if tables and filter["table"] not in tables: continue - if chains and filter["chain"] not in chains: continue + if tables and filter["table"] not in tables: + continue + if chains and filter["chain"] not in chains: + continue yield filter def raw_list(self): diff --git a/backend/utils/loader.py b/backend/utils/loader.py index 3b72b53..179c5d8 100644 --- a/backend/utils/loader.py +++ b/backend/utils/loader.py @@ -1,5 +1,6 @@ -import os, httpx +import os +import httpx from typing import Callable from fastapi import APIRouter from starlette.responses import StreamingResponse @@ -31,7 +32,8 @@ def frontend_deploy(app): return await frontend_debug_proxy(full_path) except Exception: return {"details":"Frontend not started at "+f"http://127.0.0.1:{os.getenv('F_PORT','5173')}"} - else: return await react_deploy(full_path) + else: + return await react_deploy(full_path) def list_routers(): return [ele[:-3] for ele in list_files(ROUTERS_DIR) if ele != "__init__.py" and " " not in ele and ele.endswith(".py")] @@ -79,9 +81,12 @@ def load_routers(app): if router.shutdown: shutdowns.append(router.shutdown) async def reset(reset_option:ResetRequest): - for func in resets: await run_func(func, reset_option) + for func in resets: + await run_func(func, reset_option) async def startup(): - for func in startups: await run_func(func) + for func in startups: + await run_func(func) async def shutdown(): - for func in shutdowns: await run_func(func) + for func in shutdowns: + await run_func(func) return reset, startup, shutdown diff --git a/backend/utils/sqlite.py b/backend/utils/sqlite.py index 426265c..430dbd6 100644 --- a/backend/utils/sqlite.py +++ b/backend/utils/sqlite.py @@ -1,4 +1,6 @@ -import json, sqlite3, os +import json +import sqlite3 +import os from hashlib import md5 class SQLite(): @@ -15,8 +17,10 @@ class SQLite(): self.conn = sqlite3.connect(self.db_name, check_same_thread = False) except Exception: path_name = os.path.dirname(self.db_name) - if not os.path.exists(path_name): os.makedirs(path_name) - with open(self.db_name, 'x'): pass + if not os.path.exists(path_name): + os.makedirs(path_name) + with open(self.db_name, 'x'): + pass self.conn = sqlite3.connect(self.db_name, check_same_thread = False) def dict_factory(cursor, row): d = {} @@ -36,13 +40,15 @@ class SQLite(): with open(self.db_name, "wb") as f: f.write(self.__backup) self.__backup = None - if were_active: self.connect() + if were_active: + self.connect() def delete_backup(self): self.__backup = None def disconnect(self) -> None: - if self.conn: self.conn.close() + if self.conn: + self.conn.close() self.conn = None def create_schema(self, tables = {}) -> None: @@ -50,9 +56,11 @@ class SQLite(): cur = self.conn.cursor() cur.execute("CREATE TABLE IF NOT EXISTS main.keys_values(key VARCHAR(100) PRIMARY KEY, value VARCHAR(100) NOT NULL);") for t in tables: - if t == "QUERY": continue + if t == "QUERY": + continue cur.execute('CREATE TABLE IF NOT EXISTS main.{}({});'.format(t, ''.join([(c + ' ' + tables[t][c] + ', ') for c in tables[t]])[:-2])) - if "QUERY" in tables: [cur.execute(qry) for qry in tables["QUERY"]] + if "QUERY" in tables: + [cur.execute(qry) for qry in tables["QUERY"]] cur.close() def query(self, query, *values): @@ -82,8 +90,10 @@ class SQLite(): raise e finally: cur.close() - try: self.conn.commit() - except Exception: pass + try: + self.conn.commit() + except Exception: + pass def delete(self): self.disconnect() @@ -92,7 +102,8 @@ class SQLite(): def init(self): self.connect() try: - if self.get('DB_VERSION') != self.DB_VER: raise Exception("DB_VERSION is not correct") + if self.get('DB_VERSION') != self.DB_VER: + raise Exception("DB_VERSION is not correct") except Exception: self.delete() self.connect() diff --git a/docs/FiregexInternals.png b/docs/FiregexInternals.png index 065c78d..6a19f3c 100644 Binary files a/docs/FiregexInternals.png and b/docs/FiregexInternals.png differ diff --git a/frontend/bun.lock b/frontend/bun.lock new file mode 100644 index 0000000..01d62fd --- /dev/null +++ b/frontend/bun.lock @@ -0,0 +1,484 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "firegex-frontend", + "dependencies": { + "@hello-pangea/dnd": "^16.6.0", + "@mantine/core": "^7.16.2", + "@mantine/form": "^7.16.2", + "@mantine/hooks": "^7.16.2", + "@mantine/modals": "^7.16.2", + "@mantine/notifications": "^7.16.2", + "@tanstack/react-query": "^4.36.1", + "@types/jest": "^27.5.2", + "@types/node": "^20.17.16", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "buffer": "^6.0.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-icons": "^5.4.0", + "react-router-dom": "^7.1.5", + "socket.io-client": "^4.8.1", + "typescript": "^5.7.3", + "web-vitals": "^2.1.4", + "zustand": "^5.0.3", + }, + "devDependencies": { + "@tanstack/react-query-devtools": "^4.36.1", + "@vitejs/plugin-react": "^4.3.4", + "vite": "^4.5.9", + "vite-plugin-svgr": "^3.3.0", + "vite-tsconfig-paths": "^4.3.2", + }, + }, + }, + "packages": { + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], + + "@babel/compat-data": ["@babel/compat-data@7.26.5", "", {}, "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg=="], + + "@babel/core": ["@babel/core@7.26.7", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.5", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.7", "@babel/parser": "^7.26.7", "@babel/template": "^7.25.9", "@babel/traverse": "^7.26.7", "@babel/types": "^7.26.7", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA=="], + + "@babel/generator": ["@babel/generator@7.26.5", "", { "dependencies": { "@babel/parser": "^7.26.5", "@babel/types": "^7.26.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.26.5", "", { "dependencies": { "@babel/compat-data": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="], + + "@babel/helpers": ["@babel/helpers@7.26.7", "", { "dependencies": { "@babel/template": "^7.25.9", "@babel/types": "^7.26.7" } }, "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A=="], + + "@babel/parser": ["@babel/parser@7.26.7", "", { "dependencies": { "@babel/types": "^7.26.7" }, "bin": "./bin/babel-parser.js" }, "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="], + + "@babel/runtime": ["@babel/runtime@7.26.7", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ=="], + + "@babel/template": ["@babel/template@7.25.9", "", { "dependencies": { "@babel/code-frame": "^7.25.9", "@babel/parser": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg=="], + + "@babel/traverse": ["@babel/traverse@7.26.7", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/template": "^7.25.9", "@babel/types": "^7.26.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA=="], + + "@babel/types": ["@babel/types@7.26.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + + "@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="], + + "@floating-ui/react": ["@floating-ui/react@0.26.28", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + + "@hello-pangea/dnd": ["@hello-pangea/dnd@16.6.0", "", { "dependencies": { "@babel/runtime": "^7.24.1", "css-box-model": "^1.2.1", "memoize-one": "^6.0.0", "raf-schd": "^4.0.3", "react-redux": "^8.1.3", "redux": "^4.2.1", "use-memo-one": "^1.1.3" }, "peerDependencies": { "react": "^16.8.5 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" } }, "sha512-vfZ4GydqbtUPXSLfAvKvXQ6xwRzIjUSjVU0Sx+70VOhc2xx6CdmJXJ8YhH70RpbTUGjxctslQTHul9sIOxCfFQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + + "@mantine/core": ["@mantine/core@7.16.2", "", { "dependencies": { "@floating-ui/react": "^0.26.28", "clsx": "^2.1.1", "react-number-format": "^5.4.3", "react-remove-scroll": "^2.6.2", "react-textarea-autosize": "8.5.6", "type-fest": "^4.27.0" }, "peerDependencies": { "@mantine/hooks": "7.16.2", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-6dwFz+8HrOqFan7GezgpoWyZSCxedh10S8iILGVsc3GXiD4gzo+3VZndZKccktkYZ3GVC9E3cCS3SxbiyKSAVw=="], + + "@mantine/form": ["@mantine/form@7.16.2", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" }, "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-JZkLbZ7xWAZndPrxObkf10gjHj57x8yvI/vobjDhfWN3zFPTSWmSSF6yBE1FpITseOs3oR03hlkqG6EclK6g+g=="], + + "@mantine/hooks": ["@mantine/hooks@7.16.2", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-ZFHQhDi9T+r6VR5NEeE47gigPPIAHVIKDOCWsCsbCqHc3yz5l8kiO2RdfUmsTKV2KD/AiXnAw4b6pjQEP58GOg=="], + + "@mantine/modals": ["@mantine/modals@7.16.2", "", { "peerDependencies": { "@mantine/core": "7.16.2", "@mantine/hooks": "7.16.2", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-REwAV53Fcz021EE3zLyYdkdFlfG+b24y279Y+eA1jCCH9VMLivXL+gacrox4BcpzREsic9nGVInSNv3VJwPlAQ=="], + + "@mantine/notifications": ["@mantine/notifications@7.16.2", "", { "dependencies": { "@mantine/store": "7.16.2", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "7.16.2", "@mantine/hooks": "7.16.2", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-U342XWiiRI1NvOlLsI6PH/pSNe0rxNClJ2w5orvjOMXvaAfDe52mhnzRmtzRxYENp06++3b/G7MjPH+466rF9Q=="], + + "@mantine/store": ["@mantine/store@7.16.2", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-9dEGLosrYSePlAwhfx3CxTLcWu2M98TtuYnelAiHEdNEkyafirvZxNt4paMoFXLKR1XPm5wdjDK7bdTaE0t7Og=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], + + "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + + "@svgr/babel-plugin-add-jsx-attribute": ["@svgr/babel-plugin-add-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g=="], + + "@svgr/babel-plugin-remove-jsx-attribute": ["@svgr/babel-plugin-remove-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA=="], + + "@svgr/babel-plugin-remove-jsx-empty-expression": ["@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA=="], + + "@svgr/babel-plugin-replace-jsx-attribute-value": ["@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ=="], + + "@svgr/babel-plugin-svg-dynamic-title": ["@svgr/babel-plugin-svg-dynamic-title@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og=="], + + "@svgr/babel-plugin-svg-em-dimensions": ["@svgr/babel-plugin-svg-em-dimensions@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g=="], + + "@svgr/babel-plugin-transform-react-native-svg": ["@svgr/babel-plugin-transform-react-native-svg@8.1.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q=="], + + "@svgr/babel-plugin-transform-svg-component": ["@svgr/babel-plugin-transform-svg-component@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw=="], + + "@svgr/babel-preset": ["@svgr/babel-preset@8.1.0", "", { "dependencies": { "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", "@svgr/babel-plugin-transform-svg-component": "8.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug=="], + + "@svgr/core": ["@svgr/core@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "camelcase": "^6.2.0", "cosmiconfig": "^8.1.3", "snake-case": "^3.0.4" } }, "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA=="], + + "@svgr/hast-util-to-babel-ast": ["@svgr/hast-util-to-babel-ast@8.0.0", "", { "dependencies": { "@babel/types": "^7.21.3", "entities": "^4.4.0" } }, "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q=="], + + "@svgr/plugin-jsx": ["@svgr/plugin-jsx@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "@svgr/hast-util-to-babel-ast": "8.0.0", "svg-parser": "^2.0.4" }, "peerDependencies": { "@svgr/core": "*" } }, "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA=="], + + "@tanstack/match-sorter-utils": ["@tanstack/match-sorter-utils@8.19.4", "", { "dependencies": { "remove-accents": "0.5.0" } }, "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg=="], + + "@tanstack/query-core": ["@tanstack/query-core@4.36.1", "", {}, "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA=="], + + "@tanstack/react-query": ["@tanstack/react-query@4.36.1", "", { "dependencies": { "@tanstack/query-core": "4.36.1", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-native": "*" }, "optionalPeers": ["react-dom", "react-native"] }, "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw=="], + + "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@4.36.1", "", { "dependencies": { "@tanstack/match-sorter-utils": "^8.7.0", "superjson": "^1.10.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "@tanstack/react-query": "^4.36.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-WYku83CKP3OevnYSG8Y/QO9g0rT75v1om5IvcWUwiUZJ4LanYGLVCZ8TdFG5jfsq4Ej/lu2wwDAULEUnRIMBSw=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.6.8", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], + + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + + "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], + + "@types/hoist-non-react-statics": ["@types/hoist-non-react-statics@3.3.6", "", { "dependencies": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" } }, "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw=="], + + "@types/jest": ["@types/jest@27.5.2", "", { "dependencies": { "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" } }, "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA=="], + + "@types/node": ["@types/node@20.17.16", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw=="], + + "@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="], + + "@types/react": ["@types/react@18.3.18", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ=="], + + "@types/react-dom": ["@types/react-dom@18.3.5", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q=="], + + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.3", "", {}, "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.3.4", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001696", "", {}, "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "copy-anything": ["copy-anything@3.0.5", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="], + + "cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + + "css-box-model": ["css-box-model@1.2.1", "", { "dependencies": { "tiny-invariant": "^1.0.6" } }, "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "diff-sequences": ["diff-sequences@27.5.1", "", {}, "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ=="], + + "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], + + "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.90", "", {}, "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug=="], + + "engine.io-client": ["engine.io-client@6.6.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w=="], + + "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], + + "esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + + "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], + + "jest-diff": ["jest-diff@27.5.1", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^27.5.1", "jest-get-type": "^27.5.1", "pretty-format": "^27.5.1" } }, "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw=="], + + "jest-get-type": ["jest-get-type@27.5.1", "", {}, "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw=="], + + "jest-matcher-utils": ["jest-matcher-utils@27.5.1", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^27.5.1", "jest-get-type": "^27.5.1", "pretty-format": "^27.5.1" } }, "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], + + "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + + "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="], + + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "raf-schd": ["raf-schd@4.0.3", "", {}, "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="], + + "react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], + + "react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], + + "react-icons": ["react-icons@5.4.0", "", { "peerDependencies": { "react": "*" } }, "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ=="], + + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "react-number-format": ["react-number-format@5.4.3", "", { "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VCY5hFg/soBighAoGcdE+GagkJq0230qN6jcS5sp8wQX1qy1fYN/RX7/BXkrs0oyzzwqR8/+eSUrqXbGeywdUQ=="], + + "react-redux": ["react-redux@8.1.3", "", { "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", "@types/use-sync-external-store": "^0.0.3", "hoist-non-react-statics": "^3.3.2", "react-is": "^18.0.0", "use-sync-external-store": "^1.0.0" }, "peerDependencies": { "@types/react": "^16.8 || ^17.0 || ^18.0", "@types/react-dom": "^16.8 || ^17.0 || ^18.0", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0", "react-native": ">=0.59", "redux": "^4 || ^5.0.0-beta.0" }, "optionalPeers": ["@types/react", "@types/react-dom", "react-dom", "react-native", "redux"] }, "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw=="], + + "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], + + "react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-router": ["react-router@7.1.5", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA=="], + + "react-router-dom": ["react-router-dom@7.1.5", "", { "dependencies": { "react-router": "7.1.5" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-/4f9+up0Qv92D3bB8iN5P1s3oHAepSGa9h5k6tpTFlixTTskJZwKGhJ6vRJ277tLD1zuaZTt95hyGWV1Z37csQ=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "react-textarea-autosize": ["react-textarea-autosize@8.5.6", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aT3ioKXMa8f6zHYGebhbdMD2L00tKeRX1zuVuDx9YQK/JLLRSaSxq3ugECEmUB9z2kvk6bFSIoRHLkkUv0RJiw=="], + + "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], + + "redux": ["redux@4.2.1", "", { "dependencies": { "@babel/runtime": "^7.9.2" } }, "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w=="], + + "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], + + "remove-accents": ["remove-accents@0.5.0", "", {}, "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "rollup": ["rollup@3.29.5", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w=="], + + "scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], + + "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], + + "socket.io-client": ["socket.io-client@4.8.1", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ=="], + + "socket.io-parser": ["socket.io-parser@4.2.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" } }, "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "superjson": ["superjson@1.13.3", "", { "dependencies": { "copy-anything": "^3.0.2" } }, "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "svg-parser": ["svg-parser@2.0.4", "", {}, "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ=="], + + "tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tsconfck": ["tsconfck@3.1.4", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "turbo-stream": ["turbo-stream@2.4.0", "", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="], + + "type-fest": ["type-fest@4.33.0", "", {}, "sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-composed-ref": ["use-composed-ref@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w=="], + + "use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w=="], + + "use-latest": ["use-latest@1.3.0", "", { "dependencies": { "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ=="], + + "use-memo-one": ["use-memo-one@1.1.3", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="], + + "vite": ["vite@4.5.9", "", { "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", "rollup": "^3.27.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@types/node": ">= 14", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw=="], + + "vite-plugin-svgr": ["vite-plugin-svgr@3.3.0", "", { "dependencies": { "@rollup/pluginutils": "^5.0.4", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0" }, "peerDependencies": { "vite": "^2.6.0 || 3 || 4" } }, "sha512-vWZMCcGNdPqgziYFKQ3Y95XP0d0YGp28+MM3Dp9cTa/px5CKcHHrIoPl2Jw81rgVm6/ZUNONzjXbZQZ7Kw66og=="], + + "vite-tsconfig-paths": ["vite-tsconfig-paths@4.3.2", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA=="], + + "web-vitals": ["web-vitals@2.1.4", "", {}, "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg=="], + + "ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], + + "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="], + + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + + "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "socket.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + + "socket.io-parser/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + } +} diff --git a/frontend/bun.lockb b/frontend/bun.lockb deleted file mode 100755 index 0bc4166..0000000 Binary files a/frontend/bun.lockb and /dev/null differ diff --git a/frontend/package.json b/frontend/package.json index e8b633e..8a0cba7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,25 +5,25 @@ "private": true, "dependencies": { "@hello-pangea/dnd": "^16.6.0", - "@mantine/core": "^7.13.2", - "@mantine/form": "^7.13.2", - "@mantine/hooks": "^7.13.2", - "@mantine/modals": "^7.13.2", - "@mantine/notifications": "^7.13.2", + "@mantine/core": "^7.16.2", + "@mantine/form": "^7.16.2", + "@mantine/hooks": "^7.16.2", + "@mantine/modals": "^7.16.2", + "@mantine/notifications": "^7.16.2", "@tanstack/react-query": "^4.36.1", "@types/jest": "^27.5.2", - "@types/node": "^20.16.11", - "@types/react": "^18.3.11", - "@types/react-dom": "^18.3.1", + "@types/node": "^20.17.16", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", "buffer": "^6.0.3", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-icons": "^5.3.0", - "react-router-dom": "^6.27.0", - "socket.io-client": "^4.8.0", - "typescript": "^4.9.5", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-icons": "^5.4.0", + "react-router-dom": "^7.1.5", + "socket.io-client": "^4.8.1", + "typescript": "^5.7.3", "web-vitals": "^2.1.4", - "zustand": "^5.0.0-rc.2" + "zustand": "^5.0.3" }, "scripts": { "dev": "vite", @@ -50,8 +50,8 @@ }, "devDependencies": { "@tanstack/react-query-devtools": "^4.36.1", - "@vitejs/plugin-react": "^4.3.2", - "vite": "^4.5.5", + "@vitejs/plugin-react": "^4.3.4", + "vite": "^4.5.9", "vite-plugin-svgr": "^3.3.0", "vite-tsconfig-paths": "^4.3.2" } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8a027a7..fcfd39b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,9 +8,7 @@ import { PasswordSend, ServerStatusResponse } from './js/models'; import { DEV_IP_BACKEND, errorNotify, getstatus, HomeRedirector, IS_DEV, login, setpassword } from './js/utils'; import NFRegex from './pages/NFRegex'; import io from 'socket.io-client'; -import RegexProxy from './pages/RegexProxy'; import ServiceDetailsNFRegex from './pages/NFRegex/ServiceDetails'; -import ServiceDetailsProxyRegex from './pages/RegexProxy/ServiceDetails'; import PortHijack from './pages/PortHijack'; import { Firewall } from './pages/Firewall'; import { useQueryClient } from '@tanstack/react-query'; @@ -150,9 +148,6 @@ function App() { } > } /> - } > - } /> - } /> } /> } /> diff --git a/frontend/src/components/AddNewRegex.tsx b/frontend/src/components/AddNewRegex.tsx index 623120a..86d08c7 100644 --- a/frontend/src/components/AddNewRegex.tsx +++ b/frontend/src/components/AddNewRegex.tsx @@ -1,15 +1,12 @@ -import { Button, Group, Space, TextInput, Notification, Switch, NativeSelect, Modal, Alert } from '@mantine/core'; +import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useState } from 'react'; import { RegexAddForm } from '../js/models'; import { b64decode, b64encode, getapiobject, okNotify } from '../js/utils'; import { ImCross } from "react-icons/im" -import FilterTypeSelector from './FilterTypeSelector'; -import { AiFillWarning } from 'react-icons/ai'; type RegexAddInfo = { regex:string, - type:string, mode:string, is_case_insensitive:boolean, deactive:boolean @@ -20,15 +17,13 @@ function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=> const form = useForm({ initialValues: { regex:"", - type:"blacklist", - mode:"C -> S", + mode:"C", is_case_insensitive:false, deactive:false }, validate:{ regex: (value) => value !== "" ? null : "Regex is required", - type: (value) => ["blacklist","whitelist"].includes(value) ? null : "Invalid type", - mode: (value) => ['C -> S', 'S -> C', 'C <-> S'].includes(value) ? null : "Invalid mode", + mode: (value) => ['C', 'S', 'B'].includes(value) ? null : "Invalid mode", } }) @@ -43,13 +38,11 @@ function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=> const submitRequest = (values:RegexAddInfo) => { setSubmitLoading(true) - const filter_mode = ({'C -> S':'C', 'S -> C':'S', 'C <-> S':'B'}[values.mode]) const request:RegexAddForm = { - is_blacklist:values.type !== "whitelist", is_case_sensitive: !values.is_case_insensitive, service_id: service, - mode: filter_mode?filter_mode:"B", + mode: values.mode?values.mode:"B", regex: b64encode(values.regex), active: !values.deactive } @@ -58,7 +51,7 @@ function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=> if (!res){ setSubmitLoading(false) close(); - okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} ${request.is_blacklist?"blacklist":"whitelist"} regex to ${request.service_id} service`) + okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} regex to ${request.service_id} service`) }else if (res.toLowerCase() === "invalid regex"){ setSubmitLoading(false) form.setFieldError("regex", "Invalid Regex") @@ -92,22 +85,16 @@ function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=> {...form.getInputProps('deactive', { type: 'checkbox' })} /> - S', 'S -> C', 'C <-> S']} +