Push-Abos verwenden

Pub/Sub unterstützt die Push- und Pull-Nachrichtenzustellung. Einen Überblick und Vergleich von Pull- und Push-Abos finden Sie in Abonnentenübersicht. In diesem Dokument wird die Push-Zustellung beschrieben. Eine Beschreibung der Pull-Zustellung finden Sie im Pull-Abonnentenleitfaden.

Wenn ein Abo die Push-Zustellung verwendet, sendet der Pub/Sub-Dienst Nachrichten an einen Push-Endpunkt. Der Push-Endpunkt muss eine öffentlich zugängliche HTTPS-Adresse sein. Der Server für den Push-Endpunkt muss ein gültiges SSL-Zertifikat haben, das von einer Zertifizierungsstelle signiert wurde.

Push-Abos können auch so konfiguriert werden, dass sie einen Autorisierungsheader enthalten, über den die Endpunkte die Anfragen authentifizieren können. Für App Engine Standard- und Cloud Functions-Endpunkte, die im selben Projekt wie das Abo gehostet werden, sind automatische Authentifizierungs- und Autorisierungsmechanismen verfügbar.

Nachrichten erhalten

Wenn Pub/Sub eine Nachricht an einen Push-Endpunkt sendet, sendet Pub/Sub die Nachricht im Text einer POST-Anfrage. Der Anfragetext ist ein JSON-Objekt und die Nachrichtendaten befinden sich im Feld message.data. Die Nachrichtendaten sind Base64-codiert.

Das folgende Beispiel zeigt den Text einer POST-Anfrage an einen Push-Endpunkt:

{
    "message": {
        "attributes": {
            "key": "value"
        },
        "data": "SGVsbG8gQ2xvdWQgUHViL1N1YiEgSGVyZSBpcyBteSBtZXNzYWdlIQ==",
        "messageId": "136969346945"
    },
   "subscription": "projects/myproject/subscriptions/mysubscription"
}

Wenn Sie Nachrichten von Push-Abos erhalten möchten, verwenden Sie einen Webhook und verarbeiten Sie die POST-Anfragen, die Pub/Sub an den Push-Endpunkt sendet. Die folgenden Cloud-Funktionen sind beispielsweise Webhooks, die Nachrichtendaten decodieren und drucken:

Go


// Package helloworld provides a set of Cloud Functions samples.
package helloworld

import (
	"context"
	"log"
)

// PubSubMessage is the payload of a Pub/Sub event.
type PubSubMessage struct {
	Data []byte `json:"data"`
}

// HelloPubSub consumes a Pub/Sub message.
func HelloPubSub(ctx context.Context, m PubSubMessage) error {
	name := string(m.Data) // Automatically decoded from base64.
	if name == "" {
		name = "World"
	}
	log.Printf("Hello, %s!", name)
	return nil
}

Java


import com.google.cloud.functions.BackgroundFunction;
import com.google.cloud.functions.Context;
import functions.eventpojos.PubSubMessage;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.logging.Level;
import java.util.logging.Logger;

public class HelloPubSub implements BackgroundFunction<PubSubMessage> {
  private static final Logger logger = Logger.getLogger(HelloPubSub.class.getName());

  @Override
  public void accept(PubSubMessage message, Context context) {
    String name = "world";
    if (message != null && message.getData() != null) {
      name = new String(
          Base64.getDecoder().decode(message.getData().getBytes(StandardCharsets.UTF_8)),
          StandardCharsets.UTF_8);
    }
    logger.info(String.format("Hello %s!", name));
    return;
  }
}

Node.js

/**
 * Background Cloud Function to be triggered by Pub/Sub.
 * This function is exported by index.js, and executed when
 * the trigger topic receives a message.
 *
 * @param {object} message The Pub/Sub message.
 * @param {object} context The event metadata.
 */
exports.helloPubSub = (message, context) => {
  const name = message.data
    ? Buffer.from(message.data, 'base64').toString()
    : 'World';

  console.log(`Hello, ${name}!`);
};

Python

