ASP.net'te çalışan bir Web Referans İstemcisinden RAW Sabun Verilerini Alma


95

Mevcut projemde bir web hizmeti istemcisini çekerken sorun çıkarmaya çalışıyorum. Servis Sunucusunun platformundan emin değilim (Büyük olasılıkla LAMP). Müşterimle olası sorunları ortadan kaldırdığım için çitin kendi tarafında bir hata olduğuna inanıyorum. İstemci, WSDL hizmetinden otomatik olarak oluşturulan standart bir ASMX türü web referans proxy'sidir.

Ulaşmam gereken şey RAW SOAP Mesajları (İstek ve Yanıtlar)

Bunu yapmanın en iyi yolu nedir?

Yanıtlar:


135

web.configSOAP (İstek / Yanıt) Zarfını almak için aşağıdaki değişiklikleri yaptım . Bu, tüm ham SOAP bilgilerini dosyaya çıkaracaktır trace.log.

<system.diagnostics>
  <trace autoflush="true"/>
  <sources>
    <source name="System.Net" maxdatasize="1024">
      <listeners>
        <add name="TraceFile"/>
      </listeners>
    </source>
    <source name="System.Net.Sockets" maxdatasize="1024">
      <listeners>
        <add name="TraceFile"/>
      </listeners>
    </source>
  </sources>
  <sharedListeners>
    <add name="TraceFile" type="System.Diagnostics.TextWriterTraceListener"
      initializeData="trace.log"/>
  </sharedListeners>
  <switches>
    <add name="System.Net" value="Verbose"/>
    <add name="System.Net.Sockets" value="Verbose"/>
  </switches>
</system.diagnostics>

2
Bu benim için 02 / 2012'de VS 2010 kullanarak çalışmıyor. Daha güncel bir çözüm bilen var mı?
qxotk

2
Bu faydalıdır. Bununla birlikte, benim durumumda yanıtlar Gzip'lidir ve ASCII veya onaltılı kodları birleşik çıktıdan çıkarmak bir acıdır. Alternatif çıktı yöntemleri ikili mi yoksa yalnızca metin mi?

2
@Dediqated - yukarıdaki örnekte initializeData özniteliğinin değerini ayarlayarak trace.log dosyasının yolunu ayarlayabilirsiniz.
DougCouto

3
Artık hiçbir faydası yok, ham SOAP verilerini görmek için wireshark kullandım. Yinede teşekkürler!
2013

7
İyi. Ama benim için XML şu şekildedir: System.Net Ayrıntılı: 0: [14088] 00000000: 3C 3F 78 6D 6C 20 76 65-72 73 69 6F 6E 3D 22 31: <? Xml version = "1 System.Net Ayrıntılı: 0: [14088] 00000010: 2E 30 22 20 65 6E 63 6F-64 69 6E 67 3D 22 75 74: .0 "encoding =" ut ....... Nasıl formatlanacağı hakkında herhangi bir fikir veya sadece xml verisine sahip olmak .?
Habeeb

35

Günlük dosyasına tam istek ve yanıtı kaydeden bir SoapExtension uygulayabilirsiniz. Daha sonra web.config dosyasında SoapExtension özelliğini etkinleştirebilirsiniz, bu da hata ayıklama amacıyla açıp kapatmayı kolaylaştırır. İşte kendi kullanımım için bulduğum ve değiştirdiğim bir örnek, benim durumumda günlük kaydı log4net tarafından yapıldı, ancak günlük yöntemlerini kendi yönteminizle değiştirebilirsiniz.

public class SoapLoggerExtension : SoapExtension
{
    private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
    private Stream oldStream;
    private Stream newStream;

    public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
    {
        return null;
    }

    public override object GetInitializer(Type serviceType)
    {
        return null;
    }

    public override void Initialize(object initializer)
    {

    }

    public override System.IO.Stream ChainStream(System.IO.Stream stream)
    {
        oldStream = stream;
        newStream = new MemoryStream();
        return newStream;
    }

