Unterstützt OkHttp das Akzeptieren selbstsignierter SSL-Zertifikate?


74

Ich arbeite für einen Kunden, der einen Server mit selbstsigniertem SSL-Zertifikat hat.

Ich verwende Retrofit + CustomClient mit dem umhüllten OkHttp-Client:

RestAdapter restAdapter = new RestAdapter.Builder().setEndpoint(Config.BASE_URL + Config.API_VERSION)
    .setClient(new CustomClient(new OkClient(), context))
    .build();

Unterstützt OkHttp standardmäßig das Aufrufen eines selbstsignierten SSL-Zertifizierungsservers?

Apropos. Welcher Client verwendet standardmäßig Retrofit? Ich dachte, es wäre OkHttp, aber als ich ein bisschen mehr recherchierte, wurde mir klar, dass ich OkHttp-Abhängigkeiten importieren musste


In Bezug auf den zweiten Teil verwendet Retrofit OkHttp, wenn Ihr Klassenpfad verfügbar ist. Weitere Informationen finden Sie defaultClient()in Platform.java .
Hassan Ibraheem

Siehe: [So richten Sie Retrofit2 mit einem benutzerdefinierten SSL-Zertifikat ein] ( adiyatmubarak.wordpress.com/tag/… )
Guido Mocha

Antworten:


91

Ja tut es.

Mit Retrofit können Sie Ihren benutzerdefinierten HTTP-Client einstellen, der Ihren Anforderungen entsprechend konfiguriert ist.

Für selbstsignierte SSL-Zertifikate wird hier diskutiert . Der Link enthält Codebeispiele zum Hinzufügen selbstsignierter SLL zu Android DefaultHttpClientund zum Laden dieses Clients in Retrofit.

Wenn Sie OkHttpClientselbstsigniertes SSL akzeptieren müssen, müssen Sie die benutzerdefinierte javax.net.ssl.SSLSocketFactoryInstanz über die setSslSocketFactory(SSLSocketFactory sslSocketFactory)Methode übergeben.

Die einfachste Methode, um eine Sockelfabrik zu erhalten, besteht darin, eine javax.net.ssl.SSLContextwie hier beschrieben zu beziehen .

Hier ist ein Beispiel für die Konfiguration von OkHttpClient:

OkHttpClient client = new OkHttpClient();
KeyStore keyStore = readKeyStore(); //your method to obtain KeyStore
SSLContext sslContext = SSLContext.getInstance("SSL");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "keystore_pass".toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(),trustManagerFactory.getTrustManagers(), new SecureRandom());
client.setSslSocketFactory(sslContext.getSocketFactory());

Aktualisierter Code für okhttp3 (mit Builder):

    OkHttpClient client = new OkHttpClient.Builder()
            .sslSocketFactory(sslContext.getSocketFactory())
            .build();

Das clienthier ist jetzt so konfiguriert, dass Zertifikate von Ihrem verwendet werden KeyStore. Es vertraut jedoch nur den Zertifikaten in Ihrem KeyStoreund nichts anderem, selbst wenn Ihr System ihnen standardmäßig vertraut. (Wenn Sie nur selbstsignierte Zertifikate in Ihrem haben KeyStore und versuchen, über HTTPS eine Verbindung zur Google-Hauptseite herzustellen, erhalten Sie SSLHandshakeException).

Sie können die KeyStoreInstanz aus einer Datei abrufen, wie in den Dokumenten gezeigt :

KeyStore readKeyStore() {
    KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());

    // get user password and file input stream
    char[] password = getPassword();

    java.io.FileInputStream fis = null;
    try {
        fis = new java.io.FileInputStream("keyStoreName");
        ks.load(fis, password);
    } finally {
        if (fis != null) {
            fis.close();
        }
    }
    return ks;
}

Wenn Sie auf Android sind, können Sie es in einen res/rawOrdner legen und von einer ContextInstanz mit abrufen

fis = context.getResources().openRawResource(R.raw.your_keystore_filename);

Es gibt verschiedene Diskussionen zum Erstellen Ihres Keystores. Zum Beispiel hier


Woher bekomme ich den SSLContext?
Oliver Dixon

Sehen Sie sich diese Antwort an, um die CA-Zertifikate des Systems sowie einen benutzerdefinierten Schlüsselspeicher mit Zertifikaten zu akzeptieren .
DreamOfMirrors