def hello_pubsub(event, context):
    """Background Cloud Function to be triggered by Pub/Sub.
    Args:
         event (dict):  The dictionary with data specific to this type of
         event. The `data` field contains the PubsubMessage message. The
         `attributes` field will contain custom attributes if there are any.
         context (google.cloud.functions.Context): The Cloud Functions event
         metadata. The `event_id` field contains the Pub/Sub message ID. The
         `timestamp` field contains the publish time.
    """
    import base64

    print("""This Function was triggered by messageId {} published at {}
    """.format(context.event_id, context.timestamp))

    if 'data' in event:
        name = base64.b64decode(event['data']).decode('utf-8')
    else:
        name = 'World'
    print('Hello {}!'.format(name))

Wenn Sie eine Push-Anfrage erhalten haben, geben Sie einen HTTP-Statuscode zurück. Um die Nachricht zu bestätigen, geben Sie einen der folgenden Statuscodes zurück:

  • 102
  • 200
  • 201
  • 202
  • 204

Wenn Sie eine negative Bestätigung für die Nachricht senden möchten, geben Sie einen anderen Statuscode zurück. Wenn Sie eine negative Bestätigung senden oder die Bestätigungsfrist abläuft, sendet Pub/Sub die Nachricht noch einmal. Sie können die Bestätigungsfrist für einzelne Nachrichten, die Sie von Push-Abos erhalten, nicht ändern.

Authentifizierung und Autorisierung

Wenn ein Push-Abo Authentifizierung verwendet, signiert der Pub/Sub-Dienst ein JSON Web Token (JWT) und sendet das JWT im Autorisierungsheader der Push-Anfrage. Das JWT enthält Ansprüche und eine Signatur.

Abonnenten können das JWT decodieren und Folgendes überprüfen:

  • Die Ansprüche sind korrekt.
  • Der Pub/Sub-Dienst hat die Ansprüche signiert.

Wenn Abonnenten eine Firewall verwenden, können sie keine Push-Anfragen empfangen. Um Push-Anfragen zu erhalten, müssen Sie die Firewall deaktivieren und das JWT überprüfen.

JWT-Format

Das JWT ist ein OpenIDConnect-JWT, das aus einem Header, einem Anforderungssatz und einer Signatur besteht. Der Pub/Sub-Dienst codiert das JWT als base64-String mit Punkttrennzeichen.

Der folgende Autorisierungsheader enthält beispielsweise ein codiertes JWT:

"Authorization" : "Bearer
eyJhbGciOiJSUzI1NiIsImtpZCI6IjdkNjgwZDhjNzBkNDRlOTQ3MTMzY2JkNDk5ZWJjMWE2MWMzZDVh
YmMiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tIiwiYXpwIjoiMTEzNzc0M
jY0NDYzMDM4MzIxOTY0IiwiZW1haWwiOiJnYWUtZ2NwQGFwcHNwb3QuZ3NlcnZpY2VhY2NvdW50LmNvb
SIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1NTAxODU5MzUsImlhdCI6MTU1MDE4MjMzNSwia
XNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTEzNzc0MjY0NDYzMDM4MzIxO
TY0In0.QVjyqpmadTyDZmlX2u3jWd1kJ68YkdwsRZDo-QxSPbxjug4ucLBwAs2QePrcgZ6hhkvdc4UHY
4YF3fz9g7XHULNVIzX5xh02qXEH8dK6PgGndIWcZQzjSYfgO-q-R2oo2hNM5HBBsQN4ARtGK_acG-NGG
WM3CQfahbEjZPAJe_B8M7HfIu_G5jOLZCw2EUcGo8BvEwGcLWB2WqEgRM0-xt5-UPzoa3-FpSPG7DHk7
z9zRUeq6eB__ldb-2o4RciJmjVwHgnYqn3VvlX9oVKEgXpNFhKuYA-mWh5o7BCwhujSMmFoBOh6mbIXF
cyf5UiVqKjpqEbqPGo_AvKvIQ9VTQ" 

Der Header und der Anforderungssatz sind JSON-Strings. Nach der Decodierung nehmen sie die folgende Form an:

