@@ -10,8 +10,8 @@ const path = require('path');
1010const express = require ( 'express' ) ;
1111const http = require ( 'http' ) ;
1212const https = require ( 'https' ) ;
13- const urlLib = require ( 'url' ) ;
1413const dns = require ( 'dns' ) ;
14+ const net = require ( 'net' ) ;
1515const os = require ( 'os' ) ;
1616const fs = require ( 'fs' ) ;
1717const crypto = require ( 'crypto' ) ;
@@ -146,85 +146,160 @@ if (isDev) {
146146}
147147
148148// ----------------------------- Proxy (CORS + Range) -----------------------------
149- async function isUrlSafe ( targetUrl ) {
149+ const proxyTargets = new Map ( ) ;
150+ const PROXY_TARGET_TTL_MS = 30 * 60 * 1000 ;
151+
152+ function makeProxyError ( message , statusCode = 403 ) {
153+ const err = new Error ( message ) ;
154+ err . statusCode = statusCode ;
155+ return err ;
156+ }
157+
158+ function cleanupProxyTargets ( ) {
159+ const expiresBefore = Date . now ( ) - PROXY_TARGET_TTL_MS ;
160+ for ( const [ id , entry ] of proxyTargets ) {
161+ if ( entry . createdAt < expiresBefore ) proxyTargets . delete ( id ) ;
162+ }
163+ }
164+
165+ function registerProxyTarget ( rawUrl ) {
166+ cleanupProxyTargets ( ) ;
167+ const id = crypto . randomBytes ( 16 ) . toString ( 'hex' ) ;
168+ proxyTargets . set ( id , { url : rawUrl , createdAt : Date . now ( ) } ) ;
169+ return id ;
170+ }
171+
172+ function isBlockedIp ( address ) {
173+ const family = net . isIP ( address ) ;
174+ if ( family === 4 ) {
175+ const parts = address . split ( '.' ) . map ( part => Number ( part ) ) ;
176+ if ( parts . length !== 4 || parts . some ( part => ! Number . isInteger ( part ) || part < 0 || part > 255 ) ) return true ;
177+ const [ a , b , c ] = parts ;
178+ return (
179+ a === 0 ||
180+ a === 10 ||
181+ a === 127 ||
182+ a === 169 && b === 254 ||
183+ a === 172 && b >= 16 && b <= 31 ||
184+ a === 192 && b === 168 ||
185+ a === 192 && b === 0 && c === 0 ||
186+ a === 192 && b === 0 && c === 2 ||
187+ a === 198 && ( b === 18 || b === 19 ) ||
188+ a === 198 && b === 51 && c === 100 ||
189+ a === 203 && b === 0 && c === 113 ||
190+ a >= 224
191+ ) ;
192+ }
193+
194+ if ( family === 6 ) {
195+ const lower = address . toLowerCase ( ) ;
196+ const mappedIpv4 = lower . match ( / : : f f f f : ( \d + \. \d + \. \d + \. \d + ) $ / ) ;
197+ if ( mappedIpv4 ) return isBlockedIp ( mappedIpv4 [ 1 ] ) ;
198+ return (
199+ lower === '::' ||
200+ lower === '::1' ||
201+ lower . startsWith ( 'fc' ) ||
202+ lower . startsWith ( 'fd' ) ||
203+ lower . startsWith ( 'fe80:' ) ||
204+ lower . startsWith ( 'ff' )
205+ ) ;
206+ }
207+
208+ return true ;
209+ }
210+
211+ function isBlockedHostname ( hostname ) {
212+ const normalized = String ( hostname || '' ) . replace ( / ^ \[ | \] $ / g, '' ) . toLowerCase ( ) ;
213+ if ( ! normalized ) return true ;
214+ if (
215+ normalized === 'localhost' ||
216+ normalized . endsWith ( '.localhost' ) ||
217+ normalized . endsWith ( '.local' ) ||
218+ normalized . endsWith ( '.lan' )
219+ ) {
220+ return true ;
221+ }
222+ return net . isIP ( normalized ) ? isBlockedIp ( normalized ) : false ;
223+ }
224+
225+ async function resolvePublicAddresses ( hostname ) {
226+ const family = net . isIP ( hostname ) ;
227+ const records = family
228+ ? [ { address : hostname , family } ]
229+ : await dns . promises . lookup ( hostname , { all : true , verbatim : true } ) ;
230+
231+ if ( ! records . length ) throw makeProxyError ( 'Proxy target could not be resolved.' , 403 ) ;
232+ if ( records . some ( record => isBlockedIp ( record . address ) ) ) {
233+ throw makeProxyError ( 'Proxy target resolves to a private or reserved network.' , 403 ) ;
234+ }
235+ return records ;
236+ }
237+
238+ async function buildSafeProxyRequest ( rawUrl , rangeHeader ) {
150239 try {
151- const parsed = urlLib . parse ( targetUrl ) ;
152- if ( ! [ 'http:' , 'https:' ] . includes ( parsed . protocol ) ) return false ;
153-
154- const hostname = parsed . hostname ;
155- if ( ! hostname ) return false ;
156-
157- // Check known loopback/private patterns on hostname (before DNS)
158- if ( hostname . toLowerCase ( ) === 'localhost' ) return false ;
159- if ( hostname === '::1' || hostname === '[::1]' ) return false ;
160- if ( hostname . startsWith ( '127.' ) ) return false ;
161- if ( hostname . startsWith ( '10.' ) ) return false ;
162- if ( hostname . startsWith ( '192.168.' ) ) return false ;
163- if ( hostname . startsWith ( '169.254.' ) ) return false ;
164- // 172.16.x.x - 172.31.x.x
165- const parts = hostname . split ( '.' ) ;
166- if ( parts . length === 4 && parts [ 0 ] === '172' ) {
167- const second = parseInt ( parts [ 1 ] , 10 ) ;
168- if ( second >= 16 && second <= 31 ) return false ;
240+ const parsed = new URL ( rawUrl ) ;
241+ if ( ! [ 'http:' , 'https:' ] . includes ( parsed . protocol ) ) {
242+ throw makeProxyError ( 'Only http and https proxy targets are allowed.' , 403 ) ;
169243 }
170-
171- // Resolve Hostname to IP to catch domains pointing to private IPs
172- return new Promise ( ( resolve ) => {
173- dns . lookup ( hostname , { family : 4 } , ( err , address ) => {
174- if ( err ) {
175- // Determine if we should fail open or closed. Failing closed (reject) is safer.
176- return resolve ( false ) ;
177- }
178- if ( ! address ) return resolve ( false ) ;
179-
180- // Re-check resolved IP
181- if ( address . startsWith ( '127.' ) ) return resolve ( false ) ;
182- if ( address . startsWith ( '10.' ) ) return resolve ( false ) ;
183- if ( address . startsWith ( '192.168.' ) ) return resolve ( false ) ;
184- if ( address . startsWith ( '169.254.' ) ) return resolve ( false ) ;
185-
186- const ipParts = address . split ( '.' ) ;
187- if ( ipParts . length === 4 && ipParts [ 0 ] === '172' ) {
188- const second = parseInt ( ipParts [ 1 ] , 10 ) ;
189- if ( second >= 16 && second <= 31 ) return resolve ( false ) ;
190- }
191-
192- resolve ( true ) ;
193- } ) ;
194- } ) ;
244+ if ( parsed . username || parsed . password ) {
245+ throw makeProxyError ( 'Proxy targets with credentials are not allowed.' , 403 ) ;
246+ }
247+ if ( isBlockedHostname ( parsed . hostname ) ) {
248+ throw makeProxyError ( 'Proxy target host is private or reserved.' , 403 ) ;
249+ }
250+
251+ const records = await resolvePublicAddresses ( parsed . hostname ) ;
252+ const selected = records [ 0 ] ;
253+ const headers = {
254+ 'Host' : parsed . host ,
255+ 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' ,
256+ 'Referer' : 'https://www.youtube.com/' ,
257+ 'Origin' : 'https://www.youtube.com' ,
258+ 'Accept' : '*/*' ,
259+ 'Accept-Language' : 'en-US,en;q=0.9'
260+ } ;
261+ if ( rangeHeader ) headers [ 'Range' ] = String ( rangeHeader ) . slice ( 0 , 128 ) ;
262+
263+ return {
264+ client : parsed . protocol === 'https:' ? https : http ,
265+ displayUrl : parsed . href ,
266+ options : {
267+ protocol : parsed . protocol ,
268+ hostname : selected . address ,
269+ port : parsed . port || ( parsed . protocol === 'https:' ? 443 : 80 ) ,
270+ path : `${ parsed . pathname } ${ parsed . search } ` ,
271+ method : 'GET' ,
272+ headers,
273+ servername : parsed . hostname ,
274+ family : selected . family ,
275+ lookup : ( hostname , options , callback ) => callback ( null , selected . address , selected . family ) ,
276+ timeout : 30000
277+ }
278+ } ;
195279 } catch ( e ) {
196- return false ;
280+ if ( e . statusCode ) throw e ;
281+ throw makeProxyError ( 'Invalid proxy target URL.' , 400 ) ;
197282 }
198283}
199284
200285async function startProxy ( ) {
201286 const exapp = express ( ) ;
202287
203- exapp . get ( '/proxy' , async ( req , res ) => {
204- const target = req . query . url ;
205- if ( ! target ) return res . status ( 400 ) . send ( 'Missing url ' ) ;
288+ exapp . get ( '/proxy/:id ' , async ( req , res ) => {
289+ const entry = proxyTargets . get ( req . params . id ) ;
290+ if ( ! entry ) return res . status ( 404 ) . send ( 'Proxy target expired or not found. ' ) ;
206291
207- // SSRF Protection
208- const safe = await isUrlSafe ( target ) ;
209- if ( ! safe ) {
210- console . warn ( '[Proxy] Blocked unsafe URL:' , target ) ;
211- return res . status ( 403 ) . send ( 'Forbidden: Access to private resources or invalid URL is denied.' ) ;
292+ let request ;
293+ try {
294+ request = await buildSafeProxyRequest ( entry . url , req . headers [ 'range' ] ) ;
295+ } catch ( err ) {
296+ console . warn ( '[Proxy] Blocked target:' , err . message ) ;
297+ return res . status ( err . statusCode || 403 ) . send ( err . message ) ;
212298 }
213299
214- const headers = { } ;
215- if ( req . headers [ 'range' ] ) headers [ 'Range' ] = req . headers [ 'range' ] ;
216- headers [ 'User-Agent' ] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' ;
217- headers [ 'Referer' ] = 'https://www.youtube.com/' ;
218- headers [ 'Origin' ] = 'https://www.youtube.com' ;
219- headers [ 'Accept' ] = '*/*' ;
220- headers [ 'Accept-Language' ] = 'en-US,en;q=0.9' ;
221-
222- const parsed = urlLib . parse ( target ) ;
223- const client = parsed . protocol === 'https:' ? https : http ;
300+ console . log ( '[Proxy] Fetching:' , request . displayUrl . substring ( 0 , 80 ) + '...' ) ;
224301
225- console . log ( '[Proxy] Fetching:' , target . substring ( 0 , 80 ) + '...' ) ;
226-
227- const proxReq = client . get ( target , { headers } , proxRes => {
302+ const proxReq = request . client . get ( request . options , proxRes => {
228303 console . log ( '[Proxy] Response status:' , proxRes . statusCode , 'Content-Type:' , proxRes . headers [ 'content-type' ] ) ;
229304
230305 // Content-Type'ı düzelt (video için)
@@ -256,13 +331,17 @@ async function startProxy() {
256331 if ( ! res . headersSent ) res . status ( 502 ) . send ( 'Proxy error: ' + err . message ) ;
257332 } ) ;
258333
334+ proxReq . setTimeout ( 30000 , ( ) => {
335+ proxReq . destroy ( new Error ( 'Proxy request timed out.' ) ) ;
336+ } ) ;
337+
259338 req . on ( 'close' , ( ) => {
260339 proxReq . destroy ( ) ;
261340 } ) ;
262341 } ) ;
263342
264343 return new Promise ( ( resolve ) => {
265- const server = exapp . listen ( 0 , ( ) => {
344+ const server = exapp . listen ( 0 , '127.0.0.1' , ( ) => {
266345 const { port } = server . address ( ) ;
267346 console . log ( '[Proxy] Started on port' , port ) ;
268347 resolve ( { server, port } ) ;
@@ -428,7 +507,6 @@ ipcMain.handle('fetchVideoInfo', async (event, url, cookiesOptions) => {
428507 // yt-dlp seçenekleri
429508 const ytdlpOptions = {
430509 dumpSingleJson : true ,
431- noCheckCertificates : true ,
432510 noWarnings : true ,
433511 preferFreeFormats : false
434512 } ;
@@ -486,7 +564,8 @@ ipcMain.handle('fetchVideoInfo', async (event, url, cookiesOptions) => {
486564 } ) ;
487565
488566 const previewFormat = progressive [ 0 ] || videoOnly [ 0 ] || null ;
489- const previewUrl = previewFormat ? `http://127.0.0.1:${ proxyPort } /proxy?url=${ encodeURIComponent ( previewFormat . url ) } ` : null ;
567+ const previewId = previewFormat ? registerProxyTarget ( previewFormat . url ) : null ;
568+ const previewUrl = previewId ? `http://127.0.0.1:${ proxyPort } /proxy/${ previewId } ` : null ;
490569
491570 console . log ( '[Preview] All formats count:' , formats . length ) ;
492571 console . log ( '[Preview] Progressive (playable) formats:' , progressive . length ) ;
@@ -512,7 +591,6 @@ ipcMain.handle('fetchVideoInfo', async (event, url, cookiesOptions) => {
512591 tbr : f . tbr ,
513592 abr : f . abr ,
514593 asr : f . asr ,
515- url : f . url || null ,
516594 container : f . container || null
517595 } ) ) ;
518596
@@ -607,7 +685,6 @@ ipcMain.handle('startExport', async (event, params) => {
607685 const ytdlpDownloadOptions = {
608686 format : ytFormat ,
609687 'no-warnings' : true ,
610- 'no-check-certificates' : true ,
611688 output : path . join ( tempDir , 'dl.%(ext)s' )
612689 } ;
613690
@@ -797,10 +874,18 @@ ipcMain.handle('revealInFolder', async (event, filePath) => {
797874 }
798875} ) ;
799876
877+ function sanitizeExternalUrl ( rawUrl ) {
878+ const parsed = new URL ( String ( rawUrl || '' ) ) ;
879+ if ( ! [ 'http:' , 'https:' ] . includes ( parsed . protocol ) ) {
880+ throw new Error ( 'Only http and https links can be opened.' ) ;
881+ }
882+ return parsed . href ;
883+ }
884+
800885// ----------------------------- IPC: Harici URL Aç -----------------------------
801886ipcMain . handle ( 'openExternal' , async ( event , url ) => {
802887 try {
803- await shell . openExternal ( url ) ;
888+ await shell . openExternal ( sanitizeExternalUrl ( url ) ) ;
804889 return { ok : true } ;
805890 } catch ( e ) {
806891 return { ok : false , error : String ( e ) } ;
@@ -841,4 +926,4 @@ ipcMain.handle('chooseCookiesFile', async (event) => {
841926ipcMain . handle ( 'getDefaultDocumentsPath' , async ( ) => {
842927 const documentsPath = app . getPath ( 'documents' ) ;
843928 return { ok : true , path : documentsPath } ;
844- } ) ;
929+ } ) ;
0 commit comments