2
Die Methode "sslSocketFactory (SSLSocketFactory)" von OkHttpBuilder ist jetzt veraltet und wir sollten "sslSoocketFactory (SSLSocketFactory, X506TrustManager)" verwenden. Dies verhindert, dass die Anwendung einige Reflexionen macht. Quelle: square.github.io/okhttp/3.x/okhttp/okhttp3/…
MHogge

fis = context.getResources (). openRawResource (R.raw.your_keystore_filename); - java.lang.ClassCastException: android.content.res.AssetManager $ AssetInputStream kann nicht in java.io.FileInputStream umgewandelt werden
Zon

FileInputStream ist nicht erforderlich, InputStream reicht aus.
Zon

9

Für okhttp3.OkHttpClient Version com.squareup.okhttp3: okhttp: 3.2.0 müssen Sie den folgenden Code verwenden:

import okhttp3.Call;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;

......

OkHttpClient.Builder clientBuilder = client.newBuilder().readTimeout(LOGIN_TIMEOUT_SEC, TimeUnit.SECONDS);

            boolean allowUntrusted = true;

            if (  allowUntrusted) {
                Log.w(TAG,"**** Allow untrusted SSL connection ****");
                final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        X509Certificate[] cArrr = new X509Certificate[0];
                        return cArrr;
                    }

                    @Override
                    public void checkServerTrusted(final X509Certificate[] chain,
                                                   final String authType) throws CertificateException {
                    }

                    @Override
                    public void checkClientTrusted(final X509Certificate[] chain,
                                                   final String authType) throws CertificateException {
                    }
                }};

                SSLContext sslContext = SSLContext.getInstance("SSL");

                sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
                clientBuilder.sslSocketFactory(sslContext.getSocketFactory());

                HostnameVerifier hostnameVerifier = new HostnameVerifier() {
                    @Override
                    public boolean verify(String hostname, SSLSession session) {
                        Log.d(TAG, "Trust Host :" + hostname);
                        return true;
                    }
                };
                clientBuilder.hostnameVerifier( hostnameVerifier);
            }

            final Call call = clientBuilder.build().newCall(request);

8
Downvoted, da dies die Überprüfung des Hostnamens deaktiviert und den Mann im mittleren Schutz entfernt.
Bryan

1
Diese Lösung dient nur zum Debuggen, wenn Sie kein gültiges SSL-Zertifikat haben. Nicht für Produktionscode!
Gugelhupf

Das OkHttpClient.Builder clientBuilder.sslSocketFactory () ist veraltet und funktioniert nicht mehr mit Android 10. Bitte benutze meinen Vorschlag nicht mehr.
Gugelhupf

7

Wenn Sie die Zertifizierungsstelle auf dem Gerät vorinstallieren, können Sie außerdem regelmäßig https-Aufrufe mit OKHttp und ohne spezielle SSL-Rahmen ausführen. Der Schlüssel besteht darin, die Netzwerksicherheitskonfigurationen zu Ihrem Manifest hinzuzufügen.

Der Schlüssel für mich, dies zu wissen, war, dass ich die folgende Ausnahme bekam.

" Vertrauensanker für Zertifizierungspfad nicht gefunden. "

Hier ist ein guter Artikel von Google über die Konfiguration. https://developer.android.com/training/articles/security-config

Hier ist ein Beispiel für meine network_security_config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <certificates src="user"/>
            <certificates src="system"/>
        </trust-anchors>
    </base-config>
</network-security-config>

Obwohl dies möglicherweise keine direkte Antwort auf die Frage ist, hilft mir dies zu beheben:javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
Vijay

Danke, genaue Antwort, nach der ich lange gesucht habe
Entwickler von Jokela

Funktioniert zumindest in der Android-Emulator-API 21 nicht. Es wurden viele Varianten der XML-Datei ausprobiert.
CoolMind

Ich bin mir sicher, dass mir etwas fehlt, aber das angehängte Dokument besagt, dass API 23 und darunter standardmäßig vertrauen. Standardmäßig vertrauen sichere Verbindungen (unter Verwendung von Protokollen wie TLS und HTTPS) aller Apps den vorinstallierten Systemzertifizierungsstellen, und Apps, die auf Android 6.0 (API-Stufe 23) und niedriger abzielen, vertrauen standardmäßig auch dem vom Benutzer hinzugefügten CA-Speicher
GR Envoy

