Pobieranie danych mydła RAW z klienta odniesienia sieci Web działającego w ASP.net


95

Próbuję rozwiązać problem z klientem usługi sieciowej w moim bieżącym projekcie. Nie jestem pewien platformy serwera usług (najprawdopodobniej LAMP). Uważam, że po ich stronie ogrodzenia jest błąd, ponieważ wyeliminowałem potencjalne problemy z moim klientem. Klient jest standardowym proxy odwołań do sieci WWW typu ASMX generowanym automatycznie z usługi WSDL.

To, do czego potrzebuję, to wiadomości RAW SOAP (żądanie i odpowiedzi)

Jaki jest najlepszy sposób, aby to zrobić?

Odpowiedzi:


135

Wprowadziłem następujące zmiany w web.configcelu uzyskania koperty SOAP (żądanie / odpowiedź). Spowoduje to wyświetlenie wszystkich surowych informacji SOAP do pliku 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
To nie działa dla mnie w 02/2012 przy użyciu VS 2010. Czy ktoś zna bardziej aktualne rozwiązanie?
qxotk

2
To jest pomocne. Jednak w moim przypadku odpowiedzi są spakowane za pomocą Gzipa i wyciąganie ASCII lub kodów szesnastkowych z połączonego wyjścia jest uciążliwe. Czy ich alternatywne metody wyjściowe są binarne lub tylko tekstowe?

2
@Dediqated - możesz ustawić ścieżkę do pliku trace.log, dostosowując wartość atrybutu initializeData w powyższym przykładzie.
DougCouto

3
To już nie ma sensu, użyłem wireshark, aby zobaczyć surowe dane SOAP. Ale i tak dzięki!
Dediqated

7
Dobry. Ale dla mnie XML wygląda tak: System.Net Verbose: 0: [14088] 00000000: 3C 3F 78 6D 6C 20 76 65-72 73 69 6F 6E 3D 22 31: <? Xml version = "1 System.Net Verbose: 0: [14088] 00000010: 2E 30 22 20 65 6E 63 6F-64 69 6E 67 3D 22 75 74: .0 "encoding =" ut ....... Każdy pomysł, jak to sformatować lub mieć tylko dane xml .?
Habeeb

35

Można zaimplementować SoapExtension, który rejestruje pełne żądanie i odpowiedź w pliku dziennika. Następnie możesz włączyć rozszerzenie SoapExtension w pliku web.config, co ułatwia włączanie / wyłączanie do celów debugowania. Oto przykład, który znalazłem i zmodyfikowałem na własny użytek, w moim przypadku logowanie zostało wykonane przez log4net, ale możesz zastąpić metody dziennika własnymi.

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); }
    }
}

Następnie dodaj następującą sekcję do pliku web.config, gdzie YourNamespace i YourAssembly wskazują klasę i zestaw Twojego SoapExtension:

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

Spróbuję. Przyjrzałem się rozszerzeniom mydła, ale wydawało mi się, że było to bardziej do hostingu usługi. Zaznaczę, czy zadziała :)
Andrew Harry

Tak, to zadziała, aby rejestrować wywołania wykonane z aplikacji internetowej za pośrednictwem wygenerowanego serwera proxy.
John Lemp,

To jest droga, którą wybrałem, działa naprawdę dobrze i mogę użyć xpath do maskowania wszelkich poufnych danych przed ich zarejestrowaniem.
Dave Baghdanov

Musiałem dodać nagłówki do żądania mydła na kliencie. To mi pomogło. rhyous.com/2015/04/29/…
Rhyous

Jestem również nowy w C # i nie jestem pewien, co umieścić dla nazwy zestawu. Wydaje się, że rozszerzenie nie działa.
Collin Anderson

28

Nie wiem, po co tyle zamieszania z web.config lub klasą serializatora. Poniższy kod zadziałał dla mnie:

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

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

17
Skąd się myEnvelopebierze?
ProfK

6
Nie rozumiem, dlaczego ta odpowiedź nie ma więcej pozytywnych głosów, prostych i na temat! Dobra robota!
Batista

1
myEnvalope byłby obiektem, który wysyłasz za pośrednictwem usługi sieciowej. Nie mogłem sprawić, aby to działało, jak opisano (szczególnie funkcja textWriter.tostring), ale działało wystarczająco dobrze do moich celów w trybie debugowania, ponieważ po wywołaniu serialize możesz zobaczyć surowy plik XML. Bardzo doceniam tę odpowiedź, ponieważ niektórzy z innych pracowali z mieszanymi wynikami (na przykład rejestrowanie przy użyciu pliku web.config zwróciło tylko część pliku xml i również nie było łatwe do przeanalizowania).
user2366842