{"alg":"RS256","kid":"7d680d8c70d44e947133cbd499ebc1a61c3d5abc","typ":"JWT"}

{
   "aud":"https://example.com",
   "azp":"113774264463038321964",
   "email":"gae-gcp@appspot.gserviceaccount.com",
   "sub":"113774264463038321964",
   "email_verified":true,
   "exp":1550185935,
   "iat":1550182335,
   "iss":"https://accounts.google.com"
  }

Die an Anfragen angehängten Tokens an Push-Endpunkte können bis zu einer Stunde alt sein.

Pub/Sub für Push-Authentifizierung einrichten

Die Authentifizierungskonfiguration für ein Abo besteht aus zwei Parametern:

  • Dienstkonto: Das GCP-Dienstkonto, das dem Push-Abo zugeordnet ist Push-Anfragen enthalten die Identität dieses Dienstkontos. Ein Push-Abo, das mit einem Dienstkonto mit der Rolle roles/run.invoker konfiguriert und an einen bestimmten (vollständig verwalteten) Cloud Run-Dienst gebunden ist, kann beispielsweise diesen (vollständig verwalteten) Cloud Run-Dienst aufrufen.
  • Token-Zielgruppe (optional): Ein einzelner String ohne Berücksichtigung der Groß- und Kleinschreibung, mit dem der Webhook die beabsichtigte Zielgruppe dieses bestimmten Tokens überprüfen kann

Zusätzlich zur Konfiguration dieser Felder müssen Sie Pub/Sub auch die Berechtigungen erteilen, die zum Erstellen von Tokens für Ihr Dienstkonto erforderlich sind. Pub/Sub erstellt und verwaltet ein spezielles Dienstkonto für Ihr Projekt: service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com. Dieses Dienstkonto benötigt die Rolle Dienstkonto-Tokenersteller. Wenn Sie die Cloud Console verwenden, um das Abo für die Push-Authentifizierung einzurichten, wird die Rolle automatisch zugeordnet. Andernfalls müssen Sie dem Konto die Rolle explizit zuweisen.

BEFEHLSZEILE

# grant Cloud Pub/Sub the permission to create tokens
PUBSUB_SERVICE_ACCOUNT="service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com"
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
 --member="serviceAccount:${PUBSUB_SERVICE_ACCOUNT}"\
 --role='roles/iam.serviceAccountTokenCreator'

# configure the subscription push identity
gcloud pubsub subscriptions (create|update|modify-push-config) ${SUBSCRIPTION} \
 --topic=${TOPIC} \
 --push-endpoint=${PUSH_ENDPOINT_URI} \
 --push-auth-service-account=${SERVICE_ACCOUNT_EMAIL} \
 --push-auth-token-audience=${OPTIONAL_AUDIENCE_OVERRIDE}

Konsole

  1. Zur Seite "Pub/Sub-Themen"

    Zur Seite "Themen"

  2. Klicken Sie auf einen Themennamen.

  3. Erstellen oder aktualisieren Sie ein Abo.

  4. Geben Sie eine Identität und (optional) eine Zielgruppe ein.

Authentifizierung und Autorisierung durch den Push-Endpunkt

Ansprüche

Mit dem JWT kann sichergestellt werden, dass die Anforderungen (einschließlich der email- und aud-Anforderungen) von Google unterzeichnet werden. Weitere Informationen dazu, wie die OAuth 2.0 APIs von Google sowohl für die Authentifizierung als auch für die Autorisierung verwendet werden können, finden Sie unter OpenID Connect.

Es gibt zwei Mechanismen, mit denen Sie diese Anforderungen nutzbar machen. Erstens muss in Pub/Sub der Nutzer oder das Dienstkonto, mit dem eine Dienstkontoidentität mit einem Push-Abo verknüpft wird, die Rolle Dienstkontonutzer (roles/iam.serviceAccountUser) für das Projekt oder das Dienstkonto haben.

