Come posso implementare ISerializable in .NET 4+ senza violare le regole di sicurezza dell'ereditarietà?


109

Background: Noda Time contiene molte strutture serializzabili. Anche se non mi piace la serializzazione binaria, abbiamo ricevuto molte richieste per supportarla, nella timeline 1.x. Lo supportiamo implementando l' ISerializableinterfaccia.

Abbiamo ricevuto un recente rapporto sul problema di Noda Time 2.x che non funziona in .NET Fiddle . Lo stesso codice che utilizza Noda Time 1.x funziona bene. L'eccezione generata è questa:

Regole di sicurezza sull'ereditarietà violate durante l'override del membro: 'NodaTime.Duration.System.Runtime.Serialization.ISerializable.GetObjectData (System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)'. L'accessibilità di sicurezza del metodo di sovrascrittura deve corrispondere all'accessibilità di sicurezza del metodo sottoposto a override.

L'ho ristretto al framework mirato: 1.x è destinato a .NET 3.5 (profilo client); 2.x è destinato a .NET 4.5. Hanno grandi differenze in termini di supporto PCL vs .NET Core e la struttura del file di progetto, ma sembra che questo sia irrilevante.

Sono riuscito a riprodurlo in un progetto locale, ma non ho trovato una soluzione.

Passaggi per riprodurre in VS2017:

  • Crea una nuova soluzione
  • Crea una nuova applicazione console Windows classica destinata a .NET 4.5.1. L'ho chiamato "CodeRunner".
  • Nelle proprietà del progetto passare a Firma e firmare l'assembly con una nuova chiave. Deselezionare il requisito della password e utilizzare un nome file di chiavi qualsiasi.
  • Incolla il codice seguente da sostituire Program.cs. Questa è una versione abbreviata del codice in questo esempio Microsoft . Ho mantenuto tutti i percorsi uguali, quindi se vuoi tornare al codice completo, non dovresti aver bisogno di cambiare nient'altro.

Codice:

using System;
using System.Security;
using System.Security.Permissions;

class Sandboxer : MarshalByRefObject  
{  
    static void Main()  
    {  
        var adSetup = new AppDomainSetup();  
        adSetup.ApplicationBase = System.IO.Path.GetFullPath(@"..\..\..\UntrustedCode\bin\Debug");  
        var permSet = new PermissionSet(PermissionState.None);  
        permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));  
        var fullTrustAssembly = typeof(Sandboxer).Assembly.Evidence.GetHostEvidence<System.Security.Policy.StrongName>();  
        var newDomain = AppDomain.CreateDomain("Sandbox", null, adSetup, permSet, fullTrustAssembly);  
        var handle = Activator.CreateInstanceFrom(  
            newDomain, typeof(Sandboxer).Assembly.ManifestModule.FullyQualifiedName,  
            typeof(Sandboxer).FullName  
            );  
        Sandboxer newDomainInstance = (Sandboxer) handle.Unwrap();  
        newDomainInstance.ExecuteUntrustedCode("UntrustedCode", "UntrustedCode.UntrustedClass", "IsFibonacci", new object[] { 45 });  
    }  

    public void ExecuteUntrustedCode(string assemblyName, string typeName, string entryPoint, Object[] parameters)  
    {  
        var target = System.Reflection.Assembly.Load(assemblyName).GetType(typeName).GetMethod(entryPoint);
        target.Invoke(null, parameters);
    }  
}
  • Crea un altro progetto chiamato "UntrustedCode". Dovrebbe essere un progetto di libreria di classi desktop classico.
  • Firmare l'assemblea; puoi usare una nuova chiave o la stessa di CodeRunner. (Questo è in parte per imitare la situazione Noda Time e in parte per mantenere felice l'analisi del codice.)
  • Incolla il seguente codice Class1.cs(sovrascrivendo quello che c'è):

Codice:

using System;
using System.Runtime.Serialization;
using System.Security;
using System.Security.Permissions;

// [assembly: AllowPartiallyTrustedCallers]

namespace UntrustedCode
{
    public class UntrustedClass
    {
        // Method named oddly (given the content) in order to allow MSDN
        // sample to run unchanged.
        public static bool IsFibonacci(int number)
        {
            Console.WriteLine(new CustomStruct());
            return true;
        }
    }

    [Serializable]
    public struct CustomStruct : ISerializable
    {
        private CustomStruct(SerializationInfo info, StreamingContext context) { }

        //[SecuritySafeCritical]
        //[SecurityCritical]
        //[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            throw new NotImplementedException();
        }
    }
}

