Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 40 additions & 14 deletions backend/src/api/govtool-proxy/controllers/govtool-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,56 @@ module.exports = {
try {
const baseUrl = process.env.GOVTOOL_API_BASE_URL;

// Uzmite endpoint iz query parametara
if (!baseUrl) {
return ctx.throw(500, 'GOVTOOL_API_BASE_URL is not configured');
}

const { endpoint } = ctx.request.query;

if (!endpoint) {
return ctx.throw(400, 'Endpoint parametar je obavezan');
if (!endpoint || typeof endpoint !== 'string' || endpoint.trim() === '') {
return ctx.throw(400, 'Endpoint parameter is required');
}

// Reject absolute URLs to prevent SSRF
if (/^https?:\/\//i.test(endpoint)) {
return ctx.throw(400, 'Absolute URLs are not allowed');
}

// Spojite base URL i endpoint
const fullUrl = `${baseUrl}/${endpoint}`;
console.log(fullUrl);
// Provera da li je URL validan (opciono)
// Reject protocol-relative URLs
if (/^\/\//.test(endpoint)) {
return ctx.throw(400, 'Protocol-relative URLs are not allowed');
}

// Reject path traversal sequences
if (endpoint.includes('..')) {
return ctx.throw(400, 'Path traversal is not allowed');
}

// Reject null bytes and backslashes
if (endpoint.includes('\0') || endpoint.includes('\\')) {
return ctx.throw(400, 'Invalid characters in endpoint');
}

const fullUrl = `${baseUrl.replace(/\/$/, '')}/${endpoint}`;

try {
new URL(fullUrl); // Provera da li je URL validan
new URL(fullUrl);
} catch (error) {
return ctx.throw(400, 'Nevalidan URL');
return ctx.throw(400, 'Invalid URL');
}

const headers = {};
if (process.env.GOVTOOL_API_TOKEN) {
headers.Authorization = `Bearer ${process.env.GOVTOOL_API_TOKEN}`;
}

// Pozovite eksterni API
const response = await axios.get(fullUrl);
const response = await axios.get(fullUrl, { headers });

// Vratite podatke klijentu
ctx.send(response.data);
} catch (error) {
ctx.throw(500, 'Greška pri preuzimanju podataka sa eksternog API-ja', { error });
if (error.status) throw error;
strapi.log.error('GovTool proxy error:', error);
ctx.throw(500, 'Error fetching data from external API', { error });
}
},
};
};
101 changes: 70 additions & 31 deletions backend/src/api/proxy/controllers/proxy.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,69 @@
"use strict";
const axios = require("axios");

module.exports = {
// POST /proxy
async forward(ctx) {
try {
const {
url,
method = "GET",
data = {},
params = {},
headers = {},
} = ctx.request.body;
const response = await axios({ url, method, data, params, headers });
ctx.send({ status: response.status, data: response.data });
} catch (error) {
strapi.log.error("Proxy error:", error);
ctx.status = error.response?.status || 500;
ctx.body = {
error: error.message,
details: error.response?.data || null,
};
}
},
/**
* Validates that an endpoint path is safe and cannot be used for SSRF or path traversal.
* @param {string} endpoint - The endpoint path to validate
* @returns {{ valid: boolean, error?: string }}
*/
function validateEndpoint(endpoint) {
if (!endpoint || typeof endpoint !== "string" || endpoint.trim() === "") {
return { valid: false, error: "Endpoint is required" };
}

// Reject absolute URLs to prevent SSRF
if (/^https?:\/\//i.test(endpoint)) {
return { valid: false, error: "Absolute URLs are not allowed" };
}

// Reject protocol-relative URLs
if (/^\/\//.test(endpoint)) {
return { valid: false, error: "Protocol-relative URLs are not allowed" };
}

// Reject path traversal sequences
if (endpoint.includes("..")) {
return { valid: false, error: "Path traversal is not allowed" };
}

// Reject null bytes
if (endpoint.includes("\0")) {
return { valid: false, error: "Null bytes are not allowed" };
}

// Reject backslashes (Windows path traversal)
if (endpoint.includes("\\")) {
return { valid: false, error: "Backslashes are not allowed" };
}

return { valid: true };
}

module.exports = {
// GET /proxy/govtool/:endpoint*
async getGovtoolData(ctx) {
try {
const endpoint = ctx.params.endpoint;
if (!endpoint) return ctx.badRequest("Endpoint is required");
const validation = validateEndpoint(endpoint);
if (!validation.valid) {
return ctx.badRequest(validation.error);
}

const baseUrl = process.env.GOVTOOL_API_BASE_URL;
if (!baseUrl) {
return ctx.internalServerError("GOVTOOL_API_BASE_URL is not configured");
}

const fullUrl = `${baseUrl.replace(/\/$/, "")}/${endpoint}`;

const headers = {};
if (process.env.GOVTOOL_API_TOKEN) {
headers.Authorization = `Bearer ${process.env.GOVTOOL_API_TOKEN}`;
}

const response = await axios.get(fullUrl, {
params: ctx.query,
headers: {
// Authorization: `Bearer ${process.env.GOVTOOL_API_TOKEN}`,
},
headers,
});

ctx.send({ status: response.status, data: response.data });
Expand All @@ -52,16 +80,27 @@ module.exports = {
async postGovtoolData(ctx) {
try {
const endpoint = ctx.params.endpoint;
if (!endpoint) return ctx.badRequest("Endpoint is required");
const validation = validateEndpoint(endpoint);
if (!validation.valid) {
return ctx.badRequest(validation.error);
}

const baseUrl = process.env.GOVTOOL_API_BASE_URL;
if (!baseUrl) {
return ctx.internalServerError("GOVTOOL_API_BASE_URL is not configured");
}

const fullUrl = `${baseUrl.replace(/\/$/, "")}/${endpoint}`;

const headers = {
"Content-Type": "application/json",
};
if (process.env.GOVTOOL_API_TOKEN) {
headers.Authorization = `Bearer ${process.env.GOVTOOL_API_TOKEN}`;
}

const response = await axios.post(fullUrl, ctx.request.body, {
headers: {
"Content-Type": "application/json",
// Authorization: `Bearer ${process.env.GOVTOOL_API_TOKEN}`,
},
headers,
});

ctx.send({ status: response.status, data: response.data });
Expand Down
21 changes: 6 additions & 15 deletions backend/src/api/proxy/routes/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,23 @@

module.exports = {
routes: [
{
method: 'POST',
path: '/proxy',
handler: 'proxy.forward',
config: {
roles: ['authenticated', 'public'],
auth: false
},
},
{
method: 'GET',
path: '/proxy/govtool/:endpoint*',
handler: 'proxy.getGovtoolData',
config: {
roles: ['authenticated', 'public'],
auth: false
},
roles: ['authenticated', 'public'],
auth: false
},
},
{
method: 'POST',
path: '/proxy/govtool/:endpoint*',
handler: 'proxy.postGovtoolData',
config: {
roles: ['authenticated', 'public'],
auth: false
},
roles: ['authenticated', 'public'],
auth: false
},
},
],
};
Loading