Zweitens wird der Zugriff auf die zum Signieren der Tokens verwendeten Zertifikate streng kontrolliert. Damit Pub/Sub das Token erstellen kann, muss es einen internen Google-Dienst unter Verwendung einer separaten Dienstkontoidentität zum Signieren aufrufen. Das signierende Dienstkonto muss berechtigt sein, Tokens für das angeforderte Dienstkonto oder das Projekt zu erstellen, das das Konto enthält. Hierfür wird die Berechtigung iam.serviceAccounts.getOpenIdToken oder die Rolle Dienstkonto-Tokenersteller (roles/iam.serviceAccountTokenCreator) verwendet.

Diese Rolle oder Berechtigung kann jedem Konto zugewiesen werden. Sie können jedoch den IAM-Dienst verwenden und so prüfen, ob das Pub/Sub-Signaturkonto das Konto mit dieser Berechtigung ist. Pub/Sub verwendet ein Dienstkonto wie dieses:

service-{project_number}@gcp-sa-pubsub.iam.gserviceaccount.com
  • {project_number}: das GCP-Projekt, das das Abo enthält
  • gcp-sa-pubsub: das Projekt mit Google als Inhaber, das das zum Signieren verwendete Dienstkonto enthält.

Tokens validieren

Das folgende Beispiel zeigt, wie Sie eine Push-Anfrage bei einer App Engine-Anwendung authentifizieren.

Protokoll

Anfrage:

GET https://oauth2.googleapis.com/tokeninfo?id_token={BEARER_TOKEN}

Antwort:

200 OK
{
    "alg": "RS256",
    "aud": "example.com",
    "azp": "104176025330667568672",
    "email": "{SERVICE_ACCOUNT_NAME}@{YOUR_PROJECT_NAME}.iam.gserviceaccount.com",
    "email_verified": "true",
    "exp": "1555463097",
    "iat": "1555459497",
    "iss": "https://accounts.google.com",
    "kid": "3782d3f0bc89008d9d2c01730f765cfb19d3b70e",
    "sub": "104176025330667568672",
    "typ": "JWT"
}

Java

@WebServlet(value = "/pubsub/authenticated-push")
public class PubSubAuthenticatedPush extends HttpServlet {
  private final String pubsubVerificationToken = System.getenv("PUBSUB_VERIFICATION_TOKEN");
  private final MessageRepository messageRepository;
  private final GoogleIdTokenVerifier verifier =
      new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory())
          /**
           * Please change example.com to match with value you are providing while creating
           * subscription as provided in @see <a
           * href="https://github.com/GoogleCloudPlatform/java-docs-samples/tree/master/appengine-java8/pubsub">README</a>.
           */
          .setAudience(Collections.singletonList("example.com"))
          .build();
  private final Gson gson = new Gson();
  private final JsonParser jsonParser = new JsonParser();

  @Override
  public void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws IOException, ServletException {

    // Verify that the request originates from the application.
    if (req.getParameter("token").compareTo(pubsubVerificationToken) != 0) {
      resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      return;
    }
    // Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.
    String authorizationHeader = req.getHeader("Authorization");
    if (authorizationHeader == null
        || authorizationHeader.isEmpty()
        || authorizationHeader.split(" ").length != 2) {
      resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      return;
    }
    String authorization = authorizationHeader.split(" ")[1];

    try {
      // Verify and decode the JWT.
      // Note: For high volume push requests, it would save some network overhead
      // if you verify the tokens offline by decoding them using Google's Public
      // Cert; caching already seen tokens works best when a large volume of
      // messsages have prompted a singple push server to handle them, in which
      // case they would all share the same token for a limited time window.
      GoogleIdToken idToken = verifier.verify(authorization);
      messageRepository.saveToken(authorization);
      messageRepository.saveClaim(idToken.getPayload().toPrettyString());
      // parse message object from "message" field in the request body json
      // decode message data from base64
      Message message = getMessage(req);
      messageRepository.save(message);
      // 200, 201, 204, 102 status codes are interpreted as success by the Pub/Sub system
      resp.setStatus(102);
      super.doPost(req, resp);
    } catch (Exception e) {
      resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    }
  }

  private Message getMessage(HttpServletRequest request) throws IOException {
    String requestBody = request.getReader().lines().collect(Collectors.joining("\n"));
    JsonElement jsonRoot = jsonParser.parse(requestBody);
    String messageStr = jsonRoot.getAsJsonObject().get("message").toString();
    Message message = gson.fromJson(messageStr, Message.class);
    // decode from base64
    String decoded = decode(message.getData());
    message.setData(decoded);
    return message;
  }

  private String decode(String data) {
    return new String(Base64.getDecoder().decode(data));
  }

  PubSubAuthenticatedPush(MessageRepository messageRepository) {
    this.messageRepository = messageRepository;
  }

  public PubSubAuthenticatedPush() {
    this(MessageRepositoryImpl.getInstance());
  }
}