L'esecuzione del progetto CodeRunner genera la seguente eccezione (riformattata per la leggibilità):

Eccezione non gestita: System.Reflection.TargetInvocationException:
eccezione generata dalla destinazione di una chiamata.
--->
System.TypeLoadException:
regole di sicurezza dell'ereditarietà violate durante l'override del membro:
'UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData (...).
L'accessibilità di sicurezza del metodo di sovrascrittura deve corrispondere
all'accessibilità di sicurezza del metodo sottoposto a override.

Gli attributi commentati mostrano le cose che ho provato:

  • SecurityPermissionè raccomandato da due diversi articoli di MS ( primo , secondo ), anche se è interessante notare che fanno cose diverse riguardo all'implementazione dell'interfaccia esplicita / implicita
  • SecurityCriticalè ciò che Noda Time ha attualmente, ed è ciò che suggerisce la risposta di questa domanda
  • SecuritySafeCritical è in qualche modo suggerito dai messaggi delle regole di analisi del codice
  • Senza alcun attributo, le regole di analisi del codice sono soddisfatte - con uno SecurityPermissiono più SecurityCritical presenti, le regole ti dicono di rimuovere gli attributi - a meno che tu non lo abbia AllowPartiallyTrustedCallers. Seguire i suggerimenti in entrambi i casi non aiuta.
  • Noda Time ha AllowPartiallyTrustedCallersapplicato ad esso; l'esempio qui non funziona né con né senza l'attributo applicato.

Le piste codice senza un'eccezione se aggiungo [assembly: SecurityRules(SecurityRuleSet.Level1)]al UntrustedCodemontaggio (e rimuovere il commento dalla AllowPartiallyTrustedCallersattributi), ma credo che sia una cattiva soluzione al problema che potrebbe ostacolare altro codice.

Ammetto pienamente di essere abbastanza perso quando si tratta di questo tipo di aspetto della sicurezza di .NET. Quindi cosa posso fare per scegliere come target .NET 4.5 e consentire tuttavia ai miei tipi di implementare ISerializableed essere ancora utilizzati in ambienti come .NET Fiddle?

(Anche se sto prendendo di mira .NET 4.5, credo che siano le modifiche alla politica di sicurezza di .NET 4.0 che hanno causato il problema, da cui il tag.)


È interessante notare che questa spiegazione delle modifiche al modello di sicurezza in 4.0 suggerisce che la semplice rimozione AllowPartiallyTrustedCallersdovrebbe fare il trucco, ma non sembra fare la differenza
Mathias R. Jessen

Risposte:


56

Secondo MSDN , in .NET 4.0 fondamentalmente non dovresti usare ISerializableper codice parzialmente attendibile, e invece dovresti usare ISafeSerializationData

Citando da https://docs.microsoft.com/en-us/dotnet/standard/serialization/custom-serialization

Importante

Nelle versioni precedenti a .NET Framework 4.0, la serializzazione dei dati utente personalizzati in un assembly parzialmente attendibile veniva eseguita utilizzando GetObjectData. A partire dalla versione 4.0, tale metodo è contrassegnato con l'attributo SecurityCriticalAttribute che impedisce l'esecuzione in assembly parzialmente attendibili. Per aggirare questa condizione, implementare l'interfaccia ISafeSerializationData.

Quindi probabilmente non è quello che volevi sentire se ne hai bisogno, ma non penso che ci sia alcun modo per aggirarlo mentre ISerializablecontinui a usarlo (a parte tornare alla Level1sicurezza, che hai detto che non vuoi).