    public override void ProcessMessage(SoapMessage message)
    {

        switch (message.Stage)
        {
            case SoapMessageStage.BeforeSerialize:
                break;
            case SoapMessageStage.AfterSerialize:
                Log(message, "AfterSerialize");
                    CopyStream(newStream, oldStream);
                    newStream.Position = 0;
                break;
                case SoapMessageStage.BeforeDeserialize:
                    CopyStream(oldStream, newStream);
                    Log(message, "BeforeDeserialize");
                break;
            case SoapMessageStage.AfterDeserialize:
                break;
        }
    }

    public void Log(SoapMessage message, string stage)
    {

        newStream.Position = 0;
        string contents = (message is SoapServerMessage) ? "SoapRequest " : "SoapResponse ";
        contents += stage + ";";

        StreamReader reader = new StreamReader(newStream);

        contents += reader.ReadToEnd();

        newStream.Position = 0;

        log.Debug(contents);
    }

    void ReturnStream()
    {
        CopyAndReverse(newStream, oldStream);
    }

    void ReceiveStream()
    {
        CopyAndReverse(newStream, oldStream);
    }

    public void ReverseIncomingStream()
    {
        ReverseStream(newStream);
    }

    public void ReverseOutgoingStream()
    {
        ReverseStream(newStream);
    }

    public void ReverseStream(Stream stream)
    {
        TextReader tr = new StreamReader(stream);
        string str = tr.ReadToEnd();
        char[] data = str.ToCharArray();
        Array.Reverse(data);
        string strReversed = new string(data);

        TextWriter tw = new StreamWriter(stream);
        stream.Position = 0;
        tw.Write(strReversed);
        tw.Flush();
    }
    void CopyAndReverse(Stream from, Stream to)
    {
        TextReader tr = new StreamReader(from);
        TextWriter tw = new StreamWriter(to);

        string str = tr.ReadToEnd();
        char[] data = str.ToCharArray();
        Array.Reverse(data);
        string strReversed = new string(data);
        tw.Write(strReversed);
        tw.Flush();
    }

    private void CopyStream(Stream fromStream, Stream toStream)
    {
        try
        {
            StreamReader sr = new StreamReader(fromStream);
            StreamWriter sw = new StreamWriter(toStream);
            sw.WriteLine(sr.ReadToEnd());
            sw.Flush();
        }
        catch (Exception ex)
        {
            string message = String.Format("CopyStream failed because: {0}", ex.Message);
            log.Error(message, ex);
        }
    }
}

[AttributeUsage(AttributeTargets.Method)]
public class SoapLoggerExtensionAttribute : SoapExtensionAttribute
{
    private int priority = 1; 

    public override int Priority
    {
        get { return priority; }
        set { priority = value; }
    }

    public override System.Type ExtensionType
    {
        get { return typeof (SoapLoggerExtension); }
    }
}

Ardından, aşağıdaki bölümü web.config dosyanıza eklersiniz; burada Ad Alanınız ve Montajınız, SoapExtension öğenizin sınıfına ve derlemesine işaret eder:

<webServices>
  <soapExtensionTypes>
    <add type="YourNamespace.SoapLoggerExtension, YourAssembly" 
       priority="1" group="0" />
  </soapExtensionTypes>
</webServices>

Bi şans tanıcam. Sabun uzantılarına bakmıştım ama bana daha çok hizmeti barındırmak için gelmiş gibi geldi. İşe yararsa size onay vereceğim :)
Andrew Harry

Evet, bu, oluşturulan proxy aracılığıyla web uygulamanızdan yapılan aramaları günlüğe kaydetmek için çalışacaktır.
John Lemp

Bu benim gittiğim rota, gerçekten iyi çalışıyor ve xpath'i herhangi bir hassas veriyi kaydetmeden önce maskelemek için kullanabilirim.
Dave Baghdanov

İstemcideki sabun isteğine başlıklar eklemem gerekiyordu. Bu bana yardımcı oldu. rhyous.com/2015/04/29/…
Rhyous

Ayrıca c # konusunda yeniyim ve Assembly adı için ne yazacağımı bilmiyorum. Uzantı çalışmıyor gibi görünüyor.
Collin Anderson

28

Web.config veya bir serileştirici sınıfıyla neden bu kadar yaygara koptuğundan emin değilim. Aşağıdaki kod benim için çalıştı:

XmlSerializer xmlSerializer = new XmlSerializer(myEnvelope.GetType());