Node.js

app.post('/pubsub/authenticated-push', jsonBodyParser, async (req, res) => {
  // Verify that the request originates from the application.
  if (req.query.token !== PUBSUB_VERIFICATION_TOKEN) {
    res.status(400).send('Invalid request');
    return;
  }

  // Verify that the push request originates from Cloud Pub/Sub.
  try {
    // Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.
    const bearer = req.header('Authorization');
    const [, token] = bearer.match(/Bearer (.*)/);
    tokens.push(token);

    // Verify and decode the JWT.
    // Note: For high volume push requests, it would save some network
    // overhead if you verify the tokens offline by decoding them using
    // Google's Public Cert; caching already seen tokens works best when
    // a large volume of messages have prompted a single push server to
    // handle them, in which case they would all share the same token for
    // a limited time window.
    const ticket = await authClient.verifyIdToken({
      idToken: token,
      audience: 'example.com',
    });

    const claim = ticket.getPayload();
    claims.push(claim);
  } catch (e) {
    res.status(400).send('Invalid token');
    return;
  }

  // The message is a unicode string encoded in base64.
  const message = Buffer.from(req.body.message.data, 'base64').toString(
    'utf-8'
  );

  messages.push(message);

  res.status(200).send();
});

Python

@app.route('/push-handlers/receive_messages', methods=['POST'])
def receive_messages_handler():
    # Verify that the request originates from the application.
    if (request.args.get('token', '') !=
            current_app.config['PUBSUB_VERIFICATION_TOKEN']):
        return 'Invalid request', 400

    # Verify that the push request originates from Cloud Pub/Sub.
    try:
        # Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.
        bearer_token = request.headers.get('Authorization')
        token = bearer_token.split(' ')[1]
        TOKENS.append(token)

        # Verify and decode the JWT. `verify_oauth2_token` verifies
        # the JWT signature, the `aud` claim, and the `exp` claim.
        # Note: For high volume push requests, it would save some network
        # overhead if you verify the tokens offline by downloading Google's
        # Public Cert and decode them using the `google.auth.jwt` module;
        # caching already seen tokens works best when a large volume of
        # messages have prompted a single push server to handle them, in which
        # case they would all share the same token for a limited time window.
        claim = id_token.verify_oauth2_token(token, requests.Request(),
                                             audience='example.com')
        CLAIMS.append(claim)
    except Exception as e:
        return 'Invalid token: {}\n'.format(e), 400

    envelope = json.loads(request.data.decode('utf-8'))
    payload = base64.b64decode(envelope['message']['data'])
    MESSAGES.append(payload)
    # Returning any 2xx status indicates successful receipt of the message.
    return 'OK', 200

Go