@Bimmerbound: czy to zadziała z bezpiecznymi wiadomościami mydlanymi przesyłanymi przez zaszyfrowaną sieć VPN?
Nasz człowiek w bananach

6
Ta odpowiedź nie tworzy koperty mydlanej, po prostu serializuje ją do xml. Jak otrzymać prośbę o kopertę z mydłem?
Ali Karaca

21

Wypróbuj Fiddler2 , który pozwoli ci sprawdzić żądania i odpowiedzi. Warto zauważyć, że Fiddler działa zarówno z ruchem http, jak i https.


Jest to usługa zaszyfrowana za pomocą protokołu https
Andrew Harry,

8
Fiddler może odszyfrować ruch https
Aaron Fischer

1
Uważam, że to rozwiązanie jest lepsze niż dodawanie śledzenia do pliku web / app.config. Dzięki!
andyuk

1
Ale użycie system.diagnostics w pliku konfiguracyjnym nie wymaga zainstalowania aplikacji innej firmy
Azat

1
@AaronFischer: Czy Fiddler działałby z wiadomościami mydlanymi wysyłanymi przez zaszyfrowaną sieć VPN?
Nasz człowiek w bananach

6

Wygląda na to, że rozwiązanie Tima Cartera nie działa, jeśli wywołanie odwołania internetowego zgłasza wyjątek. Próbowałem uzyskać odpowiedź nieprzetworzoną, aby móc ją zbadać (w kodzie) w programie obsługi błędów po wyrzuceniu wyjątku. Jednak stwierdzam, że dziennik odpowiedzi napisany przez metodę Tima jest pusty, gdy wywołanie zgłasza wyjątek. Nie rozumiem do końca kodu, ale wydaje się, że metoda Tima przerywa proces po punkcie, w którym .Net już unieważnił i odrzucił odpowiedź sieciową.

Pracuję z klientem, który ręcznie opracowuje usługę internetową z kodowaniem niskiego poziomu. W tym momencie dodają swoje własne wewnętrzne komunikaty o błędach procesu jako komunikaty w formacie HTML do odpowiedzi PRZED odpowiedzią w formacie SOAP. Oczywiście, pojawia się tutaj odniesienie do automagic .Net. Gdybym mógł uzyskać nieprzetworzoną odpowiedź HTTP po wyrzuceniu wyjątku, mógłbym poszukać i przeanalizować każdą odpowiedź SOAP w mieszanej zwrotnej odpowiedzi HTTP i wiedzieć, że otrzymali moje dane w porządku, czy nie.

Później ...

Oto rozwiązanie, które działa, nawet po wykonaniu (zwróć uwagę, że jestem dopiero po odpowiedzi - również mogę uzyskać żądanie):

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;
        }
    }
}

Oto jak możesz to skonfigurować w pliku konfiguracyjnym:

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

„TestCallWebService” należy zastąpić nazwą biblioteki (była to nazwa aplikacji konsoli testowej, w której pracowałem).

Naprawdę nie powinieneś iść do ChainStream; powinieneś być w stanie zrobić to prościej z ProcessMessage, ponieważ:

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
    }
}

Jeśli spojrzysz na SoapMessage.Stream, powinien to być strumień tylko do odczytu, którego możesz użyć do sprawdzenia danych w tym momencie. Jest to błąd, ponieważ jeśli odczytujesz strumień, kolejne przetwarzanie bomb bez błędów znalezionych danych (strumień był na końcu) i nie możesz zresetować pozycji do początku.

Co ciekawe, jeśli wykonasz obie metody, ChainStream i ProcessMessage, metoda ProcessMessage będzie działać, ponieważ zmieniono typ strumienia z ConnectStream na MemoryStream w ChainStream, a MemoryStream zezwala na operacje wyszukiwania. (Próbowałem rzucić ConnectStream na MemoryStream - nie było dozwolone.)

Więc ..... Microsoft powinien albo zezwolić na operacje wyszukiwania na typie ChainStream, albo uczynić SoapMessage.Stream prawdziwie kopią tylko do odczytu, tak jak powinna. (Napisz kongresmana itp.)