using (StringWriter textWriter = new StringWriter())
{
    xmlSerializer.Serialize(textWriter, myEnvelope);
    return textWriter.ToString();
}

17
Nereden myEnvelopegeliyor?
ProfK

6
Bu cevabın neden daha fazla olumlu oyu olmadığını anlamıyorum, doğrudan ve doğrudan! Aferin!
Batista

1
myEnvalope, web hizmeti aracılığıyla gönderdiğiniz nesne olacaktır. Bunu açıklandığı gibi tam olarak çalıştıramadım (özellikle textWriter.tostring işlevi), ancak serileştirmeyi çağırdıktan sonra ham xml'yi görebildiğiniz için hata ayıklama kipinde amaçlarım için yeterince iyi çalıştı. Bazıları karışık sonuçlarla çalıştığı için bu yanıtı çok takdir ediyorum (örneğin web.config kullanarak günlüğe kaydetme, xml'nin yalnızca bir kısmını döndürdü ve ayrıştırılması da çok kolay değildi).
user2366842

@Bimmerbound: Bu, şifreli bir VPN üzerinden gönderilen güvenli sabun mesajlarıyla çalışır mı?
Our Man in Bananas

6
Bu cevap sabun zarfı üretmez, sadece xml'ye serileştirir. Sabun zarf talebi nasıl alınır?
Ali Karaca

21

Fiddler2'yi deneyin , istekleri ve yanıtları incelemenizi sağlar. Fiddler'ın hem http hem de https trafiğiyle çalıştığını belirtmekte fayda var.


Bu bir https şifreli hizmettir
Andrew Harry

8
Fiddler https trafiğinin şifresini çözebilir
Aaron Fischer

1
Bu çözümü web / app.config dosyasına izleme eklemekten daha iyi buldum. Teşekkürler!
andyuk

1
Ancak yapılandırma dosyasında system.diagnostics kullanmak 3. taraf uygulamasının yüklenmesini gerektirmez
Azat

1
@AaronFischer: Fiddler, şifreli bir VPN üzerinden gönderilen sabun mesajlarıyla çalışır mı?
Our Man in Bananas

6

Görünüşe göre, web referansına yapılan çağrı bir istisna atarsa ​​Tim Carter'ın çözümü çalışmıyor. Ham web rezonansına ulaşmaya çalışıyorum, böylece istisna atıldıktan sonra hata işleyicide (kodda) inceleyebilirim. Ancak, çağrı bir istisna attığında Tim'in yöntemi tarafından yazılan yanıt günlüğünün boş olduğunu buluyorum. Kodu tam olarak anlamıyorum, ancak Tim'in yöntemi .Net'in zaten geçersiz hale getirdiği ve web yanıtını attığı noktadan sonra süreci kesiyor gibi görünüyor.

Düşük seviyeli kodlamayla manuel olarak bir web hizmeti geliştiren bir müşteriyle çalışıyorum. Bu noktada, SOAP formatlı yanıttan ÖNCE yanıta HTML formatlı mesajlar olarak kendi dahili işlem hata mesajlarını ekliyorlar. Tabii ki, automagic .Net web referansı bu konuda patladı. Bir istisna atıldıktan sonra ham HTTP yanıtına ulaşabilirsem, karışık dönen HTTP yanıtı içinde herhangi bir SOAP yanıtını arayabilir ve ayrıştırabilir ve verilerimi tamam alıp almadıklarını bilebilirdim.

Sonra ...

İşte bir yürütmeden sonra bile işe yarayan bir çözüm (yalnızca yanıtın peşindeyim - isteği de alabilirim):

namespace ChuckBevitt
{
    class GetRawResponseSoapExtension : SoapExtension
    {
        //must override these three methods
        public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
        {
            return null;
        }
        public override object GetInitializer(Type serviceType)
        {
            return null;
        }
        public override void Initialize(object initializer)
        {
        }

        private bool IsResponse = false;

        public override void ProcessMessage(SoapMessage message)
        {
            //Note that ProcessMessage gets called AFTER ChainStream.
            //That's why I'm looking for AfterSerialize, rather than BeforeDeserialize
            if (message.Stage == SoapMessageStage.AfterSerialize)
                IsResponse = true;
            else
                IsResponse = false;
        }