5

Zwei Methoden aus unserer App, um eine OkHttpClient 3.0- Instanz abzurufen , die Ihre selbstsignierten Zertifikate aus Ihrem Keystore erkennt (verwendet die vorbereitete pkcs12-Zertifikatdatei in Ihrem "rohen" Ressourcenordner Ihres Android-Projekts ):

private static OkHttpClient getSSLClient(Context context) throws
                              NoSuchAlgorithmException,
                              KeyStoreException,
                              KeyManagementException,
                              CertificateException,
                              IOException {

  OkHttpClient client;
  SSLContext sslContext;
  SSLSocketFactory sslSocketFactory;
  TrustManager[] trustManagers;
  TrustManagerFactory trustManagerFactory;
  X509TrustManager trustManager;

  trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
  trustManagerFactory.init(readKeyStore(context));
  trustManagers = trustManagerFactory.getTrustManagers();

  if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
    throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
  }

  trustManager = (X509TrustManager) trustManagers[0];

  sslContext = SSLContext.getInstance("TLS");

  sslContext.init(null, new TrustManager[]{trustManager}, null);

  sslSocketFactory = sslContext.getSocketFactory();

  client = new OkHttpClient.Builder()
      .sslSocketFactory(sslSocketFactory, trustManager)
      .build();
  return client;
}

/**
 * Get keys store. Key file should be encrypted with pkcs12 standard. It    can be done with standalone encrypting java applications like "keytool". File password is also required.
 *
 * @param context Activity or some other context.
 * @return Keys store.
 * @throws KeyStoreException
 * @throws CertificateException
 * @throws NoSuchAlgorithmException
 * @throws IOException
*/
private static KeyStore readKeyStore(Context context) throws
                          KeyStoreException,
                          CertificateException,
                          NoSuchAlgorithmException,
                          IOException {
  KeyStore keyStore;
  char[] PASSWORD = "12345678".toCharArray();
  ArrayList<InputStream> certificates;
  int certificateIndex;
  InputStream certificate;

  certificates = new ArrayList<>();
  certificates.add(context.getResources().openRawResource(R.raw.ssl_pkcs12));

keyStore = KeyStore.getInstance("pkcs12");

for (Certificate certificate : certificates) {
    try {
      keyStore.load(certificate, PASSWORD);
    } finally {
      if (certificate != null) {
        certificate.close();
      }
    }
  }
  return keyStore;
}

certificates.add(context.getResources().openRawResource(R.raw.ssl_pkcs12));Diese Zeile zeigen Fehler können Sie bitte den Grund angeben, warum? Gibt es einen Grund, dies aus der Nicht-Aktivitätsklasse zu klassifizieren
Raj Kumar

2

Ich hatte das gleiche Problem und habe es mit dem okhttp- Client wie folgt behoben :

1.) Fügen Sie die certificateDatei hinzu src/main/res/raw/, die diesen Inhalt enthält:

-----BEGIN CERTIFICATE-----
...=
-----END CERTIFICATE-----

2.) Instanziieren Sie den okHttpClient:

OkHttpClient client = new OkHttpClient.Builder()
                .sslSocketFactory(getSslContext(context).getSocketFactory())
                .build();

3.) Hier ist die verwendete getSslContext(Context context)Methode:

SSLContext getSslContext(Context context) throws Exception {
    KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); // "BKS"
    ks.load(null, null);

    InputStream is = context.getResources().openRawResource(R.raw.certificate);
    String certificate = Converter.convertStreamToString(is);

    // generate input stream for certificate factory
    InputStream stream = IOUtils.toInputStream(certificate);

    // CertificateFactory
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    // certificate
    Certificate ca;
    try {
        ca = cf.generateCertificate(stream);
    } finally {
        is.close();
    }

    ks.setCertificateEntry("my-ca", ca);

    // TrustManagerFactory
    String algorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm);
    // Create a TrustManager that trusts the CAs in our KeyStore
    tmf.init(ks);

    // Create a SSLContext with the certificate that uses tmf (TrustManager)
    sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());

    return sslContext;
}

Wenn dem SslContext mehrere Zertifikate hinzugefügt werden müssen, finden Sie hier die Lösung.


1