PS: i ISafeSerializationDatadocumenti affermano che è solo per eccezioni, ma non sembra così specifico, potresti provare ... Fondamentalmente non posso testarlo con il tuo codice di esempio (a parte la rimozione di ISerializablelavori, ma lo sapevi già) ... dovrai vedere se ISafeSerializationDatati va abbastanza bene.

PS2: l' SecurityCriticalattributo non funziona perché viene ignorato quando l'assembly viene caricato in modalità di attendibilità parziale ( sulla sicurezza di Livello2 ). Lo si può vedere sul vostro codice di esempio, se si esegue il debug targetvariabile nella ExecuteUntrustedCodedestra prima di richiamare esso, dovrete IsSecurityTransparentper truee IsSecurityCriticalper false, anche se si contrassegna il metodo con l' SecurityCriticalattributo)


Aha - grazie per la spiegazione. Peccato che l'eccezione sia così fuorviante qui. Dovrà capire cosa fare ...
Jon Skeet

@ JonSkeet Onestamente, abbandonerei la serializzazione binaria del tutto ... ma capisco che alla tua base di utenti potrebbe non piacere
Jcl

Penso che dovremo farlo, il che significa passare alla v3.0. Ha altri vantaggi però ... dovrò consultare la community di Noda Time.
Jon Skeet

12
@JonSkeet btw, se sei interessato, questo articolo spiega le differenze tra il livello 1 e il livello 2 di sicurezza (e PERCHÉ non funziona)
Jcl

8

La risposta accettata è così convincente che ho quasi creduto che non fosse un bug. Ma dopo aver fatto alcuni esperimenti ora posso dire che la sicurezza di Level2 è un completo disastro; almeno, qualcosa è davvero strano.

Un paio di giorni fa mi sono imbattuto nello stesso problema con le mie librerie. Ho creato rapidamente uno unit test; tuttavia, non sono riuscito a riprodurre il problema riscontrato in .NET Fiddle, mentre lo stesso codice ha generato "con successo" l'eccezione in un'app console. Alla fine ho trovato due modi strani per superare il problema.

TL; DR : si scopre che se si utilizza un tipo interno della libreria utilizzata nel progetto consumer, il codice parzialmente attendibile funziona come previsto: è in grado di istanziare ISerializableun'implementazione (e un codice critico per la sicurezza non può essere chiamato direttamente, ma vedi sotto). Oppure, cosa ancora più ridicola, puoi provare a creare di nuovo la sandbox se non ha funzionato per la prima volta ...

Ma vediamo un po 'di codice.

ClassLibrary.dll:

Separiamo due casi: uno per una classe normale con contenuto critico per la sicurezza e uno per l' ISerializableimplementazione:

public class CriticalClass
{
    public void SafeCode() { }

    [SecurityCritical]
    public void CriticalCode() { }

    [SecuritySafeCritical]
    public void SafeEntryForCriticalCode() => CriticalCode();
}

[Serializable]
public class SerializableCriticalClass : CriticalClass, ISerializable
{
    public SerializableCriticalClass() { }

    private SerializableCriticalClass(SerializationInfo info, StreamingContext context) { }

    [SecurityCritical]
    public void GetObjectData(SerializationInfo info, StreamingContext context) { }
}

Un modo per risolvere il problema consiste nell'usare un tipo interno dall'assembly consumer. Qualsiasi tipo lo farà; ora definisco un attributo:

[AttributeUsage(AttributeTargets.All)]
internal class InternalTypeReferenceAttribute : Attribute
{
    public InternalTypeReferenceAttribute() { }
}

E gli attributi rilevanti applicati all'assieme:

[assembly: InternalsVisibleTo("UnitTest, PublicKey=<your public key>")]
[assembly: AllowPartiallyTrustedCallers]
[assembly: SecurityRules(SecurityRuleSet.Level2, SkipVerificationInFullTrust = true)]

Firma l'assembly, applica la chiave InternalsVisibleToall'attributo e preparati per il progetto di test:

UnitTest.dll (utilizza NUnit e ClassLibrary):

Per utilizzare il trucco interno, anche l'assembly di test dovrebbe essere firmato. Attributi dell'assieme:

// Just to make the tests security transparent by default. This helps to test the full trust behavior.
[assembly: AllowPartiallyTrustedCallers] 

// !!! Comment this line out and the partial trust test cases may fail for the fist time !!!
[assembly: InternalTypeReference]

Nota : l'attributo può essere applicato ovunque. Nel mio caso, mi ci sono voluti un paio di giorni per trovare un metodo in una classe di test casuale.

Nota 2 : se si eseguono tutti i metodi di test insieme, può succedere che i test vengano superati.

Lo scheletro della classe di prova:

[TestFixture]
public class SecurityCriticalAccessTest
{
    private partial class Sandbox : MarshalByRefObject
    {
    }

    private static AppDomain CreateSandboxDomain(params IPermission[] permissions)
    {
        var evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
        var permissionSet = GetPermissionSet(permissions);
        var setup = new AppDomainSetup
        {
            ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
        };

        var assemblies = AppDomain.CurrentDomain.GetAssemblies();
        var strongNames = new List<StrongName>();
        foreach (Assembly asm in assemblies)
        {
            AssemblyName asmName = asm.GetName();
            strongNames.Add(new StrongName(new StrongNamePublicKeyBlob(asmName.GetPublicKey()), asmName.Name, asmName.Version));
        }

        return AppDomain.CreateDomain("SandboxDomain", evidence, setup, permissionSet, strongNames.ToArray());
    }

    private static PermissionSet GetPermissionSet(IPermission[] permissions)
    {
        var evidence = new Evidence();
        evidence.AddHostEvidence(new Zone(SecurityZone.Internet));
        var result = SecurityManager.GetStandardSandbox(evidence);
        foreach (var permission in permissions)
            result.AddPermission(permission);
        return result;
    }
}

E vediamo i casi di test uno per uno

Caso 1: implementazione ISerializable

Lo stesso problema della domanda. Il test supera se

  • InternalTypeReferenceAttribute viene applicata
  • si prova a creare più volte sandbox (vedere il codice)
  • oppure, se tutti i casi di test vengono eseguiti contemporaneamente e questo non è il primo

Altrimenti, arriva l' Inheritance security rules violated while overriding member...eccezione totalmente inappropriata quando si crea un'istanza SerializableCriticalClass.

[Test]
[SecuritySafeCritical] // for Activator.CreateInstance
public void SerializableCriticalClass_PartialTrustAccess()
{
    var domain = CreateSandboxDomain(
        new SecurityPermission(SecurityPermissionFlag.SerializationFormatter), // BinaryFormatter
        new ReflectionPermission(ReflectionPermissionFlag.MemberAccess)); // Assert.IsFalse
    var handle = Activator.CreateInstance(domain, Assembly.GetExecutingAssembly().FullName, typeof(Sandbox).FullName);
    var sandbox = (Sandbox)handle.Unwrap();
    try
    {
        sandbox.TestSerializableCriticalClass();
        return;
    }
    catch (Exception e)
    {
        // without [InternalTypeReference] it may fail for the first time
        Console.WriteLine($"1st try failed: {e.Message}");
    }

    domain = CreateSandboxDomain(
        new SecurityPermission(SecurityPermissionFlag.SerializationFormatter), // BinaryFormatter
        new ReflectionPermission(ReflectionPermissionFlag.MemberAccess)); // Assert.IsFalse
    handle = Activator.CreateInstance(domain, Assembly.GetExecutingAssembly().FullName, typeof(Sandbox).FullName);
    sandbox = (Sandbox)handle.Unwrap();
    sandbox.TestSerializableCriticalClass();

    Assert.Inconclusive("Meh... succeeded only for the 2nd try");
}

private partial class Sandbox
{
    public void TestSerializableCriticalClass()
    {
        Assert.IsFalse(AppDomain.CurrentDomain.IsFullyTrusted);

        // ISerializable implementer can be created.
        // !!! May fail for the first try if the test does not use any internal type of the library. !!!
        var critical = new SerializableCriticalClass();

        // Critical method can be called via a safe method
        critical.SafeEntryForCriticalCode();

        // Critical method cannot be called directly by a transparent method
        Assert.Throws<MethodAccessException>(() => critical.CriticalCode());
        Assert.Throws<MethodAccessException>(() => critical.GetObjectData(null, new StreamingContext()));

        // BinaryFormatter calls the critical method via a safe route (SerializationFormatter permission is required, though)
        new BinaryFormatter().Serialize(new MemoryStream(), critical);
    }

}