// receiveMessagesHandler validates authentication token and caches the Pub/Sub
// message received.
func (a *app) receiveMessagesHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
		return
	}

	// Verify that the request originates from the application.
	// a.pubsubVerificationToken = os.Getenv("PUBSUB_VERIFICATION_TOKEN")
	if token, ok := r.URL.Query()["token"]; !ok || len(token) != 1 || token[0] != a.pubsubVerificationToken {
		http.Error(w, "Bad token", http.StatusBadRequest)
		return
	}

	// Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.
	authHeader := r.Header.Get("Authorization")
	if authHeader == "" || len(strings.Split(authHeader, " ")) != 2 {
		http.Error(w, "Missing Authorization header", http.StatusBadRequest)
		return
	}
	token := strings.Split(authHeader, " ")[1]
	// Verify and decode the JWT.
	// If you don't need to control the HTTP client used you can use the
	// convenience method idtoken.Validate instead of creating a Validator.
	v, err := idtoken.NewValidator(r.Context(), option.WithHTTPClient(a.defaultHTTPClient))
	if err != nil {
		http.Error(w, "Unable to create Validator", http.StatusBadRequest)
		return
	}
	// Please change http://example.com to match with the value you are
	// providing while creating the subscription.
	payload, err := v.Validate(r.Context(), token, "http://example.com")
	if err != nil {
		http.Error(w, fmt.Sprintf("Invalid Token: %v", err), http.StatusBadRequest)
		return
	}
	if payload.Issuer != "accounts.google.com" && payload.Issuer != "https://accounts.google.com" {
		http.Error(w, "Wrong Issuer", http.StatusBadRequest)
	}

	var pr pushRequest
	if err := json.NewDecoder(r.Body).Decode(&pr); err != nil {
		http.Error(w, fmt.Sprintf("Could not decode body: %v", err), http.StatusBadRequest)
		return
	}

	a.messagesMu.Lock()
	defer a.messagesMu.Unlock()
	// Limit to ten.
	a.messages = append(a.messages, pr.Message.Data)
	if len(a.messages) > maxMessages {
		a.messages = a.messages[len(a.messages)-maxMessages:]
	}

	fmt.Fprint(w, "OK")
}

Weitere Beispiele zur Validierung des Inhaber-JWT finden Sie im Handbuch zu Google Log-in für Websites. Eine umfassendere Übersicht über OpenID-Tokens finden Sie im Handbuch zu OpenID Connect.

Cloud Run und App Engine

Cloud Run und App Engine authentifizieren HTTP-Aufrufe automatisch, indem von Pub/Sub generierte Tokens verifiziert werden. Die einzige für den Nutzer erforderliche Konfiguration besteht darin, dass dem Anruferkonto die erforderlichen IAM-Rollen zugewiesen werden. Sie können beispielsweise die Berechtigung zum Aufruf eines bestimmten Cloud Run-Endpunkts für ein Konto erteilen oder widerrufen. Weitere Informationen finden Sie in den folgenden Anleitungen:

Zustellung stoppen und fortsetzen

Sie können das Senden von Anfragen an den Push-Endpunkt in Pub/Sub vorübergehend anhalten. Ändern Sie dazu das Abo in Pull. Es kann einige Minuten dauern, bis diese Änderung wirksam wird.

Zum Fortsetzen der Push-Zustellung wählen Sie für die URL wieder einen gültigen Endpunkt aus. Löschen Sie das Abo, wenn Sie die Zustellung dauerhaft stoppen möchten.

Kontingente, Limits und Zustellungsgeschwindigkeit

Push-Abos unterliegen einer Reihe von Kontingent- und Ressourcenbeschränkungen.

Wenn Pub/Sub keine success-Antwort erhält, wendet Pub/Sub den exponentiellen Backoff mit einem Minimum von 100 Millisekunden und einem Maximum von 60 Sekunden an.

Pub/Sub passt die Anzahl der gleichzeitigen Push-Anfragen mithilfe eines Algorithmus für einen langsamen Start an. Die maximal zulässige Anzahl an gleichzeitigen Push-Anfragen ist das Push-Fenster. Das Push-Fenster erhöht sich bei jeder erfolgreichen Übermittlung und verringert sich bei jedem Fehler. Das System beginnt mit einem kleinen Fenster: 3 * N, wobei N die Anzahl der Publish-Regionen ist. Die maximale Fenstergröße beträgt 3,000 * N. Die tatsächliche Anzahl gleichzeitiger oder ausstehender Push-Anfragen kann mit dem Cloud Monitoring-Messwert pubsub.googleapis.com/subscription/num_outstanding_messages beobachtet werden.