From cb52fa73a6a6d7e749dee4b6c96fa1b51e8ab568 Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Sat, 9 Aug 2025 17:57:12 +0530 Subject: [PATCH 1/6] LFI vuln (v1) --- deploy/docker/docker-compose.yml | 1 + deploy/helm/templates/workshop/config.yaml | 1 + deploy/helm/values.yaml | 1 + .../serviceReport/serviceReport.tsx | 16 +- .../src/components/serviceReport/styles.css | 33 +++ services/web/src/constants/APIConstant.ts | 1 + .../serviceReport/serviceReport.tsx | 1 + services/web/src/sagas/vehicleSaga.ts | 6 + services/workshop/crapi/mechanic/urls.py | 1 + services/workshop/crapi/mechanic/views.py | 94 ++++++++ services/workshop/crapi_site/settings.py | 4 +- services/workshop/requirements.txt | 1 + services/workshop/utils/service_report.html | 227 ++++++++++++++++++ 13 files changed, 384 insertions(+), 3 deletions(-) create mode 100644 services/workshop/utils/service_report.html diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index 6fc2c725..9c0913cf 100755 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -130,6 +130,7 @@ services: - TLS_ENABLED=${TLS_ENABLED:-false} - TLS_CERTIFICATE=certs/server.crt - TLS_KEY=certs/server.key + - FILES_LIMIT=50 depends_on: postgresdb: condition: service_healthy diff --git a/deploy/helm/templates/workshop/config.yaml b/deploy/helm/templates/workshop/config.yaml index 96ef3e96..3b3f7431 100644 --- a/deploy/helm/templates/workshop/config.yaml +++ b/deploy/helm/templates/workshop/config.yaml @@ -22,3 +22,4 @@ data: SERVER_PORT: {{ .Values.workshop.port | quote }} API_GATEWAY_URL: {{ if .Values.apiGatewayServiceInstall }}"https://{{ .Values.apiGatewayService.service.name }}"{{ else }}{{ .Values.apiGatewayServiceUrl }}{{ end }} TLS_ENABLED: {{ .Values.tlsEnabled | quote }} + FILES_LIMIT: {{ .Values.workshop.config.filesLimit }} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index 3146869e..17709daa 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -126,6 +126,7 @@ workshop: postgresDbDriver: postgres mongoDbDriver: mongodb secretKey: crapi + filesLimit: 50 deploymentLabels: app: crapi-workshop podLabels: diff --git a/services/web/src/components/serviceReport/serviceReport.tsx b/services/web/src/components/serviceReport/serviceReport.tsx index b4d01da5..a965a09c 100644 --- a/services/web/src/components/serviceReport/serviceReport.tsx +++ b/services/web/src/components/serviceReport/serviceReport.tsx @@ -16,8 +16,6 @@ import React from "react"; import { Card, - Row, - Col, Descriptions, Spin, Layout, @@ -33,6 +31,7 @@ import { ToolOutlined, CommentOutlined, CalendarOutlined, + DownloadOutlined, } from "@ant-design/icons"; import "./styles.css"; @@ -65,6 +64,7 @@ interface Service { comment: string; created_on: string; }[]; + downloadUrl?: string; } interface ServiceReportProps { @@ -126,6 +126,18 @@ const ServiceReport: React.FC = ({ service }) => { } + extra={[ + + + Download Report + , + ]} /> diff --git a/services/web/src/components/serviceReport/styles.css b/services/web/src/components/serviceReport/styles.css index bac41f50..aad4ac6f 100644 --- a/services/web/src/components/serviceReport/styles.css +++ b/services/web/src/components/serviceReport/styles.css @@ -219,6 +219,34 @@ gap: var(--spacing-sm); } +/* Download Report button */ +.download-report-button { + display: inline-flex; + align-items: center; + gap: var(--spacing-xs); + background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%); + color: white; + text-decoration: none; + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--border-radius-lg); + font-size: 14px; + font-weight: 600; + transition: all 0.3s ease; + border: none; + cursor: pointer; + box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); + width: 100%; + justify-content: center; +} + +.download-report-button:hover, .download-report-button:focus { + background: linear-gradient(135deg, #7c3aed 0%, #9333ea 100%); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4); + color: white; + text-decoration: none; +} + /* Loading State */ .loading-container { display: flex; @@ -251,6 +279,11 @@ padding: var(--spacing-md); } + .download-report-button { + padding: var(--spacing-md); + font-size: 13px; + } + .ant-descriptions-item-label { font-size: 12px; } diff --git a/services/web/src/constants/APIConstant.ts b/services/web/src/constants/APIConstant.ts index 5de298fe..63d3fc2b 100644 --- a/services/web/src/constants/APIConstant.ts +++ b/services/web/src/constants/APIConstant.ts @@ -65,6 +65,7 @@ export const requestURLS: RequestURLSType = { UPDATE_SERVICE_REQUEST_STATUS: "api/mechanic/service_request/", GET_VEHICLE_SERVICES: "api/merchant/service_requests/", GET_SERVICE_REPORT: "api/mechanic/mechanic_report", + DOWNLOAD_SERVICE_REPORT: "api/mechanic/download_report", BUY_PRODUCT: "api/shop/orders", GET_ORDERS: "api/shop/orders/all", GET_ORDER_BY_ID: "api/shop/orders/", diff --git a/services/web/src/containers/serviceReport/serviceReport.tsx b/services/web/src/containers/serviceReport/serviceReport.tsx index c493fba9..ba5280d2 100644 --- a/services/web/src/containers/serviceReport/serviceReport.tsx +++ b/services/web/src/containers/serviceReport/serviceReport.tsx @@ -52,6 +52,7 @@ interface Service { comment: string; created_on: string; }[]; + downloadUrl?: string; } const mapStateToProps = (state: RootState) => ({ diff --git a/services/web/src/sagas/vehicleSaga.ts b/services/web/src/sagas/vehicleSaga.ts index 55c27730..3107cdb8 100644 --- a/services/web/src/sagas/vehicleSaga.ts +++ b/services/web/src/sagas/vehicleSaga.ts @@ -377,6 +377,12 @@ export function* getServiceReport(action: MyAction): Generator { throw responseJSON; } + const filename = `report_${reportId}.pdf`; + responseJSON.downloadUrl = + APIService.WORKSHOP_SERVICE + + requestURLS.DOWNLOAD_SERVICE_REPORT + + "?filename=" + + filename; yield put({ type: actionTypes.FETCHED_DATA, payload: responseJSON }); callback(responseTypes.SUCCESS, responseJSON); } catch (e) { diff --git a/services/workshop/crapi/mechanic/urls.py b/services/workshop/crapi/mechanic/urls.py index be7a0d6f..8050d779 100644 --- a/services/workshop/crapi/mechanic/urls.py +++ b/services/workshop/crapi/mechanic/urls.py @@ -41,5 +41,6 @@ r"service_request$", mechanic_views.MechanicServiceRequestsView.as_view(), ), + re_path(r"download_report$", mechanic_views.DownloadReportView.as_view()), re_path(r"$", mechanic_views.MechanicView.as_view()), ] diff --git a/services/workshop/crapi/mechanic/views.py b/services/workshop/crapi/mechanic/views.py index efd1405d..c80686aa 100644 --- a/services/workshop/crapi/mechanic/views.py +++ b/services/workshop/crapi/mechanic/views.py @@ -15,7 +15,12 @@ """ contains all the views related to Mechanic """ +import os import bcrypt +import logging +from urllib.parse import unquote +from django.template.loader import get_template +from xhtml2pdf import pisa from django.utils import timezone from django.views.decorators.csrf import csrf_exempt from django.urls import reverse @@ -23,6 +28,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from django.db import models +from django.http import FileResponse from crapi_site import settings from utils.jwt import jwt_auth_required from utils import messages @@ -40,6 +46,7 @@ ) from rest_framework.pagination import LimitOffsetPagination +logger = logging.getLogger() class SignUpView(APIView): """ @@ -235,6 +242,7 @@ def get(self, request, user=None): ) serializer = MechanicServiceRequestSerializer(service_request) response_data = dict(serializer.data) + service_report_pdf(response_data, report_id) return Response(response_data, status=status.HTTP_200_OK) @@ -366,3 +374,89 @@ def get(self, request, user=None, service_request_id=None): service_request = ServiceRequest.objects.get(id=service_request_id) serializer = MechanicServiceRequestSerializer(service_request) return Response(serializer.data, status=status.HTTP_200_OK) + + +class DownloadReportView(APIView): + """ + A view to download a service report. + This view contains an intentional LFI vulnerability. + """ + def get(self, request, format=None): + filename_from_user = request.query_params.get('filename') + if not filename_from_user: + return Response( + {"message": "Parameter 'filename' is required."}, + status=status.HTTP_400_BAD_REQUEST + ) + #Checks for directory traversal in plain as well as single URL-encoded form + #Since Django automatically decodes URL-encoded parameters once + if '..' in filename_from_user or '/' in filename_from_user: + return Response( + {"message": "Forbidden input."}, + status=status.HTTP_400_BAD_REQUEST + ) + filename_from_user = unquote(filename_from_user) + filename_from_user = filename_from_user.replace("../", "") + + #VULNERABLE: Double URL-encoded nested path can be used for exploit + full_path = os.path.abspath(os.path.join(settings.BASE_DIR, "reports", filename_from_user)) + print(f"Attempting to serve file from: {full_path}") + logger.info(f"Attempting to serve file from: {full_path}") + + if os.path.exists(full_path) and os.path.isfile(full_path): + return FileResponse(open(full_path, 'rb')) + elif not os.path.exists(full_path): + return Response( + {"message": f"File not found at '{full_path}'."}, + status=status.HTTP_404_NOT_FOUND + ) + else: + return Response( + {"message": f"'{full_path}' is not a file."}, + status=status.HTTP_403_FORBIDDEN + ) + + +def service_report_pdf(response_data, report_id): + """ + Generates service report's PDF file from a template and saves it to the disk. + """ + reports_dir = os.path.join(settings.BASE_DIR, 'reports') + os.makedirs(reports_dir, exist_ok=True) + report_filepath = os.path.join(reports_dir, f"report_{report_id}.pdf") + + template = get_template('service_report.html') + html_string = template.render({'service': response_data}) + with open(report_filepath, "w+b") as pdf_file: + pisa.CreatePDF(src=html_string, dest=pdf_file) + + manage_reports_directory() + + +def manage_reports_directory(): + """ + Checks reports directory and deletes the oldest one if the + count exceeds the maximum limit. + """ + try: + reports_dir = os.path.join(settings.BASE_DIR, 'reports') + report_files = os.listdir(reports_dir) + + if len(report_files) >= settings.FILES_LIMIT: + oldest_file = None + oldest_time = float('inf') + for filename in report_files: + filepath = os.path.join(reports_dir, filename) + try: + current_mtime = os.path.getmtime(filepath) + if current_mtime < oldest_time: + oldest_time = current_mtime + oldest_file = filepath + except FileNotFoundError: + continue + + if oldest_file: + os.remove(oldest_file) + + except (OSError, FileNotFoundError) as e: + print(f"Error during report directory management: {e}") \ No newline at end of file diff --git a/services/workshop/crapi_site/settings.py b/services/workshop/crapi_site/settings.py index 2f1e9359..303b8431 100644 --- a/services/workshop/crapi_site/settings.py +++ b/services/workshop/crapi_site/settings.py @@ -41,6 +41,8 @@ def get_env_value(env_variable): raise ImproperlyConfigured(error_msg) +FILES_LIMIT = int(os.environ.get("FILES_LIMIT", 50)) + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -108,7 +110,7 @@ def get_env_value(env_variable): TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], + "DIRS": [os.path.join(BASE_DIR, 'utils')], "APP_DIRS": True, "OPTIONS": { "context_processors": [ diff --git a/services/workshop/requirements.txt b/services/workshop/requirements.txt index 6c71f547..c10137dd 100644 --- a/services/workshop/requirements.txt +++ b/services/workshop/requirements.txt @@ -22,3 +22,4 @@ gunicorn==21.2.0 coverage==7.4.1 unittest-xml-reporting==3.2.0 black==24.4.2 +xhtml2pdf==0.2.17 \ No newline at end of file diff --git a/services/workshop/utils/service_report.html b/services/workshop/utils/service_report.html new file mode 100644 index 00000000..977d5686 --- /dev/null +++ b/services/workshop/utils/service_report.html @@ -0,0 +1,227 @@ + + + + + Service Report - {{ service.id }} + + + +
+
+
+

Service Report

+

+ Vehicle VIN: {{ service.vehicle.vin }} +

+
+
+ +
+
+
+
{{ service.status|upper }}
+
Report Details
+
+ + + + + + + + + + + + + + + + + + + +
Report ID{{ service.id }}
Service Status{{ service.status }}
Created On{{ service.created_on }}
Problem Details{{ service.problem_details }}
+
+
+ +
+
Service Comments
+
+
+ {% for comment in service.comments %} +
+
{{ comment.created_on }}
+
{{ comment.comment }}
+
+ {% empty %} +

No comments available for this service.

+ {% endfor %} +
+
+
+
+ +
+
+
Assigned Mechanic
+
+ + + + + + + + + + + +
Mechanic Code{{ service.mechanic.mechanic_code }}
Mechanic Email{{ service.mechanic.user.email }}
+
+
+ +
+
Vehicle Information
+
+ + + + + + + +
Vehicle VIN{{ service.vehicle.vin }}
+
+
+ +
+
Owner Information
+
+ + + + + + + + + + + +
Owner Email{{ service.vehicle.owner.email }}
Owner Phone{{ service.vehicle.owner.number }}
+
+
+
+
+
+ + \ No newline at end of file From b8f8658fb4f4f9642e184e946b4752f49ba9a474 Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Sat, 9 Aug 2025 18:26:02 +0530 Subject: [PATCH 2/6] prettier fix --- .../web/src/components/serviceReport/serviceReport.tsx | 9 +-------- services/web/src/sagas/vehicleSaga.ts | 8 ++++---- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/services/web/src/components/serviceReport/serviceReport.tsx b/services/web/src/components/serviceReport/serviceReport.tsx index a965a09c..475e68e0 100644 --- a/services/web/src/components/serviceReport/serviceReport.tsx +++ b/services/web/src/components/serviceReport/serviceReport.tsx @@ -14,14 +14,7 @@ */ import React from "react"; -import { - Card, - Descriptions, - Spin, - Layout, - Timeline, - Typography, -} from "antd"; +import { Card, Descriptions, Spin, Layout, Timeline, Typography } from "antd"; import { PageHeader } from "@ant-design/pro-components"; import { Content } from "antd/es/layout/layout"; import { diff --git a/services/web/src/sagas/vehicleSaga.ts b/services/web/src/sagas/vehicleSaga.ts index 3107cdb8..9fcf66e5 100644 --- a/services/web/src/sagas/vehicleSaga.ts +++ b/services/web/src/sagas/vehicleSaga.ts @@ -378,10 +378,10 @@ export function* getServiceReport(action: MyAction): Generator { } const filename = `report_${reportId}.pdf`; - responseJSON.downloadUrl = - APIService.WORKSHOP_SERVICE + - requestURLS.DOWNLOAD_SERVICE_REPORT + - "?filename=" + + responseJSON.downloadUrl = + APIService.WORKSHOP_SERVICE + + requestURLS.DOWNLOAD_SERVICE_REPORT + + "?filename=" + filename; yield put({ type: actionTypes.FETCHED_DATA, payload: responseJSON }); callback(responseTypes.SUCCESS, responseJSON); From ef74d61dd8f820b9e33ff984499078180cda3002 Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Sat, 23 Aug 2025 00:24:41 +0530 Subject: [PATCH 3/6] resolved comments: relaxed checks on filename to make it more realistic --- services/workshop/crapi/mechanic/views.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/services/workshop/crapi/mechanic/views.py b/services/workshop/crapi/mechanic/views.py index c80686aa..a0d06620 100644 --- a/services/workshop/crapi/mechanic/views.py +++ b/services/workshop/crapi/mechanic/views.py @@ -390,15 +390,13 @@ def get(self, request, format=None): ) #Checks for directory traversal in plain as well as single URL-encoded form #Since Django automatically decodes URL-encoded parameters once - if '..' in filename_from_user or '/' in filename_from_user: + if '../' in filename_from_user: return Response( {"message": "Forbidden input."}, status=status.HTTP_400_BAD_REQUEST ) filename_from_user = unquote(filename_from_user) - filename_from_user = filename_from_user.replace("../", "") - - #VULNERABLE: Double URL-encoded nested path can be used for exploit + #VULNERABLE: Double URL-encoded path can be used for exploit full_path = os.path.abspath(os.path.join(settings.BASE_DIR, "reports", filename_from_user)) print(f"Attempting to serve file from: {full_path}") logger.info(f"Attempting to serve file from: {full_path}") From e7c2140b7b34741b58884b32765973a9b3da657f Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Sat, 23 Aug 2025 19:02:21 +0530 Subject: [PATCH 4/6] resolved comments: standard whitelist check --- services/web/src/sagas/vehicleSaga.ts | 2 +- services/workshop/crapi/mechanic/views.py | 24 +++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/services/web/src/sagas/vehicleSaga.ts b/services/web/src/sagas/vehicleSaga.ts index 9fcf66e5..c423d571 100644 --- a/services/web/src/sagas/vehicleSaga.ts +++ b/services/web/src/sagas/vehicleSaga.ts @@ -377,7 +377,7 @@ export function* getServiceReport(action: MyAction): Generator { throw responseJSON; } - const filename = `report_${reportId}.pdf`; + const filename = `report_${reportId}`; responseJSON.downloadUrl = APIService.WORKSHOP_SERVICE + requestURLS.DOWNLOAD_SERVICE_REPORT + diff --git a/services/workshop/crapi/mechanic/views.py b/services/workshop/crapi/mechanic/views.py index a0d06620..217c5106 100644 --- a/services/workshop/crapi/mechanic/views.py +++ b/services/workshop/crapi/mechanic/views.py @@ -17,7 +17,7 @@ """ import os import bcrypt -import logging +import re from urllib.parse import unquote from django.template.loader import get_template from xhtml2pdf import pisa @@ -46,8 +46,6 @@ ) from rest_framework.pagination import LimitOffsetPagination -logger = logging.getLogger() - class SignUpView(APIView): """ Used to add a new mechanic @@ -379,7 +377,6 @@ def get(self, request, user=None, service_request_id=None): class DownloadReportView(APIView): """ A view to download a service report. - This view contains an intentional LFI vulnerability. """ def get(self, request, format=None): filename_from_user = request.query_params.get('filename') @@ -388,19 +385,15 @@ def get(self, request, format=None): {"message": "Parameter 'filename' is required."}, status=status.HTTP_400_BAD_REQUEST ) - #Checks for directory traversal in plain as well as single URL-encoded form - #Since Django automatically decodes URL-encoded parameters once - if '../' in filename_from_user: + #Checks if input before decoding contains only allowed characters + if not validate_filename(filename_from_user): return Response( {"message": "Forbidden input."}, status=status.HTTP_400_BAD_REQUEST ) + filename_from_user = unquote(filename_from_user) - #VULNERABLE: Double URL-encoded path can be used for exploit full_path = os.path.abspath(os.path.join(settings.BASE_DIR, "reports", filename_from_user)) - print(f"Attempting to serve file from: {full_path}") - logger.info(f"Attempting to serve file from: {full_path}") - if os.path.exists(full_path) and os.path.isfile(full_path): return FileResponse(open(full_path, 'rb')) elif not os.path.exists(full_path): @@ -414,6 +407,13 @@ def get(self, request, format=None): status=status.HTTP_403_FORBIDDEN ) +def validate_filename(input: str) -> bool: + """ + Allowed: alphanumerics, _, :, %HH + """ + url_encoded_pattern = re.compile(r'^(?:[A-Za-z0-9:_]|%[0-9A-Fa-f]{2})*$') + return bool(url_encoded_pattern.fullmatch(input)) + def service_report_pdf(response_data, report_id): """ @@ -421,7 +421,7 @@ def service_report_pdf(response_data, report_id): """ reports_dir = os.path.join(settings.BASE_DIR, 'reports') os.makedirs(reports_dir, exist_ok=True) - report_filepath = os.path.join(reports_dir, f"report_{report_id}.pdf") + report_filepath = os.path.join(reports_dir, f"report_{report_id}") template = get_template('service_report.html') html_string = template.render({'service': response_data}) From 8a16b7d5247989dde7711439f7f53437b668c6c9 Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Sat, 23 Aug 2025 22:26:38 +0530 Subject: [PATCH 5/6] error message updated --- services/workshop/crapi/mechanic/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/workshop/crapi/mechanic/views.py b/services/workshop/crapi/mechanic/views.py index 217c5106..684cdd63 100644 --- a/services/workshop/crapi/mechanic/views.py +++ b/services/workshop/crapi/mechanic/views.py @@ -388,7 +388,7 @@ def get(self, request, format=None): #Checks if input before decoding contains only allowed characters if not validate_filename(filename_from_user): return Response( - {"message": "Forbidden input."}, + {"message": "Invalid input."}, status=status.HTTP_400_BAD_REQUEST ) From 0e4a624987d02dac5b21fb0c852121be1ffd4e66 Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Wed, 3 Sep 2025 18:49:56 +0530 Subject: [PATCH 6/6] Updated files buffer limit to 1000 --- deploy/docker/docker-compose.yml | 2 +- deploy/helm/values.yaml | 2 +- services/workshop/crapi_site/settings.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index 9c0913cf..19ca9a20 100755 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -130,7 +130,7 @@ services: - TLS_ENABLED=${TLS_ENABLED:-false} - TLS_CERTIFICATE=certs/server.crt - TLS_KEY=certs/server.key - - FILES_LIMIT=50 + - FILES_LIMIT=1000 depends_on: postgresdb: condition: service_healthy diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index 17709daa..5b285c62 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -126,7 +126,7 @@ workshop: postgresDbDriver: postgres mongoDbDriver: mongodb secretKey: crapi - filesLimit: 50 + filesLimit: 1000 deploymentLabels: app: crapi-workshop podLabels: diff --git a/services/workshop/crapi_site/settings.py b/services/workshop/crapi_site/settings.py index 303b8431..0e88c97b 100644 --- a/services/workshop/crapi_site/settings.py +++ b/services/workshop/crapi_site/settings.py @@ -41,7 +41,7 @@ def get_env_value(env_variable): raise ImproperlyConfigured(error_msg) -FILES_LIMIT = int(os.environ.get("FILES_LIMIT", 50)) +FILES_LIMIT = int(os.environ.get("FILES_LIMIT", 1000)) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))