lampac/TorrServer/ApiController.cs
lampac-talks f843f04fd4 chore: initial commit 154.3
Signed-off-by: lampac-talks <lampac-talks@users.noreply.github.com>
2026-01-30 16:23:09 +03:00

403 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using Shared;
using Shared.Engine;
using Shared.Models.Base;
using System;
using System.Buffers;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
namespace TorrServer.Controllers
{
public class TorrServerController : BaseController
{
#region ts.js
[HttpGet]
[AllowAnonymous]
[Route("ts.js")]
[Route("ts/js/{token}")]
public ActionResult Plugin(string token)
{
string file = FileCache.ReadAllText("plugins/ts.js").Replace("{localhost}", Regex.Replace(host, "^https?://", ""));
if (!string.IsNullOrEmpty(token))
file = Regex.Replace(file, "Lampa.Storage.set\\('torrserver_login'[^\n\r]+", $"Lampa.Storage.set('torrserver_login','{HttpUtility.UrlEncode(token)}');");
return Content(file, "application/javascript; charset=utf-8");
}
#endregion
#region HttpClient
private static readonly HttpClient httpClient = new HttpClient(new SocketsHttpHandler
{
AllowAutoRedirect = true,
AutomaticDecompression = DecompressionMethods.None,
SslOptions = { RemoteCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => true },
MaxConnectionsPerServer = 100
})
{
BaseAddress = new Uri($"http://{AppInit.conf.listen.localhost}:{ModInit.tsport}"),
DefaultRequestHeaders =
{
Authorization = new AuthenticationHeaderValue("Basic", CrypTo.Base64($"ts:{ModInit.tspass}")),
},
Timeout = TimeSpan.FromSeconds(30)
};
#endregion
#region Main
[HttpGet]
[Route("ts")]
[Route("ts/static/js/{suffix}")]
async public Task<ActionResult> Main()
{
string html = null;
string pathRequest = Regex.Replace(HttpContext.Request.Path.Value, "^/ts", "");
try
{
var responseMessage = await httpClient.GetAsync(pathRequest + HttpContext.Request.QueryString.Value).ConfigureAwait(false);
html = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
}
catch { }
if (html == null)
return StatusCode(500);
if (pathRequest.Contains(".js"))
{
string key = Regex.Match(html, "\\.concat\\(([^,]+),\"/echo\"").Groups[1].Value;
html = html.Replace($".concat({key},\"/", $".concat({key},\"/ts/");
return Content(html, "application/javascript; charset=utf-8");
}
else
{
html = html.Replace("href=\"/", "href=\"/ts/").Replace("src=\"/", "src=\"/ts/");
html = html.Replace("src=\"./", "src=\"/ts/");
return Content(html, "text/html; charset=utf-8");
}
}
#endregion
#region TorAPI
[HttpGet]
[HttpPost]
[Route("ts/{*suffix}")]
async public Task Index()
{
if (HttpContext.Request.Path.Value.StartsWith("/shutdown"))
{
HttpContext.Response.StatusCode = 404;
return;
}
if (AppInit.conf.accsdb.enable)
{
#region Обработка stream потока
if (HttpContext.Request.Method == "GET" && Regex.IsMatch(HttpContext.Request.Path.Value, "^/ts/(stream|play)"))
{
await TorAPI().ConfigureAwait(false);
return;
//if (ModInit.clientIps.Contains(HttpContext.Connection.RemoteIpAddress.ToString()))
//{
// await TorAPI();
// return;
//}
//else
//{
// HttpContext.Response.StatusCode = 404;
// return;
//}
}
#endregion
#region Access-Control-Request-Headers
if (HttpContext.Request.Method == "OPTIONS" && HttpContext.Request.Headers.TryGetValue("Access-Control-Request-Headers", out var AccessControl) && AccessControl == "authorization")
{
HttpContext.Response.StatusCode = 204;
return;
}
#endregion
if (HttpContext.Request.Headers.TryGetValue("Authorization", out var Authorization))
{
byte[] data = Convert.FromBase64String(Authorization.ToString().Replace("Basic ", ""));
string[] decodedString = Encoding.UTF8.GetString(data).Split(":");
string login = decodedString[0].ToLowerAndTrim();
string passwd = decodedString[1];
if (AppInit.conf.accsdb.findUser(login) is AccsUser user && !user.ban && user.expires > DateTime.UtcNow && passwd == ModInit.conf.defaultPasswd)
{
if (ModInit.conf.group > user.group)
{
await HttpContext.Response.WriteAsync("NoAccessGroup", HttpContext.RequestAborted).ConfigureAwait(false);
return;
}
await TorAPI(user).ConfigureAwait(false);
return;
}
}
if (HttpContext.Request.Path.Value.StartsWith("/ts/echo"))
{
await HttpContext.Response.WriteAsync("MatriX.API", HttpContext.RequestAborted).ConfigureAwait(false);
return;
}
HttpContext.Response.StatusCode = 401;
HttpContext.Response.Headers["Www-Authenticate"] = "Basic realm=Authorization Required";
return;
}
else
{
await TorAPI().ConfigureAwait(false);
return;
}
}
async public Task TorAPI(AccsUser user = null)
{
string pathRequest = Regex.Replace(HttpContext.Request.Path.Value, "^/ts", "");
string servUri = $"http://{AppInit.conf.listen.localhost}:{ModInit.tsport}{pathRequest + HttpContext.Request.QueryString.Value}";
#region settings
if (pathRequest.StartsWith("/settings"))
{
if (HttpContext.Request.Method != "POST")
{
HttpContext.Response.StatusCode = 404;
await HttpContext.Response.WriteAsync("404 page not found", HttpContext.RequestAborted).ConfigureAwait(false);
return;
}
using (var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8, bufferSize: PoolInvk.bufferSize, leaveOpen: true))
{
string requestJson = await reader.ReadToEndAsync().ConfigureAwait(false);
if (requestJson.Contains("\"get\""))
{
var rs = await httpClient.PostAsync("/settings", new StringContent("{\"action\":\"get\"}", Encoding.UTF8, "application/json")).ConfigureAwait(false);
await rs.Content.CopyToAsync(HttpContext.Response.Body, HttpContext.RequestAborted).ConfigureAwait(false);
return;
}
else if (!ModInit.conf.rdb || requestInfo.IP == "127.0.0.1" || requestInfo.IP.StartsWith("192.168."))
{
await httpClient.PostAsync("/settings", new StringContent(requestJson, Encoding.UTF8, "application/json")).ConfigureAwait(false);
}
await HttpContext.Response.WriteAsync(string.Empty, HttpContext.RequestAborted).ConfigureAwait(false);
return;
}
}
#endregion
#region playlist
if (pathRequest.StartsWith("/stream/") && HttpContext.Request.QueryString.Value.Contains("&m3u"))
{
string m3u = await httpClient.GetStringAsync(servUri).ConfigureAwait(false);
HttpContext.Response.ContentType = "audio/x-mpegurl; charset=utf-8";
await HttpContext.Response.WriteAsync((m3u ?? string.Empty).Replace("/stream/", "/ts/stream/"), HttpContext.RequestAborted).ConfigureAwait(false);
return;
}
#endregion
#region multiaccess
if (ModInit.conf.multiaccess == "full" || (ModInit.conf.multiaccess == "auth" && user != null))
{
if (HttpContext.Request.Method == "POST" && pathRequest == "/torrents" && user?.group != 666)
{
HttpContext.Request.EnableBuffering();
using (var readerBody = new StreamReader(HttpContext.Request.Body, Encoding.UTF8, bufferSize: PoolInvk.bufferSize, leaveOpen: true)) // Оставляем поток открытым
{
string requestJson = await readerBody.ReadToEndAsync().ConfigureAwait(false);
if (requestJson.Contains("\"action\":\"add\"") || requestJson.Contains("\"action\":\"list\""))
{
try
{
var rs = await httpClient.PostAsync(pathRequest, new StringContent(requestJson, Encoding.UTF8, "application/json")).ConfigureAwait(false);
string json = await rs.Content.ReadAsStringAsync().ConfigureAwait(false);
string uid = user?.id ?? user?.ids?.FirstOrDefault();
HttpContext.Response.ContentType = "application/json; charset=utf-8";
if (requestJson.Contains("\"action\":\"add\""))
{
#region add
string hash = Regex.Match(json, "\"hash\":\"([^\"]+)\"").Groups[1].Value;
if (!string.IsNullOrEmpty(hash))
{
var doc = ModInit.whosehash.FindById(hash);
if (doc != null)
{
doc.ip = requestInfo.IP;
doc.uid = uid;
ModInit.whosehash.Update(doc);
}
else
{
ModInit.whosehash.Insert(new WhoseHashModel
{
id = hash,
ip = requestInfo.IP,
uid = uid
});
}
}
await HttpContext.Response.WriteAsync(json, HttpContext.RequestAborted).ConfigureAwait(false);
return;
#endregion
}
else
{
#region list
var torrents = JArray.Parse(json);
for (int i = torrents.Count - 1; i >= 0; i--)
{
var hash = torrents[i]["hash"]?.ToString();
if (!string.IsNullOrEmpty(hash))
{
var doc = ModInit.whosehash.FindById(hash);
if (doc != null)
{
if (doc.ip == requestInfo.IP || (doc.uid != null && doc.uid == uid)) { }
else
torrents.RemoveAt(i);
}
}
}
await HttpContext.Response.WriteAsync(torrents.ToString(), HttpContext.RequestAborted).ConfigureAwait(false);
return;
#endregion
}
}
catch { }
HttpContext.Response.StatusCode = 503;
await HttpContext.Response.WriteAsync(string.Empty, HttpContext.RequestAborted).ConfigureAwait(false);
return;
}
}
// Сбрасываем позицию
HttpContext.Request.Body.Position = 0;
}
}
#endregion
var request = CreateProxyHttpRequest(HttpContext, new Uri(servUri));
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
await CopyProxyHttpResponse(HttpContext, response).ConfigureAwait(false);
}
#endregion
#region CreateProxyHttpRequest
HttpRequestMessage CreateProxyHttpRequest(HttpContext context, Uri uri)
{
var request = context.Request;
var requestMessage = new HttpRequestMessage();
var requestMethod = request.Method;
if (HttpMethods.IsPost(requestMethod))
{
var streamContent = new StreamContent(request.Body);
requestMessage.Content = streamContent;
}
foreach (var header in request.Headers)
{
if (header.Key.Equals("authorization", StringComparison.OrdinalIgnoreCase))
continue;
if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null)
requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
}
requestMessage.Headers.Host = string.IsNullOrEmpty(AppInit.conf.listen.host) ? context.Request.Host.Value : AppInit.conf.listen.host;
requestMessage.RequestUri = uri;
requestMessage.Method = new HttpMethod(request.Method);
return requestMessage;
}
#endregion
#region CopyProxyHttpResponse
async Task CopyProxyHttpResponse(HttpContext context, HttpResponseMessage responseMessage)
{
var response = context.Response;
response.StatusCode = (int)responseMessage.StatusCode;
#region UpdateHeaders
void UpdateHeaders(HttpHeaders headers)
{
foreach (var header in headers)
{
if (header.Key.Equals("transfer-encoding", StringComparison.OrdinalIgnoreCase) ||
header.Key.Equals("etag", StringComparison.OrdinalIgnoreCase) ||
header.Key.Equals("connection", StringComparison.OrdinalIgnoreCase) ||
header.Key.Equals("content-security-policy", StringComparison.OrdinalIgnoreCase) ||
header.Key.Equals("content-disposition", StringComparison.OrdinalIgnoreCase))
continue;
response.Headers[header.Key] = header.Value.ToArray();
}
}
#endregion
UpdateHeaders(responseMessage.Headers);
UpdateHeaders(responseMessage.Content.Headers);
var responseStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false);
if (response.Body == null)
throw new ArgumentNullException("destination");
if (!responseStream.CanRead && !responseStream.CanWrite)
throw new ObjectDisposedException("ObjectDisposed_StreamClosed");
if (!response.Body.CanRead && !response.Body.CanWrite)
throw new ObjectDisposedException("ObjectDisposed_StreamClosed");
if (!responseStream.CanRead || !response.Body.CanWrite)
throw new NotSupportedException("NotSupported_UnreadableStream");
byte[] buffer = ArrayPool<byte>.Shared.Rent(PoolInvk.rentChunk);
try
{
int bytesRead;
while ((bytesRead = await responseStream.ReadAsync(buffer, context.RequestAborted).ConfigureAwait(false)) != 0)
await response.Body.WriteAsync(buffer, 0, bytesRead, context.RequestAborted).ConfigureAwait(false);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
#endregion
}
}