2024-06-05 11:02:56 +02:00
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
2022-12-30 10:59:29 -08:00
2023-01-28 08:15:45 -08:00
using Amazon.S3 ;
using Duplicati.Library.RestAPI ;
2022-12-30 10:59:29 -08:00
using Duplicati.Server.WebServer.RESTMethods ;
2023-01-28 08:15:45 -08:00
using Duplicati.WebserverCore ;
using Microsoft.AspNetCore.Mvc ;
using System.Collections.Concurrent ;
using System.Net ;
using System.Reflection ;
2023-03-15 22:02:53 -07:00
using System.Text ;
2023-03-13 20:07:49 -07:00
using System.Xml.Linq ;
2022-12-30 10:59:29 -08:00
2023-01-29 17:29:57 -08:00
namespace Duplicati.WebserverCore
2022-12-30 10:59:29 -08:00
{
2023-01-28 08:15:45 -08:00
public class RESTHandlerCoreController : Controller
2022-12-30 10:59:29 -08:00
{
public const string API_URI_PATH = "/api/v1" ;
public static readonly int API_URI_SEGMENTS = API_URI_PATH . Split ( new char [] { '/' }). Length ;
2023-03-13 20:07:49 -07:00
private readonly Dictionary < string , IRESTMethod > _modules = new Dictionary < string , IRESTMethod >( StringComparer . OrdinalIgnoreCase );
2022-12-30 10:59:29 -08:00
2023-01-29 17:29:57 -08:00
public RESTHandlerCoreController ( IEnumerable < IRESTMethod > restMethods )
2023-01-28 08:15:45 -08:00
{
2023-01-29 17:29:57 -08:00
Console . WriteLine ( "Loaded controller!" );
2023-03-01 21:47:31 -08:00
//FIXME: Why no methods?
2023-01-29 17:29:57 -08:00
foreach ( var method in restMethods )
{
var t = method . GetType ();
_modules . Add ( t . Name . ToLowerInvariant (), method );
}
2023-01-28 08:15:45 -08:00
}
2023-05-03 21:10:19 -07:00
[Route(API_URI_PATH + "/{module?}/{**key}")]
2023-01-28 08:15:45 -08:00
[AcceptVerbs("GET", "POST", "PUT", "DELETE", "PATCH")]
2023-03-15 22:02:53 -07:00
public async System . Threading . Tasks . Task Index ( string? module , string? key )
2023-01-28 08:15:45 -08:00
{
module = ( module != null && module != "" ) ? module : "help" ;
var method = Request . Method ;
var ci = ParseRequestCulture ( Request . Headers . Accept );
2023-03-01 21:47:31 -08:00
var info = new RequestInfo ( new LegacyHttpRequestShim ( Request ), new LegacyHttpResponseShim ( Response ), new LegacyHttpSessionShim ());
2023-01-29 17:29:57 -08:00
2023-01-28 08:15:45 -08:00
using ( Library . Localization . LocalizationService . TemporaryContext ( ci ))
{
try
{
if ( ci != null )
Response . Headers . Add ( "Content-Language" , ci . Name );
IRESTMethod mod ;
_modules . TryGetValue ( module , out mod );
if ( mod == null )
{
2023-03-15 22:02:53 -07:00
Response . StatusCode = ( int ) HttpStatusCode . NotFound ;
await Response . Body . WriteAsync ( Encoding . UTF8 . GetBytes ( "No such module" ));
return ;
2023-01-28 08:15:45 -08:00
}
else if ( method == "GET" && mod is IRESTMethodGET get )
{
//if (info.Request.Form != HttpServer.HttpForm.EmptyForm)
//{
// if (info.Request.QueryString == HttpServer.HttpInput.Empty)
// {
// var r = info.Request.GetType().GetField("_queryString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
// r.SetValue(info.Request, new HttpServer.HttpInput("formdata"));
// }
// foreach (HttpServer.HttpInputItem v in info.Request.Form)
// if (!info.Request.QueryString.Contains(v.Name))
// info.Request.QueryString.Add(v.Name, v.Value);
//}
get . GET ( key , info );
}
2024-05-22 11:11:08 +02:00
else if ( method == "PUT" && mod is IRESTMethodPUT put )
2023-01-28 08:15:45 -08:00
put . PUT ( key , info );
else if ( method == "POST" && mod is IRESTMethodPOST post )
{
//if (info.Request.Form == HttpServer.HttpForm.EmptyForm || info.Request.Form == HttpServer.HttpInput.Empty)
//{
// var r = info.Request.GetType().GetMethod("AssignForm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new Type[] { typeof(HttpServer.HttpForm) }, null);
// r.Invoke(info.Request, new object[] { new HttpServer.HttpForm(info.Request.QueryString) });
//}
//else
//{
// foreach (HttpServer.HttpInputItem v in info.Request.QueryString)
// if (!info.Request.Form.Contains(v.Name))
// info.Request.Form.Add(v.Name, v.Value);
//}
post . POST ( key , info );
}
else if ( method == "DELETE" && mod is IRESTMethodDELETE delete )
delete . DELETE ( key , info );
else if ( method == "PATCH" && mod is IRESTMethodPATCH patch )
patch . PATCH ( key , info );
else
{
2023-03-15 22:02:53 -07:00
Response . StatusCode = ( int ) HttpStatusCode . MethodNotAllowed ;
await Response . Body . WriteAsync ( Encoding . UTF8 . GetBytes ( "Method is not allowed" ));
return ;
2023-01-28 08:15:45 -08:00
}
}
catch ( Exception ex )
{
2024-05-22 11:11:08 +02:00
FIXMEGlobal . DataConnection . LogError ( "" , string . Format ( "Request for {0} gave error" , Request . Path ), ex );
2023-01-28 08:15:45 -08:00
Console . WriteLine ( ex );
try
{
var wex = ex ;
2023-03-15 22:02:53 -07:00
while ( wex is System . Reflection . TargetInvocationException && wex . InnerException != wex && wex . InnerException != null )
2023-01-28 08:15:45 -08:00
wex = wex . InnerException ;
2023-03-15 22:02:53 -07:00
Response . StatusCode = ( int ) HttpStatusCode . InternalServerError ;
await Response . Body . WriteAsync ( Encoding . UTF8 . GetBytes (
wex . Message
2023-01-28 08:15:45 -08:00
#if DEBUG
+ "\n" + wex . StackTrace
#endif
2023-03-15 22:02:53 -07:00
));
return ;
2023-01-28 08:15:45 -08:00
}
catch ( Exception flex )
{
FIXMEGlobal . DataConnection . LogError ( "" , "Reporting error gave error" , flex );
}
}
}
}
private static readonly ConcurrentDictionary < string , System . Globalization . CultureInfo > _cultureCache = new ConcurrentDictionary < string , System . Globalization . CultureInfo >( StringComparer . OrdinalIgnoreCase );
private static System . Globalization . CultureInfo ParseRequestCulture ( string acceptheader )
{
acceptheader = acceptheader ?? string . Empty ;
// Lock-free read
System . Globalization . CultureInfo ci ;
if ( _cultureCache . TryGetValue ( acceptheader , out ci ))
return ci ;
// Lock-free assignment, we might compute the value twice
return _cultureCache [ acceptheader ] =
// Parse headers like "Accept-Language: da, en-gb;q=0.8, en;q=0.7"
acceptheader
. Split ( new [] { "," }, StringSplitOptions . RemoveEmptyEntries )
. Select ( x =>
{
var opts = x . Split ( new [] { ";" }, StringSplitOptions . RemoveEmptyEntries );
var lang = opts . FirstOrDefault ();
var weight =
opts . Where ( y => y . StartsWith ( "q=" , StringComparison . OrdinalIgnoreCase ))
. Select ( y =>
{
float f ;
float . TryParse ( y . Substring ( 2 ), System . Globalization . NumberStyles . Float , System . Globalization . CultureInfo . InvariantCulture , out f );
return f ;
}). FirstOrDefault ();
// Set the default weight=1
if ( weight <= 0.001 && weight >= 0 )
weight = 1 ;
return new KeyValuePair < string , float >( lang , weight );
})
// Handle priority
. OrderByDescending ( x => x . Value )
. Select ( x => x . Key )
. Distinct ()
// Filter invalid/unsupported items
. Where ( x => ! string . IsNullOrWhiteSpace ( x ) && Library . Localization . LocalizationService . ParseCulture ( x ) != null )
. Select ( x => Library . Localization . LocalizationService . ParseCulture ( x ))
// And get the first that works
. FirstOrDefault ( System . Globalization . CultureInfo . InvariantCulture );
}
}
public static class RESTHandlerCoreExtensions
{
public static IHostBuilder UseRESTHandlers (
this IHostBuilder builder )
2022-12-30 10:59:29 -08:00
{
var lst =
from n in typeof ( IRESTMethod ). Assembly . GetTypes ()
where
n . Namespace == typeof ( IRESTMethod ). Namespace
&&
typeof ( IRESTMethod ). IsAssignableFrom ( n )
&&
! n . IsAbstract
&&
! n . IsInterface
select n ;
2023-01-28 08:15:45 -08:00
builder . ConfigureServices ( services =>
2022-12-30 10:59:29 -08:00
{
2023-01-28 08:15:45 -08:00
foreach ( var t in lst )
{
2023-03-13 20:07:49 -07:00
services . AddSingleton < IRESTMethod >(( IRESTMethod ?) Activator . CreateInstance ( t ) ?? throw new NullReferenceException ( "RestMethod " + nameof ( t ) + " is null" ));
2023-01-28 08:15:45 -08:00
}
});
return builder ;
}
2023-01-29 17:29:57 -08:00
//public static IEndpointRouteBuilder UseRESTHandlerEndpoints(this IEndpointRouteBuilder builder)
//{
// builder.MapFallback(async (HttpContext context) => {
// await System.Threading.Tasks.Task.Delay(1);
// // context.
// //TODO: Register controllers without this middleware ?
// });
// return builder;
//}
2022-12-30 10:59:29 -08:00
}
2023-01-28 08:15:45 -08:00
2022-12-30 10:59:29 -08:00
}