4faf7a527b
Security (from a dynamic security audit of the branch): - H1: UnityWebRequestTransport disables auto-redirect on auth-bearing requests (redirectLimit=0) so a provider 3xx can't re-send the API key to a redirect host. - H2/P8: per-kind result-extension allowlist in AssetGenJobManager.WriteFile (+ defense-in-depth in the audio/image import pipelines) — a provider can no longer land a .cs/.asmdef/.meta/.asset under Assets/ (Editor RCE). - H3: ProviderHttp.RequireHost pins the submit URL and the provider-supplied response_url to https://queue.fal.run before the fal key is attached (both fal image + audio adapters). Correctness (from code review): - C1: Tripo image->3D now sends model_version. - C2/C3/C6/C10: FalAudioAdapter.BuildBody is catalog-driven — duration- required models (CassetteAI SFX/Music, Stable Audio) send a default duration when the caller passes 0 (fixes the default-input 422), fractional durations floor to >=1, Lyria stays prompt-only and its GUI no longer advertises a duration it ignores, and the clamp ceilings come from the catalog (no more duplicated 190/30/180). - C4: an unmapped fal poll status now fails fast instead of polling to the 600s timeout (both fal adapters). - C5: a stale/invalid selected-model pref is cleared on dropdown fallback. - C7: the audio fal-key status refreshes when the shared 2D fal key changes. Cleanup: extract AssetGenModelCatalog.ResolveModel (dedupes the model- resolution chain across the three generate tools) + DefaultModelId no-alloc. Verified: full EditMode suite 1166 tests, 0 failures (+19 new regression tests); 34 Python asset-gen tests pass. Claude-Session: https://claude.ai/code/session_015KYy51gwBuhDuLZXXoqc98
95 lines
3.4 KiB
C#
95 lines
3.4 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using UnityEngine.Networking;
|
|
|
|
namespace MCPForUnity.Editor.Services.AssetGen.Http
|
|
{
|
|
/// <summary>
|
|
/// Production <see cref="IHttpTransport"/> backed by UnityWebRequest. Must be invoked on the
|
|
/// Unity main thread (the asset-gen job manager guarantees this in Phase 3). The send is
|
|
/// awaited via a <see cref="TaskCompletionSource{T}"/> wired to the async op's completed
|
|
/// callback, so the call never blocks the editor loop.
|
|
/// </summary>
|
|
public sealed class UnityWebRequestTransport : IHttpTransport
|
|
{
|
|
public Task<HttpResult> SendAsync(HttpRequestSpec spec, CancellationToken ct)
|
|
{
|
|
if (spec == null) throw new ArgumentNullException(nameof(spec));
|
|
|
|
var tcs = new TaskCompletionSource<HttpResult>();
|
|
|
|
var request = new UnityWebRequest(spec.Url, spec.Method ?? UnityWebRequest.kHttpVerbGET)
|
|
{
|
|
downloadHandler = new DownloadHandlerBuffer()
|
|
};
|
|
if (spec.Body != null)
|
|
{
|
|
request.uploadHandler = new UploadHandlerRaw(spec.Body);
|
|
}
|
|
if (!string.IsNullOrEmpty(spec.ContentType))
|
|
{
|
|
request.SetRequestHeader("Content-Type", spec.ContentType);
|
|
}
|
|
if (spec.Headers != null)
|
|
{
|
|
foreach (var kv in spec.Headers)
|
|
{
|
|
request.SetRequestHeader(kv.Key, kv.Value);
|
|
}
|
|
}
|
|
// UnityWebRequest re-sends the Authorization header to a 3xx target by default. Never
|
|
// follow a redirect on an auth-bearing request — the key must not leak to the redirect
|
|
// host. No-auth downloads may still follow.
|
|
if (CarriesAuth(spec)) request.redirectLimit = 0;
|
|
|
|
CancellationTokenRegistration ctReg = default;
|
|
if (ct.CanBeCanceled)
|
|
{
|
|
ctReg = ct.Register(() =>
|
|
{
|
|
try { request.Abort(); } catch { /* ignore */ }
|
|
tcs.TrySetCanceled();
|
|
});
|
|
}
|
|
|
|
var op = request.SendWebRequest();
|
|
op.completed += _ =>
|
|
{
|
|
try
|
|
{
|
|
var result = new HttpResult
|
|
{
|
|
Status = (int)request.responseCode,
|
|
Body = request.downloadHandler?.data,
|
|
Text = request.downloadHandler?.text,
|
|
IsSuccess = request.result == UnityWebRequest.Result.Success
|
|
};
|
|
tcs.TrySetResult(result);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
tcs.TrySetException(e);
|
|
}
|
|
finally
|
|
{
|
|
ctReg.Dispose();
|
|
request.Dispose();
|
|
}
|
|
};
|
|
|
|
return tcs.Task;
|
|
}
|
|
|
|
/// <summary>True iff the request carries an Authorization header (case-insensitive key).</summary>
|
|
internal static bool CarriesAuth(HttpRequestSpec spec)
|
|
{
|
|
if (spec?.Headers == null) return false;
|
|
foreach (var kv in spec.Headers)
|
|
if (string.Equals(kv.Key, "Authorization", StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
return false;
|
|
}
|
|
}
|
|
}
|