Jeszcze jeden punkt. Po utworzeniu sposobu na odzyskanie nieprzetworzonej odpowiedzi HTTP po wyjątku nadal nie otrzymałem pełnej odpowiedzi (określonej przez sniffer HTTP). Dzieje się tak, ponieważ gdy programistyczna usługa sieciowa dodawała komunikaty o błędach HTML na początku odpowiedzi, nie dostosowywała nagłówka Content-Length, więc wartość Content-Length była mniejsza niż rozmiar rzeczywistej treści odpowiedzi. Wszystko, co otrzymałem, to liczba znaków wartości Content-Length - reszty brakowało. Oczywiście, kiedy .Net czyta strumień odpowiedzi, po prostu czyta liczbę znaków Content-Length i nie pozwala na to, aby wartość Content-Length była błędna. Tak powinno być; ale jeśli wartość nagłówka Content-Length jest nieprawidłowa, jedynym sposobem, w jaki kiedykolwiek otrzymasz całą treść odpowiedzi, jest sniffer HTTP (używam analizatora HTTP zhttp://www.ieinspector.com ).


2

Wolałbym, aby platforma wykonywała rejestrowanie za Ciebie, przechodząc do strumienia rejestrowania, który rejestruje, gdy struktura przetwarza ten strumień. Poniższe nie są tak przejrzyste, jak bym chciał, ponieważ nie możesz zdecydować między żądaniem a odpowiedzią w metodzie ChainStream. Oto jak sobie z tym radzę. Z podziękowaniami dla Jona Hanny za nadrzędny pomysł na transmisję

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 to zadziałało, jedyne, co usunąłem, to część klienta, która daje mi nazwę z dwukropkiem, powodując wyjątek. Więc kiedy usunę tę linię, działa dobrze dla tych, którzy chcą mieć jeden plik na każde żądanie / odpowiedź. Jedyną rzeczą do dodania jest coś oczywistego, jeśli przeczytasz inne odpowiedzi, a to że musisz dodać węzeł web.config, aby wskazać soapExtension. Dzięki!
netadictos

1

Oto uproszczona wersja najlepszej odpowiedzi. Dodaj to do <configuration>elementu swojego web.configlub App.configpliku. Utworzy trace.logplik w bin/Debugfolderze twojego projektu . Lub możesz określić bezwzględną ścieżkę do pliku dziennika przy użyciu initializeDataatrybutu.

  <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>

Ostrzega, że atrybuty maxdatasizei tracemodenie są dozwolone, ale zwiększają ilość danych, które mogą być rejestrowane, i unikają rejestrowania wszystkiego w postaci szesnastkowej.


0

Nie określiłeś języka, którego używasz, ale zakładając C # / .NET, możesz użyć rozszerzeń SOAP .

W przeciwnym razie użyj sniffera, takiego jak Wireshark


1
Tak, wypróbowałem wireshark, może pokazać mi tylko informacje nagłówka, zawartość mydła jest zaszyfrowana.
Andrew Harry,

Może dlatego, że używasz protokołu HTTPS. Rozszerzenia SOAP, o ile pamiętam, działają na wyższym poziomie, więc powinieneś być w stanie zobaczyć dane po ich odszyfrowaniu.
rbrayb

2
Użyj Fiddlera zamiast Wiresharka, odszyfrowuje https po wyjęciu z pudełka.
Rodrigo Strauss,

-1

Zdaję sobie sprawę, że jestem dość spóźniony na imprezę, a ponieważ język nie został określony, oto rozwiązanie VB.NET oparte na odpowiedzi Bimmerbound, na wypadek gdyby ktoś się na to natknął i potrzebował rozwiązania. Uwaga: musisz mieć odniesienie do klasy stringbuilder w swoim projekcie, jeśli jeszcze tego nie zrobiłeś.

 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

Po prostu wywołaj funkcję, a zwróci ona ciąg znaków z zserializowanym kodem XML obiektu, który próbujesz przekazać do usługi sieciowej (realistycznie powinno to działać również dla każdego obiektu, który chcesz do niego rzucić).

Na marginesie, wywołanie replace w funkcji przed zwróceniem xml ma na celu usunięcie znaków vbCrLf z wyniku. Mój miał ich kilka w wygenerowanym pliku XML, ale oczywiście będzie się to różnić w zależności od tego, co próbujesz serializować i myślę, że mogą zostać usunięte podczas wysyłania obiektu do usługi internetowej.


Ktokolwiek uderzył mnie negatywnym głosem, daj mi znać, czego mi brakuje / co można poprawić.
user2366842
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.