Gegen Retrofit 1.9 konnte ich jedes Zertifikat mit folgender Strategie akzeptieren: Verwendung auf eigenes Risiko! Das Akzeptieren eines Zertifikats ist gefährlich und Sie sollten die Konsequenzen verstehen. Einige relevante Teile stammen von org.apache.http.ssl, daher müssen Sie hier möglicherweise importieren.

// ...

    Client httpClient = getHttpClient();

    RestAdapter adapter = new RestAdapter.Builder()
        .setClient(httpClient)
        // ... the rest of your builder setup
        .build();

// ...

private Client getHttpClient() {
    try {
        // Allow self-signed (and actually any) SSL certificate to be trusted in this context
        TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;

        SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom()
            .loadTrustMaterial(null, acceptingTrustStrategy)
            .build();

        sslContext.getSocketFactory();

        SSLSocketFactory sf = sslContext.getSocketFactory();

        OkHttpClient client = new OkHttpClient();
        client.setSslSocketFactory(sf);

        return new OkClient(client);
    } catch (Exception e) {
        throw new RuntimeException("Failed to create new HTTP client", e);
    }
}

0

Ich weiß, dass dieser Beitrag ziemlich alt ist, aber ich möchte die Lösung, die für mich funktioniert hat, mit dem neuesten Update von OkHttp teilen, der 3.12.1Version in der Zeit , in der ich schreibe.

Zunächst müssen Sie das KeyStore-Objekt erhalten, das dann dem TrustManager hinzugefügt wird:

/**
 *  @param context The Android context to be used for retrieving the keystore from raw resource
 * @return the KeyStore read or null on error
 */
private static KeyStore readKeyStore(Context context) {

    char[] password = "keystore_password".toCharArray();

    // for non-android usage:
    // try(FileInputStream is = new FileInputStream(keystoreName)) {

    try(InputStream is = context.getResources().openRawResource(R.raw.keystore)) {
        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        ks.load(is, password);
        return ks;
    } catch (CertificateException e) {
        e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (KeyStoreException e) {
        e.printStackTrace();
    }

    return null;
}

Jetzt können Sie das OkHttpClientmit dem selbstsignierten Zertifikat erstellte Zertifikat in Ihrem Schlüsselspeicher erhalten:

/**
 * @param context The Android context used to obtain the KeyStore
 * @return the builded OkHttpClient or null on error
 */
public static OkHttpClient getOkHttpClient(Context context) {

    try {
        TrustManagerFactory trustManagerFactory = TrustManagerFactory
                .getInstance(TrustManagerFactory.getDefaultAlgorithm());

        trustManagerFactory.init(readKeyStore(context));

        X509TrustManager trustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0];
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[]{trustManager}, null);

        return new OkHttpClient.Builder()
                .hostnameVerifier((hostname, session) -> {
                    HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
                    /* Never return true without verifying the hostname, otherwise you will be vulnerable
                    to man in the middle attacks. */
                    return  hv.verify("your_hostname_here", session);
                })
                .sslSocketFactory(sslContext.getSocketFactory(), trustManager)
                .build();

    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (KeyStoreException e) {
        e.printStackTrace();
    } catch (KeyManagementException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    }

    return null;
}

Denken Sie daran, dass es sehr entmutigt ist , immer wahr zurückzukehren hostnameVerifier, um das Risiko eines Menschen bei mittleren Angriffen zu vermeiden.


-4

Mit dem folgenden Code können Sie einen OkHttp-Client erstellen, der mit Retrofit verwendet werden kann. Mailmustdies Antwort ist "besser" in dem Sinne, dass sie sicherer ist, aber das folgende Codefragment ist schneller zu implementieren

import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.ResponseBody;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import okio.BufferedSink;
import retrofit.client.Header;
import retrofit.client.OkClient;
import retrofit.client.Request;
import retrofit.client.Response;
import retrofit.mime.TypedInput;
import retrofit.mime.TypedOutput;

import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