Caso 2: classe normale con membri critici per la sicurezza

Il test viene superato nelle stesse condizioni del primo. Tuttavia, il problema qui è completamente diverso: un codice parzialmente attendibile può accedere direttamente a un membro critico per la sicurezza .

[Test]
[SecuritySafeCritical] // for Activator.CreateInstance
public void CriticalClass_PartialTrustAccess()
{
    var domain = CreateSandboxDomain(
        new ReflectionPermission(ReflectionPermissionFlag.MemberAccess), // Assert.IsFalse
        new EnvironmentPermission(PermissionState.Unrestricted)); // Assert.Throws (if fails)
    var handle = Activator.CreateInstance(domain, Assembly.GetExecutingAssembly().FullName, typeof(Sandbox).FullName);
    var sandbox = (Sandbox)handle.Unwrap();
    try
    {
        sandbox.TestCriticalClass();
        return;
    }
    catch (Exception e)
    {
        // without [InternalTypeReference] it may fail for the first time
        Console.WriteLine($"1st try failed: {e.Message}");
    }

    domain = CreateSandboxDomain(
        new ReflectionPermission(ReflectionPermissionFlag.MemberAccess)); // Assert.IsFalse
    handle = Activator.CreateInstance(domain, Assembly.GetExecutingAssembly().FullName, typeof(Sandbox).FullName);
    sandbox = (Sandbox)handle.Unwrap();
    sandbox.TestCriticalClass();

    Assert.Inconclusive("Meh... succeeded only for the 2nd try");
}

private partial class Sandbox
{
    public void TestCriticalClass()
    {
        Assert.IsFalse(AppDomain.CurrentDomain.IsFullyTrusted);

        // A type containing critical methods can be created
        var critical = new CriticalClass();

        // Critical method can be called via a safe method
        critical.SafeEntryForCriticalCode();

        // Critical method cannot be called directly by a transparent method
        // !!! May fail for the first time if the test does not use any internal type of the library. !!!
        // !!! Meaning, a partially trusted code has more right than a fully trusted one and is       !!!
        // !!! able to call security critical method directly.                                        !!!
        Assert.Throws<MethodAccessException>(() => critical.CriticalCode());
    }
}

Caso 3-4: versioni con attendibilità totale del caso 1-2

Per ragioni di completezza, ecco gli stessi casi di quelli sopra eseguiti in un dominio completamente attendibile. Se rimuovi [assembly: AllowPartiallyTrustedCallers]i test falliscono perché puoi accedere direttamente al codice critico (poiché i metodi non sono più trasparenti per impostazione predefinita).

[Test]
public void CriticalClass_FullTrustAccess()
{
    Assert.IsTrue(AppDomain.CurrentDomain.IsFullyTrusted);

    // A type containing critical methods can be created
    var critical = new CriticalClass();

    // Critical method cannot be called directly by a transparent method
    Assert.Throws<MethodAccessException>(() => critical.CriticalCode());

    // Critical method can be called via a safe method
    critical.SafeEntryForCriticalCode();
}

[Test]
public void SerializableCriticalClass_FullTrustAccess()
{
    Assert.IsTrue(AppDomain.CurrentDomain.IsFullyTrusted);

    // ISerializable implementer can be created
    var critical = new SerializableCriticalClass();

    // Critical method cannot be called directly by a transparent method (see also AllowPartiallyTrustedCallersAttribute)
    Assert.Throws<MethodAccessException>(() => critical.CriticalCode());
    Assert.Throws<MethodAccessException>(() => critical.GetObjectData(null, default(StreamingContext)));

    // Critical method can be called via a safe method
    critical.SafeEntryForCriticalCode();

    // BinaryFormatter calls the critical method via a safe route
    new BinaryFormatter().Serialize(new MemoryStream(), critical);
}