        public override Stream ChainStream(Stream stream)
        {
            if (IsResponse)
            {
                StreamReader sr = new StreamReader(stream);
                string response = sr.ReadToEnd();
                sr.Close();
                sr.Dispose();

                File.WriteAllText(@"C:\test.txt", response);

                byte[] ResponseBytes = Encoding.ASCII.GetBytes(response);
                MemoryStream ms = new MemoryStream(ResponseBytes);
                return ms;

            }
            else
                return stream;
        }
    }
}

Yapılandırma dosyasında bunu şu şekilde yapılandırabilirsiniz:

<configuration>
     ...
  <system.web>
    <webServices>
      <soapExtensionTypes>
        <add type="ChuckBevitt.GetRawResponseSoapExtension, TestCallWebService"
           priority="1" group="0" />
      </soapExtensionTypes>
    </webServices>
  </system.web>
</configuration>

"TestCallWebService", kitaplığın adı ile değiştirilmelidir (bu, çalıştığım test konsolu uygulamasının adıdır).

Gerçekten ChainStream'e gitmeniz gerekmemelidir; şu şekilde ProcessMessage'dan daha basit bir şekilde yapabilmelisiniz:

public override void ProcessMessage(SoapMessage message)
{
    if (message.Stage == SoapMessageStage.BeforeDeserialize)
    {
        StreamReader sr = new StreamReader(message.Stream);
        File.WriteAllText(@"C:\test.txt", sr.ReadToEnd());
        message.Stream.Position = 0; //Will blow up 'cause type of stream ("ConnectStream") doesn't alow seek so can't reset position
    }
}

SoapMessage.Stream'e bakarsanız, bu noktada verileri incelemek için kullanabileceğiniz salt okunur bir akış olması gerekir. Bu bir hata çünkü eğer akışı okursanız, veri bulunmayan sonraki işlem bombaları hatası (akış sondaydı) ve konumu başa sıfırlayamazsınız.

İlginç bir şekilde, her iki yöntemi de, ChainStream ve ProcessMessage yollarını yaparsanız, ProcessMessage yöntemi, akış türünü ChainStream'de ConnectStream'den MemoryStream'e değiştirdiğiniz için çalışır ve MemoryStream arama işlemlerine izin verir. (ConnectStream'i MemoryStream'e yayınlamayı denedim - izin verilmedi.)

Yani ..... Microsoft, ChainStream türünde arama işlemlerine izin vermeli veya SoapMessage.Stream'i olması gerektiği gibi gerçekten salt okunur bir kopya yapmalıdır. (Kongre üyenize vb. Yazın ...)

