Paijo commited on
Commit
eb4aafa
·
verified ·
1 Parent(s): 98429a8

update app/routers/proxies.py

Browse files
Files changed (1) hide show
  1. app/routers/proxies.py +465 -0
app/routers/proxies.py ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, Query, HTTPException, Request, status
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from typing import List, Optional
4
+ from pydantic import BaseModel
5
+ from datetime import datetime
6
+ from slowapi import Limiter
7
+ from slowapi.util import get_remote_address
8
+ import aiohttp
9
+ import asyncio
10
+ import time
11
+
12
+ from app.database import get_db
13
+ from app.db_storage import db_storage
14
+ from app.dependencies import require_admin
15
+
16
+ # Rate limiter for this router
17
+ limiter = Limiter(key_func=get_remote_address)
18
+
19
+ router = APIRouter(prefix="/api/v1", tags=["proxies"])
20
+
21
+
22
+ class ProxyResponse(BaseModel):
23
+ id: int
24
+ url: str
25
+ protocol: str
26
+ ip: Optional[str]
27
+ port: Optional[int]
28
+ country_code: Optional[str]
29
+ country_name: Optional[str]
30
+ state: Optional[str]
31
+ city: Optional[str]
32
+ latency_ms: Optional[int]
33
+ speed_mbps: Optional[float]
34
+ anonymity: Optional[str]
35
+ proxy_type: Optional[str]
36
+ can_access_google: Optional[bool]
37
+ quality_score: Optional[int]
38
+ is_working: bool
39
+ last_validated: Optional[str]
40
+
41
+ class Config:
42
+ from_attributes = True
43
+
44
+
45
+ class ProxiesListResponse(BaseModel):
46
+ total: int
47
+ count: int
48
+ offset: int
49
+ limit: int
50
+ proxies: List[ProxyResponse]
51
+
52
+
53
+ @router.get("/proxies/advanced", response_model=ProxiesListResponse)
54
+ async def get_proxies_advanced(
55
+ protocol: Optional[str] = Query(None, description="Filter by protocol"),
56
+ country_code: Optional[str] = Query(
57
+ None, description="Filter by country code (e.g., US, GB)"
58
+ ),
59
+ anonymity: Optional[str] = Query(
60
+ None, description="Filter by anonymity level (transparent, anonymous, elite)"
61
+ ),
62
+ proxy_type: Optional[str] = Query(
63
+ None, description="Filter by type (datacenter, residential, mobile)"
64
+ ),
65
+ can_access_google: Optional[bool] = Query(
66
+ None, description="Filter by Google accessibility"
67
+ ),
68
+ min_quality: Optional[int] = Query(
69
+ None, ge=0, le=100, description="Minimum quality score (0-100)"
70
+ ),
71
+ min_speed: Optional[float] = Query(None, ge=0, description="Minimum speed in Mbps"),
72
+ max_latency: Optional[int] = Query(None, ge=0, description="Maximum latency in ms"),
73
+ is_working: bool = Query(True, description="Show only working proxies"),
74
+ order_by: str = Query(
75
+ "quality_score",
76
+ description="Sort by: quality_score, latency_ms, speed_mbps, created_at",
77
+ ),
78
+ order_direction: str = Query("desc", description="Sort direction: asc or desc"),
79
+ limit: int = Query(100, ge=1, le=1000, description="Number of results"),
80
+ offset: int = Query(0, ge=0, description="Offset for pagination"),
81
+ session: AsyncSession = Depends(get_db),
82
+ ):
83
+ proxies, total = await db_storage.get_proxies(
84
+ session=session,
85
+ protocol=protocol,
86
+ country_code=country_code,
87
+ anonymity=anonymity,
88
+ min_quality=min_quality,
89
+ is_working=is_working,
90
+ limit=limit,
91
+ offset=offset,
92
+ order_by=order_by,
93
+ )
94
+
95
+ filtered_proxies = []
96
+ for proxy in proxies:
97
+ if proxy_type and proxy.proxy_type != proxy_type:
98
+ continue
99
+ if (
100
+ can_access_google is not None
101
+ and proxy.can_access_google != can_access_google
102
+ ):
103
+ continue
104
+ if min_speed is not None and (
105
+ proxy.speed_mbps is None or proxy.speed_mbps < min_speed
106
+ ):
107
+ continue
108
+ if max_latency is not None and (
109
+ proxy.latency_ms is None or proxy.latency_ms > max_latency
110
+ ):
111
+ continue
112
+
113
+ filtered_proxies.append(proxy)
114
+
115
+ return ProxiesListResponse(
116
+ total=total,
117
+ count=len(filtered_proxies),
118
+ offset=offset,
119
+ limit=limit,
120
+ proxies=[
121
+ ProxyResponse(
122
+ **{
123
+ **proxy.__dict__,
124
+ "last_validated": proxy.last_validated.isoformat()
125
+ if proxy.last_validated
126
+ else None,
127
+ }
128
+ )
129
+ for proxy in filtered_proxies
130
+ ],
131
+ )
132
+
133
+
134
+ @router.get("/proxies/filters/options")
135
+ async def get_filter_options(session: AsyncSession = Depends(get_db)):
136
+ from sqlalchemy import select, func, distinct
137
+ from app.db_models import Proxy
138
+
139
+ protocols_result = await session.execute(
140
+ select(distinct(Proxy.protocol)).where(Proxy.is_working == True)
141
+ )
142
+ protocols = [p for p in protocols_result.scalars().all() if p]
143
+
144
+ countries_result = await session.execute(
145
+ select(
146
+ Proxy.country_code, Proxy.country_name, func.count(Proxy.id).label("count")
147
+ )
148
+ .where(Proxy.is_working == True, Proxy.country_code.isnot(None))
149
+ .group_by(Proxy.country_code, Proxy.country_name)
150
+ .order_by(func.count(Proxy.id).desc())
151
+ .limit(50)
152
+ )
153
+ countries = [
154
+ {"code": row.country_code, "name": row.country_name, "count": row.count}
155
+ for row in countries_result.all()
156
+ ]
157
+
158
+ anonymity_levels = ["transparent", "anonymous", "elite"]
159
+ proxy_types = ["datacenter", "residential", "mobile", "unknown"]
160
+
161
+ quality_ranges = [
162
+ {"label": "Excellent (80-100)", "min": 80, "max": 100},
163
+ {"label": "Good (60-79)", "min": 60, "max": 79},
164
+ {"label": "Fair (40-59)", "min": 40, "max": 59},
165
+ {"label": "Poor (0-39)", "min": 0, "max": 39},
166
+ ]
167
+
168
+ return {
169
+ "protocols": protocols,
170
+ "countries": countries,
171
+ "anonymity_levels": anonymity_levels,
172
+ "proxy_types": proxy_types,
173
+ "quality_ranges": quality_ranges,
174
+ "sort_options": [
175
+ {"value": "quality_score", "label": "Quality Score"},
176
+ {"value": "latency_ms", "label": "Latency (fastest first)"},
177
+ {"value": "speed_mbps", "label": "Speed (fastest first)"},
178
+ {"value": "created_at", "label": "Recently Added"},
179
+ ],
180
+ }
181
+
182
+
183
+ @router.get("/proxies/export")
184
+ @limiter.limit("100/hour") # Rate limit: 100 exports per hour
185
+ async def export_proxies(
186
+ request: Request,
187
+ format: str = Query("txt", description="Export format: txt, json, csv, pac"),
188
+ protocol: Optional[str] = None,
189
+ country_code: Optional[str] = None,
190
+ min_quality: Optional[int] = None,
191
+ limit: int = Query(1000, ge=1, le=10000),
192
+ session: AsyncSession = Depends(get_db),
193
+ ):
194
+ from fastapi.responses import PlainTextResponse, StreamingResponse
195
+ import json
196
+ import io
197
+
198
+ proxies, _ = await db_storage.get_proxies(
199
+ session=session,
200
+ protocol=protocol,
201
+ country_code=country_code,
202
+ min_quality=min_quality,
203
+ is_working=True,
204
+ limit=limit,
205
+ offset=0,
206
+ order_by="quality_score",
207
+ )
208
+
209
+ if format == "txt":
210
+ content = "\n".join([proxy.url for proxy in proxies])
211
+ return PlainTextResponse(content=content, media_type="text/plain")
212
+
213
+ elif format == "json":
214
+ data = [
215
+ {
216
+ "url": proxy.url,
217
+ "protocol": proxy.protocol,
218
+ "country": proxy.country_code,
219
+ "latency_ms": proxy.latency_ms,
220
+ "anonymity": proxy.anonymity,
221
+ "quality_score": proxy.quality_score,
222
+ }
223
+ for proxy in proxies
224
+ ]
225
+ return PlainTextResponse(
226
+ content=json.dumps(data, indent=2), media_type="application/json"
227
+ )
228
+
229
+ elif format == "csv":
230
+ import csv
231
+
232
+ output = io.StringIO()
233
+ writer = csv.writer(output)
234
+ writer.writerow(
235
+ ["URL", "Protocol", "Country", "Latency(ms)", "Anonymity", "Quality"]
236
+ )
237
+
238
+ for proxy in proxies:
239
+ writer.writerow(
240
+ [
241
+ proxy.url,
242
+ proxy.protocol,
243
+ proxy.country_code or "",
244
+ proxy.latency_ms or "",
245
+ proxy.anonymity or "",
246
+ proxy.quality_score or "",
247
+ ]
248
+ )
249
+
250
+ return StreamingResponse(
251
+ iter([output.getvalue()]),
252
+ media_type="text/csv",
253
+ headers={"Content-Disposition": "attachment; filename=proxies.csv"},
254
+ )
255
+
256
+ elif format == "pac":
257
+ # Generate PAC (Proxy Auto-Config) file for browser configuration
258
+ # Filter to HTTP/HTTPS proxies only (PAC doesn't support other protocols)
259
+ http_proxies = [p for p in proxies if p.protocol.lower() in ["http", "https"]]
260
+
261
+ if not http_proxies:
262
+ proxy_list = "DIRECT"
263
+ else:
264
+ # Build proxy list (round-robin load balancing)
265
+ proxy_list = "; ".join(
266
+ [
267
+ f"PROXY {p.ip}:{p.port}"
268
+ for p in http_proxies[:10] # Limit to top 10 for performance
269
+ ]
270
+ )
271
+ proxy_list += "; DIRECT"
272
+
273
+ pac_content = f"""function FindProxyForURL(url, host) {{
274
+ // 1proxy PAC File - Auto-generated proxy configuration
275
+ // Generated: {datetime.utcnow().isoformat()}
276
+ // Total proxies: {len(http_proxies)}
277
+
278
+ // Bypass localhost and private networks
279
+ if (isPlainHostName(host) ||
280
+ shExpMatch(host, "*.local") ||
281
+ isInNet(host, "10.0.0.0", "255.0.0.0") ||
282
+ isInNet(host, "172.16.0.0", "255.240.0.0") ||
283
+ isInNet(host, "192.168.0.0", "255.255.0.0") ||
284
+ isInNet(host, "127.0.0.0", "255.0.0.0")) {{
285
+ return "DIRECT";
286
+ }}
287
+
288
+ // Use proxy for all other requests (round-robin)
289
+ return "{proxy_list}";
290
+ }}"""
291
+
292
+ return PlainTextResponse(
293
+ content=pac_content,
294
+ media_type="application/x-ns-proxy-autoconfig",
295
+ headers={"Content-Disposition": "attachment; filename=1proxy.pac"},
296
+ )
297
+
298
+ return {"error": "Invalid format. Supported: txt, json, csv, pac"}
299
+
300
+
301
+ @router.get("/proxies/random", response_model=ProxyResponse)
302
+ async def get_random_proxy(
303
+ protocol: Optional[str] = Query(None, description="Filter by protocol"),
304
+ country_code: Optional[str] = Query(None, description="Filter by country code"),
305
+ min_quality: Optional[int] = Query(None, description="Minimum quality score"),
306
+ anonymity: Optional[str] = Query(
307
+ None, description="Filter by anonymity (transparent, anonymous, elite)"
308
+ ),
309
+ max_latency: Optional[int] = Query(None, description="Maximum latency in ms"),
310
+ exclude: Optional[str] = Query(
311
+ None,
312
+ description="Comma-separated list of IPs to exclude (e.g., '1.2.3.4,5.6.7.8')",
313
+ ),
314
+ session: AsyncSession = Depends(get_db),
315
+ ):
316
+ """
317
+ Get a random high-quality proxy with smart filtering.
318
+
319
+ Use the 'exclude' parameter to implement rotation by excluding
320
+ previously used IPs. This ensures you get different proxies
321
+ on each request.
322
+
323
+ Example: /proxies/random?min_quality=70&exclude=192.168.1.1,10.0.0.1
324
+ """
325
+
326
+ # Parse exclude list
327
+ excluded_ips = set()
328
+ if exclude:
329
+ excluded_ips = set(ip.strip() for ip in exclude.split(",") if ip.strip())
330
+
331
+ proxy = await db_storage.get_random_proxy(
332
+ session=session,
333
+ protocol=protocol,
334
+ country_code=country_code,
335
+ min_quality=min_quality,
336
+ anonymity=anonymity,
337
+ max_latency=max_latency,
338
+ )
339
+
340
+ # If proxy is in exclude list, try to get another one
341
+ max_attempts = 5
342
+ attempts = 0
343
+ while proxy and proxy.ip in excluded_ips and attempts < max_attempts:
344
+ proxy = await db_storage.get_random_proxy(
345
+ session=session,
346
+ protocol=protocol,
347
+ country_code=country_code,
348
+ min_quality=min_quality,
349
+ anonymity=anonymity,
350
+ max_latency=max_latency,
351
+ )
352
+ attempts += 1
353
+
354
+ if not proxy:
355
+ raise HTTPException(status_code=404, detail="No matching proxies found")
356
+
357
+ if proxy.ip in excluded_ips:
358
+ raise HTTPException(
359
+ status_code=404,
360
+ detail="No proxies available that are not in the exclude list",
361
+ )
362
+
363
+ return ProxyResponse(
364
+ **{
365
+ **proxy.__dict__,
366
+ "last_validated": proxy.last_validated.isoformat()
367
+ if proxy.last_validated
368
+ else None,
369
+ }
370
+ )
371
+
372
+
373
+ class ProxyTestRequest(BaseModel):
374
+ proxy_url: str
375
+ target_url: str = "https://www.google.com"
376
+ timeout: int = 5
377
+
378
+
379
+ class ProxyTestResponse(BaseModel):
380
+ proxy_url: str
381
+ target_url: str
382
+ working: bool
383
+ latency_ms: Optional[int]
384
+ status_code: Optional[int]
385
+ error: Optional[str]
386
+ tested_at: str
387
+
388
+
389
+ @router.post("/proxies/test", response_model=ProxyTestResponse)
390
+ @limiter.limit("10/minute") # Rate limit: 10 tests per minute to prevent abuse
391
+ async def test_proxy(request: Request, test_request: ProxyTestRequest):
392
+ """
393
+ Test if a proxy works by making a request through it.
394
+
395
+ This endpoint is rate-limited to prevent abuse.
396
+ Free tier: 10 tests per minute.
397
+ """
398
+ tested_at = datetime.utcnow().isoformat()
399
+
400
+ try:
401
+ # Parse proxy URL
402
+ if not test_request.proxy_url.startswith(("http://", "https://", "socks5://")):
403
+ raise HTTPException(
404
+ status_code=400,
405
+ detail="Invalid proxy URL. Must start with http://, https://, or socks5://",
406
+ )
407
+
408
+ start_time = time.time()
409
+
410
+ # Create aiohttp session with proxy
411
+ timeout_config = aiohttp.ClientTimeout(total=test_request.timeout)
412
+
413
+ async with aiohttp.ClientSession(timeout=timeout_config) as session:
414
+ try:
415
+ async with session.get(
416
+ test_request.target_url,
417
+ proxy=test_request.proxy_url,
418
+ ssl=False, # Skip SSL verification for testing
419
+ ) as response:
420
+ latency_ms = int((time.time() - start_time) * 1000)
421
+
422
+ return ProxyTestResponse(
423
+ proxy_url=test_request.proxy_url,
424
+ target_url=test_request.target_url,
425
+ working=True,
426
+ latency_ms=latency_ms,
427
+ status_code=response.status,
428
+ error=None,
429
+ tested_at=tested_at,
430
+ )
431
+ except aiohttp.ClientError as e:
432
+ return ProxyTestResponse(
433
+ proxy_url=test_request.proxy_url,
434
+ target_url=test_request.target_url,
435
+ working=False,
436
+ latency_ms=None,
437
+ status_code=None,
438
+ error=f"Connection error: {str(e)}",
439
+ tested_at=tested_at,
440
+ )
441
+
442
+ except asyncio.TimeoutError:
443
+ return ProxyTestResponse(
444
+ proxy_url=test_request.proxy_url,
445
+ target_url=test_request.target_url,
446
+ working=False,
447
+ latency_ms=None,
448
+ status_code=None,
449
+ error="Connection timeout",
450
+ tested_at=tested_at,
451
+ )
452
+
453
+
454
+ @router.delete("/proxies/{proxy_id}", status_code=status.HTTP_204_NO_CONTENT)
455
+ @limiter.limit("30/minute")
456
+ async def delete_proxy(
457
+ request: Request,
458
+ proxy_id: int,
459
+ session: AsyncSession = Depends(get_db),
460
+ admin_user=Depends(require_admin),
461
+ ):
462
+ success = await db_storage.delete_proxy(session, proxy_id)
463
+ if not success:
464
+ raise HTTPException(status_code=404, detail="Proxy not found")
465
+ return None