Epilogo:

Ovviamente, questo non risolverà il tuo problema con .NET Fiddle. Ma ora sarei molto sorpreso se non fosse un bug nel framework.

La domanda più grande per me ora è la parte citata nella risposta accettata. Come sono venute fuori queste sciocchezze? Il ISafeSerializationDatachiaramente non è una soluzione per qualsiasi cosa: esso viene utilizzato esclusivamente dalla base Exceptiondi classe e se si sottoscrive l' SerializeObjectStateevento (il motivo per cui non è che un metodo di override?), Allora lo stato sarà anche consumato dal Exception.GetObjectDataalla fine.

Il AllowPartiallyTrustedCallers/ SecurityCritical/ SecuritySafeCriticaltriumvirato di attributi è stato progettato esattamente per l'uso mostrato sopra. Mi sembra una totale assurdità che un codice parzialmente affidabile non possa nemmeno istanziare un tipo indipendentemente dal tentativo di utilizzare i suoi membri critici per la sicurezza. Ma è un'assurdità ancora più grande (un buco di sicurezza in realtà) che un codice parzialmente affidabile possa accedere direttamente a un metodo critico per la sicurezza (vedi caso 2 ) mentre ciò è vietato per metodi trasparenti anche da un dominio completamente affidabile.

Quindi, se il tuo progetto consumer è un test o un altro noto assemblaggio, il trucco interno può essere utilizzato perfettamente. Per .NET Fiddle e altri ambienti sandbox di vita reale l'unica soluzione è tornare indietro SecurityRuleSet.Level1fino a quando il problema non viene risolto da Microsoft.


Aggiornamento: Un biglietto di comunità di sviluppatori è stata creata per il rilascio.


2

Secondo MSDN vedi:

Come correggere le violazioni?

Per correggere una violazione di questa regola, rendere visibile e sovrascrivibile il metodo GetObjectData e assicurarsi che tutti i campi dell'istanza siano inclusi nel processo di serializzazione o contrassegnati esplicitamente con l' attributo NonSerializedAttribute .

L' esempio seguente risolve le due violazioni precedenti fornendo un'implementazione sostituibile di ISerializable.GetObjectData sulla classe Book e fornendo un'implementazione di ISerializable.GetObjectData sulla classe Library.

using System;
using System.Security.Permissions;
using System.Runtime.Serialization;

namespace Samples2
{
    [Serializable]
    public class Book : ISerializable
    {
        private readonly string _Title;

        public Book(string title)
        {
            if (title == null)
                throw new ArgumentNullException("title");

            _Title = title;
        }

        protected Book(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
                throw new ArgumentNullException("info");

            _Title = info.GetString("Title");
        }

        public string Title
        {
            get { return _Title; }
        }

        [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
        protected virtual void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("Title", _Title);
        }

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
                throw new ArgumentNullException("info");

            GetObjectData(info, context);
        }
    }

    [Serializable]
    public class LibraryBook : Book
    {
        private readonly DateTime _CheckedOut;

        public LibraryBook(string title, DateTime checkedOut)
            : base(title)
        {
            _CheckedOut = checkedOut;
        }

        protected LibraryBook(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
            _CheckedOut = info.GetDateTime("CheckedOut");
        }

        public DateTime CheckedOut
        {
            get { return _CheckedOut; }
        }

        [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
        protected override void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            base.GetObjectData(info, context);

            info.AddValue("CheckedOut", _CheckedOut);
        }
    }
}

2
L'articolo a cui ti sei collegato è per CA2240, che non è attivato: il codice non lo viola. È una struttura, quindi è effettivamente sigillata; non ha campi; si implementa in modo GetObjectDataesplicito, ma farlo in modo implicito non aiuta.
Jon Skeet

15
Certo, e grazie per aver provato, ma sto spiegando perché non funziona. (E come raccomandazione - per qualcosa di complicato come questo, in cui la domanda include un esempio verificabile, è una buona idea provare ad applicare la correzione suggerita e vedere se effettivamente aiuta.)
Jon Skeet
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.