Bir nokta daha. Bir istisnadan sonra ham HTTP yanıtını almanın bir yolunu oluşturduktan sonra, yine de tam yanıtı alamadım (bir HTTP algılayıcısı tarafından belirlendiği gibi). Bunun nedeni, geliştirme web hizmetinin HTML hata mesajlarını yanıtın başlangıcına eklediğinde Content-Length başlığını ayarlamamasıydı, bu nedenle Content-Length değeri gerçek yanıt gövdesinin boyutundan daha küçüktü. Elimdeki tek şey İçerik Uzunluğu değerindeki karakter sayısıydı - gerisi eksikti. Açıkçası, .Net yanıt akışını okuduğunda, yalnızca İçerik Uzunluğu sayısını okur ve İçerik Uzunluğu değerinin muhtemelen yanlış olmasına izin vermez. Bu olması gerektiği gibi; ancak Content-Length üstbilgisi değeri yanlışsa, yanıt gövdesinin tamamını almanın tek yolu bir HTTP algılayıcısı kullanmaktır (http://www.ieinspector.com ).


2

Akışın temelini oluşturan çerçeve süreçleri olarak günlüğe kaydedilen bir günlük akışına bağlanarak çerçevenin sizin için günlük kaydını yapmasını tercih ederim. ChainStream yönteminde istek ve yanıt arasında karar veremediğiniz için aşağıdakiler istediğim kadar temiz değil. Aşağıda bunu nasıl hallettiğim. Bir akış fikrini geçersiz kılan Jon Hanna'ya teşekkürler

public class LoggerSoapExtension : SoapExtension
{
    private static readonly string LOG_DIRECTORY = ConfigurationManager.AppSettings["LOG_DIRECTORY"];
    private LogStream _logger;

    public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
    {
        return null;
    }
    public override object GetInitializer(Type serviceType)
    {
        return null;
    }
    public override void Initialize(object initializer)
    {
    }
    public override System.IO.Stream ChainStream(System.IO.Stream stream)
    {
        _logger = new LogStream(stream);
        return _logger;
    }
    public override void ProcessMessage(SoapMessage message)
    {
        if (LOG_DIRECTORY != null)
        {
            switch (message.Stage)
            {
                case SoapMessageStage.BeforeSerialize:
                    _logger.Type = "request";
                    break;
                case SoapMessageStage.AfterSerialize:
                    break;
                case SoapMessageStage.BeforeDeserialize:
                    _logger.Type = "response";
                    break;
                case SoapMessageStage.AfterDeserialize:
                    break;
            }
        }
    }
    internal class LogStream : Stream
    {
        private Stream _source;
        private Stream _log;
        private bool _logSetup;
        private string _type;

        public LogStream(Stream source)
        {
            _source = source;
        }
        internal string Type
        {
            set { _type = value; }
        }
        private Stream Logger
        {
            get
            {
                if (!_logSetup)
                {
                    if (LOG_DIRECTORY != null)
                    {
                        try
                        {
                            DateTime now = DateTime.Now;
                            string folder = LOG_DIRECTORY + now.ToString("yyyyMMdd");
                            string subfolder = folder + "\\" + now.ToString("HH");
                            string client = System.Web.HttpContext.Current != null && System.Web.HttpContext.Current.Request != null && System.Web.HttpContext.Current.Request.UserHostAddress != null ? System.Web.HttpContext.Current.Request.UserHostAddress : string.Empty;
                            string ticks = now.ToString("yyyyMMdd'T'HHmmss.fffffff");
                            if (!Directory.Exists(folder))
                                Directory.CreateDirectory(folder);
                            if (!Directory.Exists(subfolder))
                                Directory.CreateDirectory(subfolder);
                            _log = new FileStream(new System.Text.StringBuilder(subfolder).Append('\\').Append(client).Append('_').Append(ticks).Append('_').Append(_type).Append(".xml").ToString(), FileMode.Create);
                        }
                        catch
                        {
                            _log = null;
                        }
                    }
                    _logSetup = true;
                }
                return _log;
            }
        }
        public override bool CanRead
        {
            get
            {
                return _source.CanRead;
            }
        }
        public override bool CanSeek
        {
            get
            {
                return _source.CanSeek;
            }
        }

        public override bool CanWrite
        {
            get
            {
                return _source.CanWrite;
            }
        }

        public override long Length
        {
            get
            {
                return _source.Length;
            }
        }

        public override long Position
        {
            get
            {
                return _source.Position;
            }
            set
            {
                _source.Position = value;
            }
        }

        public override void Flush()
        {
            _source.Flush();
            if (Logger != null)
                Logger.Flush();
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            return _source.Seek(offset, origin);
        }

        public override void SetLength(long value)
        {
            _source.SetLength(value);
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            count = _source.Read(buffer, offset, count);
            if (Logger != null)
                Logger.Write(buffer, offset, count);
            return count;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            _source.Write(buffer, offset, count);
            if (Logger != null)
                Logger.Write(buffer, offset, count);
        }
        public override int ReadByte()
        {
            int ret = _source.ReadByte();
            if (ret != -1 && Logger != null)
                Logger.WriteByte((byte)ret);
            return ret;
        }
        public override void Close()
        {
            _source.Close();
            if (Logger != null)
                Logger.Close();
            base.Close();
        }
        public override int ReadTimeout
        {
            get { return _source.ReadTimeout; }
            set { _source.ReadTimeout = value; }
        }
        public override int WriteTimeout
        {
            get { return _source.WriteTimeout; }
            set { _source.WriteTimeout = value; }
        }
    }
}
[AttributeUsage(AttributeTargets.Method)]
public class LoggerSoapExtensionAttribute : SoapExtensionAttribute
{
    private int priority = 1;
    public override int Priority
    {
        get
        {
            return priority;
        }
        set
        {
            priority = value;
        }
    }
    public override System.Type ExtensionType
    {
        get
        {
            return typeof(LoggerSoapExtension);
        }
    }
}

1
@TimCarter benim için çalıştı, sildiğim tek şey müşteri kısmı, bana iki nokta üst üste ismiyle bir istisnaya neden olan bir isim veriyor. Bu satırı bir kez sildiğimde, her istek / yanıt için bir dosyaya sahip olmak isteyenler için iyi çalışıyor. Eklenecek tek şey, diğer yanıtları okursanız bariz bir şeydir ve soapExtension'ı belirtmek için web.config düğümünü eklemeniz gerekir. Teşekkürler!
netadictos

1

İşte en iyi cevabın basitleştirilmiş bir versiyonu. Bunu veya dosyanızın <configuration>öğesine ekleyin . Projenizin klasöründe bir dosya oluşturacaktır . Ya da, niteliği kullanarak günlük dosyası için mutlak bir yol belirtebilirsiniz .web.configApp.configtrace.logbin/DebuginitializeData

  <system.diagnostics>
    <trace autoflush="true"/>
    <sources>
      <source name="System.Net" maxdatasize="9999" tracemode="protocolonly">
        <listeners>
          <add name="TraceFile" type="System.Diagnostics.TextWriterTraceListener" initializeData="trace.log"/>
        </listeners>
      </source>
    </sources>
    <switches>
      <add name="System.Net" value="Verbose"/>
    </switches>
  </system.diagnostics>

maxdatasizeVe tracemodeözniteliklerine izin verilmediği konusunda uyarır , ancak günlüğe kaydedilebilecek veri miktarını artırır ve her şeyi onaltılı olarak günlüğe kaydetmekten kaçınırlar.


0

Hangi dili kullandığınızı belirtmediniz, ancak C # / .NET olduğunu varsayarak SOAP uzantılarını kullanabileceğinizi varsayın .

Aksi takdirde, Wireshark gibi bir algılayıcı kullanın


1
Evet, wireshark denendi, bana sadece başlık bilgisini gösterebilir, sabun içeriği şifrelenmiştir.
Andrew Harry

Bu belki https kullandığınız içindir. SABUN uzantıları daha yüksek seviyede çalıştığım için, şifresi çözüldükten sonra verileri görebilmeniz gerekir.
rbrayb

2
Wireshark yerine Fiddler kullanın, https'yi kutudan çıkarır.
Rodrigo Strauss

-1

Partiye oldukça geç kaldığımın farkındayım ve dil aslında belirtilmediğinden, burada birilerinin buna rastlaması ve bir çözüme ihtiyaç duyması durumunda Bimmerbound'un cevabına dayanan bir VB.NET çözümü var. Not: Henüz yapmadıysanız, projenizdeki stringbuilder sınıfına bir referansınız olması gerekir.

 Shared Function returnSerializedXML(ByVal obj As Object) As String
    Dim xmlSerializer As New System.Xml.Serialization.XmlSerializer(obj.GetType())
    Dim xmlSb As New StringBuilder
    Using textWriter As New IO.StringWriter(xmlSb)
        xmlSerializer.Serialize(textWriter, obj)
    End Using


    returnSerializedXML = xmlSb.ToString().Replace(vbCrLf, "")

End Function

Sadece işlevi çağırın ve web hizmetine iletmeye çalıştığınız nesnenin serileştirilmiş xml'sini içeren bir dize döndürür (gerçekçi olarak, bu, ona atmayı düşündüğünüz herhangi bir nesne için de işe yaramalıdır).

Bir yan not olarak, xml döndürülmeden önce işlevdeki değiştirme çağrısı vbCrLf karakterlerini çıktıdan çıkarmaktır. Benimkinde üretilen xml içinde bunlardan bir sürü vardı, ancak bu açık bir şekilde serileştirmeye çalıştığınız şeye bağlı olarak değişecek ve web hizmetine gönderilen nesne sırasında çıkarılabileceklerini düşünüyorum.


Olumsuz oyla bana tokat atan kişiye, neyi kaçırdığımı / nelerin iyileştirilebileceğini bana bildirmekten lütfen çekinmeyin.
user2366842
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.