public class TrustingOkClient extends OkClient {

static final int CONNECT_TIMEOUT_MILLIS = 15 * 1000; // 15s
static final int READ_TIMEOUT_MILLIS = 20 * 1000; // 20s

private static OkHttpClient generateDefaultOkHttp() {
    OkHttpClient client = new OkHttpClient();
    client.setConnectTimeout(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
    client.setReadTimeout(READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);



    final TrustManager[] certs = new TrustManager[]{new X509TrustManager() {

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        @Override
        public void checkServerTrusted(final X509Certificate[] chain,
                                       final String authType) throws CertificateException {
        }

        @Override
        public void checkClientTrusted(final X509Certificate[] chain,
                                       final String authType) throws CertificateException {
        }
    }};

    SSLContext ctx = null;
    try {
        ctx = SSLContext.getInstance("TLS");
        ctx.init(null, certs, new SecureRandom());
    } catch (final java.security.GeneralSecurityException ex) {
    }

    try {
        final HostnameVerifier hostnameVerifier = new HostnameVerifier() {
            @Override
            public boolean verify(final String hostname,
                                  final SSLSession session) {
                return true;
            }
        };
        client.setHostnameVerifier(hostnameVerifier);
        client.setSslSocketFactory(ctx.getSocketFactory());
    } catch (final Exception e) {
    }
    return client;
}

private final OkHttpClient client;

public TrustingOkClient() {
    this(generateDefaultOkHttp());
}

public TrustingOkClient(OkHttpClient client) {
    if (client == null) throw new NullPointerException("client == null");
    this.client = client;
}

@Override public Response execute(Request request) throws IOException {
    return parseResponse(client.newCall(createRequest(request)).execute());
}

static com.squareup.okhttp.Request createRequest(Request request) {
    com.squareup.okhttp.Request.Builder builder = new com.squareup.okhttp.Request.Builder()
            .url(request.getUrl())
            .method(request.getMethod(), createRequestBody(request.getBody()));

    List<Header> headers = request.getHeaders();
    for (int i = 0, size = headers.size(); i < size; i++) {
        Header header = headers.get(i);
        String value = header.getValue();
        if (value == null) value = "";
        builder.addHeader(header.getName(), value);
    }

    return builder.build();
}

static Response parseResponse(com.squareup.okhttp.Response response) {
    return new Response(response.request().urlString(), response.code(), response.message(),
            createHeaders(response.headers()), createResponseBody(response.body()));
}

private static RequestBody createRequestBody(final TypedOutput body) {
    if (body == null) {
        return null;
    }
    final MediaType mediaType = MediaType.parse(body.mimeType());
    return new RequestBody() {
        @Override public MediaType contentType() {
            return mediaType;
        }

        @Override public void writeTo(BufferedSink sink) throws IOException {
            body.writeTo(sink.outputStream());
        }

        @Override public long contentLength() {
            return body.length();
        }
    };
}

private static TypedInput createResponseBody(final ResponseBody body) {
    try {
        if (body.contentLength() == 0) {
            return null;
        }
        return new TypedInput() {
            @Override public String mimeType() {
                MediaType mediaType = body.contentType();
                return mediaType == null ? null : mediaType.toString();
            }

            @Override public long length() {
                try {
                    return body.contentLength();
                } catch (Exception exception) {
                    System.out.println(exception.toString());
                }
                throw new Error("createResponseBody has invalid length for its response");
            }

            @Override public InputStream in() throws IOException {
                return body.byteStream();
            }
        };
    } catch (Exception exception) {
        System.out.println(exception.toString());
    }
    throw new Error("createResponseBody has invalid content length for its response");
}

private static List<Header> createHeaders(Headers headers) {
    int size = headers.size();
    List<Header> headerList = new ArrayList<Header>(size);
    for (int i = 0; i < size; i++) {
        headerList.add(new Header(headers.name(i), headers.value(i)));
    }
    return headerList;
}

}

8
Ich möchte darauf hinweisen, dass dies nicht nur "weniger sicher" als die akzeptierte Antwort ist, sondern völlig unsicher.
GreyBeardedGeek

1
Die Lösung ermöglicht die Verwendung von https über HTTP, was sicherer ist, aber nicht vor Menschen im mittleren Angriff schützt. Es ist etwas besser als gar nicht sicher und ermöglicht es einer in der Entwicklung befindlichen App, eine Verbindung zu einem https-Dienst herzustellen, der später ein ordnungsgemäßes Zertifikat für 50 Dollar erhalten würde. Viel besser, als viele Stunden damit zu verbringen, ein selbstsigniertes Zertifikat für die Arbeit mit einem Android-Client zu erhalten
Bruno Carrier

1
Downvoted, da dies die Überprüfung des Hostnamens deaktiviert und den Mann im mittleren Schutz entfernt. Die Implementierung einer akzeptierten Antwort sollte nicht "viele Stunden" dauern.
Bryan
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.