[
  {
    "path": ".azuredevops/dependabot.yml",
    "content": "version: 2\r\n\r\n# Disabling dependabot on Azure DevOps as this is a mirrored repo. Updates should go through github.\r\nenable-campaigned-updates: false\r\nenable-security-updates: false\r\n"
  },
  {
    "path": ".config/CredScanSuppressions.json",
    "content": "{\n    \"tool\": \"Credential Scanner\",\n    \"suppressions\": [\n        {\n            \"_justification\": \"Legitimate key/cert used for testing\",\n            \"file\": [\n                \"testassets/BenchmarkApp/testCert.pfx\",\n                \"test/TestCertificates/testCert.pfx\",\n                \"test/Kubernetes.Tests/Certificates/key.der\",\n                \"test/Kubernetes.Tests/Certificates/key.pem\"\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": ".config/tsaoptions.json",
    "content": "{\n  \"areaPath\": \"DevDiv\\\\ASP.NET Core\\\\YARP\",\n  \"codebaseName\": \"ReverseProxy\",\n  \"instanceUrl\": \"https://devdiv.visualstudio.com/\",\n  \"iterationPath\": \"DevDiv\",\n  \"notificationAliases\": [\n    \"dotnetrp@microsoft.com\"\n  ],\n  \"projectName\": \"DEVDIV\",\n  \"repositoryName\": \"yarp\",\n  \"template\": \"TFSDEVDIV\"\n}\n"
  },
  {
    "path": ".dockerignore",
    "content": "**/.classpath\n**/.dockerignore\n**/.env\n**/.git\n**/.gitignore\n**/.project\n**/.settings\n**/.toolstarget\n**/.vs\n**/.vscode\n**/*.*proj.user\n**/*.dbmdl\n**/*.jfm\n**/azds.yaml\n**/bin\n**/charts\n**/docker-compose*\n**/Dockerfile*\n**/node_modules\n**/npm-debug.log\n**/obj\n**/secrets.dev.yaml\n**/values.dev.yaml\nLICENSE\nREADME.md\n**/ingress.yaml\n**/ingress-controller.yaml\n**/backend.yaml\n**/ingress-sample.yaml\n.dotnet\nTestResults\n"
  },
  {
    "path": ".editorconfig",
    "content": "; EditorConfig to support per-solution formatting.\n; Use the EditorConfig VS add-in to make this work.\n; http://editorconfig.org/\n;\n; Here are some resources for what's supported for .NET/C#\n; https://kent-boogaart.com/blog/editorconfig-reference-for-c-developers\n; https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference?view=vs-2017\n;\n; Be **careful** editing this because some of the rules don't support adding a severity level\n; For instance if you change to `dotnet_sort_system_directives_first = true:warning` (adding `:warning`)\n; then the rule will be silently ignored.\n\n; This is the default for the codeline.\nroot = true\n\n[*]\nindent_style = space\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.cs]\nindent_size = 4\n\n[*.{xml,config,*proj,nuspec,props,resx,targets,yml,tasks}]\nindent_size = 2\n\n[*.json]\nindent_size = 2\n\n[*.sh]\nindent_size = 4\nend_of_line = lf\n\n\n## The .NET Style\n## Things that are commented out are available to configure but we generally don't have a preference.\n[*.{cs, vb}]\n\n# Organize using directives\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#organize-using-directives\ndotnet_sort_system_directives_first = true\n# dotnet_separate_import_directive_groups = false\n\n## TODO: Swap things back to suggestion from error before merging. Just doing this to find the violations quickly and fix them.\n\n# Naming rules\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-naming-conventions?view=vs-2019\n\n# Inspired by, but modified from, the Roslyn style: https://github.com/dotnet/roslyn/blob/75fcec13fdaa6f0f38f8ef1d7238d947df55cf5e/.editorconfig#L59\n\n# Non-private static fields are PascalCase\ndotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion\ndotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields\ndotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style\n\ndotnet_naming_symbols.non_private_static_fields.applicable_kinds = field\ndotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected\ndotnet_naming_symbols.non_private_static_fields.required_modifiers = static\n\ndotnet_naming_style.non_private_static_field_style.capitalization = pascal_case\n\n# Non-private readonly fields are PascalCase\ndotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion\ndotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields\ndotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style\n\ndotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field\ndotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected\ndotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly\n\ndotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case\n\n# Constants are PascalCase\ndotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion\ndotnet_naming_rule.constants_should_be_pascal_case.symbols = constants\ndotnet_naming_rule.constants_should_be_pascal_case.style = constant_style\n\ndotnet_naming_symbols.constants.applicable_kinds = field, local\ndotnet_naming_symbols.constants.required_modifiers = const\n\ndotnet_naming_style.constant_style.capitalization = pascal_case\n\n# Instance fields are camelCase and start with _\ndotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion\ndotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields\ndotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style\n\ndotnet_naming_symbols.instance_fields.applicable_kinds = field\n\ndotnet_naming_style.instance_field_style.capitalization = camel_case\ndotnet_naming_style.instance_field_style.required_prefix = _\n\n# Locals and parameters are camelCase\ndotnet_naming_rule.locals_should_be_camel_case.severity = suggestion\ndotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters\ndotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style\n\ndotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local\n\ndotnet_naming_style.camel_case_style.capitalization = camel_case\n\n# Local functions are PascalCase\ndotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion\ndotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions\ndotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style\n\ndotnet_naming_symbols.local_functions.applicable_kinds = local_function\n\ndotnet_naming_style.local_function_style.capitalization = pascal_case\n\n# By default, name items with PascalCase\ndotnet_naming_rule.members_should_be_pascal_case.severity = suggestion\ndotnet_naming_rule.members_should_be_pascal_case.symbols = all_members\ndotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style\n\ndotnet_naming_symbols.all_members.applicable_kinds = *\n\ndotnet_naming_style.pascal_case_style.capitalization = pascal_case\n\n# Don't use \"this.\" anywhere.\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#this-and-me\ndotnet_style_qualification_for_field = false:error\ndotnet_style_qualification_for_property = false:error\ndotnet_style_qualification_for_method = false:error\ndotnet_style_qualification_for_event = false:error\n\n# Use 'string' instead of 'String', 'int' instead of 'Int32', etc.\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#language-keywords\ndotnet_style_predefined_type_for_locals_parameters_members = true:error\ndotnet_style_predefined_type_for_member_access = true:error\n\n# Explicitly specify modifiers and always mark fields that can be 'readonly'\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#normalize-modifiers\ndotnet_style_require_accessibility_modifiers = always:error\ndotnet_style_readonly_field = true:error\n\n# We generally don't have rules about parentheses.\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#parentheses-preferences\n# dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent\n# dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent\n# dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent\n# dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent\n\n# Expression preferences\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#expression-level-preferences\ndotnet_style_prefer_auto_properties = true:error\n# dotnet_style_object_initializer = true:suggestion\n# dotnet_style_collection_initializer = true:suggestion\n# dotnet_style_explicit_tuple_names = true:suggestion\n# dotnet_style_prefer_inferred_tuple_names = true:suggestion\n# dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion\n# dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion\n# dotnet_style_prefer_conditional_expression_over_return = true:suggestion\n# dotnet_style_prefer_compound_assignment = true:suggestion\n\n# Null-checking preferences\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#null-checking-preferences\n# dotnet_style_coalesce_expression = true:suggestion\n# dotnet_style_null_propagation = true:suggestion\n\n# Parameter preferences\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#parameter-preferences\ndotnet_code_quality_unused_parameters = all:error\n\n## C#-specific style\n[*.cs]\n\n# License header\nfile_header_template = Licensed to the .NET Foundation under one or more agreements.\\nThe .NET Foundation licenses this file to you under the MIT license.\n\n# Preferred order of modfiers.\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#normalize-modifiers\ncsharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:error\n\n# Implicit and explicit types\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#implicit-and-explicit-types\ncsharp_style_var_for_built_in_types = true:error\ncsharp_style_var_when_type_is_apparent = true:error\ncsharp_style_var_elsewhere = true:error\n\n# Expression-bodied members\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#expression-bodied-members\n# csharp_style_expression_bodied_methods = false:silent\n# csharp_style_expression_bodied_constructors = false:silent\n# csharp_style_expression_bodied_operators = false:silent\n# csharp_style_expression_bodied_properties = true:suggestion\n# csharp_style_expression_bodied_indexers = true:suggestion\n# csharp_style_expression_bodied_accessors = true:suggestion\n# csharp_style_expression_bodied_lambdas = true:silent\n# csharp_style_expression_bodied_local_functions = false:silent\n\n# Pattern matching\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#pattern-matching\n# csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion\n# csharp_style_pattern_matching_over_as_with_null_check = true:suggestion\n\n# Inlined variable declarations (out var i)\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#inlined-variable-declarations\ncsharp_style_inlined_variable_declaration = true:error\n\n# Expression preferences ('default' vs 'default(T)')\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#c-expression-level-preferences\ncsharp_prefer_simple_default_expression = true:error\n\n# Null-checking preferences\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#c-null-checking-preferences\n# csharp_style_throw_expression = true:suggestion\ncsharp_style_conditional_delegate_call = true:error\n\n# Code block preferences\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#code-block-preferences\ncsharp_prefer_braces = true:error\n\n# Unused value preferences\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#unused-value-preferences\n# This one is S P I C Y. Enabling either option *forces* you to put something on the left-side when you call a method that returns a value.\n# csharp_style_unused_value_expression_statement_preference = discard_variable:silent\n# csharp_style_unused_value_assignment_preference = discard_variable:suggestion\n\n# Index and Range preferences\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#index-and-range-preferences\n# csharp_style_prefer_index_operator = true:suggestion\n# csharp_style_prefer_range_operator = true:suggestion\n\n# Miscellaneous preferences\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#miscellaneous-preferences\ncsharp_using_directive_placement = outside_namespace:error\n# csharp_style_deconstructed_variable_declaration = true:suggestion\n# csharp_style_pattern_local_over_anonymous_function = true:suggestion\n# csharp_prefer_static_local_function = true:suggestion\n# csharp_prefer_simple_using_statement = true:suggestion\n# csharp_style_prefer_switch_expression = true:suggestion\ncsharp_style_namespace_declarations=file_scoped:suggestion\n\n# New-line options\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#new-line-options\ncsharp_new_line_before_open_brace = methods, properties, control_blocks, types, anonymous_methods, lambdas, object_collection_array_initializers, accessors\ncsharp_new_line_before_else = true\ncsharp_new_line_before_catch = true\ncsharp_new_line_before_finally = true\ncsharp_new_line_before_members_in_object_initializers = true\ncsharp_new_line_before_members_in_anonymous_types = true\ncsharp_new_line_between_query_expression_clauses = true\n\n# Indentation options\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#indentation-options\ncsharp_indent_case_contents = true\ncsharp_indent_switch_labels = true\ncsharp_indent_labels = one_less_than_current\ncsharp_indent_block_contents = true\ncsharp_indent_braces = false\ncsharp_indent_case_contents_when_block = true\n\n# Spacing options\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#spacing-options\ncsharp_space_after_cast = false\ncsharp_space_after_keywords_in_control_flow_statements = true\ncsharp_space_between_parentheses = false\ncsharp_space_before_colon_in_inheritance_clause = true\ncsharp_space_after_colon_in_inheritance_clause = true\ncsharp_space_around_binary_operators = before_and_after\ncsharp_space_between_method_declaration_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_empty_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_name_and_open_parenthesis = false\ncsharp_space_between_method_call_parameter_list_parentheses = false\ncsharp_space_between_method_call_empty_parameter_list_parentheses = false\ncsharp_space_between_method_call_name_and_opening_parenthesis = false\ncsharp_space_after_comma = true\ncsharp_space_before_comma = false\ncsharp_space_after_dot = false\ncsharp_space_before_dot = false\ncsharp_space_after_semicolon_in_for_statement = true\ncsharp_space_before_semicolon_in_for_statement = false\ncsharp_space_around_declaration_statements = false\ncsharp_space_before_open_square_brackets = false\ncsharp_space_between_empty_square_brackets = false\ncsharp_space_between_square_brackets = false\n\n# Wrap options\n# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#wrap-options\ncsharp_preserve_single_line_statements = true\ncsharp_preserve_single_line_blocks = true\n"
  },
  {
    "path": ".gitattributes",
    "content": "###############################################################################\n# Set default behavior to automatically normalize line endings.\n###############################################################################\n* text=auto\n\n###############################################################################\n# Make sh files under the build directory always have LF as line endings\n###############################################################################\n*.sh eol=lf\n\n###############################################################################\n# Set default behavior for command prompt diff.\n#\n# This is need for earlier builds of msysgit that does not have it on by\n# default for csharp files.\n# Note: This is only used by command line\n###############################################################################\n#*.cs     diff=csharp\n\n###############################################################################\n# Set the merge driver for project and solution files\n#\n# Merging from the command prompt will add diff markers to the files if there\n# are conflicts (Merging from VS is not affected by the settings below, in VS\n# the diff markers are never inserted). Diff markers may cause the following\n# file extensions to fail to load in VS. An alternative would be to treat\n# these files as binary and thus will always conflict and require user\n# intervention with every merge. To do so, just uncomment the entries below\n###############################################################################\n#*.sln       merge=binary\n#*.csproj    merge=binary\n#*.vbproj    merge=binary\n#*.vcxproj   merge=binary\n#*.vcproj    merge=binary\n#*.dbproj    merge=binary\n#*.fsproj    merge=binary\n#*.lsproj    merge=binary\n#*.wixproj   merge=binary\n#*.modelproj merge=binary\n#*.sqlproj   merge=binary\n#*.wwaproj   merge=binary\n\n###############################################################################\n# behavior for image files\n#\n# image files are treated as binary by default.\n###############################################################################\n#*.jpg   binary\n#*.png   binary\n#*.gif   binary\n\n###############################################################################\n# diff behavior for common document formats\n#\n# Convert binary document formats to text before diffing them. This feature\n# is only available from the command line. Turn it on by uncommenting the\n# entries below.\n###############################################################################\n#*.doc   diff=astextplain\n#*.DOC   diff=astextplain\n#*.docx  diff=astextplain\n#*.DOCX  diff=astextplain\n#*.dot   diff=astextplain\n#*.DOT   diff=astextplain\n#*.pdf   diff=astextplain\n#*.PDF   diff=astextplain\n#*.rtf   diff=astextplain\n#*.RTF   diff=astextplain\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# Users referenced in this file will automatically be requested as reviewers for PRs that modify the given paths.\n# See https://help.github.com/articles/about-code-owners/\n\n*   @MihaZupan @benjaminpetit\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.md",
    "content": "---\nname: Bug report\nabout: Create a report about something that is not working\nlabels: \"Type: Bug\"\n---\n\n### Describe the bug\nA clear and concise description of what the bug is.\n\n### To Reproduce\n<!--\nWhat steps can we follow to reproduce the issue?\n\n\nGot Exceptions? Include both the message and the stack trace\n-->\n\n### Further technical details\n\n- Include the version of the packages you are using\n- The platform (Linux/macOS/Windows)"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feedback.md",
    "content": "---\nname: Feedback\nabout: Tell us what you think!\nlabels: \"Type: Feedback\"\n---\n\n### Some details\n\n**How many backends are in your application?**\n\n- [ ] 1-2\n- [ ] 3-5\n- [ ] 6-10\n- [ ] 10+\n\n**How do you host your application?**\n\n- [ ] Kubernetes\n- [ ] Azure App Service\n- [ ] Azure VMs\n- [ ] Other Cloud Provider (please include details below)\n- [ ] Other Hosting model (please include details below)\n\n### What did you think of YARP?"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/idea.md",
    "content": "---\nname: Idea\nabout: Ideas, feature requests, and wishes\nlabels: \"Type: Idea\"\n---\n\n### What should we add or change to make your life better?\n\n### Why is this important to you?"
  },
  {
    "path": ".github/policies/resourceManagement.yml",
    "content": "id: \r\nname: GitOps.PullRequestIssueManagement\r\ndescription: GitOps.PullRequestIssueManagement primitive\r\nowner: \r\nresource: repository\r\ndisabled: false\r\nwhere: \r\nconfiguration:\r\n  resourceManagementConfiguration:\r\n    scheduledSearches: []\r\n    eventResponderTasks:\r\n    - if:\r\n      - payloadType: Pull_Request\r\n      - hasLabel:\r\n          label: Auto-Merge\r\n      then:\r\n      - enableAutoMerge:\r\n          mergeMethod: Squash\r\n      description: \r\n    - if:\r\n      - payloadType: Pull_Request\r\n      - labelRemoved:\r\n          label: Auto-Merge\r\n      then:\r\n      - disableAutoMerge\r\n      description: \r\n    - if:\r\n      - payloadType: Pull_Request\r\n      - isAction:\r\n          action: Opened\r\n      - isActivitySender:\r\n          user: dotnet-maestro[bot]\r\n          issueAuthor: False\r\n      - titleContains:\r\n          pattern: Update dependencies\r\n          isRegex: False\r\n      then:\r\n      - approvePullRequest:\r\n          comment: Auto-approving dependency update.\r\n      description: \r\nonFailure: \r\nonSuccess: \r\n"
  },
  {
    "path": ".github/workflows/docker_build.yml",
    "content": "name: Dockerfiles Build\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    steps:\n      # Check out the branch\n      - name: Checkout Code\n        uses: actions/checkout@v2\n      # Setup up builder\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v2\n      # Run a build for the Dockerfiles but don't publish\n      - name: Build Combined\n        id: docker_combined\n        uses: docker/build-push-action@v2\n        with:\n          context: ./\n          file: ./samples/KubernetesIngress.Sample/Combined/Dockerfile\n          push: false\n          tags: microsoft/yarp-combined:latest\n      - name: Build Ingress\n        id: docker_ingress\n        uses: docker/build-push-action@v2\n        with:\n          context: ./\n          file: ./samples/KubernetesIngress.Sample/Ingress/Dockerfile\n          push: false\n          tags: microsoft/yarp-ingress:latest\n      - name: Build Monitor\n        id: docker_monitor\n        uses: docker/build-push-action@v2\n        with:\n          context: ./\n          file: ./samples/KubernetesIngress.Sample/Monitor/Dockerfile\n          push: false\n          tags: microsoft/yarp-monitor:latest\n      - name: Build Backend\n        id: docker_backend\n        uses: docker/build-push-action@v2\n        with:\n          context: ./\n          file: ./samples/KubernetesIngress.Sample/backend/Dockerfile\n          push: false\n          tags: microsoft/yarp-backend:latest\n"
  },
  {
    "path": ".github/workflows/markdownlint-problem-matcher.json",
    "content": "{\n    \"problemMatcher\": [\n        {\n            \"owner\": \"markdownlint\",\n            \"pattern\": [\n                {\n                    \"regexp\": \"^([^:]*):(\\\\d+):?(\\\\d+)?\\\\s([\\\\w-\\\\/]*)\\\\s(.*)$\",\n                    \"file\": 1,\n                    \"line\": 2,\n                    \"column\": 3,\n                    \"code\": 4,\n                    \"message\": 5\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": ".github/workflows/markdownlint.yml",
    "content": "name: Markdownlint\n\npermissions:\n  contents: read\n\n# run even on changes without markdown changes, so that we can\n# make it in GitHub a required check for PR's\non:\n  pull_request:\n\njobs:\n  lint:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v6\n    - name: Use Node.js\n      uses: actions/setup-node@v6\n      with:\n        node-version: 'lts/*'\n    - name: Run Markdownlint\n      run: |\n        echo \"::add-matcher::.github/workflows/markdownlint-problem-matcher.json\"\n        npm i -g markdownlint-cli\n        markdownlint --ignore-path .github/workflows/markdownlintignore \"**/*.md\"\n"
  },
  {
    "path": ".github/workflows/markdownlintignore",
    "content": "# Ignore files under eng/common (managed by dotnet/arcade)\neng/common/\n"
  },
  {
    "path": ".gitignore",
    "content": "## Arcade specific things\n\n# Ignore the local '.dotnet' runtime directory and local '.packages' directory (only during CI builds, but still shouldn't ever be checked in)\n.dotnet/\n.packages/\n\n## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n# Rider \n.idea/\n\n# nohup\nnohup.out\n"
  },
  {
    "path": ".markdownlint.json",
    "content": "{\n    \"default\": true,\n    \"ul-indent\":                       false,\n    \"no-trailing-spaces\":              false,\n    \"line-length\":                     false,\n    \"blanks-around-headings\":          false,\n    \"no-duplicate-heading\":            { \"siblings_only\": true },\n    \"no-trailing-punctuation\":         false,\n    \"ol-prefix\":                       { \"one_or_ordered\": true },\n    \"blanks-around-fences\":            false,\n    \"blanks-around-lists\":             false,\n    \"no-inline-html\":                  { \"allowed_elements\": [ \"summary\", \"details\" ]},\n    \"no-bare-urls\":                    false,\n    \"single-trailing-newline\":         false,\n    \"emphasis-style\":                  false,\n    \"first-line-heading\":              false,\n    \"no-space-in-code\":                false,\n\n    // rule settings and options are documented in https://github.com/DavidAnson/markdownlint\n    // feel free to disable more low value rules in here; get rule name from the error message.\n    // the purpose of the linter is to catch significant issues like broken links.\n}"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Code of Conduct\n\nThis project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community.\nFor more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct)."
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# YARP Contribution Guide\n\nWe're excited to accept contributions, but like all open source projects need to set some guidelines on how and what to contribute. There can be nothing more frustrating than working on a change for some time, only to have it sitting forever as a PR. The purpose of this doc is to set expectations for how best to contribute so that YARP can benefit from the communities skills and knowledge.\n\n## General feedback and discussions?\nStart a discussion on the [repository issue tracker](https://github.com/dotnet/yarp/issues).\n\n## Issues\n\nWe love issues. Issues are the best place to discuss bugs, feature requests, ideas, designs etc. Issues are the way for everyone to communicate. We use issues to communicate across the team. Almost all contributions should start with an issue.\n\nIf the conversations on an issue wander off from the initial topic, and new ideas or issues get introduced, then those should be split off into separate issues. That makes it much easier to triage the issue as the decision can/should apply to the main concept, and those separate issues won't be lost and will also be considered. So if in doubt create a new issue, and add links from both the new and original issue to each other. The new issue can always be closed if it turns out to be a duplicate.\n\nIn particular, conversations on closed issues *won't cause an issue to be re-opened*, and are unlikely to be noticed, so please create a new issue and refer to the old one with a link.\n\n## Bugs\n\nAs long as humans write software, there will be bugs. If you find a bug, file an issue. \n\nThe line between a bug and a feature request or design change are tricky. For the purposes of this section a bug is where the code doesn't do what was intended by the design of the feature - it may be because of human error, or not considering cases that occur in the real world. If you feel confident about the fix, create a Pull Request (PR). Bug PR's should be small and uncontroversial, and therefore easily integrated.\n\n## Reporting security issues and bugs\nSecurity issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC)  secure@microsoft.com. You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the MSRC PGP key, can be found in the [Security TechCenter](https://technet.microsoft.com/en-us/security/ff852094.aspx).\n\n## Contributing code and content\n\nWe accept fixes and features! Here are some resources to help you get started on how to contribute code or new content.\n\n* Look at the [Contributor documentation](/docs/) to get started on building the source code on your own.\n* [\"Help wanted\" issues](https://github.com/dotnet/yarp/labels/help%20wanted) - these issues are up for grabs. Comment on an issue if you want to create a fix.\n* [\"Good first issue\" issues](https://github.com/dotnet/yarp/labels/good%20first%20issue) - we think these are a good for newcomers.\n\n### Identifying the scale\n\nIf you would like to contribute to one of our repositories, first identify the scale of what you would like to contribute. If it is small (grammar/spelling or a bug fix) feel free to start working on a fix. If you are submitting a feature or substantial code contribution, please discuss it with the team and ensure it follows the product roadmap. You might also read these two blogs posts on contributing code: [Open Source Contribution Etiquette](http://tirania.org/blog/archive/2010/Dec-31.html) by Miguel de Icaza and [Don't \"Push\" Your Pull Requests](https://www.igvita.com/2011/12/19/dont-push-your-pull-requests/) by Ilya Grigorik. All code submissions will be rigorously reviewed and tested by the YARP team, and only those that meet an extremely high bar for both quality and design/roadmap appropriateness will be merged into the source.\n\n### Roadmap\n\nOur primary focus is going to be on features and work items that are listed in the latest milestone for the next release.\nEvery feature should have an issue, where scoping and design will be discussed. If you wish to contribute, we'd prefer to agree to a design in the issue, before submitting a PR.\nThe last thing we want is for you to spend time working on a feature and then have the PR rejected or sit and get stale.\n\nThere is a cost to accepting PRs. We really appreciate your help and contributions but it takes time for us to review your code, and the team will be responsible for maintaining it.\nWe're happy to take a look at PRs contributing other features, but our focus will be on the work we already have planned.\nThose decisions aren't final though, and we change them over time as we learn new things. Feel free to file issues or comment on existing ones if you have new data to provide!\n\n### Extensibility\n\nOne of the primary goals of YARP is to be easily extensible. Each deployment situation will be different, and the features that are \"in the box\" may not do exactly what you need. The goal is for you to be able to insert additional modules in the pipeline, or replace a module to achieve the functionality that you need. The answer to many feature requests may be that it should be a custom module, rather than a change to the existing feature. In those cases, we are unlikely to want a PR for the module, but will be very interested in any changes to the core that enable the extensibility for you to achieve your scenario.\n\n### Submitting a pull request\n\nYou will need to sign a [Contributor License Agreement](https://cla.dotnetfoundation.org/) when submitting your pull request.\nTo complete the Contributor License Agreement (CLA), you will need to follow the instructions provided by the CLA bot when you send the pull request.\nThis needs to only be done once for any .NET Foundation OSS project.\n\nIf you don't know what a pull request is read this article: https://help.github.com/articles/using-pull-requests.\nMake sure the repository can build and all tests pass. Familiarize yourself with the project workflow and our coding conventions.\n\n### Feedback\n\nYour pull request will now go through extensive checks by the subject matter experts on our team. Please be patient; we have hundreds of pull requests across all of our repositories. Update your pull request according to feedback until it is approved by one of the YARP team members. All changes go through this process and a PR may go through multiple revisions until its accepted. This document has been through the same process, and if you look at the history there were probably multiple edits to each PR. After that, one of our team members may adjust the branch you merge into based on the expected release schedule.\n\n## Code of conduct\n\nSee [CODE-OF-CONDUCT.md](./CODE_OF_CONDUCT.md)\n"
  },
  {
    "path": "Directory.Build.props",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project>\n  <Import Project=\"Sdk.props\" Sdk=\"Microsoft.DotNet.Arcade.Sdk\" />\n  <Import Project=\"TFMs.props\" />\n\n  <PropertyGroup>\n    <Copyright>© Microsoft Corporation. All rights reserved.</Copyright>\n    <PackageIcon></PackageIcon>\n    <PackageIconFullPath></PackageIconFullPath>\n    <LangVersion>12.0</LangVersion>\n    <PackageLicenseExpression>MIT</PackageLicenseExpression>\n    <StrongNameKeyId>Microsoft</StrongNameKeyId>\n    <EmbedUntrackedSources>true</EmbedUntrackedSources>\n    <IncludeSymbols>true</IncludeSymbols>\n    <Deterministic>true</Deterministic>\n  </PropertyGroup>\n\n  <PropertyGroup>\n    <MoqPublicKey>0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7</MoqPublicKey>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "Directory.Build.targets",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project>\n  <Import Project=\"Sdk.targets\" Sdk=\"Microsoft.DotNet.Arcade.Sdk\" />\n\n  <ItemGroup Condition=\"'$(IsTestProject)' != 'true' and '$(IsSampleProject)' != 'true'\">\n    <EditorConfigFiles Include=\"$(MSBuildThisFileDirectory)eng\\CodeAnalysis.src.globalconfig\" />\n  </ItemGroup>\n\n  <ItemGroup Condition=\"'$(IsTestProject)' == 'true' or '$(IsSampleProject)' == 'true'\">\n    <EditorConfigFiles Include=\"$(MSBuildThisFileDirectory)eng\\CodeAnalysis.test.globalconfig\" />\n  </ItemGroup>\n</Project>"
  },
  {
    "path": "LICENSE.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) .NET Foundation and Contributors\n\nAll rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "NuGet.config",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n  <packageSources>\n    <clear />\n    <!-- Feed to use to restore the Arcade SDK from -->\n    <add key=\"dotnet-eng\" value=\"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json\" />\n    <!-- Feeds to use to restore dependent packages from -->\n    <add key=\"dotnet-public\" value=\"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json\" />\n    <add key=\"dotnet-tools\" value=\"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json\" />\n    <!-- Feed for benchmark infrastructure to restore Microsoft.NETCore.App.Runtime for self-contained builds -->\n    <add key=\"dotnet6\" value=\"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json\" />\n    <add key=\"dotnet7\" value=\"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json\" />\n    <add key=\"dotnet8\" value=\"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json\" />\n  </packageSources>\n  <disabledPackageSources>\n    <clear />\n  </disabledPackageSources>\n</configuration>\n"
  },
  {
    "path": "README.md",
    "content": "![YARP Icon](assets/icon.png)\n# Welcome to the YARP project\n\nYARP (which stands for \"Yet Another Reverse Proxy\") is a project to create a reverse proxy server. We found a bunch of internal teams at Microsoft who were either building a reverse proxy for their service or had been asking about APIs and tech for building one, so we decided to get them all together to work on a common solution, this project.\n\nYARP is a reverse proxy toolkit for building fast proxy servers in .NET using the infrastructure from ASP.NET and .NET. The key differentiator for YARP is that it's been designed to be easily customized and tweaked to match the specific needs of each deployment scenario. \n\nWe expect YARP to ship as a library and project template that together provide a robust, performant proxy server. Its pipeline and modules are designed so that you can then customize the functionality for your needs. For example, while YARP supports configuration files, we expect that many users will want to manage the configuration programmatically based on their own backend configuration management system, YARP will provide a configuration API to enable that customization in-proc.  YARP is designed with customizability as a primary scenario, rather than requiring you to break out to script or having to rebuild from source.\n\n# Getting started\n\n- See our [Getting Started](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/getting-started) docs.\n- Try our [previews](https://github.com/dotnet/yarp/releases).\n- Try our latest [daily build](/docs/DailyBuilds.md).\n- See our [support policy](/docs/roadmap.md).\n\n# Updates\n\nFor regular updates, see our [releases page](https://github.com/dotnet/yarp/releases). Subscribe to release notifications on this repository to be notified of future updates (Watch -> Custom -> Releases).\n\nIf you want to live on the bleeding edge, you can pickup the [daily builds](/docs/DailyBuilds.md).\n\n# Build\n\nTo build the repo, you should only need to run `build.cmd` (on Windows) or `build.sh` (on Linux or macOS). The script will download the .NET SDK and build the solution.\n\nFor VS on Windows, install the latest [VS 2022](https://visualstudio.microsoft.com/downloads/) release and then run the `startvs.cmd` script to launch Visual Studio using the appropriate local copy of the .NET SDK.\n\nTo set up local development with Visual Studio, Visual Studio for Mac or Visual Studio Code, you need to put the local copy of the .NET SDK in your `PATH` environment variable. Our `Restore` script fetches the latest build of .NET and installs it to a `.dotnet` directory *within* this repository.\n\nWe provide some scripts to set all this up for you. Just follow these steps:\n\n1. Run the `restore.cmd`/`restore.sh` script to fetch the required .NET SDK locally (to the `.dotnet` directory within this repo)\n1. \"Dot-source\" the `activate` script to put the local .NET SDK on the PATH\n    1. For PowerShell, run: `. .\\activate.ps1` (note the leading `. `, it is required!)\n    1. For Linux/macOS/WSL, run: `. ./activate.sh`\n    1. For CMD, there is no supported script. You can manually add the `.dotnet` directory **within this repo** to your `PATH`. Ensure `where dotnet` shows a path within this repository!\n1. Launch VS, VS for Mac, or VS Code!\n\nWhen you're done, you can run the `deactivate` function to undo the changes to your `PATH`.\n\nIf you're having trouble building the project, or developing in Visual Studio, please file an issue to let us know and we'll help out (and fix our scripts/tools as needed)!\n\n# Testing\n\nThe command to build and run all tests: `build.cmd/sh -test`.\nTo run specific test you may use XunitMethodName property: `dotnet build /t:Test /p:XunitMethodName={FullyQualifiedNamespace}.{ClassName}.{MethodName}`.\nThe tests can also be run from Visual Studio if launched using `startvs.cmd`.\n\n# Roadmap\n\nsee [docs/roadmap.md](/docs/roadmap.md)\n\n# Reporting security issues and bugs\n\nSecurity issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) at `secure@microsoft.com`. You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including [the MSRC PGP key](https://www.microsoft.com/msrc/pgp-key-msrc), can be found at the [Microsoft Security Response Center](https://www.microsoft.com/msrc).\n\n# Contributing\n\nThis project welcomes contributions and suggestions.\nCheck out the [contributing](CONTRIBUTING.md) page for more info.\n\nThis project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community.\nFor more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "<!-- BEGIN MICROSOFT SECURITY.MD V0.0.3 BLOCK -->\n\n## Security\n\nMicrosoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).\n\nIf you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below.\n\n## Reporting Security Issues\n\n**Please do not report security vulnerabilities through public GitHub issues.**\n\nInstead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).\n\nIf you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com).  If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).\n\nYou should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).\n\nPlease include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:\n\n  * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)\n  * Full paths of source file(s) related to the manifestation of the issue\n  * The location of the affected source code (tag/branch/commit or direct URL)\n  * Any special configuration required to reproduce the issue\n  * Step-by-step instructions to reproduce the issue\n  * Proof-of-concept or exploit code (if possible)\n  * Impact of the issue, including how an attacker might exploit the issue\n\nThis information will help us triage your report more quickly.\n\nIf you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.\n\n## Preferred Languages\n\nWe prefer all communications to be in English.\n\n## Policy\n\nMicrosoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).\n\n<!-- END MICROSOFT SECURITY.MD BLOCK -->\n"
  },
  {
    "path": "TFMs.props",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project>\n  <PropertyGroup>\n    <LatestDevTFM>net9.0</LatestDevTFM>\n    <ReleaseTFMs>net8.0</ReleaseTFMs>\n    <TestTFMs>net8.0;net9.0</TestTFMs>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "THIRD-PARTY-NOTICES.TXT",
    "content": "License notice for TlsFrame Parser\n-------------------------------\n\nhttps://github.com/dotnet/runtime/blob/master/LICENSE.txt\n\nThe MIT License (MIT)\n\nCopyright (c) .NET Foundation and Contributors\n\nAll rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "YARP.slnx",
    "content": "<Solution>\n  <Folder Name=\"/samples/\">\n    <Project Path=\"samples/BasicYarpSample/BasicYarpSample.csproj\" />\n    <Project Path=\"samples/ReverseProxy.Auth.Sample/ReverseProxy.Auth.Sample.csproj\" />\n    <Project Path=\"samples/ReverseProxy.Code.Sample/ReverseProxy.Code.Sample.csproj\" />\n    <Project Path=\"samples/ReverseProxy.Config.Sample/ReverseProxy.Config.Sample.csproj\" />\n    <Project Path=\"samples/ReverseProxy.ConfigFilter.Sample/ReverseProxy.ConfigFilter.Sample.csproj\" />\n    <Project Path=\"samples/ReverseProxy.Direct.Sample/ReverseProxy.Direct.Sample.csproj\" />\n    <Project Path=\"samples/ReverseProxy.LetsEncrypt.Sample/ReverseProxy.LetsEncrypt.Sample.csproj\" />\n    <Project Path=\"samples/ReverseProxy.Metrics.Sample/ReverseProxy.Metrics.Sample.csproj\" />\n    <Project Path=\"samples/ReverseProxy.Transforms.Sample/ReverseProxy.Transforms.Sample.csproj\" />\n    <Project Path=\"samples/SampleServer/SampleServer.csproj\" />\n  </Folder>\n  <Folder Name=\"/samples/HttpSys.Sample/\">\n    <Project Path=\"samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/ReverseProxy.HttpSysDelegation.Sample.csproj\" />\n    <Project Path=\"samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/SampleHttpSysServer.csproj\" />\n  </Folder>\n  <Folder Name=\"/samples/KubernetesIngress.Sample/\">\n    <File Path=\"samples/KubernetesIngress.Sample/README.md\" />\n  </Folder>\n  <Folder Name=\"/samples/KubernetesIngress.Sample/Combined/\">\n    <Project Path=\"samples/KubernetesIngress.Sample/Combined/Yarp.Kubernetes.IngressController.csproj\" />\n  </Folder>\n  <Folder Name=\"/samples/KubernetesIngress.Sample/Ingress/\">\n    <Project Path=\"samples/KubernetesIngress.Sample/Ingress/Yarp.Kubernetes.Ingress.csproj\" />\n    <Project Path=\"samples/KubernetesIngress.Sample/Monitor/Yarp.Kubernetes.Monitor.csproj\" />\n  </Folder>\n  <Folder Name=\"/samples/KubernetesIngress.Sample/Sample/\">\n    <Project Path=\"samples/KubernetesIngress.Sample/backend/backend.csproj\" />\n  </Folder>\n  <Folder Name=\"/samples/Prometheus/\">\n    <Project Path=\"samples/Prometheus/HttpLoadApp/HttpLoadApp.csproj\" />\n    <Project Path=\"samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/ReverseProxy.Metrics.Prometheus.Sample.csproj\" />\n  </Folder>\n  <Folder Name=\"/Solution Items/\">\n    <File Path=\".editorconfig\" />\n    <File Path=\"eng/Versions.props\" />\n    <File Path=\"global.json\" />\n    <File Path=\"NuGet.config\" />\n    <File Path=\"TFMs.props\" />\n  </Folder>\n  <Folder Name=\"/src/\">\n    <Project Path=\"src/Application/Yarp.Application.csproj\" />\n    <Project Path=\"src/Kubernetes.Controller/Yarp.Kubernetes.Controller.csproj\" />\n    <Project Path=\"src/ReverseProxy/Yarp.ReverseProxy.csproj\" />\n    <Project Path=\"src/TelemetryConsumption/Yarp.Telemetry.Consumption.csproj\" />\n  </Folder>\n  <Folder Name=\"/test/\">\n    <Project Path=\"test/Kubernetes.Tests/Yarp.Kubernetes.Tests.csproj\" />\n    <Project Path=\"test/ReverseProxy.FunctionalTests/Yarp.ReverseProxy.FunctionalTests.csproj\" />\n    <Project Path=\"test/ReverseProxy.Tests/Yarp.ReverseProxy.Tests.csproj\" />\n    <Project Path=\"test/Tests.Common/Yarp.Tests.Common.csproj\" />\n  </Folder>\n  <Folder Name=\"/testassets/\">\n    <Project Path=\"testassets/BenchmarkApp/BenchmarkApp.csproj\" />\n    <Project Path=\"testassets/ReverseProxy.Code/ReverseProxy.Code.csproj\" />\n    <Project Path=\"testassets/ReverseProxy.Config/ReverseProxy.Config.csproj\" />\n    <Project Path=\"testassets/ReverseProxy.Direct/ReverseProxy.Direct.csproj\" />\n    <Project Path=\"testassets/TestClient/TestClient.csproj\" />\n    <Project Path=\"testassets/TestServer/TestServer.csproj\" />\n  </Folder>\n</Solution>\n"
  },
  {
    "path": "activate.ps1",
    "content": "#\n# This file must be used by invoking \". .\\activate.ps1\" from the command line.\n# You cannot run it directly. See https://docs.microsoft.com/powershell/module/microsoft.powershell.core/about/about_scripts#script-scope-and-dot-sourcing\n#\n# To exit from the environment this creates, execute the 'deactivate' function.\n#\n\nif ($MyInvocation.CommandOrigin -eq 'runspace') {\n    Write-Host -f Red \"This script cannot be invoked directly.\"\n    Write-Host -f Red \"To function correctly, this script file must be 'dot sourced' by calling `\". $PSCommandPath`\" (notice the dot at the beginning).\"\n    exit 1\n}\n\nfunction deactivate ([switch]$init) {\n\n    # reset old environment variables\n    if (Test-Path variable:_OLD_PATH) {\n        $env:PATH = $_OLD_PATH\n        Remove-Item variable:_OLD_PATH\n    }\n\n    if (test-path function:_old_prompt) {\n        Set-Item Function:prompt -Value $function:_old_prompt -ea ignore\n        remove-item function:_old_prompt\n    }\n\n    Remove-Item env:DOTNET_ROOT -ea ignore\n    Remove-Item env:DOTNET_MULTILEVEL_LOOKUP -ea ignore\n    if (-not $init) {\n        # Remove the deactivate function\n        Remove-Item function:deactivate\n    }\n}\n\n# Cleanup the environment\ndeactivate -init\n\n$_OLD_PATH = $env:PATH\n# Tell dotnet where to find itself\n$env:DOTNET_ROOT = \"$PSScriptRoot\\.dotnet\"\n${env:DOTNET_ROOT(x86)} = \"$PSScriptRoot\\.dotnet\\x86\"\n# Tell dotnet not to look beyond the DOTNET_ROOT folder for more dotnet things\n$env:DOTNET_MULTILEVEL_LOOKUP = 0\n# Put dotnet first on PATH\n$env:PATH = \"${env:DOTNET_ROOT};${env:PATH}\"\n\n# Set the shell prompt\nif (-not $env:DISABLE_CUSTOM_PROMPT) {\n    $function:_old_prompt = $function:prompt\n    function dotnet_prompt {\n        # Add a prefix to the current prompt, but don't discard it.\n        write-host \"($( split-path $PSScriptRoot -leaf )) \" -nonewline\n        & $function:_old_prompt\n    }\n\n    Set-Item Function:prompt -Value $function:dotnet_prompt -ea ignore\n}\n\nWrite-Host -f Magenta \"Enabled the .NET Core environment. Execute 'deactivate' to exit.\"\nif (-not (Test-Path \"${env:DOTNET_ROOT}\\dotnet.exe\")) {\n    Write-Host -f Yellow \".NET Core has not been installed yet. Run $PSScriptRoot\\restore.cmd to install it.\"\n}\nelse {\n    Write-Host \"dotnet = ${env:DOTNET_ROOT}\\dotnet.exe\"\n}\n"
  },
  {
    "path": "activate.sh",
    "content": "#\n# This file must be used by invoking \"source activate.sh\" from the command line.\n# You cannot run it directly.\n# To exit from the environment this creates, execute the 'deactivate' function.\n\n_RED=\"\\033[0;31m\"\n_MAGENTA=\"\\033[0;95m\"\n_YELLOW=\"\\033[0;33m\"\n_RESET=\"\\033[0m\"\n\n# This detects if a script was sourced or invoked directly\n# See https://stackoverflow.com/a/28776166/2526265\nsourced=0\nif [ -n \"$ZSH_EVAL_CONTEXT\" ]; then\n  case $ZSH_EVAL_CONTEXT in *:file) sourced=1;; esac\n  THIS_SCRIPT=\"${0:-}\"\nelif [ -n \"$KSH_VERSION\" ]; then\n  [ \"$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)\" != \"$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})\" ] && sourced=1\n  THIS_SCRIPT=\"${0:-}\"\nelif [ -n \"$BASH_VERSION\" ]; then\n  (return 2>/dev/null) && sourced=1\n  THIS_SCRIPT=\"$BASH_SOURCE\"\nelse # All other shells: examine $0 for known shell binary filenames\n  # Detects `sh` and `dash`; add additional shell filenames as needed.\n  case ${0##*/} in sh|dash) sourced=1;; esac\n  THIS_SCRIPT=\"${0:-}\"\nfi\n\nif [ $sourced -eq 0 ]; then\n    printf \"${_RED}This script cannot be invoked directly.${_RESET}\\n\"\n    printf \"${_RED}To function correctly, this script file must be sourced by calling \\\"source $0\\\".${_RESET}\\n\"\n    exit 1\nfi\n\ndeactivate () {\n\n    # reset old environment variables\n    if [ ! -z \"${_OLD_PATH:-}\" ] ; then\n        export PATH=\"$_OLD_PATH\"\n        unset _OLD_PATH\n    fi\n\n    if [ ! -z \"${_OLD_PS1:-}\" ] ; then\n        export PS1=\"$_OLD_PS1\"\n        unset _OLD_PS1\n    fi\n\n    # This should detect bash and zsh, which have a hash command that must\n    # be called to get it to forget past commands.  Without forgetting\n    # past commands the $PATH changes we made may not be respected\n    if [ -n \"${BASH:-}\" ] || [ -n \"${ZSH_VERSION:-}\" ] ; then\n        hash -r 2>/dev/null\n    fi\n\n    unset DOTNET_ROOT\n    unset DOTNET_MULTILEVEL_LOOKUP\n    if [ ! \"${1:-}\" = \"init\" ] ; then\n        # Remove the deactivate function\n        unset -f deactivate\n    fi\n}\n\n# Cleanup the environment\ndeactivate init\n\nDIR=\"$( cd \"$( dirname \"$THIS_SCRIPT\" )\" && pwd )\"\n_OLD_PATH=\"$PATH\"\n# Tell dotnet where to find itself\nexport DOTNET_ROOT=\"$DIR/.dotnet\"\n# Tell dotnet not to look beyond the DOTNET_ROOT folder for more dotnet things\nexport DOTNET_MULTILEVEL_LOOKUP=0\n# Put dotnet first on PATH\nexport PATH=\"$DOTNET_ROOT:$PATH\"\n\n# Set the shell prompt\nif [ -z \"${DISABLE_CUSTOM_PROMPT:-}\" ] ; then\n    _OLD_PS1=\"$PS1\"\n    export PS1=\"(`basename \\\"$DIR\\\"`) $PS1\"\nfi\n\n# This should detect bash and zsh, which have a hash command that must\n# be called to get it to forget past commands.  Without forgetting\n# past commands the $PATH changes we made may not be respected\nif [ -n \"${BASH:-}\" ] || [ -n \"${ZSH_VERSION:-}\" ] ; then\n    hash -r 2>/dev/null\nfi\n\nprintf \"${_MAGENTA}Enabled the .NET Core environment. Execute 'deactivate' to exit.${_RESET}\\n\"\n\nif [ ! -f \"$DOTNET_ROOT/dotnet\" ]; then\n    printf \"${_YELLOW}.NET Core has not been installed yet. Run $DIR/restore.sh to install it.${_RESET}\\n\"\nelse\n    printf \"dotnet = $DOTNET_ROOT/dotnet\\n\"\nfi\n"
  },
  {
    "path": "azure-pipelines-nonprod.yml",
    "content": "variables:\n  # Needed for Arcade template\n- name: _TeamName\n  value: AspNetCore\n  # Needed for Microbuild template\n- name: TeamName\n  value: AspNetCore\n- name: DOTNET_SKIP_FIRST_TIME_EXPERIENCE\n  value: true\n- name: _PublishUsingPipelines\n  value: true\n- name: _BuildConfig\n  value: Release\n# Rely on task Arcade injects, not auto-injected build step.\n- template: /eng/common/templates/variables/pool-providers.yml@self\n- name: skipComponentGovernanceDetection\n  value: true\n\ntrigger:\n  batch: true\n  branches:\n    exclude:\n    - main\n    - release/*\n    - internal/release/*\n\npr:\n  autoCancel: false\n  branches:\n    include:\n    - '*'\n\nresources:\n  repositories:\n  - repository: MicroBuildTemplate\n    type: git\n    name: 1ESPipelineTemplates/MicroBuildTemplate\n    ref: refs/tags/release\n\nextends:\n  template: azure-pipelines/MicroBuild.1ES.Unofficial.yml@MicroBuildTemplate\n  parameters:\n    featureFlags:\n      autoBaseline: true\n    sdl:\n      sourceAnalysisPool:\n        name: $(DncEngInternalBuildPool)\n        image: 1es-windows-2022\n        os: windows\n      policheck:\n          enabled: true\n      tsa:\n        enabled: true\n      codeql:\n        tsaEnabled: true\n      binskim:\n        enabled: true\n        # See https://dev.azure.com/securitytools/SecurityIntegration/_wiki/wikis/Guardian/1378/Glob-Format\n        analyzeTargetGlob: '**\\bin\\Yarp.ReverseProxy\\**.dll;**\\bin\\Yarp.Telemetry.Consumption\\**.dll'\n        preReleaseVersion: '4.3.1'\n    stages:\n    - stage: build\n      displayName: Build\n      jobs:\n      - template: /eng/common/templates-official/jobs/jobs.yml@self\n        parameters:\n          enableMicrobuild: true\n          enablePublishBuildArtifacts: true\n          enablePublishTestResults: true\n          enablePublishBuildAssets: true\n          enablePublishUsingPipelines: ${{ variables._PublishUsingPipelines }}\n          enableTelemetry: true\n          mergeTestResults: true\n          jobs:\n          - job: Windows\n            pool:\n              name: $(DncEngInternalBuildPool)\n              image: 1es-windows-2022\n              os: windows\n            variables:\n            - group: Publish-Build-Assets\n            - name: _OfficialBuildArgs\n              value: /p:DotNetSignType=$(_SignType)\n                     /p:TeamName=$(_TeamName)\n                     /p:DotNetPublishUsingPipelines=$(_PublishUsingPipelines)\n                     /p:OfficialBuildId=$(BUILD.BUILDNUMBER)\n            - name: _SignType\n              value: real\n            steps:\n            - checkout: self\n              clean: true\n            - script: eng\\common\\cibuild.cmd -configuration $(_BuildConfig) -prepareMachine $(_OfficialBuildArgs)\n              displayName: Build and Publish\n            - task: 1ES.PublishBuildArtifacts@1\n              displayName: Upload TestResults\n              condition: always()\n              continueOnError: true\n              inputs:\n                PathtoPublish: artifacts/TestResults/$(_BuildConfig)/\n                ArtifactName: $(Agent.Os)_$(Agent.JobName) TestResults\n                PublishLocation: Container\n            - task: 1ES.PublishBuildArtifacts@1\n              displayName: Upload package artifacts\n              condition: and(succeeded(), eq(variables['system.pullrequest.isfork'], false), eq(variables['_BuildConfig'], 'Release'))\n              inputs:\n                PathtoPublish: artifacts/packages/\n                ArtifactName: artifacts\n                PublishLocation: Container\n    - template: /eng/common/templates-official/post-build/post-build.yml@self\n      parameters:\n        publishingInfraVersion: 3\n        enableSymbolValidation: false\n        enableSourceLinkValidation: false\n        enableSigningValidation: false\n        enableNugetValidation: false\n        SDLValidationParameters:\n          enable: true\n          continueOnError: false\n          params: ' -SourceToolsList @(\"policheck\",\"credscan\")\n          -TsaInstanceURL $(_TsaInstanceURL)\n          -TsaProjectName $(_TsaProjectName)\n          -TsaNotificationEmail $(_TsaNotificationEmail)\n          -TsaCodebaseAdmin $(_TsaCodebaseAdmin)\n          -TsaBugAreaPath $(_TsaBugAreaPath)\n          -TsaIterationPath $(_TsaIterationPath)\n          -TsaRepositoryName \"ReverseProxy\"\n          -TsaCodebaseName \"ReverseProxy\"\n          -TsaPublish $True -PoliCheckAdditionalRunConfigParams @(\"UserExclusionPath < $(Build.SourcesDirectory)/eng/PoliCheckExclusions.xml\")'\n"
  },
  {
    "path": "azure-pipelines-pr.yml",
    "content": "#\n# See https://docs.microsoft.com/azure/devops/pipelines/yaml-schema for details\n#\n\n\nvariables:\n  - name: _TeamName\n    value: AspNetCore\n  - name: DOTNET_SKIP_FIRST_TIME_EXPERIENCE\n    value: true\n  - name: _PublishUsingPipelines\n    value: true\n  - name: _BuildConfig\n    value: Release\n  - template: /eng/common/templates/variables/pool-providers.yml\n  # Rely on task Arcade injects, not auto-injected build step.\n  - name: skipComponentGovernanceDetection\n    value: true\n  # Rely on explicit tasks, not auto-injected build steps. CodeQL3000 not enabled by default in any case.\n  - name: Codeql.SkipTaskAutoInjection\n    value: true\n\npr:\n  autoCancel: false\n  branches:\n    include:\n    - '*'\n\nstages:\n- stage: build\n  displayName: Build\n  jobs:\n  - template: /eng/common/templates/jobs/jobs.yml\n    parameters:\n      enableMicrobuild: true\n      enablePublishBuildArtifacts: true\n      enablePublishTestResults: true\n      enablePublishBuildAssets: true\n      enablePublishUsingPipelines: true\n      enableTelemetry: true\n      mergeTestResults: true\n      jobs:\n      - job: Windows\n        pool:\n          name: $(DncEngPublicBuildPool)\n          demands: ImageOverride -equals windows.vs2019.amd64.open\n        variables:\n          - name: _OfficialBuildArgs\n            value: ''\n          - name: _SignType\n            value: test\n\n        steps:\n        - checkout: self\n          clean: true\n        - script: eng\\common\\cibuild.cmd -configuration $(_BuildConfig) -prepareMachine $(_OfficialBuildArgs)\n          displayName: Build and Publish\n        - task: PublishBuildArtifacts@1\n          displayName: Upload TestResults\n          condition: always()\n          continueOnError: true\n          inputs:\n            pathtoPublish: artifacts/TestResults/$(_BuildConfig)/\n            artifactName: $(Agent.Os)_$(Agent.JobName) TestResults\n            artifactType: Container\n            parallel: true\n        - task: PublishBuildArtifacts@1\n          displayName: Upload package artifacts\n          condition: and(succeeded(), eq(variables['system.pullrequest.isfork'], false), eq(variables['_BuildConfig'], 'Release'))\n          inputs:\n            pathtoPublish: artifacts/packages/\n            artifactName: artifacts\n            artifactType: Container\n            parallel: true\n\n      - job: Ubuntu\n        pool:\n          vmImage: ubuntu-latest\n        variables:\n        - name: _SignType\n          value: none\n        steps:\n        - checkout: self\n          clean: true\n        - script: eng/common/cibuild.sh\n            --configuration $(_BuildConfig)\n            --prepareMachine\n          displayName: Build\n        - task: PublishBuildArtifacts@1\n          displayName: Upload TestResults\n          condition: always()\n          continueOnError: true\n          inputs:\n            pathtoPublish: artifacts/TestResults/$(_BuildConfig)/\n            artifactName: $(Agent.Os)_$(Agent.JobName) TestResults\n            artifactType: Container\n            parallel: true\n\n      - job: macOS_latest\n        displayName: 'macOS latest'\n        pool:\n          vmImage: macOS-latest\n        variables:\n        - name: _SignType\n          value: none\n        steps:\n        - checkout: self\n          clean: true\n        - script: eng/common/cibuild.sh\n            --configuration $(_BuildConfig)\n            --prepareMachine\n          displayName: Build\n        - task: PublishBuildArtifacts@1\n          displayName: Upload TestResults\n          condition: always()\n          continueOnError: true\n          inputs:\n            pathtoPublish: artifacts/TestResults/$(_BuildConfig)/\n            artifactName: $(Agent.Os)_$(Agent.JobName) TestResults\n            artifactType: Container\n            parallel: true\n"
  },
  {
    "path": "azure-pipelines.yml",
    "content": "variables:\n  # Needed for Arcade template\n- name: _TeamName\n  value: AspNetCore\n  # Needed for Microbuild template\n- name: TeamName\n  value: AspNetCore\n- name: DOTNET_SKIP_FIRST_TIME_EXPERIENCE\n  value: true\n- name: _PublishUsingPipelines\n  value: true\n- name: _BuildConfig\n  value: Release\n# Rely on task Arcade injects, not auto-injected build step.\n- template: /eng/common/templates/variables/pool-providers.yml@self\n- name: skipComponentGovernanceDetection\n  value: true\n\ntrigger:\n  batch: true\n  branches:\n    include:\n    - main\n    - release/*\n    - internal/release/*\n\nresources:\n  repositories:\n  - repository: MicroBuildTemplate\n    type: git\n    name: 1ESPipelineTemplates/MicroBuildTemplate\n    ref: refs/tags/release\n\nextends:\n  template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate\n  parameters:\n    featureFlags:\n      autoBaseline: true\n    sdl:\n      sourceAnalysisPool:\n        name: $(DncEngInternalBuildPool)\n        image: 1es-windows-2022\n        os: windows\n      policheck:\n          enabled: true\n      tsa:\n        enabled: true\n      codeql:\n        tsaEnabled: true\n      binskim:\n        enabled: true\n        # See https://dev.azure.com/securitytools/SecurityIntegration/_wiki/wikis/Guardian/1378/Glob-Format\n        analyzeTargetGlob: '**\\bin\\Yarp.ReverseProxy\\**.dll;**\\bin\\Yarp.Telemetry.Consumption\\**.dll'\n        preReleaseVersion: '4.3.1'\n    stages:\n    - stage: build\n      displayName: Build\n      jobs:\n      - template: /eng/common/templates-official/jobs/jobs.yml@self\n        parameters:\n          enableMicrobuild: true\n          enablePublishBuildArtifacts: true\n          enablePublishTestResults: true\n          enablePublishBuildAssets: true\n          enablePublishUsingPipelines: ${{ variables._PublishUsingPipelines }}\n          enableTelemetry: true\n          mergeTestResults: true\n          jobs:\n          - job: Windows\n            pool:\n              name: $(DncEngInternalBuildPool)\n              image: 1es-windows-2022\n              os: windows\n            variables:\n            - group: Publish-Build-Assets\n            - name: _OfficialBuildArgs\n              value: /p:DotNetSignType=$(_SignType)\n                     /p:TeamName=$(_TeamName)\n                     /p:DotNetPublishUsingPipelines=$(_PublishUsingPipelines)\n                     /p:OfficialBuildId=$(BUILD.BUILDNUMBER)\n            - name: _SignType\n              value: real\n            steps:\n            - checkout: self\n              clean: true\n            - script: eng\\common\\cibuild.cmd -configuration $(_BuildConfig) -prepareMachine $(_OfficialBuildArgs)\n              displayName: Build and Publish\n            - task: 1ES.PublishBuildArtifacts@1\n              displayName: Upload TestResults\n              condition: always()\n              continueOnError: true\n              inputs:\n                PathtoPublish: artifacts/TestResults/$(_BuildConfig)/\n                ArtifactName: $(Agent.Os)_$(Agent.JobName) TestResults\n                PublishLocation: Container\n            - task: 1ES.PublishBuildArtifacts@1\n              displayName: Upload package artifacts\n              condition: and(succeeded(), eq(variables['system.pullrequest.isfork'], false), eq(variables['_BuildConfig'], 'Release'))\n              inputs:\n                PathtoPublish: artifacts/packages/\n                ArtifactName: artifacts\n                PublishLocation: Container\n    - template: /eng/common/templates-official/post-build/post-build.yml@self\n      parameters:\n        publishingInfraVersion: 3\n        enableSymbolValidation: false\n        enableSourceLinkValidation: false\n        enableSigningValidation: false\n        enableNugetValidation: false\n        SDLValidationParameters:\n          enable: true\n          continueOnError: false\n          params: ' -SourceToolsList @(\"policheck\",\"credscan\")\n          -TsaInstanceURL $(_TsaInstanceURL)\n          -TsaProjectName $(_TsaProjectName)\n          -TsaNotificationEmail $(_TsaNotificationEmail)\n          -TsaCodebaseAdmin $(_TsaCodebaseAdmin)\n          -TsaBugAreaPath $(_TsaBugAreaPath)\n          -TsaIterationPath $(_TsaIterationPath)\n          -TsaRepositoryName \"ReverseProxy\"\n          -TsaCodebaseName \"ReverseProxy\"\n          -TsaPublish $True -PoliCheckAdditionalRunConfigParams @(\"UserExclusionPath < $(Build.SourcesDirectory)/eng/PoliCheckExclusions.xml\")'\n"
  },
  {
    "path": "build.cmd",
    "content": "@echo off\npowershell -ExecutionPolicy ByPass -NoProfile -command \"& \"\"\"%~dp0eng\\common\\Build.ps1\"\"\" -restore -build %*\"\n"
  },
  {
    "path": "build.sh",
    "content": "#!/usr/bin/env bash\n\nsource=\"${BASH_SOURCE[0]}\"\n\n# resolve $SOURCE until the file is no longer a symlink\nwhile [[ -h $source ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\n\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\"$scriptroot/eng/common/build.sh\" --build --restore $@\n"
  },
  {
    "path": "docs/DailyBuilds.md",
    "content": "How to get daily builds of YARP\n===============================\n\nDaily builds include the latest source code changes. They are not supported for production use and are subject to frequent changes, but we strive to make sure daily builds function correctly.\n\nIf you want to download the latest daily build and use it in a project, then you need to:\n\n- Obtain the latest [build of the .NET Core SDK](https://github.com/dotnet/sdk#installing-the-sdk).\n- Add a NuGet.Config to your project directory with the following content:\n\n  ```xml\n  <?xml version=\"1.0\" encoding=\"utf-8\"?>\n  <configuration>\n      <packageSources>\n          <clear />\n          <add key=\".NET Libraries Daily\" value=\"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json\" />\n          <!-- The .NET Libraries Transport Daily feed is only needed for the Yarp.Kubernetes.Controller package -->\n          <add key=\".NET Libraries Transport Daily\" value=\"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries-transport/nuget/v3/index.json\" />\n          <add key=\"NuGet.org\" value=\"https://api.nuget.org/v3/index.json\" />\n      </packageSources>\n  </configuration>\n  ```\n\n  *NOTE: This NuGet.Config should be with your application unless you want nightly packages to potentially start being restored for other apps on the machine.*\n\nThen follow the [Getting Started](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/getting-started) guide to set up a project and add the nuget package dependency. Note daily builds use a higher preview version than given in the docs.\n\nSome features, such as new target frameworks, may require prerelease tooling builds for Visual Studio.\nThese are available in the [Visual Studio Preview](https://www.visualstudio.com/vs/preview/).\n\nTo debug daily builds using Visual Studio\n------------------------------------------\n\n- *Enable Source Link support* in Visual Studio should be enabled.\n- *Enable source server support* in Visual should be enabled.\n- *Enable Just My Code* should be disabled\n- Under Symbols enable the *Microsoft Symbol Servers* setting.\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Docs Folder\n\nThis folder contains:\n\n* [Design Notes](designs/) - These are the design notes used to guide our development. They aren't designed to be usage guides but may help contributors in understanding why some patterns were used.\n* [Operations](operations/) - These are operational docs for running releases and other infrastructure related to the project.\n\nPublic documentation is available at [learn.microsoft.com](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/getting-started)."
  },
  {
    "path": "docs/designs/README.md",
    "content": "# Design Meeting Notes\n\nRough meeting notes from design meetings."
  },
  {
    "path": "docs/designs/config.md",
    "content": "# Config based proxy apps\n\n> [!CAUTION]\n> These are archived design discussions. Information may be outdated and inaccurate.\n\nRE: https://github.com/dotnet/yarp/issues/9\n\nConfig based proxies are common and we'll need to support at least basic proxy scenarios from config. Here are some initial considerations:\n\n- Config sources and systems\n- Define routes based on host and/or path\n- List multiple back-ends per route for load balancing\n- A restart should not be needed to pick up config changes\n- You should be able to augment a route's configuration in code. Kestrel does something similar using named endpoints.\n\n## Config systems:\n\nWe have three relevant components that already have config systems: Kestrel, UrlRewrite, and ReverseProxy.\n\n- [Kestrel](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/endpoints)\n- [UrlRewrite](https://github.com/dotnet/aspnetcore/blob/f4d81e3af2b969744a57d76d4d622036ac514a6a/src/Middleware/Rewrite/sample/UrlRewrite.xml#L1-L11)\n- [ReverseProxy](https://github.com/dotnet/yarp/blob/b2cf5bdddf7962a720672a75f2e93913d16dfee7/samples/IslandGateway.Sample/appsettings.json#L10-L34)\n\nProposals:\n- The Kestrel config and the Proxy/Gateway config should remain adjacent, not merged. Inbound and outbound are distinct concerns. As long as both are available in the same broader config system then that's close enough.\n- UrlRewrite should also remain as is. It's not ideal that it's in a separate file and format from the rest of the config, but we'll wait and see if that is a long term blocker.\n\n## Route config:\n\nThe proxy has a [config mechanism](https://github.com/dotnet/yarp/blob/b2cf5bdddf7962a720672a75f2e93913d16dfee7/samples/IslandGateway.Sample/appsettings.json#L26-L32) to define routes and map those to back end groups.\n```json\n      \"Routes\": [\n        {\n          \"RouteId\": \"backend1/route1\",\n          \"BackendId\": \"backend1\",\n          \"Rule\": \"Host('localhost') && Path('/{**catchall}')\"\n        }\n      ]\n```\nThis maps to a [ProxyRoute](https://github.com/dotnet/yarp/blob/b2cf5bdddf7962a720672a75f2e93913d16dfee7/src/IslandGateway.Core/Abstractions/RouteDiscovery/Contract/GatewayRoute.cs) type.\n\nThis basic structure is useful though the \"Rule\" [system](https://github.com/dotnet/yarp/blob/b2cf5bdddf7962a720672a75f2e93913d16dfee7/src/IslandGateway.Core/Service/Config/RuleParsing/RuleParser.cs) seems overly complex. Need to circle back with DavidN on this. We may be able to simplify that down to independent keys for matching Host, Path, Header, etc.. It's not clear that the additional `&&` or `||` aspects are necessary here. If we used separate properties then it would be implicitly `&&` based. To achieve `||` you'd define additional routes. This is also an area where augmenting with code defined constraints could be useful to handle the more complex scenarios. \n\nThe ProxyRoute.Metadata dictionary may be able to be replaced or supplemented by giving direct access to the config node for that route. Compare to Kestrel's [EndpointConfig.ConfigSection](https://github.com/dotnet/aspnetcore/blob/f4d81e3af2b969744a57d76d4d622036ac514a6a/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs#L168-L175) property. That would allow for augmenting an endpoint with additional complex custom entries that the app code can reference for additional config actions.\n\nUpdate: The custom rule system was modified by [#24](https://github.com/dotnet/yarp/pull/24) so that the config now looks like this:\n```json\n       \"Routes\": [\n         {\n           \"RouteId\": \"backend1/route1\",\n           \"BackendId\": \"backend1\",\n           \"Match\": {\n             \"Methods\": [ \"GET\", \"POST\" ],\n             \"Host\": \"localhost\",\n             \"Path\": \"/{**catchall}\"\n           }\n         }\n       ]\n```\n\n## Backend configuration\n\nThe proxy code defines the types [Backend](https://github.com/dotnet/yarp/blob/b2cf5bdddf7962a720672a75f2e93913d16dfee7/src/IslandGateway.Core/Abstractions/BackendDiscovery/Contract/Backend.cs) and [BackendEndpoint](https://github.com/dotnet/yarp/blob/b2cf5bdddf7962a720672a75f2e93913d16dfee7/src/IslandGateway.Core/Abstractions/BackendEndpointDiscovery/Contract/BackendEndpoint.cs) and allows these to be defined via config and referenced by name from routes.\n\nA BackendEndpoint defines a specific service instance with an id, address, and associated metadata.\n\nA Backend is a collection of one or more BackendEndpoints and a set of policies for choosing which endpoint to rout each request to (load balancing, circuit breakers, health checks, affinities, etc.). This seems a bit monolithic compared to our initial design explorations. We anticipate wanting to break these policies up into distinct steps in a pipeline to make them more replaceable. That said, we'll still need a config model for the default set of components and it may look very much like what's already here.\n\nQuestion: Why are the backends and the endpoints listed separately in config rather than nested? Object model links endpoints 1:1 with backends, so there doesn't seem to be a reason to list them separately.\n\nExisting:\n```json\n      \"Backends\": [\n        {\n          \"BackendId\": \"backend1\"\n        },\n        {\n          \"BackendId\": \"backend2\"\n        }\n      ],\n      \"Endpoints\": {\n        \"backend1\": [\n          {\n            \"EndpointId\": \"backend1/endpoint1\",\n            \"Address\": \"https://localhost:10000/\"\n          }\n        ],\n        \"backend2\": [\n          {\n            \"EndpointId\": \"backend2/endpoint1\",\n            \"Address\": \"https://localhost:10001/\"\n          }\n        ]\n      },\n```\nNested:\n```json\n      \"Backends\": [\n        {\n          \"BackendId\": \"backend1\",\n          \"Endpoints\": [\n            {\n              \"EndpointId\": \"backend1/endpoint1\",\n              \"Address\": \"https://localhost:10000/\"\n            }\n          ],\n        },\n        {\n          \"BackendId\": \"backend2\"\n          \"Endpoints\": [\n            {\n              \"EndpointId\": \"backend2/endpoint1\",\n              \"Address\": \"https://localhost:10001/\"\n            }\n          ],\n        }\n      ],\n```\nAdditional feedback: Why is it using arrays instead of objects? These items are not order sensitive, and they already have id properties anyways.\n```json\n      \"Backends\": {\n        \"backend1\" : {\n          \"Endpoints\": [\n            \"endpoint1\": {\n              \"Address\": \"https://localhost:10000/\"\n            }\n          },\n        },\n        \"backend2\": {\n          \"Endpoints\": {\n            \"endpoint1\": {\n              \"Address\": \"https://localhost:10001/\"\n            }\n          },\n        }\n      },\n```\n\nUpdate: The backend and endpoint layout has been modified to the following:\n```json\n\n    \"Backends\": {\n      \"backend1\": {\n        \"Endpoints\": {\n          \"backend1/endpoint1\": {\n            \"Address\": \"https://localhost:10000/\"\n          }\n        }\n      },\n      \"backend2\": {\n        \"Endpoints\": {\n          \"backend2/endpoint1\": {\n            \"Address\": \"https://localhost:10001/\"\n          }\n        }\n      }\n    },\n```\n\n## Config reloading\n\nConfig reloading is not yet a blocking requirement but we do expect to need it in the future. This design needs to factor in how reloading might work when it does get added.\n\n**NOTE** The proxy code has a concept of Signals that is used to convey config change. We need to see how this integrates with change notifications from our config sources and flows through the system.\n\nThe Extensions config and options systems have support for change detection and reloading but very few components take advantage of it. Logging is the primary consumer today.\n\nOne concern is that some change notification sources like files can trigger multiple times for a single event. The config system does not have built in handling for this, it's up to consumers to 'debounce' and filter out redundant notifications.\n\nKestrel support for reloading config is tracked by https://github.com/dotnet/aspnetcore/issues/19376.\n\nReloading proxy config will need to happen atomically and avoid disrupting requests already in flight. We may need to rebuild portions of the app pipeline and swap them out for new requests, drain the old requests, and clean up the old pipelines. We also want to avoid a full reset for small config changes where possible. E.g. if only one route changes then ideally we'd only rebuild that route.\n\nReloading should be something you can opt into or out of. Right now this is only possible at the config level by opting in or out for a config source, but that affects the whole app.\n\nUpdates:\n\nConfig reload for proxy routes, backends, and endpoints already works. You edit appsettings.json and it automatically reloads and reconfigures the routes. Note the config change notification usually gets fired twice and logs \"Applying proxy configs\" each time, but the change diff logic prevents an unnecessary update the second time. We may still want to do some debounce detection to prevent extra config diffs, but that's lower priority.\n\n@halter73 raised the question of how much effort we put in to make the kestrel reload atomic with the routing reload? Conceptually it makes sense to keep the two in sync, but programmatically it's quite difficult as there's no connection between the two systems. They'd be reacting to the same change notification event in serial and requests in flight may see one set of changes without the other. We discussed this in the weekly sync and decided that since kestrel endpoint changes will be a less common scenario we won't initually worry about the atomicity here until we have customer feedback that demonstrates issues.\n\nAlso, when a kestrel endpoint is modified or removed should existing connections on that endpoint be eagerly drained and closed, or should they be allowed to run a normal lifecycle? Kestrel does not currently track active connection per endpoint so additional tracking would be needed if we wanted to shut them down.\n\nUpdates:\n\nThe config reload code has been moved from the sample into the product assemblies.\n\n## Augmenting config via code\n\nSome things are easier to do in code and we want to be able to support that while still pulling more transient data from config. [Kestrel](https://github.com/dotnet/aspnetcore/blob/aff01ebd7f82d641a4cfbd4a34954300311d9c2b/src/Servers/Kestrel/samples/SampleApp/Startup.cs#L138-L147) has a model where endpoints are named in config and then can be reference by name in code for additional configuration.\n```json\n{\n  \"Kestrel\": {\n    \"Endpoints\": {\n      \"NamedEndpoint\": { \"Url\": \"http://*:6000\" },\n      \"NamedHttpsEndpoint\": {\n        \"Url\": \"https://*:6443\",\n      }\n    }\n  }\n}\n```\n```csharp\n    options.Endpoint(\"NamedEndpoint\", opt =>\n    {\n\n    })\n    .Endpoint(\"NamedHttpsEndpoint\", opt =>\n    {\n        opt.HttpsOptions.SslProtocols = SslProtocols.Tls12;\n    });\n```\n\nThe proxy code already has named routes, backends, backend endpoints, etc., so we should be able to build a similar code augmentation for those.\n\nReloadable config complicates this pattern. The code augmentation actions will need to be captured for the lifetime of the app rather than just for startup so they can be re-run later.\n"
  },
  {
    "path": "docs/designs/route-extensibility.md",
    "content": "## Problem Statement\n\n> [!CAUTION]\n> These are archived design discussions. Information may be outdated and inaccurate.\n\nToday if you want to extend the route or clusters, you can only do it through the metadata property on each, which is a Dictionary<string, string>. If you want to be able to have structured data its not possible without you forcing it into a string and then parsing it when needed. There are scenarios like A/B testing, or authenticating with back end servers (not pass thru) where you want to be able to store a structure of data in config, and have it available at runtime on the route/cluster objects.\n\nIf we want there to be pre-built extensions to YARP (#1714), then there needs to be a way for each of the extensions to have its own config data on routes and clusters, and for them to not step on each others toes.\n\n## Why is this important to you?\n\nTaking a canonical example of A/B testing. In this scenario you want to be able to direct traffic to multiple clusters with the traffic patterns determined based on additional criteria. For example, You may want to be able to have a collection of clusters that are used for a route, together with percentages. So the config could look something like:\n\n```json\n{\n  \"ReverseProxy\": {\n    \"Routes\": {\n      \"route1\": {\n        \"ClusterId\": \"ignore\",\n        \"Match\": {\n          \"Path\": \"{**catch-all}\"\n        },\n        \"Extensions\": {\n          \"A-B\": [\n            {\n              \"ClusterId\": \"c1\",\n              \"Load\": 0.4\n            },\n            {\n              \"ClusterId\": \"c2\",\n              \"Load\": 0.6\n            },\n            {\n              \"ClusterId\": \"experimental\",\n              \"Load\": 0.01\n            },\n            {\n              \"ClusterId\": \"TelemetrySample\",\n              \"Load\": 0.01\n            }\n          ]\n        }\n      },\n```\nThe above example is adding an \"A-B\" extension to the route with its own data. There could be other extensions each of which have their own configuration data.\n\n## What would this look like\n\n### Requirements\n* Multiple extensions can be added to YARP, each of which can have their own config state.\n* Be able to have arbitrary data as part of config for routes and/or clusters\n  * YARP should not dictate a specific data structure for the extension config \n* Be able to access that data as objects from middleware directly off the route and clusters\n* Be able to remote that data in the case of a distributed configuration server\n\n## Proposal\n\n* Add an Extensions collection to Route and Cluster. This should follow the same pattern as http features, using an `IReadOnlyDictionary<Type, object>` and be accessed based on the type of the extension. This would then be accessible in proxy middleware from the route or cluster objects via an `Extensions` property:\n\n```c#\npublic void Configure(IApplicationBuilder app, IProxyStateLookup lookup)\n{\n    app.UseRouting();\n    app.UseEndpoints(endpoints =>\n    {\n        // We can customize the proxy pipeline and add/remove/replace steps\n        endpoints.MapReverseProxy(proxyPipeline =>\n        {\n            // Use a custom proxy middleware, defined below\n            proxyPipeline.Use((context, next) =>\n            {\n                var proxyFeature = context.Features.Get<IReverseProxyFeature>();\n                var abstate = proxyFeature.Route.Extensions[typeof(ABState)];\n                var newClusterName = abstate.SelectSlice(new Random().NextDouble());\n                if (lookup?.TryGetCluster(newClusterName, out var cluster))\n                {\n                    context.ReassignProxyRequest(cluster);\n                }\n                return next();\n            });\n            proxyPipeline.UseSessionAffinity();\n            proxyPipeline.UseLoadBalancing();\n        });\n    });\n}\n```\n\nUsing a dictionary based on object type enables easy access to the object at runtime, and a strongly typed result.\n\n* There needs to be a way for YARP to construct these strongly typed extensions based on the `IConfiguration` provider. There needs to be a mapping between the key in config, and the type that will be used to store the data. That mapping is handled by enabling factories that can be registered:\n```c#\nservices.AddReverseProxy()\n    .LoadFromConfig(Configuration.GetSection(\"ReverseProxy\"))\n    .AddRouteExtension(\"A-B\", (section, _, _) => new ABState(section));\n```\n  > Along with a similar mechanism for clusters. The extension registration would be something like:\n```c#\nstatic IReverseProxyBuilder AddRouteExtension(this IReverseProxyBuilder builder, string sectionName, Func<IConfigurationSection, RouteConfig, ExtensionType, ExtensionType> factory)  \n```\n\n> Where the factory is passed:\n  * The IConfigurationSection for the extension\n  * The route object that is being extended\n  * The existing extension instance in the case of configuration updates\n>\n> When the configuration is parsed, the factory is called based on the configuration key name, and the resultant object is added to the route/cluster objects.\n>\n> Regular YARP config will do a diff merge to handle config changes, and create new objects if applicable. The same mechanism needs to be used for extensions. If the configuration is updated, and an existing instance of the extension exists, then it will be passed to the factory. The factory can compare the current instance and re-use it, or copy its data across to a new instance based on the changes. Instances must stable, so existing instances shouldn't be modified if it would affect existing in-flight requests. YARP can't really enforce rules on how the objects are changed as we want the types to be user defined. \n\n* When using a custom config provider, the Extensions collection can be populated by the custom provider directly, or the provider can expose an `IConfigurationSection` implementation and use the factory as described below.\n\n* When YARP is integrated with 3rd party config systems, such as K8s or ServiceFabric, those systems typically have a way of expressing custom properties, some of which will be used by YARP for the definition of routes/ clusters etc. To facilitate the ability for route and cluster extensions to be expressed within those systems, the integration provider should expose an `IConfigurationSection` implementation that maps between the integrated persistence format and YARP. \n  `IConfigurationSection` is essentially a name/value lookup API, it should map pretty reasonably to the YAML or JSON formats used by the configuration systems, and not be an undue burden to implement on these integrations.\n\n* Integration with 3rd party config systems can involve a remote process that YARP can pull its configuration from. I am [proposing another feature](https://github.com/dotnet/yarp/issues/1710), that we formalize this pattern and have the ability to create a central YARP config provider, to which multiple YARP proxies can bind. This enables scalability in terms of being able to push config to multiple instances of YARP at once.\n\n* To support this scenario, we should serialize the IConfiguration data and pass that across to the proxy instances.\n"
  },
  {
    "path": "docs/designs/yarp-tunneling.md",
    "content": "# YARP Tunneling\n\n> [!CAUTION]\n> These are archived design discussions. Information may be outdated and inaccurate.\n\n## Introduction\nWhile many organizations are moving their computing to the cloud, there are occasions where you need to be able to run some services in a local datacenter. The problem then is if you need to be able to communicate with those services from the cloud. Creating a VPN network connection to Azure or other cloud provider is possible, but usually involves a lot of red tape and configuration complexity as the two networks need to be integrated.\n\nIf all that the cloud needs to access is resources that are exposed over http, then a simpler solution is to have a gateway that can route traffic to the remote resource. Additionally, outbound connections over http(s) are not usually blocked, so having a on-prem gateway make an outbound connection to the cloud, is the easiest way to establish the route. This is the basis behind the [Azure Relay](https://learn.microsoft.com/azure/azure-relay/relay-what-is-it) service offering.\n\nThat is the principle of the tunnel feature for YARP. You operate two instances of the YARP proxy service, configured as a tunnel. The advantage over Azure Relay is that using a reverse proxy as an on-prem gateway means that both cloud and back end services can be used without needing to update the application other than addresses. This is particularly useful for services that may have been written by a 3rd party, or are no longer under active development, and so making changes to the configuration is complicated and expensive. Relay requires the sender and receiver to be updated to use its connection protocol.\n\n![Tunnel diagram](https://github.com/assets/95136/52d7491b-6e8a-4a2c-a51d-0734b3e41930)\n\nIn the on-prem data center, you run an instance of YARP, we'll call this the back-end proxy. This is configured with routes to the resources that should be externally accessible - only routes that are configured via this proxy will be exposed. The back-end proxy is configured to create a tunnel connection to the front-end instance by specifying the connection URL and security details for the connection.\n\nThe instance in the cloud, we'll refer to as the front-end, will be configured with a tunnel endpoint URL to be used by the on-prem proxy. The on-prem proxy will create a websocket connection to the tunnel endpoint, this will map the tunnel to a specific cluster. Routes can be directed to use the tunnel connection to the back-end by using the cluster that is used for the tunnel.\n\n## Tunnel Protocol\n\nThe tunnel will establish a Websockets connection between the back-end and the front-end. The back-end will establish the connection so that it can more easily break through firewalls. Once the WSS connection is created, it will be treated as a stream over which HTTP/2 traffic will be routed. HTTP/2 is used so that multiple simultaneous requests can be multiplexed over a single connection. The HTTP/2 protocol is only used between the two proxies, the connections either side can be any protocol that the proxy supports. This means we don't put any specific capability requirement on the destination servers. \n\nIf the tunnel connection is broken, the back-end will attempt to reconnect to the front-end:\n- If the connection fails, then it will continue to reconnect every 30s until the connection is re-established.\n- If the connection is refused with a 500 series error, then it will be retried at the next 30s timeout. \n- If the connection is refused with a 400 series error then further connections for that tunnel will not be made.\n\n> Issue: Do we need an API for the tunnel? As its created from code on the back-end, the app could have additional logic for control over the duration. Does it have API for status, clean shutdown, etc.\n>\n> Issue: Will additional connections be created for scalability - H2 perf becomes limited after 100 simultaneous requests. How does the front-end know to pair a second back-end connection?\n\nThe Front End should keep the WSS connection alive by sending pings every 30s if there is no other traffic. This should be done at the WSS layer.\n\n## Moving pieces\n\n| Location | Name | Description |\n| --- | --- | --- |\n| front-end | EndPoint | The endpoint that the back-end proxy will connect to to create a tunnel. |\n| front-end | Cluster | The cluster that will direct to back-end proxy(ies) that have created tunnels. |\n| front-end | Routes | Routes need to be configured to route specific URLs to the tunnel, by using clusters that are a tunnel. | \n| back-end | Tunnel URL(s) | The URL(s) for the front-end endpoint that can be used to establish the tunnel. |\n| back-end | Routes | The back-end needs to have routes defined that will direct traffic to local resources. |\n\n## front-end \nThe front-end is the proxy that will be called by clients to be able to access resources via the back-end proxy. It will route traffic over a tunnel created using a WSS connection from the back-end proxy. YARP needs a mechanism to know which requests will be routed via the tunnel. This will be achieved by extending the existing cluster concept in YARP - The request to create a tunnel will specify the name of a cluster. Once the tunnel is established, it will be treated as a dynmamically created destination for the named cluster. Routes will not need to be changed, they will point at the cluster, and the tunnels will be used in the same way as destinations. \n\nTunnel services must be enabled by the proxy server:\n\n``` C#\nbuilder.Services.AddReverseProxy()\n       .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"));\n\nbuilder.Services.AddTunnelServices();\n```\n\nThe front-end needs to have a tunnel endpoint that the back-end will connect to. The endpoint should be parameterized to include the name of the cluster as part of the URL, and a callback that is used to validate the connection is approved:\n - Including the ClusterId in the URL enables the same endpoint mechanism to be used for multiple clusters. \n - Using a callback for authentication enables whatever scheme the proxy author(s) wish to use.\n   - Trying to encode specific auth schemes will invariably miss a scenario that is needed.\n   - The samples that we produce should be based around client certs as it is a good way to manage secure shared secrets.\n\n``` C#\napp.MapReversProxy();\napp.MapTunnel(\"/tunnel/{ClusterId}\", async (connectionContext, cluster) => {\n\n    // Use the extensions feature https://github.com/dotnet/yarp/issues/1709 to add auth data for the tunnel\n    var tunnelAuth = cluster.Extensions[typeof(TunnelAuth)]; \n    if (!context.Connection.ClientCertificate.Verify()) return false;\n    foreach (var c in tunnelAuth.Certs)\n    {\n        if (c.ThumbPrint == context.Connection.ClientCertificate.Thumbprint) return true;\n    }\n    return false;\n});\n```\n\nThe front-end should have configuration for routes that direct to a cluster that is for the tunnel. The cluster must be marked as IsTunnel to enable tunnel capability, and must *not* include other destinations. The cluster's destinations will be supplied dynamically by back-ends creating tunnel connections.\n\nIn the following case it uses the Extensions feature to enable storing thumbprints for client certs that are used to authenticate tunnel connections. The Route will direct all traffic under the path `/OnPrem/*` to the tunnel.\n\n``` json\n{\n    \"ReverseProxy\":\n    {\n        \"Routes\" : {\n            \"OnPrem\" : {\n                \"Match\" : {\n                    \"Path\" : \"/OnPrem/{**any}\"\n                },\n                \"ClusterId\" : \"MyTunnel1\"\n            }\n        },\n        \"Clusters\" : {\n            \"MyTunnel1\" : {\n                \"IsTunnel\" : true,\n                \"Extensions\" : {\n                    \"TunnelAuth\" : {\n                        \"Certs\" : {\n                            \"name1\" : \"thumbprint1\",\n                            \"name2\" : \"thumbprint2\"\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n```\n\nTo ensure scalability, multiple back-end proxy instances should be able to create tunnel connections for the same cluster. When that happens, the load balancing rules for the cluster should apply, and balance between the active tunnels. Similarly multiple front-ends can be used to reduce the problems with a single point of failure.\n\n## back-end\n\nThe back-end instance is the proxy that will reside on the same network as the resources that should be exposed. The back-end will need to be able to connect to those resources, and also be able to create a WebSocket connection to the front-end proxy server(s) via whatever firewalls are between them.\n\nThe back-end proxy is configured with routes and destinations that it wishes to expose to the front end. Security is maintained because only URLs matching its routes will be proxyable via it. This prevents attacks at the front-end having arbitrary access to other resources on the back-end network - they need to be explicitly included in the back-end route table.\n\nThe outbound connection to the front end needs to be explicitly made for each tunnel that the back-end wishes to create.\n\n``` C#\nbuilder.Services.AddReverseProxy()\n       .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"));\n\nvar url = builder.Configuration[\"Tunnel:Url\"]!; // Eg https://Myfront-end.MyCorp.com/tunnel/MyTunnel1\n\n// Setup additional details for the connection, auth and headers\nvar tunnelOptions = new TunnelOptions(){\n       TunnelClient = new SocketsHttpHandler(),\n       ClientCertificates = new X509CertificateCollection { cert }\n       AuthCallback = AuthServer;\n       };\ntunnelOptions.Headers.Add(\"MyJWTToken\", tokenString);\n\nbuilder.WebHost.UseTunnelTransport(url, tunnelOptions);\n```\n\nThe tunnel creation takes an options class that enables the HttpHandler, headers to be set for the tunnel creation to enable authentication flexibility by the app developers. It also should include a callback to enable validation of the server certificate.\n\nThe back-end configuration would use the standard routes and cluster definitions.\n\n``` json\n{\n    \"ReverseProxy\":\n    {\n        \"Routes\": {\n            \"CNCMilling\": {\n                \"Match\": {\n                    \"Path\": \"/OnPrem/CNCMilling/{**any}\"\n                },\n                \"ClusterId\": \"Milling\"\n            }\n            \"3DPrinting\" : {\n                \"Match\": {\n                    \"Path\": \"/OnPrem/Extrusion/{**any}\"\n                },\n                \"ClusterId\": \"3dPrinting\"\n            }\n        },\n        \"Clusters\": {\n            \"Milling\": {\n                \"Destinations\": {\n                    \"Bay12\" : \"https://bay12-efd432/\",\n                    \"Bay15\" : \"https://bay15-j377d3/\"\n                }\n            }\n            \"3dPrinting\": {\n                \"Destinations\": {\n                    \"Bay41-controller\" : \"https://bay41-controller/\"\n                }\n            }\n        }\n    }\n}\n```\n\nIn the above case, requests using the paths `/OnPrem/CNCMilling/*` and `/OnPrem/Extrusion/*` will be routed by the back-end to their respective services - other paths would result in an error.\n\nNote: Active health checks probably don't make sense to be performed by the front-end against the back-end. Passive health checks will verify the overall condition of the tunnel.\n\n## Scalability\n\nIn a large deployment, there needs to be the ability to have multiple front-end and back-end proxies:\n- If the front-end receives multiple tunnel connections, then it should treat them as if the cluster has multiple destinations. The cluster can use the load balancing policy to select how it decides to route traffic to the back-end proxies.\n\n> Note: The front-end proxy will not be aware of the actual destinations that serve resources - each back-end should have its own cluster definition for the actual destinations, and so can include multiple servers for any route/cluster combination.\n\n- A back-end proxy should be able to create tunnels to multiple front-ends. The tunnels can be to related front-end proxies that are sharing the same load, or to front-ends in different cloud deployments. This enables the Front Ends to be very specific to particular deployments - and have constrained v-Lan configurations in the cloud.\n\n## Authentication\n\nThe authentication options for ASP.NET are diverse, and IT departments will likely have their own conditions on what is required to be able to secure a tunnel. So rather than trying to implement the combinatorial matrix of what customers could need, we should use a callback so that the proxy author can decide.\n\nSamples should be created that show best practices using a secure mechanism such as client a certificate.\n\n*Issue:* Does the back-end need additional mechanisms to validate the connection to the front-end, or is TLS/SNI sufficient?\n\n## Security\n\nThe purpose of the tunnel is to simplify service exposure by creating a tunnel through the firewall that enables external requests to be made to destination servers on the back-end network. There are a number of mitigations that reduces the risk of this feature:\n\n- No endpoints are exposed via the firewall - it does not expose any new endpoints that could act as attack vectors. The tunnel is an outbound connection made between the back-end and the front-end.\n- Traffic directed via the tunnel will need to have corresponding routes in the Back End configuration. Traffic will only be routed if there is a respective route and cluster configuration. Tunnel traffic can't specify arbitrary URLs that would be directed to a hostname not included in the back-end route table configuration.\n- Tunnel connections should only be over HTTPs\n\n## Metrics\n\n? What telemetry and events are needed for this?\n\n## Error conditions\n\n| Condition | Description |\n| --- | --- |\n| No tunnel has connected | If the front end receives a request for a route that is backed by a tunnel and no tunnels have been created, then it should respond to those requests with a 502 \"Bad Gateway\" error |\n\n## Web Transport\n\nWeb Transport is an interesting future protocol choice for the tunnel.\n"
  },
  {
    "path": "docs/operations/BackportingToPreview.md",
    "content": "# Backporting changes to a preview branch\n\nBackporting changes is very similar to a regular release. Changes are made on the preview branch, the builds are validated and ultimately released.\n\n- Checkout the preview branch\n\n  `git checkout release/1.0.0-previewX`\n- Make and commit any changes\n- Push the changes **to your own fork** and submit a PR against the preview branch\n- Once the PR is merged, wait for the internal [`microsoft-reverse-proxy-official`](https://dev.azure.com/dnceng/internal/_build?definitionId=809&_a=summary&view=branches) pipeline to produce a build\n- Validate the build the same way you would for a regular release [docs](https://github.com/dotnet/yarp/blob/main/docs/operations/Release.md#validate-the-final-build)\n- Package Artifacts from this build can be shared to validate the patch. Optionally, the artifacts from the [public pipeline](https://dev.azure.com/dnceng/public/_build?definitionId=807&view=branches) can be used\n- Continue iterating on the preview branch until satisfied with the validation of the change\n- [Release the build](https://github.com/dotnet/yarp/blob/main/docs/operations/Release.md#release-the-build) from the preview branch\n- Create a new git tag for the released commit\n\n  **While still on the preview branch:**\n  - `git tag v1.0.0-previewX.build.d`\n  - `git push upstream --tags`\n- Create a new [release](https://github.com/dotnet/yarp/releases).\n\n## Internal fixes\n\nIssues with significant security or disclosure concerns need to be fixed privately first. All of this work will happen on the internal Azdo repo and be merged to the public github repo at the time of disclosure.\n\n- Make a separate clone of https://dev.azure.com/dnceng/internal/_git/dotnet-yarp to avoid accidentally pushing to the public repo.\n- Create a branch named `internal/release/{version being patched}` starting from the tagged commit of the prior release.\n- Update versioning as needed.\n- Create a feature branch, fix the issue, and send a PR using Azdo.\n- Release the build from the internal branch.\n- Tag the commit and push it to the public repo. Do not push to regular `main` or `release/*` branches in the internal mirror.\n- Cherry pick the changes to public main as needed.\n- Finish the standard release checklist.\n"
  },
  {
    "path": "docs/operations/Branching.md",
    "content": "# Branching Tasks\n\nWhen we are ready to branch our code, we first need to create the branch:\n\n1. In a local clone, run `git checkout main` and `git pull origin main` to make sure you have the latest `main`\n1. Run `git checkout -b release/1.1.0-previewX` where `X` is the YARP preview number. When releasing a non-preview version, use `release/1.1` instead of `release/1.1.0` so that the branch can be used for future patches.\n1. If you are releasing a non-preview version:\n    - Set the `PreReleaseVersionLabel` in [`eng/Versions.props`] to `rtw`.\n    - Run `build.cmd -pack`\n    - Verify that the packages in `artifacts/packages/Debug/Shipping` do not have a suffix after the intended version. For example the name should be `Yarp.ReverseProxy.1.1.0.nupkg` and not `Yarp.ReverseProxy.1.1.0-dev.nupkg`.\n1. Run `git push origin release/1.1.0-previewX` to push the branch to the server.\n\nUpdate branding in `main`:\n\n1. Edit the file [`eng/Versions.props`]\n2. Set `PreReleaseVersionLabel` to `preview.X` (where `X` is the next preview number)\n3. Send a PR and merge it ASAP (auto-merge is your friend).\n\nUpdate the runtimes and SDKs in `global.json` in `main`:\n\nCheck that the global.json includes the latest 8.0 runtime versions from [the .NET 8.0 download page](https://dotnet.microsoft.com/download/dotnet/8.0), and 9.0 from [the .NET 9.0 download page](https://dotnet.microsoft.com/download/dotnet/9.0).\n\n[`eng/Versions.props`]: ../../eng/Versions.props"
  },
  {
    "path": "docs/operations/DependencyFlow.md",
    "content": "# Dependency Flow\n\n## Dependency Flow Overview\n\n*For full documentation on Arcade, Maestro and `darc`, see [the Arcade documentation](https://github.com/dotnet/arcade/tree/main/Documentation)*\n\nWe use the .NET Engineering System ([Arcade](https://github.com/dotnet/arcade)) to build this repo. Part of the engineering system is a service called \"Maestro\" which manages dependency flow between repositories. When one repository finishes building, it can automatically publish it's build to a Maestro \"Channel\". Other repos can subscribe to that channel to receive updated builds. Maestro will automatically open a PR to update dependencies in repositories that are subscribed to changes in dependent repositories.\n\nMaestro can be queried and controlled using the `darc` command line tool. To use `darc` you will need to be a member of the [`dotnet/arcade-contrib` GitHub Team](https://github.com/orgs/dotnet/teams/arcade-contrib). To set up `darc`:\n\n1. Run `.\\eng\\common\\darc-init.ps1` to install the global tool.\n2. Once installed, run `darc authenticate` and follow the instructions.\n\nRunning `darc` with no args will show a list of commands. The `darc help [command]` command will give you help on a specific command.\n\nRepositories can be configured to publish builds automatically to a certain channel, based on the branch.\nTo see the current mappings for a repository, you can run `darc get-default-channels --source-repo [repo]`, where `[repo]` is any substring that matches a full GitHub URL for a repo in the system. The easiest way to use `[repo]` is to just specify the `[owner]/[name]` form for a repo. For example:\n\n```shell\n> darc get-default-channels --source-repo dotnet/aspnetcore\n(3796) https://github.com/dotnet/aspnetcore @ release/6.0 -> .NET 6\n(5027) https://github.com/dotnet/aspnetcore @ release/8.0 -> .NET 8\n(5731) https://github.com/dotnet/aspnetcore @ release/9.0 -> .NET 9\n(5732) https://github.com/dotnet/aspnetcore @ main -> .NET 10\n(6050) https://github.com/dotnet/aspnetcore @ release/10.0-preview1 -> .NET 10 Preview 1\n```\n\nSubscriptions are managed using the `get-subscriptions`, `add-subscription` and `update-subscription` commands. You can view all subscriptions in the system by running `darc get-subscription`. You can also filter subscriptions by the source and target using the `--source-repo [repo]` and `--target-repo [repo]` arguments. For example, to see everything that `dotnet/yarp` is subscribed to:\n\n```shell\n> darc get-subscriptions --target-repo dotnet/yarp\nhttps://github.com/dotnet/arcade (.NET Eng - Latest) ==> 'https://github.com/dotnet/yarp' ('main')\n  - Id: 1751e896-c0f1-4247-3909-08d8c8762e9e\n  - Update Frequency: EveryWeek\n  - Enabled: True\n  - Batchable: False\n  - PR Failure Notification tags:\n  - Source-enabled: False\n  - Merge Policies:\n    Standard\nhttps://github.com/dotnet/arcade (.NET Eng - Latest) ==> 'https://github.com/dotnet/yarp' ('release/2.2')\n  - Id: ebd75d9f-8988-4f50-bd1d-83dfc79fb7ba\n  - Update Frequency: EveryWeek\n  - Enabled: True\n  - Batchable: False\n  - PR Failure Notification tags:\n  - Source-enabled: False\n  - Merge Policies:\n    Standard\n```\n\nTo add a new subscription, run `darc add-subscription` with no arguments. An editor window will open with a TODO script like this:\n\n```yaml\nChannel: <required>\nSource Repository URL: <required>\nTarget Repository URL: <required>\nTarget Branch: <required>\nUpdate Frequency: <'none', 'everyDay', 'everyBuild', 'twiceDaily', 'everyWeek'>\nBatchable: False\nMerge Policies: []\n```\n\nA number of comments will also be present, describing available values and what they do. Fill these fields in, for example:\n\n```yaml\nChannel: .NET Eng - Latest\nSource Repository URL: https://github.com/dotnet/arcade\nTarget Repository URL: https://github.com/dotnet/yarp\nTarget Branch: release/42\nUpdate Frequency: EveryWeek\nBatchable: False\nMerge Policies:\n- Name: Standard\n```\n\nSave and exit the editor and the subscription will be created.\n\nSimilarly, you can edit an existing subscription by using `darc update-subscription --id [ID]` (get the `[ID]` value from `get-subscriptions`). This will open the same TODO script, but with the current values filled in. Just update them, then save and exit to update.\n\n## Prerequisites\n\n* Properly configured `darc` global tool, including having run `darc authenticate`.\n\n## When we are ready to branch\n\nFollow the initial branching steps from [Branching.md](Branching.md).\n\nYARP uses darc to consume up-to-date Arcade bits every week.\nSet up dependency flow for the new branch:\n\n1. Run `darc add-subscription`\n2. Fill in the template that opens in your editor as follows:\n    * `Channel` = `.NET Eng - Latest`\n    * `Source Repository URL` = `https://github.com/dotnet/arcade`\n    * `Target Repository URL` = `https://github.com/dotnet/yarp`\n    * `Target Branch` = `release/X` (where `X` is the YARP release version)\n    * `Update Frequency` = `EveryWeek`\n    * `Merge Policies` is a multiline value, it should look like this:\n      ```yaml\n      Merge Policies:\n      - Name: Standard\n        Properties: {}\n      ```\n3. Save and close the editor window."
  },
  {
    "path": "docs/operations/README.md",
    "content": "# Operations Playbook\n\n*This documentation is primarily for project maintainers, though contributors are welcome to read and learn about our process!*\n\nThis section has docs on various operations and tasks to be performed in the repo.\n\n* [Branching](Branching.md)\n* [Releasing](Release.md)\n* [Backporting changes onto a preview branch](BackportingToPreview.md)\n* [Dependency flow](DependencyFlow.md)"
  },
  {
    "path": "docs/operations/Release.md",
    "content": "# Releasing YARP\n\nThis document provides a guide on how to release a preview of YARP.\n\nTo keep track of the process, open a [release checklist issue](https://github.com/dotnet/yarp/issues/new?title=Preview%20X%20release%20checklist&body=See%20%5BRelease.md%5D%28https%3A%2F%2Fgithub.com%2Fdotnet%2Fyarp%2Fblob%2Fmain%2Fdocs%2Foperations%2FRelease.md%29%20for%20detailed%20instructions.%0A%0A-%20%5B%20%5D%20Ensure%20there%27s%20a%20release%20branch%20created%20%28see%20%5BBranching%5D%28https%3A%2F%2Fgithub.com%2Fdotnet%2Fyarp%2Fblob%2Fmain%2Fdocs%2Foperations%2FBranching.md%29%29%0A-%20%5B%20%5D%20Ensure%20the%20%60Version.props%60%20has%20the%20%60PreReleaseVersionLabel%60%20updated%20to%20the%20next%20preview%0A-%20%5B%20%5D%20Identify%20and%20validate%20the%20build%20on%20the%20%60dotnet-yarp-official%60%20pipeline%0A-%20%5B%20%5D%20Release%20the%20build%0A-%20%5B%20%5D%20Tag%20the%20commit%0A-%20%5B%20%5D%20Draft%20release%20notes%0A-%20%5B%20%5D%20Publish%20release%20notes%0A-%20%5B%20%5D%20Close%20the%20%5Bold%20milestone%5D%28https%3A%2F%2Fgithub.com%2Fdotnet%2Fyarp%2Fmilestones%29%0A-%20%5B%20%5D%20Announce%20on%20social%20media%0A-%20%5B%20%5D%20Set%20the%20preview%20branch%20to%20protected%0A-%20%5B%20%5D%20Delete%20the%20%5Bprevious%20preview%20branch%5D%28https%3A%2F%2Fgithub.com%2Fdotnet%2Fyarp%2Fbranches%29%0A-%20%5B%20%5D%20Request%20source%20code%20archival).\n\n## Versioning\n\nEnsure the eng/Versions.props file has the expected versions and pre-release labels. For a final release set PreReleaseVersionLabel to `rtw`.\n\n## Ensure there's a release branch created.\n\nSee [Branching](Branching.md):\n- Make the next preview branch.\n- Update the branding in main.\n- Update the global.json runtime and SDK versions in main.\n\n## Identify the Final Build\n\nFirst, identify the final build of the [`dotnet-yarp-official` Azure Pipeline](https://dev.azure.com/dnceng/internal/_build?definitionId=809&_a=summary) (on dnceng/internal). The final build will be the latest successful build **in the relevant `release/x` branch**. Use the \"Branches\" tab on Azure DevOps to help identify it. If the branch hasn't been mirrored yet (see [`dotnet-mirror-dnceng` pipeline](https://dev.azure.com/dnceng/internal/_build?definitionId=1387)) and there are no outstanding changesets in the branch, the build of the corresponding commit from the main branch can be used.\n\nOnce you've identified that build, click in to the build details.\n\n## Validate the Final Build\n\nAt this point, you can perform any validation that makes sense. At a minimum, we should validate that the sample can run with the candidate packages. You can download the final build using the \"Artifacts\" which can be accessed under \"Related\" in the header:\n\n![image](https://github.com/user-attachments/assets/27ddf12d-f4b7-4faa-862e-d2d1d6eafea9)\n\nThe packages can be accessed from the `PackageArtifacts` artifact:\n\n![image](https://github.com/user-attachments/assets/264b8c6d-8108-4536-a61b-421aa652df73)\n\n### Consume .nupkg\n\n- Visual Studio: Place it in a local folder and add that folder as a nuget feed in Visual Studio.\n- Command Line: `dotnet nuget add source <directory> -n local`\n\nWalk through the [Getting Started](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/getting-started) instructions and update them in the release branch as needed.\n\nAlso validate any major new scenarios this release and their associated docs.\n\n## Release the build\n\nOnce validation has been completed, it's time to release.\n\nGo to the [`dotnet-yarp-release` pipeline](https://dev.azure.com/dnceng/internal/_build?definitionId=1448) and select \"Run Pipeline\".\n\nUnder \"Resources\", select the pipeline run that you've validated artifacts for.\n\n![image](https://github.com/user-attachments/assets/efa38319-2620-4deb-aca3-d2bf23c991cc)\n![image](https://github.com/user-attachments/assets/e25419e6-498d-4cc2-bf98-b5b2bc77c251)\n![image](https://github.com/user-attachments/assets/e2f7f965-b4f7-40dc-ba2f-6a91062271d5)\n\nTriple-check the version numbers of the packages in the artifacts against whatever validation was done at this point.\n\n![image](https://github.com/user-attachments/assets/187d65d9-3c0f-4418-ab13-a77e0ad1b8e9)\n\nSelect \"Run\". Unless you're a release approver, you're done here!\n\n## Approve the release\n\nThe Azure Pipeline will send an email to all the release approvers asking one of them to approve the release.\n\n![image](https://github.com/user-attachments/assets/e772e31c-8aea-4bca-a21f-8bd62f61365f)\n\nClick \"Review Manual Validation\", or navigate to the release pipeline directly in Azure DevOps. You'll see that the stage is \"Pending Approval\"\n\nEnter a comment such as \"release for preview X\" approve finalize the release.\n**After approving, packages will be published automatically**. It *is* possible to cancel the pipeline, but it might be too late. See \"Troubleshooting\" below.\n\nThe packages will be pushed and when the \"NuGet.org\" stage turns green, the packages are published!\n\n**Note:** NuGet publishing is quick, but there is a background indexing process that can mean it will take up to several hours for packages to become available\n\n## Tag the commit\n\nCreate and push a git tag for the commit associated with the final build (not necessarily the HEAD of the current release branch). See prior tags for the preferred format. Use a lightweight tag, not annotated.\n\n`git tag v1.0.0-previewX`\n\nPush the tag change to the upstream repo (**not your fork**)\n\n`git push upstream v1.0.0-previewX`\n\n## Draft release notes\n\nCreate a draft release at https://github.com/dotnet/yarp/releases using the new tag. See prior releases for the recommended content and format.\n\n## Publish the release notes\n\nPublish the draft release notes. These should be referencing the latest docs, packages, etc..\n\n## Close the old milestone\n\nIt should be empty now. If it's not, move the outstanding issues to the next one.\n\n## Announce on social media\n\nDavid Fowler has a lot of twitter followers interested in YARP. Tweet a link to the release notes and let him retweet it.\n\n## Set the preview branch to protected\n\nThis is to avoid accidental pushes to/deletions of the preview branch.\n\n## Delete the previous preview branch\n\nThere should only be one [preview branch on the repo](https://github.com/dotnet/yarp/branches) after this point.\n\n## Request source code archival\n1. Go to the internal https://dpsopsrequestforms.azurewebsites.net portal\n2. Select \"Source Code Archival\"\n3. Proceed through steps 1,2 till the step 3 (all prerequisites are already fulfilled)\n4. Fill in the required fields on the steps 3, 4, 5. Please, see the recommended values below.\n5. At the last step, check all the info and and submit the request\n6. Go to \"My Request\" tab and wait for a new ticket to appear on the list\n7. Copy the ticket link from \"Ticket ID\" column which will look like `https://prod.******`\n8. Replace the `prod` word to `portal`\n9. Navigate to the fixed link and check that the ticket is actually created\n10. That's all the actions needed to be done immediately. Afterwards, periodically track the ticket progress. It might take many hours.\n11. [Offline] Wait for the archival completion report to arrive. Check that the size and number of archived files match the YARP repo.\n\n### Recommended fields' values for archival request form\n\n| Field | Value |\n| --- | --- |\n| Team Alias | dotnetrp |\n| Business Group Name | Devdiv |\n| Product Name | YARP |\n| Version | \\<release version\\> |\n| Production Type | dotNET |\n| Release Type | \\<RC or Release\\> |\n| Operating System(s) | Cross Platform |\n| Product Language(s) | English |\n| Release Date | \\<release date\\> |\n| File Count | \\<rough number of files in repo\\> |\n| Back Up Type | Code Repo(Git URL/AzureDevOps) |\n| Repo URL | \\<link to the internal AzDo YARP repo\\> |\n| OwnerAlias | dotnetrp |\n| File Collection | Build Scripts, Help Utility Source Code, Source Code |\n| Data Size | \\<rough total files size in MB\\> |\n\n## Troubleshooting\n\n### Authentication Errors\n\nThe pipeline is authenticated via a \"Service Connection\" in Azure DevOps. If there are authentication errors, it's likely the API key is invalid. Follow these steps to update the API key:\n\n1. Go to NuGet.org, log in with an account associated with an `@microsoft.com` address that has access to the `dotnetframework` organization.\n2. Generate a new API key with \"dotnetframework\" as the Package Owner and \"*\" as the Package \"glob\".\n3. Copy that API key and fill it in to the \"nuget.org (dotnetframework organization)\" [Service Connection](https://dev.azure.com/dnceng/internal/_settings/adminservices) in Azure DevOps.\n\nIn the event you don't have access, contact `dnceng@microsoft.com` for guidance.\n\n### Accidental Overpublish\n\nIn the event you overpublish (publish a package that wasn't intended to be released), you should \"unlist\" the package on NuGet. It is not possible to delete packages on NuGet.org, by design, but you can remove them from search results. Users who reference the version you published directly will still be able to download it, but it won't show up in search queries or non-version-specific actions (like installing the latest).\n\n1. Go to NuGet.org, log in with an account associated with an `@microsoft.com` address that has access to the `dotnetframework` organization.\n2. Go to the package page and click \"Manage package\" on the \"Info\" sidebar on the right.\n3. Expand \"Listing\"\n4. Select the version that was accidentally published\n5. Uncheck the \"List in search results\" box\n6. Click \"Save\"\n\n### Package was rejected\n\nNuGet.org has special criteria for all packages starting `Microsoft.`. If the package is rejected for not meeting one of those criteria, go to the [NuGet @ Microsoft](http://aka.ms/nuget) page for more information on required criteria and guidance for how to configure the package appropriately.\n"
  },
  {
    "path": "docs/roadmap.md",
    "content": "# YARP Roadmap\n\n## Supported YARP versions\n\n[Latest releases](https://github.com/dotnet/yarp/releases)\n\n| Version | Release Date | Latest Patch Version | End of Support |\n| -- | -- | -- | -- |\n| [YARP 2.3](https://github.com/dotnet/yarp/releases/tag/v2.3.0) | February 27, 2025 | [2.3.0](https://github.com/dotnet/yarp/releases/tag/v2.3.0) | |\n\n### End-of-life YARP versions\n\n| Version | Released date | Final Patch Version | End of support |\n| -- | -- | -- | -- |\n| [YARP 2.2](https://github.com/dotnet/yarp/releases/tag/v2.2.0) | September 3, 2024 | [2.2.0](https://github.com/dotnet/yarp/releases/tag/v2.2.0) | August 27, 2025 |\n| [YARP 2.1](https://github.com/dotnet/yarp/releases/tag/v2.1.0) | November 17, 2023 | [2.1.0](https://github.com/dotnet/yarp/releases/tag/v2.1.0) | March 3, 2025 |\n| [YARP 2.0](https://github.com/dotnet/yarp/releases/tag/v2.0.0) | February 14, 2023 | [2.0.1](https://github.com/dotnet/yarp/releases/tag/v2.0.1) | May 17, 2024 |\n| [YARP 1.1](https://github.com/dotnet/yarp/releases/tag/v1.1.0) | May 2, 2022 | [1.1.2](https://github.com/dotnet/yarp/releases/tag/v1.1.2) | August 14, 2023 |\n| [YARP 1.0](https://github.com/dotnet/yarp/releases/tag/v1.0.0) | November 9, 2021 | [1.0.1](https://github.com/dotnet/yarp/releases/tag/v1.0.1) | November 2, 2022 |\n\n## Support\n\nYARP support is provided by the product team - the engineers working on YARP - which is a combination of members from ASP.NET and the .NET library teams. We do not provide 24/7 support or 'carry pagers', but we generally have good coverage. Bugs should be reported in GitHub using the issue templates, and will typically be responded to within 24hrs. If you find a security issue we ask you to [report it via the Microsoft Security Response Center (MSRC)](https://github.com/dotnet/yarp/blob/main/SECURITY.md).\n\nThe support period for YARP releases is as follows:\n\n| Release | Issue Type | Support period |\n| --- | --- | --- |\n| Major or minor version | Security Bugs, Major behavior defects | Until next GA + 6 Months |\n| Patch version | Minor behavior defects | Until next GA |\n| Preview | Security Bugs, Major behavior defects | Until next preview |\n| | All other | None - may be addressed by next preview |\n\nFor example, if 2 months after 1.3 (making up a number) is released, a security issue is found, then we will patch:\n- 1.3 - its the latest release\n- 1.2 - as it has 4 months of support remaining\n- 1.1 - provided that 1.2 was released less than 6 months before\n\nThis support schedule is designed to provide a reasonable time period for customers to be able to update to new releases. \n\n### Building your own copy of a release\n\nYARP is an open source project, so any customers that need fixes faster, or for older releases, are able to build their own copy of YARP. The build environment for YARP is included in the repo and maintained in sync with the source. Each release (GA and Preview) of YARP is tagged, which means that if you need to patch a specific release you can sync to the tag and build. For example you can rebuild v1.0.0 with:\n\n```shell\ngit clone -b v1.0.0 https://github.com/dotnet/yarp.git yarp1_0_0\ncd yarp1_0_0\nrestore.cmd \n\n<make your changes>\n\nbuild.cmd -c release\n```\n\nThis will produce the `Yarp.ReverseProxy.dll` into `artifacts/bin/Yarp.ReverseProxy/Release/net6.0`, and peer folder(s) for .NET 5 & Core 3.1. If you need to build a nuget package, that can be done with: \n\n```shell\npack.cmd -c release\n```\n\nThe nuget package will be output to `artifacts/packages/release/Shipping`. \n"
  },
  {
    "path": "dotnet-yarp-release.yml",
    "content": "trigger: none\nname: $(Date:yyyyMMdd).$(Rev:r)\n\nvariables:\n- name: NuGetApiKey\n  value: \n- name: NuGetFeed\n  value: https://api.nuget.org/v3/index.json\n\nresources:\n  repositories:\n  - repository: 1ESPipelineTemplates\n    type: git\n    name: 1ESPipelineTemplates/1ESPipelineTemplates\n    ref: refs/tags/release\n  pipelines:\n  - pipeline: yarp-build\n    source: dotnet\\yarp\\dotnet-yarp-official\n\nextends:\n  template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates\n  parameters:\n    pool:\n      name: NetCore1ESPool-Internal\n      image: 1es-windows-2022\n      os: windows\n\n    stages:\n    - stage: release\n      displayName: Release to NuGet\n      jobs:\n      - job: PreDeploymentApprovalJob\n        displayName: Pre-Deployment Approval\n        condition: succeeded()\n        timeoutInMinutes: 2880\n        pool: server\n        steps:\n        - task: ManualValidation@1\n          inputs:\n            notifyUsers: |-\n              karelz@microsoft.com,\n              samsp@microsoft.com,\n              adityam@microsoft.com,\n              mizupan@microsoft.com,\n              bpetit@microsoft.com\n            approvers: |-\n              karelz@microsoft.com,\n              samsp@microsoft.com,\n              adityam@microsoft.com\n\n      - job: NuGetPush\n        dependsOn: PreDeploymentApprovalJob\n        condition: succeeded()\n        timeoutInMinutes: 30\n        templateContext:\n          type: releaseJob\n          isProduction: true\n          inputs:\n          - input: pipelineArtifact\n            pipeline: yarp-build\n            artifactName: artifacts\n        steps:\n        - task: NuGetToolInstaller@1\n          displayName: Prepare NuGet tool\n        - task: PowerShell@2\n          displayName: NuGet push\n          inputs:\n            targetType: inline\n            script: |\n              tree $(Pipeline.Workspace)\\Release\\Shipping /f\n\n              Get-ChildItem \"$(Pipeline.Workspace)\\Release\\Shipping\\*\" -Filter *.nupkg -Exclude *.symbols.nupkg | ForEach-Object {\n                  $name = $_.Name\n                  Write-Host \"Processing $name ...\"\n                  if ($name.StartsWith(\"Yarp.ReverseProxy.\") -or $name.StartsWith(\"Yarp.Telemetry.Consumption.\")) {\n                      Write-Host \"  Publishing $name\"\n                      nuget push -Source $env:NuGetFeed -ApiKey $env:NuGetApiKey $_.FullName\n                  } else {\n                      Write-Host \"  Skipping $name (update the script to change this)\"\n                  }\n              }\n          env:\n            NuGetApiKey: $(NuGetApiKey)\n"
  },
  {
    "path": "eng/Build.props",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n    <ItemGroup>\n        <ProjectToBuild Include=\"$(RepoRoot)src\\**\\*.csproj\" />\n        <ProjectToBuild Include=\"$(RepoRoot)test\\**\\*.csproj\" />\n        <ProjectToBuild Include=\"$(RepoRoot)samples\\**\\*.csproj\" />\n        <ProjectToBuild Include=\"$(RepoRoot)testassets\\**\\*.csproj\" />\n        <ProjectToBuild Include=\"$(RepoRoot)eng\\yarpapppack\\**\\*.csproj\" />\n    </ItemGroup>\n</Project>\n"
  },
  {
    "path": "eng/CodeAnalysis.src.globalconfig",
    "content": "is_global = true\n\n# AD0001: Analyzer threw an exception\ndotnet_diagnostic.AD0001.severity = warning\n\n# BCL0001: Ensure minimum API surface is respected\ndotnet_diagnostic.BCL0001.severity = warning\n\n# BCL0010: AppContext default value expected to be true\ndotnet_diagnostic.BCL0010.severity = warning\n\n# BCL0011: AppContext default value defined in if statement with incorrect pattern\ndotnet_diagnostic.BCL0011.severity = warning\n\n# BCL0012: AppContext default value defined in if statement at root of switch case\ndotnet_diagnostic.BCL0012.severity = warning\n\n# BCL0015: Invalid P/Invoke call\ndotnet_diagnostic.BCL0015.severity = none\n\n# BCL0020: Invalid SR.Format call\ndotnet_diagnostic.BCL0020.severity = warning\n\n# SYSLIB1045: Convert to 'GeneratedRegexAttribute'.\ndotnet_diagnostic.SYSLIB1045.severity = warning\n\n# SYSLIB1054: Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time\ndotnet_diagnostic.SYSLIB1054.severity = warning\n\n# SYSLIB1055: Invalid 'CustomMarshallerAttribute' usage\ndotnet_diagnostic.SYSLIB1055.severity = error\n\n# SYSLIB1056:Specified marshaller type is invalid\ndotnet_diagnostic.SYSLIB1056.severity = error\n\n# SYSLIB1057: Marshaller type does not have the required shape\ndotnet_diagnostic.SYSLIB1057.severity = error\n\n# SYSLIB1058: Invalid 'NativeMarshallingAttribute' usage\ndotnet_diagnostic.SYSLIB1058.severity = error\n\n# SYSLIB1060: Specified marshaller type is invalid\ndotnet_diagnostic.SYSLIB1060.severity = error\n\n# SYSLIB1061: Marshaller type has incompatible method signatures\ndotnet_diagnostic.SYSLIB1061.severity = error\n\n# CA1000: Do not declare static members on generic types\ndotnet_diagnostic.CA1000.severity = none\n\n# CA1001: Types that own disposable fields should be disposable\ndotnet_diagnostic.CA1001.severity = none\n\n# CA1002: Do not expose generic lists\ndotnet_diagnostic.CA1002.severity = none\n\n# CA1003: Use generic event handler instances\ndotnet_diagnostic.CA1003.severity = none\n\n# CA1005: Avoid excessive parameters on generic types\ndotnet_diagnostic.CA1005.severity = none\n\n# CA1008: Enums should have zero value\ndotnet_diagnostic.CA1008.severity = none\n\n# CA1010: Generic interface should also be implemented\ndotnet_diagnostic.CA1010.severity = none\n\n# CA1012: Abstract types should not have public constructors\ndotnet_diagnostic.CA1012.severity = none\n\n# CA1014: Mark assemblies with CLSCompliant\ndotnet_diagnostic.CA1014.severity = none\n\n# CA1016: Mark assemblies with assembly version\ndotnet_diagnostic.CA1016.severity = none\n\n# CA1017: Mark assemblies with ComVisible\ndotnet_diagnostic.CA1017.severity = none\n\n# CA1018: Mark attributes with AttributeUsageAttribute\ndotnet_diagnostic.CA1018.severity = warning\n\n# CA1019: Define accessors for attribute arguments\ndotnet_diagnostic.CA1019.severity = none\n\n# CA1021: Avoid out parameters\ndotnet_diagnostic.CA1021.severity = none\n\n# CA1024: Use properties where appropriate\ndotnet_diagnostic.CA1024.severity = none\n\n# CA1027: Mark enums with FlagsAttribute\ndotnet_diagnostic.CA1027.severity = none\n\n# CA1028: Enum Storage should be Int32\ndotnet_diagnostic.CA1028.severity = none\n\n# CA1030: Use events where appropriate\ndotnet_diagnostic.CA1030.severity = none\n\n# CA1031: Do not catch general exception types\ndotnet_diagnostic.CA1031.severity = none\n\n# CA1032: Implement standard exception constructors\ndotnet_diagnostic.CA1032.severity = none\n\n# CA1033: Interface methods should be callable by child types\ndotnet_diagnostic.CA1033.severity = none\n\n# CA1034: Nested types should not be visible\ndotnet_diagnostic.CA1034.severity = none\n\n# CA1036: Override methods on comparable types\ndotnet_diagnostic.CA1036.severity = none\n\n# CA1040: Avoid empty interfaces\ndotnet_diagnostic.CA1040.severity = none\n\n# CA1041: Provide ObsoleteAttribute message\ndotnet_diagnostic.CA1041.severity = none\n\n# CA1043: Use Integral Or String Argument For Indexers\ndotnet_diagnostic.CA1043.severity = none\n\n# CA1044: Properties should not be write only\ndotnet_diagnostic.CA1044.severity = none\n\n# CA1045: Do not pass types by reference\ndotnet_diagnostic.CA1045.severity = none\n\n# CA1046: Do not overload equality operator on reference types\ndotnet_diagnostic.CA1046.severity = none\n\n# CA1047: Do not declare protected member in sealed type\ndotnet_diagnostic.CA1047.severity = warning\n\n# CA1050: Declare types in namespaces\ndotnet_diagnostic.CA1050.severity = warning\n\n# CA1051: Do not declare visible instance fields\ndotnet_diagnostic.CA1051.severity = none\n\n# CA1052: Static holder types should be Static or NotInheritable\ndotnet_diagnostic.CA1052.severity = warning\ndotnet_code_quality.CA1052.api_surface = private, internal\n\n# CA1054: URI-like parameters should not be strings\ndotnet_diagnostic.CA1054.severity = none\n\n# CA1055: URI-like return values should not be strings\ndotnet_diagnostic.CA1055.severity = none\n\n# CA1056: URI-like properties should not be strings\ndotnet_diagnostic.CA1056.severity = none\n\n# CA1058: Types should not extend certain base types\ndotnet_diagnostic.CA1058.severity = none\n\n# CA1060: Move pinvokes to native methods class\ndotnet_diagnostic.CA1060.severity = none\n\n# CA1061: Do not hide base class methods\ndotnet_diagnostic.CA1061.severity = none\n\n# CA1062: Validate arguments of public methods\ndotnet_diagnostic.CA1062.severity = none\n\n# CA1063: Implement IDisposable Correctly\ndotnet_diagnostic.CA1063.severity = none\n\n# CA1064: Exceptions should be public\ndotnet_diagnostic.CA1064.severity = none\n\n# CA1065: Do not raise exceptions in unexpected locations\ndotnet_diagnostic.CA1065.severity = none\n\n# CA1066: Implement IEquatable when overriding Object.Equals\ndotnet_diagnostic.CA1066.severity = warning\n\n# CA1067: Override Object.Equals(object) when implementing IEquatable<T>\ndotnet_diagnostic.CA1067.severity = warning\n\n# CA1068: CancellationToken parameters must come last\ndotnet_diagnostic.CA1068.severity = none\n\n# CA1069: Enums values should not be duplicated\ndotnet_diagnostic.CA1069.severity = none\n\n# CA1070: Do not declare event fields as virtual\ndotnet_diagnostic.CA1070.severity = suggestion\n\n# CA1200: Avoid using cref tags with a prefix\ndotnet_diagnostic.CA1200.severity = suggestion\n\n# CA1303: Do not pass literals as localized parameters\ndotnet_diagnostic.CA1303.severity = none\n\n# CA1304: Specify CultureInfo\ndotnet_diagnostic.CA1304.severity = none\n\n# CA1305: Specify IFormatProvider\ndotnet_diagnostic.CA1305.severity = none\n\n# CA1307: Specify StringComparison for clarity\ndotnet_diagnostic.CA1307.severity = none\n\n# CA1308: Normalize strings to uppercase\ndotnet_diagnostic.CA1308.severity = none\n\n# CA1309: Use ordinal string comparison\ndotnet_diagnostic.CA1309.severity = none\n\n# CA1310: Specify StringComparison for correctness\ndotnet_diagnostic.CA1310.severity = suggestion\n\n# CA1311: Specify a culture or use an invariant version\ndotnet_diagnostic.CA1311.severity = warning\n\n# CA1401: P/Invokes should not be visible\ndotnet_diagnostic.CA1401.severity = warning\n\n# CA1416: Validate platform compatibility\ndotnet_diagnostic.CA1416.severity = warning\n\n# CA1417: Do not use 'OutAttribute' on string parameters for P/Invokes\ndotnet_diagnostic.CA1417.severity = warning\n\n# CA1418: Use valid platform string\ndotnet_diagnostic.CA1418.severity = warning\n\n# CA1419: Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle'\ndotnet_diagnostic.CA1419.severity = warning\n\n# CA1420: Property, type, or attribute requires runtime marshalling\ndotnet_diagnostic.CA1420.severity = warning\n\n# CA1421: This method uses runtime marshalling even when the 'DisableRuntimeMarshallingAttribute' is applied\ndotnet_diagnostic.CA1421.severity = suggestion\n\n# CA1422: Validate platform compatibility\ndotnet_diagnostic.CA1422.severity = warning\n\n# CA1501: Avoid excessive inheritance\ndotnet_diagnostic.CA1501.severity = none\n\n# CA1502: Avoid excessive complexity\ndotnet_diagnostic.CA1502.severity = none\n\n# CA1505: Avoid unmaintainable code\ndotnet_diagnostic.CA1505.severity = none\n\n# CA1506: Avoid excessive class coupling\ndotnet_diagnostic.CA1506.severity = none\n\n# CA1507: Use nameof to express symbol names\ndotnet_diagnostic.CA1507.severity = warning\n\n# CA1508: Avoid dead conditional code\ndotnet_diagnostic.CA1508.severity = none\n\n# CA1509: Invalid entry in code metrics rule specification file\ndotnet_diagnostic.CA1509.severity = none\n\n# CA1510: Use ArgumentNullException throw helper\n# dotnet_diagnostic.CA1510.severity = warning\n\n# CA1511: Use ArgumentException throw helper\ndotnet_diagnostic.CA1511.severity = warning\n\n# CA1512: Use ArgumentOutOfRangeException throw helper\ndotnet_diagnostic.CA1512.severity = warning\n\n# CA1513: Use ObjectDisposedException throw helper\ndotnet_diagnostic.CA1513.severity = warning\n\n# CA1514: Avoid redundant length argument\ndotnet_diagnostic.CA1514.severity = warning\n\n# CA1515: Consider making public types internal\ndotnet_diagnostic.CA1515.severity = none\n\n# CA1700: Do not name enum values 'Reserved'\ndotnet_diagnostic.CA1700.severity = none\n\n# CA1707: Identifiers should not contain underscores\ndotnet_diagnostic.CA1707.severity = none\n\n# CA1708: Identifiers should differ by more than case\ndotnet_diagnostic.CA1708.severity = none\n\n# CA1710: Identifiers should have correct suffix\ndotnet_diagnostic.CA1710.severity = none\n\n# CA1711: Identifiers should not have incorrect suffix\ndotnet_diagnostic.CA1711.severity = none\n\n# CA1712: Do not prefix enum values with type name\ndotnet_diagnostic.CA1712.severity = none\n\n# CA1713: Events should not have 'Before' or 'After' prefix\ndotnet_diagnostic.CA1713.severity = none\n\n# CA1715: Identifiers should have correct prefix\ndotnet_diagnostic.CA1715.severity = none\n\n# CA1716: Identifiers should not match keywords\ndotnet_diagnostic.CA1716.severity = none\n\n# CA1720: Identifier contains type name\ndotnet_diagnostic.CA1720.severity = none\n\n# CA1721: Property names should not match get methods\ndotnet_diagnostic.CA1721.severity = none\n\n# CA1724: Type names should not match namespaces\ndotnet_diagnostic.CA1724.severity = none\n\n# CA1725: Parameter names should match base declaration\ndotnet_diagnostic.CA1725.severity = suggestion\n\n# CA1727: Use PascalCase for named placeholders\n# dotnet_diagnostic.CA1727.severity = warning\n\n# CA1802: Use literals where appropriate\ndotnet_diagnostic.CA1802.severity = warning\ndotnet_code_quality.CA1802.api_surface = private, internal\n\n# CA1805: Do not initialize unnecessarily\ndotnet_diagnostic.CA1805.severity = warning\n\n# CA1806: Do not ignore method results\ndotnet_diagnostic.CA1806.severity = none\n\n# CA1810: Initialize reference type static fields inline\ndotnet_diagnostic.CA1810.severity = warning\n\n# CA1812: Avoid uninstantiated internal classes\ndotnet_diagnostic.CA1812.severity = none\n\n# CA1813: Avoid unsealed attributes\ndotnet_diagnostic.CA1813.severity = none\n\n# CA1814: Prefer jagged arrays over multidimensional\ndotnet_diagnostic.CA1814.severity = none\n\n# CA1815: Override equals and operator equals on value types\ndotnet_diagnostic.CA1815.severity = none\n\n# CA1816: Dispose methods should call SuppressFinalize\ndotnet_diagnostic.CA1816.severity = none\n\n# CA1819: Properties should not return arrays\ndotnet_diagnostic.CA1819.severity = none\n\n# CA1820: Test for empty strings using string length\ndotnet_diagnostic.CA1820.severity = suggestion\n\n# CA1821: Remove empty Finalizers\ndotnet_diagnostic.CA1821.severity = warning\n\n# CA1822: Mark members as static\ndotnet_diagnostic.CA1822.severity = warning\ndotnet_code_quality.CA1822.api_surface = private, internal\n\n# CA1823: Avoid unused private fields\ndotnet_diagnostic.CA1823.severity = warning\n\n# CA1824: Mark assemblies with NeutralResourcesLanguageAttribute\ndotnet_diagnostic.CA1824.severity = warning\n\n# CA1825: Avoid zero-length array allocations\ndotnet_diagnostic.CA1825.severity = warning\n\n# CA1826: Do not use Enumerable methods on indexable collections\ndotnet_diagnostic.CA1826.severity = warning\n\n# CA1827: Do not use Count() or LongCount() when Any() can be used\ndotnet_diagnostic.CA1827.severity = warning\n\n# CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used\ndotnet_diagnostic.CA1828.severity = warning\n\n# CA1829: Use Length/Count property instead of Count() when available\ndotnet_diagnostic.CA1829.severity = warning\n\n# CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder\ndotnet_diagnostic.CA1830.severity = warning\n\n# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate\ndotnet_diagnostic.CA1831.severity = warning\n\n# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate\ndotnet_diagnostic.CA1832.severity = warning\n\n# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate\ndotnet_diagnostic.CA1833.severity = warning\n\n# CA1834: Consider using 'StringBuilder.Append(char)' when applicable\ndotnet_diagnostic.CA1834.severity = warning\n\n# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'\ndotnet_diagnostic.CA1835.severity = warning\n\n# CA1836: Prefer IsEmpty over Count\ndotnet_diagnostic.CA1836.severity = warning\n\n# CA1837: Use 'Environment.ProcessId'\ndotnet_diagnostic.CA1837.severity = warning\n\n# CA1838: Avoid 'StringBuilder' parameters for P/Invokes\ndotnet_diagnostic.CA1838.severity = warning\n\n# CA1839: Use 'Environment.ProcessPath'\ndotnet_diagnostic.CA1839.severity = warning\n\n# CA1840: Use 'Environment.CurrentManagedThreadId'\ndotnet_diagnostic.CA1840.severity = warning\n\n# CA1841: Prefer Dictionary.Contains methods\ndotnet_diagnostic.CA1841.severity = warning\n\n# CA1842: Do not use 'WhenAll' with a single task\ndotnet_diagnostic.CA1842.severity = warning\n\n# CA1843: Do not use 'WaitAll' with a single task\ndotnet_diagnostic.CA1843.severity = warning\n\n# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream'\ndotnet_diagnostic.CA1844.severity = warning\n\n# CA1845: Use span-based 'string.Concat'\ndotnet_diagnostic.CA1845.severity = warning\n\n# CA1846: Prefer 'AsSpan' over 'Substring'\ndotnet_diagnostic.CA1846.severity = warning\n\n# CA1847: Use char literal for a single character lookup\ndotnet_diagnostic.CA1847.severity = warning\n\n# CA1848: Use the LoggerMessage delegates\ndotnet_diagnostic.CA1848.severity = none\n\n# CA1849: Call async methods when in an async method\ndotnet_diagnostic.CA1849.severity = suggestion\n\n# CA1850: Prefer static 'HashData' method over 'ComputeHash'\ndotnet_diagnostic.CA1850.severity = warning\n\n# CA1851: Possible multiple enumerations of 'IEnumerable' collection\ndotnet_diagnostic.CA1851.severity = suggestion\n\n# CA1852: Seal internal types\ndotnet_diagnostic.CA1852.severity = warning\n\n# CA1853: Unnecessary call to 'Dictionary.ContainsKey(key)'\ndotnet_diagnostic.CA1853.severity = warning\n\n# CA1854: Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method\ndotnet_diagnostic.CA1854.severity = warning\n\n# CA1855: Prefer 'Clear' over 'Fill'\ndotnet_diagnostic.CA1855.severity = warning\n\n# CA1856: Incorrect usage of ConstantExpected attribute\ndotnet_diagnostic.CA1856.severity = error\n\n# CA1857: A constant is expected for the parameter\ndotnet_diagnostic.CA1857.severity = warning\n\n# CA1858: Use 'StartsWith' instead of 'IndexOf'\ndotnet_diagnostic.CA1858.severity = warning\n\n# CA1859: Use concrete types when possible for improved performance\ndotnet_diagnostic.CA1859.severity = warning\n\n# CA1860: Avoid using 'Enumerable.Any()' extension method\ndotnet_diagnostic.CA1860.severity = warning\n\n# CA1861: Avoid constant arrays as arguments\ndotnet_diagnostic.CA1861.severity = warning\n\n# CA1862: Prefer using 'StringComparer'/'StringComparison' to perform case-insensitive string comparisons\ndotnet_diagnostic.CA1862.severity = suggestion\n\n# CA1863: Use 'CompositeFormat'\ndotnet_diagnostic.CA1863.severity = suggestion\n\n# CA1864: Prefer the 'IDictionary.TryAdd(TKey, TValue)' method\ndotnet_diagnostic.CA1864.severity = warning\n\n# CA1865: Use char overload\ndotnet_diagnostic.CA1865.severity = warning\n\n# CA1866: Use char overload\ndotnet_diagnostic.CA1866.severity = warning\n\n# CA1867: Use char overload\ndotnet_diagnostic.CA1867.severity = warning\n\n# CA1868: Unnecessary call to 'Contains' for sets\ndotnet_diagnostic.CA1868.severity = warning\n\n# CA1869: Cache and reuse 'JsonSerializerOptions' instances\ndotnet_diagnostic.CA1869.severity = warning\n\n# CA1870: Use a cached 'SearchValues' instance\ndotnet_diagnostic.CA1870.severity = warning\n\n# CA1871: Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull'\ndotnet_diagnostic.CA1871.severity = warning\n\n# CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'\ndotnet_diagnostic.CA1872.severity = warning\n\n# CA2000: Dispose objects before losing scope\ndotnet_diagnostic.CA2000.severity = none\n\n# CA2002: Do not lock on objects with weak identity\ndotnet_diagnostic.CA2002.severity = none\n\n# CA2007: Consider calling ConfigureAwait on the awaited task\n# dotnet_diagnostic.CA2007.severity = warning\n\n# CA2008: Do not create tasks without passing a TaskScheduler\ndotnet_diagnostic.CA2008.severity = warning\n\n# CA2009: Do not call ToImmutableCollection on an ImmutableCollection value\ndotnet_diagnostic.CA2009.severity = warning\n\n# CA2011: Avoid infinite recursion\ndotnet_diagnostic.CA2011.severity = warning\n\n# CA2012: Use ValueTasks correctly\ndotnet_diagnostic.CA2012.severity = warning\n\n# CA2013: Do not use ReferenceEquals with value types\ndotnet_diagnostic.CA2013.severity = warning\n\n# CA2014: Do not use stackalloc in loops\ndotnet_diagnostic.CA2014.severity = warning\n\n# CA2015: Do not define finalizers for types derived from MemoryManager<T>\ndotnet_diagnostic.CA2015.severity = warning\n\n# CA2016: Forward the 'CancellationToken' parameter to methods\ndotnet_diagnostic.CA2016.severity = warning\n\n# CA2017: Parameter count mismatch\ndotnet_diagnostic.CA2017.severity = warning\n\n# CA2018: 'Buffer.BlockCopy' expects the number of bytes to be copied for the 'count' argument\ndotnet_diagnostic.CA2018.severity = warning\n\n# CA2019: Improper 'ThreadStatic' field initialization\ndotnet_diagnostic.CA2019.severity = warning\n\n# CA2020: Prevent behavioral change\ndotnet_diagnostic.CA2020.severity = warning\n\n# CA2021: Do not call Enumerable.Cast<T> or Enumerable.OfType<T> with incompatible types\ndotnet_diagnostic.CA2021.severity = warning\n\n# CA2022: Avoid inexact read with 'Stream.Read'\ndotnet_diagnostic.CA2022.severity = warning\n\n# CA2100: Review SQL queries for security vulnerabilities\ndotnet_diagnostic.CA2100.severity = none\n\n# CA2101: Specify marshaling for P/Invoke string arguments\ndotnet_diagnostic.CA2101.severity = none\n\n# CA2119: Seal methods that satisfy private interfaces\ndotnet_diagnostic.CA2119.severity = none\n\n# CA2153: Do Not Catch Corrupted State Exceptions\ndotnet_diagnostic.CA2153.severity = none\n\n# CA2200: Rethrow to preserve stack details\ndotnet_diagnostic.CA2200.severity = warning\n\n# CA2201: Do not raise reserved exception types\ndotnet_diagnostic.CA2201.severity = none\n\n# CA2207: Initialize value type static fields inline\ndotnet_diagnostic.CA2207.severity = warning\n\n# CA2208: Instantiate argument exceptions correctly\ndotnet_diagnostic.CA2208.severity = warning\ndotnet_code_quality.CA2208.api_surface = public\n\n# CA2211: Non-constant fields should not be visible\ndotnet_diagnostic.CA2211.severity = none\n\n# CA2213: Disposable fields should be disposed\ndotnet_diagnostic.CA2213.severity = none\n\n# CA2214: Do not call overridable methods in constructors\ndotnet_diagnostic.CA2214.severity = none\n\n# CA2215: Dispose methods should call base class dispose\ndotnet_diagnostic.CA2215.severity = none\n\n# CA2216: Disposable types should declare finalizer\ndotnet_diagnostic.CA2216.severity = none\n\n# CA2217: Do not mark enums with FlagsAttribute\ndotnet_diagnostic.CA2217.severity = none\n\n# CA2218: Override GetHashCode on overriding Equals\ndotnet_diagnostic.CA2218.severity = none\n\n# CA2219: Do not raise exceptions in finally clauses\ndotnet_diagnostic.CA2219.severity = none\n\n# CA2224: Override Equals on overloading operator equals\ndotnet_diagnostic.CA2224.severity = none\n\n# CA2225: Operator overloads have named alternates\ndotnet_diagnostic.CA2225.severity = none\n\n# CA2226: Operators should have symmetrical overloads\ndotnet_diagnostic.CA2226.severity = none\n\n# CA2227: Collection properties should be read only\ndotnet_diagnostic.CA2227.severity = none\n\n# CA2231: Overload operator equals on overriding value type Equals\ndotnet_diagnostic.CA2231.severity = none\n\n# CA2234: Pass system uri objects instead of strings\ndotnet_diagnostic.CA2234.severity = none\n\n# CA2235: Mark all non-serializable fields\ndotnet_diagnostic.CA2235.severity = none\n\n# CA2237: Mark ISerializable types with serializable\ndotnet_diagnostic.CA2237.severity = none\n\n# CA2241: Provide correct arguments to formatting methods\ndotnet_diagnostic.CA2241.severity = warning\n\n# CA2242: Test for NaN correctly\ndotnet_diagnostic.CA2242.severity = warning\n\n# CA2243: Attribute string literals should parse correctly\ndotnet_diagnostic.CA2243.severity = warning\n\n# CA2244: Do not duplicate indexed element initializations\ndotnet_diagnostic.CA2244.severity = warning\n\n# CA2245: Do not assign a property to itself\ndotnet_diagnostic.CA2245.severity = warning\n\n# CA2246: Assigning symbol and its member in the same statement\ndotnet_diagnostic.CA2246.severity = warning\n\n# CA2247: Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum\ndotnet_diagnostic.CA2247.severity = warning\n\n# CA2248: Provide correct 'enum' argument to 'Enum.HasFlag'\ndotnet_diagnostic.CA2248.severity = warning\n\n# CA2249: Consider using 'string.Contains' instead of 'string.IndexOf'\ndotnet_diagnostic.CA2249.severity = warning\n\n# CA2250: Use 'ThrowIfCancellationRequested'\ndotnet_diagnostic.CA2250.severity = warning\n\n# CA2251: Use 'string.Equals'\ndotnet_diagnostic.CA2251.severity = warning\n\n# CA2252: This API requires opting into preview features\ndotnet_diagnostic.CA2252.severity = error\n\n# CA2253: Named placeholders should not be numeric values\ndotnet_diagnostic.CA2253.severity = warning\n\n# CA2254: Template should be a static expression\ndotnet_diagnostic.CA2254.severity = none\n\n# CA2255: The 'ModuleInitializer' attribute should not be used in libraries\ndotnet_diagnostic.CA2255.severity = warning\n\n# CA2256: All members declared in parent interfaces must have an implementation in a DynamicInterfaceCastableImplementation-attributed interface\ndotnet_diagnostic.CA2256.severity = warning\n\n# CA2257: Members defined on an interface with the 'DynamicInterfaceCastableImplementationAttribute' should be 'static'\ndotnet_diagnostic.CA2257.severity = warning\n\n# CA2258: Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported\ndotnet_diagnostic.CA2258.severity = warning\n\n# CA2259: 'ThreadStatic' only affects static fields\ndotnet_diagnostic.CA2259.severity = warning\n\n# CA2260: Use correct type parameter\ndotnet_diagnostic.CA2260.severity = warning\n\n# CA2261: Do not use ConfigureAwaitOptions.SuppressThrowing with Task<TResult>\ndotnet_diagnostic.CA2261.severity = warning\n\n# CA2262: Set 'MaxResponseHeadersLength' properly\ndotnet_diagnostic.CA2262.severity = warning\n\n# CA2263: Prefer generic overload when type is known\ndotnet_diagnostic.CA2263.severity = suggestion\n\n# CA2264: Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull'\ndotnet_diagnostic.CA2264.severity = warning\n\n# CA2265: Do not compare Span<T> to 'null' or 'default'\ndotnet_diagnostic.CA2265.severity = warning\n\n# CA2300: Do not use insecure deserializer BinaryFormatter\ndotnet_diagnostic.CA2300.severity = none\n\n# CA2301: Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder\ndotnet_diagnostic.CA2301.severity = none\n\n# CA2302: Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize\ndotnet_diagnostic.CA2302.severity = none\n\n# CA2305: Do not use insecure deserializer LosFormatter\ndotnet_diagnostic.CA2305.severity = none\n\n# CA2310: Do not use insecure deserializer NetDataContractSerializer\ndotnet_diagnostic.CA2310.severity = none\n\n# CA2311: Do not deserialize without first setting NetDataContractSerializer.Binder\ndotnet_diagnostic.CA2311.severity = none\n\n# CA2312: Ensure NetDataContractSerializer.Binder is set before deserializing\ndotnet_diagnostic.CA2312.severity = none\n\n# CA2315: Do not use insecure deserializer ObjectStateFormatter\ndotnet_diagnostic.CA2315.severity = none\n\n# CA2321: Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver\ndotnet_diagnostic.CA2321.severity = none\n\n# CA2322: Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing\ndotnet_diagnostic.CA2322.severity = none\n\n# CA2326: Do not use TypeNameHandling values other than None\ndotnet_diagnostic.CA2326.severity = none\n\n# CA2327: Do not use insecure JsonSerializerSettings\ndotnet_diagnostic.CA2327.severity = none\n\n# CA2328: Ensure that JsonSerializerSettings are secure\ndotnet_diagnostic.CA2328.severity = none\n\n# CA2329: Do not deserialize with JsonSerializer using an insecure configuration\ndotnet_diagnostic.CA2329.severity = none\n\n# CA2330: Ensure that JsonSerializer has a secure configuration when deserializing\ndotnet_diagnostic.CA2330.severity = none\n\n# CA2350: Do not use DataTable.ReadXml() with untrusted data\ndotnet_diagnostic.CA2350.severity = none\n\n# CA2351: Do not use DataSet.ReadXml() with untrusted data\ndotnet_diagnostic.CA2351.severity = none\n\n# CA2352: Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks\ndotnet_diagnostic.CA2352.severity = none\n\n# CA2353: Unsafe DataSet or DataTable in serializable type\ndotnet_diagnostic.CA2353.severity = none\n\n# CA2354: Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks\ndotnet_diagnostic.CA2354.severity = none\n\n# CA2355: Unsafe DataSet or DataTable type found in deserializable object graph\ndotnet_diagnostic.CA2355.severity = none\n\n# CA2356: Unsafe DataSet or DataTable type in web deserializable object graph\ndotnet_diagnostic.CA2356.severity = none\n\n# CA2361: Ensure auto-generated class containing DataSet.ReadXml() is not used with untrusted data\ndotnet_diagnostic.CA2361.severity = none\n\n# CA2362: Unsafe DataSet or DataTable in auto-generated serializable type can be vulnerable to remote code execution attacks\ndotnet_diagnostic.CA2362.severity = none\n\n# CA3001: Review code for SQL injection vulnerabilities\ndotnet_diagnostic.CA3001.severity = none\n\n# CA3002: Review code for XSS vulnerabilities\ndotnet_diagnostic.CA3002.severity = none\n\n# CA3003: Review code for file path injection vulnerabilities\ndotnet_diagnostic.CA3003.severity = none\n\n# CA3004: Review code for information disclosure vulnerabilities\ndotnet_diagnostic.CA3004.severity = none\n\n# CA3005: Review code for LDAP injection vulnerabilities\ndotnet_diagnostic.CA3005.severity = none\n\n# CA3006: Review code for process command injection vulnerabilities\ndotnet_diagnostic.CA3006.severity = none\n\n# CA3007: Review code for open redirect vulnerabilities\ndotnet_diagnostic.CA3007.severity = none\n\n# CA3008: Review code for XPath injection vulnerabilities\ndotnet_diagnostic.CA3008.severity = none\n\n# CA3009: Review code for XML injection vulnerabilities\ndotnet_diagnostic.CA3009.severity = none\n\n# CA3010: Review code for XAML injection vulnerabilities\ndotnet_diagnostic.CA3010.severity = none\n\n# CA3011: Review code for DLL injection vulnerabilities\ndotnet_diagnostic.CA3011.severity = none\n\n# CA3012: Review code for regex injection vulnerabilities\ndotnet_diagnostic.CA3012.severity = none\n\n# CA3061: Do Not Add Schema By URL\ndotnet_diagnostic.CA3061.severity = warning\n\n# CA3075: Insecure DTD processing in XML\ndotnet_diagnostic.CA3075.severity = warning\n\n# CA3076: Insecure XSLT script processing\ndotnet_diagnostic.CA3076.severity = warning\n\n# CA3077: Insecure Processing in API Design, XmlDocument and XmlTextReader\ndotnet_diagnostic.CA3077.severity = warning\n\n# CA3147: Mark Verb Handlers With Validate Antiforgery Token\ndotnet_diagnostic.CA3147.severity = warning\n\n# CA5350: Do Not Use Weak Cryptographic Algorithms\ndotnet_diagnostic.CA5350.severity = warning\n\n# CA5351: Do Not Use Broken Cryptographic Algorithms\ndotnet_diagnostic.CA5351.severity = warning\n\n# CA5358: Review cipher mode usage with cryptography experts\ndotnet_diagnostic.CA5358.severity = none\n\n# CA5359: Do Not Disable Certificate Validation\ndotnet_diagnostic.CA5359.severity = warning\n\n# CA5360: Do Not Call Dangerous Methods In Deserialization\ndotnet_diagnostic.CA5360.severity = warning\n\n# CA5361: Do Not Disable SChannel Use of Strong Crypto\ndotnet_diagnostic.CA5361.severity = warning\n\n# CA5362: Potential reference cycle in deserialized object graph\ndotnet_diagnostic.CA5362.severity = none\n\n# CA5363: Do Not Disable Request Validation\ndotnet_diagnostic.CA5363.severity = warning\n\n# CA5364: Do Not Use Deprecated Security Protocols\ndotnet_diagnostic.CA5364.severity = warning\n\n# CA5365: Do Not Disable HTTP Header Checking\ndotnet_diagnostic.CA5365.severity = warning\n\n# CA5366: Use XmlReader for 'DataSet.ReadXml()'\ndotnet_diagnostic.CA5366.severity = none\n\n# CA5367: Do Not Serialize Types With Pointer Fields\ndotnet_diagnostic.CA5367.severity = none\n\n# CA5368: Set ViewStateUserKey For Classes Derived From Page\ndotnet_diagnostic.CA5368.severity = warning\n\n# CA5369: Use XmlReader for 'XmlSerializer.Deserialize()'\ndotnet_diagnostic.CA5369.severity = none\n\n# CA5370: Use XmlReader for XmlValidatingReader constructor\ndotnet_diagnostic.CA5370.severity = warning\n\n# CA5371: Use XmlReader for 'XmlSchema.Read()'\ndotnet_diagnostic.CA5371.severity = none\n\n# CA5372: Use XmlReader for XPathDocument constructor\ndotnet_diagnostic.CA5372.severity = none\n\n# CA5373: Do not use obsolete key derivation function\ndotnet_diagnostic.CA5373.severity = warning\n\n# CA5374: Do Not Use XslTransform\ndotnet_diagnostic.CA5374.severity = warning\n\n# CA5375: Do Not Use Account Shared Access Signature\ndotnet_diagnostic.CA5375.severity = none\n\n# CA5376: Use SharedAccessProtocol HttpsOnly\ndotnet_diagnostic.CA5376.severity = warning\n\n# CA5377: Use Container Level Access Policy\ndotnet_diagnostic.CA5377.severity = warning\n\n# CA5378: Do not disable ServicePointManagerSecurityProtocols\ndotnet_diagnostic.CA5378.severity = warning\n\n# CA5379: Ensure Key Derivation Function algorithm is sufficiently strong\ndotnet_diagnostic.CA5379.severity = warning\n\n# CA5380: Do Not Add Certificates To Root Store\ndotnet_diagnostic.CA5380.severity = warning\n\n# CA5381: Ensure Certificates Are Not Added To Root Store\ndotnet_diagnostic.CA5381.severity = warning\n\n# CA5382: Use Secure Cookies In ASP.NET Core\ndotnet_diagnostic.CA5382.severity = none\n\n# CA5383: Ensure Use Secure Cookies In ASP.NET Core\ndotnet_diagnostic.CA5383.severity = none\n\n# CA5384: Do Not Use Digital Signature Algorithm (DSA)\ndotnet_diagnostic.CA5384.severity = warning\n\n# CA5385: Use Rivest-Shamir-Adleman (RSA) Algorithm With Sufficient Key Size\ndotnet_diagnostic.CA5385.severity = warning\n\n# CA5386: Avoid hardcoding SecurityProtocolType value\ndotnet_diagnostic.CA5386.severity = none\n\n# CA5387: Do Not Use Weak Key Derivation Function With Insufficient Iteration Count\ndotnet_diagnostic.CA5387.severity = none\n\n# CA5388: Ensure Sufficient Iteration Count When Using Weak Key Derivation Function\ndotnet_diagnostic.CA5388.severity = none\n\n# CA5389: Do Not Add Archive Item's Path To The Target File System Path\ndotnet_diagnostic.CA5389.severity = none\n\n# CA5390: Do not hard-code encryption key\ndotnet_diagnostic.CA5390.severity = none\n\n# CA5391: Use antiforgery tokens in ASP.NET Core MVC controllers\ndotnet_diagnostic.CA5391.severity = none\n\n# CA5392: Use DefaultDllImportSearchPaths attribute for P/Invokes\ndotnet_diagnostic.CA5392.severity = none\n\n# CA5393: Do not use unsafe DllImportSearchPath value\ndotnet_diagnostic.CA5393.severity = none\n\n# CA5394: Do not use insecure randomness\ndotnet_diagnostic.CA5394.severity = none\n\n# CA5395: Miss HttpVerb attribute for action methods\ndotnet_diagnostic.CA5395.severity = none\n\n# CA5396: Set HttpOnly to true for HttpCookie\ndotnet_diagnostic.CA5396.severity = none\n\n# CA5397: Do not use deprecated SslProtocols values\ndotnet_diagnostic.CA5397.severity = none\n\n# CA5398: Avoid hardcoded SslProtocols values\ndotnet_diagnostic.CA5398.severity = none\n\n# CA5399: HttpClients should enable certificate revocation list checks\ndotnet_diagnostic.CA5399.severity = none\n\n# CA5400: Ensure HttpClient certificate revocation list check is not disabled\ndotnet_diagnostic.CA5400.severity = none\n\n# CA5401: Do not use CreateEncryptor with non-default IV\ndotnet_diagnostic.CA5401.severity = none\n\n# CA5402: Use CreateEncryptor with the default IV\ndotnet_diagnostic.CA5402.severity = none\n\n# CA5403: Do not hard-code certificate\ndotnet_diagnostic.CA5403.severity = none\n\n# CA5404: Do not disable token validation checks\ndotnet_diagnostic.CA5404.severity = none\n\n# CA5405: Do not always skip token validation in delegates\ndotnet_diagnostic.CA5405.severity = none\n\n# IL3000: Avoid using accessing Assembly file path when publishing as a single-file\ndotnet_diagnostic.IL3000.severity = warning\n\n# IL3001: Avoid using accessing Assembly file path when publishing as a single-file\ndotnet_diagnostic.IL3001.severity = warning\n\n# IL3002: Using member with RequiresAssemblyFilesAttribute can break functionality when embedded in a single-file app\ndotnet_diagnostic.IL3002.severity = warning\n\n# RS1001: Missing diagnostic analyzer attribute\ndotnet_diagnostic.RS1001.severity = warning\n\n# RS1002: Missing kind argument when registering an analyzer action\ndotnet_diagnostic.RS1002.severity = warning\n\n# RS1003: Unsupported SymbolKind argument when registering a symbol analyzer action\ndotnet_diagnostic.RS1003.severity = warning\n\n# RS1004: Recommend adding language support to diagnostic analyzer\ndotnet_diagnostic.RS1004.severity = warning\n\n# RS1005: ReportDiagnostic invoked with an unsupported DiagnosticDescriptor\ndotnet_diagnostic.RS1005.severity = warning\n\n# RS1006: Invalid type argument for DiagnosticAnalyzer's Register method\ndotnet_diagnostic.RS1006.severity = warning\n\n# RS1007: Provide localizable arguments to diagnostic descriptor constructor\ndotnet_diagnostic.RS1007.severity = none\n\n# RS1008: Avoid storing per-compilation data into the fields of a diagnostic analyzer\ndotnet_diagnostic.RS1008.severity = warning\n\n# RS1009: Only internal implementations of this interface are allowed\ndotnet_diagnostic.RS1009.severity = error\n\n# RS1010: Create code actions should have a unique EquivalenceKey for FixAll occurrences support\ndotnet_diagnostic.RS1010.severity = warning\n\n# RS1011: Use code actions that have a unique EquivalenceKey for FixAll occurrences support\ndotnet_diagnostic.RS1011.severity = warning\n\n# RS1012: Start action has no registered actions\ndotnet_diagnostic.RS1012.severity = warning\n\n# RS1013: Start action has no registered non-end actions\ndotnet_diagnostic.RS1013.severity = warning\n\n# RS1014: Do not ignore values returned by methods on immutable objects\ndotnet_diagnostic.RS1014.severity = warning\n\n# RS1015: Provide non-null 'helpLinkUri' value to diagnostic descriptor constructor\ndotnet_diagnostic.RS1015.severity = none\n\n# RS1016: Code fix providers should provide FixAll support\ndotnet_diagnostic.RS1016.severity = suggestion\n\n# RS1017: DiagnosticId for analyzers must be a non-null constant\ndotnet_diagnostic.RS1017.severity = warning\n\n# RS1018: DiagnosticId for analyzers must be in specified format\ndotnet_diagnostic.RS1018.severity = warning\n\n# RS1019: DiagnosticId must be unique across analyzers\ndotnet_diagnostic.RS1019.severity = warning\n\n# RS1020: Category for analyzers must be from the specified values\ndotnet_diagnostic.RS1020.severity = none\n\n# RS1021: Invalid entry in analyzer category and diagnostic ID range specification file\ndotnet_diagnostic.RS1021.severity = warning\n\n# RS1022: Do not use types from Workspaces assembly in an analyzer\ndotnet_diagnostic.RS1022.severity = warning\n\n# RS1023: Upgrade MSBuildWorkspace\ndotnet_diagnostic.RS1023.severity = warning\n\n# RS1024: Symbols should be compared for equality\ndotnet_diagnostic.RS1024.severity = warning\n\n# RS1025: Configure generated code analysis\ndotnet_diagnostic.RS1025.severity = warning\n\n# RS1026: Enable concurrent execution\ndotnet_diagnostic.RS1026.severity = warning\n\n# RS1027: Types marked with DiagnosticAnalyzerAttribute(s) should inherit from DiagnosticAnalyzer\ndotnet_diagnostic.RS1027.severity = warning\n\n# RS1028: Provide non-null 'customTags' value to diagnostic descriptor constructor\ndotnet_diagnostic.RS1028.severity = none\n\n# RS1029: Do not use reserved diagnostic IDs\ndotnet_diagnostic.RS1029.severity = warning\n\n# RS1030: Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer\ndotnet_diagnostic.RS1030.severity = warning\n\n# RS1031: Define diagnostic title correctly\ndotnet_diagnostic.RS1031.severity = warning\n\n# RS1032: Define diagnostic message correctly\ndotnet_diagnostic.RS1032.severity = warning\n\n# RS1033: Define diagnostic description correctly\ndotnet_diagnostic.RS1033.severity = warning\n\n# RS1034: Prefer 'IsKind' for checking syntax kinds\ndotnet_diagnostic.RS1034.severity = warning\n\n# RS1035: Do not use APIs banned for analyzers\ndotnet_diagnostic.RS1035.severity = error\n\n# RS1036: Specify analyzer banned API enforcement setting\ndotnet_diagnostic.RS1036.severity = warning\n\n# RS1037: Add \"CompilationEnd\" custom tag to compilation end diagnostic descriptor\ndotnet_diagnostic.RS1037.severity = warning\n\n# RS1038: Compiler extensions should be implemented in assemblies with compiler-provided references\ndotnet_diagnostic.RS1038.severity = suggestion\n\n# RS2000: Add analyzer diagnostic IDs to analyzer release\ndotnet_diagnostic.RS2000.severity = warning\n\n# RS2001: Ensure up-to-date entry for analyzer diagnostic IDs are added to analyzer release\ndotnet_diagnostic.RS2001.severity = warning\n\n# RS2002: Do not add removed analyzer diagnostic IDs to unshipped analyzer release\ndotnet_diagnostic.RS2002.severity = warning\n\n# RS2003: Shipped diagnostic IDs that are no longer reported should have an entry in the 'Removed Rules' table in unshipped file\ndotnet_diagnostic.RS2003.severity = warning\n\n# RS2004: Diagnostic IDs marked as removed in analyzer release file should not be reported by analyzers\ndotnet_diagnostic.RS2004.severity = warning\n\n# RS2005: Remove duplicate entries for diagnostic ID in the same analyzer release\ndotnet_diagnostic.RS2005.severity = warning\n\n# RS2006: Remove duplicate entries for diagnostic ID between analyzer releases\ndotnet_diagnostic.RS2006.severity = warning\n\n# RS2007: Invalid entry in analyzer release file\ndotnet_diagnostic.RS2007.severity = warning\n\n# RS2008: Enable analyzer release tracking\ndotnet_diagnostic.RS2008.severity = warning\n\n# SA0001: XML comments\ndotnet_diagnostic.SA0001.severity = none\n\n# SA1000: Spacing around keywords\ndotnet_diagnostic.SA1000.severity = warning\n\n# SA1001: Commas should not be preceded by whitespace\ndotnet_diagnostic.SA1001.severity = warning\n\n# SA1002: Semicolons should not be preceded by a space\ndotnet_diagnostic.SA1002.severity = none\n\n# SA1003: Operator should not appear at the end of a line\ndotnet_diagnostic.SA1003.severity = none\n\n# SA1004: Documentation line should begin with a space\ndotnet_diagnostic.SA1004.severity = none\n\n# SA1005: Single line comment should begin with a space\ndotnet_diagnostic.SA1005.severity = none\n\n# SA1008: Opening parenthesis should not be preceded by a space\ndotnet_diagnostic.SA1008.severity = none\n\n# SA1009: Closing parenthesis should not be followed by a space\ndotnet_diagnostic.SA1009.severity = none\n\n# SA1010: Opening square brackets should not be preceded by a space\ndotnet_diagnostic.SA1010.severity = none\n\n# SA1011: Closing square bracket should be followed by a space\ndotnet_diagnostic.SA1011.severity = none\n\n# SA1012: Opening brace should be followed by a space\ndotnet_diagnostic.SA1012.severity = none\n\n# SA1013: Closing brace should be preceded by a space\ndotnet_diagnostic.SA1013.severity = none\n\n# SA1014: Opening generic brackets should not be preceded by a space\ndotnet_diagnostic.SA1014.severity = warning\n\n# SA1015: Closing generic bracket should not be followed by a space\ndotnet_diagnostic.SA1015.severity = none\n\n# SA1018: Nullable type symbol should not be preceded by a space\ndotnet_diagnostic.SA1018.severity = warning\n\n# SA1020: Increment symbol should not be preceded by a space\ndotnet_diagnostic.SA1020.severity = warning\n\n# SA1021: Negative sign should be preceded by a space\ndotnet_diagnostic.SA1021.severity = none\n\n# SA1023: Dereference symbol '*' should not be preceded by a space.\"\ndotnet_diagnostic.SA1023.severity = none\n\n# SA1024: Colon should be followed by a space\ndotnet_diagnostic.SA1024.severity = none\n\n# SA1025: Code should not contain multiple whitespace characters in a row\ndotnet_diagnostic.SA1025.severity = none\n\n# SA1026: Keyword followed by span or blank line\ndotnet_diagnostic.SA1026.severity = warning\n\n# SA1027: Tabs and spaces should be used correctly\ndotnet_diagnostic.SA1027.severity = warning\n\n# SA1028: Code should not contain trailing whitespace\ndotnet_diagnostic.SA1028.severity = warning\n\n# SA1100: Do not prefix calls with base unless local implementation exists\ndotnet_diagnostic.SA1100.severity = none\n\n# SA1101: Prefix local calls with this\ndotnet_diagnostic.SA1101.severity = none\n\n# SA1102: Query clause should follow previous clause\ndotnet_diagnostic.SA1102.severity = warning\n\n# SA1105: Query clauses spanning multiple lines should begin on own line\ndotnet_diagnostic.SA1105.severity = warning\n\n# SA1106: Code should not contain empty statements\ndotnet_diagnostic.SA1106.severity = none\n\n# SA1107: Code should not contain multiple statements on one line\ndotnet_diagnostic.SA1107.severity = none\n\n# SA1108: Block statements should not contain embedded comments\ndotnet_diagnostic.SA1108.severity = none\n\n# SA1110: Opening parenthesis or bracket should be on declaration line\ndotnet_diagnostic.SA1110.severity = none\n\n# SA1111: Closing parenthesis should be on line of last parameter\ndotnet_diagnostic.SA1111.severity = none\n\n# SA1113: Comma should be on the same line as previous parameter\ndotnet_diagnostic.SA1113.severity = warning\n\n# SA1114: Parameter list should follow declaration\ndotnet_diagnostic.SA1114.severity = none\n\n# SA1115: Parameter should begin on the line after the previous parameter\ndotnet_diagnostic.SA1115.severity = warning\n\n# SA1116: Split parameters should start on line after declaration\ndotnet_diagnostic.SA1116.severity = none\n\n# SA1117: Parameters should be on same line or separate lines\ndotnet_diagnostic.SA1117.severity = none\n\n# SA1118: Parameter should not span multiple lines\ndotnet_diagnostic.SA1118.severity = none\n\n# SA1119: Statement should not use unnecessary parenthesis\ndotnet_diagnostic.SA1119.severity = none\n\n# SA1120: Comments should contain text\ndotnet_diagnostic.SA1120.severity = none\n\n# SA1121: Use built-in type alias\ndotnet_diagnostic.SA1121.severity = warning\n\n# SA1122: Use string.Empty for empty strings\ndotnet_diagnostic.SA1122.severity = none\n\n# SA1123: Region should not be located within a code element\ndotnet_diagnostic.SA1123.severity = none\n\n# SA1124: Do not use regions\ndotnet_diagnostic.SA1124.severity = none\n\n# SA1125: Use shorthand for nullable types\ndotnet_diagnostic.SA1125.severity = none\n\n# SA1127: Generic type constraints should be on their own line\ndotnet_diagnostic.SA1127.severity = none\n\n# SA1128: Put constructor initializers on their own line\ndotnet_diagnostic.SA1128.severity = none\n\n# SA1129: Do not use default value type constructor\ndotnet_diagnostic.SA1129.severity = warning\n\n# SA1130: Use lambda syntax\ndotnet_diagnostic.SA1130.severity = none\n\n# SA1131: Constant values should appear on the right-hand side of comparisons\ndotnet_diagnostic.SA1131.severity = none\n\n# SA1132: Do not combine fields\ndotnet_diagnostic.SA1132.severity = none\n\n# SA1133: Do not combine attributes\ndotnet_diagnostic.SA1133.severity = none\n\n# SA1134: Each attribute should be placed on its own line of code\ndotnet_diagnostic.SA1134.severity = none\n\n# SA1135: Using directive should be qualified\ndotnet_diagnostic.SA1135.severity = none\n\n# SA1136: Enum values should be on separate lines\ndotnet_diagnostic.SA1136.severity = warning\n\n# SA1137: Elements should have the same indentation\ndotnet_diagnostic.SA1137.severity = none\n\n# SA1139: Use literal suffix notation instead of casting\ndotnet_diagnostic.SA1139.severity = none\n\n# SA1141: Use tuple syntax\ndotnet_diagnostic.SA1141.severity = warning\n\n# SA1142: Refer to tuple elements by name\ndotnet_diagnostic.SA1142.severity = warning\n\n# SA1200: Using directive should appear within a namespace declaration\ndotnet_diagnostic.SA1200.severity = none\n\n# SA1201: Elements should appear in the correct order\ndotnet_diagnostic.SA1201.severity = none\n\n# SA1202: Elements should be ordered by access\ndotnet_diagnostic.SA1202.severity = none\n\n# SA1203: Constants should appear before fields\ndotnet_diagnostic.SA1203.severity = none\n\n# SA1204: Static elements should appear before instance elements\ndotnet_diagnostic.SA1204.severity = none\n\n# SA1205: Partial elements should declare an access modifier\ndotnet_diagnostic.SA1205.severity = warning\n\n# SA1206: Keyword ordering\ndotnet_diagnostic.SA1206.severity = warning\n\n# SA1208: Using directive ordering\ndotnet_diagnostic.SA1208.severity = none\n\n# SA1209: Using alias directives should be placed after all using namespace directives\ndotnet_diagnostic.SA1209.severity = none\n\n# SA1210: Using directives should be ordered alphabetically by the namespaces\ndotnet_diagnostic.SA1210.severity = none\n\n# SA1211: Using alias directive ordering\ndotnet_diagnostic.SA1211.severity = none\n\n# SA1212: A get accessor appears after a set accessor within a property or indexer\ndotnet_diagnostic.SA1212.severity = warning\n\n# SA1214: Readonly fields should appear before non-readonly fields\ndotnet_diagnostic.SA1214.severity = none\n\n# SA1216: Using static directives should be placed at the correct location\ndotnet_diagnostic.SA1216.severity = none\n\n# SA1300: Element should begin with an uppercase letter\ndotnet_diagnostic.SA1300.severity = none\n\n# SA1302: Interface names should begin with I\ndotnet_diagnostic.SA1302.severity = warning\n\n# SA1303: Const field names should begin with upper-case letter\ndotnet_diagnostic.SA1303.severity = none\n\n# SA1304: Non-private readonly fields should begin with upper-case letter\ndotnet_diagnostic.SA1304.severity = none\n\n# SA1306: Field should begin with lower-case letter\ndotnet_diagnostic.SA1306.severity = none\n\n# SA1307: Field should begin with upper-case letter\ndotnet_diagnostic.SA1307.severity = none\n\n# SA1308: Field should not begin with the prefix 's_'\ndotnet_diagnostic.SA1308.severity = none\n\n# SA1309: Field names should not begin with underscore\ndotnet_diagnostic.SA1309.severity = none\n\n# SA1310: Field should not contain an underscore\ndotnet_diagnostic.SA1310.severity = none\n\n# SA1311: Static readonly fields should begin with upper-case letter\ndotnet_diagnostic.SA1311.severity = none\n\n# SA1312: Variable should begin with lower-case letter\ndotnet_diagnostic.SA1312.severity = none\n\n# SA1313: Parameter should begin with lower-case letter\ndotnet_diagnostic.SA1313.severity = none\n\n# SA1314: Type parameter names should begin with T\ndotnet_diagnostic.SA1314.severity = none\n\n# SA1316: Tuple element names should use correct casing\ndotnet_diagnostic.SA1316.severity = none\n\n# SA1400: Member should declare an access modifier\ndotnet_diagnostic.SA1400.severity = warning\n\n# SA1401: Fields should be private\ndotnet_diagnostic.SA1401.severity = none\n\n# SA1402: File may only contain a single type\ndotnet_diagnostic.SA1402.severity = none\n\n# SA1403: File may only contain a single namespace\ndotnet_diagnostic.SA1403.severity = none\n\n# SA1404: Code analysis suppression should have justification\ndotnet_diagnostic.SA1404.severity = warning\n\n# SA1405: Debug.Assert should provide message text\ndotnet_diagnostic.SA1405.severity = none\n\n# SA1407: Arithmetic expressions should declare precedence\ndotnet_diagnostic.SA1407.severity = none\n\n# SA1408: Conditional expressions should declare precedence\ndotnet_diagnostic.SA1408.severity = none\n\n# SA1410: Remove delegate parens when possible\ndotnet_diagnostic.SA1410.severity = warning\n\n# SA1411: Attribute constructor shouldn't use unnecessary parenthesis\ndotnet_diagnostic.SA1411.severity = warning\n\n# SA1413: Use trailing comma in multi-line initializers\ndotnet_diagnostic.SA1413.severity = none\n\n# SA1414: Tuple types in signatures should have element names\ndotnet_diagnostic.SA1414.severity = none\n\n# SA1500: Braces for multi-line statements should not share line\ndotnet_diagnostic.SA1500.severity = none\n\n# SA1501: Statement should not be on a single line\ndotnet_diagnostic.SA1501.severity = none\n\n# SA1502: Element should not be on a single line\ndotnet_diagnostic.SA1502.severity = none\n\n# SA1503: Braces should not be omitted\ndotnet_diagnostic.SA1503.severity = none\n\n# SA1504: All accessors should be single-line or multi-line\ndotnet_diagnostic.SA1504.severity = none\n\n# SA1505: An opening brace should not be followed by a blank line\ndotnet_diagnostic.SA1505.severity = none\n\n# SA1506: Element documentation headers should not be followed by blank line\ndotnet_diagnostic.SA1506.severity = none\n\n# SA1507: Code should not contain multiple blank lines in a row\ndotnet_diagnostic.SA1507.severity = none\n\n# SA1508: A closing brace should not be preceded by a blank line\ndotnet_diagnostic.SA1508.severity = none\n\n# SA1509: Opening braces should not be preceded by blank line\ndotnet_diagnostic.SA1509.severity = none\n\n# SA1510: 'else' statement should not be preceded by a blank line\ndotnet_diagnostic.SA1510.severity = none\n\n# SA1512: Single-line comments should not be followed by blank line\ndotnet_diagnostic.SA1512.severity = none\n\n# SA1513: Closing brace should be followed by blank line\ndotnet_diagnostic.SA1513.severity = none\n\n# SA1514: Element documentation header should be preceded by blank line\ndotnet_diagnostic.SA1514.severity = none\n\n# SA1515: Single-line comment should be preceded by blank line\ndotnet_diagnostic.SA1515.severity = none\n\n# SA1516: Elements should be separated by blank line\ndotnet_diagnostic.SA1516.severity = none\n\n# SA1517: Code should not contain blank lines at start of file\ndotnet_diagnostic.SA1517.severity = warning\n\n# SA1518: Code should not contain blank lines at the end of the file\ndotnet_diagnostic.SA1518.severity = warning\n\n# SA1519: Braces should not be omitted from multi-line child statement\ndotnet_diagnostic.SA1519.severity = none\n\n# SA1520: Use braces consistently\ndotnet_diagnostic.SA1520.severity = none\n\n# SA1600: Elements should be documented\ndotnet_diagnostic.SA1600.severity = none\n\n# SA1601: Partial elements should be documented\ndotnet_diagnostic.SA1601.severity = none\n\n# SA1602: Enumeration items should be documented\ndotnet_diagnostic.SA1602.severity = none\n\n# SA1604: Element documentation should have summary\ndotnet_diagnostic.SA1604.severity = none\n\n# SA1605: Partial element documentation should have summary\ndotnet_diagnostic.SA1605.severity = none\n\n# SA1606: Element documentation should have summary text\ndotnet_diagnostic.SA1606.severity = none\n\n# SA1608: Element documentation should not have default summary\ndotnet_diagnostic.SA1608.severity = none\n\n# SA1610: Property documentation should have value text\ndotnet_diagnostic.SA1610.severity = none\n\n# SA1611: The documentation for parameter 'message' is missing\ndotnet_diagnostic.SA1611.severity = none\n\n# SA1612: The parameter documentation is at incorrect position\ndotnet_diagnostic.SA1612.severity = none\n\n# SA1614: Element parameter documentation should have text\ndotnet_diagnostic.SA1614.severity = none\n\n# SA1615: Element return value should be documented\ndotnet_diagnostic.SA1615.severity = none\n\n# SA1616: Element return value documentation should have text\ndotnet_diagnostic.SA1616.severity = none\n\n# SA1618: The documentation for type parameter is missing\ndotnet_diagnostic.SA1618.severity = none\n\n# SA1619: The documentation for type parameter is missing\ndotnet_diagnostic.SA1619.severity = none\n\n# SA1622: Generic type parameter documentation should have text\ndotnet_diagnostic.SA1622.severity = none\n\n# SA1623: Property documentation text\ndotnet_diagnostic.SA1623.severity = none\n\n# SA1624: Because the property only contains a visible get accessor, the documentation summary text should begin with 'Gets'\ndotnet_diagnostic.SA1624.severity = none\n\n# SA1625: Element documentation should not be copied and pasted\ndotnet_diagnostic.SA1625.severity = none\n\n# SA1626: Single-line comments should not use documentation style slashes\ndotnet_diagnostic.SA1626.severity = none\n\n# SA1627: The documentation text within the \\'exception\\' tag should not be empty\ndotnet_diagnostic.SA1627.severity = none\n\n# SA1629: Documentation text should end with a period\ndotnet_diagnostic.SA1629.severity = none\n\n# SA1633: File should have header\ndotnet_diagnostic.SA1633.severity = none\n\n# SA1642: Constructor summary documentation should begin with standard text\ndotnet_diagnostic.SA1642.severity = none\n\n# SA1643: Destructor summary documentation should begin with standard text\ndotnet_diagnostic.SA1643.severity = none\n\n# SA1649: File name should match first type name\ndotnet_diagnostic.SA1649.severity = none\n\n# IDE0001: Simplify name\ndotnet_diagnostic.IDE0001.severity = suggestion\n\n# IDE0002: Simplify member access\ndotnet_diagnostic.IDE0002.severity = suggestion\n\n# IDE0003: Remove this or Me qualification\ndotnet_diagnostic.IDE0003.severity = suggestion\n\n# IDE0004: Remove Unnecessary Cast\ndotnet_diagnostic.IDE0004.severity = suggestion\n\n# IDE0005: Using directive is unnecessary.\ndotnet_diagnostic.IDE0005.severity = suggestion\n\n# IDE0007: Use implicit type\ndotnet_diagnostic.IDE0007.severity = silent\n\n# IDE0008: Use explicit type\ndotnet_diagnostic.IDE0008.severity = suggestion\n\n# IDE0009: Add this or Me qualification\ndotnet_diagnostic.IDE0009.severity = silent\n\n# IDE0010: Add missing cases\ndotnet_diagnostic.IDE0010.severity = silent\n\n# IDE0011: Add braces\ndotnet_diagnostic.IDE0011.severity = silent\n\n# IDE0016: Use 'throw' expression\ndotnet_diagnostic.IDE0016.severity = silent\n\n# IDE0017: Simplify object initialization\ndotnet_diagnostic.IDE0017.severity = suggestion\n\n# IDE0018: Inline variable declaration\ndotnet_diagnostic.IDE0018.severity = suggestion\n\n# IDE0019: Use pattern matching to avoid as followed by a null check\ndotnet_diagnostic.IDE0019.severity = suggestion\n\n# IDE0020: Use pattern matching to avoid is check followed by a cast (with variable)\ndotnet_diagnostic.IDE0020.severity = warning\n\n# IDE0021: Use expression body for constructors\ndotnet_diagnostic.IDE0021.severity = silent\n\n# IDE0022: Use expression body for methods\ndotnet_diagnostic.IDE0022.severity = silent\n\n# IDE0023: Use expression body for operators\ndotnet_diagnostic.IDE0023.severity = silent\n\n# IDE0024: Use expression body for operators\ndotnet_diagnostic.IDE0024.severity = silent\n\n# IDE0025: Use expression body for properties\ndotnet_diagnostic.IDE0025.severity = silent\n\n# IDE0026: Use expression body for indexers\ndotnet_diagnostic.IDE0026.severity = silent\n\n# IDE0027: Use expression body for accessors\ndotnet_diagnostic.IDE0027.severity = silent\n\n# IDE0028: Simplify collection initialization\ndotnet_diagnostic.IDE0028.severity = suggestion\n\n# IDE0029: Use coalesce expression\ndotnet_diagnostic.IDE0029.severity = warning\n\n# IDE0030: Use coalesce expression\ndotnet_diagnostic.IDE0030.severity = warning\n\n# IDE0031: Use null propagation\ndotnet_diagnostic.IDE0031.severity = warning\n\n# IDE0032: Use auto property\ndotnet_diagnostic.IDE0032.severity = silent\n\n# IDE0033: Use explicitly provided tuple name\ndotnet_diagnostic.IDE0033.severity = suggestion\n\n# IDE0034: Simplify 'default' expression\ndotnet_diagnostic.IDE0034.severity = suggestion\n\n# IDE0035: Remove unreachable code\ndotnet_diagnostic.IDE0035.severity = suggestion\n\n# IDE0036: Order modifiers\ndotnet_diagnostic.IDE0036.severity = warning\n\n# IDE0037: Use inferred member name\ndotnet_diagnostic.IDE0037.severity = silent\n\n# IDE0038: Use pattern matching to avoid is check followed by a cast (without variable)\ndotnet_diagnostic.IDE0038.severity = suggestion\n\n# IDE0039: Use local function\ndotnet_diagnostic.IDE0039.severity = suggestion\n\n# IDE0040: Add accessibility modifiers\ndotnet_diagnostic.IDE0040.severity = suggestion\n\n# IDE0041: Use 'is null' check\ndotnet_diagnostic.IDE0041.severity = warning\n\n# IDE0042: Deconstruct variable declaration\ndotnet_diagnostic.IDE0042.severity = silent\n\n# IDE0043: Invalid format string\ndotnet_diagnostic.IDE0043.severity = warning\n\n# IDE0044: Add readonly modifier\ndotnet_diagnostic.IDE0044.severity = suggestion\n\n# IDE0045: Use conditional expression for assignment\ndotnet_diagnostic.IDE0045.severity = suggestion\n\n# IDE0046: Use conditional expression for return\ndotnet_diagnostic.IDE0046.severity = suggestion\n\n# IDE0047: Remove unnecessary parentheses\ndotnet_diagnostic.IDE0047.severity = silent\n\n# IDE0048: Add parentheses for clarity\ndotnet_diagnostic.IDE0048.severity = silent\n\n# IDE0049: Use language keywords instead of framework type names for type references\ndotnet_diagnostic.IDE0049.severity = warning\n\n# IDE0051: Remove unused private members\ndotnet_diagnostic.IDE0051.severity = suggestion\n\n# IDE0052: Remove unread private members\ndotnet_diagnostic.IDE0052.severity = suggestion\n\n# IDE0053: Use expression body for lambdas\ndotnet_diagnostic.IDE0053.severity = silent\n\n# IDE0054: Use compound assignment\ndotnet_diagnostic.IDE0054.severity = warning\n\n# IDE0055: Fix formatting\ndotnet_diagnostic.IDE0055.severity = suggestion\n\n# IDE0056: Use index operator\ndotnet_diagnostic.IDE0056.severity = suggestion\n\n# IDE0057: Use range operator\ndotnet_diagnostic.IDE0057.severity = suggestion\n\n# IDE0058: Expression value is never used\ndotnet_diagnostic.IDE0058.severity = silent\n\n# IDE0059: Unnecessary assignment of a value\ndotnet_diagnostic.IDE0059.severity = warning\n\n# IDE0060: Remove unused parameter\ndotnet_diagnostic.IDE0060.severity = warning\ndotnet_code_quality_unused_parameters = non_public\n\n# IDE0061: Use expression body for local functions\ndotnet_diagnostic.IDE0061.severity = silent\n\n# IDE0062: Make local function 'static'\ndotnet_diagnostic.IDE0062.severity = warning\n\n# IDE0063: Use simple 'using' statement\ndotnet_diagnostic.IDE0063.severity = silent\n\n# IDE0064: Make readonly fields writable\ndotnet_diagnostic.IDE0064.severity = silent\n\n# IDE0065: Misplaced using directive\ndotnet_diagnostic.IDE0065.severity = warning\n\n# IDE0066: Convert switch statement to expression\ndotnet_diagnostic.IDE0066.severity = suggestion\n\n# IDE0070: Use 'System.HashCode'\ndotnet_diagnostic.IDE0070.severity = suggestion\n\n# IDE0071: Simplify interpolation\ndotnet_diagnostic.IDE0071.severity = warning\n\n# IDE0072: Add missing cases\ndotnet_diagnostic.IDE0072.severity = silent\n\n# IDE0073: The file header is missing or not located at the top of the file\ndotnet_diagnostic.IDE0073.severity = error\n\n# IDE0074: Use compound assignment\ndotnet_diagnostic.IDE0074.severity = warning\n\n# IDE0075: Simplify conditional expression\ndotnet_diagnostic.IDE0075.severity = silent\n\n# IDE0076: Invalid global 'SuppressMessageAttribute'\ndotnet_diagnostic.IDE0076.severity = warning\n\n# IDE0077: Avoid legacy format target in 'SuppressMessageAttribute'\ndotnet_diagnostic.IDE0077.severity = silent\n\n# IDE0078: Use pattern matching\ndotnet_diagnostic.IDE0078.severity = suggestion\n\n# IDE0079: Remove unnecessary suppression\ndotnet_diagnostic.IDE0079.severity = suggestion\n\n# IDE0080: Remove unnecessary suppression operator\ndotnet_diagnostic.IDE0080.severity = warning\n\n# IDE0081: Remove unnecessary suppression operator\ndotnet_diagnostic.IDE0081.severity = none\n\n# IDE0082: 'typeof' can be converted  to 'nameof'\ndotnet_diagnostic.IDE0082.severity = warning\n\n# IDE0083: Use pattern matching\ndotnet_diagnostic.IDE0083.severity = silent\n\n# IDE0084: Use pattern matching (IsNot operator)\ndotnet_diagnostic.IDE0084.severity = none\n\n# IDE0090: Use 'new(...)'\ndotnet_diagnostic.IDE0090.severity = silent\n\n# IDE0100: Remove redundant equality\ndotnet_diagnostic.IDE0100.severity = warning\n\n# IDE0110: Remove unnecessary discard\ndotnet_diagnostic.IDE0110.severity = warning\n\n# IDE0120: Simplify LINQ expression\ndotnet_diagnostic.IDE0120.severity = none\n\n# IDE0130: Namespace does not match folder structure\ndotnet_diagnostic.IDE0130.severity = silent\n\n# IDE0140: Simplify object creation\ndotnet_diagnostic.IDE0140.severity = none\n\n# IDE0150: Prefer 'null' check over type check\ndotnet_diagnostic.IDE0150.severity = silent\n\n# IDE0160: Convert to block scoped namespace\ndotnet_diagnostic.IDE0160.severity = silent\n\n# IDE0161: Convert to file-scoped namespace\ndotnet_diagnostic.IDE0161.severity = silent\n\n# IDE0170: Simplify property pattern\ndotnet_diagnostic.IDE0170.severity = warning\n\n# IDE0180: Use tuple swap\ndotnet_diagnostic.IDE0180.severity = suggestion\n\n# IDE0200: Remove unnecessary lambda expression\ndotnet_diagnostic.IDE0200.severity = warning\n\n# IDE0210: Use top-level statements\ndotnet_diagnostic.IDE0210.severity = none\n\n# IDE0211: Convert to 'Program.Main' style program\ndotnet_diagnostic.IDE0211.severity = none\n\n# IDE0220: foreach cast\ndotnet_diagnostic.IDE0220.severity = silent\n\n# IDE0230: Use UTF8 string literal\ndotnet_diagnostic.IDE0230.severity = suggestion\n\n# IDE0240: Remove redundant nullable directive\ndotnet_diagnostic.IDE0240.severity = suggestion\n\n# IDE0241: Remove unnecessary nullable directive\ndotnet_diagnostic.IDE0241.severity = suggestion\n\n# IDE0250: Make struct readonly\ndotnet_diagnostic.IDE0250.severity = suggestion\n\n# IDE0251: Make member readonly\ndotnet_diagnostic.IDE0251.severity = suggestion\n\n# IDE0260: Use pattern matching\ndotnet_diagnostic.IDE0260.severity = suggestion\n\n# IDE0270: Use coalesce expression\ndotnet_diagnostic.IDE0270.severity = suggestion\n\n# IDE0280: Use 'nameof'\ndotnet_diagnostic.IDE0280.severity = warning\n\n# IDE0290: Use primary constructor\ndotnet_diagnostic.IDE0290.severity = suggestion\n\n# IDE0300: Use collection expression for array\ndotnet_diagnostic.IDE0300.severity = suggestion\n\n# IDE0301: Use collection expression for empty\ndotnet_diagnostic.IDE0301.severity = suggestion\n\n# IDE0302: Use collection expression for stackalloc\ndotnet_diagnostic.IDE0302.severity = suggestion\n\n# IDE0303: Use collection expression for Create()\ndotnet_diagnostic.IDE0303.severity = suggestion\n\n# IDE0304: Use collection expression for builder\ndotnet_diagnostic.IDE0304.severity = suggestion\n\n# IDE0305: Use collection expression for fluent\ndotnet_diagnostic.IDE0305.severity = suggestion\n\n# IDE1005: Delegate invocation can be simplified.\ndotnet_diagnostic.IDE1005.severity = warning\n\n# IDE1006: Naming styles\ndotnet_diagnostic.IDE1006.severity = silent\n\n# IDE2000: Allow multiple blank lines\ndotnet_diagnostic.IDE2000.severity = silent\n\n# IDE2001: Embedded statements must be on their own line\ndotnet_diagnostic.IDE2001.severity = silent\n\n# IDE2002: Consecutive braces must not have blank line between them\ndotnet_diagnostic.IDE2002.severity = silent\n\n# IDE2003: Allow statement immediately after block\ndotnet_diagnostic.IDE2003.severity = silent\n\n# IDE2004: Blank line not allowed after constructor initializer colon\ndotnet_diagnostic.IDE2004.severity = silent\n\n# IDE2005: Blank line not allowed after conditional expression token\ndotnet_diagnostic.IDE2005.severity = silent\n\n# IDE2006: Blank line not allowed after arrow expression clause token\ndotnet_diagnostic.IDE2006.severity = silent\n"
  },
  {
    "path": "eng/CodeAnalysis.test.globalconfig",
    "content": "is_global = true\n\n# AD0001: Analyzer threw an exception\ndotnet_diagnostic.AD0001.severity = none\n\n# BCL0001: Ensure minimum API surface is respected\ndotnet_diagnostic.BCL0001.severity = none\n\n# BCL0010: AppContext default value expected to be true\ndotnet_diagnostic.BCL0010.severity = none\n\n# BCL0011: AppContext default value defined in if statement with incorrect pattern\ndotnet_diagnostic.BCL0011.severity = none\n\n# BCL0012: AppContext default value defined in if statement at root of switch case\ndotnet_diagnostic.BCL0012.severity = none\n\n# BCL0015: Invalid P/Invoke call\ndotnet_diagnostic.BCL0015.severity = none\n\n# BCL0020: Invalid SR.Format call\ndotnet_diagnostic.BCL0020.severity = none\n\n# SYSLIB1045: Convert to 'GeneratedRegexAttribute'.\ndotnet_diagnostic.SYSLIB1045.severity = none\n\n# SYSLIB1054: Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time\ndotnet_diagnostic.SYSLIB1054.severity = none\n\n# SYSLIB1055: Invalid 'CustomMarshallerAttribute' usage\ndotnet_diagnostic.SYSLIB1055.severity = error\n\n# SYSLIB1056:Specified marshaller type is invalid\ndotnet_diagnostic.SYSLIB1056.severity = error\n\n# SYSLIB1057: Marshaller type does not have the required shape\ndotnet_diagnostic.SYSLIB1057.severity = error\n\n# SYSLIB1058: Invalid 'NativeMarshallingAttribute' usage\ndotnet_diagnostic.SYSLIB1058.severity = error\n\n# SYSLIB1060: Specified marshaller type is invalid\ndotnet_diagnostic.SYSLIB1060.severity = error\n\n# SYSLIB1061: Marshaller type has incompatible method signatures\ndotnet_diagnostic.SYSLIB1061.severity = error\n\n# CA1000: Do not declare static members on generic types\ndotnet_diagnostic.CA1000.severity = none\n\n# CA1001: Types that own disposable fields should be disposable\ndotnet_diagnostic.CA1001.severity = none\n\n# CA1002: Do not expose generic lists\ndotnet_diagnostic.CA1002.severity = none\n\n# CA1003: Use generic event handler instances\ndotnet_diagnostic.CA1003.severity = none\n\n# CA1005: Avoid excessive parameters on generic types\ndotnet_diagnostic.CA1005.severity = none\n\n# CA1008: Enums should have zero value\ndotnet_diagnostic.CA1008.severity = none\n\n# CA1010: Generic interface should also be implemented\ndotnet_diagnostic.CA1010.severity = none\n\n# CA1012: Abstract types should not have public constructors\ndotnet_diagnostic.CA1012.severity = none\n\n# CA1014: Mark assemblies with CLSCompliant\ndotnet_diagnostic.CA1014.severity = none\n\n# CA1016: Mark assemblies with assembly version\ndotnet_diagnostic.CA1016.severity = none\n\n# CA1017: Mark assemblies with ComVisible\ndotnet_diagnostic.CA1017.severity = none\n\n# CA1018: Mark attributes with AttributeUsageAttribute\ndotnet_diagnostic.CA1018.severity = none\n\n# CA1019: Define accessors for attribute arguments\ndotnet_diagnostic.CA1019.severity = none\n\n# CA1021: Avoid out parameters\ndotnet_diagnostic.CA1021.severity = none\n\n# CA1024: Use properties where appropriate\ndotnet_diagnostic.CA1024.severity = none\n\n# CA1027: Mark enums with FlagsAttribute\ndotnet_diagnostic.CA1027.severity = none\n\n# CA1028: Enum Storage should be Int32\ndotnet_diagnostic.CA1028.severity = none\n\n# CA1030: Use events where appropriate\ndotnet_diagnostic.CA1030.severity = none\n\n# CA1031: Do not catch general exception types\ndotnet_diagnostic.CA1031.severity = none\n\n# CA1032: Implement standard exception constructors\ndotnet_diagnostic.CA1032.severity = none\n\n# CA1033: Interface methods should be callable by child types\ndotnet_diagnostic.CA1033.severity = none\n\n# CA1034: Nested types should not be visible\ndotnet_diagnostic.CA1034.severity = none\n\n# CA1036: Override methods on comparable types\ndotnet_diagnostic.CA1036.severity = none\n\n# CA1040: Avoid empty interfaces\ndotnet_diagnostic.CA1040.severity = none\n\n# CA1041: Provide ObsoleteAttribute message\ndotnet_diagnostic.CA1041.severity = none\n\n# CA1043: Use Integral Or String Argument For Indexers\ndotnet_diagnostic.CA1043.severity = none\n\n# CA1044: Properties should not be write only\ndotnet_diagnostic.CA1044.severity = none\n\n# CA1045: Do not pass types by reference\ndotnet_diagnostic.CA1045.severity = none\n\n# CA1046: Do not overload equality operator on reference types\ndotnet_diagnostic.CA1046.severity = none\n\n# CA1047: Do not declare protected member in sealed type\ndotnet_diagnostic.CA1047.severity = none\n\n# CA1050: Declare types in namespaces\ndotnet_diagnostic.CA1050.severity = none\n\n# CA1051: Do not declare visible instance fields\ndotnet_diagnostic.CA1051.severity = none\n\n# CA1052: Static holder types should be Static or NotInheritable\ndotnet_diagnostic.CA1052.severity = none\n\n# CA1054: URI-like parameters should not be strings\ndotnet_diagnostic.CA1054.severity = none\n\n# CA1055: URI-like return values should not be strings\ndotnet_diagnostic.CA1055.severity = none\n\n# CA1056: URI-like properties should not be strings\ndotnet_diagnostic.CA1056.severity = none\n\n# CA1058: Types should not extend certain base types\ndotnet_diagnostic.CA1058.severity = none\n\n# CA1060: Move pinvokes to native methods class\ndotnet_diagnostic.CA1060.severity = none\n\n# CA1061: Do not hide base class methods\ndotnet_diagnostic.CA1061.severity = none\n\n# CA1062: Validate arguments of public methods\ndotnet_diagnostic.CA1062.severity = none\n\n# CA1063: Implement IDisposable Correctly\ndotnet_diagnostic.CA1063.severity = none\n\n# CA1064: Exceptions should be public\ndotnet_diagnostic.CA1064.severity = none\n\n# CA1065: Do not raise exceptions in unexpected locations\ndotnet_diagnostic.CA1065.severity = none\n\n# CA1066: Implement IEquatable when overriding Object.Equals\ndotnet_diagnostic.CA1066.severity = none\n\n# CA1067: Override Object.Equals(object) when implementing IEquatable<T>\ndotnet_diagnostic.CA1067.severity = none\n\n# CA1068: CancellationToken parameters must come last\ndotnet_diagnostic.CA1068.severity = none\n\n# CA1069: Enums values should not be duplicated\ndotnet_diagnostic.CA1069.severity = none\n\n# CA1070: Do not declare event fields as virtual\ndotnet_diagnostic.CA1070.severity = none\n\n# CA1200: Avoid using cref tags with a prefix\ndotnet_diagnostic.CA1200.severity = none\n\n# CA1303: Do not pass literals as localized parameters\ndotnet_diagnostic.CA1303.severity = none\n\n# CA1304: Specify CultureInfo\ndotnet_diagnostic.CA1304.severity = none\n\n# CA1305: Specify IFormatProvider\ndotnet_diagnostic.CA1305.severity = none\n\n# CA1307: Specify StringComparison for clarity\ndotnet_diagnostic.CA1307.severity = none\n\n# CA1308: Normalize strings to uppercase\ndotnet_diagnostic.CA1308.severity = none\n\n# CA1309: Use ordinal string comparison\ndotnet_diagnostic.CA1309.severity = none\n\n# CA1310: Specify StringComparison for correctness\ndotnet_diagnostic.CA1310.severity = none\n\n# CA1311: Specify a culture or use an invariant version\ndotnet_diagnostic.CA1311.severity = none\n\n# CA1401: P/Invokes should not be visible\ndotnet_diagnostic.CA1401.severity = none\n\n# CA1416: Validate platform compatibility\ndotnet_diagnostic.CA1416.severity = none\n\n# CA1417: Do not use 'OutAttribute' on string parameters for P/Invokes\ndotnet_diagnostic.CA1417.severity = none\n\n# CA1418: Use valid platform string\ndotnet_diagnostic.CA1418.severity = none\n\n# CA1419: Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle'\ndotnet_diagnostic.CA1419.severity = none\n\n# CA1420: Property, type, or attribute requires runtime marshalling\ndotnet_diagnostic.CA1420.severity = none\n\n# CA1421: This method uses runtime marshalling even when the 'DisableRuntimeMarshallingAttribute' is applied\ndotnet_diagnostic.CA1421.severity = none\n\n# CA1422: Validate platform compatibility\ndotnet_diagnostic.CA1422.severity = none\n\n# CA1501: Avoid excessive inheritance\ndotnet_diagnostic.CA1501.severity = none\n\n# CA1502: Avoid excessive complexity\ndotnet_diagnostic.CA1502.severity = none\n\n# CA1505: Avoid unmaintainable code\ndotnet_diagnostic.CA1505.severity = none\n\n# CA1506: Avoid excessive class coupling\ndotnet_diagnostic.CA1506.severity = none\n\n# CA1507: Use nameof to express symbol names\ndotnet_diagnostic.CA1507.severity = none\n\n# CA1508: Avoid dead conditional code\ndotnet_diagnostic.CA1508.severity = none\n\n# CA1509: Invalid entry in code metrics rule specification file\ndotnet_diagnostic.CA1509.severity = none\n\n# CA1510: Use ArgumentNullException throw helper\ndotnet_diagnostic.CA1510.severity = none\n\n# CA1511: Use ArgumentException throw helper\ndotnet_diagnostic.CA1511.severity = none\n\n# CA1512: Use ArgumentOutOfRangeException throw helper\ndotnet_diagnostic.CA1512.severity = none\n\n# CA1513: Use ObjectDisposedException throw helper\ndotnet_diagnostic.CA1513.severity = none\n\n# CA1514: Avoid redundant length argument\ndotnet_diagnostic.CA1514.severity = none\n\n# CA1515: Consider making public types internal\ndotnet_diagnostic.CA1515.severity = none\n\n# CA1700: Do not name enum values 'Reserved'\ndotnet_diagnostic.CA1700.severity = none\n\n# CA1707: Identifiers should not contain underscores\ndotnet_diagnostic.CA1707.severity = none\n\n# CA1708: Identifiers should differ by more than case\ndotnet_diagnostic.CA1708.severity = none\n\n# CA1710: Identifiers should have correct suffix\ndotnet_diagnostic.CA1710.severity = none\n\n# CA1711: Identifiers should not have incorrect suffix\ndotnet_diagnostic.CA1711.severity = none\n\n# CA1712: Do not prefix enum values with type name\ndotnet_diagnostic.CA1712.severity = none\n\n# CA1713: Events should not have 'Before' or 'After' prefix\ndotnet_diagnostic.CA1713.severity = none\n\n# CA1715: Identifiers should have correct prefix\ndotnet_diagnostic.CA1715.severity = none\n\n# CA1716: Identifiers should not match keywords\ndotnet_diagnostic.CA1716.severity = none\n\n# CA1720: Identifier contains type name\ndotnet_diagnostic.CA1720.severity = none\n\n# CA1721: Property names should not match get methods\ndotnet_diagnostic.CA1721.severity = none\n\n# CA1724: Type names should not match namespaces\ndotnet_diagnostic.CA1724.severity = none\n\n# CA1725: Parameter names should match base declaration\ndotnet_diagnostic.CA1725.severity = none\n\n# CA1727: Use PascalCase for named placeholders\ndotnet_diagnostic.CA1727.severity = none\n\n# CA1802: Use literals where appropriate\ndotnet_diagnostic.CA1802.severity = none\n\n# CA1805: Do not initialize unnecessarily\ndotnet_diagnostic.CA1805.severity = none\n\n# CA1806: Do not ignore method results\ndotnet_diagnostic.CA1806.severity = none\n\n# CA1810: Initialize reference type static fields inline\ndotnet_diagnostic.CA1810.severity = none\n\n# CA1812: Avoid uninstantiated internal classes\ndotnet_diagnostic.CA1812.severity = none\n\n# CA1813: Avoid unsealed attributes\ndotnet_diagnostic.CA1813.severity = none\n\n# CA1814: Prefer jagged arrays over multidimensional\ndotnet_diagnostic.CA1814.severity = none\n\n# CA1815: Override equals and operator equals on value types\ndotnet_diagnostic.CA1815.severity = none\n\n# CA1816: Dispose methods should call SuppressFinalize\ndotnet_diagnostic.CA1816.severity = none\n\n# CA1819: Properties should not return arrays\ndotnet_diagnostic.CA1819.severity = none\n\n# CA1820: Test for empty strings using string length\ndotnet_diagnostic.CA1820.severity = none\n\n# CA1821: Remove empty Finalizers\ndotnet_diagnostic.CA1821.severity = none\n\n# CA1822: Mark members as static\ndotnet_diagnostic.CA1822.severity = none\n\n# CA1823: Avoid unused private fields\ndotnet_diagnostic.CA1823.severity = none\n\n# CA1824: Mark assemblies with NeutralResourcesLanguageAttribute\ndotnet_diagnostic.CA1824.severity = none\n\n# CA1825: Avoid zero-length array allocations.\ndotnet_diagnostic.CA1825.severity = none\n\n# CA1826: Do not use Enumerable methods on indexable collections\ndotnet_diagnostic.CA1826.severity = none\n\n# CA1827: Do not use Count() or LongCount() when Any() can be used\ndotnet_diagnostic.CA1827.severity = none\n\n# CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used\ndotnet_diagnostic.CA1828.severity = none\n\n# CA1829: Use Length/Count property instead of Count() when available\ndotnet_diagnostic.CA1829.severity = none\n\n# CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder\ndotnet_diagnostic.CA1830.severity = none\n\n# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate\ndotnet_diagnostic.CA1831.severity = none\n\n# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate\ndotnet_diagnostic.CA1832.severity = none\n\n# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate\ndotnet_diagnostic.CA1833.severity = none\n\n# CA1834: Consider using 'StringBuilder.Append(char)' when applicable\ndotnet_diagnostic.CA1834.severity = none\n\n# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'\ndotnet_diagnostic.CA1835.severity = none\n\n# CA1836: Prefer IsEmpty over Count\ndotnet_diagnostic.CA1836.severity = none\n\n# CA1837: Use 'Environment.ProcessId'\ndotnet_diagnostic.CA1837.severity = none\n\n# CA1838: Avoid 'StringBuilder' parameters for P/Invokes\ndotnet_diagnostic.CA1838.severity = none\n\n# CA1839: Use 'Environment.ProcessPath'\ndotnet_diagnostic.CA1839.severity = none\n\n# CA1840: Use 'Environment.CurrentManagedThreadId'\ndotnet_diagnostic.CA1840.severity = none\n\n# CA1841: Prefer Dictionary.Contains methods\ndotnet_diagnostic.CA1841.severity = none\n\n# CA1842: Do not use 'WhenAll' with a single task\ndotnet_diagnostic.CA1842.severity = none\n\n# CA1843: Do not use 'WaitAll' with a single task\ndotnet_diagnostic.CA1843.severity = none\n\n# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream'\ndotnet_diagnostic.CA1844.severity = none\n\n# CA1845: Use span-based 'string.Concat'\ndotnet_diagnostic.CA1845.severity = none\n\n# CA1846: Prefer 'AsSpan' over 'Substring'\ndotnet_diagnostic.CA1846.severity = none\n\n# CA1847: Use char literal for a single character lookup\ndotnet_diagnostic.CA1847.severity = none\n\n# CA1848: Use the LoggerMessage delegates\ndotnet_diagnostic.CA1848.severity = none\n\n# CA1849: Call async methods when in an async method\ndotnet_diagnostic.CA1849.severity = none\n\n# CA1850: Prefer static 'HashData' method over 'ComputeHash'\ndotnet_diagnostic.CA1850.severity = none\n\n# CA1851: Possible multiple enumerations of 'IEnumerable' collection\ndotnet_diagnostic.CA1851.severity = none\n\n# CA1852: Seal internal types\ndotnet_diagnostic.CA1852.severity = none\n\n# CA1853: Unnecessary call to 'Dictionary.ContainsKey(key)'\ndotnet_diagnostic.CA1853.severity = none\n\n# CA1854: Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method\ndotnet_diagnostic.CA1854.severity = none\n\n# CA1855: Prefer 'Clear' over 'Fill'\ndotnet_diagnostic.CA1855.severity = none\n\n# CA1856: Incorrect usage of ConstantExpected attribute\ndotnet_diagnostic.CA1856.severity = none\n\n# CA1857: A constant is expected for the parameter\ndotnet_diagnostic.CA1857.severity = none\n\n# CA1858: Use 'StartsWith' instead of 'IndexOf'\ndotnet_diagnostic.CA1858.severity = none\n\n# CA1859: Use concrete types when possible for improved performance\ndotnet_diagnostic.CA1859.severity = none\n\n# CA1860: Avoid using 'Enumerable.Any()' extension method\ndotnet_diagnostic.CA1860.severity = none\n\n# CA1861: Avoid constant arrays as arguments\ndotnet_diagnostic.CA1861.severity = none\n\n# CA1862: Prefer using 'StringComparer'/'StringComparison' to perform case-insensitive string comparisons\ndotnet_diagnostic.CA1862.severity = none\n\n# CA1863: Use 'CompositeFormat'\ndotnet_diagnostic.CA1863.severity = none\n\n# CA1864: Prefer the 'IDictionary.TryAdd(TKey, TValue)' method\ndotnet_diagnostic.CA1864.severity = none\n\n# CA1865: Use char overload\ndotnet_diagnostic.CA1865.severity = none\n\n# CA1866: Use char overload\ndotnet_diagnostic.CA1866.severity = none\n\n# CA1867: Use char overload\ndotnet_diagnostic.CA1867.severity = none\n\n# CA1868: Unnecessary call to 'Contains' for sets\ndotnet_diagnostic.CA1868.severity = none\n\n# CA1869: Cache and reuse 'JsonSerializerOptions' instances\ndotnet_diagnostic.CA1869.severity = none\n\n# CA1870: Use a cached 'SearchValues' instance\ndotnet_diagnostic.CA1870.severity = none\n\n# CA1871: Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull'\ndotnet_diagnostic.CA1871.severity = none\n\n# CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'\ndotnet_diagnostic.CA1872.severity = none\n\n# CA2000: Dispose objects before losing scope\ndotnet_diagnostic.CA2000.severity = none\n\n# CA2002: Do not lock on objects with weak identity\ndotnet_diagnostic.CA2002.severity = none\n\n# CA2007: Consider calling ConfigureAwait on the awaited task\ndotnet_diagnostic.CA2007.severity = none\n\n# CA2008: Do not create tasks without passing a TaskScheduler\ndotnet_diagnostic.CA2008.severity = none\n\n# CA2009: Do not call ToImmutableCollection on an ImmutableCollection value\ndotnet_diagnostic.CA2009.severity = none\n\n# CA2011: Avoid infinite recursion\ndotnet_diagnostic.CA2011.severity = none\n\n# CA2012: Use ValueTasks correctly\ndotnet_diagnostic.CA2012.severity = none\n\n# CA2013: Do not use ReferenceEquals with value types\ndotnet_diagnostic.CA2013.severity = none\n\n# CA2014: Do not use stackalloc in loops.\ndotnet_diagnostic.CA2014.severity = none\n\n# CA2015: Do not define finalizers for types derived from MemoryManager<T>\ndotnet_diagnostic.CA2015.severity = none\n\n# CA2016: Forward the 'CancellationToken' parameter to methods\ndotnet_diagnostic.CA2016.severity = none\n\n# CA2017: Parameter count mismatch\ndotnet_diagnostic.CA2017.severity = none\n\n# CA2018: 'Buffer.BlockCopy' expects the number of bytes to be copied for the 'count' argument\ndotnet_diagnostic.CA2018.severity = none\n\n# CA2019: Improper 'ThreadStatic' field initialization\ndotnet_diagnostic.CA2019.severity = none\n\n# CA2020: Prevent behavioral change\ndotnet_diagnostic.CA2020.severity = none\n\n# CA2021: Do not call Enumerable.Cast<T> or Enumerable.OfType<T> with incompatible types\ndotnet_diagnostic.CA2021.severity = none\n\n# CA2022: Avoid inexact read with 'Stream.Read'\ndotnet_diagnostic.CA2022.severity = none\n\n# CA2100: Review SQL queries for security vulnerabilities\ndotnet_diagnostic.CA2100.severity = none\n\n# CA2101: Specify marshaling for P/Invoke string arguments\ndotnet_diagnostic.CA2101.severity = none\n\n# CA2119: Seal methods that satisfy private interfaces\ndotnet_diagnostic.CA2119.severity = none\n\n# CA2153: Do Not Catch Corrupted State Exceptions\ndotnet_diagnostic.CA2153.severity = none\n\n# CA2200: Rethrow to preserve stack details\ndotnet_diagnostic.CA2200.severity = none\n\n# CA2201: Do not raise reserved exception types\ndotnet_diagnostic.CA2201.severity = none\n\n# CA2207: Initialize value type static fields inline\ndotnet_diagnostic.CA2207.severity = none\n\n# CA2208: Instantiate argument exceptions correctly\ndotnet_diagnostic.CA2208.severity = none\n\n# CA2211: Non-constant fields should not be visible\ndotnet_diagnostic.CA2211.severity = none\n\n# CA2213: Disposable fields should be disposed\ndotnet_diagnostic.CA2213.severity = none\n\n# CA2214: Do not call overridable methods in constructors\ndotnet_diagnostic.CA2214.severity = none\n\n# CA2215: Dispose methods should call base class dispose\ndotnet_diagnostic.CA2215.severity = none\n\n# CA2216: Disposable types should declare finalizer\ndotnet_diagnostic.CA2216.severity = none\n\n# CA2217: Do not mark enums with FlagsAttribute\ndotnet_diagnostic.CA2217.severity = none\n\n# CA2218: Override GetHashCode on overriding Equals\ndotnet_diagnostic.CA2218.severity = none\n\n# CA2219: Do not raise exceptions in finally clauses\ndotnet_diagnostic.CA2219.severity = none\n\n# CA2224: Override Equals on overloading operator equals\ndotnet_diagnostic.CA2224.severity = none\n\n# CA2225: Operator overloads have named alternates\ndotnet_diagnostic.CA2225.severity = none\n\n# CA2226: Operators should have symmetrical overloads\ndotnet_diagnostic.CA2226.severity = none\n\n# CA2227: Collection properties should be read only\ndotnet_diagnostic.CA2227.severity = none\n\n# CA2229: Implement serialization constructors\ndotnet_diagnostic.CA2229.severity = none\n\n# CA2231: Overload operator equals on overriding value type Equals\ndotnet_diagnostic.CA2231.severity = none\n\n# CA2234: Pass system uri objects instead of strings\ndotnet_diagnostic.CA2234.severity = none\n\n# CA2235: Mark all non-serializable fields\ndotnet_diagnostic.CA2235.severity = none\n\n# CA2237: Mark ISerializable types with serializable\ndotnet_diagnostic.CA2237.severity = none\n\n# CA2241: Provide correct arguments to formatting methods\ndotnet_diagnostic.CA2241.severity = none\n\n# CA2242: Test for NaN correctly\ndotnet_diagnostic.CA2242.severity = none\n\n# CA2243: Attribute string literals should parse correctly\ndotnet_diagnostic.CA2243.severity = none\n\n# CA2244: Do not duplicate indexed element initializations\ndotnet_diagnostic.CA2244.severity = none\n\n# CA2245: Do not assign a property to itself\ndotnet_diagnostic.CA2245.severity = none\n\n# CA2246: Assigning symbol and its member in the same statement\ndotnet_diagnostic.CA2246.severity = none\n\n# CA2247: Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum\ndotnet_diagnostic.CA2247.severity = none\n\n# CA2248: Provide correct 'enum' argument to 'Enum.HasFlag'\ndotnet_diagnostic.CA2248.severity = none\n\n# CA2249: Consider using 'string.Contains' instead of 'string.IndexOf'\ndotnet_diagnostic.CA2249.severity = none\n\n# CA2250: Use 'ThrowIfCancellationRequested'\ndotnet_diagnostic.CA2250.severity = none\n\n# CA2251: Use 'string.Equals'\ndotnet_diagnostic.CA2251.severity = none\n\n# CA2252: This API requires opting into preview features\ndotnet_diagnostic.CA2252.severity = error\n\n# CA2253: Named placeholders should not be numeric values\ndotnet_diagnostic.CA2253.severity = none\n\n# CA2254: Template should be a static expression\ndotnet_diagnostic.CA2254.severity = none\n\n# CA2255: The 'ModuleInitializer' attribute should not be used in libraries\ndotnet_diagnostic.CA2255.severity = none\n\n# CA2256: All members declared in parent interfaces must have an implementation in a DynamicInterfaceCastableImplementation-attributed interface\ndotnet_diagnostic.CA2256.severity = none\n\n# CA2257: Members defined on an interface with the 'DynamicInterfaceCastableImplementationAttribute' should be 'static'\ndotnet_diagnostic.CA2257.severity = none\n\n# CA2258: Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported\ndotnet_diagnostic.CA2258.severity = none\n\n# CA2259: 'ThreadStatic' only affects static fields\ndotnet_diagnostic.CA2259.severity = none\n\n# CA2260: Use correct type parameter\ndotnet_diagnostic.CA2260.severity = none\n\n# CA2261: Do not use ConfigureAwaitOptions.SuppressThrowing with Task<TResult>\ndotnet_diagnostic.CA2261.severity = none\n\n# CA2262: Set 'MaxResponseHeadersLength' properly\ndotnet_diagnostic.CA2262.severity = none\n\n# CA2263: Prefer generic overload when type is known\ndotnet_diagnostic.CA2263.severity = none\n\n# CA2264: Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull'\ndotnet_diagnostic.CA2264.severity = none\n\n# CA2265: Do not compare Span<T> to 'null' or 'default'\ndotnet_diagnostic.CA2265.severity = none\n\n# CA2300: Do not use insecure deserializer BinaryFormatter\ndotnet_diagnostic.CA2300.severity = none\n\n# CA2301: Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder\ndotnet_diagnostic.CA2301.severity = none\n\n# CA2302: Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize\ndotnet_diagnostic.CA2302.severity = none\n\n# CA2305: Do not use insecure deserializer LosFormatter\ndotnet_diagnostic.CA2305.severity = none\n\n# CA2310: Do not use insecure deserializer NetDataContractSerializer\ndotnet_diagnostic.CA2310.severity = none\n\n# CA2311: Do not deserialize without first setting NetDataContractSerializer.Binder\ndotnet_diagnostic.CA2311.severity = none\n\n# CA2312: Ensure NetDataContractSerializer.Binder is set before deserializing\ndotnet_diagnostic.CA2312.severity = none\n\n# CA2315: Do not use insecure deserializer ObjectStateFormatter\ndotnet_diagnostic.CA2315.severity = none\n\n# CA2321: Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver\ndotnet_diagnostic.CA2321.severity = none\n\n# CA2322: Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing\ndotnet_diagnostic.CA2322.severity = none\n\n# CA2326: Do not use TypeNameHandling values other than None\ndotnet_diagnostic.CA2326.severity = none\n\n# CA2327: Do not use insecure JsonSerializerSettings\ndotnet_diagnostic.CA2327.severity = none\n\n# CA2328: Ensure that JsonSerializerSettings are secure\ndotnet_diagnostic.CA2328.severity = none\n\n# CA2329: Do not deserialize with JsonSerializer using an insecure configuration\ndotnet_diagnostic.CA2329.severity = none\n\n# CA2330: Ensure that JsonSerializer has a secure configuration when deserializing\ndotnet_diagnostic.CA2330.severity = none\n\n# CA2350: Do not use DataTable.ReadXml() with untrusted data\ndotnet_diagnostic.CA2350.severity = none\n\n# CA2351: Do not use DataSet.ReadXml() with untrusted data\ndotnet_diagnostic.CA2351.severity = none\n\n# CA2352: Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks\ndotnet_diagnostic.CA2352.severity = none\n\n# CA2353: Unsafe DataSet or DataTable in serializable type\ndotnet_diagnostic.CA2353.severity = none\n\n# CA2354: Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks\ndotnet_diagnostic.CA2354.severity = none\n\n# CA2355: Unsafe DataSet or DataTable type found in deserializable object graph\ndotnet_diagnostic.CA2355.severity = none\n\n# CA2356: Unsafe DataSet or DataTable type in web deserializable object graph\ndotnet_diagnostic.CA2356.severity = none\n\n# CA2361: Ensure auto-generated class containing DataSet.ReadXml() is not used with untrusted data\ndotnet_diagnostic.CA2361.severity = none\n\n# CA2362: Unsafe DataSet or DataTable in auto-generated serializable type can be vulnerable to remote code execution attacks\ndotnet_diagnostic.CA2362.severity = none\n\n# CA3001: Review code for SQL injection vulnerabilities\ndotnet_diagnostic.CA3001.severity = none\n\n# CA3002: Review code for XSS vulnerabilities\ndotnet_diagnostic.CA3002.severity = none\n\n# CA3003: Review code for file path injection vulnerabilities\ndotnet_diagnostic.CA3003.severity = none\n\n# CA3004: Review code for information disclosure vulnerabilities\ndotnet_diagnostic.CA3004.severity = none\n\n# CA3005: Review code for LDAP injection vulnerabilities\ndotnet_diagnostic.CA3005.severity = none\n\n# CA3006: Review code for process command injection vulnerabilities\ndotnet_diagnostic.CA3006.severity = none\n\n# CA3007: Review code for open redirect vulnerabilities\ndotnet_diagnostic.CA3007.severity = none\n\n# CA3008: Review code for XPath injection vulnerabilities\ndotnet_diagnostic.CA3008.severity = none\n\n# CA3009: Review code for XML injection vulnerabilities\ndotnet_diagnostic.CA3009.severity = none\n\n# CA3010: Review code for XAML injection vulnerabilities\ndotnet_diagnostic.CA3010.severity = none\n\n# CA3011: Review code for DLL injection vulnerabilities\ndotnet_diagnostic.CA3011.severity = none\n\n# CA3012: Review code for regex injection vulnerabilities\ndotnet_diagnostic.CA3012.severity = none\n\n# CA3061: Do Not Add Schema By URL\ndotnet_diagnostic.CA3061.severity = none\n\n# CA3075: Insecure DTD processing in XML\ndotnet_diagnostic.CA3075.severity = none\n\n# CA3076: Insecure XSLT script processing\ndotnet_diagnostic.CA3076.severity = none\n\n# CA3077: Insecure Processing in API Design, XmlDocument and XmlTextReader\ndotnet_diagnostic.CA3077.severity = none\n\n# CA3147: Mark Verb Handlers With Validate Antiforgery Token\ndotnet_diagnostic.CA3147.severity = none\n\n# CA5350: Do Not Use Weak Cryptographic Algorithms\ndotnet_diagnostic.CA5350.severity = none\n\n# CA5351: Do Not Use Broken Cryptographic Algorithms\ndotnet_diagnostic.CA5351.severity = none\n\n# CA5358: Review cipher mode usage with cryptography experts\ndotnet_diagnostic.CA5358.severity = none\n\n# CA5359: Do Not Disable Certificate Validation\ndotnet_diagnostic.CA5359.severity = none\n\n# CA5360: Do Not Call Dangerous Methods In Deserialization\ndotnet_diagnostic.CA5360.severity = none\n\n# CA5361: Do Not Disable SChannel Use of Strong Crypto\ndotnet_diagnostic.CA5361.severity = none\n\n# CA5362: Potential reference cycle in deserialized object graph\ndotnet_diagnostic.CA5362.severity = none\n\n# CA5363: Do Not Disable Request Validation\ndotnet_diagnostic.CA5363.severity = none\n\n# CA5364: Do Not Use Deprecated Security Protocols\ndotnet_diagnostic.CA5364.severity = none\n\n# CA5365: Do Not Disable HTTP Header Checking\ndotnet_diagnostic.CA5365.severity = none\n\n# CA5366: Use XmlReader for 'DataSet.ReadXml()'\ndotnet_diagnostic.CA5366.severity = none\n\n# CA5367: Do Not Serialize Types With Pointer Fields\ndotnet_diagnostic.CA5367.severity = none\n\n# CA5368: Set ViewStateUserKey For Classes Derived From Page\ndotnet_diagnostic.CA5368.severity = none\n\n# CA5369: Use XmlReader for 'XmlSerializer.Deserialize()'\ndotnet_diagnostic.CA5369.severity = none\n\n# CA5370: Use XmlReader for XmlValidatingReader constructor\ndotnet_diagnostic.CA5370.severity = none\n\n# CA5371: Use XmlReader for 'XmlSchema.Read()'\ndotnet_diagnostic.CA5371.severity = none\n\n# CA5372: Use XmlReader for XPathDocument constructor\ndotnet_diagnostic.CA5372.severity = none\n\n# CA5373: Do not use obsolete key derivation function\ndotnet_diagnostic.CA5373.severity = none\n\n# CA5374: Do Not Use XslTransform\ndotnet_diagnostic.CA5374.severity = none\n\n# CA5375: Do Not Use Account Shared Access Signature\ndotnet_diagnostic.CA5375.severity = none\n\n# CA5376: Use SharedAccessProtocol HttpsOnly\ndotnet_diagnostic.CA5376.severity = none\n\n# CA5377: Use Container Level Access Policy\ndotnet_diagnostic.CA5377.severity = none\n\n# CA5378: Do not disable ServicePointManagerSecurityProtocols\ndotnet_diagnostic.CA5378.severity = none\n\n# CA5379: Ensure Key Derivation Function algorithm is sufficiently strong\ndotnet_diagnostic.CA5379.severity = none\n\n# CA5380: Do Not Add Certificates To Root Store\ndotnet_diagnostic.CA5380.severity = none\n\n# CA5381: Ensure Certificates Are Not Added To Root Store\ndotnet_diagnostic.CA5381.severity = none\n\n# CA5382: Use Secure Cookies In ASP.Net Core\ndotnet_diagnostic.CA5382.severity = none\n\n# CA5383: Ensure Use Secure Cookies In ASP.NET Core\ndotnet_diagnostic.CA5383.severity = none\n\n# CA5384: Do Not Use Digital Signature Algorithm (DSA)\ndotnet_diagnostic.CA5384.severity = none\n\n# CA5385: Use Rivest-Shamir-Adleman (RSA) Algorithm With Sufficient Key Size\ndotnet_diagnostic.CA5385.severity = none\n\n# CA5386: Avoid hardcoding SecurityProtocolType value\ndotnet_diagnostic.CA5386.severity = none\n\n# CA5387: Do Not Use Weak Key Derivation Function With Insufficient Iteration Count\ndotnet_diagnostic.CA5387.severity = none\n\n# CA5388: Ensure Sufficient Iteration Count When Using Weak Key Derivation Function\ndotnet_diagnostic.CA5388.severity = none\n\n# CA5389: Do Not Add Archive Item's Path To The Target File System Path\ndotnet_diagnostic.CA5389.severity = none\n\n# CA5390: Do not hard-code encryption key\ndotnet_diagnostic.CA5390.severity = none\n\n# CA5391: Use antiforgery tokens in ASP.NET Core MVC controllers\ndotnet_diagnostic.CA5391.severity = none\n\n# CA5392: Use DefaultDllImportSearchPaths attribute for P/Invokes\ndotnet_diagnostic.CA5392.severity = none\n\n# CA5393: Do not use unsafe DllImportSearchPath value\ndotnet_diagnostic.CA5393.severity = none\n\n# CA5394: Do not use insecure randomness\ndotnet_diagnostic.CA5394.severity = none\n\n# CA5395: Miss HttpVerb attribute for action methods\ndotnet_diagnostic.CA5395.severity = none\n\n# CA5396: Set HttpOnly to true for HttpCookie\ndotnet_diagnostic.CA5396.severity = none\n\n# CA5397: Do not use deprecated SslProtocols values\ndotnet_diagnostic.CA5397.severity = none\n\n# CA5398: Avoid hardcoded SslProtocols values\ndotnet_diagnostic.CA5398.severity = none\n\n# CA5399: HttpClients should enable certificate revocation list checks\ndotnet_diagnostic.CA5399.severity = none\n\n# CA5400: Ensure HttpClient certificate revocation list check is not disabled\ndotnet_diagnostic.CA5400.severity = none\n\n# CA5401: Do not use CreateEncryptor with non-default IV\ndotnet_diagnostic.CA5401.severity = none\n\n# CA5402: Use CreateEncryptor with the default IV\ndotnet_diagnostic.CA5402.severity = none\n\n# CA5403: Do not hard-code certificate\ndotnet_diagnostic.CA5403.severity = none\n\n# CA5404: Do not disable token validation checks\ndotnet_diagnostic.CA5404.severity = none\n\n# CA5405: Do not always skip token validation in delegates\ndotnet_diagnostic.CA5405.severity = none\n\n# IL3000: Avoid using accessing Assembly file path when publishing as a single-file\ndotnet_diagnostic.IL3000.severity = none\n\n# IL3001: Avoid using accessing Assembly file path when publishing as a single-file\ndotnet_diagnostic.IL3001.severity = none\n\n# IL3002: Using member with RequiresAssemblyFilesAttribute can break functionality when embedded in a single-file app\ndotnet_diagnostic.IL3002.severity = none\n\n# RS1001: Missing diagnostic analyzer attribute\ndotnet_diagnostic.RS1001.severity = none\n\n# RS1002: Missing kind argument when registering an analyzer action\ndotnet_diagnostic.RS1002.severity = none\n\n# RS1003: Unsupported SymbolKind argument when registering a symbol analyzer action\ndotnet_diagnostic.RS1003.severity = none\n\n# RS1004: Recommend adding language support to diagnostic analyzer\ndotnet_diagnostic.RS1004.severity = none\n\n# RS1005: ReportDiagnostic invoked with an unsupported DiagnosticDescriptor\ndotnet_diagnostic.RS1005.severity = none\n\n# RS1006: Invalid type argument for DiagnosticAnalyzer's Register method\ndotnet_diagnostic.RS1006.severity = none\n\n# RS1007: Provide localizable arguments to diagnostic descriptor constructor\ndotnet_diagnostic.RS1007.severity = none\n\n# RS1008: Avoid storing per-compilation data into the fields of a diagnostic analyzer\ndotnet_diagnostic.RS1008.severity = none\n\n# RS1009: Only internal implementations of this interface are allowed\ndotnet_diagnostic.RS1009.severity = none\n\n# RS1010: Create code actions should have a unique EquivalenceKey for FixAll occurrences support\ndotnet_diagnostic.RS1010.severity = none\n\n# RS1011: Use code actions that have a unique EquivalenceKey for FixAll occurrences support\ndotnet_diagnostic.RS1011.severity = none\n\n# RS1012: Start action has no registered actions\ndotnet_diagnostic.RS1012.severity = none\n\n# RS1013: Start action has no registered non-end actions\ndotnet_diagnostic.RS1013.severity = none\n\n# RS1014: Do not ignore values returned by methods on immutable objects\ndotnet_diagnostic.RS1014.severity = none\n\n# RS1015: Provide non-null 'helpLinkUri' value to diagnostic descriptor constructor\ndotnet_diagnostic.RS1015.severity = none\n\n# RS1016: Code fix providers should provide FixAll support\ndotnet_diagnostic.RS1016.severity = suggestion\n\n# RS1017: DiagnosticId for analyzers must be a non-null constant\ndotnet_diagnostic.RS1017.severity = none\n\n# RS1018: DiagnosticId for analyzers must be in specified format\ndotnet_diagnostic.RS1018.severity = none\n\n# RS1019: DiagnosticId must be unique across analyzers\ndotnet_diagnostic.RS1019.severity = none\n\n# RS1020: Category for analyzers must be from the specified values\ndotnet_diagnostic.RS1020.severity = none\n\n# RS1021: Invalid entry in analyzer category and diagnostic ID range specification file\ndotnet_diagnostic.RS1021.severity = none\n\n# RS1022: Do not use types from Workspaces assembly in an analyzer\ndotnet_diagnostic.RS1022.severity = none\n\n# RS1023: Upgrade MSBuildWorkspace\ndotnet_diagnostic.RS1023.severity = none\n\n# RS1024: Symbols should be compared for equality\ndotnet_diagnostic.RS1024.severity = none\n\n# RS1025: Configure generated code analysis\ndotnet_diagnostic.RS1025.severity = none\n\n# RS1026: Enable concurrent execution\ndotnet_diagnostic.RS1026.severity = none\n\n# RS1027: Types marked with DiagnosticAnalyzerAttribute(s) should inherit from DiagnosticAnalyzer\ndotnet_diagnostic.RS1027.severity = none\n\n# RS1028: Provide non-null 'customTags' value to diagnostic descriptor constructor\ndotnet_diagnostic.RS1028.severity = none\n\n# RS1029: Do not use reserved diagnostic IDs\ndotnet_diagnostic.RS1029.severity = none\n\n# RS1030: Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer\ndotnet_diagnostic.RS1030.severity = none\n\n# RS1031: Define diagnostic title correctly\ndotnet_diagnostic.RS1031.severity = none\n\n# RS1032: Define diagnostic message correctly\ndotnet_diagnostic.RS1032.severity = none\n\n# RS1033: Define diagnostic description correctly\ndotnet_diagnostic.RS1033.severity = none\n\n# RS1034: Prefer 'IsKind' for checking syntax kinds\ndotnet_diagnostic.RS1034.severity = none\n\n# RS1035: Do not use APIs banned for analyzers\ndotnet_diagnostic.RS1035.severity = none\n\n# RS1036: Specify analyzer banned API enforcement setting\ndotnet_diagnostic.RS1036.severity = none\n\n# RS1037: Add \"CompilationEnd\" custom tag to compilation end diagnostic descriptor\ndotnet_diagnostic.RS1037.severity = none\n\n# RS1038: Compiler extensions should be implemented in assemblies with compiler-provided references\ndotnet_diagnostic.RS1038.severity = suggestion\n\n# RS2000: Add analyzer diagnostic IDs to analyzer release\ndotnet_diagnostic.RS2000.severity = none\n\n# RS2001: Ensure up-to-date entry for analyzer diagnostic IDs are added to analyzer release\ndotnet_diagnostic.RS2001.severity = none\n\n# RS2002: Do not add removed analyzer diagnostic IDs to unshipped analyzer release\ndotnet_diagnostic.RS2002.severity = none\n\n# RS2003: Shipped diagnostic IDs that are no longer reported should have an entry in the 'Removed Rules' table in unshipped file\ndotnet_diagnostic.RS2003.severity = none\n\n# RS2004: Diagnostic IDs marked as removed in analyzer release file should not be reported by analyzers\ndotnet_diagnostic.RS2004.severity = none\n\n# RS2005: Remove duplicate entries for diagnostic ID in the same analyzer release\ndotnet_diagnostic.RS2005.severity = none\n\n# RS2006: Remove duplicate entries for diagnostic ID between analyzer releases\ndotnet_diagnostic.RS2006.severity = none\n\n# RS2007: Invalid entry in analyzer release file\ndotnet_diagnostic.RS2007.severity = none\n\n# RS2008: Enable analyzer release tracking\ndotnet_diagnostic.RS2008.severity = none\n\n# SA0001: XML comments\ndotnet_diagnostic.SA0001.severity = none\n\n# SA1000: Spacing around keywords\ndotnet_diagnostic.SA1000.severity = none\n\n# SA1001: Commas should not be preceded by whitespace\ndotnet_diagnostic.SA1001.severity = none\n\n# SA1002: Semicolons should not be preceded by a space\ndotnet_diagnostic.SA1002.severity = none\n\n# SA1003: Operator should not appear at the end of a line\ndotnet_diagnostic.SA1003.severity = none\n\n# SA1004: Documentation line should begin with a space\ndotnet_diagnostic.SA1004.severity = none\n\n# SA1005: Single line comment should begin with a space\ndotnet_diagnostic.SA1005.severity = none\n\n# SA1008: Opening parenthesis should not be preceded by a space\ndotnet_diagnostic.SA1008.severity = none\n\n# SA1009: Closing parenthesis should not be followed by a space\ndotnet_diagnostic.SA1009.severity = none\n\n# SA1010: Opening square brackets should not be preceded by a space\ndotnet_diagnostic.SA1010.severity = none\n\n# SA1011: Closing square bracket should be followed by a space\ndotnet_diagnostic.SA1011.severity = none\n\n# SA1012: Opening brace should be followed by a space\ndotnet_diagnostic.SA1012.severity = none\n\n# SA1013: Closing brace should be preceded by a space\ndotnet_diagnostic.SA1013.severity = none\n\n# SA1014: Opening generic brackets should not be preceded by a space\ndotnet_diagnostic.SA1014.severity = none\n\n# SA1015: Closing generic bracket should not be followed by a space\ndotnet_diagnostic.SA1015.severity = none\n\n# SA1018: Nullable type symbol should not be preceded by a space\ndotnet_diagnostic.SA1018.severity = none\n\n# SA1020: Increment symbol should not be preceded by a space\ndotnet_diagnostic.SA1020.severity = none\n\n# SA1021: Negative sign should be preceded by a space\ndotnet_diagnostic.SA1021.severity = none\n\n# SA1023: Dereference symbol '*' should not be preceded by a space.\"\ndotnet_diagnostic.SA1023.severity = none\n\n# SA1024: Colon should be followed by a space\ndotnet_diagnostic.SA1024.severity = none\n\n# SA1025: Code should not contain multiple whitespace characters in a row\ndotnet_diagnostic.SA1025.severity = none\n\n# SA1026: Keyword followed by span or blank line\ndotnet_diagnostic.SA1026.severity = none\n\n# SA1027: Tabs and spaces should be used correctly\ndotnet_diagnostic.SA1027.severity = none\n\n# SA1028: Code should not contain trailing whitespace\ndotnet_diagnostic.SA1028.severity = none\n\n# SA1100: Do not prefix calls with base unless local implementation exists\ndotnet_diagnostic.SA1100.severity = none\n\n# SA1101: Prefix local calls with this\ndotnet_diagnostic.SA1101.severity = none\n\n# SA1102: Query clause should follow previous clause\ndotnet_diagnostic.SA1102.severity = none\n\n# SA1105: Query clauses spanning multiple lines should begin on own line\ndotnet_diagnostic.SA1105.severity = none\n\n# SA1106: Code should not contain empty statements\ndotnet_diagnostic.SA1106.severity = none\n\n# SA1107: Code should not contain multiple statements on one line\ndotnet_diagnostic.SA1107.severity = none\n\n# SA1108: Block statements should not contain embedded comments\ndotnet_diagnostic.SA1108.severity = none\n\n# SA1110: Opening parenthesis or bracket should be on declaration line\ndotnet_diagnostic.SA1110.severity = none\n\n# SA1111: Closing parenthesis should be on line of last parameter\ndotnet_diagnostic.SA1111.severity = none\n\n# SA1113: Comma should be on the same line as previous parameter\ndotnet_diagnostic.SA1113.severity = none\n\n# SA1114: Parameter list should follow declaration\ndotnet_diagnostic.SA1114.severity = none\n\n# SA1115: Parameter should begin on the line after the previous parameter\ndotnet_diagnostic.SA1115.severity = none\n\n# SA1116: Split parameters should start on line after declaration\ndotnet_diagnostic.SA1116.severity = none\n\n# SA1117: Parameters should be on same line or separate lines\ndotnet_diagnostic.SA1117.severity = none\n\n# SA1118: Parameter should not span multiple lines\ndotnet_diagnostic.SA1118.severity = none\n\n# SA1119: Statement should not use unnecessary parenthesis\ndotnet_diagnostic.SA1119.severity = none\n\n# SA1120: Comments should contain text\ndotnet_diagnostic.SA1120.severity = none\n\n# SA1121: Use built-in type alias\ndotnet_diagnostic.SA1121.severity = none\n\n# SA1122: Use string.Empty for empty strings\ndotnet_diagnostic.SA1122.severity = none\n\n# SA1123: Region should not be located within a code element\ndotnet_diagnostic.SA1123.severity = none\n\n# SA1124: Do not use regions\ndotnet_diagnostic.SA1124.severity = none\n\n# SA1125: Use shorthand for nullable types\ndotnet_diagnostic.SA1125.severity = none\n\n# SA1127: Generic type constraints should be on their own line\ndotnet_diagnostic.SA1127.severity = none\n\n# SA1128: Put constructor initializers on their own line\ndotnet_diagnostic.SA1128.severity = none\n\n# SA1129: Do not use default value type constructor\ndotnet_diagnostic.SA1129.severity = none\n\n# SA1130: Use lambda syntax\ndotnet_diagnostic.SA1130.severity = none\n\n# SA1131: Constant values should appear on the right-hand side of comparisons\ndotnet_diagnostic.SA1131.severity = none\n\n# SA1132: Do not combine fields\ndotnet_diagnostic.SA1132.severity = none\n\n# SA1133: Do not combine attributes\ndotnet_diagnostic.SA1133.severity = none\n\n# SA1134: Each attribute should be placed on its own line of code\ndotnet_diagnostic.SA1134.severity = none\n\n# SA1135: Using directive should be qualified\ndotnet_diagnostic.SA1135.severity = none\n\n# SA1136: Enum values should be on separate lines\ndotnet_diagnostic.SA1136.severity = none\n\n# SA1137: Elements should have the same indentation\ndotnet_diagnostic.SA1137.severity = none\n\n# SA1139: Use literal suffix notation instead of casting\ndotnet_diagnostic.SA1139.severity = none\n\n# SA1141: Use tuple syntax\ndotnet_diagnostic.SA1141.severity = none\n\n# SA1142: Refer to tuple elements by name\ndotnet_diagnostic.SA1142.severity = none\n\n# SA1200: Using directive should appear within a namespace declaration\ndotnet_diagnostic.SA1200.severity = none\n\n# SA1201: Elements should appear in the correct order\ndotnet_diagnostic.SA1201.severity = none\n\n# SA1202: Elements should be ordered by access\ndotnet_diagnostic.SA1202.severity = none\n\n# SA1203: Constants should appear before fields\ndotnet_diagnostic.SA1203.severity = none\n\n# SA1204: Static elements should appear before instance elements\ndotnet_diagnostic.SA1204.severity = none\n\n# SA1205: Partial elements should declare an access modifier\ndotnet_diagnostic.SA1205.severity = none\n\n# SA1206: Keyword ordering\ndotnet_diagnostic.SA1206.severity = none\n\n# SA1208: Using directive ordering\ndotnet_diagnostic.SA1208.severity = none\n\n# SA1209: Using alias directives should be placed after all using namespace directives\ndotnet_diagnostic.SA1209.severity = none\n\n# SA1210: Using directives should be ordered alphabetically by the namespaces\ndotnet_diagnostic.SA1210.severity = none\n\n# SA1211: Using alias directive ordering\ndotnet_diagnostic.SA1211.severity = none\n\n# SA1212: A get accessor appears after a set accessor within a property or indexer\ndotnet_diagnostic.SA1212.severity = none\n\n# SA1214: Readonly fields should appear before non-readonly fields\ndotnet_diagnostic.SA1214.severity = none\n\n# SA1216: Using static directives should be placed at the correct location\ndotnet_diagnostic.SA1216.severity = none\n\n# SA1300: Element should begin with an uppercase letter\ndotnet_diagnostic.SA1300.severity = none\n\n# SA1302: Interface names should begin with I\ndotnet_diagnostic.SA1302.severity = none\n\n# SA1303: Const field names should begin with upper-case letter\ndotnet_diagnostic.SA1303.severity = none\n\n# SA1304: Non-private readonly fields should begin with upper-case letter\ndotnet_diagnostic.SA1304.severity = none\n\n# SA1306: Field should begin with lower-case letter\ndotnet_diagnostic.SA1306.severity = none\n\n# SA1307: Field should begin with upper-case letter\ndotnet_diagnostic.SA1307.severity = none\n\n# SA1308: Field should not begin with the prefix 's_'\ndotnet_diagnostic.SA1308.severity = none\n\n# SA1309: Field names should not begin with underscore\ndotnet_diagnostic.SA1309.severity = none\n\n# SA1310: Field should not contain an underscore\ndotnet_diagnostic.SA1310.severity = none\n\n# SA1311: Static readonly fields should begin with upper-case letter\ndotnet_diagnostic.SA1311.severity = none\n\n# SA1312: Variable should begin with lower-case letter\ndotnet_diagnostic.SA1312.severity = none\n\n# SA1313: Parameter should begin with lower-case letter\ndotnet_diagnostic.SA1313.severity = none\n\n# SA1314: Type parameter names should begin with T\ndotnet_diagnostic.SA1314.severity = none\n\n# SA1316: Tuple element names should use correct casing\ndotnet_diagnostic.SA1316.severity = none\n\n# SA1400: Member should declare an access modifier\ndotnet_diagnostic.SA1400.severity = none\n\n# SA1401: Fields should be private\ndotnet_diagnostic.SA1401.severity = none\n\n# SA1402: File may only contain a single type\ndotnet_diagnostic.SA1402.severity = none\n\n# SA1403: File may only contain a single namespace\ndotnet_diagnostic.SA1403.severity = none\n\n# SA1404: Code analysis suppression should have justification\ndotnet_diagnostic.SA1404.severity = none\n\n# SA1405: Debug.Assert should provide message text\ndotnet_diagnostic.SA1405.severity = none\n\n# SA1407: Arithmetic expressions should declare precedence\ndotnet_diagnostic.SA1407.severity = none\n\n# SA1408: Conditional expressions should declare precedence\ndotnet_diagnostic.SA1408.severity = none\n\n# SA1410: Remove delegate parens when possible\ndotnet_diagnostic.SA1410.severity = none\n\n# SA1411: Attribute constructor shouldn't use unnecessary parenthesis\ndotnet_diagnostic.SA1411.severity = none\n\n# SA1413: Use trailing comma in multi-line initializers\ndotnet_diagnostic.SA1413.severity = none\n\n# SA1414: Tuple types in signatures should have element names\ndotnet_diagnostic.SA1414.severity = none\n\n# SA1500: Braces for multi-line statements should not share line\ndotnet_diagnostic.SA1500.severity = none\n\n# SA1501: Statement should not be on a single line\ndotnet_diagnostic.SA1501.severity = none\n\n# SA1502: Element should not be on a single line\ndotnet_diagnostic.SA1502.severity = none\n\n# SA1503: Braces should not be omitted\ndotnet_diagnostic.SA1503.severity = none\n\n# SA1504: All accessors should be single-line or multi-line\ndotnet_diagnostic.SA1504.severity = none\n\n# SA1505: An opening brace should not be followed by a blank line\ndotnet_diagnostic.SA1505.severity = none\n\n# SA1506: Element documentation headers should not be followed by blank line\ndotnet_diagnostic.SA1506.severity = none\n\n# SA1507: Code should not contain multiple blank lines in a row\ndotnet_diagnostic.SA1507.severity = none\n\n# SA1508: A closing brace should not be preceded by a blank line\ndotnet_diagnostic.SA1508.severity = none\n\n# SA1509: Opening braces should not be preceded by blank line\ndotnet_diagnostic.SA1509.severity = none\n\n# SA1510: 'else' statement should not be preceded by a blank line\ndotnet_diagnostic.SA1510.severity = none\n\n# SA1512: Single-line comments should not be followed by blank line\ndotnet_diagnostic.SA1512.severity = none\n\n# SA1513: Closing brace should be followed by blank line\ndotnet_diagnostic.SA1513.severity = none\n\n# SA1514: Element documentation header should be preceded by blank line\ndotnet_diagnostic.SA1514.severity = none\n\n# SA1515: Single-line comment should be preceded by blank line\ndotnet_diagnostic.SA1515.severity = none\n\n# SA1516: Elements should be separated by blank line\ndotnet_diagnostic.SA1516.severity = none\n\n# SA1517: Code should not contain blank lines at start of file\ndotnet_diagnostic.SA1517.severity = none\n\n# SA1518: Code should not contain blank lines at the end of the file\ndotnet_diagnostic.SA1518.severity = none\n\n# SA1519: Braces should not be omitted from multi-line child statement\ndotnet_diagnostic.SA1519.severity = none\n\n# SA1520: Use braces consistently\ndotnet_diagnostic.SA1520.severity = none\n\n# SA1600: Elements should be documented\ndotnet_diagnostic.SA1600.severity = none\n\n# SA1601: Partial elements should be documented\ndotnet_diagnostic.SA1601.severity = none\n\n# SA1602: Enumeration items should be documented\ndotnet_diagnostic.SA1602.severity = none\n\n# SA1604: Element documentation should have summary\ndotnet_diagnostic.SA1604.severity = none\n\n# SA1605: Partial element documentation should have summary\ndotnet_diagnostic.SA1605.severity = none\n\n# SA1606: Element documentation should have summary text\ndotnet_diagnostic.SA1606.severity = none\n\n# SA1608: Element documentation should not have default summary\ndotnet_diagnostic.SA1608.severity = none\n\n# SA1610: Property documentation should have value text\ndotnet_diagnostic.SA1610.severity = none\n\n# SA1611: The documentation for parameter 'message' is missing\ndotnet_diagnostic.SA1611.severity = none\n\n# SA1612: The parameter documentation is at incorrect position\ndotnet_diagnostic.SA1612.severity = none\n\n# SA1614: Element parameter documentation should have text\ndotnet_diagnostic.SA1614.severity = none\n\n# SA1615: Element return value should be documented\ndotnet_diagnostic.SA1615.severity = none\n\n# SA1616: Element return value documentation should have text\ndotnet_diagnostic.SA1616.severity = none\n\n# SA1618: The documentation for type parameter is missing\ndotnet_diagnostic.SA1618.severity = none\n\n# SA1619: The documentation for type parameter is missing\ndotnet_diagnostic.SA1619.severity = none\n\n# SA1622: Generic type parameter documentation should have text\ndotnet_diagnostic.SA1622.severity = none\n\n# SA1623: Property documentation text\ndotnet_diagnostic.SA1623.severity = none\n\n# SA1624: Because the property only contains a visible get accessor, the documentation summary text should begin with 'Gets'\ndotnet_diagnostic.SA1624.severity = none\n\n# SA1625: Element documentation should not be copied and pasted\ndotnet_diagnostic.SA1625.severity = none\n\n# SA1626: Single-line comments should not use documentation style slashes\ndotnet_diagnostic.SA1626.severity = none\n\n# SA1627: The documentation text within the \\'exception\\' tag should not be empty\ndotnet_diagnostic.SA1627.severity = none\n\n# SA1629: Documentation text should end with a period\ndotnet_diagnostic.SA1629.severity = none\n\n# SA1633: File should have header\ndotnet_diagnostic.SA1633.severity = none\n\n# SA1642: Constructor summary documentation should begin with standard text\ndotnet_diagnostic.SA1642.severity = none\n\n# SA1643: Destructor summary documentation should begin with standard text\ndotnet_diagnostic.SA1643.severity = none\n\n# SA1649: File name should match first type name\ndotnet_diagnostic.SA1649.severity = none\n\n# IDE0001: Simplify name\ndotnet_diagnostic.IDE0001.severity = silent\n\n# IDE0002: Simplify member access\ndotnet_diagnostic.IDE0002.severity = silent\n\n# IDE0003: Remove this or Me qualification\ndotnet_diagnostic.IDE0003.severity = silent\n\n# IDE0004: Remove Unnecessary Cast\ndotnet_diagnostic.IDE0004.severity = silent\n\n# IDE0005: Using directive is unnecessary.\ndotnet_diagnostic.IDE0005.severity = silent\n\n# IDE0007: Use implicit type\ndotnet_diagnostic.IDE0007.severity = silent\n\n# IDE0008: Use explicit type\ndotnet_diagnostic.IDE0008.severity = silent\n\n# IDE0009: Add this or Me qualification\ndotnet_diagnostic.IDE0009.severity = silent\n\n# IDE0010: Add missing cases\ndotnet_diagnostic.IDE0010.severity = silent\n\n# IDE0011: Add braces\ndotnet_diagnostic.IDE0011.severity = silent\n\n# IDE0016: Use 'throw' expression\ndotnet_diagnostic.IDE0016.severity = silent\n\n# IDE0017: Simplify object initialization\ndotnet_diagnostic.IDE0017.severity = silent\n\n# IDE0018: Inline variable declaration\ndotnet_diagnostic.IDE0018.severity = silent\n\n# IDE0019: Use pattern matching to avoid as followed by a null check\ndotnet_diagnostic.IDE0019.severity = silent\n\n# IDE0020: Use pattern matching to avoid is check followed by a cast (with variable)\ndotnet_diagnostic.IDE0020.severity = silent\n\n# IDE0021: Use expression body for constructors\ndotnet_diagnostic.IDE0021.severity = silent\n\n# IDE0022: Use expression body for methods\ndotnet_diagnostic.IDE0022.severity = silent\n\n# IDE0023: Use expression body for operators\ndotnet_diagnostic.IDE0023.severity = silent\n\n# IDE0024: Use expression body for operators\ndotnet_diagnostic.IDE0024.severity = silent\n\n# IDE0025: Use expression body for properties\ndotnet_diagnostic.IDE0025.severity = silent\n\n# IDE0026: Use expression body for indexers\ndotnet_diagnostic.IDE0026.severity = silent\n\n# IDE0027: Use expression body for accessors\ndotnet_diagnostic.IDE0027.severity = silent\n\n# IDE0028: Simplify collection initialization\ndotnet_diagnostic.IDE0028.severity = silent\n\n# IDE0029: Use coalesce expression\ndotnet_diagnostic.IDE0029.severity = silent\n\n# IDE0030: Use coalesce expression\ndotnet_diagnostic.IDE0030.severity = silent\n\n# IDE0031: Use null propagation\ndotnet_diagnostic.IDE0031.severity = silent\n\n# IDE0032: Use auto property\ndotnet_diagnostic.IDE0032.severity = silent\n\n# IDE0033: Use explicitly provided tuple name\ndotnet_diagnostic.IDE0033.severity = silent\n\n# IDE0034: Simplify 'default' expression\ndotnet_diagnostic.IDE0034.severity = silent\n\n# IDE0035: Remove unreachable code\ndotnet_diagnostic.IDE0035.severity = silent\n\n# IDE0036: Order modifiers\ndotnet_diagnostic.IDE0036.severity = silent\n\n# IDE0037: Use inferred member name\ndotnet_diagnostic.IDE0037.severity = silent\n\n# IDE0038: Use pattern matching to avoid is check followed by a cast (without variable)\ndotnet_diagnostic.IDE0038.severity = silent\n\n# IDE0039: Use local function\ndotnet_diagnostic.IDE0039.severity = silent\n\n# IDE0040: Add accessibility modifiers\ndotnet_diagnostic.IDE0040.severity = silent\n\n# IDE0041: Use 'is null' check\ndotnet_diagnostic.IDE0041.severity = silent\n\n# IDE0042: Deconstruct variable declaration\ndotnet_diagnostic.IDE0042.severity = silent\n\n# IDE0043: Invalid format string\ndotnet_diagnostic.IDE0043.severity = silent\n\n# IDE0044: Add readonly modifier\ndotnet_diagnostic.IDE0044.severity = silent\n\n# IDE0045: Use conditional expression for assignment\ndotnet_diagnostic.IDE0045.severity = silent\n\n# IDE0046: Use conditional expression for return\ndotnet_diagnostic.IDE0046.severity = silent\n\n# IDE0047: Remove unnecessary parentheses\ndotnet_diagnostic.IDE0047.severity = silent\n\n# IDE0048: Add parentheses for clarity\ndotnet_diagnostic.IDE0048.severity = silent\n\n# IDE0049: Use language keywords instead of framework type names for type references\ndotnet_diagnostic.IDE0049.severity = silent\n\n# IDE0051: Remove unused private members\ndotnet_diagnostic.IDE0051.severity = silent\n\n# IDE0052: Remove unread private members\ndotnet_diagnostic.IDE0052.severity = silent\n\n# IDE0053: Use expression body for lambdas\ndotnet_diagnostic.IDE0053.severity = silent\n\n# IDE0054: Use compound assignment\ndotnet_diagnostic.IDE0054.severity = silent\n\n# IDE0055: Fix formatting\ndotnet_diagnostic.IDE0055.severity = silent\n\n# IDE0056: Use index operator\ndotnet_diagnostic.IDE0056.severity = silent\n\n# IDE0057: Use range operator\ndotnet_diagnostic.IDE0057.severity = silent\n\n# IDE0058: Expression value is never used\ndotnet_diagnostic.IDE0058.severity = silent\n\n# IDE0059: Unnecessary assignment of a value\ndotnet_diagnostic.IDE0059.severity = silent\n\n# IDE0060: Remove unused parameter\ndotnet_diagnostic.IDE0060.severity = silent\n\n# IDE0061: Use expression body for local functions\ndotnet_diagnostic.IDE0061.severity = silent\n\n# IDE0062: Make local function 'static'\ndotnet_diagnostic.IDE0062.severity = silent\n\n# IDE0063: Use simple 'using' statement\ndotnet_diagnostic.IDE0063.severity = silent\n\n# IDE0064: Make readonly fields writable\ndotnet_diagnostic.IDE0064.severity = silent\n\n# IDE0065: Misplaced using directive\ndotnet_diagnostic.IDE0065.severity = silent\n\n# IDE0066: Convert switch statement to expression\ndotnet_diagnostic.IDE0066.severity = silent\n\n# IDE0070: Use 'System.HashCode'\ndotnet_diagnostic.IDE0070.severity = silent\n\n# IDE0071: Simplify interpolation\ndotnet_diagnostic.IDE0071.severity = silent\n\n# IDE0072: Add missing cases\ndotnet_diagnostic.IDE0072.severity = silent\n\n# IDE0073: The file header is missing or not located at the top of the file\ndotnet_diagnostic.IDE0073.severity = error\n\n# IDE0074: Use compound assignment\ndotnet_diagnostic.IDE0074.severity = silent\n\n# IDE0075: Simplify conditional expression\ndotnet_diagnostic.IDE0075.severity = silent\n\n# IDE0076: Invalid global 'SuppressMessageAttribute'\ndotnet_diagnostic.IDE0076.severity = silent\n\n# IDE0077: Avoid legacy format target in 'SuppressMessageAttribute'\ndotnet_diagnostic.IDE0077.severity = silent\n\n# IDE0078: Use pattern matching\ndotnet_diagnostic.IDE0078.severity = silent\n\n# IDE0079: RemoveUnnecessarySuppression\ndotnet_diagnostic.IDE0079.severity = silent\n\n# IDE0080: Remove unnecessary suppression operator\ndotnet_diagnostic.IDE0080.severity = silent\n\n# IDE0081: RemoveUnnecessaryByVal\ndotnet_diagnostic.IDE0081.severity = silent\n\n# IDE0082: 'typeof' can be converted  to 'nameof'\ndotnet_diagnostic.IDE0082.severity = silent\n\n# IDE0083: Use pattern matching\ndotnet_diagnostic.IDE0083.severity = silent\n\n# IDE0084: Use pattern matching (IsNot operator)\ndotnet_diagnostic.IDE0084.severity = silent\n\n# IDE0090: Use 'new(...)'\ndotnet_diagnostic.IDE0090.severity = silent\n\n# IDE0100: Remove redundant equality\ndotnet_diagnostic.IDE0100.severity = silent\n\n# IDE0110: Remove unnecessary discard\ndotnet_diagnostic.IDE0110.severity = silent\n\n# IDE0120: Simplify LINQ expression\ndotnet_diagnostic.IDE0120.severity = silent\n\n# IDE0130: Namespace does not match folder structure\ndotnet_diagnostic.IDE0130.severity = silent\n\n# IDE0140: Simplify object creation\ndotnet_diagnostic.IDE0140.severity = silent\n\n# IDE0150: Prefer 'null' check over type check\ndotnet_diagnostic.IDE0150.severity = silent\n\n# IDE0160: Convert to block scoped namespace\ndotnet_diagnostic.IDE0160.severity = silent\n\n# IDE0161: Convert to file-scoped namespace\ndotnet_diagnostic.IDE0161.severity = silent\n\n# IDE0170: Simplify property pattern\ndotnet_diagnostic.IDE0170.severity = silent\n\n# IDE0180: Use tuple swap\ndotnet_diagnostic.IDE0180.severity = silent\n\n# IDE0200: Remove unnecessary lambda expression\ndotnet_diagnostic.IDE0200.severity = silent\n\n# IDE0210: Use top-level statements\ndotnet_diagnostic.IDE0210.severity = silent\n\n# IDE0211: Convert to 'Program.Main' style program\ndotnet_diagnostic.IDE0211.severity = silent\n\n# IDE0220: foreach cast\ndotnet_diagnostic.IDE0220.severity = silent\n\n# IDE0230: Use UTF8 string literal\ndotnet_diagnostic.IDE0230.severity = silent\n\n# IDE0240: Remove redundant nullable directive\ndotnet_diagnostic.IDE0240.severity = silent\n\n# IDE0241: Remove unnecessary nullable directive\ndotnet_diagnostic.IDE0241.severity = silent\n\n# IDE0250: Make struct readonly\ndotnet_diagnostic.IDE0250.severity = silent\n\n# IDE0251: Make member readonly\ndotnet_diagnostic.IDE0251.severity = silent\n\n# IDE0260: Use pattern matching\ndotnet_diagnostic.IDE0260.severity = silent\n\n# IDE0270: Use coalesce expression\ndotnet_diagnostic.IDE0270.severity = silent\n\n# IDE0280: Use 'nameof'\ndotnet_diagnostic.IDE0280.severity = silent\n\n# IDE0290: Use primary constructor\ndotnet_diagnostic.IDE0290.severity = silent\n\n# IDE0300: Use collection expression for array\ndotnet_diagnostic.IDE0300.severity = silent\n\n# IDE0301: Use collection expression for empty\ndotnet_diagnostic.IDE0301.severity = silent\n\n# IDE0302: Use collection expression for stackalloc\ndotnet_diagnostic.IDE0302.severity = silent\n\n# IDE0303: Use collection expression for Create()\ndotnet_diagnostic.IDE0303.severity = silent\n\n# IDE0304: Use collection expression for builder\ndotnet_diagnostic.IDE0304.severity = silent\n\n# IDE0305: Use collection expression for fluent\ndotnet_diagnostic.IDE0305.severity = silent\n\n# IDE1005: Delegate invocation can be simplified.\ndotnet_diagnostic.IDE1005.severity = silent\n\n# IDE1006: Naming Styles\ndotnet_diagnostic.IDE1006.severity = silent\n\n# IDE2000: C#\ndotnet_diagnostic.IDE2000.severity = silent\n\n# IDE2001: Embedded statements must be on their own line\ndotnet_diagnostic.IDE2001.severity = silent\n\n# IDE2002: Consecutive braces must not have blank line between them\ndotnet_diagnostic.IDE2002.severity = silent\n\n# IDE2003: C#\ndotnet_diagnostic.IDE2003.severity = silent\n\n# IDE2004: Blank line not allowed after constructor initializer colon\ndotnet_diagnostic.IDE2004.severity = silent\n\n# IDE2005: Blank line not allowed after conditional expression token\ndotnet_diagnostic.IDE2005.severity = silent\n\n# IDE2006: Blank line not allowed after arrow expression clause token\ndotnet_diagnostic.IDE2006.severity = silent\n\n# xUnit1000: Test classes must be public\ndotnet_diagnostic.xUnit1000.severity = warning\n\n# xUnit1001: Fact methods cannot have parameters\ndotnet_diagnostic.xUnit1001.severity = warning\n\n# xUnit1002: Test methods cannot have multiple Fact or Theory attributes\ndotnet_diagnostic.xUnit1002.severity = warning\n\n# xUnit1003: Theory methods must have test data\ndotnet_diagnostic.xUnit1003.severity = warning\n\n# xUnit1004: Test methods should not be skipped\n# dotnet_diagnostic.xUnit1004.severity = warning\n\n# xUnit1005: Fact methods should not have test data\ndotnet_diagnostic.xUnit1005.severity = warning\n\n# xUnit1006: Theory methods should have parameters\ndotnet_diagnostic.xUnit1006.severity = warning\n\n# xUnit1007: ClassData must point at a valid class\ndotnet_diagnostic.xUnit1007.severity = warning\n\n# xUnit1008: Test data attribute should only be used on a Theory\ndotnet_diagnostic.xUnit1008.severity = warning\n\n# xUnit1009: InlineData must match the number of method parameters\ndotnet_diagnostic.xUnit1009.severity = warning\n\n# xUnit1010: The value is not convertible to the method parameter type\ndotnet_diagnostic.xUnit1010.severity = warning\n\n# xUnit1011: There is no matching method parameter\ndotnet_diagnostic.xUnit1011.severity = warning\n\n# xUnit1012: Null should not be used for value type parameters\ndotnet_diagnostic.xUnit1012.severity = warning\n\n# xUnit1013: Public methods should be marked as test\ndotnet_diagnostic.xUnit1013.severity = warning\n\n# xUnit1014: MemberData should use nameof operator for member name\ndotnet_diagnostic.xUnit1014.severity = warning\n\n# xUnit1015: MemberData must reference an existing member\ndotnet_diagnostic.xUnit1015.severity = warning\n\n# xUnit1016: MemberData must reference a public member\ndotnet_diagnostic.xUnit1016.severity = warning\n\n# xUnit1017: MemberData must reference a static member\ndotnet_diagnostic.xUnit1017.severity = warning\n\n# xUnit1018: MemberData must reference a valid member kind\ndotnet_diagnostic.xUnit1018.severity = warning\n\n# xUnit1019: MemberData must reference a member providing a valid data type\ndotnet_diagnostic.xUnit1019.severity = warning\n\n# xUnit1020: MemberData must reference a property with a getter\ndotnet_diagnostic.xUnit1020.severity = warning\n\n# xUnit1021: MemberData should not have parameters if the referenced member is not a method\ndotnet_diagnostic.xUnit1021.severity = warning\n\n# xUnit1022: Theory methods cannot have a parameter array\ndotnet_diagnostic.xUnit1022.severity = warning\n\n# xUnit1023: Theory methods cannot have default parameter values\ndotnet_diagnostic.xUnit1023.severity = warning\n\n# xUnit1024: Test methods cannot have overloads\ndotnet_diagnostic.xUnit1024.severity = warning\n\n# xUnit1025: InlineData should be unique within the Theory it belongs to\ndotnet_diagnostic.xUnit1025.severity = warning\n\n# xUnit1026: Theory methods should use all of their parameters\ndotnet_diagnostic.xUnit1026.severity = warning\n\n# xUnit1030: Test methods should not call ConfigureAwait(), as it may bypass parallelization limits.\ndotnet_diagnostic.xUnit1030.severity = none\n\n# xUnit1031: Test methods must not use blocking task operations, as they can cause deadlocks. Use an async test method and await instead.\ndotnet_diagnostic.xUnit1031.severity = none\n\n# xUnit1051: Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken to allow test cancellation to be more responsive.\ndotnet_diagnostic.xUnit1051.severity = none\n\n# xUnit2000: Constants and literals should be the expected argument\ndotnet_diagnostic.xUnit2000.severity = warning\n\n# xUnit2001: Do not use invalid equality check\ndotnet_diagnostic.xUnit2001.severity = warning\n\n# xUnit2002: Do not use null check on value type\ndotnet_diagnostic.xUnit2002.severity = warning\n\n# xUnit2003: Do not use equality check to test for null value\ndotnet_diagnostic.xUnit2003.severity = warning\n\n# xUnit2004: Do not use equality check to test for boolean conditions\ndotnet_diagnostic.xUnit2004.severity = warning\n\n# xUnit2005: Do not use identity check on value type\ndotnet_diagnostic.xUnit2005.severity = warning\n\n# xUnit2006: Do not use invalid string equality check\ndotnet_diagnostic.xUnit2006.severity = warning\n\n# xUnit2007: Do not use typeof expression to check the type\ndotnet_diagnostic.xUnit2007.severity = warning\n\n# xUnit2008: Do not use boolean check to match on regular expressions\ndotnet_diagnostic.xUnit2008.severity = warning\n\n# xUnit2009: Do not use boolean check to check for substrings\ndotnet_diagnostic.xUnit2009.severity = warning\n\n# xUnit2010: Do not use boolean check to check for string equality\ndotnet_diagnostic.xUnit2010.severity = warning\n\n# xUnit2011: Do not use empty collection check\ndotnet_diagnostic.xUnit2011.severity = warning\n\n# xUnit2012: Do not use Enumerable.Any() to check if a value exists in a collection\ndotnet_diagnostic.xUnit2012.severity = warning\n\n# xUnit2013: Do not use equality check to check for collection size.\ndotnet_diagnostic.xUnit2013.severity = none\n\n# xUnit2014: Do not use throws check to check for asynchronously thrown exception\ndotnet_diagnostic.xUnit2014.severity = none\n\n# xUnit2015: Do not use typeof expression to check the exception type\ndotnet_diagnostic.xUnit2015.severity = warning\n\n# xUnit2016: Keep precision in the allowed range when asserting equality of doubles or decimals\ndotnet_diagnostic.xUnit2016.severity = warning\n\n# xUnit2017: Do not use Contains() to check if a value exists in a collection\ndotnet_diagnostic.xUnit2017.severity = none\n\n# xUnit2018: Do not compare an object's exact type to an abstract class or interface\ndotnet_diagnostic.xUnit2018.severity = warning\n\n# xUnit2019: Do not use obsolete throws check to check for asynchronously thrown exception\ndotnet_diagnostic.xUnit2019.severity = warning\n\n# xUnit3000: Test case classes must derive directly or indirectly from Xunit.LongLivedMarshalByRefObject\ndotnet_diagnostic.xUnit3000.severity = warning\n\n# xUnit3001: Classes that implement Xunit.Abstractions.IXunitSerializable must have a public parameterless constructor\ndotnet_diagnostic.xUnit3001.severity = warning\n"
  },
  {
    "path": "eng/PoliCheckExclusions.xml",
    "content": "<PoliCheckExclusions>\n\t<Exclusion Type=\"FileName\">SWAGGER.JSON</Exclusion>\n</PoliCheckExclusions>\n"
  },
  {
    "path": "eng/Publishing.props",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project>\n   <PropertyGroup>\n      <PublishingVersion>3</PublishingVersion>\n   </PropertyGroup>\n\n   <PropertyGroup>\n      <_UploadPathRoot>reverse-proxy</_UploadPathRoot>\n   </PropertyGroup>\n\n   <PropertyGroup>\n      <PublishDependsOnTargets>$(PublishDependsOnTargets);_PublishBlobItems</PublishDependsOnTargets>\n\n      <!-- NOTE: This property is also defined on the root-level Directory.Build.props, but that file is not imported by the Publishing project.\n           Pulling it in here will cause different issues as that file will conflict with Arcade's publishing logic, so as a workaround we define it here.\n           If you are editing this property, make sure to also edit the one in Directory.Build.props. -->\n      <YarpAppArtifactsOutputDir>$([MSBuild]::NormalizeDirectory('$(ArtifactsDir)',\n         'YarpAppArtifacts', '$(Configuration)'))</YarpAppArtifactsOutputDir>\n   </PropertyGroup>\n\n   <ItemGroup>\n      <_YarpAppFilesToPublish Include=\"$(YarpAppArtifactsOutputDir)\\**\\*.tar.gz\" />\n      <_YarpAppFilesToPublish Include=\"$(YarpAppArtifactsOutputDir)\\**\\*.tar.gz.sha512\" />\n   </ItemGroup>\n\n   <Target Name=\"_PublishBlobItems\">\n      <!-- \n      For blob items, we want to make sure that the version we get back is not stable, even when the repo\n      is producing stable versions.\n      This is because we want to be able to re-spin the build if necessary without hitting issues of blob\n      items clashing with each other. For this reason,\n      We will pass SuppressFinalPackageVersion as true when fetching the package version so that we get\n      back a version with a prerelease suffix.\n      -->\n      <MSBuild Projects=\"$(RepoRoot)src\\Application\\Yarp.Application.csproj\"\n         Targets=\"ReturnPackageVersion\"\n         SkipNonexistentProjects=\"false\"\n         Properties=\"SuppressFinalPackageVersion=true\">\n         <Output TaskParameter=\"TargetOutputs\" PropertyName=\"_PackageVersion\" />\n      </MSBuild>\n\n      <ItemGroup>\n         <ItemsToPushToBlobFeed Include=\"@(_YarpAppFilesToPublish)\">\n            <IsShipping>true</IsShipping>\n            <PublishFlatContainer>true</PublishFlatContainer>\n            <RelativeBlobPath>$(_UploadPathRoot)/$(_PackageVersion)/%(Filename)%(Extension)</RelativeBlobPath>\n         </ItemsToPushToBlobFeed>\n      </ItemGroup>\n   </Target>\n\n</Project>"
  },
  {
    "path": "eng/Signing.props",
    "content": "<Project>\n  <ItemGroup Label=\"File signing information\">\n    <!-- Third-party components which should be signed.  -->\n    <FileSignInfo Include=\"HealthChecks.ApplicationStatus.dll\" CertificateName=\"3PartySHA2\" />\n    <FileSignInfo Include=\"OpenTelemetry.dll\" CertificateName=\"3PartySHA2\" />\n    <FileSignInfo Include=\"OpenTelemetry.Api.dll\" CertificateName=\"3PartySHA2\" />\n    <FileSignInfo Include=\"OpenTelemetry.Api.ProviderBuilderExtensions.dll\" CertificateName=\"3PartySHA2\" />\n    <FileSignInfo Include=\"OpenTelemetry.Exporter.OpenTelemetryProtocol.dll\" CertificateName=\"3PartySHA2\" />\n    <FileSignInfo Include=\"OpenTelemetry.Extensions.Hosting.dll\" CertificateName=\"3PartySHA2\" />\n    <FileSignInfo Include=\"OpenTelemetry.Instrumentation.AspNetCore.dll\" CertificateName=\"3PartySHA2\" />\n    <FileSignInfo Include=\"OpenTelemetry.Instrumentation.GrpcNetClient.dll\" CertificateName=\"3PartySHA2\" />\n    <FileSignInfo Include=\"OpenTelemetry.Instrumentation.Http.dll\" CertificateName=\"3PartySHA2\" />\n    <FileSignInfo Include=\"OpenTelemetry.Instrumentation.Runtime.dll\" CertificateName=\"3PartySHA2\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "eng/Version.Details.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Dependencies>\n  <ProductDependencies>\n  </ProductDependencies>\n  <ToolsetDependencies>\n    <Dependency Name=\"Microsoft.DotNet.Arcade.Sdk\" Version=\"11.0.0-beta.26122.1\">\n      <Uri>https://github.com/dotnet/arcade</Uri>\n      <Sha>88c88084abfa1e379f54933af89e43fa774e323c</Sha>\n    </Dependency>\n    <Dependency Name=\"Microsoft.DotNet.Helix.Sdk\" Version=\"11.0.0-beta.26122.1\">\n      <Uri>https://github.com/dotnet/arcade</Uri>\n      <Sha>88c88084abfa1e379f54933af89e43fa774e323c</Sha>\n    </Dependency>\n    <Dependency Name=\"Microsoft.DotNet.XUnitV3Extensions\" Version=\"11.0.0-beta.26122.1\">\n      <Uri>https://github.com/dotnet/arcade</Uri>\n      <Sha>88c88084abfa1e379f54933af89e43fa774e323c</Sha>\n    </Dependency>\n  </ToolsetDependencies>\n</Dependencies>\n"
  },
  {
    "path": "eng/Versions.props",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n  <PropertyGroup>\n    <VersionPrefix>3.0.0</VersionPrefix>\n    <!--\n      For sorting purposes, it is very important there be a '.' between the word 'preview' and the preview number.\n      That ensures that if we get up to preview 10, it doesn't sort as between preview 1 and preview 2!\n      See https://semver.org/spec/v2.0.0.html for details.\n    -->\n    <PreReleaseVersionLabel>preview.1</PreReleaseVersionLabel>\n    <DotNetFinalVersionKind Condition=\"'$(PreReleaseVersionLabel)' == 'rtw'\">release</DotNetFinalVersionKind>\n  </PropertyGroup>\n  <!--Package versions-->\n  <PropertyGroup>\n    <SystemIOHashingVersion>8.0.0</SystemIOHashingVersion>\n    <MicrosoftAspNetCoreTestHostVersion>6.0.36</MicrosoftAspNetCoreTestHostVersion>\n    <MicrosoftCrankEventSourcesVersion>0.2.0-alpha.24576.2</MicrosoftCrankEventSourcesVersion>\n    <MicrosoftDotNetXUnitV3ExtensionsPackageVersion>11.0.0-beta.26122.1</MicrosoftDotNetXUnitV3ExtensionsPackageVersion>\n    <CoverletCollectorVersion>6.0.0</CoverletCollectorVersion>\n    <MoqVersion>4.18.4</MoqVersion>\n    <AutofacVersion>4.9.4</AutofacVersion>\n    <AutofacExtrasMoqVersion>4.3.0</AutofacExtrasMoqVersion>\n    <PollyVersion>8.4.2</PollyVersion>\n    <LettuceEncryptVersion>1.3.0</LettuceEncryptVersion>\n    <PrometheusNetVersion>8.2.1</PrometheusNetVersion>\n    <SerilogExtensionsLoggingVersion>3.0.1</SerilogExtensionsLoggingVersion>\n    <SerilogFormattingCompactVersion>1.1.0</SerilogFormattingCompactVersion>\n    <SerilogSinksConsoleVersion>3.1.1</SerilogSinksConsoleVersion>\n    <YamlDotNetVersion>16.3.0</YamlDotNetVersion>\n    <KubernetesClientVersion>18.0.13</KubernetesClientVersion>\n    <JsonSchemaNetVersion>7.0.2</JsonSchemaNetVersion>\n    <NewtonsoftJsonVersion>13.0.3</NewtonsoftJsonVersion>\n    <!-- Container app dependencies -->\n    <YarpNugetVersion>2.3.0</YarpNugetVersion>\n    <MicrosoftExtensionsServiceDiscovery>9.1.0</MicrosoftExtensionsServiceDiscovery>\n    <MicrosoftExtensionsServiceDiscoveryYarp>9.1.0</MicrosoftExtensionsServiceDiscoveryYarp>\n    <OpenTelemetryExporterOpenTelemetryProtocol>1.12.0 </OpenTelemetryExporterOpenTelemetryProtocol>\n    <OpenTelemetryExtensionsHosting>1.12.0</OpenTelemetryExtensionsHosting>\n    <OpenTelemetryInstrumentationAspNetCore>1.12.0</OpenTelemetryInstrumentationAspNetCore>\n    <OpenTelemetryInstrumentationHttp>1.12.0</OpenTelemetryInstrumentationHttp>\n    <OpenTelemetryInstrumentationRuntime>1.12.0</OpenTelemetryInstrumentationRuntime>\n    <AspNetCoreHealthChecksApplicationStatus>9.0.0</AspNetCoreHealthChecksApplicationStatus>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "eng/common/BuildConfiguration/build-configuration.json",
    "content": "{\n  \"RetryCountLimit\": 1,\n  \"RetryByAnyError\": false\n}\n"
  },
  {
    "path": "eng/common/CIBuild.cmd",
    "content": "@echo off\npowershell -ExecutionPolicy ByPass -NoProfile -command \"& \"\"\"%~dp0Build.ps1\"\"\" -restore -build -test -sign -pack -publish -ci %*\"\n"
  },
  {
    "path": "eng/common/PSScriptAnalyzerSettings.psd1",
    "content": "@{\n    IncludeRules=@('PSAvoidUsingCmdletAliases',\n                   'PSAvoidUsingWMICmdlet',\n                   'PSAvoidUsingPositionalParameters',\n                   'PSAvoidUsingInvokeExpression',\n                   'PSUseDeclaredVarsMoreThanAssignments',\n                   'PSUseCmdletCorrectly',\n                   'PSStandardDSCFunctionsInResource',\n                   'PSUseIdenticalMandatoryParametersForDSC',\n                   'PSUseIdenticalParametersForDSC')\n}"
  },
  {
    "path": "eng/common/README.md",
    "content": "# Don't touch this folder\n\n                uuuuuuuuuuuuuuuuuuuu\n              u\" uuuuuuuuuuuuuuuuuu \"u\n            u\" u$$$$$$$$$$$$$$$$$$$$u \"u\n          u\" u$$$$$$$$$$$$$$$$$$$$$$$$u \"u\n        u\" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$u \"u\n      u\" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u \"u\n    u\" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u \"u\n    $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $\n    $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $\n    $ $$$\" ... \"$...  ...$\" ... \"$$$  ... \"$$$ $\n    $ $$$u `\"$$$$$$$  $$$  $$$$$  $$  $$$  $$$ $\n    $ $$$$$$uu \"$$$$  $$$  $$$$$  $$  \"\"\" u$$$ $\n    $ $$$\"\"$$$  $$$$  $$$u \"$$$\" u$$  $$$$$$$$ $\n    $ $$$$....,$$$$$..$$$$$....,$$$$..$$$$$$$$ $\n    $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $\n    \"u \"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$\" u\"\n      \"u \"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$\" u\"\n        \"u \"$$$$$$$$$$$$$$$$$$$$$$$$$$$$\" u\"\n          \"u \"$$$$$$$$$$$$$$$$$$$$$$$$\" u\"\n            \"u \"$$$$$$$$$$$$$$$$$$$$\" u\"\n              \"u \"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\" u\"\n                \"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\n\n!!! Changes made in this directory are subject to being overwritten by automation !!!\n\nThe files in this directory are shared by all Arcade repos and managed by automation. If you need to make changes to these files, open an issue or submit a pull request to https://github.com/dotnet/arcade first.\n"
  },
  {
    "path": "eng/common/SetupNugetSources.ps1",
    "content": "# This script adds internal feeds required to build commits that depend on internal package sources. For instance,\n# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. In addition also enables\n# disabled internal Maestro (darc-int*) feeds.\n#\n# Optionally, this script also adds a credential entry for each of the internal feeds if supplied.\n#\n# See example call for this script below.\n#\n#  - task: PowerShell@2\n#    displayName: Setup internal Feeds Credentials\n#    condition: eq(variables['Agent.OS'], 'Windows_NT')\n#    inputs:\n#      filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.ps1\n#      arguments: -ConfigFile $(System.DefaultWorkingDirectory)/NuGet.config -Password $Env:Token\n#    env:\n#      Token: $(dn-bot-dnceng-artifact-feeds-rw)\n#\n# Note that the NuGetAuthenticate task should be called after SetupNugetSources.\n# This ensures that:\n# - Appropriate creds are set for the added internal feeds (if not supplied to the scrupt)\n# - The credential provider is installed.\n#\n# This logic is also abstracted into enable-internal-sources.yml.\n\n[CmdletBinding()]\nparam (\n    [Parameter(Mandatory = $true)][string]$ConfigFile,\n    $Password\n)\n\n$ErrorActionPreference = \"Stop\"\nSet-StrictMode -Version 2.0\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\n. $PSScriptRoot\\tools.ps1\n\n# Adds or enables the package source with the given name\nfunction AddOrEnablePackageSource($sources, $disabledPackageSources, $SourceName, $SourceEndPoint, $creds, $Username, $pwd) {\n    if ($disabledPackageSources -eq $null -or -not (EnableInternalPackageSource -DisabledPackageSources $disabledPackageSources -Creds $creds -PackageSourceName $SourceName)) {\n        AddPackageSource -Sources $sources -SourceName $SourceName -SourceEndPoint $SourceEndPoint -Creds $creds -Username $userName -pwd $Password\n    }\n}\n\n# Add source entry to PackageSources\nfunction AddPackageSource($sources, $SourceName, $SourceEndPoint, $creds, $Username, $pwd) {\n    $packageSource = $sources.SelectSingleNode(\"add[@key='$SourceName']\")\n    \n    if ($packageSource -eq $null)\n    {\n        Write-Host \"Adding package source $SourceName\"\n\n        $packageSource = $doc.CreateElement(\"add\")\n        $packageSource.SetAttribute(\"key\", $SourceName)\n        $packageSource.SetAttribute(\"value\", $SourceEndPoint)\n        $sources.AppendChild($packageSource) | Out-Null\n    }\n    else {\n        Write-Host \"Package source $SourceName already present and enabled.\"\n    }\n\n    AddCredential -Creds $creds -Source $SourceName -Username $Username -pwd $pwd\n}\n\n# Add a credential node for the specified source\nfunction AddCredential($creds, $source, $username, $pwd) {\n    # If no cred supplied, don't do anything.\n    if (!$pwd) {\n        return;\n    }\n\n    Write-Host \"Inserting credential for feed: \" $source\n\n    # Looks for credential configuration for the given SourceName. Create it if none is found.\n    $sourceElement = $creds.SelectSingleNode($Source)\n    if ($sourceElement -eq $null)\n    {\n        $sourceElement = $doc.CreateElement($Source)\n        $creds.AppendChild($sourceElement) | Out-Null\n    }\n\n    # Add the <Username> node to the credential if none is found.\n    $usernameElement = $sourceElement.SelectSingleNode(\"add[@key='Username']\")\n    if ($usernameElement -eq $null)\n    {\n        $usernameElement = $doc.CreateElement(\"add\")\n        $usernameElement.SetAttribute(\"key\", \"Username\")\n        $sourceElement.AppendChild($usernameElement) | Out-Null\n    }\n    $usernameElement.SetAttribute(\"value\", $Username)\n\n    # Add the <ClearTextPassword> to the credential if none is found.\n    # Add it as a clear text because there is no support for encrypted ones in non-windows .Net SDKs.\n    #   -> https://github.com/NuGet/Home/issues/5526\n    $passwordElement = $sourceElement.SelectSingleNode(\"add[@key='ClearTextPassword']\")\n    if ($passwordElement -eq $null)\n    {\n        $passwordElement = $doc.CreateElement(\"add\")\n        $passwordElement.SetAttribute(\"key\", \"ClearTextPassword\")\n        $sourceElement.AppendChild($passwordElement) | Out-Null\n    }\n    \n    $passwordElement.SetAttribute(\"value\", $pwd)\n}\n\n# Enable all darc-int package sources.\nfunction EnableMaestroInternalPackageSources($DisabledPackageSources, $Creds) {\n    $maestroInternalSources = $DisabledPackageSources.SelectNodes(\"add[contains(@key,'darc-int')]\")\n    ForEach ($DisabledPackageSource in $maestroInternalSources) {\n        EnableInternalPackageSource -DisabledPackageSources $DisabledPackageSources -Creds $Creds -PackageSourceName $DisabledPackageSource.key\n    }\n}\n\n# Enables an internal package source by name, if found. Returns true if the package source was found and enabled, false otherwise.\nfunction EnableInternalPackageSource($DisabledPackageSources, $Creds, $PackageSourceName) {\n    $DisabledPackageSource = $DisabledPackageSources.SelectSingleNode(\"add[@key='$PackageSourceName']\")\n    if ($DisabledPackageSource) {\n        Write-Host \"Enabling internal source '$($DisabledPackageSource.key)'.\"\n        \n        # Due to https://github.com/NuGet/Home/issues/10291, we must actually remove the disabled entries\n        $DisabledPackageSources.RemoveChild($DisabledPackageSource)\n\n        AddCredential -Creds $creds -Source $DisabledPackageSource.Key -Username $userName -pwd $Password\n        return $true\n    }\n    return $false\n}\n\nif (!(Test-Path $ConfigFile -PathType Leaf)) {\n  Write-PipelineTelemetryError -Category 'Build' -Message \"Eng/common/SetupNugetSources.ps1 returned a non-zero exit code. Couldn't find the NuGet config file: $ConfigFile\"\n  ExitWithExitCode 1\n}\n\n# Load NuGet.config\n$doc = New-Object System.Xml.XmlDocument\n$filename = (Get-Item $ConfigFile).FullName\n$doc.Load($filename)\n\n# Get reference to <PackageSources> - fail if none exist\n$sources = $doc.DocumentElement.SelectSingleNode(\"packageSources\")\nif ($sources -eq $null) {\n    Write-PipelineTelemetryError -Category 'Build' -Message \"Eng/common/SetupNugetSources.ps1 returned a non-zero exit code. NuGet config file must contain a packageSources section: $ConfigFile\"\n    ExitWithExitCode 1\n}\n\n$creds = $null\n$feedSuffix = \"v3/index.json\"\nif ($Password) {\n    $feedSuffix = \"v2\"\n    # Looks for a <PackageSourceCredentials> node. Create it if none is found.\n    $creds = $doc.DocumentElement.SelectSingleNode(\"packageSourceCredentials\")\n    if ($creds -eq $null) {\n        $creds = $doc.CreateElement(\"packageSourceCredentials\")\n        $doc.DocumentElement.AppendChild($creds) | Out-Null\n    }\n}\n\n$userName = \"dn-bot\"\n\n# Check for disabledPackageSources; we'll enable any darc-int ones we find there\n$disabledSources = $doc.DocumentElement.SelectSingleNode(\"disabledPackageSources\")\nif ($disabledSources -ne $null) {\n    Write-Host \"Checking for any darc-int disabled package sources in the disabledPackageSources node\"\n    EnableMaestroInternalPackageSources -DisabledPackageSources $disabledSources -Creds $creds\n}\n$dotnetVersions = @('5','6','7','8','9','10')\n\nforeach ($dotnetVersion in $dotnetVersions) {\n    $feedPrefix = \"dotnet\" + $dotnetVersion;\n    $dotnetSource = $sources.SelectSingleNode(\"add[@key='$feedPrefix']\")\n    if ($dotnetSource -ne $null) {\n        AddOrEnablePackageSource -Sources $sources -DisabledPackageSources $disabledSources -SourceName \"$feedPrefix-internal\" -SourceEndPoint \"https://pkgs.dev.azure.com/dnceng/internal/_packaging/$feedPrefix-internal/nuget/$feedSuffix\" -Creds $creds -Username $userName -pwd $Password\n        AddOrEnablePackageSource -Sources $sources -DisabledPackageSources $disabledSources -SourceName \"$feedPrefix-internal-transport\" -SourceEndPoint \"https://pkgs.dev.azure.com/dnceng/internal/_packaging/$feedPrefix-internal-transport/nuget/$feedSuffix\" -Creds $creds -Username $userName -pwd $Password\n    }\n}\n\n$doc.Save($filename)\n"
  },
  {
    "path": "eng/common/SetupNugetSources.sh",
    "content": "#!/usr/bin/env bash\n\n# This script adds internal feeds required to build commits that depend on internal package sources. For instance,\n# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. In addition also enables\n# disabled internal Maestro (darc-int*) feeds.\n# \n# Optionally, this script also adds a credential entry for each of the internal feeds if supplied.\n#\n# See example call for this script below.\n#\n#  - task: Bash@3\n#    displayName: Setup Internal Feeds\n#    inputs:\n#      filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.sh\n#      arguments: $(System.DefaultWorkingDirectory)/NuGet.config\n#    condition: ne(variables['Agent.OS'], 'Windows_NT')\n#  - task: NuGetAuthenticate@1\n#\n# Note that the NuGetAuthenticate task should be called after SetupNugetSources.\n# This ensures that:\n# - Appropriate creds are set for the added internal feeds (if not supplied to the scrupt)\n# - The credential provider is installed.\n#\n# This logic is also abstracted into enable-internal-sources.yml.\n\nConfigFile=$1\nCredToken=$2\nNL='\\n'\nTB='    '\n\nsource=\"${BASH_SOURCE[0]}\"\n\n# resolve $source until the file is no longer a symlink\nwhile [[ -h \"$source\" ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\n. \"$scriptroot/tools.sh\"\n\nif [ ! -f \"$ConfigFile\" ]; then\n    Write-PipelineTelemetryError -Category 'Build' \"Error: Eng/common/SetupNugetSources.sh returned a non-zero exit code. Couldn't find the NuGet config file: $ConfigFile\"\n    ExitWithExitCode 1\nfi\n\nif [[ `uname -s` == \"Darwin\" ]]; then\n    NL=$'\\\\\\n'\n    TB=''\nfi\n\n# Enables an internal package source by name, if found. Returns 0 if found and enabled, 1 if not found.\nEnableInternalPackageSource() {\n    local PackageSourceName=\"$1\"\n    \n    # Check if disabledPackageSources section exists\n    grep -i \"<disabledPackageSources>\" \"$ConfigFile\" > /dev/null\n    if [ \"$?\" != \"0\" ]; then\n        return 1  # No disabled sources section\n    fi\n    \n    # Check if this source name is disabled\n    grep -i \"<add key=\\\"$PackageSourceName\\\" value=\\\"true\\\"\" \"$ConfigFile\" > /dev/null\n    if [ \"$?\" == \"0\" ]; then\n        echo \"Enabling internal source '$PackageSourceName'.\"\n        # Remove the disabled entry (including any surrounding comments or whitespace on the same line)\n        sed -i.bak \"/<add key=\\\"$PackageSourceName\\\" value=\\\"true\\\" \\/>/d\" \"$ConfigFile\"\n        \n        # Add the source name to PackageSources for credential handling\n        PackageSources+=(\"$PackageSourceName\")\n        return 0  # Found and enabled\n    fi\n    \n    return 1  # Not found in disabled sources\n}\n\n# Add source entry to PackageSources\nAddPackageSource() {\n    local SourceName=\"$1\"\n    local SourceEndPoint=\"$2\"\n    \n    # Check if source already exists\n    grep -i \"<add key=\\\"$SourceName\\\"\" \"$ConfigFile\" > /dev/null\n    if [ \"$?\" == \"0\" ]; then\n        echo \"Package source $SourceName already present and enabled.\"\n        PackageSources+=(\"$SourceName\")\n        return\n    fi\n    \n    echo \"Adding package source $SourceName\"\n    PackageSourcesNodeFooter=\"</packageSources>\"\n    PackageSourceTemplate=\"${TB}<add key=\\\"$SourceName\\\" value=\\\"$SourceEndPoint\\\" />\"\n    \n    sed -i.bak \"s|$PackageSourcesNodeFooter|$PackageSourceTemplate${NL}$PackageSourcesNodeFooter|\" \"$ConfigFile\"\n    PackageSources+=(\"$SourceName\")\n}\n\n# Adds or enables the package source with the given name\nAddOrEnablePackageSource() {\n    local SourceName=\"$1\"\n    local SourceEndPoint=\"$2\"\n    \n    # Try to enable if disabled, if not found then add new source\n    EnableInternalPackageSource \"$SourceName\"\n    if [ \"$?\" != \"0\" ]; then\n        AddPackageSource \"$SourceName\" \"$SourceEndPoint\"\n    fi\n}\n\n# Enable all darc-int package sources\nEnableMaestroInternalPackageSources() {\n    # Check if disabledPackageSources section exists\n    grep -i \"<disabledPackageSources>\" \"$ConfigFile\" > /dev/null\n    if [ \"$?\" != \"0\" ]; then\n        return  # No disabled sources section\n    fi\n    \n    # Find all darc-int disabled sources\n    local DisabledDarcIntSources=()\n    DisabledDarcIntSources+=$(grep -oh '\"darc-int-[^\"]*\" value=\"true\"' \"$ConfigFile\" | tr -d '\"')\n    \n    for DisabledSourceName in ${DisabledDarcIntSources[@]} ; do\n        if [[ $DisabledSourceName == darc-int* ]]; then\n            EnableInternalPackageSource \"$DisabledSourceName\"\n        fi\n    done\n}\n\n# Ensure there is a <packageSources>...</packageSources> section.\ngrep -i \"<packageSources>\" $ConfigFile\nif [ \"$?\" != \"0\" ]; then\n    Write-PipelineTelemetryError -Category 'Build' \"Error: Eng/common/SetupNugetSources.sh returned a non-zero exit code. NuGet config file must contain a packageSources section: $ConfigFile\"\n    ExitWithExitCode 1\nfi\n\nPackageSources=()\n\n# Set feed suffix based on whether credentials are provided\nFeedSuffix=\"v3/index.json\"\nif [ -n \"$CredToken\" ]; then\n    FeedSuffix=\"v2\"\n    \n    # Ensure there is a <packageSourceCredentials>...</packageSourceCredentials> section.\n    grep -i \"<packageSourceCredentials>\" $ConfigFile\n    if [ \"$?\" != \"0\" ]; then\n        echo \"Adding <packageSourceCredentials>...</packageSourceCredentials> section.\"\n\n        PackageSourcesNodeFooter=\"</packageSources>\"\n        PackageSourceCredentialsTemplate=\"${TB}<packageSourceCredentials>${NL}${TB}</packageSourceCredentials>\"\n\n        sed -i.bak \"s|$PackageSourcesNodeFooter|$PackageSourcesNodeFooter${NL}$PackageSourceCredentialsTemplate|\" $ConfigFile\n    fi\nfi\n\n# Check for disabledPackageSources; we'll enable any darc-int ones we find there\ngrep -i \"<disabledPackageSources>\" $ConfigFile > /dev/null\nif [ \"$?\" == \"0\" ]; then\n    echo \"Checking for any darc-int disabled package sources in the disabledPackageSources node\"\n    EnableMaestroInternalPackageSources\nfi\n\nDotNetVersions=('5' '6' '7' '8' '9' '10')\n\nfor DotNetVersion in ${DotNetVersions[@]} ; do\n    FeedPrefix=\"dotnet${DotNetVersion}\";\n    grep -i \"<add key=\\\"$FeedPrefix\\\"\" $ConfigFile > /dev/null\n    if [ \"$?\" == \"0\" ]; then\n        AddOrEnablePackageSource \"$FeedPrefix-internal\" \"https://pkgs.dev.azure.com/dnceng/internal/_packaging/$FeedPrefix-internal/nuget/$FeedSuffix\"\n        AddOrEnablePackageSource \"$FeedPrefix-internal-transport\" \"https://pkgs.dev.azure.com/dnceng/internal/_packaging/$FeedPrefix-internal-transport/nuget/$FeedSuffix\"\n    fi\ndone\n\n# I want things split line by line\nPrevIFS=$IFS\nIFS=$'\\n'\nPackageSources+=\"$IFS\"\nPackageSources+=$(grep -oh '\"darc-int-[^\"]*\"' $ConfigFile | tr -d '\"')\nIFS=$PrevIFS\n\nif [ \"$CredToken\" ]; then\n    for FeedName in ${PackageSources[@]} ; do\n        # Check if there is no existing credential for this FeedName\n        grep -i \"<$FeedName>\" $ConfigFile \n        if [ \"$?\" != \"0\" ]; then\n            echo \"\tInserting credential for feed: $FeedName\"\n\n            PackageSourceCredentialsNodeFooter=\"</packageSourceCredentials>\"\n            NewCredential=\"${TB}${TB}<$FeedName>${NL}${TB}<add key=\\\"Username\\\" value=\\\"dn-bot\\\" />${NL}${TB}${TB}<add key=\\\"ClearTextPassword\\\" value=\\\"$CredToken\\\" />${NL}${TB}${TB}</$FeedName>\"\n\n            sed -i.bak \"s|$PackageSourceCredentialsNodeFooter|$NewCredential${NL}$PackageSourceCredentialsNodeFooter|\" $ConfigFile\n        fi\n    done\nfi\n"
  },
  {
    "path": "eng/common/build.cmd",
    "content": "@echo off\npowershell -ExecutionPolicy ByPass -NoProfile -command \"& \"\"\"%~dp0build.ps1\"\"\" %*\"\nexit /b %ErrorLevel%\n"
  },
  {
    "path": "eng/common/build.ps1",
    "content": "[CmdletBinding(PositionalBinding=$false)]\nParam(\n  [string][Alias('c')]$configuration = \"Debug\",\n  [string]$platform = $null,\n  [string] $projects,\n  [string][Alias('v')]$verbosity = \"minimal\",\n  [string] $msbuildEngine = $null,\n  [bool] $warnAsError = $true,\n  [bool] $nodeReuse = $true,\n  [switch] $buildCheck = $false,\n  [switch][Alias('r')]$restore,\n  [switch] $deployDeps,\n  [switch][Alias('b')]$build,\n  [switch] $rebuild,\n  [switch] $deploy,\n  [switch][Alias('t')]$test,\n  [switch] $integrationTest,\n  [switch] $performanceTest,\n  [switch] $sign,\n  [switch] $pack,\n  [switch] $publish,\n  [switch] $clean,\n  [switch][Alias('pb')]$productBuild,\n  [switch]$fromVMR,\n  [switch][Alias('bl')]$binaryLog,\n  [switch][Alias('nobl')]$excludeCIBinarylog,\n  [switch] $ci,\n  [switch] $prepareMachine,\n  [string] $runtimeSourceFeed = '',\n  [string] $runtimeSourceFeedKey = '',\n  [switch] $excludePrereleaseVS,\n  [switch] $nativeToolsOnMachine,\n  [switch] $help,\n  [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties\n)\n\n# Unset 'Platform' environment variable to avoid unwanted collision in InstallDotNetCore.targets file\n# some computer has this env var defined (e.g. Some HP)\nif($env:Platform) {\n  $env:Platform=\"\"  \n}\nfunction Print-Usage() {\n  Write-Host \"Common settings:\"\n  Write-Host \"  -configuration <value>  Build configuration: 'Debug' or 'Release' (short: -c)\"\n  Write-Host \"  -platform <value>       Platform configuration: 'x86', 'x64' or any valid Platform value to pass to msbuild\"\n  Write-Host \"  -verbosity <value>      Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic] (short: -v)\"\n  Write-Host \"  -binaryLog              Output binary log (short: -bl)\"\n  Write-Host \"  -help                   Print help and exit\"\n  Write-Host \"\"\n\n  Write-Host \"Actions:\"\n  Write-Host \"  -restore                Restore dependencies (short: -r)\"\n  Write-Host \"  -build                  Build solution (short: -b)\"\n  Write-Host \"  -rebuild                Rebuild solution\"\n  Write-Host \"  -deploy                 Deploy built VSIXes\"\n  Write-Host \"  -deployDeps             Deploy dependencies (e.g. VSIXes for integration tests)\"\n  Write-Host \"  -test                   Run all unit tests in the solution (short: -t)\"\n  Write-Host \"  -integrationTest        Run all integration tests in the solution\"\n  Write-Host \"  -performanceTest        Run all performance tests in the solution\"\n  Write-Host \"  -pack                   Package build outputs into NuGet packages and Willow components\"\n  Write-Host \"  -sign                   Sign build outputs\"\n  Write-Host \"  -publish                Publish artifacts (e.g. symbols)\"\n  Write-Host \"  -clean                  Clean the solution\"\n  Write-Host \"  -productBuild           Build the solution in the way it will be built in the full .NET product (VMR) build (short: -pb)\"\n  Write-Host \"\"\n\n  Write-Host \"Advanced settings:\"\n  Write-Host \"  -projects <value>       Semi-colon delimited list of sln/proj's to build. Globbing is supported (*.sln)\"\n  Write-Host \"  -ci                     Set when running on CI server\"\n  Write-Host \"  -excludeCIBinarylog     Don't output binary log (short: -nobl)\"\n  Write-Host \"  -prepareMachine         Prepare machine for CI run, clean up processes after build\"\n  Write-Host \"  -warnAsError <value>    Sets warnaserror msbuild parameter ('true' or 'false')\"\n  Write-Host \"  -msbuildEngine <value>  Msbuild engine to use to run build ('dotnet', 'vs', or unspecified).\"\n  Write-Host \"  -excludePrereleaseVS    Set to exclude build engines in prerelease versions of Visual Studio\"\n  Write-Host \"  -nativeToolsOnMachine   Sets the native tools on machine environment variable (indicating that the script should use native tools on machine)\"\n  Write-Host \"  -nodeReuse <value>      Sets nodereuse msbuild parameter ('true' or 'false')\"\n  Write-Host \"  -buildCheck             Sets /check msbuild parameter\"\n  Write-Host \"  -fromVMR                Set when building from within the VMR\"\n  Write-Host \"\"\n\n  Write-Host \"Command line arguments not listed above are passed thru to msbuild.\"\n  Write-Host \"The above arguments can be shortened as much as to be unambiguous (e.g. -co for configuration, -t for test, etc.).\"\n}\n\n. $PSScriptRoot\\tools.ps1\n\nfunction InitializeCustomToolset {\n  if (-not $restore) {\n    return\n  }\n\n  $script = Join-Path $EngRoot 'restore-toolset.ps1'\n\n  if (Test-Path $script) {\n    . $script\n  }\n}\n\nfunction Build {\n  $toolsetBuildProj = InitializeToolset\n  InitializeCustomToolset\n\n  $bl = if ($binaryLog) { '/bl:' + (Join-Path $LogDir 'Build.binlog') } else { '' }\n  $platformArg = if ($platform) { \"/p:Platform=$platform\" } else { '' }\n  $check = if ($buildCheck) { '/check' } else { '' }\n\n  if ($projects) {\n    # Re-assign properties to a new variable because PowerShell doesn't let us append properties directly for unclear reasons.\n    # Explicitly set the type as string[] because otherwise PowerShell would make this char[] if $properties is empty.\n    [string[]] $msbuildArgs = $properties\n    \n    # Resolve relative project paths into full paths \n    $projects = ($projects.Split(';').ForEach({Resolve-Path $_}) -join ';')\n    \n    $msbuildArgs += \"/p:Projects=$projects\"\n    $properties = $msbuildArgs\n  }\n\n  MSBuild $toolsetBuildProj `\n    $bl `\n    $platformArg `\n    $check `\n    /p:Configuration=$configuration `\n    /p:RepoRoot=$RepoRoot `\n    /p:Restore=$restore `\n    /p:DeployDeps=$deployDeps `\n    /p:Build=$build `\n    /p:Rebuild=$rebuild `\n    /p:Deploy=$deploy `\n    /p:Test=$test `\n    /p:Pack=$pack `\n    /p:DotNetBuild=$productBuild `\n    /p:DotNetBuildFromVMR=$fromVMR `\n    /p:IntegrationTest=$integrationTest `\n    /p:PerformanceTest=$performanceTest `\n    /p:Sign=$sign `\n    /p:Publish=$publish `\n    /p:RestoreStaticGraphEnableBinaryLogger=$binaryLog `\n    @properties\n}\n\ntry {\n  if ($clean) {\n    if (Test-Path $ArtifactsDir) {\n      Remove-Item -Recurse -Force $ArtifactsDir\n      Write-Host 'Artifacts directory deleted.'\n    }\n    exit 0\n  }\n\n  if ($help -or (($null -ne $properties) -and ($properties.Contains('/help') -or $properties.Contains('/?')))) {\n    Print-Usage\n    exit 0\n  }\n\n  if ($ci) {\n    if (-not $excludeCIBinarylog) {\n      $binaryLog = $true\n    }\n    $nodeReuse = $false\n  }\n\n  if ($nativeToolsOnMachine) {\n    $env:NativeToolsOnMachine = $true\n  }\n  if ($restore) {\n    InitializeNativeTools\n  }\n\n  Build\n}\ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Category 'InitializeToolset' -Message $_\n  ExitWithExitCode 1\n}\n\nExitWithExitCode 0\n"
  },
  {
    "path": "eng/common/build.sh",
    "content": "#!/usr/bin/env bash\n\n# Stop script if unbound variable found (use ${var:-} if intentional)\nset -u\n\n# Stop script if command returns non-zero exit code.\n# Prevents hidden errors caused by missing error code propagation.\nset -e\n\nusage()\n{\n  echo \"Common settings:\"\n  echo \"  --configuration <value>    Build configuration: 'Debug' or 'Release' (short: -c)\"\n  echo \"  --verbosity <value>        Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic] (short: -v)\"\n  echo \"  --binaryLog                Create MSBuild binary log (short: -bl)\"\n  echo \"  --help                     Print help and exit (short: -h)\"\n  echo \"\"\n\n  echo \"Actions:\"\n  echo \"  --restore                  Restore dependencies (short: -r)\"\n  echo \"  --build                    Build solution (short: -b)\"\n  echo \"  --sourceBuild              Source-build the solution (short: -sb)\"\n  echo \"                             Will additionally trigger the following actions: --restore, --build, --pack\"\n  echo \"                             If --configuration is not set explicitly, will also set it to 'Release'\"\n  echo \"  --productBuild             Build the solution in the way it will be built in the full .NET product (VMR) build (short: -pb)\"\n  echo \"                             Will additionally trigger the following actions: --restore, --build, --pack\"\n  echo \"                             If --configuration is not set explicitly, will also set it to 'Release'\"\n  echo \"  --rebuild                  Rebuild solution\"\n  echo \"  --test                     Run all unit tests in the solution (short: -t)\"\n  echo \"  --integrationTest          Run all integration tests in the solution\"\n  echo \"  --performanceTest          Run all performance tests in the solution\"\n  echo \"  --pack                     Package build outputs into NuGet packages and Willow components\"\n  echo \"  --sign                     Sign build outputs\"\n  echo \"  --publish                  Publish artifacts (e.g. symbols)\"\n  echo \"  --clean                    Clean the solution\"\n  echo \"\"\n\n  echo \"Advanced settings:\"\n  echo \"  --projects <value>       Project or solution file(s) to build\"\n  echo \"  --ci                     Set when running on CI server\"\n  echo \"  --excludeCIBinarylog     Don't output binary log (short: -nobl)\"\n  echo \"  --prepareMachine         Prepare machine for CI run, clean up processes after build\"\n  echo \"  --nodeReuse <value>      Sets nodereuse msbuild parameter ('true' or 'false')\"\n  echo \"  --warnAsError <value>    Sets warnaserror msbuild parameter ('true' or 'false')\"\n  echo \"  --buildCheck <value>     Sets /check msbuild parameter\"\n  echo \"  --fromVMR                Set when building from within the VMR\"\n  echo \"\"\n  echo \"Command line arguments not listed above are passed thru to msbuild.\"\n  echo \"Arguments can also be passed in with a single hyphen.\"\n}\n\nsource=\"${BASH_SOURCE[0]}\"\n\n# resolve $source until the file is no longer a symlink\nwhile [[ -h \"$source\" ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\nrestore=false\nbuild=false\nsource_build=false\nproduct_build=false\nfrom_vmr=false\nrebuild=false\ntest=false\nintegration_test=false\nperformance_test=false\npack=false\npublish=false\nsign=false\npublic=false\nci=false\nclean=false\n\nwarn_as_error=true\nnode_reuse=true\nbuild_check=false\nbinary_log=false\nexclude_ci_binary_log=false\npipelines_log=false\n\nprojects=''\nconfiguration=''\nprepare_machine=false\nverbosity='minimal'\nruntime_source_feed=''\nruntime_source_feed_key=''\n\nproperties=()\nwhile [[ $# -gt 0 ]]; do\n  opt=\"$(echo \"${1/#--/-}\" | tr \"[:upper:]\" \"[:lower:]\")\"\n  case \"$opt\" in\n    -help|-h)\n      usage\n      exit 0\n      ;;\n    -clean)\n      clean=true\n      ;;\n    -configuration|-c)\n      configuration=$2\n      shift\n      ;;\n    -verbosity|-v)\n      verbosity=$2\n      shift\n      ;;\n    -binarylog|-bl)\n      binary_log=true\n      ;;\n    -excludecibinarylog|-nobl)\n      exclude_ci_binary_log=true\n      ;;\n    -pipelineslog|-pl)\n      pipelines_log=true\n      ;;\n    -restore|-r)\n      restore=true\n      ;;\n    -build|-b)\n      build=true\n      ;;\n    -rebuild)\n      rebuild=true\n      ;;\n    -pack)\n      pack=true\n      ;;\n    -sourcebuild|-source-build|-sb)\n      build=true\n      source_build=true\n      product_build=true\n      restore=true\n      pack=true\n      ;;\n    -productbuild|-product-build|-pb)\n      build=true\n      product_build=true\n      restore=true\n      pack=true\n      ;;\n    -fromvmr|-from-vmr)\n      from_vmr=true\n      ;;\n    -test|-t)\n      test=true\n      ;;\n    -integrationtest)\n      integration_test=true\n      ;;\n    -performancetest)\n      performance_test=true\n      ;;\n    -sign)\n      sign=true\n      ;;\n    -publish)\n      publish=true\n      ;;\n    -preparemachine)\n      prepare_machine=true\n      ;;\n    -projects)\n      projects=$2\n      shift\n      ;;\n    -ci)\n      ci=true\n      ;;\n    -warnaserror)\n      warn_as_error=$2\n      shift\n      ;;\n    -nodereuse)\n      node_reuse=$2\n      shift\n      ;;\n    -buildcheck)\n      build_check=true\n      ;;\n    -runtimesourcefeed)\n      runtime_source_feed=$2\n      shift\n      ;;\n     -runtimesourcefeedkey)\n      runtime_source_feed_key=$2\n      shift\n      ;;\n    *)\n      properties+=(\"$1\")\n      ;;\n  esac\n\n  shift\ndone\n\nif [[ -z \"$configuration\" ]]; then\n  if [[ \"$source_build\" = true ]]; then configuration=\"Release\"; else configuration=\"Debug\"; fi\nfi\n\nif [[ \"$ci\" == true ]]; then\n  pipelines_log=true\n  node_reuse=false\n  if [[ \"$exclude_ci_binary_log\" == false ]]; then\n    binary_log=true\n  fi\nfi\n\n. \"$scriptroot/tools.sh\"\n\nfunction InitializeCustomToolset {\n  local script=\"$eng_root/restore-toolset.sh\"\n\n  if [[ -a \"$script\" ]]; then\n    . \"$script\"\n  fi\n}\n\nfunction Build {\n  InitializeToolset\n  InitializeCustomToolset\n\n  if [[ ! -z \"$projects\" ]]; then\n    properties+=(\"/p:Projects=$projects\")\n  fi\n\n  local bl=\"\"\n  if [[ \"$binary_log\" == true ]]; then\n    bl=\"/bl:\\\"$log_dir/Build.binlog\\\"\"\n  fi\n\n  local check=\"\"\n  if [[ \"$build_check\" == true ]]; then\n    check=\"/check\"\n  fi\n\n  MSBuild $_InitializeToolset \\\n    $bl \\\n    $check \\\n    /p:Configuration=$configuration \\\n    /p:RepoRoot=\"$repo_root\" \\\n    /p:Restore=$restore \\\n    /p:Build=$build \\\n    /p:DotNetBuild=$product_build \\\n    /p:DotNetBuildSourceOnly=$source_build \\\n    /p:DotNetBuildFromVMR=$from_vmr \\\n    /p:Rebuild=$rebuild \\\n    /p:Test=$test \\\n    /p:Pack=$pack \\\n    /p:IntegrationTest=$integration_test \\\n    /p:PerformanceTest=$performance_test \\\n    /p:Sign=$sign \\\n    /p:Publish=$publish \\\n    /p:RestoreStaticGraphEnableBinaryLogger=$binary_log \\\n    ${properties[@]+\"${properties[@]}\"}\n\n  ExitWithExitCode 0\n}\n\nif [[ \"$clean\" == true ]]; then\n  if [ -d \"$artifacts_dir\" ]; then\n    rm -rf $artifacts_dir\n    echo \"Artifacts directory deleted.\"\n  fi\n  exit 0\nfi\n\nif [[ \"$restore\" == true ]]; then\n  InitializeNativeTools\nfi\n\nBuild\n"
  },
  {
    "path": "eng/common/cibuild.sh",
    "content": "#!/usr/bin/env bash\n\nsource=\"${BASH_SOURCE[0]}\"\n\n# resolve $SOURCE until the file is no longer a symlink\nwhile [[ -h $source ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n\n  # if $source was a relative symlink, we need to resolve it relative to the path where \n  # the symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\n. \"$scriptroot/build.sh\" --restore --build --test --pack --publish --ci $@\n"
  },
  {
    "path": "eng/common/core-templates/job/job.yml",
    "content": "parameters:\n# Job schema parameters - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job\n  cancelTimeoutInMinutes: ''\n  condition: ''\n  container: ''\n  continueOnError: false\n  dependsOn: ''\n  displayName: ''\n  pool: ''\n  steps: []\n  strategy: ''\n  timeoutInMinutes: ''\n  variables: []\n  workspace: ''\n  templateContext: {}\n\n# Job base template specific parameters\n  # See schema documentation - https://github.com/dotnet/arcade/blob/master/Documentation/AzureDevOps/TemplateSchema.md\n  # publishing defaults\n  artifacts: ''\n  enableMicrobuild: false\n  enablePreviewMicrobuild: false\n  microbuildPluginVersion: 'latest'\n  enableMicrobuildForMacAndLinux: false\n  microbuildUseESRP: true\n  enablePublishBuildArtifacts: false\n  enablePublishBuildAssets: false\n  enablePublishTestResults: false\n  enableBuildRetry: false\n  mergeTestResults: false\n  testRunTitle: ''\n  testResultsFormat: ''\n  name: ''\n  componentGovernanceSteps: []\n  preSteps: []\n  artifactPublishSteps: []\n  runAsPublic: false\n\n# 1es specific parameters\n  is1ESPipeline: ''\n\njobs:\n- job: ${{ parameters.name }}\n\n  ${{ if ne(parameters.cancelTimeoutInMinutes, '') }}:\n    cancelTimeoutInMinutes: ${{ parameters.cancelTimeoutInMinutes }}\n\n  ${{ if ne(parameters.condition, '') }}:\n    condition: ${{ parameters.condition }}\n\n  ${{ if ne(parameters.container, '') }}:\n    container: ${{ parameters.container }}\n\n  ${{ if ne(parameters.continueOnError, '') }}:\n    continueOnError: ${{ parameters.continueOnError }}\n\n  ${{ if ne(parameters.dependsOn, '') }}:\n    dependsOn: ${{ parameters.dependsOn }}\n\n  ${{ if ne(parameters.displayName, '') }}:\n    displayName: ${{ parameters.displayName }}\n\n  ${{ if ne(parameters.pool, '') }}:\n    pool: ${{ parameters.pool }}\n\n  ${{ if ne(parameters.strategy, '') }}:\n    strategy: ${{ parameters.strategy }}\n\n  ${{ if ne(parameters.timeoutInMinutes, '') }}:\n    timeoutInMinutes: ${{ parameters.timeoutInMinutes }}\n\n  ${{ if ne(parameters.templateContext, '') }}:\n    templateContext: ${{ parameters.templateContext }}\n\n  variables:\n  - name: AllowPtrToDetectTestRunRetryFiles\n    value: true\n  - ${{ if ne(parameters.enableTelemetry, 'false') }}:\n    - name: DOTNET_CLI_TELEMETRY_PROFILE\n      value: '$(Build.Repository.Uri)'\n  # Retry signature validation up to three times, waiting 2 seconds between attempts.\n  # See https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu3028#retry-untrusted-root-failures\n  - name: NUGET_EXPERIMENTAL_CHAIN_BUILD_RETRY_POLICY\n    value: 3,2000\n  - ${{ each variable in parameters.variables }}:\n    # handle name-value variable syntax\n    # example:\n    # - name: [key]\n    #   value: [value]\n    - ${{ if ne(variable.name, '') }}:\n      - name: ${{ variable.name }}\n        value: ${{ variable.value }}\n\n    # handle variable groups\n    - ${{ if ne(variable.group, '') }}:\n      - group: ${{ variable.group }}\n\n    # handle template variable syntax\n    # example:\n    # - template: path/to/template.yml\n    #   parameters:\n    #     [key]: [value]\n    - ${{ if ne(variable.template, '') }}:\n      - template: ${{ variable.template }}\n        ${{ if ne(variable.parameters, '') }}:\n          parameters: ${{ variable.parameters }}\n\n    # handle key-value variable syntax.\n    # example:\n    # - [key]: [value]\n    - ${{ if and(eq(variable.name, ''), eq(variable.group, ''), eq(variable.template, '')) }}:\n      - ${{ each pair in variable }}:\n        - name: ${{ pair.key }}\n          value: ${{ pair.value }}\n\n  # DotNet-HelixApi-Access provides 'HelixApiAccessToken' for internal builds\n  - ${{ if and(eq(parameters.enableTelemetry, 'true'), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:\n    - group: DotNet-HelixApi-Access\n\n  ${{ if ne(parameters.workspace, '') }}:\n    workspace: ${{ parameters.workspace }}\n\n  steps:\n  - ${{ if eq(parameters.is1ESPipeline, '') }}:\n    - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error\n\n  - ${{ if ne(parameters.preSteps, '') }}:\n    - ${{ each preStep in parameters.preSteps }}:\n      - ${{ preStep }}\n\n  - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:\n    - template: /eng/common/core-templates/steps/install-microbuild.yml\n      parameters:\n        enableMicrobuild: ${{ parameters.enableMicrobuild }}\n        enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }}\n        microbuildPluginVersion: ${{ parameters.microbuildPluginVersion }}\n        enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }}\n        microbuildUseESRP: ${{ parameters.microbuildUseESRP }}\n        continueOnError: ${{ parameters.continueOnError }}\n\n  - ${{ if and(eq(parameters.runAsPublic, 'false'), eq(variables['System.TeamProject'], 'internal')) }}:\n    - task: NuGetAuthenticate@1\n\n  - ${{ if and(ne(parameters.artifacts.download, 'false'), ne(parameters.artifacts.download, '')) }}:\n    - task: DownloadPipelineArtifact@2\n      inputs:\n        buildType: current\n        artifactName: ${{ coalesce(parameters.artifacts.download.name, 'Artifacts_$(Agent.OS)_$(_BuildConfig)') }}\n        targetPath: ${{ coalesce(parameters.artifacts.download.path, 'artifacts') }}\n        itemPattern: ${{ coalesce(parameters.artifacts.download.pattern, '**') }}\n\n  - ${{ each step in parameters.steps }}:\n    - ${{ step }}\n\n  - ${{ each step in parameters.componentGovernanceSteps }}:\n    - ${{ step }}\n\n  - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:\n    - template: /eng/common/core-templates/steps/cleanup-microbuild.yml\n      parameters:\n        enableMicrobuild: ${{ parameters.enableMicrobuild }}\n        enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }}\n        microbuildPluginVersion: ${{ parameters.microbuildPluginVersion }}\n        enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }}\n        continueOnError: ${{ parameters.continueOnError }}\n\n  # Publish test results\n  - ${{ if or(and(eq(parameters.enablePublishTestResults, 'true'), eq(parameters.testResultsFormat, '')), eq(parameters.testResultsFormat, 'xunit')) }}:\n    - task: PublishTestResults@2\n      displayName: Publish XUnit Test Results\n      inputs:\n        testResultsFormat: 'xUnit'\n        testResultsFiles: '*.xml'\n        searchFolder: '$(System.DefaultWorkingDirectory)/artifacts/TestResults/$(_BuildConfig)'\n        testRunTitle: ${{ coalesce(parameters.testRunTitle, parameters.name, '$(System.JobName)') }}-xunit\n        mergeTestResults: ${{ parameters.mergeTestResults }}\n      continueOnError: true\n      condition: always()\n  - ${{ if or(and(eq(parameters.enablePublishTestResults, 'true'), eq(parameters.testResultsFormat, '')), eq(parameters.testResultsFormat, 'vstest')) }}:\n    - task: PublishTestResults@2\n      displayName: Publish TRX Test Results\n      inputs:\n        testResultsFormat: 'VSTest'\n        testResultsFiles: '*.trx'\n        searchFolder: '$(System.DefaultWorkingDirectory)/artifacts/TestResults/$(_BuildConfig)'\n        testRunTitle: ${{ coalesce(parameters.testRunTitle, parameters.name, '$(System.JobName)') }}-trx\n        mergeTestResults: ${{ parameters.mergeTestResults }}\n      continueOnError: true\n      condition: always()\n\n  # gather artifacts\n  - ${{ if ne(parameters.artifacts.publish, '') }}:\n    - ${{ if and(ne(parameters.artifacts.publish.artifacts, 'false'), ne(parameters.artifacts.publish.artifacts, '')) }}:\n      - task: CopyFiles@2\n        displayName: Gather binaries for publish to artifacts\n        inputs:\n          SourceFolder: 'artifacts/bin'\n          Contents: '**'\n          TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/bin'\n      - task: CopyFiles@2\n        displayName: Gather packages for publish to artifacts\n        inputs:\n          SourceFolder: 'artifacts/packages'\n          Contents: '**'\n          TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/packages'\n    - ${{ if and(ne(parameters.artifacts.publish.logs, 'false'), ne(parameters.artifacts.publish.logs, '')) }}:\n      - task: CopyFiles@2\n        displayName: Gather logs for publish to artifacts\n        inputs:\n          SourceFolder: 'artifacts/log'\n          Contents: '**'\n          TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/log'\n        continueOnError: true\n        condition: always()\n      \n  - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}:\n    - task: CopyFiles@2\n      displayName: Gather logs for publish to artifacts\n      inputs:\n        SourceFolder: 'artifacts/log/$(_BuildConfig)'\n        Contents: '**'\n        TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/log/$(_BuildConfig)'\n      continueOnError: true\n      condition: always()\n  - ${{ if eq(parameters.enableBuildRetry, 'true') }}:\n    - task: CopyFiles@2\n      displayName: Gather buildconfiguration for build retry\n      inputs:\n        SourceFolder: '$(System.DefaultWorkingDirectory)/eng/common/BuildConfiguration'\n        Contents: '**'\n        TargetFolder: '$(Build.ArtifactStagingDirectory)/eng/common/BuildConfiguration'\n      continueOnError: true\n      condition: always()\n  - ${{ each step in parameters.artifactPublishSteps }}:\n    - ${{ step }}\n"
  },
  {
    "path": "eng/common/core-templates/job/onelocbuild.yml",
    "content": "parameters:\n  # Optional: dependencies of the job\n  dependsOn: ''\n\n  # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool\n  pool: ''\n\n  CeapexPat: $(dn-bot-ceapex-package-r) # PAT for the loc AzDO instance https://dev.azure.com/ceapex\n  GithubPat: $(BotAccount-dotnet-bot-repo-PAT)\n\n  SourcesDirectory: $(System.DefaultWorkingDirectory)\n  CreatePr: true\n  AutoCompletePr: false\n  ReusePr: true\n  UseLfLineEndings: true\n  UseCheckedInLocProjectJson: false\n  SkipLocProjectJsonGeneration: false\n  LanguageSet: VS_Main_Languages\n  LclSource: lclFilesInRepo\n  LclPackageId: ''\n  RepoType: gitHub\n  GitHubOrg: dotnet\n  MirrorRepo: ''\n  MirrorBranch: main\n  condition: ''\n  JobNameSuffix: ''\n  is1ESPipeline: ''\njobs:\n- job: OneLocBuild${{ parameters.JobNameSuffix }}\n\n  dependsOn: ${{ parameters.dependsOn }}\n\n  displayName: OneLocBuild${{ parameters.JobNameSuffix }}\n\n  variables:\n    - group: OneLocBuildVariables # Contains the CeapexPat and GithubPat\n    - name: _GenerateLocProjectArguments\n      value: -SourcesDirectory ${{ parameters.SourcesDirectory }}\n        -LanguageSet \"${{ parameters.LanguageSet }}\"\n        -CreateNeutralXlfs\n    - ${{ if eq(parameters.UseCheckedInLocProjectJson, 'true') }}:\n      - name: _GenerateLocProjectArguments\n        value: ${{ variables._GenerateLocProjectArguments }} -UseCheckedInLocProjectJson\n    - template: /eng/common/core-templates/variables/pool-providers.yml\n      parameters:\n        is1ESPipeline: ${{ parameters.is1ESPipeline }}\n\n  ${{ if ne(parameters.pool, '') }}:\n    pool: ${{ parameters.pool }}\n  ${{ if eq(parameters.pool, '') }}:\n    pool:\n      # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com)\n      ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}:\n        name: AzurePipelines-EO\n        image: 1ESPT-Windows2022\n        demands: Cmd\n        os: windows\n      # If it's not devdiv, it's dnceng\n      ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}:\n        name: $(DncEngInternalBuildPool)\n        image: 1es-windows-2022\n        os: windows\n\n  steps:\n    - ${{ if eq(parameters.is1ESPipeline, '') }}:\n      - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error\n\n    - ${{ if ne(parameters.SkipLocProjectJsonGeneration, 'true') }}:\n      - task: Powershell@2\n        inputs:\n          filePath: $(System.DefaultWorkingDirectory)/eng/common/generate-locproject.ps1\n          arguments: $(_GenerateLocProjectArguments)\n        displayName: Generate LocProject.json\n        condition: ${{ parameters.condition }}\n\n    - task: OneLocBuild@2\n      displayName: OneLocBuild\n      env:\n        SYSTEM_ACCESSTOKEN: $(System.AccessToken)\n      inputs:\n        locProj: eng/Localize/LocProject.json\n        outDir: $(Build.ArtifactStagingDirectory)\n        lclSource: ${{ parameters.LclSource }}\n        lclPackageId: ${{ parameters.LclPackageId }}\n        isCreatePrSelected: ${{ parameters.CreatePr }}\n        isAutoCompletePrSelected: ${{ parameters.AutoCompletePr }}\n        ${{ if eq(parameters.CreatePr, true) }}:\n          isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }}\n          isShouldReusePrSelected: ${{ parameters.ReusePr }}\n        packageSourceAuth: patAuth\n        patVariable: ${{ parameters.CeapexPat }}\n        ${{ if eq(parameters.RepoType, 'gitHub') }}:\n          repoType: ${{ parameters.RepoType }}\n          gitHubPatVariable: \"${{ parameters.GithubPat }}\"\n        ${{ if ne(parameters.MirrorRepo, '') }}:\n          isMirrorRepoSelected: true\n          gitHubOrganization: ${{ parameters.GitHubOrg }}\n          mirrorRepo: ${{ parameters.MirrorRepo }}\n          mirrorBranch: ${{ parameters.MirrorBranch }}\n      condition: ${{ parameters.condition }}\n\n    # Copy the locProject.json to the root of the Loc directory, then publish a pipeline artifact\n    - task: CopyFiles@2\n      displayName: Copy LocProject.json\n      inputs:\n        SourceFolder: '$(System.DefaultWorkingDirectory)/eng/Localize/'\n        Contents: 'LocProject.json'\n        TargetFolder: '$(Build.ArtifactStagingDirectory)/loc'\n      condition: ${{ parameters.condition }}\n\n    - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml\n      parameters:\n        is1ESPipeline: ${{ parameters.is1ESPipeline }}\n        args:\n          targetPath: '$(Build.ArtifactStagingDirectory)/loc'\n          artifactName: 'Loc'\n          displayName: 'Publish Localization Files'\n          condition: ${{ parameters.condition }}\n"
  },
  {
    "path": "eng/common/core-templates/job/publish-build-assets.yml",
    "content": "parameters:\n  configuration: 'Debug'\n\n  # Optional: condition for the job to run\n  condition: ''\n\n  # Optional: 'true' if future jobs should run even if this job fails\n  continueOnError: false\n\n  # Optional: dependencies of the job\n  dependsOn: ''\n\n  # Optional: Include PublishBuildArtifacts task\n  enablePublishBuildArtifacts: false\n\n  # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool\n  pool: {}\n\n  # Optional: should run as a public build even in the internal project\n  #           if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects.\n  runAsPublic: false\n\n  # Optional: whether the build's artifacts will be published using release pipelines or direct feed publishing\n  publishAssetsImmediately: false\n\n  artifactsPublishingAdditionalParameters: ''\n\n  signingValidationAdditionalParameters: ''\n\n  is1ESPipeline: ''\n\n  # Optional: 🌤️ or not the build has assets it wants to publish to BAR\n  isAssetlessBuild: false\n\n  # Optional, publishing version\n  publishingVersion: 3\n\n  # Optional: A minimatch pattern for the asset manifests to publish to BAR\n  assetManifestsPattern: '*/manifests/**/*.xml'\n\n  repositoryAlias: self\n\n  officialBuildId: ''\n\njobs:\n- job: Asset_Registry_Publish\n\n  dependsOn: ${{ parameters.dependsOn }}\n  timeoutInMinutes: 150\n\n  ${{ if eq(parameters.publishAssetsImmediately, 'true') }}:\n    displayName: Publish Assets\n  ${{ else }}:\n    displayName: Publish to Build Asset Registry\n\n  variables:\n  - template: /eng/common/core-templates/variables/pool-providers.yml\n    parameters:\n      is1ESPipeline: ${{ parameters.is1ESPipeline }}\n  - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:\n    - group: Publish-Build-Assets\n    - group: AzureDevOps-Artifact-Feeds-Pats\n    - name: runCodesignValidationInjection\n      value: false\n    # unconditional - needed for logs publishing (redactor tool version)\n    - template: /eng/common/core-templates/post-build/common-variables.yml\n  - name: OfficialBuildId\n    ${{ if ne(parameters.officialBuildId, '') }}:\n      value: ${{ parameters.officialBuildId }}\n    ${{ else }}:\n      value: $(Build.BuildNumber)\n\n  pool:\n    # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com)\n    ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}:\n      name: AzurePipelines-EO\n      image: 1ESPT-Windows2022\n      demands: Cmd\n      os: windows\n    # If it's not devdiv, it's dnceng\n    ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}:\n      name: NetCore1ESPool-Publishing-Internal\n      image: windows.vs2022.amd64\n      os: windows\n  steps:\n  - ${{ if eq(parameters.is1ESPipeline, '') }}:\n    - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error\n\n  - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:\n    - checkout: ${{ parameters.repositoryAlias }}\n      fetchDepth: 3\n      clean: true\n\n    - ${{ if eq(parameters.isAssetlessBuild, 'false') }}:\n      - ${{ if eq(parameters.publishingVersion, 3) }}:\n        - task: DownloadPipelineArtifact@2\n          displayName: Download Asset Manifests\n          inputs:\n            artifactName: AssetManifests\n            targetPath: '$(Build.StagingDirectory)/AssetManifests'\n          condition: ${{ parameters.condition }}\n          continueOnError: ${{ parameters.continueOnError }}\n      - ${{ if eq(parameters.publishingVersion, 4) }}:\n        - task: DownloadPipelineArtifact@2\n          displayName: Download V4 asset manifests\n          inputs:\n            itemPattern: '*/manifests/**/*.xml'\n            targetPath: '$(Build.StagingDirectory)/AllAssetManifests'\n          condition: ${{ parameters.condition }}\n          continueOnError: ${{ parameters.continueOnError }}\n        - task: CopyFiles@2\n          displayName: Copy V4 asset manifests to AssetManifests\n          inputs:\n            SourceFolder: '$(Build.StagingDirectory)/AllAssetManifests'\n            Contents: ${{ parameters.assetManifestsPattern }}\n            TargetFolder: '$(Build.StagingDirectory)/AssetManifests'\n            flattenFolders: true\n          condition: ${{ parameters.condition }}\n          continueOnError: ${{ parameters.continueOnError }}\n\n    - task: NuGetAuthenticate@1\n\n    # Populate internal runtime variables.\n    - template: /eng/common/templates/steps/enable-internal-sources.yml\n      ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}:\n        parameters:\n            legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw)\n\n    - template: /eng/common/templates/steps/enable-internal-runtimes.yml\n\n    - task: AzureCLI@2\n      displayName: Publish Build Assets\n      inputs:\n        azureSubscription: \"Darc: Maestro Production\"\n        scriptType: ps\n        scriptLocation: scriptPath\n        scriptPath: $(System.DefaultWorkingDirectory)/eng/common/sdk-task.ps1\n        arguments: -task PublishBuildAssets -restore -msbuildEngine dotnet\n          /p:ManifestsPath='$(Build.StagingDirectory)/AssetManifests'\n          /p:IsAssetlessBuild=${{ parameters.isAssetlessBuild }}\n          /p:MaestroApiEndpoint=https://maestro.dot.net\n          /p:OfficialBuildId=$(OfficialBuildId)\n          -runtimeSourceFeed https://ci.dot.net/internal\n          -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)'\n\n      condition: ${{ parameters.condition }}\n      continueOnError: ${{ parameters.continueOnError }}\n\n    - task: powershell@2\n      displayName: Create ReleaseConfigs Artifact\n      inputs:\n        targetType: inline\n        script: |\n          New-Item -Path \"$(Build.StagingDirectory)/ReleaseConfigs\" -ItemType Directory -Force\n          $filePath = \"$(Build.StagingDirectory)/ReleaseConfigs/ReleaseConfigs.txt\"\n          Add-Content -Path $filePath -Value $(BARBuildId)\n          Add-Content -Path $filePath -Value \"$(DefaultChannels)\"\n          Add-Content -Path $filePath -Value $(IsStableBuild)\n\n          $symbolExclusionfile = \"$(System.DefaultWorkingDirectory)/eng/SymbolPublishingExclusionsFile.txt\"\n          if (Test-Path -Path $symbolExclusionfile)\n          {\n            Write-Host \"SymbolExclusionFile exists\"\n            Copy-Item -Path $symbolExclusionfile -Destination \"$(Build.StagingDirectory)/ReleaseConfigs\"\n          }\n\n    - ${{ if eq(parameters.publishingVersion, 4) }}:\n      - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml\n        parameters:\n          is1ESPipeline: ${{ parameters.is1ESPipeline }}\n          args:\n            targetPath: '$(Build.ArtifactStagingDirectory)/MergedManifest.xml'\n            artifactName: AssetManifests\n            displayName: 'Publish Merged Manifest'\n            retryCountOnTaskFailure: 10 # for any logs being locked\n            sbomEnabled: false # we don't need SBOM for logs\n\n    - template: /eng/common/core-templates/steps/publish-build-artifacts.yml\n      parameters:\n        is1ESPipeline: ${{ parameters.is1ESPipeline }}\n        args:\n          displayName: Publish ReleaseConfigs Artifact\n          pathToPublish: '$(Build.StagingDirectory)/ReleaseConfigs'\n          publishLocation: Container\n          artifactName: ReleaseConfigs\n\n    - ${{ if or(eq(parameters.publishAssetsImmediately, 'true'), eq(parameters.isAssetlessBuild, 'true')) }}:\n      - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml\n        parameters:\n          BARBuildId: ${{ parameters.BARBuildId }}\n          PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }}\n          is1ESPipeline: ${{ parameters.is1ESPipeline }}\n          \n      # Darc is targeting 8.0, so make sure it's installed\n      - task: UseDotNet@2\n        inputs:\n          version: 8.0.x\n\n      - task: AzureCLI@2\n        displayName: Publish Using Darc\n        inputs:\n          azureSubscription: \"Darc: Maestro Production\"\n          scriptType: ps\n          scriptLocation: scriptPath\n          scriptPath: $(System.DefaultWorkingDirectory)/eng/common/post-build/publish-using-darc.ps1\n          arguments: >\n            -BuildId $(BARBuildId)\n            -PublishingInfraVersion 3\n            -AzdoToken '$(System.AccessToken)'\n            -WaitPublishingFinish true\n            -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}'\n            -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}'\n            -SkipAssetsPublishing '${{ parameters.isAssetlessBuild }}'\n            -runtimeSourceFeed https://ci.dot.net/internal\n            -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)'\n\n    - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}:\n      - template: /eng/common/core-templates/steps/publish-logs.yml\n        parameters:\n          is1ESPipeline: ${{ parameters.is1ESPipeline }}\n          JobLabel: 'Publish_Artifacts_Logs'\n"
  },
  {
    "path": "eng/common/core-templates/job/source-build.yml",
    "content": "parameters:\n  # This template adds arcade-powered source-build to CI. The template produces a server job with a\n  # default ID 'Source_Build_Complete' to put in a dependency list if necessary.\n\n  # Specifies the prefix for source-build jobs added to pipeline. Use this if disambiguation needed.\n  jobNamePrefix: 'Source_Build'\n\n  # Defines the platform on which to run the job. By default, a linux-x64 machine, suitable for\n  # managed-only repositories. This is an object with these properties:\n  #\n  # name: ''\n  #   The name of the job. This is included in the job ID.\n  # targetRID: ''\n  #   The name of the target RID to use, instead of the one auto-detected by Arcade.\n  # portableBuild: false\n  #   Enables non-portable mode. This means a more specific RID (e.g. fedora.32-x64 rather than\n  #   linux-x64), and compiling against distro-provided packages rather than portable ones. The\n  #   default is portable mode.\n  # skipPublishValidation: false\n  #   Disables publishing validation.  By default, a check is performed to ensure no packages are\n  #   published by source-build.\n  # container: ''\n  #   A container to use. Runs in docker.\n  # pool: {}\n  #   A pool to use. Runs directly on an agent.\n  # buildScript: ''\n  #   Specifies the build script to invoke to perform the build in the repo. The default\n  #   './build.sh' should work for typical Arcade repositories, but this is customizable for\n  #   difficult situations.\n  # buildArguments: ''\n  #   Specifies additional build arguments to pass to the build script.\n  # jobProperties: {}\n  #   A list of job properties to inject at the top level, for potential extensibility beyond\n  #   container and pool.\n  platform: {}\n\n  is1ESPipeline: ''\n\n  # If set to true and running on a non-public project,\n  # Internal nuget and blob storage locations will be enabled.\n  # This is not enabled by default because many repositories do not need internal sources\n  # and do not need to have the required service connections approved in the pipeline.\n  enableInternalSources: false\n\njobs:\n- job: ${{ parameters.jobNamePrefix }}_${{ parameters.platform.name }}\n  displayName: Source-Build (${{ parameters.platform.name }})\n\n  ${{ each property in parameters.platform.jobProperties }}:\n    ${{ property.key }}: ${{ property.value }}\n\n  ${{ if ne(parameters.platform.container, '') }}:\n    container: ${{ parameters.platform.container }}\n\n  ${{ if eq(parameters.platform.pool, '') }}:\n    # The default VM host AzDO pool. This should be capable of running Docker containers: almost all\n    # source-build builds run in Docker, including the default managed platform.\n    # /eng/common/core-templates/variables/pool-providers.yml can't be used here (some customers declare variables already), so duplicate its logic\n    ${{ if eq(parameters.is1ESPipeline, 'true') }}:\n      pool:\n        ${{ if eq(variables['System.TeamProject'], 'public') }}:\n          name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')]\n          demands: ImageOverride -equals build.azurelinux.3.amd64.open\n        ${{ if eq(variables['System.TeamProject'], 'internal') }}:\n          name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')]\n          image: build.azurelinux.3.amd64\n          os: linux\n    ${{ else }}:\n      pool:\n        ${{ if eq(variables['System.TeamProject'], 'public') }}:\n          name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')]\n          demands: ImageOverride -equals build.azurelinux.3.amd64.open\n        ${{ if eq(variables['System.TeamProject'], 'internal') }}:\n          name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')]\n          demands: ImageOverride -equals build.azurelinux.3.amd64\n  ${{ if ne(parameters.platform.pool, '') }}:\n    pool: ${{ parameters.platform.pool }}\n\n  workspace:\n    clean: all\n\n  steps:\n  - ${{ if eq(parameters.is1ESPipeline, '') }}:\n    - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error\n\n  - ${{ if eq(parameters.enableInternalSources, true) }}:\n    - template: /eng/common/core-templates/steps/enable-internal-sources.yml\n      parameters:\n        is1ESPipeline: ${{ parameters.is1ESPipeline }}\n    - template: /eng/common/core-templates/steps/enable-internal-runtimes.yml\n      parameters:\n        is1ESPipeline: ${{ parameters.is1ESPipeline }}\n  - template: /eng/common/core-templates/steps/source-build.yml\n    parameters:\n      is1ESPipeline: ${{ parameters.is1ESPipeline }}\n      platform: ${{ parameters.platform }}\n"
  },
  {
    "path": "eng/common/core-templates/job/source-index-stage1.yml",
    "content": "parameters:\n  runAsPublic: false\n  sourceIndexBuildCommand: powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command \"eng/common/build.ps1 -restore -build -binarylog -ci\"\n  preSteps: []\n  binlogPath: artifacts/log/Debug/Build.binlog\n  condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')\n  dependsOn: ''\n  pool: ''\n  is1ESPipeline: ''\n\njobs:\n- job: SourceIndexStage1\n  dependsOn: ${{ parameters.dependsOn }}\n  condition: ${{ parameters.condition }}\n  variables:\n  - name: BinlogPath\n    value: ${{ parameters.binlogPath }}\n  - template: /eng/common/core-templates/variables/pool-providers.yml\n    parameters:\n      is1ESPipeline: ${{ parameters.is1ESPipeline }}\n\n  ${{ if ne(parameters.pool, '') }}:\n    pool: ${{ parameters.pool }}\n  ${{ if eq(parameters.pool, '') }}:\n    pool:\n      ${{ if eq(variables['System.TeamProject'], 'public') }}:\n        name: $(DncEngPublicBuildPool)\n        image: windows.vs2026preview.scout.amd64.open\n      ${{ if eq(variables['System.TeamProject'], 'internal') }}:\n        name: $(DncEngInternalBuildPool)\n        image: windows.vs2026preview.scout.amd64\n\n  steps:\n  - ${{ if eq(parameters.is1ESPipeline, '') }}:\n    - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error\n\n  - ${{ each preStep in parameters.preSteps }}:\n    - ${{ preStep }}\n  - script: ${{ parameters.sourceIndexBuildCommand }}\n    displayName: Build Repository\n\n  - template: /eng/common/core-templates/steps/source-index-stage1-publish.yml\n    parameters:\n      binLogPath: ${{ parameters.binLogPath }}\n"
  },
  {
    "path": "eng/common/core-templates/jobs/codeql-build.yml",
    "content": "parameters:\n  # See schema documentation in /Documentation/AzureDevOps/TemplateSchema.md\n  continueOnError: false\n  # Required: A collection of jobs to run - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job\n  jobs: []\n  # Optional: if specified, restore and use this version of Guardian instead of the default.\n  overrideGuardianVersion: ''\n  is1ESPipeline: ''\n\njobs:\n- template: /eng/common/core-templates/jobs/jobs.yml\n  parameters:\n    is1ESPipeline: ${{ parameters.is1ESPipeline }}\n    enableMicrobuild: false\n    enablePublishBuildArtifacts: false\n    enablePublishTestResults: false\n    enablePublishBuildAssets: false\n    enableTelemetry: true\n\n    variables:\n      - group: Publish-Build-Assets\n      # The Guardian version specified in 'eng/common/sdl/packages.config'. This value must be kept in\n      # sync with the packages.config file.\n      - name: DefaultGuardianVersion\n        value: 0.109.0\n      - name: GuardianPackagesConfigFile\n        value: $(System.DefaultWorkingDirectory)\\eng\\common\\sdl\\packages.config\n      - name: GuardianVersion\n        value: ${{ coalesce(parameters.overrideGuardianVersion, '$(DefaultGuardianVersion)') }}\n  \n    jobs: ${{ parameters.jobs }}\n        \n"
  },
  {
    "path": "eng/common/core-templates/jobs/jobs.yml",
    "content": "parameters:\n  # See schema documentation in /Documentation/AzureDevOps/TemplateSchema.md\n  continueOnError: false\n\n  # Optional: Include PublishBuildArtifacts task\n  enablePublishBuildArtifacts: false\n\n  # Optional: Enable running the source-build jobs to build repo from source\n  enableSourceBuild: false\n\n  # Optional: Parameters for source-build template.\n  #           See /eng/common/core-templates/jobs/source-build.yml for options\n  sourceBuildParameters: []\n\n  graphFileGeneration:\n    # Optional: Enable generating the graph files at the end of the build\n    enabled: false\n    # Optional: Include toolset dependencies in the generated graph files\n    includeToolset: false\n    \n  # Required: A collection of jobs to run - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job\n  jobs: []\n\n  # Optional: Override automatically derived dependsOn value for \"publish build assets\" job\n  publishBuildAssetsDependsOn: ''\n\n  # Optional: Publish the assets as soon as the publish to BAR stage is complete, rather doing so in a separate stage.\n  publishAssetsImmediately: false\n\n  # Optional: 🌤️ or not the build has assets it wants to publish to BAR\n  isAssetlessBuild: false\n\n  # Optional: If using publishAssetsImmediately and additional parameters are needed, can be used to send along additional parameters (normally sent to post-build.yml)\n  artifactsPublishingAdditionalParameters: ''\n  signingValidationAdditionalParameters: ''\n\n  # Optional: should run as a public build even in the internal project\n  #           if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects.\n  runAsPublic: false\n\n  enableSourceIndex: false\n  sourceIndexParams: {}\n\n  artifacts: {}\n  is1ESPipeline: ''\n  repositoryAlias: self\n  officialBuildId: ''\n\n# Internal resources (telemetry, microbuild) can only be accessed from non-public projects,\n# and some (Microbuild) should only be applied to non-PR cases for internal builds.\n\njobs:\n- ${{ each job in parameters.jobs }}:\n  - ${{ if eq(parameters.is1ESPipeline, 'true') }}:\n    - template: /eng/common/templates-official/job/job.yml\n      parameters: \n        # pass along parameters\n        ${{ each parameter in parameters }}:\n          ${{ if ne(parameter.key, 'jobs') }}:\n            ${{ parameter.key }}: ${{ parameter.value }}\n\n        # pass along job properties\n        ${{ each property in job }}:\n          ${{ if ne(property.key, 'job') }}:\n            ${{ property.key }}: ${{ property.value }}\n\n        name: ${{ job.job }}\n\n  - ${{ else }}:\n    - template: /eng/common/templates/job/job.yml\n      parameters: \n        # pass along parameters\n        ${{ each parameter in parameters }}:\n          ${{ if ne(parameter.key, 'jobs') }}:\n            ${{ parameter.key }}: ${{ parameter.value }}\n\n        # pass along job properties\n        ${{ each property in job }}:\n          ${{ if ne(property.key, 'job') }}:\n            ${{ property.key }}: ${{ property.value }}\n\n        name: ${{ job.job }}\n\n- ${{ if eq(parameters.enableSourceBuild, true) }}:\n  - template: /eng/common/core-templates/jobs/source-build.yml\n    parameters:\n      is1ESPipeline: ${{ parameters.is1ESPipeline }}\n      ${{ each parameter in parameters.sourceBuildParameters }}:\n        ${{ parameter.key }}: ${{ parameter.value }}\n\n- ${{ if eq(parameters.enableSourceIndex, 'true') }}:\n  - template: ../job/source-index-stage1.yml\n    parameters:\n      is1ESPipeline: ${{ parameters.is1ESPipeline }}\n      runAsPublic: ${{ parameters.runAsPublic }}\n      ${{ each parameter in parameters.sourceIndexParams }}:\n        ${{ parameter.key }}: ${{ parameter.value }}\n\n- ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:\n  - ${{ if or(eq(parameters.enablePublishBuildAssets, true), eq(parameters.artifacts.publish.manifests, 'true'), ne(parameters.artifacts.publish.manifests, ''), eq(parameters.isAssetlessBuild, true)) }}:\n    - template: ../job/publish-build-assets.yml\n      parameters:\n        is1ESPipeline: ${{ parameters.is1ESPipeline }}\n        continueOnError: ${{ parameters.continueOnError }}\n        dependsOn:\n        - ${{ if ne(parameters.publishBuildAssetsDependsOn, '') }}:\n          - ${{ each job in parameters.publishBuildAssetsDependsOn }}:\n            - ${{ job.job }}\n        - ${{ if eq(parameters.publishBuildAssetsDependsOn, '') }}:\n          - ${{ each job in parameters.jobs }}:\n            - ${{ job.job }}\n\n        runAsPublic: ${{ parameters.runAsPublic }}\n        publishAssetsImmediately: ${{ or(parameters.publishAssetsImmediately, parameters.isAssetlessBuild) }}\n        isAssetlessBuild: ${{ parameters.isAssetlessBuild }}\n        enablePublishBuildArtifacts: ${{ parameters.enablePublishBuildArtifacts }}\n        artifactsPublishingAdditionalParameters: ${{ parameters.artifactsPublishingAdditionalParameters }}\n        signingValidationAdditionalParameters: ${{ parameters.signingValidationAdditionalParameters }}\n        repositoryAlias: ${{ parameters.repositoryAlias }}\n        officialBuildId: ${{ parameters.officialBuildId }}\n"
  },
  {
    "path": "eng/common/core-templates/jobs/source-build.yml",
    "content": "parameters:\n  # This template adds arcade-powered source-build to CI. A job is created for each platform, as\n  # well as an optional server job that completes when all platform jobs complete.\n\n  # See /eng/common/core-templates/job/source-build.yml\n  jobNamePrefix: 'Source_Build'\n\n  # This is the default platform provided by Arcade, intended for use by a managed-only repo.\n  defaultManagedPlatform:\n    name: 'Managed'\n    container: 'mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream-10-amd64'\n\n  # Defines the platforms on which to run build jobs. One job is created for each platform, and the\n  # object in this array is sent to the job template as 'platform'. If no platforms are specified,\n  # one job runs on 'defaultManagedPlatform'.\n  platforms: []\n\n  is1ESPipeline: ''\n\n  # If set to true and running on a non-public project,\n  # Internal nuget and blob storage locations will be enabled.\n  # This is not enabled by default because many repositories do not need internal sources\n  # and do not need to have the required service connections approved in the pipeline.\n  enableInternalSources: false\n\njobs:\n\n- ${{ each platform in parameters.platforms }}:\n  - template: /eng/common/core-templates/job/source-build.yml\n    parameters:\n      is1ESPipeline: ${{ parameters.is1ESPipeline }}\n      jobNamePrefix: ${{ parameters.jobNamePrefix }}\n      platform: ${{ platform }}\n      enableInternalSources: ${{ parameters.enableInternalSources }}\n\n- ${{ if eq(length(parameters.platforms), 0) }}:\n  - template: /eng/common/core-templates/job/source-build.yml\n    parameters:\n      is1ESPipeline: ${{ parameters.is1ESPipeline }}\n      jobNamePrefix: ${{ parameters.jobNamePrefix }}\n      platform: ${{ parameters.defaultManagedPlatform }}\n      enableInternalSources: ${{ parameters.enableInternalSources }}\n"
  },
  {
    "path": "eng/common/core-templates/post-build/common-variables.yml",
    "content": "variables:\n  - group: Publish-Build-Assets\n\n  # Whether the build is internal or not\n  - name: IsInternalBuild\n    value: ${{ and(ne(variables['System.TeamProject'], 'public'), contains(variables['Build.SourceBranch'], 'internal')) }}\n\n  # Default Maestro++ API Endpoint and API Version\n  - name: MaestroApiEndPoint\n    value: \"https://maestro.dot.net\"\n  - name: MaestroApiVersion\n    value: \"2020-02-20\"\n\n  - name: SourceLinkCLIVersion\n    value: 3.0.0\n  - name: SymbolToolVersion\n    value: 1.0.1\n  - name: BinlogToolVersion\n    value: 1.0.11\n\n  - name: runCodesignValidationInjection\n    value: false\n"
  },
  {
    "path": "eng/common/core-templates/post-build/post-build.yml",
    "content": "parameters:\n# Which publishing infra should be used. THIS SHOULD MATCH THE VERSION ON THE BUILD MANIFEST.\n# Publishing V1 is no longer supported\n# Publishing V2 is no longer supported\n# Publishing V3 is the default\n- name: publishingInfraVersion\n  displayName: Which version of publishing should be used to promote the build definition?\n  type: number\n  default: 3\n  values:\n  - 3\n\n- name: BARBuildId\n  displayName: BAR Build Id\n  type: number\n  default: 0\n\n- name: PromoteToChannelIds\n  displayName: Channel to promote BARBuildId to\n  type: string\n  default: ''\n\n- name: enableSourceLinkValidation\n  displayName: Enable SourceLink validation\n  type: boolean\n  default: false\n\n- name: enableSigningValidation\n  displayName: Enable signing validation\n  type: boolean\n  default: true\n\n- name: enableSymbolValidation\n  displayName: Enable symbol validation\n  type: boolean\n  default: false\n\n- name: enableNugetValidation\n  displayName: Enable NuGet validation\n  type: boolean\n  default: true\n\n- name: publishInstallersAndChecksums\n  displayName: Publish installers and checksums\n  type: boolean\n  default: true\n\n- name: requireDefaultChannels\n  displayName: Fail the build if there are no default channel(s) registrations for the current build\n  type: boolean\n  default: false\n\n- name: SDLValidationParameters\n  type: object\n  default:\n    enable: false\n    publishGdn: false\n    continueOnError: false\n    params: ''\n    artifactNames: ''\n    downloadArtifacts: true\n\n- name: isAssetlessBuild\n  type: boolean\n  displayName: Is Assetless Build\n  default: false\n\n# These parameters let the user customize the call to sdk-task.ps1 for publishing\n# symbols & general artifacts as well as for signing validation\n- name: symbolPublishingAdditionalParameters\n  displayName: Symbol publishing additional parameters\n  type: string\n  default: ''\n\n- name: artifactsPublishingAdditionalParameters\n  displayName: Artifact publishing additional parameters\n  type: string\n  default: ''\n\n- name: signingValidationAdditionalParameters\n  displayName: Signing validation additional parameters\n  type: string\n  default: ''\n\n# Which stages should finish execution before post-build stages start\n- name: validateDependsOn\n  type: object\n  default:\n  - build\n\n- name: publishDependsOn\n  type: object\n  default:\n  - Validate\n\n# Optional: Call asset publishing rather than running in a separate stage\n- name: publishAssetsImmediately\n  type: boolean\n  default: false\n\n- name: is1ESPipeline\n  type: boolean\n  default: false\n\nstages:\n- ${{ if or(eq( parameters.enableNugetValidation, 'true'), eq(parameters.enableSigningValidation, 'true'), eq(parameters.enableSourceLinkValidation, 'true'), eq(parameters.SDLValidationParameters.enable, 'true')) }}:\n  - stage: Validate\n    dependsOn: ${{ parameters.validateDependsOn }}\n    displayName: Validate Build Assets\n    variables:\n    - template: /eng/common/core-templates/post-build/common-variables.yml\n    - template: /eng/common/core-templates/variables/pool-providers.yml\n      parameters:\n        is1ESPipeline: ${{ parameters.is1ESPipeline }}\n    jobs:\n    - job:\n      displayName: NuGet Validation\n      condition: and(succeededOrFailed(), eq( ${{ parameters.enableNugetValidation }}, 'true'))\n      pool:\n        # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com)\n        ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}:\n          name: AzurePipelines-EO\n          image: 1ESPT-Windows2022\n          demands: Cmd\n          os: windows\n        # If it's not devdiv, it's dnceng\n        ${{ else }}:\n          ${{ if eq(parameters.is1ESPipeline, true) }}:\n            name: $(DncEngInternalBuildPool)\n            image: windows.vs2026preview.scout.amd64\n            os: windows\n          ${{ else }}:\n            name: $(DncEngInternalBuildPool)\n            demands: ImageOverride -equals windows.vs2026preview.scout.amd64\n\n      steps:\n      - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml\n        parameters:\n          BARBuildId: ${{ parameters.BARBuildId }}\n          PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }}\n          is1ESPipeline: ${{ parameters.is1ESPipeline }}\n\n      - task: DownloadBuildArtifacts@0\n        displayName: Download Package Artifacts\n        inputs:\n          buildType: specific\n          buildVersionToDownload: specific\n          project: $(AzDOProjectName)\n          pipeline: $(AzDOPipelineId)\n          buildId: $(AzDOBuildId)\n          artifactName: PackageArtifacts\n          checkDownloadedFiles: true\n\n      - task: PowerShell@2\n        displayName: Validate\n        inputs:\n          filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/nuget-validation.ps1\n          arguments: -PackagesPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/\n\n    - job:\n      displayName: Signing Validation\n      condition: and( eq( ${{ parameters.enableSigningValidation }}, 'true'), ne( variables['PostBuildSign'], 'true'))\n      pool:\n        # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com)\n        ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}:\n          name: AzurePipelines-EO\n          image: 1ESPT-Windows2022\n          demands: Cmd\n          os: windows\n        # If it's not devdiv, it's dnceng\n        ${{ else }}:\n          ${{ if eq(parameters.is1ESPipeline, true) }}:\n            name: $(DncEngInternalBuildPool)\n            image: 1es-windows-2022\n            os: windows\n          ${{ else }}:\n            name: $(DncEngInternalBuildPool)\n            demands: ImageOverride -equals windows.vs2026preview.scout.amd64\n      steps:\n      - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml\n        parameters:\n          BARBuildId: ${{ parameters.BARBuildId }}\n          PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }}\n          is1ESPipeline: ${{ parameters.is1ESPipeline }}\n\n      - task: DownloadBuildArtifacts@0\n        displayName: Download Package Artifacts\n        inputs:\n          buildType: specific\n          buildVersionToDownload: specific\n          project: $(AzDOProjectName)\n          pipeline: $(AzDOPipelineId)\n          buildId: $(AzDOBuildId)\n          artifactName: PackageArtifacts\n          checkDownloadedFiles: true\n\n      # This is necessary whenever we want to publish/restore to an AzDO private feed\n      # Since sdk-task.ps1 tries to restore packages we need to do this authentication here\n      # otherwise it'll complain about accessing a private feed.\n      - task: NuGetAuthenticate@1\n        displayName: 'Authenticate to AzDO Feeds'\n\n      # Signing validation will optionally work with the buildmanifest file which is downloaded from\n      # Azure DevOps above.\n      - task: PowerShell@2\n        displayName: Validate\n        inputs:\n          filePath: eng\\common\\sdk-task.ps1\n          arguments: -task SigningValidation -restore -msbuildEngine vs\n            /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts'\n            /p:SignCheckExclusionsFile='$(System.DefaultWorkingDirectory)/eng/SignCheckExclusionsFile.txt'\n            ${{ parameters.signingValidationAdditionalParameters }}\n\n      - template: /eng/common/core-templates/steps/publish-logs.yml\n        parameters:\n          is1ESPipeline: ${{ parameters.is1ESPipeline }}\n          StageLabel: 'Validation'\n          JobLabel: 'Signing'\n          BinlogToolVersion: $(BinlogToolVersion)\n\n    - job:\n      displayName: SourceLink Validation\n      condition: eq( ${{ parameters.enableSourceLinkValidation }}, 'true')\n      pool:\n        # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com)\n        ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}:\n          name: AzurePipelines-EO\n          image: 1ESPT-Windows2022\n          demands: Cmd\n          os: windows\n        # If it's not devdiv, it's dnceng\n        ${{ else }}:\n          ${{ if eq(parameters.is1ESPipeline, true) }}:\n            name: $(DncEngInternalBuildPool)\n            image: 1es-windows-2022\n            os: windows\n          ${{ else }}:\n            name: $(DncEngInternalBuildPool)\n            demands: ImageOverride -equals windows.vs2026preview.scout.amd64\n      steps:\n      - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml\n        parameters:\n          BARBuildId: ${{ parameters.BARBuildId }}\n          PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }}\n          is1ESPipeline: ${{ parameters.is1ESPipeline }}\n\n      - task: DownloadBuildArtifacts@0\n        displayName: Download Blob Artifacts\n        inputs:\n          buildType: specific\n          buildVersionToDownload: specific\n          project: $(AzDOProjectName)\n          pipeline: $(AzDOPipelineId)\n          buildId: $(AzDOBuildId)\n          artifactName: BlobArtifacts\n          checkDownloadedFiles: true\n\n      - task: PowerShell@2\n        displayName: Validate\n        inputs:\n          filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/sourcelink-validation.ps1\n          arguments: -InputPath $(Build.ArtifactStagingDirectory)/BlobArtifacts/ \n            -ExtractPath $(Agent.BuildDirectory)/Extract/ \n            -GHRepoName $(Build.Repository.Name) \n            -GHCommit $(Build.SourceVersion)\n            -SourcelinkCliVersion $(SourceLinkCLIVersion)\n        continueOnError: true\n\n- ${{ if ne(parameters.publishAssetsImmediately, 'true') }}:\n  - stage: publish_using_darc\n    ${{ if or(eq(parameters.enableNugetValidation, 'true'), eq(parameters.enableSigningValidation, 'true'), eq(parameters.enableSourceLinkValidation, 'true'), eq(parameters.SDLValidationParameters.enable, 'true')) }}:\n      dependsOn: ${{ parameters.publishDependsOn }}\n    ${{ else }}:\n      dependsOn: ${{ parameters.validateDependsOn }}\n    displayName: Publish using Darc\n    variables:\n    - template: /eng/common/core-templates/post-build/common-variables.yml\n    - template: /eng/common/core-templates/variables/pool-providers.yml\n      parameters:\n        is1ESPipeline: ${{ parameters.is1ESPipeline }}\n    jobs:\n    - job:\n      displayName: Publish Using Darc\n      timeoutInMinutes: 120\n      pool:\n        # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com)\n        ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}:\n          name: AzurePipelines-EO\n          image: 1ESPT-Windows2022\n          demands: Cmd\n          os: windows\n        # If it's not devdiv, it's dnceng\n        ${{ else }}:\n          ${{ if eq(parameters.is1ESPipeline, true) }}:\n            name: NetCore1ESPool-Publishing-Internal\n            image: windows.vs2022.amd64\n            os: windows\n          ${{ else }}:\n            name: NetCore1ESPool-Publishing-Internal\n            demands: ImageOverride -equals windows.vs2022.amd64\n      steps:\n      - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml\n        parameters:\n          BARBuildId: ${{ parameters.BARBuildId }}\n          PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }}\n          is1ESPipeline: ${{ parameters.is1ESPipeline }}\n\n      - task: NuGetAuthenticate@1\n\n      # Populate internal runtime variables.\n      - template: /eng/common/templates/steps/enable-internal-sources.yml\n        parameters:\n          legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw)\n\n      - template: /eng/common/templates/steps/enable-internal-runtimes.yml\n\n      - task: UseDotNet@2\n        inputs:\n          version: 8.0.x\n\n      - task: AzureCLI@2\n        displayName: Publish Using Darc\n        inputs:\n          azureSubscription: \"Darc: Maestro Production\"\n          scriptType: ps\n          scriptLocation: scriptPath\n          scriptPath: $(System.DefaultWorkingDirectory)/eng/common/post-build/publish-using-darc.ps1\n          arguments: >\n              -BuildId $(BARBuildId)\n              -PublishingInfraVersion ${{ parameters.publishingInfraVersion }}\n              -AzdoToken '$(System.AccessToken)'\n              -WaitPublishingFinish true\n              -RequireDefaultChannels ${{ parameters.requireDefaultChannels }}\n              -ArtifactsPublishingAdditionalParameters '${{ parameters.artifactsPublishingAdditionalParameters }}'\n              -SymbolPublishingAdditionalParameters '${{ parameters.symbolPublishingAdditionalParameters }}'\n              -SkipAssetsPublishing '${{ parameters.isAssetlessBuild }}'\n              -runtimeSourceFeed https://ci.dot.net/internal \n              -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)'\n"
  },
  {
    "path": "eng/common/core-templates/post-build/setup-maestro-vars.yml",
    "content": "parameters:\n  BARBuildId: ''\n  PromoteToChannelIds: ''\n  is1ESPipeline: ''\n\nsteps:\n  - ${{ if eq(parameters.is1ESPipeline, '') }}:\n    - 'Illegal entry point, is1ESPipeline is not defined. Repository yaml should not directly reference templates in core-templates folder.': error\n\n  - ${{ if eq(coalesce(parameters.PromoteToChannelIds, 0), 0) }}:\n    - task: DownloadBuildArtifacts@0\n      displayName: Download Release Configs\n      inputs:\n        buildType: current\n        artifactName: ReleaseConfigs\n        checkDownloadedFiles: true\n\n  - task: AzureCLI@2\n    name: setReleaseVars\n    displayName: Set Release Configs Vars\n    inputs:\n      azureSubscription: \"Darc: Maestro Production\"\n      scriptType: pscore\n      scriptLocation: inlineScript\n      inlineScript: |\n        try {\n          if (!$Env:PromoteToMaestroChannels -or $Env:PromoteToMaestroChannels.Trim() -eq '') {\n            $Content = Get-Content $(Build.StagingDirectory)/ReleaseConfigs/ReleaseConfigs.txt\n\n            $BarId = $Content | Select -Index 0\n            $Channels = $Content | Select -Index 1\n            $IsStableBuild = $Content | Select -Index 2\n\n            $AzureDevOpsProject = $Env:System_TeamProject\n            $AzureDevOpsBuildDefinitionId = $Env:System_DefinitionId\n            $AzureDevOpsBuildId = $Env:Build_BuildId\n          }\n          else {\n            . $(System.DefaultWorkingDirectory)\\eng\\common\\tools.ps1\n            $darc = Get-Darc\n            $buildInfo = & $darc get-build `\n              --id ${{ parameters.BARBuildId }} `\n              --extended `\n              --output-format json `\n              --ci `\n              | convertFrom-Json\n\n            $BarId = ${{ parameters.BARBuildId }}\n            $Channels = $Env:PromoteToMaestroChannels -split \",\"\n            $Channels = $Channels -join \"][\"\n            $Channels = \"[$Channels]\"\n\n            $IsStableBuild = $buildInfo.stable\n            $AzureDevOpsProject = $buildInfo.azureDevOpsProject\n            $AzureDevOpsBuildDefinitionId = $buildInfo.azureDevOpsBuildDefinitionId\n            $AzureDevOpsBuildId = $buildInfo.azureDevOpsBuildId\n          }\n\n          Write-Host \"##vso[task.setvariable variable=BARBuildId]$BarId\"\n          Write-Host \"##vso[task.setvariable variable=TargetChannels]$Channels\"\n          Write-Host \"##vso[task.setvariable variable=IsStableBuild]$IsStableBuild\"\n\n          Write-Host \"##vso[task.setvariable variable=AzDOProjectName]$AzureDevOpsProject\"\n          Write-Host \"##vso[task.setvariable variable=AzDOPipelineId]$AzureDevOpsBuildDefinitionId\"\n          Write-Host \"##vso[task.setvariable variable=AzDOBuildId]$AzureDevOpsBuildId\"\n        }\n        catch {\n          Write-Host $_\n          Write-Host $_.Exception\n          Write-Host $_.ScriptStackTrace\n          exit 1\n        }\n    env:\n      PromoteToMaestroChannels: ${{ parameters.PromoteToChannelIds }}\n"
  },
  {
    "path": "eng/common/core-templates/steps/cleanup-microbuild.yml",
    "content": "parameters:\n  # Enable cleanup tasks for MicroBuild\n  enableMicrobuild: false\n  # Enable cleanup tasks for MicroBuild on Mac and Linux\n  # Will be ignored if 'enableMicrobuild' is false or 'Agent.Os' is 'Windows_NT'\n  enableMicrobuildForMacAndLinux: false\n  continueOnError: false\n\nsteps:\n  - ${{ if eq(parameters.enableMicrobuild, 'true') }}:\n    - task: MicroBuildCleanup@1\n      displayName: Execute Microbuild cleanup tasks\n      condition: and(\n        always(),\n        or(\n          and(\n            eq(variables['Agent.Os'], 'Windows_NT'),\n            in(variables['_SignType'], 'real', 'test')\n          ),\n          and(\n            ${{ eq(parameters.enableMicrobuildForMacAndLinux, true) }},\n            ne(variables['Agent.Os'], 'Windows_NT'),\n            eq(variables['_SignType'], 'real')\n          )\n        ))\n      continueOnError: ${{ parameters.continueOnError }}\n      env:\n        TeamName: $(_TeamName)\n"
  },
  {
    "path": "eng/common/core-templates/steps/component-governance.yml",
    "content": "parameters:\n  disableComponentGovernance: false\n  componentGovernanceIgnoreDirectories: ''\n  is1ESPipeline: false\n  displayName: 'Component Detection'\n\nsteps:\n- ${{ if eq(parameters.disableComponentGovernance, 'true') }}:\n  - script: echo \"##vso[task.setvariable variable=skipComponentGovernanceDetection]true\"\n    displayName: Set skipComponentGovernanceDetection variable\n- ${{ if ne(parameters.disableComponentGovernance, 'true') }}:\n  - task: ComponentGovernanceComponentDetection@0\n    continueOnError: true\n    displayName: ${{ parameters.displayName }}\n    inputs:\n      ignoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }}\n"
  },
  {
    "path": "eng/common/core-templates/steps/enable-internal-runtimes.yml",
    "content": "# Obtains internal runtime download credentials and populates the 'dotnetbuilds-internal-container-read-token-base64'\n# variable with the base64-encoded SAS token, by default\n\nparameters:\n- name: federatedServiceConnection\n  type: string\n  default: 'dotnetbuilds-internal-read'\n- name: outputVariableName\n  type: string\n  default: 'dotnetbuilds-internal-container-read-token-base64'\n- name: expiryInHours\n  type: number\n  default: 1\n- name: base64Encode\n  type: boolean\n  default: true\n- name: is1ESPipeline\n  type: boolean\n  default: false\n\nsteps:\n- ${{ if ne(variables['System.TeamProject'], 'public') }}:\n  - template: /eng/common/core-templates/steps/get-delegation-sas.yml\n    parameters:\n      federatedServiceConnection: ${{ parameters.federatedServiceConnection }}\n      outputVariableName: ${{ parameters.outputVariableName }}\n      expiryInHours: ${{ parameters.expiryInHours }}\n      base64Encode: ${{ parameters.base64Encode }}\n      storageAccount: dotnetbuilds\n      container: internal\n      permissions: rl\n      is1ESPipeline: ${{ parameters.is1ESPipeline }}"
  },
  {
    "path": "eng/common/core-templates/steps/enable-internal-sources.yml",
    "content": "parameters:\n# This is the Azure federated service connection that we log into to get an access token.\n- name: nugetFederatedServiceConnection\n  type: string\n  default: 'dnceng-artifacts-feeds-read'\n- name: is1ESPipeline\n  type: boolean\n  default: false\n# Legacy parameters to allow for PAT usage\n- name: legacyCredential\n  type: string\n  default: ''\n\nsteps:\n- ${{ if ne(variables['System.TeamProject'], 'public') }}:\n  - ${{ if ne(parameters.legacyCredential, '') }}:\n    - task: PowerShell@2\n      displayName: Setup Internal Feeds\n      inputs:\n        filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.ps1\n        arguments: -ConfigFile $(System.DefaultWorkingDirectory)/NuGet.config -Password $Env:Token\n      env:\n        Token: ${{ parameters.legacyCredential }}\n  # If running on dnceng (internal project), just use the default behavior for NuGetAuthenticate.\n  # If running on DevDiv, NuGetAuthenticate is not really an option. It's scoped to a single feed, and we have many feeds that\n  # may be added. Instead, we'll use the traditional approach (add cred to nuget.config), but use an account token.\n  - ${{ else }}:\n    - ${{ if eq(variables['System.TeamProject'], 'internal') }}:\n      - task: PowerShell@2\n        displayName: Setup Internal Feeds\n        inputs:\n          filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.ps1\n          arguments: -ConfigFile $(System.DefaultWorkingDirectory)/NuGet.config\n    - ${{ else }}:\n      - template: /eng/common/templates/steps/get-federated-access-token.yml\n        parameters:\n          federatedServiceConnection: ${{ parameters.nugetFederatedServiceConnection }}\n          outputVariableName: 'dnceng-artifacts-feeds-read-access-token'\n      - task: PowerShell@2\n        displayName: Setup Internal Feeds\n        inputs:\n          filePath: $(System.DefaultWorkingDirectory)/eng/common/SetupNugetSources.ps1\n          arguments: -ConfigFile $(System.DefaultWorkingDirectory)/NuGet.config -Password $(dnceng-artifacts-feeds-read-access-token)\n  # This is required in certain scenarios to install the ADO credential provider.\n  # It installed by default in some msbuild invocations (e.g. VS msbuild), but needs to be installed for others\n  # (e.g. dotnet msbuild).\n  - task: NuGetAuthenticate@1\n"
  },
  {
    "path": "eng/common/core-templates/steps/generate-sbom.yml",
    "content": "# BuildDropPath - The root folder of the drop directory for which the manifest file will be generated.\n# PackageName - The name of the package this SBOM represents.\n# PackageVersion - The version of the package this SBOM represents. \n# ManifestDirPath - The path of the directory where the generated manifest files will be placed\n# IgnoreDirectories - Directories to ignore for SBOM generation. This will be passed through to the CG component detector.\n\nparameters:\n  PackageVersion: 11.0.0\n  BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts'\n  PackageName: '.NET'\n  ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom\n  IgnoreDirectories: ''\n  sbomContinueOnError: true\n  is1ESPipeline: false\n  # disable publishArtifacts if some other step is publishing the artifacts (like job.yml).\n  publishArtifacts: true\n\nsteps:\n- task: PowerShell@2 \n  displayName: Prep for SBOM generation in (Non-linux)\n  condition: or(eq(variables['Agent.Os'], 'Windows_NT'), eq(variables['Agent.Os'], 'Darwin'))\n  inputs: \n    filePath: ./eng/common/generate-sbom-prep.ps1\n    arguments: ${{parameters.manifestDirPath}}\n\n# Chmodding is a workaround for https://github.com/dotnet/arcade/issues/8461\n- script: |\n    chmod +x ./eng/common/generate-sbom-prep.sh\n    ./eng/common/generate-sbom-prep.sh ${{parameters.manifestDirPath}}\n  displayName: Prep for SBOM generation in (Linux)\n  condition: eq(variables['Agent.Os'], 'Linux')\n  continueOnError: ${{ parameters.sbomContinueOnError }}\n\n- task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0\n  displayName: 'Generate SBOM manifest'\n  continueOnError: ${{ parameters.sbomContinueOnError }}\n  inputs:\n      PackageName: ${{ parameters.packageName }}\n      BuildDropPath: ${{ parameters.buildDropPath }}\n      PackageVersion: ${{ parameters.packageVersion }}\n      ManifestDirPath: ${{ parameters.manifestDirPath }}/$(ARTIFACT_NAME)\n      ${{ if ne(parameters.IgnoreDirectories, '') }}:\n        AdditionalComponentDetectorArgs: '--IgnoreDirectories ${{ parameters.IgnoreDirectories }}'\n\n- ${{ if eq(parameters.publishArtifacts, 'true')}}:\n  - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml\n    parameters:\n      is1ESPipeline: ${{ parameters.is1ESPipeline }}\n      args:\n        displayName: Publish SBOM manifest\n        continueOnError: ${{parameters.sbomContinueOnError}}\n        targetPath: '${{ parameters.manifestDirPath }}'\n        artifactName: $(ARTIFACT_NAME)\n\n"
  },
  {
    "path": "eng/common/core-templates/steps/get-delegation-sas.yml",
    "content": "parameters:\n- name: federatedServiceConnection\n  type: string\n- name: outputVariableName\n  type: string\n- name: expiryInHours\n  type: number\n  default: 1\n- name: base64Encode\n  type: boolean\n  default: false\n- name: storageAccount\n  type: string\n- name: container\n  type: string\n- name: permissions\n  type: string\n  default: 'rl'\n- name: is1ESPipeline\n  type: boolean\n  default: false\n\nsteps:\n- task: AzureCLI@2\n  displayName: 'Generate delegation SAS Token for ${{ parameters.storageAccount }}/${{ parameters.container }}'\n  inputs:\n    azureSubscription: ${{ parameters.federatedServiceConnection }}\n    scriptType: 'pscore'\n    scriptLocation: 'inlineScript'\n    inlineScript: |\n      # Calculate the expiration of the SAS token and convert to UTC\n      $expiry = (Get-Date).AddHours(${{ parameters.expiryInHours }}).ToUniversalTime().ToString(\"yyyy-MM-ddTHH:mm:ssZ\")\n\n      $sas = az storage container generate-sas --account-name ${{ parameters.storageAccount }} --name ${{ parameters.container }} --permissions ${{ parameters.permissions }} --expiry $expiry --auth-mode login --as-user -o tsv\n\n      if ($LASTEXITCODE -ne 0) {\n        Write-Error \"Failed to generate SAS token.\"\n        exit 1\n      }\n\n      if ('${{ parameters.base64Encode }}' -eq 'true') {\n        $sas = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($sas))\n      }\n\n      Write-Host \"Setting '${{ parameters.outputVariableName }}' with the access token value\"\n      Write-Host \"##vso[task.setvariable variable=${{ parameters.outputVariableName }};issecret=true]$sas\"\n"
  },
  {
    "path": "eng/common/core-templates/steps/get-federated-access-token.yml",
    "content": "parameters:\n- name: federatedServiceConnection\n  type: string\n- name: outputVariableName\n  type: string\n- name: is1ESPipeline\n  type: boolean\n- name: stepName\n  type: string\n  default: 'getFederatedAccessToken'\n- name: condition\n  type: string\n  default: ''\n# Resource to get a token for. Common values include:\n# - '499b84ac-1321-427f-aa17-267ca6975798' for Azure DevOps\n# - 'https://storage.azure.com/' for storage\n# Defaults to Azure DevOps\n- name: resource\n  type: string\n  default: '499b84ac-1321-427f-aa17-267ca6975798'\n- name: isStepOutputVariable\n  type: boolean\n  default: false\n\nsteps:\n- task: AzureCLI@2\n  displayName: 'Getting federated access token for feeds'\n  name: ${{ parameters.stepName }}\n  ${{ if ne(parameters.condition, '') }}:\n    condition: ${{ parameters.condition }}\n  inputs:\n    azureSubscription: ${{ parameters.federatedServiceConnection }}\n    scriptType: 'pscore'\n    scriptLocation: 'inlineScript'\n    inlineScript: |\n      $accessToken = az account get-access-token --query accessToken --resource ${{ parameters.resource }} --output tsv\n      if ($LASTEXITCODE -ne 0) {\n        Write-Error \"Failed to get access token for resource '${{ parameters.resource }}'\"\n        exit 1\n      }\n      Write-Host \"Setting '${{ parameters.outputVariableName }}' with the access token value\"\n      Write-Host \"##vso[task.setvariable variable=${{ parameters.outputVariableName }};issecret=true;isOutput=${{ parameters.isStepOutputVariable }}]$accessToken\""
  },
  {
    "path": "eng/common/core-templates/steps/install-microbuild-impl.yml",
    "content": "parameters:\n  - name: microbuildTaskInputs\n    type: object\n    default: {}\n\n  - name: microbuildEnv\n    type: object\n    default: {}\n\n  - name: enablePreviewMicrobuild\n    type: boolean\n    default: false\n\n  - name: condition\n    type: string\n\n  - name: continueOnError\n    type: boolean\n\nsteps:\n- ${{ if eq(parameters.enablePreviewMicrobuild, true) }}:\n    - task: MicroBuildSigningPluginPreview@4\n      displayName: Install Preview MicroBuild plugin\n      inputs: ${{ parameters.microbuildTaskInputs }}\n      env: ${{ parameters.microbuildEnv }}\n      continueOnError: ${{ parameters.continueOnError }}\n      condition: ${{ parameters.condition }}\n- ${{ else }}:\n  - task: MicroBuildSigningPlugin@4\n    displayName: Install MicroBuild plugin\n    inputs: ${{ parameters.microbuildTaskInputs }}\n    env: ${{ parameters.microbuildEnv }}\n    continueOnError: ${{ parameters.continueOnError }}\n    condition: ${{ parameters.condition }}\n"
  },
  {
    "path": "eng/common/core-templates/steps/install-microbuild.yml",
    "content": "parameters:\n  # Enable install tasks for MicroBuild\n  enableMicrobuild: false\n  # Enable install tasks for MicroBuild on Mac and Linux\n  # Will be ignored if 'enableMicrobuild' is false or 'Agent.Os' is 'Windows_NT'\n  enableMicrobuildForMacAndLinux: false\n  # Enable preview version of MB signing plugin\n  enablePreviewMicrobuild: false\n  # Determines whether the ESRP service connection information should be passed to the signing plugin.\n  # This overlaps with _SignType to some degree. We only need the service connection for real signing.\n  # It's important that the service connection not be passed to the MicroBuildSigningPlugin task in this place.\n  # Doing so will cause the service connection to be authorized for the pipeline, which isn't allowed and won't work for non-prod.\n  # Unfortunately, _SignType can't be used to exclude the use of the service connection in non-real sign scenarios. The\n  # variable is not available in template expression. _SignType has a very large proliferation across .NET, so replacing it is tough.\n  microbuildUseESRP: true\n  # Microbuild installation directory\n  microBuildOutputFolder: $(Agent.TempDirectory)/MicroBuild\n  # Microbuild version\n  microbuildPluginVersion: 'latest'\n\n  continueOnError: false\n\nsteps:\n  - ${{ if eq(parameters.enableMicrobuild, 'true') }}:\n    - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, 'true') }}:\n      # Needed to download the MicroBuild plugin nupkgs on Mac and Linux when nuget.exe is unavailable\n      - task: UseDotNet@2\n        displayName: Install .NET 8.0 SDK for MicroBuild Plugin\n        inputs:\n          packageType: sdk\n          version: 8.0.x\n          installationPath: ${{ parameters.microBuildOutputFolder }}/.dotnet-microbuild\n        condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'))\n\n      - script: |\n          set -euo pipefail\n\n          # UseDotNet@2 prepends the dotnet executable path to the PATH variable, so we can call dotnet directly\n          version=$(dotnet --version)\n          cat << 'EOF' > ${{ parameters.microBuildOutputFolder }}/global.json\n          {\n            \"sdk\": {\n              \"version\": \"$version\",\n              \"paths\": [\n                \"${{ parameters.microBuildOutputFolder }}/.dotnet-microbuild\"\n              ],\n              \"errorMessage\": \"The .NET SDK version $version is required to install the MicroBuild signing plugin.\"\n            }\n          }\n          EOF\n        displayName: 'Add global.json to MicroBuild Installation path'\n        workingDirectory: ${{ parameters.microBuildOutputFolder }}\n        condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'))\n\n    - script: |\n        REM Check if ESRP is disabled while SignType is real\n        if /I \"${{ parameters.microbuildUseESRP }}\"==\"false\" if /I \"$(_SignType)\"==\"real\" (\n          echo Error: ESRP must be enabled when SignType is real.\n          exit /b 1\n        )\n      displayName: 'Validate ESRP usage (Windows)'\n      condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT'))\n    - script: |\n        # Check if ESRP is disabled while SignType is real\n        if [ \"${{ parameters.microbuildUseESRP }}\" = \"false\" ] && [ \"$(_SignType)\" = \"real\" ]; then\n          echo \"Error: ESRP must be enabled when SignType is real.\"\n          exit 1\n        fi\n      displayName: 'Validate ESRP usage (Non-Windows)'\n      condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'))\n\n    # Two different MB install steps. This is due to not being able to use the agent OS during\n    # YAML expansion, and Windows vs. Linux/Mac uses different service connections. However,\n    # we can avoid including the MB install step if not enabled at all. This avoids a bunch of\n    # extra pipeline authorizations, since most pipelines do not sign on non-Windows.\n    - template: /eng/common/core-templates/steps/install-microbuild-impl.yml@self\n      parameters:\n        enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }}\n        microbuildTaskInputs:\n          signType: $(_SignType)\n          zipSources: false\n          feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json\n          version: ${{ parameters.microbuildPluginVersion }}\n          ${{ if eq(parameters.microbuildUseESRP, true) }}:\n            ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)'\n            ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}:\n              ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea\n            ${{ else }}:\n              ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca\n        microbuildEnv:\n          TeamName: $(_TeamName)\n          MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }}\n          SYSTEM_ACCESSTOKEN: $(System.AccessToken)\n        continueOnError: ${{ parameters.continueOnError }}\n        condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT'), in(variables['_SignType'], 'real', 'test'))\n\n    - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, true) }}:\n      - template: /eng/common/core-templates/steps/install-microbuild-impl.yml@self\n        parameters:\n          enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }}\n          microbuildTaskInputs:\n            signType: $(_SignType)\n            zipSources: false\n            feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json\n            version: ${{ parameters.microbuildPluginVersion }}\n            workingDirectory: ${{ parameters.microBuildOutputFolder }}\n            ${{ if eq(parameters.microbuildUseESRP, true) }}:\n              ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)'\n              ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}:\n                ConnectedPMEServiceName: beb8cb23-b303-4c95-ab26-9e44bc958d39\n              ${{ else }}:\n                ConnectedPMEServiceName: c24de2a5-cc7a-493d-95e4-8e5ff5cad2bc\n          microbuildEnv:\n            TeamName: $(_TeamName)\n            MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }}\n            SYSTEM_ACCESSTOKEN: $(System.AccessToken)\n          continueOnError: ${{ parameters.continueOnError }}\n          condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'),  eq(variables['_SignType'], 'real'))\n"
  },
  {
    "path": "eng/common/core-templates/steps/publish-build-artifacts.yml",
    "content": "parameters:\n- name: is1ESPipeline\n  type: boolean\n  default: false\n- name: args\n  type: object\n  default: {}\nsteps:\n- ${{ if ne(parameters.is1ESPipeline, true) }}:\n  - template: /eng/common/templates/steps/publish-build-artifacts.yml\n    parameters:\n      is1ESPipeline: ${{ parameters.is1ESPipeline }}\n      ${{ each parameter in parameters.args }}:\n        ${{ parameter.key }}: ${{ parameter.value }}\n- ${{ else }}:\n  - template: /eng/common/templates-official/steps/publish-build-artifacts.yml\n    parameters:\n      is1ESPipeline: ${{ parameters.is1ESPipeline }}\n      ${{ each parameter in parameters.args }}:\n        ${{ parameter.key }}: ${{ parameter.value }}"
  },
  {
    "path": "eng/common/core-templates/steps/publish-logs.yml",
    "content": "parameters:\n  StageLabel: ''\n  JobLabel: ''\n  CustomSensitiveDataList: ''\n  # A default - in case value from eng/common/core-templates/post-build/common-variables.yml is not passed\n  BinlogToolVersion: '1.0.11'\n  is1ESPipeline: false\n\nsteps:\n- task: Powershell@2\n  displayName: Prepare Binlogs to Upload\n  inputs:\n    targetType: inline\n    script: |\n      New-Item -ItemType Directory $(System.DefaultWorkingDirectory)/PostBuildLogs/${{parameters.StageLabel}}/${{parameters.JobLabel}}/\n      Move-Item -Path $(System.DefaultWorkingDirectory)/artifacts/log/Debug/* $(System.DefaultWorkingDirectory)/PostBuildLogs/${{parameters.StageLabel}}/${{parameters.JobLabel}}/\n  continueOnError: true\n  condition: always()\n    \n- task: PowerShell@2\n  displayName: Redact Logs\n  inputs:\n    filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/redact-logs.ps1\n    # For now this needs to have explicit list of all sensitive data. Taken from eng/publishing/v3/publish.yml\n    # Sensitive data can as well be added to $(System.DefaultWorkingDirectory)/eng/BinlogSecretsRedactionFile.txt'\n    #  If the file exists - sensitive data for redaction will be sourced from it\n    #  (single entry per line, lines starting with '# ' are considered comments and skipped)\n    arguments: -InputPath '$(System.DefaultWorkingDirectory)/PostBuildLogs' \n      -BinlogToolVersion '${{parameters.BinlogToolVersion}}'\n      -TokensFilePath '$(System.DefaultWorkingDirectory)/eng/BinlogSecretsRedactionFile.txt'\n      -runtimeSourceFeed https://ci.dot.net/internal \n      -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)'\n      '$(publishing-dnceng-devdiv-code-r-build-re)'\n      '$(MaestroAccessToken)'\n      '$(dn-bot-all-orgs-artifact-feeds-rw)'\n      '$(akams-client-id)'\n      '$(microsoft-symbol-server-pat)'\n      '$(symweb-symbol-server-pat)'\n      '$(dnceng-symbol-server-pat)'\n      '$(dn-bot-all-orgs-build-rw-code-rw)'\n      '$(System.AccessToken)'\n      ${{parameters.CustomSensitiveDataList}}\n  continueOnError: true\n  condition: always()\n\n- task: CopyFiles@2\n  displayName: Gather post build logs\n  inputs:\n    SourceFolder: '$(System.DefaultWorkingDirectory)/PostBuildLogs'\n    Contents: '**'\n    TargetFolder: '$(Build.ArtifactStagingDirectory)/PostBuildLogs'\n  condition: always()\n\n- template: /eng/common/core-templates/steps/publish-build-artifacts.yml\n  parameters:\n    is1ESPipeline: ${{ parameters.is1ESPipeline }}\n    args:\n      displayName: Publish Logs\n      pathToPublish: '$(Build.ArtifactStagingDirectory)/PostBuildLogs'\n      publishLocation: Container\n      artifactName: PostBuildLogs\n      continueOnError: true\n      condition: always()\n"
  },
  {
    "path": "eng/common/core-templates/steps/publish-pipeline-artifacts.yml",
    "content": "parameters:\n- name: is1ESPipeline\n  type: boolean\n  default: false\n\n- name: args\n  type: object\n  default: {}  \n\nsteps:\n- ${{ if ne(parameters.is1ESPipeline, true) }}:\n  - template: /eng/common/templates/steps/publish-pipeline-artifacts.yml\n    parameters:\n      ${{ each parameter in parameters }}:\n        ${{ parameter.key }}: ${{ parameter.value }}\n- ${{ else }}:\n  - template: /eng/common/templates-official/steps/publish-pipeline-artifacts.yml\n    parameters:\n      ${{ each parameter in parameters }}:\n        ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/core-templates/steps/retain-build.yml",
    "content": "parameters:\n  # Optional azure devops PAT with build execute permissions for the build's organization,\n  # only needed if the build that should be retained ran on a different organization than \n  # the pipeline where this template is executing from\n  Token: ''\n  # Optional BuildId to retain, defaults to the current running build\n  BuildId: ''\n  # Azure devops Organization URI for the build in the https://dev.azure.com/<organization> format.\n  # Defaults to the organization the current pipeline is running on\n  AzdoOrgUri: '$(System.CollectionUri)'\n  # Azure devops project for the build. Defaults to the project the current pipeline is running on\n  AzdoProject: '$(System.TeamProject)'\n\nsteps:\n  - task: powershell@2\n    inputs:\n      targetType: 'filePath'\n      filePath: eng/common/retain-build.ps1\n      pwsh: true\n      arguments: >\n        -AzdoOrgUri: ${{parameters.AzdoOrgUri}}\n        -AzdoProject ${{parameters.AzdoProject}}\n        -Token ${{coalesce(parameters.Token, '$env:SYSTEM_ACCESSTOKEN') }}\n        -BuildId ${{coalesce(parameters.BuildId, '$env:BUILD_ID')}}\n    displayName: Enable permanent build retention\n    env:\n      SYSTEM_ACCESSTOKEN: $(System.AccessToken)\n      BUILD_ID: $(Build.BuildId)"
  },
  {
    "path": "eng/common/core-templates/steps/send-to-helix.yml",
    "content": "# Please remember to update the documentation if you make changes to these parameters!\nparameters:\n  HelixSource: 'pr/default'              # required -- sources must start with pr/, official/, prodcon/, or agent/\n  HelixType: 'tests/default/'            # required -- Helix telemetry which identifies what type of data this is; should include \"test\" for clarity and must end in '/'\n  HelixBuild: $(Build.BuildNumber)       # required -- the build number Helix will use to identify this -- automatically set to the AzDO build number\n  HelixTargetQueues: ''                  # required -- semicolon-delimited list of Helix queues to test on; see https://helix.dot.net/ for a list of queues\n  HelixAccessToken: ''                   # required -- access token to make Helix API requests; should be provided by the appropriate variable group\n  HelixProjectPath: 'eng/common/helixpublish.proj'  # optional -- path to the project file to build relative to BUILD_SOURCESDIRECTORY\n  HelixProjectArguments: ''              # optional -- arguments passed to the build command\n  HelixConfiguration: ''                 # optional -- additional property attached to a job\n  HelixPreCommands: ''                   # optional -- commands to run before Helix work item execution\n  HelixPostCommands: ''                  # optional -- commands to run after Helix work item execution\n  WorkItemDirectory: ''                  # optional -- a payload directory to zip up and send to Helix; requires WorkItemCommand; incompatible with XUnitProjects\n  WorkItemCommand: ''                    # optional -- a command to execute on the payload; requires WorkItemDirectory; incompatible with XUnitProjects\n  WorkItemTimeout: ''                    # optional -- a timeout in TimeSpan.Parse-ready value (e.g. 00:02:00) for the work item command; requires WorkItemDirectory; incompatible with XUnitProjects\n  CorrelationPayloadDirectory: ''        # optional -- a directory to zip up and send to Helix as a correlation payload\n  XUnitProjects: ''                      # optional -- semicolon-delimited list of XUnitProjects to parse and send to Helix; requires XUnitRuntimeTargetFramework, XUnitPublishTargetFramework, XUnitRunnerVersion, and IncludeDotNetCli=true\n  XUnitWorkItemTimeout: ''               # optional -- the workitem timeout in seconds for all workitems created from the xUnit projects specified by XUnitProjects\n  XUnitPublishTargetFramework: ''        # optional -- framework to use to publish your xUnit projects\n  XUnitRuntimeTargetFramework: ''        # optional -- framework to use for the xUnit console runner\n  XUnitRunnerVersion: ''                 # optional -- version of the xUnit nuget package you wish to use on Helix; required for XUnitProjects\n  IncludeDotNetCli: false                # optional -- true will download a version of the .NET CLI onto the Helix machine as a correlation payload; requires DotNetCliPackageType and DotNetCliVersion\n  DotNetCliPackageType: ''               # optional -- either 'sdk', 'runtime' or 'aspnetcore-runtime'; determines whether the sdk or runtime will be sent to Helix; see https://raw.githubusercontent.com/dotnet/core/main/release-notes/releases-index.json\n  DotNetCliVersion: ''                   # optional -- version of the CLI to send to Helix; based on this: https://raw.githubusercontent.com/dotnet/core/main/release-notes/releases-index.json\n  WaitForWorkItemCompletion: true        # optional -- true will make the task wait until work items have been completed and fail the build if work items fail. False is \"fire and forget.\"\n  IsExternal: false                      # [DEPRECATED] -- doesn't do anything, jobs are external if HelixAccessToken is empty and Creator is set\n  HelixBaseUri: 'https://helix.dot.net/' # optional -- sets the Helix API base URI (allows targeting https://helix.int-dot.net )\n  Creator: ''                            # optional -- if the build is external, use this to specify who is sending the job\n  DisplayNamePrefix: 'Run Tests'         # optional -- rename the beginning of the displayName of the steps in AzDO \n  condition: succeeded()                 # optional -- condition for step to execute; defaults to succeeded()\n  continueOnError: false                 # optional -- determines whether to continue the build if the step errors; defaults to false\n\nsteps:\n  - powershell: 'powershell \"$env:BUILD_SOURCESDIRECTORY\\eng\\common\\msbuild.ps1 $env:BUILD_SOURCESDIRECTORY/${{ parameters.HelixProjectPath }} /restore /p:TreatWarningsAsErrors=false ${{ parameters.HelixProjectArguments }} /t:Test /bl:$env:BUILD_SOURCESDIRECTORY\\artifacts\\log\\$env:BuildConfig\\SendToHelix.binlog\"'\n    displayName: ${{ parameters.DisplayNamePrefix }} (Windows)\n    env:\n      BuildConfig: $(_BuildConfig)\n      HelixSource: ${{ parameters.HelixSource }}\n      HelixType: ${{ parameters.HelixType }}\n      HelixBuild: ${{ parameters.HelixBuild }}\n      HelixConfiguration:  ${{ parameters.HelixConfiguration }}\n      HelixTargetQueues: ${{ parameters.HelixTargetQueues }}\n      HelixAccessToken: ${{ parameters.HelixAccessToken }}\n      HelixPreCommands: ${{ parameters.HelixPreCommands }}\n      HelixPostCommands: ${{ parameters.HelixPostCommands }}\n      WorkItemDirectory: ${{ parameters.WorkItemDirectory }}\n      WorkItemCommand: ${{ parameters.WorkItemCommand }}\n      WorkItemTimeout: ${{ parameters.WorkItemTimeout }}\n      CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }}\n      XUnitProjects: ${{ parameters.XUnitProjects }}\n      XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }}\n      XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }}\n      XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }}\n      XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }}\n      IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }}\n      DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }}\n      DotNetCliVersion: ${{ parameters.DotNetCliVersion }}\n      WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }}\n      HelixBaseUri: ${{ parameters.HelixBaseUri }}\n      Creator: ${{ parameters.Creator }}\n      SYSTEM_ACCESSTOKEN: $(System.AccessToken)\n    condition: and(${{ parameters.condition }}, eq(variables['Agent.Os'], 'Windows_NT'))\n    continueOnError: ${{ parameters.continueOnError }}\n  - script: $BUILD_SOURCESDIRECTORY/eng/common/msbuild.sh $BUILD_SOURCESDIRECTORY/${{ parameters.HelixProjectPath }} /restore /p:TreatWarningsAsErrors=false ${{ parameters.HelixProjectArguments }} /t:Test /bl:$BUILD_SOURCESDIRECTORY/artifacts/log/$BuildConfig/SendToHelix.binlog\n    displayName: ${{ parameters.DisplayNamePrefix }} (Unix)\n    env:\n      BuildConfig: $(_BuildConfig)\n      HelixSource: ${{ parameters.HelixSource }}\n      HelixType: ${{ parameters.HelixType }}\n      HelixBuild: ${{ parameters.HelixBuild }}\n      HelixConfiguration:  ${{ parameters.HelixConfiguration }}\n      HelixTargetQueues: ${{ parameters.HelixTargetQueues }}\n      HelixAccessToken: ${{ parameters.HelixAccessToken }}\n      HelixPreCommands: ${{ parameters.HelixPreCommands }}\n      HelixPostCommands: ${{ parameters.HelixPostCommands }}\n      WorkItemDirectory: ${{ parameters.WorkItemDirectory }}\n      WorkItemCommand: ${{ parameters.WorkItemCommand }}\n      WorkItemTimeout: ${{ parameters.WorkItemTimeout }}\n      CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }}\n      XUnitProjects: ${{ parameters.XUnitProjects }}\n      XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }}\n      XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }}\n      XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }}\n      XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }}\n      IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }}\n      DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }}\n      DotNetCliVersion: ${{ parameters.DotNetCliVersion }}\n      WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }}\n      HelixBaseUri: ${{ parameters.HelixBaseUri }}\n      Creator: ${{ parameters.Creator }}\n      SYSTEM_ACCESSTOKEN: $(System.AccessToken)\n    condition: and(${{ parameters.condition }}, ne(variables['Agent.Os'], 'Windows_NT'))\n    continueOnError: ${{ parameters.continueOnError }}\n"
  },
  {
    "path": "eng/common/core-templates/steps/source-build.yml",
    "content": "parameters:\n  # This template adds arcade-powered source-build to CI.\n\n  # This is a 'steps' template, and is intended for advanced scenarios where the existing build\n  # infra has a careful build methodology that must be followed. For example, a repo\n  # (dotnet/runtime) might choose to clone the GitHub repo only once and store it as a pipeline\n  # artifact for all subsequent jobs to use, to reduce dependence on a strong network connection to\n  # GitHub. Using this steps template leaves room for that infra to be included.\n\n  # Defines the platform on which to run the steps. See 'eng/common/core-templates/job/source-build.yml'\n  # for details. The entire object is described in the 'job' template for simplicity, even though\n  # the usage of the properties on this object is split between the 'job' and 'steps' templates.\n  platform: {}\n  is1ESPipeline: false\n\nsteps:\n# Build. Keep it self-contained for simple reusability. (No source-build-specific job variables.)\n- script: |\n    set -x\n    df -h\n\n    # If building on the internal project, the internal storage variable may be available (usually only if needed)\n    # In that case, add variables to allow the download of internal runtimes if the specified versions are not found\n    # in the default public locations.\n    internalRuntimeDownloadArgs=\n    if [ '$(dotnetbuilds-internal-container-read-token-base64)' != '$''(dotnetbuilds-internal-container-read-token-base64)' ]; then\n      internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://ci.dot.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)'\n    fi\n\n    buildConfig=Release\n    # Check if AzDO substitutes in a build config from a variable, and use it if so.\n    if [ '$(_BuildConfig)' != '$''(_BuildConfig)' ]; then\n      buildConfig='$(_BuildConfig)'\n    fi\n\n    targetRidArgs=\n    if [ '${{ parameters.platform.targetRID }}' != '' ]; then\n      targetRidArgs='/p:TargetRid=${{ parameters.platform.targetRID }}'\n    fi\n\n    portableBuildArgs=\n    if [ '${{ parameters.platform.portableBuild }}' != '' ]; then\n      portableBuildArgs='/p:PortableBuild=${{ parameters.platform.portableBuild }}'\n    fi\n\n    ${{ coalesce(parameters.platform.buildScript, './build.sh') }} --ci \\\n      --configuration $buildConfig \\\n      --restore --build --pack -bl \\\n      --source-build \\\n      ${{ parameters.platform.buildArguments }} \\\n      $internalRuntimeDownloadArgs \\\n      $targetRidArgs \\\n      $portableBuildArgs \\\n  displayName: Build\n\n- template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml\n  parameters:\n    is1ESPipeline: ${{ parameters.is1ESPipeline }}\n    args:\n      displayName: Publish BuildLogs\n      targetPath: artifacts/log/${{ coalesce(variables._BuildConfig, 'Release') }}\n      artifactName: BuildLogs_SourceBuild_${{ parameters.platform.name }}_Attempt$(System.JobAttempt)\n      continueOnError: true\n      condition: succeededOrFailed()\n      sbomEnabled: false  # we don't need SBOM for logs\n"
  },
  {
    "path": "eng/common/core-templates/steps/source-index-stage1-publish.yml",
    "content": "parameters:\n  sourceIndexUploadPackageVersion: 2.0.0-20250906.1\n  sourceIndexProcessBinlogPackageVersion: 1.0.1-20250906.1\n  sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json\n  binlogPath: artifacts/log/Debug/Build.binlog\n\nsteps:\n- task: UseDotNet@2\n  displayName: \"Source Index: Use .NET 9 SDK\"\n  inputs:\n    packageType: sdk\n    version: 9.0.x\n    installationPath: $(Agent.TempDirectory)/dotnet\n    workingDirectory: $(Agent.TempDirectory)\n\n- script: |\n    $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --source ${{parameters.sourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools\n    $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --source ${{parameters.sourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools\n  displayName: \"Source Index: Download netsourceindex Tools\"\n  # Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk.\n  workingDirectory: $(Agent.TempDirectory)\n\n- script: $(Agent.TempDirectory)/.source-index/tools/BinLogToSln -i ${{parameters.BinlogPath}} -r $(System.DefaultWorkingDirectory) -n $(Build.Repository.Name) -o .source-index/stage1output\n  displayName: \"Source Index: Process Binlog into indexable sln\"\n\n- ${{ if and(ne(parameters.runAsPublic, 'true'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:\n  - task: AzureCLI@2\n    displayName: \"Source Index: Upload Source Index stage1 artifacts to Azure\"\n    inputs:\n      azureSubscription: 'SourceDotNet Stage1 Publish'\n      addSpnToEnvironment: true\n      scriptType: 'ps'\n      scriptLocation: 'inlineScript'\n      inlineScript: |\n        $(Agent.TempDirectory)/.source-index/tools/UploadIndexStage1 -i .source-index/stage1output -n $(Build.Repository.Name) -s netsourceindexstage1 -b stage1\n"
  },
  {
    "path": "eng/common/core-templates/variables/pool-providers.yml",
    "content": "parameters:\n  is1ESPipeline: false\n\nvariables:\n  - ${{ if eq(parameters.is1ESPipeline, 'true') }}:\n    - template: /eng/common/templates-official/variables/pool-providers.yml\n  - ${{ else }}:\n    - template: /eng/common/templates/variables/pool-providers.yml"
  },
  {
    "path": "eng/common/cross/armel/tizen/tizen.patch",
    "content": "diff -u -r a/usr/lib/libc.so b/usr/lib/libc.so\n--- a/usr/lib/libc.so\t2016-12-30 23:00:08.284951863 +0900\n+++ b/usr/lib/libc.so\t2016-12-30 23:00:32.140951815 +0900\n@@ -2,4 +2,4 @@\n    Use the shared library, but some functions are only in\n    the static library, so try that secondarily.  */\n OUTPUT_FORMAT(elf32-littlearm)\n-GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a  AS_NEEDED ( /lib/ld-linux.so.3 ) )\n+GROUP ( libc.so.6 libc_nonshared.a  AS_NEEDED ( ld-linux.so.3 ) )\n"
  },
  {
    "path": "eng/common/cross/build-android-rootfs.sh",
    "content": "#!/usr/bin/env bash\nset -e\n__NDK_Version=r21\n\nusage()\n{\n    echo \"Creates a toolchain and sysroot used for cross-compiling for Android.\"\n    echo\n    echo \"Usage: $0 [BuildArch] [ApiLevel] [--ndk NDKVersion]\"\n    echo\n    echo \"BuildArch is the target architecture of Android. Currently only arm64 is supported.\"\n    echo \"ApiLevel is the target Android API level. API levels usually match to Android releases. See https://source.android.com/source/build-numbers.html\"\n    echo \"NDKVersion is the version of Android NDK. The default is r21. See https://developer.android.com/ndk/downloads/revision_history\"\n    echo\n    echo \"By default, the toolchain and sysroot will be generated in cross/android-rootfs/toolchain/[BuildArch]. You can change this behavior\"\n    echo \"by setting the TOOLCHAIN_DIR environment variable\"\n    echo\n    echo \"By default, the NDK will be downloaded into the cross/android-rootfs/android-ndk-$__NDK_Version directory. If you already have an NDK installation,\"\n    echo \"you can set the NDK_DIR environment variable to have this script use that installation of the NDK.\"\n    echo \"By default, this script will generate a file, android_platform, in the root of the ROOTFS_DIR directory that contains the RID for the supported and tested Android build: android.28-arm64. This file is to replace '/etc/os-release', which is not available for Android.\"\n    exit 1\n}\n\n__ApiLevel=28 # The minimum platform for arm64 is API level 21 but the minimum version that support glob(3) is 28. See $ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/glob.h\n__BuildArch=arm64\n__AndroidArch=aarch64\n__AndroidToolchain=aarch64-linux-android\n\nwhile :; do\n    if [[ \"$#\" -le 0 ]]; then\n        break\n    fi\n\n    i=$1\n\n    lowerI=\"$(echo $i | tr \"[:upper:]\" \"[:lower:]\")\"\n    case $lowerI in\n        -?|-h|--help)\n            usage\n            exit 1\n            ;;\n        arm64)\n            __BuildArch=arm64\n            __AndroidArch=aarch64\n            __AndroidToolchain=aarch64-linux-android\n            ;;\n        arm)\n            __BuildArch=arm\n            __AndroidArch=arm\n            __AndroidToolchain=arm-linux-androideabi\n            ;;\n        --ndk)\n            shift\n            __NDK_Version=$1\n            ;;\n        *[0-9])\n            __ApiLevel=$i\n            ;;\n        *)\n            __UnprocessedBuildArgs=\"$__UnprocessedBuildArgs $i\"\n            ;;\n    esac\n    shift\ndone\n\nif [[ \"$__NDK_Version\" == \"r21\" ]] || [[ \"$__NDK_Version\" == \"r22\" ]]; then\n    __NDK_File_Arch_Spec=-x86_64\n    __SysRoot=sysroot\nelse\n    __NDK_File_Arch_Spec=\n    __SysRoot=toolchains/llvm/prebuilt/linux-x86_64/sysroot\nfi\n\n# Obtain the location of the bash script to figure out where the root of the repo is.\n__ScriptBaseDir=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\n__CrossDir=\"$__ScriptBaseDir/../../../.tools/android-rootfs\"\n\nif [[ ! -f \"$__CrossDir\" ]]; then\n    mkdir -p \"$__CrossDir\"\nfi\n\n# Resolve absolute path to avoid `../` in build logs\n__CrossDir=\"$( cd \"$__CrossDir\" && pwd )\"\n\n__NDK_Dir=\"$__CrossDir/android-ndk-$__NDK_Version\"\n__lldb_Dir=\"$__CrossDir/lldb\"\n__ToolchainDir=\"$__CrossDir/android-ndk-$__NDK_Version\"\n\nif [[ -n \"$TOOLCHAIN_DIR\" ]]; then\n    __ToolchainDir=$TOOLCHAIN_DIR\nfi\n\nif [[ -n \"$NDK_DIR\" ]]; then\n    __NDK_Dir=$NDK_DIR\nfi\n\necho \"Target API level: $__ApiLevel\"\necho \"Target architecture: $__BuildArch\"\necho \"NDK version: $__NDK_Version\"\necho \"NDK location: $__NDK_Dir\"\necho \"Target Toolchain location: $__ToolchainDir\"\n\n# Download the NDK if required\nif [ ! -d $__NDK_Dir ]; then\n    echo Downloading the NDK into $__NDK_Dir\n    mkdir -p $__NDK_Dir\n    wget -q --progress=bar:force:noscroll --show-progress https://dl.google.com/android/repository/android-ndk-$__NDK_Version-linux$__NDK_File_Arch_Spec.zip -O $__CrossDir/android-ndk-$__NDK_Version-linux.zip\n    unzip -q $__CrossDir/android-ndk-$__NDK_Version-linux.zip -d $__CrossDir\nfi\n\nif [ ! -d $__lldb_Dir ]; then\n    mkdir -p $__lldb_Dir\n    echo Downloading LLDB into $__lldb_Dir\n    wget -q --progress=bar:force:noscroll --show-progress https://dl.google.com/android/repository/lldb-2.3.3614996-linux-x86_64.zip -O $__CrossDir/lldb-2.3.3614996-linux-x86_64.zip\n    unzip -q $__CrossDir/lldb-2.3.3614996-linux-x86_64.zip -d $__lldb_Dir\nfi\n\necho \"Download dependencies...\"\n__TmpDir=$__CrossDir/tmp/$__BuildArch/\nmkdir -p \"$__TmpDir\"\n\n# combined dependencies for coreclr, installer and libraries\n__AndroidPackages=\"libicu\"\n__AndroidPackages+=\" libandroid-glob\"\n__AndroidPackages+=\" liblzma\"\n__AndroidPackages+=\" krb5\"\n__AndroidPackages+=\" openssl\"\n\nfor path in $(wget -qO- https://packages.termux.dev/termux-main-21/dists/stable/main/binary-$__AndroidArch/Packages |\\\n    grep -A15 \"Package: \\(${__AndroidPackages// /\\\\|}\\)\" | grep -v \"static\\|tool\" | grep Filename); do\n\n    if [[ \"$path\" != \"Filename:\" ]]; then\n        echo \"Working on: $path\"\n        wget -qO- https://packages.termux.dev/termux-main-21/$path | dpkg -x - \"$__TmpDir\"\n    fi\ndone\n\ncp -R \"$__TmpDir/data/data/com.termux/files/usr/\"* \"$__ToolchainDir/$__SysRoot/usr/\"\n\n# Generate platform file for build.sh script to assign to __DistroRid\necho \"Generating platform file...\"\necho \"RID=android.${__ApiLevel}-${__BuildArch}\" > $__ToolchainDir/$__SysRoot/android_platform\n\necho \"Now to build coreclr, libraries and host; run:\"\necho ROOTFS_DIR=$(realpath $__ToolchainDir/$__SysRoot) ./build.sh clr+libs+host --cross --arch $__BuildArch\n"
  },
  {
    "path": "eng/common/cross/build-rootfs.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nusage()\n{\n    echo \"Usage: $0 [BuildArch] [CodeName] [lldbx.y] [llvmx[.y]] [--skipunmount] --rootfsdir <directory>]\"\n    echo \"BuildArch can be: arm(default), arm64, armel, armv6, loongarch64, ppc64le, riscv64, s390x, x64, x86\"\n    echo \"CodeName - optional, Code name for Linux, can be: xenial(default), zesty, bionic, alpine\"\n    echo \"                               for alpine can be specified with version: alpineX.YY or alpineedge\"\n    echo \"                               for FreeBSD can be: freebsd13, freebsd14\"\n    echo \"                               for illumos can be: illumos\"\n    echo \"                               for Haiku can be: haiku.\"\n    echo \"lldbx.y - optional, LLDB version, can be: lldb3.9(default), lldb4.0, lldb5.0, lldb6.0 no-lldb. Ignored for alpine and FreeBSD\"\n    echo \"llvmx[.y] - optional, LLVM version for LLVM related packages.\"\n    echo \"--skipunmount - optional, will skip the unmount of rootfs folder.\"\n    echo \"--skipsigcheck - optional, will skip package signature checks (allowing untrusted packages).\"\n    echo \"--skipemulation - optional, will skip qemu and debootstrap requirement when building environment for debian based systems.\"\n    echo \"--use-mirror - optional, use mirror URL to fetch resources, when available.\"\n    echo \"--jobs N - optional, restrict to N jobs.\"\n    exit 1\n}\n\n__CodeName=xenial\n__CrossDir=$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\n__BuildArch=arm\n__AlpineArch=armv7\n__FreeBSDArch=arm\n__FreeBSDMachineArch=armv7\n__IllumosArch=arm7\n__HaikuArch=arm\n__QEMUArch=arm\n__UbuntuArch=armhf\n__UbuntuRepo=\n__UbuntuSuites=\"updates security backports\"\n__LLDB_Package=\"liblldb-3.9-dev\"\n__SkipUnmount=0\n\n# base development support\n__UbuntuPackages=\"build-essential\"\n\n__AlpinePackages=\"alpine-base\"\n__AlpinePackages+=\" build-base\"\n__AlpinePackages+=\" linux-headers\"\n__AlpinePackages+=\" lldb-dev\"\n__AlpinePackages+=\" python3\"\n__AlpinePackages+=\" libedit\"\n\n# symlinks fixer\n__UbuntuPackages+=\" symlinks\"\n\n# runtime dependencies\n__UbuntuPackages+=\" libicu-dev\"\n__UbuntuPackages+=\" liblttng-ust-dev\"\n__UbuntuPackages+=\" libunwind8-dev\"\n\n__AlpinePackages+=\" gettext-dev\"\n__AlpinePackages+=\" icu-dev\"\n__AlpinePackages+=\" libunwind-dev\"\n__AlpinePackages+=\" lttng-ust-dev\"\n__AlpinePackages+=\" compiler-rt\"\n\n# runtime libraries' dependencies\n__UbuntuPackages+=\" libcurl4-openssl-dev\"\n__UbuntuPackages+=\" libkrb5-dev\"\n__UbuntuPackages+=\" libssl-dev\"\n__UbuntuPackages+=\" zlib1g-dev\"\n__UbuntuPackages+=\" libbrotli-dev\"\n\n__AlpinePackages+=\" curl-dev\"\n__AlpinePackages+=\" krb5-dev\"\n__AlpinePackages+=\" openssl-dev\"\n__AlpinePackages+=\" zlib-dev\"\n\n__FreeBSDBase=\"13.5-RELEASE\"\n__FreeBSDPkg=\"1.21.3\"\n__FreeBSDABI=\"13\"\n__FreeBSDPackages=\"libunwind\"\n__FreeBSDPackages+=\" icu\"\n__FreeBSDPackages+=\" libinotify\"\n__FreeBSDPackages+=\" openssl\"\n__FreeBSDPackages+=\" krb5\"\n__FreeBSDPackages+=\" terminfo-db\"\n\n__IllumosPackages=\"icu\"\n__IllumosPackages+=\" mit-krb5\"\n__IllumosPackages+=\" openssl\"\n__IllumosPackages+=\" zlib\"\n\n__HaikuPackages=\"gcc_syslibs\"\n__HaikuPackages+=\" gcc_syslibs_devel\"\n__HaikuPackages+=\" gmp\"\n__HaikuPackages+=\" gmp_devel\"\n__HaikuPackages+=\" icu[0-9]+\"\n__HaikuPackages+=\" icu[0-9]*_devel\"\n__HaikuPackages+=\" krb5\"\n__HaikuPackages+=\" krb5_devel\"\n__HaikuPackages+=\" libiconv\"\n__HaikuPackages+=\" libiconv_devel\"\n__HaikuPackages+=\" llvm[0-9]*_libunwind\"\n__HaikuPackages+=\" llvm[0-9]*_libunwind_devel\"\n__HaikuPackages+=\" mpfr\"\n__HaikuPackages+=\" mpfr_devel\"\n__HaikuPackages+=\" openssl3\"\n__HaikuPackages+=\" openssl3_devel\"\n__HaikuPackages+=\" zlib\"\n__HaikuPackages+=\" zlib_devel\"\n\n# ML.NET dependencies\n__UbuntuPackages+=\" libomp5\"\n__UbuntuPackages+=\" libomp-dev\"\n\n# Taken from https://github.com/alpinelinux/alpine-chroot-install/blob/6d08f12a8a70dd9b9dc7d997c88aa7789cc03c42/alpine-chroot-install#L85-L133\n__AlpineKeys='\n4a6a0840:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1yHJxQgsHQREclQu4Ohe\\nqxTxd1tHcNnvnQTu/UrTky8wWvgXT+jpveroeWWnzmsYlDI93eLI2ORakxb3gA2O\\nQ0Ry4ws8vhaxLQGC74uQR5+/yYrLuTKydFzuPaS1dK19qJPXB8GMdmFOijnXX4SA\\njixuHLe1WW7kZVtjL7nufvpXkWBGjsfrvskdNA/5MfxAeBbqPgaq0QMEfxMAn6/R\\nL5kNepi/Vr4S39Xvf2DzWkTLEK8pcnjNkt9/aafhWqFVW7m3HCAII6h/qlQNQKSo\\nGuH34Q8GsFG30izUENV9avY7hSLq7nggsvknlNBZtFUcmGoQrtx3FmyYsIC8/R+B\\nywIDAQAB\n5243ef4b:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvNijDxJ8kloskKQpJdx+\\nmTMVFFUGDoDCbulnhZMJoKNkSuZOzBoFC94omYPtxnIcBdWBGnrm6ncbKRlR+6oy\\nDO0W7c44uHKCFGFqBhDasdI4RCYP+fcIX/lyMh6MLbOxqS22TwSLhCVjTyJeeH7K\\naA7vqk+QSsF4TGbYzQDDpg7+6aAcNzg6InNePaywA6hbT0JXbxnDWsB+2/LLSF2G\\nmnhJlJrWB1WGjkz23ONIWk85W4S0XB/ewDefd4Ly/zyIciastA7Zqnh7p3Ody6Q0\\nsS2MJzo7p3os1smGjUF158s6m/JbVh4DN6YIsxwl2OjDOz9R0OycfJSDaBVIGZzg\\ncQIDAQAB\n524d27bb:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr8s1q88XpuJWLCZALdKj\\nlN8wg2ePB2T9aIcaxryYE/Jkmtu+ZQ5zKq6BT3y/udt5jAsMrhHTwroOjIsF9DeG\\ne8Y3vjz+Hh4L8a7hZDaw8jy3CPag47L7nsZFwQOIo2Cl1SnzUc6/owoyjRU7ab0p\\niWG5HK8IfiybRbZxnEbNAfT4R53hyI6z5FhyXGS2Ld8zCoU/R4E1P0CUuXKEN4p0\\n64dyeUoOLXEWHjgKiU1mElIQj3k/IF02W89gDj285YgwqA49deLUM7QOd53QLnx+\\nxrIrPv3A+eyXMFgexNwCKQU9ZdmWa00MjjHlegSGK8Y2NPnRoXhzqSP9T9i2HiXL\\nVQIDAQAB\n5261cecb:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwlzMkl7b5PBdfMzGdCT0\\ncGloRr5xGgVmsdq5EtJvFkFAiN8Ac9MCFy/vAFmS8/7ZaGOXoCDWbYVLTLOO2qtX\\nyHRl+7fJVh2N6qrDDFPmdgCi8NaE+3rITWXGrrQ1spJ0B6HIzTDNEjRKnD4xyg4j\\ng01FMcJTU6E+V2JBY45CKN9dWr1JDM/nei/Pf0byBJlMp/mSSfjodykmz4Oe13xB\\nCa1WTwgFykKYthoLGYrmo+LKIGpMoeEbY1kuUe04UiDe47l6Oggwnl+8XD1MeRWY\\nsWgj8sF4dTcSfCMavK4zHRFFQbGp/YFJ/Ww6U9lA3Vq0wyEI6MCMQnoSMFwrbgZw\\nwwIDAQAB\n58199dcc:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3v8/ye/V/t5xf4JiXLXa\\nhWFRozsnmn3hobON20GdmkrzKzO/eUqPOKTpg2GtvBhK30fu5oY5uN2ORiv2Y2ht\\neLiZ9HVz3XP8Fm9frha60B7KNu66FO5P2o3i+E+DWTPqqPcCG6t4Znk2BypILcit\\nwiPKTsgbBQR2qo/cO01eLLdt6oOzAaF94NH0656kvRewdo6HG4urbO46tCAizvCR\\nCA7KGFMyad8WdKkTjxh8YLDLoOCtoZmXmQAiwfRe9pKXRH/XXGop8SYptLqyVVQ+\\ntegOD9wRs2tOlgcLx4F/uMzHN7uoho6okBPiifRX+Pf38Vx+ozXh056tjmdZkCaV\\naQIDAQAB\n58cbb476:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoSPnuAGKtRIS5fEgYPXD\\n8pSGvKAmIv3A08LBViDUe+YwhilSHbYXUEAcSH1KZvOo1WT1x2FNEPBEFEFU1Eyc\\n+qGzbA03UFgBNvArurHQ5Z/GngGqE7IarSQFSoqewYRtFSfp+TL9CUNBvM0rT7vz\\n2eMu3/wWG+CBmb92lkmyWwC1WSWFKO3x8w+Br2IFWvAZqHRt8oiG5QtYvcZL6jym\\nY8T6sgdDlj+Y+wWaLHs9Fc+7vBuyK9C4O1ORdMPW15qVSl4Lc2Wu1QVwRiKnmA+c\\nDsH/m7kDNRHM7TjWnuj+nrBOKAHzYquiu5iB3Qmx+0gwnrSVf27Arc3ozUmmJbLj\\nzQIDAQAB\n58e4f17d:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvBxJN9ErBgdRcPr5g4hV\\nqyUSGZEKuvQliq2Z9SRHLh2J43+EdB6A+yzVvLnzcHVpBJ+BZ9RV30EM9guck9sh\\nr+bryZcRHyjG2wiIEoduxF2a8KeWeQH7QlpwGhuobo1+gA8L0AGImiA6UP3LOirl\\nI0G2+iaKZowME8/tydww4jx5vG132JCOScMjTalRsYZYJcjFbebQQolpqRaGB4iG\\nWqhytWQGWuKiB1A22wjmIYf3t96l1Mp+FmM2URPxD1gk/BIBnX7ew+2gWppXOK9j\\n1BJpo0/HaX5XoZ/uMqISAAtgHZAqq+g3IUPouxTphgYQRTRYpz2COw3NF43VYQrR\\nbQIDAQAB\n60ac2099:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwR4uJVtJOnOFGchnMW5Y\\nj5/waBdG1u5BTMlH+iQMcV5+VgWhmpZHJCBz3ocD+0IGk2I68S5TDOHec/GSC0lv\\n6R9o6F7h429GmgPgVKQsc8mPTPtbjJMuLLs4xKc+viCplXc0Nc0ZoHmCH4da6fCV\\ntdpHQjVe6F9zjdquZ4RjV6R6JTiN9v924dGMAkbW/xXmamtz51FzondKC52Gh8Mo\\n/oA0/T0KsCMCi7tb4QNQUYrf+Xcha9uus4ww1kWNZyfXJB87a2kORLiWMfs2IBBJ\\nTmZ2Fnk0JnHDb8Oknxd9PvJPT0mvyT8DA+KIAPqNvOjUXP4bnjEHJcoCP9S5HkGC\\nIQIDAQAB\n6165ee59:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAutQkua2CAig4VFSJ7v54\\nALyu/J1WB3oni7qwCZD3veURw7HxpNAj9hR+S5N/pNeZgubQvJWyaPuQDm7PTs1+\\ntFGiYNfAsiibX6Rv0wci3M+z2XEVAeR9Vzg6v4qoofDyoTbovn2LztaNEjTkB+oK\\ntlvpNhg1zhou0jDVYFniEXvzjckxswHVb8cT0OMTKHALyLPrPOJzVtM9C1ew2Nnc\\n3848xLiApMu3NBk0JqfcS3Bo5Y2b1FRVBvdt+2gFoKZix1MnZdAEZ8xQzL/a0YS5\\nHd0wj5+EEKHfOd3A75uPa/WQmA+o0cBFfrzm69QDcSJSwGpzWrD1ScH3AK8nWvoj\\nv7e9gukK/9yl1b4fQQ00vttwJPSgm9EnfPHLAtgXkRloI27H6/PuLoNvSAMQwuCD\\nhQRlyGLPBETKkHeodfLoULjhDi1K2gKJTMhtbnUcAA7nEphkMhPWkBpgFdrH+5z4\\nLxy+3ek0cqcI7K68EtrffU8jtUj9LFTUC8dERaIBs7NgQ/LfDbDfGh9g6qVj1hZl\\nk9aaIPTm/xsi8v3u+0qaq7KzIBc9s59JOoA8TlpOaYdVgSQhHHLBaahOuAigH+VI\\nisbC9vmqsThF2QdDtQt37keuqoda2E6sL7PUvIyVXDRfwX7uMDjlzTxHTymvq2Ck\\nhtBqojBnThmjJQFgZXocHG8CAwEAAQ==\n61666e3f:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlEyxkHggKCXC2Wf5Mzx4\\nnZLFZvU2bgcA3exfNPO/g1YunKfQY+Jg4fr6tJUUTZ3XZUrhmLNWvpvSwDS19ZmC\\nIXOu0+V94aNgnhMsk9rr59I8qcbsQGIBoHzuAl8NzZCgdbEXkiY90w1skUw8J57z\\nqCsMBydAueMXuWqF5nGtYbi5vHwK42PffpiZ7G5Kjwn8nYMW5IZdL6ZnMEVJUWC9\\nI4waeKg0yskczYDmZUEAtrn3laX9677ToCpiKrvmZYjlGl0BaGp3cxggP2xaDbUq\\nqfFxWNgvUAb3pXD09JM6Mt6HSIJaFc9vQbrKB9KT515y763j5CC2KUsilszKi3mB\\nHYe5PoebdjS7D1Oh+tRqfegU2IImzSwW3iwA7PJvefFuc/kNIijfS/gH/cAqAK6z\\nbhdOtE/zc7TtqW2Wn5Y03jIZdtm12CxSxwgtCF1NPyEWyIxAQUX9ACb3M0FAZ61n\\nfpPrvwTaIIxxZ01L3IzPLpbc44x/DhJIEU+iDt6IMTrHOphD9MCG4631eIdB0H1b\\n6zbNX1CXTsafqHRFV9XmYYIeOMggmd90s3xIbEujA6HKNP/gwzO6CDJ+nHFDEqoF\\nSkxRdTkEqjTjVKieURW7Swv7zpfu5PrsrrkyGnsRrBJJzXlm2FOOxnbI2iSL1B5F\\nrO5kbUxFeZUIDq+7Yv4kLWcCAwEAAQ==\n616a9724:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnC+bR4bHf/L6QdU4puhQ\\ngl1MHePszRC38bzvVFDUJsmCaMCL2suCs2A2yxAgGb9pu9AJYLAmxQC4mM3jNqhg\\n/E7yuaBbek3O02zN/ctvflJ250wZCy+z0ZGIp1ak6pu1j14IwHokl9j36zNfGtfv\\nADVOcdpWITFFlPqwq1qt/H3UsKVmtiF3BNWWTeUEQwKvlU8ymxgS99yn0+4OPyNT\\nL3EUeS+NQJtDS01unau0t7LnjUXn+XIneWny8bIYOQCuVR6s/gpIGuhBaUqwaJOw\\n7jkJZYF2Ij7uPb4b5/R3vX2FfxxqEHqssFSg8FFUNTZz3qNZs0CRVyfA972g9WkJ\\nhPfn31pQYil4QGRibCMIeU27YAEjXoqfJKEPh4UWMQsQLrEfdGfb8VgwrPbniGfU\\nL3jKJR3VAafL9330iawzVQDlIlwGl6u77gEXMl9K0pfazunYhAp+BMP+9ot5ckK+\\nosmrqj11qMESsAj083GeFdfV3pXEIwUytaB0AKEht9DbqUfiE/oeZ/LAXgySMtVC\\nsbC4ESmgVeY2xSBIJdDyUap7FR49GGrw0W49NUv9gRgQtGGaNVQQO9oGL2PBC41P\\niWF9GLoX30HIz1P8PF/cZvicSSPkQf2Z6TV+t0ebdGNS5DjapdnCrq8m9Z0pyKsQ\\nuxAL2a7zX8l5i1CZh1ycUGsCAwEAAQ==\n616abc23:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0MfCDrhODRCIxR9Dep1s\\neXafh5CE5BrF4WbCgCsevyPIdvTeyIaW4vmO3bbG4VzhogDZju+R3IQYFuhoXP5v\\nY+zYJGnwrgz3r5wYAvPnLEs1+dtDKYOgJXQj+wLJBW1mzRDL8FoRXOe5iRmn1EFS\\nwZ1DoUvyu7/J5r0itKicZp3QKED6YoilXed+1vnS4Sk0mzN4smuMR9eO1mMCqNp9\\n9KTfRDHTbakIHwasECCXCp50uXdoW6ig/xUAFanpm9LtK6jctNDbXDhQmgvAaLXZ\\nLvFqoaYJ/CvWkyYCgL6qxvMvVmPoRv7OPcyni4xR/WgWa0MSaEWjgPx3+yj9fiMA\\n1S02pFWFDOr5OUF/O4YhFJvUCOtVsUPPfA/Lj6faL0h5QI9mQhy5Zb9TTaS9jB6p\\nLw7u0dJlrjFedk8KTJdFCcaGYHP6kNPnOxMylcB/5WcztXZVQD5WpCicGNBxCGMm\\nW64SgrV7M07gQfL/32QLsdqPUf0i8hoVD8wfQ3EpbQzv6Fk1Cn90bZqZafg8XWGY\\nwddhkXk7egrr23Djv37V2okjzdqoyLBYBxMz63qQzFoAVv5VoY2NDTbXYUYytOvG\\nGJ1afYDRVWrExCech1mX5ZVUB1br6WM+psFLJFoBFl6mDmiYt0vMYBddKISsvwLl\\nIJQkzDwtXzT2cSjoj3T5QekCAwEAAQ==\n616ac3bc:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvaaoSLab+IluixwKV5Od\\n0gib2YurjPatGIbn5Ov2DLUFYiebj2oJINXJSwUOO+4WcuHFEqiL/1rya+k5hLZt\\nhnPL1tn6QD4rESznvGSasRCQNT2vS/oyZbTYJRyAtFkEYLlq0t3S3xBxxHWuvIf0\\nqVxVNYpQWyM3N9RIeYBR/euXKJXileSHk/uq1I5wTC0XBIHWcthczGN0m9wBEiWS\\n0m3cnPk4q0Ea8mUJ91Rqob19qETz6VbSPYYpZk3qOycjKosuwcuzoMpwU8KRiMFd\\n5LHtX0Hx85ghGsWDVtS0c0+aJa4lOMGvJCAOvDfqvODv7gKlCXUpgumGpLdTmaZ8\\n1RwqspAe3IqBcdKTqRD4m2mSg23nVx2FAY3cjFvZQtfooT7q1ItRV5RgH6FhQSl7\\n+6YIMJ1Bf8AAlLdRLpg+doOUGcEn+pkDiHFgI8ylH1LKyFKw+eXaAml/7DaWZk1d\\ndqggwhXOhc/UUZFQuQQ8A8zpA13PcbC05XxN2hyP93tCEtyynMLVPtrRwDnHxFKa\\nqKzs3rMDXPSXRn3ZZTdKH3069ApkEjQdpcwUh+EmJ1Ve/5cdtzT6kKWCjKBFZP/s\\n91MlRrX2BTRdHaU5QJkUheUtakwxuHrdah2F94lRmsnQlpPr2YseJu6sIE+Dnx4M\\nCfhdVbQL2w54R645nlnohu8CAwEAAQ==\n616adfeb:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq0BFD1D4lIxQcsqEpQzU\\npNCYM3aP1V/fxxVdT4DWvSI53JHTwHQamKdMWtEXetWVbP5zSROniYKFXd/xrD9X\\n0jiGHey3lEtylXRIPxe5s+wXoCmNLcJVnvTcDtwx/ne2NLHxp76lyc25At+6RgE6\\nADjLVuoD7M4IFDkAsd8UQ8zM0Dww9SylIk/wgV3ZkifecvgUQRagrNUdUjR56EBZ\\nraQrev4hhzOgwelT0kXCu3snbUuNY/lU53CoTzfBJ5UfEJ5pMw1ij6X0r5S9IVsy\\nKLWH1hiO0NzU2c8ViUYCly4Fe9xMTFc6u2dy/dxf6FwERfGzETQxqZvSfrRX+GLj\\n/QZAXiPg5178hT/m0Y3z5IGenIC/80Z9NCi+byF1WuJlzKjDcF/TU72zk0+PNM/H\\nKuppf3JT4DyjiVzNC5YoWJT2QRMS9KLP5iKCSThwVceEEg5HfhQBRT9M6KIcFLSs\\nmFjx9kNEEmc1E8hl5IR3+3Ry8G5/bTIIruz14jgeY9u5jhL8Vyyvo41jgt9sLHR1\\n/J1TxKfkgksYev7PoX6/ZzJ1ksWKZY5NFoDXTNYUgzFUTOoEaOg3BAQKadb3Qbbq\\nXIrxmPBdgrn9QI7NCgfnAY3Tb4EEjs3ON/BNyEhUENcXOH6I1NbcuBQ7g9P73kE4\\nVORdoc8MdJ5eoKBpO8Ww8HECAwEAAQ==\n616ae350:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyduVzi1mWm+lYo2Tqt/0\\nXkCIWrDNP1QBMVPrE0/ZlU2bCGSoo2Z9FHQKz/mTyMRlhNqTfhJ5qU3U9XlyGOPJ\\npiM+b91g26pnpXJ2Q2kOypSgOMOPA4cQ42PkHBEqhuzssfj9t7x47ppS94bboh46\\nxLSDRff/NAbtwTpvhStV3URYkxFG++cKGGa5MPXBrxIp+iZf9GnuxVdST5PGiVGP\\nODL/b69sPJQNbJHVquqUTOh5Ry8uuD2WZuXfKf7/C0jC/ie9m2+0CttNu9tMciGM\\nEyKG1/Xhk5iIWO43m4SrrT2WkFlcZ1z2JSf9Pjm4C2+HovYpihwwdM/OdP8Xmsnr\\nDzVB4YvQiW+IHBjStHVuyiZWc+JsgEPJzisNY0Wyc/kNyNtqVKpX6dRhMLanLmy+\\nf53cCSI05KPQAcGj6tdL+D60uKDkt+FsDa0BTAobZ31OsFVid0vCXtsbplNhW1IF\\nHwsGXBTVcfXg44RLyL8Lk/2dQxDHNHzAUslJXzPxaHBLmt++2COa2EI1iWlvtznk\\nOk9WP8SOAIj+xdqoiHcC4j72BOVVgiITIJNHrbppZCq6qPR+fgXmXa+sDcGh30m6\\n9Wpbr28kLMSHiENCWTdsFij+NQTd5S47H7XTROHnalYDuF1RpS+DpQidT5tUimaT\\nJZDr++FjKrnnijbyNF8b98UCAwEAAQ==\n616db30d:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnpUpyWDWjlUk3smlWeA0\\nlIMW+oJ38t92CRLHH3IqRhyECBRW0d0aRGtq7TY8PmxjjvBZrxTNDpJT6KUk4LRm\\na6A6IuAI7QnNK8SJqM0DLzlpygd7GJf8ZL9SoHSH+gFsYF67Cpooz/YDqWrlN7Vw\\ntO00s0B+eXy+PCXYU7VSfuWFGK8TGEv6HfGMALLjhqMManyvfp8hz3ubN1rK3c8C\\nUS/ilRh1qckdbtPvoDPhSbTDmfU1g/EfRSIEXBrIMLg9ka/XB9PvWRrekrppnQzP\\nhP9YE3x/wbFc5QqQWiRCYyQl/rgIMOXvIxhkfe8H5n1Et4VAorkpEAXdsfN8KSVv\\nLSMazVlLp9GYq5SUpqYX3KnxdWBgN7BJoZ4sltsTpHQ/34SXWfu3UmyUveWj7wp0\\nx9hwsPirVI00EEea9AbP7NM2rAyu6ukcm4m6ATd2DZJIViq2es6m60AE6SMCmrQF\\nwmk4H/kdQgeAELVfGOm2VyJ3z69fQuywz7xu27S6zTKi05Qlnohxol4wVb6OB7qG\\nLPRtK9ObgzRo/OPumyXqlzAi/Yvyd1ZQk8labZps3e16bQp8+pVPiumWioMFJDWV\\nGZjCmyMSU8V6MB6njbgLHoyg2LCukCAeSjbPGGGYhnKLm1AKSoJh3IpZuqcKCk5C\\n8CM1S15HxV78s9dFntEqIokCAwEAAQ==\n66ba20fe:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtfB12w4ZgqsXWZDfUAV/\\n6Y4aHUKIu3q4SXrNZ7CXF9nXoAVYrS7NAxJdAodsY3vPCN0g5O8DFXR+390LdOuQ\\n+HsGKCc1k5tX5ZXld37EZNTNSbR0k+NKhd9h6X3u6wqPOx7SIKxwAQR8qeeFq4pP\\nrt9GAGlxtuYgzIIcKJPwE0dZlcBCg+GnptCUZXp/38BP1eYC+xTXSL6Muq1etYfg\\nodXdb7Yl+2h1IHuOwo5rjgY5kpY7GcAs8AjGk3lDD/av60OTYccknH0NCVSmPoXK\\nvrxDBOn0LQRNBLcAfnTKgHrzy0Q5h4TNkkyTgxkoQw5ObDk9nnabTxql732yy9BY\\ns+hM9+dSFO1HKeVXreYSA2n1ndF18YAvAumzgyqzB7I4pMHXq1kC/8bONMJxwSkS\\nYm6CoXKyavp7RqGMyeVpRC7tV+blkrrUml0BwNkxE+XnwDRB3xDV6hqgWe0XrifD\\nYTfvd9ScZQP83ip0r4IKlq4GMv/R5shcCRJSkSZ6QSGshH40JYSoiwJf5FHbj9ND\\n7do0UAqebWo4yNx63j/wb2ULorW3AClv0BCFSdPsIrCStiGdpgJDBR2P2NZOCob3\\nG9uMj+wJD6JJg2nWqNJxkANXX37Qf8plgzssrhrgOvB0fjjS7GYhfkfmZTJ0wPOw\\nA8+KzFseBh4UFGgue78KwgkCAwEAAQ==\n'\n__Keyring=\n__KeyringFile=\"/usr/share/keyrings/ubuntu-archive-keyring.gpg\"\n__SkipSigCheck=0\n__SkipEmulation=0\n__UseMirror=0\n\n__UnprocessedBuildArgs=\nwhile :; do\n    if [[ \"$#\" -le 0 ]]; then\n        break\n    fi\n\n    lowerI=\"$(echo \"$1\" | tr \"[:upper:]\" \"[:lower:]\")\"\n    case $lowerI in\n        -\\?|-h|--help)\n            usage\n            ;;\n        arm)\n            __BuildArch=arm\n            __UbuntuArch=armhf\n            __AlpineArch=armv7\n            __QEMUArch=arm\n            ;;\n        arm64)\n            __BuildArch=arm64\n            __UbuntuArch=arm64\n            __AlpineArch=aarch64\n            __QEMUArch=aarch64\n            __FreeBSDArch=arm64\n            __FreeBSDMachineArch=aarch64\n            ;;\n        armel)\n            __BuildArch=armel\n            __UbuntuArch=armel\n            __UbuntuRepo=\"http://archive.debian.org/debian/\"\n            __CodeName=buster\n            __KeyringFile=\"/usr/share/keyrings/debian-archive-keyring.gpg\"\n            __LLDB_Package=\"liblldb-6.0-dev\"\n            __UbuntuPackages=\"${__UbuntuPackages// libomp-dev/}\"\n            __UbuntuPackages=\"${__UbuntuPackages// libomp5/}\"\n            __UbuntuSuites=\n            ;;\n        armv6)\n            __BuildArch=armv6\n            __UbuntuArch=armhf\n            __QEMUArch=arm\n            __UbuntuRepo=\"http://raspbian.raspberrypi.org/raspbian/\"\n            __CodeName=buster\n            __KeyringFile=\"/usr/share/keyrings/raspbian-archive-keyring.gpg\"\n            __LLDB_Package=\"liblldb-6.0-dev\"\n            __UbuntuSuites=\n\n            if [[ -e \"$__KeyringFile\" ]]; then\n                __Keyring=\"--keyring $__KeyringFile\"\n            fi\n            ;;\n        loongarch64)\n            __BuildArch=loongarch64\n            __AlpineArch=loongarch64\n            __QEMUArch=loongarch64\n            __UbuntuArch=loong64\n            __UbuntuSuites=unreleased\n            __LLDB_Package=\"liblldb-19-dev\"\n\n            if [[ \"$__CodeName\" == \"sid\" ]]; then\n                __UbuntuRepo=\"http://ftp.ports.debian.org/debian-ports/\"\n            fi\n            ;;\n        riscv64)\n            __BuildArch=riscv64\n            __AlpineArch=riscv64\n            __AlpinePackages=\"${__AlpinePackages// lldb-dev/}\"\n            __QEMUArch=riscv64\n            __UbuntuArch=riscv64\n            __UbuntuPackages=\"${__UbuntuPackages// libunwind8-dev/}\"\n            unset __LLDB_Package\n            ;;\n        ppc64le)\n            __BuildArch=ppc64le\n            __AlpineArch=ppc64le\n            __QEMUArch=ppc64le\n            __UbuntuArch=ppc64el\n            __UbuntuRepo=\"http://ports.ubuntu.com/ubuntu-ports/\"\n            __UbuntuPackages=\"${__UbuntuPackages// libunwind8-dev/}\"\n            __UbuntuPackages=\"${__UbuntuPackages// libomp-dev/}\"\n            __UbuntuPackages=\"${__UbuntuPackages// libomp5/}\"\n            unset __LLDB_Package\n            ;;\n        s390x)\n            __BuildArch=s390x\n            __AlpineArch=s390x\n            __QEMUArch=s390x\n            __UbuntuArch=s390x\n            __UbuntuRepo=\"http://ports.ubuntu.com/ubuntu-ports/\"\n            __UbuntuPackages=\"${__UbuntuPackages// libunwind8-dev/}\"\n            __UbuntuPackages=\"${__UbuntuPackages// libomp-dev/}\"\n            __UbuntuPackages=\"${__UbuntuPackages// libomp5/}\"\n            unset __LLDB_Package\n            ;;\n        x64)\n            __BuildArch=x64\n            __AlpineArch=x86_64\n            __UbuntuArch=amd64\n            __FreeBSDArch=amd64\n            __FreeBSDMachineArch=amd64\n            __illumosArch=x86_64\n            __HaikuArch=x86_64\n            __UbuntuRepo=\"http://archive.ubuntu.com/ubuntu/\"\n            ;;\n        x86)\n            __BuildArch=x86\n            __UbuntuArch=i386\n            __AlpineArch=x86\n            __UbuntuRepo=\"http://archive.ubuntu.com/ubuntu/\"\n            ;;\n        lldb*)\n            version=\"$(echo \"$lowerI\" | tr -d '[:alpha:]-=')\"\n            majorVersion=\"${version%%.*}\"\n\n            [ -z \"${version##*.*}\" ] && minorVersion=\"${version#*.}\"\n            if [ -z \"$minorVersion\" ]; then\n                minorVersion=0\n            fi\n\n            # for versions > 6.0, lldb has dropped the minor version\n            if [ \"$majorVersion\" -le 6 ]; then\n                version=\"$majorVersion.$minorVersion\"\n            else\n                version=\"$majorVersion\"\n            fi\n\n            __LLDB_Package=\"liblldb-${version}-dev\"\n            ;;\n        no-lldb)\n            unset __LLDB_Package\n            ;;\n        llvm*)\n            version=\"$(echo \"$lowerI\" | tr -d '[:alpha:]-=')\"\n            __LLVM_MajorVersion=\"${version%%.*}\"\n\n            [ -z \"${version##*.*}\" ] && __LLVM_MinorVersion=\"${version#*.}\"\n            if [ -z \"$__LLVM_MinorVersion\" ]; then\n                __LLVM_MinorVersion=0\n            fi\n\n            # for versions > 6.0, lldb has dropped the minor version\n            if [ \"$__LLVM_MajorVersion\" -gt 6 ]; then\n                __LLVM_MinorVersion=\n            fi\n\n            ;;\n        xenial) # Ubuntu 16.04\n            __CodeName=xenial\n            ;;\n        bionic) # Ubuntu 18.04\n            __CodeName=bionic\n            ;;\n        focal) # Ubuntu 20.04\n            __CodeName=focal\n            ;;\n        jammy) # Ubuntu 22.04\n            __CodeName=jammy\n            ;;\n        noble) # Ubuntu 24.04\n            __CodeName=noble\n            __LLDB_Package=\"liblldb-19-dev\"\n            ;;\n        stretch) # Debian 9\n            __CodeName=stretch\n            __LLDB_Package=\"liblldb-6.0-dev\"\n            __KeyringFile=\"/usr/share/keyrings/debian-archive-keyring.gpg\"\n\n            if [[ -z \"$__UbuntuRepo\" ]]; then\n                __UbuntuRepo=\"http://ftp.debian.org/debian/\"\n            fi\n            ;;\n        buster) # Debian 10\n            __CodeName=buster\n            __LLDB_Package=\"liblldb-6.0-dev\"\n            __KeyringFile=\"/usr/share/keyrings/debian-archive-keyring.gpg\"\n\n            if [[ -z \"$__UbuntuRepo\" ]]; then\n                __UbuntuRepo=\"http://archive.debian.org/debian/\"\n            fi\n            ;;\n        bullseye) # Debian 11\n            __CodeName=bullseye\n            __KeyringFile=\"/usr/share/keyrings/debian-archive-keyring.gpg\"\n\n            if [[ -z \"$__UbuntuRepo\" ]]; then\n                __UbuntuRepo=\"http://ftp.debian.org/debian/\"\n            fi\n            ;;\n        bookworm) # Debian 12\n            __CodeName=bookworm\n            __KeyringFile=\"/usr/share/keyrings/debian-archive-keyring.gpg\"\n\n            if [[ -z \"$__UbuntuRepo\" ]]; then\n                __UbuntuRepo=\"http://ftp.debian.org/debian/\"\n            fi\n            ;;\n        sid) # Debian sid\n            __CodeName=sid\n            __UbuntuSuites=\n\n            # Debian-Ports architectures need different values\n            case \"$__UbuntuArch\" in\n            amd64|arm64|armel|armhf|i386|mips64el|ppc64el|riscv64|s390x)\n                __KeyringFile=\"/usr/share/keyrings/debian-archive-keyring.gpg\"\n\n                if [[ -z \"$__UbuntuRepo\" ]]; then\n                    __UbuntuRepo=\"http://ftp.debian.org/debian/\"\n                fi\n                ;;\n            *)\n                __KeyringFile=\"/usr/share/keyrings/debian-ports-archive-keyring.gpg\"\n\n                if [[ -z \"$__UbuntuRepo\" ]]; then\n                    __UbuntuRepo=\"http://ftp.ports.debian.org/debian-ports/\"\n                fi\n                ;;\n            esac\n\n            if [[ -e \"$__KeyringFile\" ]]; then\n                __Keyring=\"--keyring $__KeyringFile\"\n            fi\n            ;;\n        tizen)\n            __CodeName=\n            __UbuntuRepo=\n            __Tizen=tizen\n            ;;\n        alpine*)\n            __CodeName=alpine\n            __UbuntuRepo=\n\n            if [[ \"$lowerI\" == \"alpineedge\" ]]; then\n                __AlpineVersion=edge\n            else\n                version=\"$(echo \"$lowerI\" | tr -d '[:alpha:]-=')\"\n                __AlpineMajorVersion=\"${version%%.*}\"\n                __AlpineMinorVersion=\"${version#*.}\"\n                __AlpineVersion=\"$__AlpineMajorVersion.$__AlpineMinorVersion\"\n            fi\n            ;;\n        freebsd13)\n            __CodeName=freebsd\n            __SkipUnmount=1\n            ;;\n        freebsd14)\n            __CodeName=freebsd\n            __FreeBSDBase=\"14.3-RELEASE\"\n            __FreeBSDABI=\"14\"\n            __SkipUnmount=1\n            ;;\n        illumos)\n            __CodeName=illumos\n            __SkipUnmount=1\n            ;;\n        haiku)\n            __CodeName=haiku\n            __SkipUnmount=1\n            ;;\n        --skipunmount)\n            __SkipUnmount=1\n            ;;\n        --skipsigcheck)\n            __SkipSigCheck=1\n            ;;\n        --skipemulation)\n            __SkipEmulation=1\n            ;;\n        --rootfsdir|-rootfsdir)\n            shift\n            __RootfsDir=\"$1\"\n            ;;\n        --use-mirror)\n            __UseMirror=1\n            ;;\n        --use-jobs)\n            shift\n            MAXJOBS=$1\n            ;;\n        *)\n            __UnprocessedBuildArgs=\"$__UnprocessedBuildArgs $1\"\n            ;;\n    esac\n\n    shift\ndone\n\ncase \"$__AlpineVersion\" in\n    3.14) __AlpinePackages+=\" llvm11-libs\" ;;\n    3.15) __AlpinePackages+=\" llvm12-libs\" ;;\n    3.16) __AlpinePackages+=\" llvm13-libs\" ;;\n    3.17) __AlpinePackages+=\" llvm15-libs\" ;;\n    edge) __AlpineLlvmLibsLookup=1 ;;\n    *)\n        if [[ \"$__AlpineArch\" =~ s390x|ppc64le ]]; then\n            __AlpineVersion=3.15 # minimum version that supports lldb-dev\n            __AlpinePackages+=\" llvm12-libs\"\n        elif [[ \"$__AlpineArch\" == \"x86\" ]]; then\n            __AlpineVersion=3.17 # minimum version that supports lldb-dev\n            __AlpinePackages+=\" llvm15-libs\"\n        elif [[ \"$__AlpineArch\" == \"riscv64\" || \"$__AlpineArch\" == \"loongarch64\" ]]; then\n            __AlpineVersion=3.21 # minimum version that supports lldb-dev\n            __AlpinePackages+=\" llvm19-libs\"\n        elif [[ -n \"$__AlpineMajorVersion\" ]]; then\n            # use whichever alpine version is provided and select the latest toolchain libs\n            __AlpineLlvmLibsLookup=1\n        else\n            __AlpineVersion=3.13 # 3.13 to maximize compatibility\n            __AlpinePackages+=\" llvm10-libs\"\n        fi\nesac\n\nif [[ \"$__AlpineVersion\" =~ 3\\.1[345] ]]; then\n    # compiler-rt--static was merged in compiler-rt package in alpine 3.16\n    # for older versions, we need compiler-rt--static, so replace the name\n    __AlpinePackages=\"${__AlpinePackages/compiler-rt/compiler-rt-static}\"\nfi\n\n__UbuntuPackages+=\" ${__LLDB_Package:-}\"\n\nif [[ -z \"$__UbuntuRepo\" ]]; then\n    __UbuntuRepo=\"http://ports.ubuntu.com/\"\nfi\n\nif [[ -n \"$__LLVM_MajorVersion\" ]]; then\n    __UbuntuPackages+=\" libclang-common-${__LLVM_MajorVersion}${__LLVM_MinorVersion:+.$__LLVM_MinorVersion}-dev\"\nfi\n\nif [[ -z \"$__RootfsDir\" && -n \"$ROOTFS_DIR\" ]]; then\n    __RootfsDir=\"$ROOTFS_DIR\"\nfi\n\nif [[ -z \"$__RootfsDir\" ]]; then\n    __RootfsDir=\"$__CrossDir/../../../.tools/rootfs/$__BuildArch\"\nfi\n\nif [[ -d \"$__RootfsDir\" ]]; then\n    if [[ \"$__SkipUnmount\" == \"0\" ]]; then\n        umount \"$__RootfsDir\"/* || true\n    fi\n    rm -rf \"$__RootfsDir\"\nfi\n\nmkdir -p \"$__RootfsDir\"\n__RootfsDir=\"$( cd \"$__RootfsDir\" && pwd )\"\n\n__hasWget=\nensureDownloadTool()\n{\n    if command -v wget &> /dev/null; then\n        __hasWget=1\n    elif command -v curl &> /dev/null; then\n        __hasWget=0\n    else\n        >&2 echo \"ERROR: either wget or curl is required by this script.\"\n        exit 1\n    fi\n}\n\nif [[ \"$__CodeName\" == \"alpine\" ]]; then\n    __ApkToolsVersion=2.12.11\n    __ApkToolsDir=\"$(mktemp -d)\"\n    __ApkKeysDir=\"$(mktemp -d)\"\n    arch=\"$(uname -m)\"\n\n    ensureDownloadTool\n\n    if [[ \"$__hasWget\" == 1 ]]; then\n        wget -P \"$__ApkToolsDir\" \"https://gitlab.alpinelinux.org/api/v4/projects/5/packages/generic/v$__ApkToolsVersion/$arch/apk.static\"\n    else\n        curl -SLO --create-dirs --output-dir \"$__ApkToolsDir\" \"https://gitlab.alpinelinux.org/api/v4/projects/5/packages/generic/v$__ApkToolsVersion/$arch/apk.static\"\n    fi\n    if [[ \"$arch\" == \"x86_64\" ]]; then\n      __ApkToolsSHA512SUM=\"53e57b49230da07ef44ee0765b9592580308c407a8d4da7125550957bb72cb59638e04f8892a18b584451c8d841d1c7cb0f0ab680cc323a3015776affaa3be33\"\n    elif [[ \"$arch\" == \"aarch64\" ]]; then\n      __ApkToolsSHA512SUM=\"9e2b37ecb2b56c05dad23d379be84fd494c14bd730b620d0d576bda760588e1f2f59a7fcb2f2080577e0085f23a0ca8eadd993b4e61c2ab29549fdb71969afd0\"\n    else\n      echo \"WARNING: add missing hash for your host architecture. To find the value, use: 'find /tmp -name apk.static -exec sha512sum {} \\;'\"\n    fi\n    echo \"$__ApkToolsSHA512SUM $__ApkToolsDir/apk.static\" | sha512sum -c\n    chmod +x \"$__ApkToolsDir/apk.static\"\n\n    if [[ \"$__AlpineVersion\" == \"edge\" ]]; then\n        version=edge\n    else\n        version=\"v$__AlpineVersion\"\n    fi\n\n    for line in $__AlpineKeys; do\n        id=\"${line%%:*}\"\n        content=\"${line#*:}\"\n\n        echo -e \"-----BEGIN PUBLIC KEY-----\\n$content\\n-----END PUBLIC KEY-----\" > \"$__ApkKeysDir/alpine-devel@lists.alpinelinux.org-$id.rsa.pub\"\n    done\n\n    if [[ \"$__SkipSigCheck\" == \"1\" ]]; then\n        __ApkSignatureArg=\"--allow-untrusted\"\n    else\n        __ApkSignatureArg=\"--keys-dir $__ApkKeysDir\"\n    fi\n\n    if [[ \"$__SkipEmulation\" == \"1\" ]]; then\n        __NoEmulationArg=\"--no-scripts\"\n    fi\n\n    # initialize DB\n    # shellcheck disable=SC2086\n    \"$__ApkToolsDir/apk.static\" \\\n        -X \"http://dl-cdn.alpinelinux.org/alpine/$version/main\" \\\n        -X \"http://dl-cdn.alpinelinux.org/alpine/$version/community\" \\\n        -U $__ApkSignatureArg --root \"$__RootfsDir\" --arch \"$__AlpineArch\" --initdb add\n\n    if [[ \"$__AlpineLlvmLibsLookup\" == 1 ]]; then\n        # shellcheck disable=SC2086\n        __AlpinePackages+=\" $(\"$__ApkToolsDir/apk.static\" \\\n            -X \"http://dl-cdn.alpinelinux.org/alpine/$version/main\" \\\n            -X \"http://dl-cdn.alpinelinux.org/alpine/$version/community\" \\\n            -U $__ApkSignatureArg --root \"$__RootfsDir\" --arch \"$__AlpineArch\" \\\n            search 'llvm*-libs' | grep -E '^llvm' | sort | tail -1 | sed 's/-[^-]*//2g')\"\n    fi\n\n    # install all packages in one go\n    # shellcheck disable=SC2086\n    \"$__ApkToolsDir/apk.static\" \\\n        -X \"http://dl-cdn.alpinelinux.org/alpine/$version/main\" \\\n        -X \"http://dl-cdn.alpinelinux.org/alpine/$version/community\" \\\n        -U $__ApkSignatureArg --root \"$__RootfsDir\" --arch \"$__AlpineArch\" $__NoEmulationArg \\\n        add $__AlpinePackages\n\n    rm -r \"$__ApkToolsDir\"\nelif [[ \"$__CodeName\" == \"freebsd\" ]]; then\n    mkdir -p \"$__RootfsDir\"/usr/local/etc\n    JOBS=${MAXJOBS:=\"$(getconf _NPROCESSORS_ONLN)\"}\n\n    ensureDownloadTool\n\n    if [[ \"$__hasWget\" == 1 ]]; then\n        wget -O- \"https://download.freebsd.org/ftp/releases/${__FreeBSDArch}/${__FreeBSDMachineArch}/${__FreeBSDBase}/base.txz\" | tar -C \"$__RootfsDir\" -Jxf - ./lib ./usr/lib ./usr/libdata ./usr/include ./usr/share/keys ./etc ./bin/freebsd-version\n    else\n        curl -SL \"https://download.freebsd.org/ftp/releases/${__FreeBSDArch}/${__FreeBSDMachineArch}/${__FreeBSDBase}/base.txz\" | tar -C \"$__RootfsDir\" -Jxf - ./lib ./usr/lib ./usr/libdata ./usr/include ./usr/share/keys ./etc ./bin/freebsd-version\n    fi\n    echo \"ABI = \\\"FreeBSD:${__FreeBSDABI}:${__FreeBSDMachineArch}\\\"; FINGERPRINTS = \\\"${__RootfsDir}/usr/share/keys\\\"; REPOS_DIR = [\\\"${__RootfsDir}/etc/pkg\\\"]; REPO_AUTOUPDATE = NO; RUN_SCRIPTS = NO;\" > \"${__RootfsDir}\"/usr/local/etc/pkg.conf\n    echo \"FreeBSD: { url: \\\"pkg+http://pkg.FreeBSD.org/\\${ABI}/quarterly\\\", mirror_type: \\\"srv\\\", signature_type: \\\"fingerprints\\\", fingerprints: \\\"/usr/share/keys/pkg\\\", enabled: yes }\" > \"${__RootfsDir}\"/etc/pkg/FreeBSD.conf\n    mkdir -p \"$__RootfsDir\"/tmp\n    # get and build package manager\n    if [[ \"$__hasWget\" == 1 ]]; then\n        wget -O- \"https://github.com/freebsd/pkg/archive/${__FreeBSDPkg}.tar.gz\" | tar -C \"$__RootfsDir\"/tmp -zxf -\n    else\n        curl -SL \"https://github.com/freebsd/pkg/archive/${__FreeBSDPkg}.tar.gz\" | tar -C \"$__RootfsDir\"/tmp -zxf -\n    fi\n    cd \"$__RootfsDir/tmp/pkg-${__FreeBSDPkg}\"\n    # needed for install to succeed\n    mkdir -p \"$__RootfsDir\"/host/etc\n    ./autogen.sh && ./configure --prefix=\"$__RootfsDir\"/host && make -j \"$JOBS\" && make install\n    rm -rf \"$__RootfsDir/tmp/pkg-${__FreeBSDPkg}\"\n    # install packages we need.\n    INSTALL_AS_USER=$(whoami) \"$__RootfsDir\"/host/sbin/pkg -r \"$__RootfsDir\" -C \"$__RootfsDir\"/usr/local/etc/pkg.conf update\n    # shellcheck disable=SC2086\n    INSTALL_AS_USER=$(whoami) \"$__RootfsDir\"/host/sbin/pkg -r \"$__RootfsDir\" -C \"$__RootfsDir\"/usr/local/etc/pkg.conf install --yes $__FreeBSDPackages\nelif [[ \"$__CodeName\" == \"illumos\" ]]; then\n    mkdir \"$__RootfsDir/tmp\"\n    pushd \"$__RootfsDir/tmp\"\n    JOBS=${MAXJOBS:=\"$(getconf _NPROCESSORS_ONLN)\"}\n\n    ensureDownloadTool\n\n    echo \"Downloading sysroot.\"\n    if [[ \"$__hasWget\" == 1 ]]; then\n        wget -O- https://github.com/illumos/sysroot/releases/download/20181213-de6af22ae73b-v1/illumos-sysroot-i386-20181213-de6af22ae73b-v1.tar.gz | tar -C \"$__RootfsDir\" -xzf -\n    else\n        curl -SL https://github.com/illumos/sysroot/releases/download/20181213-de6af22ae73b-v1/illumos-sysroot-i386-20181213-de6af22ae73b-v1.tar.gz | tar -C \"$__RootfsDir\" -xzf -\n    fi\n    echo \"Building binutils. Please wait..\"\n    if [[ \"$__hasWget\" == 1 ]]; then\n        wget -O- https://ftp.gnu.org/gnu/binutils/binutils-2.42.tar.xz | tar -xJf -\n    else\n        curl -SL https://ftp.gnu.org/gnu/binutils/binutils-2.42.tar.xz | tar -xJf -\n    fi\n    mkdir build-binutils && cd build-binutils\n    ../binutils-2.42/configure --prefix=\"$__RootfsDir\" --target=\"${__illumosArch}-sun-solaris2.11\" --program-prefix=\"${__illumosArch}-illumos-\" --with-sysroot=\"$__RootfsDir\"\n    make -j \"$JOBS\" && make install && cd ..\n    echo \"Building gcc. Please wait..\"\n    if [[ \"$__hasWget\" == 1 ]]; then\n        wget -O- https://ftp.gnu.org/gnu/gcc/gcc-13.3.0/gcc-13.3.0.tar.xz | tar -xJf -\n    else\n        curl -SL https://ftp.gnu.org/gnu/gcc/gcc-13.3.0/gcc-13.3.0.tar.xz | tar -xJf -\n    fi\n    CFLAGS=\"-fPIC\"\n    CXXFLAGS=\"-fPIC\"\n    CXXFLAGS_FOR_TARGET=\"-fPIC\"\n    CFLAGS_FOR_TARGET=\"-fPIC\"\n    export CFLAGS CXXFLAGS CXXFLAGS_FOR_TARGET CFLAGS_FOR_TARGET\n    mkdir build-gcc && cd build-gcc\n    ../gcc-13.3.0/configure --prefix=\"$__RootfsDir\" --target=\"${__illumosArch}-sun-solaris2.11\" --program-prefix=\"${__illumosArch}-illumos-\" --with-sysroot=\"$__RootfsDir\" --with-gnu-as       \\\n        --with-gnu-ld --disable-nls --disable-libgomp --disable-libquadmath --disable-libssp --disable-libvtv --disable-libcilkrts --disable-libada --disable-libsanitizer \\\n        --disable-libquadmath-support --disable-shared --enable-tls\n    make -j \"$JOBS\" && make install && cd ..\n    BaseUrl=https://pkgsrc.smartos.org\n    if [[ \"$__UseMirror\" == 1 ]]; then\n        BaseUrl=https://pkgsrc.smartos.skylime.net\n    fi\n    BaseUrl=\"$BaseUrl/packages/SmartOS/2019Q4/${__illumosArch}/All\"\n    echo \"Downloading manifest\"\n    if [[ \"$__hasWget\" == 1 ]]; then\n        wget \"$BaseUrl\"\n    else\n        curl -SLO \"$BaseUrl\"\n    fi\n    echo \"Downloading dependencies.\"\n    read -ra array <<<\"$__IllumosPackages\"\n    for package in \"${array[@]}\"; do\n        echo \"Installing '$package'\"\n        # find last occurrence of package in listing and extract its name\n        package=\"$(sed -En '/.*href=\"('\"$package\"'-[0-9].*).tgz\".*/h;$!d;g;s//\\1/p' All)\"\n        echo \"Resolved name '$package'\"\n        if [[ \"$__hasWget\" == 1 ]]; then\n            wget \"$BaseUrl\"/\"$package\".tgz\n        else\n            curl -SLO \"$BaseUrl\"/\"$package\".tgz\n        fi\n        ar -x \"$package\".tgz\n        tar --skip-old-files -xzf \"$package\".tmp.tg* -C \"$__RootfsDir\" 2>/dev/null\n    done\n    echo \"Cleaning up temporary files.\"\n    popd\n    rm -rf \"$__RootfsDir\"/{tmp,+*}\n    mkdir -p \"$__RootfsDir\"/usr/include/net\n    mkdir -p \"$__RootfsDir\"/usr/include/netpacket\n    if [[ \"$__hasWget\" == 1 ]]; then\n        wget -P \"$__RootfsDir\"/usr/include/net https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/io/bpf/net/bpf.h\n        wget -P \"$__RootfsDir\"/usr/include/net https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/io/bpf/net/dlt.h\n        wget -P \"$__RootfsDir\"/usr/include/netpacket https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/inet/sockmods/netpacket/packet.h\n        wget -P \"$__RootfsDir\"/usr/include/sys https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/sys/sdt.h\n    else\n        curl -SLO --create-dirs --output-dir \"$__RootfsDir\"/usr/include/net https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/io/bpf/net/bpf.h\n        curl -SLO --create-dirs --output-dir \"$__RootfsDir\"/usr/include/net https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/io/bpf/net/dlt.h\n        curl -SLO --create-dirs --output-dir \"$__RootfsDir\"/usr/include/netpacket https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/inet/sockmods/netpacket/packet.h\n        curl -SLO --create-dirs --output-dir \"$__RootfsDir\"/usr/include/sys https://raw.githubusercontent.com/illumos/illumos-gate/master/usr/src/uts/common/sys/sdt.h\n    fi\nelif [[ \"$__CodeName\" == \"haiku\" ]]; then\n    JOBS=${MAXJOBS:=\"$(getconf _NPROCESSORS_ONLN)\"}\n\n    echo \"Building Haiku sysroot for $__HaikuArch\"\n    mkdir -p \"$__RootfsDir/tmp\"\n    pushd \"$__RootfsDir/tmp\"\n\n    mkdir \"$__RootfsDir/tmp/download\"\n\n    ensureDownloadTool\n\n    echo \"Downloading Haiku package tools\"\n    git clone https://github.com/haiku/haiku-toolchains-ubuntu --depth 1 \"$__RootfsDir/tmp/script\"\n    if [[ \"$__hasWget\" == 1 ]]; then\n        wget -O \"$__RootfsDir/tmp/download/hosttools.zip\" \"$(\"$__RootfsDir/tmp/script/fetch.sh\" --hosttools)\"\n    else\n        curl -SLo \"$__RootfsDir/tmp/download/hosttools.zip\" \"$(\"$__RootfsDir/tmp/script/fetch.sh\" --hosttools)\"\n    fi\n\n    unzip -o \"$__RootfsDir/tmp/download/hosttools.zip\" -d \"$__RootfsDir/tmp/bin\"\n\n    HaikuBaseUrl=\"https://eu.hpkg.haiku-os.org/haiku/master/$__HaikuArch/current\"\n    HaikuPortsBaseUrl=\"https://eu.hpkg.haiku-os.org/haikuports/master/$__HaikuArch/current\"\n\n    echo \"Downloading HaikuPorts package repository index...\"\n    if [[ \"$__hasWget\" == 1 ]]; then\n        wget -P \"$__RootfsDir/tmp/download\" \"$HaikuPortsBaseUrl/repo\"\n    else\n        curl -SLO --create-dirs --output-dir \"$__RootfsDir/tmp/download\" \"$HaikuPortsBaseUrl/repo\"\n    fi\n\n    echo \"Downloading Haiku packages\"\n    read -ra array <<<\"$__HaikuPackages\"\n    for package in \"${array[@]}\"; do\n        echo \"Downloading $package...\"\n        hpkgFilename=\"$(LD_LIBRARY_PATH=\"$__RootfsDir/tmp/bin\" \"$__RootfsDir/tmp/bin/package_repo\" list -f \"$__RootfsDir/tmp/download/repo\" |\n            grep -E \"${package}-\" | sort -V | tail -n 1 | xargs)\"\n        if [ -z \"$hpkgFilename\" ]; then\n            >&2 echo \"ERROR: package $package missing.\"\n            exit 1\n        fi\n        echo \"Resolved filename: $hpkgFilename...\"\n        hpkgDownloadUrl=\"$HaikuPortsBaseUrl/packages/$hpkgFilename\"\n        if [[ \"$__hasWget\" == 1 ]]; then\n            wget -P \"$__RootfsDir/tmp/download\" \"$hpkgDownloadUrl\"\n        else\n            curl -SLO --create-dirs --output-dir \"$__RootfsDir/tmp/download\" \"$hpkgDownloadUrl\"\n        fi\n    done\n    for package in haiku haiku_devel; do\n        echo \"Downloading $package...\"\n        if [[ \"$__hasWget\" == 1 ]]; then\n            hpkgVersion=\"$(wget -qO- \"$HaikuBaseUrl\" | sed -n 's/^.*version: \"\\([^\"]*\\)\".*$/\\1/p')\"\n            wget -P \"$__RootfsDir/tmp/download\" \"$HaikuBaseUrl/packages/$package-$hpkgVersion-1-$__HaikuArch.hpkg\"\n        else\n            hpkgVersion=\"$(curl -sSL \"$HaikuBaseUrl\" | sed -n 's/^.*version: \"\\([^\"]*\\)\".*$/\\1/p')\"\n            curl -SLO --create-dirs --output-dir \"$__RootfsDir/tmp/download\" \"$HaikuBaseUrl/packages/$package-$hpkgVersion-1-$__HaikuArch.hpkg\"\n        fi\n    done\n\n    # Set up the sysroot\n    echo \"Setting up sysroot and extracting required packages\"\n    mkdir -p \"$__RootfsDir/boot/system\"\n    for file in \"$__RootfsDir/tmp/download/\"*.hpkg; do\n        echo \"Extracting $file...\"\n        LD_LIBRARY_PATH=\"$__RootfsDir/tmp/bin\" \"$__RootfsDir/tmp/bin/package\" extract -C \"$__RootfsDir/boot/system\" \"$file\"\n    done\n\n    # Download buildtools\n    echo \"Downloading Haiku buildtools\"\n    if [[ \"$__hasWget\" == 1 ]]; then\n        wget -O \"$__RootfsDir/tmp/download/buildtools.zip\" \"$(\"$__RootfsDir/tmp/script/fetch.sh\" --buildtools --arch=$__HaikuArch)\"\n    else\n        curl -SLo \"$__RootfsDir/tmp/download/buildtools.zip\" \"$(\"$__RootfsDir/tmp/script/fetch.sh\" --buildtools --arch=$__HaikuArch)\"\n    fi\n    unzip -o \"$__RootfsDir/tmp/download/buildtools.zip\" -d \"$__RootfsDir\"\n\n    # Cleaning up temporary files\n    echo \"Cleaning up temporary files\"\n    popd\n    rm -rf \"$__RootfsDir/tmp\"\nelif [[ -n \"$__CodeName\" ]]; then\n    __Suites=\"$__CodeName $(for suite in $__UbuntuSuites; do echo -n \"$__CodeName-$suite \"; done)\"\n\n    if [[ \"$__SkipEmulation\" == \"1\" ]]; then\n        if [[ -z \"$AR\" ]]; then\n            if command -v ar &>/dev/null; then\n                AR=\"$(command -v ar)\"\n            elif command -v llvm-ar &>/dev/null; then\n                AR=\"$(command -v llvm-ar)\"\n            else\n                echo \"Unable to find ar or llvm-ar on PATH, add them to PATH or set AR environment variable pointing to the available AR tool\"\n                exit 1\n            fi\n        fi\n\n        PYTHON=${PYTHON_EXECUTABLE:-python3}\n\n        # shellcheck disable=SC2086,SC2046\n        echo running \"$PYTHON\" \"$__CrossDir/install-debs.py\" --arch \"$__UbuntuArch\" --mirror \"$__UbuntuRepo\" --rootfsdir \"$__RootfsDir\" --artool \"$AR\" \\\n            $(for suite in $__Suites; do echo -n \"--suite $suite \"; done) \\\n            $__UbuntuPackages\n\n        # shellcheck disable=SC2086,SC2046\n        \"$PYTHON\" \"$__CrossDir/install-debs.py\" --arch \"$__UbuntuArch\" --mirror \"$__UbuntuRepo\" --rootfsdir \"$__RootfsDir\" --artool \"$AR\" \\\n            $(for suite in $__Suites; do echo -n \"--suite $suite \"; done) \\\n            $__UbuntuPackages\n\n        exit 0\n    fi\n\n    __UpdateOptions=\n    if [[ \"$__SkipSigCheck\" == \"0\" ]]; then\n        __Keyring=\"$__Keyring --force-check-gpg\"\n    else\n        __Keyring=\n        __UpdateOptions=\"--allow-unauthenticated --allow-insecure-repositories\"\n    fi\n\n    # shellcheck disable=SC2086\n    echo running debootstrap \"--variant=minbase\" $__Keyring --arch \"$__UbuntuArch\" \"$__CodeName\" \"$__RootfsDir\" \"$__UbuntuRepo\"\n\n    # shellcheck disable=SC2086\n    if ! debootstrap \"--variant=minbase\" $__Keyring --arch \"$__UbuntuArch\" \"$__CodeName\" \"$__RootfsDir\" \"$__UbuntuRepo\"; then\n        echo \"debootstrap failed! dumping debootstrap.log\"\n        cat \"$__RootfsDir/debootstrap/debootstrap.log\"\n        exit 1\n    fi\n\n    rm -rf \"$__RootfsDir\"/etc/apt/*.{sources,list} \"$__RootfsDir\"/etc/apt/sources.list.d\n    mkdir -p \"$__RootfsDir/etc/apt/sources.list.d/\"\n\n    # shellcheck disable=SC2086\n    cat > \"$__RootfsDir/etc/apt/sources.list.d/$__CodeName.sources\" <<EOF\nTypes: deb\nURIs: $__UbuntuRepo\nSuites: $__Suites\nComponents: main universe\nSigned-By: $__KeyringFile\nEOF\n\n    # shellcheck disable=SC2086\n    chroot \"$__RootfsDir\" apt-get update $__UpdateOptions\n    chroot \"$__RootfsDir\" apt-get -f -y install\n    # shellcheck disable=SC2086\n    chroot \"$__RootfsDir\" apt-get -y install $__UbuntuPackages\n    chroot \"$__RootfsDir\" symlinks -cr /usr\n    chroot \"$__RootfsDir\" apt-get clean\n\n    if [[ \"$__SkipUnmount\" == \"0\" ]]; then\n        umount \"$__RootfsDir\"/* || true\n    fi\nelif [[ \"$__Tizen\" == \"tizen\" ]]; then\n    ROOTFS_DIR=\"$__RootfsDir\" \"$__CrossDir/tizen-build-rootfs.sh\" \"$__BuildArch\"\nelse\n    echo \"Unsupported target platform.\"\n    usage\nfi\n"
  },
  {
    "path": "eng/common/cross/install-debs.py",
    "content": "#!/usr/bin/env python3\n\nimport argparse\nimport asyncio\nimport aiohttp\nimport gzip\nimport os\nimport re\nimport shutil\nimport subprocess\nimport sys\nimport tarfile\nimport tempfile\nimport zstandard\n\nfrom collections import deque\nfrom functools import cmp_to_key\n\nasync def download_file(session, url, dest_path, max_retries=3, retry_delay=2, timeout=60):\n    \"\"\"Asynchronous file download with retries.\"\"\"\n    attempt = 0\n    while attempt < max_retries:\n        try:\n            async with session.get(url, timeout=aiohttp.ClientTimeout(total=timeout)) as response:\n                if response.status == 200:\n                    with open(dest_path, \"wb\") as f:\n                        content = await response.read()\n                        f.write(content)\n                    print(f\"Downloaded {url} at {dest_path}\")\n                    return\n                else:\n                    print(f\"Failed to download {url}, Status Code: {response.status}\")\n                    break\n        except (asyncio.CancelledError, asyncio.TimeoutError, aiohttp.ClientError) as e:\n            print(f\"Error downloading {url}: {type(e).__name__} - {e}. Retrying...\")\n\n        attempt += 1\n        await asyncio.sleep(retry_delay)\n\n    print(f\"Failed to download {url} after {max_retries} attempts.\")\n\nasync def download_deb_files_parallel(mirror, packages, tmp_dir):\n    \"\"\"Download .deb files in parallel.\"\"\"\n    os.makedirs(tmp_dir, exist_ok=True)\n\n    tasks = []\n    timeout = aiohttp.ClientTimeout(total=60)\n    async with aiohttp.ClientSession(timeout=timeout) as session:\n        for pkg, info in packages.items():\n            filename = info.get(\"Filename\")\n            if filename:\n                url = f\"{mirror}/{filename}\"\n                dest_path = os.path.join(tmp_dir, os.path.basename(filename))\n                tasks.append(asyncio.create_task(download_file(session, url, dest_path)))\n\n        await asyncio.gather(*tasks)\n\nasync def download_package_index_parallel(mirror, arch, suites):\n    \"\"\"Download package index files for specified suites and components entirely in memory.\"\"\"\n    tasks = []\n    timeout = aiohttp.ClientTimeout(total=60)\n\n    async with aiohttp.ClientSession(timeout=timeout) as session:\n        for suite in suites:\n            for component in [\"main\", \"universe\"]:\n                url = f\"{mirror}/dists/{suite}/{component}/binary-{arch}/Packages.gz\"\n                tasks.append(fetch_and_decompress(session, url))\n\n        results = await asyncio.gather(*tasks, return_exceptions=True)\n\n    merged_content = \"\"\n    for result in results:\n        if isinstance(result, str):\n            if merged_content:\n                merged_content += \"\\n\\n\"\n            merged_content += result\n\n    return merged_content\n\nasync def fetch_and_decompress(session, url):\n    \"\"\"Fetch and decompress the Packages.gz file.\"\"\"\n    try:\n        async with session.get(url) as response:\n            if response.status == 200:\n                compressed_data = await response.read()\n                decompressed_data = gzip.decompress(compressed_data).decode('utf-8')\n                print(f\"Downloaded index: {url}\")\n                return decompressed_data\n            else:\n                print(f\"Skipped index: {url} (doesn't exist)\")\n                return None\n    except Exception as e:\n        print(f\"Error fetching {url}: {e}\")\n\ndef parse_debian_version(version):\n    \"\"\"Parse a Debian package version into epoch, upstream version, and revision.\"\"\"\n    match = re.match(r'^(?:(\\d+):)?([^-]+)(?:-(.+))?$', version)\n    if not match:\n        raise ValueError(f\"Invalid Debian version format: {version}\")\n    epoch, upstream, revision = match.groups()\n    return int(epoch) if epoch else 0, upstream, revision or \"\"\n\ndef compare_upstream_version(v1, v2):\n    \"\"\"Compare upstream or revision parts using Debian rules.\"\"\"\n    def tokenize(version):\n        tokens = re.split(r'([0-9]+|[A-Za-z]+)', version)\n        return [int(x) if x.isdigit() else x for x in tokens if x]\n\n    tokens1 = tokenize(v1)\n    tokens2 = tokenize(v2)\n\n    for token1, token2 in zip(tokens1, tokens2):\n        if type(token1) == type(token2):\n            if token1 != token2:\n                return (token1 > token2) - (token1 < token2)\n        else:\n            return -1 if isinstance(token1, str) else 1\n\n    return len(tokens1) - len(tokens2)\n\ndef compare_debian_versions(version1, version2):\n    \"\"\"Compare two Debian package versions.\"\"\"\n    epoch1, upstream1, revision1 = parse_debian_version(version1)\n    epoch2, upstream2, revision2 = parse_debian_version(version2)\n\n    if epoch1 != epoch2:\n        return epoch1 - epoch2\n\n    result = compare_upstream_version(upstream1, upstream2)\n    if result != 0:\n        return result\n\n    return compare_upstream_version(revision1, revision2)\n\ndef resolve_dependencies(packages, aliases, desired_packages):\n    \"\"\"Recursively resolves dependencies for the desired packages.\"\"\"\n    resolved = []\n    to_process = deque(desired_packages)\n\n    while to_process:\n        current = to_process.popleft()\n        resolved_package = current if current in packages else aliases.get(current, [None])[0]\n\n        if not resolved_package:\n            print(f\"Error: Package '{current}' was not found in the available packages.\")\n            sys.exit(1)\n\n        if resolved_package not in resolved:\n            resolved.append(resolved_package)\n\n            deps = packages.get(resolved_package, {}).get(\"Depends\", \"\")\n            if deps:\n                deps = [dep.split(' ')[0] for dep in deps.split(', ') if dep]\n                for dep in deps:\n                    if dep not in resolved and dep not in to_process and dep in packages:\n                        to_process.append(dep)\n\n    return resolved\n\ndef parse_package_index(content):\n    \"\"\"Parses the Packages.gz file and returns package information.\"\"\"\n    packages = {}\n    aliases = {}\n    entries = re.split(r'\\n\\n+', content)\n\n    for entry in entries:\n        fields = dict(re.findall(r'^(\\S+): (.+)$', entry, re.MULTILINE))\n        if \"Package\" in fields:\n            package_name = fields[\"Package\"]\n            version = fields.get(\"Version\")\n            filename = fields.get(\"Filename\")\n            depends = fields.get(\"Depends\")\n            provides = fields.get(\"Provides\", None)\n\n            # Only update if package_name is not in packages or if the new version is higher\n            if package_name not in packages or compare_debian_versions(version, packages[package_name][\"Version\"]) > 0:\n                packages[package_name] = {\n                    \"Version\": version,\n                    \"Filename\": filename,\n                    \"Depends\": depends\n                }\n\n                # Update aliases if package provides any alternatives\n                if provides:\n                    provides_list = [x.strip() for x in provides.split(\",\")]\n                    for alias in provides_list:\n                        # Strip version specifiers\n                        alias_name = re.sub(r'\\s*\\(=.*\\)', '', alias)\n                        if alias_name not in aliases:\n                            aliases[alias_name] = []\n                        if package_name not in aliases[alias_name]:\n                            aliases[alias_name].append(package_name)\n\n    return packages, aliases\n\ndef install_packages(mirror, packages_info, aliases, tmp_dir, extract_dir, ar_tool, desired_packages):\n    \"\"\"Downloads .deb files and extracts them.\"\"\"\n    resolved_packages = resolve_dependencies(packages_info, aliases, desired_packages)\n    print(f\"Resolved packages (including dependencies): {resolved_packages}\")\n\n    packages_to_download = {}\n\n    for pkg in resolved_packages:\n        if pkg in packages_info:\n            packages_to_download[pkg] = packages_info[pkg]\n\n        if pkg in aliases:\n            for alias in aliases[pkg]:\n                if alias in packages_info:\n                    packages_to_download[alias] = packages_info[alias]\n\n    asyncio.run(download_deb_files_parallel(mirror, packages_to_download, tmp_dir))\n\n    package_to_deb_file_map = {}\n    for pkg in resolved_packages:\n        pkg_info = packages_info.get(pkg)\n        if pkg_info:\n            deb_filename = pkg_info.get(\"Filename\")\n            if deb_filename:\n                deb_file_path = os.path.join(tmp_dir, os.path.basename(deb_filename))\n                package_to_deb_file_map[pkg] = deb_file_path\n\n    for pkg in reversed(resolved_packages):\n        deb_file = package_to_deb_file_map.get(pkg)\n        if deb_file and os.path.exists(deb_file):\n            extract_deb_file(deb_file, tmp_dir, extract_dir, ar_tool)\n\n    print(\"All done!\")\n\ndef extract_deb_file(deb_file, tmp_dir, extract_dir, ar_tool):\n    \"\"\"Extract .deb file contents\"\"\"\n\n    os.makedirs(extract_dir, exist_ok=True)\n\n    with tempfile.TemporaryDirectory(dir=tmp_dir) as tmp_subdir:\n        result = subprocess.run(f\"{ar_tool} t {os.path.abspath(deb_file)}\", cwd=tmp_subdir, check=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\n        tar_filename = None\n        for line in result.stdout.decode().splitlines():\n            if line.startswith(\"data.tar\"):\n                tar_filename = line.strip()\n                break\n\n        if not tar_filename:\n            raise FileNotFoundError(f\"Could not find 'data.tar.*' in {deb_file}.\")\n\n        tar_file_path = os.path.join(tmp_subdir, tar_filename)\n        print(f\"Extracting {tar_filename} from {deb_file}..\")\n\n        subprocess.run(f\"{ar_tool} p {os.path.abspath(deb_file)} {tar_filename} > {tar_file_path}\", check=True, shell=True)\n\n        file_extension = os.path.splitext(tar_file_path)[1].lower()\n\n        if file_extension == \".xz\":\n            mode = \"r:xz\"\n        elif file_extension == \".gz\":\n            mode = \"r:gz\"\n        elif file_extension == \".zst\":\n            # zstd is not supported by standard library yet\n            decompressed_tar_path = tar_file_path.replace(\".zst\", \"\")\n            with open(tar_file_path, \"rb\") as zst_file, open(decompressed_tar_path, \"wb\") as decompressed_file:\n                dctx = zstandard.ZstdDecompressor()\n                dctx.copy_stream(zst_file, decompressed_file)\n\n            tar_file_path = decompressed_tar_path\n            mode = \"r\"\n        else:\n            raise ValueError(f\"Unsupported compression format: {file_extension}\")\n\n        with tarfile.open(tar_file_path, mode) as tar:\n            tar.extractall(path=extract_dir, filter='fully_trusted')\n\ndef finalize_setup(rootfsdir):\n    lib_dir = os.path.join(rootfsdir, 'lib')\n    usr_lib_dir = os.path.join(rootfsdir, 'usr', 'lib')\n\n    if os.path.exists(lib_dir):\n        if os.path.islink(lib_dir):\n            os.remove(lib_dir)\n        else:\n            os.makedirs(usr_lib_dir, exist_ok=True)\n\n            for item in os.listdir(lib_dir):\n                src = os.path.join(lib_dir, item)\n                dest = os.path.join(usr_lib_dir, item)\n\n                if os.path.isdir(src):\n                    shutil.copytree(src, dest, dirs_exist_ok=True)\n                else:\n                    shutil.copy2(src, dest)\n\n            shutil.rmtree(lib_dir)\n\n    os.symlink(usr_lib_dir, lib_dir)\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=\"Generate rootfs for .NET runtime on Debian-like OS\")\n    parser.add_argument(\"--distro\", required=False, help=\"Distro name (e.g., debian, ubuntu, etc.)\")\n    parser.add_argument(\"--arch\", required=True, help=\"Architecture (e.g., amd64, loong64, etc.)\")\n    parser.add_argument(\"--rootfsdir\", required=True, help=\"Destination directory.\")\n    parser.add_argument('--suite', required=True, action='append', help='Specify one or more repository suites to collect index data.')\n    parser.add_argument(\"--mirror\", required=False, help=\"Mirror (e.g., http://ftp.debian.org/debian-ports etc.)\")\n    parser.add_argument(\"--artool\", required=False, default=\"ar\", help=\"ar tool to extract debs (e.g., ar, llvm-ar etc.)\")\n    parser.add_argument(\"packages\", nargs=\"+\", help=\"List of package names to be installed.\")\n\n    args = parser.parse_args()\n\n    if args.mirror is None:\n        if args.distro == \"ubuntu\":\n            args.mirror = \"http://archive.ubuntu.com/ubuntu\" if args.arch in [\"amd64\", \"i386\"] else \"http://ports.ubuntu.com/ubuntu-ports\"\n        elif args.distro == \"debian\":\n            args.mirror = \"http://ftp.debian.org/debian-ports\"\n        else:\n            raise Exception(\"Unsupported distro\")\n\n    DESIRED_PACKAGES = args.packages + [ # base packages\n        \"dpkg\",\n        \"busybox\",\n        \"libc-bin\",\n        \"base-files\",\n        \"base-passwd\",\n        \"debianutils\"\n    ]\n\n    print(f\"Creating rootfs. rootfsdir: {args.rootfsdir}, distro: {args.distro}, arch: {args.arch}, suites: {args.suite}, mirror: {args.mirror}\")\n\n    package_index_content = asyncio.run(download_package_index_parallel(args.mirror, args.arch, args.suite))\n\n    packages_info, aliases = parse_package_index(package_index_content)\n\n    with tempfile.TemporaryDirectory() as tmp_dir:\n        install_packages(args.mirror, packages_info, aliases, tmp_dir, args.rootfsdir, args.artool, DESIRED_PACKAGES)\n\n    finalize_setup(args.rootfsdir)\n"
  },
  {
    "path": "eng/common/cross/riscv64/tizen/tizen.patch",
    "content": "diff -u -r a/usr/lib/libc.so b/usr/lib/libc.so\n--- a/usr/lib64/libc.so\t2016-12-30 23:00:08.284951863 +0900\n+++ b/usr/lib64/libc.so\t2016-12-30 23:00:32.140951815 +0900\n@@ -2,4 +2,4 @@\n    Use the shared library, but some functions are only in\n    the static library, so try that secondarily.  */\n OUTPUT_FORMAT(elf64-littleriscv)\n-GROUP ( /lib64/libc.so.6 /usr/lib64/libc_nonshared.a  AS_NEEDED ( /lib64/ld-linux-riscv64-lp64d.so.1 ) )\n+GROUP ( libc.so.6 libc_nonshared.a  AS_NEEDED ( ld-linux-riscv64-lp64d.so.1 ) )\n"
  },
  {
    "path": "eng/common/cross/tizen-build-rootfs.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\nARCH=$1\nLINK_ARCH=$ARCH\n\ncase \"$ARCH\" in\n    arm)\n        TIZEN_ARCH=\"armv7hl\"\n        ;;\n    armel)\n        TIZEN_ARCH=\"armv7l\"\n        LINK_ARCH=\"arm\"\n        ;;\n    arm64)\n        TIZEN_ARCH=\"aarch64\"\n        ;;\n    x86)\n        TIZEN_ARCH=\"i686\"\n        ;;\n    x64)\n        TIZEN_ARCH=\"x86_64\"\n        LINK_ARCH=\"x86\"\n        ;;\n    riscv64)\n        TIZEN_ARCH=\"riscv64\"\n        LINK_ARCH=\"riscv\"\n        ;;\n    *)\n        echo \"Unsupported architecture for tizen: $ARCH\"\n        exit 1\nesac\n\n__CrossDir=$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\n__TIZEN_CROSSDIR=\"$__CrossDir/${ARCH}/tizen\"\n\nif [[ -z \"$ROOTFS_DIR\" ]]; then\n    echo \"ROOTFS_DIR is not defined.\"\n    exit 1;\nfi\n\nTIZEN_TMP_DIR=$ROOTFS_DIR/tizen_tmp\nmkdir -p $TIZEN_TMP_DIR\n\n# Download files\necho \">>Start downloading files\"\nVERBOSE=1 $__CrossDir/tizen-fetch.sh $TIZEN_TMP_DIR $TIZEN_ARCH\necho \"<<Finish downloading files\"\n\necho \">>Start constructing Tizen rootfs\"\nTIZEN_RPM_FILES=`ls $TIZEN_TMP_DIR/*.rpm`\ncd $ROOTFS_DIR\nfor f in $TIZEN_RPM_FILES; do\n    rpm2cpio $f  | cpio -idm --quiet\ndone\necho \"<<Finish constructing Tizen rootfs\"\n\n# Cleanup tmp\nrm -rf $TIZEN_TMP_DIR\n\n# Configure Tizen rootfs\necho \">>Start configuring Tizen rootfs\"\nln -sfn asm-${LINK_ARCH} ./usr/include/asm\npatch -p1 < $__TIZEN_CROSSDIR/tizen.patch\nif [[ \"$TIZEN_ARCH\" == \"riscv64\" ]]; then\n    echo \"Fixing broken symlinks in $PWD\"\n    rm ./usr/lib64/libresolv.so\n    ln -s ../../lib64/libresolv.so.2 ./usr/lib64/libresolv.so\n    rm ./usr/lib64/libpthread.so\n    ln -s ../../lib64/libpthread.so.0 ./usr/lib64/libpthread.so\n    rm ./usr/lib64/libdl.so\n    ln -s ../../lib64/libdl.so.2 ./usr/lib64/libdl.so\n    rm ./usr/lib64/libutil.so\n    ln -s ../../lib64/libutil.so.1 ./usr/lib64/libutil.so\n    rm ./usr/lib64/libm.so\n    ln -s ../../lib64/libm.so.6 ./usr/lib64/libm.so\n    rm ./usr/lib64/librt.so\n    ln -s ../../lib64/librt.so.1 ./usr/lib64/librt.so\n    rm ./lib/ld-linux-riscv64-lp64d.so.1\n    ln -s ../lib64/ld-linux-riscv64-lp64d.so.1 ./lib/ld-linux-riscv64-lp64d.so.1\nfi\necho \"<<Finish configuring Tizen rootfs\"\n"
  },
  {
    "path": "eng/common/cross/tizen-fetch.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\nif [[ -z \"${VERBOSE// }\" ]] || [ \"$VERBOSE\" -ne \"$VERBOSE\" ] 2>/dev/null; then\n    VERBOSE=0\nfi\n\nLog()\n{\n    if [ $VERBOSE -ge 1 ]; then\n        echo ${@:2}\n    fi\n}\n\nInform()\n{\n    Log 1 -e \"\\x1B[0;34m$@\\x1B[m\"\n}\n\nDebug()\n{\n    Log 2 -e \"\\x1B[0;32m$@\\x1B[m\"\n}\n\nError()\n{\n    >&2 Log 0 -e \"\\x1B[0;31m$@\\x1B[m\"\n}\n\nFetch()\n{\n    URL=$1\n    FILE=$2\n    PROGRESS=$3\n    if [ $VERBOSE -ge 1 ] && [ $PROGRESS ]; then\n        CURL_OPT=\"--progress-bar\"\n    else\n        CURL_OPT=\"--silent\"\n    fi\n    curl $CURL_OPT $URL > $FILE\n}\n\nhash curl 2> /dev/null || { Error \"Require 'curl' Aborting.\"; exit 1; }\nhash xmllint 2> /dev/null || { Error \"Require 'xmllint' Aborting.\"; exit 1; }\nhash sha256sum 2> /dev/null || { Error \"Require 'sha256sum' Aborting.\"; exit 1; }\n\nTMPDIR=$1\nif [ ! -d $TMPDIR ]; then\n    TMPDIR=./tizen_tmp\n    Debug \"Create temporary directory : $TMPDIR\"\n    mkdir -p $TMPDIR\nfi\n\nTIZEN_ARCH=$2\n\nTIZEN_URL=http://download.tizen.org/snapshots/TIZEN/Tizen\nBUILD_XML=build.xml\nREPOMD_XML=repomd.xml\nPRIMARY_XML=primary.xml\nTARGET_URL=\"http://__not_initialized\"\n\nXpath_get()\n{\n    XPATH_RESULT=''\n    XPATH=$1\n    XML_FILE=$2\n    RESULT=$(xmllint --xpath $XPATH $XML_FILE)\n    if [[ -z ${RESULT// } ]]; then\n        Error \"Can not find target from $XML_FILE\"\n        Debug \"Xpath = $XPATH\"\n        exit 1\n    fi\n    XPATH_RESULT=$RESULT\n}\n\nfetch_tizen_pkgs_init()\n{\n    TARGET=$1\n    PROFILE=$2\n    Debug \"Initialize TARGET=$TARGET, PROFILE=$PROFILE\"\n\n    TMP_PKG_DIR=$TMPDIR/tizen_${PROFILE}_pkgs\n    if [ -d $TMP_PKG_DIR ]; then rm -rf $TMP_PKG_DIR; fi\n    mkdir -p $TMP_PKG_DIR\n\n    PKG_URL=$TIZEN_URL/$PROFILE/latest\n\n    BUILD_XML_URL=$PKG_URL/$BUILD_XML\n    TMP_BUILD=$TMP_PKG_DIR/$BUILD_XML\n    TMP_REPOMD=$TMP_PKG_DIR/$REPOMD_XML\n    TMP_PRIMARY=$TMP_PKG_DIR/$PRIMARY_XML\n    TMP_PRIMARYGZ=${TMP_PRIMARY}.gz\n\n    Fetch $BUILD_XML_URL $TMP_BUILD\n\n    Debug \"fetch $BUILD_XML_URL to $TMP_BUILD\"\n\n    TARGET_XPATH=\"//build/buildtargets/buildtarget[@name=\\\"$TARGET\\\"]/repo[@type=\\\"binary\\\"]/text()\"\n    Xpath_get $TARGET_XPATH $TMP_BUILD\n    TARGET_PATH=$XPATH_RESULT\n    TARGET_URL=$PKG_URL/$TARGET_PATH\n\n    REPOMD_URL=$TARGET_URL/repodata/repomd.xml\n    PRIMARY_XPATH='string(//*[local-name()=\"data\"][@type=\"primary\"]/*[local-name()=\"location\"]/@href)'\n\n    Fetch $REPOMD_URL $TMP_REPOMD\n\n    Debug \"fetch $REPOMD_URL to $TMP_REPOMD\"\n\n    Xpath_get $PRIMARY_XPATH $TMP_REPOMD\n    PRIMARY_XML_PATH=$XPATH_RESULT\n    PRIMARY_URL=$TARGET_URL/$PRIMARY_XML_PATH\n\n    Fetch $PRIMARY_URL $TMP_PRIMARYGZ\n\n    Debug \"fetch $PRIMARY_URL to $TMP_PRIMARYGZ\"\n\n    gunzip $TMP_PRIMARYGZ\n\n    Debug \"unzip $TMP_PRIMARYGZ to $TMP_PRIMARY\"\n}\n\nfetch_tizen_pkgs()\n{\n    ARCH=$1\n    PACKAGE_XPATH_TPL='string(//*[local-name()=\"metadata\"]/*[local-name()=\"package\"][*[local-name()=\"name\"][text()=\"_PKG_\"]][*[local-name()=\"arch\"][text()=\"_ARCH_\"]]/*[local-name()=\"location\"]/@href)'\n\n    PACKAGE_CHECKSUM_XPATH_TPL='string(//*[local-name()=\"metadata\"]/*[local-name()=\"package\"][*[local-name()=\"name\"][text()=\"_PKG_\"]][*[local-name()=\"arch\"][text()=\"_ARCH_\"]]/*[local-name()=\"checksum\"]/text())'\n\n    for pkg in ${@:2}\n    do\n        Inform \"Fetching... $pkg\"\n        XPATH=${PACKAGE_XPATH_TPL/_PKG_/$pkg}\n        XPATH=${XPATH/_ARCH_/$ARCH}\n        Xpath_get $XPATH $TMP_PRIMARY\n        PKG_PATH=$XPATH_RESULT\n\n        XPATH=${PACKAGE_CHECKSUM_XPATH_TPL/_PKG_/$pkg}\n        XPATH=${XPATH/_ARCH_/$ARCH}\n        Xpath_get $XPATH $TMP_PRIMARY\n        CHECKSUM=$XPATH_RESULT\n\n        PKG_URL=$TARGET_URL/$PKG_PATH\n        PKG_FILE=$(basename $PKG_PATH)\n        PKG_PATH=$TMPDIR/$PKG_FILE\n\n        Debug \"Download $PKG_URL to $PKG_PATH\"\n        Fetch $PKG_URL $PKG_PATH true\n\n        echo \"$CHECKSUM $PKG_PATH\" | sha256sum -c - > /dev/null\n        if [ $? -ne 0 ]; then\n            Error \"Fail to fetch $PKG_URL to $PKG_PATH\"\n            Debug \"Checksum = $CHECKSUM\"\n            exit 1\n        fi\n    done\n}\n\nBASE=\"Tizen-Base\"\nUNIFIED=\"Tizen-Unified\"\n\nInform \"Initialize ${TIZEN_ARCH} base\"\nfetch_tizen_pkgs_init standard $BASE\nInform \"fetch common packages\"\nfetch_tizen_pkgs ${TIZEN_ARCH} gcc gcc-devel-static glibc glibc-devel libicu libicu-devel libatomic linux-glibc-devel keyutils keyutils-devel libkeyutils\nInform \"fetch coreclr packages\"\nfetch_tizen_pkgs ${TIZEN_ARCH} libgcc libstdc++ libstdc++-devel libunwind libunwind-devel lttng-ust-devel lttng-ust userspace-rcu-devel userspace-rcu\nif [ \"$TIZEN_ARCH\" != \"riscv64\" ]; then\n    fetch_tizen_pkgs ${TIZEN_ARCH} lldb lldb-devel\nfi\nInform \"fetch corefx packages\"\nfetch_tizen_pkgs ${TIZEN_ARCH} libcom_err libcom_err-devel zlib zlib-devel libopenssl11 libopenssl1.1-devel krb5 krb5-devel\n\nInform \"Initialize standard unified\"\nfetch_tizen_pkgs_init standard $UNIFIED\nInform \"fetch corefx packages\"\nfetch_tizen_pkgs ${TIZEN_ARCH} gssdp gssdp-devel tizen-release\n\n"
  },
  {
    "path": "eng/common/cross/toolchain.cmake",
    "content": "set(CROSS_ROOTFS $ENV{ROOTFS_DIR})\n\n# reset platform variables (e.g. cmake 3.25 sets LINUX=1)\nunset(LINUX)\nunset(FREEBSD)\nunset(ILLUMOS)\nunset(ANDROID)\nunset(TIZEN)\nunset(HAIKU)\n\nset(TARGET_ARCH_NAME $ENV{TARGET_BUILD_ARCH})\nif(EXISTS ${CROSS_ROOTFS}/bin/freebsd-version)\n  set(CMAKE_SYSTEM_NAME FreeBSD)\n  set(FREEBSD 1)\nelseif(EXISTS ${CROSS_ROOTFS}/usr/platform/i86pc)\n  set(CMAKE_SYSTEM_NAME SunOS)\n  set(ILLUMOS 1)\nelseif(EXISTS ${CROSS_ROOTFS}/boot/system/develop/headers/config/HaikuConfig.h)\n  set(CMAKE_SYSTEM_NAME Haiku)\n  set(HAIKU 1)\nelse()\n  set(CMAKE_SYSTEM_NAME Linux)\n  set(LINUX 1)\nendif()\nset(CMAKE_SYSTEM_VERSION 1)\n\nif(EXISTS ${CROSS_ROOTFS}/etc/tizen-release)\n  set(TIZEN 1)\nelseif(EXISTS ${CROSS_ROOTFS}/android_platform)\n  set(ANDROID 1)\nendif()\n\nif(TARGET_ARCH_NAME STREQUAL \"arm\")\n  set(CMAKE_SYSTEM_PROCESSOR armv7l)\n  if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv7-alpine-linux-musleabihf)\n    set(TOOLCHAIN \"armv7-alpine-linux-musleabihf\")\n  elseif(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv6-alpine-linux-musleabihf)\n    set(TOOLCHAIN \"armv6-alpine-linux-musleabihf\")\n  else()\n    set(TOOLCHAIN \"arm-linux-gnueabihf\")\n  endif()\n  if(TIZEN)\n    set(TIZEN_TOOLCHAIN \"armv7hl-tizen-linux-gnueabihf\")\n  endif()\nelseif(TARGET_ARCH_NAME STREQUAL \"arm64\")\n  set(CMAKE_SYSTEM_PROCESSOR aarch64)\n  if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/aarch64-alpine-linux-musl)\n    set(TOOLCHAIN \"aarch64-alpine-linux-musl\")\n  elseif(LINUX)\n    set(TOOLCHAIN \"aarch64-linux-gnu\")\n    if(TIZEN)\n      set(TIZEN_TOOLCHAIN \"aarch64-tizen-linux-gnu\")\n    endif()\n  elseif(FREEBSD)\n    set(triple \"aarch64-unknown-freebsd12\")\n  endif()\nelseif(TARGET_ARCH_NAME STREQUAL \"armel\")\n  set(CMAKE_SYSTEM_PROCESSOR armv7l)\n  set(TOOLCHAIN \"arm-linux-gnueabi\")\n  if(TIZEN)\n    set(TIZEN_TOOLCHAIN \"armv7l-tizen-linux-gnueabi\")\n  endif()\nelseif(TARGET_ARCH_NAME STREQUAL \"armv6\")\n  set(CMAKE_SYSTEM_PROCESSOR armv6l)\n  if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv6-alpine-linux-musleabihf)\n    set(TOOLCHAIN \"armv6-alpine-linux-musleabihf\")\n  else()\n    set(TOOLCHAIN \"arm-linux-gnueabihf\")\n  endif()\nelseif(TARGET_ARCH_NAME STREQUAL \"loongarch64\")\n  set(CMAKE_SYSTEM_PROCESSOR \"loongarch64\")\n  if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/loongarch64-alpine-linux-musl)\n    set(TOOLCHAIN \"loongarch64-alpine-linux-musl\")\n  else()\n    set(TOOLCHAIN \"loongarch64-linux-gnu\")\n  endif()\nelseif(TARGET_ARCH_NAME STREQUAL \"ppc64le\")\n  set(CMAKE_SYSTEM_PROCESSOR ppc64le)\n  if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/powerpc64le-alpine-linux-musl)\n    set(TOOLCHAIN \"powerpc64le-alpine-linux-musl\")\n  else()\n    set(TOOLCHAIN \"powerpc64le-linux-gnu\")\n  endif()\nelseif(TARGET_ARCH_NAME STREQUAL \"riscv64\")\n  set(CMAKE_SYSTEM_PROCESSOR riscv64)\n  if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/riscv64-alpine-linux-musl)\n    set(TOOLCHAIN \"riscv64-alpine-linux-musl\")\n  else()\n    set(TOOLCHAIN \"riscv64-linux-gnu\")\n    if(TIZEN)\n      set(TIZEN_TOOLCHAIN \"riscv64-tizen-linux-gnu\")\n    endif()\n  endif()\nelseif(TARGET_ARCH_NAME STREQUAL \"s390x\")\n  set(CMAKE_SYSTEM_PROCESSOR s390x)\n  if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/s390x-alpine-linux-musl)\n    set(TOOLCHAIN \"s390x-alpine-linux-musl\")\n  else()\n    set(TOOLCHAIN \"s390x-linux-gnu\")\n  endif()\nelseif(TARGET_ARCH_NAME STREQUAL \"x64\")\n  set(CMAKE_SYSTEM_PROCESSOR x86_64)\n  if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/x86_64-alpine-linux-musl)\n    set(TOOLCHAIN \"x86_64-alpine-linux-musl\")\n  elseif(LINUX)\n    set(TOOLCHAIN \"x86_64-linux-gnu\")\n    if(TIZEN)\n      set(TIZEN_TOOLCHAIN \"x86_64-tizen-linux-gnu\")\n    endif()\n  elseif(FREEBSD)\n    set(triple \"x86_64-unknown-freebsd12\")\n  elseif(ILLUMOS)\n    set(TOOLCHAIN \"x86_64-illumos\")\n  elseif(HAIKU)\n    set(TOOLCHAIN \"x86_64-unknown-haiku\")\n  endif()\nelseif(TARGET_ARCH_NAME STREQUAL \"x86\")\n  set(CMAKE_SYSTEM_PROCESSOR i686)\n  if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/i586-alpine-linux-musl)\n    set(TOOLCHAIN \"i586-alpine-linux-musl\")\n  else()\n    set(TOOLCHAIN \"i686-linux-gnu\")\n  endif()\n  if(TIZEN)\n    set(TIZEN_TOOLCHAIN \"i586-tizen-linux-gnu\")\n  endif()\nelse()\n  message(FATAL_ERROR \"Arch is ${TARGET_ARCH_NAME}. Only arm, arm64, armel, armv6, loongarch64, ppc64le, riscv64, s390x, x64 and x86 are supported!\")\nendif()\n\nif(DEFINED ENV{TOOLCHAIN})\n  set(TOOLCHAIN $ENV{TOOLCHAIN})\nendif()\n\n# Specify include paths\nif(TIZEN)\n  function(find_toolchain_dir prefix)\n    # Dynamically find the version subdirectory\n    file(GLOB DIRECTORIES \"${prefix}/*\")\n    list(GET DIRECTORIES 0 FIRST_MATCH)\n    get_filename_component(TOOLCHAIN_VERSION ${FIRST_MATCH} NAME)\n\n    set(TIZEN_TOOLCHAIN_PATH \"${prefix}/${TOOLCHAIN_VERSION}\" PARENT_SCOPE)\n  endfunction()\n\n  if(TARGET_ARCH_NAME MATCHES \"^(arm|armel|x86)$\")\n    find_toolchain_dir(\"${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}\")\n  else()\n    find_toolchain_dir(\"${CROSS_ROOTFS}/usr/lib64/gcc/${TIZEN_TOOLCHAIN}\")\n  endif()\n\n  message(STATUS \"TIZEN_TOOLCHAIN_PATH set to: ${TIZEN_TOOLCHAIN_PATH}\")\n\n  include_directories(SYSTEM ${TIZEN_TOOLCHAIN_PATH}/include/c++)\n  include_directories(SYSTEM ${TIZEN_TOOLCHAIN_PATH}/include/c++/${TIZEN_TOOLCHAIN})\nendif()\n\nfunction(locate_toolchain_exec exec var)\n    set(TOOLSET_PREFIX ${TOOLCHAIN}-)\n    string(TOUPPER ${exec} EXEC_UPPERCASE)\n    if(NOT \"$ENV{CLR_${EXEC_UPPERCASE}}\" STREQUAL \"\")\n        set(${var} \"$ENV{CLR_${EXEC_UPPERCASE}}\" PARENT_SCOPE)\n        return()\n    endif()\n\n    find_program(EXEC_LOCATION_${exec}\n        NAMES\n        \"${TOOLSET_PREFIX}${exec}${CLR_CMAKE_COMPILER_FILE_NAME_VERSION}\"\n        \"${TOOLSET_PREFIX}${exec}\")\n\n    if (EXEC_LOCATION_${exec} STREQUAL \"EXEC_LOCATION_${exec}-NOTFOUND\")\n        message(FATAL_ERROR \"Unable to find toolchain executable. Name: ${exec}, Prefix: ${TOOLSET_PREFIX}.\")\n    endif()\n    set(${var} ${EXEC_LOCATION_${exec}} PARENT_SCOPE)\nendfunction()\n\nif(ANDROID)\n    if(TARGET_ARCH_NAME STREQUAL \"arm\")\n        set(ANDROID_ABI armeabi-v7a)\n    elseif(TARGET_ARCH_NAME STREQUAL \"arm64\")\n        set(ANDROID_ABI arm64-v8a)\n    endif()\n\n    # extract platform number required by the NDK's toolchain\n    file(READ \"${CROSS_ROOTFS}/android_platform\" RID_FILE_CONTENTS)\n    string(REPLACE \"RID=\" \"\" ANDROID_RID \"${RID_FILE_CONTENTS}\")\n    string(REGEX REPLACE \".*\\\\.([0-9]+)-.*\" \"\\\\1\" ANDROID_PLATFORM \"${ANDROID_RID}\")\n\n    set(ANDROID_TOOLCHAIN clang)\n    set(FEATURE_EVENT_TRACE 0) # disable event trace as there is no lttng-ust package in termux repository\n    set(CMAKE_SYSTEM_LIBRARY_PATH \"${CROSS_ROOTFS}/usr/lib\")\n    set(CMAKE_SYSTEM_INCLUDE_PATH \"${CROSS_ROOTFS}/usr/include\")\n\n    # include official NDK toolchain script\n    include(${CROSS_ROOTFS}/../build/cmake/android.toolchain.cmake)\nelseif(FREEBSD)\n    # we cross-compile by instructing clang\n    set(CMAKE_C_COMPILER_TARGET ${triple})\n    set(CMAKE_CXX_COMPILER_TARGET ${triple})\n    set(CMAKE_ASM_COMPILER_TARGET ${triple})\n    set(CMAKE_SYSROOT \"${CROSS_ROOTFS}\")\n    set(CMAKE_EXE_LINKER_FLAGS \"${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=lld\")\n    set(CMAKE_SHARED_LINKER_FLAGS \"${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=lld\")\n    set(CMAKE_MODULE_LINKER_FLAGS \"${CMAKE_MODULE_LINKER_FLAGS} -fuse-ld=lld\")\nelseif(ILLUMOS)\n    set(CMAKE_SYSROOT \"${CROSS_ROOTFS}\")\n    set(CMAKE_SYSTEM_PREFIX_PATH \"${CROSS_ROOTFS}\")\n    set(CMAKE_C_STANDARD_LIBRARIES \"${CMAKE_C_STANDARD_LIBRARIES} -lssp\")\n    set(CMAKE_CXX_STANDARD_LIBRARIES \"${CMAKE_CXX_STANDARD_LIBRARIES} -lssp\")\n\n    include_directories(SYSTEM ${CROSS_ROOTFS}/include)\n\n    locate_toolchain_exec(gcc CMAKE_C_COMPILER)\n    locate_toolchain_exec(g++ CMAKE_CXX_COMPILER)\nelseif(HAIKU)\n    set(CMAKE_SYSROOT \"${CROSS_ROOTFS}\")\n    set(CMAKE_PROGRAM_PATH \"${CMAKE_PROGRAM_PATH};${CROSS_ROOTFS}/cross-tools-x86_64/bin\")\n    set(CMAKE_SYSTEM_PREFIX_PATH \"${CROSS_ROOTFS}\")\n    set(CMAKE_C_STANDARD_LIBRARIES \"${CMAKE_C_STANDARD_LIBRARIES} -lssp\")\n    set(CMAKE_CXX_STANDARD_LIBRARIES \"${CMAKE_CXX_STANDARD_LIBRARIES} -lssp\")\n\n    locate_toolchain_exec(gcc CMAKE_C_COMPILER)\n    locate_toolchain_exec(g++ CMAKE_CXX_COMPILER)\n\n    # let CMake set up the correct search paths\n    include(Platform/Haiku)\nelse()\n    set(CMAKE_SYSROOT \"${CROSS_ROOTFS}\")\n\n    set(CMAKE_C_COMPILER_EXTERNAL_TOOLCHAIN \"${CROSS_ROOTFS}/usr\")\n    set(CMAKE_CXX_COMPILER_EXTERNAL_TOOLCHAIN \"${CROSS_ROOTFS}/usr\")\n    set(CMAKE_ASM_COMPILER_EXTERNAL_TOOLCHAIN \"${CROSS_ROOTFS}/usr\")\nendif()\n\n# Specify link flags\n\nfunction(add_toolchain_linker_flag Flag)\n  set(Config \"${ARGV1}\")\n  set(CONFIG_SUFFIX \"\")\n  if (NOT Config STREQUAL \"\")\n    set(CONFIG_SUFFIX \"_${Config}\")\n  endif()\n  set(\"CMAKE_EXE_LINKER_FLAGS${CONFIG_SUFFIX}_INIT\" \"${CMAKE_EXE_LINKER_FLAGS${CONFIG_SUFFIX}_INIT} ${Flag}\" PARENT_SCOPE)\n  set(\"CMAKE_SHARED_LINKER_FLAGS${CONFIG_SUFFIX}_INIT\" \"${CMAKE_SHARED_LINKER_FLAGS${CONFIG_SUFFIX}_INIT} ${Flag}\" PARENT_SCOPE)\nendfunction()\n\nif(LINUX)\n  add_toolchain_linker_flag(\"-Wl,--rpath-link=${CROSS_ROOTFS}/lib/${TOOLCHAIN}\")\n  add_toolchain_linker_flag(\"-Wl,--rpath-link=${CROSS_ROOTFS}/usr/lib/${TOOLCHAIN}\")\nendif()\n\nif(TARGET_ARCH_NAME MATCHES \"^(arm|armel)$\")\n  if(TIZEN)\n    add_toolchain_linker_flag(\"-B${TIZEN_TOOLCHAIN_PATH}\")\n    add_toolchain_linker_flag(\"-L${CROSS_ROOTFS}/lib\")\n    add_toolchain_linker_flag(\"-L${CROSS_ROOTFS}/usr/lib\")\n    add_toolchain_linker_flag(\"-L${TIZEN_TOOLCHAIN_PATH}\")\n  endif()\nelseif(TARGET_ARCH_NAME MATCHES \"^(arm64|x64|riscv64)$\")\n  if(TIZEN)\n    add_toolchain_linker_flag(\"-B${TIZEN_TOOLCHAIN_PATH}\")\n    add_toolchain_linker_flag(\"-L${CROSS_ROOTFS}/lib64\")\n    add_toolchain_linker_flag(\"-L${CROSS_ROOTFS}/usr/lib64\")\n    add_toolchain_linker_flag(\"-L${TIZEN_TOOLCHAIN_PATH}\")\n\n    add_toolchain_linker_flag(\"-Wl,--rpath-link=${CROSS_ROOTFS}/lib64\")\n    add_toolchain_linker_flag(\"-Wl,--rpath-link=${CROSS_ROOTFS}/usr/lib64\")\n    add_toolchain_linker_flag(\"-Wl,--rpath-link=${TIZEN_TOOLCHAIN_PATH}\")\n  endif()\nelseif(TARGET_ARCH_NAME STREQUAL \"s390x\")\n  add_toolchain_linker_flag(\"--target=${TOOLCHAIN}\")\nelseif(TARGET_ARCH_NAME STREQUAL \"x86\")\n  if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/i586-alpine-linux-musl)\n    add_toolchain_linker_flag(\"--target=${TOOLCHAIN}\")\n    add_toolchain_linker_flag(\"-Wl,--rpath-link=${CROSS_ROOTFS}/usr/lib/gcc/${TOOLCHAIN}\")\n  endif()\n  add_toolchain_linker_flag(-m32)\n  if(TIZEN)\n    add_toolchain_linker_flag(\"-B${TIZEN_TOOLCHAIN_PATH}\")\n    add_toolchain_linker_flag(\"-L${CROSS_ROOTFS}/lib\")\n    add_toolchain_linker_flag(\"-L${CROSS_ROOTFS}/usr/lib\")\n    add_toolchain_linker_flag(\"-L${TIZEN_TOOLCHAIN_PATH}\")\n  endif()\nelseif(ILLUMOS)\n  add_toolchain_linker_flag(\"-L${CROSS_ROOTFS}/lib/amd64\")\n  add_toolchain_linker_flag(\"-L${CROSS_ROOTFS}/usr/amd64/lib\")\nelseif(HAIKU)\n  add_toolchain_linker_flag(\"-lnetwork\")\n  add_toolchain_linker_flag(\"-lroot\")\nendif()\n\n# Specify compile options\n\nif((TARGET_ARCH_NAME MATCHES \"^(arm|arm64|armel|armv6|loongarch64|ppc64le|riscv64|s390x|x64|x86)$\" AND NOT ANDROID AND NOT FREEBSD) OR ILLUMOS OR HAIKU)\n  set(CMAKE_C_COMPILER_TARGET ${TOOLCHAIN})\n  set(CMAKE_CXX_COMPILER_TARGET ${TOOLCHAIN})\n  set(CMAKE_ASM_COMPILER_TARGET ${TOOLCHAIN})\nendif()\n\nif(TARGET_ARCH_NAME MATCHES \"^(arm|armel)$\")\n  add_compile_options(-mthumb)\n  if (NOT DEFINED CLR_ARM_FPU_TYPE)\n    set (CLR_ARM_FPU_TYPE vfpv3)\n  endif (NOT DEFINED CLR_ARM_FPU_TYPE)\n\n  add_compile_options (-mfpu=${CLR_ARM_FPU_TYPE})\n  if (NOT DEFINED CLR_ARM_FPU_CAPABILITY)\n    set (CLR_ARM_FPU_CAPABILITY 0x7)\n  endif (NOT DEFINED CLR_ARM_FPU_CAPABILITY)\n\n  add_definitions (-DCLR_ARM_FPU_CAPABILITY=${CLR_ARM_FPU_CAPABILITY})\n\n  # persist variables across multiple try_compile passes\n  list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES CLR_ARM_FPU_TYPE CLR_ARM_FPU_CAPABILITY)\n\n  if(TARGET_ARCH_NAME STREQUAL \"armel\")\n    add_compile_options(-mfloat-abi=softfp)\n  endif()\nelseif(TARGET_ARCH_NAME STREQUAL \"s390x\")\n  add_compile_options(\"--target=${TOOLCHAIN}\")\nelseif(TARGET_ARCH_NAME STREQUAL \"x86\")\n  if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/i586-alpine-linux-musl)\n    add_compile_options(--target=${TOOLCHAIN})\n  endif()\n  add_compile_options(-m32)\n  add_compile_options(-Wno-error=unused-command-line-argument)\nendif()\n\nif(TIZEN)\n  if(TARGET_ARCH_NAME MATCHES \"^(arm|armel|arm64|x86)$\")\n    add_compile_options(-Wno-deprecated-declarations) # compile-time option\n    add_compile_options(-D__extern_always_inline=inline) # compile-time option\n  endif()\nendif()\n\n# Set LLDB include and library paths for builds that need lldb.\nif(TARGET_ARCH_NAME MATCHES \"^(arm|armel|x86)$\")\n  if(TARGET_ARCH_NAME STREQUAL \"x86\")\n    set(LLVM_CROSS_DIR \"$ENV{LLVM_CROSS_HOME}\")\n  else() # arm/armel case\n    set(LLVM_CROSS_DIR \"$ENV{LLVM_ARM_HOME}\")\n  endif()\n  if(LLVM_CROSS_DIR)\n    set(WITH_LLDB_LIBS \"${LLVM_CROSS_DIR}/lib/\" CACHE STRING \"\")\n    set(WITH_LLDB_INCLUDES \"${LLVM_CROSS_DIR}/include\" CACHE STRING \"\")\n    set(LLDB_H \"${WITH_LLDB_INCLUDES}\" CACHE STRING \"\")\n    set(LLDB \"${LLVM_CROSS_DIR}/lib/liblldb.so\" CACHE STRING \"\")\n  else()\n    if(TARGET_ARCH_NAME STREQUAL \"x86\")\n      set(WITH_LLDB_LIBS \"${CROSS_ROOTFS}/usr/lib/i386-linux-gnu\" CACHE STRING \"\")\n      set(CHECK_LLVM_DIR \"${CROSS_ROOTFS}/usr/lib/llvm-3.8/include\")\n      if(EXISTS \"${CHECK_LLVM_DIR}\" AND IS_DIRECTORY \"${CHECK_LLVM_DIR}\")\n        set(WITH_LLDB_INCLUDES \"${CHECK_LLVM_DIR}\")\n      else()\n        set(WITH_LLDB_INCLUDES \"${CROSS_ROOTFS}/usr/lib/llvm-3.6/include\")\n      endif()\n    else() # arm/armel case\n      set(WITH_LLDB_LIBS \"${CROSS_ROOTFS}/usr/lib/${TOOLCHAIN}\" CACHE STRING \"\")\n      set(WITH_LLDB_INCLUDES \"${CROSS_ROOTFS}/usr/lib/llvm-3.6/include\" CACHE STRING \"\")\n    endif()\n  endif()\nendif()\n\n# Set C++ standard library options if specified\nset(CLR_CMAKE_CXX_STANDARD_LIBRARY \"\" CACHE STRING \"Standard library flavor to link against. Only supported with the Clang compiler.\")\nif (CLR_CMAKE_CXX_STANDARD_LIBRARY)\n  add_compile_options($<$<COMPILE_LANG_AND_ID:CXX,Clang>:--stdlib=${CLR_CMAKE_CXX_STANDARD_LIBRARY}>)\n  add_link_options($<$<LINK_LANG_AND_ID:CXX,Clang>:--stdlib=${CLR_CMAKE_CXX_STANDARD_LIBRARY}>)\nendif()\n\noption(CLR_CMAKE_CXX_STANDARD_LIBRARY_STATIC \"Statically link against the C++ standard library\" OFF)\nif(CLR_CMAKE_CXX_STANDARD_LIBRARY_STATIC)\n  add_link_options($<$<LINK_LANGUAGE:CXX>:-static-libstdc++>)\nendif()\n\nset(CLR_CMAKE_CXX_ABI_LIBRARY \"\" CACHE STRING \"C++ ABI implementation library to link against. Only supported with the Clang compiler.\")\nif (CLR_CMAKE_CXX_ABI_LIBRARY)\n  # The user may specify the ABI library with the 'lib' prefix, like 'libstdc++'. Strip the prefix here so the linker finds the right library.\n  string(REGEX REPLACE \"^lib(.+)\" \"\\\\1\" CLR_CMAKE_CXX_ABI_LIBRARY ${CLR_CMAKE_CXX_ABI_LIBRARY})\n  # We need to specify this as a linker-backend option as Clang will filter this option out when linking to libc++.\n  add_link_options(\"LINKER:-l${CLR_CMAKE_CXX_ABI_LIBRARY}\")\nendif()\n\nset(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\nset(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n"
  },
  {
    "path": "eng/common/darc-init.ps1",
    "content": "param (\n    $darcVersion = $null,\n    $versionEndpoint = 'https://maestro.dot.net/api/assets/darc-version?api-version=2020-02-20',\n    $verbosity = 'minimal',\n    $toolpath = $null\n)\n\n. $PSScriptRoot\\tools.ps1\n\nfunction InstallDarcCli ($darcVersion, $toolpath) {\n  $darcCliPackageName = 'microsoft.dotnet.darc'\n\n  $dotnetRoot = InitializeDotNetCli -install:$true\n  $dotnet = \"$dotnetRoot\\dotnet.exe\"\n  $toolList = & \"$dotnet\" tool list -g\n\n  if ($toolList -like \"*$darcCliPackageName*\") {\n    & \"$dotnet\" tool uninstall $darcCliPackageName -g\n  }\n\n  # If the user didn't explicitly specify the darc version,\n  # query the Maestro API for the correct version of darc to install.\n  if (-not $darcVersion) {\n    $darcVersion = $(Invoke-WebRequest -Uri $versionEndpoint -UseBasicParsing).Content\n  }\n\n  $arcadeServicesSource = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json'\n\n  Write-Host \"Installing Darc CLI version $darcVersion...\"\n  Write-Host 'You may need to restart your command window if this is the first dotnet tool you have installed.'\n  if (-not $toolpath) {\n    Write-Host \"'$dotnet' tool install $darcCliPackageName --version $darcVersion --add-source '$arcadeServicesSource' -v $verbosity -g\"\n    & \"$dotnet\" tool install $darcCliPackageName --version $darcVersion --add-source \"$arcadeServicesSource\" -v $verbosity -g\n  }else {\n    Write-Host \"'$dotnet' tool install $darcCliPackageName --version $darcVersion --add-source '$arcadeServicesSource' -v $verbosity --tool-path '$toolpath'\"\n    & \"$dotnet\" tool install $darcCliPackageName --version $darcVersion --add-source \"$arcadeServicesSource\" -v $verbosity --tool-path \"$toolpath\"\n  }\n}\n\ntry {\n  InstallDarcCli $darcVersion $toolpath\n}\ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Category 'Darc' -Message $_\n  ExitWithExitCode 1\n}"
  },
  {
    "path": "eng/common/darc-init.sh",
    "content": "#!/usr/bin/env bash\n\nsource=\"${BASH_SOURCE[0]}\"\ndarcVersion=''\nversionEndpoint='https://maestro.dot.net/api/assets/darc-version?api-version=2020-02-20'\nverbosity='minimal'\n\nwhile [[ $# -gt 0 ]]; do\n  opt=\"$(echo \"$1\" | tr \"[:upper:]\" \"[:lower:]\")\"\n  case \"$opt\" in\n    --darcversion)\n      darcVersion=$2\n      shift\n      ;;\n    --versionendpoint)\n      versionEndpoint=$2\n      shift\n      ;;\n    --verbosity)\n      verbosity=$2\n      shift\n      ;;\n    --toolpath)\n      toolpath=$2\n      shift\n      ;;\n    *)\n      echo \"Invalid argument: $1\"\n      usage\n      exit 1\n      ;;\n  esac\n\n  shift\ndone\n\n# resolve $source until the file is no longer a symlink\nwhile [[ -h \"$source\" ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\n. \"$scriptroot/tools.sh\"\n\nif [ -z \"$darcVersion\" ]; then\n  darcVersion=$(curl -X GET \"$versionEndpoint\" -H \"accept: text/plain\")\nfi\n\nfunction InstallDarcCli {\n  local darc_cli_package_name=\"microsoft.dotnet.darc\"\n\n  InitializeDotNetCli true\n  local dotnet_root=$_InitializeDotNetCli\n\n  if [ -z \"$toolpath\" ]; then\n    local tool_list=$($dotnet_root/dotnet tool list -g)\n    if [[ $tool_list = *$darc_cli_package_name* ]]; then\n      echo $($dotnet_root/dotnet tool uninstall $darc_cli_package_name -g)\n    fi\n  else\n    local tool_list=$($dotnet_root/dotnet tool list --tool-path \"$toolpath\")\n    if [[ $tool_list = *$darc_cli_package_name* ]]; then\n      echo $($dotnet_root/dotnet tool uninstall $darc_cli_package_name --tool-path \"$toolpath\")\n    fi\n  fi\n\n  local arcadeServicesSource=\"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json\"\n\n  echo \"Installing Darc CLI version $darcVersion...\"\n  echo \"You may need to restart your command shell if this is the first dotnet tool you have installed.\"\n  if [ -z \"$toolpath\" ]; then\n    echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source \"$arcadeServicesSource\" -v $verbosity -g)\n  else\n    echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source \"$arcadeServicesSource\" -v $verbosity --tool-path \"$toolpath\")\n  fi\n}\n\nInstallDarcCli\n"
  },
  {
    "path": "eng/common/dotnet-install.cmd",
    "content": "@echo off\npowershell -ExecutionPolicy ByPass -NoProfile -command \"& \"\"\"%~dp0dotnet-install.ps1\"\"\" %*\""
  },
  {
    "path": "eng/common/dotnet-install.ps1",
    "content": "[CmdletBinding(PositionalBinding=$false)]\nParam(\n  [string] $verbosity = 'minimal',\n  [string] $architecture = '',\n  [string] $version = 'Latest',\n  [string] $runtime = 'dotnet',\n  [string] $RuntimeSourceFeed = '',\n  [string] $RuntimeSourceFeedKey = ''\n)\n\n. $PSScriptRoot\\tools.ps1\n\n$dotnetRoot = Join-Path $RepoRoot '.dotnet'\n\n$installdir = $dotnetRoot\ntry {\n    if ($architecture -and $architecture.Trim() -eq 'x86') {\n        $installdir = Join-Path $installdir 'x86'\n    }\n    InstallDotNet $installdir $version $architecture $runtime $true -RuntimeSourceFeed $RuntimeSourceFeed -RuntimeSourceFeedKey $RuntimeSourceFeedKey\n}\ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Category 'InitializeToolset' -Message $_\n  ExitWithExitCode 1\n}\n\nExitWithExitCode 0\n"
  },
  {
    "path": "eng/common/dotnet-install.sh",
    "content": "#!/usr/bin/env bash\n\nsource=\"${BASH_SOURCE[0]}\"\n# resolve $source until the file is no longer a symlink\nwhile [[ -h \"$source\" ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\n. \"$scriptroot/tools.sh\"\n\nversion='Latest'\narchitecture=''\nruntime='dotnet'\nruntimeSourceFeed=''\nruntimeSourceFeedKey=''\nwhile [[ $# -gt 0 ]]; do\n  opt=\"$(echo \"$1\" | tr \"[:upper:]\" \"[:lower:]\")\"\n  case \"$opt\" in\n    -version|-v)\n      shift\n      version=\"$1\"\n      ;;\n    -architecture|-a)\n      shift\n      architecture=\"$1\"\n      ;;\n    -runtime|-r)\n      shift\n      runtime=\"$1\"\n      ;;\n    -runtimesourcefeed)\n      shift\n      runtimeSourceFeed=\"$1\"\n      ;;\n    -runtimesourcefeedkey)\n      shift\n      runtimeSourceFeedKey=\"$1\"\n      ;;\n    *)\n      Write-PipelineTelemetryError -Category 'Build' -Message \"Invalid argument: $1\"\n      exit 1\n      ;;\n  esac\n  shift\ndone\n\n# Use uname to determine what the CPU is, see https://en.wikipedia.org/wiki/Uname#Examples\ncpuname=$(uname -m)\ncase $cpuname in\n  arm64|aarch64)\n    buildarch=arm64\n    if [ \"$(getconf LONG_BIT)\" -lt 64 ]; then\n        # This is 32-bit OS running on 64-bit CPU (for example Raspberry Pi OS)\n        buildarch=arm\n    fi\n    ;;\n  loongarch64)\n    buildarch=loongarch64\n    ;;\n  amd64|x86_64)\n    buildarch=x64\n    ;;\n  armv*l)\n    buildarch=arm\n    ;;\n  i[3-6]86)\n    buildarch=x86\n    ;;\n  riscv64)\n    buildarch=riscv64\n    ;;\n  *)\n    echo \"Unknown CPU $cpuname detected, treating it as x64\"\n    buildarch=x64\n    ;;\nesac\n\ndotnetRoot=\"${repo_root}.dotnet\"\nif [[ $architecture != \"\" ]] && [[ $architecture != $buildarch ]]; then\n  dotnetRoot=\"$dotnetRoot/$architecture\"\nfi\n\nInstallDotNet \"$dotnetRoot\" $version \"$architecture\" $runtime true $runtimeSourceFeed $runtimeSourceFeedKey || {\n  local exit_code=$?\n  Write-PipelineTelemetryError -Category 'InitializeToolset' -Message \"dotnet-install.sh failed (exit code '$exit_code').\" >&2\n  ExitWithExitCode $exit_code\n}\n\nExitWithExitCode 0\n"
  },
  {
    "path": "eng/common/dotnet.cmd",
    "content": "@echo off\n\n:: This script is used to install the .NET SDK.\n:: It will also invoke the SDK with any provided arguments.\n\npowershell -ExecutionPolicy ByPass -NoProfile -command \"& \"\"\"%~dp0dotnet.ps1\"\"\" %*\"\nexit /b %ErrorLevel%\n"
  },
  {
    "path": "eng/common/dotnet.ps1",
    "content": "# This script is used to install the .NET SDK.\n# It will also invoke the SDK with any provided arguments.\n\n. $PSScriptRoot\\tools.ps1\n$dotnetRoot = InitializeDotNetCli -install:$true\n\n# Invoke acquired SDK with args if they are provided\nif ($args.count -gt 0) {\n  $env:DOTNET_NOLOGO=1\n  & \"$dotnetRoot\\dotnet.exe\" $args\n}\n"
  },
  {
    "path": "eng/common/dotnet.sh",
    "content": "#!/usr/bin/env bash\n\n# This script is used to install the .NET SDK.\n# It will also invoke the SDK with any provided arguments.\n\nsource=\"${BASH_SOURCE[0]}\"\n# resolve $SOURCE until the file is no longer a symlink\nwhile [[ -h $source ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\nsource $scriptroot/tools.sh\nInitializeDotNetCli true # install\n\n# Invoke acquired SDK with args if they are provided\nif [[ $# -gt 0 ]]; then\n  __dotnetDir=${_InitializeDotNetCli}\n  dotnetPath=${__dotnetDir}/dotnet\n  ${dotnetPath} \"$@\"\nfi\n"
  },
  {
    "path": "eng/common/enable-cross-org-publishing.ps1",
    "content": "param(\n  [string] $token\n)\n\n\n. $PSScriptRoot\\pipeline-logging-functions.ps1\n\n# Write-PipelineSetVariable will no-op if a variable named $ci is not defined\n# Since this script is only ever called in AzDO builds, just universally set it\n$ci = $true\n\nWrite-PipelineSetVariable -Name 'VSS_NUGET_ACCESSTOKEN' -Value $token -IsMultiJobVariable $false\nWrite-PipelineSetVariable -Name 'VSS_NUGET_URI_PREFIXES' -Value 'https://dnceng.pkgs.visualstudio.com/;https://pkgs.dev.azure.com/dnceng/;https://devdiv.pkgs.visualstudio.com/;https://pkgs.dev.azure.com/devdiv/' -IsMultiJobVariable $false\n"
  },
  {
    "path": "eng/common/generate-locproject.ps1",
    "content": "Param(\n    [Parameter(Mandatory=$true)][string] $SourcesDirectory,     # Directory where source files live; if using a Localize directory it should live in here\n    [string] $LanguageSet = 'VS_Main_Languages',                # Language set to be used in the LocProject.json\n    [switch] $UseCheckedInLocProjectJson,                       # When set, generates a LocProject.json and compares it to one that already exists in the repo; otherwise just generates one\n    [switch] $CreateNeutralXlfs                                 # Creates neutral xlf files. Only set to false when running locally\n)\n\n# Generates LocProject.json files for the OneLocBuild task. OneLocBuildTask is described here:\n# https://ceapex.visualstudio.com/CEINTL/_wiki/wikis/CEINTL.wiki/107/Localization-with-OneLocBuild-Task\n\nSet-StrictMode -Version 2.0\n$ErrorActionPreference = \"Stop\"\n. $PSScriptRoot\\pipeline-logging-functions.ps1\n\n$exclusionsFilePath = \"$SourcesDirectory\\eng\\Localize\\LocExclusions.json\"\n$exclusions = @{ Exclusions = @() }\nif (Test-Path -Path $exclusionsFilePath)\n{\n    $exclusions = Get-Content \"$exclusionsFilePath\" | ConvertFrom-Json\n}\n\nPush-Location \"$SourcesDirectory\" # push location for Resolve-Path -Relative to work\n\n# Template files\n$jsonFiles = @()\n$jsonTemplateFiles = Get-ChildItem -Recurse -Path \"$SourcesDirectory\" | Where-Object { $_.FullName -Match \"\\.template\\.config\\\\localize\\\\.+\\.en\\.json\" } # .NET templating pattern\n$jsonTemplateFiles | ForEach-Object {\n    $null = $_.Name -Match \"(.+)\\.[\\w-]+\\.json\" # matches '[filename].[langcode].json\n\n    $destinationFile = \"$($_.Directory.FullName)\\$($Matches.1).json\"\n    $jsonFiles += Copy-Item \"$($_.FullName)\" -Destination $destinationFile -PassThru\n}\n\n$jsonWinformsTemplateFiles = Get-ChildItem -Recurse -Path \"$SourcesDirectory\" | Where-Object { $_.FullName -Match \"en\\\\strings\\.json\" } # current winforms pattern\n\n$wxlFilesV3 = @()\n$wxlFilesV5 = @()\n$wxlFiles = Get-ChildItem -Recurse -Path \"$SourcesDirectory\" | Where-Object { $_.FullName -Match \"\\\\.+\\.wxl\" -And -Not( $_.Directory.Name -Match \"\\d{4}\" ) } # localized files live in four digit lang ID directories; this excludes them\nif (-not $wxlFiles) {\n    $wxlEnFiles = Get-ChildItem -Recurse -Path \"$SourcesDirectory\" | Where-Object { $_.FullName -Match \"\\\\1033\\\\.+\\.wxl\" } #  pick up en files (1033 = en) specifically so we can copy them to use as the neutral xlf files\n    if ($wxlEnFiles) {\n        $wxlFiles = @()\n        $wxlEnFiles | ForEach-Object {\n            $destinationFile = \"$($_.Directory.Parent.FullName)\\$($_.Name)\"\n            $content = Get-Content $_.FullName -Raw\n\n            # Split files on schema to select different parser settings in the generated project.\n            if ($content -like \"*http://wixtoolset.org/schemas/v4/wxl*\")\n            {\n                $wxlFilesV5 += Copy-Item $_.FullName -Destination $destinationFile -PassThru\n            }\n            elseif ($content -like \"*http://schemas.microsoft.com/wix/2006/localization*\")\n            {\n                $wxlFilesV3 += Copy-Item $_.FullName -Destination $destinationFile -PassThru\n            }\n        }\n    }\n}\n\n$macosHtmlEnFiles = Get-ChildItem -Recurse -Path \"$SourcesDirectory\" | Where-Object { $_.FullName -Match \"en\\.lproj\\\\.+\\.html$\" } # add installer HTML files\n$macosHtmlFiles = @()\nif ($macosHtmlEnFiles) {\n    $macosHtmlEnFiles | ForEach-Object {\n        $destinationFile = \"$($_.Directory.Parent.FullName)\\$($_.Name)\"\n        $macosHtmlFiles += Copy-Item \"$($_.FullName)\" -Destination $destinationFile -PassThru\n    }\n}\n\n$xlfFiles = @()\n\n$allXlfFiles = Get-ChildItem -Recurse -Path \"$SourcesDirectory\\*\\*.xlf\"\n$langXlfFiles = @()\nif ($allXlfFiles) {\n    $null = $allXlfFiles[0].FullName -Match \"\\.([\\w-]+)\\.xlf\" # matches '[langcode].xlf'\n    $firstLangCode = $Matches.1\n    $langXlfFiles = Get-ChildItem -Recurse -Path \"$SourcesDirectory\\*\\*.$firstLangCode.xlf\"\n}\n$langXlfFiles | ForEach-Object {\n    $null = $_.Name -Match \"(.+)\\.[\\w-]+\\.xlf\" # matches '[filename].[langcode].xlf\n\n    $destinationFile = \"$($_.Directory.FullName)\\$($Matches.1).xlf\"\n    $xlfFiles += Copy-Item \"$($_.FullName)\" -Destination $destinationFile -PassThru\n}\n\n$locFiles = $jsonFiles + $jsonWinformsTemplateFiles + $xlfFiles\n\n$locJson = @{\n    Projects = @(\n        @{\n            LanguageSet = $LanguageSet\n            LocItems = @(\n                $locFiles | ForEach-Object {\n                    $outputPath = \"$(($_.DirectoryName | Resolve-Path -Relative) + \"\\\")\"\n                    $continue = $true\n                    foreach ($exclusion in $exclusions.Exclusions) {\n                        if ($_.FullName.Contains($exclusion))\n                        {\n                            $continue = $false\n                        }\n                    }\n                    $sourceFile = ($_.FullName | Resolve-Path -Relative)\n                    if (!$CreateNeutralXlfs -and $_.Extension -eq '.xlf') {\n                        Remove-Item -Path $sourceFile\n                    }\n                    if ($continue)\n                    {\n                        if ($_.Directory.Name -eq 'en' -and $_.Extension -eq '.json') {\n                            return @{\n                                SourceFile = $sourceFile\n                                CopyOption = \"LangIDOnPath\"\n                                OutputPath = \"$($_.Directory.Parent.FullName | Resolve-Path -Relative)\\\"\n                            }\n                        } else {\n                            return @{\n                                SourceFile = $sourceFile\n                                CopyOption = \"LangIDOnName\"\n                                OutputPath = $outputPath\n                            }\n                        }\n                    }\n                }\n            )\n        },\n        @{\n            LanguageSet = $LanguageSet\n            CloneLanguageSet = \"WiX_CloneLanguages\"\n            LssFiles = @( \"wxl_loc.lss\" )\n            LocItems = @(\n                $wxlFilesV3 | ForEach-Object {\n                    $outputPath = \"$($_.Directory.FullName | Resolve-Path -Relative)\\\"\n                    $continue = $true\n                    foreach ($exclusion in $exclusions.Exclusions) {\n                        if ($_.FullName.Contains($exclusion)) {\n                            $continue = $false\n                        }\n                    }\n                    $sourceFile = ($_.FullName | Resolve-Path -Relative)\n                    if ($continue)\n                    {\n                        return @{\n                            SourceFile = $sourceFile\n                            CopyOption = \"LangIDOnPath\"\n                            OutputPath = $outputPath\n                        }\n                    }\n                }\n            )\n        },\n        @{\n            LanguageSet = $LanguageSet\n            CloneLanguageSet = \"WiX_CloneLanguages\"\n            LssFiles = @( \"P210WxlSchemaV4.lss\" )\n            LocItems = @(\n                $wxlFilesV5 | ForEach-Object {\n                    $outputPath = \"$($_.Directory.FullName | Resolve-Path -Relative)\\\"\n                    $continue = $true\n                    foreach ($exclusion in $exclusions.Exclusions) {\n                        if ($_.FullName.Contains($exclusion)) {\n                            $continue = $false\n                        }\n                    }\n                    $sourceFile = ($_.FullName | Resolve-Path -Relative)\n                    if ($continue)\n                    {\n                        return @{\n                            SourceFile = $sourceFile\n                            CopyOption = \"LangIDOnPath\"\n                            OutputPath = $outputPath\n                        }\n                    }\n                }\n            )\n        },\n        @{\n            LanguageSet = $LanguageSet\n            CloneLanguageSet = \"VS_macOS_CloneLanguages\"\n            LssFiles = @( \".\\eng\\common\\loc\\P22DotNetHtmlLocalization.lss\" )\n            LocItems = @(\n                $macosHtmlFiles | ForEach-Object {\n                    $outputPath = \"$($_.Directory.FullName | Resolve-Path -Relative)\\\"\n                    $continue = $true\n                    foreach ($exclusion in $exclusions.Exclusions) {\n                        if ($_.FullName.Contains($exclusion)) {\n                            $continue = $false\n                        }\n                    }\n                    $sourceFile = ($_.FullName | Resolve-Path -Relative)\n                    $lciFile = $sourceFile + \".lci\"\n                    if ($continue) {\n                        $result = @{\n                            SourceFile = $sourceFile\n                            CopyOption = \"LangIDOnPath\"\n                            OutputPath = $outputPath\n                        }\n                        if (Test-Path $lciFile -PathType Leaf) {\n                            $result[\"LciFile\"] = $lciFile\n                        }\n                        return $result\n                    }\n                }\n            )\n        }\n    )\n}\n\n$json = ConvertTo-Json $locJson -Depth 5\nWrite-Host \"LocProject.json generated:`n`n$json`n`n\"\nPop-Location\n\nif (!$UseCheckedInLocProjectJson) {\n    New-Item \"$SourcesDirectory\\eng\\Localize\\LocProject.json\" -Force # Need this to make sure the Localize directory is created\n    Set-Content \"$SourcesDirectory\\eng\\Localize\\LocProject.json\" $json\n}\nelse {\n    New-Item \"$SourcesDirectory\\eng\\Localize\\LocProject-generated.json\" -Force # Need this to make sure the Localize directory is created\n    Set-Content \"$SourcesDirectory\\eng\\Localize\\LocProject-generated.json\" $json\n\n    if ((Get-FileHash \"$SourcesDirectory\\eng\\Localize\\LocProject-generated.json\").Hash -ne (Get-FileHash \"$SourcesDirectory\\eng\\Localize\\LocProject.json\").Hash) {\n        Write-PipelineTelemetryError -Category \"OneLocBuild\" -Message \"Existing LocProject.json differs from generated LocProject.json. Download LocProject-generated.json and compare them.\"\n\n        exit 1\n    }\n    else {\n        Write-Host \"Generated LocProject.json and current LocProject.json are identical.\"\n    }\n}\n"
  },
  {
    "path": "eng/common/generate-sbom-prep.ps1",
    "content": "Param(\n    [Parameter(Mandatory=$true)][string] $ManifestDirPath    # Manifest directory where sbom will be placed\n)\n\n. $PSScriptRoot\\pipeline-logging-functions.ps1\n\n# Normally - we'd listen to the manifest path given, but 1ES templates will overwrite if this level gets uploaded directly\n# with their own overwriting ours. So we create it as a sub directory of the requested manifest path.\n$ArtifactName = \"${env:SYSTEM_STAGENAME}_${env:AGENT_JOBNAME}_SBOM\"\n$SafeArtifactName = $ArtifactName -replace '[\"/:<>\\\\|?@*\"() ]', '_'\n$SbomGenerationDir = Join-Path $ManifestDirPath $SafeArtifactName\n\nWrite-Host \"Artifact name before : $ArtifactName\"\nWrite-Host \"Artifact name after : $SafeArtifactName\"\n\nWrite-Host \"Creating dir $ManifestDirPath\"\n\n# create directory for sbom manifest to be placed\nif (!(Test-Path -path $SbomGenerationDir))\n{\n  New-Item -ItemType Directory -path $SbomGenerationDir\n  Write-Host \"Successfully created directory $SbomGenerationDir\"\n}\nelse{\n  Write-PipelineTelemetryError -category 'Build'  \"Unable to create sbom folder.\"\n}\n\nWrite-Host \"Updating artifact name\"\nWrite-Host \"##vso[task.setvariable variable=ARTIFACT_NAME]$SafeArtifactName\"\n"
  },
  {
    "path": "eng/common/generate-sbom-prep.sh",
    "content": "#!/usr/bin/env bash\n\nsource=\"${BASH_SOURCE[0]}\"\n\n# resolve $SOURCE until the file is no longer a symlink\nwhile [[ -h $source ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n. $scriptroot/pipeline-logging-functions.sh\n\n\n# replace all special characters with _, some builds use special characters like : in Agent.Jobname, that is not a permissible name while uploading artifacts.\nartifact_name=$SYSTEM_STAGENAME\"_\"$AGENT_JOBNAME\"_SBOM\"\nsafe_artifact_name=\"${artifact_name//[\"/:<>\\\\|?@*$\" ]/_}\"\nmanifest_dir=$1\n\n# Normally - we'd listen to the manifest path given, but 1ES templates will overwrite if this level gets uploaded directly\n# with their own overwriting ours. So we create it as a sub directory of the requested manifest path.\nsbom_generation_dir=\"$manifest_dir/$safe_artifact_name\"\n\nif [ ! -d \"$sbom_generation_dir\" ] ; then\n  mkdir -p \"$sbom_generation_dir\"\n  echo \"Sbom directory created.\" $sbom_generation_dir\nelse\n  Write-PipelineTelemetryError -category 'Build'  \"Unable to create sbom folder.\"\nfi\n\necho \"Artifact name before : \"$artifact_name\necho \"Artifact name after : \"$safe_artifact_name\nexport ARTIFACT_NAME=$safe_artifact_name\necho \"##vso[task.setvariable variable=ARTIFACT_NAME]$safe_artifact_name\"\n\nexit 0\n"
  },
  {
    "path": "eng/common/helixpublish.proj",
    "content": "<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. -->\n<Project Sdk=\"Microsoft.DotNet.Helix.Sdk\" DefaultTargets=\"Test\">\n\n  <PropertyGroup>\n    <Language>msbuild</Language>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <HelixCorrelationPayload Include=\"$(CorrelationPayloadDirectory)\">\n      <PayloadDirectory>%(Identity)</PayloadDirectory>\n    </HelixCorrelationPayload>\n  </ItemGroup>\n\n  <ItemGroup>\n    <HelixWorkItem Include=\"WorkItem\" Condition=\"'$(WorkItemDirectory)' != ''\">\n      <PayloadDirectory>$(WorkItemDirectory)</PayloadDirectory>\n      <Command>$(WorkItemCommand)</Command>\n      <Timeout Condition=\"'$(WorkItemTimeout)' != ''\">$(WorkItemTimeout)</Timeout>\n    </HelixWorkItem>\n  </ItemGroup>\n\n  <ItemGroup>\n    <XUnitProject Include=\"$(XUnitProjects.Split(';'))\">\n      <Arguments />\n    </XUnitProject>\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "eng/common/init-tools-native.cmd",
    "content": "@echo off\npowershell -NoProfile -NoLogo -ExecutionPolicy ByPass -command \"& \"\"\"%~dp0init-tools-native.ps1\"\"\" %*\"\nexit /b %ErrorLevel%"
  },
  {
    "path": "eng/common/init-tools-native.ps1",
    "content": "<#\n.SYNOPSIS\nEntry point script for installing native tools\n\n.DESCRIPTION\nReads $RepoRoot\\global.json file to determine native assets to install\nand executes installers for those tools\n\n.PARAMETER BaseUri\nBase file directory or Url from which to acquire tool archives\n\n.PARAMETER InstallDirectory\nDirectory to install native toolset.  This is a command-line override for the default\nInstall directory precedence order:\n- InstallDirectory command-line override\n- NETCOREENG_INSTALL_DIRECTORY environment variable\n- (default) %USERPROFILE%/.netcoreeng/native\n\n.PARAMETER Clean\nSwitch specifying to not install anything, but cleanup native asset folders\n\n.PARAMETER Force\nClean and then install tools\n\n.PARAMETER DownloadRetries\nTotal number of retry attempts\n\n.PARAMETER RetryWaitTimeInSeconds\nWait time between retry attempts in seconds\n\n.PARAMETER GlobalJsonFile\nFile path to global.json file\n\n.PARAMETER PathPromotion\nOptional switch to enable either promote native tools specified in the global.json to the path (in Azure Pipelines)\nor break the build if a native tool is not found on the path (on a local dev machine)\n\n.NOTES\n#>\n[CmdletBinding(PositionalBinding=$false)]\nParam (\n  [string] $BaseUri = 'https://netcorenativeassets.blob.core.windows.net/resource-packages/external',\n  [string] $InstallDirectory,\n  [switch] $Clean = $False,\n  [switch] $Force = $False,\n  [int] $DownloadRetries = 5,\n  [int] $RetryWaitTimeInSeconds = 30,\n  [string] $GlobalJsonFile,\n  [switch] $PathPromotion\n)\n\nif (!$GlobalJsonFile) {\n  $GlobalJsonFile = Join-Path (Get-Item $PSScriptRoot).Parent.Parent.FullName 'global.json'\n}\n\nSet-StrictMode -version 2.0\n$ErrorActionPreference='Stop'\n\n. $PSScriptRoot\\pipeline-logging-functions.ps1\nImport-Module -Name (Join-Path $PSScriptRoot 'native\\CommonLibrary.psm1')\n\ntry {\n  # Define verbose switch if undefined\n  $Verbose = $VerbosePreference -Eq 'Continue'\n\n  $EngCommonBaseDir = Join-Path $PSScriptRoot 'native\\'\n  $NativeBaseDir = $InstallDirectory\n  if (!$NativeBaseDir) {\n    $NativeBaseDir = CommonLibrary\\Get-NativeInstallDirectory\n  }\n  $Env:CommonLibrary_NativeInstallDir = $NativeBaseDir\n  $InstallBin = Join-Path $NativeBaseDir 'bin'\n  $InstallerPath = Join-Path $EngCommonBaseDir 'install-tool.ps1'\n\n  # Process tools list\n  Write-Host \"Processing $GlobalJsonFile\"\n  If (-Not (Test-Path $GlobalJsonFile)) {\n    Write-Host \"Unable to find '$GlobalJsonFile'\"\n    exit 0\n  }\n  $NativeTools = Get-Content($GlobalJsonFile) -Raw |\n                    ConvertFrom-Json |\n                    Select-Object -Expand 'native-tools' -ErrorAction SilentlyContinue\n  if ($NativeTools) {\n    if ($PathPromotion -eq $True) {\n      $ArcadeToolsDirectory = \"$env:SYSTEMDRIVE\\arcade-tools\"\n      if (Test-Path $ArcadeToolsDirectory) { # if this directory exists, we should use native tools on machine\n        $NativeTools.PSObject.Properties | ForEach-Object {\n          $ToolName = $_.Name\n          $ToolVersion = $_.Value\n          $InstalledTools = @{}\n\n          if ((Get-Command \"$ToolName\" -ErrorAction SilentlyContinue) -eq $null) {\n            if ($ToolVersion -eq \"latest\") {\n              $ToolVersion = \"\"\n            }\n            $ToolDirectories = (Get-ChildItem -Path \"$ArcadeToolsDirectory\" -Filter \"$ToolName-$ToolVersion*\" | Sort-Object -Descending)\n            if ($ToolDirectories -eq $null) {\n              Write-Error \"Unable to find directory for $ToolName $ToolVersion; please make sure the tool is installed on this image.\"\n              exit 1\n            }\n            $ToolDirectory = $ToolDirectories[0]\n            $BinPathFile = \"$($ToolDirectory.FullName)\\binpath.txt\"\n            if (-not (Test-Path -Path \"$BinPathFile\")) {\n              Write-Error \"Unable to find binpath.txt in '$($ToolDirectory.FullName)' ($ToolName $ToolVersion); artifact is either installed incorrectly or is not a bootstrappable tool.\"\n              exit 1\n            }\n            $BinPath = Get-Content \"$BinPathFile\"\n            $ToolPath = Convert-Path -Path $BinPath\n            Write-Host \"Adding $ToolName to the path ($ToolPath)...\"\n            Write-Host \"##vso[task.prependpath]$ToolPath\"\n            $env:PATH = \"$ToolPath;$env:PATH\"\n            $InstalledTools += @{ $ToolName = $ToolDirectory.FullName }\n          }\n        }\n        return $InstalledTools\n      } else {\n        $NativeTools.PSObject.Properties | ForEach-Object {\n          $ToolName = $_.Name\n          $ToolVersion = $_.Value\n\n          if ((Get-Command \"$ToolName\" -ErrorAction SilentlyContinue) -eq $null) {\n            Write-PipelineTelemetryError -Category 'NativeToolsBootstrap' -Message \"$ToolName not found on path. Please install $ToolName $ToolVersion before proceeding.\"\n            Write-PipelineTelemetryError -Category 'NativeToolsBootstrap' -Message \"If this is running on a build machine, the arcade-tools directory was not found, which means there's an error with the image.\"\n          }\n        }\n        exit 0\n      }\n    } else {\n      $NativeTools.PSObject.Properties | ForEach-Object {\n        $ToolName = $_.Name\n        $ToolVersion = $_.Value\n        $LocalInstallerArguments =  @{ ToolName = \"$ToolName\" }\n        $LocalInstallerArguments += @{ InstallPath = \"$InstallBin\" }\n        $LocalInstallerArguments += @{ BaseUri = \"$BaseUri\" }\n        $LocalInstallerArguments += @{ CommonLibraryDirectory = \"$EngCommonBaseDir\" }\n        $LocalInstallerArguments += @{ Version = \"$ToolVersion\" }\n  \n        if ($Verbose) {\n          $LocalInstallerArguments += @{ Verbose = $True }\n        }\n        if (Get-Variable 'Force' -ErrorAction 'SilentlyContinue') {\n          if($Force) {\n            $LocalInstallerArguments += @{ Force = $True }\n          }\n        }\n        if ($Clean) {\n          $LocalInstallerArguments += @{ Clean = $True }\n        }\n  \n        Write-Verbose \"Installing $ToolName version $ToolVersion\"\n        Write-Verbose \"Executing '$InstallerPath $($LocalInstallerArguments.Keys.ForEach({\"-$_ '$($LocalInstallerArguments.$_)'\"}) -join ' ')'\"\n        & $InstallerPath @LocalInstallerArguments\n        if ($LASTEXITCODE -Ne \"0\") {\n          $errMsg = \"$ToolName installation failed\"\n          if ((Get-Variable 'DoNotAbortNativeToolsInstallationOnFailure' -ErrorAction 'SilentlyContinue') -and $DoNotAbortNativeToolsInstallationOnFailure) {\n              $showNativeToolsWarning = $true\n              if ((Get-Variable 'DoNotDisplayNativeToolsInstallationWarnings' -ErrorAction 'SilentlyContinue') -and $DoNotDisplayNativeToolsInstallationWarnings) {\n                  $showNativeToolsWarning = $false\n              }\n              if ($showNativeToolsWarning) {\n                  Write-Warning $errMsg\n              }\n              $toolInstallationFailure = $true\n          } else {\n              # We cannot change this to Write-PipelineTelemetryError because of https://github.com/dotnet/arcade/issues/4482\n              Write-Host $errMsg\n              exit 1\n          }\n        }\n      }\n  \n      if ((Get-Variable 'toolInstallationFailure' -ErrorAction 'SilentlyContinue') -and $toolInstallationFailure) {\n          # We cannot change this to Write-PipelineTelemetryError because of https://github.com/dotnet/arcade/issues/4482\n          Write-Host 'Native tools bootstrap failed'\n          exit 1\n      }\n    }\n  }\n  else {\n    Write-Host 'No native tools defined in global.json'\n    exit 0\n  }\n\n  if ($Clean) {\n    exit 0\n  }\n  if (Test-Path $InstallBin) {\n    Write-Host 'Native tools are available from ' (Convert-Path -Path $InstallBin)\n    Write-Host \"##vso[task.prependpath]$(Convert-Path -Path $InstallBin)\"\n    return $InstallBin\n  }\n  elseif (-not ($PathPromotion)) {\n    Write-PipelineTelemetryError -Category 'NativeToolsBootstrap' -Message 'Native tools install directory does not exist, installation failed'\n    exit 1\n  }\n  exit 0\n}\ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Category 'NativeToolsBootstrap' -Message $_\n  ExitWithExitCode 1\n}\n"
  },
  {
    "path": "eng/common/init-tools-native.sh",
    "content": "#!/usr/bin/env bash\n\nsource=\"${BASH_SOURCE[0]}\"\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\nbase_uri='https://netcorenativeassets.blob.core.windows.net/resource-packages/external'\ninstall_directory=''\nclean=false\nforce=false\ndownload_retries=5\nretry_wait_time_seconds=30\nglobal_json_file=\"$(dirname \"$(dirname \"${scriptroot}\")\")/global.json\"\ndeclare -a native_assets\n\n. $scriptroot/pipeline-logging-functions.sh\n. $scriptroot/native/common-library.sh\n\nwhile (($# > 0)); do\n  lowerI=\"$(echo $1 | tr \"[:upper:]\" \"[:lower:]\")\"\n  case $lowerI in\n    --baseuri)\n      base_uri=$2\n      shift 2\n      ;;\n    --installdirectory)\n      install_directory=$2\n      shift 2\n      ;;\n    --clean)\n      clean=true\n      shift 1\n      ;;\n    --force)\n      force=true\n      shift 1\n      ;;\n    --donotabortonfailure)\n      donotabortonfailure=true\n      shift 1\n      ;;\n    --donotdisplaywarnings)\n      donotdisplaywarnings=true\n      shift 1\n      ;;\n    --downloadretries)\n      download_retries=$2\n      shift 2\n      ;;\n    --retrywaittimeseconds)\n      retry_wait_time_seconds=$2\n      shift 2\n      ;;\n    --help)\n      echo \"Common settings:\"\n      echo \"  --installdirectory                  Directory to install native toolset.\"\n      echo \"                                      This is a command-line override for the default\"\n      echo \"                                      Install directory precedence order:\"\n      echo \"                                          - InstallDirectory command-line override\"\n      echo \"                                          - NETCOREENG_INSTALL_DIRECTORY environment variable\"\n      echo \"                                          - (default) %USERPROFILE%/.netcoreeng/native\"\n      echo \"\"\n      echo \"  --clean                             Switch specifying not to install anything, but cleanup native asset folders\"\n      echo \"  --donotabortonfailure               Switch specifiying whether to abort native tools installation on failure\"\n      echo \"  --donotdisplaywarnings              Switch specifiying whether to display warnings during native tools installation on failure\"\n      echo \"  --force                             Clean and then install tools\"\n      echo \"  --help                              Print help and exit\"\n      echo \"\"\n      echo \"Advanced settings:\"\n      echo \"  --baseuri <value>                   Base URI for where to download native tools from\"\n      echo \"  --downloadretries <value>           Number of times a download should be attempted\"\n      echo \"  --retrywaittimeseconds <value>      Wait time between download attempts\"\n      echo \"\"\n      exit 0\n      ;;\n  esac\ndone\n\nfunction ReadGlobalJsonNativeTools {\n  # happy path: we have a proper JSON parsing tool `jq(1)` in PATH!\n  if command -v jq &> /dev/null; then\n\n    # jq: read each key/value pair under \"native-tools\" entry and emit:\n    #   KEY=\"<entry-key>\" VALUE=\"<entry-value>\"\n    # followed by a null byte.\n    #\n    # bash: read line with null byte delimeter and push to array (for later `eval`uation).\n\n    while IFS= read -rd '' line; do\n      native_assets+=(\"$line\")\n    done < <(jq -r '. |\n        select(has(\"native-tools\")) |\n        .\"native-tools\" |\n        keys[] as $k |\n        @sh \"KEY=\\($k) VALUE=\\(.[$k])\\u0000\"' \"$global_json_file\")\n\n    return\n  fi\n\n  # Warning: falling back to manually parsing JSON, which is not recommended.\n\n  # Following routine matches the output and escaping logic of jq(1)'s @sh formatter used above.\n  # It has been tested with several weird strings with escaped characters in entries (key and value)\n  # and results were compared with the output of jq(1) in binary representation using xxd(1);\n  # just before the assignment to 'native_assets' array (above and below).\n\n  # try to capture the section under \"native-tools\".\n  if [[ ! \"$(cat \"$global_json_file\")\" =~ \\\"native-tools\\\"[[:space:]\\:\\{]*([^\\}]+) ]]; then\n    return\n  fi\n\n  section=\"${BASH_REMATCH[1]}\"\n\n  parseStarted=0\n  possibleEnd=0\n  escaping=0\n  escaped=0\n  isKey=1\n\n  for (( i=0; i<${#section}; i++ )); do\n    char=\"${section:$i:1}\"\n    if ! ((parseStarted)) && [[ \"$char\" =~ [[:space:],:] ]]; then continue; fi\n\n    if ! ((escaping)) && [[ \"$char\" == \"\\\\\" ]]; then\n      escaping=1\n    elif ((escaping)) && ! ((escaped)); then\n      escaped=1\n    fi\n\n    if ! ((parseStarted)) && [[ \"$char\" == \"\\\"\" ]]; then\n      parseStarted=1\n      possibleEnd=0\n    elif [[ \"$char\" == \"'\" ]]; then\n      token=\"$token'\\\\\\''\"\n      possibleEnd=0\n    elif ((escaping)) || [[ \"$char\" != \"\\\"\" ]]; then\n      token=\"$token$char\"\n      possibleEnd=1\n    fi\n\n    if ((possibleEnd)) && ! ((escaping)) && [[ \"$char\" == \"\\\"\" ]]; then\n      # Use printf to unescape token to match jq(1)'s @sh formatting rules.\n      # do not use 'token=\"$(printf \"$token\")\"' syntax, as $() eats the trailing linefeed.\n      printf -v token \"'$token'\"\n\n      if ((isKey)); then\n        KEY=\"$token\"\n        isKey=0\n      else\n        line=\"KEY=$KEY VALUE=$token\"\n        native_assets+=(\"$line\")\n        isKey=1\n      fi\n\n      # reset for next token\n      parseStarted=0\n      token=\n    elif ((escaping)) && ((escaped)); then\n      escaping=0\n      escaped=0\n    fi\n  done\n}\n\nnative_base_dir=$install_directory\nif [[ -z $install_directory ]]; then\n  native_base_dir=$(GetNativeInstallDirectory)\nfi\n\ninstall_bin=\"${native_base_dir}/bin\"\ninstalled_any=false\n\nReadGlobalJsonNativeTools\n\nif [[ ${#native_assets[@]} -eq 0 ]]; then\n  echo \"No native tools defined in global.json\"\n  exit 0;\nelse\n  native_installer_dir=\"$scriptroot/native\"\n  for index in \"${!native_assets[@]}\"; do\n    eval \"${native_assets[\"$index\"]}\"\n\n    installer_path=\"$native_installer_dir/install-$KEY.sh\"\n    installer_command=\"$installer_path\"\n    installer_command+=\" --baseuri $base_uri\"\n    installer_command+=\" --installpath $install_bin\"\n    installer_command+=\" --version $VALUE\"\n    echo $installer_command\n\n    if [[ $force = true ]]; then\n      installer_command+=\" --force\"\n    fi\n\n    if [[ $clean = true ]]; then\n      installer_command+=\" --clean\"\n    fi\n\n    if [[ -a $installer_path ]]; then\n      $installer_command\n      if [[ $? != 0 ]]; then\n        if [[ $donotabortonfailure = true ]]; then\n          if [[ $donotdisplaywarnings != true ]]; then\n            Write-PipelineTelemetryError -category 'NativeToolsBootstrap' \"Execution Failed\"\n          fi\n        else\n          Write-PipelineTelemetryError -category 'NativeToolsBootstrap' \"Execution Failed\"\n          exit 1\n        fi\n      else\n        $installed_any = true\n      fi\n    else\n      if [[ $donotabortonfailure == true ]]; then\n        if [[ $donotdisplaywarnings != true ]]; then\n          Write-PipelineTelemetryError -category 'NativeToolsBootstrap' \"Execution Failed: no install script\"\n        fi\n      else\n        Write-PipelineTelemetryError -category 'NativeToolsBootstrap' \"Execution Failed: no install script\"\n        exit 1\n      fi\n    fi\n  done\nfi\n\nif [[ $clean = true ]]; then\n  exit 0\nfi\n\nif [[ -d $install_bin ]]; then\n  echo \"Native tools are available from $install_bin\"\n  echo \"##vso[task.prependpath]$install_bin\"\nelse\n  if [[ $installed_any = true ]]; then\n    Write-PipelineTelemetryError -category 'NativeToolsBootstrap' \"Native tools install directory does not exist, installation failed\"\n    exit 1\n  fi\nfi\n\nexit 0\n"
  },
  {
    "path": "eng/common/internal/Directory.Build.props",
    "content": "<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. -->\n<Project>\n\n  <PropertyGroup>\n    <ImportDirectoryBuildTargets>false</ImportDirectoryBuildTargets>\n    <ImportDirectoryPackagesProps>false</ImportDirectoryPackagesProps>\n  </PropertyGroup>\n\n  <Import Project=\"Sdk.props\" Sdk=\"Microsoft.DotNet.Arcade.Sdk\" />\n\n</Project>\n"
  },
  {
    "path": "eng/common/internal/NuGet.config",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n  <packageSources>\n    <clear />\n    <add key=\"dotnet-core-internal-tooling\" value=\"https://pkgs.dev.azure.com/devdiv/_packaging/dotnet-core-internal-tooling/nuget/v3/index.json\" />\n  </packageSources>\n  <packageSourceMapping>\n    <clear />\n  </packageSourceMapping>\n</configuration>\n"
  },
  {
    "path": "eng/common/internal/Tools.csproj",
    "content": "<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. -->\n<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net472</TargetFramework>\n    <AutomaticallyUseReferenceAssemblyPackages>false</AutomaticallyUseReferenceAssemblyPackages>\n    <BuildWithNetFrameworkHostedCompiler>false</BuildWithNetFrameworkHostedCompiler>\n  </PropertyGroup>\n  <ItemGroup>\n    <!-- Clear references, the SDK may add some depending on UsuingToolXxx settings, but we only want to restore the following -->\n    <PackageReference Remove=\"@(PackageReference)\"/>\n    <PackageReference Include=\"Microsoft.ManifestTool.CrossPlatform\" Version=\"$(MicrosoftManifestToolCrossPlatformVersion)\" />\n    <PackageReference Include=\"Microsoft.VisualStudioEng.MicroBuild.Core\" Version=\"$(MicrosoftVisualStudioEngMicroBuildCoreVersion)\" />\n    <PackageReference Include=\"Microsoft.VisualStudioEng.MicroBuild.Plugins.SwixBuild\" Version=\"$(MicrosoftVisualStudioEngMicroBuildPluginsSwixBuildVersion)\" />\n    <PackageReference Include=\"Microsoft.DotNet.IBCMerge\" Version=\"$(MicrosoftDotNetIBCMergeVersion)\" Condition=\"'$(UsingToolIbcOptimization)' == 'true'\" />\n    <PackageReference Include=\"Drop.App\" Version=\"$(DropAppVersion)\" ExcludeAssets=\"all\" Condition=\"'$(UsingToolVisualStudioIbcTraining)' == 'true'\"/>\n  </ItemGroup>\n\n  <!-- Repository extensibility point -->\n  <Import Project=\"$(RepositoryEngineeringDir)InternalTools.props\" Condition=\"Exists('$(RepositoryEngineeringDir)InternalTools.props')\" />\n\n</Project>\n"
  },
  {
    "path": "eng/common/internal-feed-operations.ps1",
    "content": "param(\n  [Parameter(Mandatory=$true)][string] $Operation,\n  [string] $AuthToken,\n  [string] $CommitSha,\n  [string] $RepoName,\n  [switch] $IsFeedPrivate\n)\n\n$ErrorActionPreference = 'Stop'\nSet-StrictMode -Version 2.0\n. $PSScriptRoot\\tools.ps1\n\n# Sets VSS_NUGET_EXTERNAL_FEED_ENDPOINTS based on the \"darc-int-*\" feeds defined in NuGet.config. This is needed\n# in build agents by CredProvider to authenticate the restore requests to internal feeds as specified in\n# https://github.com/microsoft/artifacts-credprovider/blob/0f53327cd12fd893d8627d7b08a2171bf5852a41/README.md#environment-variables. This should ONLY be called from identified\n# internal builds\nfunction SetupCredProvider {\n  param(\n    [string] $AuthToken\n  )    \n\n  # Install the Cred Provider NuGet plugin\n  Write-Host 'Setting up Cred Provider NuGet plugin in the agent...'\n  Write-Host \"Getting 'installcredprovider.ps1' from 'https://github.com/microsoft/artifacts-credprovider'...\"\n\n  $url = 'https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.ps1'\n  \n  Write-Host \"Writing the contents of 'installcredprovider.ps1' locally...\"\n  Invoke-WebRequest $url -UseBasicParsing -OutFile installcredprovider.ps1\n  \n  Write-Host 'Installing plugin...'\n  .\\installcredprovider.ps1 -Force\n  \n  Write-Host \"Deleting local copy of 'installcredprovider.ps1'...\"\n  Remove-Item .\\installcredprovider.ps1\n\n  if (-Not(\"$env:USERPROFILE\\.nuget\\plugins\\netcore\")) {\n    Write-PipelineTelemetryError -Category 'Arcade' -Message 'CredProvider plugin was not installed correctly!'\n    ExitWithExitCode 1  \n  } \n  else {\n    Write-Host 'CredProvider plugin was installed correctly!'\n  }\n\n  # Then, we set the 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS' environment variable to restore from the stable \n  # feeds successfully\n\n  $nugetConfigPath = Join-Path $RepoRoot \"NuGet.config\"\n\n  if (-Not (Test-Path -Path $nugetConfigPath)) {\n    Write-PipelineTelemetryError -Category 'Build' -Message 'NuGet.config file not found in repo root!'\n    ExitWithExitCode 1\n  }\n  \n  $endpoints = New-Object System.Collections.ArrayList\n  $nugetConfigPackageSources = Select-Xml -Path $nugetConfigPath -XPath \"//packageSources/add[contains(@key, 'darc-int-')]/@value\" | foreach{$_.Node.Value}\n  \n  if (($nugetConfigPackageSources | Measure-Object).Count -gt 0 ) {\n    foreach ($stableRestoreResource in $nugetConfigPackageSources) {\n      $trimmedResource = ([string]$stableRestoreResource).Trim()\n      [void]$endpoints.Add(@{endpoint=\"$trimmedResource\"; password=\"$AuthToken\"}) \n    }\n  }\n\n  if (($endpoints | Measure-Object).Count -gt 0) {\n      $endpointCredentials = @{endpointCredentials=$endpoints} | ConvertTo-Json -Compress\n\n     # Create the environment variables the AzDo way\n      Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data $endpointCredentials -Properties @{\n        'variable' = 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS'\n        'issecret' = 'false'\n      } \n\n      # We don't want sessions cached since we will be updating the endpoints quite frequently\n      Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data 'False' -Properties @{\n        'variable' = 'NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED'\n        'issecret' = 'false'\n      } \n  }\n  else\n  {\n    Write-Host 'No internal endpoints found in NuGet.config'\n  }\n}\n\n#Workaround for https://github.com/microsoft/msbuild/issues/4430\nfunction InstallDotNetSdkAndRestoreArcade {\n  $dotnetTempDir = Join-Path $RepoRoot \"dotnet\"\n  $dotnetSdkVersion=\"2.1.507\" # After experimentation we know this version works when restoring the SDK (compared to 3.0.*)\n  $dotnet = \"$dotnetTempDir\\dotnet.exe\"\n  $restoreProjPath = \"$PSScriptRoot\\restore.proj\"\n  \n  Write-Host \"Installing dotnet SDK version $dotnetSdkVersion to restore Arcade SDK...\"\n  InstallDotNetSdk \"$dotnetTempDir\" \"$dotnetSdkVersion\"\n  \n  '<Project Sdk=\"Microsoft.DotNet.Arcade.Sdk\"/>' | Out-File \"$restoreProjPath\"\n\n  & $dotnet restore $restoreProjPath\n\n  Write-Host 'Arcade SDK restored!'\n\n  if (Test-Path -Path $restoreProjPath) {\n    Remove-Item $restoreProjPath\n  }\n\n  if (Test-Path -Path $dotnetTempDir) {\n    Remove-Item $dotnetTempDir -Recurse\n  }\n}\n\ntry {\n  Push-Location $PSScriptRoot\n\n  if ($Operation -like 'setup') {\n    SetupCredProvider $AuthToken\n  } \n  elseif ($Operation -like 'install-restore') {\n    InstallDotNetSdkAndRestoreArcade\n  }\n  else {\n    Write-PipelineTelemetryError -Category 'Arcade' -Message \"Unknown operation '$Operation'!\"\n    ExitWithExitCode 1  \n  }\n} \ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Category 'Arcade' -Message $_\n  ExitWithExitCode 1\n} \nfinally {\n  Pop-Location\n}\n"
  },
  {
    "path": "eng/common/internal-feed-operations.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\n# Sets VSS_NUGET_EXTERNAL_FEED_ENDPOINTS based on the \"darc-int-*\" feeds defined in NuGet.config. This is needed\n# in build agents by CredProvider to authenticate the restore requests to internal feeds as specified in\n# https://github.com/microsoft/artifacts-credprovider/blob/0f53327cd12fd893d8627d7b08a2171bf5852a41/README.md#environment-variables. \n# This should ONLY be called from identified internal builds\nfunction SetupCredProvider {\n  local authToken=$1\n  \n  # Install the Cred Provider NuGet plugin\n  echo \"Setting up Cred Provider NuGet plugin in the agent...\"...\n  echo \"Getting 'installcredprovider.ps1' from 'https://github.com/microsoft/artifacts-credprovider'...\"\n\n  local url=\"https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh\"  \n  \n  echo \"Writing the contents of 'installcredprovider.ps1' locally...\"\n  local installcredproviderPath=\"installcredprovider.sh\"\n  if command -v curl > /dev/null; then\n    curl $url > \"$installcredproviderPath\"\n  else   \n    wget -q -O \"$installcredproviderPath\" \"$url\"\n  fi\n  \n  echo \"Installing plugin...\"\n  . \"$installcredproviderPath\"\n  \n  echo \"Deleting local copy of 'installcredprovider.sh'...\"\n  rm installcredprovider.sh\n\n  if [ ! -d \"$HOME/.nuget/plugins\" ]; then\n    Write-PipelineTelemetryError -category 'Build' 'CredProvider plugin was not installed correctly!'\n    ExitWithExitCode 1  \n  else \n    echo \"CredProvider plugin was installed correctly!\"\n  fi\n\n  # Then, we set the 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS' environment variable to restore from the stable \n  # feeds successfully\n\n  local nugetConfigPath=\"{$repo_root}NuGet.config\"\n\n  if [ ! \"$nugetConfigPath\" ]; then\n    Write-PipelineTelemetryError -category 'Build' \"NuGet.config file not found in repo's root!\"\n    ExitWithExitCode 1  \n  fi\n  \n  local endpoints='['\n  local nugetConfigPackageValues=`cat \"$nugetConfigPath\" | grep \"key=\\\"darc-int-\"`\n  local pattern=\"value=\\\"(.*)\\\"\"\n\n  for value in $nugetConfigPackageValues \n  do\n    if [[ $value =~ $pattern ]]; then\n      local endpoint=\"${BASH_REMATCH[1]}\"  \n      endpoints+=\"{\\\"endpoint\\\": \\\"$endpoint\\\", \\\"password\\\": \\\"$authToken\\\"},\"\n    fi\n  done\n  \n  endpoints=${endpoints%?}\n  endpoints+=']'\n\n  if [ ${#endpoints} -gt 2 ]; then \n      local endpointCredentials=\"{\\\"endpointCredentials\\\": \"$endpoints\"}\"\n\n      echo \"##vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$endpointCredentials\"\n      echo \"##vso[task.setvariable variable=NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED]False\"\n  else\n    echo \"No internal endpoints found in NuGet.config\"\n  fi\n} \n\n# Workaround for https://github.com/microsoft/msbuild/issues/4430\nfunction InstallDotNetSdkAndRestoreArcade {\n  local dotnetTempDir=\"$repo_root/dotnet\"\n  local dotnetSdkVersion=\"2.1.507\" # After experimentation we know this version works when restoring the SDK (compared to 3.0.*)\n  local restoreProjPath=\"$repo_root/eng/common/restore.proj\"\n  \n  echo \"Installing dotnet SDK version $dotnetSdkVersion to restore Arcade SDK...\"\n  echo \"<Project Sdk=\\\"Microsoft.DotNet.Arcade.Sdk\\\"/>\" > \"$restoreProjPath\"\n  \n  InstallDotNetSdk \"$dotnetTempDir\" \"$dotnetSdkVersion\"\n\n  local res=`$dotnetTempDir/dotnet restore $restoreProjPath`\n  echo \"Arcade SDK restored!\"\n\n  # Cleanup\n  if [ \"$restoreProjPath\" ]; then\n    rm \"$restoreProjPath\"\n  fi\n\n  if [ \"$dotnetTempDir\" ]; then\n    rm -r $dotnetTempDir\n  fi\n}\n\nsource=\"${BASH_SOURCE[0]}\"\noperation=''\nauthToken=''\nrepoName=''\n\nwhile [[ $# -gt 0 ]]; do\n  opt=\"$(echo \"$1\" | tr \"[:upper:]\" \"[:lower:]\")\"\n  case \"$opt\" in\n    --operation)\n      operation=$2\n      shift\n      ;;\n    --authtoken)\n      authToken=$2\n      shift\n      ;;\n    *)\n      echo \"Invalid argument: $1\"\n      usage\n      exit 1\n      ;;\n  esac\n\n  shift\ndone\n\nwhile [[ -h \"$source\" ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\n. \"$scriptroot/tools.sh\"\n\nif [ \"$operation\" = \"setup\" ]; then\n  SetupCredProvider $authToken\nelif [ \"$operation\" = \"install-restore\" ]; then\n  InstallDotNetSdkAndRestoreArcade\nelse\n  echo \"Unknown operation '$operation'!\"\nfi\n"
  },
  {
    "path": "eng/common/loc/P22DotNetHtmlLocalization.lss",
    "content": "<?xml version=\"1.0\"?>\n<LS_SETTINGS_FILE>\n  <LS_SETTINGS_DESCRIPTION>\n    <![CDATA[]]>\n  </LS_SETTINGS_DESCRIPTION>\n  <optionSet id=\"LSOptions\">\n    <optionSet id=\"Defaults\" displayName=\"Options Defaults\">\n      <optionSet id=\"Espresso\" displayName=\"Espresso\"/>\n      <optionSet id=\"Parsers\" displayName=\"Parsers\"/>\n    </optionSet>\n    <optionSet id=\"User\" displayName=\"User Overrides\">\n      <optionSet id=\"Espresso\" displayName=\"Espresso\"/>\n      <optionSet id=\"Parsers\" displayName=\"Parsers\"/>\n    </optionSet>\n    <optionSet id=\"Project\" displayName=\"Project Overrides\">\n      <optionSet id=\"Espresso\" displayName=\"Espresso\"/>\n      <optionSet id=\"Parsers\" displayName=\"Parsers\">\n        <optionSet id=\"Parser 22\" displayName=\"POMHTML Parser options\" helpText=\"POMHTML Parser options for Localization Studio.\">\n          <option id=\"SetCharsetInfo\" displayName=\"Set or add Charset information when Generating\" helpText=\"Add Charset information to the Generated file. This is in the format &lt;META HTTP-EQUIV=&quot;Content-Type&quot; CONTENT=&quot;text/html; charset=windows-1252&quot;&gt;. This is based on the Project Target Langauge. If the dir attribute value in the HTML tag i.e. &lt;HTML dir=&quot;ltr&quot;&gt; is not localizable or is missing, its value will be set automatically on Generate if the reading order of the target language is different from the source language.\">\n            <boolean defaultValue=\"1\" currentValue=\"0\"/>\n          </option>\n          <option id=\"IncTermNoResId\" displayName=\"Include Terms which have no Resource Identifier\" helpText=\"Include Terms which have no Resource Identifier. Terms without Resource Identifiers cannot be Updated or Uploaded if they change.\">\n            <boolean defaultValue=\"0\" currentValue=\"1\"/>\n          </option>\n        </optionSet>\n      </optionSet>\n    </optionSet>\n  </optionSet>\n</LS_SETTINGS_FILE>"
  },
  {
    "path": "eng/common/msbuild.ps1",
    "content": "[CmdletBinding(PositionalBinding=$false)]\nParam(\n  [string] $verbosity = 'minimal',\n  [bool] $warnAsError = $true,\n  [bool] $nodeReuse = $true,\n  [switch] $ci,\n  [switch] $prepareMachine,\n  [switch] $excludePrereleaseVS,\n  [string] $msbuildEngine = $null,\n  [Parameter(ValueFromRemainingArguments=$true)][String[]]$extraArgs\n)\n\n. $PSScriptRoot\\tools.ps1\n\ntry {\n  if ($ci) {\n    $nodeReuse = $false\n  }\n\n  MSBuild @extraArgs\n} \ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Category 'Build' -Message $_\n  ExitWithExitCode 1\n}\n\nExitWithExitCode 0"
  },
  {
    "path": "eng/common/msbuild.sh",
    "content": "#!/usr/bin/env bash\n\nsource=\"${BASH_SOURCE[0]}\"\n\n# resolve $source until the file is no longer a symlink\nwhile [[ -h \"$source\" ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\nverbosity='minimal'\nwarn_as_error=true\nnode_reuse=true\nprepare_machine=false\nextra_args=''\n\nwhile (($# > 0)); do\n  lowerI=\"$(echo $1 | tr \"[:upper:]\" \"[:lower:]\")\"\n  case $lowerI in\n    --verbosity)\n      verbosity=$2\n      shift 2\n      ;;\n    --warnaserror)\n      warn_as_error=$2\n      shift 2\n      ;;\n    --nodereuse)\n      node_reuse=$2\n      shift 2\n      ;;\n    --ci)\n      ci=true\n      shift 1\n      ;;\n    --preparemachine)\n      prepare_machine=true\n      shift 1\n      ;;\n      *)\n      extra_args=\"$extra_args $1\"\n      shift 1\n      ;;\n  esac\ndone\n\n. \"$scriptroot/tools.sh\"\n\nif [[ \"$ci\" == true ]]; then\n  node_reuse=false\nfi\n\nMSBuild $extra_args\nExitWithExitCode 0\n"
  },
  {
    "path": "eng/common/native/CommonLibrary.psm1",
    "content": "<#\n.SYNOPSIS\nHelper module to install an archive to a directory\n\n.DESCRIPTION\nHelper module to download and extract an archive to a specified directory\n\n.PARAMETER Uri\nUri of artifact to download\n\n.PARAMETER InstallDirectory\nDirectory to extract artifact contents to\n\n.PARAMETER Force\nForce download / extraction if file or contents already exist. Default = False\n\n.PARAMETER DownloadRetries\nTotal number of retry attempts. Default = 5\n\n.PARAMETER RetryWaitTimeInSeconds\nWait time between retry attempts in seconds. Default = 30\n\n.NOTES\nReturns False if download or extraction fail, True otherwise\n#>\nfunction DownloadAndExtract {\n  [CmdletBinding(PositionalBinding=$false)]\n  Param (\n    [Parameter(Mandatory=$True)]\n    [string] $Uri,\n    [Parameter(Mandatory=$True)]\n    [string] $InstallDirectory,\n    [switch] $Force = $False,\n    [int] $DownloadRetries = 5,\n    [int] $RetryWaitTimeInSeconds = 30\n  )\n  # Define verbose switch if undefined\n  $Verbose = $VerbosePreference -Eq \"Continue\"\n\n  $TempToolPath = CommonLibrary\\Get-TempPathFilename -Path $Uri\n\n  # Download native tool\n  $DownloadStatus = CommonLibrary\\Get-File -Uri $Uri `\n                                           -Path $TempToolPath `\n                                           -DownloadRetries $DownloadRetries `\n                                           -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds `\n                                           -Force:$Force `\n                                           -Verbose:$Verbose\n\n  if ($DownloadStatus -Eq $False) {\n    Write-Error \"Download failed from $Uri\"\n    return $False\n  }\n\n  # Extract native tool\n  $UnzipStatus = CommonLibrary\\Expand-Zip -ZipPath $TempToolPath `\n                                          -OutputDirectory $InstallDirectory `\n                                          -Force:$Force `\n                                          -Verbose:$Verbose\n\n  if ($UnzipStatus -Eq $False) {\n    # Retry Download one more time with Force=true\n    $DownloadRetryStatus = CommonLibrary\\Get-File -Uri $Uri `\n                                             -Path $TempToolPath `\n                                             -DownloadRetries 1 `\n                                             -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds `\n                                             -Force:$True `\n                                             -Verbose:$Verbose\n\n    if ($DownloadRetryStatus -Eq $False) {\n      Write-Error \"Last attempt of download failed as well\"\n      return $False\n    }\n\n    # Retry unzip again one more time with Force=true\n    $UnzipRetryStatus = CommonLibrary\\Expand-Zip -ZipPath $TempToolPath `\n                                            -OutputDirectory $InstallDirectory `\n                                            -Force:$True `\n                                            -Verbose:$Verbose\n    if ($UnzipRetryStatus -Eq $False)\n    {\n      Write-Error \"Last attempt of unzip failed as well\"\n      # Clean up partial zips and extracts\n      if (Test-Path $TempToolPath) {\n        Remove-Item $TempToolPath -Force\n      }\n      if (Test-Path $InstallDirectory) {\n        Remove-Item $InstallDirectory -Force -Recurse\n      }\n      return $False\n    }\n  }\n\n  return $True\n}\n\n<#\n.SYNOPSIS\nDownload a file, retry on failure\n\n.DESCRIPTION\nDownload specified file and retry if attempt fails\n\n.PARAMETER Uri\nUri of file to download. If Uri is a local path, the file will be copied instead of downloaded\n\n.PARAMETER Path\nPath to download or copy uri file to\n\n.PARAMETER Force\nOverwrite existing file if present. Default = False\n\n.PARAMETER DownloadRetries\nTotal number of retry attempts. Default = 5\n\n.PARAMETER RetryWaitTimeInSeconds\nWait time between retry attempts in seconds Default = 30\n\n#>\nfunction Get-File {\n  [CmdletBinding(PositionalBinding=$false)]\n  Param (\n    [Parameter(Mandatory=$True)]\n    [string] $Uri,\n    [Parameter(Mandatory=$True)]\n    [string] $Path,\n    [int] $DownloadRetries = 5,\n    [int] $RetryWaitTimeInSeconds = 30,\n    [switch] $Force = $False\n  )\n  $Attempt = 0\n\n  if ($Force) {\n    if (Test-Path $Path) {\n      Remove-Item $Path -Force\n    }\n  }\n  if (Test-Path $Path) {\n    Write-Host \"File '$Path' already exists, skipping download\"\n    return $True\n  }\n\n  $DownloadDirectory = Split-Path -ErrorAction Ignore -Path \"$Path\" -Parent\n  if (-Not (Test-Path $DownloadDirectory)) {\n    New-Item -path $DownloadDirectory -force -itemType \"Directory\" | Out-Null\n  }\n\n  $TempPath = \"$Path.tmp\"\n  if (Test-Path -IsValid -Path $Uri) {\n    Write-Verbose \"'$Uri' is a file path, copying temporarily to '$TempPath'\"\n    Copy-Item -Path $Uri -Destination $TempPath\n    Write-Verbose \"Moving temporary file to '$Path'\"\n    Move-Item -Path $TempPath -Destination $Path\n    return $?\n  }\n  else {\n    Write-Verbose \"Downloading $Uri\"\n    # Don't display the console progress UI - it's a huge perf hit\n    $ProgressPreference = 'SilentlyContinue'   \n    while($Attempt -Lt $DownloadRetries)\n    {\n      try {\n        Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $TempPath\n        Write-Verbose \"Downloaded to temporary location '$TempPath'\"\n        Move-Item -Path $TempPath -Destination $Path\n        Write-Verbose \"Moved temporary file to '$Path'\"\n        return $True\n      }\n      catch {\n        $Attempt++\n        if ($Attempt -Lt $DownloadRetries) {\n          $AttemptsLeft = $DownloadRetries - $Attempt\n          Write-Warning \"Download failed, $AttemptsLeft attempts remaining, will retry in $RetryWaitTimeInSeconds seconds\"\n          Start-Sleep -Seconds $RetryWaitTimeInSeconds\n        }\n        else {\n          Write-Error $_\n          Write-Error $_.Exception\n        }\n      }\n    }\n  }\n\n  return $False\n}\n\n<#\n.SYNOPSIS\nGenerate a shim for a native tool\n\n.DESCRIPTION\nCreates a wrapper script (shim) that passes arguments forward to native tool assembly\n\n.PARAMETER ShimName\nThe name of the shim\n\n.PARAMETER ShimDirectory\nThe directory where shims are stored\n\n.PARAMETER ToolFilePath\nPath to file that shim forwards to\n\n.PARAMETER Force\nReplace shim if already present.  Default = False\n\n.NOTES\nReturns $True if generating shim succeeds, $False otherwise\n#>\nfunction New-ScriptShim {\n  [CmdletBinding(PositionalBinding=$false)]\n  Param (\n    [Parameter(Mandatory=$True)]\n    [string] $ShimName,\n    [Parameter(Mandatory=$True)]\n    [string] $ShimDirectory,\n    [Parameter(Mandatory=$True)]\n    [string] $ToolFilePath,\n    [Parameter(Mandatory=$True)]\n    [string] $BaseUri,\n    [switch] $Force\n  )\n  try {\n    Write-Verbose \"Generating '$ShimName' shim\"\n\n    if (-Not (Test-Path $ToolFilePath)){\n      Write-Error \"Specified tool file path '$ToolFilePath' does not exist\"\n      return $False\n    }\n\n    # WinShimmer is a small .NET Framework program that creates .exe shims to bootstrapped programs\n    # Many of the checks for installed programs expect a .exe extension for Windows tools, rather\n    # than a .bat or .cmd file.\n    # Source: https://github.com/dotnet/arcade/tree/master/src/WinShimmer\n    if (-Not (Test-Path \"$ShimDirectory\\WinShimmer\\winshimmer.exe\")) {\n      $InstallStatus = DownloadAndExtract -Uri \"$BaseUri/windows/winshimmer/WinShimmer.zip\" `\n                                          -InstallDirectory $ShimDirectory\\WinShimmer `\n                                          -Force:$Force `\n                                          -DownloadRetries 2 `\n                                          -RetryWaitTimeInSeconds 5 `\n                                          -Verbose:$Verbose\n    }\n\n    if ((Test-Path (Join-Path $ShimDirectory \"$ShimName.exe\"))) {\n      Write-Host \"$ShimName.exe already exists; replacing...\"\n      Remove-Item (Join-Path $ShimDirectory \"$ShimName.exe\")\n    }\n\n    & \"$ShimDirectory\\WinShimmer\\winshimmer.exe\" $ShimName $ToolFilePath $ShimDirectory\n    return $True\n  }\n  catch {\n    Write-Host $_\n    Write-Host $_.Exception\n    return $False\n  }\n}\n\n<#\n.SYNOPSIS\nReturns the machine architecture of the host machine\n\n.NOTES\nReturns 'x64' on 64 bit machines\n Returns 'x86' on 32 bit machines\n#>\nfunction Get-MachineArchitecture {\n  $ProcessorArchitecture = $Env:PROCESSOR_ARCHITECTURE\n  $ProcessorArchitectureW6432 = $Env:PROCESSOR_ARCHITEW6432\n  if($ProcessorArchitecture -Eq \"X86\")\n  {\n    if(($ProcessorArchitectureW6432 -Eq \"\") -Or\n       ($ProcessorArchitectureW6432 -Eq \"X86\")) {\n        return \"x86\"\n    }\n    $ProcessorArchitecture = $ProcessorArchitectureW6432\n  }\n  if (($ProcessorArchitecture -Eq \"AMD64\") -Or\n      ($ProcessorArchitecture -Eq \"IA64\") -Or\n      ($ProcessorArchitecture -Eq \"ARM64\") -Or\n      ($ProcessorArchitecture -Eq \"LOONGARCH64\") -Or\n      ($ProcessorArchitecture -Eq \"RISCV64\")) {\n    return \"x64\"\n  }\n  return \"x86\"\n}\n\n<#\n.SYNOPSIS\nGet the name of a temporary folder under the native install directory\n#>\nfunction Get-TempDirectory {\n  return Join-Path (Get-NativeInstallDirectory) \"temp/\"\n}\n\nfunction Get-TempPathFilename {\n  [CmdletBinding(PositionalBinding=$false)]\n  Param (\n    [Parameter(Mandatory=$True)]\n    [string] $Path\n  )\n  $TempDir = CommonLibrary\\Get-TempDirectory\n  $TempFilename = Split-Path $Path -leaf\n  $TempPath = Join-Path $TempDir $TempFilename\n  return $TempPath\n}\n\n<#\n.SYNOPSIS\nReturns the base directory to use for native tool installation\n\n.NOTES\nReturns the value of the NETCOREENG_INSTALL_DIRECTORY if that environment variable\nis set, or otherwise returns an install directory under the %USERPROFILE%\n#>\nfunction Get-NativeInstallDirectory {\n  $InstallDir = $Env:NETCOREENG_INSTALL_DIRECTORY\n  if (!$InstallDir) {\n    $InstallDir = Join-Path $Env:USERPROFILE \".netcoreeng/native/\"\n  }\n  return $InstallDir\n}\n\n<#\n.SYNOPSIS\nUnzip an archive\n\n.DESCRIPTION\nPowershell module to unzip an archive to a specified directory\n\n.PARAMETER ZipPath (Required)\nPath to archive to unzip\n\n.PARAMETER OutputDirectory (Required)\nOutput directory for archive contents\n\n.PARAMETER Force\nOverwrite output directory contents if they already exist\n\n.NOTES\n- Returns True and does not perform an extraction if output directory already exists but Overwrite is not True.\n- Returns True if unzip operation is successful\n- Returns False if Overwrite is True and it is unable to remove contents of OutputDirectory\n- Returns False if unable to extract zip archive\n#>\nfunction Expand-Zip {\n  [CmdletBinding(PositionalBinding=$false)]\n  Param (\n    [Parameter(Mandatory=$True)]\n    [string] $ZipPath,\n    [Parameter(Mandatory=$True)]\n    [string] $OutputDirectory,\n    [switch] $Force\n  )\n\n  Write-Verbose \"Extracting '$ZipPath' to '$OutputDirectory'\"\n  try {\n    if ((Test-Path $OutputDirectory) -And (-Not $Force)) {\n      Write-Host \"Directory '$OutputDirectory' already exists, skipping extract\"\n      return $True\n    }\n    if (Test-Path $OutputDirectory) {\n      Write-Verbose \"'Force' is 'True', but '$OutputDirectory' exists, removing directory\"\n      Remove-Item $OutputDirectory -Force -Recurse\n      if ($? -Eq $False) {\n        Write-Error \"Unable to remove '$OutputDirectory'\"\n        return $False\n      }\n    }\n\n    $TempOutputDirectory = Join-Path \"$(Split-Path -Parent $OutputDirectory)\" \"$(Split-Path -Leaf $OutputDirectory).tmp\"\n    if (Test-Path $TempOutputDirectory) {\n      Remove-Item $TempOutputDirectory -Force -Recurse\n    }\n    New-Item -Path $TempOutputDirectory -Force -ItemType \"Directory\" | Out-Null\n\n    Add-Type -assembly \"system.io.compression.filesystem\"\n    [io.compression.zipfile]::ExtractToDirectory(\"$ZipPath\", \"$TempOutputDirectory\")\n    if ($? -Eq $False) {\n      Write-Error \"Unable to extract '$ZipPath'\"\n      return $False\n    }\n\n    Move-Item -Path $TempOutputDirectory -Destination $OutputDirectory\n  }\n  catch {\n    Write-Host $_\n    Write-Host $_.Exception\n\n    return $False\n  }\n  return $True\n}\n\nexport-modulemember -function DownloadAndExtract\nexport-modulemember -function Expand-Zip\nexport-modulemember -function Get-File\nexport-modulemember -function Get-MachineArchitecture\nexport-modulemember -function Get-NativeInstallDirectory\nexport-modulemember -function Get-TempDirectory\nexport-modulemember -function Get-TempPathFilename\nexport-modulemember -function New-ScriptShim\n"
  },
  {
    "path": "eng/common/native/common-library.sh",
    "content": "#!/usr/bin/env bash\n\nfunction GetNativeInstallDirectory {\n  local install_dir\n\n  if [[ -z $NETCOREENG_INSTALL_DIRECTORY ]]; then\n    install_dir=$HOME/.netcoreeng/native/\n  else\n    install_dir=$NETCOREENG_INSTALL_DIRECTORY\n  fi\n\n  echo $install_dir\n  return 0\n}\n\nfunction GetTempDirectory {\n\n  echo $(GetNativeInstallDirectory)temp/\n  return 0\n}\n\nfunction ExpandZip {\n  local zip_path=$1\n  local output_directory=$2\n  local force=${3:-false}\n\n  echo \"Extracting $zip_path to $output_directory\"\n  if [[ -d $output_directory ]] && [[ $force = false ]]; then\n    echo \"Directory '$output_directory' already exists, skipping extract\"\n    return 0\n  fi\n\n  if [[ -d $output_directory ]]; then\n    echo \"'Force flag enabled, but '$output_directory' exists. Removing directory\"\n    rm -rf $output_directory\n    if [[ $? != 0 ]]; then\n      Write-PipelineTelemetryError -category 'NativeToolsBootstrap' \"Unable to remove '$output_directory'\"\n      return 1\n    fi\n  fi\n\n  echo \"Creating directory: '$output_directory'\"\n  mkdir -p $output_directory\n\n  echo \"Extracting archive\"\n  tar -xf $zip_path -C $output_directory\n  if [[ $? != 0 ]]; then\n    Write-PipelineTelemetryError -category 'NativeToolsBootstrap' \"Unable to extract '$zip_path'\"\n    return 1\n  fi\n\n  return 0\n}\n\nfunction GetCurrentOS {\n  local unameOut=\"$(uname -s)\"\n  case $unameOut in\n    Linux*)     echo \"Linux\";;\n    Darwin*)    echo \"MacOS\";;\n  esac\n  return 0\n}\n\nfunction GetFile {\n  local uri=$1\n  local path=$2\n  local force=${3:-false}\n  local download_retries=${4:-5}\n  local retry_wait_time_seconds=${5:-30}\n\n  if [[ -f $path ]]; then\n    if [[ $force = false ]]; then\n      echo \"File '$path' already exists. Skipping download\"\n      return 0\n    else\n      rm -rf $path\n    fi\n  fi\n\n  if [[ -f $uri ]]; then\n    echo \"'$uri' is a file path, copying file to '$path'\"\n    cp $uri $path\n    return $?\n  fi\n\n  echo \"Downloading $uri\"\n  # Use curl if available, otherwise use wget\n  if command -v curl > /dev/null; then\n    curl \"$uri\" -sSL --retry $download_retries --retry-delay $retry_wait_time_seconds --create-dirs -o \"$path\" --fail\n  else\n    wget -q -O \"$path\" \"$uri\" --tries=\"$download_retries\"\n  fi\n\n  return $?\n}\n\nfunction GetTempPathFileName {\n  local path=$1\n\n  local temp_dir=$(GetTempDirectory)\n  local temp_file_name=$(basename $path)\n  echo $temp_dir$temp_file_name\n  return 0\n}\n\nfunction DownloadAndExtract {\n  local uri=$1\n  local installDir=$2\n  local force=${3:-false}\n  local download_retries=${4:-5}\n  local retry_wait_time_seconds=${5:-30}\n\n  local temp_tool_path=$(GetTempPathFileName $uri)\n\n  echo \"downloading to: $temp_tool_path\"\n\n  # Download file\n  GetFile \"$uri\" \"$temp_tool_path\" $force $download_retries $retry_wait_time_seconds\n  if [[ $? != 0 ]]; then\n    Write-PipelineTelemetryError -category 'NativeToolsBootstrap' \"Failed to download '$uri' to '$temp_tool_path'.\"\n    return 1\n  fi\n\n  # Extract File\n  echo \"extracting from  $temp_tool_path to $installDir\"\n  ExpandZip \"$temp_tool_path\" \"$installDir\" $force $download_retries $retry_wait_time_seconds\n  if [[ $? != 0 ]]; then\n    Write-PipelineTelemetryError -category 'NativeToolsBootstrap' \"Failed to extract '$temp_tool_path' to '$installDir'.\"\n    return 1\n  fi\n\n  return 0\n}\n\nfunction NewScriptShim {\n  local shimpath=$1\n  local tool_file_path=$2\n  local force=${3:-false}\n\n  echo \"Generating '$shimpath' shim\"\n  if [[ -f $shimpath ]]; then\n    if [[ $force = false ]]; then\n      echo \"File '$shimpath' already exists.\" >&2\n      return 1\n    else\n      rm -rf $shimpath\n    fi\n  fi\n  \n  if [[ ! -f $tool_file_path ]]; then\n    # try to see if the path is lower cased\n    tool_file_path=\"$(echo $tool_file_path | tr \"[:upper:]\" \"[:lower:]\")\" \n    if [[ ! -f $tool_file_path ]]; then\n      Write-PipelineTelemetryError -category 'NativeToolsBootstrap' \"Specified tool file path:'$tool_file_path' does not exist\"\n      return 1\n    fi\n  fi\n\n  local shim_contents=$'#!/usr/bin/env bash\\n'\n  shim_contents+=\"SHIMARGS=\"$'$1\\n'\n  shim_contents+=\"$tool_file_path\"$' $SHIMARGS\\n'\n\n  # Write shim file\n  echo \"$shim_contents\" > $shimpath\n\n  chmod +x $shimpath\n\n  echo \"Finished generating shim '$shimpath'\"\n\n  return $?\n}\n\n"
  },
  {
    "path": "eng/common/native/init-compiler.sh",
    "content": "#!/bin/sh\n#\n# This file detects the C/C++ compiler and exports it to the CC/CXX environment variables\n#\n# NOTE: some scripts source this file and rely on stdout being empty, make sure\n# to not output *anything* here, unless it is an error message that fails the\n# build.\n\nif [ -z \"$build_arch\" ] || [ -z \"$compiler\" ]; then\n  echo \"Usage...\"\n  echo \"build_arch=<ARCH> compiler=<NAME> init-compiler.sh\"\n  echo \"Specify the target architecture.\"\n  echo \"Specify the name of compiler (clang or gcc).\"\n  exit 1\nfi\n\ncase \"$compiler\" in\n    clang*|-clang*|--clang*)\n        # clangx.y or clang-x.y\n        version=\"$(echo \"$compiler\" | tr -d '[:alpha:]-=')\"\n        majorVersion=\"${version%%.*}\"\n\n        # LLVM based on v18 released in early 2024, with two releases per year\n        maxVersion=\"$((18 + ((($(date +%Y) - 2024) * 12 + $(date +%-m) - 3) / 6)))\"\n        compiler=clang\n        ;;\n\n    gcc*|-gcc*|--gcc*)\n        # gccx.y or gcc-x.y\n        version=\"$(echo \"$compiler\" | tr -d '[:alpha:]-=')\"\n        majorVersion=\"${version%%.*}\"\n\n        # GCC based on v14 released in early 2024, with one release per year\n        maxVersion=\"$((14 + ((($(date +%Y) - 2024) * 12 + $(date +%-m) - 3) / 12)))\"\n        compiler=gcc\n        ;;\nesac\n\ncxxCompiler=\"$compiler++\"\n\n# clear the existing CC and CXX from environment\nCC=\nCXX=\nLDFLAGS=\n\nif [ \"$compiler\" = \"gcc\" ]; then cxxCompiler=\"g++\"; fi\n\ncheck_version_exists() {\n    desired_version=-1\n\n    # Set up the environment to be used for building with the desired compiler.\n    if command -v \"$compiler-$1\" > /dev/null; then\n        desired_version=\"-$1\"\n    elif command -v \"$compiler$1\" > /dev/null; then\n        desired_version=\"$1\"\n    fi\n\n    echo \"$desired_version\"\n}\n\n__baseOS=\"$(uname)\"\nset_compiler_version_from_CC() {\n    if [ \"$__baseOS\" = \"Darwin\" ]; then\n        # On Darwin, the versions from -version/-dumpversion refer to Xcode\n        # versions, not llvm versions, so we can't rely on them.\n        return\n    fi\n\n    version=\"$(\"$CC\" -dumpversion)\"\n    if [ -z \"$version\" ]; then\n        echo \"Error: $CC -dumpversion didn't provide a version\"\n        exit 1\n    fi\n\n    # gcc and clang often display 3 part versions. However, gcc can show only 1 part in some environments.\n    IFS=. read -r majorVersion _ <<EOF\n$version\nEOF\n}\n\nif [ -z \"$CLR_CC\" ]; then\n\n    # Set default versions\n    if [ -z \"$majorVersion\" ]; then\n        minVersion=8\n        maxVersion=\"$((maxVersion + 1))\" # +1 for headspace\n        i=\"$maxVersion\"\n        while [ \"$i\" -ge $minVersion ]; do\n            desired_version=\"$(check_version_exists \"$i\")\"\n            if [ \"$desired_version\" != \"-1\" ]; then majorVersion=\"$i\"; break; fi\n            i=$((i - 1))\n        done\n\n        if [ -z \"$majorVersion\" ]; then\n            if ! command -v \"$compiler\" > /dev/null; then\n                echo \"Error: No compatible version of $compiler was found within the range of $minVersion to $maxVersion. Please upgrade your toolchain or specify the compiler explicitly using CLR_CC and CLR_CXX environment variables.\"\n                exit 1\n            fi\n\n            CC=\"$(command -v \"$compiler\" 2> /dev/null)\"\n            CXX=\"$(command -v \"$cxxCompiler\" 2> /dev/null)\"\n            set_compiler_version_from_CC\n        fi\n    else\n        desired_version=\"$(check_version_exists \"$majorVersion\")\"\n        if [ \"$desired_version\" = \"-1\" ]; then\n            echo \"Error: Could not find specific version of $compiler: $majorVersion.\"\n            exit 1\n        fi\n    fi\n\n    if [ -z \"$CC\" ]; then\n        CC=\"$(command -v \"$compiler$desired_version\" 2> /dev/null)\"\n        CXX=\"$(command -v \"$cxxCompiler$desired_version\" 2> /dev/null)\"\n        if [ -z \"$CXX\" ]; then CXX=\"$(command -v \"$cxxCompiler\" 2> /dev/null)\"; fi\n        set_compiler_version_from_CC\n    fi\nelse\n    if [ ! -f \"$CLR_CC\" ]; then\n        echo \"Error: CLR_CC is set but path '$CLR_CC' does not exist\"\n        exit 1\n    fi\n    CC=\"$CLR_CC\"\n    CXX=\"$CLR_CXX\"\n    set_compiler_version_from_CC\nfi\n\nif [ -z \"$CC\" ]; then\n    echo \"Error: Unable to find $compiler.\"\n    exit 1\nfi\n\nif [ \"$__baseOS\" != \"Darwin\" ]; then\n    # On Darwin, we always want to use the Apple linker.\n\n    # Only lld version >= 9 can be considered stable. lld supports s390x starting from 18.0.\n    if [ \"$compiler\" = \"clang\" ] && [ -n \"$majorVersion\" ] && [ \"$majorVersion\" -ge 9 ] && { [ \"$build_arch\" != \"s390x\" ] || [ \"$majorVersion\" -ge 18 ]; }; then\n        if \"$CC\" -fuse-ld=lld -Wl,--version >/dev/null 2>&1; then\n            LDFLAGS=\"-fuse-ld=lld\"\n        fi\n    fi\nfi\n\nSCAN_BUILD_COMMAND=\"$(command -v \"scan-build$desired_version\" 2> /dev/null)\"\n\nexport CC CXX LDFLAGS SCAN_BUILD_COMMAND\n"
  },
  {
    "path": "eng/common/native/init-distro-rid.sh",
    "content": "#!/bin/sh\n\n# getNonPortableDistroRid\n#\n# Input:\n#   targetOs: (str)\n#   targetArch: (str)\n#   rootfsDir: (str)\n#\n# Return:\n#   non-portable rid\ngetNonPortableDistroRid()\n{\n    targetOs=\"$1\"\n    targetArch=\"$2\"\n    rootfsDir=\"$3\"\n    nonPortableRid=\"\"\n\n    if [ \"$targetOs\" = \"linux\" ]; then\n        # shellcheck disable=SC1091\n        if [ -e \"${rootfsDir}/etc/os-release\" ]; then\n            . \"${rootfsDir}/etc/os-release\"\n            if echo \"${VERSION_ID:-}\" | grep -qE '^([[:digit:]]|\\.)+$'; then\n                nonPortableRid=\"${ID}.${VERSION_ID}-${targetArch}\"\n            else\n                # Rolling release distros either do not set VERSION_ID, set it as blank or\n                # set it to non-version looking string (such as TEMPLATE_VERSION_ID on ArchLinux);\n                # so omit it here to be consistent with everything else.\n                nonPortableRid=\"${ID}-${targetArch}\"\n            fi\n        elif [ -e \"${rootfsDir}/android_platform\" ]; then\n            # shellcheck disable=SC1091\n            . \"${rootfsDir}/android_platform\"\n            nonPortableRid=\"$RID\"\n        fi\n    fi\n\n    if [ \"$targetOs\" = \"freebsd\" ]; then\n        # $rootfsDir can be empty. freebsd-version is a shell script and should always work.\n        __freebsd_major_version=$(\"$rootfsDir\"/bin/freebsd-version | cut -d'.' -f1)\n        nonPortableRid=\"freebsd.$__freebsd_major_version-${targetArch}\"\n    elif command -v getprop >/dev/null && getprop ro.product.system.model | grep -qi android; then\n        __android_sdk_version=$(getprop ro.build.version.sdk)\n        nonPortableRid=\"android.$__android_sdk_version-${targetArch}\"\n    elif [ \"$targetOs\" = \"illumos\" ]; then\n        __uname_version=$(uname -v)\n        nonPortableRid=\"illumos-${targetArch}\"\n    elif [ \"$targetOs\" = \"solaris\" ]; then\n        __uname_version=$(uname -v)\n        __solaris_major_version=$(echo \"$__uname_version\" | cut -d'.' -f1)\n        nonPortableRid=\"solaris.$__solaris_major_version-${targetArch}\"\n    elif [ \"$targetOs\" = \"haiku\" ]; then\n        __uname_release=\"$(uname -r)\"\n        nonPortableRid=haiku.r\"$__uname_release\"-\"$targetArch\"\n    fi\n\n    echo \"$nonPortableRid\" | tr '[:upper:]' '[:lower:]'\n}\n\n# initDistroRidGlobal\n#\n# Input:\n#   os: (str)\n#   arch: (str)\n#   rootfsDir?: (nullable:string)\n#\n# Return:\n#   None\n#\n# Notes:\n#   It is important to note that the function does not return anything, but it\n#   exports the following variables on success:\n#     __DistroRid   : Non-portable rid of the target platform.\n#     __PortableTargetOS  : OS-part of the portable rid that corresponds to the target platform.\ninitDistroRidGlobal()\n{\n    targetOs=\"$1\"\n    targetArch=\"$2\"\n    rootfsDir=\"\"\n    if [ $# -ge 3 ]; then\n        rootfsDir=\"$3\"\n    fi\n\n    if [ -n \"${rootfsDir}\" ]; then\n        # We may have a cross build. Check for the existence of the rootfsDir\n        if [ ! -e \"${rootfsDir}\" ]; then\n            echo \"Error: rootfsDir has been passed, but the location is not valid.\"\n            exit 1\n        fi\n    fi\n\n    __DistroRid=$(getNonPortableDistroRid \"${targetOs}\" \"${targetArch}\" \"${rootfsDir}\")\n\n    if [ -z \"${__PortableTargetOS:-}\" ]; then\n        __PortableTargetOS=\"$targetOs\"\n\n        STRINGS=\"$(command -v strings || true)\"\n        if [ -z \"$STRINGS\" ]; then\n            STRINGS=\"$(command -v llvm-strings || true)\"\n        fi\n\n        # Check for musl-based distros (e.g. Alpine Linux, Void Linux).\n        if \"${rootfsDir}/usr/bin/ldd\" --version 2>&1 | grep -q musl ||\n                ( [ -n \"$STRINGS\" ] && \"$STRINGS\" \"${rootfsDir}/usr/bin/ldd\" 2>&1 | grep -q musl ); then\n            __PortableTargetOS=\"linux-musl\"\n        fi\n    fi\n\n    export __DistroRid __PortableTargetOS\n}\n"
  },
  {
    "path": "eng/common/native/init-os-and-arch.sh",
    "content": "#!/bin/sh\n\n# Use uname to determine what the OS is.\nOSName=$(uname -s | tr '[:upper:]' '[:lower:]')\n\nif command -v getprop && getprop ro.product.system.model 2>&1 | grep -qi android; then\n    OSName=\"android\"\nfi\n\ncase \"$OSName\" in\nfreebsd|linux|netbsd|openbsd|sunos|android|haiku)\n    os=\"$OSName\" ;;\ndarwin)\n    os=osx ;;\n*)\n    echo \"Unsupported OS $OSName detected!\"\n    exit 1 ;;\nesac\n\n# On Solaris, `uname -m` is discouraged, see https://docs.oracle.com/cd/E36784_01/html/E36870/uname-1.html\n# and `uname -p` returns processor type (e.g. i386 on amd64).\n# The appropriate tool to determine CPU is isainfo(1) https://docs.oracle.com/cd/E36784_01/html/E36870/isainfo-1.html.\nif [ \"$os\" = \"sunos\" ]; then\n    if uname -o 2>&1 | grep -q illumos; then\n        os=\"illumos\"\n    else\n        os=\"solaris\"\n    fi\n    CPUName=$(isainfo -n)\nelse\n    # For the rest of the operating systems, use uname(1) to determine what the CPU is.\n    CPUName=$(uname -m)\nfi\n\ncase \"$CPUName\" in\n    arm64|aarch64)\n        arch=arm64\n        if [ \"$(getconf LONG_BIT)\" -lt 64 ]; then\n            # This is 32-bit OS running on 64-bit CPU (for example Raspberry Pi OS)\n            arch=arm\n        fi\n        ;;\n\n    loongarch64)\n        arch=loongarch64\n        ;;\n\n    riscv64)\n        arch=riscv64\n        ;;\n\n    amd64|x86_64)\n        arch=x64\n        ;;\n\n    armv7l|armv8l)\n        # shellcheck disable=SC1091\n        if (NAME=\"\"; . /etc/os-release; test \"$NAME\" = \"Tizen\"); then\n            arch=armel\n        else\n            arch=arm\n        fi\n        ;;\n\n    armv6l)\n        arch=armv6\n        ;;\n\n    i[3-6]86)\n        echo \"Unsupported CPU $CPUName detected, build might not succeed!\"\n        arch=x86\n        ;;\n\n    s390x)\n        arch=s390x\n        ;;\n\n    ppc64le)\n        arch=ppc64le\n        ;;\n    *)\n        echo \"Unknown CPU $CPUName detected!\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "eng/common/native/install-cmake-test.sh",
    "content": "#!/usr/bin/env bash\n\nsource=\"${BASH_SOURCE[0]}\"\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\n. $scriptroot/common-library.sh\n\nbase_uri=\ninstall_path=\nversion=\nclean=false\nforce=false\ndownload_retries=5\nretry_wait_time_seconds=30\n\nwhile (($# > 0)); do\n  lowerI=\"$(echo $1 | tr \"[:upper:]\" \"[:lower:]\")\"\n  case $lowerI in\n    --baseuri)\n      base_uri=$2\n      shift 2\n      ;;\n    --installpath)\n      install_path=$2\n      shift 2\n      ;;\n    --version)\n      version=$2\n      shift 2\n      ;;\n    --clean)\n      clean=true\n      shift 1\n      ;;\n    --force)\n      force=true\n      shift 1\n      ;;\n    --downloadretries)\n      download_retries=$2\n      shift 2\n      ;;\n    --retrywaittimeseconds)\n      retry_wait_time_seconds=$2\n      shift 2\n      ;;\n    --help)\n      echo \"Common settings:\"\n      echo \"  --baseuri <value>        Base file directory or Url wrom which to acquire tool archives\"\n      echo \"  --installpath <value>    Base directory to install native tool to\"\n      echo \"  --clean                  Don't install the tool, just clean up the current install of the tool\"\n      echo \"  --force                  Force install of tools even if they previously exist\"\n      echo \"  --help                   Print help and exit\"\n      echo \"\"\n      echo \"Advanced settings:\"\n      echo \"  --downloadretries        Total number of retry attempts\"\n      echo \"  --retrywaittimeseconds   Wait time between retry attempts in seconds\"\n      echo \"\"\n      exit 0\n      ;;\n  esac\ndone\n\ntool_name=\"cmake-test\"\ntool_os=$(GetCurrentOS)\ntool_folder=\"$(echo $tool_os | tr \"[:upper:]\" \"[:lower:]\")\"\ntool_arch=\"x86_64\"\ntool_name_moniker=\"$tool_name-$version-$tool_os-$tool_arch\"\ntool_install_directory=\"$install_path/$tool_name/$version\"\ntool_file_path=\"$tool_install_directory/$tool_name_moniker/bin/$tool_name\"\nshim_path=\"$install_path/$tool_name.sh\"\nuri=\"${base_uri}/$tool_folder/$tool_name/$tool_name_moniker.tar.gz\"\n\n# Clean up tool and installers\nif [[ $clean = true ]]; then\n  echo \"Cleaning $tool_install_directory\"\n  if [[ -d $tool_install_directory ]]; then\n    rm -rf $tool_install_directory\n  fi\n\n  echo \"Cleaning $shim_path\"\n  if [[ -f $shim_path ]]; then\n    rm -rf $shim_path\n  fi\n\n  tool_temp_path=$(GetTempPathFileName $uri)\n  echo \"Cleaning $tool_temp_path\"\n  if [[ -f $tool_temp_path ]]; then\n    rm -rf $tool_temp_path\n  fi\n\n  exit 0\nfi\n\n# Install tool\nif [[ -f $tool_file_path ]] && [[ $force = false ]]; then\n  echo \"$tool_name ($version) already exists, skipping install\"\n  exit 0\nfi\n\nDownloadAndExtract $uri $tool_install_directory $force $download_retries $retry_wait_time_seconds\n\nif [[ $? != 0 ]]; then\n  Write-PipelineTelemetryError -category 'NativeToolsBootstrap' 'Installation failed'\n  exit 1\nfi\n\n# Generate Shim\n# Always rewrite shims so that we are referencing the expected version\nNewScriptShim $shim_path $tool_file_path true\n\nif [[ $? != 0 ]]; then\n  Write-PipelineTelemetryError -category 'NativeToolsBootstrap' 'Shim generation failed'\n  exit 1\nfi\n\nexit 0\n"
  },
  {
    "path": "eng/common/native/install-cmake.sh",
    "content": "#!/usr/bin/env bash\n\nsource=\"${BASH_SOURCE[0]}\"\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\n. $scriptroot/common-library.sh\n\nbase_uri=\ninstall_path=\nversion=\nclean=false\nforce=false\ndownload_retries=5\nretry_wait_time_seconds=30\n\nwhile (($# > 0)); do\n  lowerI=\"$(echo $1 | tr \"[:upper:]\" \"[:lower:]\")\"\n  case $lowerI in\n    --baseuri)\n      base_uri=$2\n      shift 2\n      ;;\n    --installpath)\n      install_path=$2\n      shift 2\n      ;;\n    --version)\n      version=$2\n      shift 2\n      ;;\n    --clean)\n      clean=true\n      shift 1\n      ;;\n    --force)\n      force=true\n      shift 1\n      ;;\n    --downloadretries)\n      download_retries=$2\n      shift 2\n      ;;\n    --retrywaittimeseconds)\n      retry_wait_time_seconds=$2\n      shift 2\n      ;;\n    --help)\n      echo \"Common settings:\"\n      echo \"  --baseuri <value>        Base file directory or Url wrom which to acquire tool archives\"\n      echo \"  --installpath <value>    Base directory to install native tool to\"\n      echo \"  --clean                  Don't install the tool, just clean up the current install of the tool\"\n      echo \"  --force                  Force install of tools even if they previously exist\"\n      echo \"  --help                   Print help and exit\"\n      echo \"\"\n      echo \"Advanced settings:\"\n      echo \"  --downloadretries        Total number of retry attempts\"\n      echo \"  --retrywaittimeseconds   Wait time between retry attempts in seconds\"\n      echo \"\"\n      exit 0\n      ;;\n  esac\ndone\n\ntool_name=\"cmake\"\ntool_os=$(GetCurrentOS)\ntool_folder=\"$(echo $tool_os | tr \"[:upper:]\" \"[:lower:]\")\"\ntool_arch=\"x86_64\"\ntool_name_moniker=\"$tool_name-$version-$tool_os-$tool_arch\"\ntool_install_directory=\"$install_path/$tool_name/$version\"\ntool_file_path=\"$tool_install_directory/$tool_name_moniker/bin/$tool_name\"\nshim_path=\"$install_path/$tool_name.sh\"\nuri=\"${base_uri}/$tool_folder/$tool_name/$tool_name_moniker.tar.gz\"\n\n# Clean up tool and installers\nif [[ $clean = true ]]; then\n  echo \"Cleaning $tool_install_directory\"\n  if [[ -d $tool_install_directory ]]; then\n    rm -rf $tool_install_directory\n  fi\n\n  echo \"Cleaning $shim_path\"\n  if [[ -f $shim_path ]]; then\n    rm -rf $shim_path\n  fi\n\n  tool_temp_path=$(GetTempPathFileName $uri)\n  echo \"Cleaning $tool_temp_path\"\n  if [[ -f $tool_temp_path ]]; then\n    rm -rf $tool_temp_path\n  fi\n\n  exit 0\nfi\n\n# Install tool\nif [[ -f $tool_file_path ]] && [[ $force = false ]]; then\n  echo \"$tool_name ($version) already exists, skipping install\"\n  exit 0\nfi\n\nDownloadAndExtract $uri $tool_install_directory $force $download_retries $retry_wait_time_seconds\n\nif [[ $? != 0 ]]; then\n  Write-PipelineTelemetryError -category 'NativeToolsBootstrap' 'Installation failed'\n  exit 1\nfi\n\n# Generate Shim\n# Always rewrite shims so that we are referencing the expected version\nNewScriptShim $shim_path $tool_file_path true\n\nif [[ $? != 0 ]]; then\n  Write-PipelineTelemetryError -category 'NativeToolsBootstrap' 'Shim generation failed'\n  exit 1\nfi\n\nexit 0\n"
  },
  {
    "path": "eng/common/native/install-dependencies.sh",
    "content": "#!/bin/sh\n\nset -e\n\n# This is a simple script primarily used for CI to install necessary dependencies\n#\n# Usage:\n#\n# ./install-dependencies.sh <OS>\n\nos=\"$(echo \"$1\" | tr \"[:upper:]\" \"[:lower:]\")\"\n\nif [ -z \"$os\" ]; then\n    . \"$(dirname \"$0\")\"/init-os-and-arch.sh\nfi\n\ncase \"$os\" in\n    linux)\n        if [ -e /etc/os-release ]; then\n            . /etc/os-release\n        fi\n\n        if [ \"$ID\" = \"debian\" ] || [ \"$ID_LIKE\" = \"debian\" ]; then\n            apt update\n\n            apt install -y build-essential gettext locales cmake llvm clang lld lldb liblldb-dev libunwind8-dev libicu-dev liblttng-ust-dev \\\n                libssl-dev libkrb5-dev pigz cpio ninja-build\n\n            localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8\n        elif [ \"$ID\" = \"fedora\" ] || [ \"$ID\" = \"rhel\" ] || [ \"$ID\" = \"azurelinux\" ] || [ \"$ID\" = \"centos\" ]; then\n            pkg_mgr=\"$(command -v tdnf 2>/dev/null || command -v dnf)\"\n            $pkg_mgr install -y cmake llvm lld lldb clang python curl libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio ninja-build\n        elif [ \"$ID\" = \"amzn\" ]; then\n            dnf install -y cmake llvm lld lldb clang python libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio ninja-build\n        elif [ \"$ID\" = \"alpine\" ]; then\n            apk add build-base cmake bash curl clang llvm-dev lld lldb krb5-dev lttng-ust-dev icu-dev openssl-dev pigz cpio ninja\n        else\n            echo \"Unsupported distro. distro: $ID\"\n            exit 1\n        fi\n        ;;\n\n    osx|maccatalyst|ios|iossimulator|tvos|tvossimulator)\n        echo \"Installed xcode version: $(xcode-select -p)\"\n\n        export HOMEBREW_NO_INSTALL_CLEANUP=1\n        export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1\n        # Skip brew update for now, see https://github.com/actions/setup-python/issues/577\n        # brew update --preinstall\n        brew bundle --no-upgrade --file=- <<EOF\nbrew \"cmake\"\nbrew \"icu4c\"\nbrew \"openssl@3\"\nbrew \"pkgconf\"\nbrew \"python3\"\nbrew \"pigz\"\nbrew \"ninja\"\nEOF\n        ;;\n\n    *)\n        echo \"Unsupported platform. OS: $os\"\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "eng/common/native/install-tool.ps1",
    "content": "<#\n.SYNOPSIS\nInstall native tool\n\n.DESCRIPTION\nInstall cmake native tool from Azure blob storage\n\n.PARAMETER InstallPath\nBase directory to install native tool to\n\n.PARAMETER BaseUri\nBase file directory or Url from which to acquire tool archives\n\n.PARAMETER CommonLibraryDirectory\nPath to folder containing common library modules\n\n.PARAMETER Force\nForce install of tools even if they previously exist\n\n.PARAMETER Clean\nDon't install the tool, just clean up the current install of the tool\n\n.PARAMETER DownloadRetries\nTotal number of retry attempts\n\n.PARAMETER RetryWaitTimeInSeconds\nWait time between retry attempts in seconds\n\n.NOTES\nReturns 0 if install succeeds, 1 otherwise\n#>\n[CmdletBinding(PositionalBinding=$false)]\nParam (\n  [Parameter(Mandatory=$True)]\n  [string] $ToolName,\n  [Parameter(Mandatory=$True)]\n  [string] $InstallPath,\n  [Parameter(Mandatory=$True)]\n  [string] $BaseUri,\n  [Parameter(Mandatory=$True)]\n  [string] $Version,\n  [string] $CommonLibraryDirectory = $PSScriptRoot,\n  [switch] $Force = $False,\n  [switch] $Clean = $False,\n  [int] $DownloadRetries = 5,\n  [int] $RetryWaitTimeInSeconds = 30\n)\n\n. $PSScriptRoot\\..\\pipeline-logging-functions.ps1\n\n# Import common library modules\nImport-Module -Name (Join-Path $CommonLibraryDirectory \"CommonLibrary.psm1\")\n\ntry {\n  # Define verbose switch if undefined\n  $Verbose = $VerbosePreference -Eq \"Continue\"\n\n  $Arch = CommonLibrary\\Get-MachineArchitecture\n  $ToolOs = \"win64\"\n  if($Arch -Eq \"x32\") {\n    $ToolOs = \"win32\"\n  }\n  $ToolNameMoniker = \"$ToolName-$Version-$ToolOs-$Arch\"\n  $ToolInstallDirectory = Join-Path $InstallPath \"$ToolName\\$Version\\\"\n  $Uri = \"$BaseUri/windows/$ToolName/$ToolNameMoniker.zip\"\n  $ShimPath = Join-Path $InstallPath \"$ToolName.exe\"\n\n  if ($Clean) {\n    Write-Host \"Cleaning $ToolInstallDirectory\"\n    if (Test-Path $ToolInstallDirectory) {\n      Remove-Item $ToolInstallDirectory -Force -Recurse\n    }\n    Write-Host \"Cleaning $ShimPath\"\n    if (Test-Path $ShimPath) {\n      Remove-Item $ShimPath -Force\n    }\n    $ToolTempPath = CommonLibrary\\Get-TempPathFilename -Path $Uri\n    Write-Host \"Cleaning $ToolTempPath\"\n    if (Test-Path $ToolTempPath) {\n      Remove-Item $ToolTempPath -Force\n    }\n    exit 0\n  }\n\n  # Install tool\n  if ((Test-Path $ToolInstallDirectory) -And (-Not $Force)) {\n    Write-Verbose \"$ToolName ($Version) already exists, skipping install\"\n  }\n  else {\n    $InstallStatus = CommonLibrary\\DownloadAndExtract -Uri $Uri `\n                                                      -InstallDirectory $ToolInstallDirectory `\n                                                      -Force:$Force `\n                                                      -DownloadRetries $DownloadRetries `\n                                                      -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds `\n                                                      -Verbose:$Verbose\n\n    if ($InstallStatus -Eq $False) {\n      Write-PipelineTelemetryError \"Installation failed\" -Category \"NativeToolsetBootstrapping\"\n      exit 1\n    }\n  }\n\n  $ToolFilePath = Get-ChildItem $ToolInstallDirectory -Recurse -Filter \"$ToolName.exe\" | % { $_.FullName }\n  if (@($ToolFilePath).Length -Gt 1) {\n    Write-Error \"There are multiple copies of $ToolName in $($ToolInstallDirectory): `n$(@($ToolFilePath | out-string))\"\n    exit 1\n  } elseif (@($ToolFilePath).Length -Lt 1) {\n    Write-Host \"$ToolName was not found in $ToolInstallDirectory.\"\n    exit 1\n  }\n\n  # Generate shim\n  # Always rewrite shims so that we are referencing the expected version\n  $GenerateShimStatus = CommonLibrary\\New-ScriptShim -ShimName $ToolName `\n                                                     -ShimDirectory $InstallPath `\n                                                     -ToolFilePath \"$ToolFilePath\" `\n                                                     -BaseUri $BaseUri `\n                                                     -Force:$Force `\n                                                     -Verbose:$Verbose\n\n  if ($GenerateShimStatus -Eq $False) {\n    Write-PipelineTelemetryError \"Generate shim failed\" -Category \"NativeToolsetBootstrapping\"\n    return 1\n  }\n\n  exit 0\n}\ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Category \"NativeToolsetBootstrapping\" -Message $_\n  exit 1\n}\n"
  },
  {
    "path": "eng/common/pipeline-logging-functions.ps1",
    "content": "# Source for this file was taken from https://github.com/microsoft/azure-pipelines-task-lib/blob/11c9439d4af17e6475d9fe058e6b2e03914d17e6/powershell/VstsTaskSdk/LoggingCommandFunctions.ps1 and modified.\n\n# NOTE: You should not be calling these method directly as they are likely to change.  Instead you should be calling the Write-Pipeline* functions defined in tools.ps1\n\n$script:loggingCommandPrefix = '##vso['\n$script:loggingCommandEscapeMappings = @( # TODO: WHAT ABOUT \"=\"? WHAT ABOUT \"%\"?\n    New-Object psobject -Property @{ Token = ';' ; Replacement = '%3B' }\n    New-Object psobject -Property @{ Token = \"`r\" ; Replacement = '%0D' }\n    New-Object psobject -Property @{ Token = \"`n\" ; Replacement = '%0A' }\n    New-Object psobject -Property @{ Token = \"]\" ; Replacement = '%5D' }\n)\n# TODO: BUG: Escape % ???\n# TODO: Add test to verify don't need to escape \"=\".\n\n# Specify \"-Force\" to force pipeline formatted output even if \"$ci\" is false or not set\nfunction Write-PipelineTelemetryError {\n    [CmdletBinding()]\n    param(\n        [Parameter(Mandatory = $true)]\n        [string]$Category,\n        [Parameter(Mandatory = $true)]\n        [string]$Message,\n        [Parameter(Mandatory = $false)]\n        [string]$Type = 'error',\n        [string]$ErrCode,\n        [string]$SourcePath,\n        [string]$LineNumber,\n        [string]$ColumnNumber,\n        [switch]$AsOutput,\n        [switch]$Force)\n\n    $PSBoundParameters.Remove('Category') | Out-Null\n\n    if ($Force -Or ((Test-Path variable:ci) -And $ci)) {\n        $Message = \"(NETCORE_ENGINEERING_TELEMETRY=$Category) $Message\"\n    }\n    $PSBoundParameters.Remove('Message') | Out-Null\n    $PSBoundParameters.Add('Message', $Message)\n    Write-PipelineTaskError @PSBoundParameters\n}\n\n# Specify \"-Force\" to force pipeline formatted output even if \"$ci\" is false or not set\nfunction Write-PipelineTaskError {\n    [CmdletBinding()]\n    param(\n        [Parameter(Mandatory = $true)]\n        [string]$Message,\n        [Parameter(Mandatory = $false)]\n        [string]$Type = 'error',\n        [string]$ErrCode,\n        [string]$SourcePath,\n        [string]$LineNumber,\n        [string]$ColumnNumber,\n        [switch]$AsOutput,\n        [switch]$Force\n    )\n\n    if (!$Force -And (-Not (Test-Path variable:ci) -Or !$ci)) {\n        if ($Type -eq 'error') {\n            Write-Host $Message -ForegroundColor Red\n            return\n        }\n        elseif ($Type -eq 'warning') {\n            Write-Host $Message -ForegroundColor Yellow\n            return\n        }\n    }\n\n    if (($Type -ne 'error') -and ($Type -ne 'warning')) {\n        Write-Host $Message\n        return\n    }\n    $PSBoundParameters.Remove('Force') | Out-Null      \n    if (-not $PSBoundParameters.ContainsKey('Type')) {\n        $PSBoundParameters.Add('Type', 'error')\n    }\n    Write-LogIssue @PSBoundParameters\n}\n  \nfunction Write-PipelineSetVariable {\n    [CmdletBinding()]\n    param(\n        [Parameter(Mandatory = $true)]\n        [string]$Name,\n        [string]$Value,\n        [switch]$Secret,\n        [switch]$AsOutput,\n        [bool]$IsMultiJobVariable = $true)\n\n    if ((Test-Path variable:ci) -And $ci) {\n        Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data $Value -Properties @{\n            'variable' = $Name\n            'isSecret' = $Secret\n            'isOutput' = $IsMultiJobVariable\n        } -AsOutput:$AsOutput\n    }\n}\n  \nfunction Write-PipelinePrependPath {\n    [CmdletBinding()]\n    param(\n        [Parameter(Mandatory = $true)]\n        [string]$Path,\n        [switch]$AsOutput)\n\n    if ((Test-Path variable:ci) -And $ci) {\n        Write-LoggingCommand -Area 'task' -Event 'prependpath' -Data $Path -AsOutput:$AsOutput\n    }\n}\n\nfunction Write-PipelineSetResult {\n    [CmdletBinding()]\n    param(\n        [ValidateSet(\"Succeeded\", \"SucceededWithIssues\", \"Failed\", \"Cancelled\", \"Skipped\")]\n        [Parameter(Mandatory = $true)]\n        [string]$Result,\n        [string]$Message)\n    if ((Test-Path variable:ci) -And $ci) {\n        Write-LoggingCommand -Area 'task' -Event 'complete' -Data $Message -Properties @{\n            'result' = $Result\n        }\n    }\n}\n\n<########################################\n# Private functions.\n########################################>\nfunction Format-LoggingCommandData {\n    [CmdletBinding()]\n    param([string]$Value, [switch]$Reverse)\n\n    if (!$Value) {\n        return ''\n    }\n\n    if (!$Reverse) {\n        foreach ($mapping in $script:loggingCommandEscapeMappings) {\n            $Value = $Value.Replace($mapping.Token, $mapping.Replacement)\n        }\n    }\n    else {\n        for ($i = $script:loggingCommandEscapeMappings.Length - 1 ; $i -ge 0 ; $i--) {\n            $mapping = $script:loggingCommandEscapeMappings[$i]\n            $Value = $Value.Replace($mapping.Replacement, $mapping.Token)\n        }\n    }\n\n    return $Value\n}\n\nfunction Format-LoggingCommand {\n    [CmdletBinding()]\n    param(\n        [Parameter(Mandatory = $true)]\n        [string]$Area,\n        [Parameter(Mandatory = $true)]\n        [string]$Event,\n        [string]$Data,\n        [hashtable]$Properties)\n\n    # Append the preamble.\n    [System.Text.StringBuilder]$sb = New-Object -TypeName System.Text.StringBuilder\n    $null = $sb.Append($script:loggingCommandPrefix).Append($Area).Append('.').Append($Event)\n\n    # Append the properties.\n    if ($Properties) {\n        $first = $true\n        foreach ($key in $Properties.Keys) {\n            [string]$value = Format-LoggingCommandData $Properties[$key]\n            if ($value) {\n                if ($first) {\n                    $null = $sb.Append(' ')\n                    $first = $false\n                }\n                else {\n                    $null = $sb.Append(';')\n                }\n\n                $null = $sb.Append(\"$key=$value\")\n            }\n        }\n    }\n\n    # Append the tail and output the value.\n    $Data = Format-LoggingCommandData $Data\n    $sb.Append(']').Append($Data).ToString()\n}\n\nfunction Write-LoggingCommand {\n    [CmdletBinding(DefaultParameterSetName = 'Parameters')]\n    param(\n        [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')]\n        [string]$Area,\n        [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')]\n        [string]$Event,\n        [Parameter(ParameterSetName = 'Parameters')]\n        [string]$Data,\n        [Parameter(ParameterSetName = 'Parameters')]\n        [hashtable]$Properties,\n        [Parameter(Mandatory = $true, ParameterSetName = 'Object')]\n        $Command,\n        [switch]$AsOutput)\n\n    if ($PSCmdlet.ParameterSetName -eq 'Object') {\n        Write-LoggingCommand -Area $Command.Area -Event $Command.Event -Data $Command.Data -Properties $Command.Properties -AsOutput:$AsOutput\n        return\n    }\n\n    $command = Format-LoggingCommand -Area $Area -Event $Event -Data $Data -Properties $Properties\n    if ($AsOutput) {\n        $command\n    }\n    else {\n        Write-Host $command\n    }\n}\n\nfunction Write-LogIssue {\n    [CmdletBinding()]\n    param(\n        [ValidateSet('warning', 'error')]\n        [Parameter(Mandatory = $true)]\n        [string]$Type,\n        [string]$Message,\n        [string]$ErrCode,\n        [string]$SourcePath,\n        [string]$LineNumber,\n        [string]$ColumnNumber,\n        [switch]$AsOutput)\n\n    $command = Format-LoggingCommand -Area 'task' -Event 'logissue' -Data $Message -Properties @{\n        'type'         = $Type\n        'code'         = $ErrCode\n        'sourcepath'   = $SourcePath\n        'linenumber'   = $LineNumber\n        'columnnumber' = $ColumnNumber\n    }\n    if ($AsOutput) {\n        return $command\n    }\n\n    if ($Type -eq 'error') {\n        $foregroundColor = $host.PrivateData.ErrorForegroundColor\n        $backgroundColor = $host.PrivateData.ErrorBackgroundColor\n        if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) {\n            $foregroundColor = [System.ConsoleColor]::Red\n            $backgroundColor = [System.ConsoleColor]::Black\n        }\n    }\n    else {\n        $foregroundColor = $host.PrivateData.WarningForegroundColor\n        $backgroundColor = $host.PrivateData.WarningBackgroundColor\n        if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) {\n            $foregroundColor = [System.ConsoleColor]::Yellow\n            $backgroundColor = [System.ConsoleColor]::Black\n        }\n    }\n\n    Write-Host $command -ForegroundColor $foregroundColor -BackgroundColor $backgroundColor\n}\n"
  },
  {
    "path": "eng/common/pipeline-logging-functions.sh",
    "content": "#!/usr/bin/env bash\n\nfunction Write-PipelineTelemetryError {\n  local telemetry_category=''\n  local force=false\n  local function_args=()\n  local message=''\n  while [[ $# -gt 0 ]]; do\n    opt=\"$(echo \"${1/#--/-}\" | tr \"[:upper:]\" \"[:lower:]\")\"\n    case \"$opt\" in\n      -category|-c)\n        telemetry_category=$2\n        shift\n        ;;\n      -force|-f)\n        force=true\n        ;;\n      -*)\n        function_args+=(\"$1 $2\")\n        shift\n        ;;\n      *)\n        message=$*\n        ;;\n    esac\n    shift\n  done\n\n  if [[ $force != true ]] && [[ \"$ci\" != true ]]; then\n    echo \"$message\" >&2\n    return\n  fi\n\n  if [[ $force == true ]]; then\n    function_args+=(\"-force\")\n  fi\n  message=\"(NETCORE_ENGINEERING_TELEMETRY=$telemetry_category) $message\"\n  function_args+=(\"$message\")\n  Write-PipelineTaskError ${function_args[@]}\n}\n\nfunction Write-PipelineTaskError {\n  local message_type=\"error\"\n  local sourcepath=''\n  local linenumber=''\n  local columnnumber=''\n  local error_code=''\n  local force=false\n\n  while [[ $# -gt 0 ]]; do\n    opt=\"$(echo \"${1/#--/-}\" | tr \"[:upper:]\" \"[:lower:]\")\"\n    case \"$opt\" in\n      -type|-t)\n        message_type=$2\n        shift\n        ;;\n      -sourcepath|-s)\n        sourcepath=$2\n        shift\n        ;;\n      -linenumber|-ln)\n        linenumber=$2\n        shift\n        ;;\n      -columnnumber|-cn)\n        columnnumber=$2\n        shift\n        ;;\n      -errcode|-e)\n        error_code=$2\n        shift\n        ;;\n      -force|-f)\n        force=true\n        ;;\n      *)\n        break\n        ;;\n    esac\n\n    shift\n  done\n\n  if [[ $force != true ]] && [[ \"$ci\" != true ]]; then\n    echo \"$@\" >&2\n    return\n  fi\n\n  local message=\"##vso[task.logissue\"\n\n  message=\"$message type=$message_type\"\n\n  if [ -n \"$sourcepath\" ]; then\n    message=\"$message;sourcepath=$sourcepath\"\n  fi\n\n  if [ -n \"$linenumber\" ]; then\n    message=\"$message;linenumber=$linenumber\"\n  fi\n\n  if [ -n \"$columnnumber\" ]; then\n    message=\"$message;columnnumber=$columnnumber\"\n  fi\n\n  if [ -n \"$error_code\" ]; then\n    message=\"$message;code=$error_code\"\n  fi\n\n  message=\"$message]$*\"\n  echo \"$message\"\n}\n\nfunction Write-PipelineSetVariable {\n  if [[ \"$ci\" != true ]]; then\n    return\n  fi\n\n  local name=''\n  local value=''\n  local secret=false\n  local as_output=false\n  local is_multi_job_variable=true\n\n  while [[ $# -gt 0 ]]; do\n    opt=\"$(echo \"${1/#--/-}\" | tr \"[:upper:]\" \"[:lower:]\")\"\n    case \"$opt\" in\n      -name|-n)\n        name=$2\n        shift\n        ;;\n      -value|-v)\n        value=$2\n        shift\n        ;;\n      -secret|-s)\n        secret=true\n        ;;\n      -as_output|-a)\n        as_output=true\n        ;;\n      -is_multi_job_variable|-i)\n        is_multi_job_variable=$2\n        shift\n        ;;\n    esac\n    shift\n  done\n\n  value=${value/;/%3B}\n  value=${value/\\\\r/%0D}\n  value=${value/\\\\n/%0A}\n  value=${value/]/%5D}\n\n  local message=\"##vso[task.setvariable variable=$name;isSecret=$secret;isOutput=$is_multi_job_variable]$value\"\n\n  if [[ \"$as_output\" == true ]]; then\n    $message\n  else\n    echo \"$message\"\n  fi\n}\n\nfunction Write-PipelinePrependPath {\n  local prepend_path=''\n\n  while [[ $# -gt 0 ]]; do\n    opt=\"$(echo \"${1/#--/-}\" | tr \"[:upper:]\" \"[:lower:]\")\"\n    case \"$opt\" in\n      -path|-p)\n        prepend_path=$2\n        shift\n        ;;\n    esac\n    shift\n  done\n\n  export PATH=\"$prepend_path:$PATH\"\n\n  if [[ \"$ci\" == true ]]; then\n    echo \"##vso[task.prependpath]$prepend_path\"\n  fi\n}\n\nfunction Write-PipelineSetResult {\n  local result=''\n  local message=''\n\n  while [[ $# -gt 0 ]]; do\n    opt=\"$(echo \"${1/#--/-}\" | tr \"[:upper:]\" \"[:lower:]\")\"\n    case \"$opt\" in\n      -result|-r)\n        result=$2\n        shift\n        ;;\n      -message|-m)\n        message=$2\n        shift\n        ;;\n    esac\n    shift\n  done\n\n  if [[ \"$ci\" == true ]]; then\n    echo \"##vso[task.complete result=$result;]$message\"\n  fi\n}\n"
  },
  {
    "path": "eng/common/post-build/check-channel-consistency.ps1",
    "content": "param(\n  [Parameter(Mandatory=$true)][string] $PromoteToChannels,            # List of channels that the build should be promoted to\n  [Parameter(Mandatory=$true)][array] $AvailableChannelIds            # List of channel IDs available in the YAML implementation\n)\n\ntry {\n  $ErrorActionPreference = 'Stop'\n  Set-StrictMode -Version 2.0\n\n  # `tools.ps1` checks $ci to perform some actions. Since the post-build\n  # scripts don't necessarily execute in the same agent that run the\n  # build.ps1/sh script this variable isn't automatically set.\n  $ci = $true\n  $disableConfigureToolsetImport = $true\n  . $PSScriptRoot\\..\\tools.ps1\n\n  if ($PromoteToChannels -eq \"\") {\n    Write-PipelineTaskError -Type 'warning' -Message \"This build won't publish assets as it's not configured to any Maestro channel. If that wasn't intended use Darc to configure a default channel using add-default-channel for this branch or to promote it to a channel using add-build-to-channel. See https://github.com/dotnet/arcade/blob/main/Documentation/Darc.md#assigning-an-individual-build-to-a-channel for more info.\"\n    ExitWithExitCode 0\n  }\n\n  # Check that every channel that Maestro told to promote the build to \n  # is available in YAML\n  $PromoteToChannelsIds = $PromoteToChannels -split \"\\D\" | Where-Object { $_ }\n\n  $hasErrors = $false\n\n  foreach ($id in $PromoteToChannelsIds) {\n    if (($id -ne 0) -and ($id -notin $AvailableChannelIds)) {\n      Write-PipelineTaskError -Message \"Channel $id is not present in the post-build YAML configuration! This is an error scenario. Please contact @dnceng.\"\n      $hasErrors = $true\n    }\n  }\n\n  # The `Write-PipelineTaskError` doesn't error the script and we might report several errors\n  # in the previous lines. The check below makes sure that we return an error state from the\n  # script if we reported any validation error\n  if ($hasErrors) {\n    ExitWithExitCode 1 \n  }\n\n  Write-Host 'done.'\n} \ncatch {\n  Write-Host $_\n  Write-PipelineTelemetryError -Category 'CheckChannelConsistency' -Message \"There was an error while trying to check consistency of Maestro default channels for the build and post-build YAML configuration.\"\n  ExitWithExitCode 1\n}\n"
  },
  {
    "path": "eng/common/post-build/nuget-validation.ps1",
    "content": "# This script validates NuGet package metadata information using this \n# tool: https://github.com/NuGet/NuGetGallery/tree/jver-verify/src/VerifyMicrosoftPackage\n\nparam(\n  [Parameter(Mandatory=$true)][string] $PackagesPath # Path to where the packages to be validated are\n)\n\n# `tools.ps1` checks $ci to perform some actions. Since the post-build\n# scripts don't necessarily execute in the same agent that run the\n# build.ps1/sh script this variable isn't automatically set.\n$ci = $true\n$disableConfigureToolsetImport = $true\n. $PSScriptRoot\\..\\tools.ps1\n\ntry {\n  & $PSScriptRoot\\nuget-verification.ps1 ${PackagesPath}\\*.nupkg\n} \ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Category 'NuGetValidation' -Message $_\n  ExitWithExitCode 1\n}\n"
  },
  {
    "path": "eng/common/post-build/nuget-verification.ps1",
    "content": "<#\n.SYNOPSIS\n    Verifies that Microsoft NuGet packages have proper metadata.\n.DESCRIPTION\n    Downloads a verification tool and runs metadata validation on the provided NuGet packages. This script writes an\n    error if any of the provided packages fail validation. All arguments provided to this PowerShell script that do not\n    match PowerShell parameters are passed on to the verification tool downloaded during the execution of this script.\n.PARAMETER NuGetExePath\n    The path to the nuget.exe binary to use. If not provided, nuget.exe will be downloaded into the -DownloadPath\n    directory.\n.PARAMETER PackageSource\n    The package source to use to download the verification tool. If not provided, nuget.org will be used.\n.PARAMETER DownloadPath\n    The directory path to download the verification tool and nuget.exe to. If not provided,\n    %TEMP%\\NuGet.VerifyNuGetPackage will be used.\n.PARAMETER args\n    Arguments that will be passed to the verification tool.\n.EXAMPLE\n    PS> .\\verify.ps1 *.nupkg\n    Verifies the metadata of all .nupkg files in the currect working directory.\n.EXAMPLE\n    PS> .\\verify.ps1 --help\n    Displays the help text of the downloaded verifiction tool.\n.LINK\n    https://github.com/NuGet/NuGetGallery/blob/master/src/VerifyMicrosoftPackage/README.md\n#>\n\n# This script was copied from https://github.com/NuGet/NuGetGallery/blob/3e25ad135146676bcab0050a516939d9958bfa5d/src/VerifyMicrosoftPackage/verify.ps1\n\n[CmdletBinding(PositionalBinding = $false)]\nparam(\n   [string]$NuGetExePath,\n   [string]$PackageSource = \"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json\",\n   [string]$DownloadPath,\n   [Parameter(ValueFromRemainingArguments = $true)]\n   [string[]]$args\n)\n\n# The URL to download nuget.exe.\n$nugetExeUrl = \"https://dist.nuget.org/win-x86-commandline/v4.9.4/nuget.exe\"\n\n# The package ID of the verification tool.\n$packageId = \"NuGet.VerifyMicrosoftPackage\"\n\n# The location that nuget.exe and the verification tool will be downloaded to.\nif (!$DownloadPath) {\n    $DownloadPath = (Join-Path $env:TEMP \"NuGet.VerifyMicrosoftPackage\")\n}\n\n$fence = New-Object -TypeName string -ArgumentList '=', 80\n\n# Create the download directory, if it doesn't already exist.\nif (!(Test-Path $DownloadPath)) {\n    New-Item -ItemType Directory $DownloadPath | Out-Null\n}\nWrite-Host \"Using download path: $DownloadPath\"\n\nif ($NuGetExePath) {\n    $nuget = $NuGetExePath\n} else {\n    $downloadedNuGetExe = Join-Path $DownloadPath \"nuget.exe\"\n    \n    # Download nuget.exe, if it doesn't already exist.\n    if (!(Test-Path $downloadedNuGetExe)) {\n        Write-Host \"Downloading nuget.exe from $nugetExeUrl...\"\n        $ProgressPreference = 'SilentlyContinue'\n        try {\n            Invoke-WebRequest $nugetExeUrl -UseBasicParsing -OutFile $downloadedNuGetExe\n            $ProgressPreference = 'Continue'\n        } catch {\n            $ProgressPreference = 'Continue'\n            Write-Error $_\n            Write-Error \"nuget.exe failed to download.\"\n            exit\n        }\n    }\n\n    $nuget = $downloadedNuGetExe\n}\n\nWrite-Host \"Using nuget.exe path: $nuget\"\nWrite-Host \" \"\n\n# Download the latest version of the verification tool.\nWrite-Host \"Downloading the latest version of $packageId from $packageSource...\"\nWrite-Host $fence\n& $nuget install $packageId `\n    -Prerelease `\n    -OutputDirectory $DownloadPath `\n    -Source $PackageSource\nWrite-Host $fence\nWrite-Host \" \"\n\nif ($LASTEXITCODE -ne 0) {\n    Write-Error \"nuget.exe failed to fetch the verify tool.\"\n    exit\n}\n\n# Find the most recently downloaded tool\nWrite-Host \"Finding the most recently downloaded verification tool.\"\n$verifyProbePath = Join-Path $DownloadPath \"$packageId.*\"\n$verifyPath = Get-ChildItem -Path $verifyProbePath -Directory `\n    | Sort-Object -Property LastWriteTime -Descending `\n    | Select-Object -First 1\n$verify = Join-Path $verifyPath \"tools\\NuGet.VerifyMicrosoftPackage.exe\"\nWrite-Host \"Using verification tool: $verify\"\nWrite-Host \" \"\n\n# Execute the verification tool.\nWrite-Host \"Executing the verify tool...\"\nWrite-Host $fence\n& $verify $args\nWrite-Host $fence\nWrite-Host \" \"\n\n# Respond to the exit code.\nif ($LASTEXITCODE -ne 0) {\n    Write-Error \"The verify tool found some problems.\"\n} else {\n    Write-Output \"The verify tool succeeded.\"\n}\n"
  },
  {
    "path": "eng/common/post-build/publish-using-darc.ps1",
    "content": "param(\n  [Parameter(Mandatory=$true)][int] $BuildId,\n  [Parameter(Mandatory=$true)][int] $PublishingInfraVersion,\n  [Parameter(Mandatory=$true)][string] $AzdoToken,\n  [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = 'https://maestro.dot.net',\n  [Parameter(Mandatory=$true)][string] $WaitPublishingFinish,\n  [Parameter(Mandatory=$false)][string] $ArtifactsPublishingAdditionalParameters,\n  [Parameter(Mandatory=$false)][string] $SymbolPublishingAdditionalParameters,\n  [Parameter(Mandatory=$false)][string] $RequireDefaultChannels,\n  [Parameter(Mandatory=$false)][string] $SkipAssetsPublishing,\n  [Parameter(Mandatory=$false)][string] $runtimeSourceFeed,\n  [Parameter(Mandatory=$false)][string] $runtimeSourceFeedKey\n)\n\ntry {\n  # `tools.ps1` checks $ci to perform some actions. Since the post-build\n  # scripts don't necessarily execute in the same agent that run the\n  # build.ps1/sh script this variable isn't automatically set.\n  $ci = $true\n  $disableConfigureToolsetImport = $true\n  . $PSScriptRoot\\..\\tools.ps1\n\n  $darc = Get-Darc\n\n  $optionalParams = [System.Collections.ArrayList]::new()\n\n  if (\"\" -ne $ArtifactsPublishingAdditionalParameters) {\n    $optionalParams.Add(\"--artifact-publishing-parameters\") | Out-Null\n    $optionalParams.Add($ArtifactsPublishingAdditionalParameters) | Out-Null\n  }\n\n  if (\"\" -ne $SymbolPublishingAdditionalParameters) {\n    $optionalParams.Add(\"--symbol-publishing-parameters\") | Out-Null\n    $optionalParams.Add($SymbolPublishingAdditionalParameters) | Out-Null\n  }\n\n  if (\"false\" -eq $WaitPublishingFinish) {\n    $optionalParams.Add(\"--no-wait\") | Out-Null\n  }\n  \n  if (\"true\" -eq $RequireDefaultChannels) {\n    $optionalParams.Add(\"--default-channels-required\") | Out-Null\n  }\n\n  if (\"true\" -eq $SkipAssetsPublishing) {\n    $optionalParams.Add(\"--skip-assets-publishing\") | Out-Null\n  }\n\n  & $darc add-build-to-channel `\n    --id $buildId `\n    --publishing-infra-version $PublishingInfraVersion `\n    --default-channels `\n    --source-branch main `\n    --azdev-pat \"$AzdoToken\" `\n    --bar-uri \"$MaestroApiEndPoint\" `\n    --ci `\n    --verbose `\n\t@optionalParams\n\n  if ($LastExitCode -ne 0) {\n    Write-Host \"Problems using Darc to promote build ${buildId} to default channels. Stopping execution...\"\n    exit 1\n  }\n\n  Write-Host 'done.'\n}\ncatch {\n  Write-Host $_\n  Write-PipelineTelemetryError -Category 'PromoteBuild' -Message \"There was an error while trying to publish build '$BuildId' to default channels.\"\n  ExitWithExitCode 1\n}\n"
  },
  {
    "path": "eng/common/post-build/redact-logs.ps1",
    "content": "[CmdletBinding(PositionalBinding=$False)]\nparam(\n  [Parameter(Mandatory=$true, Position=0)][string] $InputPath,\n  [Parameter(Mandatory=$true)][string] $BinlogToolVersion,\n  [Parameter(Mandatory=$false)][string] $DotnetPath,\n  [Parameter(Mandatory=$false)][string] $PackageFeed = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json',\n  # File with strings to redact - separated by newlines.\n  #  For comments start the line with '# ' - such lines are ignored \n  [Parameter(Mandatory=$false)][string] $TokensFilePath,\n  [Parameter(ValueFromRemainingArguments=$true)][String[]]$TokensToRedact,\n  [Parameter(Mandatory=$false)][string] $runtimeSourceFeed,\n  [Parameter(Mandatory=$false)][string] $runtimeSourceFeedKey\n)\n\ntry {\n  $ErrorActionPreference = 'Stop'\n  Set-StrictMode -Version 2.0\n\n  # `tools.ps1` checks $ci to perform some actions. Since the post-build\n  # scripts don't necessarily execute in the same agent that run the\n  # build.ps1/sh script this variable isn't automatically set.\n  $ci = $true\n  $disableConfigureToolsetImport = $true\n  . $PSScriptRoot\\..\\tools.ps1\n\n  $packageName = 'binlogtool'\n\n  $dotnet = $DotnetPath\n\n  if (!$dotnet) {\n    $dotnetRoot = InitializeDotNetCli -install:$true\n    $dotnet = \"$dotnetRoot\\dotnet.exe\"\n  }\n  \n  $toolList = & \"$dotnet\" tool list -g\n\n  if ($toolList -like \"*$packageName*\") {\n    & \"$dotnet\" tool uninstall $packageName -g\n  }\n\n  $toolPath  = \"$PSScriptRoot\\..\\..\\..\\.tools\"\n  $verbosity = 'minimal'\n  \n  New-Item -ItemType Directory -Force -Path $toolPath\n  \n  Push-Location -Path $toolPath\n\n  try {\n    Write-Host \"Installing Binlog redactor CLI...\"\n    Write-Host \"'$dotnet' new tool-manifest\"\n    & \"$dotnet\" new tool-manifest\n    Write-Host \"'$dotnet' tool install $packageName --local --add-source '$PackageFeed' -v $verbosity --version $BinlogToolVersion\"\n    & \"$dotnet\" tool install $packageName --local --add-source \"$PackageFeed\" -v $verbosity --version $BinlogToolVersion\n\n    if (Test-Path $TokensFilePath) {\n        Write-Host \"Adding additional sensitive data for redaction from file: \" $TokensFilePath\n        $TokensToRedact += Get-Content -Path $TokensFilePath | Foreach {$_.Trim()} | Where { $_ -notmatch \"^# \" }\n    }\n\n    $optionalParams = [System.Collections.ArrayList]::new()\n  \n    Foreach ($p in $TokensToRedact)\n    {\n      if($p -match '^\\$\\(.*\\)$')\n      {\n        Write-Host (\"Ignoring token {0} as it is probably unexpanded AzDO variable\"  -f $p)\n      }          \n      elseif($p)\n      {\n        $optionalParams.Add(\"-p:\" + $p) | Out-Null\n      }\n    }\n\n    & $dotnet binlogtool redact --input:$InputPath --recurse --in-place `\n      @optionalParams\n\n    if ($LastExitCode -ne 0) {\n      Write-PipelineTelemetryError -Category 'Redactor' -Type 'warning' -Message \"Problems using Redactor tool (exit code: $LastExitCode). But ignoring them now.\"\n    }\n  }\n  finally {\n    Pop-Location\n  }\n\n  Write-Host 'done.'\n} \ncatch {\n  Write-Host $_\n  Write-PipelineTelemetryError -Category 'Redactor' -Message \"There was an error while trying to redact logs. Error: $_\"\n  ExitWithExitCode 1\n}\n"
  },
  {
    "path": "eng/common/post-build/sourcelink-validation.ps1",
    "content": "param(\n  [Parameter(Mandatory=$true)][string] $InputPath,              # Full path to directory where Symbols.NuGet packages to be checked are stored\n  [Parameter(Mandatory=$true)][string] $ExtractPath,            # Full path to directory where the packages will be extracted during validation\n  [Parameter(Mandatory=$false)][string] $GHRepoName,            # GitHub name of the repo including the Org. E.g., dotnet/arcade\n  [Parameter(Mandatory=$false)][string] $GHCommit,              # GitHub commit SHA used to build the packages\n  [Parameter(Mandatory=$true)][string] $SourcelinkCliVersion    # Version of SourceLink CLI to use\n)\n\n$ErrorActionPreference = 'Stop'\nSet-StrictMode -Version 2.0\n\n# `tools.ps1` checks $ci to perform some actions. Since the post-build\n# scripts don't necessarily execute in the same agent that run the\n# build.ps1/sh script this variable isn't automatically set.\n$ci = $true\n$disableConfigureToolsetImport = $true\n. $PSScriptRoot\\..\\tools.ps1\n\n# Cache/HashMap (File -> Exist flag) used to consult whether a file exist \n# in the repository at a specific commit point. This is populated by inserting\n# all files present in the repo at a specific commit point.\n$global:RepoFiles = @{}\n\n# Maximum number of jobs to run in parallel\n$MaxParallelJobs = 16\n\n$MaxRetries = 5\n$RetryWaitTimeInSeconds = 30\n\n# Wait time between check for system load\n$SecondsBetweenLoadChecks = 10\n\nif (!$InputPath -or !(Test-Path $InputPath)){\n  Write-Host \"No files to validate.\"\n  ExitWithExitCode 0\n}\n\n$ValidatePackage = {\n  param( \n    [string] $PackagePath                                 # Full path to a Symbols.NuGet package\n  )\n\n  . $using:PSScriptRoot\\..\\tools.ps1\n\n  # Ensure input file exist\n  if (!(Test-Path $PackagePath)) {\n    Write-Host \"Input file does not exist: $PackagePath\"\n    return [pscustomobject]@{\n      result = 1\n      packagePath = $PackagePath\n    }\n  }\n\n  # Extensions for which we'll look for SourceLink information\n  # For now we'll only care about Portable & Embedded PDBs\n  $RelevantExtensions = @('.dll', '.exe', '.pdb')\n \n  Write-Host -NoNewLine 'Validating ' ([System.IO.Path]::GetFileName($PackagePath)) '...'\n\n  $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath)\n  $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageId\n  $FailedFiles = 0\n\n  Add-Type -AssemblyName System.IO.Compression.FileSystem\n\n  [System.IO.Directory]::CreateDirectory($ExtractPath)  | Out-Null\n\n  try {\n    $zip = [System.IO.Compression.ZipFile]::OpenRead($PackagePath)\n\n    $zip.Entries | \n      Where-Object {$RelevantExtensions -contains [System.IO.Path]::GetExtension($_.Name)} |\n        ForEach-Object {\n          $FileName = $_.FullName\n          $Extension = [System.IO.Path]::GetExtension($_.Name)\n          $FakeName = -Join((New-Guid), $Extension)\n          $TargetFile = Join-Path -Path $ExtractPath -ChildPath $FakeName \n\n          # We ignore resource DLLs\n          if ($FileName.EndsWith('.resources.dll')) {\n            return [pscustomobject]@{\n              result = 0\n              packagePath = $PackagePath\n            }\n          }\n\n          [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $TargetFile, $true)\n\n          $ValidateFile = {\n            param( \n              [string] $FullPath,                                # Full path to the module that has to be checked\n              [string] $RealPath,\n              [ref] $FailedFiles\n            )\n\n            $sourcelinkExe = \"$env:USERPROFILE\\.dotnet\\tools\"\n            $sourcelinkExe = Resolve-Path \"$sourcelinkExe\\sourcelink.exe\"\n            $SourceLinkInfos = & $sourcelinkExe print-urls $FullPath | Out-String\n\n            if ($LASTEXITCODE -eq 0 -and -not ([string]::IsNullOrEmpty($SourceLinkInfos))) {\n              $NumFailedLinks = 0\n\n              # We only care about Http addresses\n              $Matches = (Select-String '(http[s]?)(:\\/\\/)([^\\s,]+)' -Input $SourceLinkInfos -AllMatches).Matches\n\n              if ($Matches.Count -ne 0) {\n                $Matches.Value |\n                  ForEach-Object {\n                    $Link = $_\n                    $CommitUrl = \"https://raw.githubusercontent.com/${using:GHRepoName}/${using:GHCommit}/\"\n                    \n                    $FilePath = $Link.Replace($CommitUrl, \"\")\n                    $Status = 200\n                    $Cache = $using:RepoFiles\n\n                    $attempts = 0\n\n                    while ($attempts -lt $using:MaxRetries) {\n                      if ( !($Cache.ContainsKey($FilePath)) ) {\n                        try {\n                          $Uri = $Link -as [System.URI]\n                        \n                          if ($Link -match \"submodules\") {\n                            # Skip submodule links until sourcelink properly handles submodules\n                            $Status = 200\n                          }\n                          elseif ($Uri.AbsoluteURI -ne $null -and ($Uri.Host -match 'github' -or $Uri.Host -match 'githubusercontent')) {\n                            # Only GitHub links are valid\n                            $Status = (Invoke-WebRequest -Uri $Link -UseBasicParsing -Method HEAD -TimeoutSec 5).StatusCode\n                          }\n                          else {\n                            # If it's not a github link, we want to break out of the loop and not retry.\n                            $Status = 0\n                            $attempts = $using:MaxRetries\n                          }\n                        }\n                        catch {\n                          Write-Host $_\n                          $Status = 0\n                        }\n                      }\n\n                      if ($Status -ne 200) {\n                        $attempts++\n                        \n                        if  ($attempts -lt $using:MaxRetries)\n                        {\n                          $attemptsLeft = $using:MaxRetries - $attempts\n                          Write-Warning \"Download failed, $attemptsLeft attempts remaining, will retry in $using:RetryWaitTimeInSeconds seconds\"\n                          Start-Sleep -Seconds $using:RetryWaitTimeInSeconds\n                        }\n                        else {\n                          if ($NumFailedLinks -eq 0) {\n                            if ($FailedFiles.Value -eq 0) {\n                              Write-Host\n                            }\n  \n                            Write-Host \"`tFile $RealPath has broken links:\"\n                          }\n  \n                          Write-Host \"`t`tFailed to retrieve $Link\"\n  \n                          $NumFailedLinks++\n                        }\n                      }\n                      else {\n                        break\n                      }\n                    }\n                  }\n              }\n\n              if ($NumFailedLinks -ne 0) {\n                $FailedFiles.value++\n                $global:LASTEXITCODE = 1\n              }\n            }\n          }\n        \n          &$ValidateFile $TargetFile $FileName ([ref]$FailedFiles)\n        }\n  }\n  catch {\n    Write-Host $_\n  }\n  finally {\n    $zip.Dispose() \n  }\n\n  if ($FailedFiles -eq 0) {\n    Write-Host 'Passed.'\n    return [pscustomobject]@{\n      result = 0\n      packagePath = $PackagePath\n    }\n  }\n  else {\n    Write-PipelineTelemetryError -Category 'SourceLink' -Message \"$PackagePath has broken SourceLink links.\"\n    return [pscustomobject]@{\n      result = 1\n      packagePath = $PackagePath\n    }\n  }\n}\n\nfunction CheckJobResult(\n    $result, \n    $packagePath,\n    [ref]$ValidationFailures,\n    [switch]$logErrors) {\n  if ($result -ne '0') {\n    if ($logErrors) {\n      Write-PipelineTelemetryError -Category 'SourceLink' -Message \"$packagePath has broken SourceLink links.\"\n    }\n    $ValidationFailures.Value++\n  }\n}\n\nfunction ValidateSourceLinkLinks {\n  if ($GHRepoName -ne '' -and !($GHRepoName -Match '^[^\\s\\/]+/[^\\s\\/]+$')) {\n    if (!($GHRepoName -Match '^[^\\s-]+-[^\\s]+$')) {\n      Write-PipelineTelemetryError -Category 'SourceLink' -Message \"GHRepoName should be in the format <org>/<repo> or <org>-<repo>. '$GHRepoName'\"\n      ExitWithExitCode 1\n    }\n    else {\n      $GHRepoName = $GHRepoName -replace '^([^\\s-]+)-([^\\s]+)$', '$1/$2';\n    }\n  }\n\n  if ($GHCommit -ne '' -and !($GHCommit -Match '^[0-9a-fA-F]{40}$')) {\n    Write-PipelineTelemetryError -Category 'SourceLink' -Message \"GHCommit should be a 40 chars hexadecimal string. '$GHCommit'\"\n    ExitWithExitCode 1\n  }\n\n  if ($GHRepoName -ne '' -and $GHCommit -ne '') {\n    $RepoTreeURL = -Join('http://api.github.com/repos/', $GHRepoName, '/git/trees/', $GHCommit, '?recursive=1')\n    $CodeExtensions = @('.cs', '.vb', '.fs', '.fsi', '.fsx', '.fsscript')\n\n    try {\n      # Retrieve the list of files in the repo at that particular commit point and store them in the RepoFiles hash\n      $Data = Invoke-WebRequest $RepoTreeURL -UseBasicParsing | ConvertFrom-Json | Select-Object -ExpandProperty tree\n  \n      foreach ($file in $Data) {\n        $Extension = [System.IO.Path]::GetExtension($file.path)\n\n        if ($CodeExtensions.Contains($Extension)) {\n          $RepoFiles[$file.path] = 1\n        }\n      }\n    }\n    catch {\n      Write-Host \"Problems downloading the list of files from the repo. Url used: $RepoTreeURL . Execution will proceed without caching.\"\n    }\n  }\n  elseif ($GHRepoName -ne '' -or $GHCommit -ne '') {\n    Write-Host 'For using the http caching mechanism both GHRepoName and GHCommit should be informed.'\n  }\n  \n  if (Test-Path $ExtractPath) {\n    Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue\n  }\n\n  $ValidationFailures = 0\n\n  # Process each NuGet package in parallel\n  Get-ChildItem \"$InputPath\\*.symbols.nupkg\" |\n    ForEach-Object {\n      Write-Host \"Starting $($_.FullName)\"\n      Start-Job -ScriptBlock $ValidatePackage -ArgumentList $_.FullName | Out-Null\n      $NumJobs = @(Get-Job -State 'Running').Count\n      \n      while ($NumJobs -ge $MaxParallelJobs) {\n        Write-Host \"There are $NumJobs validation jobs running right now. Waiting $SecondsBetweenLoadChecks seconds to check again.\"\n        sleep $SecondsBetweenLoadChecks\n        $NumJobs = @(Get-Job -State 'Running').Count\n      }\n\n      foreach ($Job in @(Get-Job -State 'Completed')) {\n        $jobResult = Wait-Job -Id $Job.Id | Receive-Job\n        CheckJobResult $jobResult.result $jobResult.packagePath ([ref]$ValidationFailures) -LogErrors\n        Remove-Job -Id $Job.Id\n      }\n    }\n\n  foreach ($Job in @(Get-Job)) {\n    $jobResult = Wait-Job -Id $Job.Id | Receive-Job\n    CheckJobResult $jobResult.result $jobResult.packagePath ([ref]$ValidationFailures)\n    Remove-Job -Id $Job.Id\n  }\n  if ($ValidationFailures -gt 0) {\n    Write-PipelineTelemetryError -Category 'SourceLink' -Message \"$ValidationFailures package(s) failed validation.\"\n    ExitWithExitCode 1\n  }\n}\n\nfunction InstallSourcelinkCli {\n  $sourcelinkCliPackageName = 'sourcelink'\n\n  $dotnetRoot = InitializeDotNetCli -install:$true\n  $dotnet = \"$dotnetRoot\\dotnet.exe\"\n  $toolList = & \"$dotnet\" tool list --global\n\n  if (($toolList -like \"*$sourcelinkCliPackageName*\") -and ($toolList -like \"*$sourcelinkCliVersion*\")) {\n    Write-Host \"SourceLink CLI version $sourcelinkCliVersion is already installed.\"\n  }\n  else {\n    Write-Host \"Installing SourceLink CLI version $sourcelinkCliVersion...\"\n    Write-Host 'You may need to restart your command window if this is the first dotnet tool you have installed.'\n    & \"$dotnet\" tool install $sourcelinkCliPackageName --version $sourcelinkCliVersion --verbosity \"minimal\" --global \n  }\n}\n\ntry {\n  InstallSourcelinkCli\n\n  foreach ($Job in @(Get-Job)) {\n    Remove-Job -Id $Job.Id\n  }\n\n  ValidateSourceLinkLinks \n}\ncatch {\n  Write-Host $_.Exception\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Category 'SourceLink' -Message $_\n  ExitWithExitCode 1\n}\n"
  },
  {
    "path": "eng/common/post-build/symbols-validation.ps1",
    "content": "param(\n  [Parameter(Mandatory = $true)][string] $InputPath, # Full path to directory where NuGet packages to be checked are stored\n  [Parameter(Mandatory = $true)][string] $ExtractPath, # Full path to directory where the packages will be extracted during validation\n  [Parameter(Mandatory = $true)][string] $DotnetSymbolVersion, # Version of dotnet symbol to use\n  [Parameter(Mandatory = $false)][switch] $CheckForWindowsPdbs, # If we should check for the existence of windows pdbs in addition to portable PDBs\n  [Parameter(Mandatory = $false)][switch] $ContinueOnError, # If we should keep checking symbols after an error\n  [Parameter(Mandatory = $false)][switch] $Clean,           # Clean extracted symbols directory after checking symbols\n  [Parameter(Mandatory = $false)][string] $SymbolExclusionFile  # Exclude the symbols in the file from publishing to symbol server\n)\n\n. $PSScriptRoot\\..\\tools.ps1\n# Maximum number of jobs to run in parallel\n$MaxParallelJobs = 16\n\n# Max number of retries\n$MaxRetry = 5\n\n# Wait time between check for system load\n$SecondsBetweenLoadChecks = 10\n\n# Set error codes\nSet-Variable -Name \"ERROR_BADEXTRACT\" -Option Constant -Value -1\nSet-Variable -Name \"ERROR_FILEDOESNOTEXIST\" -Option Constant -Value -2\n\n$WindowsPdbVerificationParam = \"\"\nif ($CheckForWindowsPdbs) {\n  $WindowsPdbVerificationParam = \"--windows-pdbs\"\n}\n\n$ExclusionSet = New-Object System.Collections.Generic.HashSet[string];\n\nif (!$InputPath -or !(Test-Path $InputPath)){\n  Write-Host \"No symbols to validate.\"\n  ExitWithExitCode 0\n}\n\n#Check if the path exists\nif ($SymbolExclusionFile -and (Test-Path $SymbolExclusionFile)){\n  [string[]]$Exclusions = Get-Content \"$SymbolExclusionFile\"\n  $Exclusions | foreach { if($_ -and $_.Trim()){$ExclusionSet.Add($_)} }\n}\nelse{\n  Write-Host \"Symbol Exclusion file does not exists. No symbols to exclude.\"\n}\n\n$CountMissingSymbols = {\n  param( \n    [string] $PackagePath, # Path to a NuGet package\n    [string] $WindowsPdbVerificationParam # If we should check for the existence of windows pdbs in addition to portable PDBs\n  )\n\n  Add-Type -AssemblyName System.IO.Compression.FileSystem\n\n  Write-Host \"Validating $PackagePath \"\n\n  # Ensure input file exist\n  if (!(Test-Path $PackagePath)) {\n    Write-PipelineTaskError \"Input file does not exist: $PackagePath\"\n    return [pscustomobject]@{\n      result      = $using:ERROR_FILEDOESNOTEXIST\n      packagePath = $PackagePath\n    }\n  }\n  \n  # Extensions for which we'll look for symbols\n  $RelevantExtensions = @('.dll', '.exe', '.so', '.dylib')\n\n  # How many files are missing symbol information\n  $MissingSymbols = 0\n\n  $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath)\n  $PackageGuid = New-Guid\n  $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageGuid\n  $SymbolsPath = Join-Path -Path $ExtractPath -ChildPath 'Symbols'\n  \n  try {\n    [System.IO.Compression.ZipFile]::ExtractToDirectory($PackagePath, $ExtractPath)\n  }\n  catch {\n    Write-Host \"Something went wrong extracting $PackagePath\"\n    Write-Host $_\n    return [pscustomobject]@{\n      result      = $using:ERROR_BADEXTRACT\n      packagePath = $PackagePath\n    }\n  }\n\n  Get-ChildItem -Recurse $ExtractPath |\n  Where-Object { $RelevantExtensions -contains $_.Extension } |\n  ForEach-Object {\n    $FileName = $_.FullName\n    if ($FileName -Match '\\\\ref\\\\') {\n      Write-Host \"`t Ignoring reference assembly file \" $FileName\n      return\n    }\n\n    $FirstMatchingSymbolDescriptionOrDefault = {\n      param( \n        [string] $FullPath, # Full path to the module that has to be checked\n        [string] $TargetServerParam, # Parameter to pass to `Symbol Tool` indicating the server to lookup for symbols\n        [string] $WindowsPdbVerificationParam, # Parameter to pass to potential check for windows-pdbs.\n        [string] $SymbolsPath\n      )\n\n      $FileName = [System.IO.Path]::GetFileName($FullPath)\n      $Extension = [System.IO.Path]::GetExtension($FullPath)\n\n      # Those below are potential symbol files that the `dotnet symbol` might\n      # return. Which one will be returned depend on the type of file we are\n      # checking and which type of file was uploaded.\n\n      # The file itself is returned\n      $SymbolPath = $SymbolsPath + '\\' + $FileName\n\n      # PDB file for the module\n      $PdbPath = $SymbolPath.Replace($Extension, '.pdb')\n\n      # PDB file for R2R module (created by crossgen)\n      $NGenPdb = $SymbolPath.Replace($Extension, '.ni.pdb')\n\n      # DBG file for a .so library\n      $SODbg = $SymbolPath.Replace($Extension, '.so.dbg')\n\n      # DWARF file for a .dylib\n      $DylibDwarf = $SymbolPath.Replace($Extension, '.dylib.dwarf')\n\n      $dotnetSymbolExe = \"$env:USERPROFILE\\.dotnet\\tools\"\n      $dotnetSymbolExe = Resolve-Path \"$dotnetSymbolExe\\dotnet-symbol.exe\"\n\n      $totalRetries = 0\n\n      while ($totalRetries -lt $using:MaxRetry) {\n\n        # Save the output and get diagnostic output\n        $output = & $dotnetSymbolExe --symbols --modules $WindowsPdbVerificationParam $TargetServerParam $FullPath -o $SymbolsPath --diagnostics | Out-String\n\n        if ((Test-Path $PdbPath) -and (Test-path $SymbolPath)) {\n          return 'Module and PDB for Module'\n        }\n        elseif ((Test-Path $NGenPdb) -and (Test-Path $PdbPath) -and (Test-Path $SymbolPath)) {\n          return 'Dll, PDB and NGen PDB'\n        }\n        elseif ((Test-Path $SODbg) -and (Test-Path $SymbolPath)) {\n          return 'So and DBG for SO'\n        }  \n        elseif ((Test-Path $DylibDwarf) -and (Test-Path $SymbolPath)) {\n          return 'Dylib and Dwarf for Dylib'\n        }  \n        elseif (Test-Path $SymbolPath) {\n          return 'Module'\n        }\n        else\n        {\n          $totalRetries++\n        }\n      }\n      \n      return $null\n    }\n\n    $FileRelativePath = $FileName.Replace(\"$ExtractPath\\\", \"\")\n    if (($($using:ExclusionSet) -ne $null) -and ($($using:ExclusionSet).Contains($FileRelativePath) -or ($($using:ExclusionSet).Contains($FileRelativePath.Replace(\"\\\", \"/\"))))){\n      Write-Host \"Skipping $FileName from symbol validation\"\n    }\n\n    else {\n      $FileGuid = New-Guid\n      $ExpandedSymbolsPath = Join-Path -Path $SymbolsPath -ChildPath $FileGuid\n\n      $SymbolsOnMSDL = & $FirstMatchingSymbolDescriptionOrDefault `\n          -FullPath $FileName `\n          -TargetServerParam '--microsoft-symbol-server' `\n          -SymbolsPath \"$ExpandedSymbolsPath-msdl\" `\n          -WindowsPdbVerificationParam $WindowsPdbVerificationParam\n      $SymbolsOnSymWeb = & $FirstMatchingSymbolDescriptionOrDefault `\n          -FullPath $FileName `\n          -TargetServerParam '--internal-server' `\n          -SymbolsPath \"$ExpandedSymbolsPath-symweb\" `\n          -WindowsPdbVerificationParam $WindowsPdbVerificationParam\n\n      Write-Host -NoNewLine \"`t Checking file \" $FileName \"... \"\n  \n      if ($SymbolsOnMSDL -ne $null -and $SymbolsOnSymWeb -ne $null) {\n        Write-Host \"Symbols found on MSDL ($SymbolsOnMSDL) and SymWeb ($SymbolsOnSymWeb)\"\n      }\n      else {\n        $MissingSymbols++\n\n        if ($SymbolsOnMSDL -eq $null -and $SymbolsOnSymWeb -eq $null) {\n          Write-Host 'No symbols found on MSDL or SymWeb!'\n        }\n        else {\n          if ($SymbolsOnMSDL -eq $null) {\n            Write-Host 'No symbols found on MSDL!'\n          }\n          else {\n            Write-Host 'No symbols found on SymWeb!'\n          }\n        }\n      }\n    }\n  }\n  \n  if ($using:Clean) {\n    Remove-Item $ExtractPath -Recurse -Force\n  }\n  \n  Pop-Location\n\n  return [pscustomobject]@{\n    result      = $MissingSymbols\n    packagePath = $PackagePath\n  }\n}\n\nfunction CheckJobResult(\n  $result, \n  $packagePath,\n  [ref]$DupedSymbols,\n  [ref]$TotalFailures) {\n  if ($result -eq $ERROR_BADEXTRACT) {\n    Write-PipelineTelemetryError -Category 'CheckSymbols' -Message \"$packagePath has duplicated symbol files\"\n    $DupedSymbols.Value++\n  } \n  elseif ($result -eq $ERROR_FILEDOESNOTEXIST) {\n    Write-PipelineTelemetryError -Category 'CheckSymbols' -Message \"$packagePath does not exist\"\n    $TotalFailures.Value++\n  }\n  elseif ($result -gt '0') {\n    Write-PipelineTelemetryError -Category 'CheckSymbols' -Message \"Missing symbols for $result modules in the package $packagePath\"\n    $TotalFailures.Value++\n  }\n  else {\n    Write-Host \"All symbols verified for package $packagePath\"\n  }\n}\n\nfunction CheckSymbolsAvailable {\n  if (Test-Path $ExtractPath) {\n    Remove-Item $ExtractPath -Force  -Recurse -ErrorAction SilentlyContinue\n  }\n\n  $TotalPackages = 0\n  $TotalFailures = 0\n  $DupedSymbols = 0\n\n  Get-ChildItem \"$InputPath\\*.nupkg\" |\n    ForEach-Object {\n      $FileName = $_.Name\n      $FullName = $_.FullName\n\n      # These packages from Arcade-Services include some native libraries that\n      # our current symbol uploader can't handle. Below is a workaround until\n      # we get issue: https://github.com/dotnet/arcade/issues/2457 sorted.\n      if ($FileName -Match 'Microsoft\\.DotNet\\.Darc\\.') {\n        Write-Host \"Ignoring Arcade-services file: $FileName\"\n        Write-Host\n        return\n      }\n      elseif ($FileName -Match 'Microsoft\\.DotNet\\.Maestro\\.Tasks\\.') {\n        Write-Host \"Ignoring Arcade-services file: $FileName\"\n        Write-Host\n        return\n      }\n\n      $TotalPackages++\n\n      Start-Job -ScriptBlock $CountMissingSymbols -ArgumentList @($FullName,$WindowsPdbVerificationParam) | Out-Null\n\n      $NumJobs = @(Get-Job -State 'Running').Count\n\n      while ($NumJobs -ge $MaxParallelJobs) {\n        Write-Host \"There are $NumJobs validation jobs running right now. Waiting $SecondsBetweenLoadChecks seconds to check again.\"\n        sleep $SecondsBetweenLoadChecks\n        $NumJobs = @(Get-Job -State 'Running').Count\n      }\n\n      foreach ($Job in @(Get-Job -State 'Completed')) {\n        $jobResult = Wait-Job -Id $Job.Id | Receive-Job\n        CheckJobResult $jobResult.result $jobResult.packagePath ([ref]$DupedSymbols) ([ref]$TotalFailures)\n        Remove-Job -Id $Job.Id\n      }\n      Write-Host\n    }\n\n  foreach ($Job in @(Get-Job)) {\n    $jobResult = Wait-Job -Id $Job.Id | Receive-Job\n    CheckJobResult $jobResult.result $jobResult.packagePath ([ref]$DupedSymbols) ([ref]$TotalFailures)\n  }\n\n  if ($TotalFailures -gt 0 -or $DupedSymbols -gt 0) {\n    if ($TotalFailures -gt 0) {\n      Write-PipelineTelemetryError -Category 'CheckSymbols' -Message \"Symbols missing for $TotalFailures/$TotalPackages packages\"\n    }\n\n    if ($DupedSymbols -gt 0) {\n      Write-PipelineTelemetryError -Category 'CheckSymbols' -Message \"$DupedSymbols/$TotalPackages packages had duplicated symbol files and could not be extracted\"\n    }\n    \n    ExitWithExitCode 1\n  }\n  else {\n    Write-Host \"All symbols validated!\"\n  }\n}\n\nfunction InstallDotnetSymbol {\n  $dotnetSymbolPackageName = 'dotnet-symbol'\n\n  $dotnetRoot = InitializeDotNetCli -install:$true\n  $dotnet = \"$dotnetRoot\\dotnet.exe\"\n  $toolList = & \"$dotnet\" tool list --global\n\n  if (($toolList -like \"*$dotnetSymbolPackageName*\") -and ($toolList -like \"*$dotnetSymbolVersion*\")) {\n    Write-Host \"dotnet-symbol version $dotnetSymbolVersion is already installed.\"\n  }\n  else {\n    Write-Host \"Installing dotnet-symbol version $dotnetSymbolVersion...\"\n    Write-Host 'You may need to restart your command window if this is the first dotnet tool you have installed.'\n    & \"$dotnet\" tool install $dotnetSymbolPackageName --version $dotnetSymbolVersion --verbosity \"minimal\" --global\n  }\n}\n\ntry {\n  InstallDotnetSymbol\n\n  foreach ($Job in @(Get-Job)) {\n    Remove-Job -Id $Job.Id\n  }\n\n  CheckSymbolsAvailable\n}\ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Category 'CheckSymbols' -Message $_\n  ExitWithExitCode 1\n}\n"
  },
  {
    "path": "eng/common/retain-build.ps1",
    "content": "\nParam(\n[Parameter(Mandatory=$true)][int] $buildId,\n[Parameter(Mandatory=$true)][string] $azdoOrgUri, \n[Parameter(Mandatory=$true)][string] $azdoProject,\n[Parameter(Mandatory=$true)][string] $token\n)\n\n$ErrorActionPreference = 'Stop'\nSet-StrictMode -Version 2.0\n\nfunction Get-AzDOHeaders(\n    [string] $token)\n{\n    $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(\":${token}\"))\n    $headers = @{\"Authorization\"=\"Basic $base64AuthInfo\"}\n    return $headers\n}\n\nfunction Update-BuildRetention(\n    [string] $azdoOrgUri,\n    [string] $azdoProject,\n    [int] $buildId,\n    [string] $token)\n{\n    $headers = Get-AzDOHeaders -token $token\n    $requestBody = \"{\n        `\"keepForever`\": `\"true`\"\n    }\"\n\n    $requestUri = \"${azdoOrgUri}/${azdoProject}/_apis/build/builds/${buildId}?api-version=6.0\"\n    write-Host \"Attempting to retain build using the following URI: ${requestUri} ...\"\n\n    try {\n        Invoke-RestMethod -Uri $requestUri -Method Patch -Body $requestBody -Header $headers -contentType \"application/json\"\n        Write-Host \"Updated retention settings for build ${buildId}.\"\n    }\n    catch {\n        Write-Error \"Failed to update retention settings for build: $_.Exception.Response.StatusDescription\"\n        exit 1\n    }\n}\n\nUpdate-BuildRetention -azdoOrgUri $azdoOrgUri -azdoProject $azdoProject -buildId $buildId -token $token\nexit 0\n"
  },
  {
    "path": "eng/common/sdk-task.ps1",
    "content": "[CmdletBinding(PositionalBinding=$false)]\nParam(\n  [string] $configuration = 'Debug',\n  [string] $task,\n  [string] $verbosity = 'minimal',\n  [string] $msbuildEngine = $null,\n  [switch] $restore,\n  [switch] $prepareMachine,\n  [switch][Alias('nobl')]$excludeCIBinaryLog,\n  [switch]$noWarnAsError,\n  [switch] $help,\n  [string] $runtimeSourceFeed = '',\n  [string] $runtimeSourceFeedKey = '',\n  [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties\n)\n\n$ci = $true\n$binaryLog = if ($excludeCIBinaryLog) { $false } else { $true }\n$warnAsError = if ($noWarnAsError) { $false } else { $true }\n\n. $PSScriptRoot\\tools.ps1\n\nfunction Print-Usage() {\n  Write-Host \"Common settings:\"\n  Write-Host \"  -task <value>           Name of Arcade task (name of a project in SdkTasks directory of the Arcade SDK package)\"\n  Write-Host \"  -restore                Restore dependencies\"\n  Write-Host \"  -verbosity <value>      Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]\"\n  Write-Host \"  -help                   Print help and exit\"\n  Write-Host \"\"\n\n  Write-Host \"Advanced settings:\"\n  Write-Host \"  -prepareMachine         Prepare machine for CI run\"\n  Write-Host \"  -msbuildEngine <value>  Msbuild engine to use to run build ('dotnet', 'vs', or unspecified).\"\n  Write-Host \"  -excludeCIBinaryLog     When running on CI, allow no binary log (short: -nobl)\"\n  Write-Host \"\"\n  Write-Host \"Command line arguments not listed above are passed thru to msbuild.\"\n}\n\nfunction Build([string]$target) {\n  $logSuffix = if ($target -eq 'Execute') { '' } else { \".$target\" }\n  $log = Join-Path $LogDir \"$task$logSuffix.binlog\"\n  $binaryLogArg = if ($binaryLog) { \"/bl:$log\" } else { \"\" }\n  $outputPath = Join-Path $ToolsetDir \"$task\\\"\n\n  MSBuild $taskProject `\n    $binaryLogArg `\n    /t:$target `\n    /p:Configuration=$configuration `\n    /p:RepoRoot=$RepoRoot `\n    /p:BaseIntermediateOutputPath=$outputPath `\n    /v:$verbosity `\n    @properties\n}\n\ntry {\n  if ($help -or (($null -ne $properties) -and ($properties.Contains('/help') -or $properties.Contains('/?')))) {\n    Print-Usage\n    exit 0\n  }\n\n  if ($task -eq \"\") {\n    Write-PipelineTelemetryError -Category 'Build' -Message \"Missing required parameter '-task <value>'\"\n    Print-Usage\n    ExitWithExitCode 1\n  }\n\n  if( $msbuildEngine -eq \"vs\") {\n    # Ensure desktop MSBuild is available for sdk tasks.\n    if( -not ($GlobalJson.tools.PSObject.Properties.Name -contains \"vs\" )) {\n      $GlobalJson.tools | Add-Member -Name \"vs\" -Value (ConvertFrom-Json \"{ `\"version`\": `\"16.5`\" }\") -MemberType NoteProperty\n    }\n    if( -not ($GlobalJson.tools.PSObject.Properties.Name -match \"xcopy-msbuild\" )) {\n      $GlobalJson.tools | Add-Member -Name \"xcopy-msbuild\" -Value \"18.0.0\" -MemberType NoteProperty\n    }\n    if ($GlobalJson.tools.\"xcopy-msbuild\".Trim() -ine \"none\") {\n        $xcopyMSBuildToolsFolder = InitializeXCopyMSBuild $GlobalJson.tools.\"xcopy-msbuild\" -install $true\n    }\n    if ($xcopyMSBuildToolsFolder -eq $null) {\n      throw 'Unable to get xcopy downloadable version of msbuild'\n    }\n\n    $global:_MSBuildExe = \"$($xcopyMSBuildToolsFolder)\\MSBuild\\Current\\Bin\\MSBuild.exe\"\n  }\n\n  $taskProject = GetSdkTaskProject $task\n  if (!(Test-Path $taskProject)) {\n    Write-PipelineTelemetryError -Category 'Build' -Message \"Unknown task: $task\"\n    ExitWithExitCode 1\n  }\n\n  if ($restore) {\n    Build 'Restore'\n  }\n\n  Build 'Execute'\n}\ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Category 'Build' -Message $_\n  ExitWithExitCode 1\n}\n\nExitWithExitCode 0\n"
  },
  {
    "path": "eng/common/sdk-task.sh",
    "content": "#!/usr/bin/env bash\n\nshow_usage() {\n    echo \"Common settings:\"\n    echo \"  --task <value>           Name of Arcade task (name of a project in SdkTasks directory of the Arcade SDK package)\"\n    echo \"  --restore                Restore dependencies\"\n    echo \"  --verbosity <value>      Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]\"\n    echo \"  --help                   Print help and exit\"\n    echo \"\"\n\n    echo \"Advanced settings:\"\n    echo \"  --excludeCIBinarylog     Don't output binary log (short: -nobl)\"\n    echo \"  --noWarnAsError          Do not warn as error\"\n    echo \"\"\n    echo \"Command line arguments not listed above are passed thru to msbuild.\"\n}\n\nsource=\"${BASH_SOURCE[0]}\"\n\n# resolve $source until the file is no longer a symlink\nwhile [[ -h \"$source\" ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\nBuild() {\n    local target=$1\n    local log_suffix=\"\"\n    [[ \"$target\" != \"Execute\" ]] && log_suffix=\".$target\"\n    local log=\"$log_dir/$task$log_suffix.binlog\"\n    local binaryLogArg=\"\"\n    [[ $binary_log == true ]] && binaryLogArg=\"/bl:$log\"\n    local output_path=\"$toolset_dir/$task/\"\n\n    MSBuild \"$taskProject\" \\\n        $binaryLogArg \\\n        /t:\"$target\" \\\n        /p:Configuration=\"$configuration\" \\\n        /p:RepoRoot=\"$repo_root\" \\\n        /p:BaseIntermediateOutputPath=\"$output_path\" \\\n        /v:\"$verbosity\" \\\n        $properties\n}\n\nbinary_log=true\nconfiguration=\"Debug\"\nverbosity=\"minimal\"\nexclude_ci_binary_log=false\nrestore=false\nhelp=false\nproperties=''\nwarnAsError=true\n\nwhile (($# > 0)); do\n  lowerI=\"$(echo $1 | tr \"[:upper:]\" \"[:lower:]\")\"\n  case $lowerI in\n    --task)\n      task=$2\n      shift 2\n      ;;\n    --restore)\n      restore=true\n      shift 1\n      ;;\n    --verbosity)\n      verbosity=$2\n      shift 2\n      ;;\n    --excludecibinarylog|--nobl)\n      binary_log=false\n      exclude_ci_binary_log=true\n      shift 1\n      ;;\n    --noWarnAsError)\n      warnAsError=false\n      shift 1\n      ;;\n    --help)\n      help=true\n      shift 1\n      ;;\n    *)\n      properties=\"$properties $1\"\n      shift 1\n      ;;\n  esac\ndone\n\nci=true\n\nif $help; then\n  show_usage\n  exit 0\nfi\n\n. \"$scriptroot/tools.sh\"\nInitializeToolset\n\nif [[ -z \"$task\" ]]; then\n    Write-PipelineTelemetryError -Category 'Task' -Name 'MissingTask' -Message \"Missing required parameter '-task <value>'\"\n    ExitWithExitCode 1\nfi\n\ntaskProject=$(GetSdkTaskProject \"$task\")\nif [[ ! -e \"$taskProject\" ]]; then\n    Write-PipelineTelemetryError -Category 'Task' -Name 'UnknownTask' -Message \"Unknown task: $task\"\n    ExitWithExitCode 1\nfi\n\nif $restore; then\n    Build \"Restore\"\nfi\n\nBuild \"Execute\"\n\n\nExitWithExitCode 0\n"
  },
  {
    "path": "eng/common/sdl/NuGet.config",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n  <solution>\n    <add key=\"disableSourceControlIntegration\" value=\"true\" />\n  </solution>\n  <packageSources>\n    <clear />\n    <add key=\"guardian\" value=\"https://securitytools.pkgs.visualstudio.com/_packaging/Guardian/nuget/v3/index.json\" />\n  </packageSources>\n  <packageSourceMapping>\n    <packageSource key=\"guardian\">\n      <package pattern=\"microsoft.guardian.cli\" />\n    </packageSource>\n  </packageSourceMapping>\n  <disabledPackageSources>\n    <clear />\n  </disabledPackageSources>\n</configuration>\n"
  },
  {
    "path": "eng/common/sdl/configure-sdl-tool.ps1",
    "content": "Param(\n  [string] $GuardianCliLocation,\n  [string] $WorkingDirectory,\n  [string] $TargetDirectory,\n  [string] $GdnFolder,\n  # The list of Guardian tools to configure. For each object in the array:\n  # - If the item is a [hashtable], it must contain these entries:\n  #   - Name = The tool name as Guardian knows it.\n  #   - Scenario = (Optional) Scenario-specific name for this configuration entry. It must be unique\n  #     among all tool entries with the same Name.\n  #   - Args = (Optional) Array of Guardian tool configuration args, like '@(\"Target > C:\\temp\")'\n  # - If the item is a [string] $v, it is treated as '@{ Name=\"$v\" }'\n  [object[]] $ToolsList,\n  [string] $GuardianLoggerLevel='Standard',\n  # Optional: Additional params to add to any tool using CredScan.\n  [string[]] $CrScanAdditionalRunConfigParams,\n  # Optional: Additional params to add to any tool using PoliCheck.\n  [string[]] $PoliCheckAdditionalRunConfigParams,\n  # Optional: Additional params to add to any tool using CodeQL/Semmle.\n  [string[]] $CodeQLAdditionalRunConfigParams,\n  # Optional: Additional params to add to any tool using Binskim.\n  [string[]] $BinskimAdditionalRunConfigParams\n)\n\n$ErrorActionPreference = 'Stop'\nSet-StrictMode -Version 2.0\n$disableConfigureToolsetImport = $true\n$global:LASTEXITCODE = 0\n\ntry {\n  # `tools.ps1` checks $ci to perform some actions. Since the SDL\n  # scripts don't necessarily execute in the same agent that run the\n  # build.ps1/sh script this variable isn't automatically set.\n  $ci = $true\n  . $PSScriptRoot\\..\\tools.ps1\n\n  # Normalize tools list: all in [hashtable] form with defined values for each key.\n  $ToolsList = $ToolsList |\n    ForEach-Object {\n      if ($_ -is [string]) {\n        $_ = @{ Name = $_ }\n      }\n\n      if (-not ($_['Scenario'])) { $_.Scenario = \"\" }\n      if (-not ($_['Args'])) { $_.Args = @() }\n      $_\n    }\n  \n  Write-Host \"List of tools to configure:\"\n  $ToolsList | ForEach-Object { $_ | Out-String | Write-Host }\n\n  # We store config files in the r directory of .gdn\n  $gdnConfigPath = Join-Path $GdnFolder 'r'\n  $ValidPath = Test-Path $GuardianCliLocation\n\n  if ($ValidPath -eq $False)\n  {\n    Write-PipelineTelemetryError -Force -Category 'Sdl' -Message \"Invalid Guardian CLI Location.\"\n    ExitWithExitCode 1\n  }\n\n  foreach ($tool in $ToolsList) {\n    # Put together the name and scenario to make a unique key.\n    $toolConfigName = $tool.Name\n    if ($tool.Scenario) {\n      $toolConfigName += \"_\" + $tool.Scenario\n    }\n\n    Write-Host \"=== Configuring $toolConfigName...\"\n\n    $gdnConfigFile = Join-Path $gdnConfigPath \"$toolConfigName-configure.gdnconfig\"\n\n    # For some tools, add default and automatic args.\n    switch -Exact ($tool.Name) {\n      'credscan' {\n        if ($targetDirectory) {\n          $tool.Args += \"`\"TargetDirectory < $TargetDirectory`\"\"\n        }\n        $tool.Args += \"`\"OutputType < pre`\"\"\n        $tool.Args += $CrScanAdditionalRunConfigParams\n      }\n      'policheck' {\n        if ($targetDirectory) {\n          $tool.Args += \"`\"Target < $TargetDirectory`\"\"\n        }\n        $tool.Args += $PoliCheckAdditionalRunConfigParams\n      }\n      {$_ -in 'semmle', 'codeql'} {\n        if ($targetDirectory) {\n          $tool.Args += \"`\"SourceCodeDirectory < $TargetDirectory`\"\"\n        }\n        $tool.Args += $CodeQLAdditionalRunConfigParams\n      }\n      'binskim' {\n        if ($targetDirectory) {\n          # Binskim crashes due to specific PDBs. GitHub issue: https://github.com/microsoft/binskim/issues/924.\n          # We are excluding all `_.pdb` files from the scan.\n          $tool.Args += \"`\"Target < $TargetDirectory\\**;-:file|$TargetDirectory\\**\\_.pdb`\"\"\n        }\n        $tool.Args += $BinskimAdditionalRunConfigParams\n      }\n    }\n\n    # Create variable pointing to the args array directly so we can use splat syntax later.\n    $toolArgs = $tool.Args\n\n    # Configure the tool. If args array is provided or the current tool has some default arguments\n    # defined, add \"--args\" and splat each element on the end. Arg format is \"{Arg id} < {Value}\",\n    # one per parameter. Doc page for \"guardian configure\":\n    # https://dev.azure.com/securitytools/SecurityIntegration/_wiki/wikis/Guardian/1395/configure\n    Exec-BlockVerbosely {\n      & $GuardianCliLocation configure `\n        --working-directory $WorkingDirectory `\n        --tool $tool.Name `\n        --output-path $gdnConfigFile `\n        --logger-level $GuardianLoggerLevel `\n        --noninteractive `\n        --force `\n        $(if ($toolArgs) { \"--args\" }) @toolArgs\n      Exit-IfNZEC \"Sdl\"\n    }\n\n    Write-Host \"Created '$toolConfigName' configuration file: $gdnConfigFile\"\n  }\n}\ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_\n  ExitWithExitCode 1\n}\n"
  },
  {
    "path": "eng/common/sdl/execute-all-sdl-tools.ps1",
    "content": "Param(\n  [string] $GuardianPackageName,                                                                 # Required: the name of guardian CLI package (not needed if GuardianCliLocation is specified)\n  [string] $NugetPackageDirectory,                                                               # Required: directory where NuGet packages are installed (not needed if GuardianCliLocation is specified)\n  [string] $GuardianCliLocation,                                                                 # Optional: Direct location of Guardian CLI executable if GuardianPackageName & NugetPackageDirectory are not specified\n  [string] $Repository=$env:BUILD_REPOSITORY_NAME,                                               # Required: the name of the repository (e.g. dotnet/arcade)\n  [string] $BranchName=$env:BUILD_SOURCEBRANCH,                                                  # Optional: name of branch or version of gdn settings; defaults to master\n  [string] $SourceDirectory=$env:BUILD_SOURCESDIRECTORY,                                         # Required: the directory where source files are located\n  [string] $ArtifactsDirectory = (Join-Path $env:BUILD_ARTIFACTSTAGINGDIRECTORY ('artifacts')),  # Required: the directory where build artifacts are located\n  [string] $AzureDevOpsAccessToken,                                                              # Required: access token for dnceng; should be provided via KeyVault\n\n  # Optional: list of SDL tools to run on source code. See 'configure-sdl-tool.ps1' for tools list\n  # format.\n  [object[]] $SourceToolsList,\n  # Optional: list of SDL tools to run on built artifacts. See 'configure-sdl-tool.ps1' for tools\n  # list format.\n  [object[]] $ArtifactToolsList,\n  # Optional: list of SDL tools to run without automatically specifying a target directory. See\n  # 'configure-sdl-tool.ps1' for tools list format.\n  [object[]] $CustomToolsList,\n\n  [bool] $TsaPublish=$False,                                                                     # Optional: true will publish results to TSA; only set to true after onboarding to TSA; TSA is the automated framework used to upload test results as bugs.\n  [string] $TsaBranchName=$env:BUILD_SOURCEBRANCH,                                               # Optional: required for TSA publish; defaults to $(Build.SourceBranchName); TSA is the automated framework used to upload test results as bugs.\n  [string] $TsaRepositoryName=$env:BUILD_REPOSITORY_NAME,                                        # Optional: TSA repository name; will be generated automatically if not submitted; TSA is the automated framework used to upload test results as bugs.\n  [string] $BuildNumber=$env:BUILD_BUILDNUMBER,                                                  # Optional: required for TSA publish; defaults to $(Build.BuildNumber)\n  [bool] $UpdateBaseline=$False,                                                                 # Optional: if true, will update the baseline in the repository; should only be run after fixing any issues which need to be fixed\n  [bool] $TsaOnboard=$False,                                                                     # Optional: if true, will onboard the repository to TSA; should only be run once; TSA is the automated framework used to upload test results as bugs.\n  [string] $TsaInstanceUrl,                                                                      # Optional: only needed if TsaOnboard or TsaPublish is true; the instance-url registered with TSA; TSA is the automated framework used to upload test results as bugs.\n  [string] $TsaCodebaseName,                                                                     # Optional: only needed if TsaOnboard or TsaPublish is true; the name of the codebase registered with TSA; TSA is the automated framework used to upload test results as bugs.\n  [string] $TsaProjectName,                                                                      # Optional: only needed if TsaOnboard or TsaPublish is true; the name of the project registered with TSA; TSA is the automated framework used to upload test results as bugs.\n  [string] $TsaNotificationEmail,                                                                # Optional: only needed if TsaOnboard is true; the email(s) which will receive notifications of TSA bug filings (e.g. alias@microsoft.com); TSA is the automated framework used to upload test results as bugs.\n  [string] $TsaCodebaseAdmin,                                                                    # Optional: only needed if TsaOnboard is true; the aliases which are admins of the TSA codebase (e.g. DOMAIN\\alias); TSA is the automated framework used to upload test results as bugs.\n  [string] $TsaBugAreaPath,                                                                      # Optional: only needed if TsaOnboard is true; the area path where TSA will file bugs in AzDO; TSA is the automated framework used to upload test results as bugs.\n  [string] $TsaIterationPath,                                                                    # Optional: only needed if TsaOnboard is true; the iteration path where TSA will file bugs in AzDO; TSA is the automated framework used to upload test results as bugs.\n  [string] $GuardianLoggerLevel='Standard',                                                      # Optional: the logger level for the Guardian CLI; options are Trace, Verbose, Standard, Warning, and Error\n  [string[]] $CrScanAdditionalRunConfigParams,                                                   # Optional: Additional Params to custom build a CredScan run config in the format @(\"xyz:abc\",\"sdf:1\")\n  [string[]] $PoliCheckAdditionalRunConfigParams,                                                # Optional: Additional Params to custom build a Policheck run config in the format @(\"xyz:abc\",\"sdf:1\")\n  [string[]] $CodeQLAdditionalRunConfigParams,                                                   # Optional: Additional Params to custom build a Semmle/CodeQL run config in the format @(\"xyz < abc\",\"sdf < 1\")\n  [string[]] $BinskimAdditionalRunConfigParams,                                                  # Optional: Additional Params to custom build a Binskim run config in the format @(\"xyz < abc\",\"sdf < 1\")\n  [bool] $BreakOnFailure=$False                                                                  # Optional: Fail the build if there were errors during the run\n)\n\ntry {\n  $ErrorActionPreference = 'Stop'\n  Set-StrictMode -Version 2.0\n  $disableConfigureToolsetImport = $true\n  $global:LASTEXITCODE = 0\n\n  # `tools.ps1` checks $ci to perform some actions. Since the SDL\n  # scripts don't necessarily execute in the same agent that run the\n  # build.ps1/sh script this variable isn't automatically set.\n  $ci = $true\n  . $PSScriptRoot\\..\\tools.ps1\n\n  #Replace repo names to the format of org/repo\n  if (!($Repository.contains('/'))) {\n    $RepoName = $Repository -replace '(.*?)-(.*)', '$1/$2';\n  }\n  else{\n    $RepoName = $Repository;\n  }\n\n  if ($GuardianPackageName) {\n    $guardianCliLocation = Join-Path $NugetPackageDirectory (Join-Path $GuardianPackageName (Join-Path 'tools' 'guardian.cmd'))\n  } else {\n    $guardianCliLocation = $GuardianCliLocation\n  }\n\n  $workingDirectory = (Split-Path $SourceDirectory -Parent)\n  $ValidPath = Test-Path $guardianCliLocation\n\n  if ($ValidPath -eq $False)\n  {\n    Write-PipelineTelemetryError -Force -Category 'Sdl' -Message 'Invalid Guardian CLI Location.'\n    ExitWithExitCode 1\n  }\n\n  Exec-BlockVerbosely {\n    & $(Join-Path $PSScriptRoot 'init-sdl.ps1') -GuardianCliLocation $guardianCliLocation -Repository $RepoName -BranchName $BranchName -WorkingDirectory $workingDirectory -AzureDevOpsAccessToken $AzureDevOpsAccessToken -GuardianLoggerLevel $GuardianLoggerLevel\n  }\n  $gdnFolder = Join-Path $workingDirectory '.gdn'\n\n  if ($TsaOnboard) {\n    if ($TsaCodebaseName -and $TsaNotificationEmail -and $TsaCodebaseAdmin -and $TsaBugAreaPath) {\n      Exec-BlockVerbosely {\n        & $guardianCliLocation tsa-onboard --codebase-name \"$TsaCodebaseName\" --notification-alias \"$TsaNotificationEmail\" --codebase-admin \"$TsaCodebaseAdmin\" --instance-url \"$TsaInstanceUrl\" --project-name \"$TsaProjectName\" --area-path \"$TsaBugAreaPath\" --iteration-path \"$TsaIterationPath\" --working-directory $workingDirectory --logger-level $GuardianLoggerLevel\n      }\n      if ($LASTEXITCODE -ne 0) {\n        Write-PipelineTelemetryError -Force -Category 'Sdl' -Message \"Guardian tsa-onboard failed with exit code $LASTEXITCODE.\"\n        ExitWithExitCode $LASTEXITCODE\n      }\n    } else {\n      Write-PipelineTelemetryError -Force -Category 'Sdl' -Message 'Could not onboard to TSA -- not all required values ($TsaCodebaseName, $TsaNotificationEmail, $TsaCodebaseAdmin, $TsaBugAreaPath) were specified.'\n      ExitWithExitCode 1\n    }\n  }\n\n  # Configure a list of tools with a default target directory. Populates the \".gdn/r\" directory.\n  function Configure-ToolsList([object[]] $tools, [string] $targetDirectory) {\n    if ($tools -and $tools.Count -gt 0) {\n      Exec-BlockVerbosely {\n        & $(Join-Path $PSScriptRoot 'configure-sdl-tool.ps1') `\n          -GuardianCliLocation $guardianCliLocation `\n          -WorkingDirectory $workingDirectory `\n          -TargetDirectory $targetDirectory `\n          -GdnFolder $gdnFolder `\n          -ToolsList $tools `\n          -AzureDevOpsAccessToken $AzureDevOpsAccessToken `\n          -GuardianLoggerLevel $GuardianLoggerLevel `\n          -CrScanAdditionalRunConfigParams $CrScanAdditionalRunConfigParams `\n          -PoliCheckAdditionalRunConfigParams $PoliCheckAdditionalRunConfigParams `\n          -CodeQLAdditionalRunConfigParams $CodeQLAdditionalRunConfigParams `\n          -BinskimAdditionalRunConfigParams $BinskimAdditionalRunConfigParams\n        if ($BreakOnFailure) {\n          Exit-IfNZEC \"Sdl\"\n        }\n      }\n    }\n  }\n\n  # Configure Artifact and Source tools with default Target directories.\n  Configure-ToolsList $ArtifactToolsList $ArtifactsDirectory\n  Configure-ToolsList $SourceToolsList $SourceDirectory\n  # Configure custom tools with no default Target directory.\n  Configure-ToolsList $CustomToolsList $null\n\n  # At this point, all tools are configured in the \".gdn\" directory. Run them all in a single call.\n  # (If we used \"run\" multiple times, each run would overwrite data from earlier runs.)\n  Exec-BlockVerbosely {\n    & $(Join-Path $PSScriptRoot 'run-sdl.ps1') `\n      -GuardianCliLocation $guardianCliLocation `\n      -WorkingDirectory $SourceDirectory `\n      -UpdateBaseline $UpdateBaseline `\n      -GdnFolder $gdnFolder\n  }\n\n  if ($TsaPublish) {\n    if ($TsaBranchName -and $BuildNumber) {\n      if (-not $TsaRepositoryName) {\n        $TsaRepositoryName = \"$($Repository)-$($BranchName)\"\n      }\n      Exec-BlockVerbosely {\n        & $guardianCliLocation tsa-publish --all-tools --repository-name \"$TsaRepositoryName\" --branch-name \"$TsaBranchName\" --build-number \"$BuildNumber\" --onboard $True --codebase-name \"$TsaCodebaseName\" --notification-alias \"$TsaNotificationEmail\" --codebase-admin \"$TsaCodebaseAdmin\" --instance-url \"$TsaInstanceUrl\" --project-name \"$TsaProjectName\" --area-path \"$TsaBugAreaPath\" --iteration-path \"$TsaIterationPath\" --working-directory $workingDirectory  --logger-level $GuardianLoggerLevel\n      }\n      if ($LASTEXITCODE -ne 0) {\n        Write-PipelineTelemetryError -Force -Category 'Sdl' -Message \"Guardian tsa-publish failed with exit code $LASTEXITCODE.\"\n        ExitWithExitCode $LASTEXITCODE\n      }\n    } else {\n      Write-PipelineTelemetryError -Force -Category 'Sdl' -Message 'Could not publish to TSA -- not all required values ($TsaBranchName, $BuildNumber) were specified.'\n      ExitWithExitCode 1\n    }\n  }\n\n  if ($BreakOnFailure) {\n    Write-Host \"Failing the build in case of breaking results...\"\n    Exec-BlockVerbosely {\n      & $guardianCliLocation break --working-directory $workingDirectory --logger-level $GuardianLoggerLevel\n    }\n  } else {\n    Write-Host \"Letting the build pass even if there were breaking results...\"\n  }\n}\ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_\n  exit 1\n}\n"
  },
  {
    "path": "eng/common/sdl/extract-artifact-archives.ps1",
    "content": "# This script looks for each archive file in a directory and extracts it into the target directory.\n# For example, the file \"$InputPath/bin.tar.gz\" extracts to \"$ExtractPath/bin.tar.gz.extracted/**\".\n# Uses the \"tar\" utility added to Windows 10 / Windows 2019 that supports tar.gz and zip.\nparam(\n  # Full path to directory where archives are stored.\n  [Parameter(Mandatory=$true)][string] $InputPath,\n  # Full path to directory to extract archives into. May be the same as $InputPath.\n  [Parameter(Mandatory=$true)][string] $ExtractPath\n)\n\n$ErrorActionPreference = 'Stop'\nSet-StrictMode -Version 2.0\n\n$disableConfigureToolsetImport = $true\n\ntry {\n  # `tools.ps1` checks $ci to perform some actions. Since the SDL\n  # scripts don't necessarily execute in the same agent that run the\n  # build.ps1/sh script this variable isn't automatically set.\n  $ci = $true\n  . $PSScriptRoot\\..\\tools.ps1\n\n  Measure-Command {\n    $jobs = @()\n\n    # Find archive files for non-Windows and Windows builds.\n    $archiveFiles = @(\n      Get-ChildItem (Join-Path $InputPath \"*.tar.gz\")\n      Get-ChildItem (Join-Path $InputPath \"*.zip\")\n    )\n\n    foreach ($targzFile in $archiveFiles) {\n      $jobs += Start-Job -ScriptBlock {\n        $file = $using:targzFile\n        $fileName = [System.IO.Path]::GetFileName($file)\n        $extractDir = Join-Path $using:ExtractPath \"$fileName.extracted\"\n\n        New-Item $extractDir -ItemType Directory -Force | Out-Null\n\n        Write-Host \"Extracting '$file' to '$extractDir'...\"\n\n        # Pipe errors to stdout to prevent PowerShell detecting them and quitting the job early.\n        # This type of quit skips the catch, so we wouldn't be able to tell which file triggered the\n        # error. Save output so it can be stored in the exception string along with context.\n        $output = tar -xf $file -C $extractDir 2>&1\n        # Handle NZEC manually rather than using Exit-IfNZEC: we are in a background job, so we\n        # don't have access to the outer scope.\n        if ($LASTEXITCODE -ne 0) {\n          throw \"Error extracting '$file': non-zero exit code ($LASTEXITCODE). Output: '$output'\"\n        }\n\n        Write-Host \"Extracted to $extractDir\"\n      }\n    }\n\n    Receive-Job $jobs -Wait\n  }\n}\ncatch {\n  Write-Host $_\n  Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_\n  ExitWithExitCode 1\n}\n"
  },
  {
    "path": "eng/common/sdl/extract-artifact-packages.ps1",
    "content": "param(\n  [Parameter(Mandatory=$true)][string] $InputPath,              # Full path to directory where artifact packages are stored\n  [Parameter(Mandatory=$true)][string] $ExtractPath            # Full path to directory where the packages will be extracted\n)\n\n$ErrorActionPreference = 'Stop'\nSet-StrictMode -Version 2.0\n\n$disableConfigureToolsetImport = $true\n\nfunction ExtractArtifacts {\n  if (!(Test-Path $InputPath)) {\n    Write-Host \"Input Path does not exist: $InputPath\"\n    ExitWithExitCode 0\n  }\n  $Jobs = @()\n  Get-ChildItem \"$InputPath\\*.nupkg\" |\n    ForEach-Object {\n      $Jobs += Start-Job -ScriptBlock $ExtractPackage -ArgumentList $_.FullName\n    }\n\n  foreach ($Job in $Jobs) {\n    Wait-Job -Id $Job.Id | Receive-Job\n  }\n}\n\ntry {\n  # `tools.ps1` checks $ci to perform some actions. Since the SDL\n  # scripts don't necessarily execute in the same agent that run the\n  # build.ps1/sh script this variable isn't automatically set.\n  $ci = $true\n  . $PSScriptRoot\\..\\tools.ps1\n\n  $ExtractPackage = {\n    param( \n      [string] $PackagePath                                 # Full path to a NuGet package\n    )\n\n    if (!(Test-Path $PackagePath)) {\n      Write-PipelineTelemetryError -Category 'Build' -Message \"Input file does not exist: $PackagePath\"\n      ExitWithExitCode 1\n    }\n\n    $RelevantExtensions = @('.dll', '.exe', '.pdb')\n    Write-Host -NoNewLine 'Extracting ' ([System.IO.Path]::GetFileName($PackagePath)) '...'\n\n    $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath)\n    $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageId\n\n    Add-Type -AssemblyName System.IO.Compression.FileSystem\n\n    [System.IO.Directory]::CreateDirectory($ExtractPath);\n\n    try {\n      $zip = [System.IO.Compression.ZipFile]::OpenRead($PackagePath)\n  \n      $zip.Entries | \n      Where-Object {$RelevantExtensions -contains [System.IO.Path]::GetExtension($_.Name)} |\n        ForEach-Object {\n            $TargetPath = Join-Path -Path $ExtractPath -ChildPath (Split-Path -Path $_.FullName)\n            [System.IO.Directory]::CreateDirectory($TargetPath);\n\n            $TargetFile = Join-Path -Path $ExtractPath -ChildPath $_.FullName\n            [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $TargetFile)\n          }\n    }\n    catch {\n      Write-Host $_\n      Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_\n      ExitWithExitCode 1\n    }\n    finally {\n      $zip.Dispose() \n    }\n  }\n  Measure-Command { ExtractArtifacts }\n}\ncatch {\n  Write-Host $_\n  Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_\n  ExitWithExitCode 1\n}\n"
  },
  {
    "path": "eng/common/sdl/init-sdl.ps1",
    "content": "Param(\n  [string] $GuardianCliLocation,\n  [string] $Repository,\n  [string] $BranchName='master',\n  [string] $WorkingDirectory,\n  [string] $AzureDevOpsAccessToken,\n  [string] $GuardianLoggerLevel='Standard'\n)\n\n$ErrorActionPreference = 'Stop'\nSet-StrictMode -Version 2.0\n$disableConfigureToolsetImport = $true\n$global:LASTEXITCODE = 0\n\n# `tools.ps1` checks $ci to perform some actions. Since the SDL\n# scripts don't necessarily execute in the same agent that run the\n# build.ps1/sh script this variable isn't automatically set.\n$ci = $true\n. $PSScriptRoot\\..\\tools.ps1\n\n# Don't display the console progress UI - it's a huge perf hit\n$ProgressPreference = 'SilentlyContinue'\n\n# Construct basic auth from AzDO access token; construct URI to the repository's gdn folder stored in that repository; construct location of zip file\n$encodedPat = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(\":$AzureDevOpsAccessToken\"))\n$escapedRepository = [Uri]::EscapeDataString(\"/$Repository/$BranchName/.gdn\")\n$uri = \"https://dev.azure.com/dnceng/internal/_apis/git/repositories/sdl-tool-cfg/Items?path=$escapedRepository&versionDescriptor[versionOptions]=0&`$format=zip&api-version=5.0\"\n$zipFile = \"$WorkingDirectory/gdn.zip\"\n\nAdd-Type -AssemblyName System.IO.Compression.FileSystem\n$gdnFolder = (Join-Path $WorkingDirectory '.gdn')\n\ntry {\n  # if the folder does not exist, we'll do a guardian init and push it to the remote repository\n  Write-Host 'Initializing Guardian...'\n  Write-Host \"$GuardianCliLocation init --working-directory $WorkingDirectory --logger-level $GuardianLoggerLevel\"\n  & $GuardianCliLocation init --working-directory $WorkingDirectory --logger-level $GuardianLoggerLevel\n  if ($LASTEXITCODE -ne 0) {\n    Write-PipelineTelemetryError -Force -Category 'Build' -Message \"Guardian init failed with exit code $LASTEXITCODE.\"\n    ExitWithExitCode $LASTEXITCODE\n  }\n  # We create the mainbaseline so it can be edited later\n  Write-Host \"$GuardianCliLocation baseline --working-directory $WorkingDirectory --name mainbaseline\"\n  & $GuardianCliLocation baseline --working-directory $WorkingDirectory --name mainbaseline\n  if ($LASTEXITCODE -ne 0) {\n    Write-PipelineTelemetryError -Force -Category 'Build' -Message \"Guardian baseline failed with exit code $LASTEXITCODE.\"\n    ExitWithExitCode $LASTEXITCODE\n  }\n  ExitWithExitCode 0\n}\ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_\n  ExitWithExitCode 1\n}\n"
  },
  {
    "path": "eng/common/sdl/packages.config",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<packages>\n  <package id=\"Microsoft.Guardian.Cli\" version=\"0.199.0\"/>\n</packages>\n"
  },
  {
    "path": "eng/common/sdl/run-sdl.ps1",
    "content": "Param(\n  [string] $GuardianCliLocation,\n  [string] $WorkingDirectory,\n  [string] $GdnFolder,\n  [string] $UpdateBaseline,\n  [string] $GuardianLoggerLevel='Standard'\n)\n\n$ErrorActionPreference = 'Stop'\nSet-StrictMode -Version 2.0\n$disableConfigureToolsetImport = $true\n$global:LASTEXITCODE = 0\n\ntry {\n  # `tools.ps1` checks $ci to perform some actions. Since the SDL\n  # scripts don't necessarily execute in the same agent that run the\n  # build.ps1/sh script this variable isn't automatically set.\n  $ci = $true\n  . $PSScriptRoot\\..\\tools.ps1\n\n  # We store config files in the r directory of .gdn\n  $gdnConfigPath = Join-Path $GdnFolder 'r'\n  $ValidPath = Test-Path $GuardianCliLocation\n\n  if ($ValidPath -eq $False)\n  {\n    Write-PipelineTelemetryError -Force -Category 'Sdl' -Message \"Invalid Guardian CLI Location.\"\n    ExitWithExitCode 1\n  }\n\n  $gdnConfigFiles = Get-ChildItem $gdnConfigPath -Recurse -Include '*.gdnconfig'\n  Write-Host \"Discovered Guardian config files:\"\n  $gdnConfigFiles | Out-String | Write-Host\n\n  Exec-BlockVerbosely {\n    & $GuardianCliLocation run `\n      --working-directory $WorkingDirectory `\n      --baseline mainbaseline `\n      --update-baseline $UpdateBaseline `\n      --logger-level $GuardianLoggerLevel `\n      --config @gdnConfigFiles\n    Exit-IfNZEC \"Sdl\"\n  }\n}\ncatch {\n  Write-Host $_.ScriptStackTrace\n  Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_\n  ExitWithExitCode 1\n}\n"
  },
  {
    "path": "eng/common/sdl/sdl.ps1",
    "content": "\nfunction Install-Gdn {\n    param(\n        [Parameter(Mandatory=$true)]\n        [string]$Path,\n\n        # If omitted, install the latest version of Guardian, otherwise install that specific version.\n        [string]$Version\n    )\n\n    $ErrorActionPreference = 'Stop'\n    Set-StrictMode -Version 2.0\n    $disableConfigureToolsetImport = $true\n    $global:LASTEXITCODE = 0\n\n    # `tools.ps1` checks $ci to perform some actions. Since the SDL\n    # scripts don't necessarily execute in the same agent that run the\n    # build.ps1/sh script this variable isn't automatically set.\n    $ci = $true\n    . $PSScriptRoot\\..\\tools.ps1\n\n    $argumentList = @(\"install\", \"Microsoft.Guardian.Cli\", \"-Source https://securitytools.pkgs.visualstudio.com/_packaging/Guardian/nuget/v3/index.json\", \"-OutputDirectory $Path\", \"-NonInteractive\", \"-NoCache\")\n\n    if ($Version) {\n        $argumentList += \"-Version $Version\"\n    }\n    \n    Start-Process nuget -Verbose -ArgumentList $argumentList -NoNewWindow -Wait\n\n    $gdnCliPath = Get-ChildItem -Filter guardian.cmd -Recurse -Path $Path\n\n    if (!$gdnCliPath)\n    {\n        Write-PipelineTelemetryError -Category 'Sdl' -Message 'Failure installing Guardian'\n    }\n\n    return $gdnCliPath.FullName\n}"
  },
  {
    "path": "eng/common/sdl/trim-assets-version.ps1",
    "content": "<#\n.SYNOPSIS\nInstall and run the 'Microsoft.DotNet.VersionTools.Cli' tool with the 'trim-artifacts-version' command to trim the version from the NuGet assets file name.\n\n.PARAMETER InputPath\nFull path to directory where artifact packages are stored\n\n.PARAMETER Recursive\nSearch for NuGet packages recursively\n\n#>\n\nParam(\n  [string] $InputPath,\n  [bool] $Recursive = $true\n)\n\n$CliToolName = \"Microsoft.DotNet.VersionTools.Cli\"\n\nfunction Install-VersionTools-Cli {\n  param(\n      [Parameter(Mandatory=$true)][string]$Version\n  )\n\n  Write-Host \"Installing the package '$CliToolName' with a version of '$version' ...\"\n  $feed = \"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json\"\n\n  $argumentList = @(\"tool\", \"install\", \"--local\", \"$CliToolName\", \"--add-source $feed\", \"--no-cache\", \"--version $Version\", \"--create-manifest-if-needed\")\n  Start-Process \"$dotnet\" -Verbose -ArgumentList $argumentList -NoNewWindow -Wait\n}\n\n# -------------------------------------------------------------------\n\nif (!(Test-Path $InputPath)) {\n  Write-Host \"Input Path '$InputPath' does not exist\"\n  ExitWithExitCode 1\n}\n\n$ErrorActionPreference = 'Stop'\nSet-StrictMode -Version 2.0\n\n$disableConfigureToolsetImport = $true\n$global:LASTEXITCODE = 0\n\n# `tools.ps1` checks $ci to perform some actions. Since the SDL\n# scripts don't necessarily execute in the same agent that run the\n# build.ps1/sh script this variable isn't automatically set.\n$ci = $true\n. $PSScriptRoot\\..\\tools.ps1\n\ntry {\n  $dotnetRoot = InitializeDotNetCli -install:$true\n  $dotnet = \"$dotnetRoot\\dotnet.exe\"\n\n  $toolsetVersion = Read-ArcadeSdkVersion\n  Install-VersionTools-Cli -Version $toolsetVersion\n\n  $cliToolFound = (& \"$dotnet\" tool list --local | Where-Object {$_.Split(' ')[0] -eq $CliToolName})\n  if ($null -eq $cliToolFound) {\n    Write-PipelineTelemetryError -Force -Category 'Sdl' -Message \"The '$CliToolName' tool is not installed.\"\n    ExitWithExitCode 1\n  }\n\n  Exec-BlockVerbosely {\n    & \"$dotnet\" $CliToolName trim-assets-version `\n      --assets-path $InputPath `\n      --recursive $Recursive\n    Exit-IfNZEC \"Sdl\"\n  }\n}\ncatch {\n  Write-Host $_\n  Write-PipelineTelemetryError -Force -Category 'Sdl' -Message $_\n  ExitWithExitCode 1\n}\n"
  },
  {
    "path": "eng/common/template-guidance.md",
    "content": "# Overview\n\nArcade provides templates for public (`/templates`) and 1ES pipeline templates (`/templates-official`) scenarios.  Pipelines which are required to be managed by 1ES pipeline templates should reference `/templates-offical`, all other pipelines may reference `/templates`.\n\n## How to use\n\nBasic guidance is:\n\n- 1ES Pipeline Template or 1ES Microbuild template runs should reference `eng/common/templates-official`. Any internal production-graded pipeline should use these templates.\n\n- All other runs should reference `eng/common/templates`.\n\nSee [azure-pipelines.yml](../../azure-pipelines.yml) (templates-official example) or [azure-pipelines-pr.yml](../../azure-pipelines-pr.yml) (templates example) for examples.\n\n#### The `templateIs1ESManaged` parameter\n\nThe `templateIs1ESManaged` is available on most templates and affects which of the variants is used for nested templates. See [Development Notes](#development-notes) below for more information on the `templateIs1ESManaged1 parameter.\n\n- For templates under `job/`, `jobs/`, `steps`, or `post-build/`, this parameter must be explicitly set.\n\n## Multiple outputs\n\n1ES pipeline templates impose a policy where every publish artifact execution results in additional security scans being injected into your pipeline.  When using `templates-official/jobs/jobs.yml`, Arcade reduces the number of additional security injections by gathering all publishing outputs into the [Build.ArtifactStagingDirectory](https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services), and utilizing the [outputParentDirectory](https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-docs/1es-pipeline-templates/features/outputs#multiple-outputs) feature of 1ES pipeline templates.  When implementing your pipeline, if you ensure publish artifacts are located in the `$(Build.ArtifactStagingDirectory)`, and utilize the 1ES provided template context, then you can reduce the number of security scans for your pipeline.\n\nExample:\n``` yaml\n# azure-pipelines.yml\nextends:\n  template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate\n  parameters:\n    stages:\n    - stage: build\n      jobs:\n      - template: /eng/common/templates-official/jobs/jobs.yml@self\n        parameters:\n          # 1ES makes use of outputs to reduce security task injection overhead\n          templateContext:\n            outputs:\n            - output: pipelineArtifact\n              displayName: 'Publish logs from source'\n              continueOnError: true\n              condition: always()\n              targetPath: $(Build.ArtifactStagingDirectory)/artifacts/log\n              artifactName: Logs\n          jobs:\n          - job: Windows\n            steps:\n            - script: echo \"friendly neighborhood\" > artifacts/marvel/spiderman.txt\n          # copy build outputs to artifact staging directory for publishing\n          - task: CopyFiles@2\n              displayName: Gather build output\n              inputs:\n                SourceFolder: '$(System.DefaultWorkingDirectory)/artifacts/marvel'\n                Contents: '**'\n                TargetFolder: '$(Build.ArtifactStagingDirectory)/artifacts/marvel'\n```\n\nNote: Multiple outputs are ONLY applicable to 1ES PT publishing (only usable when referencing `templates-official`).\n\n## Development notes\n\n**Folder / file structure**\n\n``` text\neng\\common\\\n    [templates || templates-official]\\\n        job\\\n            job.yml                          (shim + artifact publishing logic)\n            onelocbuild.yml                  (shim)\n            publish-build-assets.yml         (shim)\n            source-build.yml                 (shim)\n            source-index-stage1.yml          (shim)\n        jobs\\\n            codeql-build.yml                 (shim)\n            jobs.yml                         (shim)\n            source-build.yml                 (shim)\n        post-build\\\n            post-build.yml                   (shim)\n            common-variabls.yml              (shim)\n            setup-maestro-vars.yml           (shim)\n        steps\\\n            publish-build-artifacts.yml      (logic)\n            publish-pipeline-artifacts.yml   (logic)\n            component-governance.yml         (shim)\n            generate-sbom.yml                (shim)\n            publish-logs.yml                 (shim)\n            retain-build.yml                 (shim)\n            send-to-helix.yml                (shim)\n            source-build.yml                 (shim)\n        variables\\\n            pool-providers.yml               (logic + redirect) # templates/variables/pool-providers.yml will redirect to templates-official/variables/pool-providers.yml if you are running in the internal project\n            sdl-variables.yml                (logic)\n    core-templates\\\n        job\\\n            job.yml                          (logic)\n            onelocbuild.yml                  (logic)\n            publish-build-assets.yml         (logic)\n            source-build.yml                 (logic)\n            source-index-stage1.yml          (logic)\n        jobs\\\n            codeql-build.yml                 (logic)\n            jobs.yml                         (logic)\n            source-build.yml                 (logic)\n        post-build\\\n            common-variabls.yml              (logic)\n            post-build.yml                   (logic)\n            setup-maestro-vars.yml           (logic)\n        steps\\\n            component-governance.yml         (logic)\n            generate-sbom.yml                (logic)\n            publish-build-artifacts.yml      (redirect)\n            publish-logs.yml                 (logic)\n            publish-pipeline-artifacts.yml   (redirect)\n            retain-build.yml                 (logic)\n            send-to-helix.yml                (logic)\n            source-build.yml                 (logic)\n        variables\\\n            pool-providers.yml               (redirect)\n```\n\nIn the table above, a file is designated as \"shim\", \"logic\", or \"redirect\".\n\n- shim - represents a yaml file which is an intermediate step between pipeline logic and .Net Core Engineering's templates (`core-templates`) and defines the `is1ESPipeline` parameter value.\n\n- logic - represents actual base template logic.\n\n- redirect- represents a file in `core-templates` which redirects to the \"logic\" file in either `templates` or `templates-official`.\n\nLogic for Arcade's templates live **primarily** in the `core-templates` folder.  The exceptions to the location of the logic files are around artifact publishing, which is handled differently between 1es pipeline templates and standard templates.  `templates` and `templates-official` provide shim entry points which redirect to `core-templates` while also defining the `is1ESPipeline` parameter.  If a shim is referenced in `templates`, then `is1ESPipeline` is set to `false`.  If a shim is referenced in `templates-official`, then `is1ESPipeline` is set to `true`.\n\nWithin `templates` and `templates-official`, the templates at the \"stages\", and \"jobs\" / \"job\" level have been replaced with shims.  Templates at the \"steps\" and \"variables\" level are typically too granular to be replaced with shims and instead persist logic which is directly applicable to either scenario.\n\nWithin `core-templates`, there are a handful of places where logic is dependent on which shim entry point was used.  In those places, we redirect back to the respective logic file in `templates` or `templates-official`.\n"
  },
  {
    "path": "eng/common/templates/job/job.yml",
    "content": "parameters: \n  enablePublishBuildArtifacts: false\n  disableComponentGovernance: ''\n  componentGovernanceIgnoreDirectories: ''\n# Sbom related params\n  enableSbom: true\n  runAsPublic: false\n  PackageVersion: 9.0.0\n  BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts'\n\njobs:\n- template: /eng/common/core-templates/job/job.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ if and(ne(parameter.key, 'steps'), ne(parameter.key, 'is1ESPipeline')) }}:\n        ${{ parameter.key }}: ${{ parameter.value }}\n\n    steps:\n    - ${{ each step in parameters.steps }}:\n      - ${{ step }}\n\n    componentGovernanceSteps:\n    - template: /eng/common/templates/steps/component-governance.yml\n      parameters:\n        ${{ if eq(parameters.disableComponentGovernance, '') }}:\n          ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(parameters.runAsPublic, 'false'), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/dotnet/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/microsoft/'), eq(variables['Build.SourceBranch'], 'refs/heads/main'))) }}:\n            disableComponentGovernance: false\n          ${{ else }}:\n            disableComponentGovernance: true\n        ${{ else }}:\n          disableComponentGovernance: ${{ parameters.disableComponentGovernance }}\n        componentGovernanceIgnoreDirectories: ${{ parameters.componentGovernanceIgnoreDirectories }}\n\n    artifactPublishSteps:\n    - ${{ if ne(parameters.artifacts.publish, '') }}:\n      - ${{ if and(ne(parameters.artifacts.publish.artifacts, 'false'), ne(parameters.artifacts.publish.artifacts, '')) }}:\n        - template: /eng/common/core-templates/steps/publish-build-artifacts.yml\n          parameters:\n            is1ESPipeline: false\n            args:\n              displayName: Publish pipeline artifacts\n              pathToPublish: '$(Build.ArtifactStagingDirectory)/artifacts'\n              publishLocation: Container\n              artifactName: ${{ coalesce(parameters.artifacts.publish.artifacts.name , 'Artifacts_$(Agent.Os)_$(_BuildConfig)') }}\n              continueOnError: true\n              condition: always()\n              retryCountOnTaskFailure: 10 # for any logs being locked\n      - ${{ if and(ne(parameters.artifacts.publish.logs, 'false'), ne(parameters.artifacts.publish.logs, '')) }}:\n        - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml\n          parameters:\n            is1ESPipeline: false\n            args:\n              targetPath: '$(Build.ArtifactStagingDirectory)/artifacts/log'\n              artifactName: ${{ coalesce(parameters.artifacts.publish.logs.name, 'Logs_Build_$(Agent.Os)_$(_BuildConfig)') }}\n              displayName: 'Publish logs'\n              continueOnError: true\n              condition: always()\n              retryCountOnTaskFailure: 10 # for any logs being locked\n              sbomEnabled: false  # we don't need SBOM for logs\n\n    - ${{ if ne(parameters.enablePublishBuildArtifacts, 'false') }}:\n      - template: /eng/common/core-templates/steps/publish-build-artifacts.yml\n        parameters:\n          is1ESPipeline: false\n          args:\n            displayName: Publish Logs\n            pathToPublish: '$(Build.ArtifactStagingDirectory)/artifacts/log/$(_BuildConfig)'\n            publishLocation: Container\n            artifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)_Attempt$(System.JobAttempt)' ) }}\n            continueOnError: true\n            condition: always()\n\n    - ${{ if eq(parameters.enableBuildRetry, 'true') }}:\n      - template: /eng/common/core-templates/steps/publish-pipeline-artifacts.yml\n        parameters:\n          is1ESPipeline: false\n          args:\n            targetPath: '$(System.DefaultWorkingDirectory)\\eng\\common\\BuildConfiguration'\n            artifactName: 'BuildConfiguration'\n            displayName: 'Publish build retry configuration'\n            continueOnError: true\n            sbomEnabled: false  # we don't need SBOM for BuildConfiguration\n"
  },
  {
    "path": "eng/common/templates/job/onelocbuild.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/job/onelocbuild.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/job/publish-build-assets.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/job/publish-build-assets.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/job/source-build.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/job/source-build.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/job/source-index-stage1.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/job/source-index-stage1.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/jobs/codeql-build.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/jobs/codeql-build.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/jobs/jobs.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/jobs/jobs.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/jobs/source-build.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/jobs/source-build.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}"
  },
  {
    "path": "eng/common/templates/post-build/common-variables.yml",
    "content": "variables:\n- template: /eng/common/core-templates/post-build/common-variables.yml\n  parameters:\n    # Specifies whether to use 1ES\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}"
  },
  {
    "path": "eng/common/templates/post-build/post-build.yml",
    "content": "stages:\n- template: /eng/common/core-templates/post-build/post-build.yml\n  parameters:\n    # Specifies whether to use 1ES\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}"
  },
  {
    "path": "eng/common/templates/post-build/setup-maestro-vars.yml",
    "content": "steps:\n- template: /eng/common/core-templates/post-build/setup-maestro-vars.yml\n  parameters:\n    # Specifies whether to use 1ES\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}"
  },
  {
    "path": "eng/common/templates/steps/component-governance.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/component-governance.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/steps/enable-internal-runtimes.yml",
    "content": "# Obtains internal runtime download credentials and populates the 'dotnetbuilds-internal-container-read-token-base64'\n# variable with the base64-encoded SAS token, by default\n\nsteps:\n- template: /eng/common/core-templates/steps/enable-internal-runtimes.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/steps/enable-internal-sources.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/enable-internal-sources.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}"
  },
  {
    "path": "eng/common/templates/steps/generate-sbom.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/generate-sbom.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/steps/get-delegation-sas.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/get-delegation-sas.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/steps/get-federated-access-token.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/get-federated-access-token.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}"
  },
  {
    "path": "eng/common/templates/steps/publish-build-artifacts.yml",
    "content": "parameters:\n- name: is1ESPipeline\n  type: boolean\n  default: false\n\n- name: displayName\n  type: string\n  default: 'Publish to Build Artifact'\n\n- name: condition\n  type: string\n  default: succeeded()\n\n- name: artifactName\n  type: string\n\n- name: pathToPublish\n  type: string\n\n- name: continueOnError\n  type: boolean\n  default: false\n\n- name: publishLocation\n  type: string\n  default: 'Container'\n\n- name: retryCountOnTaskFailure\n  type: string\n  default: 10\n\nsteps:\n- ${{ if eq(parameters.is1ESPipeline, true) }}:\n  - 'eng/common/templates cannot be referenced from a 1ES managed template': error\n- task: PublishBuildArtifacts@1\n  displayName: ${{ parameters.displayName }}\n  condition: ${{ parameters.condition }}\n  ${{ if parameters.continueOnError }}:\n    continueOnError: ${{ parameters.continueOnError }}\n  inputs:\n    PublishLocation: ${{ parameters.publishLocation }}  \n    PathtoPublish: ${{ parameters.pathToPublish }}\n    ${{ if parameters.artifactName }}:\n      ArtifactName: ${{ parameters.artifactName }}\n    ${{ if parameters.retryCountOnTaskFailure }}:\n      retryCountOnTaskFailure: ${{ parameters.retryCountOnTaskFailure }}\n"
  },
  {
    "path": "eng/common/templates/steps/publish-logs.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/publish-logs.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/steps/publish-pipeline-artifacts.yml",
    "content": "parameters:\n- name: is1ESPipeline\n  type: boolean\n  default: false\n\n- name: args\n  type: object\n  default: {}\n\nsteps:\n- ${{ if eq(parameters.is1ESPipeline, true) }}:\n  - 'eng/common/templates cannot be referenced from a 1ES managed template': error\n- task: PublishPipelineArtifact@1\n  displayName: ${{ coalesce(parameters.args.displayName, 'Publish to Build Artifact') }}\n  ${{ if parameters.args.condition }}:\n    condition: ${{ parameters.args.condition }}\n  ${{ else }}:\n    condition: succeeded()\n  ${{ if parameters.args.continueOnError }}:\n    continueOnError: ${{ parameters.args.continueOnError }}\n  inputs:\n    targetPath: ${{ parameters.args.targetPath }}\n    ${{ if parameters.args.artifactName }}:\n      artifactName: ${{ parameters.args.artifactName }}\n    ${{ if parameters.args.publishLocation }}:\n      publishLocation: ${{ parameters.args.publishLocation }}\n    ${{ if parameters.args.fileSharePath }}:\n      fileSharePath: ${{ parameters.args.fileSharePath }}\n    ${{ if parameters.args.Parallel }}:\n      parallel: ${{ parameters.args.Parallel }}\n    ${{ if parameters.args.parallelCount }}:\n      parallelCount: ${{ parameters.args.parallelCount }}\n    ${{ if parameters.args.properties }}:\n      properties: ${{ parameters.args.properties }}"
  },
  {
    "path": "eng/common/templates/steps/retain-build.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/retain-build.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/steps/send-to-helix.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/send-to-helix.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/steps/source-build.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/source-build.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/steps/source-index-stage1-publish.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/source-index-stage1-publish.yml\n  parameters:\n    is1ESPipeline: false\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates/steps/vmr-sync.yml",
    "content": "### These steps synchronize new code from product repositories into the VMR (https://github.com/dotnet/dotnet).\n### They initialize the darc CLI and pull the new updates.\n### Changes are applied locally onto the already cloned VMR (located in $vmrPath).\n\nparameters:\n- name: targetRef\n  displayName: Target revision in dotnet/<repo> to synchronize\n  type: string\n  default: $(Build.SourceVersion)\n\n- name: vmrPath\n  displayName: Path where the dotnet/dotnet is checked out to\n  type: string\n  default: $(Agent.BuildDirectory)/vmr\n\n- name: additionalSyncs\n  displayName: Optional list of package names whose repo's source will also be synchronized in the local VMR, e.g. NuGet.Protocol\n  type: object\n  default: []\n\nsteps:\n- checkout: vmr\n  displayName: Clone dotnet/dotnet\n  path: vmr\n  clean: true\n\n- checkout: self\n  displayName: Clone $(Build.Repository.Name)\n  path: repo\n  fetchDepth: 0\n\n# This step is needed so that when we get a detached HEAD / shallow clone,\n# we still pull the commit into the temporary repo clone to use it during the sync.\n# Also unshallow the clone so that forwardflow command would work.\n- script: |\n    git branch repo-head\n    git rev-parse HEAD\n  displayName: Label PR commit\n  workingDirectory: $(Agent.BuildDirectory)/repo\n\n- script: |\n    git config --global user.name \"dotnet-maestro[bot]\"\n    git config --global user.email \"dotnet-maestro[bot]@users.noreply.github.com\"\n  displayName: Set git author to dotnet-maestro[bot]\n  workingDirectory: ${{ parameters.vmrPath }}\n\n- script: |\n    ./eng/common/vmr-sync.sh                         \\\n      --vmr ${{ parameters.vmrPath }}         \\\n      --tmp $(Agent.TempDirectory)            \\\n      --azdev-pat '$(dn-bot-all-orgs-code-r)' \\\n      --ci                                    \\\n      --debug\n\n    if [ \"$?\" -ne 0 ]; then\n      echo \"##vso[task.logissue type=error]Failed to synchronize the VMR\"\n      exit 1\n    fi\n  displayName: Sync repo into VMR (Unix)\n  condition: ne(variables['Agent.OS'], 'Windows_NT')\n  workingDirectory: $(Agent.BuildDirectory)/repo\n\n- script: |\n    git config --global diff.astextplain.textconv echo\n    git config --system core.longpaths true\n  displayName: Configure Windows git (longpaths, astextplain)\n  condition: eq(variables['Agent.OS'], 'Windows_NT')\n\n- powershell: |\n    ./eng/common/vmr-sync.ps1                      `\n      -vmr ${{ parameters.vmrPath }}        `\n      -tmp $(Agent.TempDirectory)           `\n      -azdevPat '$(dn-bot-all-orgs-code-r)' `\n      -ci                                   `\n      -debugOutput\n\n    if ($LASTEXITCODE -ne 0) {\n      echo \"##vso[task.logissue type=error]Failed to synchronize the VMR\"\n      exit 1\n    }\n  displayName: Sync repo into VMR (Windows)\n  condition: eq(variables['Agent.OS'], 'Windows_NT')\n  workingDirectory: $(Agent.BuildDirectory)/repo\n\n- ${{ if eq(variables['Build.Reason'], 'PullRequest') }}:\n  - task: CopyFiles@2\n    displayName: Collect failed patches\n    condition: failed()\n    inputs:\n      SourceFolder: '$(Agent.TempDirectory)'\n      Contents: '*.patch'\n      TargetFolder: '$(Build.ArtifactStagingDirectory)/FailedPatches'\n\n  - publish: '$(Build.ArtifactStagingDirectory)/FailedPatches'\n    artifact: $(System.JobDisplayName)_FailedPatches\n    displayName: Upload failed patches\n    condition: failed()\n\n- ${{ each assetName in parameters.additionalSyncs }}:\n  # The vmr-sync script ends up staging files in the local VMR so we have to commit those\n  - script:\n      git commit --allow-empty -am \"Forward-flow $(Build.Repository.Name)\"\n    displayName: Commit local VMR changes\n    workingDirectory: ${{ parameters.vmrPath }}\n\n  - script: |\n      set -ex\n\n      echo \"Searching for details of asset ${{ assetName }}...\"\n\n      # Use darc to get dependencies information\n      dependencies=$(./.dotnet/dotnet darc get-dependencies --name '${{ assetName }}' --ci)\n\n      # Extract repository URL and commit hash\n      repository=$(echo \"$dependencies\" | grep 'Repo:' | sed 's/Repo:[[:space:]]*//' | head -1)\n\n      if [ -z \"$repository\" ]; then\n        echo \"##vso[task.logissue type=error]Asset ${{ assetName }} not found in the dependency list\"\n        exit 1\n      fi\n\n      commit=$(echo \"$dependencies\" | grep 'Commit:' | sed 's/Commit:[[:space:]]*//' | head -1)\n\n      echo \"Updating the VMR from $repository / $commit...\"\n      cd ..\n      git clone $repository ${{ assetName }}\n      cd ${{ assetName }}\n      git checkout $commit\n      git branch \"sync/$commit\"\n\n      ./eng/common/vmr-sync.sh                         \\\n        --vmr ${{ parameters.vmrPath }}         \\\n        --tmp $(Agent.TempDirectory)            \\\n        --azdev-pat '$(dn-bot-all-orgs-code-r)' \\\n        --ci                                    \\\n        --debug\n\n      if [ \"$?\" -ne 0 ]; then\n        echo \"##vso[task.logissue type=error]Failed to synchronize the VMR\"\n        exit 1\n      fi\n    displayName: Sync ${{ assetName }} into (Unix)\n    condition: ne(variables['Agent.OS'], 'Windows_NT')\n    workingDirectory: $(Agent.BuildDirectory)/repo\n\n  - powershell: |\n      $ErrorActionPreference = 'Stop'\n\n      Write-Host \"Searching for details of asset ${{ assetName }}...\"\n\n      $dependencies = .\\.dotnet\\dotnet darc get-dependencies --name '${{ assetName }}' --ci\n\n      $repository = $dependencies | Select-String -Pattern 'Repo:\\s+([^\\s]+)' | Select-Object -First 1\n      $repository -match 'Repo:\\s+([^\\s]+)' | Out-Null\n      $repository = $matches[1]\n\n      if ($repository -eq $null) {\n          Write-Error \"Asset ${{ assetName }} not found in the dependency list\"\n          exit 1\n      }\n\n      $commit = $dependencies | Select-String -Pattern 'Commit:\\s+([^\\s]+)' | Select-Object -First 1\n      $commit -match 'Commit:\\s+([^\\s]+)' | Out-Null\n      $commit = $matches[1]\n\n      Write-Host \"Updating the VMR from $repository / $commit...\"\n      cd ..\n      git clone $repository ${{ assetName }}\n      cd ${{ assetName }}\n      git checkout $commit\n      git branch \"sync/$commit\"\n\n      .\\eng\\common\\vmr-sync.ps1 `\n        -vmr ${{ parameters.vmrPath }}             `\n        -tmp $(Agent.TempDirectory)                `\n        -azdevPat '$(dn-bot-all-orgs-code-r)'      `\n        -ci                                        `\n        -debugOutput\n\n      if ($LASTEXITCODE -ne 0) {\n        echo \"##vso[task.logissue type=error]Failed to synchronize the VMR\"\n        exit 1\n      }\n    displayName: Sync ${{ assetName }} into (Windows)\n    condition: ne(variables['Agent.OS'], 'Windows_NT')\n    workingDirectory: $(Agent.BuildDirectory)/repo\n"
  },
  {
    "path": "eng/common/templates/variables/pool-providers.yml",
    "content": "# Select a pool provider based off branch name. Anything with branch name containing 'release' must go into an -Svc pool,\n# otherwise it should go into the \"normal\" pools. This separates out the queueing and billing of released branches.\n\n# Motivation:\n#   Once a given branch of a repository's output has been officially \"shipped\" once, it is then considered to be COGS\n#   (Cost of goods sold) and should be moved to a servicing pool provider. This allows both separation of queueing\n#   (allowing release builds and main PR builds to not intefere with each other) and billing (required for COGS.\n#   Additionally, the pool provider name itself may be subject to change when the .NET Core Engineering Services\n#   team needs to move resources around and create new and potentially differently-named pools. Using this template\n#   file from an Arcade-ified repo helps guard against both having to update one's release/* branches and renaming.\n\n# How to use:\n#  This yaml assumes your shipped product branches use the naming convention \"release/...\" (which many do).\n#  If we find alternate naming conventions in broad usage it can be added to the condition below.\n#\n#  First, import the template in an arcade-ified repo to pick up the variables, e.g.:\n#\n#  variables:\n#  - template: /eng/common/templates/variables/pool-providers.yml\n#\n#  ... then anywhere specifying the pool provider use the runtime variables,\n#      $(DncEngInternalBuildPool) and $  (DncEngPublicBuildPool), e.g.:\n#\n#        pool:\n#           name: $(DncEngInternalBuildPool)\n#           demands: ImageOverride -equals windows.vs2022.amd64\nvariables:\n  - ${{ if eq(variables['System.TeamProject'], 'internal') }}:\n    - template: /eng/common/templates-official/variables/pool-providers.yml\n  - ${{ else }}:\n    # Coalesce the target and source branches so we know when a PR targets a release branch\n    # If these variables are somehow missing, fall back to main (tends to have more capacity)\n\n    # Any new -Svc alternative pools should have variables added here to allow for splitting work\n    - name: DncEngPublicBuildPool\n      value: $[\n          replace(\n            replace(\n              eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'),\n              True,\n              'NetCore-Svc-Public'\n            ),\n            False,\n            'NetCore-Public'\n          )\n        ]\n\n    - name: DncEngInternalBuildPool\n      value: $[\n          replace(\n            replace(\n              eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'),\n              True,\n              'NetCore1ESPool-Svc-Internal'\n            ),\n            False,\n            'NetCore1ESPool-Internal'\n          )\n        ]\n"
  },
  {
    "path": "eng/common/templates/vmr-build-pr.yml",
    "content": "# This pipeline is used for running the VMR verification of the PR changes in repo-level PRs.\n#\n# It will run a full set of verification jobs defined in:\n# https://github.com/dotnet/dotnet/blob/10060d128e3f470e77265f8490f5e4f72dae738e/eng/pipelines/templates/stages/vmr-build.yml#L27-L38\n#\n# For repos that do not need to run the full set, you would do the following:\n#\n# 1. Copy this YML file to a repo-specific location, i.e. outside of eng/common.\n#\n# 2. Add `verifications` parameter to VMR template reference\n#\n#    Examples:\n#    - For source-build stage 1 verification, add the following:\n#        verifications: [ \"source-build-stage1\" ]\n#\n#    - For Windows only verifications, add the following:\n#        verifications: [ \"unified-build-windows-x64\", \"unified-build-windows-x86\" ]\n\ntrigger: none\npr: none\n\nvariables:\n- template: /eng/common/templates/variables/pool-providers.yml@self\n\n- name: skipComponentGovernanceDetection  # we run CG on internal builds only\n  value: true\n\n- name: Codeql.Enabled  # we run CodeQL on internal builds only\n  value: false\n\nresources:\n  repositories:\n  - repository: vmr\n    type: github\n    name: dotnet/dotnet\n    endpoint: dotnet\n    ref: refs/heads/main # Set to whatever VMR branch the PR build should insert into\n\nstages:\n- template: /eng/pipelines/templates/stages/vmr-build.yml@vmr\n  parameters:\n    isBuiltFromVmr: false\n    scope: lite\n"
  },
  {
    "path": "eng/common/templates-official/job/job.yml",
    "content": "parameters:\n# Sbom related params\n  enableSbom: true\n  runAsPublic: false\n  PackageVersion: 9.0.0\n  BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts'\n\njobs:\n- template: /eng/common/core-templates/job/job.yml\n  parameters:\n    is1ESPipeline: true\n\n    componentGovernanceSteps:\n    - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(parameters.enableSbom, 'true')) }}:\n      - template: /eng/common/templates/steps/generate-sbom.yml\n        parameters:\n          PackageVersion: ${{ parameters.packageVersion }}\n          BuildDropPath: ${{ parameters.buildDropPath }}\n          ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom\n          publishArtifacts: false\n\n    # publish artifacts\n    # for 1ES managed templates, use the templateContext.output to handle multiple outputs.\n    templateContext:\n      outputParentDirectory: $(Build.ArtifactStagingDirectory)\n      outputs:\n      - ${{ if ne(parameters.artifacts.publish, '') }}:\n        - ${{ if and(ne(parameters.artifacts.publish.artifacts, 'false'), ne(parameters.artifacts.publish.artifacts, '')) }}:\n          - output: buildArtifacts\n            displayName: Publish pipeline artifacts\n            PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts'\n            ArtifactName: ${{ coalesce(parameters.artifacts.publish.artifacts.name , 'Artifacts_$(Agent.Os)_$(_BuildConfig)') }}\n            condition: always()\n            retryCountOnTaskFailure: 10 # for any logs being locked\n            continueOnError: true\n        - ${{ if and(ne(parameters.artifacts.publish.logs, 'false'), ne(parameters.artifacts.publish.logs, '')) }}:\n          - output: pipelineArtifact\n            targetPath: '$(Build.ArtifactStagingDirectory)/artifacts/log'\n            artifactName: ${{ coalesce(parameters.artifacts.publish.logs.name, 'Logs_Build_$(Agent.Os)_$(_BuildConfig)_Attempt$(System.JobAttempt)') }}\n            displayName: 'Publish logs'\n            continueOnError: true\n            condition: always()\n            retryCountOnTaskFailure: 10 # for any logs being locked\n            sbomEnabled: false  # we don't need SBOM for logs\n\n      - ${{ if eq(parameters.enablePublishBuildArtifacts, true) }}:\n        - output: buildArtifacts\n          displayName: Publish Logs\n          PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts/log/$(_BuildConfig)'\n          publishLocation: Container\n          ArtifactName: ${{ coalesce(parameters.enablePublishBuildArtifacts.artifactName, '$(Agent.Os)_$(Agent.JobName)_Attempt$(System.JobAttempt)' ) }}\n          continueOnError: true\n          condition: always()\n          sbomEnabled: false  # we don't need SBOM for logs\n\n      - ${{ if eq(parameters.enableBuildRetry, 'true') }}:\n        - output: pipelineArtifact\n          targetPath: '$(Build.ArtifactStagingDirectory)/artifacts/eng/common/BuildConfiguration'\n          artifactName: 'BuildConfiguration'\n          displayName: 'Publish build retry configuration'\n          continueOnError: true\n          sbomEnabled: false  # we don't need SBOM for BuildConfiguration\n\n      - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(parameters.enableSbom, 'true')) }}:\n        - output: pipelineArtifact\n          displayName: Publish SBOM manifest\n          continueOnError: true\n          targetPath: $(Build.ArtifactStagingDirectory)/sbom\n          artifactName: $(ARTIFACT_NAME)\n\n      # add any outputs provided via root yaml\n      - ${{ if ne(parameters.templateContext.outputs, '') }}:\n        - ${{ each output in parameters.templateContext.outputs }}:\n          - ${{ output }}\n      \n      # add any remaining templateContext properties\n      ${{ each context in parameters.templateContext }}:\n        ${{ if and(ne(context.key, 'outputParentDirectory'), ne(context.key, 'outputs')) }}:\n          ${{ context.key }}: ${{ context.value }}\n\n    ${{ each parameter in parameters }}:\n      ${{ if and(ne(parameter.key, 'templateContext'), ne(parameter.key, 'is1ESPipeline')) }}:\n        ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/job/onelocbuild.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/job/onelocbuild.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/job/publish-build-assets.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/job/publish-build-assets.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/job/source-build.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/job/source-build.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/job/source-index-stage1.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/job/source-index-stage1.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/jobs/codeql-build.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/jobs/codeql-build.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/jobs/jobs.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/jobs/jobs.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/jobs/source-build.yml",
    "content": "jobs:\n- template: /eng/common/core-templates/jobs/source-build.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}"
  },
  {
    "path": "eng/common/templates-official/post-build/common-variables.yml",
    "content": "variables:\n- template: /eng/common/core-templates/post-build/common-variables.yml\n  parameters:\n    # Specifies whether to use 1ES\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}"
  },
  {
    "path": "eng/common/templates-official/post-build/post-build.yml",
    "content": "stages:\n- template: /eng/common/core-templates/post-build/post-build.yml\n  parameters:\n    # Specifies whether to use 1ES\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/post-build/setup-maestro-vars.yml",
    "content": "steps:\n- template: /eng/common/core-templates/post-build/setup-maestro-vars.yml\n  parameters:\n    # Specifies whether to use 1ES\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}"
  },
  {
    "path": "eng/common/templates-official/steps/component-governance.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/component-governance.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/steps/enable-internal-runtimes.yml",
    "content": "# Obtains internal runtime download credentials and populates the 'dotnetbuilds-internal-container-read-token-base64'\n# variable with the base64-encoded SAS token, by default\nsteps:\n- template: /eng/common/core-templates/steps/enable-internal-runtimes.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/steps/enable-internal-sources.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/enable-internal-sources.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}"
  },
  {
    "path": "eng/common/templates-official/steps/generate-sbom.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/generate-sbom.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/steps/get-delegation-sas.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/get-delegation-sas.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/steps/get-federated-access-token.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/get-federated-access-token.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}"
  },
  {
    "path": "eng/common/templates-official/steps/publish-build-artifacts.yml",
    "content": "parameters:\n- name: displayName\n  type: string\n  default: 'Publish to Build Artifact'\n\n- name: condition\n  type: string\n  default: succeeded()\n\n- name: artifactName\n  type: string\n\n- name: pathToPublish\n  type: string\n\n- name: continueOnError\n  type: boolean\n  default: false\n\n- name: publishLocation\n  type: string\n  default: 'Container'\n\n- name: is1ESPipeline\n  type: boolean\n  default: true\n\n- name: retryCountOnTaskFailure\n  type: string\n  default: 10\n  \nsteps:\n- ${{ if ne(parameters.is1ESPipeline, true) }}:\n  - 'eng/common/templates-official cannot be referenced from a non-1ES managed template': error\n- task: 1ES.PublishBuildArtifacts@1\n  displayName: ${{ parameters.displayName }}\n  condition: ${{ parameters.condition }}\n  ${{ if parameters.continueOnError }}:\n    continueOnError: ${{ parameters.continueOnError }}\n  inputs:\n    PublishLocation: ${{ parameters.publishLocation }}\n    PathtoPublish: ${{ parameters.pathToPublish }}\n    ${{ if parameters.artifactName }}:\n      ArtifactName: ${{ parameters.artifactName }}\n    ${{ if parameters.retryCountOnTaskFailure }}:\n      retryCountOnTaskFailure: ${{ parameters.retryCountOnTaskFailure }}\n"
  },
  {
    "path": "eng/common/templates-official/steps/publish-logs.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/publish-logs.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/steps/publish-pipeline-artifacts.yml",
    "content": "parameters:\n- name: is1ESPipeline\n  type: boolean\n  default: true\n\n- name: args\n  type: object\n  default: {}\n\nsteps:\n- ${{ if ne(parameters.is1ESPipeline, true) }}:\n  - 'eng/common/templates-official cannot be referenced from a non-1ES managed template': error\n- task: 1ES.PublishPipelineArtifact@1\n  displayName: ${{ coalesce(parameters.args.displayName, 'Publish to Build Artifact') }}\n  ${{ if parameters.args.condition }}:\n    condition: ${{ parameters.args.condition }}\n  ${{ else }}:\n    condition: succeeded()\n  ${{ if parameters.args.continueOnError }}:\n    continueOnError: ${{ parameters.args.continueOnError }}\n  inputs:\n    targetPath: ${{ parameters.args.targetPath }}\n    ${{ if parameters.args.artifactName }}:\n      artifactName: ${{ parameters.args.artifactName }}\n    ${{ if parameters.args.properties }}:\n      properties: ${{ parameters.args.properties }}\n    ${{ if parameters.args.sbomEnabled }}:\n      sbomEnabled: ${{ parameters.args.sbomEnabled }}\n"
  },
  {
    "path": "eng/common/templates-official/steps/retain-build.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/retain-build.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/steps/send-to-helix.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/send-to-helix.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/steps/source-build.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/source-build.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/steps/source-index-stage1-publish.yml",
    "content": "steps:\n- template: /eng/common/core-templates/steps/source-index-stage1-publish.yml\n  parameters:\n    is1ESPipeline: true\n\n    ${{ each parameter in parameters }}:\n      ${{ parameter.key }}: ${{ parameter.value }}\n"
  },
  {
    "path": "eng/common/templates-official/variables/pool-providers.yml",
    "content": "# Select a pool provider based off branch name. Anything with branch name containing 'release' must go into an -Svc pool, \n# otherwise it should go into the \"normal\" pools. This separates out the queueing and billing of released branches.\n\n# Motivation: \n#   Once a given branch of a repository's output has been officially \"shipped\" once, it is then considered to be COGS\n#   (Cost of goods sold) and should be moved to a servicing pool provider. This allows both separation of queueing\n#   (allowing release builds and main PR builds to not intefere with each other) and billing (required for COGS.\n#   Additionally, the pool provider name itself may be subject to change when the .NET Core Engineering Services \n#   team needs to move resources around and create new and potentially differently-named pools. Using this template \n#   file from an Arcade-ified repo helps guard against both having to update one's release/* branches and renaming.\n\n# How to use: \n#  This yaml assumes your shipped product branches use the naming convention \"release/...\" (which many do).\n#  If we find alternate naming conventions in broad usage it can be added to the condition below.\n#\n#  First, import the template in an arcade-ified repo to pick up the variables, e.g.:\n#\n#  variables:\n#  - template: /eng/common/templates-official/variables/pool-providers.yml\n#\n#  ... then anywhere specifying the pool provider use the runtime variables,\n#      $(DncEngInternalBuildPool)\n#\n#        pool:\n#           name: $(DncEngInternalBuildPool)\n#           image: 1es-windows-2022\n\nvariables:\n  # Coalesce the target and source branches so we know when a PR targets a release branch\n  # If these variables are somehow missing, fall back to main (tends to have more capacity)\n\n  # Any new -Svc alternative pools should have variables added here to allow for splitting work\n\n  - name: DncEngInternalBuildPool\n    value: $[\n        replace(\n          replace(\n            eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'),\n            True,\n            'NetCore1ESPool-Svc-Internal'\n          ),\n          False,\n          'NetCore1ESPool-Internal'\n        )\n      ]"
  },
  {
    "path": "eng/common/templates-official/variables/sdl-variables.yml",
    "content": "variables:\n# The Guardian version specified in 'eng/common/sdl/packages.config'. This value must be kept in\n# sync with the packages.config file.\n- name: DefaultGuardianVersion\n  value: 0.109.0\n- name: GuardianPackagesConfigFile\n  value: $(System.DefaultWorkingDirectory)\\eng\\common\\sdl\\packages.config"
  },
  {
    "path": "eng/common/tools.ps1",
    "content": "# Initialize variables if they aren't already defined.\n# These may be defined as parameters of the importing script, or set after importing this script.\n\n# CI mode - set to true on CI server for PR validation build or official build.\n[bool]$ci = if (Test-Path variable:ci) { $ci } else { $false }\n\n# Build configuration. Common values include 'Debug' and 'Release', but the repository may use other names.\n[string]$configuration = if (Test-Path variable:configuration) { $configuration } else { 'Debug' }\n\n# Set to true to opt out of outputting binary log while running in CI\n[bool]$excludeCIBinarylog = if (Test-Path variable:excludeCIBinarylog) { $excludeCIBinarylog } else { $false }\n\n# Set to true to output binary log from msbuild. Note that emitting binary log slows down the build.\n[bool]$binaryLog = if (Test-Path variable:binaryLog) { $binaryLog } else { $ci -and !$excludeCIBinarylog }\n\n# Set to true to use the pipelines logger which will enable Azure logging output.\n# https://github.com/Microsoft/azure-pipelines-tasks/blob/master/docs/authoring/commands.md\n# This flag is meant as a temporary opt-opt for the feature while validate it across\n# our consumers. It will be deleted in the future.\n[bool]$pipelinesLog = if (Test-Path variable:pipelinesLog) { $pipelinesLog } else { $ci }\n\n# Turns on machine preparation/clean up code that changes the machine state (e.g. kills build processes).\n[bool]$prepareMachine = if (Test-Path variable:prepareMachine) { $prepareMachine } else { $false }\n\n# True to restore toolsets and dependencies.\n[bool]$restore = if (Test-Path variable:restore) { $restore } else { $true }\n\n# Adjusts msbuild verbosity level.\n[string]$verbosity = if (Test-Path variable:verbosity) { $verbosity } else { 'minimal' }\n\n# Set to true to reuse msbuild nodes. Recommended to not reuse on CI.\n[bool]$nodeReuse = if (Test-Path variable:nodeReuse) { $nodeReuse } else { !$ci }\n\n# Configures warning treatment in msbuild.\n[bool]$warnAsError = if (Test-Path variable:warnAsError) { $warnAsError } else { $true }\n\n# Specifies which msbuild engine to use for build: 'vs', 'dotnet' or unspecified (determined based on presence of tools.vs in global.json).\n[string]$msbuildEngine = if (Test-Path variable:msbuildEngine) { $msbuildEngine } else { $null }\n\n# True to attempt using .NET Core already that meets requirements specified in global.json\n# installed on the machine instead of downloading one.\n[bool]$useInstalledDotNetCli = if (Test-Path variable:useInstalledDotNetCli) { $useInstalledDotNetCli } else { $true }\n\n# Enable repos to use a particular version of the on-line dotnet-install scripts.\n#    default URL: https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.ps1\n[string]$dotnetInstallScriptVersion = if (Test-Path variable:dotnetInstallScriptVersion) { $dotnetInstallScriptVersion } else { 'v1' }\n\n# True to use global NuGet cache instead of restoring packages to repository-local directory.\n[bool]$useGlobalNuGetCache = if (Test-Path variable:useGlobalNuGetCache) { $useGlobalNuGetCache } else { !$ci }\n\n# True to exclude prerelease versions Visual Studio during build\n[bool]$excludePrereleaseVS = if (Test-Path variable:excludePrereleaseVS) { $excludePrereleaseVS } else { $false }\n\n# An array of names of processes to stop on script exit if prepareMachine is true.\n$processesToStopOnExit = if (Test-Path variable:processesToStopOnExit) { $processesToStopOnExit } else { @('msbuild', 'dotnet', 'vbcscompiler') }\n\n$disableConfigureToolsetImport = if (Test-Path variable:disableConfigureToolsetImport) { $disableConfigureToolsetImport } else { $null }\n\nset-strictmode -version 2.0\n$ErrorActionPreference = 'Stop'\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\n# If specifies, provides an alternate path for getting .NET Core SDKs and Runtimes. This script will still try public sources first.\n[string]$runtimeSourceFeed = if (Test-Path variable:runtimeSourceFeed) { $runtimeSourceFeed } else { $null }\n# Base-64 encoded SAS token that has permission to storage container described by $runtimeSourceFeed\n[string]$runtimeSourceFeedKey = if (Test-Path variable:runtimeSourceFeedKey) { $runtimeSourceFeedKey } else { $null }\n\n# True when the build is running within the VMR.\n[bool]$fromVMR = if (Test-Path variable:fromVMR) { $fromVMR } else { $false }\n\nfunction Create-Directory ([string[]] $path) {\n    New-Item -Path $path -Force -ItemType 'Directory' | Out-Null\n}\n\nfunction Unzip([string]$zipfile, [string]$outpath) {\n  Add-Type -AssemblyName System.IO.Compression.FileSystem\n  [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath)\n}\n\n# This will exec a process using the console and return it's exit code.\n# This will not throw when the process fails.\n# Returns process exit code.\nfunction Exec-Process([string]$command, [string]$commandArgs) {\n  $startInfo = New-Object System.Diagnostics.ProcessStartInfo\n  $startInfo.FileName = $command\n  $startInfo.Arguments = $commandArgs\n  $startInfo.UseShellExecute = $false\n  $startInfo.WorkingDirectory = Get-Location\n\n  $process = New-Object System.Diagnostics.Process\n  $process.StartInfo = $startInfo\n  $process.Start() | Out-Null\n\n  $finished = $false\n  try {\n    while (-not $process.WaitForExit(100)) {\n      # Non-blocking loop done to allow ctr-c interrupts\n    }\n\n    $finished = $true\n    return $global:LASTEXITCODE = $process.ExitCode\n  }\n  finally {\n    # If we didn't finish then an error occurred or the user hit ctrl-c.  Either\n    # way kill the process\n    if (-not $finished) {\n      $process.Kill()\n    }\n  }\n}\n\n# Take the given block, print it, print what the block probably references from the current set of\n# variables using low-effort string matching, then run the block.\n#\n# This is intended to replace the pattern of manually copy-pasting a command, wrapping it in quotes,\n# and printing it using \"Write-Host\". The copy-paste method is more readable in build logs, but less\n# maintainable and less reliable. It is easy to make a mistake and modify the command without\n# properly updating the \"Write-Host\" line, resulting in misleading build logs. The probability of\n# this mistake makes the pattern hard to trust when it shows up in build logs. Finding the bug in\n# existing source code can also be difficult, because the strings are not aligned to each other and\n# the line may be 300+ columns long.\n#\n# By removing the need to maintain two copies of the command, Exec-BlockVerbosely avoids the issues.\n#\n# In Bash (or any posix-like shell), \"set -x\" prints usable verbose output automatically.\n# \"Set-PSDebug\" appears to be similar at first glance, but unfortunately, it isn't very useful: it\n# doesn't print any info about the variables being used by the command, which is normally the\n# interesting part to diagnose.\nfunction Exec-BlockVerbosely([scriptblock] $block) {\n  Write-Host \"--- Running script block:\"\n  $blockString = $block.ToString().Trim()\n  Write-Host $blockString\n\n  Write-Host \"--- List of variables that might be used:\"\n  # For each variable x in the environment, check the block for a reference to x via simple \"$x\" or\n  # \"@x\" syntax. This doesn't detect other ways to reference variables (\"${x}\" nor \"$variable:x\",\n  # among others). It only catches what this function was originally written for: simple\n  # command-line commands.\n  $variableTable = Get-Variable |\n    Where-Object {\n      $blockString.Contains(\"`$$($_.Name)\") -or $blockString.Contains(\"@$($_.Name)\")\n    } |\n    Format-Table -AutoSize -HideTableHeaders -Wrap |\n    Out-String\n  Write-Host $variableTable.Trim()\n\n  Write-Host \"--- Executing:\"\n  & $block\n  Write-Host \"--- Done running script block!\"\n}\n\n# createSdkLocationFile parameter enables a file being generated under the toolset directory\n# which writes the sdk's location into. This is only necessary for cmd --> powershell invocations\n# as dot sourcing isn't possible.\nfunction InitializeDotNetCli([bool]$install, [bool]$createSdkLocationFile) {\n  if (Test-Path variable:global:_DotNetInstallDir) {\n    return $global:_DotNetInstallDir\n  }\n\n  # Disable first run since we do not need all ASP.NET packages restored.\n  $env:DOTNET_NOLOGO=1\n\n  # Disable telemetry on CI.\n  if ($ci) {\n    $env:DOTNET_CLI_TELEMETRY_OPTOUT=1\n  }\n\n  # Find the first path on %PATH% that contains the dotnet.exe\n  if ($useInstalledDotNetCli -and (-not $globalJsonHasRuntimes) -and ($env:DOTNET_INSTALL_DIR -eq $null)) {\n    $dotnetExecutable = GetExecutableFileName 'dotnet'\n    $dotnetCmd = Get-Command $dotnetExecutable -ErrorAction SilentlyContinue\n\n    if ($dotnetCmd -ne $null) {\n      $env:DOTNET_INSTALL_DIR = Split-Path $dotnetCmd.Path -Parent\n    }\n  }\n\n  $dotnetSdkVersion = $GlobalJson.tools.dotnet\n\n  # Use dotnet installation specified in DOTNET_INSTALL_DIR if it contains the required SDK version,\n  # otherwise install the dotnet CLI and SDK to repo local .dotnet directory to avoid potential permission issues.\n  if ((-not $globalJsonHasRuntimes) -and (-not [string]::IsNullOrEmpty($env:DOTNET_INSTALL_DIR)) -and (Test-Path(Join-Path $env:DOTNET_INSTALL_DIR \"sdk\\$dotnetSdkVersion\"))) {\n    $dotnetRoot = $env:DOTNET_INSTALL_DIR\n  } else {\n    $dotnetRoot = Join-Path $RepoRoot '.dotnet'\n\n    if (-not (Test-Path(Join-Path $dotnetRoot \"sdk\\$dotnetSdkVersion\"))) {\n      if ($install) {\n        InstallDotNetSdk $dotnetRoot $dotnetSdkVersion\n      } else {\n        Write-PipelineTelemetryError -Category 'InitializeToolset' -Message \"Unable to find dotnet with SDK version '$dotnetSdkVersion'\"\n        ExitWithExitCode 1\n      }\n    }\n\n    $env:DOTNET_INSTALL_DIR = $dotnetRoot\n  }\n\n  # Creates a temporary file under the toolset dir.\n  # The following code block is protecting against concurrent access so that this function can\n  # be called in parallel.\n  if ($createSdkLocationFile) {\n    do {\n      $sdkCacheFileTemp = Join-Path $ToolsetDir $([System.IO.Path]::GetRandomFileName())\n    }\n    until (!(Test-Path $sdkCacheFileTemp))\n    Set-Content -Path $sdkCacheFileTemp -Value $dotnetRoot\n\n    try {\n      Move-Item -Force $sdkCacheFileTemp (Join-Path $ToolsetDir 'sdk.txt')\n    } catch {\n      # Somebody beat us\n      Remove-Item -Path $sdkCacheFileTemp\n    }\n  }\n\n  # Add dotnet to PATH. This prevents any bare invocation of dotnet in custom\n  # build steps from using anything other than what we've downloaded.\n  # It also ensures that VS msbuild will use the downloaded sdk targets.\n  $env:PATH = \"$dotnetRoot;$env:PATH\"\n\n  # Make Sure that our bootstrapped dotnet cli is available in future steps of the Azure Pipelines build\n  Write-PipelinePrependPath -Path $dotnetRoot\n\n  Write-PipelineSetVariable -Name 'DOTNET_NOLOGO' -Value '1'\n\n  return $global:_DotNetInstallDir = $dotnetRoot\n}\n\nfunction Retry($downloadBlock, $maxRetries = 5) {\n  $retries = 1\n\n  while($true) {\n    try {\n      & $downloadBlock\n      break\n    }\n    catch {\n      Write-PipelineTelemetryError -Category 'InitializeToolset' -Message $_\n    }\n\n    if (++$retries -le $maxRetries) {\n      $delayInSeconds = [math]::Pow(2, $retries) - 1 # Exponential backoff\n      Write-Host \"Retrying. Waiting for $delayInSeconds seconds before next attempt ($retries of $maxRetries).\"\n      Start-Sleep -Seconds $delayInSeconds\n    }\n    else {\n      Write-PipelineTelemetryError -Category 'InitializeToolset' -Message \"Unable to download file in $maxRetries attempts.\"\n      break\n    }\n  }\n}\n\nfunction GetDotNetInstallScript([string] $dotnetRoot) {\n  $installScript = Join-Path $dotnetRoot 'dotnet-install.ps1'\n  $shouldDownload = $false\n  \n  if (!(Test-Path $installScript)) {\n    $shouldDownload = $true\n  } else {\n    # Check if the script is older than 30 days\n    $fileAge = (Get-Date) - (Get-Item $installScript).LastWriteTime\n    if ($fileAge.Days -gt 30) {\n      Write-Host \"Existing install script is too old, re-downloading...\"\n      $shouldDownload = $true\n    }\n  }\n  \n  if ($shouldDownload) {\n    Create-Directory $dotnetRoot\n    $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit\n    $uri = \"https://builds.dotnet.microsoft.com/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.ps1\"\n\n    Retry({\n      Write-Host \"GET $uri\"\n      Invoke-WebRequest $uri -UseBasicParsing -OutFile $installScript\n    })\n  }\n\n  return $installScript\n}\n\nfunction InstallDotNetSdk([string] $dotnetRoot, [string] $version, [string] $architecture = '', [switch] $noPath) {\n  InstallDotNet $dotnetRoot $version $architecture '' $false $runtimeSourceFeed $runtimeSourceFeedKey -noPath:$noPath\n}\n\nfunction InstallDotNet([string] $dotnetRoot,\n  [string] $version,\n  [string] $architecture = '',\n  [string] $runtime = '',\n  [bool] $skipNonVersionedFiles = $false,\n  [string] $runtimeSourceFeed = '',\n  [string] $runtimeSourceFeedKey = '',\n  [switch] $noPath) {\n\n  $dotnetVersionLabel = \"'sdk v$version'\"\n\n  if ($runtime -ne '' -and $runtime -ne 'sdk') {\n    $runtimePath = $dotnetRoot\n    $runtimePath = $runtimePath + \"\\shared\"\n    if ($runtime -eq \"dotnet\") { $runtimePath = $runtimePath + \"\\Microsoft.NETCore.App\" }\n    if ($runtime -eq \"aspnetcore\") { $runtimePath = $runtimePath + \"\\Microsoft.AspNetCore.App\" }\n    if ($runtime -eq \"windowsdesktop\") { $runtimePath = $runtimePath + \"\\Microsoft.WindowsDesktop.App\" }\n    $runtimePath = $runtimePath + \"\\\" + $version\n  \n    $dotnetVersionLabel = \"runtime toolset '$runtime/$architecture v$version'\"\n\n    if (Test-Path $runtimePath) {\n      Write-Host \"  Runtime toolset '$runtime/$architecture v$version' already installed.\"\n      $installSuccess = $true\n      Exit\n    }\n  }\n\n  $installScript = GetDotNetInstallScript $dotnetRoot\n  $installParameters = @{\n    Version = $version\n    InstallDir = $dotnetRoot\n  }\n\n  if ($architecture) { $installParameters.Architecture = $architecture }\n  if ($runtime) { $installParameters.Runtime = $runtime }\n  if ($skipNonVersionedFiles) { $installParameters.SkipNonVersionedFiles = $skipNonVersionedFiles }\n  if ($noPath) { $installParameters.NoPath = $True }\n\n  $variations = @()\n  $variations += @($installParameters)\n\n  $dotnetBuilds = $installParameters.Clone()\n  $dotnetbuilds.AzureFeed = \"https://ci.dot.net/public\"\n  $variations += @($dotnetBuilds)\n\n  if ($runtimeSourceFeed) {\n    $runtimeSource = $installParameters.Clone()\n    $runtimeSource.AzureFeed = $runtimeSourceFeed\n    if ($runtimeSourceFeedKey) {\n      $decodedBytes = [System.Convert]::FromBase64String($runtimeSourceFeedKey)\n      $decodedString = [System.Text.Encoding]::UTF8.GetString($decodedBytes)\n      $runtimeSource.FeedCredential = $decodedString\n    }\n    $variations += @($runtimeSource)\n  }\n\n  $installSuccess = $false\n  foreach ($variation in $variations) {\n    if ($variation | Get-Member AzureFeed) {\n      $location = $variation.AzureFeed\n    } else {\n      $location = \"public location\";\n    }\n    Write-Host \"  Attempting to install $dotnetVersionLabel from $location.\"\n    try {\n      & $installScript @variation\n      $installSuccess = $true\n      break\n    }\n    catch {\n      Write-Host \"  Failed to install $dotnetVersionLabel from $location.\"\n    }\n  }\n  if (-not $installSuccess) {\n    Write-PipelineTelemetryError -Category 'InitializeToolset' -Message \"Failed to install $dotnetVersionLabel from any of the specified locations.\"\n    ExitWithExitCode 1\n  }\n}\n\n#\n# Locates Visual Studio MSBuild installation.\n# The preference order for MSBuild to use is as follows:\n#\n#   1. MSBuild from an active VS command prompt\n#   2. MSBuild from a compatible VS installation\n#   3. MSBuild from the xcopy tool package\n#\n# Returns full path to msbuild.exe.\n# Throws on failure.\n#\nfunction InitializeVisualStudioMSBuild([bool]$install, [object]$vsRequirements = $null) {\n  if (-not (IsWindowsPlatform)) {\n    throw \"Cannot initialize Visual Studio on non-Windows\"\n  }\n\n  if (Test-Path variable:global:_MSBuildExe) {\n    return $global:_MSBuildExe\n  }\n\n  # Minimum VS version to require.\n  $vsMinVersionReqdStr = '17.7'\n  $vsMinVersionReqd = [Version]::new($vsMinVersionReqdStr)\n\n  # If the version of msbuild is going to be xcopied,\n  # use this version. Version matches a package here:\n  # https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/Microsoft.DotNet.Arcade.MSBuild.Xcopy/versions/18.0.0\n  $defaultXCopyMSBuildVersion = '18.0.0'\n\n  if (!$vsRequirements) {\n    if (Get-Member -InputObject $GlobalJson.tools -Name 'vs') {\n      $vsRequirements = $GlobalJson.tools.vs\n    }\n    else {\n      $vsRequirements = New-Object PSObject -Property @{ version = $vsMinVersionReqdStr }\n    }\n  }\n  $vsMinVersionStr = if ($vsRequirements.version) { $vsRequirements.version } else { $vsMinVersionReqdStr }\n  $vsMinVersion = [Version]::new($vsMinVersionStr)\n\n  # Try msbuild command available in the environment.\n  if ($env:VSINSTALLDIR -ne $null) {\n    $msbuildCmd = Get-Command 'msbuild.exe' -ErrorAction SilentlyContinue\n    if ($msbuildCmd -ne $null) {\n      # Workaround for https://github.com/dotnet/roslyn/issues/35793\n      # Due to this issue $msbuildCmd.Version returns 0.0.0.0 for msbuild.exe 16.2+\n      $msbuildVersion = [Version]::new((Get-Item $msbuildCmd.Path).VersionInfo.ProductVersion.Split([char[]]@('-', '+'))[0])\n\n      if ($msbuildVersion -ge $vsMinVersion) {\n        return $global:_MSBuildExe = $msbuildCmd.Path\n      }\n\n      # Report error - the developer environment is initialized with incompatible VS version.\n      throw \"Developer Command Prompt for VS $($env:VisualStudioVersion) is not recent enough. Please upgrade to $vsMinVersionStr or build from a plain CMD window\"\n    }\n  }\n\n  # Locate Visual Studio installation or download x-copy msbuild.\n  $vsInfo = LocateVisualStudio $vsRequirements\n  if ($vsInfo -ne $null -and $env:ForceUseXCopyMSBuild -eq $null) {\n    # Ensure vsInstallDir has a trailing slash\n    $vsInstallDir = Join-Path $vsInfo.installationPath \"\\\"\n    $vsMajorVersion = $vsInfo.installationVersion.Split('.')[0]\n\n    InitializeVisualStudioEnvironmentVariables $vsInstallDir $vsMajorVersion\n  } else {\n    if (Get-Member -InputObject $GlobalJson.tools -Name 'xcopy-msbuild') {\n      $xcopyMSBuildVersion = $GlobalJson.tools.'xcopy-msbuild'\n      $vsMajorVersion = $xcopyMSBuildVersion.Split('.')[0]\n    } else {\n      #if vs version provided in global.json is incompatible (too low) then use the default version for xcopy msbuild download\n      if($vsMinVersion -lt $vsMinVersionReqd){\n        Write-Host \"Using xcopy-msbuild version of $defaultXCopyMSBuildVersion since VS version $vsMinVersionStr provided in global.json is not compatible\"\n        $xcopyMSBuildVersion = $defaultXCopyMSBuildVersion\n        $vsMajorVersion = $xcopyMSBuildVersion.Split('.')[0]\n      }\n      else{\n        # If the VS version IS compatible, look for an xcopy msbuild package\n        # with a version matching VS.\n        # Note: If this version does not exist, then an explicit version of xcopy msbuild\n        # can be specified in global.json. This will be required for pre-release versions of msbuild.\n        $vsMajorVersion = $vsMinVersion.Major\n        $vsMinorVersion = $vsMinVersion.Minor\n        $xcopyMSBuildVersion = \"$vsMajorVersion.$vsMinorVersion.0\"\n      }\n    }\n\n    $vsInstallDir = $null\n    if ($xcopyMSBuildVersion.Trim() -ine \"none\") {\n        $vsInstallDir = InitializeXCopyMSBuild $xcopyMSBuildVersion $install\n        if ($vsInstallDir -eq $null) {\n            throw \"Could not xcopy msbuild. Please check that package 'Microsoft.DotNet.Arcade.MSBuild.Xcopy @ $xcopyMSBuildVersion' exists on feed 'dotnet-eng'.\"\n        }\n    }\n    if ($vsInstallDir -eq $null) {\n      throw 'Unable to find Visual Studio that has required version and components installed'\n    }\n  }\n\n  $msbuildVersionDir = if ([int]$vsMajorVersion -lt 16) { \"$vsMajorVersion.0\" } else { \"Current\" }\n\n  $local:BinFolder = Join-Path $vsInstallDir \"MSBuild\\$msbuildVersionDir\\Bin\"\n  $local:Prefer64bit = if (Get-Member -InputObject $vsRequirements -Name 'Prefer64bit') { $vsRequirements.Prefer64bit } else { $false }\n  if ($local:Prefer64bit -and (Test-Path(Join-Path $local:BinFolder \"amd64\"))) {\n    $global:_MSBuildExe = Join-Path $local:BinFolder \"amd64\\msbuild.exe\"\n  } else {\n    $global:_MSBuildExe = Join-Path $local:BinFolder \"msbuild.exe\"\n  }\n\n  return $global:_MSBuildExe\n}\n\nfunction InitializeVisualStudioEnvironmentVariables([string] $vsInstallDir, [string] $vsMajorVersion) {\n  $env:VSINSTALLDIR = $vsInstallDir\n  Set-Item \"env:VS$($vsMajorVersion)0COMNTOOLS\" (Join-Path $vsInstallDir \"Common7\\Tools\\\")\n\n  $vsSdkInstallDir = Join-Path $vsInstallDir \"VSSDK\\\"\n  if (Test-Path $vsSdkInstallDir) {\n    Set-Item \"env:VSSDK$($vsMajorVersion)0Install\" $vsSdkInstallDir\n    $env:VSSDKInstall = $vsSdkInstallDir\n  }\n}\n\nfunction InstallXCopyMSBuild([string]$packageVersion) {\n  return InitializeXCopyMSBuild $packageVersion -install $true\n}\n\nfunction InitializeXCopyMSBuild([string]$packageVersion, [bool]$install) {\n  $packageName = 'Microsoft.DotNet.Arcade.MSBuild.Xcopy'\n  $packageDir = Join-Path $ToolsDir \"msbuild\\$packageVersion\"\n  $packagePath = Join-Path $packageDir \"$packageName.$packageVersion.nupkg\"\n\n  if (!(Test-Path $packageDir)) {\n    if (!$install) {\n      return $null\n    }\n\n    Create-Directory $packageDir\n\n    Write-Host \"Downloading $packageName $packageVersion\"\n    $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit\n    Retry({\n      Invoke-WebRequest \"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/flat2/$packageName/$packageVersion/$packageName.$packageVersion.nupkg\" -UseBasicParsing -OutFile $packagePath\n    })\n\n    if (!(Test-Path $packagePath)) {\n      Write-PipelineTelemetryError -Category 'InitializeToolset' -Message \"See https://dev.azure.com/dnceng/internal/_wiki/wikis/DNCEng%20Services%20Wiki/1074/Updating-Microsoft.DotNet.Arcade.MSBuild.Xcopy-WAS-RoslynTools.MSBuild-(xcopy-msbuild)-generation?anchor=troubleshooting for help troubleshooting issues with XCopy MSBuild\"\n      throw\n    }\n    Unzip $packagePath $packageDir\n  }\n\n  return Join-Path $packageDir 'tools'\n}\n\n#\n# Locates Visual Studio instance that meets the minimal requirements specified by tools.vs object in global.json.\n#\n# The following properties of tools.vs are recognized:\n#   \"version\": \"{major}.{minor}\"\n#       Two part minimal VS version, e.g. \"15.9\", \"16.0\", etc.\n#   \"components\": [\"componentId1\", \"componentId2\", ...]\n#       Array of ids of workload components that must be available in the VS instance.\n#       See e.g. https://docs.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-enterprise?view=vs-2017\n#\n# Returns JSON describing the located VS instance (same format as returned by vswhere),\n# or $null if no instance meeting the requirements is found on the machine.\n#\nfunction LocateVisualStudio([object]$vsRequirements = $null){\n  if (-not (IsWindowsPlatform)) {\n    throw \"Cannot run vswhere on non-Windows platforms.\"\n  }\n\n  if (Get-Member -InputObject $GlobalJson.tools -Name 'vswhere') {\n    $vswhereVersion = $GlobalJson.tools.vswhere\n  } else {\n    # keep this in sync with the VSWhereVersion in DefaultVersions.props\n    $vswhereVersion = '3.1.7'\n  }\n\n  $vsWhereDir = Join-Path $ToolsDir \"vswhere\\$vswhereVersion\"\n  $vsWhereExe = Join-Path $vsWhereDir 'vswhere.exe'\n\n  if (!(Test-Path $vsWhereExe)) {\n    Create-Directory $vsWhereDir\n    Write-Host \"Downloading vswhere $vswhereVersion\"\n    $ProgressPreference = 'SilentlyContinue' # Don't display the console progress UI - it's a huge perf hit\n    Retry({\n      Invoke-WebRequest \"https://netcorenativeassets.blob.core.windows.net/resource-packages/external/windows/vswhere/$vswhereVersion/vswhere.exe\" -UseBasicParsing -OutFile $vswhereExe\n    })\n  }\n\n  if (!$vsRequirements) {\n    if (Get-Member -InputObject $GlobalJson.tools -Name 'vs' -ErrorAction SilentlyContinue) {\n      $vsRequirements = $GlobalJson.tools.vs\n    } else {\n      $vsRequirements = $null\n    }\n  }\n\n  $args = @('-latest', '-format', 'json', '-requires', 'Microsoft.Component.MSBuild', '-products', '*')\n\n  if (!$excludePrereleaseVS) {\n    $args += '-prerelease'\n  }\n\n  if ($vsRequirements -and (Get-Member -InputObject $vsRequirements -Name 'version' -ErrorAction SilentlyContinue)) {\n    $args += '-version'\n    $args += $vsRequirements.version\n  }\n\n  if ($vsRequirements -and (Get-Member -InputObject $vsRequirements -Name 'components' -ErrorAction SilentlyContinue)) {\n    foreach ($component in $vsRequirements.components) {\n      $args += '-requires'\n      $args += $component\n    }\n  }\n\n  $vsInfo =& $vsWhereExe $args | ConvertFrom-Json\n\n  if ($lastExitCode -ne 0) {\n    return $null\n  }\n\n  if ($null -eq $vsInfo -or $vsInfo.Count -eq 0) {\n    throw \"No instance of Visual Studio meeting the requirements specified was found. Requirements: $($args -join ' ')\"\n    return $null\n  }\n\n  # use first matching instance\n  return $vsInfo[0]\n}\n\nfunction InitializeBuildTool() {\n  if (Test-Path variable:global:_BuildTool) {\n    # If the requested msbuild parameters do not match, clear the cached variables.\n    if($global:_BuildTool.Contains('ExcludePrereleaseVS') -and $global:_BuildTool.ExcludePrereleaseVS -ne $excludePrereleaseVS) {\n      Remove-Item variable:global:_BuildTool\n      Remove-Item variable:global:_MSBuildExe\n    } else {\n      return $global:_BuildTool\n    }\n  }\n\n  if (-not $msbuildEngine) {\n    $msbuildEngine = GetDefaultMSBuildEngine\n  }\n\n  # Initialize dotnet cli if listed in 'tools'\n  $dotnetRoot = $null\n  if (Get-Member -InputObject $GlobalJson.tools -Name 'dotnet') {\n    $dotnetRoot = InitializeDotNetCli -install:$restore\n  }\n\n  if ($msbuildEngine -eq 'dotnet') {\n    if (!$dotnetRoot) {\n      Write-PipelineTelemetryError -Category 'InitializeToolset' -Message \"/global.json must specify 'tools.dotnet'.\"\n      ExitWithExitCode 1\n    }\n    $dotnetPath = Join-Path $dotnetRoot (GetExecutableFileName 'dotnet')\n\n    $buildTool = @{ Path = $dotnetPath; Command = 'msbuild'; Tool = 'dotnet'; Framework = 'net' }\n  } elseif ($msbuildEngine -eq \"vs\") {\n    try {\n      $msbuildPath = InitializeVisualStudioMSBuild -install:$restore\n    } catch {\n      Write-PipelineTelemetryError -Category 'InitializeToolset' -Message $_\n      ExitWithExitCode 1\n    }\n\n    $buildTool = @{ Path = $msbuildPath; Command = \"\"; Tool = \"vs\"; Framework = \"netframework\"; ExcludePrereleaseVS = $excludePrereleaseVS }\n  } else {\n    Write-PipelineTelemetryError -Category 'InitializeToolset' -Message \"Unexpected value of -msbuildEngine: '$msbuildEngine'.\"\n    ExitWithExitCode 1\n  }\n\n  return $global:_BuildTool = $buildTool\n}\n\nfunction GetDefaultMSBuildEngine() {\n  # Presence of tools.vs indicates the repo needs to build using VS msbuild on Windows.\n  if (Get-Member -InputObject $GlobalJson.tools -Name 'vs') {\n    return 'vs'\n  }\n\n  if (Get-Member -InputObject $GlobalJson.tools -Name 'dotnet') {\n    return 'dotnet'\n  }\n\n  Write-PipelineTelemetryError -Category 'InitializeToolset' -Message \"-msbuildEngine must be specified, or /global.json must specify 'tools.dotnet' or 'tools.vs'.\"\n  ExitWithExitCode 1\n}\n\nfunction GetNuGetPackageCachePath() {\n  if ($env:NUGET_PACKAGES -eq $null) {\n    # Use local cache on CI to ensure deterministic build.\n    # Avoid using the http cache as workaround for https://github.com/NuGet/Home/issues/3116\n    # use global cache in dev builds to avoid cost of downloading packages.\n    # For directory normalization, see also: https://github.com/NuGet/Home/issues/7968\n    if ($useGlobalNuGetCache) {\n      $env:NUGET_PACKAGES = Join-Path $env:UserProfile '.nuget\\packages\\'\n    } else {\n      $env:NUGET_PACKAGES = Join-Path $RepoRoot '.packages\\'\n    }\n  }\n\n  return $env:NUGET_PACKAGES\n}\n\n# Returns a full path to an Arcade SDK task project file.\nfunction GetSdkTaskProject([string]$taskName) {\n  return Join-Path (Split-Path (InitializeToolset) -Parent) \"SdkTasks\\$taskName.proj\"\n}\n\nfunction InitializeNativeTools() {\n  if (-Not (Test-Path variable:DisableNativeToolsetInstalls) -And (Get-Member -InputObject $GlobalJson -Name \"native-tools\")) {\n    $nativeArgs= @{}\n    if ($ci) {\n      $nativeArgs = @{\n        InstallDirectory = \"$ToolsDir\"\n      }\n    }\n    if ($env:NativeToolsOnMachine) {\n      Write-Host \"Variable NativeToolsOnMachine detected, enabling native tool path promotion...\"\n      $nativeArgs += @{ PathPromotion = $true }\n    }\n    & \"$PSScriptRoot/init-tools-native.ps1\" @nativeArgs\n  }\n}\n\nfunction Read-ArcadeSdkVersion() {\n  return $GlobalJson.'msbuild-sdks'.'Microsoft.DotNet.Arcade.Sdk'\n}\n\nfunction InitializeToolset() {\n  # For Unified Build/Source-build support, check whether the environment variable is\n  # set. If it is, then use this as the toolset build project.\n  if ($env:_InitializeToolset -ne $null) {\n    return $global:_InitializeToolset = $env:_InitializeToolset\n  }\n\n  if (Test-Path variable:global:_InitializeToolset) {\n    return $global:_InitializeToolset\n  }\n\n  $nugetCache = GetNuGetPackageCachePath\n\n  $toolsetVersion = Read-ArcadeSdkVersion\n  $toolsetLocationFile = Join-Path $ToolsetDir \"$toolsetVersion.txt\"\n\n  if (Test-Path $toolsetLocationFile) {\n    $path = Get-Content $toolsetLocationFile -TotalCount 1\n    if (Test-Path $path) {\n      return $global:_InitializeToolset = $path\n    }\n  }\n\n  if (-not $restore) {\n    Write-PipelineTelemetryError -Category 'InitializeToolset' -Message \"Toolset version $toolsetVersion has not been restored.\"\n    ExitWithExitCode 1\n  }\n\n  $buildTool = InitializeBuildTool\n\n  $proj = Join-Path $ToolsetDir 'restore.proj'\n  $bl = if ($binaryLog) { '/bl:' + (Join-Path $LogDir 'ToolsetRestore.binlog') } else { '' }\n\n  '<Project Sdk=\"Microsoft.DotNet.Arcade.Sdk\"/>' | Set-Content $proj\n\n  MSBuild-Core $proj $bl /t:__WriteToolsetLocation /clp:ErrorsOnly`;NoSummary /p:__ToolsetLocationOutputFile=$toolsetLocationFile\n\n  $path = Get-Content $toolsetLocationFile -Encoding UTF8 -TotalCount 1\n  if (!(Test-Path $path)) {\n    throw \"Invalid toolset path: $path\"\n  }\n\n  return $global:_InitializeToolset = $path\n}\n\nfunction ExitWithExitCode([int] $exitCode) {\n  if ($ci -and $prepareMachine) {\n    Stop-Processes\n  }\n  exit $exitCode\n}\n\n# Check if $LASTEXITCODE is a nonzero exit code (NZEC). If so, print a Azure Pipeline error for\n# diagnostics, then exit the script with the $LASTEXITCODE.\nfunction Exit-IfNZEC([string] $category = \"General\") {\n  Write-Host \"Exit code $LASTEXITCODE\"\n  if ($LASTEXITCODE -ne 0) {\n    $message = \"Last command failed with exit code $LASTEXITCODE.\"\n    Write-PipelineTelemetryError -Force -Category $category -Message $message\n    ExitWithExitCode $LASTEXITCODE\n  }\n}\n\nfunction Stop-Processes() {\n  Write-Host 'Killing running build processes...'\n  foreach ($processName in $processesToStopOnExit) {\n    Get-Process -Name $processName -ErrorAction SilentlyContinue | Stop-Process\n  }\n}\n\n#\n# Executes msbuild (or 'dotnet msbuild') with arguments passed to the function.\n# The arguments are automatically quoted.\n# Terminates the script if the build fails.\n#\nfunction MSBuild() {\n  if ($pipelinesLog) {\n    $buildTool = InitializeBuildTool\n\n    if ($ci -and $buildTool.Tool -eq 'dotnet') {\n      $env:NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS = 20\n      $env:NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS = 20\n      Write-PipelineSetVariable -Name 'NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS' -Value '20'\n      Write-PipelineSetVariable -Name 'NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS' -Value '20'\n    }\n\n    Enable-Nuget-EnhancedRetry\n\n    $toolsetBuildProject = InitializeToolset\n    $basePath = Split-Path -parent $toolsetBuildProject\n    $selectedPath = Join-Path $basePath (Join-Path $buildTool.Framework 'Microsoft.DotNet.ArcadeLogging.dll')\n\n    if (-not $selectedPath) {\n      Write-PipelineTelemetryError -Category 'Build' -Message \"Unable to find arcade sdk logger assembly: $selectedPath\"\n      ExitWithExitCode 1\n    }\n\n    $args += \"/logger:$selectedPath\"\n  }\n\n  MSBuild-Core @args\n}\n\n#\n# Executes msbuild (or 'dotnet msbuild') with arguments passed to the function.\n# The arguments are automatically quoted.\n# Terminates the script if the build fails.\n#\nfunction MSBuild-Core() {\n  if ($ci) {\n    if (!$binaryLog -and !$excludeCIBinarylog) {\n      Write-PipelineTelemetryError -Category 'Build' -Message 'Binary log must be enabled in CI build, or explicitly opted-out from with the -excludeCIBinarylog switch.'\n      ExitWithExitCode 1\n    }\n\n    if ($nodeReuse) {\n      Write-PipelineTelemetryError -Category 'Build' -Message 'Node reuse must be disabled in CI build.'\n      ExitWithExitCode 1\n    }\n  }\n\n  Enable-Nuget-EnhancedRetry\n\n  $buildTool = InitializeBuildTool\n\n  $cmdArgs = \"$($buildTool.Command) /m /nologo /clp:Summary /v:$verbosity /nr:$nodeReuse /p:ContinuousIntegrationBuild=$ci\"\n\n  # Add -mt flag for MSBuild multithreaded mode if enabled via environment variable\n  if ($env:MSBUILD_MT_ENABLED -eq \"1\") {\n    $cmdArgs += ' -mt'\n  }\n\n  if ($warnAsError) {\n    $cmdArgs += ' /warnaserror /p:TreatWarningsAsErrors=true'\n  }\n  else {\n    $cmdArgs += ' /p:TreatWarningsAsErrors=false'\n  }\n\n  foreach ($arg in $args) {\n    if ($null -ne $arg -and $arg.Trim() -ne \"\") {\n      if ($arg.EndsWith('\\')) {\n        $arg = $arg + \"\\\"\n      }\n      $cmdArgs += \" `\"$arg`\"\"\n    }\n  }\n\n  # Be sure quote the path in case there are spaces in the dotnet installation location.\n  $env:ARCADE_BUILD_TOOL_COMMAND = \"`\"$($buildTool.Path)`\" $cmdArgs\"\n\n  $exitCode = Exec-Process $buildTool.Path $cmdArgs\n\n  if ($exitCode -ne 0) {\n    # We should not Write-PipelineTaskError here because that message shows up in the build summary\n    # The build already logged an error, that's the reason it failed. Producing an error here only adds noise.\n    Write-Host \"Build failed with exit code $exitCode. Check errors above.\" -ForegroundColor Red\n\n    $buildLog = GetMSBuildBinaryLogCommandLineArgument $args\n    if ($null -ne $buildLog) {\n      Write-Host \"See log: $buildLog\" -ForegroundColor DarkGray\n    }\n\n    # When running on Azure Pipelines, override the returned exit code to avoid double logging.\n    # Skip this when the build is a child of the VMR build.\n    if ($ci -and $env:SYSTEM_TEAMPROJECT -ne $null -and !$fromVMR) {\n      Write-PipelineSetResult -Result \"Failed\" -Message \"msbuild execution failed.\"\n      # Exiting with an exit code causes the azure pipelines task to log yet another \"noise\" error\n      # The above Write-PipelineSetResult will cause the task to be marked as failure without adding yet another error\n      ExitWithExitCode 0\n    } else {\n      ExitWithExitCode $exitCode\n    }\n  }\n}\n\nfunction GetMSBuildBinaryLogCommandLineArgument($arguments) {\n  foreach ($argument in $arguments) {\n    if ($argument -ne $null) {\n      $arg = $argument.Trim()\n      if ($arg.StartsWith('/bl:', \"OrdinalIgnoreCase\")) {\n        return $arg.Substring('/bl:'.Length)\n      }\n\n      if ($arg.StartsWith('/binaryLogger:', 'OrdinalIgnoreCase')) {\n        return $arg.Substring('/binaryLogger:'.Length)\n      }\n    }\n  }\n\n  return $null\n}\n\nfunction GetExecutableFileName($baseName) {\n  if (IsWindowsPlatform) {\n    return \"$baseName.exe\"\n  }\n  else {\n    return $baseName\n  }\n}\n\nfunction IsWindowsPlatform() {\n  return [environment]::OSVersion.Platform -eq [PlatformID]::Win32NT\n}\n\nfunction Get-Darc($version) {\n  $darcPath  = \"$TempDir\\darc\\$([guid]::NewGuid())\"\n  if ($version -ne $null) {\n    & $PSScriptRoot\\darc-init.ps1 -toolpath $darcPath -darcVersion $version | Out-Host\n  } else {\n    & $PSScriptRoot\\darc-init.ps1 -toolpath $darcPath | Out-Host\n  }\n  return \"$darcPath\\darc.exe\"\n}\n\n. $PSScriptRoot\\pipeline-logging-functions.ps1\n\n$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\\..\\')\n$EngRoot = Resolve-Path (Join-Path $PSScriptRoot '..')\n$ArtifactsDir = Join-Path $RepoRoot 'artifacts'\n$ToolsetDir = Join-Path $ArtifactsDir 'toolset'\n$ToolsDir = Join-Path $RepoRoot '.tools'\n$LogDir = Join-Path (Join-Path $ArtifactsDir 'log') $configuration\n$TempDir = Join-Path (Join-Path $ArtifactsDir 'tmp') $configuration\n$GlobalJson = Get-Content -Raw -Path (Join-Path $RepoRoot 'global.json') | ConvertFrom-Json\n# true if global.json contains a \"runtimes\" section\n$globalJsonHasRuntimes = if ($GlobalJson.tools.PSObject.Properties.Name -Match 'runtimes') { $true } else { $false }\n\nCreate-Directory $ToolsetDir\nCreate-Directory $TempDir\nCreate-Directory $LogDir\n\nWrite-PipelineSetVariable -Name 'Artifacts' -Value $ArtifactsDir\nWrite-PipelineSetVariable -Name 'Artifacts.Toolset' -Value $ToolsetDir\nWrite-PipelineSetVariable -Name 'Artifacts.Log' -Value $LogDir\nWrite-PipelineSetVariable -Name 'TEMP' -Value $TempDir\nWrite-PipelineSetVariable -Name 'TMP' -Value $TempDir\n\n# Import custom tools configuration, if present in the repo.\n# Note: Import in global scope so that the script set top-level variables without qualification.\nif (!$disableConfigureToolsetImport) {\n  $configureToolsetScript = Join-Path $EngRoot 'configure-toolset.ps1'\n  if (Test-Path $configureToolsetScript) {\n    . $configureToolsetScript\n    if ((Test-Path variable:failOnConfigureToolsetError) -And $failOnConfigureToolsetError) {\n      if ((Test-Path variable:LastExitCode) -And ($LastExitCode -ne 0)) {\n        Write-PipelineTelemetryError -Category 'Build' -Message 'configure-toolset.ps1 returned a non-zero exit code'\n        ExitWithExitCode $LastExitCode\n      }\n    }\n  }\n}\n\n#\n# If $ci flag is set, turn on (and log that we did) special environment variables for improved Nuget client retry logic.\n#\nfunction Enable-Nuget-EnhancedRetry() {\n    if ($ci) {\n      Write-Host \"Setting NUGET enhanced retry environment variables\"\n      $env:NUGET_ENABLE_ENHANCED_HTTP_RETRY = 'true'\n      $env:NUGET_ENHANCED_MAX_NETWORK_TRY_COUNT = 6\n      $env:NUGET_ENHANCED_NETWORK_RETRY_DELAY_MILLISECONDS = 1000\n      $env:NUGET_RETRY_HTTP_429 = 'true'\n      Write-PipelineSetVariable -Name 'NUGET_ENABLE_ENHANCED_HTTP_RETRY' -Value 'true'\n      Write-PipelineSetVariable -Name 'NUGET_ENHANCED_MAX_NETWORK_TRY_COUNT' -Value '6'\n      Write-PipelineSetVariable -Name 'NUGET_ENHANCED_NETWORK_RETRY_DELAY_MILLISECONDS' -Value '1000'\n      Write-PipelineSetVariable -Name 'NUGET_RETRY_HTTP_429' -Value 'true'\n    }\n}\n"
  },
  {
    "path": "eng/common/tools.sh",
    "content": "#!/usr/bin/env bash\n\n# Initialize variables if they aren't already defined.\n\n# CI mode - set to true on CI server for PR validation build or official build.\nci=${ci:-false}\n\n# Build mode\nsource_build=${source_build:-false}\n\n# Set to true to use the pipelines logger which will enable Azure logging output.\n# https://github.com/Microsoft/azure-pipelines-tasks/blob/master/docs/authoring/commands.md\n# This flag is meant as a temporary opt-opt for the feature while validate it across\n# our consumers. It will be deleted in the future.\nif [[ \"$ci\" == true ]]; then\n  pipelines_log=${pipelines_log:-true}\nelse\n  pipelines_log=${pipelines_log:-false}\nfi\n\n# Build configuration. Common values include 'Debug' and 'Release', but the repository may use other names.\nconfiguration=${configuration:-'Debug'}\n\n# Set to true to opt out of outputting binary log while running in CI\nexclude_ci_binary_log=${exclude_ci_binary_log:-false}\n\nif [[ \"$ci\" == true && \"$exclude_ci_binary_log\" == false ]]; then\n  binary_log_default=true\nelse\n  binary_log_default=false\nfi\n\n# Set to true to output binary log from msbuild. Note that emitting binary log slows down the build.\nbinary_log=${binary_log:-$binary_log_default}\n\n# Turns on machine preparation/clean up code that changes the machine state (e.g. kills build processes).\nprepare_machine=${prepare_machine:-false}\n\n# True to restore toolsets and dependencies.\nrestore=${restore:-true}\n\n# Adjusts msbuild verbosity level.\nverbosity=${verbosity:-'minimal'}\n\n# Set to true to reuse msbuild nodes. Recommended to not reuse on CI.\nif [[ \"$ci\" == true ]]; then\n  node_reuse=${node_reuse:-false}\nelse\n  node_reuse=${node_reuse:-true}\nfi\n\n# Configures warning treatment in msbuild.\nwarn_as_error=${warn_as_error:-true}\n\n# True to attempt using .NET Core already that meets requirements specified in global.json\n# installed on the machine instead of downloading one.\nuse_installed_dotnet_cli=${use_installed_dotnet_cli:-true}\n\n# Enable repos to use a particular version of the on-line dotnet-install scripts.\n#    default URL: https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.sh\ndotnetInstallScriptVersion=${dotnetInstallScriptVersion:-'v1'}\n\n# True to use global NuGet cache instead of restoring packages to repository-local directory.\n# Keep in sync with NuGetPackageroot in Arcade SDK's RepositoryLayout.props.\nif [[ \"$ci\" == true || \"$source_build\" == true ]]; then\n  use_global_nuget_cache=${use_global_nuget_cache:-false}\nelse\n  use_global_nuget_cache=${use_global_nuget_cache:-true}\nfi\n\n# Used when restoring .NET SDK from alternative feeds\nruntime_source_feed=${runtime_source_feed:-''}\nruntime_source_feed_key=${runtime_source_feed_key:-''}\n\n# True when the build is running within the VMR.\nfrom_vmr=${from_vmr:-false}\n\n# Resolve any symlinks in the given path.\nfunction ResolvePath {\n  local path=$1\n\n  while [[ -h $path ]]; do\n    local dir=\"$( cd -P \"$( dirname \"$path\" )\" && pwd )\"\n    path=\"$(readlink \"$path\")\"\n\n    # if $path was a relative symlink, we need to resolve it relative to the path where the\n    # symlink file was located\n    [[ $path != /* ]] && path=\"$dir/$path\"\n  done\n\n  # return value\n  _ResolvePath=\"$path\"\n}\n\n# ReadVersionFromJson [json key]\nfunction ReadGlobalVersion {\n  local key=$1\n\n  if command -v jq &> /dev/null; then\n    _ReadGlobalVersion=\"$(jq -r \".[] | select(has(\\\"$key\\\")) | .\\\"$key\\\"\" \"$global_json_file\")\"\n  elif [[ \"$(cat \"$global_json_file\")\" =~ \\\"$key\\\"[[:space:]\\:]*\\\"([^\\\"]+) ]]; then\n    _ReadGlobalVersion=${BASH_REMATCH[1]}\n  fi\n\n  if [[ -z \"$_ReadGlobalVersion\" ]]; then\n    Write-PipelineTelemetryError -category 'Build' \"Error: Cannot find \\\"$key\\\" in $global_json_file\"\n    ExitWithExitCode 1\n  fi\n}\n\nfunction InitializeDotNetCli {\n  if [[ -n \"${_InitializeDotNetCli:-}\" ]]; then\n    return\n  fi\n\n  local install=$1\n\n  # Disable first run since we want to control all package sources\n  export DOTNET_NOLOGO=1\n\n  # Disable telemetry on CI\n  if [[ $ci == true ]]; then\n    export DOTNET_CLI_TELEMETRY_OPTOUT=1\n  fi\n\n  # LTTNG is the logging infrastructure used by Core CLR. Need this variable set\n  # so it doesn't output warnings to the console.\n  export LTTNG_HOME=\"$HOME\"\n\n  # Find the first path on $PATH that contains the dotnet.exe\n  if [[ \"$use_installed_dotnet_cli\" == true && $global_json_has_runtimes == false && -z \"${DOTNET_INSTALL_DIR:-}\" ]]; then\n    local dotnet_path=`command -v dotnet`\n    if [[ -n \"$dotnet_path\" ]]; then\n      ResolvePath \"$dotnet_path\"\n      export DOTNET_INSTALL_DIR=`dirname \"$_ResolvePath\"`\n    fi\n  fi\n\n  ReadGlobalVersion \"dotnet\"\n  local dotnet_sdk_version=$_ReadGlobalVersion\n  local dotnet_root=\"\"\n\n  # Use dotnet installation specified in DOTNET_INSTALL_DIR if it contains the required SDK version,\n  # otherwise install the dotnet CLI and SDK to repo local .dotnet directory to avoid potential permission issues.\n  if [[ $global_json_has_runtimes == false && -n \"${DOTNET_INSTALL_DIR:-}\" && -d \"$DOTNET_INSTALL_DIR/sdk/$dotnet_sdk_version\" ]]; then\n    dotnet_root=\"$DOTNET_INSTALL_DIR\"\n  else\n    dotnet_root=\"${repo_root}.dotnet\"\n\n    export DOTNET_INSTALL_DIR=\"$dotnet_root\"\n\n    if [[ ! -d \"$DOTNET_INSTALL_DIR/sdk/$dotnet_sdk_version\" ]]; then\n      if [[ \"$install\" == true ]]; then\n        InstallDotNetSdk \"$dotnet_root\" \"$dotnet_sdk_version\"\n      else\n        Write-PipelineTelemetryError -category 'InitializeToolset' \"Unable to find dotnet with SDK version '$dotnet_sdk_version'\"\n        ExitWithExitCode 1\n      fi\n    fi\n  fi\n\n  # Add dotnet to PATH. This prevents any bare invocation of dotnet in custom\n  # build steps from using anything other than what we've downloaded.\n  Write-PipelinePrependPath -path \"$dotnet_root\"\n\n  Write-PipelineSetVariable -name \"DOTNET_NOLOGO\" -value \"1\"\n\n  # return value\n  _InitializeDotNetCli=\"$dotnet_root\"\n}\n\nfunction InstallDotNetSdk {\n  local root=$1\n  local version=$2\n  local architecture=\"unset\"\n  if [[ $# -ge 3 ]]; then\n    architecture=$3\n  fi\n  InstallDotNet \"$root\" \"$version\" $architecture 'sdk' 'true' $runtime_source_feed $runtime_source_feed_key\n}\n\nfunction InstallDotNet {\n  local root=$1\n  local version=$2\n  local runtime=$4\n\n  local dotnetVersionLabel=\"'$runtime v$version'\"\n  if [[ -n \"${4:-}\" ]] && [ \"$4\" != 'sdk' ]; then\n    runtimePath=\"$root\"\n    runtimePath=\"$runtimePath/shared\"\n    case \"$runtime\" in\n      dotnet)\n        runtimePath=\"$runtimePath/Microsoft.NETCore.App\"\n        ;;\n      aspnetcore)\n        runtimePath=\"$runtimePath/Microsoft.AspNetCore.App\"\n        ;;\n      windowsdesktop)\n        runtimePath=\"$runtimePath/Microsoft.WindowsDesktop.App\"\n        ;;\n      *)\n        ;;\n    esac\n    runtimePath=\"$runtimePath/$version\"\n\n    dotnetVersionLabel=\"runtime toolset '$runtime/$architecture v$version'\"\n\n    if [ -d \"$runtimePath\" ]; then\n      echo \"  Runtime toolset '$runtime/$architecture v$version' already installed.\"\n      local installSuccess=1\n      return\n    fi\n  fi\n\n  GetDotNetInstallScript \"$root\"\n  local install_script=$_GetDotNetInstallScript\n\n  local installParameters=(--version $version --install-dir \"$root\")\n\n  if [[ -n \"${3:-}\" ]] && [ \"$3\" != 'unset' ]; then\n    installParameters+=(--architecture $3)\n  fi\n  if [[ -n \"${4:-}\" ]] && [ \"$4\" != 'sdk' ]; then\n    installParameters+=(--runtime $4)\n  fi\n  if [[ \"$#\" -ge \"5\" ]] && [[ \"$5\" != 'false' ]]; then\n    installParameters+=(--skip-non-versioned-files)\n  fi\n\n  local variations=() # list of variable names with parameter arrays in them\n\n  local public_location=(\"${installParameters[@]}\")\n  variations+=(public_location)\n\n  local dotnetbuilds=(\"${installParameters[@]}\" --azure-feed \"https://ci.dot.net/public\")\n  variations+=(dotnetbuilds)\n\n  if [[ -n \"${6:-}\" ]]; then\n    variations+=(private_feed)\n    local private_feed=(\"${installParameters[@]}\" --azure-feed $6)\n    if [[ -n \"${7:-}\" ]]; then\n      # The 'base64' binary on alpine uses '-d' and doesn't support '--decode'\n      # '-d'. To work around this, do a simple detection and switch the parameter\n      # accordingly.\n      decodeArg=\"--decode\"\n      if base64 --help 2>&1 | grep -q \"BusyBox\"; then\n          decodeArg=\"-d\"\n      fi\n      decodedFeedKey=`echo $7 | base64 $decodeArg`\n      private_feed+=(--feed-credential $decodedFeedKey)\n    fi\n  fi\n\n  local installSuccess=0\n  for variationName in \"${variations[@]}\"; do\n    local name=\"$variationName[@]\"\n    local variation=(\"${!name}\")\n    echo \"  Attempting to install $dotnetVersionLabel from $variationName.\"\n    bash \"$install_script\" \"${variation[@]}\" && installSuccess=1\n    if [[ \"$installSuccess\" -eq 1 ]]; then\n      break\n    fi\n\n    echo \"  Failed to install $dotnetVersionLabel from $variationName.\"\n  done\n\n  if [[ \"$installSuccess\" -eq 0 ]]; then\n    Write-PipelineTelemetryError -category 'InitializeToolset' \"Failed to install $dotnetVersionLabel from any of the specified locations.\"\n    ExitWithExitCode 1\n  fi\n}\n\nfunction with_retries {\n  local maxRetries=5\n  local retries=1\n  echo \"Trying to run '$@' for maximum of $maxRetries attempts.\"\n  while [[ $((retries++)) -le $maxRetries ]]; do\n    \"$@\"\n\n    if [[ $? == 0 ]]; then\n      echo \"Ran '$@' successfully.\"\n      return 0\n    fi\n\n    timeout=$((3**$retries-1))\n    echo \"Failed to execute '$@'. Waiting $timeout seconds before next attempt ($retries out of $maxRetries).\" 1>&2\n    sleep $timeout\n  done\n\n  echo \"Failed to execute '$@' for $maxRetries times.\" 1>&2\n\n  return 1\n}\n\nfunction GetDotNetInstallScript {\n  local root=$1\n  local install_script=\"$root/dotnet-install.sh\"\n  local install_script_url=\"https://builds.dotnet.microsoft.com/dotnet/scripts/$dotnetInstallScriptVersion/dotnet-install.sh\"\n  local timestamp_file=\"$root/.dotnet-install.timestamp\"\n  local should_download=false\n\n  if [[ ! -a \"$install_script\" ]]; then\n    should_download=true\n  elif [[ -f \"$timestamp_file\" ]]; then\n    # Check if the script is older than 30 days using timestamp file\n    local download_time=$(cat \"$timestamp_file\" 2>/dev/null || echo \"0\")\n    local current_time=$(date +%s)\n    local age_seconds=$((current_time - download_time))\n    \n    # 30 days = 30 * 24 * 60 * 60 = 2592000 seconds\n    if [[ $age_seconds -gt 2592000 ]]; then\n      echo \"Existing install script is too old, re-downloading...\"\n      should_download=true\n    fi\n  else\n    # No timestamp file exists, assume script is old and re-download\n    echo \"No timestamp found for existing install script, re-downloading...\"\n    should_download=true\n  fi\n\n  if [[ \"$should_download\" == true ]]; then\n    mkdir -p \"$root\"\n\n    echo \"Downloading '$install_script_url'\"\n\n    # Use curl if available, otherwise use wget\n    if command -v curl > /dev/null; then\n      # first, try directly, if this fails we will retry with verbose logging\n      curl \"$install_script_url\" -sSL --retry 10 --create-dirs -o \"$install_script\" || {\n        if command -v openssl &> /dev/null; then\n          echo \"Curl failed; dumping some information about dotnet.microsoft.com for later investigation\"\n          echo | openssl s_client -showcerts -servername dotnet.microsoft.com  -connect dotnet.microsoft.com:443 || true\n        fi\n        echo \"Will now retry the same URL with verbose logging.\"\n        with_retries curl \"$install_script_url\" -sSL --verbose --retry 10 --create-dirs -o \"$install_script\" || {\n          local exit_code=$?\n          Write-PipelineTelemetryError -category 'InitializeToolset' \"Failed to acquire dotnet install script (exit code '$exit_code').\"\n          ExitWithExitCode $exit_code\n        }\n      }\n    else\n      with_retries wget -v -O \"$install_script\" \"$install_script_url\" || {\n        local exit_code=$?\n        Write-PipelineTelemetryError -category 'InitializeToolset' \"Failed to acquire dotnet install script (exit code '$exit_code').\"\n        ExitWithExitCode $exit_code\n      }\n    fi\n    \n    # Create timestamp file to track download time in seconds from epoch\n    date +%s > \"$timestamp_file\"\n  fi\n  # return value\n  _GetDotNetInstallScript=\"$install_script\"\n}\n\nfunction InitializeBuildTool {\n  if [[ -n \"${_InitializeBuildTool:-}\" ]]; then\n    return\n  fi\n\n  InitializeDotNetCli $restore\n\n  # return values\n  _InitializeBuildTool=\"$_InitializeDotNetCli/dotnet\"\n  _InitializeBuildToolCommand=\"msbuild\"\n}\n\nfunction GetNuGetPackageCachePath {\n  if [[ -z ${NUGET_PACKAGES:-} ]]; then\n    if [[ \"$use_global_nuget_cache\" == true ]]; then\n      export NUGET_PACKAGES=\"$HOME/.nuget/packages/\"\n    else\n      export NUGET_PACKAGES=\"$repo_root/.packages/\"\n    fi\n  fi\n\n  # return value\n  _GetNuGetPackageCachePath=$NUGET_PACKAGES\n}\n\nfunction InitializeNativeTools() {\n  if [[ -n \"${DisableNativeToolsetInstalls:-}\" ]]; then\n    return\n  fi\n  if grep -Fq \"native-tools\" $global_json_file\n  then\n    local nativeArgs=\"\"\n    if [[ \"$ci\" == true ]]; then\n      nativeArgs=\"--installDirectory $tools_dir\"\n    fi\n    \"$_script_dir/init-tools-native.sh\" $nativeArgs\n  fi\n}\n\nfunction InitializeToolset {\n  if [[ -n \"${_InitializeToolset:-}\" ]]; then\n    return\n  fi\n\n  GetNuGetPackageCachePath\n\n  ReadGlobalVersion \"Microsoft.DotNet.Arcade.Sdk\"\n\n  local toolset_version=$_ReadGlobalVersion\n  local toolset_location_file=\"$toolset_dir/$toolset_version.txt\"\n\n  if [[ -a \"$toolset_location_file\" ]]; then\n    local path=`cat \"$toolset_location_file\"`\n    if [[ -a \"$path\" ]]; then\n      # return value\n      _InitializeToolset=\"$path\"\n      return\n    fi\n  fi\n\n  if [[ \"$restore\" != true ]]; then\n    Write-PipelineTelemetryError -category 'InitializeToolset' \"Toolset version $toolset_version has not been restored.\"\n    ExitWithExitCode 2\n  fi\n\n  local proj=\"$toolset_dir/restore.proj\"\n\n  local bl=\"\"\n  if [[ \"$binary_log\" == true ]]; then\n    bl=\"/bl:$log_dir/ToolsetRestore.binlog\"\n  fi\n\n  echo '<Project Sdk=\"Microsoft.DotNet.Arcade.Sdk\"/>' > \"$proj\"\n  MSBuild-Core \"$proj\" $bl /t:__WriteToolsetLocation /clp:ErrorsOnly\\;NoSummary /p:__ToolsetLocationOutputFile=\"$toolset_location_file\"\n\n  local toolset_build_proj=`cat \"$toolset_location_file\"`\n\n  if [[ ! -a \"$toolset_build_proj\" ]]; then\n    Write-PipelineTelemetryError -category 'Build' \"Invalid toolset path: $toolset_build_proj\"\n    ExitWithExitCode 3\n  fi\n\n  # return value\n  _InitializeToolset=\"$toolset_build_proj\"\n}\n\nfunction ExitWithExitCode {\n  if [[ \"$ci\" == true && \"$prepare_machine\" == true ]]; then\n    StopProcesses\n  fi\n  exit $1\n}\n\nfunction StopProcesses {\n  echo \"Killing running build processes...\"\n  pkill -9 \"dotnet\" || true\n  pkill -9 \"vbcscompiler\" || true\n  return 0\n}\n\nfunction MSBuild {\n  local args=( \"$@\" )\n  if [[ \"$pipelines_log\" == true ]]; then\n    InitializeBuildTool\n    InitializeToolset\n\n    if [[ \"$ci\" == true ]]; then\n      export NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS=20\n      export NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS=20\n      Write-PipelineSetVariable -name \"NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS\" -value \"20\"\n      Write-PipelineSetVariable -name \"NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS\" -value \"20\"\n    fi\n\n    local toolset_dir=\"${_InitializeToolset%/*}\"\n    local selectedPath=\"$toolset_dir/net/Microsoft.DotNet.ArcadeLogging.dll\"\n\n    if [[ -z \"$selectedPath\" ]]; then\n      Write-PipelineTelemetryError -category 'Build'  \"Unable to find arcade sdk logger assembly: $selectedPath\"\n      ExitWithExitCode 1\n    fi\n\n    args+=( \"-logger:$selectedPath\" )\n  fi\n\n  MSBuild-Core \"${args[@]}\"\n}\n\nfunction MSBuild-Core {\n  if [[ \"$ci\" == true ]]; then\n    if [[ \"$binary_log\" != true && \"$exclude_ci_binary_log\" != true ]]; then\n      Write-PipelineTelemetryError -category 'Build'  \"Binary log must be enabled in CI build, or explicitly opted-out from with the -noBinaryLog switch.\"\n      ExitWithExitCode 1\n    fi\n\n    if [[ \"$node_reuse\" == true ]]; then\n      Write-PipelineTelemetryError -category 'Build'  \"Node reuse must be disabled in CI build.\"\n      ExitWithExitCode 1\n    fi\n  fi\n\n  InitializeBuildTool\n\n  local warnaserror_switch=\"\"\n  if [[ $warn_as_error == true ]]; then\n    warnaserror_switch=\"/warnaserror\"\n  fi\n\n  function RunBuildTool {\n    export ARCADE_BUILD_TOOL_COMMAND=\"$_InitializeBuildTool $@\"\n\n    \"$_InitializeBuildTool\" \"$@\" || {\n      local exit_code=$?\n      # We should not Write-PipelineTaskError here because that message shows up in the build summary\n      # The build already logged an error, that's the reason it failed. Producing an error here only adds noise.\n      echo \"Build failed with exit code $exit_code. Check errors above.\"\n\n      # When running on Azure Pipelines, override the returned exit code to avoid double logging.\n      # Skip this when the build is a child of the VMR build.\n      if [[ \"$ci\" == true && -n ${SYSTEM_TEAMPROJECT:-} && \"$from_vmr\" != true ]]; then\n        Write-PipelineSetResult -result \"Failed\" -message \"msbuild execution failed.\"\n        # Exiting with an exit code causes the azure pipelines task to log yet another \"noise\" error\n        # The above Write-PipelineSetResult will cause the task to be marked as failure without adding yet another error\n        ExitWithExitCode 0\n      else\n        ExitWithExitCode $exit_code\n      fi\n    }\n  }\n\n  # Add -mt flag for MSBuild multithreaded mode if enabled via environment variable\n  local mt_switch=\"\"\n  if [[ \"${MSBUILD_MT_ENABLED:-}\" == \"1\" ]]; then\n    mt_switch=\"-mt\"\n  fi\n\n  RunBuildTool \"$_InitializeBuildToolCommand\" /m /nologo /clp:Summary /v:$verbosity /nr:$node_reuse $warnaserror_switch $mt_switch /p:TreatWarningsAsErrors=$warn_as_error /p:ContinuousIntegrationBuild=$ci \"$@\"\n}\n\nfunction GetDarc {\n    darc_path=\"$temp_dir/darc\"\n    version=\"$1\"\n\n    if [[ -n \"$version\" ]]; then\n      version=\"--darcversion $version\"\n    fi\n\n    \"$eng_root/common/darc-init.sh\" --toolpath \"$darc_path\" $version\n    darc_tool=\"$darc_path/darc\"\n}\n\n# Returns a full path to an Arcade SDK task project file.\nfunction GetSdkTaskProject {\n  taskName=$1\n  echo \"$(dirname $_InitializeToolset)/SdkTasks/$taskName.proj\"\n}\n\nResolvePath \"${BASH_SOURCE[0]}\"\n_script_dir=`dirname \"$_ResolvePath\"`\n\n. \"$_script_dir/pipeline-logging-functions.sh\"\n\neng_root=`cd -P \"$_script_dir/..\" && pwd`\nrepo_root=`cd -P \"$_script_dir/../..\" && pwd`\nrepo_root=\"${repo_root}/\"\nartifacts_dir=\"${repo_root}artifacts\"\ntoolset_dir=\"$artifacts_dir/toolset\"\ntools_dir=\"${repo_root}.tools\"\nlog_dir=\"$artifacts_dir/log/$configuration\"\ntemp_dir=\"$artifacts_dir/tmp/$configuration\"\n\nglobal_json_file=\"${repo_root}global.json\"\n# determine if global.json contains a \"runtimes\" entry\nglobal_json_has_runtimes=false\nif command -v jq &> /dev/null; then\n  if jq -e '.tools | has(\"runtimes\")' \"$global_json_file\" &> /dev/null; then\n    global_json_has_runtimes=true\n  fi\nelif [[ \"$(cat \"$global_json_file\")\" =~ \\\"runtimes\\\"[[:space:]\\:]*\\{ ]]; then\n  global_json_has_runtimes=true\nfi\n\n# HOME may not be defined in some scenarios, but it is required by NuGet\nif [[ -z $HOME ]]; then\n  export HOME=\"${repo_root}artifacts/.home/\"\n  mkdir -p \"$HOME\"\nfi\n\nmkdir -p \"$toolset_dir\"\nmkdir -p \"$temp_dir\"\nmkdir -p \"$log_dir\"\n\nWrite-PipelineSetVariable -name \"Artifacts\" -value \"$artifacts_dir\"\nWrite-PipelineSetVariable -name \"Artifacts.Toolset\" -value \"$toolset_dir\"\nWrite-PipelineSetVariable -name \"Artifacts.Log\" -value \"$log_dir\"\nWrite-PipelineSetVariable -name \"Temp\" -value \"$temp_dir\"\nWrite-PipelineSetVariable -name \"TMP\" -value \"$temp_dir\"\n\n# Import custom tools configuration, if present in the repo.\nif [ -z \"${disable_configure_toolset_import:-}\" ]; then\n  configure_toolset_script=\"$eng_root/configure-toolset.sh\"\n  if [[ -a \"$configure_toolset_script\" ]]; then\n    . \"$configure_toolset_script\"\n  fi\nfi\n\n# TODO: https://github.com/dotnet/arcade/issues/1468\n# Temporary workaround to avoid breaking change.\n# Remove once repos are updated.\nif [[ -n \"${useInstalledDotNetCli:-}\" ]]; then\n  use_installed_dotnet_cli=\"$useInstalledDotNetCli\"\nfi\n"
  },
  {
    "path": "eng/common/vmr-sync.ps1",
    "content": "<#\n.SYNOPSIS\n\nThis script is used for synchronizing the current repository into a local VMR.\nIt pulls the current repository's code into the specified VMR directory for local testing or\nSource-Build validation.\n\n.DESCRIPTION\n\nThe tooling used for synchronization will clone the VMR repository into a temporary folder if\nit does not already exist. These clones can be reused in future synchronizations, so it is\nrecommended to dedicate a folder for this to speed up re-runs.\n\n.EXAMPLE\n  Synchronize current repository into a local VMR:\n    ./vmr-sync.ps1 -vmrDir \"$HOME/repos/dotnet\" -tmpDir \"$HOME/repos/tmp\"\n\n.PARAMETER tmpDir\nRequired. Path to the temporary folder where repositories will be cloned\n\n.PARAMETER vmrBranch\nOptional. Branch of the 'dotnet/dotnet' repo to synchronize. The VMR will be checked out to this branch\n\n.PARAMETER azdevPat\nOptional. Azure DevOps PAT to use for cloning private repositories.\n\n.PARAMETER vmrDir\nOptional. Path to the dotnet/dotnet repository. When null, gets cloned to the temporary folder\n\n.PARAMETER debugOutput\nOptional. Enables debug logging in the darc vmr command.\n\n.PARAMETER ci\nOptional. Denotes that the script is running in a CI environment.\n#>\nparam (\n  [Parameter(Mandatory=$true, HelpMessage=\"Path to the temporary folder where repositories will be cloned\")]\n  [string][Alias('t', 'tmp')]$tmpDir,\n  [string][Alias('b', 'branch')]$vmrBranch,\n  [string]$remote,\n  [string]$azdevPat,\n  [string][Alias('v', 'vmr')]$vmrDir,\n  [switch]$ci,\n  [switch]$debugOutput\n)\n\nfunction Fail {\n  Write-Host \"> $($args[0])\" -ForegroundColor 'Red'\n}\n\nfunction Highlight {\n  Write-Host \"> $($args[0])\" -ForegroundColor 'Cyan'\n}\n\n$verbosity = 'verbose'\nif ($debugOutput) {\n  $verbosity = 'debug'\n}\n# Validation\n\nif (-not $tmpDir) {\n  Fail \"Missing -tmpDir argument. Please specify the path to the temporary folder where the repositories will be cloned\"\n  exit 1\n}\n\n# Sanitize the input\n\nif (-not $vmrDir) {\n  $vmrDir = Join-Path $tmpDir 'dotnet'\n}\n\nif (-not (Test-Path -Path $tmpDir -PathType Container)) {\n  New-Item -ItemType Directory -Path $tmpDir | Out-Null\n}\n\n# Prepare the VMR\n\nif (-not (Test-Path -Path $vmrDir -PathType Container)) {\n  Highlight \"Cloning 'dotnet/dotnet' into $vmrDir..\"\n  git clone https://github.com/dotnet/dotnet $vmrDir\n\n  if ($vmrBranch) {\n    git -C $vmrDir switch -c $vmrBranch\n  }\n}\nelse {\n  if ((git -C $vmrDir diff --quiet) -eq $false) {\n    Fail \"There are changes in the working tree of $vmrDir. Please commit or stash your changes\"\n    exit 1\n  }\n\n  if ($vmrBranch) {\n    Highlight \"Preparing $vmrDir\"\n    git -C $vmrDir checkout $vmrBranch\n    git -C $vmrDir pull\n  }\n}\n\nSet-StrictMode -Version Latest\n\n# Prepare darc\n\nHighlight 'Installing .NET, preparing the tooling..'\n. .\\eng\\common\\tools.ps1\n$dotnetRoot = InitializeDotNetCli -install:$true\n$env:DOTNET_ROOT = $dotnetRoot\n$darc = Get-Darc\n\nHighlight \"Starting the synchronization of VMR..\"\n\n# Synchronize the VMR\n$versionDetailsPath = Resolve-Path (Join-Path $PSScriptRoot '..\\Version.Details.xml') | Select-Object -ExpandProperty Path\n[xml]$versionDetails = Get-Content -Path $versionDetailsPath\n$repoName = $versionDetails.SelectSingleNode('//Source').Mapping\nif (-not $repoName) {\n  Fail \"Failed to resolve repo mapping from $versionDetailsPath\"\n  exit 1\n}\n\n$darcArgs = (\n  \"vmr\", \"forwardflow\",\n  \"--tmp\", $tmpDir,\n  \"--$verbosity\",\n  $vmrDir\n)\n\nif ($ci) {\n  $darcArgs += (\"--ci\")\n}\n\nif ($azdevPat) {\n  $darcArgs += (\"--azdev-pat\", $azdevPat)\n}\n\n& \"$darc\" $darcArgs\n\nif ($LASTEXITCODE -eq 0) {\n  Highlight \"Synchronization succeeded\"\n}\nelse {\n  Highlight \"Failed to flow code into the local VMR. Falling back to resetting the VMR to match repo contents...\"\n  git -C $vmrDir reset --hard\n\n  $resetArgs = (\n    \"vmr\", \"reset\",\n    \"${repoName}:HEAD\",\n    \"--vmr\", $vmrDir,\n    \"--tmp\", $tmpDir,\n    \"--additional-remotes\", \"${repoName}:${repoRoot}\"\n  )\n\n  & \"$darc\" $resetArgs\n\n  if ($LASTEXITCODE -eq 0) {\n    Highlight \"Successfully reset the VMR using 'darc vmr reset'\"\n  }\n  else {\n    Fail \"Synchronization of repo to VMR failed!\"\n    Fail \"'$vmrDir' is left in its last state (re-run of this script will reset it).\"\n    Fail \"Please inspect the logs which contain path to the failing patch file (use -debugOutput to get all the details).\"\n    Fail \"Once you make changes to the conflicting VMR patch, commit it locally and re-run this script.\"\n    exit 1\n  }\n}\n"
  },
  {
    "path": "eng/common/vmr-sync.sh",
    "content": "#!/bin/bash\n\n### This script is used for synchronizing the current repository into a local VMR.\n### It pulls the current repository's code into the specified VMR directory for local testing or\n### Source-Build validation.\n###\n### The tooling used for synchronization will clone the VMR repository into a temporary folder if\n### it does not already exist. These clones can be reused in future synchronizations, so it is\n### recommended to dedicate a folder for this to speed up re-runs.\n###\n### USAGE:\n###   Synchronize current repository into a local VMR:\n###     ./vmr-sync.sh --tmp \"$HOME/repos/tmp\" \"$HOME/repos/dotnet\"\n###\n### Options:\n###   -t, --tmp, --tmp-dir PATH\n###       Required. Path to the temporary folder where repositories will be cloned\n###\n###   -b, --branch, --vmr-branch BRANCH_NAME\n###       Optional. Branch of the 'dotnet/dotnet' repo to synchronize. The VMR will be checked out to this branch\n###\n###   --debug\n###       Optional. Turns on the most verbose logging for the VMR tooling\n###\n###   --remote name:URI\n###       Optional. Additional remote to use during the synchronization\n###       This can be used to synchronize to a commit from a fork of the repository\n###       Example: 'runtime:https://github.com/yourfork/runtime'\n###\n###   --azdev-pat\n###       Optional. Azure DevOps PAT to use for cloning private repositories.\n###\n###   -v, --vmr, --vmr-dir PATH\n###       Optional. Path to the dotnet/dotnet repository. When null, gets cloned to the temporary folder\n\nsource=\"${BASH_SOURCE[0]}\"\n\n# resolve $source until the file is no longer a symlink\nwhile [[ -h \"$source\" ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\nfunction print_help () {\n    sed -n '/^### /,/^$/p' \"$source\" | cut -b 5-\n}\n\nCOLOR_RED=$(tput setaf 1 2>/dev/null || true)\nCOLOR_CYAN=$(tput setaf 6 2>/dev/null || true)\nCOLOR_CLEAR=$(tput sgr0 2>/dev/null || true)\nCOLOR_RESET=uniquesearchablestring\nFAILURE_PREFIX='> '\n\nfunction fail () {\n  echo \"${COLOR_RED}$FAILURE_PREFIX${1//${COLOR_RESET}/${COLOR_RED}}${COLOR_CLEAR}\" >&2\n}\n\nfunction highlight () {\n  echo \"${COLOR_CYAN}$FAILURE_PREFIX${1//${COLOR_RESET}/${COLOR_CYAN}}${COLOR_CLEAR}\"\n}\n\ntmp_dir=''\nvmr_dir=''\nvmr_branch=''\nadditional_remotes=''\nverbosity=verbose\nazdev_pat=''\nci=false\n\nwhile [[ $# -gt 0 ]]; do\n  opt=\"$(echo \"$1\" | tr \"[:upper:]\" \"[:lower:]\")\"\n  case \"$opt\" in\n    -t|--tmp|--tmp-dir)\n      tmp_dir=$2\n      shift\n      ;;\n    -v|--vmr|--vmr-dir)\n      vmr_dir=$2\n      shift\n      ;;\n    -b|--branch|--vmr-branch)\n      vmr_branch=$2\n      shift\n      ;;\n    --remote)\n      additional_remotes=\"$additional_remotes $2\"\n      shift\n      ;;\n    --azdev-pat)\n      azdev_pat=$2\n      shift\n      ;;\n    --ci)\n      ci=true\n      ;;\n    -d|--debug)\n      verbosity=debug\n      ;;\n    -h|--help)\n      print_help\n      exit 0\n      ;;\n    *)\n      fail \"Invalid argument: $1\"\n      print_help\n      exit 1\n      ;;\n  esac\n\n  shift\ndone\n\n# Validation\n\nif [[ -z \"$tmp_dir\" ]]; then\n  fail \"Missing --tmp-dir argument. Please specify the path to the temporary folder where the repositories will be cloned\"\n  exit 1\nfi\n\n# Sanitize the input\n\nif [[ -z \"$vmr_dir\" ]]; then\n  vmr_dir=\"$tmp_dir/dotnet\"\nfi\n\nif [[ ! -d \"$tmp_dir\" ]]; then\n  mkdir -p \"$tmp_dir\"\nfi\n\nif [[ \"$verbosity\" == \"debug\" ]]; then\n  set -x\nfi\n\n# Prepare the VMR\n\nif [[ ! -d \"$vmr_dir\" ]]; then\n  highlight \"Cloning 'dotnet/dotnet' into $vmr_dir..\"\n  git clone https://github.com/dotnet/dotnet \"$vmr_dir\"\n\n  if [[ -n \"$vmr_branch\" ]]; then\n    git -C \"$vmr_dir\" switch -c \"$vmr_branch\"\n  fi\nelse\n  if ! git -C \"$vmr_dir\" diff --quiet; then\n    fail \"There are changes in the working tree of $vmr_dir. Please commit or stash your changes\"\n    exit 1\n  fi\n\n  if [[ -n \"$vmr_branch\" ]]; then\n    highlight \"Preparing $vmr_dir\"\n    git -C \"$vmr_dir\" checkout \"$vmr_branch\"\n    git -C \"$vmr_dir\" pull\n  fi\nfi\n\nset -e\n\n# Prepare darc\n\nhighlight 'Installing .NET, preparing the tooling..'\nsource \"./eng/common/tools.sh\"\nInitializeDotNetCli true\nGetDarc\ndotnetDir=$( cd ./.dotnet/; pwd -P )\ndotnet=$dotnetDir/dotnet\n\nhighlight \"Starting the synchronization of VMR..\"\nset +e\n\nif [[ -n \"$additional_remotes\" ]]; then\n  additional_remotes=\"--additional-remotes $additional_remotes\"\nfi\n\nif [[ -n \"$azdev_pat\" ]]; then\n  azdev_pat=\"--azdev-pat $azdev_pat\"\nfi\n\nci_arg=''\nif [[ \"$ci\" == \"true\" ]]; then\n  ci_arg=\"--ci\"\nfi\n\n# Synchronize the VMR\n\nversion_details_path=$(cd \"$scriptroot/..\"; pwd -P)/Version.Details.xml\nrepo_name=$(grep -m 1 '<Source ' \"$version_details_path\" | sed -n 's/.*Mapping=\"\\([^\"]*\\)\".*/\\1/p')\nif [[ -z \"$repo_name\" ]]; then\n  fail \"Failed to resolve repo mapping from $version_details_path\"\n  exit 1\nfi\n\nexport DOTNET_ROOT=\"$dotnetDir\"\n\n\"$darc_tool\" vmr forwardflow \\\n  --tmp \"$tmp_dir\"             \\\n  $azdev_pat                   \\\n  --$verbosity                 \\\n  $ci_arg                      \\\n  $additional_remotes          \\\n  \"$vmr_dir\"\n\nif [[ $? == 0 ]]; then\n  highlight \"Synchronization succeeded\"\nelse\n  highlight \"Failed to flow code into the local VMR. Falling back to resetting the VMR to match repo contents...\"\n  git -C \"$vmr_dir\" reset --hard\n\n  \"$darc_tool\" vmr reset \\\n    \"$repo_name:HEAD\"                              \\\n    --vmr \"$vmr_dir\"                               \\\n    --tmp \"$tmp_dir\"                               \\\n    --additional-remotes \"$repo_name:$repo_root\"\n\n  if [[ $? == 0 ]]; then\n    highlight \"Successfully reset the VMR using 'darc vmr reset'\"\n  else\n    fail \"Synchronization of repo to VMR failed!\"\n    fail \"'$vmr_dir' is left in its last state (re-run of this script will reset it).\"\n    fail \"Please inspect the logs which contain path to the failing patch file (use --debug to get all the details).\"\n    fail \"Once you make changes to the conflicting VMR patch, commit it locally and re-run this script.\"\n    exit 1\n  fi\nfi\n"
  },
  {
    "path": "eng/sdl-tsa-vars.config",
    "content": "-SourceToolsList @(\"policheck\",\"credscan\")\n-ArtifactToolsList @(\"binskim\")\n-TsaInstanceURL https://devdiv.visualstudio.com/\n-TsaProjectName DEVDIV\n-TsaNotificationEmail dotnetrp@microsoft.com\n-TsaCodebaseAdmin REDMOND\\karelz\n-TsaBugAreaPath \"DevDiv\\ASP.NET Core\\YARP\"\n-TsaIterationPath DevDiv\n-TsaRepositoryName ReverseProxy\n-TsaCodebaseName ReverseProxy\n-TsaOnboard $True\n-TsaPublish $True\n-PoliCheckAdditionalRunConfigParams @(\"UserExclusionPath < $(Build.SourcesDirectory)/s/eng/PoliCheckExclusions.xml\")\n"
  },
  {
    "path": "eng/yarpapppack/Common.projitems",
    "content": "<Project>\n  <Import Sdk=\"Microsoft.NET.Sdk\" Project=\"Sdk.props\" />\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <IsPackable>true</IsPackable>\n    <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>\n    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>\n    <IncludeBuildOutput>false</IncludeBuildOutput>\n    <PackageOutputPath Condition=\" '$(PackageOutputPath)' == '' \">$(ArtifactsShippingPackagesDir)</PackageOutputPath>\n    <YarpAppArtifactsOutputDir>$([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'YarpAppArtifacts', '$(Configuration)'))</YarpAppArtifactsOutputDir>\n\n  </PropertyGroup>\n\n  <PropertyGroup>\n    <PackageId>Yarp.Application.$(YarpAppRuntime)</PackageId>\n    <Description>Reverse proxy</Description>\n  </PropertyGroup>\n\n  <Import Sdk=\"Microsoft.NET.Sdk\" Project=\"Sdk.targets\" />\n\n  <Target Name=\"Build\" />\n\n  <PropertyGroup>\n      <YarpArchiveDirectory>$([MSBuild]::NormalizeDirectory('$(YarpAppArtifactsOutputDir)', '$(YarpAppRuntime)'))</YarpArchiveDirectory>\n      <YarpArchiveBaseName>$(YarpArchiveDirectory)reverse-proxy-$(YarpAppRuntime)</YarpArchiveBaseName>\n      <YarpArchiveFileName>$(YarpArchiveBaseName).tar.gz</YarpArchiveFileName>\n      <YarpArchiveSha512Name>$(YarpArchiveFileName).sha512</YarpArchiveSha512Name>\n      <DirectoryToArchive>$([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'Yarp.Application', '$(Configuration)', '$(TargetFramework)', '$(YarpAppRuntime)/', 'publish'))</DirectoryToArchive>\n  </PropertyGroup>\n\n  <Target Name=\"BeforeBuild\" BeforeTargets=\"Build\">\n    <MSBuild Projects=\"../../src/Application/Yarp.Application.csproj\" Targets=\"publish\" Properties=\"Configuration=$(Configuration);Platform=$(Platform);TargetFramework=$(TargetFramework);RuntimeIdentifier=$(YarpAppRuntime)\" />\n\n    <!-- After publishing the project, we ensure that the published assets get packed in the nuspec. -->\n    <ItemGroup>\n      <_PublishItems Include=\"$(ArtifactsBinDir)/Yarp.Application/$(Configuration)/$(TargetFramework)/$(YarpAppRuntime)/publish/**/*\" />\n      <None Include=\"@(_PublishItems)\" Pack=\"true\" PackagePath=\"tools/\" />\n      <GenerateChecksumItems Include=\"$(YarpArchiveFileName)\">\n        <DestinationPath>$(YarpArchiveSha512Name)</DestinationPath>\n      </GenerateChecksumItems>\n    </ItemGroup>\n\n    <MakeDir Directories=\"$(YarpArchiveDirectory)\" />\n    <Exec Command=\"tar -cvzf $(YarpArchiveFileName) .\"\n          IgnoreExitCode=\"false\"\n          WorkingDirectory=\"$(DirectoryToArchive)\" />\n\n    <!-- Throw an error if _PublishItems is empty. -->\n    <Error Condition=\"'@(_PublishItems)' == ''\" Text=\"No files were found to pack. Ensure that the project being packed has a publish target defined.\" />\n  </Target>\n\n</Project>"
  },
  {
    "path": "eng/yarpapppack/yarpapppack-linux-arm64.csproj",
    "content": "<Project>\n\n  <PropertyGroup>\n    <YarpAppRuntime>linux-arm64</YarpAppRuntime>\n    <YarpAppPlatformType>Unix</YarpAppPlatformType>\n  </PropertyGroup>\n\n  <Import Project=\"Common.projitems\" />\n\n</Project>"
  },
  {
    "path": "eng/yarpapppack/yarpapppack-linux-x64.csproj",
    "content": "<Project>\n\n  <PropertyGroup>\n    <YarpAppRuntime>linux-x64</YarpAppRuntime>\n    <YarpAppPlatformType>Unix</YarpAppPlatformType>\n  </PropertyGroup>\n\n  <Import Project=\"Common.projitems\" />\n\n</Project>"
  },
  {
    "path": "es-metadata.yml",
    "content": "schemaVersion: 0.0.1\nisProduction: true\naccountableOwners:\n  service: b317f6f4-0741-4d30-9f8c-9f0e61554b81\nrouting:\n  defaultAreaPath:\n    org: devdiv\n    path: DevDiv\\NET Libraries\\Networking\n"
  },
  {
    "path": "global.json",
    "content": "{\n  \"sdk\": {\n    \"version\": \"11.0.100-preview.1.26104.118\"\n  },\n  \"tools\": {\n    \"dotnet\": \"11.0.100-preview.1.26104.118\",\n    \"runtimes\": {\n      \"dotnet\": [\n        \"8.0.13\",\n        \"9.0.2\"\n      ],\n      \"aspnetcore\": [\n        \"8.0.13\",\n        \"9.0.2\"\n      ]\n    }\n  },\n  \"test\": {\n    \"runner\": \"Microsoft.Testing.Platform\"\n  },\n  \"msbuild-sdks\": {\n    \"Microsoft.DotNet.Arcade.Sdk\": \"11.0.0-beta.26122.1\",\n    \"Microsoft.DotNet.Helix.Sdk\": \"11.0.0-beta.26122.1\"\n  }\n}\n"
  },
  {
    "path": "pack.cmd",
    "content": "@echo off\npowershell -ExecutionPolicy ByPass -NoProfile -command \"& \"\"\"%~dp0eng\\common\\Build.ps1\"\"\" -restore -build -pack %*\"\n"
  },
  {
    "path": "pack.sh",
    "content": "#!/usr/bin/env bash\n\nsource=\"${BASH_SOURCE[0]}\"\n\n# resolve $SOURCE until the file is no longer a symlink\nwhile [[ -h $source ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\n\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\"$scriptroot/eng/common/build.sh\" --build --restore --pack $@\n"
  },
  {
    "path": "restore.cmd",
    "content": "@echo off\npowershell -ExecutionPolicy ByPass -NoProfile -command \"& \"\"\"%~dp0eng\\common\\Build.ps1\"\"\" -restore %*\""
  },
  {
    "path": "restore.sh",
    "content": "#!/usr/bin/env bash\n\nsource=\"${BASH_SOURCE[0]}\"\n\n# resolve $SOURCE until the file is no longer a symlink\nwhile [[ -h $source ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\n\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\"$scriptroot/eng/common/build.sh\" --restore $@"
  },
  {
    "path": "samples/BasicYarpSample/BasicYarpSample.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/BasicYarpSample/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddControllers();\n// Add the reverse proxy capability to the server\nbuilder.Services.AddReverseProxy()\n    // Initialize the reverse proxy from the \"ReverseProxy\" section of configuration\n    .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"));\n\nvar app = builder.Build();\n\n// Register the reverse proxy routes\napp.MapReverseProxy();\n\napp.Run();\n"
  },
  {
    "path": "samples/BasicYarpSample/Properties/launchSettings.json",
    "content": "﻿{\n  \"$schema\": \"http://json.schemastore.org/launchsettings.json\",\n  \"profiles\": {\n    \"YARP Proxy Sample\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"launchUrl\": \"\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/BasicYarpSample/README.md",
    "content": "# Basic YARP Sample\n\nThis sample shows how to consume the YARP Library to produce a simple reverse proxy server. \nThe proxy server is implemented as a plugin component for ASP.NET Core applications. ASP.NET Core servers like Kestrel provide the front end for the proxy by listening for http requests and then passing them to the proxy for paths that the proxy has registered. The proxy handles the requests by:\n- Mapping the request URL path to a route in proxy configuration.\n- Routes are mapped to clusters which are a collection of destination endpoints.\n- The destinations are filtered based on health status, and session affinity (not used in this sample).\n- From the remaining destinations, one is selected using a load balancing algorithm.\n- The request is proxied to that destination.\n\nThis sample reads its configuration from the [appsettings.json](appsettings.json) file which defines 2 routes and clusters:\n\n- *AnExample* - this route has a path of *{\\*\\*catch-all}* which means that it will match any path, unless there is another more specific route.\n   It routes to a cluster named *example* which has a single destination of http://example.com\n- *route2* - this route matches a path of */something/{\\*any}* which means that it will match any path that begins with \"/something/\". \n    As its a more specific route, it will match before the route above, even though it is listed second. \n    This routes to a cluster named *cluster2* with 2 destinations. \n    It will load balance between those 2 destinations using a Power of two choices algorithm. \n    That algorithm is best with more than 2 choices, but shows how to specify an algorithm in config.\n\n**Note:** The destination addresses used in the sample are using DNS names rather than IP addresses, this is so that the sample can be run and used without further changes. In a typical deployment, the destination servers should be specified with protocol, IP & ports, such as \"https://123.4.5.6:7890\"\n\nThe proxy will listen to HTTP requests on port 5000, and HTTPS on port 5001. These are changeable via the URLs property in config, and can be limited to just one protocol if required.\n\n## Files\n- [BasicYarpSample.csproj](BasicYarpSample.csproj) - A C# project file (conceptually similar to a make file) that tells it to target the .NET 8 runtime, and to reference the proxy library from [nuget](https://www.nuget.org/packages/Yarp.ReverseProxy/) (.NET's package manager).\n- [Program.cs](Program.cs) - Provides the main entrypoint for .NET which uses a WebApplication to initialize the server which listens for http requests. This is also used to configure and control how http requests are handled by the server. In this sample, it does the bare minimum of:\n  - Adding proxy functionality to the services collection.\n  - Specifying that the proxy configuration will come from the config file (alternatively it could be specified via code).\n  - Telling ASP.NET to use its routing service, to register the routes from YARP into its routing table, and use YARP to handle those requests.\n- [appsettings.json](appsettings.json) - The configuration file for the .NET app, including sections for Kestrel, logging and the YARP proxy configuration. \n- [Properties/launchSettings.json](Properties/launchSettings.json) - A configuration file used by Visual Studio to tell it how to start the app when debugging.\n\n## Getting started\n\n### Command line\n\n- Download and install the .NET SDK (free) from https://dotnet.microsoft.com/download if not already installed. Versions are available for Windows, Linux and MacOS.\n- Clone or extract a zip of the sample files.\n- Use ```dotnet run``` either within the sample folder or passing in the path to the .csproj file to start the server.\n- File change notification is used for the appsettings.json file so changes can be made on the fly.\n\n### Visual Studio Code\n- Download and install Visual Studio Code (free) from https://code.visualstudio.com/ - versions are available for Windows, Linux and MacOS.\n- Download and install the .NET SDK from https://dotnet.microsoft.com/download if not already installed. Versions are available for Windows, Linux and MacOS.\n- Open the folder for the sample in VS Code (File->Open Folder).\n- Press F5 to debug, or Ctrl + F5 to run the sample without debugging.\n\n### Visual Studio\n\n- Download and install Visual Studio from https://visualstudio.microsoft.com/ - versions are available for Windows and MacOS, including a free community edition.\n- Open the project file.\n- Press F5 to debug, or Ctrl + F5 to run the sample without debugging.\n\n## Things to try\n- Change the ports the proxy listens on using the URLs property in configuration or on the command line.\n- Change the routes and destinations used by the proxy.\n- A web server sample is available in the [SampleServer](../SampleServer) folder. It will output the request headers as part of the response body so they can be examined with a browser.\n    - The URLs the server listens to can be changed on the command line, so that multiple instances can be run. \n     eg ```dotnet run --project ../SampleServer --Urls \"http://localhost:10000;https://localhost:10010\"```\n"
  },
  {
    "path": "samples/BasicYarpSample/appsettings.json",
    "content": "{\n  // Base URLs the server listens on, must be configured independently of the routes below.\n  // Can also be configured via Kestrel/Endpoints, see https://docs.microsoft.com/aspnet/core/fundamentals/servers/kestrel/endpoints\n  \"Urls\": \"http://localhost:5000;https://localhost:5001\",\n\n  //Sets the Logging level for ASP.NET\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      // Uncomment to hide diagnostic messages from runtime and proxy\n      // \"Microsoft\": \"Warning\",\n      // \"Yarp\" : \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n\n  \"ReverseProxy\": {\n    // Routes tell the proxy which requests to forward\n    \"Routes\": {\n      \"minimumroute\": {\n        // Matches anything and routes it to www.example.com\n        \"ClusterId\": \"minimumcluster\",\n        \"Match\": {\n          \"Path\": \"{**catch-all}\"\n        }\n      },\n      \"route2\": {\n        // matches /something/* and routes to 2 external addresses\n        \"ClusterId\": \"cluster2\",\n        \"Match\": {\n          \"Path\": \"/something/{*any}\"\n        }\n      }\n    },\n    // Clusters tell the proxy where and how to forward requests\n    \"Clusters\": {\n      \"minimumcluster\": {\n        \"Destinations\": {\n          \"example.com\": {\n            \"Address\": \"http://www.example.com/\"\n          }\n        }\n      },\n      \"cluster2\": {\n        \"Destinations\": {\n          \"first_destination\": {\n            \"Address\": \"https://contoso.com\"\n          },\n          \"another_destination\": {\n            \"Address\": \"https://bing.com\"\n          }\n        },\n        \"LoadBalancingPolicy\": \"PowerOfTwoChoices\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Directory.Build.props",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project>\n  <!-- Recurse up. -->\n  <Import Project=\"$(MSBuildThisFileDirectory)..\\Directory.Build.props\" />\n\n  <PropertyGroup>\n    <IsSampleProject>true</IsSampleProject>\n    <IsShipping>false</IsShipping>\n    <WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Combined/Dockerfile",
    "content": "#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.\n\nFROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base\nWORKDIR /app\nEXPOSE 80\nEXPOSE 443\n\nFROM mcr.microsoft.com/dotnet/sdk:8.0  AS publish\nWORKDIR /src\n# We need to install the SDK manually because we might target an unreleased SDK\nCOPY [\"global.json\", \"\"]\nRUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --jsonfile global.json\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT 1\n\n# Copy csproj files and other files needed for restoring (to build a nuget cache layer to speed up rebuilds)\nCOPY [\"samples/KubernetesIngress.Sample/Combined/Yarp.Kubernetes.IngressController.csproj\", \"samples/KubernetesIngress.Sample/Combined/\"]\nCOPY [\"src/ReverseProxy/Yarp.ReverseProxy.csproj\", \"src/ReverseProxy/\"]\nCOPY [\"src/Kubernetes.Controller/Yarp.Kubernetes.Controller.csproj\", \"src/Kubernetes.Controller/\"]\nCOPY [\"src/Directory.Build.props\", \"src/\"]\nCOPY [\"Directory.Build.*\", \"./\"]\nCOPY [\"TFMs.props\", \"\"]\nCOPY [\"NuGet.config\", \"\"]\nCOPY [\"eng/Versions.props\", \"eng/\"]\n\n# Build a cache layer with all of the nuget packages\nRUN /root/.dotnet/dotnet restore samples/KubernetesIngress.Sample/Combined/Yarp.Kubernetes.IngressController.csproj\n\n# Copy the remaining source files\nWORKDIR /src\nCOPY . .\n\nWORKDIR /src/samples/KubernetesIngress.Sample/Combined/\nRUN /root/.dotnet/dotnet publish -c Release --no-restore -o /app/publish -f net8.0\n\nFROM base AS final\nWORKDIR /app\nCOPY --from=publish /app/publish .\nENTRYPOINT [\"dotnet\", \"Yarp.Kubernetes.IngressController.dll\"]\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Combined/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Serilog;\nusing Serilog.Sinks.SystemConsole.Themes;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nusing var serilog = new LoggerConfiguration()\n    .MinimumLevel.Debug()\n    .Enrich.FromLogContext()\n    .WriteTo.Console(theme: AnsiConsoleTheme.Code)\n    .CreateLogger();\nbuilder.Logging.ClearProviders();\nbuilder.Logging.AddSerilog(serilog, dispose: false);\n\nbuilder.Configuration.AddJsonFile(\"/app/config/yarp.json\", optional: true);\nbuilder.WebHost.UseKubernetesReverseProxyCertificateSelector();\nbuilder.Services.AddKubernetesReverseProxy(builder.Configuration);\n\nvar app = builder.Build();\n\napp.MapReverseProxy();\n\napp.Run();\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Combined/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"Ingress\": {\n      \"commandName\": \"Project\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"applicationUrl\": \"https://localhost:5021;http://localhost:5020\"\n    },\n    \"Docker\": {\n      \"commandName\": \"Docker\",\n      \"launchUrl\": \"{Scheme}://{ServiceHost}:{ServicePort}\",\n      \"publishAllPorts\": true,\n      \"useSSL\": true\n    }\n  }\n}\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Combined/README.md",
    "content": "# Yarp Ingress Controller\n\nThis directory contains a sample ingress as well as the definition for the Kubernetes manifests for the ingress controller.\n\nThe sample ingress controller is a single deployable.\n\n## Building the Docker Image\n\nFrom the base directory for this repo (where the .slnx file is), run the command:\n\n```bash\ndocker build -t yarp-combined:latest -f ./samples/KubernetesIngress.Sample/Combined/Dockerfile .\n```\n\n## Deploying the Sample Ingress Controller\n\n1. Open the [ingress-controller.yaml](./ingress-controller.yaml) file\n2. Modify the container image to match the name used when building the image, e.g. change `<REGISTRY_NAME>/yarp-combined:<TAG>` to `yarp-combined:latest`\n3. From the root of this repo. run the command `kubectl apply -f ./samples/KubernetesIngress.Sample/Combined/ingress-controller.yaml`\n\nTo undeploy the ingress controller, run the command `kubectl delete -f ./samples/KubernetesIngress.Sample/Combined/ingress-controller.yaml`\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Combined/Yarp.Kubernetes.IngressController.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <UserSecretsId>78d1f3b4-abce-4c5a-b914-3321fab1f8d0</UserSecretsId>\n    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>\n    <IsPackable>$('System.TeamProject') != 'internal'</IsPackable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Serilog.Extensions.Logging\" Version=\"$(SerilogExtensionsLoggingVersion)\" />\n    <PackageReference Include=\"Serilog.Formatting.Compact\" Version=\"$(SerilogFormattingCompactVersion)\" />\n    <PackageReference Include=\"Serilog.Sinks.Console\" Version=\"$(SerilogSinksConsoleVersion)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\..\\src\\Kubernetes.Controller\\Yarp.Kubernetes.Controller.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Combined/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }  \n}\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Combined/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"Yarp\": {\n    \"ControllerClass\": \"microsoft.com/ingress-yarp\",\n    \"ServerCertificates\": false,\n    \"DefaultSslCertificate\": \"yarp/yarp-ingress-tls\"\n  }\n}\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Combined/ingress-controller.yaml",
    "content": "kind: Namespace\napiVersion: v1\nmetadata:\n  name: yarp\n---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: yarp-config\n  namespace: yarp\ndata:\n  yarp.json: |\n    {\n      \"Yarp\": {\n        \"ControllerClass\": \"microsoft.com/ingress-yarp\",\n        \"ServerCertificates\": false,\n        \"DefaultSslCertificate\": \"yarp/yarp-ingress-tls\",\n        \"ControllerServiceName\": \"ingress-yarp-controller\",\n        \"ControllerServiceNamespace\": \"yarp\"\n      }\n    }\n---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: yarp-serviceaccount\n  namespace: yarp\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: yarp-ingress-clusterrole\n  namespace: yarp\nrules:\n- apiGroups:\n  - \"\"\n  resources:\n  - endpoints\n  - nodes\n  - pods\n  - secrets\n  - namespaces\n  verbs:\n  - list\n  - watch\n- apiGroups:\n  - \"\"\n  resources:\n  - nodes\n  verbs:\n  - get\n- apiGroups:\n  - \"\"\n  resources:\n  - services\n  verbs:\n  - get\n  - list\n  - watch\n- apiGroups:\n  - \"\"\n  resources:\n  - services/status\n  verbs:\n  - get\n- apiGroups:\n  - networking.k8s.io\n  - extensions\n  - networking.internal.knative.dev\n  resources:\n  - ingresses\n  - ingressclasses\n  verbs:\n  - get\n  - list\n  - watch\n- apiGroups:\n  - networking.k8s.io\n  resources:\n  - events\n  verbs:\n  - create\n  - patch\n- apiGroups:\n  - networking.k8s.io\n  - extensions\n  - networking.internal.knative.dev\n  resources:\n  - ingresses/status\n  verbs:\n  - get\n  - update\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: yarp-ingress-clusterrole-nisa-binding\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: yarp-ingress-clusterrole\nsubjects:\n- kind: ServiceAccount\n  name: yarp-serviceaccount\n  namespace: yarp\n---\napiVersion: networking.k8s.io/v1\nkind: IngressClass\nmetadata:\n  name: yarp\n  annotations:\n    #ingressclass.kubernetes.io/is-default-class: \"true\"\nspec:\n  controller: microsoft.com/ingress-yarp\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: ingress-yarp-controller\n  namespace: yarp\nspec:\n  ports:\n  - name: proxy\n    port: 80\n    protocol: TCP\n    targetPort: 8000\n  - name: proxy-ssl\n    port: 443\n    protocol: TCP\n    targetPort: 8443\n  selector:\n    app: ingress-yarp-controller\n  type: LoadBalancer\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  labels:\n    app: ingress-yarp-controller\n  name: ingress-yarp\n  namespace: yarp\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: ingress-yarp-controller\n  template:\n    metadata:\n      labels:\n        app: ingress-yarp-controller\n    spec:\n      containers:\n      - name: yarp-controller\n        imagePullPolicy: IfNotPresent\n        image: <REGISTRY_NAME>/yarp-combined:<TAG>\n        ports:\n        - containerPort: 8000\n          name: proxy\n          protocol: TCP\n        - containerPort: 8443\n          name: proxy-ssl\n          protocol: TCP\n        env:\n        - name: ASPNETCORE_URLS\n          value: http://*:8000;https://*:8443\n        volumeMounts:\n          - name: config\n            readOnly: true\n            mountPath: /app/config\n      volumes:\n        - name: config\n          configMap:\n            name: yarp-config\n      serviceAccountName: yarp-serviceaccount\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Ingress/Dockerfile",
    "content": "#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.\n\nFROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base\nWORKDIR /app\nEXPOSE 80\nEXPOSE 443\n\nFROM mcr.microsoft.com/dotnet/sdk:8.0  AS publish\nWORKDIR /src\n# We need to install the SDK manually because we might target an unreleased SDK\nCOPY [\"global.json\", \"\"]\nRUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --jsonfile global.json\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT 1\n\n# Copy csproj files and other files needed for restoring (to build a nuget cache layer to speed up rebuilds)\nCOPY [\"samples/KubernetesIngress.Sample/Ingress/Yarp.Kubernetes.Ingress.csproj\", \"samples/KubernetesIngress.Sample/Ingress/\"]\nCOPY [\"src/ReverseProxy/Yarp.ReverseProxy.csproj\", \"src/ReverseProxy/\"]\nCOPY [\"src/Kubernetes.Controller/Yarp.Kubernetes.Controller.csproj\", \"src/Kubernetes.Controller/\"]\nCOPY [\"src/Directory.Build.props\", \"src/\"]\nCOPY [\"Directory.Build.*\", \"./\"]\nCOPY [\"TFMs.props\", \"\"]\nCOPY [\"NuGet.config\", \"\"]\nCOPY [\"eng/Versions.props\", \"eng/\"]\n\n# Build a cache layer with all of the nuget packages\nRUN /root/.dotnet/dotnet restore samples/KubernetesIngress.Sample/Ingress/Yarp.Kubernetes.Ingress.csproj\n\n# Copy the remaining source files\nWORKDIR /src\nCOPY . .\n\nWORKDIR /src/samples/KubernetesIngress.Sample/Ingress/\nRUN /root/.dotnet/dotnet publish -c Release --no-restore -o /app/publish -f net8.0\n\nFROM base AS final\nWORKDIR /app\nCOPY --from=publish /app/publish .\nENTRYPOINT [\"dotnet\", \"Yarp.Kubernetes.Ingress.dll\"]\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Ingress/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Serilog;\nusing Serilog.Sinks.SystemConsole.Themes;\nusing Yarp.Kubernetes.Protocol;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nusing var serilog = new LoggerConfiguration()\n    .MinimumLevel.Debug()\n    .Enrich.FromLogContext()\n    .WriteTo.Console(theme: AnsiConsoleTheme.Code)\n    .CreateLogger();\nbuilder.Logging.ClearProviders();\nbuilder.Logging.AddSerilog(serilog, dispose: false);\n\nvar services = builder.Services;\nservices.Configure<ReceiverOptions>(builder.Configuration.Bind);\nservices.AddHostedService<Receiver>();\nservices.AddReverseProxy()\n    .LoadFromMessages();\n\nvar app = builder.Build();\n\napp.MapReverseProxy();\n\napp.Run();\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Ingress/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"Ingress\": {\n      \"commandName\": \"Project\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"applicationUrl\": \"https://localhost:5021;http://localhost:5020\"\n    },\n    \"Docker\": {\n      \"commandName\": \"Docker\",\n      \"launchUrl\": \"{Scheme}://{ServiceHost}:{ServicePort}\",\n      \"publishAllPorts\": true,\n      \"useSSL\": true\n    }\n  }\n}\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Ingress/README.md",
    "content": "# Yarp Ingress Controller\n\nThis directory contains a sample ingress as well as the definition for the Kubernetes manifests for the ingress controller.\n\nThis sample requires two applications to be deployed:\n* An Ingress (this application)\n* A Kubernetes Ingress Monitor (a process listening for changes in k8s and dispatching the Yarp configuration to ingress instances)\n\nNOTE: Yarp Kubernetes can also be configured as a combined (single) deployable. See the combined [README.md](../Combined/README.md) for more information.\n\n## Building the Docker Images\n\nFrom the base directory for this repo (where the .slnx file is), run the commands:\n\n```bash\ndocker build -t yarp-monitor:latest -f ./samples/KubernetesIngress.Sample/Monitor/Dockerfile .\ndocker build -t yarp-ingress:latest -f ./samples/KubernetesIngress.Sample/Ingress/Dockerfile .\n```\n\n## Deploying the Sample Ingress Controller\n\n1. Open the [ingress-monitor.yaml](../Monitor/ingress-monitor.yaml) file\n1. Modify the container image to match the name used when building the image, e.g. change `<REGISTRY_NAME>/yarp-monitor:<TAG>` to `yarp-monitor:latest`\n1. Run the command `kubectl apply -f ./samples/KubernetesIngress.Sample/Monitor/ingress-monitor.yaml`\n1. Open the [ingress.yaml](./ingress.yaml) file\n1. Modify the container image to match the name used when building the image, e.g. change `<REGISTRY_NAME>/yarp-ingress:<TAG>` to `yarp-ingress:latest`\n1. Run the command `kubectl apply -f ./samples/KubernetesIngress.Sample/Ingress/ingress.yaml`\n\nTo undeploy the ingress, run the commands\n```bash\nkubectl delete -f ./samples/KubernetesIngress.Sample/Ingress/ingress.yaml\nkubectl delete -f ./samples/KubernetesIngress.Sample/Monitor/ingress-monitor.yaml\n```\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Ingress/Yarp.Kubernetes.Ingress.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <UserSecretsId>b2dc6cd7-acbb-4d65-ad19-74771ff3c80f</UserSecretsId>\n    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>\n    <IsPackable>$('System.TeamProject') != 'internal'</IsPackable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Serilog.Extensions.Logging\" Version=\"$(SerilogExtensionsLoggingVersion)\" />\n    <PackageReference Include=\"Serilog.Formatting.Compact\" Version=\"$(SerilogFormattingCompactVersion)\" />\n    <PackageReference Include=\"Serilog.Sinks.Console\" Version=\"$(SerilogSinksConsoleVersion)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\..\\src\\Kubernetes.Controller\\Yarp.Kubernetes.Controller.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Ingress/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }  \n}\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Ingress/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"ControllerUrl\": \"http://yarp-controller.yarp.svc.cluster.local:8000/api/dispatch\"\n}\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Ingress/ingress.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: yarp-proxy\n  namespace: yarp\nspec:\n  ports:\n  - name: proxy\n    port: 80\n    protocol: TCP\n    targetPort: 8000\n  - name: proxy-ssl\n    port: 443\n    protocol: TCP\n    targetPort: 8443\n  selector:\n    app: ingress-yarp\n  type: LoadBalancer\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  labels:\n    app: ingress-yarp\n  name: yarp-proxy\n  namespace: yarp\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: ingress-yarp\n  template:\n    metadata:\n      labels:\n        app: ingress-yarp\n    spec:\n      containers:\n      - name: yarp-proxy\n        imagePullPolicy: IfNotPresent\n        image: <REGISTRY_NAME>/yarp-ingress:<TAG>\n        ports:\n        - containerPort: 8000\n          name: proxy\n          protocol: TCP\n        - containerPort: 8443\n          name: proxy-ssl\n          protocol: TCP\n        env:\n        - name: ASPNETCORE_URLS\n          value: http://*:8000\n---\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Monitor/Dockerfile",
    "content": "#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.\n\nFROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base\nWORKDIR /app\nEXPOSE 80\nEXPOSE 443\n\nFROM mcr.microsoft.com/dotnet/sdk:8.0  AS publish\nWORKDIR /src\n# We need to install the SDK manually because we might target an unreleased SDK\nCOPY [\"global.json\", \"\"]\nRUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --jsonfile global.json\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT 1\n\n# Copy csproj files and other files needed for restoring (to build a nuget cache layer to speed up rebuilds)\nCOPY [\"samples/KubernetesIngress.Sample/Monitor/Yarp.Kubernetes.Monitor.csproj\", \"samples/KubernetesIngress.Sample/Monitor/\"]\nCOPY [\"src/ReverseProxy/Yarp.ReverseProxy.csproj\", \"src/ReverseProxy/\"]\nCOPY [\"src/Kubernetes.Controller/Yarp.Kubernetes.Controller.csproj\", \"src/Kubernetes.Controller/\"]\nCOPY [\"src/Directory.Build.props\", \"src/\"]\nCOPY [\"Directory.Build.*\", \"./\"]\nCOPY [\"TFMs.props\", \"\"]\nCOPY [\"NuGet.config\", \"\"]\nCOPY [\"eng/Versions.props\", \"eng/\"]\n\n# Build a cache layer with all of the nuget packages\nRUN /root/.dotnet/dotnet restore samples/KubernetesIngress.Sample/Monitor/Yarp.Kubernetes.Monitor.csproj\n\n# Copy the remaining source files\nWORKDIR /src\nCOPY . .\n\nWORKDIR /src/samples/KubernetesIngress.Sample/Monitor/\nRUN /root/.dotnet/dotnet publish -c Release --no-restore -o /app/publish -f net8.0\n\nFROM base AS final\nWORKDIR /app\nCOPY --from=publish /app/publish .\nENTRYPOINT [\"dotnet\", \"Yarp.Kubernetes.Monitor.dll\"]\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Monitor/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Serilog;\nusing Serilog.Sinks.SystemConsole.Themes;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nusing var serilog = new LoggerConfiguration()\n    .MinimumLevel.Debug()\n    .Enrich.FromLogContext()\n    .WriteTo.Console(theme: AnsiConsoleTheme.Code)\n    .CreateLogger();\nbuilder.Logging.ClearProviders();\nbuilder.Logging.AddSerilog(serilog, dispose: false);\n\nbuilder.Configuration.AddJsonFile(\"/app/config/yarp.json\", optional: true);\n\nbuilder.Services.AddKubernetesIngressMonitor(builder.Configuration);\n\n// Add ASP.NET Core controller support\nbuilder.Services.AddControllers()\n    .AddKubernetesDispatchController();\n\nvar app = builder.Build();\n\napp.MapControllerRoute(\n    name: \"default\",\n    pattern: \"{controller=Home}/{action=Index}/{id?}\");\n\napp.Run();\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Monitor/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"Ingress\": {\n      \"commandName\": \"Project\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"applicationUrl\": \"https://localhost:5021;http://localhost:5020\"\n    },\n    \"Docker\": {\n      \"commandName\": \"Docker\",\n      \"launchUrl\": \"{Scheme}://{ServiceHost}:{ServicePort}\",\n      \"publishAllPorts\": true,\n      \"useSSL\": true\n    }\n  }\n}\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Monitor/README.md",
    "content": "# Yarp Ingress Monitor\n\nThis directory contains a sample ingress monitor as well as the definition for the Kubernetes manifests for the ingress controller.\n\nThis monitor works in conjunction with the Yarp Ingress. See [README.md](../Ingress/README.md) file for build and deployment instructions.\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Monitor/Yarp.Kubernetes.Monitor.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <UserSecretsId>42f98116-26c4-4115-b6af-c5dec1f88c84</UserSecretsId>\n    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>\n    <IsPackable>$('System.TeamProject') != 'internal'</IsPackable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Serilog.Extensions.Logging\" Version=\"$(SerilogExtensionsLoggingVersion)\" />\n    <PackageReference Include=\"Serilog.Formatting.Compact\" Version=\"$(SerilogFormattingCompactVersion)\" />\n    <PackageReference Include=\"Serilog.Sinks.Console\" Version=\"$(SerilogSinksConsoleVersion)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\..\\src\\Kubernetes.Controller\\Yarp.Kubernetes.Controller.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Monitor/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }  \n}\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Monitor/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"Yarp\": {\n    \"ControllerClass\": \"microsoft.com/ingress-yarp\"\n  }\n}\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/Monitor/ingress-monitor.yaml",
    "content": "kind: Namespace\napiVersion: v1\nmetadata:\n  name: yarp\n---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: yarp-config\n  namespace: yarp\ndata:\n  yarp.json: |\n    {\n      \"Yarp\": {\n        \"ControllerClass\": \"microsoft.com/ingress-yarp\",\n        \"ControllerServiceName\": \"yarp-controller\",\n        \"ControllerServiceNamespace\": \"yarp\"\n      }\n    }\n---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: yarp-serviceaccount\n  namespace: yarp\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: yarp-ingress-clusterrole\n  namespace: yarp\nrules:\n- apiGroups:\n  - \"\"\n  resources:\n  - endpoints\n  - nodes\n  - pods\n  - secrets\n  - namespaces\n  verbs:\n  - list\n  - watch\n- apiGroups:\n  - \"\"\n  resources:\n  - nodes\n  verbs:\n  - get\n- apiGroups:\n  - \"\"\n  resources:\n  - services\n  verbs:\n  - get\n  - list\n  - watch\n- apiGroups:\n  - \"\"\n  resources:\n  - services/status\n  verbs:\n  - get\n- apiGroups:\n  - networking.k8s.io\n  - extensions\n  - networking.internal.knative.dev\n  resources:\n  - ingresses\n  - ingressclasses\n  verbs:\n  - get\n  - list\n  - watch\n- apiGroups:\n  - networking.k8s.io\n  resources:\n  - events\n  verbs:\n  - create\n  - patch\n- apiGroups:\n  - networking.k8s.io\n  - extensions\n  - networking.internal.knative.dev\n  resources:\n  - ingresses/status\n  verbs:\n  - get\n  - update\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: yarp-ingress-clusterrole-nisa-binding\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: yarp-ingress-clusterrole\nsubjects:\n- kind: ServiceAccount\n  name: yarp-serviceaccount\n  namespace: yarp\n---\napiVersion: networking.k8s.io/v1\nkind: IngressClass\nmetadata:\n  name: yarp\n  annotations:\n    #ingressclass.kubernetes.io/is-default-class: true\nspec:\n  controller: microsoft.com/ingress-yarp\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: yarp-controller\n  namespace: yarp\nspec:\n  ports:\n  - name: api\n    port: 8000\n    protocol: TCP\n    targetPort: 8000\n  selector:\n    app: ingress-yarp-controller\n  type: ClusterIP\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  labels:\n    app: ingress-yarp-controller\n  name: yarp-controller\n  namespace: yarp\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: ingress-yarp-controller\n  template:\n    metadata:\n      labels:\n        app: ingress-yarp-controller\n    spec:\n      containers:\n      - name: yarp-controller\n        imagePullPolicy: IfNotPresent\n        image: <REGISTRY_NAME>/yarp-monitor:<TAG>\n        ports:\n        - containerPort: 8000\n          name: api\n          protocol: TCP\n        env:\n        - name: ASPNETCORE_URLS\n          value: http://*:8000\n        volumeMounts:\n          - name: config\n            readOnly: true\n            mountPath: /app/config\n      volumes:\n        - name: config\n          configMap:\n            name: yarp-config\n      serviceAccountName: yarp-serviceaccount\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/README.md",
    "content": "# Kubernetes Ingress Samples\n\nThese samples show how to deploy the YARP Kubernetes Ingress Controller into a Kubernetes cluster.\n\nAn [Ingress Controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/) monitors for [Ingress resources](https://kubernetes.io/docs/concepts/services-networking/ingress/) and routes traffic to services.\n\nThere are three parts to these samples:\n- [Backend](./backend/README.md)\n- [Combined Ingress Controller](./Combined/README.md)\n- [Separate Ingress Controller and Monitor](./Ingress/README.md)\n\nThe \"Backend\" is a Dockerized ASP.NET Core application that returns dummy information in web requests. This project contains Kubernetes manifest files for deploying the application and an Ingress resource into a cluster.\n\nThe Ingress Controller can be deployed either as:\n- a single deployable (see the Combined sample), or\n- as two separate deployables where one (the \"monitor\") watches the Ingress resources and the other (the \"ingress\") retrieves the YARP configuration from the \"monitor\" and handles the routing\n\nBoth of these controllers utilize the `Yarp.Kubernetes.Controller` project.\n\n## Ingress Resource\n\nThe `Yarp.Kubernetes.Controller` project currently supports the following Ingress features:\n\n- [Ingress rules](https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-rules) for host name and path-based routing to backend services\n- [Ingress class](https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class) for multiple, independent instances of the controller (cluster scope only)\n- [Default ingress class](https://kubernetes.io/docs/concepts/services-networking/ingress/#default-ingress-class) for simplifying Ingress resource configuration\n\nThe `Yarp.Kubernetes.Controller` project does not support:\n- The [TLS specification](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls) for Ingress resources (coming soon), though you could combined with the LetsEncrypt.Sample.\n- The [deprecated annotation](https://kubernetes.io/docs/concepts/services-networking/ingress/#deprecated-annotation) for ingress resources.\n\n### Annotations\n\nThe `Yarp.Kubernetes.Controller` project supports a number of **optional** annotations on Ingress resources for functionality provided by YARP.\n\nThese annotations would be specified like this:\n```yaml\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: minimal-ingress\n  namespace: default\n  annotations:\n    yarp.ingress.kubernetes.io/authorization-policy: authzpolicy\n    yarp.ingress.kubernetes.io/rate-limiter-policy: ratelimiterpolicy\n    yarp.ingress.kubernetes.io/output-cache-policy: outputcachepolicy\n    yarp.ingress.kubernetes.io/transforms: |\n      - PathRemovePrefix: \"/apis\"\n    yarp.ingress.kubernetes.io/route-headers: |\n      - Name: the-header-key\n        Values:\n        - the-header-value\n        Mode: Contains\n        IsCaseSensitive: false\n      - Name: another-header-key\n        Values:\n        - another-header-value\n        Mode: Contains\n        IsCaseSensitive: false\n    yarp.ingress.kubernetes.io/route-queryparameters: |\n      - Name: the-queryparameters-key\n        Values:\n        - the-queryparameters-value\n        Mode: Contains\n        IsCaseSensitive: false\n      - Name: another-queryparameters-key\n        Values:\n        - another-queryparameters-value\n        Mode: Contains\n        IsCaseSensitive: false\n    yarp.ingress.kubernetes.io/route-methods: |\n      - GET\n      - POST\nspec:\n  rules:\n    - http:\n        paths:\n          - path: /foo\n            pathType: Prefix\n            backend:\n              service:\n                name: frontend\n                port:\n                  number: 80\n```\n\nThe table below lists the available annotations.\n\n|Annotation|Data Type|\n|---|---|\n|yarp.ingress.kubernetes.io/authorization-policy|string|\n|yarp.ingress.kubernetes.io/rate-limiter-policy|string|\n|yarp.ingress.kubernetes.io/output-cache-policy|string|\n|yarp.ingress.kubernetes.io/backend-protocol|string|\n|yarp.ingress.kubernetes.io/cors-policy|string|\n|yarp.ingress.kubernetes.io/health-check|[ActivateHealthCheckConfig](https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.activehealthcheckconfig)|\n|yarp.ingress.kubernetes.io/http-client|[HttpClientConfig](https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.httpclientconfig)|\n|yarp.ingress.kubernetes.io/http-request|[ForwarderRequestConfig](https://learn.microsoft.com/en-us/dotnet/api/yarp.reverseproxy.forwarder.forwarderrequestconfig)|\n|yarp.ingress.kubernetes.io/load-balancing|string|\n|yarp.ingress.kubernetes.io/route-metadata|Dictionary&lt;string, string&gt;|\n|yarp.ingress.kubernetes.io/session-affinity|[SessionAffinityConfig](https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.sessionaffinityconfig)|\n|yarp.ingress.kubernetes.io/transforms|List&lt;Dictionary&lt;string, string&gt;&gt;|\n|yarp.ingress.kubernetes.io/route-headers|List&lt;[RouteHeader](https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routeheader)&gt;|\n|yarp.ingress.kubernetes.io/route-queryparameters|List&lt;[RouteQueryParameter](https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routequeryparameter)&gt;|\n|yarp.ingress.kubernetes.io/route-order|int|\n|yarp.ingress.kubernetes.io/route-methods|List&lt;string&gt;|\n\n#### Authorization Policy\n\nSee https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/authn-authz for a list of available policies, or how to add your own custom policies.\n\n`yarp.ingress.kubernetes.io/authorization-policy: anonymous`\n\n#### RateLimiter Policy\n\nSee https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/rate-limiting for a list of available policies, or how to add your own custom policies.\n\n`yarp.ingress.kubernetes.io/rate-limiter-policy: mypolicy`\n\n#### Output Cache Policy\n\n`yarp.ingress.kubernetes.io/output-cache-policy: mycachepolicy`\n\n#### Backend Protocol\n\nSpecifies the protocol of the backend service. Defaults to http.\n\n`yarp.ingress.kubernetes.io/backend-protocol: \"https\"`\n\n#### CORS Policy\n\nSee https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/cors for the list of available policies, or how to add your own custom policies.\n\n`yarp.ingress.kubernetes.io/cors-policy: mypolicy`\n\n#### Health Check\n\nProactively monitors destination health by sending periodic probing requests to designated health endpoints and analyzing responses.\n\nSee https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/dests-health-checks.\n\n```yaml\nyarp.ingress.kubernetes.io/health-check |\n  Active:\n  Enabled: true\n  Interval: '00:00:10'\n  Timeout: '00:00:10'\n  Policy: ConsecutiveFailures\n  Path: \"/api/health\"\n```\n\n#### HTTP Client\n\nConfigures the HTTP client that will be used for the destination service.\n\nSee https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/http-client-config.\n\n```yaml\nyarp.ingress.kubernetes.io/http-client: |\n  SslProtocols: Ssl3\n  MaxConnectionsPerServer: 2\n  DangerousAcceptAnyServerCertificate: true\n```\n\n#### HTTP Request\n\nConfigures the HTTP request that will be sent to the destination service.\n\nSee https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.forwarder.forwarderrequestconfig.\n\n```yaml\nyarp.ingress.kubernetes.io/http-request: |\n  ActivityTimeout: '00:01:00'\n  Version: '2.0'\n  VersionPolicy: 'RequestVersionExact'\n  AllowResponseBuffering: false\n```\n\n#### Load Balancing\n\nSee https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/load-balancing for a list of the available options.\n\n`yarp.ingress.kubernetes.io/load-balancing: Random`\n\n#### Route Metadata\n\nSee https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routeconfig.metadata#yarp-reverseproxy-configuration-routeconfig-metadata.\n\n```yaml\nyarp.ingress.kubernetes.io/route-metadata: |\n  Custom: \"orange\"\n  Tenant: \"12345\"\n```\n\n#### Session Affinity\n\nSee https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/session-affinity.\n\n```yaml\nyarp.ingress.kubernetes.io/session-affinity: |\n  Enabled: true\n  Policy: Cookie\n  FailurePolicy: Redistribute\n  AffinityKeyName: Key1\n  Cookie:\n    Domain: localhost\n    Expiration:\n    HttpOnly: true\n    IsEssential: true\n    MaxAge:\n    Path: mypath\n    SameSite: Strict\n    SecurePolicy: Always\n```\n\n#### Transforms\n\nTransforms use the YAML key-value pairs as per the YARP [Request Transforms](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/transforms#request-transforms)\n\n```yaml\nyarp.ingress.kubernetes.io/transforms: |\n  - PathPrefix: \"/apis\"\n  - RequestHeader: header1\n    Append: bar\n```\n\n#### Route Headers\n\n`route-headers` are the YAML representation of YARP [Header Based Routing](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/header-routing).\n\nSee https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routeheader.\n\n```yaml\nyarp.ingress.kubernetes.io/route-headers: |\n  - Name: the-header-key\n    Values:\n    - the-header-value\n    Mode: Contains\n    IsCaseSensitive: false\n  - Name: another-header-key\n    Values:\n    - another-header-value\n    Mode: Contains\n    IsCaseSensitive: false\n```\n\n#### Route QueryParameters\n\n`route-queryparameters` are the YAML representation of YARP [Parameter Based Routing](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/queryparameter-routing).\n\nSee https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routequeryparameter.\n\n```yaml\nyarp.ingress.kubernetes.io/route-queryparameters: |\n  - Name: the-queryparameter-name\n    Values:\n    - the-queryparameter-value\n    Mode: Contains\n    IsCaseSensitive: false\n  - Name: another-queryparameter-name\n    Values:\n    - another-queryparameter-value\n    Mode: Contains\n    IsCaseSensitive: false\n```\n\n#### Route Order\n\nSee https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routeconfig.order#yarp-reverseproxy-configuration-routeconfig-order.\n\n```yaml\nyarp.ingress.kubernetes.io/route-order: '10'\n```\n\n#### Route Methods\n\nSee https://learn.microsoft.com/dotnet/api/yarp.reverseproxy.configuration.routematch.methods#yarp-reverseproxy-configuration-routematch-methods.\n\n```yaml\nyarp.ingress.kubernetes.io/route-methods: |\n  - GET\n  - POST\n```\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/backend/Dockerfile",
    "content": "#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.\n\nFROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base\nWORKDIR /app\nEXPOSE 80\nEXPOSE 443\n\nFROM mcr.microsoft.com/dotnet/sdk:8.0  AS publish\nWORKDIR /src\n# We need to install the SDK manually because we might target an unreleased SDK\nCOPY [\"global.json\", \"\"]\nCOPY [\"TFMs.props\", \"Directory.Build.props\"]\nRUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --jsonfile global.json\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT 1\n\nCOPY [\"samples/KubernetesIngress.Sample/backend\", \"backend\"]\n\nWORKDIR /src/backend\nRUN /root/.dotnet/dotnet publish -c Release -o /app/publish -f net8.0\n\nFROM base AS final\nWORKDIR /app\nCOPY --from=publish /app/publish .\nENTRYPOINT [\"dotnet\", \"backend.dll\"]\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/backend/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Diagnostics;\nusing System.Net;\nusing System.Text.Json;\nusing Microsoft.AspNetCore.Builder;\n\nActivity.DefaultIdFormat = ActivityIdFormat.W3C;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nvar app = builder.Build();\n\napp.MapGet(\"/\", async context =>\n{\n    var backendInfo = new BackendInfo()\n    {\n        IP = context.Connection.LocalIpAddress.ToString(),\n        Hostname = Dns.GetHostName(),\n    };\n\n    context.Response.ContentType = \"application/json; charset=utf-8\";\n    await JsonSerializer.SerializeAsync(context.Response.Body, backendInfo);\n});\n\napp.Run();\n\ninternal sealed class BackendInfo\n{\n    public string IP { get; set; } = default!;\n\n    public string Hostname { get; set; } = default!;\n}\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/backend/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:21017\",\n      \"sslPort\": 44378\n    }\n  },\n  \"profiles\": {\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"backend\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"applicationUrl\": \"https://localhost:5002;http://localhost:5003\"\n    },\n    \"Docker\": {\n      \"commandName\": \"Docker\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"{Scheme}://{ServiceHost}:{ServicePort}\",\n      \"publishAllPorts\": true,\n      \"useSSL\": true\n    }\n  }\n}"
  },
  {
    "path": "samples/KubernetesIngress.Sample/backend/README.md",
    "content": "# Sample backend\n\nThis directory contains a sample ASP.NET Core application that acts as a \"backend service\" within a cluster as well as the definition for the Kubernetes manifests for the ingress controller.\n\n## Building the Docker Images\n\nFrom the base directory for this repo (where the .slnx file is), run the commands:\n\n```bash\ndocker build -t backend:latest -f ./samples/KubernetesIngress.Sample/backend/Dockerfile .\n```\n\n## Deploying the Backend\n\n1. Open the [backend.yaml](./backend.yaml) file\n1. Modify the container image to match the name used when building the image, e.g. change `<REGISTRY_NAME>/backend:<TAG>` to `backend:latest`\n1. Run the command `kubectl apply -f ./samples/KubernetesIngress.Sample/backend/backend.yaml`\n1. Run the command `kubectl apply -f ./samples/KubernetesIngress.Sample/backend/ingress-sample.yaml`\n\nTo undeploy the backend, run the commands\n```bash\nkubectl delete -f ./samples/KubernetesIngress.Sample/backend/ingress-sample.yaml\nkubectl delete -f ./samples/KubernetesIngress.Sample/backend/backend.yaml\n```\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/backend/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/backend/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/backend/backend.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <RootNamespace>Backend</RootNamespace>\n    <UserSecretsId>aaa98da6-d0d4-4ad6-9821-f66057413c3a</UserSecretsId>\n    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>\n    <DockerfileContext>..\\..\\..\\..</DockerfileContext>\n  </PropertyGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/backend/backend.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: backend\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: backend\n  template:\n    metadata:\n      labels:\n        app: backend\n    spec:\n      containers:\n      - name: backend\n        image: <REGISTRY_NAME>/backend:<TAG>\n        imagePullPolicy: IfNotPresent\n        ports:\n        - containerPort: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: backend\nspec:\n  selector:\n    app: backend\n  ports:\n  - port: 80\n    targetPort: 80\n  type: ClusterIP\n"
  },
  {
    "path": "samples/KubernetesIngress.Sample/backend/ingress-sample.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: minimal-ingress\nspec:\n  ingressClassName: yarp\n  rules:\n  - http:\n      paths:\n      - path: /\n        pathType: Prefix\n        backend:\n          service:\n            name: backend\n            port:\n              number: 80\n"
  },
  {
    "path": "samples/Prometheus/HttpLoadApp/HttpLoadApp.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n  </PropertyGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/Prometheus/HttpLoadApp/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\nvar client = new HttpClient();\n\nstring[] planets = { \"Mercury\", \"Venus\", \"Earth\", \"Mars\", \"Jupiter\" };\n\nforeach (var planet in planets)\n{\n    _ = Task.Run(() =>\n    {\n        CreateLoad(\"http://localhost:5000/\" + planet);\n    });\n}\nvar c = Console.ReadKey();\n\nasync void CreateLoad(string UrlPrefix)\n{\n    var i = 0;\n    while (true)\n    {\n        var url = UrlPrefix + \"/\" + i;\n        var resp = await client.GetAsync(url);\n        var result = await resp.Content.ReadAsStringAsync();\n        Console.WriteLine($\"Requested: {url}, Result: {resp.StatusCode}, Length: {result.Length}\");\n        i++;\n    }\n}\n"
  },
  {
    "path": "samples/Prometheus/README.md",
    "content": "# YARP Prometheus sample\n\nThis sample demonstrates how to use the ReverseProxy.Telemetry.Consumption library to listen to telemetry data from YARP and then to publish them as a endpoint for Prometheus to consume.\n\nThis sample uses the [prometheus-net](https://github.com/prometheus-net/prometheus-net) library to expose Counters, Gauges & Histograms to Prometheus, which makes exposing the telemetry much easier.\n\nInternally YARP uses EventCounters to collect telemetry events and metrics from a number of subsystems that are used toprocess the requests. The YARP telemetry library provides wrapper classes that collect these metrics and make them available for Consumption. To listen for the metrics you register classes with DI that implement an interface for each subsystem. Event/metric listeners will only be created for the subsystems that you register for, as each registration has performance implications.\n\nThe subsystems are:\n- **Proxy** which represents the overall proxy operation, and success or failure. Metrics include:\n    - Number of requests started\n    - Number of request in flight\n    - Number of requests that have failed\n- **Kestrel** which is the web server that handles incoming requests. Metrics include:\n    - Connection Rate - how many connections are opened a second\n    - Total number of connections\n    - Number of TLS handshakes\n    - Incoming queue lengths\n- **Http** which is the HttpClient which makes outgoing requests to the destination servers. Metrics include:\n    - Number of outgoing requests started\n    - Number of Requests failed\n    - Number of active requests\n    - Number of outbound connections\n- **Sockets** which collects metrics about the amount of data send and received\n- **NameResolution** which collects metrics for DNS lookup of destinations\n\n## Sample Contents\n\n- **ReverseProxy.Metrics.Prometheus.Sample**\n  - AppSettings.json - provides the configuration of routes, clusters and destinations. In this case it has 5 routes that map to a series of clusters which share 10 destinations between them.\n  - Startup.cs - follows the same pattern as other samples but also calls\n    - services.AddAllPrometheusMetrics() - to register handlers for all the metric collection\n    - proxyPipeline.UsePerRequestMetricCollection() - to add a middleware step to the proxy pipeline that can monitor the requests and has access to contextual data such as the route, cluster and destination so it can create metrics that add those as dimensions.\n    - endpoints.MapMetrics() - this adds the /metrics endpoint for prometheus-net that is polled by Prometheus.\n  - Metrics consumer classes - these all follow the same pattern - they implement the respective metrics consumption interface, handle the event with metrics and then write those using Counters, Gauges & Histograms from prometheus-net. This includes:\n    - PrometheusDNSMetrics.cs\n    - PrometheusKestrelMetrics.cs\n    - PrometheusOutboundHttpMetrics.cs\n    - PrometheusProxyMetrics.cs\n    - PrometheusSocketMetrics.cs\n  - PrometheusServiceExtensions.cs - Includes helper extension methods to perform the service registration for the above classes.\n- **HttpLoadApp**\n  - A simple app that uses HttpClient to create load against the 5 default routes defined by the proxy sample\n- **run10destinations**\n  - Scripts for Windows & Linux that will start the sample server listening to endpoints http://localhost:10000 to http://localhost:10009\n- **prometheus.yml**\n  - A sample config file for Prometheus that includes polling from http://localhost:5000/metrics for the results from the server.\n\n## Running the sample\n\n### ReverseProxy.Metrics.Prometheus.Sample\nThe sample can be started with dotnet run:\n```shell\ndotnet run --project ReverseProxy.Metrics.Prometheus.Sample --framework net5.0\n```\n\n### Destinations\nThe proxy configuration assumes that there are 10 destination endpoints running on ports 10000 to 10009. This can be done using the SampleServer included in the samples, and specifying the endpoints using the \"Urls\" command argument. This is encapsulated in [run10destinations.cmd](run10destinations.cmd) and [run10destinations.sh](run10destinations.sh)\n\n### HttpLoadApp\nTo cause the proxy to generate some metrics and with dimensions, a quick and dirty app is included that will create requests against each route. It can be started with dotnet run:\n\n```shell\ndotnet run --project HttpLoadApp\n```\n\n### Prometheus\nTo run the sample you will need Prometheus running to collect the metrics and present them. It can be downloaded from https://prometheus.io/. Prometheus requires a configuration file to tell it which endpoints to poll for metrics. \nA sample configuration file, [prometheus.yml](prometheus.yml) is included that assumes the proxy is exposing http://localhost:5000/metrics.\n\nTo start prometheus, use \n\n```shell\nprometheus --config.file prometheus.yml\n```\n\n### Viewing Results\nAssuming that you started each of the above in order, you should now see that the proxy and destination server are responding to requests, and so metrics should be being generated.\n\nOpen http://localhost:5000/metrics with your browser or curl. That is the endpoint produced by prometheus.net that exposes the metrics in the right format for prometheus to consume. You should note that the name of each metric exposed starts with \"yarp_\".\n\nOpen http://localhost:9090/graph in your browser. This is the UI for querying prometheus. In the search expression you can type \"yarp_\" to get completion of all the metrics that have been created.\n![Prometheus Screenshot](graph_screenshot.png)"
  },
  {
    "path": "samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\nusing Prometheus;\nusing Yarp.Sample;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddControllers();\nbuilder.Services.AddReverseProxy()\n    .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"));\n\n// Enable metric collection for all the underlying event counters used by YARP\nbuilder.Services.AddAllPrometheusMetrics();\n\nvar app = builder.Build();\n\n// Add the reverse proxy endpoints based on routes\napp.MapReverseProxy();\n\n// Add the /Metrics endpoint for prometheus to query on\napp.MapMetrics();\n\napp.Run();\n"
  },
  {
    "path": "samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/PrometheusDnsMetrics.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.Telemetry.Consumption;\nusing Prometheus;\n\nnamespace Yarp.Sample\n{\n    public sealed class PrometheusDnsMetrics : IMetricsConsumer<NameResolutionMetrics>\n    {\n        private static readonly Counter _dnsLookupsRequested = Metrics.CreateCounter(\n            \"yarp_dns_lookups_requested\",\n            \"Number of DNS lookups requested\"\n            );\n\n        private static readonly Gauge _averageLookupDuration = Metrics.CreateGauge(\n            \"yarp_dns_average_lookup_duration\",\n            \"Average DNS lookup duration\"\n            );\n\n        public void OnMetrics(NameResolutionMetrics previous, NameResolutionMetrics current)\n        {\n            _dnsLookupsRequested.IncTo(current.DnsLookupsRequested);\n            _averageLookupDuration.Set(current.AverageLookupDuration.TotalMilliseconds);\n        }\n    }\n}\n"
  },
  {
    "path": "samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/PrometheusForwarderMetrics.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.Telemetry.Consumption;\nusing Prometheus;\n\nnamespace Yarp.Sample\n{\n    public sealed class PrometheusForwarderMetrics : IMetricsConsumer<ForwarderMetrics>\n    {\n        private static readonly Counter _requestsStarted = Metrics.CreateCounter(\n            \"yarp_proxy_requests_started\",\n            \"Number of requests initiated through the proxy\"\n            );\n\n        private static readonly Counter _requestsFailed = Metrics.CreateCounter(\n            \"yarp_proxy_requests_failed\",\n            \"Number of proxy requests that have failed\"\n            );\n\n        private static readonly Gauge _CurrentRequests = Metrics.CreateGauge(\n            \"yarp_proxy_current_requests\",\n            \"Number of active proxy requests that have started but not yet completed or failed\"\n            );\n\n        public void OnMetrics(ForwarderMetrics previous, ForwarderMetrics current)\n        {\n            _requestsStarted.IncTo(current.RequestsStarted);\n            _requestsFailed.IncTo(current.RequestsFailed);\n            _CurrentRequests.Set(current.CurrentRequests);\n        }\n    }\n}\n"
  },
  {
    "path": "samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/PrometheusKestrelMetrics.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.Telemetry.Consumption;\nusing Prometheus;\n\nnamespace Yarp.Sample\n{\n    public sealed class PrometheusKestrelMetrics : IMetricsConsumer<KestrelMetrics>\n    {\n        private static readonly Counter _totalConnections = Metrics.CreateCounter(\n            \"yarp_kestrel_total_connections\",\n            \"Number of incoming connections opened\"\n            );\n\n        private static readonly Counter _totalTlsHandshakes = Metrics.CreateCounter(\n            \"yarp_kestrel_total_tls_Handshakes\",\n            \"Number of TLS handshakes started\"\n            );\n\n        private static readonly Gauge _currentTlsHandshakes = Metrics.CreateGauge(\n            \"yarp_kestrel_current_tls_handshakes\",\n            \"Number of active TLS handshakes that have started but not yet completed or failed\"\n            );\n\n        private static readonly Counter _failedTlsHandshakes = Metrics.CreateCounter(\n            \"yarp_kestrel_failed_tls_handshakes\",\n            \"Number of TLS handshakes that failed\"\n            );\n\n        private static readonly Gauge _currentConnections = Metrics.CreateGauge(\n            \"yarp_kestrel_current_connections\",\n            \"Number of currently open incoming connections\"\n            );\n\n        private static readonly Gauge _connectionQueueLength = Metrics.CreateGauge(\n            \"yarp_kestrel_connection_queue_length\",\n            \"Number of connections on the queue.\"\n            );\n\n        private static readonly Gauge _requestQueueLength = Metrics.CreateGauge(\n            \"yarp_kestrel_request_queue_length\",\n            \"Number of requests on the queue\"\n            );\n\n        public void OnMetrics(KestrelMetrics previous, KestrelMetrics current)\n        {\n            _totalConnections.IncTo(current.TotalConnections);\n            _totalTlsHandshakes.IncTo(current.TotalTlsHandshakes);\n            _currentTlsHandshakes.Set(current.CurrentTlsHandshakes);\n            _failedTlsHandshakes.IncTo(current.FailedTlsHandshakes);\n            _currentConnections.Set(current.CurrentConnections);\n            _connectionQueueLength.Set(current.ConnectionQueueLength);\n            _requestQueueLength.Set(current.RequestQueueLength);\n        }\n    }\n}\n"
  },
  {
    "path": "samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/PrometheusOutboundHttpMetrics.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Yarp.Telemetry.Consumption;\nusing Prometheus;\n\nnamespace Yarp.Sample\n{\n    /// <summary>\n    /// Collects outbound http metrics and exposes them using prometheus-net\n    /// </summary>\n    public sealed class PrometheusOutboundHttpMetrics : IMetricsConsumer<HttpMetrics>\n    {\n        private static readonly double CUBE_ROOT_10 = Math.Pow(10, (1.0 / 3));\n\n        private static readonly Counter _outboundRequestsStarted = Metrics.CreateCounter(\n            \"yarp_outbound_http_requests_started\",\n            \"Number of outbound requests initiated by the proxy\"\n            );\n\n        private static readonly Counter _outboundRequestsFailed = Metrics.CreateCounter(\n            \"yarp_outbound_http_requests_failed\",\n            \"Number of outbound requests failed\"\n            );\n\n        private static readonly Gauge _outboundCurrentRequests = Metrics.CreateGauge(\n            \"yarp_outbound_http_current_requests\",\n            \"Number of active outbound requests that have started but not yet completed or failed\"\n            );\n\n        private static readonly Gauge _outboundCurrentHttp11Connections = Metrics.CreateGauge(\n            \"yarp_outbound_http11_connections\",\n            \"Number of currently open HTTP 1.1 connections\"\n            );\n\n        private static readonly Gauge _outboundCurrentHttp20Connections = Metrics.CreateGauge(\n            \"yarp_outbound_http20_connections\",\n            \"Number of active proxy requests that have started but not yet completed or failed\"\n            );\n\n        private static readonly Histogram _outboundHttp11RequestQueueDuration = Metrics.CreateHistogram(\n            \"yarp_outbound_http11_request_queue_duration\",\n            \"Average time spent on queue for HTTP 1.1 requests that hit the MaxConnectionsPerServer limit in the last metrics interval\",\n            new HistogramConfiguration\n            {\n                Buckets = Histogram.ExponentialBuckets(10, CUBE_ROOT_10, 10)\n            });\n\n        private static readonly Histogram _outboundHttp20RequestQueueDuration = Metrics.CreateHistogram(\n            \"yarp_outbound_http20_request_queue_duration\",\n            \"Average time spent on queue for HTTP 2.0 requests that hit the MAX_CONCURRENT_STREAMS limit on the connection in the last metrics interval\",\n            new HistogramConfiguration\n            {\n                Buckets = Histogram.ExponentialBuckets(10, CUBE_ROOT_10, 10)\n            });\n\n        public void OnMetrics(HttpMetrics previous, HttpMetrics current)\n        {\n            _outboundRequestsStarted.IncTo(current.RequestsStarted);\n            _outboundRequestsFailed.IncTo(current.RequestsFailed);\n            _outboundCurrentRequests.Set(current.CurrentRequests);\n            _outboundCurrentHttp11Connections.Set(current.CurrentHttp11Connections);\n            _outboundCurrentHttp20Connections.Set(current.CurrentHttp20Connections);\n            _outboundHttp11RequestQueueDuration.Observe(current.Http11RequestsQueueDuration.TotalMilliseconds);\n            _outboundHttp20RequestQueueDuration.Observe(current.Http20RequestsQueueDuration.TotalMilliseconds);\n        }\n    }\n}\n"
  },
  {
    "path": "samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/PrometheusServiceExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.Telemetry.Consumption;\n\nnamespace Yarp.Sample\n{\n    public static class PrometheusServiceExtensions\n    {\n        public static IServiceCollection AddPrometheusForwarderMetrics(this IServiceCollection services)\n        {\n            services.AddTelemetryListeners();\n            services.AddSingleton<IMetricsConsumer<ForwarderMetrics>, PrometheusForwarderMetrics>();\n            return services;\n        }\n\n        public static IServiceCollection AddPrometheusDnsMetrics(this IServiceCollection services)\n        {\n            services.AddTelemetryListeners();\n            services.AddSingleton<IMetricsConsumer<NameResolutionMetrics>, PrometheusDnsMetrics>();\n            return services;\n        }\n\n        public static IServiceCollection AddPrometheusKestrelMetrics(this IServiceCollection services)\n        {\n            services.AddTelemetryListeners();\n            services.AddSingleton<IMetricsConsumer<KestrelMetrics>, PrometheusKestrelMetrics>();\n            return services;\n        }\n\n        public static IServiceCollection AddPrometheusOutboundHttpMetrics(this IServiceCollection services)\n        {\n            services.AddTelemetryListeners();\n            services.AddSingleton<IMetricsConsumer<HttpMetrics>, PrometheusOutboundHttpMetrics>();\n            return services;\n        }\n\n        public static IServiceCollection AddPrometheusSocketsMetrics(this IServiceCollection services)\n        {\n            services.AddTelemetryListeners();\n            services.AddSingleton<IMetricsConsumer<SocketsMetrics>, PrometheusSocketMetrics>();\n            return services;\n        }\n\n        public static IServiceCollection AddAllPrometheusMetrics(this IServiceCollection services)\n        {\n            services.AddPrometheusForwarderMetrics();\n            services.AddPrometheusDnsMetrics();\n            services.AddPrometheusKestrelMetrics();\n            services.AddPrometheusOutboundHttpMetrics();\n            services.AddPrometheusSocketsMetrics();\n            return services;\n        }\n    }\n}\n"
  },
  {
    "path": "samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/PrometheusSocketMetrics.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.Telemetry.Consumption;\nusing Prometheus;\n\nnamespace Yarp.Sample\n{\n    public sealed class PrometheusSocketMetrics : IMetricsConsumer<SocketsMetrics>\n    {\n        private static readonly Counter _outgoingConnectionsEstablished = Metrics.CreateCounter(\n            \"yarp_sockets_outgoing_connections_established\",\n            \"Number of outgoing (Connect) Socket connections established\"\n            );\n\n        private static readonly Counter _incomingConnectionsEstablished = Metrics.CreateCounter(\n            \"yarp_sockets_incoming_connections_established\",\n            \"Number of incoming (Accept) Socket connections established\"\n            );\n\n        private static readonly Counter _bytesReceived = Metrics.CreateCounter(\n            \"yarp_sockets_bytes_received\",\n            \"Number of bytes received\"\n            );\n\n        private static readonly Counter _bytesSent = Metrics.CreateCounter(\n            \"yarp_sockets_bytes_sent\",\n            \"Number of bytes sent\"\n            );\n\n        private static readonly Counter _datagramsReceived = Metrics.CreateCounter(\n            \"yarp_sockets_datagrams_received\",\n            \"Number of datagrams received\"\n            );\n\n        private static readonly Counter _datagramsSent = Metrics.CreateCounter(\n            \"yarp_sockets_datagrams_sent\",\n            \"Number of datagrams Sent\"\n            );\n\n        public void OnMetrics(SocketsMetrics previous, SocketsMetrics current)\n        {\n            _outgoingConnectionsEstablished.IncTo(current.OutgoingConnectionsEstablished);\n            _incomingConnectionsEstablished.IncTo(current.IncomingConnectionsEstablished);\n            _bytesReceived.IncTo(current.BytesReceived);\n            _bytesSent.IncTo(current.BytesSent);\n            _datagramsReceived.IncTo(current.DatagramsReceived);\n            _datagramsSent.IncTo(current.DatagramsSent);\n        }\n    }\n}\n"
  },
  {
    "path": "samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"https://localhost:44356/\",\n      \"sslPort\": 44356\n    }\n  },\n  \"profiles\": {\n    \"ReverseProxy.Metrics.Prometheus.Sample\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/ReverseProxy.Metrics.Prometheus.Sample.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>Yarp.Sample</RootNamespace>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"prometheus-net.AspNetCore\" Version=\"$(PrometheusNetVersion)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\..\\src\\TelemetryConsumption\\Yarp.Telemetry.Consumption.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Debug\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      // \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"Kestrel\": {\n    \"Endpoints\": {\n      \"http\": {\n        \"Url\": \"http://localhost:5000\"\n      },\n      \"https\": {\n        \"Url\": \"https://localhost:5001\"\n      }\n    }\n  },\n  \"ReverseProxy\": {\n    \"Routes\": {\n      \"Mercury\": {\n        \"ClusterId\": \"gamma\",\n        \"Match\": {\n          \"Path\": \"/Mercury/{*all}\"\n        }\n      },\n      \"Venus\": {\n        \"ClusterId\": \"gamma\",\n        \"Match\": {\n          \"Path\": \"/Venus/{*all}\"\n        }\n      },\n      \"Earth\": {\n        \"ClusterId\": \"delta\",\n        \"Match\": {\n          \"Path\": \"/Earth/{*all}\"\n        }\n      },\n      \"Mars\": {\n        \"ClusterId\": \"delta\",\n        \"Match\": {\n          \"Path\": \"/Mars/{*all}\"\n        }\n      },\n      \"Jupiter\": {\n        \"ClusterId\": \"epsilon\",\n        \"Match\": {\n          \"Path\": \"/Jupiter/{*all}\"\n        }\n      }\n    },\n    \"Clusters\": {\n      \"gamma\": {\n        \"Destinations\": {\n          \"d0\": {\n            \"Address\": \"http://localhost:10000\"\n          },\n          \"d2\": {\n            \"Address\": \"http://localhost:10002\"\n          },\n          \"d4\": {\n            \"Address\": \"http://localhost:10004\"\n          },\n          \"d6\": {\n            \"Address\": \"http://localhost:10006\"\n          },\n          \"d8\": {\n            \"Address\": \"http://localhost:10008\"\n          }\n        }\n      },\n      \"delta\": {\n        \"Destinations\": {\n          \"d1\": {\n            \"Address\": \"http://localhost:10001\"\n          },\n          \"d3\": {\n            \"Address\": \"http://localhost:10003\"\n          },\n          \"d5\": {\n            \"Address\": \"http://localhost:10005\"\n          },\n          \"d7\": {\n            \"Address\": \"http://localhost:10007\"\n          },\n          \"d9\": {\n            \"Address\": \"http://localhost:10009\"\n          }\n        }\n      },\n      \"epsilon\": {\n        \"Destinations\": {\n          \"d0\": {\n            \"Address\": \"http://localhost:10000\"\n          },\n          \"d1\": {\n            \"Address\": \"http://localhost:10001\"\n          },\n          \"d2\": {\n            \"Address\": \"http://localhost:10002\"\n          },\n          \"d3\": {\n            \"Address\": \"http://localhost:10003\"\n          },\n          \"d4\": {\n            \"Address\": \"http://localhost:10004\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/Prometheus/prometheus.yml",
    "content": "# my global config\nglobal:\n  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.\n  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.\n  # scrape_timeout is set to the global default (10s).\n\n# Alertmanager configuration\nalerting:\n  alertmanagers:\n  - static_configs:\n    - targets:\n      # - alertmanager:9093\n\n# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.\nrule_files:\n  # - \"first_rules.yml\"\n  # - \"second_rules.yml\"\n\n# A scrape configuration containing exactly one endpoint to scrape:\n# Here it's Prometheus itself.\nscrape_configs:\n  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.\n  - job_name: 'prometheus'\n\n    # metrics_path defaults to '/metrics'\n    # scheme defaults to 'http'.\n\n    static_configs:\n    - targets: ['localhost:9090']\n  \n  - job_name: 'YARP'\n    static_configs:\n      - targets: ['localhost:5000']\n\n"
  },
  {
    "path": "samples/Prometheus/run10destinations.cmd",
    "content": "dotnet run --project %~dp0/../SampleServer/SampleServer.csproj --Urls \"http://localhost:10000;http://localhost:10001;http://localhost:10002;http://localhost:10003;http://localhost:10004;http://localhost:10005;http://localhost:10006;http://localhost:10007;http://localhost:10008;http://localhost:10009\""
  },
  {
    "path": "samples/Prometheus/run10destinations.sh",
    "content": "#!/bin/bash\n \nfull_path=$(realpath $0)\ndir_path=$(dirname $full_path)\nsamples=$(dirname $dir_path )\n\ndotnet run --project $samples/SampleServer/SampleServer.csproj --Urls \"http://localhost:10000;http://localhost:10001;http://localhost:10002;http://localhost:10003;http://localhost:10004;http://localhost:10005;http://localhost:10006;http://localhost:10007;http://localhost:10008;http://localhost:10009\""
  },
  {
    "path": "samples/README.md",
    "content": "# Samples\n\n## Warning: Breaking Changes\n\nThe samples in this folder are in sync with the main branch for YARP. If there have been breaking changes to the API or configuration, they may not match what is published on Nuget. To avoid disappointment, if using the samples with the YARP library published to Nuget, please change to the branch to match the latest release or preview, either using the branch dropdown or these links:\n\n**[Samples folder in latest release/preview](https://github.com/dotnet/yarp/tree/release/latest/samples)**\n\n**[Source zip for latest release, including samples](https://github.com/dotnet/yarp/releases/latest)**\n\n----\n\nThe following samples are provided:\n\n| Name | Description |\n| ------- | ----- |\n| [Basic Yarp Sample](BasicYarpSample) | A simple sample that shows how to add YARP to the empty ASP.NET sample to create a fully functioning reverse proxy. | \n| [Configuration](ReverseProxy.Config.Sample) | Shows all the options that are available in the YARP config file |\n| [Minimal](ReverseProxy.Minimal.Sample) | Shows a minimal config-based YARP application using .NET 6's [Minimal Hosting for ASP.NET Core](https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-4/#introducing-minimal-apis) |\n| [Http.sys Delegation](ReverseProxy.HttpSysDelegation.Sample) | Shows an example of using YARP to do Http.sys queue delegation in addition to proxying. |\n| [Transforms](ReverseProxy.Transforms.Sample) | Shows how to transform headers as part of the proxy operation |\n| [Code extensibility](ReverseProxy.Code.Sample) | Shows how you can extend YARP using a custom configuration provider, and a middleware component as part of the YARP pipeline |\n| [Authentication & Authorization](ReverseProxy.Auth.Sample) | Shows how to add authentication and authorization for routes to the proxy |\n| [Configuration Filter](ReverseProxy.ConfigFilter.Sample) | Shows how to use extensibility to modify configuration as its loaded from the configuration file. This sample implements an indirection to enable config values to be pulled from environment variables which can be useful in a cloud environment. |\n| [Metrics](ReverseProxy.Metrics.Sample) | Shows how to consume YARP telemetry. This sample collects detailed timings for the sub-operations involved in the proxy process. |\n| [Using IHttpProxy Directly](ReverseProxy.Direct.Sample) | Shows how to use IHttpProxy, which performs the proxy operation, directly without using YARP's configuration, pipeline etc. |\n| [Lets Encrypt](ReverseProxy.LetsEncrypt.Sample) | Shows how to use a certificate authority such as Lets Encrypt to set up TLS termination in YARP. |\n| [Kubernetes Ingress](KubernetesIngress.Sample) | Shows how to use YARP as a Kubernetes ingress controller |\n| [Prometheus](Prometheus) | Shows how to consume the YARP telemetry library and export metrics to external telemetry such as Prometheus |\n"
  },
  {
    "path": "samples/ReverseProxy.Auth.Sample/Controllers/AccountController.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Security.Claims;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Yarp.Sample.Controllers\n{\n    [AllowAnonymous]\n    [Route(\"[controller]/[action]\")]\n    public class AccountController : Controller\n    {\n        [HttpGet]\n        public IActionResult Login(string returnUrl)\n        {\n            ViewData[\"returnUrl\"] = returnUrl;\n\n            return View();\n        }\n\n        // Processes input from the Login.cshtml page to authenticate the user\n        [HttpPost]\n        public IActionResult Login(string name, string myClaimValue, string returnUrl)\n        {\n            // Create a new identity with 2 claims based on the fields in the form\n            var identity = new ClaimsIdentity(new[]\n            {\n                new Claim(ClaimTypes.Name, name),\n                new Claim(\"myCustomClaim\", myClaimValue)\n            }, CookieAuthenticationDefaults.AuthenticationScheme);\n            var principal = new ClaimsPrincipal(identity);\n\n            return SignIn(principal, new AuthenticationProperties()\n            {\n                RedirectUri = returnUrl\n                // SignIn is the only one that requires a scheme: https://github.com/dotnet/aspnetcore/issues/23325\n            }, CookieAuthenticationDefaults.AuthenticationScheme);\n        }\n\n        [HttpPost]\n        public IActionResult Logout()\n        {\n            return SignOut(new AuthenticationProperties()\n            {\n                RedirectUri = \"/Account/LoggedOut\",\n            });\n        }\n\n        [HttpGet]\n        public IActionResult LoggedOut()\n        {\n            return View();\n        }\n\n        [HttpGet]\n        public IActionResult AccessDenied()\n        {\n            return View();\n        }\n    }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Auth.Sample/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http.Headers;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.ReverseProxy.Transforms;\nusing Yarp.Sample;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nvar services = builder.Services;\n\n// Required to supply the authentication UI in Views/*\nservices.AddRazorPages();\n\nservices.AddSingleton<TokenService>();\n\nservices.AddReverseProxy()\n    .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"))\n    .AddTransforms(transformBuilderContext =>  // Add transforms inline\n    {\n        // For each route+cluster pair decide if we want to add transforms, and if so, which?\n        // This logic is re-run each time a route is rebuilt.\n\n        // Only do this for routes that require auth.\n        if (string.Equals(\"myPolicy\", transformBuilderContext.Route.AuthorizationPolicy))\n        {\n            transformBuilderContext.AddRequestTransform(async transformContext =>\n            {\n                // AuthN and AuthZ will have already been completed after request routing.\n                var ticket = await transformContext.HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);\n                var tokenService = transformContext.HttpContext.RequestServices.GetRequiredService<TokenService>();\n                var token = await tokenService.GetAuthTokenAsync(ticket.Principal);\n\n                // Reject invalid requests\n                if (string.IsNullOrEmpty(token))\n                {\n                    var response = transformContext.HttpContext.Response;\n                    response.StatusCode = 401;\n                    return;\n                }\n\n                transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", token);\n            });\n        }\n    }); ;\n\nservices.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)\n    .AddCookie();\n\nservices.AddAuthorization(options =>\n{\n    // Creates a policy called \"myPolicy\" that depends on having a claim \"myCustomClaim\" with the value \"green\".\n    // See AccountController.Login method for where this claim is applied to the user identity\n    // This policy can then be used by routes in the proxy, see \"ClaimsAuthRoute\" in appsettings.json\n    options.AddPolicy(\"myPolicy\", builder => builder\n        .RequireClaim(\"myCustomClaim\", \"green\")\n        .RequireAuthenticatedUser());\n\n    // The default policy is to require authentication, but no additional claims\n    // Uncommenting the following would have no effect\n    // options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();\n\n    // FallbackPolicy is used for routes that do not specify a policy in config\n    // Make all routes that do not specify a policy to be anonymous (this is the default).\n    options.FallbackPolicy = null;\n    // Or make all routes that do not specify a policy require some auth:\n    // options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();\n});\n\nvar app = builder.Build();\n\napp.UseStaticFiles();\n\napp.UseAuthentication();\napp.UseAuthorization();\n\napp.MapControllers();\napp.MapReverseProxy();\n\napp.Run();\n"
  },
  {
    "path": "samples/ReverseProxy.Auth.Sample/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"https://localhost:44356/\",\n      \"sslPort\": 44356\n    }\n  },\n  \"profiles\": {\n    \"ReverseProxy.Auth.Sample\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Auth.Sample/README.md",
    "content": "# Authentication & Authorization sample\n\nThis sample shows how the YARP proxy can be integrated with the ASP.NET [authentication](https://docs.microsoft.com/aspnet/core/security/authentication) and [authorization](https://docs.microsoft.com/aspnet/core/security/authorization/introduction) system to specify claims requirements on routes that will be enforced by the proxy before it will forward applicable requests.\n\nThe sample includes the following parts:\n\n- **[Program.cs](Program.cs)**\n  Sets up the ASP.NET server to have the proxy together with the other middleware for authentication, authorization and Razor pages.\n  It sets up a custom authorization policy \"myPolicy\" with a custom claim.\n\n- **[AccountController.cs](Controllers/AccountController.cs)**\n  Handles the login UI actions, and adds a value from a field in the login page to the \"myCustomClaim\" claim in the active identity. That claim is later required by the \"myPolicy\" authorization policy created in Startup.cs\n\n- **[appsettings.json](appsettings.json)**\n  Defines the routes used by the reverse proxy including:\n  - /default - requires authentication to access\n  - /custom - uses the \"myPolicy\" authorization policy which requires authentication and a myCustomClaim value of \"green\"\n  - /open - which uses \"Anonymous\" as the authorization policy so its always open regardless of the default\n  - \\* - which uses the built-in FallbackPolicy which is configured in Startup.cs to not require authentication\n\n- **Login UI**\n  The Razor pages in [Views/Account](Views/Account) provide the pages to login, logout and be shown when access is denied.\n\n## Usage\nStart the sample with ```dotnet run``` which by default will bind to http://localhost:5000 and https://localhost:5001. Try accessing the urls \"/\", \"/default\" and \"/custom\".\n\nWhen shown the login ui, pick a value for myCustomClaim. Using \"green\" will allow access to content under \"/custom\", using other values will deny access."
  },
  {
    "path": "samples/ReverseProxy.Auth.Sample/ReverseProxy.Auth.Sample.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>Yarp.Sample</RootNamespace>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Folder Include=\"wwwroot\\\" />\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/ReverseProxy.Auth.Sample/TokenService.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Security.Claims;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Sample\n{\n    internal sealed class TokenService\n    {\n        internal Task<string> GetAuthTokenAsync(ClaimsPrincipal user)\n        {\n            // we only have tokens for bob\n            if (string.Equals(\"Bob\", user.Identity.Name))\n            {\n                return Task.FromResult(Guid.NewGuid().ToString());\n            }\n            return Task.FromResult<string>(null);\n        }\n    }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Auth.Sample/Views/Account/AccessDenied.cshtml",
    "content": "@{\n    ViewData[\"Title\"] = \"Access Denied\";\n}\n\n<header>\n    <h1 class=\"text-danger\">@ViewData[\"Title\"]</h1>\n    <p class=\"text-danger\">You do not have access to this route.</p>\n</header>\n\n\n<form action=\"/Account/Logout\" method=\"post\">\n    <input type=\"submit\" value=\"Logout\">\n</form>\n"
  },
  {
    "path": "samples/ReverseProxy.Auth.Sample/Views/Account/LoggedOut.cshtml",
    "content": "@{\n    ViewData[\"Title\"] = \"Logged Out\";\n}\nYou have been logged out.\n"
  },
  {
    "path": "samples/ReverseProxy.Auth.Sample/Views/Account/Login.cshtml",
    "content": "@{\n    ViewData[\"Title\"] = \"Login\";\n}\n\n<h2>Login</h2>\n\n<form action=\"Login\" method=\"post\">\n    <input hidden name=\"returnurl\" type=\"text\" value=\"@ViewData[\"ReturnUrl\"]\" /><br />\n    <input name=\"Name\" type=\"text\" value=\"Bob\" /><br />\n    <input name=\"myClaimValue\" type=\"text\" value=\"green\" /><br />\n    <input type=\"submit\">\n    <div><b>Note:</b>The authorization policy will check for the value of \"green\", other values should pass authentication, but not authorize for specific routes</div>\n</form>\n"
  },
  {
    "path": "samples/ReverseProxy.Auth.Sample/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Debug\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Auth.Sample/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"Kestrel\": {\n    \"Endpoints\": {\n      \"https\": {\n        \"Url\": \"https://localhost:5001\"\n      },\n      \"http\": {\n        \"Url\": \"http://localhost:5000\"\n      }\n    }\n  },\n  \"ReverseProxy\": {\n    \"Clusters\": {\n      \"cluster1\": {\n        \"Destinations\": {\n          \"cluster1/destination1\": {\n            \"Address\": \"https://example.com/\"\n          }\n        }\n      }\n    },\n    \"Routes\": {\n      \"DefaultAuthRoute\": {\n        \"ClusterId\": \"cluster1\",\n        // This route uses the built-in default authorization policy which is to require authenticated users \n        \"AuthorizationPolicy\": \"Default\",\n        \"Match\": {\n          \"Path\": \"/default\"\n        }\n      },\n      \"ClaimsAuthRoute\": {\n        \"ClusterId\": \"cluster1\",\n        // This route requires the \"myPolicy\" authorization policy which is defined in Startup.cs\n        \"AuthorizationPolicy\": \"myPolicy\",\n        \"Match\": {\n          \"Path\": \"/custom/{*any}\"\n        }\n      },\n      \"AnonymousRoute\": {\n        \"ClusterId\": \"cluster1\",\n        // This route uses the explicit name Anonymous to not require authentication\n        \"AuthorizationPolicy\": \"Anonymous\",\n        \"Match\": {\n          \"Path\": \"/open/{*any}\"\n        }\n      },\n      \"Other\": {\n        // As the following route does not define an authorization policy, it uses the fallback policy\n        // which is set in Startup.cs to be null, and so not require authentication or claims.\n        \"ClusterId\": \"cluster1\",\n        \"Match\": {\n          \"Path\": \"{**catchall}\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Code.Sample/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.ReverseProxy.Configuration;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Model;\n\nconst string DEBUG_HEADER = \"Debug\";\nconst string DEBUG_METADATA_KEY = \"debug\";\nconst string DEBUG_VALUE = \"true\";\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddControllers();\nbuilder.Services.AddReverseProxy()\n    .LoadFromMemory(GetRoutes(), GetClusters());\n\nvar app = builder.Build();\n\napp.Map(\"/update\", context =>\n{\n    context.RequestServices.GetRequiredService<InMemoryConfigProvider>().Update(GetRoutes(), GetClusters());\n    return Task.CompletedTask;\n});\n// We can customize the proxy pipeline and add/remove/replace steps\napp.MapReverseProxy(proxyPipeline =>\n{\n    // Use a custom proxy middleware, defined below\n    proxyPipeline.Use(MyCustomProxyStep);\n    // Don't forget to include these two middleware when you make a custom proxy pipeline (if you need them).\n    proxyPipeline.UseSessionAffinity();\n    proxyPipeline.UseLoadBalancing();\n});\n\napp.Run();\n\nRouteConfig[] GetRoutes()\n{\n    return\n    [\n        new RouteConfig()\n        {\n            RouteId = \"route\" + Random.Shared.Next(), // Forces a new route id each time GetRoutes is called.\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch\n            {\n                // Path or Hosts are required for each route. This catch-all pattern matches all request paths.\n                Path = \"{**catch-all}\"\n            }\n        }\n    ];\n}\n\nClusterConfig[] GetClusters()\n{\n    var debugMetadata = new Dictionary<string, string>\n    {\n        { DEBUG_METADATA_KEY, DEBUG_VALUE }\n    };\n\n    return\n    [\n        new ClusterConfig()\n        {\n            ClusterId = \"cluster1\",\n            SessionAffinity = new SessionAffinityConfig { Enabled = true, Policy = \"Cookie\", AffinityKeyName = \".Yarp.ReverseProxy.Affinity\" },\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"destination1\", new DestinationConfig() { Address = \"https://example.com\" } },\n                { \"debugdestination1\", new DestinationConfig() {\n                    Address = \"https://bing.com\",\n                    Metadata = debugMetadata  }\n                },\n            }\n        }\n    ];\n}\n\n/// <summary>\n/// Custom proxy step that filters destinations based on a header in the inbound request\n/// Looks at each destination metadata, and filters in/out based on their debug flag and the inbound header\n/// </summary>\nTask MyCustomProxyStep(HttpContext context, Func<Task> next)\n{\n    // Can read data from the request via the context\n    var useDebugDestinations = context.Request.Headers.TryGetValue(DEBUG_HEADER, out var headerValues) && headerValues.Count == 1 && headerValues[0] == DEBUG_VALUE;\n\n    // The context also stores a ReverseProxyFeature which holds proxy specific data such as the cluster, route and destinations\n    var availableDestinationsFeature = context.Features.Get<IReverseProxyFeature>();\n    var filteredDestinations = new List<DestinationState>();\n\n    // Filter destinations based on criteria\n    foreach (var d in availableDestinationsFeature.AvailableDestinations)\n    {\n        //Todo: Replace with a lookup of metadata - but not currently exposed correctly here\n        if (d.DestinationId.Contains(\"debug\") == useDebugDestinations) { filteredDestinations.Add(d); }\n    }\n    availableDestinationsFeature.AvailableDestinations = filteredDestinations;\n\n    // Important - required to move to the next step in the proxy pipeline\n    return next();\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Code.Sample/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"https://localhost:44356/\",\n      \"sslPort\": 44356\n    }\n  },\n  \"profiles\": {\n    \"ReverseProxy.Code.Sample\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Code.Sample/README.md",
    "content": "# YARP Code Extensibility Sample\n\nThis sample shows two common customizations via code of the YARP reverse proxy:\n\n- ## Dynamic configuration from code\n\n  YARP supports pulling configuration from a config file, but in many scenarios the configuration of the routes to use, and which destinations the requests should be sent to need to be programmatically fetched from another source. The extensibility of YARP makes it easy for you to fetch that data from where you need to, and then pass to the proxy as lists of objects.\n\n  This sample shows the routes and destinations being created in code, and then passed to an in-memory provider. The role of the in-memory provider is to give change notifications to YARP for when the config has been changed and needs to be updated. YARP uses a snapshot model for its configuration, so that changes are applied as an atomic action, that will apply to subsequent requests after the change is applied. Existing requests that are already being processed will be completed using the configuration snapshot from the time that they were received.\n\n  The ```IProxyConfig``` interface implemented in InMemoryConfigProvider includes a change token which is used to signal when a batch of changes to the configuration is complete, and the proxy should take a snapshot and update its internal configuration. Part of the snapshot processing is to create an optimized route table in ASP.NET, which can be a CPU intensive operation, for that reason we don't recommend signaling for updates more than once per 15 seconds. \n\n- ## Custom pipeline step\n\n  YARP uses a pipeline model for the stages involved in processing each request:\n\n  - Mapping the request path to a route and cluster of destinations\n  - Pre-assigning servers based on existing session affinity headers in the request \n  - Filtering the destination list for servers that are not healthy\n  - Load balancing between the remaining servers based on load etc\n  - Storing session affinity if applicable\n  - Transforming headers if required\n  - Proxying the request/response to/from the destination server\n\n  You can insert additional custom stages into the pipeline, or replace built-in steps with your own implementations.\n  \n  This sample adds an additional stage that will filter the destinations from a cluster based on a \"debug\" metadata attribute being included in the config data based. If a custom header \"Debug:true\" is present in the request, then destinations with the debug metadata will be retained, and others will be filtered out, or vice-versa.  \n\n## Key Files\n\nThe following files are key to implementing the features described above:\n\n- ### [Program.cs](Program.cs)\n  Provides the initialization routines for ASP.NET and the reverse proxy. It:\n  - sets up the proxy passing in the InMemoryConfigProvider instance. The sample routes and clusters definitions are created as part of this initialization. The config provider instance is used for the lifetime of the proxy.\n  - sets up the request pipeline. As an additional step is added, the proxy pipeline is configured here.\n  - ```MyCustomProxyStep``` is the implementation of the additional step. It finds the proxy functionality via features added to the HttpContext, and then filters the destinations based on the presence of a \"Debug\" header in the request.\n"
  },
  {
    "path": "samples/ReverseProxy.Code.Sample/ReverseProxy.Code.Sample.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>Yarp.Sample</RootNamespace>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/ReverseProxy.Code.Sample/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Debug\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Code.Sample/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Config.Sample/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddControllers();\nbuilder.Services.AddReverseProxy()\n    .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"));\n\nvar app = builder.Build();\n\napp.UseCors();\napp.MapReverseProxy();\n\napp.Run();\n"
  },
  {
    "path": "samples/ReverseProxy.Config.Sample/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"https://localhost:44356/\",\n      \"sslPort\": 44356\n    }\n  },\n  \"profiles\": {\n    \"ReverseProxy.Config.Sample\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Config.Sample/README.md",
    "content": "# Configuration Sample\n\nThis sample shows off the properties that can be supplied to YARP via configuration. In this case its using appsettings.json, but the same configuration properties could be supplied through code instead. See [ReverseProxy.Code.Sample](../ReverseProxy.Code.Sample) for an example\n\nThe [configuration file](appsettings.json) includes all the settings that are currently supported by YARP.\n\nThe configuration shows two routes and two clusters:\n- minimalRoute which will map to any URL\n  - Routes to cluster \"minimalCluster\" which has one destination \"www.example.com\"\n- allRouteProps route\n  - Which includes further restrictions:\n    - Path must be /download/*\n    - Host must be localhost, www.aaaaa.com or www.bbbbb.com\n    - Http Method must be GET or POST\n    - Must have a header \"MyCustomHeader\" with a value of \"value1\", \"value2\" or \"another value\"\n    - A \"MyHeader\" header will be added with the value \"MyValue\"\n    - Must have a query parameter \"MyQueryParameter\" with a value of \"value1\", \"value2\" or \"another value\"\n  - This will route to cluster \"allClusterProps\" which has 2 destinations - https://dotnet.microsoft.com and https://10.20.30.40 \n    - Requests will be [load balanced](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/load-balancing) between destinations using a \"PowerOfTwoChoices\" algorithm, which picks two destinations at random, then uses the least loaded of the two.\n    - It includes [session affinity](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/session-affinity) using a cookie which will ensure subsequent requests from the same client go to the same host.\n    - It is configured to have both active and passive [health checks](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/dests-health-checks) - note the second destination will timeout for active checks (unless you have a host with that IP on your network)\n    - It includes [HttpClient configuration](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/http-client-config) setting outbound connection properties\n    - HttpRequest properties defaulting to HTTP/2 with a 2min timeout\n\nThe other files in the sample are the same as the getting started instructions.\n\nTo make a request that would be successful against the second route, you will need a client request similar to:\n\n```bash\ncurl -v -k -X GET -H \"MyCustomHeader: value1\" https://localhost:5001/download?MyQueryParameter=value1\n```"
  },
  {
    "path": "samples/ReverseProxy.Config.Sample/ReverseProxy.Config.Sample.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>Yarp.Sample</RootNamespace>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/ReverseProxy.Config.Sample/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Debug\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Config.Sample/appsettings.json",
    "content": "{\n  // Base URLs the server listens on, must be configured independently of the routes below.\n  // Can also be configured via Kestrel/Endpoints, see https://docs.microsoft.com/aspnet/core/fundamentals/servers/kestrel/endpoints\n  \"Urls\": \"http://localhost:5000;https://localhost:5001\",\n\n  //Sets the Logging level for ASP.NET\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      // Uncomment to hide diagnostic messages from runtime and proxy\n      // \"Microsoft\": \"Warning\",\n      // \"Yarp\" : \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n\n  // Configuration for YARP\n  \"ReverseProxy\": {\n    // Routes tell the proxy which requests to forward\n    \"Routes\": {\n      \"minimalRoute\": {\n        // Matches anything and routes it to www.example.com\n        \"ClusterId\": \"minimalCluster\",\n        \"Match\": {\n          \"Path\": \"{**catch-all}\"\n        }\n      },\n      \"allRouteProps\": {\n        // matches /download/* and routes to \"allClusterProps\"\n        \"ClusterId\": \"allClusterProps\", // Name of one of the clusters\n        \"Order\": 0, // Lower numbers have higher precedence, default is 0\n        \"AuthorizationPolicy\": \"Anonymous\", // Name of the policy or \"Default\", \"Anonymous\"\n        \"CorsPolicy\": \"disable\", // Name of the CorsPolicy to apply to this route or \"default\", \"disable\"\n        \"Match\": { // Rules that have to be met for the route to match the request\n          \"Path\": \"/download/{**remainder}\", // The path to match using ASP.NET syntax.\n          \"Hosts\": [ \"localhost\", \"www.aaaaa.com\", \"www.bbbbb.com\" ], // The host names to match, unspecified is any\n          \"Methods\": [ \"GET\", \"PUT\" ], // The HTTP methods that match, unspecified is all\n          \"Headers\": [ // The headers to match, unspecified is any\n            {\n              \"Name\": \"MyCustomHeader\", // Name of the header\n              \"Values\": [ \"value1\", \"value2\", \"another value\" ], // Matches are against any of these values\n              \"Mode\": \"ExactHeader\", // or \"HeaderPrefix\", \"Exists\" , \"Contains\", \"NotContains\"\n              \"IsCaseSensitive\": true\n            }\n          ],\n          \"QueryParameters\": [ // The query parameters to match, unspecified is any\n            {\n              \"Name\": \"MyQueryParameter\", // Name of the query parameter\n              \"Values\": [ \"value1\", \"value2\", \"another value\" ], // Matches are against any of these values\n              \"Mode\": \"Exact\", // or \"Prefix\", \"Exists\" , \"Contains\", \"NotContains\"\n              \"IsCaseSensitive\": true\n            }\n          ]\n        },\n        \"Metadata\": { // List of key value pairs that can be used by custom extensions\n          \"MyName\": \"MyValue\"\n        },\n        \"Transforms\": [ // List of transforms. See ReverseProxy.Transforms.Sample for more details\n          {\n            \"RequestHeader\": \"MyHeader\",\n            \"Set\": \"MyValue\"\n          }\n        ]\n      }\n    },\n    // Clusters tell the proxy where and how to forward requests\n    \"Clusters\": { // Cluster with the minimum information\n      \"minimalCluster\": {\n        \"Destinations\": { // Specifies which back end servers requests should be routed to.\n          \"example.com\": { // name is used for logging and via extensibility\n            \"Address\": \"http://www.example.com\" // Should specify Protocol, Address/IP & Port, but not path\n          }\n        }\n      },\n      \"allClusterProps\": { // Cluster with all properties\n        \"Destinations\": { // Specifies which back end servers requests should be routed to.\n          \"first_destination\": { // name is used for logging and via extensibility\n            \"Address\": \"https://dotnet.microsoft.com\" // Should specify Protocol, Address/IP & Port, but not path\n          },\n          \"another_destination\": {\n            \"Address\": \"https://10.20.30.40\",\n            \"Health\": \"https://10.20.30.40:12345\", // override for active health checks\n            \"Host\": \"contoso\",\n            \"Metadata\": {\n              \"SomeKey\": \"SomeValue\"\n            }\n          }\n        },\n        \"LoadBalancingPolicy\": \"PowerOfTwoChoices\", // Alternatively \"First\", \"Random\", \"RoundRobin\", \"LeastRequests\"\n        \"SessionAffinity\": { // Ensures subsequent requests from a client go to the same destination server\n          \"Enabled\": true, // Defaults to 'false'\n          \"Policy\": \"HashCookie\", // Default, alternatively \"Cookie\" or \"CustomHeader\"\n          \"FailurePolicy\": \"Redistribute\", // default, alternatively \"Return503Error\"\n          \"AffinityKeyName\": \"MySessionCookieName\", // Required, no default\n          \"Cookie\": { // Options for cookie based session affinity\n            \"Path\": \"/\",\n            \"SameSite\": \"None\",\n            \"HttpOnly\": true,\n            \"Expiration\": \"00:30:00\",\n            \"Domain\": \"example.com\",\n            \"MaxAge\": \"08:00:00\",\n            \"SecurePolicy\": \"Always\",\n            \"IsEssential\": true\n          }\n        },\n        \"HealthCheck\": { // Ways to determine which destinations should be filtered out due to unhealthy state\n          \"Active\": { // Makes API calls to validate the health of each destination\n            \"Enabled\": true,\n            \"Interval\": \"00:00:10\", // How often to query for health data\n            \"Timeout\": \"00:00:10\", // Timeout for the health check request/response\n            \"Policy\": \"ConsecutiveFailures\", // Or other custom policy that has been registered\n            \"Path\": \"/favicon.ico\", // API endpoint to query for health state. Looks for 2XX response codes to indicate healthy state\n            // Typically something like \"/api/health\" but used favicon to enable sample to run\n            \"Query\": \"?healthCheck=true\" // Query string to append to the health check request\n          },\n          \"Passive\": { // Disables destinations based on HTTP response codes for proxy requests\n            \"Enabled\": true, // Defaults to false\n            \"Policy\": \"TransportFailureRate\", // Or other custom policy that has been registered\n            \"ReactivationPeriod\": \"00:00:10\" // how long before the destination is re-enabled\n          },\n          \"AvailableDestinationsPolicy\": \"HealthyOrPanic\" // Policy for which destinations can be used when sending requests\n        },\n        \"HttpClient\": { // Configuration of HttpClient instance used to contact destinations\n          \"SslProtocols\": [ \"Tls13\" ],\n          \"DangerousAcceptAnyServerCertificate\": true, // Disables destination cert validation\n          \"MaxConnectionsPerServer\": 1024, // Destination server can further limit this number\n          \"EnableMultipleHttp2Connections\": true,\n          \"RequestHeaderEncoding\": \"Latin1\", // How to interpret non ASCII characters in proxied request's header values\n          \"ResponseHeaderEncoding\": \"Latin1\", // How to interpret non ASCII characters in proxied request's response header values\n          \"WebProxy\": { // Optional proxy configuration for outgoing requests\n            \"Address\": \"http://127.0.0.1\",\n            \"BypassOnLocal\": true,\n            \"UseDefaultCredentials\": false\n          }\n        },\n        \"HttpRequest\": { // Options for sending request to destination\n          \"ActivityTimeout\": \"00:02:00\", // Activity timeout for the request\n          \"Version\": \"2\", // Http Version that should be tried first\n          \"VersionPolicy\": \"RequestVersionOrLower\", // Policy for which other versions can be be used\n          \"AllowResponseBuffering\": false\n        },\n        \"Metadata\": { // Custom Key/value pairs for extensibility\n          \"TransportFailureRateHealthPolicy.RateLimit\": \"0.5\", // Used by Passive health policy\n          \"MyKey\": \"MyValue\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.ConfigFilter.Sample/CustomConfigFilter.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.RegularExpressions;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.Sample\n{\n    public class CustomConfigFilter : IProxyConfigFilter\n    {\n        // Matches {{env_var_name}}\n        private readonly Regex _exp = new(\"\\\\{\\\\{(\\\\w+)\\\\}\\\\}\");\n\n        // Configuration filter for clusters, will be passed each cluster in turn, which it should either return as-is or\n        // clone and create a new version of with updated changes\n        //\n        // This sample looks at the destination addresses and any of the form {{key}} will be modified, looking up the key\n        // as an environment variable. This is useful when hosted in Azure etc, as it enables a simple way to replace\n        // destination addresses via the management console\n        public ValueTask<ClusterConfig> ConfigureClusterAsync(ClusterConfig origCluster, CancellationToken cancel)\n        {\n            // Each cluster has a dictionary of destinations, which is read-only, so we'll create a new one with our updates \n            var newDests = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase);\n\n            foreach (var d in origCluster.Destinations)\n            {\n                var origAddress = d.Value.Address;\n                if (_exp.IsMatch(origAddress))\n                {\n                    // Get the name of the env variable from the destination and lookup value\n                    var lookup = _exp.Matches(origAddress)[0].Groups[1].Value;\n                    var newAddress = System.Environment.GetEnvironmentVariable(lookup);\n\n                    if (string.IsNullOrWhiteSpace(newAddress))\n                    {\n                        throw new System.ArgumentException($\"Configuration Filter Error: Substitution for '{lookup}' in cluster '{d.Key}' not found as an environment variable.\");\n                    }\n\n                    // using c# 9 \"with\" to clone and initialize a new record\n                    var modifiedDest = d.Value with { Address = newAddress };\n                    newDests.Add(d.Key, modifiedDest);\n                }\n                else\n                {\n                    newDests.Add(d.Key, d.Value);\n                }\n            }\n            return new ValueTask<ClusterConfig>(origCluster with { Destinations = newDests });\n        }\n\n        public ValueTask<RouteConfig> ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel)\n        {\n            // Example: do not let config based routes take priority over code based routes.\n            // Lower numbers are higher priority. Code routes default to 0.\n            if (route.Order.HasValue && route.Order.Value < 1)\n            {\n                return new ValueTask<RouteConfig>(route with { Order = 1 });\n            }\n\n            return new ValueTask<RouteConfig>(route);\n        }\n    }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.ConfigFilter.Sample/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.Sample;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddControllers();\nbuilder.Services.AddReverseProxy()\n    .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"))\n    .AddConfigFilter<CustomConfigFilter>();\n\nvar app = builder.Build();\n\napp.MapReverseProxy();\n\napp.Run();\n"
  },
  {
    "path": "samples/ReverseProxy.ConfigFilter.Sample/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"https://localhost:44356/\",\n      \"sslPort\": 44356\n    }\n  },\n  \"profiles\": {\n    \"ReverseProxy.ConfigFilter.Sample\": {\n      \"commandName\": \"Project\",\n      \"environmentVariables\": {\n        \"Key\": \"Value\",\n        \"contoso\": \"https://contoso.com\",\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.ConfigFilter.Sample/README.md",
    "content": "# Configuration Filter Sample\n\nThis sample shows an example of a configuration filter. A configuration filter enables a callback as part of the configuration load where custom code can modify the configuration values for the proxy as they are loaded. This is valuable when the configuration file provides most of what you need, but you want to be able to tweak some values, but don't want to have to write a custom config provider.\n\n## IProxyConfigFilter\n\nThe bulk of the code is the CustomConfigFilter class which implements the IProxyConfigFilter interface. The interface has two methods which act as callbacks when Clusters and Routes are loaded from config. The methods will be called for each Route and Cluster, and as both are defined as Records, they are immutable so the method should return the same object as-is or a replacement.\n\n## CustomConfigFilter Class\n\n### ConfigureClusterAsync\nThis looks at the value of each destination and sees whether it matches the pattern {{env_var_name}}, and if so it treats it as an indirection to an environment variable, and replaces the destination address with the value of the named variable (if it exists).\n\n**Note:** AppSettings.json includes a destination of {{contoso}} which will be matched. The Properties/launchSettings.json file includes a definition of the environment variable, which will be used by Visual Studio and other tools when debugging with \"F5\"."
  },
  {
    "path": "samples/ReverseProxy.ConfigFilter.Sample/ReverseProxy.ConfigFilter.Sample.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>Yarp.Sample</RootNamespace>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/ReverseProxy.ConfigFilter.Sample/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Debug\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.ConfigFilter.Sample/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"Kestrel\": {\n    \"Endpoints\": {\n      \"https\": {\n        \"Url\": \"https://localhost:5001\"\n      },\n      \"http\": {\n        \"Url\": \"http://localhost:5000\"\n      }\n    }\n  },\n  \"ReverseProxy\": {\n    \"Routes\": {\n      \"route1\": {\n        \"ClusterId\": \"cluster1\",\n        \"Match\": {\n          \"Methods\": [ \"GET\", \"POST\" ],\n          \"Hosts\": [ \"localhost\" ],\n          \"Path\": \"/api/{**catch-all}\"\n        }\n      },\n      \"route2\": {\n        \"ClusterId\": \"cluster2\",\n        \"Match\": {\n          \"Path\": \"{**catch-all}\"\n        }\n      }\n    },\n    \"Clusters\": {\n      \"cluster1\": {\n        \"Destinations\": {\n          \"cluster1/destination1\": {\n            // Following value will be found by regex and looked up as an environment variable\n            \"Address\": \"{{contoso}}\"\n          },\n          \"cluster1/destination2\": {\n            \"Address\": \"https://bing.com/\"\n          }\n        }\n      },\n      \"cluster2\": {\n        \"Destinations\": {\n          \"cluster2/destination1\": {\n            \"Address\": \"https://example.com/\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Direct.Sample/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing System.Threading;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Transforms;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddHttpForwarder();\n\nvar app = builder.Build();\n\n// Configure our own HttpMessageInvoker for outbound calls for proxy operations\nvar httpClient = new HttpMessageInvoker(new SocketsHttpHandler\n{\n    UseProxy = false,\n    AllowAutoRedirect = false,\n    AutomaticDecompression = DecompressionMethods.None,\n    UseCookies = false,\n    EnableMultipleHttp2Connections = true,\n    ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current),\n    ConnectTimeout = TimeSpan.FromSeconds(15),\n});\n\n// Setup our own request transform class\nvar transformer = new CustomTransformer(); // or HttpTransformer.Default;\nvar requestOptions = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(100) };\n\napp.UseRouting();\n\n// When using IHttpForwarder for direct forwarding you are responsible for routing, destination discovery, load balancing, affinity, etc..\n// For an alternate example that includes those features see BasicYarpSample.\napp.Map(\"/test/{**catch-all}\", async (HttpContext httpContext, IHttpForwarder forwarder) =>\n{\n    var error = await forwarder.SendAsync(httpContext, \"https://example.com\", httpClient, requestOptions,\n        static (context, proxyRequest) =>\n        {\n            // Customize the query string:\n            var queryContext = new QueryTransformContext(context.Request);\n            queryContext.Collection.Remove(\"param1\");\n            queryContext.Collection[\"area\"] = \"xx2\";\n\n            // Assign the custom uri. Be careful about extra slashes when concatenating here. RequestUtilities.MakeDestinationAddress is a safe default.\n            proxyRequest.RequestUri = RequestUtilities.MakeDestinationAddress(\"https://example.com\", context.Request.Path, queryContext.QueryString);\n\n            // Suppress the original request header, use the one from the destination Uri.\n            proxyRequest.Headers.Host = null;\n\n            return default;\n        });\n\n    // Check if the proxy operation was successful\n    if (error != ForwarderError.None)\n    {\n        var errorFeature = httpContext.Features.Get<IForwarderErrorFeature>();\n        var exception = errorFeature.Exception;\n    }\n});\n\napp.MapForwarder(\"/sample/{id}\", \"https://httpbin.org\", \"/anything/{id}\");\napp.MapForwarder(\"/sample/anything/{id}\", \"https://httpbin.org\", b => b.AddPathRemovePrefix(\"/sample\"));\n\n// When using extension methods for registering IHttpForwarder providing configuration, transforms, and HttpMessageInvoker is optional (defaults will be used).\napp.MapForwarder(\"/{**catch-all}\", \"https://example.com\", requestOptions, transformer, httpClient);\n\napp.Run();\n\n/// <summary>\n/// Custom request transformation\n/// </summary>\ninternal sealed class CustomTransformer : HttpTransformer\n{\n    /// <summary>\n    /// A callback that is invoked prior to sending the proxied request. All HttpRequestMessage\n    /// fields are initialized except RequestUri, which will be initialized after the\n    /// callback if no value is provided. The string parameter represents the destination\n    /// URI prefix that should be used when constructing the RequestUri. The headers\n    /// are copied by the base implementation, excluding some protocol headers like HTTP/2\n    /// pseudo headers (\":authority\").\n    /// </summary>\n    /// <param name=\"httpContext\">The incoming request.</param>\n    /// <param name=\"proxyRequest\">The outgoing proxy request.</param>\n    /// <param name=\"destinationPrefix\">The uri prefix for the selected destination server which can be used to create\n    /// the RequestUri.</param>\n    public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken)\n    {\n        // Copy all request headers\n        await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken);\n\n        // Customize the query string:\n        var queryContext = new QueryTransformContext(httpContext.Request);\n        queryContext.Collection.Remove(\"param1\");\n        queryContext.Collection[\"area\"] = \"xx2\";\n\n        // Assign the custom uri. Be careful about extra slashes when concatenating here. RequestUtilities.MakeDestinationAddress is a safe default.\n        proxyRequest.RequestUri = RequestUtilities.MakeDestinationAddress(\"https://example.com\", httpContext.Request.Path, queryContext.QueryString);\n\n        // Suppress the original request header, use the one from the destination Uri.\n        proxyRequest.Headers.Host = null;\n    }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Direct.Sample/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"https://localhost:44356/\",\n      \"sslPort\": 44356\n    }\n  },\n  \"profiles\": {\n    \"ReverseProxy.Direct.Sample\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Direct.Sample/README.md",
    "content": "# YARP Direct Proxy Example\n\nSome customers who have an existing custom proxy for HTTP/1.1 are looking at YARP for a solution to handle more complex requests, such as HTTP/2, gRPC, WebSockets in future QUIC and HTTP/3. These applications have their own means of routing, load balancing, affinity, etc. and only need to forward a specific request to a specific destination. To make it easier to integrate YARP into these scenarios, the component that proxies requests is exposed via IHttpForwarder which can be called directly, and has few dependencies on the rest of YARP's infrastructure.\n\nThis example shows how to use IHttpForwarder to proxy a request to/from a specified destination.\n\nThe operation of the proxy can be thought of as:\n\n```text\n+-------------------+           +-------------------+           +-------------------+\n|      Client       |  ──(a)──► |      Proxy        |  ──(b)──► |    Destination    |\n|                   | ◄──(d)──  |                   | ◄──(c)──  |                   |\n+-------------------+           +-------------------+           +-------------------+\n```\n\n(a) and (b) show the *request* path, going from the client to the destination.\n(c) and (d) show the *response* path, going from the destination back to the client.\n\nNormal proxying comprises the following steps:\n\n| \\# | Step | Direction |\n| -- | ---- | --------- |\n| 1 | Disable ASP .NET Core limits for streaming requests | |\n| 2 | Create outgoing HttpRequestMessage | |\n| 3 | Setup copy of request body (background) | Client --► Proxy --► Destination |\n| 4 | Copy request headers | Client --► Proxy --► Destination |\n| 5 | Send the outgoing request using HttpMessageInvoker | Client --► Proxy --► Destination |\n| 6 | Copy response status line | Client ◄-- Proxy ◄-- Destination |\n| 7 | Copy response headers | Client ◄-- Proxy ◄-- Destination |\n| 8.1 | Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol. | |\n| 8.1.1 | Upgrade client channel | Client ◄--- Proxy ◄--- Destination |\n| 8.1.2 | Copy duplex streams and return | Client ◄--► Proxy ◄--► Destination |\n| 8.2 | Copy (normal) response body | Client ◄-- Proxy ◄-- Destination |\n| 9 | Copy response trailer headers and finish response | Client ◄-- Proxy ◄-- Destination |\n| 10 | Wait for completion of step 2: copying request body | Client --► Proxy --► Destination |\n\nTo enable control over mapping request and response fields and headers between the client and destination (steps 4 and 7 above), the HttpForwarder.ProxyAsync method takes a HttpTransformer. Your implementation can modify the request url, method, protocol version, response status code, or decide which headers are copied, modify them, or insert additional headers as required.\n\n**Note:** When using the HttpForwarder class directly there are some [header transforms](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/transforms) included by default, such as adding ```X-Forwarded-For``` and removing the original Host header. This is the same set of transforms included by default in the YARP pipeline model (see BasicYarpSample).\n\n## Files\n\nThe key functionality for this sample is all included in [Program.cs](Program.cs).\n"
  },
  {
    "path": "samples/ReverseProxy.Direct.Sample/ReverseProxy.Direct.Sample.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>Yarp.Sample</RootNamespace>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/ReverseProxy.Direct.Sample/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Debug\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Direct.Sample/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"Kestrel\": {\n    \"Endpoints\": {\n      \"https\": {\n        \"Url\": \"https://localhost:5001\"\n      },\n      \"http\": {\n        \"Url\": \"http://localhost:5000\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.HttpSysDelegation.Sample/README.md",
    "content": "# Http.sys Delegation Sample\nThis sample shows how to use YARP to delegate requests to other Http.sys request queues instead of or in addition to proxying requests. Using Http.sys delegation requires hosting YARP on [ASP.NET Core's Http.sys server](https://docs.microsoft.com/aspnet/core/fundamentals/servers/httpsys) and requests can only be delegated to other processes which use Http.sys for request processing (e.g. ASP.NET Core using Http.sys server or IIS).\n\n**Note:** delegation only works for ASP.NET Core 6+ running on new versions of Windows\n\n## Sample Projects\nThere are two projects as part of this sample. A sample Http.sys server where traffic will be delegated to and a YARP example which both proxies and delegates request depending on the route. Both projects use the minimal API style but this isn't a requirement.\n\n### ReverseProxy Delegation\n\nThere are four parts to enable YARP delegation support:\n- Use the ASP.NET Core Http.sys server\n```c#\nbuilder.WebHost.UseHttpSys();\n```\n- Add YARP services\n```c#\nbuilder.Services.AddReverseProxy()\n    .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"));\n```\n- Add YARP to the request pipeline. \n\nYou need to use the overload that allows you to define the middleware used in the YARP pipeline. \n```c#\napp.MapReverseProxy(proxyPipeline =>\n{\n    // Add the three middleware YARP adds by default plus the Http.sys delegation middleware\n    proxyPipeline.UseSessionAffinity(); // Has no affect on delegation destinations because the response doesn't go through YARP\n    proxyPipeline.UseLoadBalancing();\n    proxyPipeline.UsePassiveHealthChecks();\n    proxyPipeline.UseHttpSysDelegation();\n});\n```\n- Add a ReverseProxy section to appsettings.json. \n\nConfiguration is almost identical to how YARP in typically configured. The only difference is, for destinations which should use delegation, they have metadata which indicates the Http.sys queue name to delegate the request to.\n```json\n\"Destinations\": {\n  \"SampleHttpSysServer\": {\n    \"Address\": \"http://localhost:5600/\",\n    \"Metadata\": {\n      \"HttpSysDelegationQueue\": \"SampleHttpSysServerQueue\"\n    }\n  }\n}\n```\n\n## Usage\nTo run the sample:\n1. Start the SampleHttpSysServer project `dotnet run --project SampleHttpSysServer\\SampleHttpSysServer.csproj`\n2. Start the ReverseProxy.HttpSysDelegation.Sample project `dotnet run --project ReverseProxy\\ReverseProxy.HttpSysDelegation.Sample.csproj`\n\nBy default, the SampleHttpSysServer will listen to http://localhost:5600 and the ReverseProxy will listen to http://localhost:5500. The ReverseProxy will delegate any requests under the path http://localhost:5500/delegate to the SampleHttpSysServer. Any other path will be proxied to https://httpbin.org/.\n"
  },
  {
    "path": "samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Diagnostics;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nDebug.Assert(OperatingSystem.IsWindows());\nbuilder.WebHost.UseHttpSys();\nbuilder.Services.AddReverseProxy()\n    .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"));\n\nvar app = builder.Build();\n\napp.MapReverseProxy(proxyPipeline =>\n{\n    proxyPipeline.UseSessionAffinity(); // Has no effect on delegation destinations because the response doesn't go through YARP\n    proxyPipeline.UseLoadBalancing();\n    proxyPipeline.UsePassiveHealthChecks();\n    proxyPipeline.UseHttpSysDelegation();\n});\n\napp.Run();\n"
  },
  {
    "path": "samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"ReverseProxy.HttpSysDelegation\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"http://localhost:5500\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/ReverseProxy.HttpSysDelegation.Sample.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.HttpSysDelegation.Sample/ReverseProxy/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"ReverseProxy\": {\n    \"Routes\": {\n      \"delegateroute\": {\n        \"ClusterId\": \"delegatecluster\",\n        \"Match\": {\n          \"Path\": \"/delegate/{**catch-all}\"\n        }\n      },\n      \"proxyroute\": {\n        \"ClusterId\": \"proxycluster\",\n        \"Match\": {\n          \"Path\": \"{**catch-all}\"\n        }\n      }\n    },\n    \"Clusters\": {\n      \"delegatecluster\": {\n        \"Destinations\": {\n          \"SampleHttpSysServer\": {\n            \"Address\": \"http://localhost:5600/\",\n            \"Metadata\": {\n              \"HttpSysDelegationQueue\": \"SampleHttpSysServerQueue\"\n            }\n          }\n        }\n      },\n      \"proxycluster\": {\n        \"Destinations\": {\n          \"httpbin.org\": {\n            \"Address\": \"https://httpbin.org/\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Diagnostics;\nusing Microsoft.AspNetCore.Server.HttpSys;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nDebug.Assert(OperatingSystem.IsWindows());\nbuilder.WebHost.UseHttpSys(options =>\n{\n    options.RequestQueueName = \"SampleHttpSysServerQueue\";\n    options.RequestQueueMode = RequestQueueMode.Create;\n});\n\nvar app = builder.Build();\n\napp.Run(async context =>\n{\n    await context.Response.WriteAsync($\"Hello World! (PID: {Environment.ProcessId})\");\n});\napp.Run();\n"
  },
  {
    "path": "samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"SampleHttpSysServer\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"http://localhost:5600\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/SampleHttpSysServer.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n  </PropertyGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.HttpSysDelegation.Sample/SampleHttpSysServer/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "samples/ReverseProxy.LetsEncrypt.Sample/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddLettuceEncrypt();\n\nbuilder.Services.AddControllers();\n// Add the reverse proxy capability to the server\nbuilder.Services.AddReverseProxy()\n    // Initialize the reverse proxy from the \"ReverseProxy\" section of configuration\n    .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"));\n\nvar app = builder.Build();\n\n// Register the reverse proxy routes\napp.MapReverseProxy();\n\napp.Run();\n"
  },
  {
    "path": "samples/ReverseProxy.LetsEncrypt.Sample/Properties/launchSettings.json",
    "content": "﻿{\n  \"$schema\": \"http://json.schemastore.org/launchsettings.json\",\n  \"profiles\": {\n    \"ReverseProxy.LetsEncrypt.Sample\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"launchUrl\": \"\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.LetsEncrypt.Sample/README.md",
    "content": "# Lets Encrypt Sample\n\n[Lets Encrypt](https://letsencrypt.org/) is a certificate authority (CA) that provides HTTPS (SSL/TLS) certificates for free. This sample shows how to add Lets Encrypt for TLS termination in YARP by integrating with [LettuceEncrypt](https://github.com/natemcmaster/LettuceEncrypt). It allows to set up TLS between the client and YARP with minimal configuration.\n\nThe sample includes the following parts:\n\n- **[Program.cs](Program.cs)**\n  It calls `IServiceCollection.AddLettuceEncrypt` in the `ConfigureServices` method.\n\n- **[appsettings.json](appsettings.json)**\n  Sets up the required options for LettuceEncrypt including:\n  - \"DomainNames\" - at least one domain name is required\n  - \"EmailAddress\" - email address must be specified to register with the certificate authority\n"
  },
  {
    "path": "samples/ReverseProxy.LetsEncrypt.Sample/ReverseProxy.LetsEncrypt.Sample.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"LettuceEncrypt\" Version=\"$(LettuceEncryptVersion)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/ReverseProxy.LetsEncrypt.Sample/appsettings.json",
    "content": "{\n  \"Kestrel\": {\n    \"Endpoints\": {\n      \"Http\": {\n        \"Url\": \"http://*:80\"\n      },\n      \"Https\": {\n        \"Url\": \"https://*:443\"\n      }\n    }\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"ReverseProxy\": {\n    \"Routes\": {\n      \"route1\": {\n        \"ClusterId\": \"cluster1\",\n        \"Match\": {\n          \"Path\": \"{**catch-all}\"\n        }\n      }\n    },\n    \"Clusters\": {\n      \"cluster1\": {\n        \"Destinations\": {\n          \"destination1\": {\n            \"Address\": \"https://example.com/\"\n          }\n        }\n      }\n    }\n  },\n  \"LettuceEncrypt\": {\n    // Set this to automatically accept the terms of service of your certificate authority.\n    // If you don't set this in config, you will need to press \"y\" whenever the application starts\n    \"AcceptTermsOfService\": true,\n\n    // You must specify at least one domain name\n    \"DomainNames\": [ \"example.com\" ],\n\n    // You must specify an email address to register with the certificate authority\n    \"EmailAddress\": \"it-admin@example.com\"\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Metrics.Sample/ForwarderMetricsConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Yarp.Telemetry.Consumption;\n\nnamespace Yarp.Sample\n{\n    public sealed class ForwarderMetricsConsumer : IMetricsConsumer<ForwarderMetrics>\n    {\n        public void OnMetrics(ForwarderMetrics previous, ForwarderMetrics current)\n        {\n            var elapsed = current.Timestamp - previous.Timestamp;\n            var newRequests = current.RequestsStarted - previous.RequestsStarted;\n            Console.Title = $\"Forwarded {current.RequestsStarted} requests ({newRequests} in the last {(int)elapsed.TotalMilliseconds} ms)\";\n        }\n    }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Metrics.Sample/ForwarderTelemetryConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.Telemetry.Consumption;\n\nnamespace Yarp.Sample\n{\n    public sealed class ForwarderTelemetryConsumer : IForwarderTelemetryConsumer\n    {\n        public void OnForwarderStart(DateTime timestamp, string destinationPrefix)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.ProxyStartOffset = metrics.CalcOffset(timestamp);\n        }\n\n        public void OnForwarderStop(DateTime timestamp, int statusCode)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.ProxyStopOffset = metrics.CalcOffset(timestamp);\n        }\n\n        public void OnForwarderFailed(DateTime timestamp, ForwarderError error)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.Error = error;\n        }\n\n        public void OnContentTransferred(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime, TimeSpan firstReadTime)\n        {\n            var metrics = PerRequestMetrics.Current;\n\n            if (isRequest)\n            {\n                metrics.RequestBodyLength = contentLength;\n                metrics.RequestContentIops = iops;\n            }\n            else\n            {\n                // We don't get a content stop from http as it is returning a stream that is up to the consumer to\n                // read, but we know its ended here.\n                metrics.HttpResponseContentStopOffset = metrics.CalcOffset(timestamp);\n                metrics.ResponseBodyLength = contentLength;\n                metrics.ResponseContentIops = iops;\n            }\n        }\n\n        public void OnForwarderInvoke(DateTime timestamp, string clusterId, string routeId, string destinationId)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.RouteInvokeOffset = metrics.CalcOffset(timestamp);\n            metrics.RouteId = routeId;\n            metrics.ClusterId = clusterId;\n            metrics.DestinationId = destinationId;\n        }\n    }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Metrics.Sample/HttpClientTelemetryConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing Yarp.Telemetry.Consumption;\n\nnamespace Yarp.Sample\n{\n    public sealed class HttpClientTelemetryConsumer : IHttpTelemetryConsumer\n    {\n        public void OnRequestStart(DateTime timestamp, string scheme, string host, int port, string pathAndQuery, int versionMajor, int versionMinor, HttpVersionPolicy versionPolicy)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.HttpRequestStartOffset = metrics.CalcOffset(timestamp);\n        }\n\n        public void OnRequestStop(DateTime timestamp)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.HttpRequestStopOffset = metrics.CalcOffset(timestamp);\n        }\n\n        public void OnConnectionEstablished(DateTime timestamp, int versionMajor, int versionMinor)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.HttpConnectionEstablishedOffset = metrics.CalcOffset(timestamp);\n        }\n\n        public void OnRequestLeftQueue(DateTime timestamp, TimeSpan timeOnQueue, int versionMajor, int versionMinor)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.HttpRequestLeftQueueOffset = metrics.CalcOffset(timestamp);\n        }\n\n        public void OnRequestHeadersStart(DateTime timestamp)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.HttpRequestHeadersStartOffset = metrics.CalcOffset(timestamp);\n        }\n\n        public void OnRequestHeadersStop(DateTime timestamp)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.HttpRequestHeadersStopOffset = metrics.CalcOffset(timestamp);\n        }\n\n        public void OnRequestContentStart(DateTime timestamp)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.HttpRequestContentStartOffset = metrics.CalcOffset(timestamp);\n        }\n\n        public void OnRequestContentStop(DateTime timestamp, long contentLength)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.HttpRequestContentStopOffset = metrics.CalcOffset(timestamp);\n        }\n\n        public void OnResponseHeadersStart(DateTime timestamp)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.HttpResponseHeadersStartOffset = metrics.CalcOffset(timestamp);\n        }\n\n        public void OnResponseHeadersStop(DateTime timestamp)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.HttpResponseHeadersStopOffset = metrics.CalcOffset(timestamp);\n        }\n    }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Metrics.Sample/PerRequestMetrics.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading;\nusing Yarp.ReverseProxy.Forwarder;\nusing System.Text.Json;\n\nnamespace Yarp.Sample\n{\n    public class PerRequestMetrics\n    {\n        private static readonly AsyncLocal<PerRequestMetrics> _local = new AsyncLocal<PerRequestMetrics>();\n        private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { WriteIndented = true };\n\n        // Ensure we are only fetched via the factory\n        private PerRequestMetrics() { }\n\n        /// <summary>\n        /// Factory to instantiate or restore the metrics from AsyncLocal storage\n        /// </summary>\n        public static PerRequestMetrics Current => _local.Value ??= new PerRequestMetrics();\n\n        // Time the request was started via the pipeline\n        public DateTime StartTime { get; set; }\n\n        // Offset Tics for each part of the proxy operation\n        public float RouteInvokeOffset { get; set; }\n        public float ProxyStartOffset { get; set; }\n        public float HttpRequestStartOffset { get; set; }\n        public float HttpConnectionEstablishedOffset { get; set; }\n        public float HttpRequestLeftQueueOffset { get; set; }\n\n        public float HttpRequestHeadersStartOffset { get; set; }\n        public float HttpRequestHeadersStopOffset { get; set; }\n        public float HttpRequestContentStartOffset { get; set; }\n        public float HttpRequestContentStopOffset { get; set; }\n\n        public float HttpResponseHeadersStartOffset { get; set; }\n        public float HttpResponseHeadersStopOffset { get; set; }\n        public float HttpResponseContentStopOffset { get; set; }\n\n        public float HttpRequestStopOffset { get; set; }\n        public float ProxyStopOffset { get; set; }\n\n        // Info about the request\n        public ForwarderError Error { get; set; }\n        public long RequestBodyLength { get; set; }\n        public long ResponseBodyLength { get; set; }\n        public long RequestContentIops { get; set; }\n        public long ResponseContentIops { get; set; }\n        public string DestinationId { get; set; }\n        public string ClusterId { get; set; }\n        public string RouteId { get; set; }\n\n        public string ToJson()\n        {\n            return JsonSerializer.Serialize(this, _jsonOptions);\n        }\n\n        public float CalcOffset(DateTime timestamp)\n        {\n            return (float)(timestamp - StartTime).TotalMilliseconds;\n        }\n    }\n}\n\n"
  },
  {
    "path": "samples/ReverseProxy.Metrics.Sample/PerRequestYarpMetricCollectionMiddleware.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.Sample\n{\n    /// <summary>\n    ///  Middleware that collects YARP metrics and logs them at the end of each request\n    /// </summary>\n    public class PerRequestYarpMetricCollectionMiddleware\n    {\n        // Required for middleware\n        private readonly RequestDelegate _next;\n        // Supplied via DI\n        private readonly ILogger<PerRequestYarpMetricCollectionMiddleware> _logger;\n\n        public PerRequestYarpMetricCollectionMiddleware(RequestDelegate next, ILogger<PerRequestYarpMetricCollectionMiddleware> logger)\n        {\n            _logger = logger;\n            _next = next;\n        }\n\n        /// <summary>\n        /// Entrypoint for being called as part of the request pipeline\n        /// </summary>\n        public async Task InvokeAsync(HttpContext context)\n        {\n            var metrics = PerRequestMetrics.Current;\n            metrics.StartTime = DateTime.UtcNow;\n\n            // Call the next steps in the middleware, including the proxy\n            await _next(context);\n\n            // Called after the other middleware steps have completed\n            // Write the info to the console via ILogger. In a production scenario you probably want\n            // to write the results to your telemetry systems directly.\n            _logger.LogInformation(\"PerRequestMetrics: \" + metrics.ToJson());\n        }\n    }\n\n    /// <summary>\n    /// Helper to aid with registration of the middleware\n    /// </summary>\n    public static class YarpMetricCollectionMiddlewareHelper\n    {\n        public static IApplicationBuilder UsePerRequestMetricCollection(\n          this IApplicationBuilder builder)\n        {\n            return builder.UseMiddleware<PerRequestYarpMetricCollectionMiddleware>();\n        }\n    }\n}\n\n"
  },
  {
    "path": "samples/ReverseProxy.Metrics.Sample/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.Sample;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nvar services = builder.Services;\n\nservices.AddControllers();\n\nservices.AddReverseProxy()\n    .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"));\n\nservices.AddHttpContextAccessor();\n\n// Interface that collects general metrics about the proxy forwarder\nservices.AddMetricsConsumer<ForwarderMetricsConsumer>();\n\n// Registration of a consumer to events for proxy forwarder telemetry\nservices.AddTelemetryConsumer<ForwarderTelemetryConsumer>();\n\n// Registration of a consumer to events for HttpClient telemetry\nservices.AddTelemetryConsumer<HttpClientTelemetryConsumer>();\n\nservices.AddTelemetryConsumer<WebSocketsTelemetryConsumer>();\n\nvar app = builder.Build();\n\n// Custom middleware that collects and reports the proxy metrics\n// Placed at the beginning so that it is the first and last thing to run for each request\napp.UsePerRequestMetricCollection();\n\n// Middleware used to intercept the WebSocket connection and collect telemetry exposed to WebSocketsTelemetryConsumer\napp.UseWebSocketsTelemetry();\n\napp.MapReverseProxy();\n\napp.Run();\n"
  },
  {
    "path": "samples/ReverseProxy.Metrics.Sample/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"https://localhost:44356/\",\n      \"sslPort\": 44356\n    }\n  },\n  \"profiles\": {\n    \"ReverseProxy.Metrics.Sample\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Metrics.Sample/README.md",
    "content": "# ReverseProxy.Metrics.Sample\n\nThis sample demonstrates how to use the ReverseProxy.Telemetry.Consumption library to listen to telemetry data from YARP.\nIn this case it uses the events to create a per-request data structure with detailed timings for each operation that takes place as part of the proxy operation.\n\nInternally YARP uses EventSource to collect telemetry events and metrics from a number of subsystems that are used to process the requests.\nThe YARP telemetry library provides wrapper classes that collect these events metrics and make them available for Consumption.\nTo listen for the metrics you register classes with DI that implement an interface for each subsystem.\n\nThe subsystems are:\n- **Proxy** which represents the overall proxy operation, and success or failure. \n\n  Events include:\n    - When proxy requests are started and stopped\n    - When request/response bodies are processed\n\n  Metrics include:\n    - Number of requests started\n    - Number of request in flight\n    - Number of requests that have failed\n\n- **Kestrel** which is the web server that handles incoming requests.\n\n  Events include:\n    - When requests are started/stopped or fail\n    \n  Metrics include:\n    - Connection Rate - how many connections are opened a second\n    - Total number of connections\n    - Number of TLS handshakes\n    - Incoming queue length\n\n- **Http** which is the HttpClient which makes outgoing requests to the destination servers. \n\n  Events include:\n    - When connections are created\n    - When requests are started/stopped or fail\n    - When headers/contents are sent/received\n    - When requests are dequeued as connections become available\n\n  Metrics include:\n    - Number of outgoing requests started\n    - Number of requests failed\n    - Number of active requests\n    - Number of outbound connections\n\n- **Sockets** which includes events around connection attempts & metrics about the amount of data sent and received\n\n- **NameResolution** which includes events around name resolution attempts & metrics about DNS lookups of destinations\n\n- **NetSecurity** which includes events around SslStream handshakes & metrics about the number and latency of handshakes per protocol\n\n## Key Files\n\nThe following files are key to implementing the features described above:\n\n### Program.cs\n\nPerforms registrtion of the proxy, the listener classes and a custom ASP.NET middleware step that starts per-request telemetry and reports the results when complete\n\n### ProxyTelemetryConsumer.cs\n\nListens to events from the proxy telemetry and records timings and info about the high level processing involved in proxying a request.\n\n### HttpTelemetryConsumer.cs\n\nListens to events from the HttpClient telemetry and records timings and info about the outbound request and response from the destination server.\n\n### PerRequestMetrics.cs\n\nClass to store the metrics on a per request basis. Instances are stored in AsyncLocal storage for the duration of the request. \n\n### PerRequestYarpMetricCollectionMiddleware.cs\n\nASP.NET Core middleware that is the first and last thing called as part of the ASP.NET handling of the request. It initializes the per-request metrics and logs the results at the end of the request.\n"
  },
  {
    "path": "samples/ReverseProxy.Metrics.Sample/ReverseProxy.Metrics.Sample.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>Yarp.Sample</RootNamespace>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\TelemetryConsumption\\Yarp.Telemetry.Consumption.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/ReverseProxy.Metrics.Sample/WebSocketsTelemetryConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.Extensions.Logging;\nusing Yarp.Telemetry.Consumption;\n\nnamespace Yarp.Sample\n{\n    public sealed class WebSocketsTelemetryConsumer : IWebSocketsTelemetryConsumer\n    {\n        private readonly ILogger<WebSocketsTelemetryConsumer> _logger;\n\n        public WebSocketsTelemetryConsumer(ILogger<WebSocketsTelemetryConsumer> logger)\n        {\n            ArgumentNullException.ThrowIfNull(logger);\n            _logger = logger;\n        }\n\n        public void OnWebSocketClosed(DateTime timestamp, DateTime establishedTime, WebSocketCloseReason closeReason, long messagesRead, long messagesWritten)\n        {\n            _logger.LogInformation($\"WebSocket connection closed ({closeReason}) after reading {messagesRead} and writing {messagesWritten} messages over {(timestamp - establishedTime).TotalSeconds:N2} seconds.\");\n        }\n    }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Metrics.Sample/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Debug\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Metrics.Sample/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      // \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"Kestrel\": {\n    \"Endpoints\": {\n      \"http\": {\n        \"Url\": \"http://localhost:5000\"\n      },\n      \"https\": {\n        \"Url\": \"https://localhost:5001\"\n      }\n    }\n  },\n  \"ReverseProxy\": {\n    \"Routes\": {\n      \"route1\": {\n        \"ClusterId\": \"cluster1\",\n        \"Match\": {\n          \"Path\": \"{**catch-all}\"\n        }\n      }\n    },\n    \"Clusters\": {\n      \"cluster1\": {\n        \"Destinations\": {\n          \"cluster1/destination1\": {\n            \"Address\": \"https://example.com/\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Transforms.Sample/MyTransformFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing Yarp.ReverseProxy.Transforms;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.Sample\n{\n    internal sealed class MyTransformFactory : ITransformFactory\n    {\n        public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)\n        {\n            if (transformValues.TryGetValue(\"CustomTransform\", out var value))\n            {\n                if (string.IsNullOrEmpty(value))\n                {\n                    context.Errors.Add(new ArgumentException(\"A non-empty CustomTransform value is required\"));\n                }\n\n                return true; // Matched\n            }\n            return false;\n        }\n\n        public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)\n        {\n            if (transformValues.TryGetValue(\"CustomTransform\", out var value))\n            {\n                if (string.IsNullOrEmpty(value))\n                {\n                    throw new ArgumentException(\"A non-empty CustomTransform value is required\");\n                }\n\n                context.AddRequestTransform(transformContext =>\n                {\n                    transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey<string>(\"CustomTransform\"), value);\n                    return default;\n                });\n\n                return true;\n            }\n\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Transforms.Sample/MyTransformProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing Yarp.ReverseProxy.Transforms;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.Sample\n{\n    internal sealed class MyTransformProvider : ITransformProvider\n    {\n        public void ValidateRoute(TransformRouteValidationContext context)\n        {\n            // Check all routes for a custom property and validate the associated transform data.\n            if (context.Route.Metadata?.TryGetValue(\"CustomMetadata\", out var value) ?? false)\n            {\n                if (string.IsNullOrEmpty(value))\n                {\n                    context.Errors.Add(new ArgumentException(\"A non-empty CustomMetadata value is required\"));\n                }\n            }\n        }\n\n        public void ValidateCluster(TransformClusterValidationContext context)\n        {\n            // Check all clusters for a custom property and validate the associated transform data.\n            if (context.Cluster.Metadata?.TryGetValue(\"CustomMetadata\", out var value) ?? false)\n            {\n                if (string.IsNullOrEmpty(value))\n                {\n                    context.Errors.Add(new ArgumentException(\"A non-empty CustomMetadata value is required\"));\n                }\n            }\n        }\n\n        public void Apply(TransformBuilderContext transformBuildContext)\n        {\n            // Check all routes for a custom property and add the associated transform.\n            if ((transformBuildContext.Route.Metadata?.TryGetValue(\"CustomMetadata\", out var value) ?? false)\n                || (transformBuildContext.Cluster?.Metadata?.TryGetValue(\"CustomMetadata\", out value) ?? false))\n            {\n                if (string.IsNullOrEmpty(value))\n                {\n                    throw new ArgumentException(\"A non-empty CustomMetadata value is required\");\n                }\n\n                transformBuildContext.AddRequestTransform(transformContext =>\n                {\n                    transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey<string>(\"CustomMetadata\"), value);\n                    return default;\n                });\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Transforms.Sample/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http.Headers;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.ReverseProxy.Transforms;\nusing Yarp.Sample;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddControllers();\nbuilder.Services.AddReverseProxy()\n    .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"))\n    .AddTransforms<MyTransformProvider>() // Adds custom transforms via code.\n    .AddTransformFactory<MyTransformFactory>() // Adds custom transforms via config.\n    .AddTransforms(transformBuilderContext =>  // Add transforms inline\n    {\n        // For each route+cluster pair decide if we want to add transforms, and if so, which?\n        // This logic is re-run each time a route is rebuilt.\n\n        transformBuilderContext.AddPathPrefix(\"/prefix\");\n\n        // Only do this for routes that require auth.\n        if (string.Equals(\"token\", transformBuilderContext.Route.AuthorizationPolicy))\n        {\n            transformBuilderContext.AddRequestTransform(async transformContext =>\n            {\n                // AuthN and AuthZ will have already been completed after request routing.\n                var ticket = await transformContext.HttpContext.AuthenticateAsync(\"token\");\n                var tokenService = transformContext.HttpContext.RequestServices.GetRequiredService<TokenService>();\n                var token = await tokenService.GetAuthTokenAsync(ticket.Principal);\n                transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", token);\n            });\n        }\n    });\n\nvar app = builder.Build();\n\napp.UseAuthentication();\napp.UseAuthorization();\n\n// Register the reverse proxy routes\napp.MapReverseProxy();\n\napp.Run();\n"
  },
  {
    "path": "samples/ReverseProxy.Transforms.Sample/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"https://localhost:44356/\",\n      \"sslPort\": 44356\n    }\n  },\n  \"profiles\": {\n    \"ReverseProxy.Transforms.Sample\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Transforms.Sample/ReverseProxy.Transforms.Sample.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>Yarp.Sample</RootNamespace>\n    <LangVersion>latest</LangVersion>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/ReverseProxy.Transforms.Sample/TokenService.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Security.Claims;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Sample\n{\n    internal sealed class TokenService\n    {\n        internal Task<string> GetAuthTokenAsync(ClaimsPrincipal user)\n        {\n            return Task.FromResult(user.Identity.Name);\n        }\n    }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Transforms.Sample/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Debug\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/ReverseProxy.Transforms.Sample/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"Kestrel\": {\n    \"Endpoints\": {\n      \"https\": {\n        \"Url\": \"https://localhost:5001\"\n      },\n      \"http\": {\n        \"Url\": \"http://localhost:5000\"\n      }\n    }\n  },\n  \"ReverseProxy\": {\n    \"Routes\": {\n      \"route1\": {\n        \"ClusterId\": \"cluster1\",\n        \"Match\": {\n          \"Path\": \"{**catch-all}\"\n        },\n        \"Transforms\": [\n          { \"PathPrefix\": \"/prefix\" },\n          { \"RequestHeadersCopy\": true },\n          { \"RequestHeaderOriginalHost\": false },\n          {\n            \"RequestHeader\": \"foo0\",\n            \"Append\": \"bar\"\n          },\n          {\n            \"RequestHeader\": \"foo1\",\n            \"Set\": \"bar, baz\"\n          },\n          {\n            \"RequestHeader\": \"clearMe\",\n            \"Set\": \"\"\n          },\n          {\n            \"ResponseHeader\": \"foo\",\n            \"Append\": \"bar\",\n            \"When\": \"Always\"\n          },\n          {\n            \"ResponseTrailer\": \"foo\",\n            \"Append\": \"trailer\",\n            \"When\": \"Always\"\n          },\n          {\n            \"CustomTransform\": \"custom value\"\n          }\n        ]\n      }\n    },\n    \"Clusters\": {\n      \"cluster1\": {\n        \"Metadata\": {\n          \"CustomMetadata\": \"custom value\"\n        },\n        \"Destinations\": {\n          \"cluster1/destination1\": {\n            \"Address\": \"https://example.com\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/SampleServer/Controllers/HealthController.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace SampleServer.Controllers\n{\n    /// <summary>\n    /// Controller for active health check probes.\n    /// </summary>\n    [ApiController]\n    public class HealthController : ControllerBase\n    {\n        private static volatile int _count;\n        /// <summary>\n        /// Returns 200 if server is healthy.\n        /// </summary>\n        [HttpGet]\n        [Route(\"/api/health\")]\n        public IActionResult CheckHealth()\n        {\n            _count++;\n            // Simulate temporary health degradation.\n            return _count % 10 < 4 ? Ok() : StatusCode(500);\n        }\n    }\n}\n"
  },
  {
    "path": "samples/SampleServer/Controllers/HttpController.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace SampleServer.Controllers\n{\n    /// <summary>\n    /// Sample controller.\n    /// </summary>\n    [ApiController]\n    public class HttpController : ControllerBase\n    {\n        /// <summary>\n        /// Returns a 200 response.\n        /// </summary>\n        [HttpGet]\n        [Route(\"/api/noop\")]\n        public void NoOp()\n        {\n        }\n\n        /// <summary>\n        /// Returns a 200 response dumping all info from the incoming request.\n        /// </summary>\n        [HttpGet, HttpPost]\n        [Route(\"/api/dump\")]\n        [Route(\"/{**catchall}\", Order = int.MaxValue)] // Make this the default route if nothing matches\n        public async Task<IActionResult> Dump()\n        {\n            var result = new {\n                Request.Protocol,\n                Request.Method,\n                Request.Scheme,\n                Host = Request.Host.Value,\n                PathBase = Request.PathBase.Value,\n                Path = Request.Path.Value,\n                Query = Request.QueryString.Value,\n                Headers = Request.Headers.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()),\n                Time = DateTimeOffset.UtcNow,\n                Body = await new StreamReader(Request.Body).ReadToEndAsync(),\n            };\n\n            return Ok(result);\n        }\n\n        /// <summary>\n        /// Returns a 200 response dumping all info from the incoming request.\n        /// </summary>\n        [HttpGet]\n        [Route(\"/api/statuscode\")]\n        public void Status(int statusCode)\n        {\n            Response.StatusCode = statusCode;\n        }\n\n        /// <summary>\n        /// Returns a 200 response dumping all info from the incoming request.\n        /// </summary>\n        [HttpGet]\n        [Route(\"/api/headers\")]\n        public void Headers([FromBody] Dictionary<string, string> headers)\n        {\n            foreach (var (key, value) in headers)\n            {\n                Response.Headers[key] = value;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "samples/SampleServer/Controllers/WebSocketsController.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.WebSockets;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Extensions.Logging;\n\nnamespace SampleServer.Controllers\n{\n    /// <summary>\n    /// Sample controller.\n    /// </summary>\n    [ApiController]\n    public class WebSocketsController : ControllerBase\n    {\n        private readonly ILogger<WebSocketsController> _logger;\n\n        /// <summary>\n        /// Initializes a new instance of the <see cref=\"WebSocketsController\" /> class.\n        /// </summary>\n        public WebSocketsController(ILogger<WebSocketsController> logger)\n        {\n            _logger = logger;\n        }\n\n        /// <summary>\n        /// Returns a 200 response.\n        /// </summary>\n        [HttpGet]\n        [Route(\"/api/websockets\")]\n        public async Task WebSockets()\n        {\n            if (!HttpContext.WebSockets.IsWebSocketRequest)\n            {\n                HttpContext.Response.ContentType = \"text/html\";\n                await HttpContext.Response.SendFileAsync(\"./wwwroot/index.html\");\n                return;\n            }\n\n            using (var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync())\n            {\n                _logger.LogInformation(\"WebSockets established.\");\n                await RunPingPongAsync(webSocket, HttpContext.RequestAborted);\n            }\n\n            _logger.LogInformation(\"WebSockets finished.\");\n        }\n\n        private static async Task RunPingPongAsync(WebSocket webSocket, CancellationToken cancellation)\n        {\n            var buffer = new byte[1024];\n            while (true)\n            {\n                var message = await webSocket.ReceiveAsync(buffer, cancellation);\n                if (message.MessageType == WebSocketMessageType.Close)\n                {\n                    await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, \"Bye\", cancellation);\n                    return;\n                }\n\n                await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, message.Count),\n                    message.MessageType,\n                    message.EndOfMessage,\n                    cancellation);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "samples/SampleServer/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddControllers()\n    .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);\n\nvar app = builder.Build();\n\napp.UseWebSockets();\napp.MapControllers();\n\napp.Run();\n"
  },
  {
    "path": "samples/SampleServer/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"SampleServer\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "samples/SampleServer/README.md",
    "content": "# Sample Server\n\nThis is a simple web server implementation that can be used to test YARP proxy, by using it as the destination.\n\nFunctionality in this sample server includes:\n\n## Echoing of Request Headers\n\nProvided that the request URI path doesn't match other endpoints described below, then the request headers will be reported back as text in the response body. This enables you to quickly see what headers were sent, for example to analyze header transforms made by the reverse proxy.\n\n## Healthcheck status endpoint\n\n[HealthController](Controllers/HealthController.cs) implements an API endpoint for /api/health that will randomly return bad health status.\n\n## WebSockets endpoint\n\n[WebSocketsController](Controllers/WebSocketsController.cs) implements an endpoint for testing web sockets at /api/websockets.\n\n## Usage\n\nTo run the sample server use: \n- ```dotnet run``` from the sample folder\n- ```dotnet run SampleServer/SampleServer.csproj``` passing in the path to the .csproj file\n- Build an executable using ```dotnet build SampleServer.csproj``` and then run the executable directly\n\nThe server will listen to http://localhost:5000 and https://localhost:5001 by default. The ports and interface can be changed using the urls option on the cmd line. For example ```dotnet run SampleServer/SampleServer.csproj --urls \"https://localhost:10000;http://localhost:10010\"```\n"
  },
  {
    "path": "samples/SampleServer/SampleServer.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>SampleServer</RootNamespace>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "samples/SampleServer/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "samples/SampleServer/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}"
  },
  {
    "path": "samples/SampleServer/wwwroot/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\" />\n    <title></title>\n    <style>\n        table { border: 0 }\n        .commslog-data { font-family: Consolas, Courier New, Courier, monospace; }\n        .commslog-server { background-color: red; color: white }\n        .commslog-client { background-color: green; color: white }\n    </style>\n</head>\n<body>\n    <h1>WebSocket Test Page</h1>\n    <p id=\"stateLabel\">Ready to connect...</p>\n    <div>\n        <label for=\"connectionUrl\">WebSocket Server URL:</label>\n        <input id=\"connectionUrl\" />\n        <button id=\"connectButton\" type=\"submit\">Connect</button>\n    </div>\n    <div>\n        <label for=\"sendMessage\">Message to send:</label>\n        <input id=\"sendMessage\" disabled />\n        <button id=\"sendButton\" type=\"submit\" disabled>Send</button>\n        <button id=\"closeButton\" disabled>Close Socket</button>\n    </div>\n\n    <p>Note: When connected to the default server (i.e. the server in the address bar ;)), the message \"ServerClose\" will cause the server to close the connection. Similarly, the message \"ServerAbort\" will cause the server to forcibly terminate the connection without a closing handshake</p>\n\n    <h2>Communication Log</h2>\n    <table style=\"width: 800px\">\n        <thead>\n            <tr>\n                <td style=\"width: 100px\">From</td>\n                <td style=\"width: 100px\">To</td>\n                <td>Data</td>\n            </tr>\n        </thead>\n        <tbody id=\"commsLog\">\n        </tbody>\n    </table>\n\n    <script>\n        var connectionForm = document.getElementById(\"connectionForm\");\n        var connectionUrl = document.getElementById(\"connectionUrl\");\n        var connectButton = document.getElementById(\"connectButton\");\n        var stateLabel = document.getElementById(\"stateLabel\");\n        var sendMessage = document.getElementById(\"sendMessage\");\n        var sendButton = document.getElementById(\"sendButton\");\n        var sendForm = document.getElementById(\"sendForm\");\n        var commsLog = document.getElementById(\"commsLog\");\n\n        var socket;\n\n        var scheme = document.location.protocol == \"https:\" ? \"wss\" : \"ws\";\n        var port = document.location.port ? (\":\" + document.location.port) : \"\";\n\n        connectionUrl.value = scheme + \"://\" + document.location.hostname + port + \"/api/websockets\";\n\n        function updateState() {\n            function disable() {\n                sendMessage.disabled = true;\n                sendButton.disabled = true;\n                closeButton.disabled = true;\n            }\n            function enable() {\n                sendMessage.disabled = false;\n                sendButton.disabled = false;\n                closeButton.disabled = false;\n            }\n\n            connectionUrl.disabled = true;\n            connectButton.disabled = true;\n\n            if (!socket) {\n                disable();\n            } else {\n                switch (socket.readyState) {\n                    case WebSocket.CLOSED:\n                        stateLabel.innerHTML = \"Closed\";\n                        disable();\n                        connectionUrl.disabled = false;\n                        connectButton.disabled = false;\n                        break;\n                    case WebSocket.CLOSING:\n                        stateLabel.innerHTML = \"Closing...\";\n                        disable();\n                        break;\n                    case WebSocket.CONNECTING:\n                        stateLabel.innerHTML = \"Connecting...\";\n                        disable();\n                        break;\n                    case WebSocket.OPEN:\n                        stateLabel.innerHTML = \"Open\";\n                        enable();\n                        break;\n                    default:\n                        stateLabel.innerHTML = \"Unknown WebSocket State: \" + socket.readyState;\n                        disable();\n                        break;\n                }\n            }\n        }\n\n        closeButton.onclick = function () {\n            if (!socket || socket.readyState != WebSocket.OPEN) {\n                alert(\"socket not connected\");\n            }\n            socket.close(1000, \"Closing from client\");\n        }\n\n        sendButton.onclick = function () {\n            if (!socket || socket.readyState != WebSocket.OPEN) {\n                alert(\"socket not connected\");\n            }\n            var data = sendMessage.value;\n            socket.send(data);\n            commsLog.innerHTML += '<tr>' +\n                '<td class=\"commslog-client\">Client</td>' +\n                '<td class=\"commslog-server\">Server</td>' +\n                '<td class=\"commslog-data\">' + data + '</td>'\n            '</tr>';\n        }\n\n        connectButton.onclick = function() {\n            stateLabel.innerHTML = \"Connecting\";\n            socket = new WebSocket(connectionUrl.value);\n            socket.onopen = function (event) {\n                updateState();\n                commsLog.innerHTML += '<tr>' +\n                    '<td colspan=\"3\" class=\"commslog-data\">Connection opened</td>' +\n                '</tr>';\n            };\n            socket.onclose = function (event) {\n                updateState();\n                commsLog.innerHTML += '<tr>' +\n                    '<td colspan=\"3\" class=\"commslog-data\">Connection closed. Code: ' + event.code + '. Reason: ' + event.reason + '</td>' +\n                '</tr>';\n            };\n            socket.onerror = updateState;\n            socket.onmessage = function (event) {\n                commsLog.innerHTML += '<tr>' +\n                    '<td class=\"commslog-server\">Server</td>' +\n                    '<td class=\"commslog-client\">Client</td>' +\n                    '<td class=\"commslog-data\">' + event.data + '</td>'\n                '</tr>';\n            };\n        };\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "src/Application/Extensions.cs",
    "content": "using System.Net;\nusing System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\nusing HealthChecks.ApplicationStatus.DependencyInjection;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Diagnostics.HealthChecks;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing OpenTelemetry;\nusing OpenTelemetry.Exporter;\nusing OpenTelemetry.Logs;\nusing OpenTelemetry.Metrics;\nusing OpenTelemetry.Trace;\nusing static System.Net.WebRequestMethods;\n\nnamespace Microsoft.Extensions.Hosting;\n\n// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.\n// This project should be referenced by each service project in your solution.\n// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults\npublic static class Extensions\n{\n    public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder\n    {\n        builder.ConfigureOpenTelemetry();\n\n        builder.AddDefaultHealthChecks();\n\n        builder.Services.AddServiceDiscovery();\n\n        return builder;\n    }\n\n    public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder\n    {\n        var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration[\"OTEL_EXPORTER_OTLP_ENDPOINT\"]);\n\n        if (useOtlpExporter)\n        {\n            builder.Logging.AddOpenTelemetry(logging =>\n            {\n                logging.IncludeFormattedMessage = true;\n                logging.IncludeScopes = true;\n            });\n\n            var telemetryBuilder = builder.Services.AddOpenTelemetry();\n\n            telemetryBuilder\n                .WithLogging(logging => logging.AddOtlpExporter())\n                .WithMetrics(metrics =>\n                {\n                    metrics.AddAspNetCoreInstrumentation()\n                        .AddHttpClientInstrumentation()\n                        .AddRuntimeInstrumentation()\n                        .SetExemplarFilter(ExemplarFilterType.TraceBased)\n                        .AddOtlpExporter();\n                })\n                .WithTracing(tracing =>\n                {\n                    tracing.AddAspNetCoreInstrumentation()\n                        .AddHttpClientInstrumentation()\n                        .AddOtlpExporter();\n                });\n\n            if (string.Equals(Environment.GetEnvironmentVariable(\"YARP_UNSAFE_OLTP_CERT_ACCEPT_ANY_SERVER_CERTIFICATE\"), \"true\", StringComparison.InvariantCultureIgnoreCase))\n            {\n                // We cannot use UseOtlpExporter() since it doesn't support configuration via OtlpExporterOptions\n                // https://github.com/open-telemetry/opentelemetry-dotnet/issues/5802\n                builder.Services.Configure<OtlpExporterOptions>(ConfigureOtlpExporterOptions);\n            }\n        }\n\n        static void ConfigureOtlpExporterOptions(OtlpExporterOptions options)\n        {\n            options.HttpClientFactory = () =>\n            {\n                var handler = new HttpClientHandler();\n                handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;\n                var httpClient = new HttpClient(handler);\n                return httpClient;\n            };\n        }\n\n        return builder;\n    }\n\n    public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder\n    {\n        builder.Services.AddHealthChecks()\n            // Add a default liveness check on application status.\n            .AddApplicationStatus(tags: [\"live\"]);\n\n        return builder;\n    }\n}\n"
  },
  {
    "path": "src/Application/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\n\nvar builder = WebApplication.CreateBuilder();\n\n// Load configuration from file if passed\nif (args.Length == 1)\n{\n    var configFile = args[0];\n    var fileInfo = new FileInfo(configFile);\n    if (!fileInfo.Exists)\n    {\n        Console.Error.WriteLine($\"Could not find '{configFile}'.\");\n        return 2;\n    }\n    builder.Configuration.AddJsonFile(fileInfo.FullName, optional: false, reloadOnChange: true);\n    builder.Configuration.AddEnvironmentVariables();\n}\n\n// Configure YARP\nbuilder.AddServiceDefaults();\nbuilder.Services.AddServiceDiscovery();\nbuilder.Services.AddReverseProxy()\n                .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"))\n                .AddServiceDiscoveryDestinationResolver();\n\nvar app = builder.Build();\nvar isEnabledStaticFiles = Environment.GetEnvironmentVariable(\"YARP_ENABLE_STATIC_FILES\");\nif (string.Equals(isEnabledStaticFiles, \"true\", StringComparison.OrdinalIgnoreCase))\n{\n    app.UseFileServer();\n}\napp.UseRouting();\napp.MapReverseProxy();\n\nawait app.RunAsync();\n\nreturn 0;\n"
  },
  {
    "path": "src/Application/Yarp.Application.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <RuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm64;</RuntimeIdentifiers>\n    <TargetFrameworks>net9.0</TargetFrameworks>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <AssemblyName>yarp</AssemblyName>\n    <IsShipping>false</IsShipping>\n    <IsPackable>false</IsPackable>\n  </PropertyGroup>\n\n  <!-- Used by publishing infrastructure to get the version to use for blob publishing -->\n  <Target Name=\"ReturnPackageVersion\" Returns=\"$(PackageVersion)\" />\n\n  <ItemGroup>\n    <PackageReference Include=\"Yarp.ReverseProxy\" Version=\"$(YarpNugetVersion)\" />\n    <PackageReference Include=\"Microsoft.Extensions.ServiceDiscovery\" Version=\"$(MicrosoftExtensionsServiceDiscovery)\" />\n    <PackageReference Include=\"Microsoft.Extensions.ServiceDiscovery.Yarp\" Version=\"$(MicrosoftExtensionsServiceDiscoveryYarp)\" />\n\n    <PackageReference Include=\"OpenTelemetry.Exporter.OpenTelemetryProtocol\" Version=\"$(OpenTelemetryExporterOpenTelemetryProtocol)\" />\n    <PackageReference Include=\"OpenTelemetry.Extensions.Hosting\" Version=\"$(OpenTelemetryExtensionsHosting)\" />\n    <PackageReference Include=\"OpenTelemetry.Instrumentation.AspNetCore\" Version=\"$(OpenTelemetryInstrumentationAspNetCore)\" />\n    <PackageReference Include=\"OpenTelemetry.Instrumentation.Http\" Version=\"$(OpenTelemetryInstrumentationHttp)\" />\n    <PackageReference Include=\"OpenTelemetry.Instrumentation.Runtime\" Version=\"$(OpenTelemetryInstrumentationRuntime)\" />\n    <PackageReference Include=\"AspNetCore.HealthChecks.ApplicationStatus\" Version=\"$(AspNetCoreHealthChecksApplicationStatus)\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Common/Package.targets",
    "content": "<Project>\n  <ItemGroup>\n    <JsonSchemaSegment Include=\"$(MSBuildThisFileDirectory)..\\..\\ConfigurationSchema.json\"\n                       FilePathPattern=\"appsettings\\..*json\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Directory.Build.props",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project>\n  <!-- Recurse up. -->\n  <Import Project=\"$(MSBuildThisFileDirectory)..\\Directory.Build.props\" />\n\n  <!-- Include ConfigurationSchema.json in the package if it exists. -->\n  <PropertyGroup>\n    <ConfigurationSchemaPath>$(MSBuildProjectDirectory)\\ConfigurationSchema.json</ConfigurationSchemaPath>\n    <ConfigurationSchemaExists Condition=\"Exists('$(ConfigurationSchemaPath)')\">true</ConfigurationSchemaExists>\n  </PropertyGroup>\n\n  <ItemGroup Condition=\"'$(ConfigurationSchemaExists)' == 'true'\">\n    <None Include=\"$(ConfigurationSchemaPath)\"\n          Pack=\"True\"\n          PackagePath=\"ConfigurationSchema.json\" />\n  </ItemGroup>\n\n  <PropertyGroup Condition=\"'$(ConfigurationSchemaExists)' == 'true'\">\n    <TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);AddPackageTargetsInPackage</TargetsForTfmSpecificContentInPackage>\n  </PropertyGroup>\n\n  <Target Name=\"AddPackageTargetsInPackage\">\n    <ItemGroup>\n      <TfmSpecificPackageFile Include=\"$(MSBuildThisFileDirectory)Common\\Package.targets\"\n                              PackagePath=\"buildTransitive\\$(TargetFramework)\\$(PackageId).targets\" />\n    </ItemGroup>\n  </Target>\n\n  <PropertyGroup>\n    <IsShipping>true</IsShipping>\n    <IsPackable>true</IsPackable>\n    <GenerateDocumentationFile>true</GenerateDocumentationFile>\n    <PackageIcon>icon.png</PackageIcon>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <None Include=\"..\\..\\assets\\icon.png\" PackagePath=\"icon.png\" Pack=\"true\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Kubernetes.Controller/Caching/Endpoints.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s.Models;\nusing System;\nusing System.Collections.Generic;\n\nnamespace Yarp.Kubernetes.Controller.Caching;\n\n/// <summary>\n/// Holds data needed from a <see cref=\"V1Endpoints\"/> resource.\n/// </summary>\npublic struct Endpoints\n{\n    public Endpoints(V1Endpoints endpoints)\n    {\n        ArgumentNullException.ThrowIfNull(endpoints);\n\n        Name = endpoints.Name();\n        Subsets = endpoints.Subsets;\n    }\n\n    public string Name { get; set; }\n    public IList<V1EndpointSubset> Subsets { get; }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Caching/ICache.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.Models;\nusing System.Collections.Generic;\nusing System.Collections.Immutable;\nusing Yarp.Kubernetes.Controller.Services;\n\nnamespace Yarp.Kubernetes.Controller.Caching;\n\n/// <summary>\n/// ICache service interface holds onto the least amount of data necessary\n/// for <see cref=\"IReconciler\"/> to process work.\n/// </summary>\npublic interface ICache\n{\n    void Update(WatchEventType eventType, V1IngressClass ingressClass);\n    bool Update(WatchEventType eventType, V1Ingress ingress);\n    ImmutableList<string> Update(WatchEventType eventType, V1Service service);\n    ImmutableList<string> Update(WatchEventType eventType, V1Endpoints endpoints);\n    void Update(WatchEventType eventType, V1Secret secret);\n    bool TryGetReconcileData(NamespacedName key, out ReconcileData data);\n    void GetKeys(List<NamespacedName> keys);\n    IEnumerable<IngressData> GetIngresses();\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Caching/IngressCache.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.Models;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.Immutable;\nusing System.Linq;\nusing Yarp.Kubernetes.Controller.Certificates;\nusing Yarp.Kubernetes.Controller.Services;\n\nnamespace Yarp.Kubernetes.Controller.Caching;\n\n/// <summary>\n/// ICache service interface holds onto the least amount of data necessary\n/// for <see cref=\"IReconciler\"/> to process work.\n/// </summary>\npublic class IngressCache : ICache\n{\n    private readonly object _sync = new object();\n    private readonly Dictionary<string, IngressClassData> _ingressClassData = new Dictionary<string, IngressClassData>();\n    private readonly Dictionary<string, NamespaceCache> _namespaceCaches = new Dictionary<string, NamespaceCache>();\n    private readonly YarpOptions _options;\n    private readonly IServerCertificateSelector _certificateSelector;\n    private readonly ICertificateHelper _certificateHelper;\n    private readonly ILogger<IngressCache> _logger;\n\n    private bool _isDefaultController;\n\n    public IngressCache(IOptions<YarpOptions> options, IServerCertificateSelector certificateSelector, ICertificateHelper certificateHelper, ILogger<IngressCache> logger)\n    {\n        ArgumentNullException.ThrowIfNull(options?.Value);\n        ArgumentNullException.ThrowIfNull(certificateSelector);\n        ArgumentNullException.ThrowIfNull(certificateHelper);\n        ArgumentNullException.ThrowIfNull(logger);\n\n        _options = options.Value;\n        _certificateSelector = certificateSelector;\n        _certificateHelper = certificateHelper;\n        _logger = logger;\n    }\n\n    public void Update(WatchEventType eventType, V1IngressClass ingressClass)\n    {\n        ArgumentNullException.ThrowIfNull(ingressClass);\n\n        if (!string.Equals(_options.ControllerClass, ingressClass.Spec.Controller, StringComparison.OrdinalIgnoreCase))\n        {\n            _logger.LogInformation(\n                \"Ignoring {IngressClassNamespace}/{IngressClassName} as the spec.controller is not the same as this ingress\",\n                ingressClass.Metadata.NamespaceProperty,\n                ingressClass.Metadata.Name);\n            return;\n        }\n\n        var ingressClassName = ingressClass.Name();\n        lock (_sync)\n        {\n            if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)\n            {\n                _ingressClassData[ingressClassName] = new IngressClassData(ingressClass);\n            }\n            else if (eventType == WatchEventType.Deleted)\n            {\n                _ingressClassData.Remove(ingressClassName);\n            }\n\n            _isDefaultController = _ingressClassData.Values.Any(ic => ic.IsDefault);\n        }\n    }\n\n    public bool Update(WatchEventType eventType, V1Ingress ingress)\n    {\n        ArgumentNullException.ThrowIfNull(ingress);\n\n        Namespace(ingress.Namespace()).Update(eventType, ingress);\n        return true;\n    }\n\n    public ImmutableList<string> Update(WatchEventType eventType, V1Service service)\n    {\n        ArgumentNullException.ThrowIfNull(service);\n\n        return Namespace(service.Namespace()).Update(eventType, service);\n    }\n\n    public ImmutableList<string> Update(WatchEventType eventType, V1Endpoints endpoints)\n    {\n        return Namespace(endpoints.Namespace()).Update(eventType, endpoints);\n    }\n\n    public void Update(WatchEventType eventType, V1Secret secret)\n    {\n        var namespacedName = NamespacedName.From(secret);\n        _logger.LogDebug(\"Found secret '{NamespacedName}'. Checking against default {CertificateSecretName}\", namespacedName, _options.DefaultSslCertificate);\n\n        if (!string.Equals(namespacedName.ToString(), _options.DefaultSslCertificate, StringComparison.OrdinalIgnoreCase))\n        {\n            return;\n        }\n\n        _logger.LogInformation(\"Found secret `{NamespacedName}` to use as default certificate for HTTPS traffic\", namespacedName);\n\n        var certificate = _certificateHelper.ConvertCertificate(namespacedName, secret);\n        if (certificate is null)\n        {\n            return;\n        }\n\n        if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)\n        {\n            _certificateSelector.AddCertificate(namespacedName, certificate);\n        }\n        else if (eventType == WatchEventType.Deleted)\n        {\n            _certificateSelector.RemoveCertificate(namespacedName);\n        }\n    }\n\n    public bool TryGetReconcileData(NamespacedName key, out ReconcileData data)\n    {\n        return Namespace(key.Namespace).TryLookup(key, out data);\n    }\n\n    public void GetKeys(List<NamespacedName> keys)\n    {\n        lock (_sync)\n        {\n            foreach (var (ns, cache) in _namespaceCaches)\n            {\n                cache.GetKeys(ns, keys);\n            }\n        }\n    }\n\n    public IEnumerable<IngressData> GetIngresses()\n    {\n        var ingresses = new List<IngressData>();\n\n        lock (_sync)\n        {\n            foreach (var ns in _namespaceCaches)\n            {\n                ingresses.AddRange(ns.Value.GetIngresses().Where(IsYarpIngress));\n            }\n        }\n\n        return ingresses;\n    }\n\n    private bool IsYarpIngress(IngressData ingress)\n    {\n        if (ingress.Spec.IngressClassName is null)\n        {\n            return _isDefaultController;\n        }\n\n        lock (_sync)\n        {\n            return _ingressClassData.ContainsKey(ingress.Spec.IngressClassName);\n        }\n    }\n\n    private NamespaceCache Namespace(string key)\n    {\n        lock (_sync)\n        {\n            if (!_namespaceCaches.TryGetValue(key, out var value))\n            {\n                value = new NamespaceCache();\n                _namespaceCaches.Add(key, value);\n            }\n            return value;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Caching/IngressClassData.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing k8s.Models;\n\nnamespace Yarp.Kubernetes.Controller.Caching;\n\n/// <summary>\n/// Holds data needed from a <see cref=\"V1IngressClass\"/> resource.\n/// </summary>\npublic struct IngressClassData\n{\n    public IngressClassData(V1IngressClass ingressClass)\n    {\n        ArgumentNullException.ThrowIfNull(ingressClass);\n\n        IngressClass = ingressClass;\n        IsDefault = GetDefaultAnnotation(ingressClass);\n    }\n\n    public V1IngressClass IngressClass { get; }\n\n    public bool IsDefault { get; }\n\n    private static bool GetDefaultAnnotation(V1IngressClass ingressClass)\n    {\n        var annotation = ingressClass.GetAnnotation(\"ingressclass.kubernetes.io/is-default-class\");\n        return string.Equals(\"true\", annotation, StringComparison.OrdinalIgnoreCase);\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Caching/IngressData.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s.Models;\nusing System;\n\nnamespace Yarp.Kubernetes.Controller.Caching;\n\n/// <summary>\n/// Holds data needed from a <see cref=\"V1Ingress\"/> resource.\n/// </summary>\npublic struct IngressData\n{\n    public IngressData(V1Ingress ingress)\n    {\n        ArgumentNullException.ThrowIfNull(ingress);\n\n        Spec = ingress.Spec;\n        Metadata = ingress.Metadata;\n    }\n\n    public V1IngressSpec Spec { get; set; }\n    public V1ObjectMeta Metadata { get; set; }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Caching/NamespaceCache.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.Models;\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.Immutable;\nusing System.Linq;\nusing Yarp.Kubernetes.Controller.Services;\n\nnamespace Yarp.Kubernetes.Controller.Caching;\n\n/// <summary>\n/// Per-namespace cache data. Implicitly scopes name-based lookups to same namespace. Also\n/// intended to make updates faster because cross-reference dictionaries are not cluster-wide.\n/// </summary>\npublic class NamespaceCache\n{\n    private readonly object _sync = new object();\n    private readonly Dictionary<string, ImmutableList<string>> _ingressToServiceNames = new Dictionary<string, ImmutableList<string>>();\n    private readonly Dictionary<string, ImmutableList<string>> _serviceToIngressNames = new Dictionary<string, ImmutableList<string>>();\n    private readonly Dictionary<string, IngressData> _ingressData = new Dictionary<string, IngressData>();\n    private readonly Dictionary<string, ServiceData> _serviceData = new Dictionary<string, ServiceData>();\n    private readonly Dictionary<string, Endpoints> _endpointsData = new Dictionary<string, Endpoints>();\n\n    public void Update(WatchEventType eventType, V1Ingress ingress)\n    {\n        ArgumentNullException.ThrowIfNull(ingress);\n\n        var serviceNames = ImmutableList<string>.Empty;\n\n        if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)\n        {\n            // If the ingress exists, list out the related services\n            var spec = ingress.Spec;\n            var defaultBackend = spec?.DefaultBackend;\n            var defaultService = defaultBackend?.Service;\n            if (!string.IsNullOrEmpty(defaultService?.Name))\n            {\n                serviceNames = serviceNames.Add(defaultService.Name);\n            }\n\n            foreach (var rule in spec.Rules ?? Enumerable.Empty<V1IngressRule>())\n            {\n                var http = rule.Http;\n                foreach (var path in http.Paths ?? Enumerable.Empty<V1HTTPIngressPath>())\n                {\n                    var backend = path.Backend;\n                    var service = backend.Service;\n\n                    if (!serviceNames.Contains(service.Name))\n                    {\n                        serviceNames = serviceNames.Add(service.Name);\n                    }\n                }\n            }\n        }\n\n        var ingressName = ingress.Name();\n        lock (_sync)\n        {\n            var serviceNamesPrevious = ImmutableList<string>.Empty;\n            if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)\n            {\n                // If the ingress exists then remember details\n\n                _ingressData[ingressName] = new IngressData(ingress);\n\n                if (_ingressToServiceNames.TryGetValue(ingressName, out serviceNamesPrevious))\n                {\n                    _ingressToServiceNames[ingressName] = serviceNames;\n                }\n                else\n                {\n                    serviceNamesPrevious = ImmutableList<string>.Empty;\n                    _ingressToServiceNames.Add(ingressName, serviceNames);\n                }\n            }\n            else if (eventType == WatchEventType.Deleted)\n            {\n                // otherwise clear out details\n\n                _ingressData.Remove(ingressName);\n\n                if (_ingressToServiceNames.TryGetValue(ingressName, out serviceNamesPrevious))\n                {\n                    _ingressToServiceNames.Remove(ingressName);\n                }\n            }\n\n            // update cross-reference for new ingress-to-services linkage not previously known\n            foreach (var serviceName in serviceNames)\n            {\n                if (!serviceNamesPrevious.Contains(serviceName))\n                {\n                    if (_serviceToIngressNames.TryGetValue(serviceName, out var ingressNamesPrevious))\n                    {\n                        _serviceToIngressNames[serviceName] = _serviceToIngressNames[serviceName].Add(ingressName);\n                    }\n                    else\n                    {\n                        _serviceToIngressNames.Add(serviceName, ImmutableList<string>.Empty.Add(ingressName));\n                    }\n                }\n            }\n\n            // remove cross-reference for previous ingress-to-services linkage no longer present\n            foreach (var serviceName in serviceNamesPrevious)\n            {\n                if (!serviceNames.Contains(serviceName))\n                {\n                    _serviceToIngressNames[serviceName] = _serviceToIngressNames[serviceName].Remove(ingressName);\n                }\n            }\n        }\n    }\n\n    public ImmutableList<string> Update(WatchEventType eventType, V1Service service)\n    {\n        ArgumentNullException.ThrowIfNull(service);\n\n        var serviceName = service.Name();\n        lock (_sync)\n        {\n            if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)\n            {\n                _serviceData[serviceName] = new ServiceData(service);\n            }\n            else if (eventType == WatchEventType.Deleted)\n            {\n                _serviceData.Remove(serviceName);\n            }\n\n            if (_serviceToIngressNames.TryGetValue(serviceName, out var ingressNames))\n            {\n                return ingressNames;\n            }\n            else\n            {\n                return ImmutableList<string>.Empty;\n            }\n        }\n    }\n\n    public void GetKeys(string ns, List<NamespacedName> keys)\n    {\n        ArgumentNullException.ThrowIfNull(keys);\n\n        lock (_sync)\n        {\n            foreach (var name in _ingressData.Keys)\n            {\n                keys.Add(new NamespacedName(ns, name));\n            }\n        }\n    }\n\n    public ImmutableList<string> Update(WatchEventType eventType, V1Endpoints endpoints)\n    {\n        ArgumentNullException.ThrowIfNull(endpoints);\n\n        var serviceName = endpoints.Name();\n        lock (_sync)\n        {\n            if (eventType == WatchEventType.Added || eventType == WatchEventType.Modified)\n            {\n                _endpointsData[serviceName] = new Endpoints(endpoints);\n            }\n            else if (eventType == WatchEventType.Deleted)\n            {\n                _endpointsData.Remove(serviceName);\n            }\n\n            if (_serviceToIngressNames.TryGetValue(serviceName, out var ingressNames))\n            {\n                return ingressNames;\n            }\n            else\n            {\n                return ImmutableList<string>.Empty;\n            }\n        }\n    }\n\n    public IEnumerable<IngressData> GetIngresses()\n    {\n        return _ingressData.Values;\n    }\n\n    public bool IngressExists(V1Ingress ingress)\n    {\n        return _ingressData.ContainsKey(ingress.Name());\n    }\n\n    public bool TryLookup(NamespacedName key, out ReconcileData data)\n    {\n        var endpointsList = new List<Endpoints>();\n        var servicesList = new List<ServiceData>();\n\n        lock (_sync)\n        {\n            if (!_ingressData.TryGetValue(key.Name, out var ingress))\n            {\n                data = default;\n                return false;\n            }\n\n            if (_ingressToServiceNames.TryGetValue(key.Name, out var serviceNames))\n            {\n                foreach (var serviceName in serviceNames)\n                {\n                    if (_serviceData.TryGetValue(serviceName, out var serviceData))\n                    {\n                        servicesList.Add(serviceData);\n                    }\n\n                    if (_endpointsData.TryGetValue(serviceName, out var endpoints))\n                    {\n                        endpointsList.Add(endpoints);\n                    }\n                }\n            }\n\n            if (_serviceData.Count == 0)\n            {\n                data = default;\n                return false;\n            }\n\n            data = new ReconcileData(ingress, servicesList, endpointsList);\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Caching/ServiceData.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s.Models;\nusing System;\n\nnamespace Yarp.Kubernetes.Controller.Caching;\n\n/// <summary>\n/// Holds data needed from a <see cref=\"V1Service\"/> resource.\n/// </summary>\npublic struct ServiceData\n{\n    public ServiceData(V1Service service)\n    {\n        ArgumentNullException.ThrowIfNull(service);\n\n        Spec = service.Spec;\n        Metadata = service.Metadata;\n    }\n\n    public V1ServiceSpec Spec { get; set; }\n    public V1ObjectMeta Metadata { get; set; }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Certificates/CertificateHelper.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Runtime.InteropServices;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text;\nusing k8s.Models;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.Kubernetes.Controller.Certificates;\n\npublic class CertificateHelper : ICertificateHelper\n{\n    private const string TlsCertKey = \"tls.crt\";\n    private const string TlsPrivateKeyKey = \"tls.key\";\n\n    private readonly ILogger<CertificateHelper> _logger;\n\n    public CertificateHelper(ILogger<CertificateHelper> logger)\n    {\n        ArgumentNullException.ThrowIfNull(logger);\n        _logger = logger;\n    }\n\n    public X509Certificate2 ConvertCertificate(NamespacedName namespacedName, V1Secret secret)\n    {\n        try\n        {\n            var cert = secret?.Data[TlsCertKey];\n            var privateKey = secret?.Data[TlsPrivateKeyKey];\n\n            if (cert == null || cert.Length == 0 || privateKey == null || privateKey.Length == 0)\n            {\n                _logger.LogWarning(\"TLS secret '{NamespacedName}' contains invalid data.\", namespacedName);\n                return null;\n            }\n\n            var certString = EnsurePemFormat(cert, \"CERTIFICATE\");\n            var privateString = EnsurePemFormat(privateKey, \"PRIVATE KEY\");\n\n            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))\n            {\n                // Cert needs converting. Read https://github.com/dotnet/runtime/issues/23749#issuecomment-388231655\n                using var convertedCertificate = X509Certificate2.CreateFromPem(certString, privateString);\n                return new X509Certificate2(convertedCertificate.Export(X509ContentType.Pkcs12));\n            }\n\n            return X509Certificate2.CreateFromPem(certString, privateString);\n        }\n        catch (Exception ex)\n        {\n            _logger.LogWarning(ex, \"Failed to convert secret '{NamespacedName}'\", namespacedName);\n        }\n\n        return null;\n    }\n\n    /// <summary>\n    /// Kubernetes Secrets should be stored in base-64 encoded DER format (see https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets)\n    /// but need can be imported into a <see cref=\"X509Certificate2\"/> object via PEM. Before this type of secret existed, an Opaque secret would be\n    /// used containing the full PEM format, so it's possible that the incorrect format would be used.\n    /// Doing it this way means we are more tolerant in handling certs in the wrong format.\n    /// </summary>\n    /// <param name=\"data\">The raw data.</param>\n    /// <param name=\"pemType\">The type for the PEM header.</param>\n    /// <returns>The certificate data in PEM format.</returns>\n    private static string EnsurePemFormat(byte[] data, string pemType)\n    {\n        var der = Encoding.ASCII.GetString(data);\n        if (!der.StartsWith(\"---\", StringComparison.Ordinal))\n        {\n            // Convert from encoded DER to PEM\n            return $\"-----BEGIN {pemType}-----\\n{der}\\n-----END {pemType}-----\";\n        }\n\n        return der;\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Certificates/ICertificateHelper.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Security.Cryptography.X509Certificates;\nusing k8s.Models;\n\nnamespace Yarp.Kubernetes.Controller.Certificates;\n\npublic interface ICertificateHelper\n{\n    X509Certificate2 ConvertCertificate(NamespacedName namespacedName, V1Secret secret);\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Certificates/IServerCertificateSelector.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Security.Cryptography.X509Certificates;\nusing Microsoft.AspNetCore.Connections;\n\nnamespace Yarp.Kubernetes.Controller.Certificates;\n\n/// <summary>\n/// A mechanism for obtaining server certificates dynamically based on the SNI domain name.\n/// </summary>\npublic interface IServerCertificateSelector\n{\n    /// <summary>\n    /// Retrieve a certificate using the provided domain name.\n    /// </summary>\n    /// <param name=\"connectionContext\">The connection context.</param>\n    /// <param name=\"domainName\">The domain name.</param>\n    /// <returns>Either returns the specific certificate for the domain name, a wildcard certificates, or no certificate.</returns>\n    X509Certificate2 GetCertificate(ConnectionContext connectionContext, string domainName);\n\n    /// <summary>\n    /// Adds a certificate to the selector.\n    /// </summary>\n    /// <param name=\"certificateName\">An identifier for the certificate that can be used to remove it.</param>\n    /// <param name=\"certificate\">The server certificate.</param>\n    void AddCertificate(NamespacedName certificateName, X509Certificate2 certificate);\n\n    /// <summary>\n    /// Removes a certificate from the selector.\n    /// </summary>\n    /// <param name=\"certificateName\">An identifier for the certificate that can be used to remove it.</param>\n    void RemoveCertificate(NamespacedName certificateName);\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Certificates/ServerCertificateSelector.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Security.Cryptography.X509Certificates;\nusing Microsoft.AspNetCore.Connections;\n\nnamespace Yarp.Kubernetes.Controller.Certificates;\n\ninternal class ServerCertificateSelector : IServerCertificateSelector\n{\n    private X509Certificate2 _defaultCertificate;\n\n    public void AddCertificate(NamespacedName certificateName, X509Certificate2 certificate)\n    {\n        _defaultCertificate = certificate;\n    }\n\n    public X509Certificate2 GetCertificate(ConnectionContext connectionContext, string domainName)\n    {\n        return _defaultCertificate;\n    }\n\n    public void RemoveCertificate(NamespacedName certificateName)\n    {\n        _defaultCertificate = null;\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Client/GroupApiVersionKind.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s.Models;\nusing System;\nusing System.Reflection;\n\nnamespace Yarp.Kubernetes.Controller.Client;\n\npublic struct GroupApiVersionKind : IEquatable<GroupApiVersionKind>\n{\n    public GroupApiVersionKind(string group, string apiVersion, string kind)\n    {\n        ApiVersion = apiVersion;\n        GroupApiVersion = string.IsNullOrEmpty(group) ? apiVersion : $\"{group}/{apiVersion}\";\n        Kind = kind;\n    }\n\n    public string ApiVersion { get; }\n\n    public string GroupApiVersion { get; }\n\n    public string Kind { get; }\n\n    public static GroupApiVersionKind From<TResource>() => From(typeof(TResource));\n\n    public static GroupApiVersionKind From(Type resourceType)\n    {\n        var entity = resourceType.GetTypeInfo().GetCustomAttribute<KubernetesEntityAttribute>();\n\n        return new GroupApiVersionKind(\n            group: entity.Group,\n            apiVersion: entity.ApiVersion,\n            kind: entity.Kind);\n    }\n\n    public override bool Equals(object obj)\n    {\n        return obj is GroupApiVersionKind kind && Equals(kind);\n    }\n\n    public bool Equals(GroupApiVersionKind other)\n    {\n        return GroupApiVersion == other.GroupApiVersion\n            && Kind == other.Kind;\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(GroupApiVersion, Kind);\n    }\n\n    public override string ToString()\n    {\n        return $\"{Kind}.{GroupApiVersion}\";\n    }\n\n    public static bool operator ==(GroupApiVersionKind left, GroupApiVersionKind right)\n    {\n        return left.Equals(right);\n    }\n\n    public static bool operator !=(GroupApiVersionKind left, GroupApiVersionKind right)\n    {\n        return !(left == right);\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Client/IIngressResourceStatusUpdater.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Client;\n\npublic interface IIngressResourceStatusUpdater\n{\n    /// <summary>\n    /// Updates the status of cached ingresses.\n    /// </summary>\n    Task UpdateStatusAsync(CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Client/IResourceInformer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.Models;\nusing Microsoft.Extensions.Hosting;\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Client;\n\n/// <summary>\n/// Callback for resource event notifications.\n/// </summary>\n/// <typeparam name=\"TResource\">The type of <see cref=\"IKubernetesObject{V1ObjectMeta}\"/> being monitored.</typeparam>\n/// <param name=\"eventType\">The type of change event which was received.</param>\n/// <param name=\"resource\">The instance of the resource which was received.</param>\npublic delegate void ResourceInformerCallback<TResource>(WatchEventType eventType, TResource resource) where TResource : class, IKubernetesObject<V1ObjectMeta>;\n\n/// <summary>\n/// Interface IResourceInformer is a service which generates\n/// notifications for a specific type\n/// of Kubernetes object. The callback eventType informs if the notification\n/// is because it is new, modified, or has been deleted.\n/// Implements the <see cref=\"IHostedService\" />.\n/// </summary>\n/// <typeparam name=\"TResource\">The type of the t resource.</typeparam>\n/// <seealso cref=\"IObservable{T}\" />\n/// <seealso cref=\"IHostedService\" />\npublic interface IResourceInformer<TResource> : IHostedService, IResourceInformer\n    where TResource : class, IKubernetesObject<V1ObjectMeta>, new()\n{\n    /// <summary>\n    /// Registers a callback for change notification. To ensure no events are missed the registration\n    /// may be created in the constructor of a dependant <see cref=\"IHostedService\"/>. The returned\n    /// registration should be disposed when the receiver is ending its work.\n    /// </summary>\n    /// <param name=\"callback\">The delegate that is invoked with each resource notification.</param>\n    /// <returns>A registration that should be disposed to end the notifications.</returns>\n    IResourceInformerRegistration Register(ResourceInformerCallback<TResource> callback);\n}\n\npublic interface IResourceInformer\n{\n    /// <summary>\n    /// Instructs the resource informer to being watching resources. Allows the startup of informers to be synchronised.\n    /// </summary>\n    void StartWatching();\n\n    /// <summary>\n    /// Returns a task that can be awaited to know when the initial listing of resources is complete.\n    /// Once an await on this method is completed it is safe to assume that all the knowledge of this resource\n    /// type has been made available. Any new changes will be a result of receiving new updates.\n    /// </summary>\n    /// <param name=\"cancellationToken\">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>\n    /// <returns>Task.</returns>\n    Task ReadyAsync(CancellationToken cancellationToken);\n\n    /// <summary>\n    /// Registers a callback for change notification. To ensure no events are missed the registration\n    /// may be created in the constructor of a dependant <see cref=\"IHostedService\"/>. The returned\n    /// registration should be disposed when the receiver is ending its work.\n    /// </summary>\n    /// <param name=\"callback\">The delegate that is invoked with each resource notification.</param>\n    /// <returns>A registration that should be disposed to end the notifications.</returns>\n    IResourceInformerRegistration Register(ResourceInformerCallback<IKubernetesObject<V1ObjectMeta>> callback);\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Client/IResourceInformerRegistration.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Client;\n\n/// <summary>\n/// Returned by <see cref=\"IResourceInformer{TResource}.Register(ResourceInformerCallback{TResource})\"/> to control the lifetime of an event\n/// notification connection. Call <see cref=\"IDisposable.Dispose()\"/> when the lifetime of the notification receiver is ending.\n/// </summary>\npublic interface IResourceInformerRegistration : IDisposable\n{\n    /// <summary>\n    /// Returns a task that can be awaited to know when the initial listing of resources is complete.\n    /// Once an await on this method is completed it is safe to assume that all the knowledge of this resource\n    /// type has been made available. Any new changes will be a result of receiving new updates.\n    /// </summary>\n    /// <param name=\"cancellationToken\">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>\n    /// <returns>Task.</returns>\n    Task ReadyAsync(CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Client/KubernetesClientOptions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\n\nnamespace Yarp.Kubernetes.Controller.Client;\n\n/// <summary>\n/// Class KubernetesClientOptions.\n/// </summary>\npublic class KubernetesClientOptions\n{\n    /// <summary>\n    /// Gets or sets the configuration.\n    /// </summary>\n    /// <value>The configuration.</value>\n    public KubernetesClientConfiguration Configuration { get; set; }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Client/ResourceInformer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.Autorest;\nusing k8s.Models;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.Immutable;\nusing System.IO;\nusing System.Net.Sockets;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Yarp.Kubernetes.Controller.Hosting;\nusing Yarp.Kubernetes.Controller.Rate;\n\nnamespace Yarp.Kubernetes.Controller.Client;\n\n/// <summary>\n/// Class ResourceInformer.\n/// Implements the <see cref=\"IResourceInformer{TResource}\" />.\n/// Implements the <see cref=\"IDisposable\" />.\n/// </summary>\n/// <typeparam name=\"TResource\">The type of the t resource.</typeparam>\n/// <typeparam name=\"TListResource\">The type of the t resource used in lists.</typeparam>\n/// <seealso cref=\"IResourceInformer{TResource}\" />\n/// <seealso cref=\"IDisposable\" />\npublic abstract class ResourceInformer<TResource, TListResource> : BackgroundHostedService, IResourceInformer<TResource>\n    where TResource : class, IKubernetesObject<V1ObjectMeta>, new()\n    where TListResource : class, IKubernetesObject<V1ListMeta>, IItems<TResource>, new()\n{\n    private readonly object _sync = new object();\n    private readonly GroupApiVersionKind _names;\n    private readonly SemaphoreSlim _ready = new SemaphoreSlim(0);\n    private readonly SemaphoreSlim _start = new SemaphoreSlim(0);\n    private readonly ResourceSelector<TResource> _selector;\n    private ImmutableList<Registration> _registrations = ImmutableList<Registration>.Empty;\n    private Dictionary<NamespacedName, IList<V1OwnerReference>> _cache = [];\n    private string _lastResourceVersion;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"ResourceInformer{TResource, TListResource}\" /> class.\n    /// </summary>\n    /// <param name=\"client\">The client.</param>\n    /// <param name=\"selector\">A resource selector for (optionally) filtering the list of resources.</param>\n    /// <param name=\"hostApplicationLifetime\">The host application lifetime.</param>\n    /// <param name=\"logger\">The logger.</param>\n    public ResourceInformer(\n        IKubernetes client,\n        ResourceSelector<TResource> selector,\n        IHostApplicationLifetime hostApplicationLifetime,\n        ILogger logger)\n        : base(hostApplicationLifetime, logger)\n    {\n        ArgumentNullException.ThrowIfNull(client);\n        ArgumentNullException.ThrowIfNull(selector);\n\n        Client = client;\n        _selector = selector;\n        _names = GroupApiVersionKind.From<TResource>();\n    }\n\n    private enum EventType\n    {\n        SynchronizeStarted = 101,\n        SynchronizeComplete = 102,\n        WatchingResource = 103,\n        ReceivedError = 104,\n        WatchingComplete = 105,\n        InformerWatchEvent = 106,\n        DisposingToReconnect = 107,\n        IgnoringError = 108,\n    }\n\n    protected IKubernetes Client { get; init; }\n\n    /// <inheritdoc/>\n    protected override void Dispose(bool disposing)\n    {\n        if (disposing)\n        {\n            try\n            {\n                _start.Dispose();\n                _ready.Dispose();\n            }\n            catch (ObjectDisposedException)\n            {\n                // ignore redundant exception to allow shutdown sequence to progress uninterrupted\n            }\n        }\n        base.Dispose(disposing);\n    }\n\n    public void StartWatching()\n    {\n        _start.Release();\n    }\n\n    public virtual IResourceInformerRegistration Register(ResourceInformerCallback<TResource> callback)\n    {\n        return new Registration(this, callback);\n    }\n\n    public IResourceInformerRegistration Register(ResourceInformerCallback<IKubernetesObject<V1ObjectMeta>> callback)\n    {\n        return new Registration(this, (eventType, resource) => callback(eventType, resource));\n    }\n\n    /// <inheritdoc/>\n    public virtual async Task ReadyAsync(CancellationToken cancellationToken)\n    {\n        await _ready.WaitAsync(cancellationToken).ConfigureAwait(false);\n\n        // Release is called after each WaitAsync because\n        // the semaphore is being used as a manual reset event\n        _ready.Release();\n    }\n\n    /// <summary>\n    /// RunAsync starts processing when StartAsync is called, and is terminated when\n    /// StopAsync is called.\n    /// </summary>\n    /// <param name=\"cancellationToken\">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>\n    /// <returns>A <see cref=\"Task\"/> representing the result of the asynchronous operation.</returns>\n    public override async Task RunAsync(CancellationToken cancellationToken)\n    {\n        try\n        {\n            await _start.WaitAsync(cancellationToken).ConfigureAwait(false);\n\n            var limiter = new Limiter(new Limit(0.2), 3);\n            var shouldSync = true;\n            var firstSync = true;\n            while (true)\n            {\n                cancellationToken.ThrowIfCancellationRequested();\n\n                try\n                {\n                    if (shouldSync)\n                    {\n                        await ListAsync(cancellationToken).ConfigureAwait(true);\n                        shouldSync = false;\n                    }\n\n                    if (firstSync)\n                    {\n                        _ready.Release();\n                        firstSync = false;\n                    }\n\n                    await WatchAsync(cancellationToken).ConfigureAwait(true);\n                }\n                catch (IOException ex) when (ex.InnerException is SocketException)\n                {\n                    Logger.LogDebug(\n                        EventId(EventType.ReceivedError),\n                        \"Received error watching {ResourceType}: {ErrorMessage}\",\n                        typeof(TResource).Name,\n                        ex.Message);\n                }\n                catch (KubernetesException ex)\n                {\n                    Logger.LogDebug(\n                        EventId(EventType.ReceivedError),\n                        \"Received error watching {ResourceType}: {ErrorMessage}\",\n                        typeof(TResource).Name,\n                        ex.Message);\n\n                    // deal with this non-recoverable condition \"too old resource version\"\n                    // with a re-sync to listing everything again ensuring no subscribers miss updates\n                    if (ex is KubernetesException kubernetesError)\n                    {\n                        if (string.Equals(kubernetesError.Status.Reason, \"Expired\", StringComparison.Ordinal))\n                        {\n                            shouldSync = true;\n                        }\n                    }\n                }\n\n                // rate limiting the reconnect loop\n                await limiter.WaitAsync(cancellationToken).ConfigureAwait(true);\n            }\n        }\n        catch (Exception error)\n        {\n            Logger.LogInformation(\n                EventId(EventType.WatchingComplete),\n                error,\n                \"No longer watching {ResourceType} resources from API server.\",\n                typeof(TResource).Name);\n            throw;\n        }\n    }\n\n    protected abstract Task<HttpOperationResponse<TListResource>> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector<TResource> resourceSelector = null, CancellationToken cancellationToken = default);\n    protected abstract Watcher<TResource> WatchResourceListAsync(string resourceVersion = null, ResourceSelector<TResource> resourceSelector = null, Action<WatchEventType, TResource> onEvent = null, Action<Exception> onError = null, Action onClosed = null);\n\n    private static EventId EventId(EventType eventType) => new EventId((int)eventType, eventType.ToString());\n\n    private async Task ListAsync(CancellationToken cancellationToken)\n    {\n        var previousCache = _cache;\n        _cache = new Dictionary<NamespacedName, IList<V1OwnerReference>>();\n\n        if (_selector.FieldSelector is not null)\n        {\n            Logger.LogInformation(\n                EventId(EventType.SynchronizeStarted),\n                \"Started synchronizing {ResourceType} resources from API server with field selector '{FieldSelector}'.\",\n                typeof(TResource).Name,\n                _selector.FieldSelector);\n        }\n        else\n        {\n            Logger.LogInformation(\n                EventId(EventType.SynchronizeStarted),\n                \"Started synchronizing {ResourceType} resources from API server.\",\n                typeof(TResource).Name);\n        }\n\n        string continueParameter = null;\n        do\n        {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            // request next page of items\n            using var listWithHttpMessage = await RetrieveResourceListAsync(resourceVersion: _lastResourceVersion, resourceSelector: _selector, cancellationToken: cancellationToken);\n\n            var list = listWithHttpMessage.Body;\n            foreach (var item in list.Items)\n            {\n                // These properties are not already set on items while listing\n                // assigned here for consistency\n                item.ApiVersion = _names.GroupApiVersion;\n                item.Kind = _names.Kind;\n\n                var key = NamespacedName.From(item);\n                _cache[key] = item?.Metadata?.OwnerReferences;\n\n                var watchEventType = WatchEventType.Added;\n                if (previousCache.Remove(key))\n                {\n                    // an already-known key is provided as a modification for re-sync purposes\n                    watchEventType = WatchEventType.Modified;\n                }\n\n                InvokeRegistrationCallbacks(watchEventType, item);\n            }\n\n            foreach (var (key, value) in previousCache)\n            {\n                // for anything which was previously known but not part of list\n                // send a deleted notification to clear any observer caches\n                var item = new TResource\n                {\n                    ApiVersion = _names.GroupApiVersion,\n                    Kind = _names.Kind,\n                    Metadata = new V1ObjectMeta\n                    {\n                        Name = key.Name,\n                        NamespaceProperty = key.Namespace,\n                        OwnerReferences = value\n                    }\n                };\n\n                InvokeRegistrationCallbacks(WatchEventType.Deleted, item);\n            }\n\n            // keep track of values needed for next page and to start watching\n            _lastResourceVersion = list.ResourceVersion();\n            continueParameter = list.Continue();\n        }\n        while (!string.IsNullOrEmpty(continueParameter));\n\n        Logger.LogInformation(\n            EventId(EventType.SynchronizeComplete),\n            \"Completed synchronizing {ResourceType} resources from API server.\",\n            typeof(TResource).Name);\n    }\n\n    private async Task WatchAsync(CancellationToken cancellationToken)\n    {\n        Logger.LogInformation(\n            EventId(EventType.WatchingResource),\n            \"Watching {ResourceType} starting from resource version {ResourceVersion}.\",\n            typeof(TResource).Name,\n            _lastResourceVersion);\n\n        // completion source helps turn OnClose callback into something awaitable\n        var watcherCompletionSource = new TaskCompletionSource<int>();\n\n        // begin watching where list left off\n        var watcher = WatchResourceListAsync(resourceVersion: _lastResourceVersion, resourceSelector: _selector,\n            (watchEventType, item) =>\n            {\n                if (!watcherCompletionSource.Task.IsCompleted)\n                {\n                    OnEvent(watchEventType, item);\n                }\n            },\n            error =>\n            {\n                if (error is KubernetesException kubernetesError)\n                {\n                    // deal with this non-recoverable condition \"too old resource version\"\n                    if (string.Equals(kubernetesError.Status.Reason, \"Expired\", StringComparison.Ordinal))\n                    {\n                        // cause this error to surface\n                        watcherCompletionSource.TrySetException(error);\n                        throw error;\n                    }\n                }\n\n                Logger.LogDebug(\n                    EventId(EventType.IgnoringError),\n                    \"Ignoring error {ErrorType}: {ErrorMessage}\",\n                    error.GetType().Name,\n                    error.Message);\n            },\n            () =>\n            {\n                watcherCompletionSource.TrySetResult(0);\n            }\n        );\n\n        var lastEventUtc = DateTime.UtcNow;\n\n        // reconnect if no events have arrived after a certain time\n        using var checkLastEventUtcTimer = new Timer(\n            _ =>\n            {\n                var lastEvent = DateTime.UtcNow - lastEventUtc;\n                if (lastEvent > TimeSpan.FromMinutes(9.5))\n                {\n                    lastEventUtc = DateTime.MaxValue;\n                    Logger.LogDebug(\n                        EventId(EventType.DisposingToReconnect),\n                        \"Disposing watcher for {ResourceType} to cause reconnect.\",\n                        typeof(TResource).Name);\n\n                    watcherCompletionSource.TrySetCanceled();\n                    watcher.Dispose();\n\n                }\n            },\n            state: null,\n            dueTime: TimeSpan.FromSeconds(45),\n            period: TimeSpan.FromSeconds(45));\n\n        using var registration = cancellationToken.Register(watcher.Dispose);\n        try\n        {\n            await watcherCompletionSource.Task;\n        }\n        catch (TaskCanceledException)\n        {\n        }\n    }\n\n    private void OnEvent(WatchEventType watchEventType, TResource item)\n    {\n        if (watchEventType != WatchEventType.Modified || item.Kind != \"ConfigMap\")\n        {\n            Logger.LogDebug(\n                EventId(EventType.InformerWatchEvent),\n                \"Informer {ResourceType} received {WatchEventType} notification for {ItemKind}/{ItemName}.{ItemNamespace} at resource version {ResourceVersion}\",\n                typeof(TResource).Name,\n                watchEventType,\n                item.Kind,\n                item.Name(),\n                item.Namespace(),\n                item.ResourceVersion());\n        }\n\n        if (watchEventType == WatchEventType.Added ||\n            watchEventType == WatchEventType.Modified)\n        {\n            // BUGBUG: log warning if cache was not in expected state\n            _cache[NamespacedName.From(item)] = item.Metadata?.OwnerReferences;\n        }\n\n        if (watchEventType == WatchEventType.Deleted)\n        {\n            _cache.Remove(NamespacedName.From(item));\n        }\n\n        if (watchEventType == WatchEventType.Added ||\n            watchEventType == WatchEventType.Modified ||\n            watchEventType == WatchEventType.Deleted ||\n            watchEventType == WatchEventType.Bookmark)\n        {\n            _lastResourceVersion = item.ResourceVersion();\n        }\n\n        if (watchEventType == WatchEventType.Added ||\n            watchEventType == WatchEventType.Modified ||\n            watchEventType == WatchEventType.Deleted)\n        {\n            InvokeRegistrationCallbacks(watchEventType, item);\n        }\n    }\n\n    private void InvokeRegistrationCallbacks(WatchEventType eventType, TResource resource)\n    {\n\n        List<Exception> innerExceptions = default;\n        foreach (var registration in _registrations)\n        {\n            try\n            {\n                registration.Callback.Invoke(eventType, resource);\n            }\n            catch (Exception innerException)\n            {\n                innerExceptions ??= new List<Exception>();\n                innerExceptions.Add(innerException);\n            }\n        }\n\n        if (innerExceptions is not null)\n        {\n            throw new AggregateException(\"One or more exceptions thrown by ResourceInformerCallback.\", innerExceptions);\n        }\n    }\n\n    internal class Registration : IResourceInformerRegistration\n    {\n        private bool _disposedValue;\n\n        public Registration(ResourceInformer<TResource, TListResource> resourceInformer, ResourceInformerCallback<TResource> callback)\n        {\n            ResourceInformer = resourceInformer;\n            Callback = callback;\n            lock (resourceInformer._sync)\n            {\n                resourceInformer._registrations = resourceInformer._registrations.Add(this);\n            }\n        }\n\n        ~Registration()\n        {\n            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method\n            Dispose(disposing: false);\n        }\n\n        public ResourceInformer<TResource, TListResource> ResourceInformer { get; }\n        public ResourceInformerCallback<TResource> Callback { get; }\n\n        public Task ReadyAsync(CancellationToken cancellationToken) => ResourceInformer.ReadyAsync(cancellationToken);\n\n        protected virtual void Dispose(bool disposing)\n        {\n            if (!_disposedValue)\n            {\n                lock (ResourceInformer._sync)\n                {\n                    ResourceInformer._registrations = ResourceInformer._registrations.Remove(this);\n                }\n                _disposedValue = true;\n            }\n        }\n\n        public void Dispose()\n        {\n            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method\n            Dispose(disposing: true);\n            GC.SuppressFinalize(this);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Client/ResourceSelector.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.Models;\n\nnamespace Yarp.Kubernetes.Controller.Client;\n\n/// <summary>\n/// Provides a mechanism for <see cref=\"IResourceInformer{TResource}\"/> to constrain search based on fields in the resource.\n/// </summary>\npublic class ResourceSelector<TResource>\n    where TResource : class, IKubernetesObject<V1ObjectMeta>, new()\n{\n    public ResourceSelector(string fieldSelector)\n    {\n        FieldSelector = fieldSelector;\n    }\n\n    public string FieldSelector { get; }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Client/V1EndpointsResourceInformer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing k8s;\nusing k8s.Autorest;\nusing k8s.Models;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Client;\n\ninternal class V1EndpointsResourceInformer : ResourceInformer<V1Endpoints, V1EndpointsList>\n{\n    public V1EndpointsResourceInformer(\n        IKubernetes client,\n        ResourceSelector<V1Endpoints> selector,\n        IHostApplicationLifetime hostApplicationLifetime,\n        ILogger<V1EndpointsResourceInformer> logger)\n        : base(client, selector, hostApplicationLifetime, logger)\n    {\n    }\n\n    protected override Task<HttpOperationResponse<V1EndpointsList>> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector<V1Endpoints> resourceSelector = null, CancellationToken cancellationToken = default)\n    {\n        return Client.CoreV1.ListEndpointsForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken);\n    }\n\n    protected override Watcher<V1Endpoints> WatchResourceListAsync(string resourceVersion = null, ResourceSelector<V1Endpoints> resourceSelector = null, Action<WatchEventType, V1Endpoints> onEvent = null, Action<Exception> onError = null, Action onClosed = null)\n    {\n        return Client.CoreV1.WatchListEndpointsForAllNamespaces(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed);\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Client/V1IngressClassResourceInformer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing k8s;\nusing k8s.Autorest;\nusing k8s.Models;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Client;\n\ninternal class V1IngressClassResourceInformer : ResourceInformer<V1IngressClass, V1IngressClassList>\n{\n    public V1IngressClassResourceInformer(\n        IKubernetes client,\n        ResourceSelector<V1IngressClass> selector,\n        IHostApplicationLifetime hostApplicationLifetime,\n        ILogger<V1IngressClassResourceInformer> logger)\n        : base(client, selector, hostApplicationLifetime, logger)\n    {\n    }\n\n    protected override Task<HttpOperationResponse<V1IngressClassList>> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector<V1IngressClass> resourceSelector = null, CancellationToken cancellationToken = default)\n    {\n        return Client.NetworkingV1.ListIngressClassWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken);\n    }\n\n    protected override Watcher<V1IngressClass> WatchResourceListAsync(string resourceVersion = null, ResourceSelector<V1IngressClass> resourceSelector = null, Action<WatchEventType, V1IngressClass> onEvent = null, Action<Exception> onError = null, Action onClosed = null)\n    {\n        return Client.NetworkingV1.WatchListIngressClass(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed);\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Client/V1IngressResourceInformer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing k8s;\nusing k8s.Autorest;\nusing k8s.Models;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Client;\n\ninternal class V1IngressResourceInformer : ResourceInformer<V1Ingress, V1IngressList>\n{\n    public V1IngressResourceInformer(\n        IKubernetes client,\n        ResourceSelector<V1Ingress> selector,\n        IHostApplicationLifetime hostApplicationLifetime,\n        ILogger<V1IngressResourceInformer> logger)\n        : base(client, selector, hostApplicationLifetime, logger)\n    {\n    }\n\n    protected override Task<HttpOperationResponse<V1IngressList>> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector<V1Ingress> resourceSelector = null, CancellationToken cancellationToken = default)\n    {\n        return Client.NetworkingV1.ListIngressForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken);\n    }\n\n    protected override Watcher<V1Ingress> WatchResourceListAsync(string resourceVersion = null, ResourceSelector<V1Ingress> resourceSelector = null, Action<WatchEventType, V1Ingress> onEvent = null, Action<Exception> onError = null, Action onClosed = null)\n    {\n        return Client.NetworkingV1.WatchListIngressForAllNamespaces(resourceVersion: resourceVersion,\n            fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed);\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Client/V1IngressResourceStatusUpdater.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\nusing System;\nusing System.Linq;\nusing k8s;\nusing k8s.Models;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing System.Threading.Tasks;\nusing Yarp.Kubernetes.Controller.Caching;\nusing System.Threading;\n\nnamespace Yarp.Kubernetes.Controller.Client;\n\ninternal sealed class V1IngressResourceStatusUpdater : IIngressResourceStatusUpdater\n{\n    private readonly IKubernetes _client;\n    private readonly YarpOptions _options;\n    private readonly ICache _cache;\n    private readonly ILogger _logger;\n\n    public V1IngressResourceStatusUpdater(\n        IKubernetes client,\n        IOptions<YarpOptions> options,\n        ICache cache,\n        ILogger<V1ServiceResourceInformer> logger)\n    {\n        ArgumentNullException.ThrowIfNull(options?.Value);\n        _options = options.Value;\n        _client = client;\n        _cache = cache;\n        _logger = logger;\n    }\n\n    public async Task UpdateStatusAsync(CancellationToken cancellationToken)\n    {\n        var service = await _client.CoreV1.ReadNamespacedServiceStatusAsync(_options.ControllerServiceName, _options.ControllerServiceNamespace, cancellationToken: cancellationToken);\n        if (service.Status?.LoadBalancer?.Ingress is { } loadBalancerIngresses)\n        {\n            var status = new V1IngressStatus\n            {\n                LoadBalancer = new V1IngressLoadBalancerStatus\n                {\n                    Ingress = loadBalancerIngresses?.Select(ingress => new V1IngressLoadBalancerIngress\n                    {\n                        Hostname = ingress.Hostname,\n                        Ip = ingress.Ip,\n                        Ports = ingress.Ports?.Select(port => new V1IngressPortStatus\n                        {\n                            Port = port.Port, Protocol = port.Protocol, Error = port.Error\n                        }).ToArray()\n                    }).ToArray()\n                }\n            };\n\n            var ingresses = _cache.GetIngresses().ToArray();\n            foreach (var ingress in ingresses)\n            {\n                _logger.LogInformation(\"Updating ingress {IngressClassNamespace}/{IngressClassName} status.\", ingress.Metadata.NamespaceProperty, ingress.Metadata.Name);\n                var ingressStatus = await _client.NetworkingV1.ReadNamespacedIngressStatusAsync(ingress.Metadata.Name, ingress.Metadata.NamespaceProperty, cancellationToken: cancellationToken);\n                ingressStatus.Status = status;\n                await _client.NetworkingV1.ReplaceNamespacedIngressStatusAsync(ingressStatus, ingress.Metadata.Name, ingress.Metadata.NamespaceProperty, cancellationToken: cancellationToken);\n                _logger.LogInformation(\"Updated ingress {IngressClassNamespace}/{IngressClassName} status.\", ingress.Metadata.NamespaceProperty, ingress.Metadata.Name);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Client/V1SecretResourceInformer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing k8s;\nusing k8s.Autorest;\nusing k8s.Models;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Client;\n\ninternal class V1SecretResourceInformer : ResourceInformer<V1Secret, V1SecretList>\n{\n    public V1SecretResourceInformer(\n        IKubernetes client,\n        ResourceSelector<V1Secret> selector,\n        IHostApplicationLifetime hostApplicationLifetime,\n        ILogger<V1SecretResourceInformer> logger)\n        : base(client, selector, hostApplicationLifetime, logger)\n    {\n    }\n\n    protected override Task<HttpOperationResponse<V1SecretList>> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector<V1Secret> resourceSelector = null, CancellationToken cancellationToken = default)\n    {\n        return Client.CoreV1.ListSecretForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken);\n    }\n\n    protected override Watcher<V1Secret> WatchResourceListAsync(string resourceVersion = null, ResourceSelector<V1Secret> resourceSelector = null, Action<WatchEventType, V1Secret> onEvent = null, Action<Exception> onError = null, Action onClosed = null)\n    {\n        return Client.CoreV1.WatchListSecretForAllNamespaces(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed);\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Client/V1ServiceResourceInformer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing k8s;\nusing k8s.Autorest;\nusing k8s.Models;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Client;\n\ninternal class V1ServiceResourceInformer : ResourceInformer<V1Service, V1ServiceList>\n{\n    public V1ServiceResourceInformer(\n        IKubernetes client,\n        ResourceSelector<V1Service> selector,\n        IHostApplicationLifetime hostApplicationLifetime,\n        ILogger<V1ServiceResourceInformer> logger)\n        : base(client, selector, hostApplicationLifetime, logger)\n    {\n    }\n\n    protected override Task<HttpOperationResponse<V1ServiceList>> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector<V1Service> resourceSelector = null, CancellationToken cancellationToken = default)\n    {\n        return Client.CoreV1.ListServiceForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken);\n    }\n\n    protected override Watcher<V1Service> WatchResourceListAsync(string resourceVersion = null, ResourceSelector<V1Service> resourceSelector = null, Action<WatchEventType, V1Service> onEvent = null, Action<Exception> onError = null, Action onClosed = null)\n    {\n        return Client.CoreV1.WatchListServiceForAllNamespaces(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed);\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/ConfigProvider/IUpdateConfig.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.Kubernetes.Controller.Configuration;\n\npublic interface IUpdateConfig\n{\n    Task UpdateAsync(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters, CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/ConfigProvider/KubernetesConfigProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Primitives;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.Kubernetes.Controller.Configuration;\n\ninternal class KubernetesConfigProvider : IProxyConfigProvider, IUpdateConfig\n{\n    private volatile MessageConfig _config;\n\n    public KubernetesConfigProvider()\n    {\n        _config = new MessageConfig(null, null);\n    }\n\n    public IProxyConfig GetConfig() => _config;\n\n    public Task UpdateAsync(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters, CancellationToken cancellationToken)\n    {\n        var newConfig = new MessageConfig(routes, clusters);\n        var oldConfig = Interlocked.Exchange(ref _config, newConfig);\n        oldConfig.SignalChange();\n\n        return Task.CompletedTask;\n    }\n\n    private class MessageConfig : IProxyConfig\n    {\n        private readonly CancellationTokenSource _cts = new CancellationTokenSource();\n\n        public MessageConfig(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters)\n            : this(routes, clusters, Guid.NewGuid().ToString())\n        { }\n\n        public MessageConfig(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters, string revisionId)\n        {\n            ArgumentNullException.ThrowIfNull(revisionId);\n            RevisionId = revisionId;\n            Routes = routes;\n            Clusters = clusters;\n            ChangeToken = new CancellationChangeToken(_cts.Token);\n        }\n\n        public string RevisionId { get; }\n\n        public IReadOnlyList<RouteConfig> Routes { get; }\n\n        public IReadOnlyList<ClusterConfig> Clusters { get; }\n\n        public IChangeToken ChangeToken { get; }\n\n        internal void SignalChange()\n        {\n            _cts.Cancel();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Converters/ClusterTransfer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.Kubernetes.Controller.Converters;\n\ninternal sealed class ClusterTransfer\n{\n    public Dictionary<string, DestinationConfig> Destinations { get; set; } = new Dictionary<string, DestinationConfig>();\n    public string ClusterId { get; set; }\n    public string LoadBalancingPolicy { get; set; }\n    public SessionAffinityConfig SessionAffinity { get; set; }\n    public HealthCheckConfig HealthCheck { get; set; }\n    public HttpClientConfig HttpClientConfig { get; set; }\n    public ForwarderRequestConfig HttpRequest { get; set; }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Converters/YarpConfigContext.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Linq;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.Kubernetes.Controller.Converters;\n\ninternal class YarpConfigContext\n{\n    public Dictionary<string, ClusterTransfer> ClusterTransfers { get; set; } = new Dictionary<string, ClusterTransfer>();\n    public List<RouteConfig> Routes { get; set; } = new List<RouteConfig>();\n\n    public List<ClusterConfig> BuildClusterConfig()\n    {\n        return ClusterTransfers.Values.Select(c => new ClusterConfig()\n        {\n            Destinations = c.Destinations,\n            ClusterId = c.ClusterId,\n            HealthCheck = c.HealthCheck,\n            LoadBalancingPolicy = c.LoadBalancingPolicy,\n            SessionAffinity = c.SessionAffinity,\n            HttpClient = c.HttpClientConfig,\n            HttpRequest = c.HttpRequest\n        }).ToList();\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Converters/YarpIngressContext.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Yarp.Kubernetes.Controller.Caching;\n\nnamespace Yarp.Kubernetes.Controller.Converters;\n\ninternal sealed class YarpIngressContext\n{\n    public YarpIngressContext(IngressData ingress, List<ServiceData> services, List<Endpoints> endpoints)\n    {\n        Ingress = ingress;\n        Services = services;\n        Endpoints = endpoints;\n    }\n\n    public YarpIngressOptions Options { get; set; } = new YarpIngressOptions();\n    public IngressData Ingress { get; }\n    public List<ServiceData> Services { get; }\n    public List<Endpoints> Endpoints { get; }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Converters/YarpIngressOptions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.Kubernetes.Controller.Converters;\n\ninternal sealed class YarpIngressOptions\n{\n    public bool Https { get; set; }\n    public List<Dictionary<string, string>> Transforms { get; set; }\n    public string AuthorizationPolicy { get; set; }\n    public string RateLimiterPolicy { get; set; }\n    public string OutputCachePolicy { get; set; }\n    public SessionAffinityConfig SessionAffinity { get; set; }\n    public HttpClientConfig HttpClientConfig { get; set; }\n    public ForwarderRequestConfig HttpRequest { get; set; }\n    public string LoadBalancingPolicy { get; set; }\n    public string CorsPolicy { get; set; }\n    public string TimeoutPolicy { get; set; }\n    public TimeSpan? Timeout { get; set; }\n    public HealthCheckConfig HealthCheck { get; set; }\n    public Dictionary<string, string> RouteMetadata { get; set; }\n    public List<RouteHeader> RouteHeaders { get; set; }\n    public List<RouteQueryParameter> RouteQueryParameters { get; set; }\n    public int? RouteOrder { get; set; }\n    public List<string> RouteMethods { get; set; }\n}\n\ninternal sealed class RouteHeaderWrapper\n{\n    public string Name { get; init; }\n    public List<string> Values { get; init; }\n    public HeaderMatchMode Mode { get; init; }\n    public bool IsCaseSensitive { get; init; }\n\n    public RouteHeader ToRouteHeader()\n    {\n        return new RouteHeader\n        {\n            Name = Name,\n            Values = Values,\n            Mode = Mode,\n            IsCaseSensitive = IsCaseSensitive\n        };\n    }\n}\n\ninternal sealed class RouteQueryParameterWrapper\n{\n    public string Name { get; init; }\n    public List<string> Values { get; init; }\n    public QueryParameterMatchMode Mode { get; init; }\n    public bool IsCaseSensitive { get; init; }\n\n    public RouteQueryParameter ToRouteQueryParameter()\n    {\n        return new RouteQueryParameter\n        {\n            Name = Name,\n            Values = Values,\n            Mode = Mode,\n            IsCaseSensitive = IsCaseSensitive\n        };\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Converters/YarpParser.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Linq;\nusing k8s.Models;\nusing YamlDotNet.Serialization;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.Kubernetes.Controller.Caching;\nusing System.Runtime.InteropServices;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.Kubernetes.Controller.Converters;\n\ninternal static class YarpParser\n{\n    private const string ExternalNameServiceType = \"ExternalName\";\n    private static readonly Deserializer YamlDeserializer = new();\n\n    internal static void ConvertFromKubernetesIngress(YarpIngressContext ingressContext, YarpConfigContext configContext)\n    {\n        var spec = ingressContext.Ingress.Spec;\n        var defaultBackend = spec?.DefaultBackend;\n        var defaultService = defaultBackend?.Service;\n        IList<V1EndpointSubset> defaultSubsets = default;\n\n        if (!string.IsNullOrEmpty(defaultService?.Name))\n        {\n            defaultSubsets = ingressContext.Endpoints.SingleOrDefault(x => x.Name == defaultService?.Name).Subsets;\n        }\n\n        // cluster can contain multiple replicas for each destination, need to know the lookup base don endpoints\n        var options = HandleAnnotations(ingressContext, ingressContext.Ingress.Metadata);\n\n        foreach (var rule in spec?.Rules ?? Enumerable.Empty<V1IngressRule>())\n        {\n            HandleIngressRule(ingressContext, ingressContext.Endpoints, defaultSubsets, rule, configContext);\n        }\n    }\n\n    private static void HandleIngressRule(YarpIngressContext ingressContext, List<Endpoints> endpoints, IList<V1EndpointSubset> defaultSubsets, V1IngressRule rule, YarpConfigContext configContext)\n    {\n        var http = rule.Http;\n        foreach (var path in http.Paths ?? Enumerable.Empty<V1HTTPIngressPath>())\n        {\n            var service = ingressContext.Services.SingleOrDefault(s => s.Metadata.Name == path.Backend.Service.Name);\n            if (service.Spec != null)\n            {\n                if (string.Equals(service.Spec.Type, ExternalNameServiceType, StringComparison.OrdinalIgnoreCase))\n                {\n                    HandleExternalIngressRulePath(ingressContext, service.Spec.ExternalName, rule, path, configContext);\n                }\n                else\n                {\n                    var servicePort = service.Spec.Ports.SingleOrDefault(p => MatchesPort(p, path.Backend.Service.Port));\n                    if (servicePort != null)\n                    {\n                        HandleIngressRulePath(ingressContext, servicePort, endpoints, defaultSubsets, rule, path, configContext);\n                    }\n                }\n            }\n        }\n    }\n\n    private static void HandleExternalIngressRulePath(YarpIngressContext ingressContext, string externalName, V1IngressRule rule, V1HTTPIngressPath path, YarpConfigContext configContext)\n    {\n        var backend = path.Backend;\n        var ingressServiceBackend = backend.Service;\n        var routes = configContext.Routes;\n\n        var cluster = GetOrAddCluster(ingressContext, configContext, ingressServiceBackend);\n\n        var pathMatch = FixupPathMatch(path);\n        var host = rule.Host;\n\n        routes.Add(CreateRoute(ingressContext, path, cluster, pathMatch, host));\n        AddDestination(cluster, ingressContext, externalName, ingressServiceBackend.Port.Number);\n    }\n\n    private static void HandleIngressRulePath(YarpIngressContext ingressContext, V1ServicePort servicePort, List<Endpoints> endpoints, IList<V1EndpointSubset> defaultSubsets, V1IngressRule rule, V1HTTPIngressPath path, YarpConfigContext configContext)\n    {\n        var backend = path.Backend;\n        var ingressServiceBackend = backend.Service;\n        var subsets = defaultSubsets;\n        var routes = configContext.Routes;\n\n        if (!string.IsNullOrEmpty(ingressServiceBackend?.Name))\n        {\n            subsets = endpoints.SingleOrDefault(x => x.Name == ingressServiceBackend?.Name).Subsets;\n        }\n\n        var cluster = GetOrAddCluster(ingressContext, configContext, ingressServiceBackend);\n\n        // make sure cluster is present\n        foreach (var subset in subsets ?? Enumerable.Empty<V1EndpointSubset>())\n        {\n            var isRoutePresent = false;\n            foreach (var port in subset.Ports ?? Enumerable.Empty<Corev1EndpointPort>())\n            {\n                if (!MatchesPort(port, servicePort))\n                {\n                    continue;\n                }\n\n                if (!isRoutePresent)\n                {\n                    var pathMatch = FixupPathMatch(path);\n                    var host = rule.Host;\n                    routes.Add(CreateRoute(ingressContext, path, cluster, pathMatch, host));\n                    isRoutePresent = true;\n                }\n\n                // Add destination for every endpoint address\n                foreach (var address in subset.Addresses ?? Enumerable.Empty<V1EndpointAddress>())\n                {\n                    AddDestination(cluster, ingressContext, address.Ip, port.Port);\n                }\n            }\n        }\n    }\n\n    private static void AddDestination(ClusterTransfer cluster, YarpIngressContext ingressContext, string host, int? port)\n    {\n        var isHttps =\n            ingressContext.Options.Https ||\n            cluster.ClusterId.EndsWith(\":443\", StringComparison.Ordinal) ||\n            cluster.ClusterId.EndsWith(\":https\", StringComparison.OrdinalIgnoreCase);\n\n        var protocol = isHttps ? \"https\" : \"http\";\n\n        var uri = $\"{protocol}://{host}\";\n        if (port.HasValue)\n        {\n            uri += $\":{port}\";\n        }\n        cluster.Destinations[uri] = new DestinationConfig()\n        {\n            Address = uri\n        };\n    }\n\n    private static RouteConfig CreateRoute(YarpIngressContext ingressContext, V1HTTPIngressPath path, ClusterTransfer cluster, string pathMatch, string host)\n    {\n        return new RouteConfig()\n        {\n            Match = new RouteMatch()\n            {\n                Methods = ingressContext.Options.RouteMethods,\n                Hosts = host is not null ? new[] { host } : Array.Empty<string>(),\n                Path = pathMatch,\n                Headers = ingressContext.Options.RouteHeaders,\n                QueryParameters = ingressContext.Options.RouteQueryParameters\n            },\n            ClusterId = cluster.ClusterId,\n            RouteId = $\"{ingressContext.Ingress.Metadata.Name}.{ingressContext.Ingress.Metadata.NamespaceProperty}:{host}{path.Path}\",\n            Transforms = ingressContext.Options.Transforms,\n            AuthorizationPolicy = ingressContext.Options.AuthorizationPolicy,\n            RateLimiterPolicy = ingressContext.Options.RateLimiterPolicy,\n            OutputCachePolicy = ingressContext.Options.OutputCachePolicy,\n            Timeout = ingressContext.Options.Timeout,\n            TimeoutPolicy = ingressContext.Options.TimeoutPolicy,\n            CorsPolicy = ingressContext.Options.CorsPolicy,\n            Metadata = ingressContext.Options.RouteMetadata,\n            Order = ingressContext.Options.RouteOrder,\n        };\n    }\n\n    private static ClusterTransfer GetOrAddCluster(YarpIngressContext ingressContext, YarpConfigContext configContext, V1IngressServiceBackend ingressServiceBackend)\n    {\n        var clusters = configContext.ClusterTransfers;\n        // Each ingress rule path can only be for one service\n        var key = UpstreamName(ingressContext.Ingress.Metadata.NamespaceProperty, ingressServiceBackend);\n        var cluster = CollectionsMarshal.GetValueRefOrAddDefault(clusters, key, out _) ??= new ClusterTransfer();\n        cluster.ClusterId = key;\n        cluster.LoadBalancingPolicy = ingressContext.Options.LoadBalancingPolicy;\n        cluster.SessionAffinity = ingressContext.Options.SessionAffinity;\n        cluster.HealthCheck = ingressContext.Options.HealthCheck;\n        cluster.HttpClientConfig = ingressContext.Options.HttpClientConfig;\n        cluster.HttpRequest = ingressContext.Options.HttpRequest;\n        return cluster;\n    }\n\n    private static string UpstreamName(string namespaceName, V1IngressServiceBackend ingressServiceBackend)\n    {\n        if (ingressServiceBackend is not null)\n        {\n            if (ingressServiceBackend.Port.Number.HasValue && ingressServiceBackend.Port.Number.Value > 0)\n            {\n                return $\"{ingressServiceBackend.Name}.{namespaceName}:{ingressServiceBackend.Port.Number}\";\n            }\n\n            if (!string.IsNullOrWhiteSpace(ingressServiceBackend.Port.Name))\n            {\n                return $\"{ingressServiceBackend.Name}.{namespaceName}:{ingressServiceBackend.Port.Name}\";\n            }\n        }\n\n        return $\"{namespaceName}-INVALID\";\n    }\n\n    private static string FixupPathMatch(V1HTTPIngressPath path)\n    {\n        var pathMatch = path.Path;\n\n        // Prefix match is the default for implementation specific.\n        if (string.Equals(path.PathType, \"Prefix\", StringComparison.OrdinalIgnoreCase) ||\n            string.Equals(path.PathType, \"ImplementationSpecific\", StringComparison.OrdinalIgnoreCase))\n        {\n            if (!pathMatch.EndsWith('/'))\n            {\n                pathMatch += \"/\";\n            }\n            // remember for prefix matches, /foo/ works for either /foo or /foo/\n            pathMatch += \"{**catch-all}\";\n        }\n\n        return pathMatch;\n    }\n\n    private static YarpIngressOptions HandleAnnotations(YarpIngressContext context, V1ObjectMeta metadata)\n    {\n        var options = context.Options;\n        var annotations = metadata.Annotations;\n        if (annotations is null)\n        {\n            return options;\n        }\n\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/backend-protocol\", out var http))\n        {\n            options.Https = http.Equals(\"https\", StringComparison.OrdinalIgnoreCase);\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/transforms\", out var transforms))\n        {\n            options.Transforms = YamlDeserializer.Deserialize<List<Dictionary<string, string>>>(transforms);\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/authorization-policy\", out var authorizationPolicy))\n        {\n            options.AuthorizationPolicy = authorizationPolicy;\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/rate-limiter-policy\", out var rateLimiterPolicy))\n        {\n            options.RateLimiterPolicy = rateLimiterPolicy;\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/output-cache-policy\", out var outputCachePolicy))\n        {\n            options.OutputCachePolicy = outputCachePolicy;\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/timeout\", out var timeout))\n        {\n            options.Timeout = TimeSpan.Parse(timeout, CultureInfo.InvariantCulture);\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/timeout-policy\", out var timeoutPolicy))\n        {\n            options.TimeoutPolicy = timeoutPolicy;\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/cors-policy\", out var corsPolicy))\n        {\n            options.CorsPolicy = corsPolicy;\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/session-affinity\", out var sessionAffinity))\n        {\n            options.SessionAffinity = YamlDeserializer.Deserialize<SessionAffinityConfig>(sessionAffinity);\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/load-balancing\", out var loadBalancing))\n        {\n            options.LoadBalancingPolicy = loadBalancing;\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/http-client\", out var httpClientConfig))\n        {\n            options.HttpClientConfig = YamlDeserializer.Deserialize<HttpClientConfig>(httpClientConfig);\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/http-request\", out var httpRequest))\n        {\n            options.HttpRequest = YamlDeserializer.Deserialize<ForwarderRequestConfig>(httpRequest);\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/health-check\", out var healthCheck))\n        {\n            options.HealthCheck = YamlDeserializer.Deserialize<HealthCheckConfig>(healthCheck);\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/route-metadata\", out var routeMetadata))\n        {\n            options.RouteMetadata = YamlDeserializer.Deserialize<Dictionary<string, string>>(routeMetadata);\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/route-headers\", out var routeHeaders))\n        {\n            // YamlDeserializer does not support IReadOnlyList<string> in RouteHeader for now, so we use RouteHeaderWrapper to solve this problem.\n            options.RouteHeaders = YamlDeserializer.Deserialize<List<RouteHeaderWrapper>>(routeHeaders).Select(p => p.ToRouteHeader()).ToList();\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/route-queryparameters\", out var routeQueryParameters))\n        {\n            // YamlDeserializer does not support IReadOnlyList<string> in RouteParameters for now, so we use RouterQueryParameterWrapper to solve this problem.\n            options.RouteQueryParameters = YamlDeserializer.Deserialize<List<RouteQueryParameterWrapper>>(routeQueryParameters).Select(p => p.ToRouteQueryParameter()).ToList();\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/route-order\", out var routeOrder))\n        {\n            options.RouteOrder = int.Parse(routeOrder, CultureInfo.InvariantCulture);\n        }\n        if (annotations.TryGetValue(\"yarp.ingress.kubernetes.io/route-methods\", out var routeMethods))\n        {\n            options.RouteMethods = YamlDeserializer.Deserialize<List<string>>(routeMethods);\n        }\n        // metadata to support:\n        // rewrite target\n        // auth\n        // http or https\n        // default backend\n        // CORS\n        // GRPC\n        // HTTP2\n        // Connection limits\n        // rate limits\n\n        // backend health checks.\n        return options;\n    }\n\n    private static bool MatchesPort(Corev1EndpointPort port1, V1ServicePort port2)\n    {\n        if (port1 is null || port2?.TargetPort is null)\n        {\n            return false;\n        }\n        if (int.TryParse(port2.TargetPort, out var port2Number) && port2Number == port1.Port)\n        {\n            return true;\n        }\n        if (string.Equals(port2.Name, port1.Name, StringComparison.OrdinalIgnoreCase))\n        {\n            return true;\n        }\n        return false;\n    }\n\n    private static bool MatchesPort(V1ServicePort port1, V1ServiceBackendPort port2)\n    {\n        if (port1 is null || port2 is null)\n        {\n            return false;\n        }\n        if (port2.Number is not null && port2.Number == port1.Port)\n        {\n            return true;\n        }\n        if (port2.Name is not null && string.Equals(port2.Name, port1.Name, StringComparison.Ordinal))\n        {\n            return true;\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Hosting/BackgroundHostedService.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing System;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Hosting;\n\n/// <summary>\n/// Class BackgroundHostedService.\n/// Implements the <see cref=\"IHostedService\" />\n/// Implements the <see cref=\"IDisposable\" />.\n/// </summary>\n/// <seealso cref=\"IHostedService\" />\n/// <seealso cref=\"IDisposable\" />\npublic abstract class BackgroundHostedService : IHostedService, IDisposable\n{\n    private readonly IHostApplicationLifetime _hostApplicationLifetime;\n    private readonly CancellationTokenRegistration _hostApplicationStoppingRegistration;\n    private readonly CancellationTokenSource _runCancellation = new CancellationTokenSource();\n    private readonly string _serviceTypeName;\n    private bool _disposedValue;\n    private Task _runTask;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"BackgroundHostedService\"/> class.\n    /// </summary>\n    /// <param name=\"hostApplicationLifetime\">The host application lifetime.</param>\n    /// <param name=\"logger\">The logger.</param>\n    protected BackgroundHostedService(\n        IHostApplicationLifetime hostApplicationLifetime,\n        ILogger logger)\n    {\n        ArgumentNullException.ThrowIfNull(hostApplicationLifetime);\n        ArgumentNullException.ThrowIfNull(logger);\n        _hostApplicationLifetime = hostApplicationLifetime;\n        Logger = logger;\n\n        // register the stoppingToken to become cancelled as soon as the\n        // shutdown sequence is initiated.\n        _hostApplicationStoppingRegistration = _hostApplicationLifetime.ApplicationStopping.Register(_runCancellation.Cancel);\n\n        var serviceType = GetType();\n        if (serviceType.IsGenericType)\n        {\n            _serviceTypeName = $\"{serviceType.Name.Split('`').First()}<{string.Join(\",\", serviceType.GenericTypeArguments.Select(type => type.Name))}>\";\n        }\n        else\n        {\n            _serviceTypeName = serviceType.Name;\n        }\n    }\n\n    /// <summary>\n    /// Gets or sets the logger.\n    /// </summary>\n    /// <value>The logger.</value>\n    protected ILogger Logger { get; set; }\n\n    /// <summary>\n    /// Triggered when the application host is ready to start the service.\n    /// </summary>\n    /// <param name=\"cancellationToken\">Indicates that the start process has been aborted.</param>\n    /// <returns>Task.</returns>\n    public Task StartAsync(CancellationToken cancellationToken)\n    {\n        // fork off a new async causality line beginning with the call to RunAsync\n        _runTask = Task.Run(CallRunAsync, CancellationToken.None);\n\n        // the rest of the startup sequence should proceed without delay\n        return Task.CompletedTask;\n\n        // entry-point to run async background work separated from the startup sequence\n        async Task CallRunAsync()\n        {\n            // don't bother running in case of abnormally early shutdown\n            _runCancellation.Token.ThrowIfCancellationRequested();\n\n            try\n            {\n                Logger?.LogInformation(\n                    new EventId(1, \"RunStarting\"),\n                    \"Calling RunAsync for {BackgroundHostedService}\",\n                    _serviceTypeName);\n\n                try\n                {\n                    // call the overridden method\n                    await RunAsync(_runCancellation.Token).ConfigureAwait(true);\n                }\n                finally\n                {\n                    Logger?.LogInformation(\n                        new EventId(2, \"RunComplete\"),\n                        \"RunAsync completed for {BackgroundHostedService}\",\n                        _serviceTypeName);\n                }\n            }\n            catch\n            {\n                if (!_hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)\n                {\n                    // For any exception the application is instructed to tear down.\n                    // this would normally happen if IHostedService.StartAsync throws, so it\n                    // is a safe assumption the intent of an unhandled exception from background\n                    // RunAsync is the same.\n                    _hostApplicationLifetime.StopApplication();\n\n                    Logger?.LogInformation(\n                        new EventId(3, \"RequestedStopApplication\"),\n                        \"Called StopApplication for {BackgroundHostedService}\",\n                        _serviceTypeName);\n                }\n\n                throw;\n            }\n        }\n    }\n\n    /// <summary>\n    /// stop as an asynchronous operation.\n    /// </summary>\n    /// <param name=\"cancellationToken\">Indicates that the shutdown process should no longer be graceful.</param>\n    /// <returns>A <see cref=\"Task\" /> representing the result of the asynchronous operation.</returns>\n    public async Task StopAsync(CancellationToken cancellationToken)\n    {\n        try\n        {\n            // signal for the RunAsync call to be completed\n            _runCancellation.Cancel();\n\n            // join the result of the RunAsync causality line back into the results of\n            // this StopAsync call. this await statement will not complete until CallRunAsync\n            // method has unwound and returned. if RunAsync completed by throwing an exception\n            // it will be rethrown by this await. rethrown Exceptions will pass through\n            // Hosting and may be caught by Program.Main.\n            await _runTask.ConfigureAwait(false);\n        }\n        catch (OperationCanceledException)\n        {\n            // this exception is ignored - it's a natural result of cancellation token\n        }\n        finally\n        {\n            _runTask = null;\n        }\n    }\n\n    /// <summary>\n    /// Runs the asynchronous background work.\n    /// </summary>\n    /// <param name=\"cancellationToken\">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>\n    /// <returns>A <see cref=\"Task\" /> representing the result of the asynchronous operation.</returns>\n    public abstract Task RunAsync(CancellationToken cancellationToken);\n\n    protected virtual void Dispose(bool disposing)\n    {\n        if (!_disposedValue)\n        {\n            if (disposing)\n            {\n                try\n                {\n                    _runCancellation.Dispose();\n                }\n                catch (ObjectDisposedException)\n                {\n                    // ignore redundant exception to allow shutdown sequence to progress uninterrupted\n                }\n\n                try\n                {\n                    _hostApplicationStoppingRegistration.Dispose();\n                }\n                catch (ObjectDisposedException)\n                {\n                    // ignore redundant exception to allow shutdown sequence to progress uninterrupted\n                }\n            }\n\n            _disposedValue = true;\n        }\n    }\n\n    public void Dispose()\n    {\n        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method\n        Dispose(disposing: true);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Hosting/HostedServiceAdapter.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.Extensions.Hosting;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Hosting;\n\n/// <summary>\n/// Delegates start and stop calls to service-specific interface.\n/// </summary>\n/// <typeparam name=\"TService\">The service interface to delegate onto.</typeparam>\npublic class HostedServiceAdapter<TService> : IHostedService\n    where TService : IHostedService\n{\n    private readonly TService _service;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"HostedServiceAdapter{TInterface}\" /> class.\n    /// </summary>\n    /// <param name=\"service\">The service interface to delegate onto.</param>\n    public HostedServiceAdapter(TService service) => _service = service;\n\n    public Task StartAsync(CancellationToken cancellationToken) => _service.StartAsync(cancellationToken);\n\n    public Task StopAsync(CancellationToken cancellationToken) => _service.StopAsync(cancellationToken);\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Hosting/ServiceCollectionHostedServiceAdapterExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.Extensions.Hosting;\nusing System.Linq;\nusing Yarp.Kubernetes.Controller.Hosting;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\n/// <summary>\n/// Class ServiceCollectionHostedServiceAdapterExtensions.\n/// </summary>\npublic static class ServiceCollectionHostedServiceAdapterExtensions\n{\n    /// <summary>\n    /// Registers the hosted service.\n    /// </summary>\n    /// <typeparam name=\"TService\">The type of the t service.</typeparam>\n    /// <param name=\"services\">The services.</param>\n    /// <returns>IServiceCollection.</returns>\n    public static IServiceCollection RegisterHostedService<TService>(this IServiceCollection services)\n        where TService : IHostedService\n    {\n        if (!services.Any(serviceDescriptor => serviceDescriptor.ServiceType == typeof(HostedServiceAdapter<TService>)))\n        {\n            services = services.AddHostedService<HostedServiceAdapter<TService>>();\n        }\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Management/KubernetesCoreExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing Microsoft.Extensions.Options;\nusing System.Linq;\nusing Yarp.Kubernetes.Controller.Client;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\n/// <summary>\n/// Class KubernetesCoreExtensions.\n/// </summary>\npublic static class KubernetesCoreExtensions\n{\n    /// <summary>\n    /// Adds the kubernetes.\n    /// </summary>\n    /// <param name=\"services\">The services.</param>\n    /// <returns>IServiceCollection.</returns>\n    public static IServiceCollection AddKubernetesCore(this IServiceCollection services)\n    {\n        if (!services.Any(serviceDescriptor => serviceDescriptor.ServiceType == typeof(IKubernetes)))\n        {\n            services = services.Configure<KubernetesClientOptions>(options =>\n            {\n                options.Configuration ??= KubernetesClientConfiguration.BuildDefaultConfig();\n            });\n\n            services = services.AddSingleton<IKubernetes>(sp =>\n            {\n                var options = sp.GetRequiredService<IOptions<KubernetesClientOptions>>().Value;\n\n                return new Kubernetes(options.Configuration);\n            });\n        }\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Management/KubernetesReverseProxyServiceCollectionExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics.CodeAnalysis;\nusing k8s;\nusing k8s.Models;\nusing Microsoft.Extensions.Configuration;\nusing Yarp.Kubernetes.Controller;\nusing Yarp.Kubernetes.Controller.Caching;\nusing Yarp.Kubernetes.Controller.Certificates;\nusing Yarp.Kubernetes.Controller.Client;\nusing Yarp.Kubernetes.Controller.Configuration;\nusing Yarp.Kubernetes.Controller.Controllers;\nusing Yarp.Kubernetes.Controller.Dispatching;\nusing Yarp.Kubernetes.Controller.Protocol;\nusing Yarp.Kubernetes.Controller.Services;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\n/// <summary>\n/// Extensions for <see cref=\"IServiceCollection\"/>\n/// used to register the Kubernetes-based ReverseProxy's components.\n/// </summary>\npublic static class KubernetesReverseProxyServiceCollectionExtensions\n{\n    /// <summary>\n    /// Adds ReverseProxy's services to Dependency Injection.\n    /// </summary>\n    /// <param name=\"services\">Dependency injection registration.</param>\n    /// <param name=\"config\">Application configuration.</param>\n    /// <returns>The same <see cref=\"IServiceCollection\"/> for chaining.</returns>\n    public static IReverseProxyBuilder AddKubernetesReverseProxy(this IServiceCollection services, IConfiguration config)\n    {\n        // Add components from the kubernetes controller framework\n        services.AddKubernetesControllerRuntime(config);\n\n        // Add the in-memory configuration cache.\n        var provider = new KubernetesConfigProvider();\n        services.AddSingleton<IProxyConfigProvider>(provider);\n        services.AddSingleton<IUpdateConfig>(provider);\n\n        return services.AddReverseProxy();\n    }\n\n    /// <summary>\n    /// Adds an ingress controller that monitors for Ingress resource changes and notifies a Yarp \"Ingress\" application.\n    /// </summary>\n    /// <param name=\"services\">Dependency injection registration.</param>\n    /// <param name=\"config\">Application configuration.</param>\n    /// <returns>The same <see cref=\"IServiceCollection\"/> for chaining.</returns>\n    public static IServiceCollection AddKubernetesIngressMonitor(this IServiceCollection services, IConfiguration config)\n    {\n        // Add components from the kubernetes controller framework\n        services.AddKubernetesControllerRuntime(config);\n\n        // Add the dispatcher for the Ingress application to connect to.\n        services.AddSingleton<IDispatcher, Dispatcher>();\n        services.AddSingleton<IUpdateConfig, DispatchConfigProvider>();\n\n        return services;\n    }\n\n    /// <summary>\n    /// Adds the dispatching controller that allows a Yarp \"Ingress\" application to monitor for changes.\n    /// </summary>\n    /// <param name=\"builder\">The MVC builder.</param>\n    /// <returns>The same <see cref=\"IMvcBuilder\"/> for chaining.</returns>\n    public static IMvcBuilder AddKubernetesDispatchController(this IMvcBuilder builder)\n    {\n        return builder.AddApplicationPart(typeof(DispatchController).Assembly);\n    }\n\n    public static IServiceCollection AddKubernetesControllerRuntime(this IServiceCollection services, IConfiguration config)\n    {\n        ArgumentNullException.ThrowIfNull(config, nameof(config));\n\n        // Add components implemented by this application\n        services.AddHostedService<IngressController>();\n        services.AddSingleton<ICache, IngressCache>();\n        services.AddTransient<IReconciler, Reconciler>();\n        services.Configure<YarpOptions>(config.GetSection(\"Yarp\"));\n\n        // Register the necessary Kubernetes resource informers\n        services.RegisterResourceInformer<V1Ingress, V1IngressResourceInformer>();\n        services.RegisterResourceInformer<V1Service, V1ServiceResourceInformer>();\n        services.RegisterResourceInformer<V1Endpoints, V1EndpointsResourceInformer>();\n        services.RegisterResourceInformer<V1IngressClass, V1IngressClassResourceInformer>();\n\n        // We should only retrieve secrets we might be interested in (because Helm V3, for example, can generate lots of secrets)\n        services.RegisterResourceInformer<V1Secret, V1SecretResourceInformer>(\"type=kubernetes.io/tls\");\n\n        // Add the Ingress/Secret to certificate management\n        services.AddSingleton<IServerCertificateSelector, ServerCertificateSelector>();\n        services.AddSingleton<ICertificateHelper, CertificateHelper>();\n\n        // ingress status updater\n        services.AddSingleton<IIngressResourceStatusUpdater, V1IngressResourceStatusUpdater>();\n\n        return services.AddKubernetesCore();\n    }\n\n    /// <summary>\n    /// Registers the resource informer.\n    /// </summary>\n    /// <typeparam name=\"TResource\">The type of the t related resource.</typeparam>\n    /// <typeparam name=\"TService\">The implementation type of the resource informer.</typeparam>\n    /// <param name=\"services\">The services.</param>\n    /// <returns>IServiceCollection.</returns>\n    public static IServiceCollection RegisterResourceInformer<TResource, TService>(this IServiceCollection services)\n        where TResource : class, IKubernetesObject<V1ObjectMeta>, new()\n        where TService : IResourceInformer<TResource>\n    {\n        return services.RegisterResourceInformer<TResource, TService>(null);\n    }\n\n    /// <summary>\n    /// Registers the resource informer with a field selector.\n    /// </summary>\n    /// <typeparam name=\"TResource\">The type of the t related resource.</typeparam>\n    /// <typeparam name=\"TService\">The implementation type of the resource informer.</typeparam>\n    /// <param name=\"services\">The services.</param>\n    /// <param name=\"fieldSelector\">A field selector to constrain the resources the informer retrieves.</param>\n    /// <returns>IServiceCollection.</returns>\n    public static IServiceCollection RegisterResourceInformer<TResource, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection services, string fieldSelector)\n        where TResource : class, IKubernetesObject<V1ObjectMeta>, new()\n        where TService : IResourceInformer<TResource>\n    {\n        services.AddSingleton(new ResourceSelector<TResource>(fieldSelector));\n        services.AddSingleton(typeof(IResourceInformer<TResource>), typeof(TService));\n\n        return services.RegisterHostedService<IResourceInformer<TResource>>();\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Management/KubernetesReverseProxyWebHostBuilderExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.Kubernetes.Controller.Certificates;\n\nnamespace Microsoft.AspNetCore.Hosting;\n\n/// <summary>\n/// Extensions for <see cref=\"IWebHostBuilder\"/>\n/// used to register the Kubernetes-based ReverseProxy's components.\n/// </summary>\npublic static class KubernetesReverseProxyWebHostBuilderExtensions\n{\n    /// <summary>\n    /// Configures Kestrel for SNI-based certificate selection using Kubernetes Ingress TLS annotations and Kubernetes Secrets.\n    /// </summary>\n    /// <param name=\"builder\">The web host builder.</param>\n    /// <returns>The same <see cref=\"IWebHostBuilder\"/> for chaining.</returns>\n    public static IWebHostBuilder UseKubernetesReverseProxyCertificateSelector(this IWebHostBuilder builder)\n    {\n        ArgumentNullException.ThrowIfNull(builder, nameof(builder));\n\n        builder.ConfigureKestrel(kestrelOptions =>\n        {\n            kestrelOptions.ConfigureHttpsDefaults(httpsOptions =>\n            {\n                var selector = kestrelOptions.ApplicationServices.GetService<IServerCertificateSelector>();\n                if (selector is null)\n                {\n                    throw new InvalidOperationException(\"Missing required services. Did you call '.AddKubernetesReverseProxy()' when configuring services?\");\n                }\n\n                httpsOptions.ServerCertificateSelector = (connectionContext, domainName) =>\n                {\n                    return selector.GetCertificate(connectionContext, domainName);\n                };\n            });\n        });\n\n        return builder;\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/NamespacedName.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.Models;\nusing System;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Text.Json.Serialization;\n\nnamespace Yarp.Kubernetes.Controller;\n\n/// <summary>\n/// Struct NamespacedName is a value that acts as a dictionary key. It is a comparable\n/// combination of a metadata namespace and name.\n/// Implements the <see cref=\"IEquatable{T}\" />.\n/// </summary>\n/// <seealso cref=\"IEquatable{T}\" />\npublic struct NamespacedName : IEquatable<NamespacedName>\n{\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"NamespacedName\"/> struct.\n    /// </summary>\n    /// <param name=\"namespace\">The namespace.</param>\n    /// <param name=\"name\">The name.</param>\n    [JsonConstructor]\n    public NamespacedName(string @namespace, string name)\n    {\n        Namespace = @namespace;\n        Name = name;\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"NamespacedName\"/> struct.\n    /// </summary>\n    /// <param name=\"name\">The name.</param>\n    public NamespacedName(string name)\n    {\n        Namespace = null;\n        Name = name;\n    }\n\n    /// <summary>\n    /// Gets the namespace.\n    /// </summary>\n    /// <value>The namespace.</value>\n    public string Namespace { get; }\n\n    /// <summary>\n    /// Gets the name.\n    /// </summary>\n    /// <value>The name.</value>\n    public string Name { get; }\n\n    /// <summary>\n    /// Implements the == operator.\n    /// </summary>\n    /// <param name=\"left\">The left.</param>\n    /// <param name=\"right\">The right.</param>\n    /// <returns>The result of the operator.</returns>\n    public static bool operator ==(NamespacedName left, NamespacedName right)\n    {\n        return left.Equals(right);\n    }\n\n    /// <summary>\n    /// Implements the != operator.\n    /// </summary>\n    /// <param name=\"left\">The left.</param>\n    /// <param name=\"right\">The right.</param>\n    /// <returns>The result of the operator.</returns>\n    public static bool operator !=(NamespacedName left, NamespacedName right)\n    {\n        return !(left == right);\n    }\n\n    /// <summary>\n    /// Gets key values from the specified resource.\n    /// </summary>\n    /// <param name=\"resource\">The resource.</param>\n    /// <returns>NamespacedName.</returns>\n    public static NamespacedName From(IKubernetesObject<V1ObjectMeta> resource)\n    {\n        ArgumentNullException.ThrowIfNull(resource);\n\n        return new NamespacedName(resource.Namespace(), resource.Name());\n    }\n\n    /// <summary>\n    /// Gets key values from the specified metadata.\n    /// </summary>\n    /// <param name=\"metadata\">The metadata.</param>\n    /// <param name=\"ownerReference\">The owner reference.</param>\n    /// <param name=\"clusterScoped\">if set to <c>true</c> [cluster scoped].</param>\n    /// <returns>NamespacedName.</returns>\n    public static NamespacedName From(V1ObjectMeta metadata, [NotNull] V1OwnerReference ownerReference, bool? clusterScoped = null)\n    {\n        ArgumentNullException.ThrowIfNull(metadata);\n        ArgumentNullException.ThrowIfNull(ownerReference);\n\n        return new NamespacedName(\n            clusterScoped ?? false ? null : metadata.NamespaceProperty,\n            ownerReference.Name);\n    }\n\n    public override bool Equals(object obj)\n    {\n        return obj is NamespacedName name && Equals(name);\n    }\n\n    public bool Equals([AllowNull] NamespacedName other)\n    {\n        return Namespace == other.Namespace && Name == other.Name;\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(Namespace, Name);\n    }\n\n    /// <summary>\n    /// Returns a <see cref=\"string\" /> that represents this instance.\n    /// </summary>\n    /// <returns>A <see cref=\"string\" /> that represents this instance.</returns>\n    public override string ToString()\n    {\n        return $\"{Namespace}/{Name}\";\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Properties/AssemblyInfo.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\n// Copyright (c) .NET Foundation. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.\n\nusing System.Runtime.CompilerServices;\n\n[assembly: InternalsVisibleTo(\"Yarp.Kubernetes.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293\")]\n"
  },
  {
    "path": "src/Kubernetes.Controller/Protocol/DispatchActionResult.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing Yarp.Kubernetes.Protocol;\n\nnamespace Yarp.Kubernetes.Controller.Dispatching;\n\n/// <summary>\n/// DispatchActionResult is an IActionResult which registers itself as\n/// an IDispatchTarget with the provided IDispatcher. As long as the client\n/// is connected this result will continue to write data to the response.\n/// </summary>\npublic class DispatchActionResult : IActionResult, IDispatchTarget\n{\n    private static readonly byte[] _newline = Encoding.UTF8.GetBytes(Environment.NewLine);\n\n    private readonly IDispatcher _dispatcher;\n    private Task _task = Task.CompletedTask;\n    private readonly object _taskSync = new();\n    private HttpContext _httpContext;\n\n    public DispatchActionResult(IDispatcher dispatcher)\n    {\n        _dispatcher = dispatcher;\n    }\n\n    public override string ToString()\n    {\n        return $\"{_httpContext?.Connection.Id}:{_httpContext?.TraceIdentifier}\";\n    }\n\n    public async Task ExecuteResultAsync(ActionContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        var cancellationToken = context.HttpContext.RequestAborted;\n\n        _httpContext = context.HttpContext;\n        _httpContext.Response.ContentType = \"text/plain\";\n        _httpContext.Response.Headers[\"Connection\"] = \"close\";\n        await _httpContext.Response.Body.FlushAsync(cancellationToken).ConfigureAwait(false);\n\n        await _dispatcher.AttachAsync(this, cancellationToken).ConfigureAwait(false);\n\n        try\n        {\n            var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(new Message\n            {\n                MessageType = MessageType.Heartbeat\n            });\n\n            while (!cancellationToken.IsCancellationRequested)\n            {\n                await Task.Delay(TimeSpan.FromSeconds(35), cancellationToken).ConfigureAwait(false);\n                await SendAsync(utf8Bytes, cancellationToken).ConfigureAwait(false);\n            }\n        }\n        catch (TaskCanceledException)\n        {\n            // This is fine.\n        }\n        finally\n        {\n            _dispatcher.Detach(this);\n        }\n    }\n\n    public async Task SendAsync(byte[] bytes, CancellationToken cancellationToken)\n    {\n        var result = Task.CompletedTask;\n\n        lock (_taskSync)\n        {\n            cancellationToken.ThrowIfCancellationRequested();\n\n            if (_task.IsCanceled || _task.IsFaulted)\n            {\n                result = _task;\n            }\n            else\n            {\n                _task = DoSendAsync(_task, bytes);\n            }\n\n            async Task DoSendAsync(Task task, byte[] bytes)\n            {\n                await task.ConfigureAwait(false);\n                await _httpContext.Response.BodyWriter.WriteAsync(bytes, cancellationToken);\n                await _httpContext.Response.BodyWriter.WriteAsync(_newline, cancellationToken);\n                await _httpContext.Response.BodyWriter.FlushAsync(cancellationToken);\n            }\n        }\n\n        await result.ConfigureAwait(false);\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Protocol/DispatchConfigProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Yarp.Kubernetes.Controller.Configuration;\nusing Yarp.Kubernetes.Controller.Dispatching;\nusing Yarp.Kubernetes.Protocol;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.Kubernetes.Controller.Protocol;\n\npublic class DispatchConfigProvider : IUpdateConfig\n{\n    private readonly IDispatcher _dispatcher;\n\n    public DispatchConfigProvider(IDispatcher dispatcher)\n    {\n        ArgumentNullException.ThrowIfNull(dispatcher);\n        _dispatcher = dispatcher;\n    }\n\n    public async Task UpdateAsync(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters, CancellationToken cancellationToken)\n    {\n        var message = new Message\n        {\n            MessageType = MessageType.Update,\n            Key = string.Empty,\n            Cluster = clusters.ToList(),\n            Routes = routes.ToList(),\n        };\n\n        var bytes = JsonSerializer.SerializeToUtf8Bytes(message);\n\n        await _dispatcher.SendAsync(bytes, cancellationToken).ConfigureAwait(false);\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Protocol/DispatchController.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Mvc;\nusing Yarp.Kubernetes.Controller.Dispatching;\n\nnamespace Yarp.Kubernetes.Controller.Controllers;\n\n/// <summary>\n/// DispatchController provides API used by callers to begin streaming\n/// information being sent out through the <see cref=\"IDispatcher\"/> muxer.\n/// </summary>\n[Route(\"api/dispatch\")]\n[ApiController]\npublic class DispatchController : ControllerBase\n{\n    private readonly IDispatcher _dispatcher;\n\n    public DispatchController(IDispatcher dispatcher)\n    {\n        _dispatcher = dispatcher;\n    }\n\n    [HttpGet(\"/api/dispatch\")]\n    public Task<IActionResult> WatchAsync()\n    {\n        return Task.FromResult<IActionResult>(new DispatchActionResult(_dispatcher));\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Protocol/Dispatcher.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.Extensions.Logging;\nusing System.Collections.Immutable;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Dispatching;\n\n/// <summary>\n/// IDispatcher is a service interface to bridge outgoing data to the\n/// current connections.\n/// </summary>\npublic class Dispatcher : IDispatcher\n{\n    private readonly ILogger<Dispatcher> _logger;\n    private readonly object _targetsSync = new object();\n    private ImmutableList<IDispatchTarget> _targets = ImmutableList<IDispatchTarget>.Empty;\n    private byte[] _lastMessage;\n\n    public Dispatcher(ILogger<Dispatcher> logger)\n    {\n        _logger = logger;\n    }\n\n    public async Task AttachAsync(IDispatchTarget target, CancellationToken cancellationToken)\n    {\n        _logger.LogDebug(\"Attaching {DispatchTarget}\", target?.ToString());\n\n        lock (_targetsSync)\n        {\n            _targets = _targets.Add(target);\n        }\n\n        if (_lastMessage is not null)\n        {\n            await target.SendAsync(_lastMessage, cancellationToken).ConfigureAwait(false);\n        }\n    }\n\n    public void Detach(IDispatchTarget target)\n    {\n        _logger.LogDebug(\"Detaching {DispatchTarget}\", target?.ToString());\n\n        lock (_targetsSync)\n        {\n            _targets = _targets.Remove(target);\n        }\n    }\n\n    public async Task SendAsync(byte[] utf8Bytes, CancellationToken cancellationToken)\n    {\n        _lastMessage = utf8Bytes;\n\n        foreach (var target in _targets)\n        {\n            await target.SendAsync(utf8Bytes, cancellationToken).ConfigureAwait(false);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Protocol/IDispatchTarget.cs",
    "content": "﻿// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Dispatching;\n\n/// <summary>\n/// IDispatchTarget is what an <see cref=\"IDispatcher\"/> will use to\n/// dispatch information.\n/// </summary>\npublic interface IDispatchTarget\n{\n    public Task SendAsync(byte[] utf8Bytes, CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Protocol/IDispatcher.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Dispatching;\n\n/// <summary>\n/// IDispatcher is a service interface to bridge outgoing data to the\n/// current connections.\n/// </summary>\npublic interface IDispatcher\n{\n    Task AttachAsync(IDispatchTarget target, CancellationToken cancellationToken);\n    void Detach(IDispatchTarget target);\n    Task SendAsync(byte[] utf8Bytes, CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Protocol/Message.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.Kubernetes.Protocol;\n\npublic enum MessageType\n{\n    Heartbeat,\n    Update,\n    Remove,\n}\n\npublic struct Message\n{\n    [JsonConverter(typeof(JsonStringEnumConverter))]\n    public MessageType MessageType { get; set; }\n\n    public string Key { get; set; }\n\n    public List<RouteConfig> Routes { get; set; }\n\n    public List<ClusterConfig> Cluster { get; set; }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Protocol/MessageConfigProviderExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.Kubernetes.Controller.Configuration;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.Kubernetes.Protocol;\n\npublic static class MessageConfigProviderExtensions\n{\n    public static IReverseProxyBuilder LoadFromMessages(this IReverseProxyBuilder builder)\n    {\n        ArgumentNullException.ThrowIfNull(builder);\n\n        var provider = new KubernetesConfigProvider();\n        builder.Services.AddSingleton<IProxyConfigProvider>(provider);\n        builder.Services.AddSingleton<IUpdateConfig>(provider);\n        return builder;\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Protocol/Receiver.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.IO;\nusing System.Net.Http;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Yarp.Kubernetes.Controller.Configuration;\nusing Yarp.Kubernetes.Controller.Hosting;\nusing Yarp.Kubernetes.Controller.Rate;\n\nnamespace Yarp.Kubernetes.Protocol;\n\npublic class Receiver : BackgroundHostedService\n{\n    private readonly ReceiverOptions _options;\n    private readonly Limiter _limiter;\n    private readonly IUpdateConfig _proxyConfigProvider;\n\n    public Receiver(\n        IOptions<ReceiverOptions> options,\n        IHostApplicationLifetime hostApplicationLifetime,\n        ILogger<Receiver> logger,\n        IUpdateConfig proxyConfigProvider) : base(hostApplicationLifetime, logger)\n    {\n        ArgumentNullException.ThrowIfNull(options);\n\n        _options = options.Value;\n\n        _options.Client ??= new HttpMessageInvoker(new SocketsHttpHandler\n        {\n            ConnectTimeout = TimeSpan.FromSeconds(15),\n        });\n\n        // two requests per second after third failure\n        _limiter = new Limiter(new Limit(2), 3);\n        _proxyConfigProvider = proxyConfigProvider;\n    }\n\n    public override async Task RunAsync(CancellationToken cancellationToken)\n    {\n        while (!cancellationToken.IsCancellationRequested)\n        {\n            await _limiter.WaitAsync(cancellationToken).ConfigureAwait(false);\n\n            Logger.LogInformation(\"Connecting with {ControllerUrl}\", _options.ControllerUrl.ToString());\n\n            try\n            {\n                var requestMessage = new HttpRequestMessage(HttpMethod.Get, _options.ControllerUrl);\n                var responseMessage = await _options.Client.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);\n                using var stream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);\n                using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true);\n                using var cancellation = cancellationToken.Register(stream.Close);\n                while (true)\n                {\n                    var json = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);\n                    if (string.IsNullOrEmpty(json))\n                    {\n                        break;\n                    }\n\n                    var message = System.Text.Json.JsonSerializer.Deserialize<Message>(json);\n                    Logger.LogInformation(\"Received {MessageType} for {MessageKey}\", message.MessageType, message.Key);\n\n                    Logger.LogInformation(json);\n                    Logger.LogInformation(message.MessageType.ToString());\n\n                    if (message.MessageType == MessageType.Update)\n                    {\n                        await _proxyConfigProvider.UpdateAsync(message.Routes, message.Cluster, cancellation.Token).ConfigureAwait(false);\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                Logger.LogInformation(ex, \"Stream ended\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Protocol/ReceiverOptions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\n\nnamespace Yarp.Kubernetes.Protocol;\n\npublic class ReceiverOptions\n{\n    public Uri ControllerUrl { get; set; }\n\n    public HttpMessageInvoker Client { get; set; }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Queues/IWorkQueue.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Queues;\n\n/// <summary>\n/// Interface IWorkQueue holds a series of work item objects. When objects are removed from the queue they are noted\n/// as well in a processing set. If new items arrive while processing they are not added to the queue until\n/// the processing of that item is <see cref=\"Done\" />. In this way processing the same item twice simultaneously due to\n/// incoming event notifications is not possible.\n/// Ported from https://github.com/kubernetes/client-go/blob/master/util/workqueue/queue.go.\n/// </summary>\n/// <typeparam name=\"TItem\">The type of the t item.</typeparam>\npublic interface IWorkQueue<TItem> : IDisposable\n{\n    /// <summary>\n    /// Adds the specified item.\n    /// </summary>\n    /// <param name=\"item\">The item.</param>\n    void Add(TItem item);\n\n    /// <summary>\n    /// Returns number of items actively waiting in queue.\n    /// </summary>\n    /// <returns>System.Int32.</returns>\n    int Len();\n\n    /// <summary>\n    /// Gets the next item in the queue once it is available.\n    /// </summary>\n    /// <param name=\"cancellationToken\">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>\n    /// <returns>Task&lt;System.ValueTuple&lt;TItem, System.Boolean&gt;&gt;.</returns>\n    Task<(TItem item, bool shutdown)> GetAsync(CancellationToken cancellationToken);\n\n    /// <summary>\n    /// Called after <see cref=\"GetAsync\"/> to inform the queue that the item\n    /// processing is complete.\n    /// </summary>\n    /// <param name=\"item\">The item.</param>\n    void Done(TItem item);\n\n    /// <summary>\n    /// Shuts down.\n    /// </summary>\n    void ShutDown();\n\n    /// <summary>\n    /// Shutting down.\n    /// </summary>\n    /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>\n    bool ShuttingDown();\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Queues/ProcessingRateLimitedQueue.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Yarp.Kubernetes.Controller.Rate;\n\nnamespace Yarp.Kubernetes.Controller.Queues;\n\npublic class ProcessingRateLimitedQueue<TItem> : WorkQueue<TItem>\n{\n    private readonly Limiter _limiter;\n\n    public ProcessingRateLimitedQueue(double perSecond, int burst)\n    {\n        _limiter = new Limiter(new Limit(perSecond), burst);\n    }\n\n    protected override async Task OnGetAsync(CancellationToken cancellationToken)\n    {\n        var delay = _limiter.Reserve().Delay();\n        await Task.Delay(delay, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Queues/WorkQueue.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Queues;\n\n/// <summary>\n/// Class WorkQueue is the default implementation of a work queue.\n/// Implements the <see cref=\"IWorkQueue{TItem}\" />.\n/// </summary>\n/// <typeparam name=\"TItem\">The type of the t item.</typeparam>\n/// <seealso cref=\"IWorkQueue{TItem}\" />\npublic class WorkQueue<TItem> : IWorkQueue<TItem>\n{\n    private readonly object _sync = new object();\n    private readonly Dictionary<TItem, object> _dirty = new Dictionary<TItem, object>();\n    private readonly Dictionary<TItem, object> _processing = new Dictionary<TItem, object>();\n    private readonly Queue<TItem> _queue = new Queue<TItem>();\n    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(0);\n    private readonly CancellationTokenSource _shuttingDown = new CancellationTokenSource();\n    private bool _disposedValue; // To detect redundant calls\n\n    /// <summary>\n    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.\n    /// </summary>\n    public void Dispose()\n    {\n        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method\n        Dispose(disposing: true);\n        GC.SuppressFinalize(this);\n    }\n\n    /// <summary>\n    /// Adds the specified item.\n    /// </summary>\n    /// <param name=\"item\">The item.</param>\n    public void Add(TItem item)\n    {\n        lock (_sync)\n        {\n            if (_shuttingDown.IsCancellationRequested)\n            {\n                return;\n            }\n\n            if (_dirty.ContainsKey(item))\n            {\n                return;\n            }\n\n            _dirty.Add(item, null);\n            if (_processing.ContainsKey(item))\n            {\n                return;\n            }\n\n            _queue.Enqueue(item);\n            _semaphore.Release();\n        }\n    }\n\n    /// <summary>\n    /// Gets the specified cancellation token.\n    /// </summary>\n    /// <param name=\"cancellationToken\">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>\n    /// <returns>Task&lt;System.ValueTuple&lt;TItem, System.Boolean&gt;&gt;.</returns>\n    public async Task<(TItem item, bool shutdown)> GetAsync(CancellationToken cancellationToken)\n    {\n        using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _shuttingDown.Token))\n        {\n            try\n            {\n                await _semaphore.WaitAsync(linkedTokenSource.Token);\n\n                await OnGetAsync(linkedTokenSource.Token);\n            }\n            catch (OperationCanceledException)\n            {\n                if (_shuttingDown.IsCancellationRequested)\n                {\n                    return (default, true);\n                }\n\n                throw;\n            }\n        }\n\n        lock (_sync)\n        {\n            if (_queue.Count == 0 || _shuttingDown.IsCancellationRequested)\n            {\n                _semaphore.Release();\n                return (default, true);\n            }\n\n            var item = _queue.Dequeue();\n\n            _processing.Add(item, null);\n            _dirty.Remove(item);\n\n            return (item, false);\n        }\n    }\n\n    /// <summary>\n    /// Done the specified item.\n    /// </summary>\n    /// <param name=\"item\">The item.</param>\n    public void Done(TItem item)\n    {\n        lock (_sync)\n        {\n            _processing.Remove(item);\n            if (_dirty.ContainsKey(item))\n            {\n                _queue.Enqueue(item);\n                _semaphore.Release();\n            }\n        }\n    }\n\n    /// <summary>\n    /// Gets the queue length of this instance.\n    /// </summary>\n    public int Len()\n    {\n        lock (_sync)\n        {\n            return _queue.Count;\n        }\n    }\n\n    /// <summary>\n    /// Shuts down.\n    /// </summary>\n    public void ShutDown()\n    {\n        lock (_sync)\n        {\n            _shuttingDown.Cancel();\n            _semaphore.Release();\n        }\n    }\n\n    /// <summary>\n    /// Shutting down.\n    /// </summary>\n    /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>\n    public bool ShuttingDown()\n    {\n        return _shuttingDown.IsCancellationRequested;\n    }\n\n    /// <summary>\n    /// Releases unmanaged and - optionally - managed resources.\n    /// </summary>\n    /// <param name=\"disposing\"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>\n    protected virtual void Dispose(bool disposing)\n    {\n        if (!_disposedValue)\n        {\n            if (disposing)\n            {\n                _semaphore.Dispose();\n            }\n\n            _disposedValue = true;\n        }\n    }\n\n    /// <summary>\n    /// Called in GetAsync BEFORE the items is dequeued to allow rate-limiting of processing.\n    /// </summary>\n    /// <param name=\"cancellationToken\">The cancellation token that can be used by other objects or threads to receive notice of cancellation</param>\n    /// <returns>A task.</returns>\n    protected virtual Task OnGetAsync(CancellationToken cancellationToken)\n    {\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Rate/Limit.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics.CodeAnalysis;\n\nnamespace Yarp.Kubernetes.Controller.Rate;\n\n/// <summary>\n/// Struct Limit defines the maximum frequency of some events.\n/// Limit is represented as number of events per second.\n/// A zero Limit allows no events.\n/// https://github.com/golang/time/blob/master/rate/rate.go#L19\n/// Implements the <see cref=\"IEquatable{T}\" />.\n/// </summary>\n/// <seealso cref=\"IEquatable{T}\" />\npublic struct Limit : IEquatable<Limit>\n{\n    private readonly double _tokensPerSecond;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"Limit\"/> struct.\n    /// </summary>\n    /// <param name=\"perSecond\">The per second.</param>\n    public Limit(double perSecond)\n    {\n        _tokensPerSecond = perSecond;\n    }\n\n    /// <summary>\n    /// Gets a predefined maximum <see cref=\"Limit\"/>.\n    /// </summary>\n    /// <value>The maximum.</value>\n    public static Limit Max { get; } = new Limit(double.MaxValue);\n\n    /// <summary>\n    /// Implements the == operator.\n    /// </summary>\n    /// <param name=\"left\">The left.</param>\n    /// <param name=\"right\">The right.</param>\n    /// <returns>The result of the operator.</returns>\n    public static bool operator ==(Limit left, Limit right)\n    {\n        return left.Equals(right);\n    }\n\n    /// <summary>\n    /// Implements the != operator.\n    /// </summary>\n    /// <param name=\"left\">The left.</param>\n    /// <param name=\"right\">The right.</param>\n    /// <returns>The result of the operator.</returns>\n    public static bool operator !=(Limit left, Limit right)\n    {\n        return !(left == right);\n    }\n\n    /// <summary>\n    /// TokensFromDuration is a unit conversion function from a time duration to the number of tokens\n    /// which could be accumulated during that duration at a rate of limit tokens per second.\n    /// https://github.com/golang/time/blob/master/rate/rate.go#L396.\n    /// </summary>\n    /// <param name=\"duration\">The duration.</param>\n    /// <returns>System.Double.</returns>\n    public double TokensFromDuration(TimeSpan duration)\n    {\n        var sec = duration.Ticks / TimeSpan.TicksPerSecond * _tokensPerSecond;\n        var nsec = duration.Ticks % TimeSpan.TicksPerSecond * _tokensPerSecond;\n        return sec + nsec / TimeSpan.TicksPerSecond;\n    }\n\n    /// <summary>\n    /// Durations from tokens is a unit conversion function from the number of tokens to the duration\n    /// of time it takes to accumulate them at a rate of limit tokens per second.\n    /// https://github.com/golang/time/blob/master/rate/rate.go#L389.\n    /// </summary>\n    /// <param name=\"tokens\">The tokens.</param>\n    /// <returns>TimeSpan.</returns>\n    public TimeSpan DurationFromTokens(double tokens)\n    {\n        return TimeSpan.FromSeconds(tokens / _tokensPerSecond);\n    }\n\n    /// <summary>\n    /// Determines whether the specified <see cref=\"object\" /> is equal to this instance.\n    /// </summary>\n    /// <param name=\"obj\">The object to compare with the current instance.</param>\n    /// <returns><c>true</c> if the specified <see cref=\"object\" /> is equal to this instance; otherwise, <c>false</c>.</returns>\n    public override bool Equals(object obj)\n    {\n        return obj is Limit limit && Equals(limit);\n    }\n\n    /// <summary>\n    /// Indicates whether the current object is equal to another object of the same type.\n    /// </summary>\n    /// <param name=\"other\">An object to compare with this object.</param>\n    /// <returns><see langword=\"true\" /> if the current object is equal to the <paramref name=\"other\" /> parameter; otherwise, <see langword=\"false\" />.</returns>\n    public bool Equals([AllowNull] Limit other)\n    {\n        return _tokensPerSecond == other._tokensPerSecond;\n    }\n\n    /// <summary>\n    /// Returns a hash code for this instance.\n    /// </summary>\n    /// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(_tokensPerSecond);\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Rate/Limiter.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Rate;\n\n/// <summary>\n/// <para>\n/// Class Limiter controls how frequently events are allowed to happen.\n/// It implements a \"token bucket\" of size b, initially full and refilled\n/// at rate r tokens per second.\n/// Informally, in any large enough time interval, the Limiter limits the\n/// rate to r tokens per second, with a maximum burst size of b events.\n/// As a special case, if r == Inf (the infinite rate), b is ignored.\n/// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets.\n/// </para>\n/// <para>\n/// The zero value is a valid Limiter, but it will reject all events.\n/// Use NewLimiter to create non-zero Limiters.\n/// </para>\n/// <para>\n/// Limiter has three main methods, Allow, Reserve, and Wait.\n/// Most callers should use Wait.\n/// </para>\n/// <para>\n/// Each of the three methods consumes a single token.\n/// They differ in their behavior when no token is available.\n/// If no token is available, Allow returns false.\n/// If no token is available, Reserve returns a reservation for a future token\n/// and the amount of time the caller must wait before using it.\n/// If no token is available, Wait blocks until one can be obtained\n/// or its associated context.Context is canceled.\n/// The methods AllowN, ReserveN, and WaitN consume n tokens.\n/// </para>\n/// https://github.com/golang/time/blob/master/rate/rate.go#L55.\n/// </summary>\npublic class Limiter\n{\n    private readonly object _sync = new object();\n    private readonly Limit _limit;\n    private readonly TimeProvider _timeProvider;\n    private readonly int _burst;\n    private double _tokens;\n\n    /// <summary>\n    /// The last time the limiter's tokens field was updated.\n    /// </summary>\n    private DateTimeOffset _last;\n\n    /// <summary>\n    /// the latest time of a rate-limited event (past or future).\n    /// </summary>\n    private DateTimeOffset _lastEvent;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"Limiter\" /> class.\n    /// Allows events up to <see cref=\"Limit\" /><paramref name=\"limit\" /> and permits bursts of\n    /// at most <paramref name=\"burst\" /> tokens.\n    /// </summary>\n    /// <param name=\"limit\">The count per second which is allowed.</param>\n    /// <param name=\"burst\">The burst.</param>\n    /// <param name=\"timeProvider\">Accessor for the current UTC time.</param>\n    public Limiter(Limit limit, int burst, TimeProvider timeProvider = default)\n    {\n        _limit = limit;\n        _burst = burst;\n        _timeProvider = timeProvider ?? TimeProvider.System;\n    }\n\n    /// <summary>\n    /// Checks if a token is available effective immediately. If so, it is consumed.\n    /// </summary>\n    /// <returns><c>true</c> if a token is available and used, <c>false</c> otherwise.</returns>\n    public bool Allow()\n    {\n        return AllowN(_timeProvider.GetUtcNow(), 1);\n    }\n\n    /// <summary>\n    /// Checks if a number of tokens are available by a given time.\n    /// They are consumed if available.\n    /// </summary>\n    /// <param name=\"now\">The now.</param>\n    /// <param name=\"number\">The number.</param>\n    /// <returns><c>true</c> if a number token is available and used, <c>false</c> otherwise.</returns>\n    public bool AllowN(DateTimeOffset now, int number)\n    {\n        return ReserveImpl(now, number, TimeSpan.Zero).Ok;\n    }\n\n    /// <summary>\n    /// Reserves this instance.\n    /// </summary>\n    /// <returns>Reservation.</returns>\n    public Reservation Reserve()\n    {\n        return Reserve(_timeProvider.GetUtcNow(), 1);\n    }\n\n    /// <summary>\n    /// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen.\n    /// The Limiter takes this Reservation into account when allowing future events.\n    /// The returned Reservation’s OK() method returns false if n exceeds the Limiter's burst size.\n    /// Usage example:\n    /// r := lim.ReserveN(time.Now(), 1)\n    /// if !r.OK() {\n    /// return\n    /// }\n    /// time.Sleep(r.Delay())\n    /// Act()\n    /// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events.\n    /// If you need to respect a deadline or cancel the delay, use Wait instead.\n    /// To drop or skip events exceeding rate limit, use Allow instead.\n    /// </summary>\n    /// <param name=\"now\">The now.</param>\n    /// <param name=\"count\">The number.</param>\n    /// <returns>Reservation.</returns>\n    public Reservation Reserve(DateTimeOffset now, int count)\n    {\n        return ReserveImpl(now, count, TimeSpan.MaxValue);\n    }\n\n    /// <summary>\n    /// Waits the asynchronous.\n    /// </summary>\n    /// <param name=\"cancellationToken\">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>\n    /// <returns>Task.</returns>\n    public Task WaitAsync(CancellationToken cancellationToken)\n    {\n        return WaitAsync(1, cancellationToken);\n    }\n\n    /// <summary>\n    /// wait as an asynchronous operation.\n    /// </summary>\n    /// <param name=\"count\">The count.</param>\n    /// <param name=\"cancellationToken\">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>\n    /// <exception cref=\"Exception\">rate: Wait(count={count}) exceeds limiter's burst {burst}.</exception>\n    /// <returns>A <see cref=\"Task\"/> representing the asynchronous operation.</returns>\n    public async Task WaitAsync(int count, CancellationToken cancellationToken)\n    {\n        // https://github.com/golang/time/blob/master/rate/rate.go#L226\n        int burst = default;\n        Limit limit = default;\n        lock (_sync)\n        {\n            burst = _burst;\n            limit = _limit;\n        }\n\n        if (count > burst && limit != Limit.Max)\n        {\n            throw new Exception($\"rate: Wait(count={count}) exceeds limiter's burst {burst}\");\n        }\n\n        // Check if ctx is already cancelled\n        cancellationToken.ThrowIfCancellationRequested();\n\n        // Determine wait limit\n        var waitLimit = limit.DurationFromTokens(count);\n\n        while (true)\n        {\n            var now = _timeProvider.GetUtcNow();\n            var r = ReserveImpl(now, count, waitLimit);\n            if (r.Ok)\n            {\n                var delay = r.DelayFrom(now);\n                if (delay > TimeSpan.Zero)\n                {\n                    await Task.Delay(delay, cancellationToken).ConfigureAwait(false);\n                }\n\n                return;\n            }\n\n            await Task.Delay(waitLimit, cancellationToken).ConfigureAwait(false);\n        }\n    }\n\n    /// <summary>\n    /// reserveN is a helper method for AllowN, ReserveN, and WaitN.\n    /// maxFutureReserve specifies the maximum reservation wait duration allowed.\n    /// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN.\n    /// </summary>\n    /// <param name=\"now\">The now.</param>\n    /// <param name=\"number\">The number.</param>\n    /// <param name=\"maxFutureReserve\">The maximum future reserve.</param>\n    /// <returns>Reservation.</returns>\n    private Reservation ReserveImpl(DateTimeOffset now, int number, TimeSpan maxFutureReserve)\n    {\n        lock (_sync)\n        {\n            if (_limit == Limit.Max)\n            {\n                return new Reservation(\n                    timeProvider: _timeProvider,\n                    limiter: this,\n                    ok: true,\n                    tokens: number,\n                    timeToAct: now);\n            }\n\n            var (newNow, last, tokens) = Advance(now);\n            now = newNow;\n\n            // Calculate the remaining number of tokens resulting from the request.\n            tokens -= number;\n\n            // Calculate the wait duration\n            TimeSpan waitDuration = default;\n            if (tokens < 0)\n            {\n                waitDuration = _limit.DurationFromTokens(-tokens);\n            }\n\n            // Decide result\n            var ok = number <= _burst && waitDuration <= maxFutureReserve;\n\n            // Prepare reservation\n            if (ok)\n            {\n                var reservation = new Reservation(\n                    timeProvider: _timeProvider,\n                    limiter: this,\n                    ok: true,\n                    tokens: number,\n                    limit: _limit,\n                    timeToAct: now.Add(waitDuration));\n\n                _last = newNow;\n                _tokens = tokens;\n                _lastEvent = reservation.TimeToAct;\n\n                return reservation;\n            }\n            else\n            {\n                var reservation = new Reservation(\n                    timeProvider: _timeProvider,\n                    limiter: this,\n                    ok: false,\n                    limit: _limit);\n\n                _last = last;\n\n                return reservation;\n            }\n        }\n    }\n\n    /// <summary>\n    /// advance calculates and returns an updated state for lim resulting from the passage of time.\n    /// lim is not changed.\n    /// advance requires that lim.mu is held.\n    /// </summary>\n    /// <param name=\"now\">The now.</param>\n    private (DateTimeOffset newNow, DateTimeOffset newLast, double newTokens) Advance(DateTimeOffset now)\n    {\n        lock (_sync)\n        {\n            var last = _last;\n            if (now < last)\n            {\n                last = now;\n            }\n\n            // Avoid making delta overflow below when last is very old.\n            var maxElapsed = _limit.DurationFromTokens(_burst - _tokens);\n            var elapsed = now - last;\n            if (elapsed > maxElapsed)\n            {\n                elapsed = maxElapsed;\n            }\n\n            // Calculate the new number of tokens, due to time that passed.\n            var delta = _limit.TokensFromDuration(elapsed);\n            var tokens = _tokens + delta;\n            if (tokens > _burst)\n            {\n                tokens = _burst;\n            }\n\n            return (now, last, tokens);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Rate/Reservation.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\n#pragma warning disable CS0169 // The field 'Reservation.limit' is never used\n\nnamespace Yarp.Kubernetes.Controller.Rate;\n\n/// <summary>\n/// Class Reservation holds information about events that are permitted by a Limiter to happen after a delay.\n/// A Reservation may be canceled, which may enable the Limiter to permit additional events.\n/// https://github.com/golang/time/blob/master/rate/rate.go#L106.\n/// </summary>\npublic class Reservation\n{\n    private readonly TimeProvider _timeProvider;\n    private readonly Limiter _limiter;\n    private readonly Limit _limit;\n    private readonly double _tokens;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"Reservation\"/> class.\n    /// </summary>\n    /// <param name=\"timeProvider\">Gets the system time.</param>\n    /// <param name=\"limiter\">The limiter.</param>\n    /// <param name=\"ok\">if set to <c>true</c> [ok].</param>\n    /// <param name=\"tokens\">The tokens.</param>\n    /// <param name=\"timeToAct\">The time to act.</param>\n    /// <param name=\"limit\">The limit.</param>\n    public Reservation(\n        TimeProvider timeProvider,\n        Limiter limiter,\n        bool ok,\n        double tokens = default,\n        DateTimeOffset timeToAct = default,\n        Limit limit = default)\n    {\n        _timeProvider = timeProvider;\n        _limiter = limiter;\n        Ok = ok;\n        _tokens = tokens;\n        TimeToAct = timeToAct;\n        _limit = limit;\n    }\n\n    /// <summary>\n    /// Gets a value indicating whether this <see cref=\"Reservation\"/> is ok.\n    /// </summary>\n    /// <value><c>true</c> if ok; otherwise, <c>false</c>.</value>\n    public bool Ok { get; }\n\n    /// <summary>\n    /// Gets the time to act.\n    /// </summary>\n    /// <value>The time to act.</value>\n    public DateTimeOffset TimeToAct { get; }\n\n    /// <summary>\n    /// Delays this instance.\n    /// </summary>\n    /// <returns>TimeSpanOffset.</returns>\n    public TimeSpan Delay()\n    {\n        return DelayFrom(_timeProvider.GetUtcNow());\n    }\n\n    /// <summary>\n    /// Delays from.\n    /// </summary>\n    /// <param name=\"now\">The now.</param>\n    /// <returns>TimeSpan.</returns>\n    public TimeSpan DelayFrom(DateTimeOffset now)\n    {\n        // https://github.com/golang/time/blob/master/rate/rate.go#L134\n        if (!Ok)\n        {\n            return TimeSpan.MaxValue;\n        }\n\n        var delay = TimeToAct - now;\n        if (delay < TimeSpan.Zero)\n        {\n            return TimeSpan.Zero;\n        }\n\n        return delay;\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Services/IReconciler.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Services;\n\n/// <summary>\n/// IReconciler is a service interface called by the <see cref=\"IngressController\"/> to process\n/// the work items as they are dequeued.\n/// </summary>\npublic interface IReconciler\n{\n    Task ProcessAsync(CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Services/IngressController.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing k8s;\nusing k8s.Models;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Yarp.Kubernetes.Controller.Caching;\nusing Yarp.Kubernetes.Controller.Client;\nusing Yarp.Kubernetes.Controller.Hosting;\nusing Yarp.Kubernetes.Controller.Queues;\n\nnamespace Yarp.Kubernetes.Controller.Services;\n\n/// <summary>\n/// Controller receives notifications from informers. The data which is needed for processing is\n/// saved in an <see cref=\"ICache\"/> instance and resources which need to be reconciled are\n/// added to an <see cref=\"ProcessingRateLimitedQueue{QueueItem}\"/>. The background task dequeues\n/// items and passes them to an <see cref=\"IReconciler\"/> service for processing.\n/// </summary>\npublic class IngressController : BackgroundHostedService\n{\n    private readonly IReadOnlyList<IResourceInformerRegistration> _registrations;\n    private readonly ICache _cache;\n    private readonly IReconciler _reconciler;\n\n    private bool _registrationsReady;\n    private readonly WorkQueue<QueueItem> _queue;\n    private readonly QueueItem _ingressChangeQueueItem;\n\n    public IngressController(\n        ICache cache,\n        IReconciler reconciler,\n        IResourceInformer<V1Ingress> ingressInformer,\n        IResourceInformer<V1Service> serviceInformer,\n        IResourceInformer<V1Endpoints> endpointsInformer,\n        IResourceInformer<V1IngressClass> ingressClassInformer,\n        IResourceInformer<V1Secret> secretInformer,\n        IHostApplicationLifetime hostApplicationLifetime,\n        IOptions<YarpOptions> options,\n        ILogger<IngressController> logger)\n        : base(hostApplicationLifetime, logger)\n    {\n        ArgumentNullException.ThrowIfNull(ingressInformer, nameof(ingressInformer));\n        ArgumentNullException.ThrowIfNull(serviceInformer, nameof(serviceInformer));\n        ArgumentNullException.ThrowIfNull(endpointsInformer, nameof(endpointsInformer));\n        ArgumentNullException.ThrowIfNull(ingressClassInformer, nameof(ingressClassInformer));\n        ArgumentNullException.ThrowIfNull(secretInformer, nameof(secretInformer));\n        ArgumentNullException.ThrowIfNull(options, nameof(options));\n\n        var watchSecrets = options.Value.ServerCertificates;\n\n        var registrations = new List<IResourceInformerRegistration>()\n        {\n            serviceInformer.Register(Notification),\n            endpointsInformer.Register(Notification),\n            ingressClassInformer.Register(Notification),\n            ingressInformer.Register(Notification)\n        };\n\n        if (watchSecrets)\n        {\n            registrations.Add(secretInformer.Register(Notification));\n        }\n\n        _registrations = registrations;\n\n        _registrationsReady = false;\n        serviceInformer.StartWatching();\n        endpointsInformer.StartWatching();\n        ingressClassInformer.StartWatching();\n        ingressInformer.StartWatching();\n\n        if (watchSecrets)\n        {\n            secretInformer.StartWatching();\n        }\n\n        _queue = new ProcessingRateLimitedQueue<QueueItem>(perSecond: 0.5, burst: 1);\n\n        ArgumentNullException.ThrowIfNull(cache);\n        ArgumentNullException.ThrowIfNull(reconciler);\n\n        _cache = cache;\n        _reconciler = reconciler;\n\n        _ingressChangeQueueItem = new QueueItem(\"Ingress Change\");\n    }\n\n    /// <summary>\n    /// Disconnects from resource informers, and cause queue to become shut down.\n    /// </summary>\n    /// <param name=\"disposing\"></param>\n    protected override void Dispose(bool disposing)\n    {\n        if (disposing)\n        {\n            foreach (var registration in _registrations)\n            {\n                registration.Dispose();\n            }\n\n            _queue.Dispose();\n        }\n\n        base.Dispose(disposing);\n    }\n\n    /// <summary>\n    /// Called by the informer with real-time resource updates.\n    /// </summary>\n    /// <param name=\"eventType\">Indicates if the resource new, updated, or deleted.</param>\n    /// <param name=\"resource\">The information as provided by the Kubernetes API server.</param>\n    private void Notification(WatchEventType eventType, V1Ingress resource)\n    {\n        if (_cache.Update(eventType, resource))\n        {\n            NotificationIngressChanged();\n        }\n    }\n\n    private void NotificationIngressChanged()\n    {\n        if (!_registrationsReady)\n        {\n            return;\n        }\n\n        _queue.Add(_ingressChangeQueueItem);\n    }\n\n    /// <summary>\n    /// Called by the informer with real-time resource updates.\n    /// </summary>\n    /// <param name=\"eventType\">Indicates if the resource new, updated, or deleted.</param>\n    /// <param name=\"resource\">The information as provided by the Kubernetes API server.</param>\n    private void Notification(WatchEventType eventType, V1Service resource)\n    {\n        var ingressNames = _cache.Update(eventType, resource);\n        if (ingressNames.Count > 0)\n        {\n            NotificationIngressChanged();\n        }\n    }\n\n    /// <summary>\n    /// Called by the informer with real-time resource updates.\n    /// </summary>\n    /// <param name=\"eventType\">Indicates if the resource new, updated, or deleted.</param>\n    /// <param name=\"resource\">The information as provided by the Kubernetes API server.</param>\n    private void Notification(WatchEventType eventType, V1Endpoints resource)\n    {\n        var ingressNames = _cache.Update(eventType, resource);\n        if (ingressNames.Count > 0)\n        {\n            NotificationIngressChanged();\n        }\n    }\n\n    /// <summary>\n    /// Called by the informer with real-time resource updates.\n    /// </summary>\n    /// <param name=\"eventType\">Indicates if the resource new, updated, or deleted.</param>\n    /// <param name=\"resource\">The information as provided by the Kubernetes API server.</param>\n    private void Notification(WatchEventType eventType, V1IngressClass resource)\n    {\n        _cache.Update(eventType, resource);\n    }\n\n    /// <summary>\n    /// Called by the informer with real-time resource updates.\n    /// </summary>\n    /// <param name=\"eventType\">Indicates if the resource new, updated, or deleted.</param>\n    /// <param name=\"resource\">The information as provided by the Kubernetes API server.</param>\n    private void Notification(WatchEventType eventType, V1Secret resource)\n    {\n        _cache.Update(eventType, resource);\n    }\n\n    /// <summary>\n    /// Called once at startup by the hosting infrastructure. This function must remain running\n    /// for the entire lifetime of an application.\n    /// </summary>\n    /// <param name=\"cancellationToken\">Indicates when the web application is shutting down.</param>\n    /// <returns>The Task representing the async function results.</returns>\n    public override async Task RunAsync(CancellationToken cancellationToken)\n    {\n        // First wait for all informers to fully List resources before processing begins.\n        foreach (var registration in _registrations)\n        {\n            await registration.ReadyAsync(cancellationToken).ConfigureAwait(false);\n        }\n\n        // At this point we know that all the Ingress and Endpoint caches are at least in sync\n        // with cluster's state as of the start of this controller.\n        _registrationsReady = true;\n        NotificationIngressChanged();\n\n        // Now begin one loop to process work until an application shutdown is requested.\n        while (!cancellationToken.IsCancellationRequested)\n        {\n            // Dequeue the next item to process\n            var (item, shutdown) = await _queue.GetAsync(cancellationToken).ConfigureAwait(false);\n            if (shutdown)\n            {\n                Logger.LogInformation(\"Work queue has been shutdown. Exiting reconciliation loop.\");\n                return;\n            }\n\n            try\n            {\n                await _reconciler.ProcessAsync(cancellationToken).ConfigureAwait(false);\n            }\n            catch\n            {\n                Logger.LogInformation(\"Rescheduling {Change}\", item.Change);\n\n                // Any failure to process this item results in being re-queued\n                _queue.Add(item);\n            }\n            finally\n            {\n                _queue.Done(item);\n            }\n        }\n\n        Logger.LogInformation(\"Reconciliation loop cancelled\");\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Services/QueueItem.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.Kubernetes.Controller.Services;\n\n/// <summary>\n/// QueueItem acts as the \"Key\" for the _queue to manage items.\n/// </summary>\npublic struct QueueItem : IEquatable<QueueItem>\n{\n    public QueueItem(string change)\n    {\n        Change = change;\n    }\n\n    /// <summary>\n    /// This identifies that a change has occurred and either configuration requires to be rebuilt, or needs to be dispatched.\n    /// </summary>\n    public string Change { get; }\n\n    public override bool Equals(object obj)\n    {\n        return obj is QueueItem item && Equals(item);\n    }\n\n    public bool Equals(QueueItem other)\n    {\n        return Change.Equals(other.Change, StringComparison.Ordinal);\n    }\n\n    public override int GetHashCode()\n    {\n        return Change.GetHashCode();\n    }\n\n    public static bool operator ==(QueueItem left, QueueItem right)\n    {\n        return left.Equals(right);\n    }\n\n    public static bool operator !=(QueueItem left, QueueItem right)\n    {\n        return !(left == right);\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Services/ReconcileData.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Yarp.Kubernetes.Controller.Caching;\n\nnamespace Yarp.Kubernetes.Controller.Services;\n\n/// <summary>\n/// ReconcileData is the information returned from <see cref=\"ICache.TryGetReconcileData(Yarp.Kubernetes.Controller.NamespacedName, out ReconcileData)\"/>\n/// and needed by <see cref=\"IReconciler.ProcessAsync(System.Threading.CancellationToken)\"/>.\n/// </summary>\npublic struct ReconcileData\n{\n    public ReconcileData(IngressData ingress, List<ServiceData> services, List<Endpoints> endpoints)\n    {\n        Ingress = ingress;\n        ServiceList = services;\n        EndpointsList = endpoints;\n    }\n\n    public IngressData Ingress { get; }\n    public List<ServiceData> ServiceList { get; }\n    public List<Endpoints> EndpointsList { get; }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Services/Reconciler.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Logging;\nusing Yarp.Kubernetes.Controller.Caching;\nusing Yarp.Kubernetes.Controller.Client;\nusing Yarp.Kubernetes.Controller.Configuration;\nusing Yarp.Kubernetes.Controller.Converters;\n\nnamespace Yarp.Kubernetes.Controller.Services;\n\n/// <summary>\n/// IReconciler is a service interface called by the <see cref=\"IngressController\"/> to process\n/// the work items as they are dequeued.\n/// </summary>\npublic partial class Reconciler : IReconciler\n{\n    private readonly ICache _cache;\n    private readonly IUpdateConfig _updateConfig;\n    private readonly IIngressResourceStatusUpdater _ingressResourceStatusUpdater;\n    private readonly ILogger<Reconciler> _logger;\n\n    public Reconciler(ICache cache, IUpdateConfig updateConfig, IIngressResourceStatusUpdater ingressResourceStatusUpdater, ILogger<Reconciler> logger)\n    {\n        ArgumentNullException.ThrowIfNull(cache);\n        ArgumentNullException.ThrowIfNull(updateConfig);\n        ArgumentNullException.ThrowIfNull(ingressResourceStatusUpdater);\n\n        _cache = cache;\n        _updateConfig = updateConfig;\n        _ingressResourceStatusUpdater = ingressResourceStatusUpdater;\n        _logger = logger;\n    }\n\n    public async Task ProcessAsync(CancellationToken cancellationToken)\n    {\n        try\n        {\n            var ingresses = _cache.GetIngresses().ToArray();\n\n            var configContext = new YarpConfigContext();\n\n            foreach (var ingress in ingresses)\n            {\n                try\n                {\n                    if (_cache.TryGetReconcileData(new NamespacedName(ingress.Metadata.NamespaceProperty, ingress.Metadata.Name), out var data))\n                    {\n                        var ingressContext = new YarpIngressContext(ingress, data.ServiceList, data.EndpointsList);\n                        YarpParser.ConvertFromKubernetesIngress(ingressContext, configContext);\n                    }\n                }\n                catch (Exception ex)\n                {\n                    _logger.LogWarning(ex, \"Uncaught exception occurred while reconciling ingress {IngressNamespace}/{IngressName}\", ingress.Metadata.NamespaceProperty, ingress.Metadata.Name);\n                }\n            }\n\n            var clusters = configContext.BuildClusterConfig();\n\n            _logger.LogInformation(JsonSerializer.Serialize(configContext.Routes));\n            _logger.LogInformation(JsonSerializer.Serialize(clusters));\n\n            await _updateConfig.UpdateAsync(configContext.Routes, clusters, cancellationToken).ConfigureAwait(false);\n            await _ingressResourceStatusUpdater.UpdateStatusAsync(cancellationToken);\n        }\n        catch (Exception ex)\n        {\n            _logger.LogWarning(ex, \"Uncaught exception occurred while reconciling\");\n            throw;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Kubernetes.Controller/Yarp.Kubernetes.Controller.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <Description>Toolkit for building a Kubernetes Ingress Controller in .NET using the infrastructure from ASP.NET and .NET</Description>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <OutputType>Library</OutputType>\n    <NoWarn>$(NoWarn);CS8002</NoWarn>\n    <IsPackable>true</IsPackable>\n    <IsShipping>false</IsShipping>\n    <PackageTags>yarp;dotnet;reverse-proxy;aspnetcore;kubernetes</PackageTags>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"KubernetesClient\" Version=\"$(KubernetesClientVersion)\" />\n    <PackageReference Include=\"YamlDotNet\" Version=\"$(YamlDotNetVersion)\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Kubernetes.Controller/YarpOptions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.Kubernetes.Controller;\n\npublic class YarpOptions\n{\n    /// <summary>\n    /// Defines a name of the ingress controller. IngressClass \".spec.controller\" field should match this.\n    /// This field is required.\n    /// </summary>\n    public string ControllerClass { get; set; }\n\n    public bool ServerCertificates { get; set; }\n\n    public string DefaultSslCertificate { get; set; }\n\n    /// <summary>\n    /// Name of the Kubernetes Service the ingress controller is running in.\n    /// This field is required.\n    /// </summary>\n    public string ControllerServiceName { get; set; }\n\n    /// <summary>\n    /// Namespace of the Kubernetes Service the ingress controller is running in.\n    /// This field is required.\n    /// </summary>\n    public string ControllerServiceNamespace { get; set; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/ActiveHealthCheckConfig.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Active health check config.\n/// </summary>\npublic sealed record ActiveHealthCheckConfig\n{\n    /// <summary>\n    /// Whether active health checks are enabled.\n    /// </summary>\n    public bool? Enabled { get; init; }\n\n    /// <summary>\n    /// Health probe interval.\n    /// </summary>\n    public TimeSpan? Interval { get; init; }\n\n    /// <summary>\n    /// Health probe timeout, after which a destination is considered unhealthy.\n    /// </summary>\n    public TimeSpan? Timeout { get; init; }\n\n    /// <summary>\n    /// Active health check policy.\n    /// </summary>\n    public string? Policy { get; init; }\n\n    /// <summary>\n    /// HTTP health check endpoint path.\n    /// </summary>\n    public string? Path { get; init; }\n\n    /// <summary>\n    /// Query string to append to the probe, including the leading '?'.\n    /// </summary>\n    public string? Query { get; init; }\n\n    public bool Equals(ActiveHealthCheckConfig? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return Enabled == other.Enabled\n            && Interval == other.Interval\n            && Timeout == other.Timeout\n            && string.Equals(Policy, other.Policy, StringComparison.OrdinalIgnoreCase)\n            && string.Equals(Path, other.Path, StringComparison.Ordinal)\n            && string.Equals(Query, other.Query, StringComparison.Ordinal);\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(Enabled,\n            Interval,\n            Timeout,\n            Policy?.GetHashCode(StringComparison.OrdinalIgnoreCase),\n            Path?.GetHashCode(StringComparison.Ordinal),\n            Query?.GetHashCode(StringComparison.Ordinal));\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/AuthorizationConstants.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Configuration;\n\ninternal static class AuthorizationConstants\n{\n    internal const string Default = \"Default\";\n    internal const string Anonymous = \"Anonymous\";\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/ClusterConfig.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// A cluster is a group of equivalent endpoints and associated policies.\n/// </summary>\npublic sealed record ClusterConfig\n{\n    /// <summary>\n    /// The Id for this cluster. This needs to be globally unique.\n    /// This field is required.\n    /// </summary>\n    public string ClusterId { get; init; } = default!;\n\n    /// <summary>\n    /// Load balancing policy.\n    /// </summary>\n    public string? LoadBalancingPolicy { get; init; }\n\n    /// <summary>\n    /// Session affinity config.\n    /// </summary>\n    public SessionAffinityConfig? SessionAffinity { get; init; }\n\n    /// <summary>\n    /// Health checking config.\n    /// </summary>\n    public HealthCheckConfig? HealthCheck { get; init; }\n\n    /// <summary>\n    /// Config for the HTTP client that is used to call destinations in this cluster.\n    /// </summary>\n    public HttpClientConfig? HttpClient { get; init; }\n\n    /// <summary>\n    /// Config for outgoing HTTP requests.\n    /// </summary>\n    public ForwarderRequestConfig? HttpRequest { get; init; }\n\n    /// <summary>\n    /// The set of destinations associated with this cluster.\n    /// </summary>\n    public IReadOnlyDictionary<string, DestinationConfig>? Destinations { get; init; }\n\n    /// <summary>\n    /// Arbitrary key-value pairs that further describe this cluster.\n    /// </summary>\n    public IReadOnlyDictionary<string, string>? Metadata { get; init; }\n\n    public bool Equals(ClusterConfig? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return EqualsExcludingDestinations(other)\n            && CollectionEqualityHelper.Equals(Destinations, other.Destinations);\n    }\n\n    internal bool EqualsExcludingDestinations(ClusterConfig other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return string.Equals(ClusterId, other.ClusterId, StringComparison.OrdinalIgnoreCase)\n            && string.Equals(LoadBalancingPolicy, other.LoadBalancingPolicy, StringComparison.OrdinalIgnoreCase)\n            // CS0252 warning only shows up in VS https://github.com/dotnet/roslyn/issues/49302\n            && SessionAffinity == other.SessionAffinity\n            && HealthCheck == other.HealthCheck\n            && HttpClient == other.HttpClient\n            && HttpRequest == other.HttpRequest\n            && CaseSensitiveEqualHelper.Equals(Metadata, other.Metadata);\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(\n            ClusterId?.GetHashCode(StringComparison.OrdinalIgnoreCase),\n            LoadBalancingPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase),\n            SessionAffinity,\n            HealthCheck,\n            HttpClient,\n            HttpRequest,\n            CollectionEqualityHelper.GetHashCode(Destinations),\n            CaseSensitiveEqualHelper.GetHashCode(Metadata));\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/ClusterValidators/DestinationValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Configuration.ClusterValidators;\n\ninternal sealed class DestinationValidator : IClusterValidator\n{\n    public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors)\n    {\n        if (cluster.Destinations is null)\n        {\n            return ValueTask.CompletedTask;\n        }\n\n        foreach (var (name, destination) in cluster.Destinations)\n        {\n            if (string.IsNullOrEmpty(destination.Address))\n            {\n                errors.Add(new ArgumentException($\"No address found for destination '{name}' on cluster '{cluster.ClusterId}'.\"));\n            }\n        }\n\n        return ValueTask.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/ClusterValidators/HealthCheckValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Yarp.ReverseProxy.Health;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Configuration.ClusterValidators;\n\ninternal sealed class HealthCheckValidator : IClusterValidator\n{\n    private readonly FrozenDictionary<string, IAvailableDestinationsPolicy> _availableDestinationsPolicies;\n    private readonly FrozenDictionary<string, IActiveHealthCheckPolicy> _activeHealthCheckPolicies;\n    private readonly FrozenDictionary<string, IPassiveHealthCheckPolicy> _passiveHealthCheckPolicies;\n\n    public HealthCheckValidator(IEnumerable<IAvailableDestinationsPolicy> availableDestinationsPolicies,\n        IEnumerable<IActiveHealthCheckPolicy> activeHealthCheckPolicies,\n        IEnumerable<IPassiveHealthCheckPolicy> passiveHealthCheckPolicies)\n    {\n        ArgumentNullException.ThrowIfNull(availableDestinationsPolicies);\n        ArgumentNullException.ThrowIfNull(activeHealthCheckPolicies);\n        ArgumentNullException.ThrowIfNull(passiveHealthCheckPolicies);\n\n        _availableDestinationsPolicies = availableDestinationsPolicies.ToDictionaryByUniqueId(p => p.Name);\n        _activeHealthCheckPolicies = activeHealthCheckPolicies.ToDictionaryByUniqueId(p => p.Name);\n        _passiveHealthCheckPolicies = passiveHealthCheckPolicies.ToDictionaryByUniqueId(p => p.Name);\n    }\n\n    public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors)\n    {\n        var availableDestinationsPolicy = cluster.HealthCheck?.AvailableDestinationsPolicy;\n        if (string.IsNullOrEmpty(availableDestinationsPolicy))\n        {\n            // The default.\n            availableDestinationsPolicy = HealthCheckConstants.AvailableDestinations.HealthyOrPanic;\n        }\n\n        if (!_availableDestinationsPolicies.ContainsKey(availableDestinationsPolicy))\n        {\n            errors.Add(new ArgumentException($\"No matching {nameof(IAvailableDestinationsPolicy)} found for the available destinations policy '{availableDestinationsPolicy}' set on the cluster.'{cluster.ClusterId}'.\"));\n        }\n\n        ValidateActiveHealthCheck(cluster, errors);\n        ValidatePassiveHealthCheck(cluster, errors);\n\n        return ValueTask.CompletedTask;\n    }\n\n    private void ValidateActiveHealthCheck(ClusterConfig cluster, IList<Exception> errors)\n    {\n        if (!(cluster.HealthCheck?.Active?.Enabled ?? false))\n        {\n            // Active health check is disabled\n            return;\n        }\n\n        var activeOptions = cluster.HealthCheck.Active;\n        var policy = activeOptions.Policy;\n        if (string.IsNullOrEmpty(policy))\n        {\n            // default policy\n            policy = HealthCheckConstants.ActivePolicy.ConsecutiveFailures;\n        }\n        if (!_activeHealthCheckPolicies.ContainsKey(policy))\n        {\n            errors.Add(new ArgumentException($\"No matching {nameof(IActiveHealthCheckPolicy)} found for the active health check policy name '{policy}' set on the cluster '{cluster.ClusterId}'.\"));\n        }\n\n        if (activeOptions.Interval is not null && activeOptions.Interval <= TimeSpan.Zero)\n        {\n            errors.Add(new ArgumentException($\"Destination probing interval set on the cluster '{cluster.ClusterId}' must be positive.\"));\n        }\n\n        if (activeOptions.Timeout is not null && activeOptions.Timeout <= TimeSpan.Zero)\n        {\n            errors.Add(new ArgumentException($\"Destination probing timeout set on the cluster '{cluster.ClusterId}' must be positive.\"));\n        }\n    }\n\n    private void ValidatePassiveHealthCheck(ClusterConfig cluster, IList<Exception> errors)\n    {\n        if (!(cluster.HealthCheck?.Passive?.Enabled ?? false))\n        {\n            // Passive health check is disabled\n            return;\n        }\n\n        var passiveOptions = cluster.HealthCheck.Passive;\n        var policy = passiveOptions.Policy;\n        if (string.IsNullOrEmpty(policy))\n        {\n            // default policy\n            policy = HealthCheckConstants.PassivePolicy.TransportFailureRate;\n        }\n        if (!_passiveHealthCheckPolicies.ContainsKey(policy))\n        {\n            errors.Add(new ArgumentException($\"No matching {nameof(IPassiveHealthCheckPolicy)} found for the passive health check policy name '{policy}' set on the cluster '{cluster.ClusterId}'.\"));\n        }\n\n        if (passiveOptions.ReactivationPeriod is not null && passiveOptions.ReactivationPeriod <= TimeSpan.Zero)\n        {\n            errors.Add(new ArgumentException($\"Unhealthy destination reactivation period set on the cluster '{cluster.ClusterId}' must be positive.\"));\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/ClusterValidators/IClusterValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Configuration.ClusterValidators;\n\n/// <summary>\n/// Provides method to validate cluster configuration.\n/// </summary>\npublic interface IClusterValidator\n{\n\n    /// <summary>\n    /// Perform validation on a cluster configuration by adding exceptions to the provided collection.\n    /// </summary>\n    /// <param name=\"cluster\">Cluster configuration to validate</param>\n    /// <param name=\"errors\">Collection of all validation exceptions</param>\n    /// <returns>A ValueTask representing the asynchronous validation operation.</returns>\n    public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/ClusterValidators/LoadBalancingValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Yarp.ReverseProxy.LoadBalancing;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Configuration.ClusterValidators;\n\ninternal sealed class LoadBalancingValidator : IClusterValidator\n{\n    private readonly FrozenDictionary<string, ILoadBalancingPolicy> _loadBalancingPolicies;\n    public LoadBalancingValidator(IEnumerable<ILoadBalancingPolicy> loadBalancingPolicies)\n    {\n        ArgumentNullException.ThrowIfNull(loadBalancingPolicies);\n        _loadBalancingPolicies = loadBalancingPolicies.ToDictionaryByUniqueId(p => p.Name);\n    }\n\n    public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors)\n    {\n        var loadBalancingPolicy = cluster.LoadBalancingPolicy;\n        if (string.IsNullOrEmpty(loadBalancingPolicy))\n        {\n            // The default.\n            loadBalancingPolicy = LoadBalancingPolicies.PowerOfTwoChoices;\n        }\n\n        if (!_loadBalancingPolicies.ContainsKey(loadBalancingPolicy))\n        {\n            errors.Add(new ArgumentException($\"No matching {nameof(ILoadBalancingPolicy)} found for the load balancing policy '{loadBalancingPolicy}' set on the cluster '{cluster.ClusterId}'.\"));\n        }\n\n        return ValueTask.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/ClusterValidators/ProxyHttpClientValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Configuration.ClusterValidators;\n\ninternal sealed class ProxyHttpClientValidator : IClusterValidator\n{\n    public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors)\n    {\n        if (cluster.HttpClient is null)\n        {\n            // Proxy http client options are not set.\n            return ValueTask.CompletedTask;\n        }\n\n        if (cluster.HttpClient.MaxConnectionsPerServer is not null && cluster.HttpClient.MaxConnectionsPerServer <= 0)\n        {\n            errors.Add(new ArgumentException($\"Max connections per server limit set on the cluster '{cluster.ClusterId}' must be positive.\"));\n        }\n\n        var requestHeaderEncoding = cluster.HttpClient.RequestHeaderEncoding;\n        if (requestHeaderEncoding is not null)\n        {\n            try\n            {\n                Encoding.GetEncoding(requestHeaderEncoding);\n            }\n            catch (ArgumentException aex)\n            {\n                errors.Add(new ArgumentException($\"Invalid request header encoding '{requestHeaderEncoding}'.\", aex));\n            }\n        }\n\n        var responseHeaderEncoding = cluster.HttpClient.ResponseHeaderEncoding;\n        if (responseHeaderEncoding is null)\n        {\n            return ValueTask.CompletedTask;\n        }\n\n        try\n        {\n            Encoding.GetEncoding(responseHeaderEncoding);\n        }\n        catch (ArgumentException aex)\n        {\n            errors.Add(new ArgumentException($\"Invalid response header encoding '{responseHeaderEncoding}'.\", aex));\n        }\n\n        return ValueTask.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/ClusterValidators/ProxyHttpRequestValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.ReverseProxy.Configuration.ClusterValidators;\n\ninternal sealed class ProxyHttpRequestValidator(ILogger<ConfigValidator> logger) : IClusterValidator\n{\n    public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors)\n    {\n        if (cluster.HttpRequest is null)\n        {\n            // Proxy http request options are not set.\n            return ValueTask.CompletedTask;\n        }\n\n        if (cluster.HttpRequest.Version is not null &&\n            cluster.HttpRequest.Version != HttpVersion.Version10 &&\n            cluster.HttpRequest.Version != HttpVersion.Version11 &&\n            cluster.HttpRequest.Version != HttpVersion.Version20 &&\n            cluster.HttpRequest.Version != HttpVersion.Version30)\n        {\n            errors.Add(new ArgumentException($\"Outgoing request version '{cluster.HttpRequest.Version}' is not any of supported HTTP versions (1.0, 1.1, 2 and 3).\"));\n        }\n\n        if (cluster.HttpRequest.Version == HttpVersion.Version10)\n        {\n            Log.Http10Version(logger);\n        }\n\n        return ValueTask.CompletedTask;\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, Exception?> _http10RequestVersionDetected = LoggerMessage.Define(\n            LogLevel.Warning,\n            EventIds.Http10RequestVersionDetected,\n            \"The HttpRequest version is set to 1.0 which can result in poor performance and port exhaustion. Use 1.1, 2, or 3 instead.\");\n\n        public static void Http10Version(ILogger logger)\n        {\n            _http10RequestVersionDetected(logger, null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/ClusterValidators/SessionAffinityValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Yarp.ReverseProxy.SessionAffinity;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Configuration.ClusterValidators;\n\ninternal sealed class SessionAffinityValidator : IClusterValidator\n{\n    private readonly FrozenDictionary<string, IAffinityFailurePolicy> _affinityFailurePolicies;\n\n    public SessionAffinityValidator(IEnumerable<IAffinityFailurePolicy> affinityFailurePolicies)\n    {\n        ArgumentNullException.ThrowIfNull(affinityFailurePolicies);\n        _affinityFailurePolicies = affinityFailurePolicies.ToDictionaryByUniqueId(p => p.Name);\n    }\n\n    public ValueTask ValidateAsync(ClusterConfig cluster, IList<Exception> errors)\n    {\n        if (!(cluster.SessionAffinity?.Enabled ?? false))\n        {\n            // Session affinity is disabled\n            return ValueTask.CompletedTask;\n        }\n\n        // Note some affinity validation takes place in AffinitizeTransformProvider.ValidateCluster.\n        var affinityFailurePolicy = cluster.SessionAffinity.FailurePolicy;\n        if (string.IsNullOrEmpty(affinityFailurePolicy))\n        {\n            // The default.\n            affinityFailurePolicy = SessionAffinityConstants.FailurePolicies.Redistribute;\n        }\n\n        if (!_affinityFailurePolicies.ContainsKey(affinityFailurePolicy))\n        {\n            errors.Add(new ArgumentException($\"No matching {nameof(IAffinityFailurePolicy)} found for the affinity failure policy name '{affinityFailurePolicy}' set on the cluster '{cluster.ClusterId}'.\"));\n        }\n\n        if (string.IsNullOrEmpty(cluster.SessionAffinity.AffinityKeyName))\n        {\n            errors.Add(new ArgumentException($\"Affinity key name set on the cluster '{cluster.ClusterId}' must not be null.\"));\n        }\n\n        var cookieConfig = cluster.SessionAffinity.Cookie;\n\n        if (cookieConfig is null)\n        {\n            return ValueTask.CompletedTask;\n        }\n\n        if (cookieConfig.Expiration is not null && cookieConfig.Expiration <= TimeSpan.Zero)\n        {\n            errors.Add(new ArgumentException($\"Session affinity cookie expiration must be positive or null.\"));\n        }\n\n        if (cookieConfig.MaxAge is not null && cookieConfig.MaxAge <= TimeSpan.Zero)\n        {\n            errors.Add(new ArgumentException($\"Session affinity cookie max-age must be positive or null.\"));\n        }\n\n        return ValueTask.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Security.Authentication;\nusing System.Threading;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Primitives;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Configuration.ConfigProvider;\n\n/// <summary>\n/// Reacts to configuration changes and applies configurations to the Reverse Proxy core.\n/// When configs are loaded from appsettings.json, this takes care of hot updates\n/// when appsettings.json is modified on disk.\n/// </summary>\ninternal sealed class ConfigurationConfigProvider : IProxyConfigProvider, IDisposable\n{\n    private readonly object _lockObject = new();\n    private readonly ILogger<ConfigurationConfigProvider> _logger;\n    private readonly IConfiguration _configuration;\n    private ConfigurationSnapshot? _snapshot;\n    private CancellationTokenSource? _changeToken;\n    private bool _disposed;\n    private IDisposable? _subscription;\n\n    public ConfigurationConfigProvider(\n        ILogger<ConfigurationConfigProvider> logger,\n        IConfiguration configuration)\n    {\n        ArgumentNullException.ThrowIfNull(logger);\n        ArgumentNullException.ThrowIfNull(configuration);\n        _logger = logger;\n        _configuration = configuration;\n    }\n\n    public void Dispose()\n    {\n        if (!_disposed)\n        {\n            _subscription?.Dispose();\n            _changeToken?.Dispose();\n            _disposed = true;\n        }\n    }\n\n    public IProxyConfig GetConfig()\n    {\n        // First time load\n        if (_snapshot is null)\n        {\n            _subscription = ChangeToken.OnChange(_configuration.GetReloadToken, UpdateSnapshot);\n            UpdateSnapshot();\n        }\n\n        return _snapshot;\n    }\n\n    [MemberNotNull(nameof(_snapshot))]\n    private void UpdateSnapshot()\n    {\n        // Prevent overlapping updates, especially on startup.\n        lock (_lockObject)\n        {\n            Log.LoadData(_logger);\n            ConfigurationSnapshot newSnapshot;\n            try\n            {\n                newSnapshot = new ConfigurationSnapshot();\n\n                foreach (var section in _configuration.GetSection(\"Clusters\").GetChildren())\n                {\n                    newSnapshot.Clusters.Add(CreateCluster(section));\n                }\n\n                foreach (var section in _configuration.GetSection(\"Routes\").GetChildren())\n                {\n                    newSnapshot.Routes.Add(CreateRoute(section));\n                }\n            }\n            catch (Exception ex)\n            {\n                Log.ConfigurationDataConversionFailed(_logger, ex);\n\n                // Re-throw on the first time load to prevent app from starting.\n                if (_snapshot is null)\n                {\n                    throw;\n                }\n\n                return;\n            }\n\n            var oldToken = _changeToken;\n            _changeToken = new CancellationTokenSource();\n            newSnapshot.ChangeToken = new CancellationChangeToken(_changeToken.Token);\n            _snapshot = newSnapshot;\n\n            try\n            {\n                oldToken?.Cancel(throwOnFirstException: false);\n            }\n            catch (Exception ex)\n            {\n                Log.ErrorSignalingChange(_logger, ex);\n            }\n        }\n    }\n\n    private static ClusterConfig CreateCluster(IConfigurationSection section)\n    {\n        var destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase);\n        foreach (var destination in section.GetSection(nameof(ClusterConfig.Destinations)).GetChildren())\n        {\n            destinations.Add(destination.Key, CreateDestination(destination));\n        }\n\n        return new ClusterConfig\n        {\n            ClusterId = section.Key,\n            LoadBalancingPolicy = section[nameof(ClusterConfig.LoadBalancingPolicy)],\n            SessionAffinity = CreateSessionAffinityConfig(section.GetSection(nameof(ClusterConfig.SessionAffinity))),\n            HealthCheck = CreateHealthCheckConfig(section.GetSection(nameof(ClusterConfig.HealthCheck))),\n            HttpClient = CreateHttpClientConfig(section.GetSection(nameof(ClusterConfig.HttpClient))),\n            HttpRequest = CreateProxyRequestConfig(section.GetSection(nameof(ClusterConfig.HttpRequest))),\n            Metadata = section.GetSection(nameof(ClusterConfig.Metadata)).ReadStringDictionary(),\n            Destinations = destinations,\n        };\n    }\n\n    private static RouteConfig CreateRoute(IConfigurationSection section)\n    {\n        if (!string.IsNullOrEmpty(section[\"RouteId\"]))\n        {\n            throw new Exception(\"The route config format has changed, routes are now objects instead of an array. The route id must be set as the object name, not with the 'RouteId' field.\");\n        }\n\n        return new RouteConfig\n        {\n            RouteId = section.Key,\n            Order = section.ReadInt32(nameof(RouteConfig.Order)),\n            MaxRequestBodySize = section.ReadInt64(nameof(RouteConfig.MaxRequestBodySize)),\n            ClusterId = section[nameof(RouteConfig.ClusterId)],\n            AuthorizationPolicy = section[nameof(RouteConfig.AuthorizationPolicy)],\n            RateLimiterPolicy = section[nameof(RouteConfig.RateLimiterPolicy)],\n            OutputCachePolicy = section[nameof(RouteConfig.OutputCachePolicy)],\n            TimeoutPolicy = section[nameof(RouteConfig.TimeoutPolicy)],\n            Timeout = section.ReadTimeSpan(nameof(RouteConfig.Timeout)),\n            CorsPolicy = section[nameof(RouteConfig.CorsPolicy)],\n            Metadata = section.GetSection(nameof(RouteConfig.Metadata)).ReadStringDictionary(),\n            Transforms = CreateTransforms(section.GetSection(nameof(RouteConfig.Transforms))),\n            Match = CreateRouteMatch(section.GetSection(nameof(RouteConfig.Match))),\n        };\n    }\n\n    private static Dictionary<string, string>[]? CreateTransforms(IConfigurationSection section)\n    {\n        if (section.GetChildren() is var children && !children.Any())\n        {\n            return null;\n        }\n\n        return children\n            .Select(subSection => subSection.GetChildren().ToDictionary(d => d.Key, d => d.Value!, StringComparer.OrdinalIgnoreCase))\n            .ToArray();\n    }\n\n    private static RouteMatch CreateRouteMatch(IConfigurationSection section)\n    {\n        if (!section.Exists())\n        {\n            return new RouteMatch();\n        }\n\n        return new RouteMatch()\n        {\n            Methods = section.GetSection(nameof(RouteMatch.Methods)).ReadStringArray(),\n            Hosts = section.GetSection(nameof(RouteMatch.Hosts)).ReadStringArray(),\n            Path = section[nameof(RouteMatch.Path)],\n            Headers = CreateRouteHeaders(section.GetSection(nameof(RouteMatch.Headers))),\n            QueryParameters = CreateRouteQueryParameters(section.GetSection(nameof(RouteMatch.QueryParameters)))\n        };\n    }\n\n    private static RouteHeader[]? CreateRouteHeaders(IConfigurationSection section)\n    {\n        if (!section.Exists())\n        {\n            return null;\n        }\n\n        return section.GetChildren().Select(CreateRouteHeader).ToArray();\n    }\n\n    private static RouteHeader CreateRouteHeader(IConfigurationSection section)\n    {\n        return new RouteHeader()\n        {\n            Name = section[nameof(RouteHeader.Name)]!,\n            Values = section.GetSection(nameof(RouteHeader.Values)).ReadStringArray(),\n            Mode = section.ReadEnum<HeaderMatchMode>(nameof(RouteHeader.Mode)) ?? HeaderMatchMode.ExactHeader,\n            IsCaseSensitive = section.ReadBool(nameof(RouteHeader.IsCaseSensitive)) ?? false,\n        };\n    }\n\n    private static RouteQueryParameter[]? CreateRouteQueryParameters(IConfigurationSection section)\n    {\n        if (!section.Exists())\n        {\n            return null;\n        }\n\n        return section.GetChildren().Select(CreateRouteQueryParameter).ToArray();\n    }\n\n    private static RouteQueryParameter CreateRouteQueryParameter(IConfigurationSection section)\n    {\n        return new RouteQueryParameter()\n        {\n            Name = section[nameof(RouteQueryParameter.Name)]!,\n            Values = section.GetSection(nameof(RouteQueryParameter.Values)).ReadStringArray(),\n            Mode = section.ReadEnum<QueryParameterMatchMode>(nameof(RouteQueryParameter.Mode)) ?? QueryParameterMatchMode.Exact,\n            IsCaseSensitive = section.ReadBool(nameof(RouteQueryParameter.IsCaseSensitive)) ?? false,\n        };\n    }\n\n    private static SessionAffinityConfig? CreateSessionAffinityConfig(IConfigurationSection section)\n    {\n        if (!section.Exists())\n        {\n            return null;\n        }\n\n        return new SessionAffinityConfig\n        {\n            Enabled = section.ReadBool(nameof(SessionAffinityConfig.Enabled)),\n            Policy = section[nameof(SessionAffinityConfig.Policy)],\n            FailurePolicy = section[nameof(SessionAffinityConfig.FailurePolicy)],\n            AffinityKeyName = section[nameof(SessionAffinityConfig.AffinityKeyName)]!,\n            Cookie = CreateSessionAffinityCookieConfig(section.GetSection(nameof(SessionAffinityConfig.Cookie)))\n        };\n    }\n\n    private static SessionAffinityCookieConfig? CreateSessionAffinityCookieConfig(IConfigurationSection section)\n    {\n        if (!section.Exists())\n        {\n            return null;\n        }\n\n        return new SessionAffinityCookieConfig\n        {\n            Path = section[nameof(SessionAffinityCookieConfig.Path)],\n            SameSite = section.ReadEnum<SameSiteMode>(nameof(SessionAffinityCookieConfig.SameSite)),\n            HttpOnly = section.ReadBool(nameof(SessionAffinityCookieConfig.HttpOnly)),\n            MaxAge = section.ReadTimeSpan(nameof(SessionAffinityCookieConfig.MaxAge)),\n            Domain = section[nameof(SessionAffinityCookieConfig.Domain)],\n            IsEssential = section.ReadBool(nameof(SessionAffinityCookieConfig.IsEssential)),\n            SecurePolicy = section.ReadEnum<CookieSecurePolicy>(nameof(SessionAffinityCookieConfig.SecurePolicy)),\n            Expiration = section.ReadTimeSpan(nameof(SessionAffinityCookieConfig.Expiration))\n        };\n    }\n\n    private static HealthCheckConfig? CreateHealthCheckConfig(IConfigurationSection section)\n    {\n        if (!section.Exists())\n        {\n            return null;\n        }\n\n        return new HealthCheckConfig\n        {\n            Passive = CreatePassiveHealthCheckConfig(section.GetSection(nameof(HealthCheckConfig.Passive))),\n            Active = CreateActiveHealthCheckConfig(section.GetSection(nameof(HealthCheckConfig.Active))),\n            AvailableDestinationsPolicy = section[nameof(HealthCheckConfig.AvailableDestinationsPolicy)]\n        };\n    }\n\n    private static PassiveHealthCheckConfig? CreatePassiveHealthCheckConfig(IConfigurationSection section)\n    {\n        if (!section.Exists())\n        {\n            return null;\n        }\n\n        return new PassiveHealthCheckConfig\n        {\n            Enabled = section.ReadBool(nameof(PassiveHealthCheckConfig.Enabled)),\n            Policy = section[nameof(PassiveHealthCheckConfig.Policy)],\n            ReactivationPeriod = section.ReadTimeSpan(nameof(PassiveHealthCheckConfig.ReactivationPeriod))\n        };\n    }\n\n    private static ActiveHealthCheckConfig? CreateActiveHealthCheckConfig(IConfigurationSection section)\n    {\n        if (!section.Exists())\n        {\n            return null;\n        }\n\n        return new ActiveHealthCheckConfig\n        {\n            Enabled = section.ReadBool(nameof(ActiveHealthCheckConfig.Enabled)),\n            Interval = section.ReadTimeSpan(nameof(ActiveHealthCheckConfig.Interval)),\n            Timeout = section.ReadTimeSpan(nameof(ActiveHealthCheckConfig.Timeout)),\n            Policy = section[nameof(ActiveHealthCheckConfig.Policy)],\n            Path = section[nameof(ActiveHealthCheckConfig.Path)],\n            Query = section[nameof(ActiveHealthCheckConfig.Query)]\n        };\n    }\n\n    private static HttpClientConfig? CreateHttpClientConfig(IConfigurationSection section)\n    {\n        if (!section.Exists())\n        {\n            return null;\n        }\n\n        SslProtocols? sslProtocols = null;\n        if (section.GetSection(nameof(HttpClientConfig.SslProtocols)) is IConfigurationSection sslProtocolsSection)\n        {\n            foreach (var protocolConfig in sslProtocolsSection.GetChildren().Select(s => Enum.Parse<SslProtocols>(s.Value!, ignoreCase: true)))\n            {\n                sslProtocols = sslProtocols is null ? protocolConfig : sslProtocols | protocolConfig;\n            }\n        }\n\n        WebProxyConfig? webProxy;\n        var webProxySection = section.GetSection(nameof(HttpClientConfig.WebProxy));\n        if (webProxySection.Exists())\n        {\n            webProxy = new WebProxyConfig()\n            {\n                Address = webProxySection.ReadUri(nameof(WebProxyConfig.Address)),\n                BypassOnLocal = webProxySection.ReadBool(nameof(WebProxyConfig.BypassOnLocal)),\n                UseDefaultCredentials = webProxySection.ReadBool(nameof(WebProxyConfig.UseDefaultCredentials))\n            };\n        }\n        else\n        {\n            webProxy = null;\n        }\n\n        return new HttpClientConfig\n        {\n            SslProtocols = sslProtocols,\n            DangerousAcceptAnyServerCertificate = section.ReadBool(nameof(HttpClientConfig.DangerousAcceptAnyServerCertificate)),\n            MaxConnectionsPerServer = section.ReadInt32(nameof(HttpClientConfig.MaxConnectionsPerServer)),\n            EnableMultipleHttp2Connections = section.ReadBool(nameof(HttpClientConfig.EnableMultipleHttp2Connections)),\n            RequestHeaderEncoding = section[nameof(HttpClientConfig.RequestHeaderEncoding)],\n            ResponseHeaderEncoding = section[nameof(HttpClientConfig.ResponseHeaderEncoding)],\n            WebProxy = webProxy\n        };\n    }\n\n    private static ForwarderRequestConfig? CreateProxyRequestConfig(IConfigurationSection section)\n    {\n        if (!section.Exists())\n        {\n            return null;\n        }\n\n        return new ForwarderRequestConfig\n        {\n            ActivityTimeout = section.ReadTimeSpan(nameof(ForwarderRequestConfig.ActivityTimeout)),\n            Version = section.ReadVersion(nameof(ForwarderRequestConfig.Version)),\n            VersionPolicy = section.ReadEnum<HttpVersionPolicy>(nameof(ForwarderRequestConfig.VersionPolicy)),\n            AllowResponseBuffering = section.ReadBool(nameof(ForwarderRequestConfig.AllowResponseBuffering))\n        };\n    }\n\n    private static DestinationConfig CreateDestination(IConfigurationSection section)\n    {\n        return new DestinationConfig\n        {\n            Address = section[nameof(DestinationConfig.Address)]!,\n            Health = section[nameof(DestinationConfig.Health)],\n            Metadata = section.GetSection(nameof(DestinationConfig.Metadata)).ReadStringDictionary(),\n            Host = section[nameof(DestinationConfig.Host)]\n        };\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, Exception> _errorSignalingChange = LoggerMessage.Define(\n            LogLevel.Error,\n            EventIds.ErrorSignalingChange,\n            \"An exception was thrown from the change notification.\");\n\n        private static readonly Action<ILogger, Exception?> _loadData = LoggerMessage.Define(\n            LogLevel.Information,\n            EventIds.LoadData,\n            \"Loading proxy data from config.\");\n\n        private static readonly Action<ILogger, Exception> _configurationDataConversionFailed = LoggerMessage.Define(\n            LogLevel.Error,\n            EventIds.ConfigurationDataConversionFailed,\n            \"Configuration data conversion failed.\");\n\n        public static void ErrorSignalingChange(ILogger logger, Exception exception)\n        {\n            _errorSignalingChange(logger, exception);\n        }\n\n        public static void LoadData(ILogger logger)\n        {\n            _loadData(logger, null);\n        }\n\n        public static void ConfigurationDataConversionFailed(ILogger logger, Exception exception)\n        {\n            _configurationDataConversionFailed(logger, exception);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/ConfigProvider/ConfigurationReadingExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Globalization;\nusing System.Linq;\n\nnamespace Microsoft.Extensions.Configuration;\n\ninternal static class ConfigurationReadingExtensions\n{\n    internal static int? ReadInt32(this IConfiguration configuration, string name)\n    {\n        return configuration[name] is string value ? int.Parse(value, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture) : null;\n    }\n\n    internal static long? ReadInt64(this IConfiguration configuration, string name)\n    {\n        return configuration[name] is string value ? long.Parse(value, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture) : null;\n    }\n\n    internal static double? ReadDouble(this IConfiguration configuration, string name)\n    {\n        return configuration[name] is string value ? double.Parse(value, CultureInfo.InvariantCulture) : null;\n    }\n\n    internal static TimeSpan? ReadTimeSpan(this IConfiguration configuration, string name)\n    {\n        // Format \"c\" => [-][d'.']hh':'mm':'ss['.'fffffff]. \n        // You also can find more info at https://docs.microsoft.com/dotnet/standard/base-types/standard-timespan-format-strings#the-constant-c-format-specifier\n        return configuration[name] is string value ? TimeSpan.ParseExact(value, \"c\", CultureInfo.InvariantCulture) : null;\n    }\n\n    internal static Uri? ReadUri(this IConfiguration configuration, string name)\n    {\n        return configuration[name] is string value ? new Uri(value) : null;\n    }\n\n    internal static TEnum? ReadEnum<TEnum>(this IConfiguration configuration, string name) where TEnum : struct\n    {\n        return configuration[name] is string value ? Enum.Parse<TEnum>(value, ignoreCase: true) : null;\n    }\n\n    internal static bool? ReadBool(this IConfiguration configuration, string name)\n    {\n        return configuration[name] is string value ? bool.Parse(value) : null;\n    }\n\n    internal static Version? ReadVersion(this IConfiguration configuration, string name)\n    {\n        return configuration[name] is string value && !string.IsNullOrEmpty(value) ? Version.Parse(value + (value.Contains('.') ? \"\" : \".0\")) : null;\n    }\n\n    internal static IReadOnlyDictionary<string, string>? ReadStringDictionary(this IConfigurationSection section)\n    {\n        if (section.GetChildren() is var children && !children.Any())\n        {\n            return null;\n        }\n\n        return new ReadOnlyDictionary<string, string>(children.ToDictionary(s => s.Key, s => s.Value!, StringComparer.OrdinalIgnoreCase));\n    }\n\n    internal static string[]? ReadStringArray(this IConfigurationSection section)\n    {\n        if (section.GetChildren() is var children && !children.Any())\n        {\n            return null;\n        }\n\n        return children.Select(s => s.Value!).ToArray();\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/ConfigProvider/ConfigurationSnapshot.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Yarp.ReverseProxy.Configuration.ConfigProvider;\n\ninternal sealed class ConfigurationSnapshot : IProxyConfig\n{\n    public List<RouteConfig> Routes { get; internal set; } = new List<RouteConfig>();\n\n    public List<ClusterConfig> Clusters { get; internal set; } = new List<ClusterConfig>();\n\n    IReadOnlyList<RouteConfig> IProxyConfig.Routes => Routes;\n\n    IReadOnlyList<ClusterConfig> IProxyConfig.Clusters => Clusters;\n\n    // This field is required.\n    public IChangeToken ChangeToken { get; internal set; } = default!;\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/ConfigValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Yarp.ReverseProxy.Configuration.ClusterValidators;\nusing Yarp.ReverseProxy.Configuration.RouteValidators;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\ninternal sealed class ConfigValidator : IConfigValidator\n{\n    private readonly ITransformBuilder _transformBuilder;\n    private readonly IRouteValidator[] _routeValidators;\n    private readonly IClusterValidator[] _clusterValidators;\n\n    public ConfigValidator(ITransformBuilder transformBuilder,\n        IEnumerable<IRouteValidator> routeValidators,\n        IEnumerable<IClusterValidator> clusterValidators)\n    {\n        ArgumentNullException.ThrowIfNull(transformBuilder);\n        ArgumentNullException.ThrowIfNull(routeValidators);\n        ArgumentNullException.ThrowIfNull(clusterValidators);\n\n        _transformBuilder = transformBuilder;\n        _routeValidators = routeValidators.ToArray();\n        _clusterValidators = clusterValidators.ToArray();\n    }\n\n    // Note this performs all validation steps without short-circuiting in order to report all possible errors.\n    public async ValueTask<IList<Exception>> ValidateRouteAsync(RouteConfig route)\n    {\n        ArgumentNullException.ThrowIfNull(route);\n        var errors = new List<Exception>();\n\n        if (string.IsNullOrEmpty(route.RouteId))\n        {\n            errors.Add(new ArgumentException(\"Missing Route Id.\"));\n        }\n\n        errors.AddRange(_transformBuilder.ValidateRoute(route));\n\n        if (route.Match is null)\n        {\n            errors.Add(new ArgumentException($\"Route '{route.RouteId}' did not set any match criteria, it requires Hosts or Path specified. Set the Path to '/{{**catchall}}' to match all requests.\"));\n            return errors;\n        }\n\n        if ((route.Match.Hosts is null || route.Match.Hosts.All(string.IsNullOrEmpty)) && string.IsNullOrEmpty(route.Match.Path))\n        {\n            errors.Add(new ArgumentException($\"Route '{route.RouteId}' requires Hosts or Path specified. Set the Path to '/{{**catchall}}' to match all requests.\"));\n        }\n\n        foreach (var routeValidator in _routeValidators)\n        {\n            await routeValidator.ValidateAsync(route, errors);\n        }\n\n        return errors;\n    }\n\n    // Note this performs all validation steps without short-circuiting in order to report all possible errors.\n    public async ValueTask<IList<Exception>> ValidateClusterAsync(ClusterConfig cluster)\n    {\n        ArgumentNullException.ThrowIfNull(cluster);\n\n        var errors = new List<Exception>();\n\n        if (string.IsNullOrEmpty(cluster.ClusterId))\n        {\n            errors.Add(new ArgumentException(\"Missing Cluster Id.\"));\n        }\n\n        errors.AddRange(_transformBuilder.ValidateCluster(cluster));\n\n        foreach (var clusterValidator in _clusterValidators)\n        {\n            await clusterValidator.ValidateAsync(cluster, errors);\n        }\n\n        return errors;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/CorsConstants.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Configuration;\n\ninternal static class CorsConstants\n{\n    internal const string Default = \"Default\";\n    internal const string Disable = \"Disable\";\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/DestinationConfig.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Describes a destination of a cluster.\n/// </summary>\npublic sealed record DestinationConfig\n{\n    /// <summary>\n    /// Address of this destination. E.g. <c>https://127.0.0.1:123/abcd1234/</c>.\n    /// This field is required.\n    /// </summary>\n    public string Address { get; init; } = default!;\n\n    /// <summary>\n    /// Endpoint accepting active health check probes. E.g. <c>http://127.0.0.1:1234/</c>.\n    /// </summary>\n    public string? Health { get; init; }\n\n    /// <summary>\n    /// Arbitrary key-value pairs that further describe this destination.\n    /// </summary>\n    public IReadOnlyDictionary<string, string>? Metadata { get; init; }\n\n    /// <summary>\n    /// Host header value to pass to this destination.\n    /// Used as a fallback if a host is not already specified by request transforms.\n    /// </summary>\n    public string? Host { get; init; }\n\n    public bool Equals(DestinationConfig? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return string.Equals(Address, other.Address, StringComparison.OrdinalIgnoreCase)\n            && string.Equals(Health, other.Health, StringComparison.OrdinalIgnoreCase)\n            && string.Equals(Host, other.Host, StringComparison.OrdinalIgnoreCase)\n            && CaseSensitiveEqualHelper.Equals(Metadata, other.Metadata);\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(\n            Address?.GetHashCode(StringComparison.OrdinalIgnoreCase),\n            Health?.GetHashCode(StringComparison.OrdinalIgnoreCase),\n            Host?.GetHashCode(StringComparison.OrdinalIgnoreCase),\n            CaseSensitiveEqualHelper.GetHashCode(Metadata));\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/HeaderMatchMode.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// How to match header values.\n/// </summary>\npublic enum HeaderMatchMode\n{\n    /// <summary>\n    /// Any of the headers with the given name must match in its entirety, subject to case sensitivity settings.\n    /// If a header contains multiple values (separated by , or ;), they are split before matching.\n    /// A single pair of quotes will also be stripped from the value before matching.\n    /// </summary>\n    ExactHeader,\n\n    /// <summary>\n    /// Any of the headers with the given name must match by prefix, subject to case sensitivity settings.\n    /// If a header contains multiple values (separated by , or ;), they are split before matching.\n    /// A single pair of quotes will also be stripped from the value before matching.\n    /// </summary>\n    HeaderPrefix,\n\n    /// <summary>\n    /// Any of the headers with the given name must contain any of the match values, subject to case sensitivity settings.\n    /// </summary>\n    Contains,\n\n    /// <summary>\n    /// The header must exist and the value must be non-empty.\n    /// None of the headers with the given name may contain any of the match values, subject to case sensitivity settings.\n    /// </summary>\n    NotContains,\n\n    /// <summary>\n    /// The header must exist and contain any non-empty value.\n    /// If there are multiple headers with the same name, the rule will also match.\n    /// </summary>\n    Exists,\n\n    /// <summary>\n    /// The header must not exist.\n    /// </summary>\n    NotExists,\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/HealthCheckConfig.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// All health check config.\n/// </summary>\npublic sealed record HealthCheckConfig\n{\n    /// <summary>\n    /// Passive health check config.\n    /// </summary>\n    public PassiveHealthCheckConfig? Passive { get; init; }\n\n    /// <summary>\n    /// Active health check config.\n    /// </summary>\n    public ActiveHealthCheckConfig? Active { get; init; }\n\n    /// <summary>\n    /// Available destinations policy.\n    /// </summary>\n    public string? AvailableDestinationsPolicy { get; init; }\n\n    public bool Equals(HealthCheckConfig? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return Passive == other.Passive\n            && Active == other.Active\n            && string.Equals(AvailableDestinationsPolicy, other.AvailableDestinationsPolicy, StringComparison.OrdinalIgnoreCase);\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(\n            Passive,\n            Active,\n            AvailableDestinationsPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase));\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/HttpClientConfig.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing System.Security.Authentication;\nusing System.Text;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Options used for communicating with the destination servers.\n/// </summary>\n/// <remarks>\n/// If you need a more granular approach, please use a <see href=\"https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/http-client-config#custom-iforwarderhttpclientfactory\">custom implementation of <see cref=\"IForwarderHttpClientFactory\"/></see>.\n/// </remarks>\npublic sealed record HttpClientConfig\n{\n    /// <summary>\n    /// An empty options instance.\n    /// </summary>\n    public static readonly HttpClientConfig Empty = new();\n\n    /// <summary>\n    /// What TLS protocols to use.\n    /// </summary>\n    public SslProtocols? SslProtocols { get; init; }\n\n    /// <summary>\n    /// Indicates if destination server https certificate errors should be ignored.\n    /// This should only be done when using self-signed certificates.\n    /// </summary>\n    public bool? DangerousAcceptAnyServerCertificate { get; init; }\n\n    /// <summary>\n    /// Limits the number of connections used when communicating with the destination server.\n    /// </summary>\n    public int? MaxConnectionsPerServer { get; init; }\n\n    /// <summary>\n    /// Optional web proxy used when communicating with the destination server.\n    /// </summary>\n    public WebProxyConfig? WebProxy { get; init; }\n\n    /// <summary>\n    /// Gets or sets a value that indicates whether additional HTTP/2 connections can\n    /// be established to the same server when the maximum number of concurrent streams\n    /// is reached on all existing connections.\n    /// </summary>\n    public bool? EnableMultipleHttp2Connections { get; init; }\n\n    /// <summary>\n    /// Allows overriding the default (ASCII) encoding for outgoing request headers.\n    /// <para>\n    /// Setting this value will in turn set <see cref=\"SocketsHttpHandler.RequestHeaderEncodingSelector\"/> and use the selected encoding for all request headers.\n    /// The value is then parsed by <see cref=\"Encoding.GetEncoding(string)\"/>, so use values like: \"utf-8\", \"iso-8859-1\", etc.\n    /// </para>\n    /// </summary>\n    /// <remarks>\n    /// Note: If you're using an encoding other than UTF-8 here, then you may also need to configure your server to accept request headers with such an encoding via the corresponding options for the server.\n    /// <para>\n    /// For example, when using Kestrel as the server, use <see cref=\"KestrelServerOptions.RequestHeaderEncodingSelector\"/> to\n    /// <see href=\"https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/options\">configure Kestrel</see> to use the same encoding.\n    /// </para>\n    /// </remarks>\n    public string? RequestHeaderEncoding { get; init; }\n\n    /// <summary>\n    /// Allows overriding the default (Latin1) encoding for incoming request headers.\n    /// <para>\n    /// Setting this value will in turn set <see cref=\"SocketsHttpHandler.ResponseHeaderEncodingSelector\"/> and use the selected encoding for all response headers.\n    /// The value is then parsed by <see cref=\"Encoding.GetEncoding(string)\"/>, so use values like: \"utf-8\", \"iso-8859-1\", etc.\n    /// </para>\n    /// </summary>\n    /// <remarks>\n    /// Note: If you're using an encoding other than ASCII here, then you may also need to configure your server to send response headers with such an encoding via the corresponding options for the server.\n    /// <para>\n    /// For example, when using Kestrel as the server, use <see cref=\"KestrelServerOptions.ResponseHeaderEncodingSelector\"/> to\n    /// <see href=\"https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/options\">configure Kestrel</see> to use the same encoding.\n    /// </para>\n    /// </remarks>\n    public string? ResponseHeaderEncoding { get; init; }\n\n    public bool Equals(HttpClientConfig? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return SslProtocols == other.SslProtocols\n               && DangerousAcceptAnyServerCertificate == other.DangerousAcceptAnyServerCertificate\n               && MaxConnectionsPerServer == other.MaxConnectionsPerServer\n               && EnableMultipleHttp2Connections == other.EnableMultipleHttp2Connections\n               // Comparing by reference is fine here since Encoding.GetEncoding returns the same instance for each encoding.\n               && RequestHeaderEncoding == other.RequestHeaderEncoding\n               && ResponseHeaderEncoding == other.ResponseHeaderEncoding\n               && WebProxy == other.WebProxy;\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(SslProtocols,\n            DangerousAcceptAnyServerCertificate,\n            MaxConnectionsPerServer,\n            EnableMultipleHttp2Connections,\n            RequestHeaderEncoding,\n            ResponseHeaderEncoding,\n            WebProxy);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/IConfigChangeListener.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Allows subscribing to events notifying you when the configuration is loaded and applied, or when those actions fail.\n/// </summary>\npublic interface IConfigChangeListener\n{\n    /// <summary>\n    /// Invoked when an error occurs while loading the configuration.\n    /// </summary>\n    /// <param name=\"configProvider\">The instance of the configuration provider that failed to provide the configuration.</param>\n    /// <param name=\"exception\">The thrown exception.</param>\n    void ConfigurationLoadingFailed(IProxyConfigProvider configProvider, Exception exception);\n\n    /// <summary>\n    /// Invoked once the configuration have been successfully loaded.\n    /// </summary>\n    /// <param name=\"proxyConfigs\">The list of instances that have been loaded.</param>\n    void ConfigurationLoaded(IReadOnlyList<IProxyConfig> proxyConfigs);\n\n    /// <summary>\n    /// Invoked when an error occurs while applying the configuration.\n    /// </summary>\n    /// <param name=\"proxyConfigs\">The list of instances that were being processed.</param>\n    /// <param name=\"exception\">The thrown exception.</param>\n    void ConfigurationApplyingFailed(IReadOnlyList<IProxyConfig> proxyConfigs, Exception exception);\n\n    /// <summary>\n    /// Invoked once the configuration has been successfully applied.\n    /// </summary>\n    /// <param name=\"proxyConfigs\">The list of instances that have been applied.</param>\n    void ConfigurationApplied(IReadOnlyList<IProxyConfig> proxyConfigs);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/IConfigValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Provides methods to validate routes and clusters.\n/// </summary>\npublic interface IConfigValidator\n{\n    /// <summary>\n    /// Validates a route and returns all errors\n    /// </summary>\n    ValueTask<IList<Exception>> ValidateRouteAsync(RouteConfig route);\n\n    /// <summary>\n    /// Validates a cluster and returns all errors.\n    /// </summary>\n    ValueTask<IList<Exception>> ValidateClusterAsync(ClusterConfig cluster);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/IProxyConfig.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Runtime.CompilerServices;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Represents a snapshot of proxy configuration data. These properties may be accessed multiple times and should not be modified.\n/// </summary>\npublic interface IProxyConfig\n{\n    private static readonly ConditionalWeakTable<IProxyConfig, string> _revisionIdsTable = new();\n\n    /// <summary>\n    /// A unique identifier for this revision of the configuration.\n    /// </summary>\n    string RevisionId => _revisionIdsTable.GetValue(this, static _ => Guid.NewGuid().ToString());\n\n    /// <summary>\n    /// Routes matching requests to clusters.\n    /// </summary>\n    IReadOnlyList<RouteConfig> Routes { get; }\n\n    /// <summary>\n    /// Cluster information for where to proxy requests to.\n    /// </summary>\n    IReadOnlyList<ClusterConfig> Clusters { get; }\n\n    /// <summary>\n    /// A notification that triggers when this snapshot expires.\n    /// </summary>\n    IChangeToken ChangeToken { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/IProxyConfigFilter.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// A configuration filter that will run each time the proxy configuration is loaded.\n/// </summary>\npublic interface IProxyConfigFilter\n{\n    /// <summary>\n    /// Allows modification of a cluster configuration.\n    /// </summary>\n    /// <param name=\"cluster\">The <see cref=\"ClusterConfig\"/> instance to configure.</param>\n    /// <param name=\"cancel\"></param>\n    ValueTask<ClusterConfig> ConfigureClusterAsync(ClusterConfig cluster, CancellationToken cancel);\n\n    /// <summary>\n    /// Allows modification of a route configuration.\n    /// </summary>\n    /// <param name=\"route\">The <see cref=\"RouteConfig\"/> instance to configure.</param>\n    /// <param name=\"cluster\">The <see cref=\"ClusterConfig\"/> instance related to <see cref=\"RouteConfig\"/>.</param>\n    /// <param name=\"cancel\"></param>\n    ValueTask<RouteConfig> ConfigureRouteAsync(RouteConfig route, ClusterConfig? cluster, CancellationToken cancel);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/IProxyConfigProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// A data source for proxy route and cluster information.\n/// </summary>\npublic interface IProxyConfigProvider\n{\n    /// <summary>\n    /// Returns the current route and cluster data.\n    /// </summary>\n    /// <returns></returns>\n    IProxyConfig GetConfig();\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/IYarpOutputCachePolicyProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.OutputCaching;\nusing Microsoft.Extensions.Options;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n// TODO: update or remove this once AspNetCore provides a mechanism to validate the OutputCache policies https://github.com/dotnet/aspnetcore/issues/52419\n\ninternal interface IYarpOutputCachePolicyProvider\n{\n    ValueTask<object?> GetPolicyAsync(string policyName);\n}\n\ninternal sealed class YarpOutputCachePolicyProvider : IYarpOutputCachePolicyProvider\n{\n    // Workaround for https://github.com/dotnet/yarp/issues/2598 to make YARP work with NativeAOT on .NET 8. This is not needed on .NET 9+.\n    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]\n    private static readonly Type s_OutputCacheOptionsType = typeof(OutputCacheOptions);\n\n    private readonly OutputCacheOptions _outputCacheOptions;\n\n    private readonly IDictionary _policyMap;\n\n    public YarpOutputCachePolicyProvider(IOptions<OutputCacheOptions> outputCacheOptions)\n    {\n        ArgumentNullException.ThrowIfNull(outputCacheOptions?.Value);\n        _outputCacheOptions = outputCacheOptions.Value;\n\n        var property = s_OutputCacheOptionsType.GetProperty(\"NamedPolicies\", BindingFlags.Instance | BindingFlags.NonPublic);\n        if (property == null || !typeof(IDictionary).IsAssignableFrom(property.PropertyType))\n        {\n            throw new NotSupportedException(\"This version of YARP is incompatible with the current version of ASP.NET Core.\");\n        }\n        _policyMap = (property.GetValue(_outputCacheOptions, null) as IDictionary) ?? new Dictionary<string, object>();\n    }\n\n    public ValueTask<object?> GetPolicyAsync(string policyName)\n    {\n        return ValueTask.FromResult(_policyMap[policyName]);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/IYarpRateLimiterPolicyProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections;\nusing System.Reflection;\nusing Microsoft.AspNetCore.RateLimiting;\nusing Microsoft.Extensions.Options;\n\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n// TODO: update or remove this once AspNetCore provides a mechanism to validate the RateLimiter policies https://github.com/dotnet/aspnetcore/issues/45684\n\ninternal interface IYarpRateLimiterPolicyProvider\n{\n    ValueTask<object?> GetPolicyAsync(string policyName);\n}\n\ninternal sealed class YarpRateLimiterPolicyProvider : IYarpRateLimiterPolicyProvider\n{\n    private readonly RateLimiterOptions _rateLimiterOptions;\n\n    private readonly IDictionary _policyMap, _unactivatedPolicyMap;\n\n    public YarpRateLimiterPolicyProvider(IOptions<RateLimiterOptions> rateLimiterOptions)\n    {\n        ArgumentNullException.ThrowIfNull(rateLimiterOptions?.Value);\n        _rateLimiterOptions = rateLimiterOptions.Value;\n\n        var type = typeof(RateLimiterOptions);\n        var flags = BindingFlags.Instance | BindingFlags.NonPublic;\n        _policyMap = type.GetProperty(\"PolicyMap\", flags)?.GetValue(_rateLimiterOptions, null) as IDictionary\n            ?? throw new NotSupportedException(\"This version of YARP is incompatible with the current version of ASP.NET Core.\");\n        _unactivatedPolicyMap = type.GetProperty(\"UnactivatedPolicyMap\", flags)?.GetValue(_rateLimiterOptions, null) as IDictionary\n            ?? throw new NotSupportedException(\"This version of YARP is incompatible with the current version of ASP.NET Core.\");\n    }\n\n    public ValueTask<object?> GetPolicyAsync(string policyName)\n    {\n        return ValueTask.FromResult(_policyMap[policyName] ?? _unactivatedPolicyMap[policyName]);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/InMemoryConfigProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Provides an implementation of IProxyConfigProvider to support config being generated by code.\n/// </summary>\npublic sealed class InMemoryConfigProvider : IProxyConfigProvider\n{\n    // Marked as volatile so that updates are atomic\n    private volatile InMemoryConfig _config;\n\n    /// <summary>\n    /// Creates a new instance.\n    /// </summary>\n    public InMemoryConfigProvider(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters)\n        : this(routes, clusters, Guid.NewGuid().ToString())\n    { }\n\n    /// <summary>\n    /// Creates a new instance, specifying a revision id of the configuration.\n    /// </summary>\n    public InMemoryConfigProvider(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters, string revisionId)\n    {\n        _config = new InMemoryConfig(routes, clusters, revisionId);\n    }\n\n    /// <summary>\n    /// Implementation of the IProxyConfigProvider.GetConfig method to supply the current snapshot of configuration\n    /// </summary>\n    /// <returns>An immutable snapshot of the current configuration state</returns>\n    public IProxyConfig GetConfig() => _config;\n\n    /// <summary>\n    /// Swaps the config state with a new snapshot of the configuration, then signals that the old one is outdated.\n    /// </summary>\n    public void Update(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters)\n    {\n        var newConfig = new InMemoryConfig(routes, clusters);\n        UpdateInternal(newConfig);\n    }\n\n    /// <summary>\n    /// Swaps the config state with a new snapshot of the configuration, then signals that the old one is outdated.\n    /// </summary>\n    public void Update(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters, string revisionId)\n    {\n        var newConfig = new InMemoryConfig(routes, clusters, revisionId);\n        UpdateInternal(newConfig);\n    }\n\n    private void UpdateInternal(InMemoryConfig newConfig)\n    {\n        var oldConfig = Interlocked.Exchange(ref _config, newConfig);\n        oldConfig.SignalChange();\n    }\n\n    /// <summary>\n    /// Implementation of IProxyConfig which is a snapshot of the current config state. The data for this class should be immutable.\n    /// </summary>\n    private sealed class InMemoryConfig : IProxyConfig\n    {\n        // Used to implement the change token for the state\n        private readonly CancellationTokenSource _cts = new CancellationTokenSource();\n\n        public InMemoryConfig(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters)\n            : this(routes, clusters, Guid.NewGuid().ToString())\n        { }\n\n        public InMemoryConfig(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters, string revisionId)\n        {\n            ArgumentNullException.ThrowIfNull(revisionId);\n            RevisionId = revisionId;\n            Routes = routes;\n            Clusters = clusters;\n            ChangeToken = new CancellationChangeToken(_cts.Token);\n        }\n\n        /// <inheritdoc/>\n        public string RevisionId { get; }\n\n        /// <summary>\n        /// A snapshot of the list of routes for the proxy\n        /// </summary>\n        public IReadOnlyList<RouteConfig> Routes { get; }\n\n        /// <summary>\n        /// A snapshot of the list of Clusters which are collections of interchangeable destination endpoints\n        /// </summary>\n        public IReadOnlyList<ClusterConfig> Clusters { get; }\n\n        /// <summary>\n        /// Fired to indicate the proxy state has changed, and that this snapshot is now stale\n        /// </summary>\n        public IChangeToken ChangeToken { get; }\n\n        internal void SignalChange()\n        {\n            _cts.Cancel();\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/InMemoryConfigProviderExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\npublic static class InMemoryConfigProviderExtensions\n{\n    /// <summary>\n    /// Adds an InMemoryConfigProvider\n    /// </summary>\n    public static IReverseProxyBuilder LoadFromMemory(this IReverseProxyBuilder builder, IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters)\n    {\n        builder.Services.AddSingleton(new InMemoryConfigProvider(routes, clusters));\n        builder.Services.AddSingleton<IProxyConfigProvider>(s => s.GetRequiredService<InMemoryConfigProvider>());\n        return builder;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/PassiveHealthCheckConfig.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Passive health check config.\n/// </summary>\npublic sealed record PassiveHealthCheckConfig\n{\n    /// <summary>\n    /// Whether passive health checks are enabled.\n    /// </summary>\n    public bool? Enabled { get; init; }\n\n    /// <summary>\n    /// Passive health check policy.\n    /// </summary>\n    public string? Policy { get; init; }\n\n    /// <summary>\n    /// Destination reactivation period after which an unhealthy destination reverts back to <see cref=\"DestinationHealth.Unknown\"/>.\n    /// </summary>\n    public TimeSpan? ReactivationPeriod { get; init; }\n\n    public bool Equals(PassiveHealthCheckConfig? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return Enabled == other.Enabled\n            && string.Equals(Policy, other.Policy, StringComparison.OrdinalIgnoreCase)\n            && ReactivationPeriod == other.ReactivationPeriod;\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(Enabled,\n            Policy?.GetHashCode(StringComparison.OrdinalIgnoreCase),\n            ReactivationPeriod);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/QueryParameterMatchMode.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// How to match Query Parameter values.\n/// </summary>\npublic enum QueryParameterMatchMode\n{\n    /// <summary>\n    /// Query string must match in its entirety,\n    /// Subject to case sensitivity settings.\n    /// Only single query parameter name supported. If there are multiple query parameters with the same name then the match fails.\n    /// </summary>\n    Exact,\n\n    /// <summary>\n    /// Query string key must be present and substring must match for each of the respective query string values.\n    /// Subject to case sensitivity settings.\n    /// Only single query parameter name supported. If there are multiple query parameters with the same name then the match fails.\n    /// </summary>\n    Contains,\n\n    /// <summary>\n    /// Query string key must be present and value must not match for each of the respective query string values.\n    /// Subject to case sensitivity settings.\n    /// If there are multiple values then it needs to not contain ANY of the values \n    /// Only single query parameter name supported. If there are multiple query parameters with the same name then the match fails.\n    /// </summary>\n    NotContains,\n\n    /// <summary>\n    /// Query string key must be present and prefix must match for each of the respective query string values.\n    /// Subject to case sensitivity settings.\n    /// Only single query parameter name supported. If there are multiple query parameters with the same name then the match fails.\n    /// </summary>\n    Prefix,\n\n    /// <summary>\n    /// Query string key must exist and contain any non-empty value.\n    /// </summary>\n    Exists\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RateLimitingConstants.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Configuration;\n\ninternal static class RateLimitingConstants\n{\n    internal const string Default = \"Default\";\n    internal const string Disable = \"Disable\";\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteConfig.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Routing;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Describes a route that matches incoming requests based on the <see cref=\"Match\"/> criteria\n/// and proxies matching requests to the cluster identified by its <see cref=\"ClusterId\"/>.\n/// </summary>\npublic sealed record RouteConfig\n{\n    /// <summary>\n    /// Globally unique identifier of the route.\n    /// This field is required.\n    /// </summary>\n    public string RouteId { get; init; } = default!;\n\n    /// <summary>\n    /// Parameters used to match requests.\n    /// This field is required.\n    /// </summary>\n    public RouteMatch Match { get; init; } = default!;\n\n    /// <summary>\n    /// Optionally, an order value for this route. Routes with lower numbers take precedence over higher numbers.\n    /// </summary>\n    public int? Order { get; init; }\n\n    /// <summary>\n    /// Gets or sets the cluster that requests matching this route\n    /// should be proxied to.\n    /// </summary>\n    public string? ClusterId { get; init; }\n\n    /// <summary>\n    /// The name of the AuthorizationPolicy to apply to this route.\n    /// If not set then only the FallbackPolicy will apply.\n    /// Set to \"Default\" to enable authorization with the applications default policy.\n    /// Set to \"Anonymous\" to disable all authorization checks for this route.\n    /// </summary>\n    public string? AuthorizationPolicy { get; init; }\n\n    /// <summary>\n    /// The name of the RateLimiterPolicy to apply to this route.\n    /// If not set then only the GlobalLimiter will apply.\n    /// Set to \"Disable\" to disable rate limiting for this route.\n    /// Set to \"Default\" or leave empty to use the global rate limits, if any.\n    /// </summary>\n    public string? RateLimiterPolicy { get; init; }\n\n    /// <summary>\n    /// The name of the OutputCachePolicy to apply to this route.\n    /// If not set then only the BasePolicy will apply.\n    /// </summary>\n    public string? OutputCachePolicy { get; init; }\n\n    /// <summary>\n    /// The name of the TimeoutPolicy to apply to this route.\n    /// Setting both Timeout and TimeoutPolicy is an error.\n    /// If not set then only the system default will apply.\n    /// Set to \"Disable\" to disable timeouts for this route.\n    /// Set to \"Default\" or leave empty to use the system defaults, if any.\n    /// </summary>\n    public string? TimeoutPolicy { get; init; }\n\n    /// <summary>\n    /// The Timeout to apply to this route. This overrides any system defaults.\n    /// Setting both Timeout and TimeoutPolicy is an error.\n    /// Timeout granularity is limited to milliseconds.\n    /// </summary>\n    public TimeSpan? Timeout { get; init; }\n\n    /// <summary>\n    /// The name of the CorsPolicy to apply to this route.\n    /// If not set then the route won't be automatically matched for cors preflight requests.\n    /// Set to \"Default\" to enable cors with the default policy.\n    /// Set to \"Disable\" to refuses cors requests for this route.\n    /// </summary>\n    public string? CorsPolicy { get; init; }\n\n    /// <summary>\n    /// An optional override for how large request bodies can be in bytes. If set, this overrides the server's default (30MB) per request.\n    /// Set to '-1' to disable the limit for this route.\n    /// Note that this limit applies only to the YARP forwarder middleware, it does not apply when reading the request body from a custom middleware registered via\n    /// <see cref=\"ReverseProxyIEndpointRouteBuilderExtensions.MapReverseProxy(IEndpointRouteBuilder, Action{IReverseProxyApplicationBuilder})\"/>.\n    /// </summary>\n    public long? MaxRequestBodySize { get; init; }\n\n    /// <summary>\n    /// Arbitrary key-value pairs that further describe this route.\n    /// </summary>\n    public IReadOnlyDictionary<string, string>? Metadata { get; init; }\n\n    /// <summary>\n    /// Parameters used to transform the request and response. See <see cref=\"Transforms.Builder.ITransformBuilder\"/>.\n    /// </summary>\n    public IReadOnlyList<IReadOnlyDictionary<string, string>>? Transforms { get; init; }\n\n    public bool Equals(RouteConfig? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return Order == other.Order\n            && string.Equals(RouteId, other.RouteId, StringComparison.OrdinalIgnoreCase)\n            && string.Equals(ClusterId, other.ClusterId, StringComparison.OrdinalIgnoreCase)\n            && string.Equals(AuthorizationPolicy, other.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase)\n            && string.Equals(RateLimiterPolicy, other.RateLimiterPolicy, StringComparison.OrdinalIgnoreCase)\n            && string.Equals(OutputCachePolicy, other.OutputCachePolicy, StringComparison.OrdinalIgnoreCase)\n            && string.Equals(TimeoutPolicy, other.TimeoutPolicy, StringComparison.OrdinalIgnoreCase)\n            && Timeout == other.Timeout\n            && string.Equals(CorsPolicy, other.CorsPolicy, StringComparison.OrdinalIgnoreCase)\n            && Match == other.Match\n            && CaseSensitiveEqualHelper.Equals(Metadata, other.Metadata)\n            && CaseSensitiveEqualHelper.Equals(Transforms, other.Transforms);\n    }\n\n    public override int GetHashCode()\n    {\n        // HashCode.Combine(...) takes only 8 arguments\n        var hash = new HashCode();\n        hash.Add(Order);\n        hash.Add(RouteId?.GetHashCode(StringComparison.OrdinalIgnoreCase));\n        hash.Add(ClusterId?.GetHashCode(StringComparison.OrdinalIgnoreCase));\n        hash.Add(AuthorizationPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase));\n        hash.Add(RateLimiterPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase));\n        hash.Add(OutputCachePolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase));\n        hash.Add(Timeout?.GetHashCode());\n        hash.Add(TimeoutPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase));\n        hash.Add(CorsPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase));\n        hash.Add(Match);\n        hash.Add(CaseSensitiveEqualHelper.GetHashCode(Metadata));\n        hash.Add(CaseSensitiveEqualHelper.GetHashCode(Transforms));\n        return hash.ToHashCode();\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteHeader.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Route criteria for a header that must be present on the incoming request.\n/// </summary>\npublic sealed record RouteHeader\n{\n    /// <summary>\n    /// Name of the header to look for.\n    /// This field is case insensitive and required.\n    /// </summary>\n    public string Name { get; init; } = default!;\n\n    /// <summary>\n    /// A collection of acceptable header values used during routing. Only one value must match.\n    /// The list must not be empty unless using <see cref=\"HeaderMatchMode.Exists\"/> or <see cref=\"HeaderMatchMode.NotExists\"/>.\n    /// </summary>\n    public IReadOnlyList<string>? Values { get; init; }\n\n    /// <summary>\n    /// Specifies how header values should be compared (e.g. exact matches Vs. by prefix).\n    /// Defaults to <see cref=\"HeaderMatchMode.ExactHeader\"/>.\n    /// </summary>\n    public HeaderMatchMode Mode { get; init; }\n\n    /// <summary>\n    /// Specifies whether header value comparisons should ignore case.\n    /// When <c>true</c>, <see cref=\"StringComparison.Ordinal\" /> is used.\n    /// When <c>false</c>, <see cref=\"StringComparison.OrdinalIgnoreCase\" /> is used.\n    /// Defaults to <c>false</c>.\n    /// </summary>\n    public bool IsCaseSensitive { get; init; }\n\n    public bool Equals(RouteHeader? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase)\n            && Mode == other.Mode\n            && IsCaseSensitive == other.IsCaseSensitive\n            && (IsCaseSensitive\n                ? CaseSensitiveEqualHelper.Equals(Values, other.Values)\n                : CaseInsensitiveEqualHelper.Equals(Values, other.Values));\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(\n            Name?.GetHashCode(StringComparison.OrdinalIgnoreCase),\n            Mode,\n            IsCaseSensitive,\n            IsCaseSensitive\n                ? CaseSensitiveEqualHelper.GetHashCode(Values)\n                : CaseInsensitiveEqualHelper.GetHashCode(Values));\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteMatch.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Describes the matching criteria for a route.\n/// </summary>\npublic sealed record RouteMatch\n{\n    /// <summary>\n    /// Only match requests that use these optional HTTP methods. E.g. GET, POST.\n    /// </summary>\n    public IReadOnlyList<string>? Methods { get; init; }\n\n    /// <summary>\n    /// Only match requests with the given Host header.\n    /// Supports wildcards and ports. For unicode host names, do not use punycode.\n    /// </summary>\n    public IReadOnlyList<string>? Hosts { get; init; }\n\n    /// <summary>\n    /// Only match requests with the given Path pattern.\n    /// </summary>\n    public string? Path { get; init; }\n\n    /// <summary>\n    /// Only match requests that contain all of these query parameters.\n    /// </summary>\n    public IReadOnlyList<RouteQueryParameter>? QueryParameters { get; init; }\n\n    /// <summary>\n    /// Only match requests that contain all of these headers.\n    /// </summary>\n    public IReadOnlyList<RouteHeader>? Headers { get; init; }\n\n    public bool Equals(RouteMatch? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return string.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase)\n            && CaseInsensitiveEqualHelper.Equals(Hosts, other.Hosts)\n            && CaseInsensitiveEqualHelper.Equals(Methods, other.Methods)\n            && CollectionEqualityHelper.Equals(Headers, other.Headers)\n            && CollectionEqualityHelper.Equals(QueryParameters, other.QueryParameters);\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(\n            Path?.GetHashCode(StringComparison.OrdinalIgnoreCase),\n            CaseInsensitiveEqualHelper.GetHashCode(Hosts),\n            CaseInsensitiveEqualHelper.GetHashCode(Methods),\n            CollectionEqualityHelper.GetHashCode(Headers),\n            CollectionEqualityHelper.GetHashCode(QueryParameters));\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteQueryParameter.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Route criteria for a query parameter that must be present on the incoming request.\n/// </summary>\npublic sealed record RouteQueryParameter\n{\n    /// <summary>\n    /// Name of the query parameter to look for.\n    /// This field is case insensitive and required.\n    /// </summary>\n    public string Name { get; init; } = default!;\n\n    /// <summary>\n    /// A collection of acceptable query parameter values used during routing.\n    /// </summary>\n    public IReadOnlyList<string>? Values { get; init; }\n\n    /// <summary>\n    /// Specifies how query parameter values should be compared (e.g. exact matches Vs. contains).\n    /// Defaults to <see cref=\"QueryParameterMatchMode.Exact\"/>.\n    /// </summary>\n    public QueryParameterMatchMode Mode { get; init; }\n\n    /// <summary>\n    /// Specifies whether query parameter value comparisons should ignore case.\n    /// When <c>true</c>, <see cref=\"StringComparison.Ordinal\" /> is used.\n    /// When <c>false</c>, <see cref=\"StringComparison.OrdinalIgnoreCase\" /> is used.\n    /// Defaults to <c>false</c>.\n    /// </summary>\n    public bool IsCaseSensitive { get; init; }\n\n    public bool Equals(RouteQueryParameter? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase)\n            && Mode == other.Mode\n            && IsCaseSensitive == other.IsCaseSensitive\n            && (IsCaseSensitive\n                ? CaseSensitiveEqualHelper.Equals(Values, other.Values)\n                : CaseInsensitiveEqualHelper.Equals(Values, other.Values));\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(\n            Name?.GetHashCode(StringComparison.OrdinalIgnoreCase),\n            Mode,\n            IsCaseSensitive,\n            IsCaseSensitive\n                ? CaseSensitiveEqualHelper.GetHashCode(Values)\n                : CaseInsensitiveEqualHelper.GetHashCode(Values));\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteValidators/AuthorizationPolicyValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Yarp.ReverseProxy.Configuration.RouteValidators;\n\ninternal sealed class AuthorizationPolicyValidator\n    (IAuthorizationPolicyProvider authorizationPolicyProvider) : IRouteValidator\n{\n    public async ValueTask ValidateAsync(RouteConfig routeConfig, IList<Exception> errors)\n    {\n        var authorizationPolicyName = routeConfig.AuthorizationPolicy;\n        if (string.IsNullOrEmpty(authorizationPolicyName))\n        {\n            return;\n        }\n\n        if (string.Equals(AuthorizationConstants.Default, authorizationPolicyName, StringComparison.OrdinalIgnoreCase))\n        {\n            var policy = await authorizationPolicyProvider.GetPolicyAsync(authorizationPolicyName);\n            if (policy is not null)\n            {\n                errors.Add(new ArgumentException($\"The application has registered an authorization policy named '{authorizationPolicyName}' that conflicts with the reserved authorization policy name used on this route. The registered policy name needs to be changed for this route to function.\"));\n            }\n\n            return;\n        }\n\n        if (string.Equals(AuthorizationConstants.Anonymous, authorizationPolicyName,\n                StringComparison.OrdinalIgnoreCase))\n        {\n            var policy = await authorizationPolicyProvider.GetPolicyAsync(authorizationPolicyName);\n            if (policy is not null)\n            {\n                errors.Add(new ArgumentException(\n                    $\"The application has registered an authorization policy named '{authorizationPolicyName}' that conflicts with the reserved authorization policy name used on this route. The registered policy name needs to be changed for this route to function.\"));\n            }\n\n            return;\n        }\n\n        try\n        {\n            var policy = await authorizationPolicyProvider.GetPolicyAsync(authorizationPolicyName);\n            if (policy is null)\n            {\n                errors.Add(new ArgumentException(\n                    $\"Authorization policy '{authorizationPolicyName}' not found for route '{routeConfig.RouteId}'.\"));\n            }\n        }\n        catch (Exception ex)\n        {\n            errors.Add(new ArgumentException(\n                $\"Unable to retrieve the authorization policy '{authorizationPolicyName}' for route '{routeConfig.RouteId}'.\", ex));\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteValidators/CorsPolicyValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Cors.Infrastructure;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Yarp.ReverseProxy.Configuration.RouteValidators;\n\ninternal sealed class CorsPolicyValidator(ICorsPolicyProvider corsPolicyProvider) : IRouteValidator\n{\n    public async ValueTask ValidateAsync(RouteConfig routeConfig, IList<Exception> errors)\n    {\n        var corsPolicyName = routeConfig.CorsPolicy;\n        if (string.IsNullOrEmpty(corsPolicyName))\n        {\n            return;\n        }\n\n        if (string.Equals(CorsConstants.Default, corsPolicyName, StringComparison.OrdinalIgnoreCase))\n        {\n            var dummyHttpContext = new DefaultHttpContext();\n            var policy = await corsPolicyProvider.GetPolicyAsync(dummyHttpContext, corsPolicyName);\n            if (policy is not null)\n            {\n                errors.Add(new ArgumentException(\n                    $\"The application has registered a CORS policy named '{corsPolicyName}' that conflicts with the reserved CORS policy name used on this route. The registered policy name needs to be changed for this route to function.\"));\n            }\n\n            return;\n        }\n\n        if (string.Equals(CorsConstants.Disable, corsPolicyName, StringComparison.OrdinalIgnoreCase))\n        {\n            var dummyHttpContext = new DefaultHttpContext();\n            var policy = await corsPolicyProvider.GetPolicyAsync(dummyHttpContext, corsPolicyName);\n            if (policy is not null)\n            {\n                errors.Add(new ArgumentException(\n                    $\"The application has registered a CORS policy named '{corsPolicyName}' that conflicts with the reserved CORS policy name used on this route. The registered policy name needs to be changed for this route to function.\"));\n            }\n\n            return;\n        }\n\n        try\n        {\n            var dummyHttpContext = new DefaultHttpContext();\n            var policy = await corsPolicyProvider.GetPolicyAsync(dummyHttpContext, corsPolicyName);\n            if (policy is null)\n            {\n                errors.Add(new ArgumentException(\n                    $\"CORS policy '{corsPolicyName}' not found for route '{routeConfig.RouteId}'.\"));\n            }\n        }\n        catch (Exception ex)\n        {\n            errors.Add(new ArgumentException(\n                $\"Unable to retrieve the CORS policy '{corsPolicyName}' for route '{routeConfig.RouteId}'.\", ex));\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteValidators/HeadersValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Configuration.RouteValidators;\n\ninternal sealed class HeadersValidator : IRouteValidator\n{\n    public ValueTask ValidateAsync(RouteConfig routeConfig, IList<Exception> errors)\n    {\n        var route = routeConfig.Match;\n        if (route.Headers is null)\n        {\n            // Headers are optional\n            return ValueTask.CompletedTask;\n        }\n\n        foreach (var header in route.Headers)\n        {\n            if (header is null)\n            {\n                errors.Add(new ArgumentException($\"A null route header has been set for route '{routeConfig.RouteId}'.\"));\n                continue;\n            }\n\n            if (string.IsNullOrEmpty(header.Name))\n            {\n                errors.Add(new ArgumentException($\"A null or empty route header name has been set for route '{routeConfig.RouteId}'.\"));\n            }\n\n            if (header.Mode != HeaderMatchMode.Exists && header.Mode != HeaderMatchMode.NotExists\n                                                      && (header.Values is null || header.Values.Count == 0))\n            {\n                errors.Add(new ArgumentException($\"No header values were set on route header '{header.Name}' for route '{routeConfig.RouteId}'.\"));\n            }\n\n            if ((header.Mode == HeaderMatchMode.Exists || header.Mode == HeaderMatchMode.NotExists) && header.Values?.Count > 0)\n            {\n                errors.Add(new ArgumentException($\"Header values were set when using mode '{header.Mode}' on route header '{header.Name}' for route '{routeConfig.RouteId}'.\"));\n            }\n        }\n\n        return ValueTask.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteValidators/HostValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Configuration.RouteValidators;\n\ninternal sealed class HostValidator : IRouteValidator\n{\n    public ValueTask ValidateAsync(RouteConfig routeConfig, IList<Exception> errors)\n    {\n        var route = routeConfig.Match;\n        if (route.Hosts is null || route.Hosts.Count == 0)\n        {\n            // Host is optional when Path is specified\n            return ValueTask.CompletedTask;\n        }\n\n        foreach (var host in route.Hosts)\n        {\n            if (string.IsNullOrEmpty(host))\n            {\n                errors.Add(new ArgumentException($\"Empty host name has been set for route '{routeConfig.RouteId}'.\"));\n            }\n            else if (host.Contains(\"xn--\", StringComparison.OrdinalIgnoreCase))\n            {\n                errors.Add(new ArgumentException($\"Punycode host name '{host}' has been set for route '{routeConfig.RouteId}'. Use the unicode host name instead.\"));\n            }\n        }\n\n        return ValueTask.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteValidators/IRouteValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Configuration.RouteValidators;\n\n/// <summary>\n/// Provides method to validate route configuration.\n/// </summary>\npublic interface IRouteValidator\n{\n    /// <summary>\n    /// Perform validation on a route by adding exceptions to the provided collection.\n    /// </summary>\n    /// <param name=\"routeConfig\">Route configuration to validate</param>\n    /// <param name=\"errors\">Collection of all validation exceptions</param>\n    /// <returns>A ValueTask representing the asynchronous validation operation.</returns>\n    public ValueTask ValidateAsync(RouteConfig routeConfig, IList<Exception> errors);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteValidators/MethodsValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Configuration.RouteValidators;\n\ninternal sealed class MethodsValidator : IRouteValidator\n{\n    private static readonly HashSet<string> _validMethods = new(StringComparer.OrdinalIgnoreCase)\n    {\n        \"HEAD\", \"OPTIONS\", \"GET\", \"PUT\", \"POST\", \"PATCH\", \"DELETE\", \"TRACE\",\n    };\n\n    public ValueTask ValidateAsync(RouteConfig routeConfig, IList<Exception> errors)\n    {\n        var route = routeConfig.Match;\n        if (route.Methods is null)\n        {\n            // Methods are optional\n            return ValueTask.CompletedTask;\n        }\n\n        var seenMethods = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n        foreach (var method in route.Methods)\n        {\n            if (!seenMethods.Add(method))\n            {\n                errors.Add(new ArgumentException($\"Duplicate HTTP method '{method}' for route '{routeConfig.RouteId}'.\"));\n                continue;\n            }\n\n            if (!_validMethods.Contains(method))\n            {\n                errors.Add(new ArgumentException($\"Unsupported HTTP method '{method}' has been set for route '{routeConfig.RouteId}'.\"));\n            }\n        }\n\n        return ValueTask.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteValidators/OutputCachePolicyValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Configuration.RouteValidators;\n\ninternal sealed class OutputCachePolicyValidator : IRouteValidator\n{\n    private readonly IYarpOutputCachePolicyProvider _outputCachePolicyProvider;\n\n    public OutputCachePolicyValidator(IYarpOutputCachePolicyProvider outputCachePolicyProvider)\n    {\n        _outputCachePolicyProvider = outputCachePolicyProvider;\n    }\n\n    public async ValueTask ValidateAsync(RouteConfig routeConfig, IList<Exception> errors)\n    {\n        var outputCachePolicyName = routeConfig.OutputCachePolicy;\n\n        if (string.IsNullOrEmpty(outputCachePolicyName))\n        {\n            return;\n        }\n\n        try\n        {\n            var policy = await _outputCachePolicyProvider.GetPolicyAsync(outputCachePolicyName);\n\n            if (policy is null)\n            {\n                errors.Add(new ArgumentException(\n                    $\"OutputCache policy '{outputCachePolicyName}' not found for route '{routeConfig.RouteId}'.\"));\n            }\n        }\n        catch (Exception ex)\n        {\n            errors.Add(new ArgumentException(\n                $\"Unable to retrieve the OutputCache policy '{outputCachePolicyName}' for route '{routeConfig.RouteId}'.\",\n                ex));\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteValidators/PathValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Routing.Patterns;\n\nnamespace Yarp.ReverseProxy.Configuration.RouteValidators;\n\ninternal sealed class PathValidator : IRouteValidator\n{\n    public ValueTask ValidateAsync(RouteConfig routeConfig, IList<Exception> errors)\n    {\n        var route = routeConfig.Match;\n        if (string.IsNullOrEmpty(route.Path))\n        {\n            // Path is optional when Host is specified\n            return ValueTask.CompletedTask;\n        }\n\n        try\n        {\n            RoutePatternFactory.Parse(route.Path);\n        }\n        catch (RoutePatternException ex)\n        {\n            errors.Add(new ArgumentException($\"Invalid path '{route.Path}' for route '{routeConfig.RouteId}'.\", ex));\n        }\n\n        return ValueTask.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteValidators/QueryParametersValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Configuration.RouteValidators;\n\ninternal sealed class QueryParametersValidator : IRouteValidator\n{\n    public ValueTask ValidateAsync(RouteConfig routeConfig, IList<Exception> errors)\n    {\n        var route = routeConfig.Match;\n        if (route.QueryParameters is null)\n        {\n            // Query Parameters are optional\n            return ValueTask.CompletedTask;\n        }\n\n        foreach (var queryParameter in route.QueryParameters)\n        {\n            if (queryParameter is null)\n            {\n                errors.Add(new ArgumentException($\"A null route query parameter has been set for route '{routeConfig.RouteId}'.\"));\n                continue;\n            }\n\n            if (string.IsNullOrEmpty(queryParameter.Name))\n            {\n                errors.Add(new ArgumentException($\"A null or empty route query parameter name has been set for route '{routeConfig.RouteId}'.\"));\n            }\n\n            if (queryParameter.Mode != QueryParameterMatchMode.Exists\n                && (queryParameter.Values is null || queryParameter.Values.Count == 0))\n            {\n                errors.Add(new ArgumentException($\"No query parameter values were set on route query parameter '{queryParameter.Name}' for route '{routeConfig.RouteId}'.\"));\n            }\n\n            if (queryParameter.Mode == QueryParameterMatchMode.Exists && queryParameter.Values?.Count > 0)\n            {\n                errors.Add(new ArgumentException($\"Query parameter values where set when using mode '{nameof(QueryParameterMatchMode.Exists)}' on route query parameter '{queryParameter.Name}' for route '{routeConfig.RouteId}'.\"));\n            }\n        }\n\n        return ValueTask.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteValidators/RateLimitPolicyValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Configuration.RouteValidators;\n\ninternal sealed class RateLimitPolicyValidator : IRouteValidator\n{\n    private readonly IYarpRateLimiterPolicyProvider _rateLimiterPolicyProvider;\n\n    public RateLimitPolicyValidator(IYarpRateLimiterPolicyProvider rateLimiterPolicyProvider)\n    {\n        _rateLimiterPolicyProvider = rateLimiterPolicyProvider;\n    }\n\n    public async ValueTask ValidateAsync(RouteConfig routeConfig, IList<Exception> errors)\n    {\n        var rateLimiterPolicyName = routeConfig.RateLimiterPolicy;\n\n        if (string.IsNullOrEmpty(rateLimiterPolicyName))\n        {\n            return;\n        }\n\n        if (string.Equals(RateLimitingConstants.Default, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase)\n            || string.Equals(RateLimitingConstants.Disable, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase))\n        {\n            var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName);\n            if (policy is not null)\n            {\n                // We weren't expecting to find a policy with these names.\n                errors.Add(new ArgumentException(\n                    $\"The application has registered a RateLimiter policy named '{rateLimiterPolicyName}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function.\"));\n            }\n\n            return;\n        }\n\n        try\n        {\n            var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName);\n\n            if (policy is null)\n            {\n                errors.Add(new ArgumentException(\n                    $\"RateLimiter policy '{rateLimiterPolicyName}' not found for route '{routeConfig.RouteId}'.\"));\n            }\n        }\n        catch (Exception ex)\n        {\n            errors.Add(new ArgumentException(\n                $\"Unable to retrieve the RateLimiter policy '{rateLimiterPolicyName}' for route '{routeConfig.RouteId}'.\",\n                ex));\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/RouteValidators/TimeoutPolicyValidator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http.Timeouts;\nusing Microsoft.Extensions.Options;\n\nnamespace Yarp.ReverseProxy.Configuration.RouteValidators;\n\ninternal sealed class TimeoutPolicyValidator : IRouteValidator\n{\n    private readonly IOptionsMonitor<RequestTimeoutOptions> _timeoutOptions;\n\n    public TimeoutPolicyValidator(IOptionsMonitor<RequestTimeoutOptions> timeoutOptions)\n    {\n        _timeoutOptions = timeoutOptions;\n    }\n\n    public ValueTask ValidateAsync(RouteConfig routeConfig, IList<Exception> errors)\n    {\n        var timeoutPolicyName = routeConfig.TimeoutPolicy;\n        var timeout = routeConfig.Timeout;\n\n        if (!string.IsNullOrEmpty(timeoutPolicyName))\n        {\n            var policies = _timeoutOptions.CurrentValue.Policies;\n\n            if (string.Equals(TimeoutPolicyConstants.Disable, timeoutPolicyName, StringComparison.OrdinalIgnoreCase))\n            {\n                if (policies.TryGetValue(timeoutPolicyName, out var _))\n                {\n                    errors.Add(new ArgumentException($\"The application has registered a timeout policy named '{timeoutPolicyName}' that conflicts with the reserved timeout policy name used on this route. The registered policy name needs to be changed for this route to function.\"));\n                }\n            }\n            else if (!policies.TryGetValue(timeoutPolicyName, out var _))\n            {\n                errors.Add(new ArgumentException($\"Timeout policy '{timeoutPolicyName}' not found for route '{routeConfig.RouteId}'.\"));\n            }\n\n            if (timeout.HasValue)\n            {\n                errors.Add(new ArgumentException($\"Route '{routeConfig.RouteId}' has both a Timeout '{timeout}' and TimeoutPolicy '{timeoutPolicyName}'.\"));\n            }\n        }\n\n        if (timeout.HasValue && timeout.Value.TotalMilliseconds <= 0)\n        {\n            errors.Add(new ArgumentException($\"The Timeout value '{timeout.Value}' is invalid for route '{routeConfig.RouteId}'. The Timeout must be greater than zero milliseconds.\"));\n        }\n\n        return ValueTask.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/SessionAffinityConfig.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Session affinity options.\n/// </summary>\npublic sealed record SessionAffinityConfig\n{\n\n    /// <summary>\n    /// Indicates whether session affinity is enabled.\n    /// </summary>\n    public bool? Enabled { get; init; }\n\n    /// <summary>\n    /// The session affinity policy to use.\n    /// </summary>\n    public string? Policy { get; init; }\n\n    /// <summary>\n    /// Strategy for handling a missing destination for an affinitized request.\n    /// </summary>\n    public string? FailurePolicy { get; init; }\n\n    /// <summary>\n    /// Identifies the name of the field where the affinity value is stored.\n    /// For the cookie affinity policy this will be the cookie name.\n    /// For the header affinity policy this will be the header name.\n    /// This value should be unique across clusters to avoid affinity conflicts.\n    /// https://github.com/dotnet/yarp/issues/976\n    /// This field is required.\n    /// </summary>\n    public string AffinityKeyName { get; init; } = default!;\n\n    /// <summary>\n    /// Configuration of a cookie storing the session affinity key in case\n    /// the <see cref=\"Policy\"/> is set to 'Cookie'.\n    /// </summary>\n    public SessionAffinityCookieConfig? Cookie { get; init; }\n\n    public bool Equals(SessionAffinityConfig? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return Enabled == other.Enabled\n            && string.Equals(Policy, other.Policy, StringComparison.OrdinalIgnoreCase)\n            && string.Equals(FailurePolicy, other.FailurePolicy, StringComparison.OrdinalIgnoreCase)\n            && string.Equals(AffinityKeyName, other.AffinityKeyName, StringComparison.Ordinal)\n            && Cookie == other.Cookie;\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(Enabled,\n            Policy?.GetHashCode(StringComparison.OrdinalIgnoreCase),\n            FailurePolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase),\n            AffinityKeyName?.GetHashCode(StringComparison.Ordinal),\n            Cookie);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/SessionAffinityCookieConfig.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n// Mirrors CookieBuilder and CookieOptions\n// https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http.Abstractions/src/CookieBuilder.cs\n/// <summary>\n/// Config for session affinity cookies.\n/// </summary>\npublic sealed record SessionAffinityCookieConfig\n{\n    /// <summary>\n    /// The cookie path.\n    /// </summary>\n    public string? Path { get; init; }\n\n    /// <summary>\n    /// The domain to associate the cookie with.\n    /// </summary>\n    public string? Domain { get; init; }\n\n    /// <summary>\n    /// Indicates whether a cookie is accessible by client-side script.\n    /// </summary>\n    /// <remarks>Defaults to \"true\".</remarks>\n    public bool? HttpOnly { get; init; }\n\n    /// <summary>\n    /// The policy that will be used to determine <see cref=\"CookieOptions.Secure\"/>.\n    /// </summary>\n    /// <remarks>Defaults to <see cref=\"CookieSecurePolicy.None\"/>.</remarks>\n    public CookieSecurePolicy? SecurePolicy { get; init; }\n\n    /// <summary>\n    /// The SameSite attribute of the cookie.\n    /// </summary>\n    /// <remarks>Defaults to <see cref=\"SameSiteMode.Unspecified\"/>.</remarks>\n    public SameSiteMode? SameSite { get; init; }\n\n    /// <summary>\n    /// Gets or sets the lifespan of a cookie.\n    /// </summary>\n    public TimeSpan? Expiration { get; init; }\n\n    /// <summary>\n    /// Gets or sets the max-age for the cookie.\n    /// </summary>\n    public TimeSpan? MaxAge { get; init; }\n\n    /// <summary>\n    /// Indicates if this cookie is essential for the application to function correctly. If true then\n    /// consent policy checks may be bypassed.\n    /// </summary>\n    /// <remarks>Defaults to \"false\".</remarks>\n    public bool? IsEssential { get; init; }\n\n    public bool Equals(SessionAffinityCookieConfig? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return string.Equals(Path, other.Path, StringComparison.Ordinal)\n            && string.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase)\n            && HttpOnly == other.HttpOnly\n            && SecurePolicy == other.SecurePolicy\n            && SameSite == other.SameSite\n            && Expiration == other.Expiration\n            && MaxAge == other.MaxAge\n            && IsEssential == other.IsEssential;\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(Path?.GetHashCode(StringComparison.Ordinal),\n            Domain?.GetHashCode(StringComparison.OrdinalIgnoreCase),\n            HttpOnly,\n            SecurePolicy,\n            SameSite,\n            Expiration,\n            MaxAge,\n            IsEssential);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/TimeoutPolicyConstants.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Configuration;\n\ninternal static class TimeoutPolicyConstants\n{\n    internal const string Disable = \"Disable\";\n}\n"
  },
  {
    "path": "src/ReverseProxy/Configuration/WebProxyConfig.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\n/// <summary>\n/// Config used to construct <seealso cref=\"System.Net.WebProxy\"/> instance.\n/// </summary>\npublic sealed record WebProxyConfig : IEquatable<WebProxyConfig>\n{\n    /// <summary>\n    /// The URI of the proxy server.\n    /// </summary>\n    public Uri? Address { get; init; }\n\n    /// <summary>\n    /// true to bypass the proxy for local addresses; otherwise, false.\n    /// If null, default value will be used: false\n    /// </summary>\n    public bool? BypassOnLocal { get; init; }\n\n    /// <summary>\n    /// Controls whether the <seealso cref=\"System.Net.CredentialCache.DefaultCredentials\"/> are sent with requests.\n    /// If null, default value will be used: false\n    /// </summary>\n    public bool? UseDefaultCredentials { get; init; }\n\n    public bool Equals(WebProxyConfig? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return Address == other.Address\n            && BypassOnLocal == other.BypassOnLocal\n            && UseDefaultCredentials == other.UseDefaultCredentials;\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(\n            Address,\n            BypassOnLocal,\n            UseDefaultCredentials\n        );\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/ConfigurationSchema.json",
    "content": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"ReverseProxy\": {\n      \"type\": \"object\",\n      \"description\": \"Reverse proxy configuration for routes and clusters.\",\n      \"properties\": {\n        \"Routes\": {\n          \"type\": \"object\",\n          \"description\": \"Named routes that direct incoming requests to clusters.\",\n          \"patternProperties\": {\n            \".\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"ClusterId\": {\n                  \"type\": \"string\",\n                  \"description\": \"Name of the cluster this route points to.\"\n                },\n                \"Order\": {\n                  \"type\": [ \"number\", \"null\" ],\n                  \"description\": \"Order value for this route. Routes with lower numbers take precedence over higher numbers.\"\n                },\n                \"MaxRequestBodySize\": {\n                  \"type\": [ \"number\", \"null\" ],\n                  \"description\": \"An optional override for how large request bodies can be in bytes.\"\n                },\n                \"AuthorizationPolicy\": {\n                  \"type\": [ \"string\", \"null\" ],\n                  \"description\": \"Specifies which authorization policy applies, e.g. 'Default' or 'Anonymous'.\"\n                },\n                \"RateLimiterPolicy\": {\n                  \"type\": [ \"string\", \"null\" ],\n                  \"description\": \"The name of the RateLimiterPolicy to apply to this route.\"\n                },\n                \"OutputCachePolicy\": {\n                  \"type\": [ \"string\", \"null\" ],\n                  \"description\": \"The name of the OutputCachePolicy to apply to this route.\"\n                },\n                \"Timeout\": {\n                  \"type\": [ \"string\", \"null\" ],\n                  \"pattern\": \"^\\\\d*?\\\\.?\\\\d\\\\d?:\\\\d\\\\d?:\\\\d\\\\d?\\\\.?\\\\d{0,7}$\",\n                  \"description\": \"The Timeout to apply to this route. This overrides any system defaults. Setting both Timeout and TimeoutPolicy is an error. Format: 'hh:mm:ss'.\"\n                },\n                \"TimeoutPolicy\": {\n                  \"type\": [ \"string\", \"null\" ],\n                  \"description\": \"Specifies which timeout policy applies, e.g. 'Default' or 'Disable'. Setting both Timeout and TimeoutPolicy is an error.\"\n                },\n                \"CorsPolicy\": {\n                  \"type\": [ \"string\", \"null\" ],\n                  \"description\": \"Specifies which CORS policy applies, e.g. 'Default' or 'Disable'.\"\n                },\n                \"Match\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"Path\": {\n                      \"type\": [ \"string\", \"null\" ],\n                      \"description\": \"Path pattern using ASP.NET route template syntax, e.g. '/something/{**remainder}'.\"\n                    },\n                    \"Hosts\": {\n                      \"type\": [ \"array\", \"null\" ],\n                      \"description\": \"Only match requests with the given Host header. Supports wildcards and ports. For unicode host names, do not use punycode.\",\n                      \"items\": {\n                        \"type\": \"string\"\n                      }\n                    },\n                    \"Methods\": {\n                      \"description\": \"Allowed HTTP methods.\",\n                      \"items\": {\n                        \"anyOf\": [\n                          {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"DELETE\",\n                              \"GET\",\n                              \"HEAD\",\n                              \"OPTIONS\",\n                              \"PATCH\",\n                              \"POST\",\n                              \"PUT\",\n                              \"TRACE\"\n                            ]\n                          },\n                          {\n                            \"type\": [ \"array\", \"null\" ]\n                          }\n                        ]\n                      }\n                    },\n                    \"Headers\": {\n                      \"type\": [ \"array\", \"null\" ],\n                      \"description\": \"List of header match conditions.\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"Name\": {\n                            \"type\": \"string\",\n                            \"description\": \"Header name.\"\n                          },\n                          \"Values\": {\n                            \"type\": \"array\",\n                            \"description\": \"Matches against any of these values.\",\n                            \"items\": {\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"Mode\": {\n                            \"type\": \"string\",\n                            \"description\": \"How the header values should be matched.\",\n                            \"enum\": [\n                              \"ExactHeader\",\n                              \"HeaderPrefix\",\n                              \"Contains\",\n                              \"NotContains\",\n                              \"Exists\",\n                              \"NotExists\"\n                            ]\n                          },\n                          \"IsCaseSensitive\": {\n                            \"anyOf\": [\n                              {\n                                \"type\": \"boolean\"\n                              },\n                              {\n                                \"type\": \"string\",\n                                \"enum\": [\n                                  \"true\",\n                                  \"false\",\n                                  \"True\",\n                                  \"False\"\n                                ]\n                              }\n                            ]\n                          }\n                        },\n                        \"required\": [\n                          \"Name\"\n                        ],\n                        \"additionalProperties\": false\n                      }\n                    },\n                    \"QueryParameters\": {\n                      \"type\": [ \"array\", \"null\" ],\n                      \"description\": \"List of query string match conditions.\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"Name\": {\n                            \"type\": \"string\",\n                            \"description\": \"Name of the query parameter.\"\n                          },\n                          \"Values\": {\n                            \"type\": \"array\",\n                            \"description\": \"Matches against any of these values.\",\n                            \"items\": {\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"Mode\": {\n                            \"type\": \"string\",\n                            \"description\": \"How the query parameter values should be matched.\",\n                            \"enum\": [\n                              \"Exact\",\n                              \"Contains\",\n                              \"NotContains\",\n                              \"Prefix\",\n                              \"Exists\"\n                            ]\n                          },\n                          \"IsCaseSensitive\": {\n                            \"anyOf\": [\n                              {\n                                \"type\": \"boolean\"\n                              },\n                              {\n                                \"type\": \"string\",\n                                \"enum\": [\n                                  \"true\",\n                                  \"false\",\n                                  \"True\",\n                                  \"False\"\n                                ]\n                              }\n                            ]\n                          }\n                        },\n                        \"required\": [\n                          \"Name\"\n                        ],\n                        \"additionalProperties\": false\n                      }\n                    }\n                  },\n                  \"additionalProperties\": false,\n                  \"anyOf\": [\n                    {\n                      \"required\": [\n                        \"Path\"\n                      ]\n                    },\n                    {\n                      \"required\": [\n                        \"Hosts\"\n                      ]\n                    }\n                  ]\n                },\n                \"Metadata\": {\n                  \"type\": [ \"object\", \"null\" ],\n                  \"description\": \"Arbitrary key-value pairs for custom route logic.\",\n                  \"additionalProperties\": {\n                    \"type\": [ \"null\", \"number\", \"string\", \"boolean\" ]\n                  }\n                },\n                \"Transforms\": {\n                  \"type\": [ \"array\", \"null\" ],\n                  \"description\": \"List of transform objects for request customization.\",\n                  \"items\": {\n                    \"description\": \"A single transform definition.\",\n                    \"anyOf\": [\n                      {\n                        \"type\": [ \"object\", \"null\" ],\n                        \"$comment\": \"Fallback that matches any custom user-defined transforms.\",\n                        \"properties\": {\n                          \"RequestHeadersCopy\": { \"not\": {} },\n                          \"RequestHeaderOriginalHost\": { \"not\": {} },\n                          \"RequestHeader\": { \"not\": {} },\n                          \"PathRemovePrefix\": { \"not\": {} },\n                          \"PathSet\": { \"not\": {} },\n                          \"PathPrefix\": { \"not\": {} },\n                          \"QueryRouteParameter\": { \"not\": {} },\n                          \"PathPattern\": { \"not\": {} },\n                          \"QueryValueParameter\": { \"not\": {} },\n                          \"QueryRemoveParameter\": { \"not\": {} },\n                          \"HttpMethodChange\": { \"not\": {} },\n                          \"RequestHeaderRouteValue\": { \"not\": {} },\n                          \"RequestHeaderRemove\": { \"not\": {} },\n                          \"RequestHeadersAllowed\": { \"not\": {} },\n                          \"X-Forwarded\": { \"not\": {} },\n                          \"Forwarded\": { \"not\": {} },\n                          \"ClientCert\": { \"not\": {} },\n                          \"ResponseHeadersCopy\": { \"not\": {} },\n                          \"ResponseHeader\": { \"not\": {} },\n                          \"ResponseHeaderRemove\": { \"not\": {} },\n                          \"ResponseHeadersAllowed\": { \"not\": {} },\n                          \"ResponseTrailersCopy\": { \"not\": {} },\n                          \"ResponseTrailer\": { \"not\": {} },\n                          \"ResponseTrailerRemove\": { \"not\": {} },\n                          \"ResponseTrailersAllowed\": { \"not\": {} }\n                        }\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Sets whether incoming request headers are copied to the outbound request.\",\n                        \"properties\": {\n                          \"RequestHeadersCopy\": {\n                            \"description\": \"If true, copies all request headers to outbound request.\",\n                            \"anyOf\": [\n                              {\n                                \"type\": \"boolean\"\n                              },\n                              {\n                                \"type\": \"string\",\n                                \"enum\": [\n                                  \"true\",\n                                  \"false\",\n                                  \"True\",\n                                  \"False\"\n                                ]\n                              }\n                            ]\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"RequestHeadersCopy\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Specifies if the incoming request Host header should be copied to the proxy request.\",\n                        \"properties\": {\n                          \"RequestHeaderOriginalHost\": {\n                            \"description\": \"If true, preserve the original Host header; otherwise the destination's host is used.\",\n                            \"anyOf\": [\n                              {\n                                \"type\": \"boolean\"\n                              },\n                              {\n                                \"type\": \"string\",\n                                \"enum\": [\n                                  \"true\",\n                                  \"false\",\n                                  \"True\",\n                                  \"False\"\n                                ]\n                              }\n                            ]\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"RequestHeaderOriginalHost\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Transform for setting, appending, or removing a request header.\",\n                        \"properties\": {\n                          \"RequestHeader\": {\n                            \"type\": \"string\",\n                            \"description\": \"The header name to operate on.\"\n                          },\n                          \"Set\": {\n                            \"type\": \"string\",\n                            \"description\": \"Value to set the given request header to.\"\n                          },\n                          \"Append\": {\n                            \"type\": \"string\",\n                            \"description\": \"Value to append to the given request header.\"\n                          },\n                          \"Remove\": {\n                            \"description\": \"Removes the header if true.\",\n                            \"anyOf\": [\n                              {\n                                \"type\": \"boolean\"\n                              },\n                              {\n                                \"type\": \"string\",\n                                \"enum\": [\n                                  \"true\",\n                                  \"false\",\n                                  \"True\",\n                                  \"False\"\n                                ]\n                              }\n                            ]\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"anyOf\": [\n                          {\n                            \"required\": [\n                              \"RequestHeader\",\n                              \"Set\"\n                            ]\n                          },\n                          {\n                            \"required\": [\n                              \"RequestHeader\",\n                              \"Append\"\n                            ]\n                          },\n                          {\n                            \"required\": [\n                              \"RequestHeader\",\n                              \"Remove\"\n                            ]\n                          }\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Transform that removes a specified prefix from the request path.\",\n                        \"properties\": {\n                          \"PathRemovePrefix\": {\n                            \"type\": \"string\",\n                            \"description\": \"Prefix to remove from the existing request path.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"PathRemovePrefix\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Transform that replaces the entire request path with the provided value.\",\n                        \"properties\": {\n                          \"PathSet\": {\n                            \"type\": \"string\",\n                            \"description\": \"Sets the request path to this value.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"PathSet\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Transform that adds the specified prefix to the request path.\",\n                        \"properties\": {\n                          \"PathPrefix\": {\n                            \"type\": \"string\",\n                            \"description\": \"Path prefix to add.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"PathPrefix\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Transform that adds or replaces a query string parameter with a value from the route configuration.\",\n                        \"properties\": {\n                          \"QueryRouteParameter\": {\n                            \"type\": \"string\",\n                            \"description\": \"Specifies the query parameter name to add or replace.\"\n                          },\n                          \"Set\": {\n                            \"type\": \"string\",\n                            \"description\": \"Name of the route parameter to set the query parameter to.\"\n                          },\n                          \"Append\": {\n                            \"type\": \"string\",\n                            \"description\": \"Name of the route parameter to append to the query parameter.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"anyOf\": [\n                          {\n                            \"required\": [\n                              \"QueryRouteParameter\",\n                              \"Set\"\n                            ]\n                          },\n                          {\n                            \"required\": [\n                              \"QueryRouteParameter\",\n                              \"Append\"\n                            ]\n                          }\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Transform that replaces the entire request path using a pattern template, replacing {} segments with the route value.\",\n                        \"properties\": {\n                          \"PathPattern\": {\n                            \"type\": \"string\",\n                            \"description\": \"A path template starting with a '/', e.g. '/my/{plugin}/api/{**remainder}'.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"PathPattern\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Adds or replaces parameters in the request query string.\",\n                        \"properties\": {\n                          \"QueryValueParameter\": {\n                            \"type\": \"string\",\n                            \"description\": \"Name of a query string parameter.\"\n                          },\n                          \"Set\": {\n                            \"type\": \"string\",\n                            \"description\": \"Value to set the given query parameter to.\"\n                          },\n                          \"Append\": {\n                            \"type\": \"string\",\n                            \"description\": \"Value to append to the given query parameter.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"anyOf\": [\n                          {\n                            \"required\": [\n                              \"QueryValueParameter\",\n                              \"Set\"\n                            ]\n                          },\n                          {\n                            \"required\": [\n                              \"QueryValueParameter\",\n                              \"Append\"\n                            ]\n                          }\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Removes the specified parameter from the request query string.\",\n                        \"properties\": {\n                          \"QueryRemoveParameter\": {\n                            \"type\": \"string\",\n                            \"description\": \"Name of a query string parameter.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"QueryRemoveParameter\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Changes the http method used in the request.\",\n                        \"properties\": {\n                          \"HttpMethodChange\": {\n                            \"description\": \"The HTTP method to replace.\",\n                            \"anyOf\": [\n                              {\n                                \"type\": \"string\",\n                                \"enum\": [\n                                  \"DELETE\",\n                                  \"GET\",\n                                  \"HEAD\",\n                                  \"OPTIONS\",\n                                  \"PATCH\",\n                                  \"POST\",\n                                  \"PUT\",\n                                  \"TRACE\"\n                                ]\n                              },\n                              {\n                                \"type\": \"string\"\n                              }\n                            ]\n                          },\n                          \"Set\": {\n                            \"type\": \"string\",\n                            \"description\": \"The new HTTP method.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"HttpMethodChange\",\n                          \"Set\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Adds or replaces a header with a value from the route configuration.\",\n                        \"properties\": {\n                          \"RequestHeaderRouteValue\": {\n                            \"type\": \"string\",\n                            \"description\": \"The header name to operate on.\"\n                          },\n                          \"Set\": {\n                            \"type\": \"string\",\n                            \"description\": \"Route value to set the given header to.\"\n                          },\n                          \"Append\": {\n                            \"type\": \"string\",\n                            \"description\": \"Route value to append to the given header.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"anyOf\": [\n                          {\n                            \"required\": [\n                              \"RequestHeaderRouteValue\",\n                              \"Set\"\n                            ]\n                          },\n                          {\n                            \"required\": [\n                              \"RequestHeaderRouteValue\",\n                              \"Append\"\n                            ]\n                          }\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Removes the specified header from the request.\",\n                        \"properties\": {\n                          \"RequestHeaderRemove\": {\n                            \"type\": \"string\",\n                            \"description\": \"The header name.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"RequestHeaderRemove\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"YARP copies most request headers to the proxy request by default, this transform disables RequestHeadersCopy and only copies the given headers.\",\n                        \"properties\": {\n                          \"RequestHeadersAllowed\": {\n                            \"type\": \"string\",\n                            \"pattern\": \"^[a-zA-Z0-9!#$%&'*+-.^_`|~;]+$\",\n                            \"description\": \"A semicolon separated list of allowed header names.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"RequestHeadersAllowed\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Adds headers with information about the original client request.\",\n                        \"properties\": {\n                          \"X-Forwarded\": {\n                            \"type\": \"string\",\n                            \"description\": \"Default action to apply to all X-Forwarded-* headers.\",\n                            \"enum\": [\n                              \"Set\",\n                              \"Append\",\n                              \"Remove\",\n                              \"Off\"\n                            ]\n                          },\n                          \"For\": {\n                            \"type\": \"string\",\n                            \"description\": \"Action to apply to the 'For' header.\",\n                            \"enum\": [\n                              \"Set\",\n                              \"Append\",\n                              \"Remove\",\n                              \"Off\"\n                            ]\n                          },\n                          \"Proto\": {\n                            \"type\": \"string\",\n                            \"description\": \"Action to apply to the 'Proto' header.\",\n                            \"enum\": [\n                              \"Set\",\n                              \"Append\",\n                              \"Remove\",\n                              \"Off\"\n                            ]\n                          },\n                          \"Host\": {\n                            \"type\": \"string\",\n                            \"description\": \"Action to apply to the 'Host' header.\",\n                            \"enum\": [\n                              \"Set\",\n                              \"Append\",\n                              \"Remove\",\n                              \"Off\"\n                            ]\n                          },\n                          \"Prefix\": {\n                            \"type\": \"string\",\n                            \"description\": \"Action to apply to the 'Prefix' header.\",\n                            \"enum\": [\n                              \"Set\",\n                              \"Append\",\n                              \"Remove\",\n                              \"Off\"\n                            ]\n                          },\n                          \"HeaderPrefix\": {\n                            \"type\": \"string\",\n                            \"description\": \"The header name prefix.\",\n                            \"default\": \"X-Forwarded-\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"X-Forwarded\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Adds a header with information about the original client request.\",\n                        \"properties\": {\n                          \"Forwarded\": {\n                            \"type\": \"string\",\n                            \"pattern\": \"^(?:(?:for|by|proto|host),?)+$\",\n                            \"description\": \"A comma separated list containing any of these values: 'for,by,proto,host'.\"\n                          },\n                          \"ForFormat\": {\n                            \"type\": \"string\",\n                            \"description\": \"Format to apply to the 'For' header.\",\n                            \"enum\": [\n                              \"Random\",\n                              \"RandomAndPort\",\n                              \"RandomAndRandomPort\",\n                              \"Unknown\",\n                              \"UnknownAndPort\",\n                              \"UnknownAndRandomPort\",\n                              \"Ip\",\n                              \"IpAndPort\",\n                              \"IpAndRandomPort\"\n                            ]\n                          },\n                          \"ByFormat\": {\n                            \"type\": \"string\",\n                            \"description\": \"Format to apply to the 'For' header.\",\n                            \"enum\": [\n                              \"Random\",\n                              \"RandomAndPort\",\n                              \"RandomAndRandomPort\",\n                              \"Unknown\",\n                              \"UnknownAndPort\",\n                              \"UnknownAndRandomPort\",\n                              \"Ip\",\n                              \"IpAndPort\",\n                              \"IpAndRandomPort\"\n                            ]\n                          },\n                          \"Action\": {\n                            \"type\": \"string\",\n                            \"description\": \"Action to apply to the 'Forwarded' header.\",\n                            \"enum\": [\n                              \"Set\",\n                              \"Append\",\n                              \"Remove\",\n                              \"Off\"\n                            ]\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"Forwarded\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Forwards the client cert used on the inbound connection as a header to the destination.\",\n                        \"properties\": {\n                          \"ClientCert\": {\n                            \"type\": \"string\",\n                            \"description\": \"The header name to use for the forwarded client cert.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"ClientCert\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Transform controlling whether response headers are copied from the original response.\",\n                        \"properties\": {\n                          \"ResponseHeadersCopy\": {\n                            \"description\": \"If true, copies all response headers from the destination back to the client.\",\n                            \"anyOf\": [\n                              {\n                                \"type\": \"boolean\"\n                              },\n                              {\n                                \"type\": \"string\",\n                                \"enum\": [\n                                  \"true\",\n                                  \"false\",\n                                  \"True\",\n                                  \"False\"\n                                ]\n                              }\n                            ]\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"ResponseHeadersCopy\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Transform for setting, appending, or removing a response header.\",\n                        \"properties\": {\n                          \"ResponseHeader\": {\n                            \"type\": \"string\",\n                            \"description\": \"The header name to operate on.\"\n                          },\n                          \"Set\": {\n                            \"type\": \"string\",\n                            \"description\": \"Value to set the given response header to.\"\n                          },\n                          \"Append\": {\n                            \"type\": \"string\",\n                            \"description\": \"Value to append to the given response header.\"\n                          },\n                          \"When\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"Success\",\n                              \"Always\",\n                              \"Failure\"\n                            ],\n                            \"description\": \"Specifies if the response header should be included for all, successful, or failure responses. Any response with a status code less than 400 is considered a success.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"anyOf\": [\n                          {\n                            \"required\": [\n                              \"ResponseHeader\",\n                              \"Set\"\n                            ]\n                          },\n                          {\n                            \"required\": [\n                              \"ResponseHeader\",\n                              \"Append\"\n                            ]\n                          }\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Removes the specified header from the response.\",\n                        \"properties\": {\n                          \"ResponseHeaderRemove\": {\n                            \"type\": \"string\",\n                            \"description\": \"The header name.\"\n                          },\n                          \"When\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"Success\",\n                              \"Always\",\n                              \"Failure\"\n                            ],\n                            \"description\": \"Specifies if the response header should be included for all, successful, or failure responses. Any response with a status code less than 400 is considered a success.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"ResponseHeaderRemove\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"YARP copies most response headers to the proxy response by default, this transform disables ResponseHeadersCopy and only copies the given headers.\",\n                        \"properties\": {\n                          \"ResponseHeadersAllowed\": {\n                            \"type\": \"string\",\n                            \"pattern\": \"^[a-zA-Z0-9!#$%&'*+-.^_`|~;]+$\",\n                            \"description\": \"A semicolon separated list of allowed header names.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"ResponseHeadersAllowed\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Transform controlling whether trailing response headers are copied from the original response.\",\n                        \"properties\": {\n                          \"ResponseTrailersCopy\": {\n                            \"description\": \"If true, copies all trailing response headers from the destination back to the client.\",\n                            \"anyOf\": [\n                              {\n                                \"type\": \"boolean\"\n                              },\n                              {\n                                \"type\": \"string\",\n                                \"enum\": [\n                                  \"true\",\n                                  \"false\",\n                                  \"True\",\n                                  \"False\"\n                                ]\n                              }\n                            ]\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"ResponseTrailersCopy\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Transform for setting, appending, or removing a response trailer.\",\n                        \"properties\": {\n                          \"ResponseTrailer\": {\n                            \"type\": \"string\",\n                            \"description\": \"The trailer name to operate on.\"\n                          },\n                          \"Set\": {\n                            \"type\": \"string\",\n                            \"description\": \"Value to set the given response trailer to.\"\n                          },\n                          \"Append\": {\n                            \"type\": \"string\",\n                            \"description\": \"Value to append to the given response trailer.\"\n                          },\n                          \"When\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"Success\",\n                              \"Always\",\n                              \"Failure\"\n                            ],\n                            \"description\": \"Specifies if the response trailer should be included for all, successful, or failure responses. Any response with a status code less than 400 is considered a success.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"anyOf\": [\n                          {\n                            \"required\": [\n                              \"ResponseTrailer\",\n                              \"Set\"\n                            ]\n                          },\n                          {\n                            \"required\": [\n                              \"ResponseTrailer\",\n                              \"Append\"\n                            ]\n                          }\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"Removes the specified trailer from the response.\",\n                        \"properties\": {\n                          \"ResponseTrailerRemove\": {\n                            \"type\": \"string\",\n                            \"description\": \"The trailer name.\"\n                          },\n                          \"When\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"Success\",\n                              \"Always\",\n                              \"Failure\"\n                            ],\n                            \"description\": \"Specifies if the response trailer should be removed for all, successful, or failure responses. Any response with a status code less than 400 is considered a success.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"ResponseTrailerRemove\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"description\": \"YARP copies most response trailers to the proxy response by default, this transform disables ResponseTrailersCopy and only copies the given headers.\",\n                        \"properties\": {\n                          \"ResponseTrailersAllowed\": {\n                            \"type\": \"string\",\n                            \"pattern\": \"^[a-zA-Z0-9!#$%&'*+-.^_`|~;]+$\",\n                            \"description\": \"A semicolon separated list of allowed trailer names.\"\n                          }\n                        },\n                        \"additionalProperties\": false,\n                        \"required\": [\n                          \"ResponseTrailersAllowed\"\n                        ]\n                      }\n                    ]\n                  }\n                }\n              },\n              \"required\": [\n                \"ClusterId\",\n                \"Match\"\n              ],\n              \"additionalProperties\": false\n            }\n          }\n        },\n        \"Clusters\": {\n          \"type\": \"object\",\n          \"description\": \"Named clusters describing destinations.\",\n          \"patternProperties\": {\n            \".\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"Destinations\": {\n                  \"type\": \"object\",\n                  \"description\": \"Named destinations where traffic is forwarded.\",\n                  \"patternProperties\": {\n                    \".\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"Address\": {\n                          \"type\": \"string\",\n                          \"description\": \"Destination address (must include scheme).\"\n                        },\n                        \"Health\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"description\": \"Optional override URL accepting active health check probes.\"\n                        },\n                        \"Host\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"description\": \"Optional fallback host header value used if a host is not already specified by request transforms.\"\n                        },\n                        \"Metadata\": {\n                          \"type\": [ \"object\", \"null\" ],\n                          \"description\": \"Arbitrary key-value pairs for custom destination logic.\",\n                          \"additionalProperties\": {\n                            \"type\": [ \"null\", \"number\", \"string\", \"boolean\" ]\n                          }\n                        }\n                      },\n                      \"additionalProperties\": false,\n                      \"required\": [\n                        \"Address\"\n                      ]\n                    }\n                  }\n                },\n                \"LoadBalancingPolicy\": {\n                  \"anyOf\": [\n                    {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"PowerOfTwoChoices\",\n                        \"FirstAlphabetical\",\n                        \"Random\",\n                        \"RoundRobin\",\n                        \"LeastRequests\"\n                      ]\n                    },\n                    {\n                      \"type\": [ \"string\", \"null\" ]\n                    }\n                  ],\n                  \"description\": \"Determines traffic distribution among destinations.\"\n                },\n                \"SessionAffinity\": {\n                  \"type\": [ \"object\", \"null\" ],\n                  \"description\": \"Session affinity is a mechanism to bind (affinitize) a causally related request sequence to the destination that handled the first request when the load is balanced among several destinations.\",\n                  \"properties\": {\n                    \"Enabled\": {\n                      \"anyOf\": [\n                        {\n                          \"type\": [ \"boolean\", \"null\" ]\n                        },\n                        {\n                          \"type\": \"string\",\n                          \"enum\": [\n                            \"true\",\n                            \"false\",\n                            \"True\",\n                            \"False\"\n                          ]\n                        }\n                      ]\n                    },\n                    \"Policy\": {\n                      \"anyOf\": [\n                        {\n                          \"type\": \"string\",\n                          \"enum\": [\n                            \"HashCookie\",\n                            \"ArrCookie\",\n                            \"Cookie\",\n                            \"CustomHeader\"\n                          ]\n                        },\n                        {\n                          \"type\": [ \"string\", \"null\" ]\n                        }\n                      ],\n                      \"description\": \"Determines how the session will be stored and retrieved.\"\n                    },\n                    \"FailurePolicy\": {\n                      \"anyOf\": [\n                        {\n                          \"type\": \"string\",\n                          \"enum\": [\n                            \"Redistribute\",\n                            \"Return503Error\"\n                          ]\n                        },\n                        {\n                          \"type\": [ \"string\", \"null\" ]\n                        }\n                      ],\n                      \"description\": \"Strategy for handling a missing destination for an affinitized request.\"\n                    },\n                    \"AffinityKeyName\": {\n                      \"type\": \"string\",\n                      \"description\": \"Identifies the name of the field where the affinity value is stored (cookie or header name).\"\n                    },\n                    \"Cookie\": {\n                      \"type\": [ \"object\", \"null\" ],\n                      \"properties\": {\n                        \"Domain\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"description\": \"Specifies the domain of the cookie.\"\n                        },\n                        \"Path\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"description\": \"Specifies the path of the cookie.\"\n                        },\n                        \"Expiration\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"pattern\": \"^\\\\d*?\\\\.?\\\\d\\\\d?:\\\\d\\\\d?:\\\\d\\\\d?\\\\.?\\\\d{0,7}$\",\n                          \"description\": \"Specifies the expiration of the cookie. Format: 'hh:mm:ss'.\"\n                        },\n                        \"MaxAge\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"pattern\": \"^\\\\d*?\\\\.?\\\\d\\\\d?:\\\\d\\\\d?:\\\\d\\\\d?\\\\.?\\\\d{0,7}$\",\n                          \"description\": \"Specifies the maximum age of the cookie. Format: 'hh:mm:ss'.\"\n                        },\n                        \"SecurePolicy\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"enum\": [\n                            \"Always\",\n                            \"None\",\n                            \"SameAsRequest\"\n                          ],\n                          \"description\": \"Specifies the Secure attribute of the cookie.\"\n                        },\n                        \"HttpOnly\": {\n                          \"anyOf\": [\n                            {\n                              \"type\": [ \"boolean\", \"null\" ]\n                            },\n                            {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"true\",\n                                \"false\",\n                                \"True\",\n                                \"False\"\n                              ]\n                            }\n                          ],\n                          \"description\": \"Specifies whether a cookie is accessible by client-side script.\"\n                        },\n                        \"SameSite\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"enum\": [\n                            \"Lax\",\n                            \"None\",\n                            \"Strict\",\n                            \"Unspecified\"\n                          ],\n                          \"description\": \"Specifies the SameSite attribute of the cookie.\"\n                        },\n                        \"IsEssential\": {\n                          \"anyOf\": [\n                            {\n                              \"type\": [ \"boolean\", \"null\" ]\n                            },\n                            {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"true\",\n                                \"false\",\n                                \"True\",\n                                \"False\"\n                              ]\n                            }\n                          ],\n                          \"description\": \"Specifies whether a cookie is essential for the application to function correctly. If true then consent policy checks may be bypassed.\"\n                        }\n                      },\n                      \"additionalProperties\": false\n                    }\n                  },\n                  \"additionalProperties\": false,\n                  \"required\": [\n                    \"AffinityKeyName\"\n                  ]\n                },\n                \"HealthCheck\": {\n                  \"type\": [ \"object\", \"null\" ],\n                  \"description\": \"Health check configuration for destinations.\",\n                  \"properties\": {\n                    \"Active\": {\n                      \"type\": [ \"object\", \"null\" ],\n                      \"description\": \"Active health checks are based on sending health probing requests.\",\n                      \"properties\": {\n                        \"Enabled\": {\n                          \"anyOf\": [\n                            {\n                              \"type\": [ \"boolean\", \"null\" ]\n                            },\n                            {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"true\",\n                                \"false\",\n                                \"True\",\n                                \"False\"\n                              ]\n                            }\n                          ],\n                          \"description\": \"Determines if active health checks are enabled.\",\n                          \"default\": false\n                        },\n                        \"Interval\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"pattern\": \"^\\\\d*?\\\\.?\\\\d\\\\d?:\\\\d\\\\d?:\\\\d\\\\d?\\\\.?\\\\d{0,7}$\",\n                          \"description\": \"Period of sending health probing requests. Format: 'hh:mm:ss'.\",\n                          \"default\": \"00:00:15\"\n                        },\n                        \"Timeout\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"pattern\": \"^\\\\d*?\\\\.?\\\\d\\\\d?:\\\\d\\\\d?:\\\\d\\\\d?\\\\.?\\\\d{0,7}$\",\n                          \"description\": \"Period of waiting for a health check response. Format: 'hh:mm:ss'.\",\n                          \"default\": \"00:00:10\"\n                        },\n                        \"Policy\": {\n                          \"anyOf\": [\n                            {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"ConsecutiveFailures\"\n                              ]\n                            },\n                            {\n                              \"type\": [ \"string\", \"null\" ]\n                            }\n                          ],\n                          \"description\": \"Determines the health check policy.\"\n                        },\n                        \"Path\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"description\": \"HTTP health check endpoint path.\",\n                          \"default\": \"/\"\n                        },\n                        \"Query\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"description\": \"Query string to append to the probe, including the leading '?'.\"\n                        }\n                      },\n                      \"additionalProperties\": false\n                    },\n                    \"Passive\": {\n                      \"type\": [ \"object\", \"null\" ],\n                      \"description\": \"Passive health checks are based on observing the health of the responses from the destination.\",\n                      \"properties\": {\n                        \"Enabled\": {\n                          \"anyOf\": [\n                            {\n                              \"type\": [ \"boolean\", \"null\" ]\n                            },\n                            {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"true\",\n                                \"false\",\n                                \"True\",\n                                \"False\"\n                              ]\n                            }\n                          ],\n                          \"description\": \"Determines if passive health checks are enabled.\",\n                          \"default\": false\n                        },\n                        \"Policy\": {\n                          \"anyOf\": [\n                            {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"TransportFailureRate\"\n                              ]\n                            },\n                            {\n                              \"type\": [ \"string\", \"null\" ]\n                            }\n                          ],\n                          \"description\": \"Determines the health check policy.\"\n                        },\n                        \"ReactivationPeriod\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"pattern\": \"^\\\\d*?\\\\.?\\\\d\\\\d?:\\\\d\\\\d?:\\\\d\\\\d?\\\\.?\\\\d{0,7}$\",\n                          \"description\": \"Period after which an unhealthy destination reverts back to an Unknown health state. Format: 'hh:mm:ss'.\"\n                        }\n                      },\n                      \"additionalProperties\": false\n                    },\n                    \"AvailableDestinationsPolicy\": {\n                      \"anyOf\": [\n                        {\n                          \"type\": \"string\",\n                          \"enum\": [\n                            \"HealthyAndUnknown\",\n                            \"HealthyOrPanic\"\n                          ]\n                        },\n                        {\n                          \"type\": [ \"string\", \"null\" ]\n                        }\n                      ]\n                    }\n                  },\n                  \"additionalProperties\": false\n                },\n                \"HttpClient\": {\n                  \"type\": [ \"object\", \"null\" ],\n                  \"description\": \"Configuration for outbound HTTP connections.\",\n                  \"properties\": {\n                    \"SslProtocols\": {\n                      \"type\": \"array\",\n                      \"description\": \"Specifies the SSL protocols to use.\",\n                      \"items\": {\n                        \"type\": \"string\"\n                      }\n                    },\n                    \"DangerousAcceptAnyServerCertificate\": {\n                      \"anyOf\": [\n                        {\n                          \"type\": [ \"boolean\", \"null\" ]\n                        },\n                        {\n                          \"type\": \"string\",\n                          \"enum\": [\n                            \"true\",\n                            \"false\",\n                            \"True\",\n                            \"False\"\n                          ]\n                        }\n                      ],\n                      \"description\": \"Determines whether the server's SSL certificate validity is checked by the client. Setting it to true completely disables validation.\",\n                      \"default\": false\n                    },\n                    \"MaxConnectionsPerServer\": {\n                      \"type\": [ \"number\", \"null\" ],\n                      \"description\": \"Specifies the maximum number of connections per server.\"\n                    },\n                    \"EnableMultipleHttp2Connections\": {\n                      \"anyOf\": [\n                        {\n                          \"type\": [ \"boolean\", \"null\" ]\n                        },\n                        {\n                          \"type\": \"string\",\n                          \"enum\": [\n                            \"true\",\n                            \"false\",\n                            \"True\",\n                            \"False\"\n                          ]\n                        }\n                      ],\n                      \"description\": \"Determines if multiple HTTP/2 connections are enabled.\",\n                      \"default\": true\n                    },\n                    \"RequestHeaderEncoding\": {\n                      \"type\": [ \"string\", \"null\" ],\n                      \"description\": \"Specifies the encoding of request headers, e.g. 'utf-8'.\"\n                    },\n                    \"ResponseHeaderEncoding\": {\n                      \"type\": [ \"string\", \"null\" ],\n                      \"description\": \"Specifies the encoding of response headers, e.g. 'utf-8'.\"\n                    },\n                    \"WebProxy\": {\n                      \"type\": [ \"object\", \"null\" ],\n                      \"description\": \"Config used to construct a System.Net.WebProxy instance used for outgoing requests.\",\n                      \"properties\": {\n                        \"Address\": {\n                          \"type\": [ \"string\", \"null\" ],\n                          \"description\": \"The URI of the proxy server.\"\n                        },\n                        \"BypassOnLocal\": {\n                          \"anyOf\": [\n                            {\n                              \"type\": [ \"boolean\", \"null\" ]\n                            },\n                            {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"true\",\n                                \"false\",\n                                \"True\",\n                                \"False\"\n                              ]\n                            }\n                          ],\n                          \"description\": \"If true, bypasses the proxy for local addresses.\",\n                          \"default\": false\n                        },\n                        \"UseDefaultCredentials\": {\n                          \"anyOf\": [\n                            {\n                              \"type\": [ \"boolean\", \"null\" ]\n                            },\n                            {\n                              \"type\": \"string\",\n                              \"enum\": [\n                                \"true\",\n                                \"false\",\n                                \"True\",\n                                \"False\"\n                              ]\n                            }\n                          ],\n                          \"description\": \"If true, sends CredentialCache.DefaultCredentials with requests.\",\n                          \"default\": false\n                        }\n                      },\n                      \"additionalProperties\": false\n                    }\n                  },\n                  \"additionalProperties\": false\n                },\n                \"HttpRequest\": {\n                  \"type\": [ \"object\", \"null\" ],\n                  \"description\": \"Options controlling requests sent to destinations.\",\n                  \"properties\": {\n                    \"ActivityTimeout\": {\n                      \"type\": [ \"string\", \"null\" ],\n                      \"pattern\": \"^\\\\d*?\\\\.?\\\\d\\\\d?:\\\\d\\\\d?:\\\\d\\\\d?\\\\.?\\\\d{0,7}$\",\n                      \"description\": \"Specifies how long a request is allowed to remain idle between any operation completing, after which it will be canceled. Format: 'hh:mm:ss'.\",\n                      \"default\": \"00:01:40\"\n                    },\n                    \"Version\": {\n                      \"type\": [ \"string\", \"null\" ],\n                      \"description\": \"Preferred version of the outgoing request.\",\n                      \"default\": \"2.0\"\n                    },\n                    \"VersionPolicy\": {\n                      \"type\": [ \"string\", \"null\" ],\n                      \"description\": \"The policy applied to version selection, e.g. whether to prefer downgrades, upgrades or request an exact version.\",\n                      \"default\": \"RequestVersionOrLower\",\n                      \"enum\": [\n                        \"RequestVersionExact\",\n                        \"RequestVersionOrLower\",\n                        \"RequestVersionOrHigher\"\n                      ]\n                    },\n                    \"AllowResponseBuffering\": {\n                      \"anyOf\": [\n                        {\n                          \"type\": [ \"boolean\", \"null\" ]\n                        },\n                        {\n                          \"type\": \"string\",\n                          \"enum\": [\n                            \"true\",\n                            \"false\",\n                            \"True\",\n                            \"False\"\n                          ]\n                        }\n                      ],\n                      \"description\": \"Determines if response buffering is allowed.\"\n                    }\n                  },\n                  \"additionalProperties\": false\n                },\n                \"Metadata\": {\n                  \"type\": [ \"object\", \"null\" ],\n                  \"description\": \"Arbitrary key-value pairs for custom cluster logic.\",\n                  \"additionalProperties\": {\n                    \"type\": [ \"null\", \"number\", \"string\", \"boolean\" ]\n                  }\n                }\n              },\n              \"additionalProperties\": false\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Delegation/AppBuilderDelegationExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.AspNetCore.Hosting.Server;\nusing Microsoft.AspNetCore.Server.HttpSys;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.ReverseProxy.Delegation;\n\nnamespace Microsoft.AspNetCore.Builder;\n\n/// <summary>\n/// Extensions for adding delegation middleware to the pipeline.\n/// </summary>\npublic static class AppBuilderDelegationExtensions\n{\n    /// <summary>\n    /// Adds middleware to check if the selected destination should use Http.sys delegation.\n    /// If so, the request is delegated to the destination queue instead of being proxied over HTTP.\n    /// This should be placed after load balancing and passive health checks.\n    /// </summary>\n    /// <remarks>\n    /// This middleware only works with the ASP.NET Core Http.sys server implementation.\n    /// </remarks>\n    public static IReverseProxyApplicationBuilder UseHttpSysDelegation(this IReverseProxyApplicationBuilder builder)\n    {\n        // IServerDelegationFeature isn't added to DI https://github.com/dotnet/aspnetcore/issues/40043\n        _ = builder.ApplicationServices.GetRequiredService<IServer>().Features?.Get<IServerDelegationFeature>()\n            ?? throw new NotSupportedException($\"{typeof(IHttpSysRequestDelegationFeature).FullName} is not available. Http.sys delegation is only supported when using the Http.sys server\");\n\n        builder.UseMiddleware<HttpSysDelegatorMiddleware>();\n        return builder;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Delegation/DelegationExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Delegation;\n\ninternal static class DelegationExtensions\n{\n    public const string HttpSysDelegationQueueMetadataKey = \"HttpSysDelegationQueue\";\n\n    public static string? GetHttpSysDelegationQueue(this DestinationState? destination)\n    {\n        return destination?.Model?.Config?.Metadata?.TryGetValue(HttpSysDelegationQueueMetadataKey, out var name) ?? false\n            ? name\n            : null;\n    }\n\n    public static bool ShouldUseHttpSysDelegation(this DestinationState destination)\n    {\n        return destination.GetHttpSysDelegationQueue() is not null;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Delegation/DummyHttpSysDelegator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Delegation;\n\n// Only used as part of a workaround for https://github.com/dotnet/aspnetcore/issues/59166.\ninternal sealed class DummyHttpSysDelegator : IHttpSysDelegator\n{\n    public void ResetQueue(string queueName, string urlPrefix) { }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Delegation/HttpSysDelegator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Concurrent;\nusing System.Data;\nusing System.Diagnostics;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Linq;\nusing System.Runtime.CompilerServices;\nusing Microsoft.AspNetCore.Hosting.Server;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Server.HttpSys;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Delegation;\n\ninternal sealed class HttpSysDelegator : IHttpSysDelegator, IClusterChangeListener\n{\n    private const int ERROR_OBJECT_NO_LONGER_EXISTS = 0x1A97;\n\n    private readonly IServerDelegationFeature? _serverDelegationFeature;\n    private readonly ILogger<HttpSysDelegator> _logger;\n    private readonly ConcurrentDictionary<QueueKey, WeakReference<DelegationQueue>> _queues;\n    private readonly ConditionalWeakTable<DestinationState, DelegationQueue> _queuesPerDestination;\n\n    public HttpSysDelegator(\n            IServer server,\n            ILogger<HttpSysDelegator> logger)\n    {\n        ArgumentNullException.ThrowIfNull(logger);\n        _logger = logger;\n\n        // IServerDelegationFeature isn't added to DI https://github.com/dotnet/aspnetcore/issues/40043\n        // IServerDelegationFeature may not be set if not http.sys server or the OS doesn't support delegation\n        _serverDelegationFeature = server.Features?.Get<IServerDelegationFeature>();\n\n        _queues = new ConcurrentDictionary<QueueKey, WeakReference<DelegationQueue>>();\n        _queuesPerDestination = new ConditionalWeakTable<DestinationState, DelegationQueue>();\n    }\n\n    public void ResetQueue(string queueName, string urlPrefix)\n    {\n        if (_serverDelegationFeature is not null)\n        {\n            var key = new QueueKey(queueName, urlPrefix);\n            if (_queues.TryGetValue(key, out var queueWeakRef) && queueWeakRef.TryGetTarget(out var queue))\n            {\n                var detachedQueueState = queue.Detach();\n                Log.QueueReset(_logger, queueName, urlPrefix, detachedQueueState);\n            }\n        }\n    }\n\n    public void DelegateRequest(HttpContext context, DestinationState destination)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n        ArgumentNullException.ThrowIfNull(destination);\n\n        var requestDelegationFeature = context.Features.Get<IHttpSysRequestDelegationFeature>()\n                    ?? throw new InvalidOperationException($\"{typeof(IHttpSysRequestDelegationFeature).FullName} is missing.\");\n\n        if (!requestDelegationFeature.CanDelegate)\n        {\n            throw new InvalidOperationException(\n                \"Current request can't be delegated. Either the request body has started to be read or the response has started to be sent.\");\n        }\n\n        if (_serverDelegationFeature is null || !_queuesPerDestination.TryGetValue(destination, out var queue))\n        {\n            Log.QueueNotFound(_logger, destination);\n            context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;\n            context.Features.Set<IForwarderErrorFeature>(new ForwarderErrorFeature(ForwarderError.NoAvailableDestinations, ex: null));\n            return;\n        }\n\n        Delegate(context, destination, _serverDelegationFeature, requestDelegationFeature, queue, _logger, shouldRetry: true);\n\n        static void Delegate(\n            HttpContext context,\n            DestinationState destination,\n            IServerDelegationFeature serverDelegationFeature,\n            IHttpSysRequestDelegationFeature requestDelegationFeature,\n            DelegationQueue queue,\n            ILogger logger,\n            bool shouldRetry)\n        {\n            // Opportunistically retry initialization if it failed previously.\n            // This helps when the target queue wasn't yet created because\n            // the target process hadn't yet started up.\n            var queueState = queue.Initialize(serverDelegationFeature);\n            if (!queueState.IsInitialized)\n            {\n                Log.QueueNotInitialized(logger, destination, queueState, queueState.InitializationException);\n                context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;\n                context.Features.Set<IForwarderErrorFeature>(new ForwarderErrorFeature(ForwarderError.NoAvailableDestinations, queueState.InitializationException));\n                return;\n            }\n\n            try\n            {\n                Log.DelegatingRequest(logger, destination, queueState);\n                requestDelegationFeature.DelegateRequest(queueState.Rule);\n            }\n            catch (ObjectDisposedException) when (shouldRetry)\n            {\n                Log.QueueDisposed(logger, destination.GetHttpSysDelegationQueue(), destination.Model?.Config?.Address);\n\n                // Another thread detached/disposed the queue\n                // Attempt to delegate one more time which will to try re-initialize the queue\n                Delegate(context, destination, serverDelegationFeature, requestDelegationFeature, queue, logger, shouldRetry: false);\n            }\n            catch (HttpSysException ex) when (shouldRetry && ex.ErrorCode == ERROR_OBJECT_NO_LONGER_EXISTS)\n            {\n                Log.QueueNoLongerExists(logger, destination.GetHttpSysDelegationQueue(), destination.Model?.Config?.Address, queueState, ex);\n\n                // The target queue is gone. Detach from it so that we can try to re-attach.\n                queue.Detach(queueState);\n\n                // Attempt to delegate one more time which will try re-initialize the queue\n                Delegate(context, destination, serverDelegationFeature, requestDelegationFeature, queue, logger, shouldRetry: false);\n            }\n            catch (Exception ex)\n            {\n                Log.DelegationFailed(logger, destination, queueState, ex);\n                context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;\n                context.Features.Set<IForwarderErrorFeature>(new ForwarderErrorFeature(ForwarderError.Request, ex));\n            }\n        }\n    }\n\n    void IClusterChangeListener.OnClusterAdded(ClusterState cluster)\n    {\n        AddOrUpdateRules(cluster);\n    }\n\n    void IClusterChangeListener.OnClusterChanged(ClusterState cluster)\n    {\n        AddOrUpdateRules(cluster);\n        RemoveDeadQueueReferences();\n    }\n\n    void IClusterChangeListener.OnClusterRemoved(ClusterState cluster)\n    {\n        RemoveDeadQueueReferences();\n    }\n\n    private void AddOrUpdateRules(ClusterState cluster)\n    {\n        if (_serverDelegationFeature is null)\n        {\n            return;\n        }\n\n        // We support multiple destinations referencing the same queue but http.sys only\n        // allows us to create one handle to the queue, so we keep track of queues two ways.\n        // 1. We map destination => queue using a ConditionalWeakTable\n        //    This allows us to find the queue to delegate to when processing a request\n        // 2. We map (queue name + url prefix) => queue using a WeakReference\n        //    This allows us to find the already created queue if more than one destination points to the same queue.\n        //    It also allows us to find the queue by name/url prefix to support reset.\n        //\n        // Using weak references means we ensure the queue exist as long as the referencing destinations exist.\n        // Once all the destinations are gone, GC will eventually finalize the underlying SafeHandle to the http.sys\n        // queue, which will clean up references in http.sys, allowing us to re-create it again later if needed.\n        foreach (var destination in cluster.Destinations.Select(kvp => kvp.Value))\n        {\n            var queueName = destination.GetHttpSysDelegationQueue();\n            var urlPrefix = destination.Model?.Config?.Address;\n            if (queueName is not null && urlPrefix is not null)\n            {\n                var queueKey = new QueueKey(queueName, urlPrefix);\n                if (!_queuesPerDestination.TryGetValue(destination, out var queue) || !queue.Equals(queueKey))\n                {\n                    var queueWeakRef = _queues.GetOrAdd(queueKey, key => new WeakReference<DelegationQueue>(new DelegationQueue(key.QueueName, key.UrlPrefix)));\n                    if (!queueWeakRef.TryGetTarget(out queue))\n                    {\n                        // The queue was GC'd since it was originally created\n                        lock (queueWeakRef)\n                        {\n                            if (!queueWeakRef.TryGetTarget(out queue))\n                            {\n                                queue = new DelegationQueue(queueName, urlPrefix);\n                                queueWeakRef.SetTarget(queue);\n                            }\n                        }\n                    }\n\n                    var queueState = queue.Initialize(_serverDelegationFeature);\n                    if (!queueState.IsInitialized)\n                    {\n                        Log.QueueInitFailed(\n                                _logger,\n                                destination.DestinationId,\n                                queueName,\n                                urlPrefix,\n                                queueState.InitializationException);\n                    }\n\n                    _queuesPerDestination.AddOrUpdate(destination, queue);\n                }\n            }\n            else\n            {\n                // Handles the case a destination switches from delegation to proxy\n                _queuesPerDestination.Remove(destination);\n            }\n        }\n    }\n\n    private void RemoveDeadQueueReferences()\n    {\n        foreach (var kvp in _queues)\n        {\n            if (!kvp.Value.TryGetTarget(out _))\n            {\n                _queues.TryRemove(kvp);\n            }\n        }\n    }\n\n    private sealed class DelegationQueue\n    {\n        public const uint ERROR_FILE_NOT_FOUND = 2;\n\n        private readonly QueueKey _queueKey;\n        private readonly object _syncRoot;\n        private DelegationQueueState _currentState;\n\n        public DelegationQueue(string queueName, string urlPrefix)\n        {\n            _queueKey = new QueueKey(queueName, urlPrefix);\n            _syncRoot = new object();\n            _currentState = new DelegationQueueState();\n        }\n\n        public DelegationQueueState Initialize(IServerDelegationFeature delegationFeature)\n        {\n            var state = _currentState;\n            if (!state.IsInitialized && ShouldRetryInitialization(state.InitializationException))\n            {\n                lock (_syncRoot)\n                {\n                    state = _currentState;\n                    if (!state.IsInitialized && ShouldRetryInitialization(state.InitializationException))\n                    {\n                        try\n                        {\n                            state = new DelegationQueueState(delegationFeature.CreateDelegationRule(_queueKey.QueueName, _queueKey.UrlPrefix));\n                        }\n                        catch (Exception ex)\n                        {\n                            state = new DelegationQueueState(ex);\n                        }\n\n                        _currentState = state;\n                    }\n                }\n            }\n\n            return state;\n        }\n\n        public DelegationQueueState? Detach(DelegationQueueState? state = null)\n        {\n            if (state == null || state == _currentState)\n            {\n                lock (_syncRoot)\n                {\n                    if (state == null || state == _currentState)\n                    {\n                        _currentState.Rule?.Dispose();\n\n                        var oldState = _currentState;\n                        _currentState = new DelegationQueueState();\n\n                        return oldState;\n                    }\n                }\n            }\n\n            return null;\n        }\n\n        public bool Equals(QueueKey queueKey)\n        {\n            return _queueKey.Equals(queueKey);\n        }\n\n        private static bool ShouldRetryInitialization(Exception? exception)\n        {\n            return exception switch\n            {\n                null => true,\n                HttpSysException httpSysEx when httpSysEx.ErrorCode == ERROR_FILE_NOT_FOUND => true,\n                _ => false,\n            };\n        }\n    }\n\n    private sealed class DelegationQueueState\n    {\n        public DelegationQueueState()\n        {\n            IsInitialized = false;\n        }\n\n        public DelegationQueueState(DelegationRule rule)\n        {\n            IsInitialized = true;\n            Rule = rule;\n        }\n\n        public DelegationQueueState(Exception ex)\n        {\n            IsInitialized = false;\n            InitializationException = ex;\n        }\n\n        [MemberNotNullWhen(true, nameof(Rule))]\n        public bool IsInitialized { get; }\n\n        public DelegationRule? Rule { get; }\n\n        public Exception? InitializationException { get; }\n\n        public string Id { get; } = Activity.Current switch\n        {\n            { IdFormat: ActivityIdFormat.W3C } => Activity.Current.SpanId.ToHexString(),\n            { Id: not null } => Activity.Current.Id,\n            _ => ActivitySpanId.CreateRandom().ToHexString(),\n        };\n    }\n\n    private readonly struct QueueKey : IEquatable<QueueKey>\n    {\n        private readonly int _hashCode;\n\n        public QueueKey(string queueName, string urlPrefix)\n        {\n            QueueName = queueName;\n            UrlPrefix = urlPrefix;\n            _hashCode = HashCode.Combine(\n                StringComparer.OrdinalIgnoreCase.GetHashCode(queueName),\n                StringComparer.OrdinalIgnoreCase.GetHashCode(urlPrefix));\n        }\n\n        public string QueueName { get; }\n\n        public string UrlPrefix { get; }\n\n        public bool Equals(QueueKey other)\n        {\n            return _hashCode == other._hashCode\n                && string.Equals(QueueName, other.QueueName, StringComparison.OrdinalIgnoreCase)\n                && string.Equals(UrlPrefix, other.UrlPrefix, StringComparison.OrdinalIgnoreCase);\n        }\n\n        public override bool Equals([NotNullWhen(true)] object? obj)\n        {\n            return obj is QueueKey other && Equals(other);\n        }\n\n        public override int GetHashCode() => _hashCode;\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, string, string, string, Exception?> _queueInitFailed = LoggerMessage.Define<string, string, string>(\n            LogLevel.Warning,\n            EventIds.DelegationQueueInitializationFailed,\n            \"Failed to initialize queue for destination '{destinationId}' with queue name '{queueName}' and url prefix '{urlPrefix}'.\");\n\n        private static readonly Action<ILogger, string, string?, string?, Exception?> _queueNotFound = LoggerMessage.Define<string, string?, string?>(\n            LogLevel.Warning,\n            EventIds.DelegationQueueNotFound,\n            \"Failed to get delegation queue for destination '{destinationId}' with queue name '{queueName}' and url prefix '{urlPrefix}'\");\n\n        private static readonly Action<ILogger, string, string?, string?, string, Exception?> _queueNotInitialized = LoggerMessage.Define<string, string?, string?, string>(\n            LogLevel.Information,\n            EventIds.DelegationQueueNotInitialized,\n            \"Delegation queue not initialized for destination '{destinationId}' with queue '{queueName}' and url prefix '{urlPrefix}'. Current state id '{stateId}'\");\n\n        private static readonly Action<ILogger, string?, string?, string?, Exception?> _queueReset = LoggerMessage.Define<string?, string?, string?>(\n            LogLevel.Information,\n            EventIds.DelegationQueueReset,\n            \"Detached from queue with name '{queueName}' and url prefix '{urlPrefix}'. Detached queue state id '{stateId}'\");\n\n        private static readonly Action<ILogger, string?, string?, string, Exception?> _queueNoLongerExists = LoggerMessage.Define<string?, string?, string>(\n            LogLevel.Debug,\n            EventIds.DelegationQueueNoLongerExists,\n            \"Destination queue with name '{queueName}' and url prefix '{urlPrefix}' no longer exists. Detaching and attempting to re-initialize. Current state id '{stateId}'\");\n\n        private static readonly Action<ILogger, string?, string?, Exception?> _queueDisposed = LoggerMessage.Define<string?, string?>(\n            LogLevel.Debug,\n            EventIds.DelegationQueueDisposed,\n            \"Destination queue with name '{queueName}' and url prefix '{urlPrefix}' was disposed. Attempting to re-initialize.\");\n\n        private static readonly Action<ILogger, string, string?, string?, string, Exception?> _delegatingRequest = LoggerMessage.Define<string, string?, string?, string>(\n            LogLevel.Information,\n            EventIds.DelegatingRequest,\n            \"Delegating to destination '{destinationId}' with queue '{queueName}' and url prefix '{urlPrefix}'. Current state id '{stateId}'\");\n\n        private static readonly Action<ILogger, string, string?, string?, string, Exception?> _delegationFailed = LoggerMessage.Define<string, string?, string?, string>(\n            LogLevel.Error,\n            EventIds.DelegationFailed,\n            \"Failed to delegate request for destination '{destinationId}' with queue name '{queueName}' and url prefix '{urlPrefix}'. Current state id '{stateId}'\");\n\n        public static void QueueInitFailed(ILogger logger, string destinationId, string queueName, string urlPrefix, Exception? ex)\n        {\n            _queueInitFailed(logger, destinationId, queueName, urlPrefix, ex);\n        }\n\n        public static void QueueNotFound(ILogger logger, DestinationState destination)\n        {\n            _queueNotFound(logger, destination.DestinationId, destination.GetHttpSysDelegationQueue(), destination.Model?.Config?.Address, null);\n        }\n\n        public static void QueueNotInitialized(ILogger logger, DestinationState destination, DelegationQueueState queueState, Exception? ex)\n        {\n            _queueNotInitialized(logger, destination.DestinationId, destination.GetHttpSysDelegationQueue(), destination.Model?.Config?.Address, queueState.Id, ex);\n        }\n\n        public static void QueueReset(ILogger logger, string queueName, string urlPrefix, DelegationQueueState? detachedQueueState)\n        {\n            _queueReset(logger, queueName, urlPrefix, detachedQueueState?.Id, null);\n        }\n\n        public static void QueueNoLongerExists(ILogger logger, string? queueName, string? urlPrefix, DelegationQueueState queueState, Exception? ex)\n        {\n            _queueNoLongerExists(logger, queueName, urlPrefix, queueState.Id, ex);\n        }\n\n        public static void QueueDisposed(ILogger logger, string? queueName, string? urlPrefix)\n        {\n            _queueDisposed(logger, queueName, urlPrefix, null);\n        }\n\n        public static void DelegatingRequest(ILogger logger, DestinationState destination, DelegationQueueState queueState)\n        {\n            _delegatingRequest(logger, destination.DestinationId, destination.GetHttpSysDelegationQueue(), destination.Model?.Config?.Address, queueState.Id, null);\n        }\n\n        public static void DelegationFailed(ILogger logger, DestinationState destination, DelegationQueueState queueState, Exception ex)\n        {\n            _delegationFailed(logger, destination.DestinationId, destination.GetHttpSysDelegationQueue(), destination.Model?.Config?.Address, queueState.Id, ex);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Delegation/HttpSysDelegatorMiddleware.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Delegation;\n\ninternal sealed class HttpSysDelegatorMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly ILogger<HttpSysDelegatorMiddleware> _logger;\n    private readonly HttpSysDelegator _delegator;\n    private readonly IRandomFactory _randomFactory;\n\n    public HttpSysDelegatorMiddleware(\n        RequestDelegate next,\n        ILogger<HttpSysDelegatorMiddleware> logger,\n        HttpSysDelegator delegator,\n        IRandomFactory randomFactory)\n    {\n        ArgumentNullException.ThrowIfNull(next);\n        ArgumentNullException.ThrowIfNull(logger);\n        ArgumentNullException.ThrowIfNull(delegator);\n        ArgumentNullException.ThrowIfNull(randomFactory);\n        _next = next;\n        _logger = logger;\n        _delegator = delegator;\n        _randomFactory = randomFactory;\n    }\n\n    public Task Invoke(HttpContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        var reverseProxyFeature = context.GetReverseProxyFeature();\n        var destinations = reverseProxyFeature.AvailableDestinations\n            ?? throw new InvalidOperationException($\"The {nameof(IReverseProxyFeature)} Destinations collection was not set.\");\n        var cluster = reverseProxyFeature.Cluster\n            ?? throw new InvalidOperationException($\"The {nameof(IReverseProxyFeature)} Cluster was not set.\");\n\n        if (destinations.Any())\n        {\n            // This logic mimics behavior in ForwarderMiddleware, except we save the chosen destination back\n            // to the proxy feature to ensure a delegation destination doesn't slip past this middleware.\n            var destination = destinations[0];\n            if (destinations.Count > 1)\n            {\n                var random = _randomFactory.CreateRandomInstance();\n                Log.MultipleDestinationsAvailable(_logger, reverseProxyFeature.Cluster.Config.ClusterId);\n                destination = destinations[random.Next(destinations.Count)];\n                reverseProxyFeature.AvailableDestinations = destination;\n            }\n\n            if (destination.ShouldUseHttpSysDelegation())\n            {\n                reverseProxyFeature.ProxiedDestination = destination;\n\n                _delegator.DelegateRequest(context, destination);\n\n                return Task.CompletedTask;\n            }\n        }\n\n        return _next(context);\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, string, Exception?> _multipleDestinationsAvailable = LoggerMessage.Define<string>(\n            LogLevel.Warning,\n            EventIds.MultipleDestinationsAvailable,\n            \"More than one destination available for cluster '{clusterId}', load balancing may not be configured correctly. Choosing randomly.\");\n\n        public static void MultipleDestinationsAvailable(ILogger logger, string clusterId)\n        {\n            _multipleDestinationsAvailable(logger, clusterId, null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Delegation/IHttpSysDelegator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Delegation;\n\npublic interface IHttpSysDelegator\n{\n    /// <summary>\n    /// Disposes the handle to the given queue if it exists.\n    /// </summary>\n    /// <remarks>\n    /// If any destinations still reference the queue, the handle will be\n    /// re-created the next time a request is routed to one of the destinations.\n    /// </remarks>\n    /// <param name=\"queueName\">The name of the queue to reset.</param>\n    /// <param name=\"urlPrefix\">The url prefix of the queue to reset.</param>\n    void ResetQueue(string queueName, string urlPrefix);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/CallbackHttpClientFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\ninternal sealed class CallbackHttpClientFactory : ForwarderHttpClientFactory\n{\n    private readonly Action<ForwarderHttpClientContext, SocketsHttpHandler> _configureClient;\n\n    internal CallbackHttpClientFactory(ILogger<ForwarderHttpClientFactory> logger,\n        Action<ForwarderHttpClientContext, SocketsHttpHandler> configureClient) : base(logger)\n    {\n        ArgumentNullException.ThrowIfNull(configureClient);\n        _configureClient = configureClient;\n    }\n\n    protected override void ConfigureHandler(ForwarderHttpClientContext context, SocketsHttpHandler handler)\n    {\n        base.ConfigureHandler(context, handler);\n        _configureClient(context, handler);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/DirectForwardingHttpClientProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\ninternal sealed class DirectForwardingHttpClientProvider\n{\n    public HttpMessageInvoker HttpClient { get; }\n\n    public DirectForwardingHttpClientProvider() : this(new ForwarderHttpClientFactory()) { }\n\n    public DirectForwardingHttpClientProvider(IForwarderHttpClientFactory factory)\n    {\n        HttpClient = factory.CreateClient(new ForwarderHttpClientContext\n        {\n            NewConfig = HttpClientConfig.Empty\n        });\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/EmptyHttpContent.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\ninternal sealed class EmptyHttpContent : HttpContent\n{\n    protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => Task.CompletedTask;\n\n    protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) => Task.CompletedTask;\n\n    protected override bool TryComputeLength(out long length)\n    {\n        length = 0;\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/ForwarderError.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// Errors reported when forwarding a request to the destination.\n/// </summary>\npublic enum ForwarderError : int\n{\n    /// <summary>\n    /// No error.\n    /// </summary>\n    None,\n\n    /// <summary>\n    /// Failed to connect, send the request headers, or receive the response headers.\n    /// </summary>\n    Request,\n\n    /// <summary>\n    /// Timed out when trying to connect, send the request headers, or receive the response headers.\n    /// </summary>\n    RequestTimedOut,\n\n    /// <summary>\n    /// Canceled when trying to connect, send the request headers, or receive the response headers.\n    /// </summary>\n    RequestCanceled,\n\n    /// <summary>\n    /// Canceled while copying the request body.\n    /// </summary>\n    RequestBodyCanceled,\n\n    /// <summary>\n    /// Failed reading the request body from the client.\n    /// </summary>\n    RequestBodyClient,\n\n    /// <summary>\n    /// Failed writing the request body to the destination.\n    /// </summary>\n    RequestBodyDestination,\n\n    /// <summary>\n    /// Failed to copy response headers.\n    /// </summary>\n    ResponseHeaders,\n\n    /// <summary>\n    /// Canceled while copying the response body.\n    /// </summary>\n    ResponseBodyCanceled,\n\n    /// <summary>\n    /// Failed when writing response body to the client.\n    /// </summary>\n    ResponseBodyClient,\n\n    /// <summary>\n    /// Failed when reading response body from the destination.\n    /// </summary>\n    ResponseBodyDestination,\n\n    /// <summary>\n    /// Canceled while copying the upgraded response body.\n    /// </summary>\n    UpgradeRequestCanceled,\n\n    /// <summary>\n    /// Failed reading the upgraded request body from the client.\n    /// </summary>\n    UpgradeRequestClient,\n\n    /// <summary>\n    /// Failed writing the upgraded request body to the destination.\n    /// </summary>\n    UpgradeRequestDestination,\n\n    /// <summary>\n    /// Canceled while copying the upgraded response body.\n    /// </summary>\n    UpgradeResponseCanceled,\n\n    /// <summary>\n    /// Failed when writing the upgraded response body to the client.\n    /// </summary>\n    UpgradeResponseClient,\n\n    /// <summary>\n    /// Failed when reading the upgraded response body from the destination.\n    /// </summary>\n    UpgradeResponseDestination,\n\n    /// <summary>\n    /// Indicates there were no destinations remaining to proxy the request to.\n    /// The configured destinations may have been excluded due to heath or other considerations.\n    /// </summary>\n    NoAvailableDestinations,\n\n    /// <summary>\n    /// Failed while creating the request message.\n    /// </summary>\n    RequestCreation,\n\n    /// <summary>\n    /// An upgraded request was idle and canceled due to the activity timeout.\n    /// </summary>\n    UpgradeActivityTimeout,\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/ForwarderErrorFeature.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\ninternal sealed class ForwarderErrorFeature : IForwarderErrorFeature\n{\n    internal ForwarderErrorFeature(ForwarderError error, Exception? ex)\n    {\n        Error = error;\n        Exception = ex;\n    }\n\n    /// <summary>\n    /// The specified ForwarderError.\n    /// </summary>\n    public ForwarderError Error { get; }\n\n    /// <summary>\n    /// The error, if any.\n    /// </summary>\n    public Exception? Exception { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/ForwarderHttpClientContext.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// Contains the old and the new HTTP client configurations.\n/// </summary>\npublic class ForwarderHttpClientContext\n{\n    /// <summary>\n    /// Id of a <see cref=\"ClusterModel\"/> HTTP client belongs to.\n    /// </summary>\n    public string ClusterId { get; init; } = default!;\n\n    /// <summary>\n    /// Old <see cref=\"HttpClientConfig\"/> instance\n    /// from which the <see cref=\"OldClient\"/> was created.\n    /// Can be empty if a client is getting constructed for the first time.\n    /// </summary>\n    public HttpClientConfig OldConfig { get; init; } = default!;\n\n    /// <summary>\n    /// Old metadata instance from which the <see cref=\"OldClient\"/> was created, if any.\n    /// </summary>\n    public IReadOnlyDictionary<string, string>? OldMetadata { get; init; }\n\n    /// <summary>\n    /// Old <see cref=\"HttpMessageInvoker\"/> instance.\n    /// Can be null if a client is getting constructed for the first time.\n    /// </summary>\n    public HttpMessageInvoker? OldClient { get; init; }\n\n    /// <summary>\n    /// New <see cref=\"HttpClientConfig\"/> instance\n    /// specifying the settings for a new client.\n    /// </summary>\n    public HttpClientConfig NewConfig { get; init; } = default!;\n\n    /// <summary>\n    /// New metadata instance used for a new client construction, if any.\n    /// </summary>\n    public IReadOnlyDictionary<string, string>? NewMetadata { get; init; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/ForwarderHttpClientFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Net;\nusing System.Net.Http;\nusing System.Text;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// Default implementation of <see cref=\"IForwarderHttpClientFactory\"/>.\n/// </summary>\npublic class ForwarderHttpClientFactory : IForwarderHttpClientFactory\n{\n    private readonly ILogger<ForwarderHttpClientFactory> _logger;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"ForwarderHttpClientFactory\"/> class.\n    /// </summary>\n    public ForwarderHttpClientFactory() : this(NullLogger<ForwarderHttpClientFactory>.Instance) { }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"ForwarderHttpClientFactory\"/> class.\n    /// </summary>\n    public ForwarderHttpClientFactory(ILogger<ForwarderHttpClientFactory> logger)\n    {\n        ArgumentNullException.ThrowIfNull(logger);\n        _logger = logger;\n    }\n\n    /// <inheritdoc/>\n    public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context)\n    {\n        if (CanReuseOldClient(context))\n        {\n            Log.ClientReused(_logger, context.ClusterId);\n            return context.OldClient!;\n        }\n\n        var handler = new SocketsHttpHandler\n        {\n            UseProxy = false,\n            AllowAutoRedirect = false,\n            AutomaticDecompression = DecompressionMethods.None,\n            UseCookies = false,\n            EnableMultipleHttp2Connections = true,\n            ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current),\n            ConnectTimeout = TimeSpan.FromSeconds(15),\n\n            // NOTE: MaxResponseHeadersLength = 64, which means up to 64 KB of headers are allowed by default as of .NET Core 3.1.\n        };\n\n        ConfigureHandler(context, handler);\n\n        var middleware = WrapHandler(context, handler);\n\n        Log.ClientCreated(_logger, context.ClusterId);\n\n        return new HttpMessageInvoker(middleware, disposeHandler: true);\n    }\n\n    /// <summary>\n    /// Checks if the options have changed since the old client was created. If not then the\n    /// old client will be re-used. Re-use can avoid the latency of creating new connections.\n    /// </summary>\n    protected virtual bool CanReuseOldClient(ForwarderHttpClientContext context)\n    {\n        return context.OldClient is not null && context.NewConfig == context.OldConfig;\n    }\n\n    /// <summary>\n    /// Allows configuring the <see cref=\"SocketsHttpHandler\"/> instance. The base implementation\n    /// applies settings from <see cref=\"ForwarderHttpClientContext.NewConfig\"/>.\n    /// <see cref=\"SocketsHttpHandler.UseProxy\"/>, <see cref=\"SocketsHttpHandler.AllowAutoRedirect\"/>,\n    /// <see cref=\"SocketsHttpHandler.AutomaticDecompression\"/>, and <see cref=\"SocketsHttpHandler.UseCookies\"/>\n    /// are disabled prior to this call.\n    /// </summary>\n    protected virtual void ConfigureHandler(ForwarderHttpClientContext context, SocketsHttpHandler handler)\n    {\n        var newConfig = context.NewConfig;\n        if (newConfig.SslProtocols.HasValue)\n        {\n            handler.SslOptions.EnabledSslProtocols = newConfig.SslProtocols.Value;\n        }\n        if (newConfig.MaxConnectionsPerServer is not null)\n        {\n            handler.MaxConnectionsPerServer = newConfig.MaxConnectionsPerServer.Value;\n        }\n        if (newConfig.DangerousAcceptAnyServerCertificate ?? false)\n        {\n#pragma warning disable CA5359 // Do Not Disable Certificate Validation -- this setting is explicitly opt-in by the user.\n            handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };\n#pragma warning restore CA5359\n        }\n\n        handler.EnableMultipleHttp2Connections = newConfig.EnableMultipleHttp2Connections.GetValueOrDefault(true);\n\n        if (newConfig.RequestHeaderEncoding is not null)\n        {\n            var encoding = Encoding.GetEncoding(newConfig.RequestHeaderEncoding);\n            handler.RequestHeaderEncodingSelector = (_, _) => encoding;\n        }\n\n        if (newConfig.ResponseHeaderEncoding is not null)\n        {\n            var encoding = Encoding.GetEncoding(newConfig.ResponseHeaderEncoding);\n            handler.ResponseHeaderEncodingSelector = (_, _) => encoding;\n        }\n\n        var webProxy = TryCreateWebProxy(newConfig.WebProxy);\n        if (webProxy is not null)\n        {\n            handler.Proxy = webProxy;\n            handler.UseProxy = true;\n        }\n    }\n\n    private static WebProxy? TryCreateWebProxy(WebProxyConfig? webProxyConfig)\n    {\n        if (webProxyConfig is null || webProxyConfig.Address is null)\n        {\n            return null;\n        }\n\n        var webProxy = new WebProxy(webProxyConfig.Address);\n\n        webProxy.UseDefaultCredentials = webProxyConfig.UseDefaultCredentials.GetValueOrDefault(false);\n        webProxy.BypassProxyOnLocal = webProxyConfig.BypassOnLocal.GetValueOrDefault(false);\n\n        return webProxy;\n    }\n\n    /// <summary>\n    /// Adds any wrapping middleware around the <see cref=\"HttpMessageHandler\"/>.\n    /// </summary>\n    protected virtual HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler)\n    {\n        return handler;\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, string, Exception?> _clientCreated = LoggerMessage.Define<string>(\n              LogLevel.Debug,\n              EventIds.ClientCreated,\n              \"New client created for cluster '{clusterId}'.\");\n\n        private static readonly Action<ILogger, string, Exception?> _clientReused = LoggerMessage.Define<string>(\n            LogLevel.Debug,\n            EventIds.ClientReused,\n            \"Existing client reused for cluster '{clusterId}'.\");\n\n        public static void ClientCreated(ILogger logger, string clusterId)\n        {\n            _clientCreated(logger, clusterId, null);\n        }\n\n        public static void ClientReused(ILogger logger, string clusterId)\n        {\n            _clientReused(logger, clusterId, null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/ForwarderMiddleware.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// Invokes the proxy at the end of the request processing pipeline.\n/// </summary>\ninternal sealed class ForwarderMiddleware\n{\n    private readonly IRandomFactory _randomFactory;\n    private readonly RequestDelegate _next; // Unused, this middleware is always terminal\n    private readonly ILogger _logger;\n    private readonly IHttpForwarder _forwarder;\n\n    public ForwarderMiddleware(RequestDelegate next, ILogger<ForwarderMiddleware> logger, IHttpForwarder forwarder, IRandomFactory randomFactory)\n    {\n        ArgumentNullException.ThrowIfNull(next);\n        ArgumentNullException.ThrowIfNull(logger);\n        ArgumentNullException.ThrowIfNull(forwarder);\n        ArgumentNullException.ThrowIfNull(randomFactory);\n        _next = next;\n        _logger = logger;\n        _forwarder = forwarder;\n        _randomFactory = randomFactory;\n    }\n\n    /// <inheritdoc/>\n    public async Task Invoke(HttpContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        var reverseProxyFeature = context.GetReverseProxyFeature();\n        var destinations = reverseProxyFeature.AvailableDestinations\n            ?? throw new InvalidOperationException($\"The {nameof(IReverseProxyFeature)} Destinations collection was not set.\");\n\n        var route = context.GetRouteModel();\n        var cluster = route.Cluster!;\n\n        var activity = context.GetYarpActivity();\n        activity?.AddTag(\"proxy.route_id\", route.Config.RouteId);\n        activity?.AddTag(\"proxy.cluster_id\", cluster.ClusterId);\n\n        if (destinations.Count == 0)\n        {\n            Log.NoAvailableDestinations(_logger, cluster.ClusterId);\n            context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;\n            context.Features.Set<IForwarderErrorFeature>(new ForwarderErrorFeature(ForwarderError.NoAvailableDestinations, ex: null));\n            activity?.SetStatus(ActivityStatusCode.Error);\n            activity?.AddError(\"Proxy forwarding failed\", \"No available destinations to forward to\");\n            return;\n        }\n\n        var destination = destinations[0];\n        if (destinations.Count > 1)\n        {\n            var random = _randomFactory.CreateRandomInstance();\n            Log.MultipleDestinationsAvailable(_logger, cluster.ClusterId);\n            destination = destinations[random.Next(destinations.Count)];\n        }\n\n        reverseProxyFeature.ProxiedDestination = destination;\n        activity?.AddTag(\"proxy.destination_id\", destination.DestinationId);\n\n        var destinationModel = destination.Model;\n        if (destinationModel is null)\n        {\n            throw new InvalidOperationException($\"Chosen destination has no model set: '{destination.DestinationId}'\");\n        }\n\n        try\n        {\n            cluster.ConcurrencyCounter.Increment();\n            destination.ConcurrencyCounter.Increment();\n            ForwarderTelemetry.Log.ForwarderInvoke(cluster.ClusterId, route.Config.RouteId, destination.DestinationId);\n\n            var clusterConfig = reverseProxyFeature.Cluster;\n            var result = await _forwarder.SendAsync(context, destinationModel.Config.Address, clusterConfig.HttpClient,\n                clusterConfig.Config.HttpRequest ?? ForwarderRequestConfig.Empty, route.Transformer);\n\n            activity?.SetStatus((result == ForwarderError.None) ? ActivityStatusCode.Ok : ActivityStatusCode.Error);\n        }\n        finally\n        {\n            destination.ConcurrencyCounter.Decrement();\n            cluster.ConcurrencyCounter.Decrement();\n        }\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, string, Exception?> _noAvailableDestinations = LoggerMessage.Define<string>(\n            LogLevel.Warning,\n            EventIds.NoAvailableDestinations,\n            \"No available destinations after load balancing for cluster '{clusterId}'.\");\n\n        private static readonly Action<ILogger, string, Exception?> _multipleDestinationsAvailable = LoggerMessage.Define<string>(\n            LogLevel.Warning,\n            EventIds.MultipleDestinationsAvailable,\n            \"More than one destination available for cluster '{clusterId}', load balancing may not be configured correctly. Choosing randomly.\");\n\n        public static void NoAvailableDestinations(ILogger logger, string clusterId)\n        {\n            _noAvailableDestinations(logger, clusterId, null);\n        }\n\n        public static void MultipleDestinationsAvailable(ILogger logger, string clusterId)\n        {\n            _multipleDestinationsAvailable(logger, clusterId, null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/ForwarderRequestConfig.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing System.Threading;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// Config for <see cref=\"IHttpForwarder.SendAsync(HttpContext, string, HttpMessageInvoker, ForwarderRequestConfig, HttpTransformer, CancellationToken)\"/>\n/// </summary>\npublic sealed record ForwarderRequestConfig\n{\n    /// <summary>\n    /// An empty instance of this type.\n    /// </summary>\n    public static ForwarderRequestConfig Empty { get; } = new();\n\n    /// <summary>\n    /// How long a request is allowed to remain idle between any operation completing, after which it will be canceled.\n    /// The default is 100 seconds. The timeout will reset when response headers are received or after successfully reading or\n    /// writing any request, response, or streaming data like gRPC or WebSockets. TCP keep-alive packets and HTTP/2 protocol pings will\n    /// not reset the timeout, but WebSocket pings will.\n    /// </summary>\n    public TimeSpan? ActivityTimeout { get; init; }\n\n    /// <summary>\n    /// Preferred version of the outgoing request.\n    /// The default is HTTP/2.\n    /// </summary>\n    public Version? Version { get; init; }\n\n    /// <summary>\n    /// The policy applied to version selection, e.g. whether to prefer downgrades, upgrades or\n    /// request an exact version. The default is `RequestVersionOrLower`.\n    /// </summary>\n    public HttpVersionPolicy? VersionPolicy { get; init; }\n\n    /// <summary>\n    /// Allows to use write buffering when sending a response back to the client,\n    /// if the server hosting YARP (e.g. IIS) supports it.\n    /// NOTE: enabling it can break SSE (server side event) scenarios.\n    /// </summary>\n    public bool? AllowResponseBuffering { get; init; }\n\n    public bool Equals(ForwarderRequestConfig? other)\n    {\n        if (other is null)\n        {\n            return false;\n        }\n\n        return ActivityTimeout == other.ActivityTimeout\n            && VersionPolicy == other.VersionPolicy\n            && Version == other.Version\n            && AllowResponseBuffering == other.AllowResponseBuffering;\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(ActivityTimeout,\n            VersionPolicy,\n            Version,\n            AllowResponseBuffering);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/ForwarderStage.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\ninternal enum ForwarderStage : int\n{\n    SendAsyncStart = 1,\n    SendAsyncStop,\n    RequestContentTransferStart,\n    ResponseContentTransferStart,\n    ResponseUpgrade,\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/ForwarderTelemetry.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Diagnostics.Tracing;\nusing System.Threading;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n[EventSource(Name = \"Yarp.ReverseProxy\")]\ninternal sealed class ForwarderTelemetry : EventSource\n{\n    public static readonly ForwarderTelemetry Log = new();\n\n    private IncrementingPollingCounter? _startedRequestsPerSecondCounter;\n    private PollingCounter? _startedRequestsCounter;\n    private PollingCounter? _currentRequestsCounter;\n    private PollingCounter? _failedRequestsCounter;\n\n    private long _startedRequests;\n    private long _stoppedRequests;\n    private long _failedRequests;\n\n    [Event(1, Level = EventLevel.Informational)]\n    public void ForwarderStart(string destinationPrefix)\n    {\n        Interlocked.Increment(ref _startedRequests);\n\n        if (IsEnabled(EventLevel.Informational, EventKeywords.All))\n        {\n            WriteEvent(eventId: 1, destinationPrefix);\n        }\n    }\n\n    [Event(2, Level = EventLevel.Informational)]\n    public void ForwarderStop(int statusCode)\n    {\n        Interlocked.Increment(ref _stoppedRequests);\n\n        if (IsEnabled(EventLevel.Informational, EventKeywords.All))\n        {\n            WriteEvent(eventId: 2, statusCode);\n        }\n    }\n\n    [Event(3, Level = EventLevel.Informational)]\n    public void ForwarderFailed(ForwarderError error)\n    {\n        Interlocked.Increment(ref _failedRequests);\n\n        if (IsEnabled(EventLevel.Informational, EventKeywords.All))\n        {\n            Debug.Assert(sizeof(ForwarderError) == sizeof(int), \"Backing type of ForwarderError MUST NOT be changed\");\n            WriteEvent(eventId: 3, (int)error);\n        }\n    }\n\n    [Event(4, Level = EventLevel.Informational)]\n    public void ForwarderStage(ForwarderStage stage)\n    {\n        if (IsEnabled(EventLevel.Informational, EventKeywords.All))\n        {\n            Debug.Assert(sizeof(ForwarderStage) == sizeof(int), \"Backing type of ForwarderStage MUST NOT be changed\");\n            WriteEvent(eventId: 4, (int)stage);\n        }\n    }\n\n    [Event(5, Level = EventLevel.Informational)]\n    public void ContentTransferring(bool isRequest, long contentLength, long iops, long readTime, long writeTime)\n    {\n        if (IsEnabled(EventLevel.Informational, EventKeywords.All))\n        {\n            WriteEvent(eventId: 5, isRequest, contentLength, iops, readTime, writeTime);\n        }\n    }\n\n    [Event(6, Level = EventLevel.Informational)]\n    public void ContentTransferred(bool isRequest, long contentLength, long iops, long readTime, long writeTime, long firstReadTime)\n    {\n        if (IsEnabled(EventLevel.Informational, EventKeywords.All))\n        {\n            WriteEvent(eventId: 6, isRequest, contentLength, iops, readTime, writeTime, firstReadTime);\n        }\n    }\n\n    [Event(7, Level = EventLevel.Informational)]\n    public void ForwarderInvoke(string clusterId, string routeId, string destinationId)\n    {\n        if (IsEnabled(EventLevel.Informational, EventKeywords.All))\n        {\n            WriteEvent(eventId: 7, clusterId, routeId, destinationId);\n        }\n    }\n\n    protected override void OnEventCommand(EventCommandEventArgs command)\n    {\n        if (command.Command == EventCommand.Enable)\n        {\n            _startedRequestsCounter ??= new PollingCounter(\"requests-started\", this, () => Volatile.Read(ref _startedRequests))\n            {\n                DisplayName = \"Requests Started\",\n            };\n\n            _startedRequestsPerSecondCounter ??= new IncrementingPollingCounter(\"requests-started-rate\", this, () => Volatile.Read(ref _startedRequests))\n            {\n                DisplayName = \"Requests Started Rate\",\n                DisplayRateTimeScale = TimeSpan.FromSeconds(1)\n            };\n\n            _failedRequestsCounter ??= new PollingCounter(\"requests-failed\", this, () => Volatile.Read(ref _failedRequests))\n            {\n                DisplayName = \"Requests Failed\"\n            };\n\n            _currentRequestsCounter ??= new PollingCounter(\"current-requests\", this, () => -Volatile.Read(ref _stoppedRequests) + Volatile.Read(ref _startedRequests))\n            {\n                DisplayName = \"Current Requests\"\n            };\n        }\n    }\n\n    [UnconditionalSuppressMessage(\"ReflectionAnalysis\", \"IL2026:RequiresUnreferencedCode\",\n        Justification = \"Parameters to this method are primitive and are trimmer safe.\")]\n    [NonEvent]\n    private unsafe void WriteEvent(int eventId, bool arg1, long arg2, long arg3, long arg4, long arg5)\n    {\n        const int NumEventDatas = 5;\n        var descrs = stackalloc EventData[NumEventDatas];\n\n        descrs[0] = new EventData\n        {\n            DataPointer = (IntPtr)(&arg1),\n            Size = sizeof(int) // EventSource defines bool as a 32-bit type\n        };\n        descrs[1] = new EventData\n        {\n            DataPointer = (IntPtr)(&arg2),\n            Size = sizeof(long)\n        };\n        descrs[2] = new EventData\n        {\n            DataPointer = (IntPtr)(&arg3),\n            Size = sizeof(long)\n        };\n        descrs[3] = new EventData\n        {\n            DataPointer = (IntPtr)(&arg4),\n            Size = sizeof(long)\n        };\n        descrs[4] = new EventData\n        {\n            DataPointer = (IntPtr)(&arg5),\n            Size = sizeof(long)\n        };\n\n        WriteEventCore(eventId, NumEventDatas, descrs);\n    }\n\n    [UnconditionalSuppressMessage(\"ReflectionAnalysis\", \"IL2026:RequiresUnreferencedCode\",\n        Justification = \"Parameters to this method are primitive and are trimmer safe.\")]\n    [NonEvent]\n    private unsafe void WriteEvent(int eventId, bool arg1, long arg2, long arg3, long arg4, long arg5, long arg6)\n    {\n        const int NumEventDatas = 6;\n        var descrs = stackalloc EventData[NumEventDatas];\n\n        descrs[0] = new EventData\n        {\n            DataPointer = (IntPtr)(&arg1),\n            Size = sizeof(int) // EventSource defines bool as a 32-bit type\n        };\n        descrs[1] = new EventData\n        {\n            DataPointer = (IntPtr)(&arg2),\n            Size = sizeof(long)\n        };\n        descrs[2] = new EventData\n        {\n            DataPointer = (IntPtr)(&arg3),\n            Size = sizeof(long)\n        };\n        descrs[3] = new EventData\n        {\n            DataPointer = (IntPtr)(&arg4),\n            Size = sizeof(long)\n        };\n        descrs[4] = new EventData\n        {\n            DataPointer = (IntPtr)(&arg5),\n            Size = sizeof(long)\n        };\n        descrs[5] = new EventData\n        {\n            DataPointer = (IntPtr)(&arg6),\n            Size = sizeof(long)\n        };\n\n        WriteEventCore(eventId, NumEventDatas, descrs);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/HttpForwarder.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.AspNetCore.Http.Timeouts;\nusing Microsoft.AspNetCore.WebUtilities;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Net.Http.Headers;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// Default implementation of <see cref=\"IHttpForwarder\"/>.\n/// </summary>\ninternal sealed class HttpForwarder : IHttpForwarder\n{\n    private const string WebSocketName = \"websocket\";\n    private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(100);\n    private static readonly Version DefaultVersion = HttpVersion.Version20;\n    private const HttpVersionPolicy DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower;\n    private readonly ILogger _logger;\n    private readonly TimeProvider _timeProvider;\n\n    public HttpForwarder(ILogger<HttpForwarder> logger, TimeProvider timeProvider)\n    {\n        ArgumentNullException.ThrowIfNull(logger);\n        ArgumentNullException.ThrowIfNull(timeProvider);\n\n        _logger = logger;\n        _timeProvider = timeProvider;\n    }\n\n    /// <summary>\n    /// Proxies the incoming request to the destination server, and the response back to the client.\n    /// </summary>\n    /// <remarks>\n    /// In what follows, as well as throughout in Reverse Proxy, we consider\n    /// the following picture as illustrative of the Proxy.\n    /// <code>\n    ///      +-------------------+\n    ///      |  Destination      +\n    ///      +-------------------+\n    ///            ▲       |\n    ///        (b) |       | (c)\n    ///            |       ▼\n    ///      +-------------------+\n    ///      |      Proxy        +\n    ///      +-------------------+\n    ///            ▲       |\n    ///        (a) |       | (d)\n    ///            |       ▼\n    ///      +-------------------+\n    ///      | Client            +\n    ///      +-------------------+\n    /// </code>\n    ///\n    /// (a) and (b) show the *request* path, going from the client to the target.\n    /// (c) and (d) show the *response* path, going from the destination back to the client.\n    ///\n    /// Normal proxying comprises the following steps:\n    ///    (0) Disable ASP .NET Core limits for streaming requests\n    ///    (1) Create outgoing HttpRequestMessage\n    ///    (2) Setup copy of request body (background)             Client --► Proxy --► Destination\n    ///    (3) Copy request headers                                Client --► Proxy --► Destination\n    ///    (4) Send the outgoing request using HttpMessageInvoker  Client --► Proxy --► Destination\n    ///    (5) Copy response status line                           Client ◄-- Proxy ◄-- Destination\n    ///    (6) Copy response headers                               Client ◄-- Proxy ◄-- Destination\n    ///    (7-A) Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol.\n    ///        (7-A-1)  Upgrade client channel                     Client ◄--- Proxy ◄--- Destination\n    ///        (7-A-2)  Copy duplex streams and return             Client ◄--► Proxy ◄--► Destination\n    ///    (7-B) Copy (normal) response body                       Client ◄-- Proxy ◄-- Destination\n    ///    (8) Copy response trailer headers and finish response   Client ◄-- Proxy ◄-- Destination\n    ///    (9) Wait for completion of step 2: copying request body Client --► Proxy --► Destination\n    ///\n    /// ASP .NET Core (Kestrel) will finally send response trailers (if any)\n    /// after we complete the steps above and relinquish control.\n    /// </remarks>\n    public ValueTask<ForwarderError> SendAsync(\n        HttpContext context,\n        string destinationPrefix,\n        HttpMessageInvoker httpClient,\n        ForwarderRequestConfig requestConfig,\n        HttpTransformer transformer)\n        => SendAsync(context, destinationPrefix, httpClient, requestConfig, transformer, CancellationToken.None);\n\n    public async ValueTask<ForwarderError> SendAsync(\n        HttpContext context,\n        string destinationPrefix,\n        HttpMessageInvoker httpClient,\n        ForwarderRequestConfig requestConfig,\n        HttpTransformer transformer,\n        CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n        ArgumentNullException.ThrowIfNull(destinationPrefix);\n        ArgumentNullException.ThrowIfNull(httpClient);\n        ArgumentNullException.ThrowIfNull(requestConfig);\n        ArgumentNullException.ThrowIfNull(transformer);\n\n        if (RequestUtilities.IsResponseSet(context.Response))\n        {\n            throw new InvalidOperationException(\"The request cannot be forwarded, the response has already started\");\n        }\n\n        // HttpClient overload for SendAsync changes response behavior to fully buffered which impacts performance\n        // See discussion in https://github.com/dotnet/yarp/issues/458\n        if (httpClient is HttpClient)\n        {\n            throw new ArgumentException($\"The http client must be of type HttpMessageInvoker, not HttpClient\", nameof(httpClient));\n        }\n\n        // \"http://a\".Length = 8\n        if (destinationPrefix is null || destinationPrefix.Length < 8)\n        {\n            throw new ArgumentException(\"Invalid destination prefix.\", nameof(destinationPrefix));\n        }\n\n        ForwarderTelemetry.Log.ForwarderStart(destinationPrefix);\n\n        var activityCancellationSource = ActivityCancellationTokenSource.Rent(requestConfig?.ActivityTimeout ?? DefaultTimeout, context.RequestAborted, cancellationToken);\n        try\n        {\n            var isClientHttp2OrGreater = ProtocolHelper.IsHttp2OrGreater(context.Request.Protocol);\n\n            // NOTE: We heuristically assume gRPC-looking requests may require streaming semantics.\n            // See https://github.com/dotnet/yarp/issues/118 for design discussion.\n            var isStreamingRequest = isClientHttp2OrGreater && ProtocolHelper.IsGrpcContentType(context.Request.ContentType);\n\n            HttpRequestMessage? destinationRequest = null;\n            StreamCopyHttpContent? requestContent = null;\n            HttpResponseMessage destinationResponse;\n            try\n            {\n                // :: Step 1-3: Create outgoing HttpRequestMessage\n                bool tryDowngradingH2WsOnFailure;\n                (destinationRequest, requestContent, tryDowngradingH2WsOnFailure) = await CreateRequestMessageAsync(\n                    context, destinationPrefix, transformer, requestConfig, isStreamingRequest, activityCancellationSource);\n\n                // Transforms generated a response, do not proxy.\n                if (RequestUtilities.IsResponseSet(context.Response))\n                {\n                    Log.NotProxying(_logger, context.Response.StatusCode);\n                    return ForwarderError.None;\n                }\n\n                Log.Proxying(_logger, destinationRequest, isStreamingRequest);\n\n                // :: Step 4: Send the outgoing request using HttpClient\n                ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.SendAsyncStart);\n\n                try\n                {\n                    // CodeQL [SM03781] SSRF - Request endpoint is controlled by YARP configuration. Sensitive headers are not copied by default.\n                    destinationResponse = await httpClient.SendAsync(destinationRequest, activityCancellationSource.Token);\n                }\n                catch (HttpRequestException hre) when (tryDowngradingH2WsOnFailure)\n                {\n                    Debug.Assert(requestContent is null);\n                    // This is how SocketsHttpHandler communicates to us that we tried a HTTP/2 extension that wasn't\n                    // enabled by the server. We should retry on HTTP/1.1.\n                    if (hre.Data.Contains(\"SETTINGS_ENABLE_CONNECT_PROTOCOL\"))\n                    {\n                        Debug.Assert(false == (bool?)hre.Data[\"SETTINGS_ENABLE_CONNECT_PROTOCOL\"]);\n                        Log.RetryingWebSocketDowngradeNoConnect(_logger);\n                    }\n                    // This is how SocketsHttpHandler communicates to us that we tried HTTP/2, but the server only supports\n                    // HTTP/1.x (as determined by ALPN). We'll only get this when using TLS/https. Retry on HTTP/1.1.\n                    // We don't let SocketsHttpHandler downgrade automatically for us because we need to send different headers.\n                    else if (hre.Data.Contains(\"HTTP2_ENABLED\"))\n                    {\n                        Debug.Assert(false == (bool?)hre.Data[\"HTTP2_ENABLED\"]);\n                        Log.RetryingWebSocketDowngradeNoHttp2(_logger);\n                    }\n                    else\n                    {\n                        throw;\n                    }\n\n                    // Trying again\n                    activityCancellationSource.ResetTimeout();\n\n                    Debug.Assert(requestConfig?.VersionPolicy is null or HttpVersionPolicy.RequestVersionOrLower || requestConfig.Version?.Major is null or 1,\n                        \"HTTP/1.X was disallowed by policy, we shouldn't be retrying.\");\n\n                    var config = requestConfig! with\n                    {\n                        Version = HttpVersion.Version11,\n                        VersionPolicy = HttpVersionPolicy.RequestVersionExact\n                    };\n\n                    // Set the request back to null while we call into CreateRequestMessageAsync so that\n                    // potential exceptions are correctly treated as 'RequestCreation'.\n                    destinationRequest = null;\n\n                    (destinationRequest, requestContent, _) = await CreateRequestMessageAsync(\n                        context, destinationPrefix, transformer, config, isStreamingRequest, activityCancellationSource);\n\n                    // Transforms generated a response, do not proxy.\n                    if (RequestUtilities.IsResponseSet(context.Response))\n                    {\n                        Log.NotProxying(_logger, context.Response.StatusCode);\n                        return ForwarderError.None;\n                    }\n\n                    // CodeQL [SM03781] SSRF - Request endpoint is controlled by YARP configuration. Sensitive headers are not copied by default.\n                    destinationResponse = await httpClient.SendAsync(destinationRequest, activityCancellationSource.Token);\n                }\n            }\n            catch (Exception requestException)\n            {\n                return await HandleRequestFailureAsync(context, requestContent, requestException, transformer, activityCancellationSource,\n                    failedDuringRequestCreation: destinationRequest is null);\n            }\n\n            ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.SendAsyncStop);\n            // Reset the timeout since we received the response headers.\n            activityCancellationSource.ResetTimeout();\n            Log.ResponseReceived(_logger, destinationResponse);\n\n            try\n            {\n                // :: Step 5: Copy response status line Client ◄-- Proxy ◄-- Destination\n                // :: Step 6: Copy response headers Client ◄-- Proxy ◄-- Destination\n                var copyBody = await CopyResponseStatusAndHeadersAsync(destinationResponse, context, transformer, activityCancellationSource.Token);\n\n                if (!copyBody)\n                {\n                    // The transforms callback decided that the response body should be discarded.\n                    destinationResponse.Dispose();\n\n                    if (requestContent is not null && requestContent.InProgress)\n                    {\n                        activityCancellationSource.Cancel();\n                        await requestContent.ConsumptionTask;\n                    }\n\n                    return ForwarderError.None;\n                }\n            }\n            catch (Exception ex)\n            {\n                destinationResponse.Dispose();\n\n                if (requestContent is not null && requestContent.InProgress)\n                {\n                    activityCancellationSource.Cancel();\n                    await requestContent.ConsumptionTask;\n                }\n\n                ReportProxyError(context, ForwarderError.ResponseHeaders, ex);\n                // Clear the response since status code, reason and some headers might have already been copied and we want clean 502 response.\n                context.Response.Clear();\n                context.Response.StatusCode = StatusCodes.Status502BadGateway;\n                return ForwarderError.ResponseHeaders;\n            }\n\n            // :: Step 7-A: Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol.\n            // Also check for HTTP/2 CONNECT 200 responses, they function similarly.\n            if (destinationResponse.StatusCode == HttpStatusCode.SwitchingProtocols\n                || (destinationResponse.StatusCode == HttpStatusCode.OK\n                && destinationResponse.Version == HttpVersion.Version20\n                && destinationRequest.Headers.Protocol is not null\n                && destinationRequest.Method.Equals(HttpMethod.Connect))\n                )\n            {\n                Debug.Assert(requestContent?.Started != true);\n                return await HandleUpgradedResponse(context, destinationResponse, activityCancellationSource);\n            }\n\n            // NOTE: it may *seem* wise to call `context.Response.StartAsync()` at this point\n            // since it looks like we are ready to send back response headers\n            // (and this might help reduce extra delays while we wait to receive the body from the destination).\n            // HOWEVER, this would produce the wrong result if it turns out that there is no content\n            // from the destination -- instead of sending headers and terminating the stream at once,\n            // we would send headers thinking a body may be coming, and there is none.\n            // This is problematic on gRPC connections when the destination server encounters an error,\n            // in which case it immediately returns the response headers and trailing headers, but no content,\n            // and clients misbehave if the initial headers response does not indicate stream end.\n\n            // :: Step 7-B: Copy response body Client ◄-- Proxy ◄-- Destination\n            StreamCopyResult responseBodyCopyResult;\n            Exception? responseBodyException;\n\n            using (var destinationResponseStream = await destinationResponse.Content.ReadAsStreamAsync(activityCancellationSource.Token))\n            {\n                // The response content-length is enforced by the server.\n                (responseBodyCopyResult, responseBodyException) = await StreamCopier.CopyAsync(isRequest: false, destinationResponseStream, context.Response.Body, StreamCopier.UnknownLength, _timeProvider, activityCancellationSource, activityCancellationSource.Token);\n            }\n\n            if (responseBodyCopyResult != StreamCopyResult.Success)\n            {\n                return await HandleResponseBodyErrorAsync(context, requestContent, responseBodyCopyResult, responseBodyException!, activityCancellationSource);\n            }\n\n            // :: Step 8: Copy response trailer headers and finish response Client ◄-- Proxy ◄-- Destination\n            await CopyResponseTrailingHeadersAsync(destinationResponse, context, transformer, activityCancellationSource.Token);\n\n            if (isStreamingRequest)\n            {\n                // NOTE: We must call `CompleteAsync` so that Kestrel will flush all bytes to the client.\n                // In the case where there was no response body,\n                // this is also when headers and trailing headers are sent to the client.\n                // Without this, the client might wait forever waiting for response bytes,\n                // while we might wait forever waiting for request bytes,\n                // leading to a stuck connection and no way to make progress.\n                await context.Response.CompleteAsync();\n            }\n\n            // :: Step 9: Wait for completion of step 2: copying request body Client --► Proxy --► Destination\n            // NOTE: It is possible for the request body to NOT be copied even when there was an incoming request body,\n            // e.g. when the request includes header `Expect: 100-continue` and the destination produced a non-1xx response.\n            // We must only wait for the request body to complete if it actually started,\n            // otherwise we run the risk of waiting indefinitely for a task that will never complete.\n            if (requestContent is not null && requestContent.Started)\n            {\n                var (requestBodyCopyResult, requestBodyException) = await requestContent.ConsumptionTask;\n\n                if (requestBodyCopyResult != StreamCopyResult.Success)\n                {\n                    // The response succeeded. If there was a request body error then it was probably because the client or destination decided\n                    // to cancel it. Report as low severity.\n\n                    var error = requestBodyCopyResult switch\n                    {\n                        StreamCopyResult.InputError => ForwarderError.RequestBodyClient,\n                        StreamCopyResult.OutputError => ForwarderError.RequestBodyDestination,\n                        StreamCopyResult.Canceled => ForwarderError.RequestBodyCanceled,\n                        _ => throw new NotImplementedException(requestBodyCopyResult.ToString())\n                    };\n                    ReportProxyError(context, error, requestBodyException!);\n                    return error;\n                }\n            }\n        }\n        finally\n        {\n            activityCancellationSource.Return();\n            ForwarderTelemetry.Log.ForwarderStop(context.Response.StatusCode);\n        }\n\n        return ForwarderError.None;\n    }\n\n    private async ValueTask<(HttpRequestMessage, StreamCopyHttpContent?, bool)> CreateRequestMessageAsync(HttpContext context, string destinationPrefix,\n        HttpTransformer transformer, ForwarderRequestConfig? requestConfig, bool isStreamingRequest, ActivityCancellationTokenSource activityToken)\n    {\n        var destinationRequest = new HttpRequestMessage();\n\n        var upgradeFeature = context.Features.Get<IHttpUpgradeFeature>();\n        var upgradeHeader = context.Request.Headers[HeaderNames.Upgrade].ToString();\n\n        var isSpdyRequest = (upgradeFeature?.IsUpgradableRequest ?? false)\n            && upgradeHeader.StartsWith(\"SPDY/\", StringComparison.OrdinalIgnoreCase);\n        var isH1WsRequest = (upgradeFeature?.IsUpgradableRequest ?? false)\n            && string.Equals(WebSocketName, upgradeHeader, StringComparison.OrdinalIgnoreCase);\n        var connectFeature = context.Features.Get<IHttpExtendedConnectFeature>();\n        var connectProtocol = connectFeature?.Protocol;\n        var isH2WsRequest = (connectFeature?.IsExtendedConnect ?? false)\n            && string.Equals(WebSocketName, connectProtocol, StringComparison.OrdinalIgnoreCase);\n\n        var outgoingHttps = destinationPrefix.StartsWith(\"https://\", StringComparison.OrdinalIgnoreCase);\n        var outgoingVersion = requestConfig?.Version ?? DefaultVersion;\n        var outgoingPolicy = requestConfig?.VersionPolicy ?? DefaultVersionPolicy;\n        var outgoingUpgrade = false;\n        var outgoingConnect = false;\n        var tryDowngradingH2WsOnFailure = false;\n\n        if (isSpdyRequest)\n        {\n            // Can only be done on HTTP/1.1.\n            outgoingUpgrade = true;\n        }\n        else if (isH1WsRequest || isH2WsRequest)\n        {\n            switch (outgoingVersion.Major, outgoingPolicy, outgoingHttps)\n            {\n                case (2, HttpVersionPolicy.RequestVersionExact, _):\n                case (2, HttpVersionPolicy.RequestVersionOrHigher, _):\n                    outgoingConnect = true;\n                    break;\n\n                case (1, HttpVersionPolicy.RequestVersionOrHigher, true):\n                case (2, HttpVersionPolicy.RequestVersionOrLower, true):\n                case (3, HttpVersionPolicy.RequestVersionOrLower, true):\n                    // Try H2WS, downgrade if needed.\n                    outgoingConnect = true;\n                    tryDowngradingH2WsOnFailure = true;\n                    break;\n\n                default:\n                    // Override to use HTTP/1.1, nothing else is supported.\n                    outgoingUpgrade = true;\n                    break;\n            }\n        }\n\n        bool http1IsAllowed = outgoingPolicy == HttpVersionPolicy.RequestVersionOrLower || outgoingVersion.Major == 1;\n\n        if (outgoingUpgrade)\n        {\n            // Can only be done on HTTP/1.1, throw if disallowed by options.\n            if (!http1IsAllowed)\n            {\n                throw new HttpRequestException(isSpdyRequest\n                    ? \"SPDY requests require HTTP/1.1 support, but outbound HTTP/1.1 was disallowed by HttpVersionPolicy.\"\n                    : \"An outgoing HTTP/1.1 Upgrade request is required to proxy this request, but is disallowed by HttpVersionPolicy.\");\n            }\n\n            destinationRequest.Version = HttpVersion.Version11;\n            destinationRequest.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;\n            destinationRequest.Method = HttpMethod.Get;\n        }\n        else if (outgoingConnect)\n        {\n            // HTTP/2 only (for now).\n            destinationRequest.Version = HttpVersion.Version20;\n            destinationRequest.VersionPolicy = HttpVersionPolicy.RequestVersionExact;\n            destinationRequest.Method = HttpMethod.Connect;\n            destinationRequest.Headers.Protocol = connectProtocol ?? WebSocketName;\n            tryDowngradingH2WsOnFailure &= http1IsAllowed;\n        }\n        else\n        {\n            Debug.Assert(http1IsAllowed || outgoingVersion.Major != 1);\n            destinationRequest.Method = RequestUtilities.GetHttpMethod(context.Request.Method);\n            destinationRequest.Version = outgoingVersion;\n            destinationRequest.VersionPolicy = outgoingPolicy;\n        }\n\n        // :: Step 2: Setup copy of request body (background) Client --► Proxy --► Destination\n        // Note that we must do this before step (3) because step (3) may also add headers to the HttpContent that we set up here.\n        var requestContent = SetupRequestBodyCopy(context, isStreamingRequest, activityToken);\n        destinationRequest.Content = requestContent;\n\n        // :: Step 3: Copy request headers Client --► Proxy --► Destination\n        await transformer.TransformRequestAsync(context, destinationRequest, destinationPrefix, activityToken.Token);\n\n        if (!ReferenceEquals(requestContent, destinationRequest.Content) && destinationRequest.Content is not EmptyHttpContent)\n        {\n            throw new InvalidOperationException(\"Replacing the YARP outgoing request HttpContent is not supported. You should configure the HttpContext.Request instead.\");\n        }\n\n        // The transformer generated a response, do not forward.\n        if (RequestUtilities.IsResponseSet(context.Response))\n        {\n            return (destinationRequest, requestContent, false);\n        }\n\n        // Transforms may have taken a while, especially if they buffered the body, they count as forward progress.\n        activityToken.ResetTimeout();\n\n        FixupUpgradeRequestHeaders(context, destinationRequest, outgoingUpgrade, outgoingConnect);\n\n        // Allow someone to custom build the request uri, otherwise provide a default for them.\n        var request = context.Request;\n        destinationRequest.RequestUri ??= RequestUtilities.MakeDestinationAddress(destinationPrefix, request.Path, request.QueryString);\n\n        if (requestConfig?.AllowResponseBuffering != true)\n        {\n            context.Features.Get<IHttpResponseBodyFeature>()?.DisableBuffering();\n        }\n\n        return (destinationRequest, requestContent, tryDowngradingH2WsOnFailure);\n    }\n\n    // Connection and Upgrade headers were not copied with the rest of the headers.\n    private void FixupUpgradeRequestHeaders(HttpContext context, HttpRequestMessage request, bool outgoingUpgrade, bool outgoingConnect)\n    {\n        if (outgoingUpgrade)\n        {\n            // H2->H1, add Connection, Upgrade, Sec-WebSocket-Key\n            if (HttpProtocol.IsHttp2(context.Request.Protocol))\n            {\n                request.Headers.TryAddWithoutValidation(HeaderNames.Connection, HeaderNames.Upgrade);\n                request.Headers.TryAddWithoutValidation(HeaderNames.Upgrade, WebSocketName);\n\n                // The client shouldn't be sending a Sec-WebSocket-Key header with H2 WebSockets, but if it did, let's use it.\n                if (RequestUtilities.TryGetValues(request.Headers, HeaderNames.SecWebSocketKey, out var clientKey))\n                {\n                    if (!ProtocolHelper.CheckSecWebSocketKey(clientKey))\n                    {\n                        Log.InvalidSecWebSocketKeyHeader(_logger, clientKey);\n                        // The request will not be forwarded if we change the status code.\n                        context.Response.StatusCode = StatusCodes.Status400BadRequest;\n                    }\n                }\n                else\n                {\n                    var key = ProtocolHelper.CreateSecWebSocketKey();\n                    request.Headers.TryAddWithoutValidation(HeaderNames.SecWebSocketKey, key);\n                }\n            }\n            // H1->H1, re-add the original Connection, Upgrade headers.\n            else\n            {\n                var connectionValues = context.Request.Headers.GetCommaSeparatedValues(HeaderNames.Connection);\n                string? connectionUpgradeValue = null;\n                foreach (var headerValue in connectionValues)\n                {\n                    if (headerValue.Equals(HeaderNames.Upgrade, StringComparison.OrdinalIgnoreCase))\n                    {\n                        // Preserve original value, case\n                        connectionUpgradeValue = headerValue;\n                        break;\n                    }\n                }\n\n                if (connectionUpgradeValue is not null && context.Request.Headers.TryGetValue(HeaderNames.Upgrade, out var upgradeValue))\n                {\n                    request.Headers.TryAddWithoutValidation(HeaderNames.Connection, connectionUpgradeValue);\n                    request.Headers.TryAddWithoutValidation(HeaderNames.Upgrade, (IEnumerable<string>)upgradeValue);\n                }\n            }\n        }\n        // H1->H2, remove Sec-WebSocket-Key\n        else if (outgoingConnect && !HttpProtocol.IsHttp2(context.Request.Protocol))\n        {\n            var key = context.Request.Headers[HeaderNames.SecWebSocketKey];\n            if (!ProtocolHelper.CheckSecWebSocketKey(key))\n            {\n                Log.InvalidSecWebSocketKeyHeader(_logger, key);\n                // The request will not be forwarded if we change the status code.\n                context.Response.StatusCode = StatusCodes.Status400BadRequest;\n            }\n            request.Headers.Remove(HeaderNames.SecWebSocketKey);\n        }\n        // else not an upgrade, or H2->H2, no changes needed\n    }\n\n    private StreamCopyHttpContent? SetupRequestBodyCopy(HttpContext context, bool isStreamingRequest, ActivityCancellationTokenSource activityToken)\n    {\n        // If we generate an HttpContent without a Content-Length then for HTTP/1.1 HttpClient will add a Transfer-Encoding: chunked header\n        // even if it's a GET request. Some servers reject requests containing a Transfer-Encoding header if they're not expecting a body.\n        // Try to be as specific as possible about the client's intent to send a body. The one thing we don't want to do is to start\n        // reading the body early because that has side effects like 100-continue.\n        var request = context.Request;\n        var hasBody = true;\n        var contentLength = request.Headers.ContentLength;\n        var method = request.Method;\n\n        var canHaveBodyFeature = request.HttpContext.Features.Get<IHttpRequestBodyDetectionFeature>();\n        if (canHaveBodyFeature is not null)\n        {\n            // 5.0 servers provide a definitive answer for us.\n            hasBody = canHaveBodyFeature.CanHaveBody;\n        }\n        // https://tools.ietf.org/html/rfc7230#section-3.3.3\n        // All HTTP/1.1 requests should have Transfer-Encoding or Content-Length.\n        // Http.Sys/IIS will even add a Transfer-Encoding header to HTTP/2 requests with bodies for back-compat.\n        // HTTP/1.0 Connection: close bodies are only allowed on responses, not requests.\n        // https://tools.ietf.org/html/rfc1945#section-7.2.2\n        //\n        // Transfer-Encoding overrides Content-Length per spec\n        else if (request.Headers.TryGetValue(HeaderNames.TransferEncoding, out var transferEncoding)\n            && transferEncoding.Count == 1\n            && string.Equals(\"chunked\", transferEncoding.ToString(), StringComparison.OrdinalIgnoreCase))\n        {\n            hasBody = true;\n        }\n        else if (contentLength.HasValue)\n        {\n            hasBody = contentLength > 0;\n        }\n        // Kestrel HTTP/2: There are no required headers that indicate if there is a request body, so we need to sniff other fields.\n        else if (!ProtocolHelper.IsHttp2OrGreater(request.Protocol))\n        {\n            hasBody = false;\n        }\n        // https://tools.ietf.org/html/rfc7231#section-4.3.1\n        // A payload within a GET/HEAD/DELETE/CONNECT request message has no defined semantics; sending a payload body on a\n        // GET/HEAD/DELETE/CONNECT request might cause some existing implementations to reject the request.\n        // https://tools.ietf.org/html/rfc7231#section-4.3.8\n        // A client MUST NOT send a message body in a TRACE request.\n        else if (HttpMethods.IsGet(method)\n            || HttpMethods.IsHead(method)\n            || HttpMethods.IsDelete(method)\n            || HttpMethods.IsConnect(method)\n            || HttpMethods.IsTrace(method))\n        {\n            hasBody = false;\n        }\n        // else hasBody defaults to true\n\n        if (hasBody)\n        {\n            return new StreamCopyHttpContent(context, isStreamingRequest, _timeProvider, _logger, activityToken);\n        }\n\n        return null;\n    }\n\n    private ForwarderError HandleRequestBodyFailure(HttpContext context, StreamCopyResult requestBodyCopyResult, Exception requestBodyException, Exception additionalException, bool timedOut)\n    {\n        ForwarderError requestBodyError;\n        int statusCode;\n        switch (requestBodyCopyResult)\n        {\n            // Failed while trying to copy the request body from the client. It's ambiguous if the request or response failed first.\n            case StreamCopyResult.InputError:\n                requestBodyError = ForwarderError.RequestBodyClient;\n                statusCode = timedOut\n                    ? StatusCodes.Status408RequestTimeout\n                    // Attempt to capture the status code from the request exception if available.\n                    : requestBodyException is BadHttpRequestException badHttpRequestException\n                        ? badHttpRequestException.StatusCode\n                        : StatusCodes.Status400BadRequest;\n                break;\n            // Failed while trying to copy the request body to the destination. It's ambiguous if the request or response failed first.\n            case StreamCopyResult.OutputError:\n                requestBodyError = ForwarderError.RequestBodyDestination;\n                statusCode = timedOut ? StatusCodes.Status504GatewayTimeout : StatusCodes.Status502BadGateway;\n                break;\n            default:\n                throw new NotImplementedException(requestBodyCopyResult.ToString());\n        }\n\n        ReportProxyError(context, requestBodyError, new AggregateException(requestBodyException, additionalException));\n\n        // We don't know if the client is still around to see this error, but set it for diagnostics to see.\n        if (!context.Response.HasStarted)\n        {\n            // Nothing has been sent to the client yet, we can still send a good error response.\n            context.Response.Clear();\n            context.Response.StatusCode = statusCode;\n            return requestBodyError;\n        }\n\n        ResetOrAbort(context, isCancelled: requestBodyCopyResult == StreamCopyResult.Canceled);\n\n        return requestBodyError;\n    }\n\n    private async ValueTask<ForwarderError> HandleRequestFailureAsync(HttpContext context, StreamCopyHttpContent? requestContent, Exception requestException,\n        HttpTransformer transformer, ActivityCancellationTokenSource requestCancellationSource, bool failedDuringRequestCreation)\n    {\n        var triedRequestBody = requestContent?.ConsumptionTask.IsCompleted == true;\n\n        if (requestCancellationSource.CancelledByLinkedToken)\n        {\n            var requestBodyCanceled = false;\n            if (triedRequestBody)\n            {\n                var (requestBodyCopyResult, requestBodyException) = requestContent!.ConsumptionTask.Result;\n                requestBodyCanceled = requestBodyCopyResult == StreamCopyResult.Canceled;\n                if (requestBodyCanceled)\n                {\n                    requestException = new AggregateException(requestException, requestBodyException!);\n                }\n            }\n            // Either the client went away (HttpContext.RequestAborted) or the CancellationToken provided to SendAsync was signaled.\n            return await ReportErrorAsync(requestBodyCanceled ? ForwarderError.RequestBodyCanceled : ForwarderError.RequestCanceled,\n                context.RequestAborted.IsCancellationRequested ? StatusCodes.Status400BadRequest : StatusCodes.Status502BadGateway);\n        }\n\n        // Check for request body errors, these may have triggered the response error.\n        if (triedRequestBody)\n        {\n            var (requestBodyCopyResult, requestBodyException) = requestContent!.ConsumptionTask.Result;\n\n            if (requestBodyCopyResult is StreamCopyResult.InputError or StreamCopyResult.OutputError)\n            {\n                var error = HandleRequestBodyFailure(context, requestBodyCopyResult, requestBodyException!, requestException,\n                    timedOut: requestCancellationSource.IsCancellationRequested);\n\n                try\n                {\n                    await transformer.TransformResponseAsync(context, proxyResponse: null, requestCancellationSource.Token);\n                }\n                catch (OperationCanceledException)\n                {\n                    // We're about to report a more specific error, so ignore OCEs that occur here.\n                }\n\n                return error;\n            }\n        }\n\n        if (requestException is OperationCanceledException)\n        {\n            Debug.Assert(requestCancellationSource.IsCancellationRequested || requestException.ToString().Contains(\"ConnectTimeout\"), requestException.ToString());\n\n            return await ReportErrorAsync(ForwarderError.RequestTimedOut, StatusCodes.Status504GatewayTimeout);\n        }\n\n        // We couldn't communicate with the destination.\n        return await ReportErrorAsync(failedDuringRequestCreation ? ForwarderError.RequestCreation : ForwarderError.Request, StatusCodes.Status502BadGateway);\n\n        async ValueTask<ForwarderError> ReportErrorAsync(ForwarderError error, int statusCode)\n        {\n            ReportProxyError(context, error, requestException);\n            context.Response.StatusCode = statusCode;\n\n            if (requestContent is not null && requestContent.InProgress)\n            {\n                requestCancellationSource.Cancel();\n                await requestContent.ConsumptionTask;\n            }\n\n            try\n            {\n                await transformer.TransformResponseAsync(context, proxyResponse: null, requestCancellationSource.Token);\n            }\n            catch (OperationCanceledException)\n            {\n                // We may have manually cancelled the request CTS as part of error handling.\n                // We're about to report a more specific error, so ignore OCEs that occur here.\n            }\n\n            return error;\n        }\n    }\n\n    private static ValueTask<bool> CopyResponseStatusAndHeadersAsync(HttpResponseMessage source, HttpContext context, HttpTransformer transformer, CancellationToken cancellationToken)\n    {\n        context.Response.StatusCode = (int)source.StatusCode;\n\n        if (!ProtocolHelper.IsHttp2OrGreater(context.Request.Protocol))\n        {\n            // Don't explicitly set the field if the default reason phrase is used\n            if (source.ReasonPhrase != ReasonPhrases.GetReasonPhrase((int)source.StatusCode))\n            {\n                context.Features.Get<IHttpResponseFeature>()!.ReasonPhrase = source.ReasonPhrase;\n            }\n        }\n\n        // Copies headers\n        return transformer.TransformResponseAsync(context, source, cancellationToken);\n    }\n\n    private async ValueTask<ForwarderError> HandleUpgradedResponse(HttpContext context, HttpResponseMessage destinationResponse,\n        ActivityCancellationTokenSource activityCancellationSource)\n    {\n        ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.ResponseUpgrade);\n\n        var isHttp2Request = HttpProtocol.IsHttp2(context.Request.Protocol);\n        var headerError = FixupUpgradeResponseHeaders(context, destinationResponse, isHttp2Request);\n        if (headerError != ForwarderError.None)\n        {\n            destinationResponse.Dispose();\n            return headerError;\n        }\n\n        // :: Step 7-A-1: Upgrade the client channel. This will also send response headers.\n        Stream upgradeResult;\n        try\n        {\n            if (isHttp2Request)\n            {\n                var connectFeature = context.Features.Get<IHttpExtendedConnectFeature>();\n                Debug.Assert(connectFeature != null);\n                upgradeResult = await connectFeature.AcceptAsync();\n            }\n            else\n            {\n                var upgradeFeature = context.Features.Get<IHttpUpgradeFeature>();\n                Debug.Assert(upgradeFeature != null);\n                upgradeResult = await upgradeFeature.UpgradeAsync();\n            }\n\n            // Disable request timeout, if there is one, after the upgrade has been accepted\n            context.Features.Get<IHttpRequestTimeoutFeature>()?.DisableTimeout();\n        }\n        catch (Exception ex)\n        {\n            destinationResponse.Dispose();\n            ReportProxyError(context, ForwarderError.UpgradeResponseClient, ex);\n            return ForwarderError.UpgradeResponseClient;\n        }\n\n        using var clientStream = upgradeResult;\n\n        // :: Step 7-A-2: Copy duplex streams\n        using var destinationStream = await destinationResponse.Content.ReadAsStreamAsync(activityCancellationSource.Token);\n\n        var requestTask = StreamCopier.CopyAsync(isRequest: true, clientStream, destinationStream, StreamCopier.UnknownLength, _timeProvider, activityCancellationSource,\n            // HTTP/2 HttpClient request streams buffer by default.\n            autoFlush: destinationResponse.Version == HttpVersion.Version20, activityCancellationSource.Token).AsTask();\n        var responseTask = StreamCopier.CopyAsync(isRequest: false, destinationStream, clientStream, StreamCopier.UnknownLength, _timeProvider, activityCancellationSource, activityCancellationSource.Token).AsTask();\n\n        // Make sure we report the first failure.\n        var firstTask = await Task.WhenAny(requestTask, responseTask);\n        var requestFinishedFirst = firstTask == requestTask;\n        var secondTask = requestFinishedFirst ? responseTask : requestTask;\n\n        ForwarderError error;\n\n        var (firstResult, firstException) = firstTask.Result;\n        if (firstResult != StreamCopyResult.Success)\n        {\n            error = ReportResult(context, requestFinishedFirst, firstResult, firstException!, activityCancellationSource);\n            // Cancel the other direction\n            activityCancellationSource.Cancel();\n            // Wait for this to finish before exiting so the resources get cleaned up properly.\n            await secondTask;\n        }\n        else\n        {\n            var cancelReads = !requestFinishedFirst && !secondTask.IsCompleted;\n            if (cancelReads)\n            {\n                // The response is finished, unblock the incoming reads\n                activityCancellationSource.Cancel();\n            }\n\n            var (secondResult, secondException) = await secondTask;\n            if (!cancelReads && secondResult != StreamCopyResult.Success)\n            {\n                error = ReportResult(context, !requestFinishedFirst, secondResult, secondException!, activityCancellationSource);\n            }\n            else\n            {\n                error = ForwarderError.None;\n            }\n        }\n\n        return error;\n\n        ForwarderError ReportResult(HttpContext context, bool request, StreamCopyResult result, Exception exception, ActivityCancellationTokenSource activityCancellationSource)\n        {\n            var error = result switch\n            {\n                StreamCopyResult.InputError => request ? ForwarderError.UpgradeRequestClient : ForwarderError.UpgradeResponseDestination,\n                StreamCopyResult.OutputError => request ? ForwarderError.UpgradeRequestDestination : ForwarderError.UpgradeResponseClient,\n                StreamCopyResult.Canceled => request ? ForwarderError.UpgradeRequestCanceled : ForwarderError.UpgradeResponseCanceled,\n                _ => throw new NotImplementedException(result.ToString()),\n            };\n\n            if (activityCancellationSource.IsCancellationRequested && !activityCancellationSource.CancelledByLinkedToken)\n            {\n                // We haven't manually called `Cancel` on the ActivityCancellationTokenSource, and the linked tokens haven't been signaled,\n                // so the only remaining option is that this failure was caused by the ActivityTimeout firing.\n                error = ForwarderError.UpgradeActivityTimeout;\n            }\n\n            ReportProxyError(context, error, exception);\n            return error;\n        }\n    }\n\n    // The Connection and Upgrade headers were not copied by default\n    private ForwarderError FixupUpgradeResponseHeaders(HttpContext context, HttpResponseMessage response, bool isHttp2Request)\n    {\n        if (isHttp2Request)\n        {\n            // H2 <- H1 Validate & remove the Sec-WebSocket-Accept header.\n            if (response.Version != HttpVersion.Version20)\n            {\n                var success = RequestUtilities.TryGetValues(response.RequestMessage!.Headers, HeaderNames.SecWebSocketKey, out var key);\n                Debug.Assert(success);\n                var accept = context.Response.Headers[HeaderNames.SecWebSocketAccept];\n                var expectedAccept = ProtocolHelper.CreateSecWebSocketAccept(key.ToString());\n                if (!string.Equals(expectedAccept, accept, StringComparison.Ordinal)) // Base64 is case-sensitive\n                {\n                    context.Response.Clear();\n                    context.Response.StatusCode = StatusCodes.Status502BadGateway;\n                    ReportProxyError(context, ForwarderError.ResponseHeaders, new InvalidOperationException(\"The Sec-WebSocket-Accept header does not match the expected value.\"));\n                    return ForwarderError.ResponseHeaders;\n                }\n                context.Response.Headers.Remove(HeaderNames.SecWebSocketAccept);\n                context.Response.StatusCode = StatusCodes.Status200OK;\n            }\n            // else H2 <- H2, no changes needed\n            return ForwarderError.None;\n        }\n\n        // H1 <- H2\n        if (response.Version == HttpVersion.Version20)\n        {\n            // Generate and add the Sec-WebSocket-Accept header, and the Connection and Upgrade headers\n            var key = context.Request.Headers[HeaderNames.SecWebSocketKey];\n            var accept = ProtocolHelper.CreateSecWebSocketAccept(key);\n            context.Response.Headers.TryAdd(HeaderNames.SecWebSocketAccept, accept);\n            context.Response.Headers.TryAdd(HeaderNames.Connection, HeaderNames.Upgrade);\n            context.Response.Headers.TryAdd(HeaderNames.Upgrade, WebSocketName);\n            return ForwarderError.None;\n        }\n\n        // H1 <- H1\n        // Restore the Connection and Upgrade headers\n        // We don't use NonValidated for the Connection header as we do want value validation.\n        // HttpHeaders.TryGetValues will handle the parsing and split the values for us.\n        if (RequestUtilities.TryGetValues(response.Headers, HeaderNames.Upgrade, out var upgradeValues)\n            && response.Headers.TryGetValues(HeaderNames.Connection, out var connectionValues))\n        {\n            foreach (var value in connectionValues)\n            {\n                if (value.Equals(HeaderNames.Upgrade, StringComparison.OrdinalIgnoreCase))\n                {\n                    context.Response.Headers.TryAdd(HeaderNames.Connection, value);\n                    context.Response.Headers.TryAdd(HeaderNames.Upgrade, upgradeValues);\n                    break;\n                }\n            }\n        }\n\n        return ForwarderError.None;\n    }\n\n    private async ValueTask<ForwarderError> HandleResponseBodyErrorAsync(HttpContext context, StreamCopyHttpContent? requestContent, StreamCopyResult responseBodyCopyResult, Exception responseBodyException, ActivityCancellationTokenSource requestCancellationSource)\n    {\n        if (requestContent is not null && requestContent.Started)\n        {\n            var alreadyFinished = requestContent.ConsumptionTask.IsCompleted;\n\n            if (!alreadyFinished)\n            {\n                requestCancellationSource.Cancel();\n            }\n\n            var (requestBodyCopyResult, requestBodyError) = await requestContent.ConsumptionTask;\n\n            // Check for request body errors, these may have triggered the response error.\n            if (alreadyFinished && requestBodyCopyResult is StreamCopyResult.InputError or StreamCopyResult.OutputError)\n            {\n                return HandleRequestBodyFailure(context, requestBodyCopyResult, requestBodyError!, responseBodyException,\n                    timedOut: requestCancellationSource.IsCancellationRequested && !requestCancellationSource.CancelledByLinkedToken);\n            }\n        }\n\n        var error = responseBodyCopyResult switch\n        {\n            StreamCopyResult.InputError => ForwarderError.ResponseBodyDestination,\n            StreamCopyResult.OutputError => ForwarderError.ResponseBodyClient,\n            StreamCopyResult.Canceled => ForwarderError.ResponseBodyCanceled,\n            _ => throw new NotImplementedException(responseBodyCopyResult.ToString()),\n        };\n        ReportProxyError(context, error, responseBodyException);\n\n        if (!context.Response.HasStarted)\n        {\n            // Nothing has been sent to the client yet, we can still send a good error response.\n            context.Response.Clear();\n            context.Response.StatusCode = StatusCodes.Status502BadGateway;\n            return error;\n        }\n\n        // The response has already started, we must forcefully terminate it so the client doesn't get\n        // the mistaken impression that the truncated response is complete.\n        ResetOrAbort(context, isCancelled: responseBodyCopyResult == StreamCopyResult.Canceled);\n\n        return error;\n    }\n\n    private static ValueTask CopyResponseTrailingHeadersAsync(HttpResponseMessage source, HttpContext context, HttpTransformer transformer, CancellationToken cancellationToken)\n    {\n        // Copies trailers\n        return transformer.TransformResponseTrailersAsync(context, source, cancellationToken);\n    }\n\n    private void ReportProxyError(HttpContext context, ForwarderError error, Exception ex)\n    {\n        context.Features.Set<IForwarderErrorFeature>(new ForwarderErrorFeature(error, ex));\n        Log.ErrorProxying(_logger, error, ex);\n        ForwarderTelemetry.Log.ForwarderFailed(error);\n    }\n\n    private static void ResetOrAbort(HttpContext context, bool isCancelled)\n    {\n        var resetFeature = context.Features.Get<IHttpResetFeature>();\n        if (resetFeature is not null)\n        {\n            // https://tools.ietf.org/html/rfc7540#section-7\n            const int Cancelled = 2;\n            const int InternalError = 8;\n            resetFeature.Reset(isCancelled ? Cancelled : InternalError);\n            return;\n        }\n\n        context.Abort();\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, Version, int, Exception?> _responseReceived = LoggerMessage.Define<Version, int>(\n            LogLevel.Information,\n            EventIds.ResponseReceived,\n            \"Received HTTP/{version} response {statusCode}.\");\n\n        private static readonly Action<ILogger, string, string, string, string, Exception?> _proxying = LoggerMessage.Define<string, string, string, string>(\n            LogLevel.Information,\n            EventIds.Forwarding,\n            \"Proxying to {targetUrl} {version} {versionPolicy} {isStreaming}\");\n\n        private static readonly Action<ILogger, ForwarderError, string, Exception> _proxyError = LoggerMessage.Define<ForwarderError, string>(\n            LogLevel.Warning,\n            EventIds.ForwardingError,\n            \"{error}: {message}\");\n\n        private static readonly Action<ILogger, ForwarderError, string, Exception> _proxyRequestCancelled = LoggerMessage.Define<ForwarderError, string>(\n            LogLevel.Debug,\n            EventIds.ForwardingRequestCancelled,\n            \"{error}: {message}\");\n\n        private static readonly Action<ILogger, int, Exception?> _notProxying = LoggerMessage.Define<int>(\n            LogLevel.Information,\n            EventIds.NotForwarding,\n            \"Not Proxying, a {statusCode} response was set by the transforms.\");\n\n        private static readonly Action<ILogger, Exception?> _retryingWebSocketDowngradeNoConnect = LoggerMessage.Define(\n            LogLevel.Information,\n            EventIds.RetryingWebSocketDowngradeNoConnect,\n            \"Unable to proxy the WebSocket using HTTP/2, the server does not support RFC 8441, retrying with HTTP/1.1.\");\n\n        private static readonly Action<ILogger, Exception?> _retryingWebSocketDowngradeNoHttp2 = LoggerMessage.Define(\n            LogLevel.Information,\n            EventIds.RetryingWebSocketDowngradeNoHttp2,\n            \"Unable to proxy the WebSocket using HTTP/2, server does not support HTTP/2. Retrying with HTTP/1.1. Disable HTTP/2 negotiation for improved performance.\");\n\n        private static readonly Action<ILogger, string?, Exception?> _invalidKeyHeader = LoggerMessage.Define<string?>(\n            LogLevel.Warning,\n            EventIds.InvalidSecWebSocketKeyHeader,\n            \"Invalid Sec-WebSocket-Key header: '{key}'.\");\n\n        public static void ResponseReceived(ILogger logger, HttpResponseMessage msg)\n        {\n            _responseReceived(logger, msg.Version, (int)msg.StatusCode, null);\n        }\n\n        public static void Proxying(ILogger logger, HttpRequestMessage msg, bool isStreamingRequest)\n        {\n            // Avoid computing the AbsoluteUri unless logging is enabled\n            if (logger.IsEnabled(LogLevel.Information))\n            {\n                var streaming = isStreamingRequest ? \"streaming\" : string.Empty;\n                var version = HttpProtocol.GetHttpProtocol(msg.Version);\n                var versionPolicy = ProtocolHelper.GetVersionPolicy(msg.VersionPolicy);\n                _proxying(logger, msg.RequestUri!.AbsoluteUri, version, versionPolicy, streaming, null);\n            }\n        }\n\n        public static void NotProxying(ILogger logger, int statusCode)\n        {\n            _notProxying(logger, statusCode, null);\n        }\n\n        public static void InvalidSecWebSocketKeyHeader(ILogger logger, string? key)\n        {\n            _invalidKeyHeader(logger, key, null);\n        }\n\n        public static void ErrorProxying(ILogger logger, ForwarderError error, Exception ex)\n        {\n            var message = GetMessage(error);\n\n            if (error is\n                ForwarderError.RequestCanceled or\n                ForwarderError.RequestBodyCanceled or\n                ForwarderError.ResponseBodyCanceled or\n                ForwarderError.UpgradeRequestCanceled or\n                ForwarderError.UpgradeResponseCanceled)\n            {\n                // These error conditions are triggered by the client and are not generally indicative of a problem with the proxy.\n                // It's unlikely that they will be useful in most cases, so we log them at Debug level to reduce noise.\n                _proxyRequestCancelled(logger, error, message, ex);\n            }\n            else\n            {\n                _proxyError(logger, error, message, ex);\n            }\n        }\n\n        public static void RetryingWebSocketDowngradeNoConnect(ILogger logger)\n        {\n            _retryingWebSocketDowngradeNoConnect(logger, null);\n        }\n\n        public static void RetryingWebSocketDowngradeNoHttp2(ILogger logger)\n        {\n            _retryingWebSocketDowngradeNoHttp2(logger, null);\n        }\n\n        private static string GetMessage(ForwarderError error)\n        {\n            return error switch\n            {\n                ForwarderError.None => throw new NotSupportedException(\"A more specific error must be used\"),\n                ForwarderError.Request => \"An error was encountered before receiving a response.\",\n                ForwarderError.RequestCreation => \"An error was encountered while creating the request message.\",\n                ForwarderError.RequestTimedOut => \"The request timed out before receiving a response.\",\n                ForwarderError.RequestCanceled => \"The request was canceled before receiving a response.\",\n                ForwarderError.RequestBodyCanceled => \"Copying the request body was canceled.\",\n                ForwarderError.RequestBodyClient => \"The client reported an error when copying the request body.\",\n                ForwarderError.RequestBodyDestination => \"The destination reported an error when copying the request body.\",\n                ForwarderError.ResponseBodyCanceled => \"Copying the response body was canceled.\",\n                ForwarderError.ResponseBodyClient => \"The client reported an error when copying the response body.\",\n                ForwarderError.ResponseBodyDestination => \"The destination reported an error when copying the response body.\",\n                ForwarderError.ResponseHeaders => \"The destination returned a response that cannot be proxied back to the client.\",\n                ForwarderError.UpgradeRequestCanceled => \"Copying the upgraded request body was canceled.\",\n                ForwarderError.UpgradeRequestClient => \"The client reported an error when copying the upgraded request body.\",\n                ForwarderError.UpgradeRequestDestination => \"The destination reported an error when copying the upgraded request body.\",\n                ForwarderError.UpgradeResponseCanceled => \"Copying the upgraded response body was canceled.\",\n                ForwarderError.UpgradeResponseClient => \"The client reported an error when copying the upgraded response body.\",\n                ForwarderError.UpgradeResponseDestination => \"The destination reported an error when copying the upgraded response body.\",\n                ForwarderError.UpgradeActivityTimeout => \"The WebSocket connection was closed after being idle longer than the Activity Timeout.\",\n                ForwarderError.NoAvailableDestinations => throw new NotImplementedException(), // Not used in this class\n                _ => throw new NotImplementedException(error.ToString()),\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/HttpTransformer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Runtime.CompilerServices;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.Extensions.Primitives;\nusing Microsoft.Net.Http.Headers;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\npublic class HttpTransformer\n{\n    /// <summary>\n    /// A default set of transforms that adds X-Forwarded-* headers, removes the original Host value and\n    /// copies all other request and response fields and headers, except for some protocol specific values.\n    /// </summary>\n    public static readonly HttpTransformer Default = TransformBuilder.CreateTransformer(new TransformBuilderContext());\n\n    /// <summary>\n    /// An empty transformer that copies all request and response fields and headers, except for some\n    /// protocol specific values.\n    /// </summary>\n    public static readonly HttpTransformer Empty = new HttpTransformer();\n\n    /// <summary>\n    /// Used to create derived instances.\n    /// </summary>\n    protected HttpTransformer() { }\n\n    [MethodImpl(MethodImplOptions.AggressiveInlining)]\n    private static bool IsBodylessStatusCode(HttpStatusCode statusCode) =>\n        statusCode switch\n        {\n            // A 1xx response is terminated by the end of the header section; it cannot contain content\n            // or trailers.\n            // See https://www.rfc-editor.org/rfc/rfc9110.html#section-15.2-2\n            >= HttpStatusCode.Continue and < HttpStatusCode.OK => true,\n            // A 204 response is terminated by the end of the header section; it cannot contain content\n            // or trailers.\n            // See https://www.rfc-editor.org/rfc/rfc9110.html#section-15.3.5-5\n            HttpStatusCode.NoContent => true,\n            // Since the 205 status code implies that no additional content will be provided, a server\n            // MUST NOT generate content in a 205 response.\n            // See https://www.rfc-editor.org/rfc/rfc9110.html#section-15.3.6-3\n            HttpStatusCode.ResetContent => true,\n            _ => false\n        };\n\n    /// <summary>\n    /// A callback that is invoked prior to sending the proxied request. All HttpRequestMessage fields are\n    /// initialized except RequestUri, which will be initialized after the callback if no value is provided.\n    /// See <see cref=\"RequestUtilities.MakeDestinationAddress(string, PathString, QueryString)\"/> for constructing a custom request Uri.\n    /// The string parameter represents the destination URI prefix that should be used when constructing the RequestUri.\n    /// The headers are copied by the base implementation, excluding some protocol headers like HTTP/2 pseudo headers (\":authority\").\n    /// This method may be overridden to conditionally produce a response, such as for error conditions, and prevent the request from\n    /// being proxied. This is indicated by setting the `HttpResponse.StatusCode` to a value other than 200, or calling `HttpResponse.StartAsync()`,\n    /// or writing to the `HttpResponse.Body` or `BodyWriter`.\n    /// </summary>\n    /// <param name=\"httpContext\">The incoming request.</param>\n    /// <param name=\"proxyRequest\">The outgoing proxy request.</param>\n    /// <param name=\"destinationPrefix\">The uri prefix for the selected destination server which can be used to create the RequestUri.</param>\n    /// <param name=\"cancellationToken\">Indicates that the request is being canceled.</param>\n    public virtual ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken)\n#pragma warning disable CS0618 // We're calling the overload without the CancellationToken for backwards compatibility.\n        => TransformRequestAsync(httpContext, proxyRequest, destinationPrefix);\n#pragma warning restore CS0618\n\n    /// <summary>\n    /// A callback that is invoked prior to sending the proxied request. All HttpRequestMessage fields are\n    /// initialized except RequestUri, which will be initialized after the callback if no value is provided.\n    /// See <see cref=\"RequestUtilities.MakeDestinationAddress(string, PathString, QueryString)\"/> for constructing a custom request Uri.\n    /// The string parameter represents the destination URI prefix that should be used when constructing the RequestUri.\n    /// The headers are copied by the base implementation, excluding some protocol headers like HTTP/2 pseudo headers (\":authority\").\n    /// This method may be overridden to conditionally produce a response, such as for error conditions, and prevent the request from\n    /// being proxied. This is indicated by setting the `HttpResponse.StatusCode` to a value other than 200, or calling `HttpResponse.StartAsync()`,\n    /// or writing to the `HttpResponse.Body` or `BodyWriter`.\n    /// </summary>\n    /// <param name=\"httpContext\">The incoming request.</param>\n    /// <param name=\"proxyRequest\">The outgoing proxy request.</param>\n    /// <param name=\"destinationPrefix\">The uri prefix for the selected destination server which can be used to create the RequestUri.</param>\n    [Obsolete(\"This overload of TransformRequestAsync is obsolete. Override and use the overload accepting a CancellationToken instead.\")]\n    public virtual ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix)\n    {\n        foreach (var header in httpContext.Request.Headers)\n        {\n            var headerName = header.Key;\n            var headerValue = header.Value;\n            if (RequestUtilities.ShouldSkipRequestHeader(headerName))\n            {\n                continue;\n            }\n\n            RequestUtilities.AddHeader(proxyRequest, headerName, headerValue);\n        }\n\n        // https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3\n        // If a message is received with both a Transfer-Encoding and a\n        // Content-Length header field, the Transfer-Encoding overrides the\n        // Content-Length.  Such a message might indicate an attempt to\n        // perform request smuggling (Section 9.5) or response splitting\n        // (Section 9.4) and ought to be handled as an error.  A sender MUST\n        // remove the received Content-Length field prior to forwarding such\n        // a message downstream.\n        if (httpContext.Request.Headers.ContainsKey(HeaderNames.TransferEncoding)\n            && httpContext.Request.Headers.ContainsKey(HeaderNames.ContentLength))\n        {\n            proxyRequest.Content?.Headers.Remove(HeaderNames.ContentLength);\n        }\n\n        // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2\n        // The only exception to this is the TE header field, which MAY be\n        // present in an HTTP/2 request; when it is, it MUST NOT contain any\n        // value other than \"trailers\".\n        if (ProtocolHelper.IsHttp2OrGreater(httpContext.Request.Protocol))\n        {\n            var te = httpContext.Request.Headers.GetCommaSeparatedValues(HeaderNames.TE);\n            if (te is not null)\n            {\n                for (var i = 0; i < te.Length; i++)\n                {\n                    if (string.Equals(te[i], \"trailers\", StringComparison.OrdinalIgnoreCase))\n                    {\n                        var added = proxyRequest.Headers.TryAddWithoutValidation(HeaderNames.TE, te[i]);\n                        Debug.Assert(added);\n                        break;\n                    }\n                }\n            }\n        }\n\n        return default;\n    }\n\n    /// <summary>\n    /// A callback that is invoked when the proxied response is received. The status code and reason phrase will be copied\n    /// to the HttpContext.Response before the callback is invoked, but may still be modified there. The headers will be\n    /// copied to HttpContext.Response.Headers by the base implementation, excludes certain protocol headers like\n    /// `Transfer-Encoding: chunked`.\n    /// </summary>\n    /// <param name=\"httpContext\">The incoming request.</param>\n    /// <param name=\"proxyResponse\">The response from the destination. This can be null if the destination did not respond.</param>\n    /// <param name=\"cancellationToken\">Indicates that the request is being canceled.</param>\n    /// <returns>A bool indicating if the response should be proxied to the client or not. A derived implementation \n    /// that returns false may send an alternate response inline or return control to the caller for it to retry, respond, \n    /// etc.</returns>\n    public virtual ValueTask<bool> TransformResponseAsync(HttpContext httpContext, HttpResponseMessage? proxyResponse, CancellationToken cancellationToken)\n#pragma warning disable CS0618 // We're calling the overload without the CancellationToken for backwards compatibility.\n        => TransformResponseAsync(httpContext, proxyResponse);\n#pragma warning restore CS0618\n\n    /// <summary>\n    /// A callback that is invoked when the proxied response is received. The status code and reason phrase will be copied\n    /// to the HttpContext.Response before the callback is invoked, but may still be modified there. The headers will be\n    /// copied to HttpContext.Response.Headers by the base implementation, excludes certain protocol headers like\n    /// `Transfer-Encoding: chunked`.\n    /// </summary>\n    /// <param name=\"httpContext\">The incoming request.</param>\n    /// <param name=\"proxyResponse\">The response from the destination. This can be null if the destination did not respond.</param>\n    /// <returns>A bool indicating if the response should be proxied to the client or not. A derived implementation\n    /// that returns false may send an alternate response inline or return control to the caller for it to retry, respond,\n    /// etc.</returns>\n    [Obsolete(\"This overload of TransformResponseAsync is obsolete. Override and use the overload accepting a CancellationToken instead.\")]\n    public virtual ValueTask<bool> TransformResponseAsync(HttpContext httpContext, HttpResponseMessage? proxyResponse)\n    {\n        if (proxyResponse is null)\n        {\n            return new ValueTask<bool>(false);\n        }\n\n        var responseHeaders = httpContext.Response.Headers;\n        CopyResponseHeaders(proxyResponse.Headers, responseHeaders);\n        if (proxyResponse.Content is not null)\n        {\n            CopyResponseHeaders(proxyResponse.Content.Headers, responseHeaders);\n        }\n\n        // https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3\n        // If a message is received with both a Transfer-Encoding and a\n        // Content-Length header field, the Transfer-Encoding overrides the\n        // Content-Length.  Such a message might indicate an attempt to\n        // perform request smuggling (Section 9.5) or response splitting\n        // (Section 9.4) and ought to be handled as an error.  A sender MUST\n        // remove the received Content-Length field prior to forwarding such\n        // a message downstream.\n        if (proxyResponse.Content is not null\n            && proxyResponse.Headers.NonValidated.Contains(HeaderNames.TransferEncoding)\n            && proxyResponse.Content.Headers.NonValidated.Contains(HeaderNames.ContentLength))\n        {\n            httpContext.Response.Headers.Remove(HeaderNames.ContentLength);\n        }\n\n        // For responses with status codes that shouldn't include a body,\n        // we remove the 'Content-Length: 0' header if one is present.\n        if (proxyResponse.Content is not null\n            && IsBodylessStatusCode(proxyResponse.StatusCode)\n            && proxyResponse.Content.Headers.NonValidated.TryGetValues(HeaderNames.ContentLength, out var contentLengthValue)\n            && contentLengthValue.ToString() == \"0\")\n        {\n            httpContext.Response.Headers.Remove(HeaderNames.ContentLength);\n        }\n\n        return new ValueTask<bool>(true);\n    }\n\n    /// <summary>\n    /// A callback that is invoked after the response body to modify trailers, if supported. The trailers will be\n    /// copied to the HttpContext.Response by the base implementation.\n    /// </summary>\n    /// <param name=\"httpContext\">The incoming request.</param>\n    /// <param name=\"proxyResponse\">The response from the destination.</param>\n    /// <param name=\"cancellationToken\">Indicates that the request is being canceled.</param>\n    public virtual ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken)\n#pragma warning disable CS0618 // We're calling the overload without the CancellationToken for backwards compatibility.\n        => TransformResponseTrailersAsync(httpContext, proxyResponse);\n#pragma warning restore CS0618\n\n    /// <summary>\n    /// A callback that is invoked after the response body to modify trailers, if supported. The trailers will be\n    /// copied to the HttpContext.Response by the base implementation.\n    /// </summary>\n    /// <param name=\"httpContext\">The incoming request.</param>\n    /// <param name=\"proxyResponse\">The response from the destination.</param>\n    [Obsolete(\"This overload of TransformResponseTrailersAsync is obsolete. Override and use the overload accepting a CancellationToken instead.\")]\n    public virtual ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse)\n    {\n        // NOTE: Deliberately not using `context.Response.SupportsTrailers()`, `context.Response.AppendTrailer(...)`\n        // because they lookup `IHttpResponseTrailersFeature` for every call. Here we do it just once instead.\n        var responseTrailersFeature = httpContext.Features.Get<IHttpResponseTrailersFeature>();\n        var outgoingTrailers = responseTrailersFeature?.Trailers;\n        if (outgoingTrailers is not null && !outgoingTrailers.IsReadOnly)\n        {\n            // Note that trailers, if any, should already have been declared in Proxy's response\n            // by virtue of us having proxied all response headers in step 6.\n            CopyResponseHeaders(proxyResponse.TrailingHeaders, outgoingTrailers);\n        }\n\n        return default;\n    }\n\n    private static void CopyResponseHeaders(HttpHeaders source, IHeaderDictionary destination)\n    {\n        // We want to append to any prior values, if any.\n        // Not using Append here because it skips empty headers.\n        foreach (var header in source.NonValidated)\n        {\n            var headerName = header.Key;\n            if (RequestUtilities.ShouldSkipResponseHeader(headerName))\n            {\n                continue;\n            }\n\n            var currentValue = destination[headerName];\n\n            // https://github.com/dotnet/yarp/issues/2269\n            // The Strict-Transport-Security may be added by the proxy before forwarding. Only copy the header\n            // if it's not already present.\n            if (!StringValues.IsNullOrEmpty(currentValue)\n                && string.Equals(headerName, HeaderNames.StrictTransportSecurity, StringComparison.OrdinalIgnoreCase))\n            {\n                continue;\n            }\n\n            destination[headerName] = RequestUtilities.Concat(currentValue, header.Value);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/IForwarderErrorFeature.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// Stores errors and exceptions that occurred when forwarding the request to the destination.\n/// </summary>\npublic interface IForwarderErrorFeature\n{\n    /// <summary>\n    /// The specified ProxyError.\n    /// </summary>\n    ForwarderError Error { get; }\n\n    /// <summary>\n    /// An Exception that occurred when forwarding the request to the destination, if any.\n    /// </summary>\n    Exception? Exception { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/IForwarderHttpClientFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// Provides a method to create instances of <see cref=\"HttpMessageInvoker\"/>\n/// for forwarding requests to an upstream server.\n/// </summary>\n/// <remarks>\n/// This is somewhat similarly to `System.Net.Http.IHttpClientFactory`,\n/// except that this factory class is meant for direct use,\n/// which the forwarder requires in order to keep separate pools for each cluster.\n/// </remarks>\npublic interface IForwarderHttpClientFactory\n{\n    /// <summary>\n    /// Creates and configures an <see cref=\"HttpMessageInvoker\"/> instance\n    /// that can be used for forwarding requests to an upstream server.\n    /// </summary>\n    /// <param name=\"context\">An <see cref=\"ForwarderHttpClientContext\"/> carrying old and new cluster configurations.</param>\n    /// <remarks>\n    /// <para>\n    /// A call to <see cref=\"CreateClient(ForwarderHttpClientContext)\"/> can return either\n    /// a new <see cref=\"HttpMessageInvoker\"/> instance or an old one if the configuration has not changed.\n    /// If the old configuration is null, a new <see cref=\"HttpMessageInvoker\"/> is always created.\n    /// The returned <see cref=\"HttpMessageInvoker\"/> instance MUST NOT be disposed\n    /// because it can be used concurrently by several in-flight requests.\n    /// </para>\n    /// </remarks>\n    HttpMessageInvoker CreateClient(ForwarderHttpClientContext context);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/IHttpForwarder.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// Forward an HTTP request to a chosen destination.\n/// </summary>\npublic interface IHttpForwarder\n{\n    /// <summary>\n    /// Forwards the incoming request to the destination server, and the response back to the client.\n    /// </summary>\n    /// <param name=\"context\">The HttpContext to forward.</param>\n    /// <param name=\"destinationPrefix\">The url prefix for where to forward the request to.</param>\n    /// <param name=\"httpClient\">The HTTP client used to forward the request.</param>\n    /// <param name=\"requestConfig\">Config for the outgoing request.</param>\n    /// <param name=\"transformer\">Request and response transforms. Use <see cref=\"HttpTransformer.Default\"/> if\n    /// custom transformations are not needed.</param>\n    /// <returns>The result of forwarding the request and response.</returns>\n    ValueTask<ForwarderError> SendAsync(HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient,\n        ForwarderRequestConfig requestConfig, HttpTransformer transformer);\n\n    /// <summary>\n    /// Forwards the incoming request to the destination server, and the response back to the client.\n    /// </summary>\n    /// <param name=\"context\">The HttpContext to forward.</param>\n    /// <param name=\"destinationPrefix\">The url prefix for where to forward the request to.</param>\n    /// <param name=\"httpClient\">The HTTP client used to forward the request.</param>\n    /// <param name=\"requestConfig\">Config for the outgoing request.</param>\n    /// <param name=\"transformer\">Request and response transforms. Use <see cref=\"HttpTransformer.Default\"/> if\n    /// custom transformations are not needed.</param>\n    /// <param name=\"cancellationToken\">A cancellation token that can be used to abort the request.</param>\n    /// <returns>The result of forwarding the request and response.</returns>\n    ValueTask<ForwarderError> SendAsync(HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient,\n        ForwarderRequestConfig requestConfig, HttpTransformer transformer, CancellationToken cancellationToken)\n        => SendAsync(context, destinationPrefix, httpClient, requestConfig, transformer);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/IHttpForwarderExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// Extensions methods for <see cref=\"IHttpForwarder\"/>.\n/// </summary>\npublic static class IHttpForwarderExtensions\n{\n    /// <summary>\n    /// Forwards the incoming request to the destination server, and the response back to the client.\n    /// </summary>\n    /// <param name=\"forwarder\">The forwarder instance.</param>\n    /// <param name=\"context\">The HttpContext to forward.</param>\n    /// <param name=\"destinationPrefix\">The url prefix for where to forward the request to.</param>\n    /// <param name=\"requestConfig\">Config for the outgoing request.</param>\n    /// <param name=\"requestTransform\">Transform function to apply to the forwarded request.</param>\n    /// <returns>The status of a forwarding operation.</returns>\n    public static ValueTask<ForwarderError> SendAsync(this IHttpForwarder forwarder, HttpContext context, string destinationPrefix,\n        ForwarderRequestConfig? requestConfig = null, Func<HttpContext, HttpRequestMessage, ValueTask>? requestTransform = null)\n    {\n        ArgumentNullException.ThrowIfNull(forwarder);\n        ArgumentNullException.ThrowIfNull(context);\n\n        requestConfig ??= ForwarderRequestConfig.Empty;\n        var transformer = requestTransform is null ? HttpTransformer.Default : new RequestTransformer(requestTransform);\n        var httpClientProvider = context.RequestServices.GetRequiredService<DirectForwardingHttpClientProvider>();\n\n        return forwarder.SendAsync(context, destinationPrefix, httpClientProvider.HttpClient, requestConfig, transformer);\n    }\n\n    /// <summary>\n    /// Forwards the incoming request to the destination server, and the response back to the client.\n    /// </summary>\n    /// <param name=\"forwarder\">The forwarder instance.</param>\n    /// <param name=\"context\">The HttpContext to forward.</param>\n    /// <param name=\"destinationPrefix\">The url prefix for where to forward the request to.</param>\n    /// <param name=\"httpClient\">The HTTP client used to forward the request.</param>\n    /// <returns>The status of a forwarding operation.</returns>\n    public static ValueTask<ForwarderError> SendAsync(this IHttpForwarder forwarder, HttpContext context, string destinationPrefix,\n        HttpMessageInvoker httpClient)\n    {\n        ArgumentNullException.ThrowIfNull(forwarder);\n\n        return forwarder.SendAsync(context, destinationPrefix, httpClient, ForwarderRequestConfig.Empty, HttpTransformer.Default);\n    }\n\n    /// <summary>\n    /// Forwards the incoming request to the destination server, and the response back to the client.\n    /// </summary>\n    /// <param name=\"forwarder\">The forwarder instance.</param>\n    /// <param name=\"context\">The HttpContext to forward.</param>\n    /// <param name=\"destinationPrefix\">The url prefix for where to forward the request to.</param>\n    /// <param name=\"httpClient\">The HTTP client used to forward the request.</param>\n    /// <param name=\"requestConfig\">Config for the outgoing request.</param>\n    /// <returns>The status of a forwarding operation.</returns>\n    public static ValueTask<ForwarderError> SendAsync(this IHttpForwarder forwarder, HttpContext context, string destinationPrefix,\n        HttpMessageInvoker httpClient, ForwarderRequestConfig requestConfig)\n    {\n        ArgumentNullException.ThrowIfNull(forwarder);\n\n        return forwarder.SendAsync(context, destinationPrefix, httpClient, requestConfig, HttpTransformer.Default);\n    }\n\n    /// <summary>\n    /// Forwards the incoming request to the destination server, and the response back to the client.\n    /// </summary>\n    /// <param name=\"forwarder\">The forwarder instance.</param>\n    /// <param name=\"context\">The HttpContext to forward.</param>\n    /// <param name=\"destinationPrefix\">The url prefix for where to forward the request to.</param>\n    /// <param name=\"httpClient\">The HTTP client used to forward the request.</param>\n    /// <param name=\"requestTransform\">Transform function to apply to the forwarded request.</param>\n    /// <returns>The status of a forwarding operation.</returns>\n    public static ValueTask<ForwarderError> SendAsync(this IHttpForwarder forwarder, HttpContext context, string destinationPrefix,\n        HttpMessageInvoker httpClient, Func<HttpContext, HttpRequestMessage, ValueTask> requestTransform)\n    {\n        return forwarder.SendAsync(context, destinationPrefix, httpClient, ForwarderRequestConfig.Empty, requestTransform);\n    }\n\n    /// <summary>\n    /// Forwards the incoming request to the destination server, and the response back to the client.\n    /// </summary>\n    /// <param name=\"forwarder\">The forwarder instance.</param>\n    /// <param name=\"context\">The HttpContext to forward.</param>\n    /// <param name=\"destinationPrefix\">The url prefix for where to forward the request to.</param>\n    /// <param name=\"httpClient\">The HTTP client used to forward the request.</param>\n    /// <param name=\"requestConfig\">Config for the outgoing request.</param>\n    /// <param name=\"requestTransform\">Transform function to apply to the forwarded request.</param>\n    /// <returns>The status of a forwarding operation.</returns>\n    public static ValueTask<ForwarderError> SendAsync(this IHttpForwarder forwarder, HttpContext context, string destinationPrefix,\n        HttpMessageInvoker httpClient, ForwarderRequestConfig requestConfig, Func<HttpContext, HttpRequestMessage, ValueTask> requestTransform)\n    {\n        return forwarder.SendAsync(context, destinationPrefix, httpClient, requestConfig, new RequestTransformer(requestTransform));\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/ProtocolHelper.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Net;\nusing System.Net.Http;\nusing System.Security.Cryptography;\nusing System.Text;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Net.Http.Headers;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\ninternal static class ProtocolHelper\n{\n    internal static readonly Version Http2Version = HttpVersion.Version20;\n    internal static readonly Version Http11Version = HttpVersion.Version11;\n\n    internal const string GrpcContentType = \"application/grpc\";\n\n    public static bool IsHttp2OrGreater(string protocol) => HttpProtocol.IsHttp2(protocol) || HttpProtocol.IsHttp3(protocol);\n\n    public static string GetVersionPolicy(HttpVersionPolicy policy)\n    {\n        return policy switch\n        {\n            HttpVersionPolicy.RequestVersionOrLower => \"RequestVersionOrLower\",\n            HttpVersionPolicy.RequestVersionOrHigher => \"RequestVersionOrHigher\",\n            HttpVersionPolicy.RequestVersionExact => \"RequestVersionExact\",\n            _ => throw new NotImplementedException(policy.ToString()),\n        };\n    }\n\n    /// <summary>\n    /// Checks whether the provided content type header value represents a gRPC request.\n    /// </summary>\n    public static bool IsGrpcContentType(string? contentType) =>\n        contentType is not null\n        && contentType.StartsWith(GrpcContentType, StringComparison.OrdinalIgnoreCase)\n        && MediaTypeHeaderValue.TryParse(contentType, out var mediaType)\n        && mediaType.MatchesMediaType(GrpcContentType);\n\n    /// <summary>\n    /// Creates a security key for sending in the Sec-WebSocket-Key header.\n    /// </summary>\n    internal static string CreateSecWebSocketKey()\n    {\n        // The value of this header field MUST be a nonce consisting of a randomly selected 16-byte\n        // value that has been base64-encoded\n        Span<byte> bytes = stackalloc byte[16];\n        // Base64-encode a new Guid's bytes to get the security key\n        var success = Guid.NewGuid().TryWriteBytes(bytes);\n        Debug.Assert(success);\n        var secKey = Convert.ToBase64String(bytes);\n        return secKey;\n    }\n\n    internal static bool CheckSecWebSocketKey(string? key)\n    {\n        // The value of this header field MUST be a nonce consisting of a randomly selected 16-byte\n        // value that has been base64-encoded\n        return !string.IsNullOrEmpty(key) && key.Length == 24;\n    }\n\n    /// <summary>\n    /// Creates the Accept response to a given security key for sending in or verifying the Sec-WebSocket-Accept header value.\n    /// </summary>\n    internal static string CreateSecWebSocketAccept(string? key)\n    {\n        if (!CheckSecWebSocketKey(key))\n        {\n            // This could happen if a custom message handler modified headers incorrectly.\n            Debug.Fail(\"This should have already been validated elsewhere\");\n            throw new InvalidOperationException(\"Unexpected Sec-WebSocket-Key header format.\");\n        }\n\n        // GUID appended by the server as part of the security key response.  Defined in the RFC.\n        var wsServerGuidBytes = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"u8;\n        Span<byte> bytes = stackalloc byte[24 /* Base64 guid length */ + wsServerGuidBytes.Length];\n\n        // Get the corresponding ASCII bytes for seckey+wsServerGuidBytes\n        var encodedSecKeyLength = Encoding.ASCII.GetBytes(key, bytes);\n        Debug.Assert(encodedSecKeyLength == 24);\n        wsServerGuidBytes.CopyTo(bytes.Slice(encodedSecKeyLength));\n\n        // Hash the seckey+wsServerGuidBytes bytes\n#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms -- the spec demands SHA1 in this case.\n        SHA1.TryHashData(bytes, bytes, out var bytesWritten);\n#pragma warning restore CA5350\n        Debug.Assert(bytesWritten == 20 /* SHA1 hash length */);\n        var accept = Convert.ToBase64String(bytes[..bytesWritten]);\n\n        // Return the security key + accept value\n        return accept;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/RequestTransformer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\ninternal sealed class RequestTransformer : HttpTransformer\n{\n    private readonly Func<HttpContext, HttpRequestMessage, ValueTask> _requestTransform;\n\n    public RequestTransformer(Func<HttpContext, HttpRequestMessage, ValueTask> requestTransform)\n    {\n        _requestTransform = requestTransform;\n    }\n\n    public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken)\n    {\n        await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken);\n        await _requestTransform(httpContext, proxyRequest);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/RequestUtilities.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Buffers;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Runtime.CompilerServices;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing Microsoft.Net.Http.Headers;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// APIs that can be used when transforming requests.\n/// </summary>\npublic static class RequestUtilities\n{\n    // https://datatracker.ietf.org/doc/html/rfc3986/#appendix-A\n    // pchar         = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n    // pct-encoded   = \"%\" HEXDIG HEXDIG\n    // unreserved    = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n    // reserved      = gen-delims / sub-delims\n    // gen-delims    = \":\" / \"/\" / \"?\" / \"#\" / \"[\" / \"]\" / \"@\"\n    // sub-delims    = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n    private static readonly SearchValues<char> s_validPathChars =\n        SearchValues.Create(\"!$&'()*+,-./0123456789:;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~\");\n\n    /// <summary>\n    /// Converts the given HTTP method (usually obtained from <see cref=\"HttpRequest.Method\"/>)\n    /// into the corresponding <see cref=\"HttpMethod\"/> static instance.\n    /// </summary>\n    internal static HttpMethod GetHttpMethod(string method) => method switch\n    {\n        string mth when HttpMethods.IsGet(mth) => HttpMethod.Get,\n        string mth when HttpMethods.IsPost(mth) => HttpMethod.Post,\n        string mth when HttpMethods.IsPut(mth) => HttpMethod.Put,\n        string mth when HttpMethods.IsDelete(mth) => HttpMethod.Delete,\n        string mth when HttpMethods.IsOptions(mth) => HttpMethod.Options,\n        string mth when HttpMethods.IsHead(mth) => HttpMethod.Head,\n        string mth when HttpMethods.IsPatch(mth) => HttpMethod.Patch,\n        string mth when HttpMethods.IsTrace(mth) => HttpMethod.Trace,\n        // NOTE: Proxying \"CONNECT\" is not supported (by design!)\n        string mth when HttpMethods.IsConnect(mth) => throw new NotSupportedException($\"Unsupported request method '{method}'.\"),\n        _ => new HttpMethod(method)\n    };\n\n    internal static bool ShouldSkipRequestHeader(string headerName)\n    {\n        if (_headersToExclude.Contains(headerName))\n        {\n            return true;\n        }\n\n        // Filter out HTTP/2 pseudo headers like \":method\" and \":path\", those go into other fields.\n        if (headerName.StartsWith(':'))\n        {\n            return true;\n        }\n\n        return false;\n    }\n\n    internal static bool ShouldSkipResponseHeader(string headerName)\n    {\n        return _headersToExclude.Contains(headerName);\n    }\n\n    private static readonly FrozenSet<string> _headersToExclude = new HashSet<string>(17, StringComparer.OrdinalIgnoreCase)\n    {\n        HeaderNames.Connection,\n        HeaderNames.TransferEncoding,\n        HeaderNames.KeepAlive,\n        HeaderNames.Upgrade,\n        HeaderNames.ProxyConnection,\n        HeaderNames.ProxyAuthenticate,\n        \"Proxy-Authentication-Info\",\n        HeaderNames.ProxyAuthorization,\n        \"Proxy-Features\",\n        \"Proxy-Instruction\",\n        \"Security-Scheme\",\n        \"ALPN\",\n        \"Close\",\n        \"HTTP2-Settings\",\n        HeaderNames.UpgradeInsecureRequests,\n        HeaderNames.TE,\n        HeaderNames.AltSvc,\n    }.ToFrozenSet(StringComparer.OrdinalIgnoreCase);\n\n    // Headers marked as HttpHeaderType.Content in\n    // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs\n    private static readonly FrozenSet<string> _contentHeaders = new HashSet<string>(11, StringComparer.OrdinalIgnoreCase)\n    {\n        HeaderNames.Allow,\n        HeaderNames.ContentDisposition,\n        HeaderNames.ContentEncoding,\n        HeaderNames.ContentLanguage,\n        HeaderNames.ContentLength,\n        HeaderNames.ContentLocation,\n        HeaderNames.ContentMD5,\n        HeaderNames.ContentRange,\n        HeaderNames.ContentType,\n        HeaderNames.Expires,\n        HeaderNames.LastModified\n    }.ToFrozenSet(StringComparer.OrdinalIgnoreCase);\n\n    /// <summary>\n    /// Appends the given path and query to the destination prefix while avoiding duplicate '/'.\n    /// </summary>\n    /// <param name=\"destinationPrefix\">The scheme, host, port, and optional path base for the destination server.\n    /// e.g. \"http://example.com:80/path/prefix\"</param>\n    /// <param name=\"path\">The path to append.</param>\n    /// <param name=\"query\">The query to append</param>\n    public static Uri MakeDestinationAddress(string destinationPrefix, PathString path, QueryString query)\n    {\n        ReadOnlySpan<char> prefixSpan = destinationPrefix;\n\n        if (path.HasValue && destinationPrefix.EndsWith('/'))\n        {\n            // When PathString has a value it always starts with a '/'. Avoid double slashes when concatenating.\n            prefixSpan = prefixSpan[0..^1];\n        }\n\n        var targetAddress = string.Concat(prefixSpan, EncodePath(path), query.ToUriComponent());\n\n        return new Uri(targetAddress, UriKind.Absolute);\n    }\n\n    // This isn't using PathString.ToUriComponent() because it doesn't round trip some escape sequences the way we want.\n    internal static string EncodePath(PathString path)\n    {\n        var value = path.Value;\n\n        if (string.IsNullOrEmpty(value))\n        {\n            return string.Empty;\n        }\n\n        // Check if any escaping is required.\n        var indexOfInvalidChar = value.AsSpan().IndexOfAnyExcept(s_validPathChars);\n\n        return indexOfInvalidChar < 0\n            ? value\n            : EncodePath(value, indexOfInvalidChar);\n    }\n\n    private static string EncodePath(string value, int i)\n    {\n        var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);\n\n        var start = 0;\n        var count = i;\n        var requiresEscaping = false;\n\n        while (i < value.Length)\n        {\n            if (s_validPathChars.Contains(value[i]))\n            {\n                if (requiresEscaping)\n                {\n                    // the current segment requires escape\n                    builder.Append(Uri.EscapeDataString(value.Substring(start, count)));\n\n                    requiresEscaping = false;\n                    start = i;\n                    count = 0;\n                }\n\n                count++;\n                i++;\n            }\n            else\n            {\n                if (!requiresEscaping)\n                {\n                    // the current segment doesn't require escape\n                    builder.Append(value.AsSpan(start, count));\n\n                    requiresEscaping = true;\n                    start = i;\n                    count = 0;\n                }\n\n                count++;\n                i++;\n            }\n        }\n\n        Debug.Assert(count > 0);\n\n        if (requiresEscaping)\n        {\n            builder.Append(Uri.EscapeDataString(value.Substring(start, count)));\n        }\n        else\n        {\n            builder.Append(value.AsSpan(start, count));\n        }\n\n        return builder.ToString();\n    }\n\n    // Note: HttpClient.SendAsync will end up sending the union of\n    // HttpRequestMessage.Headers and HttpRequestMessage.Content.Headers.\n    // We don't really care where the proxied headers appear among those 2,\n    // as long as they appear in one (and only one, otherwise they would be duplicated).\n    // Some headers may only appear on HttpContentHeaders, in which case we inject\n    // an EmptyHttpContent - dummy 0-length container only used for headers.\n    internal static void AddHeader(HttpRequestMessage request, string headerName, StringValues value)\n    {\n        if (value.Count == 1)\n        {\n            string headerValue = value!;\n\n            if (ContainsNewLines(headerValue))\n            {\n                // TODO: Log\n                return;\n            }\n\n            if (!request.Headers.TryAddWithoutValidation(headerName, headerValue))\n            {\n                if (request.Content is null && _contentHeaders.Contains(headerName))\n                {\n                    request.Content = new EmptyHttpContent();\n                }\n\n                var added = request.Content?.Headers.TryAddWithoutValidation(headerName, headerValue);\n                // TODO: Log\n                Debug.Assert(added.GetValueOrDefault(), $\"A header was dropped; {headerName}: {headerValue}\");\n            }\n        }\n        else\n        {\n            string[] headerValues = value!;\n\n            foreach (var headerValue in headerValues)\n            {\n                if (ContainsNewLines(headerValue))\n                {\n                    // TODO: Log\n                    return;\n                }\n            }\n\n            if (!request.Headers.TryAddWithoutValidation(headerName, headerValues))\n            {\n                if (request.Content is null && _contentHeaders.Contains(headerName))\n                {\n                    request.Content = new EmptyHttpContent();\n                }\n\n                var added = request.Content?.Headers.TryAddWithoutValidation(headerName, headerValues);\n                // TODO: Log\n                Debug.Assert(added.GetValueOrDefault(), $\"A header was dropped; {headerName}: {string.Join(\", \", headerValues)}\");\n            }\n        }\n\n#if DEBUG\n        if (request.Content is EmptyHttpContent content && content.Headers.TryGetValues(HeaderNames.ContentLength, out var contentLength))\n        {\n            Debug.Assert(contentLength.Single() == \"0\", \"An actual content should have been set\");\n        }\n#endif\n\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        static bool ContainsNewLines(string value) => value.AsSpan().IndexOfAny('\\r', '\\n') >= 0;\n    }\n\n    internal static void RemoveHeader(HttpRequestMessage request, string headerName)\n    {\n        if (_contentHeaders.Contains(headerName))\n        {\n            request.Content?.Headers.Remove(headerName);\n        }\n        else\n        {\n            request.Headers.Remove(headerName);\n        }\n    }\n\n    [MethodImpl(MethodImplOptions.AggressiveInlining)]\n    internal static StringValues Concat(in StringValues existing, in HeaderStringValues values)\n    {\n        if (values.Count <= 1)\n        {\n            return StringValues.Concat(existing, values.ToString());\n        }\n        else\n        {\n            return ConcatSlow(existing, values);\n        }\n\n        static StringValues ConcatSlow(in StringValues existing, in HeaderStringValues values)\n        {\n            Debug.Assert(values.Count > 1);\n\n            var count = existing.Count;\n            var newArray = new string[count + values.Count];\n\n            if (count == 1)\n            {\n                newArray[0] = existing.ToString();\n            }\n            else\n            {\n                existing.ToArray().CopyTo(newArray, 0);\n            }\n\n            foreach (var value in values)\n            {\n                newArray[count++] = value;\n            }\n            Debug.Assert(count == newArray.Length);\n\n            return newArray;\n        }\n    }\n\n    [MethodImpl(MethodImplOptions.AggressiveInlining)]\n    internal static bool TryGetValues(HttpHeaders headers, string headerName, out StringValues values)\n    {\n        if (headers.NonValidated.TryGetValues(headerName, out var headerStringValues))\n        {\n            if (headerStringValues.Count <= 1)\n            {\n                values = headerStringValues.ToString();\n            }\n            else\n            {\n                values = ToArray(headerStringValues);\n            }\n            return true;\n        }\n\n        static StringValues ToArray(in HeaderStringValues values)\n        {\n            var array = new string[values.Count];\n            var i = 0;\n            foreach (var value in values)\n            {\n                array[i++] = value;\n            }\n            Debug.Assert(i == array.Length);\n            return array;\n        }\n\n        values = default;\n        return false;\n    }\n\n    internal static bool IsResponseSet(HttpResponse response)\n    {\n        return response.StatusCode != StatusCodes.Status200OK\n            || response.HasStarted;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/ReverseProxyPropagator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Net.Http;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// Removes existing headers and then delegates to the inner propagator.\n/// </summary>\npublic sealed class ReverseProxyPropagator : DistributedContextPropagator\n{\n    private readonly DistributedContextPropagator _innerPropagator;\n    private readonly string[] _headersToRemove;\n\n    /// <summary>\n    /// ReverseProxyPropagator removes headers pointed out in innerPropagator.\n    /// </summary>\n    public ReverseProxyPropagator(DistributedContextPropagator innerPropagator)\n    {\n        ArgumentNullException.ThrowIfNull(innerPropagator);\n        _innerPropagator = innerPropagator;\n        _headersToRemove = _innerPropagator.Fields.ToArray();\n    }\n\n    public override void Inject(Activity? activity, object? carrier, PropagatorSetterCallback? setter)\n    {\n        if (carrier is HttpRequestMessage message)\n        {\n            var headers = message.Headers;\n\n            foreach (var header in _headersToRemove)\n            {\n                headers.Remove(header);\n            }\n        }\n\n        _innerPropagator.Inject(activity, carrier, setter);\n    }\n\n    public override void ExtractTraceIdAndState(object? carrier, PropagatorGetterCallback? getter, out string? traceId, out string? traceState) =>\n        _innerPropagator.ExtractTraceIdAndState(carrier, getter, out traceId, out traceState);\n\n    public override IEnumerable<KeyValuePair<string, string?>>? ExtractBaggage(object? carrier, PropagatorGetterCallback? getter) =>\n        _innerPropagator.ExtractBaggage(carrier, getter);\n\n    public override IReadOnlyCollection<string> Fields => _innerPropagator.Fields;\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/StreamCopier.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Buffers;\nusing System.Diagnostics;\nusing System.Diagnostics.Tracing;\nusing System.IO;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// A stream copier that captures errors.\n/// </summary>\ninternal static class StreamCopier\n{\n    // Based on performance investigations, see https://github.com/dotnet/yarp/pull/330#issuecomment-758851852.\n    private const int DefaultBufferSize = 65536;\n    public const long UnknownLength = -1;\n\n    public static ValueTask<(StreamCopyResult, Exception?)> CopyAsync(bool isRequest, Stream input, Stream output, long promisedContentLength, TimeProvider timeProvider, ActivityCancellationTokenSource activityToken, CancellationToken cancellation)\n        => CopyAsync(isRequest, input, output, promisedContentLength, timeProvider, activityToken, autoFlush: false, cancellation);\n\n    public static ValueTask<(StreamCopyResult, Exception?)> CopyAsync(bool isRequest, Stream input, Stream output, long promisedContentLength, TimeProvider timeProvider, ActivityCancellationTokenSource activityToken, bool autoFlush, CancellationToken cancellation)\n    {\n        Debug.Assert(input is not null);\n        Debug.Assert(output is not null);\n        Debug.Assert(timeProvider is not null);\n        Debug.Assert(activityToken is not null);\n\n        // Avoid capturing 'isRequest' and 'timeProvider' in the state machine when telemetry is disabled\n        var telemetry = ForwarderTelemetry.Log.IsEnabled(EventLevel.Informational, EventKeywords.All)\n            ? new StreamCopierTelemetry(isRequest, timeProvider)\n            : null;\n\n        return CopyAsync(input, output, promisedContentLength, telemetry, activityToken, autoFlush, cancellation);\n    }\n\n    private static async ValueTask<(StreamCopyResult, Exception?)> CopyAsync(Stream input, Stream output, long promisedContentLength, StreamCopierTelemetry? telemetry, ActivityCancellationTokenSource activityToken, bool autoFlush, CancellationToken cancellation)\n    {\n        var buffer = ArrayPool<byte>.Shared.Rent(DefaultBufferSize);\n        var read = 0;\n        long contentLength = 0;\n        try\n        {\n            while (true)\n            {\n                read = 0;\n\n                // Issue a zero-byte read to the input stream to defer buffer allocation until data is available.\n                // Note that if the underlying stream does not supporting blocking on zero byte reads, then this will\n                // complete immediately and won't save any memory, but will still function correctly.\n                var zeroByteReadTask = input.ReadAsync(Memory<byte>.Empty, cancellation);\n                if (zeroByteReadTask.IsCompletedSuccessfully)\n                {\n                    // Consume the ValueTask's result in case it is backed by an IValueTaskSource\n                    _ = zeroByteReadTask.Result;\n                }\n                else\n                {\n                    // Take care not to return the same buffer to the pool twice in case zeroByteReadTask throws\n                    var bufferToReturn = buffer;\n                    buffer = null;\n                    ArrayPool<byte>.Shared.Return(bufferToReturn);\n\n                    await zeroByteReadTask;\n\n                    buffer = ArrayPool<byte>.Shared.Rent(DefaultBufferSize);\n                }\n\n                read = await input.ReadAsync(buffer.AsMemory(), cancellation);\n                contentLength += read;\n                // Normally this is enforced by the server, but it could get out of sync if something in the proxy modified the body.\n                if (promisedContentLength != UnknownLength && contentLength > promisedContentLength)\n                {\n                    return (StreamCopyResult.InputError, new InvalidOperationException(\"More bytes received than the specified Content-Length.\"));\n                }\n\n                telemetry?.AfterRead(contentLength);\n\n                // Success, reset the activity monitor.\n                activityToken.ResetTimeout();\n\n                // End of the source stream.\n                if (read == 0)\n                {\n                    if (promisedContentLength == UnknownLength || contentLength == promisedContentLength)\n                    {\n                        return (StreamCopyResult.Success, null);\n                    }\n                    else\n                    {\n                        // This can happen if something in the proxy consumes or modifies part or all of the request body before proxying.\n                        return (StreamCopyResult.InputError,\n                            new InvalidOperationException($\"Sent {contentLength} request content bytes, but Content-Length promised {promisedContentLength}.\"));\n                    }\n                }\n\n                await output.WriteAsync(buffer.AsMemory(0, read), cancellation);\n                if (autoFlush)\n                {\n                    // HttpClient doesn't always flush outgoing data unless the buffer is full or the caller asks.\n                    // This is a problem for streaming protocols like WebSockets and gRPC.\n                    await output.FlushAsync(cancellation);\n                }\n\n                telemetry?.AfterWrite();\n\n                // Success, reset the activity monitor.\n                activityToken.ResetTimeout();\n            }\n        }\n        catch (Exception ex)\n        {\n            if (read == 0)\n            {\n                telemetry?.AfterRead(contentLength);\n            }\n            else\n            {\n                telemetry?.AfterWrite();\n            }\n\n            if (activityToken.CancelledByLinkedToken)\n            {\n                return (StreamCopyResult.Canceled, ex);\n            }\n\n            // If the activity timeout triggered while reading or writing, blame the sender or receiver.\n            var result = read == 0 ? StreamCopyResult.InputError : StreamCopyResult.OutputError;\n            return (result, ex);\n        }\n        finally\n        {\n            if (buffer is not null)\n            {\n                ArrayPool<byte>.Shared.Return(buffer);\n            }\n\n            telemetry?.Stop();\n        }\n    }\n\n    private sealed class StreamCopierTelemetry\n    {\n        private readonly bool _isRequest;\n        private readonly TimeProvider _timeProvider;\n        private long _contentLength;\n        private long _iops;\n        private long _readTime;\n        private long _writeTime;\n        private long _firstReadTime;\n        private long _lastTime;\n        private long _nextTransferringEvent;\n\n        public StreamCopierTelemetry(bool isRequest, TimeProvider timeProvider)\n        {\n            ArgumentNullException.ThrowIfNull(timeProvider);\n\n            _isRequest = isRequest;\n            _timeProvider = timeProvider;\n            _firstReadTime = -1;\n\n            ForwarderTelemetry.Log.ForwarderStage(isRequest ? ForwarderStage.RequestContentTransferStart : ForwarderStage.ResponseContentTransferStart);\n\n            _lastTime = timeProvider.GetTimestamp();\n            _nextTransferringEvent = _lastTime + _timeProvider.TimestampFrequency;\n        }\n\n        public void AfterRead(long contentLength)\n        {\n            _contentLength = contentLength;\n            _iops++;\n\n            var readStop = _timeProvider.GetTimestamp();\n            var currentReadTime = readStop - _lastTime;\n            _lastTime = readStop;\n            _readTime += currentReadTime;\n            if (_firstReadTime < 0)\n            {\n                _firstReadTime = currentReadTime;\n            }\n        }\n\n        public void AfterWrite()\n        {\n            var writeStop = _timeProvider.GetTimestamp();\n            _writeTime += writeStop - _lastTime;\n            _lastTime = writeStop;\n\n            if (writeStop >= _nextTransferringEvent)\n            {\n                ForwarderTelemetry.Log.ContentTransferring(\n                    _isRequest,\n                    _contentLength,\n                    _iops,\n                    _timeProvider.GetElapsedTime(0, _readTime).Ticks,\n                    _timeProvider.GetElapsedTime(0, _writeTime).Ticks);\n\n                // Avoid attributing the time taken by logging ContentTransferring to the next read call\n                _lastTime = _timeProvider.GetTimestamp();\n                _nextTransferringEvent = _lastTime + _timeProvider.TimestampFrequency;\n            }\n        }\n\n        public void Stop()\n        {\n            ForwarderTelemetry.Log.ContentTransferred(\n                _isRequest,\n                _contentLength,\n                _iops,\n                _timeProvider.GetElapsedTime(0, _readTime).Ticks,\n                _timeProvider.GetElapsedTime(0, _writeTime).Ticks,\n                _timeProvider.GetElapsedTime(0, Math.Max(0, _firstReadTime)).Ticks);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/StreamCopyHttpContent.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.AspNetCore.Server.Kestrel.Core.Features;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\n/// <summary>\n/// Custom <see cref=\"HttpContent\"/>\n/// used to proxy the incoming request body to the upstream server.\n/// </summary>\n/// <remarks>\n/// <para>\n/// By implementing a custom <see cref=\"HttpContent\"/>, we are able to execute\n/// our custom code for all HTTP protocol versions.\n/// See the remarks section of <see cref=\"SerializeToStreamAsync(Stream, TransportContext)\"/>\n/// for more details.\n/// </para>\n/// <para>\n/// <see cref=\"HttpContent\"/> declares an internal property `AllowDuplex`\n/// which, when set to true, causes <see cref=\"HttpClient\"/> and friends\n/// to NOT tie up the request body stream operations to the same cancellation token\n/// that is passed to <see cref=\"HttpClient.SendAsync(HttpRequestMessage, HttpCompletionOption, CancellationToken)\"/>.\n/// </para>\n/// <para>\n/// When proxying duplex channels such as HTTP/2, gRPC,\n/// we need `HttpContent.AllowDuplex` to be true.\n/// It so happens to be by default on .NET Core 3.1. Should that ever change,\n/// this class will need to be updated.\n/// </para>\n/// </remarks>\ninternal sealed class StreamCopyHttpContent : HttpContent\n{\n    private readonly HttpContext _context;\n    // HttpClient's machinery keeps an internal buffer that doesn't get flushed to the socket on every write.\n    // Some protocols (e.g. gRPC) may rely on specific bytes being sent, and HttpClient's buffering would prevent it.\n    private bool _isStreamingRequest;\n    private readonly TimeProvider _timeProvider;\n    private readonly ILogger _logger;\n    private readonly ActivityCancellationTokenSource _activityToken;\n    private readonly TaskCompletionSource<(StreamCopyResult, Exception?)> _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);\n    private int _started;\n\n    public StreamCopyHttpContent(HttpContext context, bool isStreamingRequest, TimeProvider timeProvider, ILogger logger, ActivityCancellationTokenSource activityToken)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n        ArgumentNullException.ThrowIfNull(timeProvider);\n\n        _context = context;\n        _isStreamingRequest = isStreamingRequest;\n        _timeProvider = timeProvider;\n        _logger = logger;\n        _activityToken = activityToken;\n    }\n\n    /// <summary>\n    /// Gets a <see cref=\"Task\"/> that completes in successful or failed state\n    /// mimicking the result of SerializeToStreamAsync.\n    /// </summary>\n    public Task<(StreamCopyResult, Exception?)> ConsumptionTask => _tcs.Task;\n\n    /// <summary>\n    /// Gets a value indicating whether consumption of this content has begun.\n    /// Property <see cref=\"ConsumptionTask\"/> can be used to track the asynchronous outcome of the operation.\n    /// </summary>\n    /// <remarks>\n    /// When used as an outgoing request content with <see cref=\"HttpClient\"/>,\n    /// this should always be true by the time the task returned by\n    /// <see cref=\"HttpClient.SendAsync(HttpRequestMessage, HttpCompletionOption, CancellationToken)\"/>\n    /// completes, even when using <see cref=\"HttpCompletionOption.ResponseHeadersRead\"/>.\n    /// </remarks>\n    public bool Started => Volatile.Read(ref _started) == 1;\n\n    public bool InProgress => Started && !ConsumptionTask.IsCompleted;\n\n    /// <summary>\n    /// Copies bytes from the stream provided in our constructor into the target <paramref name=\"stream\"/>.\n    /// </summary>\n    /// <remarks>\n    /// This is used internally by HttpClient.SendAsync to send the request body.\n    /// Here's the sequence of events as of commit 17300169760c61a90cab8d913636c1058a30a8c1 (https://github.com/dotnet/corefx -- tag v3.1.1).\n    ///\n    /// <code>\n    /// HttpClient.SendAsync -->\n    /// HttpMessageInvoker.SendAsync -->\n    /// HttpClientHandler.SendAsync -->\n    /// SocketsHttpHandler.SendAsync -->\n    /// HttpConnectionHandler.SendAsync -->\n    /// HttpConnectionPoolManager.SendAsync -->\n    /// HttpConnectionPool.SendAsync --> ... -->\n    /// {\n    ///     HTTP/1.1: HttpConnection.SendAsync -->\n    ///               HttpConnection.SendAsyncCore -->\n    ///               HttpConnection.SendRequestContentAsync -->\n    ///               HttpContent.CopyToAsync\n    ///\n    ///     HTTP/2:   Http2Connection.SendAsync -->\n    ///               Http2Stream.SendRequestBodyAsync -->\n    ///               HttpContent.CopyToAsync\n    ///\n    ///     /* Only in .NET 5:\n    ///     HTTP/3:   Http3Connection.SendAsync -->\n    ///               Http3Connection.SendWithoutWaitingAsync -->\n    ///               Http3RequestStream.SendAsync -->\n    ///               Http3RequestStream.SendContentAsync -->\n    ///               HttpContent.CopyToAsync\n    ///     */\n    /// }\n    ///\n    /// HttpContent.CopyToAsync -->\n    /// HttpContent.SerializeToStreamAsync (bingo!)\n    /// </code>\n    ///\n    /// Conclusion: by overriding HttpContent.SerializeToStreamAsync,\n    /// we have full control over pumping bytes to the target stream for all protocols\n    /// (except Web Sockets, which is handled separately).\n    /// </remarks>\n    protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context)\n    {\n        return SerializeToStreamAsync(stream, context, CancellationToken.None);\n    }\n\n    protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken)\n    {\n        if (Interlocked.Exchange(ref _started, 1) == 1)\n        {\n            throw new InvalidOperationException(\"Stream was already consumed.\");\n        }\n\n        // The cancellationToken that is passed to this method is:\n        // On HTTP/1.1: Linked HttpContext.RequestAborted + Request Timeout\n        // On HTTP/2.0: SocketsHttpHandler error / the server wants us to stop sending content / H2 connection closed\n        // _cancellation will be the same as cancellationToken for HTTP/1.1, so we can avoid the overhead of linking them\n        CancellationTokenSource? linkedCts = null;\n\n        if (_activityToken.Token == cancellationToken)\n        {\n            // We're talking to the destination via HTTP/1.1, so this can't be a streaming gRPC request.\n            _isStreamingRequest = false;\n            // TODO: Log if _isStreamingRequest is true? Something went wrong with protocol selection.\n        }\n        else\n        {\n            Debug.Assert(cancellationToken.CanBeCanceled);\n            linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_activityToken.Token, cancellationToken);\n            cancellationToken = linkedCts.Token;\n\n            if (_isStreamingRequest)\n            {\n                DisableMinRequestBodyDataRateAndMaxRequestBodySize(_context);\n            }\n        }\n\n        try\n        {\n            // Immediately flush request stream to send headers\n            // https://github.com/dotnet/corefx/issues/39586#issuecomment-516210081\n            try\n            {\n                await stream.FlushAsync(cancellationToken);\n            }\n            catch (Exception ex)\n            {\n                var canceled = ex is OperationCanceledException || _activityToken.CancelledByLinkedToken;\n                _tcs.TrySetResult((canceled ? StreamCopyResult.Canceled : StreamCopyResult.OutputError, ex));\n                return;\n            }\n\n            // Check that the content-length matches the request body size. This can be removed in .NET 7 now that SocketsHttpHandler\n            // enforces this: https://github.com/dotnet/runtime/issues/62258.\n            //\n            // Note on `_isStreamingRequest`:\n            // The.NET Core HttpClient stack keeps its own buffers on top of the underlying outgoing connection socket.\n            // We flush those buffers down to the socket on every write when this is set,\n            // but it does NOT result in calls to flush on the underlying socket.\n            // This is necessary because we proxy http2 transparently,\n            // and we are deliberately unaware of packet structure used e.g. in gRPC duplex channels.\n            // Because the sockets aren't flushed, the perf impact of this choice is expected to be small.\n            // Future: It may be wise to set this to true for *all* http2 incoming requests,\n            // but for now, out of an abundance of caution, we only do it for requests that look like gRPC.\n            var (result, error) = await StreamCopier.CopyAsync(isRequest: true, _context.Request.Body, stream,\n                Headers.ContentLength ?? StreamCopier.UnknownLength, _timeProvider, _activityToken, _isStreamingRequest, cancellationToken);\n            _tcs.TrySetResult((result, error));\n\n            // Check for errors that weren't the result of the destination failing.\n            // We have to throw something here so the transport knows the body is incomplete.\n            // We can't re-throw the original exception since that would cause concurrency issues.\n            // We need to wrap it.\n            if (result == StreamCopyResult.InputError)\n            {\n                throw new IOException(\"An error occurred when reading the request body from the client.\", error);\n            }\n            if (result == StreamCopyResult.Canceled)\n            {\n                throw new OperationCanceledException(\"The request body copy was canceled.\", error);\n            }\n        }\n        finally\n        {\n            linkedCts?.Dispose();\n        }\n    }\n\n    // this is used internally by HttpContent.ReadAsStreamAsync(...)\n    protected override Task<Stream> CreateContentReadStreamAsync()\n    {\n        // Nobody should be calling this...\n        throw new NotImplementedException();\n    }\n\n    protected override bool TryComputeLength(out long length)\n    {\n        // We can't know the length of the content being pushed to the output stream.\n        length = -1;\n        return false;\n    }\n\n    /// <summary>\n    /// Disable some ASP .NET Core server limits so that we can handle long-running gRPC requests unconstrained.\n    /// Note that the gRPC server implementation on ASP .NET Core does the same for client-streaming and duplex methods.\n    /// Since in Gateway we have no way to determine if the current request requires client-streaming or duplex comm,\n    /// we do this for *all* incoming requests that look like they might be gRPC.\n    /// </summary>\n    /// <remarks>\n    /// Inspired on\n    /// <see href=\"https://github.com/grpc/grpc-dotnet/blob/3ce9b104524a4929f5014c13cd99ba9a1c2431d4/src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerCallHandlerBase.cs#L127\"/>.\n    /// </remarks>\n    private void DisableMinRequestBodyDataRateAndMaxRequestBodySize(HttpContext httpContext)\n    {\n        var minRequestBodyDataRateFeature = httpContext.Features.Get<IHttpMinRequestBodyDataRateFeature>();\n        if (minRequestBodyDataRateFeature is not null)\n        {\n            minRequestBodyDataRateFeature.MinDataRate = null;\n        }\n\n        var maxRequestBodySizeFeature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();\n        if (maxRequestBodySizeFeature is not null)\n        {\n            if (!maxRequestBodySizeFeature.IsReadOnly)\n            {\n                maxRequestBodySizeFeature.MaxRequestBodySize = null;\n            }\n            else\n            {\n                // IsReadOnly could be true if middleware has already started reading the request body\n                // In that case we can't disable the max request body size for the request stream\n                _logger.LogWarning(\"Unable to disable max request body size.\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Forwarder/StreamCopyResult.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\ninternal enum StreamCopyResult\n{\n    Success,\n    InputError,\n    OutputError,\n    Canceled\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/ActiveHealthCheckMonitor.Log.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.ReverseProxy.Health;\n\ninternal partial class ActiveHealthCheckMonitor\n{\n    private static class Log\n    {\n        private static readonly Action<ILogger, Exception> _explicitActiveCheckOfAllClustersHealthFailed = LoggerMessage.Define(\n            LogLevel.Error,\n            EventIds.ExplicitActiveCheckOfAllClustersHealthFailed,\n            \"An explicitly started active check of all clusters health failed.\");\n\n        private static readonly Action<ILogger, string, Exception> _activeHealthProbingFailedOnCluster = LoggerMessage.Define<string>(\n            LogLevel.Error,\n            EventIds.ActiveHealthProbingFailedOnCluster,\n            \"Active health probing failed on cluster `{clusterId}`.\");\n\n        private static readonly Action<ILogger, string, Exception> _errorOccurredDuringActiveHealthProbingShutdownOnCluster = LoggerMessage.Define<string>(\n            LogLevel.Error,\n            EventIds.ErrorOccurredDuringActiveHealthProbingShutdownOnCluster,\n            \"An error occurred during shutdown of an active health probing on cluster `{clusterId}`.\");\n\n        private static readonly Action<ILogger, string, string, Exception> _activeHealthProbeConstructionFailedOnCluster = LoggerMessage.Define<string, string>(\n            LogLevel.Error,\n            EventIds.ActiveHealthProbeConstructionFailedOnCluster,\n            \"Construction of an active health probe for destination `{destinationId}` on cluster `{clusterId}` failed.\");\n\n        private static readonly Action<ILogger, string, string, Exception?> _activeHealthProbeCancelledOnDestination = LoggerMessage.Define<string, string>(\n            LogLevel.Debug,\n            EventIds.ActiveHealthProbeCancelledOnDestination,\n            \"Active health probing for destination `{destinationId}` on cluster `{clusterId}` was cancelled.\");\n\n        private static readonly Action<ILogger, string, Exception?> _startingActiveHealthProbingOnCluster = LoggerMessage.Define<string>(\n            LogLevel.Debug,\n            EventIds.StartingActiveHealthProbingOnCluster,\n            \"Starting active health check probing on cluster `{clusterId}`.\");\n\n        private static readonly Action<ILogger, string, Exception?> _stoppedActiveHealthProbingOnCluster = LoggerMessage.Define<string>(\n            LogLevel.Debug,\n            EventIds.StoppedActiveHealthProbingOnCluster,\n            \"Active health check probing on cluster `{clusterId}` has stopped.\");\n\n        private static readonly Action<ILogger, string, string, int, Exception?> _destinationProbingCompleted = LoggerMessage.Define<string, string, int>(\n            LogLevel.Information,\n            EventIds.DestinationProbingCompleted,\n            \"Probing destination `{destinationId}` on cluster `{clusterId}` completed with the response code `{responseCode}`.\");\n\n        private static readonly Action<ILogger, string, string, Exception> _destinationProbingFailed = LoggerMessage.Define<string, string>(\n            LogLevel.Warning,\n            EventIds.DestinationProbingFailed,\n            \"Probing destination `{destinationId}` on cluster `{clusterId}` failed.\");\n\n        private static readonly Action<ILogger, Uri?, string, string, Exception?> _sendingHealthProbeToEndpointOfDestination = LoggerMessage.Define<Uri?, string, string>(\n            LogLevel.Debug,\n            EventIds.SendingHealthProbeToEndpointOfDestination,\n            \"Sending a health probe to endpoint `{endpointUri}` of destination `{destinationId}` on cluster `{clusterId}`.\");\n\n        public static void ExplicitActiveCheckOfAllClustersHealthFailed(ILogger logger, Exception ex)\n        {\n            _explicitActiveCheckOfAllClustersHealthFailed(logger, ex);\n        }\n\n        public static void ActiveHealthProbingFailedOnCluster(ILogger logger, string clusterId, Exception ex)\n        {\n            _activeHealthProbingFailedOnCluster(logger, clusterId, ex);\n        }\n\n        public static void ErrorOccurredDuringActiveHealthProbingShutdownOnCluster(ILogger logger, string clusterId, Exception ex)\n        {\n            _errorOccurredDuringActiveHealthProbingShutdownOnCluster(logger, clusterId, ex);\n        }\n\n        public static void ActiveHealthProbeConstructionFailedOnCluster(ILogger logger, string destinationId, string clusterId, Exception ex)\n        {\n            _activeHealthProbeConstructionFailedOnCluster(logger, destinationId, clusterId, ex);\n        }\n\n        public static void ActiveHealthProbeCancelledOnDestination(ILogger logger, string destinationId, string clusterId)\n        {\n            _activeHealthProbeCancelledOnDestination(logger, destinationId, clusterId, null);\n        }\n\n        public static void StartingActiveHealthProbingOnCluster(ILogger logger, string clusterId)\n        {\n            _startingActiveHealthProbingOnCluster(logger, clusterId, null);\n        }\n\n        public static void StoppedActiveHealthProbingOnCluster(ILogger logger, string clusterId)\n        {\n            _stoppedActiveHealthProbingOnCluster(logger, clusterId, null);\n        }\n\n        public static void DestinationProbingCompleted(ILogger logger, string destinationId, string clusterId, int responseCode)\n        {\n            _destinationProbingCompleted(logger, destinationId, clusterId, responseCode, null);\n        }\n\n        public static void DestinationProbingFailed(ILogger logger, string destinationId, string clusterId, Exception ex)\n        {\n            _destinationProbingFailed(logger, destinationId, clusterId, ex);\n        }\n\n        public static void SendingHealthProbeToEndpointOfDestination(ILogger logger, Uri? endpointUri, string destinationId, string clusterId)\n        {\n            _sendingHealthProbeToEndpointOfDestination(logger, endpointUri, destinationId, clusterId, null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/ActiveHealthCheckMonitor.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Health;\n\ninternal partial class ActiveHealthCheckMonitor : IActiveHealthCheckMonitor, IClusterChangeListener, IDisposable\n{\n    private readonly ActiveHealthCheckMonitorOptions _monitorOptions;\n    private readonly FrozenDictionary<string, IActiveHealthCheckPolicy> _policies;\n    private readonly IProbingRequestFactory _probingRequestFactory;\n    private readonly ILogger<ActiveHealthCheckMonitor> _logger;\n\n    public ActiveHealthCheckMonitor(\n        IOptions<ActiveHealthCheckMonitorOptions> monitorOptions,\n        IEnumerable<IActiveHealthCheckPolicy> policies,\n        IProbingRequestFactory probingRequestFactory,\n        TimeProvider timeProvider,\n        ILogger<ActiveHealthCheckMonitor> logger)\n    {\n        ArgumentNullException.ThrowIfNull(monitorOptions?.Value);\n        ArgumentNullException.ThrowIfNull(policies);\n        ArgumentNullException.ThrowIfNull(probingRequestFactory);\n        ArgumentNullException.ThrowIfNull(logger);\n\n        _monitorOptions = monitorOptions.Value;\n        _policies = policies.ToDictionaryByUniqueId(p => p.Name);\n        _probingRequestFactory = probingRequestFactory;\n        _logger = logger;\n        Scheduler = new EntityActionScheduler<ClusterState>(cluster => ProbeCluster(cluster), autoStart: false, runOnce: false, timeProvider);\n    }\n\n    public bool InitialProbeCompleted { get; private set; }\n\n    internal EntityActionScheduler<ClusterState> Scheduler { get; }\n\n    public Task CheckHealthAsync(IEnumerable<ClusterState> clusters)\n    {\n        return Task.Run(async () =>\n        {\n            try\n            {\n                var probeClusterTasks = new List<Task>();\n                foreach (var cluster in clusters)\n                {\n                    if ((cluster.Model.Config.HealthCheck?.Active?.Enabled).GetValueOrDefault())\n                    {\n                        probeClusterTasks.Add(ProbeCluster(cluster));\n                    }\n                }\n\n                await Task.WhenAll(probeClusterTasks);\n            }\n            catch (Exception ex)\n            {\n                Log.ExplicitActiveCheckOfAllClustersHealthFailed(_logger, ex);\n            }\n            finally\n            {\n                InitialProbeCompleted = true;\n            }\n\n            Scheduler.Start();\n        });\n    }\n\n    public void OnClusterAdded(ClusterState cluster)\n    {\n        var config = cluster.Model.Config.HealthCheck?.Active;\n        if (config is not null && config.Enabled.GetValueOrDefault())\n        {\n            Scheduler.ScheduleEntity(cluster, config.Interval ?? _monitorOptions.DefaultInterval);\n        }\n    }\n\n    public void OnClusterChanged(ClusterState cluster)\n    {\n        var config = cluster.Model.Config.HealthCheck?.Active;\n        if (config is not null && config.Enabled.GetValueOrDefault())\n        {\n            Scheduler.ChangePeriod(cluster, config.Interval ?? _monitorOptions.DefaultInterval);\n        }\n        else\n        {\n            Scheduler.UnscheduleEntity(cluster);\n        }\n    }\n\n    public void OnClusterRemoved(ClusterState cluster)\n    {\n        Scheduler.UnscheduleEntity(cluster);\n    }\n\n    public void Dispose()\n    {\n        Scheduler.Dispose();\n    }\n\n    private async Task ProbeCluster(ClusterState cluster)\n    {\n        var config = cluster.Model.Config.HealthCheck?.Active;\n        if (config is null || !config.Enabled.GetValueOrDefault())\n        {\n            return;\n        }\n\n        // Creates an Activity to trace the active health checks\n        using var activity = Observability.YarpActivitySource.StartActivity(\"proxy.cluster_health_checks\", ActivityKind.Consumer);\n        activity?.AddTag(\"proxy.cluster_id\", cluster.ClusterId);\n\n        Log.StartingActiveHealthProbingOnCluster(_logger, cluster.ClusterId);\n\n        var allDestinations = cluster.DestinationsState.AllDestinations;\n        var probeTasks = new Task<DestinationProbingResult>[allDestinations.Count];\n        var probeResults = new DestinationProbingResult[probeTasks.Length];\n\n        var timeout = config.Timeout ?? _monitorOptions.DefaultTimeout;\n\n        for (var i = 0; i < probeTasks.Length; i++)\n        {\n            probeTasks[i] = ProbeDestinationAsync(cluster, allDestinations[i], timeout);\n        }\n\n        for (var i = 0; i < probeResults.Length; i++)\n        {\n            probeResults[i] = await probeTasks[i];\n        }\n\n        try\n        {\n            var policy = _policies.GetRequiredServiceById(config.Policy, HealthCheckConstants.ActivePolicy.ConsecutiveFailures);\n            policy.ProbingCompleted(cluster, probeResults);\n            activity?.SetStatus(ActivityStatusCode.Ok);\n        }\n        catch (Exception ex)\n        {\n            Log.ActiveHealthProbingFailedOnCluster(_logger, cluster.ClusterId, ex);\n            activity?.SetStatus(ActivityStatusCode.Error);\n        }\n        finally\n        {\n            try\n            {\n                foreach (var probeResult in probeResults)\n                {\n                    probeResult.Response?.Dispose();\n                }\n            }\n            catch (Exception ex)\n            {\n                Log.ErrorOccurredDuringActiveHealthProbingShutdownOnCluster(_logger, cluster.ClusterId, ex);\n            }\n\n            Log.StoppedActiveHealthProbingOnCluster(_logger, cluster.ClusterId);\n        }\n    }\n\n    private async Task<DestinationProbingResult> ProbeDestinationAsync(ClusterState cluster, DestinationState destination, TimeSpan timeout)\n    {\n        using var probeActivity = Observability.YarpActivitySource.StartActivity(\"proxy.destination_health_check\", ActivityKind.Client);\n        probeActivity?.AddTag(\"proxy.cluster_id\", cluster.ClusterId);\n        probeActivity?.AddTag(\"proxy.destination_id\", destination.DestinationId);\n\n        using var cts = new CancellationTokenSource(timeout);\n\n        HttpRequestMessage request;\n        try\n        {\n            request = await _probingRequestFactory.CreateRequestAsync(cluster, destination, cts.Token);\n        }\n        catch (OperationCanceledException oce) when (!cts.IsCancellationRequested)\n        {\n            Log.ActiveHealthProbeCancelledOnDestination(_logger, destination.DestinationId, cluster.ClusterId);\n            return new DestinationProbingResult(destination, null, oce);\n        }\n        catch (Exception ex)\n        {\n            Log.ActiveHealthProbeConstructionFailedOnCluster(_logger, destination.DestinationId, cluster.ClusterId, ex);\n\n            probeActivity?.SetStatus(ActivityStatusCode.Error);\n\n            return new DestinationProbingResult(destination, null, ex);\n        }\n\n        try\n        {\n            Log.SendingHealthProbeToEndpointOfDestination(_logger, request.RequestUri, destination.DestinationId, cluster.ClusterId);\n            var response = await cluster.Model.HttpClient.SendAsync(request, cts.Token);\n            Log.DestinationProbingCompleted(_logger, destination.DestinationId, cluster.ClusterId, (int)response.StatusCode);\n\n            probeActivity?.SetStatus(ActivityStatusCode.Ok);\n\n            return new DestinationProbingResult(destination, response, null);\n        }\n        catch (OperationCanceledException oce) when (!cts.IsCancellationRequested)\n        {\n            Log.ActiveHealthProbeCancelledOnDestination(_logger, destination.DestinationId, cluster.ClusterId);\n            return new DestinationProbingResult(destination, null, oce);\n        }\n        catch (Exception ex)\n        {\n            Log.DestinationProbingFailed(_logger, destination.DestinationId, cluster.ClusterId, ex);\n\n            probeActivity?.SetStatus(ActivityStatusCode.Error);\n\n            return new DestinationProbingResult(destination, null, ex);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/ActiveHealthCheckMonitorOptions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// Defines options for the active health check monitor.\n/// </summary>\npublic class ActiveHealthCheckMonitorOptions\n{\n    /// <summary>\n    /// Default probing interval.\n    /// </summary>\n    public TimeSpan DefaultInterval { get; set; } = TimeSpan.FromSeconds(15);\n\n    /// <summary>\n    /// Default probes timeout.\n    /// </summary>\n    public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(10);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/AppBuilderHealthExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.ReverseProxy.Health;\n\nnamespace Microsoft.AspNetCore.Builder;\n\n/// <summary>\n/// Extensions for adding proxy middleware to the pipeline.\n/// </summary>\npublic static class AppBuilderHealthExtensions\n{\n    /// <summary>\n    /// Passively checks destinations health by watching for successes and failures in client request proxying.\n    /// </summary>\n    public static IReverseProxyApplicationBuilder UsePassiveHealthChecks(this IReverseProxyApplicationBuilder builder)\n    {\n        builder.UseMiddleware<PassiveHealthCheckMiddleware>();\n        return builder;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/ClusterDestinationsUpdater.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing System.Runtime.CompilerServices;\nusing System.Threading;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Health;\n\ninternal sealed class ClusterDestinationsUpdater : IClusterDestinationsUpdater\n{\n    private readonly ConditionalWeakTable<ClusterState, SemaphoreSlim> _clusterLocks = new ConditionalWeakTable<ClusterState, SemaphoreSlim>();\n    private readonly FrozenDictionary<string, IAvailableDestinationsPolicy> _destinationPolicies;\n\n    public ClusterDestinationsUpdater(IEnumerable<IAvailableDestinationsPolicy> destinationPolicies)\n    {\n        ArgumentNullException.ThrowIfNull(destinationPolicies);\n        _destinationPolicies = destinationPolicies.ToDictionaryByUniqueId(p => p.Name);\n    }\n\n    public void UpdateAvailableDestinations(ClusterState cluster)\n    {\n        var allDestinations = cluster.DestinationsState?.AllDestinations;\n        if (allDestinations is null)\n        {\n            throw new InvalidOperationException($\"{nameof(UpdateAllDestinations)} must be called first.\");\n        }\n\n        UpdateInternal(cluster, allDestinations, force: false);\n    }\n\n    public void UpdateAllDestinations(ClusterState cluster)\n    {\n        // Values already makes a copy of the collection, downcast to avoid making a second copy.\n        // https://github.com/dotnet/runtime/blob/e164551f1c96138521b4e58f14f8ac1e4369005d/src/libraries/System.Collections.Concurrent/src/System/Collections/Concurrent/ConcurrentDictionary.cs#L2145-L2168\n        var allDestinations = (IReadOnlyList<DestinationState>)cluster.Destinations.Values;\n        UpdateInternal(cluster, allDestinations, force: true);\n    }\n\n    private void UpdateInternal(ClusterState cluster, IReadOnlyList<DestinationState> allDestinations, bool force)\n    {\n        // Prevent overlapping updates and debounce extra concurrent calls.\n        // If there are multiple concurrent calls to rebuild the dynamic state, we want to ensure that\n        // updates don't conflict with each other. Additionally, we debounce extra concurrent calls if\n        // they arrive in a quick succession to avoid spending too much CPU on frequent state rebuilds.\n        // Specifically, only up to two threads are allowed to wait here and actually execute a rebuild,\n        // all others will be debounced and the call will return without updating the ClusterState.DestinationsState.\n        // However, changes made by those debounced threads (e.g. destination health updates) will be\n        // taken into account by one of blocked threads after they get unblocked to run a rebuild.\n        var updateLock = _clusterLocks.GetValue(cluster, _ => new SemaphoreSlim(2));\n        var lockTaken = false;\n        if (force)\n        {\n            lockTaken = true;\n            updateLock.Wait();\n        }\n        else\n        {\n            lockTaken = updateLock.Wait(0);\n        }\n\n        if (!lockTaken)\n        {\n            return;\n        }\n\n        lock (updateLock)\n        {\n            try\n            {\n                var config = cluster.Model.Config;\n                var destinationPolicy = _destinationPolicies.GetRequiredServiceById(\n                    config.HealthCheck?.AvailableDestinationsPolicy,\n                    HealthCheckConstants.AvailableDestinations.HealthyOrPanic);\n\n                var availableDestinations = destinationPolicy.GetAvailableDestinations(config, allDestinations);\n\n                cluster.DestinationsState = new ClusterDestinationsState(allDestinations, availableDestinations);\n            }\n            finally\n            {\n                // Semaphore is released while still holding the lock to AVOID the following case.\n                // The first thread (T1) finished a rebuild and left the lock while still holding the semaphore. The second thread (T2)\n                // waiting on the lock gets awaken, proceeds under the lock and begins the next rebuild. If at this exact moment\n                // the third thread (T3) enters this method and tries to acquire the semaphore, it will be debounced because\n                // the semaphore's count is still 0. However, T2 could have already made some progress and didn't observe updates made\n                // by T3.\n                // By releasing the semaphore under the lock, we make sure that in the above situation T3 will proceed till the lock and\n                // its updates will be observed anyway.\n                updateLock.Release();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/ConsecutiveFailuresHealthPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Net.Http;\nusing System.Runtime.CompilerServices;\nusing Microsoft.Extensions.Options;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Health;\n\ninternal sealed class ConsecutiveFailuresHealthPolicy : IActiveHealthCheckPolicy\n{\n    private readonly ConsecutiveFailuresHealthPolicyOptions _options;\n    private readonly ConditionalWeakTable<ClusterState, ParsedMetadataEntry<double>> _clusterThresholds = new ConditionalWeakTable<ClusterState, ParsedMetadataEntry<double>>();\n    private readonly ConditionalWeakTable<DestinationState, AtomicCounter> _failureCounters = new ConditionalWeakTable<DestinationState, AtomicCounter>();\n    private readonly IDestinationHealthUpdater _healthUpdater;\n\n    public string Name => HealthCheckConstants.ActivePolicy.ConsecutiveFailures;\n\n    public ConsecutiveFailuresHealthPolicy(IOptions<ConsecutiveFailuresHealthPolicyOptions> options, IDestinationHealthUpdater healthUpdater)\n    {\n        ArgumentNullException.ThrowIfNull(options?.Value);\n        ArgumentNullException.ThrowIfNull(healthUpdater);\n        _options = options.Value;\n        _healthUpdater = healthUpdater;\n    }\n\n    public void ProbingCompleted(ClusterState cluster, IReadOnlyList<DestinationProbingResult> probingResults)\n    {\n        if (probingResults.Count == 0)\n        {\n            return;\n        }\n\n        var threshold = GetFailureThreshold(cluster);\n\n        var newHealthStates = new NewActiveDestinationHealth[probingResults.Count];\n        for (var i = 0; i < probingResults.Count; i++)\n        {\n            var destination = probingResults[i].Destination;\n            var previousState = destination.Health.Active;\n\n            var count = _failureCounters.GetOrCreateValue(destination);\n            var newHealth = EvaluateHealthState(threshold, probingResults[i].Response, count, previousState);\n            newHealthStates[i] = new NewActiveDestinationHealth(destination, newHealth);\n        }\n\n        _healthUpdater.SetActive(cluster, newHealthStates);\n    }\n\n    private double GetFailureThreshold(ClusterState cluster)\n    {\n        var thresholdEntry = _clusterThresholds.GetValue(cluster, c => new ParsedMetadataEntry<double>(TryParse, c, ConsecutiveFailuresHealthPolicyOptions.ThresholdMetadataName));\n        return thresholdEntry.GetParsedOrDefault(_options.DefaultThreshold);\n    }\n\n    private static DestinationHealth EvaluateHealthState(double threshold, HttpResponseMessage? response, AtomicCounter count, DestinationHealth previousState)\n    {\n        DestinationHealth newHealth;\n        if (response is not null && response.IsSuccessStatusCode)\n        {\n            // Success\n            count.Reset();\n            newHealth = DestinationHealth.Healthy;\n        }\n        else\n        {\n            // Failure\n            var currentFailureCount = count.Increment();\n            newHealth = currentFailureCount < threshold ? previousState : DestinationHealth.Unhealthy;\n        }\n\n        return newHealth;\n    }\n\n    private static bool TryParse(string stringValue, out double parsedValue)\n    {\n        return double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out parsedValue);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/ConsecutiveFailuresHealthPolicyOptions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// Defines options for the consecutive failures active health check policy.\n/// </summary>\npublic class ConsecutiveFailuresHealthPolicyOptions\n{\n    /// <summary>\n    /// Name of the consecutive failure threshold metadata parameter.\n    /// It's the number of consecutive failure that needs to happen in order to mark a destination as unhealthy.\n    /// </summary>\n    public static readonly string ThresholdMetadataName = \"ConsecutiveFailuresHealthPolicy.Threshold\";\n\n    /// <summary>\n    /// Default consecutive failures threshold that is applied if it's not set on a cluster's metadata.\n    /// </summary>\n    public long DefaultThreshold { get; set; } = 2;\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/DefaultProbingRequestFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Extensions;\nusing Microsoft.Net.Http.Headers;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health;\n\ninternal sealed class DefaultProbingRequestFactory : IProbingRequestFactory\n{\n    private static readonly string? _version = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;\n    private static readonly string _defaultUserAgent = $\"YARP{(string.IsNullOrEmpty(_version) ? \"\" : $\"/{_version.Split('+')[0]}\")} (Active Health Check Monitor)\";\n\n    public HttpRequestMessage CreateRequest(ClusterModel cluster, DestinationModel destination)\n    {\n        var probeAddress = !string.IsNullOrEmpty(destination.Config.Health) ? destination.Config.Health : destination.Config.Address;\n        var probePath = cluster.Config.HealthCheck?.Active?.Path;\n        UriHelper.FromAbsolute(probeAddress, out var destinationScheme, out var destinationHost, out var destinationPathBase, out _, out _);\n        var query = QueryString.FromUriComponent(cluster.Config.HealthCheck?.Active?.Query ?? \"\");\n        var probeUri = UriHelper.BuildAbsolute(destinationScheme, destinationHost, destinationPathBase, probePath, query);\n\n        var request = new HttpRequestMessage(HttpMethod.Get, probeUri)\n        {\n            Version = cluster.Config.HttpRequest?.Version ?? HttpVersion.Version20,\n            VersionPolicy = cluster.Config.HttpRequest?.VersionPolicy ?? HttpVersionPolicy.RequestVersionOrLower,\n        };\n\n        if (!string.IsNullOrEmpty(destination.Config.Host))\n        {\n            request.Headers.Add(HeaderNames.Host, destination.Config.Host);\n        }\n\n        request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgent);\n\n        return request;\n    }\n\n    public ValueTask<HttpRequestMessage> CreateRequestAsync(ClusterState cluster, DestinationState destination, CancellationToken cancellationToken = default) =>\n        ValueTask.FromResult(CreateRequest(cluster.Model, destination.Model));\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/DestinationHealthUpdater.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health;\n\ninternal sealed class DestinationHealthUpdater : IDestinationHealthUpdater, IDisposable\n{\n    private readonly EntityActionScheduler<(ClusterState Cluster, DestinationState Destination)> _scheduler;\n    private readonly IClusterDestinationsUpdater _clusterUpdater;\n    private readonly ILogger<DestinationHealthUpdater> _logger;\n\n    public DestinationHealthUpdater(\n        TimeProvider timeProvider,\n        IClusterDestinationsUpdater clusterDestinationsUpdater,\n        ILogger<DestinationHealthUpdater> logger)\n    {\n        ArgumentNullException.ThrowIfNull(clusterDestinationsUpdater);\n        ArgumentNullException.ThrowIfNull(logger);\n\n        _scheduler = new EntityActionScheduler<(ClusterState Cluster, DestinationState Destination)>(d => Reactivate(d.Cluster, d.Destination), autoStart: true, runOnce: true, timeProvider);\n        _clusterUpdater = clusterDestinationsUpdater;\n        _logger = logger;\n    }\n\n    public void SetActive(ClusterState cluster, IEnumerable<NewActiveDestinationHealth> newHealthPairs)\n    {\n        var changed = false;\n        foreach (var newHealthPair in newHealthPairs)\n        {\n            var destination = newHealthPair.Destination;\n            var newHealth = newHealthPair.NewActiveHealth;\n\n            var healthState = destination.Health;\n            if (newHealth != healthState.Active)\n            {\n                healthState.Active = newHealth;\n                changed = true;\n                if (newHealth == DestinationHealth.Unhealthy)\n                {\n                    Log.ActiveDestinationHealthStateIsSetToUnhealthy(_logger, destination.DestinationId, cluster.ClusterId);\n                }\n                else\n                {\n                    Log.ActiveDestinationHealthStateIsSet(_logger, destination.DestinationId, cluster.ClusterId, newHealth);\n                }\n            }\n        }\n\n        if (changed)\n        {\n            _clusterUpdater.UpdateAvailableDestinations(cluster);\n        }\n    }\n\n    public void SetPassive(ClusterState cluster, DestinationState destination, DestinationHealth newHealth, TimeSpan reactivationPeriod)\n    {\n        _ = SetPassiveAsync(cluster, destination, newHealth, reactivationPeriod);\n    }\n\n    internal Task SetPassiveAsync(ClusterState cluster, DestinationState destination, DestinationHealth newHealth, TimeSpan reactivationPeriod)\n    {\n        var healthState = destination.Health;\n        if (newHealth != healthState.Passive)\n        {\n            healthState.Passive = newHealth;\n            ScheduleReactivation(cluster, destination, newHealth, reactivationPeriod);\n            return Task.Factory.StartNew(c => UpdateDestinations(c!), cluster, CancellationToken.None, TaskCreationOptions.RunContinuationsAsynchronously, TaskScheduler.Default);\n        }\n        return Task.CompletedTask;\n    }\n\n    private void UpdateDestinations(object cluster)\n    {\n        _clusterUpdater.UpdateAvailableDestinations((ClusterState)cluster);\n    }\n\n    private void ScheduleReactivation(ClusterState cluster, DestinationState destination, DestinationHealth newHealth, TimeSpan reactivationPeriod)\n    {\n        if (newHealth == DestinationHealth.Unhealthy)\n        {\n            _scheduler.ScheduleEntity((cluster, destination), reactivationPeriod);\n            Log.UnhealthyDestinationIsScheduledForReactivation(_logger, destination.DestinationId, reactivationPeriod);\n        }\n    }\n\n    public void Dispose()\n    {\n        _scheduler.Dispose();\n    }\n\n    private Task Reactivate(ClusterState cluster, DestinationState destination)\n    {\n        var healthState = destination.Health;\n        if (healthState.Passive == DestinationHealth.Unhealthy)\n        {\n            healthState.Passive = DestinationHealth.Unknown;\n            Log.PassiveDestinationHealthResetToUnknownState(_logger, destination.DestinationId);\n            _clusterUpdater.UpdateAvailableDestinations(cluster);\n        }\n\n        return Task.CompletedTask;\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, string, TimeSpan, Exception?> _unhealthyDestinationIsScheduledForReactivation = LoggerMessage.Define<string, TimeSpan>(\n            LogLevel.Warning,\n            EventIds.UnhealthyDestinationIsScheduledForReactivation,\n            \"Destination `{destinationId}` marked as 'Unhealthy` by the passive health check is scheduled for a reactivation in `{reactivationPeriod}`.\");\n\n        private static readonly Action<ILogger, string, Exception?> _passiveDestinationHealthResetToUnknownState = LoggerMessage.Define<string>(\n            LogLevel.Information,\n            EventIds.PassiveDestinationHealthResetToUnknownState,\n            \"Passive health state of the destination `{destinationId}` is reset to 'Unknown`.\");\n\n        private static readonly Action<ILogger, string, string, Exception?> _activeDestinationHealthStateIsSetToUnhealthy = LoggerMessage.Define<string, string>(\n            LogLevel.Warning,\n            EventIds.ActiveDestinationHealthStateIsSetToUnhealthy,\n            \"Active health state of destination `{destinationId}` on cluster `{clusterId}` is set to 'Unhealthy'.\");\n\n        private static readonly Action<ILogger, string, string, DestinationHealth, Exception?> _activeDestinationHealthStateIsSet = LoggerMessage.Define<string, string, DestinationHealth>(\n            LogLevel.Information,\n            EventIds.ActiveDestinationHealthStateIsSet,\n            \"Active health state of destination `{destinationId}` on cluster `{clusterId}` is set to '{newHealthState}'.\");\n\n        public static void ActiveDestinationHealthStateIsSetToUnhealthy(ILogger logger, string destinationId, string clusterId)\n        {\n            _activeDestinationHealthStateIsSetToUnhealthy(logger, destinationId, clusterId, null);\n        }\n\n        public static void ActiveDestinationHealthStateIsSet(ILogger logger, string destinationId, string clusterId, DestinationHealth newHealthState)\n        {\n            _activeDestinationHealthStateIsSet(logger, destinationId, clusterId, newHealthState, null);\n        }\n\n        public static void UnhealthyDestinationIsScheduledForReactivation(ILogger logger, string destinationId, TimeSpan reactivationPeriod)\n        {\n            _unhealthyDestinationIsScheduledForReactivation(logger, destinationId, reactivationPeriod, null);\n        }\n\n        public static void PassiveDestinationHealthResetToUnknownState(ILogger logger, string destinationId)\n        {\n            _passiveDestinationHealthResetToUnknownState(logger, destinationId, null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/DestinationProbingResult.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// Result of a destination's active health probing.\n/// </summary>\npublic readonly struct DestinationProbingResult\n{\n    public DestinationProbingResult(DestinationState destination, HttpResponseMessage? response, Exception? exception)\n    {\n        ArgumentNullException.ThrowIfNull(destination);\n        Destination = destination;\n        Response = response;\n        Exception = exception;\n    }\n\n    /// <summary>\n    /// Probed destination.\n    /// </summary>\n    public DestinationState Destination { get; }\n\n    /// <summary>\n    /// Response received.\n    /// It can be null in case of a failure.\n    /// </summary>\n    public HttpResponseMessage? Response { get; }\n\n    /// <summary>\n    /// Exception thrown during probing.\n    /// It is null in case of a success.\n    /// </summary>\n    public Exception? Exception { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/EntityActionScheduler.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// Periodically invokes specified actions on registered entities.\n/// </summary>\n/// <remarks>\n/// It creates a separate <see cref=\"Timer\"/> for each registration which is considered\n/// reasonably efficient because .NET already maintains a process-wide managed timer queue.\n/// There are 2 scheduling modes supported: run once and infinite run. In \"run once\" mode,\n/// an entity gets unscheduled after the respective timer fired for the first time whereas\n/// in \"infinite run\" entities get repeatedly rescheduled until either they are explicitly removed\n/// or the <see cref=\"EntityActionScheduler{T}\"/> instance is disposed.\n/// </remarks>\ninternal sealed class EntityActionScheduler<T> : IDisposable where T : notnull\n{\n    private readonly ConcurrentDictionary<T, SchedulerEntry> _entries = new();\n    private readonly WeakReference<EntityActionScheduler<T>> _weakThisRef;\n    private readonly Func<T, Task> _action;\n    private readonly bool _runOnce;\n    private readonly TimeProvider _timeProvider;\n\n    private const int NotStarted = 0;\n    private const int Started = 1;\n    private const int Disposed = 2;\n    private int _status;\n\n    public EntityActionScheduler(Func<T, Task> action, bool autoStart, bool runOnce, TimeProvider timeProvider)\n    {\n        ArgumentNullException.ThrowIfNull(action);\n        ArgumentNullException.ThrowIfNull(timeProvider);\n\n        _action = action;\n        _runOnce = runOnce;\n        _timeProvider = timeProvider;\n        _status = autoStart ? Started : NotStarted;\n        _weakThisRef = new WeakReference<EntityActionScheduler<T>>(this);\n    }\n\n    public void Dispose()\n    {\n        Volatile.Write(ref _status, Disposed);\n\n        foreach (var entry in _entries.Values)\n        {\n            entry.Dispose();\n        }\n    }\n\n    public void Start()\n    {\n        if (Interlocked.CompareExchange(ref _status, Started, NotStarted) != NotStarted)\n        {\n            return;\n        }\n\n        foreach (var entry in _entries.Values)\n        {\n            entry.EnsureStarted();\n        }\n    }\n\n    public void ScheduleEntity(T entity, TimeSpan period)\n    {\n        // Ensure the Timer has a weak reference to this scheduler; otherwise,\n        // EntityActionScheduler can be rooted by the Timer implementation.\n        var entry = new SchedulerEntry(_weakThisRef, entity, period, _timeProvider);\n\n        if (_entries.TryAdd(entity, entry))\n        {\n            // Scheduler could have been started while we were adding the new entry.\n            // Start timer here to ensure it's not forgotten.\n            if (Volatile.Read(ref _status) == Started)\n            {\n                entry.EnsureStarted();\n            }\n        }\n        else\n        {\n            entry.Dispose();\n        }\n    }\n\n    public void ChangePeriod(T entity, TimeSpan newPeriod)\n    {\n        Debug.Assert(!_runOnce, \"Calling ChangePeriod on a RunOnce scheduler may cause the callback to fire twice\");\n\n        if (_entries.TryGetValue(entity, out var entry))\n        {\n            entry.ChangePeriod(newPeriod);\n        }\n        else\n        {\n            ScheduleEntity(entity, newPeriod);\n        }\n    }\n\n    public void UnscheduleEntity(T entity)\n    {\n        if (_entries.TryRemove(entity, out var entry))\n        {\n            entry.Dispose();\n        }\n    }\n\n    public bool IsScheduled(T entity)\n    {\n        return _entries.ContainsKey(entity);\n    }\n\n    private sealed class SchedulerEntry : IDisposable\n    {\n        private readonly WeakReference<EntityActionScheduler<T>> _scheduler;\n        private readonly T _entity;\n        private readonly ITimer _timer;\n        private TimeSpan _period;\n        private bool _timerStarted;\n        private bool _runningCallback;\n\n        public SchedulerEntry(WeakReference<EntityActionScheduler<T>> scheduler, T entity, TimeSpan period, TimeProvider timeProvider)\n        {\n            _scheduler = scheduler;\n            _entity = entity;\n            _period = period;\n\n            // Don't capture the current ExecutionContext and its AsyncLocals onto the timer causing them to live forever\n            var restoreFlow = false;\n            try\n            {\n                if (!ExecutionContext.IsFlowSuppressed())\n                {\n                    ExecutionContext.SuppressFlow();\n                    restoreFlow = true;\n                }\n\n                _timer = timeProvider.CreateTimer(static s => _ = TimerCallback(s), this, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);\n            }\n            finally\n            {\n                if (restoreFlow)\n                {\n                    ExecutionContext.RestoreFlow();\n                }\n            }\n        }\n\n        public void ChangePeriod(TimeSpan newPeriod)\n        {\n            lock (this)\n            {\n                _period = newPeriod;\n                if (_timerStarted && !_runningCallback)\n                {\n                    SetTimer();\n                }\n            }\n        }\n\n        public void EnsureStarted()\n        {\n            lock (this)\n            {\n                if (!_timerStarted)\n                {\n                    SetTimer();\n                }\n            }\n        }\n\n        private void SetTimer()\n        {\n            Debug.Assert(Monitor.IsEntered(this));\n            Debug.Assert(!_runningCallback);\n\n            _timerStarted = true;\n\n            try\n            {\n                _timer.Change(_period, Timeout.InfiniteTimeSpan);\n            }\n            catch (ObjectDisposedException)\n            {\n                // It can be thrown if the timer has been already disposed.\n                // Just suppress it.\n            }\n        }\n\n        public void Dispose()\n        {\n            _timer.Dispose();\n        }\n\n        // Timer.Change is racy as the callback could already be scheduled while we are starting the timer again.\n        // Avoid running the callback multiple times concurrently by using the _runningCallback flag.\n        private static async Task TimerCallback(object? state)\n        {\n            var entry = (SchedulerEntry)state!;\n\n            lock (entry)\n            {\n                if (entry._runningCallback)\n                {\n                    return;\n                }\n\n                entry._runningCallback = true;\n            }\n\n            if (!entry._scheduler.TryGetTarget(out var scheduler))\n            {\n                return;\n            }\n\n            var pair = new KeyValuePair<T, SchedulerEntry>(entry._entity, entry);\n\n            if (scheduler._runOnce && scheduler._entries.TryRemove(pair))\n            {\n                entry.Dispose();\n            }\n\n            try\n            {\n                await scheduler._action(entry._entity);\n\n                if (!scheduler._runOnce && scheduler._entries.Contains(pair))\n                {\n                    // This entry has not been unscheduled - set the timer again\n                    lock (entry)\n                    {\n                        entry._runningCallback = false;\n                        entry.SetTimer();\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                // We are running on the ThreadPool, don't propagate exceptions\n                Debug.Fail(ex.ToString()); // TODO: Log\n                if (scheduler._entries.TryRemove(pair))\n                {\n                    entry.Dispose();\n                }\n                return;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/HealthCheckConstants.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Health;\n\npublic static class HealthCheckConstants\n{\n    public static class PassivePolicy\n    {\n        public static readonly string TransportFailureRate = nameof(TransportFailureRate);\n    }\n\n    public static class ActivePolicy\n    {\n        public static readonly string ConsecutiveFailures = nameof(ConsecutiveFailures);\n    }\n\n    public static class AvailableDestinations\n    {\n        /// <summary>\n        /// Marks destination as available for proxying requests to if its health state\n        /// is either 'Healthy' or 'Unknown'. If no destinations are available then\n        /// requests will get a 503 error.\n        /// </summary>\n        /// <remarks>It applies only if active or passive health checks are enabled.</remarks>\n        public static readonly string HealthyAndUnknown = nameof(HealthyAndUnknown);\n\n        /// <summary>\n        /// Calls <see cref=\"HealthyAndUnknown\"/> policy at first to determine\n        /// destinations' availability. If no available destinations are returned\n        /// from this call, it marks all cluster's destination as available.\n        /// </summary>\n        public static readonly string HealthyOrPanic = nameof(HealthyOrPanic);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/HealthyAndUnknownDestinationsPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Linq;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health;\n\n// Policy marking destinations as available only if their active and passive health states\n/// are either 'Healthy' or 'Unknown'/>.\ninternal class HealthyAndUnknownDestinationsPolicy : IAvailableDestinationsPolicy\n{\n    public virtual string Name => HealthCheckConstants.AvailableDestinations.HealthyAndUnknown;\n\n    public virtual IReadOnlyList<DestinationState> GetAvailableDestinations(ClusterConfig config, IReadOnlyList<DestinationState> allDestinations)\n    {\n        var availableDestinations = allDestinations;\n        var activeEnabled = (config.HealthCheck?.Active?.Enabled).GetValueOrDefault();\n        var passiveEnabled = (config.HealthCheck?.Passive?.Enabled).GetValueOrDefault();\n\n        if (activeEnabled || passiveEnabled)\n        {\n            availableDestinations = allDestinations.Where(destination =>\n            {\n                // Only consider the current state if those checks are enabled.\n                var healthState = destination.Health;\n                var active = activeEnabled ? healthState.Active : DestinationHealth.Unknown;\n                var passive = passiveEnabled ? healthState.Passive : DestinationHealth.Unknown;\n\n                return active != DestinationHealth.Unhealthy && passive != DestinationHealth.Unhealthy;\n            }).ToList();\n        }\n\n        return availableDestinations;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/HealthyOrPanicDestinationsPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health;\n\ninternal sealed class HealthyOrPanicDestinationsPolicy : HealthyAndUnknownDestinationsPolicy\n{\n    public override string Name => HealthCheckConstants.AvailableDestinations.HealthyOrPanic;\n\n    public override IReadOnlyList<DestinationState> GetAvailableDestinations(ClusterConfig config, IReadOnlyList<DestinationState> allDestinations)\n    {\n        var availableDestinations = base.GetAvailableDestinations(config, allDestinations);\n        return availableDestinations.Count > 0 ? availableDestinations : allDestinations;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/IActiveHealthCheckMonitor.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// Actively monitors destinations health.\n/// </summary>\npublic interface IActiveHealthCheckMonitor\n{\n    /// <summary>\n    /// Gets a value that determines whether the initial round of active health checks have run, regardless of the results.\n    /// </summary>\n    /// <returns>\n    /// <c>false</c> until the initial round of health check requests has been processed.\n    /// <c>true</c> when all the initially configured destinations have been queried, regardless their availability or returned status code.\n    /// The property stays <c>true</c> for the rest of the proxy process lifetime.\n    /// </returns>\n    bool InitialProbeCompleted { get; }\n\n    /// <summary>\n    /// Checks health of all clusters' destinations.\n    /// </summary>\n    /// <param name=\"clusters\">Clusters to check the health of their destinations.</param>\n    /// <returns><see cref=\"Task\"/> representing the health check process.</returns>\n    Task CheckHealthAsync(IEnumerable<ClusterState> clusters);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/IActiveHealthCheckPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// Active health check evaluation policy.\n/// </summary>\npublic interface IActiveHealthCheckPolicy\n{\n    /// <summary>\n    /// Policy's name.\n    /// </summary>\n    string Name { get; }\n\n    /// <summary>\n    /// Analyzes results of active health probes sent to destinations and calculates their new health states.\n    /// </summary>\n    /// <param name=\"cluster\">Cluster.</param>\n    /// <param name=\"probingResults\">Destination probing results.</param>\n    void ProbingCompleted(ClusterState cluster, IReadOnlyList<DestinationProbingResult> probingResults);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/IAvailableDestinationsPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// Policy evaluating which destinations should be available for proxying requests to.\n/// </summary>\npublic interface IAvailableDestinationsPolicy\n{\n    /// <summary>\n    /// Policy name.\n    /// </summary>\n    string Name { get; }\n\n    /// <summary>\n    /// Reviews all given destinations and returns the ones available for proxying requests to.\n    /// </summary>\n    /// <param name=\"config\">Target cluster.</param>\n    /// <param name=\"allDestinations\">All destinations configured for the target cluster.</param>\n    /// <returns></returns>\n    IReadOnlyList<DestinationState> GetAvailableDestinations(ClusterConfig config, IReadOnlyList<DestinationState> allDestinations);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/IClusterDestinationsUpdater.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// Updates the cluster's destination collections.\n/// </summary>\npublic interface IClusterDestinationsUpdater\n{\n    /// <summary>\n    /// Updates the cluster's collection of destination available for proxying requests to.\n    /// Call this if health state has changed for any destinations.\n    /// </summary>\n    /// <param name=\"cluster\">The <see cref=\"ClusterState\"/> owing the destinations.</param>\n    void UpdateAvailableDestinations(ClusterState cluster);\n\n    /// <summary>\n    /// Updates the cluster's collection of all configured destinations.\n    /// Call this after destinations have been added, removed, or their configuration changed.\n    /// This does not need to be called for state updates like health, use UpdateAvailableDestinations for state updates.\n    /// </summary>\n    /// <param name=\"cluster\">The <see cref=\"ClusterState\"/> owing the destinations.</param>\n    void UpdateAllDestinations(ClusterState cluster);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/IDestinationHealthUpdater.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// Updates destinations' health states when it's requested by a health check policy\n/// while taking into account not only the new evaluated value but also the overall current cluster's health state.\n/// </summary>\npublic interface IDestinationHealthUpdater\n{\n    /// <summary>\n    /// Sets the passive health on the given <paramref name=\"destination\"/>.\n    /// </summary>\n    /// <param name=\"cluster\">Cluster.</param>\n    /// <param name=\"destination\">Destination.</param>\n    /// <param name=\"newHealth\">New passive health value.</param>\n    /// <param name=\"reactivationPeriod\">If <paramref name=\"newHealth\"/> is <see cref=\"DestinationHealth.Unhealthy\"/>,\n    /// this parameter specifies a reactivation period after which the destination's <see cref=\"DestinationHealthState.Passive\"/> value\n    /// will be reset to <see cref=\"DestinationHealth.Unknown\"/>. Otherwise, it's not used.</param>\n    void SetPassive(ClusterState cluster, DestinationState destination, DestinationHealth newHealth, TimeSpan reactivationPeriod);\n\n    /// <summary>\n    /// Sets the active health values on the given destinations.\n    /// </summary>\n    /// <param name=\"cluster\">Cluster.</param>\n    /// <param name=\"newHealthStates\">New active health states.</param>\n    void SetActive(ClusterState cluster, IEnumerable<NewActiveDestinationHealth> newHealthStates);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/IPassiveHealthCheckPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// Passive health check evaluation policy.\n/// </summary>\npublic interface IPassiveHealthCheckPolicy\n{\n    /// <summary>\n    /// Policy's name.\n    /// </summary>\n    string Name { get; }\n\n    /// <summary>\n    /// Registers a successful or failed request and evaluates a new <see cref=\"DestinationHealthState.Passive\"/> value.\n    /// </summary>\n    /// <param name=\"context\">Context.</param>\n    /// <param name=\"cluster\">Request's cluster.</param>\n    /// <param name=\"destination\">Request's destination.</param>\n    void RequestProxied(HttpContext context, ClusterState cluster, DestinationState destination);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/IProbingRequestFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// A factory for creating <see cref=\"HttpRequestMessage\"/>s for active health probes to be sent to destinations.\n/// </summary>\npublic interface IProbingRequestFactory\n{\n    /// <summary>\n    /// Creates a probing request.\n    /// </summary>\n    /// <param name=\"cluster\">The cluster being probed.</param>\n    /// <param name=\"destination\">The destination being probed.</param>\n    /// <returns>Probing <see cref=\"HttpRequestMessage\"/>.</returns>\n    HttpRequestMessage CreateRequest(ClusterModel cluster, DestinationModel destination);\n\n    /// <summary>\n    /// Creates a probing request.\n    /// </summary>\n    /// <param name=\"cluster\">The cluster being probed.</param>\n    /// <param name=\"destination\">The destination being probed.</param>\n    /// <param name=\"cancellationToken\">A token to cancel the operation.</param>\n    /// <returns>Probing <see cref=\"HttpRequestMessage\"/>.</returns>\n    ValueTask<HttpRequestMessage> CreateRequestAsync(ClusterState cluster, DestinationState destination, CancellationToken cancellationToken = default) =>\n        ValueTask.FromResult(CreateRequest(cluster.Model, destination.Model));\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/NewActiveDestinationHealth.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// Stores a new active health state for the given destination.\n/// </summary>\npublic readonly struct NewActiveDestinationHealth\n{\n    public NewActiveDestinationHealth(DestinationState destination, DestinationHealth newActiveHealth)\n    {\n        Destination = destination;\n        NewActiveHealth = newActiveHealth;\n    }\n\n    public DestinationState Destination { get; }\n\n    public DestinationHealth NewActiveHealth { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/PassiveHealthCheckMiddleware.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Health;\n\npublic class PassiveHealthCheckMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly FrozenDictionary<string, IPassiveHealthCheckPolicy> _policies;\n\n    public PassiveHealthCheckMiddleware(RequestDelegate next, IEnumerable<IPassiveHealthCheckPolicy> policies)\n    {\n        ArgumentNullException.ThrowIfNull(next);\n        ArgumentNullException.ThrowIfNull(policies);\n        _next = next;\n        _policies = policies.ToDictionaryByUniqueId(p => p.Name);\n    }\n\n    public async Task Invoke(HttpContext context)\n    {\n        await _next(context);\n\n        var proxyFeature = context.GetReverseProxyFeature();\n        var options = proxyFeature.Cluster.Config.HealthCheck?.Passive;\n\n        // Do nothing if no target destination has been chosen for the request.\n        if (options is null || !options.Enabled.GetValueOrDefault() || proxyFeature.ProxiedDestination is null)\n        {\n            return;\n        }\n\n        var policy = _policies.GetRequiredServiceById(options.Policy, HealthCheckConstants.PassivePolicy.TransportFailureRate);\n        var cluster = context.GetRouteModel().Cluster!;\n        policy.RequestProxied(context, cluster, proxyFeature.ProxiedDestination);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/TransportFailureRateHealthPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Runtime.CompilerServices;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Options;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// Calculates the proxied requests failure rate for each destination and marks it as unhealthy if the specified limit is exceeded.\n/// </summary>\n/// <remarks>\n/// Rate is calculated as a percentage of failed requests to the total number of request proxied to a destination in the given period of time. Failed and total counters are tracked\n/// in a sliding time window which means that only the recent readings fitting in the window are taken into account. The window is implemented as a linked-list of timestamped records\n/// where each record contains the difference from the previous one in the number of failed and total requests. Additionally, there are 2 destination-wide counters storing aggregated values\n/// to enable a fast calculation of the current failure rate. When a new proxied request is reported, its status firstly affects those 2 aggregated counters and then also gets put\n/// in the record history. Once some record moves out of the detection time window, the failed and total counter deltas stored on it get subtracted from the respective aggregated counters.\n/// </remarks>\ninternal sealed class TransportFailureRateHealthPolicy : IPassiveHealthCheckPolicy\n{\n    private static readonly TimeSpan _defaultReactivationPeriod = TimeSpan.FromSeconds(60);\n    private readonly IDestinationHealthUpdater _healthUpdater;\n    private readonly TransportFailureRateHealthPolicyOptions _policyOptions;\n    private readonly TimeProvider _timeProvider;\n    private readonly ConditionalWeakTable<ClusterState, ParsedMetadataEntry<double>> _clusterFailureRateLimits = new ConditionalWeakTable<ClusterState, ParsedMetadataEntry<double>>();\n    private readonly ConditionalWeakTable<DestinationState, ProxiedRequestHistory> _requestHistories = new ConditionalWeakTable<DestinationState, ProxiedRequestHistory>();\n\n    public string Name => HealthCheckConstants.PassivePolicy.TransportFailureRate;\n\n    public TransportFailureRateHealthPolicy(\n        IOptions<TransportFailureRateHealthPolicyOptions> policyOptions,\n        TimeProvider timeProvider,\n        IDestinationHealthUpdater healthUpdater)\n    {\n        ArgumentNullException.ThrowIfNull(timeProvider);\n        ArgumentNullException.ThrowIfNull(policyOptions?.Value);\n        ArgumentNullException.ThrowIfNull(healthUpdater);\n\n        _timeProvider = timeProvider;\n        _policyOptions = policyOptions.Value;\n        _healthUpdater = healthUpdater;\n    }\n\n    public void RequestProxied(HttpContext context, ClusterState cluster, DestinationState destination)\n    {\n        var newHealth = EvaluateProxiedRequest(cluster, destination, DetermineIfDestinationFailed(context));\n        var clusterReactivationPeriod = cluster.Model.Config.HealthCheck?.Passive?.ReactivationPeriod ?? _defaultReactivationPeriod;\n        // Avoid reactivating until the history has expired so that it does not affect future health assessments.\n        var reactivationPeriod = clusterReactivationPeriod >= _policyOptions.DetectionWindowSize ? clusterReactivationPeriod : _policyOptions.DetectionWindowSize;\n        _healthUpdater.SetPassive(cluster, destination, newHealth, reactivationPeriod);\n    }\n\n    private DestinationHealth EvaluateProxiedRequest(ClusterState cluster, DestinationState destination, bool failed)\n    {\n        var history = _requestHistories.GetOrCreateValue(destination);\n        var rateLimitEntry = _clusterFailureRateLimits.GetValue(cluster, c => new ParsedMetadataEntry<double>(TryParse, c, TransportFailureRateHealthPolicyOptions.FailureRateLimitMetadataName));\n        var rateLimit = rateLimitEntry.GetParsedOrDefault(_policyOptions.DefaultFailureRateLimit);\n        lock (history)\n        {\n            var failureRate = history.AddNew(\n                _timeProvider,\n                _policyOptions.DetectionWindowSize,\n                _policyOptions.MinimalTotalCountThreshold,\n                failed);\n            return failureRate < rateLimit ? DestinationHealth.Healthy : DestinationHealth.Unhealthy;\n        }\n    }\n\n    private static bool TryParse(string stringValue, out double parsedValue)\n    {\n        return double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out parsedValue);\n    }\n\n    private static bool DetermineIfDestinationFailed(HttpContext context)\n    {\n        var errorFeature = context.Features.Get<IForwarderErrorFeature>();\n        if (errorFeature is null)\n        {\n            return false;\n        }\n\n        if (context.RequestAborted.IsCancellationRequested)\n        {\n            // The client disconnected/canceled the request - the failure may not be the destination's fault\n            return false;\n        }\n\n        var error = errorFeature.Error;\n\n        return error == ForwarderError.Request\n            || error == ForwarderError.RequestTimedOut\n            || error == ForwarderError.RequestBodyDestination\n            || error == ForwarderError.ResponseBodyDestination\n            || error == ForwarderError.UpgradeRequestDestination\n            || error == ForwarderError.UpgradeResponseDestination;\n    }\n\n    private sealed class ProxiedRequestHistory\n    {\n        private long _nextRecordCreatedAt;\n        private long _nextRecordTotalCount;\n        private long _nextRecordFailedCount;\n        private long _failedCount;\n        private double _totalCount;\n        private readonly Queue<HistoryRecord> _records = new Queue<HistoryRecord>();\n\n        public double AddNew(TimeProvider timeProvider, TimeSpan detectionWindowSize, int totalCountThreshold, bool failed)\n        {\n            var eventTime = timeProvider.GetTimestamp();\n            var detectionWindowSizeLong = detectionWindowSize.TotalSeconds * timeProvider.TimestampFrequency;\n            if (_nextRecordCreatedAt == 0)\n            {\n                // Initialization.\n                _nextRecordCreatedAt = eventTime + timeProvider.TimestampFrequency;\n            }\n\n            // Don't create a new record on each event because it can negatively affect performance.\n            // Instead, accumulate failed and total request counts reported during some period\n            // and then add only one record storing them.\n            if (eventTime >= _nextRecordCreatedAt)\n            {\n                _records.Enqueue(new HistoryRecord(_nextRecordCreatedAt, _nextRecordTotalCount, _nextRecordFailedCount));\n                _nextRecordCreatedAt = eventTime + timeProvider.TimestampFrequency;\n                _nextRecordTotalCount = 0;\n                _nextRecordFailedCount = 0;\n            }\n\n            _nextRecordTotalCount++;\n            _totalCount++;\n            if (failed)\n            {\n                _failedCount++;\n                _nextRecordFailedCount++;\n            }\n\n            while (_records.Count > 0 && (eventTime - _records.Peek().RecordedAt > detectionWindowSizeLong))\n            {\n                var removed = _records.Dequeue();\n                _failedCount -= removed.FailedCount;\n                _totalCount -= removed.TotalCount;\n            }\n\n            return _totalCount < totalCountThreshold || _totalCount == 0 ? 0.0 : _failedCount / _totalCount;\n        }\n\n        private readonly struct HistoryRecord\n        {\n            public HistoryRecord(long recordedAt, long totalCount, long failedCount)\n            {\n                RecordedAt = recordedAt;\n                TotalCount = totalCount;\n                FailedCount = failedCount;\n            }\n\n            public long RecordedAt { get; }\n\n            public long TotalCount { get; }\n\n            public long FailedCount { get; }\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Health/TransportFailureRateHealthPolicyOptions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Health;\n\n/// <summary>\n/// Defines options for the transport failure rate passive health policy.\n/// </summary>\npublic class TransportFailureRateHealthPolicyOptions\n{\n    /// <summary>\n    /// Name of failure rate limit metadata parameter. Destination marked as unhealthy once this limit is reached.\n    /// </summary>\n    public static readonly string FailureRateLimitMetadataName = \"TransportFailureRateHealthPolicy.RateLimit\";\n\n    /// <summary>\n    /// Period of time while detected failures are kept and taken into account in the rate calculation.\n    /// The default is 60 seconds.\n    /// </summary>\n    public TimeSpan DetectionWindowSize { get; set; } = TimeSpan.FromSeconds(60);\n\n    /// <summary>\n    /// Minimal total number of requests which must be proxied to a destination within the detection window\n    /// before this policy starts evaluating the destination's health and enforcing the failure rate limit.\n    /// The default is 10.\n    /// </summary>\n    public int MinimalTotalCountThreshold { get; set; } = 10;\n\n    /// <summary>\n    /// Default failure rate limit for a destination to be marked as unhealthy that is applied if it's not set on a cluster's metadata.\n    /// It's calculated as a percentage of failed requests out of all requests proxied to the same destination in the <see cref=\"DetectionWindowSize\"/> period.\n    /// The value is in range (0,1). The default is 0.3 (30%).\n    /// </summary>\n    public double DefaultFailureRateLimit { get; set; } = 0.3;\n}\n"
  },
  {
    "path": "src/ReverseProxy/Limits/LimitsMiddleware.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.ReverseProxy.Limits;\n\n/// <summary>\n/// Updates request limits based on route config. This is implemented as middleware at the end of the proxy\n/// pipeline so that apps can call ReassignProxyRequest to move the request to a different route before limits are applied. While similar to a proposed aspnetcore feature (https://github.com/dotnet/aspnetcore/issues/40452),\n/// the possibility of reassigning routes means we need to apply this limit very late. Trying to apply it twice could\n/// result in unexpected behavior like being unable to set it back to the server default.\n/// </summary>\ninternal sealed class LimitsMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly ILogger _logger;\n\n    public LimitsMiddleware(RequestDelegate next, ILogger<LimitsMiddleware> logger)\n    {\n        ArgumentNullException.ThrowIfNull(next);\n        ArgumentNullException.ThrowIfNull(logger);\n\n        _next = next;\n        _logger = logger;\n    }\n\n    /// <inheritdoc/>\n    public Task Invoke(HttpContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        var config = context.GetRouteModel().Config;\n\n        if (config.MaxRequestBodySize.HasValue)\n        {\n            var sizeFeature = context.Features.Get<IHttpMaxRequestBodySizeFeature>();\n            if (sizeFeature != null && !sizeFeature.IsReadOnly)\n            {\n                // -1 for disabled\n                var limit = config.MaxRequestBodySize.Value;\n                long? newValue = limit == -1 ? null : limit;\n                sizeFeature.MaxRequestBodySize = newValue;\n                Log.MaxRequestBodySizeSet(_logger, limit);\n            }\n        }\n\n        return _next(context);\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, long?, Exception?> _maxRequestBodySizeSet = LoggerMessage.Define<long?>(\n            LogLevel.Debug,\n            EventIds.MaxRequestBodySizeSet,\n            \"The MaxRequestBodySize has been set to '{limit}'.\");\n\n        public static void MaxRequestBodySizeSet(ILogger logger, long? limit)\n        {\n            _maxRequestBodySizeSet(logger, limit, null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/LoadBalancing/AppBuilderLoadBalancingExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.ReverseProxy.LoadBalancing;\n\nnamespace Microsoft.AspNetCore.Builder;\n\n/// <summary>\n/// Extensions for adding proxy middleware to the pipeline.\n/// </summary>\npublic static class AppBuilderLoadBalancingExtensions\n{\n    /// <summary>\n    /// Load balances across the available endpoints.\n    /// </summary>\n    public static IReverseProxyApplicationBuilder UseLoadBalancing(this IReverseProxyApplicationBuilder builder)\n    {\n        builder.UseMiddleware<LoadBalancingMiddleware>();\n        return builder;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/LoadBalancing/FirstLoadBalancingPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.LoadBalancing;\n\n/// <summary>\n/// Select the alphabetically first available destination without considering load. This is useful for dual destination fail-over systems.\n/// </summary>\ninternal sealed class FirstLoadBalancingPolicy : ILoadBalancingPolicy\n{\n    public string Name => LoadBalancingPolicies.FirstAlphabetical;\n\n    public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList<DestinationState> availableDestinations)\n    {\n        if (availableDestinations.Count == 0)\n        {\n            return null;\n        }\n\n        var selectedDestination = availableDestinations[0];\n        for (var i = 1; i < availableDestinations.Count; i++)\n        {\n            var destination = availableDestinations[i];\n            if (string.Compare(selectedDestination.DestinationId, destination.DestinationId, StringComparison.OrdinalIgnoreCase) > 0)\n            {\n                selectedDestination = destination;\n            }\n        }\n\n        return selectedDestination;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/LoadBalancing/ILoadBalancingPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.LoadBalancing;\n\n/// <summary>\n/// Provides a method that applies a load balancing policy\n/// to select a destination.\n/// </summary>\npublic interface ILoadBalancingPolicy\n{\n    /// <summary>\n    ///  A unique identifier for this load balancing policy. This will be referenced from config.\n    /// </summary>\n    string Name { get; }\n\n    /// <summary>\n    /// Picks a destination to send traffic to.\n    /// </summary>\n    DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList<DestinationState> availableDestinations);\n}\n"
  },
  {
    "path": "src/ReverseProxy/LoadBalancing/LeastRequestsLoadBalancingPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.LoadBalancing;\n\ninternal sealed class LeastRequestsLoadBalancingPolicy : ILoadBalancingPolicy\n{\n    public string Name => LoadBalancingPolicies.LeastRequests;\n\n    public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList<DestinationState> availableDestinations)\n    {\n        if (availableDestinations.Count == 0)\n        {\n            return null;\n        }\n\n        var destinationCount = availableDestinations.Count;\n        var leastRequestsDestination = availableDestinations[0];\n        var leastRequestsCount = leastRequestsDestination.ConcurrentRequestCount;\n        for (var i = 1; i < destinationCount; i++)\n        {\n            var destination = availableDestinations[i];\n            var endpointRequestCount = destination.ConcurrentRequestCount;\n            if (endpointRequestCount < leastRequestsCount)\n            {\n                leastRequestsDestination = destination;\n                leastRequestsCount = endpointRequestCount;\n            }\n        }\n        return leastRequestsDestination;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/LoadBalancing/LoadBalancingMiddleware.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.LoadBalancing;\n\n/// <summary>\n/// Load balances across the available destinations.\n/// </summary>\ninternal sealed class LoadBalancingMiddleware\n{\n    private readonly ILogger _logger;\n    private readonly FrozenDictionary<string, ILoadBalancingPolicy> _loadBalancingPolicies;\n    private readonly RequestDelegate _next;\n\n    public LoadBalancingMiddleware(\n        RequestDelegate next,\n        ILogger<LoadBalancingMiddleware> logger,\n        IEnumerable<ILoadBalancingPolicy> loadBalancingPolicies)\n    {\n        ArgumentNullException.ThrowIfNull(next);\n        ArgumentNullException.ThrowIfNull(logger);\n        ArgumentNullException.ThrowIfNull(loadBalancingPolicies);\n        _next = next;\n        _logger = logger;\n        _loadBalancingPolicies = loadBalancingPolicies.ToDictionaryByUniqueId(p => p.Name);\n    }\n\n    public Task Invoke(HttpContext context)\n    {\n        var proxyFeature = context.GetReverseProxyFeature();\n\n        var destinations = proxyFeature.AvailableDestinations;\n        var destinationCount = destinations.Count;\n\n        DestinationState? destination;\n\n        if (destinationCount == 0)\n        {\n            destination = null;\n        }\n        else if (destinationCount == 1)\n        {\n            destination = destinations[0];\n        }\n        else\n        {\n            var currentPolicy = _loadBalancingPolicies.GetRequiredServiceById(proxyFeature.Cluster.Config.LoadBalancingPolicy, LoadBalancingPolicies.PowerOfTwoChoices);\n            destination = currentPolicy.PickDestination(context, proxyFeature.Route.Cluster!, destinations);\n        }\n\n        if (destination is null)\n        {\n            // We intentionally do not short circuit here, we allow for later middleware to decide how to handle this case.\n            Log.NoAvailableDestinations(_logger, proxyFeature.Cluster.Config.ClusterId);\n            proxyFeature.AvailableDestinations = Array.Empty<DestinationState>();\n        }\n        else\n        {\n            proxyFeature.AvailableDestinations = destination;\n        }\n\n        return _next(context);\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, string, Exception?> _noAvailableDestinations = LoggerMessage.Define<string>(\n            LogLevel.Warning,\n            EventIds.NoAvailableDestinations,\n            \"No available destinations after load balancing for cluster '{clusterId}'.\");\n\n        public static void NoAvailableDestinations(ILogger logger, string clusterId)\n        {\n            _noAvailableDestinations(logger, clusterId, null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/LoadBalancing/LoadBalancingPolicies.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.LoadBalancing;\n\n/// <summary>\n/// Names of built-in load balancing policies.\n/// </summary>\npublic static class LoadBalancingPolicies\n{\n    /// <summary>\n    /// Select the alphabetically first available destination without considering load. This is useful for dual destination fail-over systems.\n    /// </summary>\n    public static string FirstAlphabetical => nameof(FirstAlphabetical);\n\n    /// <summary>\n    /// Select a destination randomly.\n    /// </summary>\n    public static string Random => nameof(Random);\n\n    /// <summary>\n    /// Select a destination by cycling through them in order.\n    /// </summary>\n    public static string RoundRobin => nameof(RoundRobin);\n\n    /// <summary>\n    /// Select the destination with the least assigned requests. This requires examining all destinations.\n    /// </summary>\n    public static string LeastRequests => nameof(LeastRequests);\n\n    /// <summary>\n    /// Select two random destinations and then select the one with the least assigned requests.\n    /// This avoids the overhead of LeastRequests and the worst case for Random where it selects a busy destination.\n    /// </summary>\n    public static string PowerOfTwoChoices => nameof(PowerOfTwoChoices);\n}\n"
  },
  {
    "path": "src/ReverseProxy/LoadBalancing/PowerOfTwoChoicesLoadBalancingPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.LoadBalancing;\n\ninternal sealed class PowerOfTwoChoicesLoadBalancingPolicy : ILoadBalancingPolicy\n{\n    private readonly IRandomFactory _randomFactory;\n\n    public PowerOfTwoChoicesLoadBalancingPolicy(IRandomFactory randomFactory)\n    {\n        _randomFactory = randomFactory;\n    }\n\n    public string Name => LoadBalancingPolicies.PowerOfTwoChoices;\n\n    public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList<DestinationState> availableDestinations)\n    {\n        var destinationCount = availableDestinations.Count;\n        if (destinationCount == 0)\n        {\n            return null;\n        }\n\n        if (destinationCount == 1)\n        {\n            return availableDestinations[0];\n        }\n\n        // Pick two, and then return the least busy. This avoids the effort of searching the whole list, but\n        // still avoids overloading a single destination.\n        var random = _randomFactory.CreateRandomInstance();\n        var firstIndex = random.Next(destinationCount);\n        int secondIndex;\n        do\n        {\n            secondIndex = random.Next(destinationCount);\n        } while (firstIndex == secondIndex);\n        var first = availableDestinations[firstIndex];\n        var second = availableDestinations[secondIndex];\n        return (first.ConcurrentRequestCount <= second.ConcurrentRequestCount) ? first : second;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/LoadBalancing/RandomLoadBalancingPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.LoadBalancing;\n\ninternal sealed class RandomLoadBalancingPolicy : ILoadBalancingPolicy\n{\n    private readonly IRandomFactory _randomFactory;\n\n    public RandomLoadBalancingPolicy(IRandomFactory randomFactory)\n    {\n        _randomFactory = randomFactory;\n    }\n\n    public string Name => LoadBalancingPolicies.Random;\n\n    public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList<DestinationState> availableDestinations)\n    {\n        if (availableDestinations.Count == 0)\n        {\n            return null;\n        }\n\n        var random = _randomFactory.CreateRandomInstance();\n        return availableDestinations[random.Next(availableDestinations.Count)];\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/LoadBalancing/RoundRobinLoadBalancingPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Runtime.CompilerServices;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.LoadBalancing;\n\ninternal sealed class RoundRobinLoadBalancingPolicy : ILoadBalancingPolicy\n{\n    private readonly ConditionalWeakTable<ClusterState, AtomicCounter> _counters = new();\n\n    public string Name => LoadBalancingPolicies.RoundRobin;\n\n    public DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList<DestinationState> availableDestinations)\n    {\n        if (availableDestinations.Count == 0)\n        {\n            return null;\n        }\n\n        var counter = _counters.GetOrCreateValue(cluster);\n\n        // Increment returns the new value and we want the first return value to be 0.\n        var offset = counter.Increment() - 1;\n\n        // Preventing negative indices from being computed by masking off sign.\n        // Ordering of index selection is consistent across all offsets.\n        // There may be a discontinuity when the sign of offset changes.\n        return availableDestinations[(offset & 0x7FFFFFFF) % availableDestinations.Count];\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Management/IProxyStateLookup.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Diagnostics.CodeAnalysis;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy;\n\n/// <summary>\n/// Allows access to the proxy's current set of routes and clusters.\n/// </summary>\npublic interface IProxyStateLookup\n{\n    /// <summary>\n    /// Retrieves a specific route by id, if present.\n    /// </summary>\n    bool TryGetRoute(string id, [NotNullWhen(true)] out RouteModel? route);\n\n    /// <summary>\n    /// Enumerates all current routes. This is thread safe but the collection may change mid-enumeration if the configuration is reloaded.\n    /// </summary>\n    IEnumerable<RouteModel> GetRoutes();\n\n    /// <summary>\n    /// Retrieves a specific cluster by id, if present.\n    /// </summary>\n    bool TryGetCluster(string id, [NotNullWhen(true)] out ClusterState? cluster);\n\n    /// <summary>\n    /// Enumerates all current clusters. This is thread safe but the collection may change mid-enumeration if the configuration is reloaded.\n    /// </summary>\n    IEnumerable<ClusterState> GetClusters();\n}\n"
  },
  {
    "path": "src/ReverseProxy/Management/IReverseProxyBuilder.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\n/// <summary>\n/// Reverse Proxy builder interface.\n/// </summary>\npublic interface IReverseProxyBuilder\n{\n    /// <summary>\n    /// Gets the services.\n    /// </summary>\n    IServiceCollection Services { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Management/IReverseProxyBuilderExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Linq;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Configuration.ClusterValidators;\nusing Yarp.ReverseProxy.Configuration.RouteValidators;\nusing Yarp.ReverseProxy.Delegation;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Health;\nusing Yarp.ReverseProxy.LoadBalancing;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Routing;\nusing Yarp.ReverseProxy.ServiceDiscovery;\nusing Yarp.ReverseProxy.SessionAffinity;\nusing Yarp.ReverseProxy.Transforms;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Management;\n\ninternal static class IReverseProxyBuilderExtensions\n{\n    public static IReverseProxyBuilder AddConfigBuilder(this IReverseProxyBuilder builder)\n    {\n        builder.Services.TryAddSingleton<IYarpRateLimiterPolicyProvider, YarpRateLimiterPolicyProvider>();\n        builder.Services.TryAddSingleton<IYarpOutputCachePolicyProvider, YarpOutputCachePolicyProvider>();\n        builder.Services.TryAddSingleton<IConfigValidator, ConfigValidator>();\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IRouteValidator, AuthorizationPolicyValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IRouteValidator, RateLimitPolicyValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IRouteValidator, OutputCachePolicyValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IRouteValidator, TimeoutPolicyValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IRouteValidator, CorsPolicyValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IRouteValidator, HeadersValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IRouteValidator, HostValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IRouteValidator, MethodsValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IRouteValidator, PathValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IRouteValidator, QueryParametersValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IClusterValidator, DestinationValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IClusterValidator, LoadBalancingValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IClusterValidator, HealthCheckValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IClusterValidator, SessionAffinityValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IClusterValidator, ProxyHttpClientValidator>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IClusterValidator, ProxyHttpRequestValidator>());\n        builder.Services.TryAddSingleton<IRandomFactory, RandomFactory>();\n        builder.AddTransformFactory<ForwardedTransformFactory>();\n        builder.AddTransformFactory<HttpMethodTransformFactory>();\n        builder.AddTransformFactory<PathTransformFactory>();\n        builder.AddTransformFactory<QueryTransformFactory>();\n        builder.AddTransformFactory<RequestHeadersTransformFactory>();\n        builder.AddTransformFactory<ResponseTransformFactory>();\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HeaderMatcherPolicy>());\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, QueryParameterMatcherPolicy>());\n        return builder;\n    }\n\n    public static IReverseProxyBuilder AddRuntimeStateManagers(this IReverseProxyBuilder builder)\n    {\n        builder.Services.TryAddSingleton<IDestinationHealthUpdater, DestinationHealthUpdater>();\n\n        builder.Services.TryAddSingleton<IClusterDestinationsUpdater, ClusterDestinationsUpdater>();\n        builder.Services.TryAddEnumerable(new[] {\n            ServiceDescriptor.Singleton<IAvailableDestinationsPolicy, HealthyAndUnknownDestinationsPolicy>(),\n            ServiceDescriptor.Singleton<IAvailableDestinationsPolicy, HealthyOrPanicDestinationsPolicy>()\n        });\n        return builder;\n    }\n\n    public static IReverseProxyBuilder AddConfigManager(this IReverseProxyBuilder builder)\n    {\n        builder.Services.TryAddSingleton<ProxyConfigManager>();\n        builder.Services.TryAddSingleton<IProxyStateLookup>(sp => sp.GetRequiredService<ProxyConfigManager>());\n        return builder;\n    }\n\n    public static IReverseProxyBuilder AddProxy(this IReverseProxyBuilder builder)\n    {\n        builder.Services.TryAddSingleton<IForwarderHttpClientFactory, ForwarderHttpClientFactory>();\n\n        builder.Services.AddHttpForwarder();\n        return builder;\n    }\n\n    public static IReverseProxyBuilder AddLoadBalancingPolicies(this IReverseProxyBuilder builder)\n    {\n        builder.Services.TryAddSingleton<IRandomFactory, RandomFactory>();\n\n        builder.Services.TryAddEnumerable(new[] {\n            ServiceDescriptor.Singleton<ILoadBalancingPolicy, FirstLoadBalancingPolicy>(),\n            ServiceDescriptor.Singleton<ILoadBalancingPolicy, LeastRequestsLoadBalancingPolicy>(),\n            ServiceDescriptor.Singleton<ILoadBalancingPolicy, RandomLoadBalancingPolicy>(),\n            ServiceDescriptor.Singleton<ILoadBalancingPolicy, PowerOfTwoChoicesLoadBalancingPolicy>(),\n            ServiceDescriptor.Singleton<ILoadBalancingPolicy, RoundRobinLoadBalancingPolicy>()\n        });\n\n        return builder;\n    }\n\n    public static IReverseProxyBuilder AddSessionAffinityPolicies(this IReverseProxyBuilder builder)\n    {\n        builder.Services.TryAddEnumerable(new[] {\n            ServiceDescriptor.Singleton<IAffinityFailurePolicy, RedistributeAffinityFailurePolicy>(),\n            ServiceDescriptor.Singleton<IAffinityFailurePolicy, Return503ErrorAffinityFailurePolicy>()\n        });\n        builder.Services.TryAddEnumerable(new[] {\n            ServiceDescriptor.Singleton<ISessionAffinityPolicy, CookieSessionAffinityPolicy>(),\n            ServiceDescriptor.Singleton<ISessionAffinityPolicy, HashCookieSessionAffinityPolicy>(),\n            ServiceDescriptor.Singleton<ISessionAffinityPolicy, ArrCookieSessionAffinityPolicy>(),\n            ServiceDescriptor.Singleton<ISessionAffinityPolicy, CustomHeaderSessionAffinityPolicy>()\n        });\n        builder.AddTransforms<AffinitizeTransformProvider>();\n\n        return builder;\n    }\n\n    public static IReverseProxyBuilder AddActiveHealthChecks(this IReverseProxyBuilder builder)\n    {\n        builder.Services.TryAddSingleton<IProbingRequestFactory, DefaultProbingRequestFactory>();\n\n        // Avoid registering several IActiveHealthCheckMonitor implementations.\n        if (!builder.Services.Any(d => d.ServiceType == typeof(IActiveHealthCheckMonitor)))\n        {\n            builder.Services.AddSingleton<ActiveHealthCheckMonitor>();\n            builder.Services.AddSingleton<IActiveHealthCheckMonitor>(p =>\n                p.GetRequiredService<ActiveHealthCheckMonitor>());\n            builder.Services.AddSingleton<IClusterChangeListener>(p =>\n                p.GetRequiredService<ActiveHealthCheckMonitor>());\n        }\n\n        builder.Services.AddSingleton<IActiveHealthCheckPolicy, ConsecutiveFailuresHealthPolicy>();\n        return builder;\n    }\n\n    public static IReverseProxyBuilder AddPassiveHealthCheck(this IReverseProxyBuilder builder)\n    {\n        builder.Services.AddSingleton<IPassiveHealthCheckPolicy, TransportFailureRateHealthPolicy>();\n        return builder;\n    }\n\n    public static IReverseProxyBuilder AddHttpSysDelegation(this IReverseProxyBuilder builder)\n    {\n        builder.Services.AddSingleton<HttpSysDelegator>();\n        builder.Services.TryAddSingleton<IHttpSysDelegator>(p => p.GetRequiredService<HttpSysDelegator>());\n        builder.Services.AddSingleton<IClusterChangeListener>(p => p.GetRequiredService<HttpSysDelegator>());\n\n        return builder;\n    }\n\n    public static IReverseProxyBuilder AddDestinationResolver(this IReverseProxyBuilder builder)\n    {\n        builder.Services.TryAddSingleton<IDestinationResolver, NoOpDestinationResolver>();\n        return builder;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Management/ProxyConfigManager.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Diagnostics;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Primitives;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Health;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Routing;\nusing Yarp.ReverseProxy.ServiceDiscovery;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Management;\n\n/// <summary>\n/// Provides a method to apply Proxy configuration changes.\n/// Also an Implementation of <see cref=\"EndpointDataSource\"/> that supports being dynamically updated\n/// in a thread-safe manner while avoiding locks on the hot path.\n/// </summary>\n// https://github.com/dotnet/aspnetcore/blob/cbe16474ce9db7ff588aed89596ff4df5c3f62e1/src/Mvc/Mvc.Core/src/Routing/ActionEndpointDataSourceBase.cs\ninternal sealed class ProxyConfigManager : EndpointDataSource, IProxyStateLookup, IDisposable\n{\n    private static readonly IReadOnlyDictionary<string, ClusterConfig> _emptyClusterDictionary = new ReadOnlyDictionary<string, ClusterConfig>(new Dictionary<string, ClusterConfig>());\n\n    private readonly object _syncRoot = new();\n    private readonly ILogger<ProxyConfigManager> _logger;\n    private readonly IProxyConfigProvider[] _providers;\n    private readonly ConfigState[] _configs;\n    private readonly IClusterChangeListener[] _clusterChangeListeners;\n    private readonly ConcurrentDictionary<string, ClusterState> _clusters = new(StringComparer.OrdinalIgnoreCase);\n    private readonly ConcurrentDictionary<string, RouteState> _routes = new(StringComparer.OrdinalIgnoreCase);\n    private readonly IProxyConfigFilter[] _filters;\n    private readonly IConfigValidator _configValidator;\n    private readonly IForwarderHttpClientFactory _httpClientFactory;\n    private readonly ProxyEndpointFactory _proxyEndpointFactory;\n    private readonly ITransformBuilder _transformBuilder;\n    private readonly List<Action<EndpointBuilder>> _conventions;\n    private readonly IActiveHealthCheckMonitor _activeHealthCheckMonitor;\n    private readonly IClusterDestinationsUpdater _clusterDestinationsUpdater;\n    private readonly IDestinationResolver _destinationResolver;\n    private readonly IConfigChangeListener[] _configChangeListeners;\n    private List<Endpoint>? _endpoints;\n    private CancellationTokenSource _endpointsChangeSource = new();\n    private IChangeToken _endpointsChangeToken;\n\n    private CancellationTokenSource _configChangeSource = new();\n\n    public ProxyConfigManager(\n        ILogger<ProxyConfigManager> logger,\n        IEnumerable<IProxyConfigProvider> providers,\n        IEnumerable<IClusterChangeListener> clusterChangeListeners,\n        IEnumerable<IProxyConfigFilter> filters,\n        IConfigValidator configValidator,\n        ProxyEndpointFactory proxyEndpointFactory,\n        ITransformBuilder transformBuilder,\n        IForwarderHttpClientFactory httpClientFactory,\n        IActiveHealthCheckMonitor activeHealthCheckMonitor,\n        IClusterDestinationsUpdater clusterDestinationsUpdater,\n        IEnumerable<IConfigChangeListener> configChangeListeners,\n        IDestinationResolver destinationResolver)\n    {\n        ArgumentNullException.ThrowIfNull(logger);\n        ArgumentNullException.ThrowIfNull(providers);\n        ArgumentNullException.ThrowIfNull(clusterChangeListeners);\n        ArgumentNullException.ThrowIfNull(filters);\n        ArgumentNullException.ThrowIfNull(configValidator);\n        ArgumentNullException.ThrowIfNull(proxyEndpointFactory);\n        ArgumentNullException.ThrowIfNull(transformBuilder);\n        ArgumentNullException.ThrowIfNull(httpClientFactory);\n        ArgumentNullException.ThrowIfNull(activeHealthCheckMonitor);\n        ArgumentNullException.ThrowIfNull(clusterDestinationsUpdater);\n        ArgumentNullException.ThrowIfNull(configChangeListeners);\n        ArgumentNullException.ThrowIfNull(destinationResolver);\n\n        _logger = logger;\n        _providers = providers.ToArray();\n        _clusterChangeListeners = clusterChangeListeners.ToArray();\n        _filters = filters.ToArray();\n        _configValidator = configValidator;\n        _proxyEndpointFactory = proxyEndpointFactory;\n        _transformBuilder = transformBuilder;\n        _httpClientFactory = httpClientFactory;\n        _activeHealthCheckMonitor = activeHealthCheckMonitor;\n        _clusterDestinationsUpdater = clusterDestinationsUpdater;\n        _destinationResolver = destinationResolver;\n        _configChangeListeners = configChangeListeners.ToArray();\n\n        if (_providers.Length == 0)\n        {\n            throw new ArgumentException($\"At least one {nameof(IProxyConfigProvider)} is required.\", nameof(providers));\n        }\n\n        _configs = new ConfigState[_providers.Length];\n\n        _conventions = new List<Action<EndpointBuilder>>();\n        DefaultBuilder = new ReverseProxyConventionBuilder(_conventions);\n\n        _endpointsChangeToken = new CancellationChangeToken(_endpointsChangeSource.Token);\n    }\n\n    public ReverseProxyConventionBuilder DefaultBuilder { get; }\n\n    // EndpointDataSource\n\n    /// <inheritdoc/>\n    public override IReadOnlyList<Endpoint> Endpoints\n    {\n        get\n        {\n            // The Endpoints needs to be lazy the first time to give a chance to ReverseProxyConventionBuilder to add its conventions.\n            // Endpoints are accessed by routing on the first request.\n            if (_endpoints is null)\n            {\n                lock (_syncRoot)\n                {\n                    if (_endpoints is null)\n                    {\n                        CreateEndpoints();\n                    }\n                }\n            }\n            return _endpoints;\n        }\n    }\n\n    [MemberNotNull(nameof(_endpoints))]\n    private void CreateEndpoints()\n    {\n        var endpoints = new List<Endpoint>();\n        // Directly enumerate the ConcurrentDictionary to limit locking and copying.\n        foreach (var existingRoute in _routes)\n        {\n            // Only rebuild the endpoint for modified routes or clusters.\n            var endpoint = existingRoute.Value.CachedEndpoint;\n            if (endpoint is null)\n            {\n                endpoint = _proxyEndpointFactory.CreateEndpoint(existingRoute.Value.Model, _conventions);\n                existingRoute.Value.CachedEndpoint = endpoint;\n            }\n            endpoints.Add(endpoint);\n        }\n\n        UpdateEndpoints(endpoints);\n    }\n\n    /// <inheritdoc/>\n    public override IChangeToken GetChangeToken() => Volatile.Read(ref _endpointsChangeToken);\n\n    private static IProxyConfig[] ExtractListOfProxyConfigs(IEnumerable<ConfigState> configStates)\n    {\n        return configStates.Select(state => state.LatestConfig).ToArray();\n    }\n\n    internal async Task<EndpointDataSource> InitialLoadAsync()\n    {\n        // Trigger the first load immediately and throw if it fails.\n        // We intend this to crash the app, so we don't try listening for further changes.\n        try\n        {\n            var routes = new List<RouteConfig>();\n            var clusters = new List<ClusterConfig>();\n\n            // Begin resolving config providers concurrently.\n            var resolvedConfigs = new List<(int Index, IProxyConfigProvider Provider, ValueTask<IProxyConfig> Config)>(_providers.Length);\n            for (var i = 0; i < _providers.Length; i++)\n            {\n                var provider = _providers[i];\n                var configLoadTask = LoadConfigAsync(provider, cancellationToken: default);\n                resolvedConfigs.Add((i, provider, configLoadTask));\n            }\n\n            // Wait for all configs to be resolved.\n            foreach (var (i, provider, configLoadTask) in resolvedConfigs)\n            {\n                var config = await configLoadTask;\n                _configs[i] = new ConfigState(provider, config);\n                routes.AddRange(config.Routes ?? Array.Empty<RouteConfig>());\n                clusters.AddRange(config.Clusters ?? Array.Empty<ClusterConfig>());\n            }\n\n            var proxyConfigs = ExtractListOfProxyConfigs(_configs);\n\n            foreach (var configChangeListener in _configChangeListeners)\n            {\n                configChangeListener.ConfigurationLoaded(proxyConfigs);\n            }\n\n            await ApplyConfigAsync(routes, clusters);\n\n            foreach (var configChangeListener in _configChangeListeners)\n            {\n                configChangeListener.ConfigurationApplied(proxyConfigs);\n            }\n\n            ListenForConfigChanges();\n        }\n        catch (Exception ex)\n        {\n            throw new InvalidOperationException(\"Unable to load or apply the proxy configuration.\", ex);\n        }\n\n        // Initial active health check is run in the background.\n        // Directly enumerate the ConcurrentDictionary to limit locking and copying.\n        _ = _activeHealthCheckMonitor.CheckHealthAsync(_clusters.Select(pair => pair.Value));\n        return this;\n    }\n\n    private async Task ReloadConfigAsync()\n    {\n        _configChangeSource.Dispose();\n\n        var sourcesChanged = false;\n        var routes = new List<RouteConfig>();\n        var clusters = new List<ClusterConfig>();\n        var reloadedConfigs = new List<(ConfigState Config, ValueTask<IProxyConfig> ResolveTask)>();\n\n        // Start reloading changed configurations.\n        foreach (var instance in _configs)\n        {\n            if (instance.LatestConfig.ChangeToken.HasChanged)\n            {\n                try\n                {\n                    var reloadTask = LoadConfigAsync(instance.Provider, cancellationToken: default);\n                    reloadedConfigs.Add((instance, reloadTask));\n                }\n                catch (Exception ex)\n                {\n                    OnConfigLoadError(instance, ex);\n                }\n            }\n        }\n\n        // Wait for all changed config providers to be reloaded.\n        foreach (var (instance, loadTask) in reloadedConfigs)\n        {\n            try\n            {\n                instance.LatestConfig = await loadTask.ConfigureAwait(false);\n                instance.LoadFailed = false;\n                sourcesChanged = true;\n            }\n            catch (Exception ex)\n            {\n                OnConfigLoadError(instance, ex);\n            }\n        }\n\n        // Extract the routes and clusters from the configs, regardless of whether they were reloaded.\n        foreach (var instance in _configs)\n        {\n            if (instance.LatestConfig.Routes is { Count: > 0 } updatedRoutes)\n            {\n                routes.AddRange(updatedRoutes);\n            }\n\n            if (instance.LatestConfig.Clusters is { Count: > 0 } updatedClusters)\n            {\n                clusters.AddRange(updatedClusters);\n            }\n        }\n\n        var proxyConfigs = ExtractListOfProxyConfigs(_configs);\n        foreach (var configChangeListener in _configChangeListeners)\n        {\n            configChangeListener.ConfigurationLoaded(proxyConfigs);\n        }\n\n        try\n        {\n            // Only reload if at least one provider changed.\n            if (sourcesChanged)\n            {\n                var hasChanged = await ApplyConfigAsync(routes, clusters);\n                lock (_syncRoot)\n                {\n                    // Skip if changes are signaled before the endpoints are initialized for the first time.\n                    // The endpoint conventions might not be ready yet.\n                    if (hasChanged && _endpoints is not null)\n                    {\n                        CreateEndpoints();\n                    }\n                }\n            }\n\n            foreach (var configChangeListener in _configChangeListeners)\n            {\n                configChangeListener.ConfigurationApplied(proxyConfigs);\n            }\n        }\n        catch (Exception ex)\n        {\n            Log.ErrorApplyingConfig(_logger, ex);\n\n            foreach (var configChangeListener in _configChangeListeners)\n            {\n                configChangeListener.ConfigurationApplyingFailed(proxyConfigs, ex);\n            }\n        }\n\n        ListenForConfigChanges();\n\n        void OnConfigLoadError(ConfigState instance, Exception ex)\n        {\n            instance.LoadFailed = true;\n            Log.ErrorReloadingConfig(_logger, ex);\n\n            foreach (var configChangeListener in _configChangeListeners)\n            {\n                configChangeListener.ConfigurationLoadingFailed(instance.Provider, ex);\n            }\n        }\n    }\n\n    private static void ValidateConfigProperties(IProxyConfig config)\n    {\n        if (config is null)\n        {\n            throw new InvalidOperationException($\"{nameof(IProxyConfigProvider.GetConfig)} returned a null value.\");\n        }\n\n        if (config.ChangeToken is null)\n        {\n            throw new InvalidOperationException($\"{nameof(IProxyConfig.ChangeToken)} has a null value.\");\n        }\n    }\n\n    private ValueTask<IProxyConfig> LoadConfigAsync(IProxyConfigProvider provider, CancellationToken cancellationToken)\n    {\n        var config = provider.GetConfig();\n        ValidateConfigProperties(config);\n\n        if (_destinationResolver.GetType() == typeof(NoOpDestinationResolver))\n        {\n            return new(config);\n        }\n\n        return LoadConfigAsyncCore(config, cancellationToken);\n    }\n\n    private async ValueTask<IProxyConfig> LoadConfigAsyncCore(IProxyConfig config, CancellationToken cancellationToken)\n    {\n        List<(int Index, ValueTask<ResolvedDestinationCollection> Task)> resolverTasks = new();\n        List<ClusterConfig> clusters = new(config.Clusters);\n        List<IChangeToken>? changeTokens = null;\n        for (var i = 0; i < clusters.Count; i++)\n        {\n            var cluster = clusters[i];\n            if (cluster.Destinations is { Count: > 0 } destinations)\n            {\n                // Resolve destinations if there are any.\n                var task = _destinationResolver.ResolveDestinationsAsync(destinations, cancellationToken);\n                resolverTasks.Add((i, task));\n            }\n        }\n\n        if (resolverTasks.Count > 0)\n        {\n            foreach (var (i, task) in resolverTasks)\n            {\n                ResolvedDestinationCollection resolvedDestinations;\n                try\n                {\n                    resolvedDestinations = await task;\n                }\n                catch (Exception exception)\n                {\n                    var cluster = clusters[i];\n                    throw new InvalidOperationException($\"Error resolving destinations for cluster {cluster.ClusterId}\", exception);\n                }\n\n                clusters[i] = clusters[i] with { Destinations = resolvedDestinations.Destinations };\n                if (resolvedDestinations.ChangeToken is { } token)\n                {\n                    changeTokens ??= new();\n                    changeTokens.Add(token);\n                }\n            }\n\n            IChangeToken changeToken;\n            if (changeTokens is not null)\n            {\n                // Combine change tokens from the resolver with the configuration's existing change token.\n                changeTokens.Add(config.ChangeToken);\n                changeToken = new CompositeChangeToken(changeTokens);\n            }\n            else\n            {\n                changeToken = config.ChangeToken;\n            }\n\n            // Return updated config\n            return new ResolvedProxyConfig(config, clusters, changeToken);\n        }\n\n        return config;\n    }\n\n    private sealed class ResolvedProxyConfig : IProxyConfig\n    {\n        private readonly IProxyConfig _innerConfig;\n\n        public ResolvedProxyConfig(IProxyConfig innerConfig, IReadOnlyList<ClusterConfig> clusters, IChangeToken changeToken)\n        {\n            _innerConfig = innerConfig;\n            Clusters = clusters;\n            ChangeToken = changeToken;\n        }\n\n        public IReadOnlyList<RouteConfig> Routes => _innerConfig.Routes;\n        public IReadOnlyList<ClusterConfig> Clusters { get; }\n        public IChangeToken ChangeToken { get; }\n    }\n\n    private void ListenForConfigChanges()\n    {\n        // Use a central change token to avoid overlap between different sources.\n        var source = new CancellationTokenSource();\n        _configChangeSource = source;\n        var poll = false;\n\n        foreach (var configState in _configs)\n        {\n            if (configState.LoadFailed)\n            {\n                // We can't register for change notifications if the last load failed.\n                poll = true;\n                continue;\n            }\n\n            configState.CallbackCleanup?.Dispose();\n            var token = configState.LatestConfig.ChangeToken;\n            if (token.ActiveChangeCallbacks)\n            {\n                configState.CallbackCleanup = token.RegisterChangeCallback(SignalChange, source);\n            }\n            else\n            {\n                poll = true;\n            }\n        }\n\n        if (poll)\n        {\n            source.CancelAfter(TimeSpan.FromMinutes(5));\n        }\n\n        // Don't register until we're done hooking everything up to avoid cancellation races.\n        source.Token.Register(ReloadConfig, this);\n\n        static void SignalChange(object? obj)\n        {\n            var token = (CancellationTokenSource)obj!;\n            try\n            {\n                token.Cancel();\n            }\n            // Don't throw if the source was already disposed.\n            catch (ObjectDisposedException) { }\n        }\n\n        static void ReloadConfig(object? state)\n        {\n            var manager = (ProxyConfigManager)state!;\n            _ = manager.ReloadConfigAsync();\n        }\n    }\n\n    // Throws for validation failures\n    private async Task<bool> ApplyConfigAsync(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters)\n    {\n        var (configuredClusters, clusterErrors) = await VerifyClustersAsync(clusters, cancellation: default);\n        var (configuredRoutes, routeErrors) = await VerifyRoutesAsync(routes, configuredClusters, cancellation: default);\n\n        if (routeErrors.Count > 0 || clusterErrors.Count > 0)\n        {\n            throw new AggregateException(\"The proxy config is invalid.\", routeErrors.Concat(clusterErrors));\n        }\n\n        // Update clusters first because routes need to reference them.\n        UpdateRuntimeClusters(configuredClusters);\n        var routesChanged = UpdateRuntimeRoutes(configuredRoutes);\n        return routesChanged;\n    }\n\n    private async Task<(IList<RouteConfig>, IList<Exception>)> VerifyRoutesAsync(IReadOnlyList<RouteConfig> routes, IReadOnlyDictionary<string, ClusterConfig> clusters, CancellationToken cancellation)\n    {\n        if (routes is null)\n        {\n            return (Array.Empty<RouteConfig>(), Array.Empty<Exception>());\n        }\n\n        var seenRouteIds = new HashSet<string>(routes.Count, StringComparer.OrdinalIgnoreCase);\n        var configuredRoutes = new List<RouteConfig>(routes.Count);\n        var errors = new List<Exception>();\n\n        foreach (var r in routes)\n        {\n            if (seenRouteIds.Contains(r.RouteId))\n            {\n                errors.Add(new ArgumentException($\"Duplicate route '{r.RouteId}'\"));\n                continue;\n            }\n\n            var route = r;\n\n            try\n            {\n                if (_filters.Length != 0)\n                {\n                    ClusterConfig? cluster = null;\n                    if (route.ClusterId is not null)\n                    {\n                        clusters.TryGetValue(route.ClusterId, out cluster);\n                    }\n\n                    foreach (var filter in _filters)\n                    {\n                        route = await filter.ConfigureRouteAsync(route, cluster, cancellation);\n                    }\n                }\n            }\n            catch (Exception ex)\n            {\n                errors.Add(new Exception($\"An exception was thrown from the configuration callbacks for route '{r.RouteId}'.\", ex));\n                continue;\n            }\n\n            var routeErrors = await _configValidator.ValidateRouteAsync(route);\n            if (routeErrors.Count > 0)\n            {\n                errors.AddRange(routeErrors);\n                continue;\n            }\n\n            seenRouteIds.Add(route.RouteId);\n            configuredRoutes.Add(route);\n        }\n\n        if (errors.Count > 0)\n        {\n            return (Array.Empty<RouteConfig>(), errors);\n        }\n\n        return (configuredRoutes, errors);\n    }\n\n    private async Task<(IReadOnlyDictionary<string, ClusterConfig>, IList<Exception>)> VerifyClustersAsync(IReadOnlyList<ClusterConfig> clusters, CancellationToken cancellation)\n    {\n        if (clusters is null)\n        {\n            return (_emptyClusterDictionary, Array.Empty<Exception>());\n        }\n\n        var configuredClusters = new Dictionary<string, ClusterConfig>(clusters.Count, StringComparer.OrdinalIgnoreCase);\n        var errors = new List<Exception>();\n        // The IProxyConfigProvider provides a fresh snapshot that we need to reconfigure each time.\n        foreach (var c in clusters)\n        {\n            try\n            {\n                if (configuredClusters.ContainsKey(c.ClusterId))\n                {\n                    errors.Add(new ArgumentException($\"Duplicate cluster '{c.ClusterId}'.\"));\n                    continue;\n                }\n\n                // Don't modify the original\n                var cluster = c;\n\n                foreach (var filter in _filters)\n                {\n                    cluster = await filter.ConfigureClusterAsync(cluster, cancellation);\n                }\n\n                var clusterErrors = await _configValidator.ValidateClusterAsync(cluster);\n                if (clusterErrors.Count > 0)\n                {\n                    errors.AddRange(clusterErrors);\n                    continue;\n                }\n\n                configuredClusters.Add(cluster.ClusterId, cluster);\n            }\n            catch (Exception ex)\n            {\n                errors.Add(new ArgumentException($\"An exception was thrown from the configuration callbacks for cluster '{c.ClusterId}'.\", ex));\n            }\n        }\n\n        if (errors.Count > 0)\n        {\n            return (_emptyClusterDictionary, errors);\n        }\n\n        return (configuredClusters, errors);\n    }\n\n    private void UpdateRuntimeClusters(IReadOnlyDictionary<string, ClusterConfig> incomingClusters)\n    {\n        var desiredClusters = new HashSet<string>(incomingClusters.Count, StringComparer.OrdinalIgnoreCase);\n\n        foreach (var incomingCluster in incomingClusters.Values)\n        {\n            var added = desiredClusters.Add(incomingCluster.ClusterId);\n            Debug.Assert(added);\n\n            if (_clusters.TryGetValue(incomingCluster.ClusterId, out var currentCluster))\n            {\n                var destinationsChanged = UpdateRuntimeDestinations(incomingCluster.Destinations, currentCluster.Destinations);\n\n                var currentClusterModel = currentCluster.Model;\n\n                var httpClient = _httpClientFactory.CreateClient(new ForwarderHttpClientContext\n                {\n                    ClusterId = currentCluster.ClusterId,\n                    OldConfig = currentClusterModel.Config.HttpClient ?? HttpClientConfig.Empty,\n                    OldMetadata = currentClusterModel.Config.Metadata,\n                    OldClient = currentClusterModel.HttpClient,\n                    NewConfig = incomingCluster.HttpClient ?? HttpClientConfig.Empty,\n                    NewMetadata = incomingCluster.Metadata\n                });\n\n                var newClusterModel = new ClusterModel(incomingCluster, httpClient);\n\n                // Excludes destination changes, they're tracked separately.\n                var configChanged = currentClusterModel.HasConfigChanged(newClusterModel);\n                if (configChanged)\n                {\n                    currentCluster.Revision++;\n                    Log.ClusterChanged(_logger, incomingCluster.ClusterId);\n                }\n\n                if (destinationsChanged || configChanged)\n                {\n                    // Config changed, so update runtime cluster\n                    currentCluster.Model = newClusterModel;\n\n                    _clusterDestinationsUpdater.UpdateAllDestinations(currentCluster);\n\n                    foreach (var listener in _clusterChangeListeners)\n                    {\n                        listener.OnClusterChanged(currentCluster);\n                    }\n                }\n            }\n            else\n            {\n                var newClusterState = new ClusterState(incomingCluster.ClusterId);\n\n                UpdateRuntimeDestinations(incomingCluster.Destinations, newClusterState.Destinations);\n\n                var httpClient = _httpClientFactory.CreateClient(new ForwarderHttpClientContext\n                {\n                    ClusterId = newClusterState.ClusterId,\n                    NewConfig = incomingCluster.HttpClient ?? HttpClientConfig.Empty,\n                    NewMetadata = incomingCluster.Metadata\n                });\n\n                newClusterState.Model = new ClusterModel(incomingCluster, httpClient);\n                newClusterState.Revision++;\n                Log.ClusterAdded(_logger, incomingCluster.ClusterId);\n\n                _clusterDestinationsUpdater.UpdateAllDestinations(newClusterState);\n\n                added = _clusters.TryAdd(newClusterState.ClusterId, newClusterState);\n                Debug.Assert(added);\n\n                foreach (var listener in _clusterChangeListeners)\n                {\n                    listener.OnClusterAdded(newClusterState);\n                }\n            }\n        }\n\n        // Directly enumerate the ConcurrentDictionary to limit locking and copying.\n        foreach (var existingClusterPair in _clusters)\n        {\n            var existingCluster = existingClusterPair.Value;\n            if (!desiredClusters.Contains(existingCluster.ClusterId))\n            {\n                // NOTE 1: Remove is safe to do within the `foreach` loop on ConcurrentDictionary\n                //\n                // NOTE 2: Removing the cluster from _clusters is safe and existing\n                // ASP .NET Core endpoints will continue to work with their existing behavior (until those endpoints are updated)\n                // and the Garbage Collector won't destroy this cluster object while it's referenced elsewhere.\n                Log.ClusterRemoved(_logger, existingCluster.ClusterId);\n                var removed = _clusters.TryRemove(existingCluster.ClusterId, out var _);\n                Debug.Assert(removed);\n\n                foreach (var listener in _clusterChangeListeners)\n                {\n                    listener.OnClusterRemoved(existingCluster);\n                }\n            }\n        }\n    }\n\n    private bool UpdateRuntimeDestinations(IReadOnlyDictionary<string, DestinationConfig>? incomingDestinations, ConcurrentDictionary<string, DestinationState> currentDestinations)\n    {\n        var desiredDestinations = new HashSet<string>(incomingDestinations?.Count ?? 0, StringComparer.OrdinalIgnoreCase);\n        var changed = false;\n\n        if (incomingDestinations is not null)\n        {\n            foreach (var incomingDestination in incomingDestinations)\n            {\n                var added = desiredDestinations.Add(incomingDestination.Key);\n                Debug.Assert(added);\n\n                if (currentDestinations.TryGetValue(incomingDestination.Key, out var currentDestination))\n                {\n                    if (currentDestination.Model.HasChanged(incomingDestination.Value))\n                    {\n                        Log.DestinationChanged(_logger, incomingDestination.Key);\n                        currentDestination.Model = new DestinationModel(incomingDestination.Value);\n                        changed = true;\n                    }\n                }\n                else\n                {\n                    Log.DestinationAdded(_logger, incomingDestination.Key);\n                    var newDestination = new DestinationState(incomingDestination.Key)\n                    {\n                        Model = new DestinationModel(incomingDestination.Value),\n                    };\n                    added = currentDestinations.TryAdd(newDestination.DestinationId, newDestination);\n                    Debug.Assert(added);\n                    changed = true;\n                }\n            }\n        }\n\n        // Directly enumerate the ConcurrentDictionary to limit locking and copying.\n        foreach (var existingDestinationPair in currentDestinations)\n        {\n            var id = existingDestinationPair.Value.DestinationId;\n            if (!desiredDestinations.Contains(id))\n            {\n                // NOTE 1: Remove is safe to do within the `foreach` loop on ConcurrentDictionary\n                //\n                // NOTE 2: Removing the endpoint from `IEndpointManager` is safe and existing\n                // clusters will continue to work with their existing behavior (until those clusters are updated)\n                // and the Garbage Collector won't destroy this cluster object while it's referenced elsewhere.\n                Log.DestinationRemoved(_logger, id);\n                var removed = currentDestinations.TryRemove(id, out var _);\n                Debug.Assert(removed);\n                changed = true;\n            }\n        }\n\n        return changed;\n    }\n\n    private bool UpdateRuntimeRoutes(IList<RouteConfig> incomingRoutes)\n    {\n        var desiredRoutes = new HashSet<string>(incomingRoutes.Count, StringComparer.OrdinalIgnoreCase);\n        var changed = false;\n\n        foreach (var incomingRoute in incomingRoutes)\n        {\n            var added = desiredRoutes.Add(incomingRoute.RouteId);\n            Debug.Assert(added);\n\n            // Note that this can be null, and that is fine. The resulting route may match\n            // but would then fail to route, which is exactly what we were instructed to do in this case\n            // since no valid cluster was specified.\n            _clusters.TryGetValue(incomingRoute.ClusterId ?? string.Empty, out var cluster);\n\n            if (_routes.TryGetValue(incomingRoute.RouteId, out var currentRoute))\n            {\n                if (currentRoute.Model.HasConfigChanged(incomingRoute, cluster, currentRoute.ClusterRevision))\n                {\n                    currentRoute.CachedEndpoint = null; // Recreate endpoint\n                    var newModel = BuildRouteModel(incomingRoute, cluster);\n                    currentRoute.Model = newModel;\n                    currentRoute.ClusterRevision = cluster?.Revision;\n                    changed = true;\n                    Log.RouteChanged(_logger, currentRoute.RouteId);\n                }\n            }\n            else\n            {\n                var newModel = BuildRouteModel(incomingRoute, cluster);\n                var newState = new RouteState(incomingRoute.RouteId)\n                {\n                    Model = newModel,\n                    ClusterRevision = cluster?.Revision,\n                };\n                added = _routes.TryAdd(newState.RouteId, newState);\n                Debug.Assert(added);\n                changed = true;\n                Log.RouteAdded(_logger, newState.RouteId);\n            }\n        }\n\n        // Directly enumerate the ConcurrentDictionary to limit locking and copying.\n        foreach (var existingRoutePair in _routes)\n        {\n            var routeId = existingRoutePair.Value.RouteId;\n            if (!desiredRoutes.Contains(routeId))\n            {\n                // NOTE 1: Remove is safe to do within the `foreach` loop on ConcurrentDictionary\n                //\n                // NOTE 2: Removing the route from _routes is safe and existing\n                // ASP.NET Core endpoints will continue to work with their existing behavior since\n                // their copy of `RouteModel` is immutable and remains operational in whichever state is was in.\n                Log.RouteRemoved(_logger, routeId);\n                var removed = _routes.TryRemove(routeId, out var _);\n                Debug.Assert(removed);\n                changed = true;\n            }\n        }\n\n        return changed;\n    }\n\n    /// <summary>\n    /// Applies a new set of ASP .NET Core endpoints. Changes take effect immediately.\n    /// </summary>\n    /// <param name=\"endpoints\">New endpoints to apply.</param>\n    [MemberNotNull(nameof(_endpoints))]\n    private void UpdateEndpoints(List<Endpoint> endpoints)\n    {\n        ArgumentNullException.ThrowIfNull(endpoints);\n\n        lock (_syncRoot)\n        {\n            // These steps are done in a specific order to ensure callers always see a consistent state.\n\n            // Step 1 - capture old token\n            var oldCancellationTokenSource = _endpointsChangeSource;\n\n            // Step 2 - update endpoints\n            Volatile.Write(ref _endpoints, endpoints);\n\n            // Step 3 - create new change token\n            _endpointsChangeSource = new CancellationTokenSource();\n            Volatile.Write(ref _endpointsChangeToken, new CancellationChangeToken(_endpointsChangeSource.Token));\n\n            // Step 4 - trigger old token\n            oldCancellationTokenSource?.Cancel();\n        }\n    }\n\n    private RouteModel BuildRouteModel(RouteConfig source, ClusterState? cluster)\n    {\n        var transforms = _transformBuilder.Build(source, cluster?.Model?.Config);\n\n        return new RouteModel(source, cluster, transforms);\n    }\n\n    public bool TryGetRoute(string id, [NotNullWhen(true)] out RouteModel? route)\n    {\n        if (_routes.TryGetValue(id, out var routeState))\n        {\n            route = routeState.Model;\n            return true;\n        }\n\n        route = null;\n        return false;\n    }\n\n    public IEnumerable<RouteModel> GetRoutes()\n    {\n        foreach (var (_, route) in _routes)\n        {\n            yield return route.Model;\n        }\n    }\n\n    public bool TryGetCluster(string id, [NotNullWhen(true)] out ClusterState? cluster)\n    {\n        return _clusters.TryGetValue(id, out cluster!);\n    }\n\n    public IEnumerable<ClusterState> GetClusters()\n    {\n        foreach (var (_, cluster) in _clusters)\n        {\n            yield return cluster;\n        }\n    }\n\n    public void Dispose()\n    {\n        _configChangeSource.Dispose();\n        foreach (var instance in _configs)\n        {\n            instance?.CallbackCleanup?.Dispose();\n        }\n    }\n\n    private sealed class ConfigState\n    {\n        public ConfigState(IProxyConfigProvider provider, IProxyConfig config)\n        {\n            Provider = provider;\n            LatestConfig = config;\n        }\n\n        public IProxyConfigProvider Provider { get; }\n\n        public IProxyConfig LatestConfig { get; set; }\n\n        public bool LoadFailed { get; set; }\n\n        public IDisposable? CallbackCleanup { get; set; }\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, string, Exception?> _clusterAdded = LoggerMessage.Define<string>(\n            LogLevel.Debug,\n            EventIds.ClusterAdded,\n            \"Cluster '{clusterId}' has been added.\");\n\n        private static readonly Action<ILogger, string, Exception?> _clusterChanged = LoggerMessage.Define<string>(\n            LogLevel.Debug,\n            EventIds.ClusterChanged,\n            \"Cluster '{clusterId}' has changed.\");\n\n        private static readonly Action<ILogger, string, Exception?> _clusterRemoved = LoggerMessage.Define<string>(\n            LogLevel.Debug,\n            EventIds.ClusterRemoved,\n            \"Cluster '{clusterId}' has been removed.\");\n\n        private static readonly Action<ILogger, string, Exception?> _destinationAdded = LoggerMessage.Define<string>(\n            LogLevel.Debug,\n            EventIds.DestinationAdded,\n            \"Destination '{destinationId}' has been added.\");\n\n        private static readonly Action<ILogger, string, Exception?> _destinationChanged = LoggerMessage.Define<string>(\n            LogLevel.Debug,\n            EventIds.DestinationChanged,\n            \"Destination '{destinationId}' has changed.\");\n\n        private static readonly Action<ILogger, string, Exception?> _destinationRemoved = LoggerMessage.Define<string>(\n            LogLevel.Debug,\n            EventIds.DestinationRemoved,\n            \"Destination '{destinationId}' has been removed.\");\n\n        private static readonly Action<ILogger, string, Exception?> _routeAdded = LoggerMessage.Define<string>(\n            LogLevel.Debug,\n            EventIds.RouteAdded,\n            \"Route '{routeId}' has been added.\");\n\n        private static readonly Action<ILogger, string, Exception?> _routeChanged = LoggerMessage.Define<string>(\n            LogLevel.Debug,\n            EventIds.RouteChanged,\n            \"Route '{routeId}' has changed.\");\n\n        private static readonly Action<ILogger, string, Exception?> _routeRemoved = LoggerMessage.Define<string>(\n            LogLevel.Debug,\n            EventIds.RouteRemoved,\n            \"Route '{routeId}' has been removed.\");\n\n        private static readonly Action<ILogger, Exception> _errorReloadingConfig = LoggerMessage.Define(\n            LogLevel.Error,\n            EventIds.ErrorReloadingConfig,\n            \"Failed to reload config. Unable to register for change notifications, polling for changes until successful.\");\n\n        private static readonly Action<ILogger, Exception> _errorApplyingConfig = LoggerMessage.Define(\n            LogLevel.Error,\n            EventIds.ErrorApplyingConfig,\n            \"Failed to apply the new config.\");\n\n        public static void ClusterAdded(ILogger logger, string clusterId)\n        {\n            _clusterAdded(logger, clusterId, null);\n        }\n\n        public static void ClusterChanged(ILogger logger, string clusterId)\n        {\n            _clusterChanged(logger, clusterId, null);\n        }\n\n        public static void ClusterRemoved(ILogger logger, string clusterId)\n        {\n            _clusterRemoved(logger, clusterId, null);\n        }\n\n        public static void DestinationAdded(ILogger logger, string destinationId)\n        {\n            _destinationAdded(logger, destinationId, null);\n        }\n\n        public static void DestinationChanged(ILogger logger, string destinationId)\n        {\n            _destinationChanged(logger, destinationId, null);\n        }\n\n        public static void DestinationRemoved(ILogger logger, string destinationId)\n        {\n            _destinationRemoved(logger, destinationId, null);\n        }\n\n        public static void RouteAdded(ILogger logger, string routeId)\n        {\n            _routeAdded(logger, routeId, null);\n        }\n\n        public static void RouteChanged(ILogger logger, string routeId)\n        {\n            _routeChanged(logger, routeId, null);\n        }\n\n        public static void RouteRemoved(ILogger logger, string routeId)\n        {\n            _routeRemoved(logger, routeId, null);\n        }\n\n        public static void ErrorReloadingConfig(ILogger logger, Exception ex)\n        {\n            _errorReloadingConfig(logger, ex);\n        }\n\n        public static void ErrorApplyingConfig(ILogger logger, Exception ex)\n        {\n            _errorApplyingConfig(logger, ex);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Management/ReverseProxyBuilder.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Yarp.ReverseProxy.Management;\n\n/// <summary>\n/// Reverse Proxy builder for DI configuration.\n/// </summary>\ninternal sealed class ReverseProxyBuilder : IReverseProxyBuilder\n{\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"ReverseProxyBuilder\"/> class.\n    /// </summary>\n    /// <param name=\"services\">Services collection.</param>\n    public ReverseProxyBuilder(IServiceCollection services)\n    {\n        ArgumentNullException.ThrowIfNull(services);\n        Services = services;\n    }\n\n    /// <summary>\n    /// Gets the services collection.\n    /// </summary>\n    public IServiceCollection Services { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Management/ReverseProxyServiceCollectionExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Linq;\nusing System.Net.Http;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Configuration.ConfigProvider;\nusing Yarp.ReverseProxy.Delegation;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Management;\nusing Yarp.ReverseProxy.Routing;\nusing Yarp.ReverseProxy.ServiceDiscovery;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\n/// <summary>\n/// Extensions for <see cref=\"IServiceCollection\"/>\n/// used to register the ReverseProxy's components.\n/// </summary>\npublic static class ReverseProxyServiceCollectionExtensions\n{\n    /// <summary>\n    /// Registers the <see cref=\"IHttpForwarder\"/> service for direct forwarding scenarios.\n    /// </summary>\n    public static IServiceCollection AddHttpForwarder(this IServiceCollection services)\n    {\n        services.TryAddSingleton(TimeProvider.System);\n        services.TryAddSingleton<IHttpForwarder, HttpForwarder>();\n        services.TryAddSingleton<ITransformBuilder, TransformBuilder>();\n\n        services.AddSingleton<DirectForwardingHttpClientProvider>();\n\n        return services;\n    }\n\n    /// <summary>\n    /// Adds ReverseProxy's services to Dependency Injection.\n    /// </summary>\n    public static IReverseProxyBuilder AddReverseProxy(this IServiceCollection services)\n    {\n        var builder = new ReverseProxyBuilder(services);\n        builder\n            .AddConfigBuilder()\n            .AddRuntimeStateManagers()\n            .AddConfigManager()\n            .AddSessionAffinityPolicies()\n            .AddActiveHealthChecks()\n            .AddPassiveHealthCheck()\n            .AddLoadBalancingPolicies()\n            .AddDestinationResolver()\n            .AddProxy();\n\n        if (OperatingSystem.IsWindows())\n        {\n            // Workaround for https://github.com/dotnet/aspnetcore/issues/59166.\n            // .NET 9.0 packages for Ubuntu ship a broken Microsoft.AspNetCore.Server.HttpSys assembly.\n            // Avoid loading types from that assembly on Linux unless the user explicitly tries to do so.\n            builder.AddHttpSysDelegation();\n        }\n        else\n        {\n            // Add a no-op delegator in case someone is injecting the interface in their cross-plat logic.\n            builder.Services.TryAddSingleton<IHttpSysDelegator, DummyHttpSysDelegator>();\n        }\n\n        services.TryAddSingleton<ProxyEndpointFactory>();\n\n        services.AddDataProtection();\n        services.AddAuthorization();\n        services.AddCors();\n        services.AddRouting();\n\n        return builder;\n    }\n\n    /// <summary>\n    /// Loads routes and endpoints from config.\n    /// </summary>\n    public static IReverseProxyBuilder LoadFromConfig(this IReverseProxyBuilder builder, IConfiguration config)\n    {\n        ArgumentNullException.ThrowIfNull(config);\n\n        builder.Services.AddSingleton<IProxyConfigProvider>(sp =>\n        {\n            // This is required because we're capturing the configuration via a closure\n            return new ConfigurationConfigProvider(sp.GetRequiredService<ILogger<ConfigurationConfigProvider>>(), config);\n        });\n\n        return builder;\n    }\n\n    /// <summary>\n    /// Registers a singleton IProxyConfigFilter service. Multiple filters are allowed, and they will be run in registration order.\n    /// </summary>\n    /// <typeparam name=\"TService\">A class that implements IProxyConfigFilter.</typeparam>\n    public static IReverseProxyBuilder AddConfigFilter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IReverseProxyBuilder builder) where TService : class, IProxyConfigFilter\n    {\n        ArgumentNullException.ThrowIfNull(builder);\n\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IProxyConfigFilter, TService>());\n        return builder;\n    }\n\n    /// <summary>\n    /// Provides a callback that will be run for each route to conditionally add transforms.\n    /// <see cref=\"AddTransforms(IReverseProxyBuilder, Action{TransformBuilderContext})\"/> can be called multiple times to\n    /// provide multiple callbacks.\n    /// </summary>\n    public static IReverseProxyBuilder AddTransforms(this IReverseProxyBuilder builder, Action<TransformBuilderContext> action)\n    {\n        ArgumentNullException.ThrowIfNull(action);\n\n        builder.Services.AddSingleton<ITransformProvider>(new ActionTransformProvider(action));\n        return builder;\n    }\n\n    /// <summary>\n    /// Provides a <see cref=\"ITransformProvider\"/> implementation that will be run for each route to conditionally add transforms.\n    /// <see cref=\"AddTransforms{T}(IReverseProxyBuilder)\"/> can be called multiple times to provide multiple distinct types.\n    /// </summary>\n    public static IReverseProxyBuilder AddTransforms<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(this IReverseProxyBuilder builder) where T : class, ITransformProvider\n    {\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ITransformProvider, T>());\n        return builder;\n    }\n\n    /// <summary>\n    /// Adds a <see cref=\"ITransformFactory\"/> implementation that will be used to read route transform config and generate\n    /// the associated transform actions. <see cref=\"AddTransformFactory{T}(IReverseProxyBuilder)\"/> can be called multiple\n    /// times to provide multiple distinct types.\n    /// </summary>\n    public static IReverseProxyBuilder AddTransformFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(this IReverseProxyBuilder builder) where T : class, ITransformFactory\n    {\n        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ITransformFactory, T>());\n        return builder;\n    }\n\n    /// <summary>\n    /// Provides a callback to customize <see cref=\"SocketsHttpHandler\"/> settings used for proxying requests.\n    /// This will be called each time a cluster is added or changed. Cluster settings are applied to the handler before\n    /// the callback. Custom data can be provided in the cluster metadata.\n    /// </summary>\n    public static IReverseProxyBuilder ConfigureHttpClient(this IReverseProxyBuilder builder, Action<ForwarderHttpClientContext, SocketsHttpHandler> configure)\n    {\n        ArgumentNullException.ThrowIfNull(configure);\n\n        // Avoid overriding any other custom factories. This does not handle the case where a IForwarderHttpClientFactory\n        // is registered after this call.\n        var service = builder.Services.FirstOrDefault(service => service.ServiceType == typeof(IForwarderHttpClientFactory));\n        if (service is not null)\n        {\n            if (service.ImplementationType != typeof(ForwarderHttpClientFactory))\n            {\n                throw new InvalidOperationException($\"ConfigureHttpClient will override the custom IForwarderHttpClientFactory type.\");\n            }\n        }\n\n        builder.Services.AddSingleton<IForwarderHttpClientFactory>(services =>\n        {\n            var logger = services.GetRequiredService<ILogger<ForwarderHttpClientFactory>>();\n            return new CallbackHttpClientFactory(logger, configure);\n        });\n        return builder;\n    }\n\n    /// <summary>\n    /// Provides a <see cref=\"IDestinationResolver\"/> implementation which uses <see cref=\"System.Net.Dns\"/> to resolve destinations.\n    /// </summary>\n    public static IReverseProxyBuilder AddDnsDestinationResolver(this IReverseProxyBuilder builder, Action<DnsDestinationResolverOptions>? configureOptions = null)\n    {\n        builder.Services.AddSingleton<IDestinationResolver, DnsDestinationResolver>();\n        if (configureOptions is not null)\n        {\n            builder.Services.Configure(configureOptions);\n        }\n\n        return builder;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/ClusterDestinationsState.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\n\nnamespace Yarp.ReverseProxy.Model;\n\npublic sealed class ClusterDestinationsState\n{\n    public ClusterDestinationsState(\n        IReadOnlyList<DestinationState> allDestinations,\n        IReadOnlyList<DestinationState> availableDestinations)\n    {\n        ArgumentNullException.ThrowIfNull(allDestinations);\n        ArgumentNullException.ThrowIfNull(availableDestinations);\n\n        AllDestinations = allDestinations;\n        AvailableDestinations = availableDestinations;\n    }\n\n    public IReadOnlyList<DestinationState> AllDestinations { get; }\n\n    public IReadOnlyList<DestinationState> AvailableDestinations { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/ClusterModel.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Model;\n\n/// <summary>\n/// Immutable representation of the portions of a cluster\n/// that only change in reaction to configuration changes\n/// (e.g. http client options).\n/// </summary>\n/// <remarks>\n/// All members must remain immutable to avoid thread safety issues.\n/// Instead, instances of <see cref=\"ClusterModel\"/> are replaced\n/// in their entirety when values need to change.\n/// </remarks>\npublic sealed class ClusterModel\n{\n    /// <summary>\n    /// Creates a new Instance.\n    /// </summary>\n    public ClusterModel(\n        ClusterConfig config,\n        HttpMessageInvoker httpClient)\n    {\n        ArgumentNullException.ThrowIfNull(config);\n        ArgumentNullException.ThrowIfNull(httpClient);\n\n        Config = config;\n        HttpClient = httpClient;\n    }\n\n    /// <summary>\n    /// The config for this cluster.\n    /// </summary>\n    public ClusterConfig Config { get; }\n\n    /// <summary>\n    /// An <see cref=\"HttpMessageInvoker\"/> that used for proxying requests to an upstream server.\n    /// </summary>\n    public HttpMessageInvoker HttpClient { get; }\n\n    // We intentionally do not consider destination changes when updating the cluster Revision.\n    // Revision is used to rebuild routing endpoints which should be unrelated to destinations,\n    // and destinations are the most likely to change.\n    internal bool HasConfigChanged(ClusterModel newModel)\n    {\n        return !Config.EqualsExcludingDestinations(newModel.Config) || newModel.HttpClient != HttpClient;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/ClusterState.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Concurrent;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Model;\n\n/// <summary>\n/// Representation of a cluster for use at runtime.\n/// </summary>\npublic sealed class ClusterState\n{\n    private volatile ClusterDestinationsState _destinationsState = new ClusterDestinationsState(Array.Empty<DestinationState>(), Array.Empty<DestinationState>());\n    private volatile ClusterModel _model = default!; // Initialized right after construction.\n\n    /// <summary>\n    /// Creates a new instance. This constructor is for tests and infrastructure, this type is normally constructed by the configuration\n    /// loading infrastructure.\n    /// </summary>\n    public ClusterState(string clusterId)\n    {\n        ArgumentNullException.ThrowIfNull(clusterId);\n        ClusterId = clusterId;\n    }\n\n    /// <summary>\n    /// Constructor overload to additionally initialize the <see cref=\"ClusterModel\"/> for tests and infrastructure,\n    /// such as updating the <see cref=\"ReverseProxyFeature\"/> via <see cref=\"HttpContextFeaturesExtensions\"/>\n    /// </summary>\n    /// <exception cref=\"ArgumentNullException\"><paramref name=\"model\"/> is <see langword=\"null\"/>.</exception>\n    public ClusterState(string clusterId, ClusterModel model) : this(clusterId)\n    {\n        ArgumentNullException.ThrowIfNull(model);\n        Model = model;\n    }\n\n    /// <summary>\n    /// The cluster's unique id.\n    /// </summary>\n    public string ClusterId { get; }\n\n    /// <summary>\n    /// Encapsulates parts of a cluster that can change atomically in reaction to config changes.\n    /// </summary>\n    public ClusterModel Model\n    {\n        get => _model;\n        internal set => _model = value ?? throw new ArgumentNullException(nameof(value));\n    }\n\n    /// <summary>\n    /// All the destinations associated with this cluster. This collection is populated by the configuration system\n    /// and should only be directly modified in a test environment.\n    /// Call <see cref=\"Health.IClusterDestinationsUpdater\"/> after modifying this collection.\n    /// </summary>\n    public ConcurrentDictionary<string, DestinationState> Destinations { get; } = new(StringComparer.OrdinalIgnoreCase);\n\n    /// <summary>\n    /// Stores the state of cluster's destinations that can change atomically\n    /// in reaction to runtime state changes (e.g. changes of destinations' health).\n    /// </summary>\n    public ClusterDestinationsState DestinationsState\n    {\n        get => _destinationsState;\n        set => _destinationsState = value ?? throw new ArgumentNullException(nameof(value));\n    }\n\n    /// <summary>\n    /// Keeps track of the total number of concurrent requests on this cluster.\n    /// </summary>\n    internal AtomicCounter ConcurrencyCounter { get; } = new AtomicCounter();\n\n    /// <summary>\n    /// Tracks changes to the cluster configuration for use with rebuilding dependent endpoints. Destination changes do not affect this property.\n    /// </summary>\n    internal int Revision { get; set; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/DestinationHealth.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Model;\n\npublic enum DestinationHealth\n{\n    Unknown,\n\n    Healthy,\n\n    Unhealthy,\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/DestinationHealthState.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Model;\n\n/// <summary>\n/// Tracks destination passive and active health states.\n/// </summary>\npublic class DestinationHealthState\n{\n    private volatile DestinationHealth _active;\n    private volatile DestinationHealth _passive;\n\n    /// <summary>\n    /// Passive health state.\n    /// </summary>\n    public DestinationHealth Passive\n    {\n        get => _passive;\n        set => _passive = value;\n    }\n\n    /// <summary>\n    /// Active health state.\n    /// </summary>\n    public DestinationHealth Active\n    {\n        get => _active;\n        set => _active = value;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/DestinationModel.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Model;\n\n/// <summary>\n/// Immutable representation of the portions of a destination\n/// that only change in reaction to configuration changes\n/// (e.g. address).\n/// </summary>\n/// <remarks>\n/// All members must remain immutable to avoid thread safety issues.\n/// Instead, instances of <see cref=\"DestinationModel\"/> are replaced\n/// in their entirety when values need to change.\n/// </remarks>\npublic sealed class DestinationModel\n{\n    /// <summary>\n    /// Creates a new instance. This constructor is for tests and infrastructure, this type is normally constructed by\n    /// the configuration loading infrastructure.\n    /// </summary>\n    public DestinationModel(DestinationConfig destination)\n    {\n        ArgumentNullException.ThrowIfNull(destination);\n        Config = destination;\n    }\n\n    /// <summary>\n    /// This destination's configuration.\n    /// </summary>\n    public DestinationConfig Config { get; }\n\n    internal bool HasChanged(DestinationConfig destination)\n    {\n        return Config != destination;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/DestinationState.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Model;\n\n/// <summary>\n/// Representation of a cluster's destination for use at runtime.\n/// </summary>\npublic sealed class DestinationState : IReadOnlyList<DestinationState>\n{\n    private volatile DestinationModel _model = default!;\n\n    /// <summary>\n    /// Creates a new instance. This constructor is for tests and infrastructure, this type is normally constructed by\n    /// the configuration loading infrastructure.\n    /// </summary>\n    public DestinationState(string destinationId)\n    {\n        ArgumentException.ThrowIfNullOrEmpty(destinationId);\n        DestinationId = destinationId;\n    }\n\n    /// <summary>\n    /// Constructor overload to additionally initialize the <see cref=\"DestinationModel\"/> for tests and infrastructure,\n    /// such as updating the <see cref=\"ReverseProxyFeature\"/> via <see cref=\"HttpContextFeaturesExtensions\"/>\n    /// </summary>\n    /// <exception cref=\"ArgumentNullException\"><paramref name=\"model\"/> is <see langword=\"null\"/>.</exception>\n    public DestinationState(string destinationId, DestinationModel model) : this(destinationId)\n    {\n        ArgumentNullException.ThrowIfNull(model);\n        Model = model;\n    }\n\n    /// <summary>\n    /// The destination's unique id.\n    /// </summary>\n    public string DestinationId { get; }\n\n    /// <summary>\n    /// A snapshot of the current configuration\n    /// </summary>\n    public DestinationModel Model\n    {\n        get => _model;\n        internal set => _model = value ?? throw new ArgumentNullException(nameof(value));\n    }\n\n    /// <summary>\n    /// Mutable health state for this destination.\n    /// </summary>\n    public DestinationHealthState Health { get; } = new DestinationHealthState();\n\n    /// <summary>\n    /// Keeps track of the total number of concurrent requests on this endpoint.\n    /// The setter should only be used for testing purposes.\n    /// </summary>\n    public int ConcurrentRequestCount\n    {\n        get => ConcurrencyCounter.Value;\n        set => ConcurrencyCounter.Value = value;\n    }\n\n    internal AtomicCounter ConcurrencyCounter { get; } = new AtomicCounter();\n\n    DestinationState IReadOnlyList<DestinationState>.this[int index]\n        => index == 0 ? this : throw new IndexOutOfRangeException();\n\n    int IReadOnlyCollection<DestinationState>.Count => 1;\n\n    private Enumerator GetEnumerator()\n    {\n        return new Enumerator(this);\n    }\n\n    IEnumerator<DestinationState> IEnumerable<DestinationState>.GetEnumerator()\n    {\n        return GetEnumerator();\n    }\n\n    IEnumerator IEnumerable.GetEnumerator()\n    {\n        return GetEnumerator();\n    }\n\n    private struct Enumerator : IEnumerator<DestinationState>\n    {\n        private bool _read;\n\n        internal Enumerator(DestinationState instance)\n        {\n            Current = instance;\n            _read = false;\n        }\n\n        public DestinationState Current { get; }\n\n        object IEnumerator.Current => Current;\n\n        public bool MoveNext()\n        {\n            if (!_read)\n            {\n                _read = true;\n                return true;\n            }\n            return false;\n        }\n\n        public void Dispose()\n        {\n\n        }\n\n        void IEnumerator.Reset()\n        {\n            throw new NotSupportedException();\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/HttpContextFeaturesExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Microsoft.AspNetCore.Http;\n\n/// <summary>\n/// Extension methods for fetching proxy configuration from the current HttpContext.\n/// </summary>\npublic static class HttpContextFeaturesExtensions\n{\n    /// <summary>\n    /// Retrieves the <see cref=\"RouteModel\"/> instance associated with the current request.\n    /// </summary>\n    public static RouteModel GetRouteModel(this HttpContext context)\n    {\n        var proxyFeature = context.GetReverseProxyFeature();\n\n        var route = proxyFeature.Route\n            ?? throw new InvalidOperationException($\"The {typeof(IReverseProxyFeature).FullName} is missing the {typeof(RouteModel).FullName}.\");\n\n        return route;\n    }\n\n    /// <summary>\n    /// Retrieves the <see cref=\"IReverseProxyFeature\"/> instance associated with the current request.\n    /// </summary>\n    public static IReverseProxyFeature GetReverseProxyFeature(this HttpContext context)\n    {\n        return context.Features.Get<IReverseProxyFeature>() ?? throw new InvalidOperationException($\"{typeof(IReverseProxyFeature).FullName} is missing.\");\n    }\n\n    /// <summary>\n    /// Retrieves the <see cref=\"IForwarderErrorFeature\"/> instance associated with the current request, if any.\n    /// </summary>\n    public static IForwarderErrorFeature? GetForwarderErrorFeature(this HttpContext context)\n    {\n        return context.Features.Get<IForwarderErrorFeature>();\n    }\n\n    // Compare to ProxyPipelineInitializerMiddleware\n    /// <summary>\n    /// Replaces the assigned cluster and destinations in <see cref=\"IReverseProxyFeature\"/> with the new <see cref=\"ClusterState\"/>,\n    /// causing the request to be sent to the new cluster instead.\n    /// </summary>\n    public static void ReassignProxyRequest(this HttpContext context, ClusterState cluster)\n    {\n        var oldFeature = context.GetReverseProxyFeature();\n        var destinations = cluster.DestinationsState;\n        var newFeature = new ReverseProxyFeature()\n        {\n            Route = oldFeature.Route,\n            Cluster = cluster.Model,\n            AllDestinations = destinations.AllDestinations,\n            AvailableDestinations = destinations.AvailableDestinations,\n            ProxiedDestination = oldFeature.ProxiedDestination,\n        };\n        context.Features.Set<IReverseProxyFeature>(newFeature);\n    }\n\n    // ReassignProxyRequest overload to also replace the route when updating IReverseProxyFeature\n    /// <summary>\n    /// Replaces the assigned route, cluster, and destinations in <see cref=\"IReverseProxyFeature\"/> with the new <see cref=\"RouteModel\"/>\n    /// and new <see cref=\"ClusterState\"/>, causing the request to be sent using the new route to the new cluster.\n    /// </summary>\n    public static void ReassignProxyRequest(this HttpContext context, RouteModel route, ClusterState cluster)\n    {\n        var oldFeature = context.GetReverseProxyFeature();\n        var destinations = cluster.DestinationsState;\n        var newFeature = new ReverseProxyFeature()\n        {\n            Route = route,\n            Cluster = cluster.Model,\n            AllDestinations = destinations.AllDestinations,\n            AvailableDestinations = destinations.AvailableDestinations,\n            ProxiedDestination = oldFeature.ProxiedDestination,\n        };\n        context.Features.Set<IReverseProxyFeature>(newFeature);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/IClusterChangeListener.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Model;\n\n/// <summary>\n/// Listener for changes in the clusters.\n/// </summary>\npublic interface IClusterChangeListener\n{\n    /// <summary>\n    /// Gets called after a new <see cref=\"ClusterState\"/> has been added.\n    /// </summary>\n    /// <param name=\"cluster\">Added <see cref=\"ClusterState\"/> instance.</param>\n    void OnClusterAdded(ClusterState cluster);\n\n    /// <summary>\n    /// Gets called after an existing <see cref=\"ClusterState\"/> has been changed.\n    /// </summary>\n    /// <param name=\"cluster\">Changed <see cref=\"ClusterState\"/> instance.</param>\n    void OnClusterChanged(ClusterState cluster);\n\n    /// <summary>\n    /// Gets called after an existing <see cref=\"ClusterState\"/> has been removed.\n    /// </summary>\n    /// <param name=\"cluster\">Removed <see cref=\"ClusterState\"/> instance.</param>\n    void OnClusterRemoved(ClusterState cluster);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/IReverseProxyApplicationBuilder.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Microsoft.AspNetCore.Builder;\n\n/// <summary>\n/// An <see cref=\"IApplicationBuilder\"/> for building the `MapReverseProxy` pipeline.\n/// </summary>\npublic interface IReverseProxyApplicationBuilder : IApplicationBuilder\n{\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/IReverseProxyFeature.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\n\nnamespace Yarp.ReverseProxy.Model;\n\n/// <summary>\n/// Stores the current proxy configuration used when processing the request.\n/// </summary>\npublic interface IReverseProxyFeature\n{\n    /// <summary>\n    /// The route model for the current request.\n    /// </summary>\n    RouteModel Route { get; }\n\n    /// <summary>\n    /// The cluster model for the current request.\n    /// </summary>\n    ClusterModel Cluster { get; }\n\n    /// <summary>\n    /// All destinations for the current cluster.\n    /// </summary>\n    IReadOnlyList<DestinationState> AllDestinations { get; }\n\n    /// <summary>\n    /// Cluster destinations that can handle the current request. This will initially include all destinations except those\n    /// currently marked as unhealthy if health checks are enabled.\n    /// </summary>\n    IReadOnlyList<DestinationState> AvailableDestinations { get; set; }\n\n    /// <summary>\n    /// The actual destination that the request was proxied to.\n    /// </summary>\n    DestinationState? ProxiedDestination { get; set; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/ProxyPipelineInitializerMiddleware.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing System.Threading;\nusing Microsoft.AspNetCore.Http.Timeouts;\nusing Microsoft.Extensions.Options;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Model;\n\n/// <summary>\n/// Initializes the proxy processing pipeline with the available healthy destinations.\n/// </summary>\ninternal sealed class ProxyPipelineInitializerMiddleware\n{\n    private readonly ILogger _logger;\n    private readonly RequestDelegate _next;\n    private readonly IOptionsMonitor<RequestTimeoutOptions> _timeoutOptions;\n\n    public ProxyPipelineInitializerMiddleware(RequestDelegate next, ILogger<ProxyPipelineInitializerMiddleware> logger, IOptionsMonitor<RequestTimeoutOptions> timeoutOptions)\n    {\n        ArgumentNullException.ThrowIfNull(logger);\n        ArgumentNullException.ThrowIfNull(next);\n        ArgumentNullException.ThrowIfNull(timeoutOptions);\n\n        _logger = logger;\n        _next = next;\n        _timeoutOptions = timeoutOptions;\n    }\n\n    public Task Invoke(HttpContext context)\n    {\n        var endpoint = context.GetEndpoint()\n           ?? throw new InvalidOperationException($\"Routing Endpoint wasn't set for the current request.\");\n\n        var route = endpoint.Metadata.GetMetadata<RouteModel>()\n            ?? throw new InvalidOperationException($\"Routing Endpoint is missing {typeof(RouteModel).FullName} metadata.\");\n\n        var cluster = route.Cluster;\n        // TODO: Validate on load https://github.com/dotnet/yarp/issues/797\n        if (cluster is null)\n        {\n            Log.NoClusterFound(_logger, route.Config.RouteId);\n            context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;\n            return Task.CompletedTask;\n        }\n\n        EnsureRequestTimeoutPolicyIsAppliedCorrectly(context, endpoint, route);\n\n        var destinationsState = cluster.DestinationsState;\n        context.Features.Set<IReverseProxyFeature>(new ReverseProxyFeature\n        {\n            Route = route,\n            Cluster = cluster.Model,\n            AllDestinations = destinationsState.AllDestinations,\n            AvailableDestinations = destinationsState.AvailableDestinations,\n        });\n\n        var activity = Observability.YarpActivitySource.CreateActivity(\"proxy.forwarder\", ActivityKind.Server);\n\n        return activity is null\n            ? _next(context)\n            : AwaitWithActivity(context, activity);\n    }\n\n    private async Task AwaitWithActivity(HttpContext context, Activity activity)\n    {\n        context.SetYarpActivity(activity);\n\n        activity.Start();\n        try\n        {\n            await _next(context);\n        }\n        finally\n        {\n            activity.Dispose();\n        }\n    }\n\n    private void EnsureRequestTimeoutPolicyIsAppliedCorrectly(HttpContext context, Endpoint endpoint, RouteModel route)\n    {\n        // There's no way to detect the presence of the timeout middleware before this, only the options.\n        if (endpoint.Metadata.GetMetadata<RequestTimeoutAttribute>() is { } requestTimeout &&\n            context.Features.Get<IHttpRequestTimeoutFeature>() is null &&\n            // The feature is skipped if the request is already canceled. We'll handle canceled requests later for consistency.\n            !context.RequestAborted.IsCancellationRequested &&\n            // The policy may set the timeout to null / infinite.\n            TimeoutPolicyRequestedATimeoutBeSet(requestTimeout))\n        {\n            // A timeout should have been set.\n            // Out of an abundance of caution, refuse the request rather than allowing it to proceed without the configured timeout.\n            ThrowIfDebuggerNotAttached(route);\n        }\n\n        void ThrowIfDebuggerNotAttached(RouteModel route)\n        {\n            // The feature is skipped if the debugger is attached.\n            if (!Debugger.IsAttached)\n            {\n                Log.TimeoutNotApplied(_logger, route.Config.RouteId);\n\n                throw new InvalidOperationException(\n                    $\"The timeout was not applied for route '{route.Config.RouteId}', \" +\n                    \"ensure `IApplicationBuilder.UseRequestTimeouts()` is called between \" +\n                    \"`IApplicationBuilder.UseRouting()` and `IApplicationBuilder.UseEndpoints()`.\");\n            }\n        }\n    }\n\n    private bool TimeoutPolicyRequestedATimeoutBeSet(RequestTimeoutAttribute requestTimeout)\n    {\n        if (requestTimeout.Timeout is not TimeSpan timeout)\n        {\n            if (requestTimeout.PolicyName is not string policyName)\n            {\n                Debug.Fail(\"Either Timeout or PolicyName should have been set.\");\n                return false;\n            }\n\n            if (!_timeoutOptions.CurrentValue.Policies.TryGetValue(policyName, out var policy))\n            {\n                // This should only happen if the policy existed at some point, but the options were updated to remove it.\n                return false;\n            }\n\n            if (policy.Timeout is null)\n            {\n                // The policy requested no timeout.\n                return false;\n            }\n\n            timeout = policy.Timeout.Value;\n        }\n\n        return timeout != Timeout.InfiniteTimeSpan;\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, string, Exception?> _noClusterFound = LoggerMessage.Define<string>(\n            LogLevel.Information,\n            EventIds.NoClusterFound,\n            \"Route '{routeId}' has no cluster information.\");\n\n        private static readonly Action<ILogger, string, Exception?> _timeoutNotApplied = LoggerMessage.Define<string>(\n            LogLevel.Error,\n            EventIds.TimeoutNotApplied,\n            \"The timeout was not applied for route '{routeId}', ensure `IApplicationBuilder.UseRequestTimeouts()` is called between `IApplicationBuilder.UseRouting()` and `IApplicationBuilder.UseEndpoints()`.\");\n\n        public static void NoClusterFound(ILogger logger, string routeId)\n        {\n            _noClusterFound(logger, routeId, null);\n        }\n\n        public static void TimeoutNotApplied(ILogger logger, string routeId)\n        {\n            _timeoutNotApplied(logger, routeId, null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/README.md",
    "content": "# ReverseProxy.RuntimeModel namespace\n\nClasses in this folder define the internal representation\nof ReverseProxy's runtime state used in perf-critical code paths.\n\nAll classes should be immutable, and all members and members of members\nMUST be either:\n\n   A) immutable\n   B) `AtomicHolder<T>` wrapping an immutable type `T`.\n   C) Thread-safe (e.g. `AtomicCounter`)\n\nThis ensures we can easily handle hot-swappable configurations\nwithout explicit synchronization overhead across threads,\nand each thread can operate safely with up-to-date yet consistent information\n(always the latest and consistent snapshot available when processing of a request starts).\n\n## Class naming conventions\n\n* Classes named `*Info` (`RouteInfo`, `ClusterInfo`, `EndpointInfo`)\n  represent the 3 primary abstractions in Reverse Proxy (Routes, Clusters and Destinations);\n\n* Classes named `*Config` (`RouteConfig`, `ClusterConfig`, `EndpointConfig`)\n  represent portions of the 3 abstractions that only change in reaction to \n  Reverse Proxy config changes.\n  For example, when the health check interval for a cluster is updated,\n  a new instance of `ClusterConfig` is created with the new values,\n  and the corresponding `AtomicHolder` in `ClusterInfo` is updated to point at the new instance;\n\n* Classes named `*DynamicState` (`ClusterDynamicState`, `EndpointDynamicState`)\n  represent portions of the 3 abstractions that change in reaction to\n  Reverse Proxy's runtime state.\n  For example, when new destinations are discovered for a cluster,\n  a new instance of `ClusterDynamicState` is created with the new values,\n  and the corresponding `AtomicHolder` in `ClusterInfo` is updated to point at the new instance;\n"
  },
  {
    "path": "src/ReverseProxy/Model/ReverseProxyApplicationBuilder.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\n\nnamespace Yarp.ReverseProxy.Model;\n\npublic class ReverseProxyApplicationBuilder : IReverseProxyApplicationBuilder\n{\n    private readonly IApplicationBuilder _applicationBuilder;\n\n    public ReverseProxyApplicationBuilder(IApplicationBuilder applicationBuilder)\n    {\n        ArgumentNullException.ThrowIfNull(applicationBuilder);\n        _applicationBuilder = applicationBuilder;\n    }\n\n    public IServiceProvider ApplicationServices\n    {\n        get => _applicationBuilder.ApplicationServices;\n        set => _applicationBuilder.ApplicationServices = value;\n    }\n\n    public IFeatureCollection ServerFeatures => _applicationBuilder.ServerFeatures;\n\n    public IDictionary<string, object?> Properties => _applicationBuilder.Properties;\n\n    public RequestDelegate Build() => _applicationBuilder.Build();\n\n    public IApplicationBuilder New() => _applicationBuilder.New();\n\n    public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)\n        => _applicationBuilder.Use(middleware);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/ReverseProxyFeature.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\n\nnamespace Yarp.ReverseProxy.Model;\n\n/// <summary>\n/// Stores the current proxy configuration used when processing the request.\n/// </summary>\npublic class ReverseProxyFeature : IReverseProxyFeature\n{\n    private IReadOnlyList<DestinationState> _availableDestinations = default!;\n\n    /// <inheritdoc/>\n    public RouteModel Route { get; init; } = default!;\n\n    /// <inheritdoc/>\n    public ClusterModel Cluster { get; set; } = default!;\n\n    /// <inheritdoc/>\n    public IReadOnlyList<DestinationState> AllDestinations { get; init; } = default!;\n\n    /// <inheritdoc/>\n    public IReadOnlyList<DestinationState> AvailableDestinations\n    {\n        get => _availableDestinations;\n        set => _availableDestinations = value ?? throw new ArgumentNullException(nameof(value));\n    }\n\n    /// <inheritdoc/>\n    public DestinationState? ProxiedDestination { get; set; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/RouteModel.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Model;\n\n/// <summary>\n/// Immutable representation of the portions of a route\n/// that only change in reaction to configuration changes.\n/// </summary>\n/// <remarks>\n/// All members must remain immutable to avoid thread safety issues.\n/// Instead, instances of <see cref=\"RouteModel\"/> are replaced\n/// in their entirety when values need to change.\n/// </remarks>\npublic sealed class RouteModel\n{\n    /// <summary>\n    /// Creates a new instance.\n    /// </summary>\n    public RouteModel(\n        RouteConfig config,\n        ClusterState? cluster,\n        HttpTransformer transformer)\n    {\n        ArgumentNullException.ThrowIfNull(config);\n        ArgumentNullException.ThrowIfNull(transformer);\n\n        Config = config;\n        Cluster = cluster;\n        Transformer = transformer;\n    }\n\n    // May not be populated if the cluster config is missing. https://github.com/dotnet/yarp/issues/797\n    /// <summary>\n    /// The <see cref=\"ClusterState\"/> instance associated with this route.\n    /// </summary>\n    public ClusterState? Cluster { get; }\n\n    /// <summary>\n    /// Transforms to apply for this route.\n    /// </summary>\n    public HttpTransformer Transformer { get; }\n\n    /// <summary>\n    /// The configuration data used to build this route.\n    /// </summary>\n    public RouteConfig Config { get; }\n\n    internal bool HasConfigChanged(RouteConfig newConfig, ClusterState? cluster, int? routeRevision)\n    {\n        return Cluster != cluster || routeRevision != cluster?.Revision || !Config.Equals(newConfig);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Model/RouteState.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Yarp.ReverseProxy.Model;\n\n/// <summary>\n/// Representation of a route for use at runtime.\n/// </summary>\ninternal sealed class RouteState\n{\n    private volatile RouteModel _model = default!;\n\n    public RouteState(string routeId)\n    {\n        ArgumentException.ThrowIfNullOrEmpty(routeId);\n        RouteId = routeId;\n    }\n\n    public string RouteId { get; }\n\n    /// <summary>\n    /// Encapsulates parts of a route that can change atomically\n    /// in reaction to config changes.\n    /// </summary>\n    internal RouteModel Model\n    {\n        get => _model;\n        set { ArgumentNullException.ThrowIfNull(value); _model = value; }\n    }\n\n    /// <summary>\n    /// Tracks changes to the cluster configuration for use with rebuilding the route endpoint.\n    /// </summary>\n    internal int? ClusterRevision { get; set; }\n\n    /// <summary>\n    /// A cached Endpoint that will be cleared and rebuilt if the RouteConfig or cluster config change.\n    /// </summary>\n    internal Endpoint? CachedEndpoint { get; set; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/README.md",
    "content": "YARP (Yet Another Reverse Proxy) is a highly customizable reverse proxy built using .NET. The biggest differentiator between YARP and other reverse proxies is how it is built and packaged – YARP is supplied as a library and samples showing how to create a proxy that is customized to the needs of your specific scenarios.\n\nTo learn more see the docs at https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/getting-started, the GitHub repo at https://github.com/dotnet/yarp, and the 1.0 Announcement Blog post at https://devblogs.microsoft.com/dotnet/announcing-yarp-1-0-release/.\n"
  },
  {
    "path": "src/ReverseProxy/Routing/DirectForwardingIEndpointRouteBuilderExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Net.Http;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Transforms;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Microsoft.AspNetCore.Builder;\n\n/// <summary>\n/// Extension methods for <see cref=\"IEndpointRouteBuilder\"/> used to add direct forwarding to the ASP.NET Core request pipeline.\n/// </summary>\npublic static class DirectForwardingIEndpointRouteBuilderExtensions\n{\n    /// <summary>\n    /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using default configuration for the outgoing request, default transforms, and default HTTP client.\n    /// </summary>\n    public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints,\n        [StringSyntax(\"Route\")] string pattern, string destinationPrefix)\n    {\n        return endpoints.MapForwarder(pattern, destinationPrefix, ForwarderRequestConfig.Empty);\n    }\n\n    /// <summary>\n    /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination and target path applying route values from the pattern using default configuration for the outgoing request, and default HTTP client.\n    /// </summary>\n    public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints,\n        [StringSyntax(\"Route\")] string pattern, string destinationPrefix, [StringSyntax(\"Route\")] string targetPath)\n    {\n        return endpoints.MapForwarder(pattern, destinationPrefix, ForwarderRequestConfig.Empty, targetPath);\n    }\n\n    /// <summary>\n    /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination and target path applying route values from the pattern using customized configuration for the outgoing request, and default HTTP client.\n    /// </summary>\n    public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints,\n        [StringSyntax(\"Route\")] string pattern, string destinationPrefix, ForwarderRequestConfig requestConfig, [StringSyntax(\"Route\")] string targetPath)\n    {\n        return endpoints.MapForwarder(pattern, destinationPrefix, requestConfig, b => b.AddPathRouteValues(targetPath));\n    }\n\n    /// <summary>\n    /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using default configuration for the outgoing request, customized transforms, and default HTTP client.\n    /// </summary>\n    public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints,\n        [StringSyntax(\"Route\")] string pattern, string destinationPrefix, Action<TransformBuilderContext> configureTransform)\n    {\n        return endpoints.MapForwarder(pattern, destinationPrefix, ForwarderRequestConfig.Empty, configureTransform);\n    }\n\n    /// <summary>\n    /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using customized configuration for the outgoing request, customized transforms, and default HTTP client.\n    /// </summary>\n    public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints,\n        [StringSyntax(\"Route\")] string pattern, string destinationPrefix, ForwarderRequestConfig requestConfig, Action<TransformBuilderContext> configureTransform)\n    {\n        var transformBuilder = endpoints.ServiceProvider.GetRequiredService<ITransformBuilder>();\n\n        var transformer = transformBuilder.Create(configureTransform);\n\n        return endpoints.MapForwarder(pattern, destinationPrefix, requestConfig, transformer);\n    }\n\n    /// <summary>\n    /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using customized configuration for the outgoing request, default transforms, and default HTTP client.\n    /// </summary>\n    public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints,\n        [StringSyntax(\"Route\")] string pattern, string destinationPrefix, ForwarderRequestConfig requestConfig)\n    {\n        return endpoints.MapForwarder(pattern, destinationPrefix, requestConfig, HttpTransformer.Default);\n    }\n\n    /// <summary>\n    /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using customized configuration for the outgoing request, customized transforms, and default HTTP client.\n    /// </summary>\n    public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints,\n        [StringSyntax(\"Route\")] string pattern, string destinationPrefix, ForwarderRequestConfig requestConfig, HttpTransformer transformer)\n    {\n        var httpClientProvider = endpoints.ServiceProvider.GetRequiredService<DirectForwardingHttpClientProvider>();\n\n        return endpoints.MapForwarder(pattern, destinationPrefix, requestConfig, transformer, httpClientProvider.HttpClient);\n    }\n\n    /// <summary>\n    /// Adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using customized configuration for the outgoing request, customized transforms, and customized HTTP client.\n    /// </summary>\n    public static IEndpointConventionBuilder MapForwarder(this IEndpointRouteBuilder endpoints,\n        [StringSyntax(\"Route\")] string pattern, string destinationPrefix, ForwarderRequestConfig requestConfig, HttpTransformer transformer, HttpMessageInvoker httpClient)\n    {\n        ArgumentNullException.ThrowIfNull(endpoints);\n        ArgumentNullException.ThrowIfNull(destinationPrefix);\n        ArgumentNullException.ThrowIfNull(httpClient);\n        ArgumentNullException.ThrowIfNull(requestConfig);\n        ArgumentNullException.ThrowIfNull(transformer);\n\n        var forwarder = endpoints.ServiceProvider.GetRequiredService<IHttpForwarder>();\n\n        return endpoints.Map(pattern, async httpContext =>\n        {\n            await forwarder.SendAsync(httpContext, destinationPrefix, httpClient, requestConfig, transformer);\n        });\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Routing/HeaderMatcher.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Microsoft.Net.Http.Headers;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Routing;\n\n/// <summary>\n/// A request header matcher used during routing.\n/// </summary>\ninternal sealed class HeaderMatcher\n{\n    /// <summary>\n    /// Creates a new instance.\n    /// </summary>\n    public HeaderMatcher(string name, IReadOnlyList<string>? values, HeaderMatchMode mode, bool isCaseSensitive)\n    {\n        if (string.IsNullOrEmpty(name))\n        {\n            throw new ArgumentException(\"A header name is required.\", nameof(name));\n        }\n        if ((mode != HeaderMatchMode.Exists && mode != HeaderMatchMode.NotExists)\n            && (values is null || values.Count == 0))\n        {\n            throw new ArgumentException(\"Header values must have at least one value.\", nameof(values));\n        }\n        if ((mode == HeaderMatchMode.Exists || mode == HeaderMatchMode.NotExists) && values?.Count > 0)\n        {\n            throw new ArgumentException($\"Header values must not be specified when using '{mode}'.\", nameof(values));\n        }\n        if (values is not null && values.Any(string.IsNullOrEmpty))\n        {\n            throw new ArgumentNullException(nameof(values), \"Header values must be not be empty.\");\n        }\n\n        Name = name;\n        Values = values?.ToArray() ?? Array.Empty<string>();\n        Mode = mode;\n        Comparison = isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;\n        Separator = name.Equals(HeaderNames.Cookie, StringComparison.OrdinalIgnoreCase) ? ';' : ',';\n    }\n\n    /// <summary>\n    /// Name of the header to look for.\n    /// </summary>\n    public string Name { get; }\n\n    /// <summary>\n    /// Returns a read-only collection of acceptable header values used during routing.\n    /// At least one value is required unless <see cref=\"Mode\"/> is set to <see cref=\"HeaderMatchMode.Exists\"/>\n    /// or <see cref=\"HeaderMatchMode.NotExists\"/>.\n    /// </summary>\n    public string[] Values { get; }\n\n    /// <summary>\n    /// Specifies how header values should be compared (e.g. exact matches Vs. by prefix).\n    /// Defaults to <see cref=\"HeaderMatchMode.ExactHeader\"/>.\n    /// </summary>\n    public HeaderMatchMode Mode { get; }\n\n    public StringComparison Comparison { get; }\n\n    public char Separator { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Routing/HeaderMatcherPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.AspNetCore.Routing.Matching;\nusing Microsoft.Extensions.Primitives;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Routing;\n\ninternal sealed class HeaderMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, IEndpointSelectorPolicy\n{\n    /// <inheritdoc/>\n    // Run after HttpMethodMatcherPolicy (-1000) and HostMatcherPolicy (-100), but before default (0)\n    public override int Order => -50;\n\n    /// <inheritdoc/>\n    public IComparer<Endpoint> Comparer => new HeaderMetadataEndpointComparer();\n\n    /// <inheritdoc/>\n    bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)\n    {\n        ArgumentNullException.ThrowIfNull(endpoints);\n\n        // When the node contains dynamic endpoints we can't make any assumptions.\n        if (ContainsDynamicEndpoints(endpoints))\n        {\n            return true;\n        }\n\n        return AppliesToEndpointsCore(endpoints);\n    }\n\n    private static bool AppliesToEndpointsCore(IReadOnlyList<Endpoint> endpoints)\n    {\n        return endpoints.Any(e =>\n        {\n            var metadata = e.Metadata.GetMetadata<IHeaderMetadata>();\n            return metadata?.Matchers?.Length > 0;\n        });\n    }\n\n    /// <inheritdoc/>\n    public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)\n    {\n        ArgumentNullException.ThrowIfNull(httpContext);\n        ArgumentNullException.ThrowIfNull(candidates);\n\n        var headers = httpContext.Request.Headers;\n\n        for (var i = 0; i < candidates.Count; i++)\n        {\n            if (!candidates.IsValidCandidate(i))\n            {\n                continue;\n            }\n\n            var matchers = candidates[i].Endpoint.Metadata.GetMetadata<IHeaderMetadata>()?.Matchers;\n\n            if (matchers is null)\n            {\n                continue;\n            }\n\n            foreach (var matcher in matchers)\n            {\n                headers.TryGetValue(matcher.Name, out var requestHeaderValues);\n                var valueIsEmpty = StringValues.IsNullOrEmpty(requestHeaderValues);\n\n                var matched = matcher.Mode switch\n                {\n                    HeaderMatchMode.Exists => !valueIsEmpty,\n                    HeaderMatchMode.NotExists => valueIsEmpty,\n                    HeaderMatchMode.ExactHeader => !valueIsEmpty && TryMatchExactOrPrefix(matcher, requestHeaderValues),\n                    HeaderMatchMode.HeaderPrefix => !valueIsEmpty && TryMatchExactOrPrefix(matcher, requestHeaderValues),\n                    HeaderMatchMode.Contains => !valueIsEmpty && TryMatchContainsOrNotContains(matcher, requestHeaderValues),\n                    HeaderMatchMode.NotContains => valueIsEmpty || TryMatchContainsOrNotContains(matcher, requestHeaderValues),\n                    _ => false\n                };\n\n                if (!matched)\n                {\n                    candidates.SetValidity(i, false);\n                    break;\n                }\n            }\n        }\n\n        return Task.CompletedTask;\n    }\n\n    private static bool TryMatchExactOrPrefix(HeaderMatcher matcher, StringValues requestHeaderValues)\n    {\n        var requestHeaderCount = requestHeaderValues.Count;\n\n        for (var i = 0; i < requestHeaderCount; i++)\n        {\n            var requestValue = requestHeaderValues[i].AsSpan();\n\n            while (!requestValue.IsEmpty)\n            {\n                requestValue = requestValue.TrimStart(' ');\n\n                // Find the end of the next value.\n                // Separators inside a quote pair must be ignored as they are a part of the value.\n                var separatorOrQuoteIndex = requestValue.IndexOfAny('\"', matcher.Separator);\n                while (separatorOrQuoteIndex != -1 && requestValue[separatorOrQuoteIndex] == '\"')\n                {\n                    var closingQuoteIndex = requestValue.Slice(separatorOrQuoteIndex + 1).IndexOf('\"');\n                    if (closingQuoteIndex == -1)\n                    {\n                        separatorOrQuoteIndex = -1;\n                    }\n                    else\n                    {\n                        var offset = separatorOrQuoteIndex + closingQuoteIndex + 2;\n                        separatorOrQuoteIndex = requestValue.Slice(offset).IndexOfAny('\"', matcher.Separator);\n                        if (separatorOrQuoteIndex != -1)\n                        {\n                            separatorOrQuoteIndex += offset;\n                        }\n                    }\n                }\n\n                ReadOnlySpan<char> value;\n                if (separatorOrQuoteIndex == -1)\n                {\n                    value = requestValue;\n                    requestValue = default;\n                }\n                else\n                {\n                    value = requestValue.Slice(0, separatorOrQuoteIndex);\n                    requestValue = requestValue.Slice(separatorOrQuoteIndex + 1);\n                }\n\n                if (value.Length > 1 && value[0] == '\"' && value[^1] == '\"')\n                {\n                    value = value.Slice(1, value.Length - 2);\n                }\n\n                foreach (var expectedValue in matcher.Values)\n                {\n                    if (matcher.Mode == HeaderMatchMode.ExactHeader\n                        ? value.Equals(expectedValue, matcher.Comparison)\n                        : value.StartsWith(expectedValue, matcher.Comparison))\n                    {\n                        return true;\n                    }\n                }\n            }\n        }\n\n        return false;\n    }\n\n    private static bool TryMatchContainsOrNotContains(HeaderMatcher matcher, StringValues requestHeaderValues)\n    {\n        Debug.Assert(matcher.Mode is HeaderMatchMode.Contains or HeaderMatchMode.NotContains, $\"{matcher.Mode}\");\n\n        var requestHeaderCount = requestHeaderValues.Count;\n\n        for (var i = 0; i < requestHeaderCount; i++)\n        {\n            var requestValue = requestHeaderValues[i];\n            if (requestValue is null)\n            {\n                continue;\n            }\n\n            foreach (var expectedValue in matcher.Values)\n            {\n                if (requestValue.Contains(expectedValue, matcher.Comparison))\n                {\n                    return matcher.Mode != HeaderMatchMode.NotContains;\n                }\n            }\n        }\n\n        return matcher.Mode == HeaderMatchMode.NotContains;\n    }\n\n    private sealed class HeaderMetadataEndpointComparer : EndpointMetadataComparer<IHeaderMetadata>\n    {\n        protected override int CompareMetadata(IHeaderMetadata? x, IHeaderMetadata? y)\n        {\n            return (y?.Matchers?.Length ?? 0).CompareTo(x?.Matchers?.Length ?? 0);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Routing/HeaderMetadata.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace Yarp.ReverseProxy.Routing;\n\n/// <summary>\n/// Represents request header metadata used during routing.\n/// </summary>\ninternal sealed class HeaderMetadata : IHeaderMetadata\n{\n    public HeaderMetadata(IReadOnlyList<HeaderMatcher> matchers)\n    {\n        ArgumentNullException.ThrowIfNull(matchers);\n        Matchers = matchers.ToArray();\n    }\n\n    /// <inheritdoc/>\n    public HeaderMatcher[] Matchers { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Routing/IHeaderMetadata.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Routing;\n\n/// <summary>\n/// Represents request header metadata used during routing.\n/// </summary>\ninternal interface IHeaderMetadata\n{\n    /// <summary>\n    /// One or more matchers to apply to the request headers.\n    /// </summary>\n    HeaderMatcher[] Matchers { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Routing/IQueryParameterMetadata.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Routing;\n\n/// <summary>\n/// Represents request query parameter metadata used during routing.\n/// </summary>\ninternal interface IQueryParameterMetadata\n{\n    /// <summary>\n    /// One or more matchers to apply to the request query parameters.\n    /// </summary>\n    QueryParameterMatcher[] Matchers { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Routing/ProxyEndpointFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Cors;\nusing Microsoft.AspNetCore.Cors.Infrastructure;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Timeouts;\nusing Microsoft.AspNetCore.RateLimiting;\nusing Microsoft.AspNetCore.OutputCaching;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.AspNetCore.Routing.Patterns;\nusing Yarp.ReverseProxy.Model;\nusing CorsConstants = Yarp.ReverseProxy.Configuration.CorsConstants;\nusing AuthorizationConstants = Yarp.ReverseProxy.Configuration.AuthorizationConstants;\nusing RateLimitingConstants = Yarp.ReverseProxy.Configuration.RateLimitingConstants;\nusing TimeoutPolicyConstants = Yarp.ReverseProxy.Configuration.TimeoutPolicyConstants;\n\nnamespace Yarp.ReverseProxy.Routing;\n\ninternal sealed class ProxyEndpointFactory\n{\n    private static readonly IAuthorizeData _defaultAuthorization = new AuthorizeAttribute();\n    private static readonly DisableRateLimitingAttribute _disableRateLimit = new();\n    private static readonly DisableRequestTimeoutAttribute _disableRequestTimeout = new();\n    private static readonly IEnableCorsAttribute _defaultCors = new EnableCorsAttribute();\n    private static readonly IDisableCorsAttribute _disableCors = new DisableCorsAttribute();\n    private static readonly IAllowAnonymous _allowAnonymous = new AllowAnonymousAttribute();\n\n    private RequestDelegate? _pipeline;\n\n    public Endpoint CreateEndpoint(RouteModel route, IReadOnlyList<Action<EndpointBuilder>> conventions)\n    {\n        var config = route.Config;\n        var match = config.Match;\n\n        // Catch-all pattern when no path was specified\n        var pathPattern = string.IsNullOrEmpty(match.Path) ? \"/{**catchall}\" : match.Path;\n\n        var endpointBuilder = new RouteEndpointBuilder(\n            requestDelegate: _pipeline ?? throw new InvalidOperationException(\"The pipeline hasn't been provided yet.\"),\n            routePattern: RoutePatternFactory.Parse(pathPattern),\n            order: config.Order.GetValueOrDefault())\n        {\n            DisplayName = config.RouteId\n        };\n\n        endpointBuilder.Metadata.Add(route);\n\n        if (match.Hosts is not null && match.Hosts.Count != 0)\n        {\n            endpointBuilder.Metadata.Add(new HostAttribute(match.Hosts.ToArray()));\n        }\n\n        if (match.Headers is not null && match.Headers.Count > 0)\n        {\n            var matchers = new List<HeaderMatcher>(match.Headers.Count);\n            foreach (var header in match.Headers)\n            {\n                matchers.Add(new HeaderMatcher(header.Name, header.Values, header.Mode, header.IsCaseSensitive));\n            }\n\n            endpointBuilder.Metadata.Add(new HeaderMetadata(matchers));\n        }\n\n        if (match.QueryParameters is not null && match.QueryParameters.Count > 0)\n        {\n            var matchers = new List<QueryParameterMatcher>(match.QueryParameters.Count);\n            foreach (var queryparam in match.QueryParameters)\n            {\n                matchers.Add(new QueryParameterMatcher(queryparam.Name, queryparam.Values, queryparam.Mode, queryparam.IsCaseSensitive));\n            }\n\n            endpointBuilder.Metadata.Add(new QueryParameterMetadata(matchers));\n        }\n\n        bool acceptCorsPreflight;\n        if (string.Equals(CorsConstants.Default, config.CorsPolicy, StringComparison.OrdinalIgnoreCase))\n        {\n            endpointBuilder.Metadata.Add(_defaultCors);\n            acceptCorsPreflight = true;\n        }\n        else if (string.Equals(CorsConstants.Disable, config.CorsPolicy, StringComparison.OrdinalIgnoreCase))\n        {\n            endpointBuilder.Metadata.Add(_disableCors);\n            acceptCorsPreflight = true;\n        }\n        else if (!string.IsNullOrEmpty(config.CorsPolicy))\n        {\n            endpointBuilder.Metadata.Add(new EnableCorsAttribute(config.CorsPolicy));\n            acceptCorsPreflight = true;\n        }\n        else\n        {\n            acceptCorsPreflight = false;\n        }\n\n        if (match.Methods is not null && match.Methods.Count > 0)\n        {\n            endpointBuilder.Metadata.Add(new HttpMethodMetadata(match.Methods, acceptCorsPreflight));\n        }\n\n        if (string.Equals(AuthorizationConstants.Default, config.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase))\n        {\n            endpointBuilder.Metadata.Add(_defaultAuthorization);\n        }\n        else if (string.Equals(AuthorizationConstants.Anonymous, config.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase))\n        {\n            endpointBuilder.Metadata.Add(_allowAnonymous);\n        }\n        else if (!string.IsNullOrEmpty(config.AuthorizationPolicy))\n        {\n            endpointBuilder.Metadata.Add(new AuthorizeAttribute(config.AuthorizationPolicy));\n        }\n\n        if (string.Equals(RateLimitingConstants.Default, config.RateLimiterPolicy, StringComparison.OrdinalIgnoreCase))\n        {\n            // No-op (middleware applies the default)\n        }\n        else if (string.Equals(RateLimitingConstants.Disable, config.RateLimiterPolicy, StringComparison.OrdinalIgnoreCase))\n        {\n            endpointBuilder.Metadata.Add(_disableRateLimit);\n        }\n        else if (!string.IsNullOrEmpty(config.RateLimiterPolicy))\n        {\n            endpointBuilder.Metadata.Add(new EnableRateLimitingAttribute(config.RateLimiterPolicy));\n        }\n\n        if (!string.IsNullOrEmpty(config.OutputCachePolicy))\n        {\n            endpointBuilder.Metadata.Add(new OutputCacheAttribute { PolicyName = config.OutputCachePolicy });\n        }\n\n        if (string.Equals(TimeoutPolicyConstants.Disable, config.TimeoutPolicy, StringComparison.OrdinalIgnoreCase))\n        {\n            endpointBuilder.Metadata.Add(_disableRequestTimeout);\n        }\n        // The config validator shouldn't allow both TimeoutPolicy and Timeout, so we don't have to consider priority.\n        else if (!string.IsNullOrEmpty(config.TimeoutPolicy))\n        {\n            endpointBuilder.Metadata.Add(new RequestTimeoutAttribute(config.TimeoutPolicy));\n        }\n        else if (config.Timeout.HasValue)\n        {\n            endpointBuilder.Metadata.Add(new RequestTimeoutAttribute((int)config.Timeout.Value.TotalMilliseconds));\n        }\n\n        for (var i = 0; i < conventions.Count; i++)\n        {\n            conventions[i](endpointBuilder);\n        }\n\n        return endpointBuilder.Build();\n    }\n\n    public void SetProxyPipeline(RequestDelegate pipeline)\n    {\n        ArgumentNullException.ThrowIfNull(pipeline);\n        _pipeline = pipeline;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Routing/QueryParameterMatcher.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Routing;\n\n/// <summary>\n/// A request query parameter matcher used during routing.\n/// </summary>\ninternal sealed class QueryParameterMatcher\n{\n    /// <summary>\n    /// Creates a new instance.\n    /// </summary>\n    public QueryParameterMatcher(string name, IReadOnlyList<string>? values, QueryParameterMatchMode mode, bool isCaseSensitive)\n    {\n        ArgumentException.ThrowIfNullOrEmpty(name);\n        if (mode != QueryParameterMatchMode.Exists\n            && (values is null || values.Count == 0))\n        {\n            throw new ArgumentException(\"Query parameter values must have at least one value.\", nameof(values));\n        }\n        if (mode == QueryParameterMatchMode.Exists && values?.Count > 0)\n        {\n            throw new ArgumentException($\"Query parameter values must not be specified when using '{nameof(QueryParameterMatchMode.Exists)}'.\", nameof(values));\n        }\n        if (values is not null && values.Any(string.IsNullOrEmpty))\n        {\n            throw new ArgumentNullException(nameof(values), \"Query parameter values must not be empty.\");\n        }\n\n        Name = name;\n        Values = values?.ToArray() ?? Array.Empty<string>();\n        Mode = mode;\n        Comparison = isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;\n    }\n\n    /// <summary>\n    /// Name of the query parameter to look for.\n    /// </summary>\n    public string Name { get; }\n\n    /// <summary>\n    /// Returns a read-only collection of acceptable query parameter values used during routing.\n    /// At least one value is required unless <see cref=\"Mode\"/> is set to <see cref=\"QueryParameterMatchMode.Exists\"/>.\n    /// </summary>\n    public string[] Values { get; }\n\n    /// <summary>\n    /// Specifies how query parameter values should be compared (e.g. exact matches Vs. contains).\n    /// Defaults to <see cref=\"QueryParameterMatchMode.Exact\"/>.\n    /// </summary>\n    public QueryParameterMatchMode Mode { get; }\n\n    public StringComparison Comparison { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Routing/QueryParameterMatcherPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.AspNetCore.Routing.Matching;\nusing Microsoft.Extensions.Primitives;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Routing;\n\ninternal sealed class QueryParameterMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, IEndpointSelectorPolicy\n{\n    /// <inheritdoc/>\n    // Run after HttpMethodMatcherPolicy (-1000) and HostMatcherPolicy (-100), and HeaderMatcherPolicy (-50), but before default (0)\n    public override int Order => -25;\n\n    /// <inheritdoc/>\n    public IComparer<Endpoint> Comparer => new QueryParameterMetadataEndpointComparer();\n\n    /// <inheritdoc/>\n    bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)\n    {\n        ArgumentNullException.ThrowIfNull(endpoints);\n\n        // When the node contains dynamic endpoints we can't make any assumptions.\n        if (ContainsDynamicEndpoints(endpoints))\n        {\n            return true;\n        }\n\n        return AppliesToEndpointsCore(endpoints);\n    }\n\n    private static bool AppliesToEndpointsCore(IReadOnlyList<Endpoint> endpoints)\n    {\n        return endpoints.Any(e =>\n        {\n            var metadata = e.Metadata.GetMetadata<IQueryParameterMetadata>();\n            return metadata?.Matchers?.Length > 0;\n        });\n    }\n\n    /// <inheritdoc/>\n    public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)\n    {\n        ArgumentNullException.ThrowIfNull(httpContext);\n        ArgumentNullException.ThrowIfNull(candidates);\n\n        var query = httpContext.Request.Query;\n\n        for (var i = 0; i < candidates.Count; i++)\n        {\n            if (!candidates.IsValidCandidate(i))\n            {\n                continue;\n            }\n\n            var matchers = candidates[i].Endpoint.Metadata.GetMetadata<IQueryParameterMetadata>()?.Matchers;\n\n            if (matchers is null)\n            {\n                continue;\n            }\n\n            foreach (var matcher in matchers)\n            {\n                query.TryGetValue(matcher.Name, out var requestQueryParameterValues);\n                var valueIsEmpty = StringValues.IsNullOrEmpty(requestQueryParameterValues);\n\n                var matched = matcher.Mode switch\n                {\n                    QueryParameterMatchMode.Exists => !valueIsEmpty,\n                    QueryParameterMatchMode.Exact => !valueIsEmpty && TryMatch(matcher, requestQueryParameterValues),\n                    QueryParameterMatchMode.Prefix => !valueIsEmpty && TryMatch(matcher, requestQueryParameterValues),\n                    QueryParameterMatchMode.Contains => !valueIsEmpty && TryMatch(matcher, requestQueryParameterValues),\n                    QueryParameterMatchMode.NotContains => valueIsEmpty || TryMatch(matcher, requestQueryParameterValues),\n                    _ => false\n                };\n\n                if (!matched)\n                {\n                    candidates.SetValidity(i, false);\n                    break;\n                }\n            }\n        }\n\n        return Task.CompletedTask;\n    }\n\n    private static bool TryMatch(QueryParameterMatcher matcher, StringValues requestHeaderValues)\n    {\n        var requestHeaderCount = requestHeaderValues.Count;\n\n        for (var i = 0; i < requestHeaderCount; i++)\n        {\n            var requestValue = requestHeaderValues[i];\n            if (requestValue is null)\n            {\n                continue;\n            }\n\n            foreach (var expectedValue in matcher.Values)\n            {\n                if (TryMatch(matcher, requestValue, expectedValue))\n                {\n                    return matcher.Mode != QueryParameterMatchMode.NotContains;\n                }\n            }\n        }\n\n        return matcher.Mode == QueryParameterMatchMode.NotContains;\n\n        static bool TryMatch(QueryParameterMatcher matcher, string queryValue, string expectedValue)\n        {\n            return matcher.Mode switch\n            {\n                QueryParameterMatchMode.Exact => queryValue.Equals(expectedValue, matcher.Comparison),\n                QueryParameterMatchMode.Prefix => queryValue.StartsWith(expectedValue, matcher.Comparison),\n                _ => queryValue.Contains(expectedValue, matcher.Comparison)\n            };\n        }\n    }\n\n    private sealed class QueryParameterMetadataEndpointComparer : EndpointMetadataComparer<IQueryParameterMetadata>\n    {\n        protected override int CompareMetadata(IQueryParameterMetadata? x, IQueryParameterMetadata? y)\n        {\n            return (y?.Matchers?.Length ?? 0).CompareTo(x?.Matchers?.Length ?? 0);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Routing/QueryParameterMetadata.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace Yarp.ReverseProxy.Routing;\n\n/// <summary>\n/// Represents request query parameter metadata used during routing.\n/// </summary>\ninternal sealed class QueryParameterMetadata : IQueryParameterMetadata\n{\n    public QueryParameterMetadata(IReadOnlyList<QueryParameterMatcher> matchers)\n    {\n        ArgumentNullException.ThrowIfNull(matchers);\n        Matchers = matchers.ToArray();\n    }\n\n    /// <inheritdoc/>\n    public QueryParameterMatcher[] Matchers { get; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Routing/ReverseProxyConventionBuilder.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Microsoft.AspNetCore.Builder;\n\npublic class ReverseProxyConventionBuilder : IEndpointConventionBuilder\n{\n    private readonly List<Action<EndpointBuilder>> _conventions;\n\n    internal ReverseProxyConventionBuilder(List<Action<EndpointBuilder>> conventions)\n    {\n        ArgumentNullException.ThrowIfNull(conventions);\n        _conventions = conventions;\n    }\n\n    /// <summary>\n    /// Adds the specified convention to the builder. Conventions are used to customize <see cref=\"EndpointBuilder\"/> instances.\n    /// </summary>\n    /// <param name=\"convention\">The convention to add to the builder.</param>\n    public void Add(Action<EndpointBuilder> convention)\n    {\n        ArgumentNullException.ThrowIfNull(convention);\n\n        _conventions.Add(convention);\n    }\n\n    /// <summary>\n    /// Configures the endpoints for all routes \n    /// </summary>\n    /// <param name=\"convention\">The convention to add to the builder.</param>\n    /// <returns></returns>\n    public ReverseProxyConventionBuilder ConfigureEndpoints(Action<IEndpointConventionBuilder> convention)\n    {\n        ArgumentNullException.ThrowIfNull(convention);\n\n        void Action(EndpointBuilder endpointBuilder)\n        {\n            var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder);\n            convention(conventionBuilder);\n        }\n\n        Add(Action);\n\n        return this;\n    }\n\n    /// <summary>\n    /// Configures the endpoints for all routes \n    /// </summary>\n    /// <param name=\"convention\">The convention to add to the builder.</param>\n    /// <returns></returns>\n    public ReverseProxyConventionBuilder ConfigureEndpoints(Action<IEndpointConventionBuilder, RouteConfig> convention)\n    {\n        ArgumentNullException.ThrowIfNull(convention);\n\n        void Action(EndpointBuilder endpointBuilder)\n        {\n            var route = endpointBuilder.Metadata.OfType<RouteModel>().Single();\n            var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder);\n            convention(conventionBuilder, route.Config);\n        }\n\n        Add(Action);\n\n        return this;\n    }\n\n    /// <summary>\n    /// Configures the endpoints for all routes \n    /// </summary>\n    /// <param name=\"convention\">The convention to add to the builder.</param>\n    /// <returns></returns>\n    public ReverseProxyConventionBuilder ConfigureEndpoints(Action<IEndpointConventionBuilder, RouteConfig, ClusterConfig?> convention)\n    {\n        ArgumentNullException.ThrowIfNull(convention);\n\n        void Action(EndpointBuilder endpointBuilder)\n        {\n            var routeModel = endpointBuilder.Metadata.OfType<RouteModel>().Single();\n\n            var clusterConfig = routeModel.Cluster?.Model.Config;\n            var routeConfig = routeModel.Config;\n            var conventionBuilder = new EndpointBuilderConventionBuilder(endpointBuilder);\n            convention(conventionBuilder, routeConfig, clusterConfig);\n        }\n\n        Add(Action);\n\n        return this;\n    }\n\n    private sealed class EndpointBuilderConventionBuilder : IEndpointConventionBuilder\n    {\n        private readonly EndpointBuilder _endpointBuilder;\n\n        public EndpointBuilderConventionBuilder(EndpointBuilder endpointBuilder)\n        {\n            _endpointBuilder = endpointBuilder;\n        }\n\n        public void Add(Action<EndpointBuilder> convention)\n        {\n            convention(_endpointBuilder);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Routing/ReverseProxyIEndpointRouteBuilderExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Limits;\nusing Yarp.ReverseProxy.Management;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Routing;\n\nnamespace Microsoft.AspNetCore.Builder;\n\n/// <summary>\n/// Extension methods for <see cref=\"IEndpointRouteBuilder\"/>\n/// used to add Reverse Proxy to the ASP .NET Core request pipeline.\n/// </summary>\npublic static class ReverseProxyIEndpointRouteBuilderExtensions\n{\n    /// <summary>\n    /// Adds Reverse Proxy routes to the route table using the default processing pipeline.\n    /// </summary>\n    public static ReverseProxyConventionBuilder MapReverseProxy(this IEndpointRouteBuilder endpoints)\n    {\n        return endpoints.MapReverseProxy(app =>\n        {\n            app.UseSessionAffinity();\n            app.UseLoadBalancing();\n            app.UsePassiveHealthChecks();\n        });\n    }\n\n    /// <summary>\n    /// Adds Reverse Proxy routes to the route table with the customized processing pipeline. The pipeline includes\n    /// by default the initialization step and the final proxy step, but not LoadBalancingMiddleware or other intermediate components.\n    /// </summary>\n    public static ReverseProxyConventionBuilder MapReverseProxy(this IEndpointRouteBuilder endpoints, Action<IReverseProxyApplicationBuilder> configureApp)\n    {\n        ArgumentNullException.ThrowIfNull(endpoints);\n        ArgumentNullException.ThrowIfNull(configureApp);\n\n        var proxyAppBuilder = new ReverseProxyApplicationBuilder(endpoints.CreateApplicationBuilder());\n        proxyAppBuilder.UseMiddleware<ProxyPipelineInitializerMiddleware>();\n        configureApp(proxyAppBuilder);\n        proxyAppBuilder.UseMiddleware<LimitsMiddleware>();\n        proxyAppBuilder.UseMiddleware<ForwarderMiddleware>();\n        var app = proxyAppBuilder.Build();\n\n        var proxyEndpointFactory = endpoints.ServiceProvider.GetRequiredService<ProxyEndpointFactory>();\n        proxyEndpointFactory.SetProxyPipeline(app);\n\n        return GetOrCreateDataSource(endpoints).DefaultBuilder;\n    }\n\n    private static ProxyConfigManager GetOrCreateDataSource(IEndpointRouteBuilder endpoints)\n    {\n        var dataSource = endpoints.DataSources.OfType<ProxyConfigManager>().FirstOrDefault();\n        if (dataSource is null)\n        {\n            dataSource = endpoints.ServiceProvider.GetRequiredService<ProxyConfigManager>();\n            endpoints.DataSources.Add(dataSource);\n\n            // Config validation is async but startup is sync. We want this to block so that A) any validation errors can prevent\n            // the app from starting, and B) so that all the config is ready before the server starts accepting requests.\n            // Reloads will be async.\n            dataSource.InitialLoadAsync().GetAwaiter().GetResult();\n        }\n\n        return dataSource;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/ServiceDiscovery/DnsDestinationResolver.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Options;\nusing Microsoft.Extensions.Primitives;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.ServiceDiscovery;\n\n/// <summary>\n/// Implementation of <see cref=\"IDestinationResolver\"/> which resolves host names to IP addresses using DNS.\n/// </summary>\ninternal sealed class DnsDestinationResolver : IDestinationResolver\n{\n    private readonly IOptionsMonitor<DnsDestinationResolverOptions> _options;\n\n    /// <summary>\n    /// Initializes a new <see cref=\"DnsDestinationResolver\"/> instance.\n    /// </summary>\n    /// <param name=\"options\">The options.</param>\n    public DnsDestinationResolver(IOptionsMonitor<DnsDestinationResolverOptions> options)\n    {\n        _options = options;\n    }\n\n    /// <inheritdoc/>\n    public async ValueTask<ResolvedDestinationCollection> ResolveDestinationsAsync(IReadOnlyDictionary<string, DestinationConfig> destinations, CancellationToken cancellationToken)\n    {\n        var options = _options.CurrentValue;\n        Dictionary<string, DestinationConfig> results = new();\n        var tasks = new List<Task<List<(string Name, DestinationConfig Config)>>>(destinations.Count);\n        foreach (var (destinationId, destinationConfig) in destinations)\n        {\n            tasks.Add(ResolveHostAsync(options, destinationId, destinationConfig, cancellationToken));\n        }\n\n        await Task.WhenAll(tasks);\n        foreach (var task in tasks)\n        {\n            var configs = await task;\n            foreach (var (name, config) in configs)\n            {\n                results[name] = config;\n            }\n        }\n\n        var changeToken = options.RefreshPeriod switch\n        {\n            { } refreshPeriod when refreshPeriod > TimeSpan.Zero => new CancellationChangeToken(new CancellationTokenSource(refreshPeriod).Token),\n            _ => null,\n        };\n\n        return new ResolvedDestinationCollection(results, changeToken);\n    }\n\n    private static async Task<List<(string Name, DestinationConfig Config)>> ResolveHostAsync(\n        DnsDestinationResolverOptions options,\n        string originalName,\n        DestinationConfig originalConfig,\n        CancellationToken cancellationToken)\n    {\n        var originalUri = new Uri(originalConfig.Address);\n        var originalHost = originalConfig.Host is { Length: > 0 } host ? host : originalUri.Authority;\n        var hostName = originalUri.DnsSafeHost;\n        IPAddress[] addresses;\n        try\n        {\n            addresses = options.AddressFamily switch\n            {\n                { } addressFamily => await Dns.GetHostAddressesAsync(hostName, addressFamily, cancellationToken).ConfigureAwait(false),\n                null => await Dns.GetHostAddressesAsync(hostName, cancellationToken).ConfigureAwait(false)\n            };\n        }\n        catch (Exception exception)\n        {\n            throw new InvalidOperationException($\"Failed to resolve host '{hostName}'. See {nameof(Exception.InnerException)} for details.\", exception);\n        }\n\n        var results = new List<(string Name, DestinationConfig Config)>(addresses.Length);\n        var uriBuilder = new UriBuilder(originalUri);\n        var healthUri = originalConfig.Health is { Length: > 0 } health ? new Uri(health) : null;\n        var healthUriBuilder = healthUri is { } ? new UriBuilder(healthUri) : null;\n        foreach (var address in addresses)\n        {\n            var addressString = address.ToString();\n            uriBuilder.Host = addressString;\n            var resolvedAddress = uriBuilder.Uri.ToString();\n            var healthAddress = originalConfig.Health;\n            if (healthUriBuilder is not null)\n            {\n                healthUriBuilder.Host = addressString;\n                healthAddress = healthUriBuilder.Uri.ToString();\n            }\n\n            var name = $\"{originalName}[{addressString}]\";\n            var config = originalConfig with { Host = originalHost, Address = resolvedAddress, Health = healthAddress };\n            results.Add((name, config));\n        }\n\n        return results;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/ServiceDiscovery/DnsDestinationResolverOptions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Sockets;\n\nnamespace Yarp.ReverseProxy.ServiceDiscovery;\n\n/// <summary>\n/// Options for <see cref=\"DnsDestinationResolver\"/>.\n/// </summary>\npublic class DnsDestinationResolverOptions\n{\n    /// <summary>\n    /// The period between requesting a refresh of a resolved name.\n    /// </summary>\n    /// <remarks>\n    /// Defaults to 5 minutes.\n    /// </remarks>\n    public TimeSpan? RefreshPeriod { get; set; } = TimeSpan.FromMinutes(5);\n\n    /// <summary>\n    /// The optional address family to query for.\n    /// Use <see cref=\"AddressFamily.InterNetwork\"/> for IPv4 addresses and <see cref=\"AddressFamily.InterNetworkV6\"/> for IPv6 addresses.\n    /// </summary>\n    /// <remarks>\n    /// Defaults to <see langword=\"null\"/> (any address).\n    /// </remarks>\n    public AddressFamily? AddressFamily { get; set; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/ServiceDiscovery/IDestinationResolver.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.ServiceDiscovery;\n\n/// <summary>\n/// Resolves destination addresses.\n/// </summary>\npublic interface IDestinationResolver\n{\n    /// <summary>\n    /// Resolves the provided destinations and returns resolved destinations.\n    /// </summary>\n    /// <param name=\"destinations\">The destinations to resolve.</param>\n    /// <param name=\"cancellationToken\">The cancellation token.</param>\n    /// <returns>\n    /// The resolved destinations and a change token used to indicate when resolution should be performed again.\n    /// </returns>\n    ValueTask<ResolvedDestinationCollection> ResolveDestinationsAsync(\n        IReadOnlyDictionary<string, DestinationConfig> destinations,\n        CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "src/ReverseProxy/ServiceDiscovery/NoOpDestinationResolver.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.ServiceDiscovery;\n\n/// <summary>\n/// An <see cref=\"IDestinationResolver\"/> which performs no action.\n/// </summary>\ninternal sealed class NoOpDestinationResolver : IDestinationResolver\n{\n    public ValueTask<ResolvedDestinationCollection> ResolveDestinationsAsync(IReadOnlyDictionary<string, DestinationConfig> destinations, CancellationToken cancellationToken)\n        => new(new ResolvedDestinationCollection(destinations, changeToken: null));\n}\n"
  },
  {
    "path": "src/ReverseProxy/ServiceDiscovery/ResolvedDestinationCollection.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Microsoft.Extensions.Primitives;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.ServiceDiscovery\n{\n    /// <summary>\n    /// Represents a collection of resolved destinations.\n    /// </summary>\n    public sealed class ResolvedDestinationCollection\n    {\n        public ResolvedDestinationCollection(IReadOnlyDictionary<string, DestinationConfig> destinations, IChangeToken? changeToken)\n        {\n            Destinations = destinations;\n            ChangeToken = changeToken;\n        }\n\n        /// <summary>\n        /// Gets the map of destination names to destination configurations.\n        /// </summary>\n        public IReadOnlyDictionary<string, DestinationConfig> Destinations { get; init; }\n\n        /// <summary>\n        /// Gets the optional change token used to signal when this collection should be refreshed.\n        /// </summary>\n        public IChangeToken? ChangeToken { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/AffinitizeTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Transforms;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\n/// <summary>\n/// Affinitizes the request to a chosen <see cref=\"DestinationState\"/>.\n/// </summary>\ninternal sealed class AffinitizeTransform : ResponseTransform\n{\n    private readonly ISessionAffinityPolicy _sessionAffinityPolicy;\n\n    public AffinitizeTransform(ISessionAffinityPolicy sessionAffinityPolicy)\n    {\n        ArgumentNullException.ThrowIfNull(sessionAffinityPolicy);\n        _sessionAffinityPolicy = sessionAffinityPolicy;\n    }\n\n    public override ValueTask ApplyAsync(ResponseTransformContext context)\n    {\n        var proxyFeature = context.HttpContext.GetReverseProxyFeature();\n        var options = proxyFeature.Cluster.Config.SessionAffinity;\n        // The transform should only be added to routes that have affinity enabled.\n        // However, the cluster can be re-assigned dynamically.\n        if (options is null || !options.Enabled.GetValueOrDefault())\n        {\n            return default;\n        }\n\n        Debug.Assert(proxyFeature.Route.Cluster is not null);\n        Debug.Assert(proxyFeature.ProxiedDestination is not null);\n\n        return _sessionAffinityPolicy.AffinitizeResponseAsync(\n            context.HttpContext,\n            proxyFeature.Route.Cluster,\n            options,\n            proxyFeature.ProxiedDestination,\n            context.CancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/AffinitizeTransformProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Transforms.Builder;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\ninternal sealed class AffinitizeTransformProvider : ITransformProvider\n{\n    private readonly FrozenDictionary<string, ISessionAffinityPolicy> _sessionAffinityPolicies;\n\n    public AffinitizeTransformProvider(IEnumerable<ISessionAffinityPolicy> sessionAffinityPolicies)\n    {\n        ArgumentNullException.ThrowIfNull(sessionAffinityPolicies);\n        _sessionAffinityPolicies = sessionAffinityPolicies.ToDictionaryByUniqueId(p => p.Name);\n    }\n\n    public void ValidateRoute(TransformRouteValidationContext context)\n    {\n    }\n\n    public void ValidateCluster(TransformClusterValidationContext context)\n    {\n        // Other affinity validation logic is covered by ConfigValidator.ValidateSessionAffinity.\n        if (!(context.Cluster.SessionAffinity?.Enabled ?? false))\n        {\n            return;\n        }\n\n        var policy = context.Cluster.SessionAffinity.Policy;\n        if (string.IsNullOrEmpty(policy))\n        {\n            // The default.\n            policy = SessionAffinityConstants.Policies.HashCookie;\n        }\n\n        if (!_sessionAffinityPolicies.ContainsKey(policy))\n        {\n            context.Errors.Add(new ArgumentException($\"No matching {nameof(ISessionAffinityPolicy)} found for the session affinity policy '{policy}' set on the cluster '{context.Cluster.ClusterId}'.\"));\n        }\n    }\n\n    public void Apply(TransformBuilderContext context)\n    {\n        var options = context.Cluster?.SessionAffinity;\n\n        if (options is not null && options.Enabled.GetValueOrDefault())\n        {\n            var policy = _sessionAffinityPolicies.GetRequiredServiceById(options.Policy, SessionAffinityConstants.Policies.HashCookie);\n            context.ResponseTransforms.Add(new AffinitizeTransform(policy));\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/AffinityHelpers.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\ninternal static class AffinityHelpers\n{\n    internal static CookieOptions CreateCookieOptions(SessionAffinityCookieConfig? config, bool isHttps, TimeProvider timeProvider)\n    {\n        return new CookieOptions\n        {\n            Path = config?.Path ?? \"/\",\n            SameSite = config?.SameSite ?? SameSiteMode.Unspecified,\n            HttpOnly = config?.HttpOnly ?? true,\n            MaxAge = config?.MaxAge,\n            Domain = config?.Domain,\n            IsEssential = config?.IsEssential ?? false,\n            Secure = config?.SecurePolicy == CookieSecurePolicy.Always || (config?.SecurePolicy == CookieSecurePolicy.SameAsRequest && isHttps),\n            Expires = config?.Expiration is not null ? timeProvider.GetUtcNow().Add(config.Expiration.Value) : default(DateTimeOffset?),\n        };\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/AffinityResult.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\n/// <summary>\n/// Affinity resolution result.\n/// </summary>\npublic readonly struct AffinityResult\n{\n    public IReadOnlyList<DestinationState>? Destinations { get; }\n\n    public AffinityStatus Status { get; }\n\n    public AffinityResult(IReadOnlyList<DestinationState>? destinations, AffinityStatus status)\n    {\n        Destinations = destinations;\n        Status = status;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/AffinityStatus.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\n/// <summary>\n/// Affinity resolution status.\n/// </summary>\npublic enum AffinityStatus\n{\n    OK,\n    AffinityKeyNotSet,\n    AffinityKeyExtractionFailed,\n    DestinationNotFound\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/AppBuilderSessionAffinityExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.ReverseProxy.SessionAffinity;\n\nnamespace Microsoft.AspNetCore.Builder;\n\n/// <summary>\n/// Extensions for adding proxy middleware to the pipeline.\n/// </summary>\npublic static class AppBuilderSessionAffinityExtensions\n{\n    /// <summary>\n    /// Checks if a request has an established affinity relationship and if the associated destination is available.\n    /// This should be placed before load balancing and other destination selection components.\n    /// Requests without an affinity relationship will be processed normally and have the affinity relationship\n    /// established by a later component.\n    /// </summary>\n    public static IReverseProxyApplicationBuilder UseSessionAffinity(this IReverseProxyApplicationBuilder builder)\n    {\n        builder.UseMiddleware<SessionAffinityMiddleware>();\n        return builder;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/ArrCookieSessionAffinityPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Runtime.CompilerServices;\nusing System.Security.Cryptography;\nusing System.Text;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\ninternal sealed class ArrCookieSessionAffinityPolicy : BaseHashCookieSessionAffinityPolicy\n{\n    private readonly ConditionalWeakTable<DestinationState, string> _hashes = new();\n\n    public ArrCookieSessionAffinityPolicy(\n        TimeProvider timeProvider,\n        ILogger<ArrCookieSessionAffinityPolicy> logger)\n        : base(timeProvider, logger) { }\n\n    public override string Name => SessionAffinityConstants.Policies.ArrCookie;\n\n    protected override string GetDestinationHash(DestinationState d)\n    {\n        return _hashes.GetValue(d, static d =>\n        {\n            // Matches the format used by ARR\n            var destinationIdBytes = Encoding.Unicode.GetBytes(d.DestinationId.ToLowerInvariant());\n            var hashBytes = SHA256.HashData(destinationIdBytes);\n            return Convert.ToHexString(hashBytes);\n        });\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/BaseEncryptedSessionAffinityPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\ninternal abstract class BaseEncryptedSessionAffinityPolicy<T> : ISessionAffinityPolicy\n{\n    private readonly IDataProtector _dataProtector;\n    protected static readonly object AffinityKeyId = new object();\n    protected readonly ILogger Logger;\n\n    protected BaseEncryptedSessionAffinityPolicy(IDataProtectionProvider dataProtectionProvider, ILogger logger)\n    {\n        ArgumentNullException.ThrowIfNull(logger);\n        _dataProtector = dataProtectionProvider?.CreateProtector(GetType().FullName!) ?? throw new ArgumentNullException(nameof(dataProtectionProvider));\n        Logger = logger;\n    }\n\n    public abstract string Name { get; }\n\n    public void AffinitizeResponse(HttpContext context, ClusterState cluster, SessionAffinityConfig config, DestinationState destination)\n    {\n        if (!config.Enabled.GetValueOrDefault())\n        {\n            throw new InvalidOperationException($\"Session affinity is disabled for cluster.\");\n        }\n\n        if (context.RequestAborted.IsCancellationRequested)\n        {\n            // Avoid wasting time if the client is already gone.\n            return;\n        }\n\n        // Affinity key is set on the response only if it's a new affinity.\n        if (!context.Items.ContainsKey(AffinityKeyId))\n        {\n            var affinityKey = GetDestinationAffinityKey(destination);\n            SetAffinityKey(context, cluster, config, affinityKey);\n        }\n    }\n\n    public virtual AffinityResult FindAffinitizedDestinations(HttpContext context, ClusterState cluster, SessionAffinityConfig config, IReadOnlyList<DestinationState> destinations)\n    {\n        if (!config.Enabled.GetValueOrDefault())\n        {\n            throw new InvalidOperationException($\"Session affinity is disabled for cluster {cluster.ClusterId}.\");\n        }\n\n        var requestAffinityKey = GetRequestAffinityKey(context, cluster, config);\n\n        if (requestAffinityKey.Key is null)\n        {\n            return new AffinityResult(null, requestAffinityKey.ExtractedSuccessfully ? AffinityStatus.AffinityKeyNotSet : AffinityStatus.AffinityKeyExtractionFailed);\n        }\n\n        IReadOnlyList<DestinationState>? matchingDestinations = null;\n        if (destinations.Count > 0)\n        {\n            for (var i = 0; i < destinations.Count; i++)\n            {\n                // TODO: Add fast destination lookup by ID\n                if (requestAffinityKey.Key.Equals(GetDestinationAffinityKey(destinations[i])))\n                {\n                    // It's allowed to affinitize a request to a pool of destinations to enable load-balancing among them.\n                    // However, we currently stop after the first match found to avoid performance degradation.\n                    matchingDestinations = destinations[i];\n                    break;\n                }\n            }\n\n            if (matchingDestinations is null)\n            {\n                Log.DestinationMatchingToAffinityKeyNotFound(Logger, cluster.ClusterId);\n            }\n        }\n        else\n        {\n            Log.AffinityCannotBeEstablishedBecauseNoDestinationsFound(Logger, cluster.ClusterId);\n        }\n\n        // Empty destination list passed to this method is handled the same way as if no matching destinations are found.\n        if (matchingDestinations is null)\n        {\n            return new AffinityResult(null, AffinityStatus.DestinationNotFound);\n        }\n\n        context.Items[AffinityKeyId] = requestAffinityKey;\n        return new AffinityResult(matchingDestinations, AffinityStatus.OK);\n    }\n\n    protected abstract T GetDestinationAffinityKey(DestinationState destination);\n\n    protected abstract (T? Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config);\n\n    protected abstract void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config, T unencryptedKey);\n\n    protected string Protect(string unencryptedKey)\n    {\n        if (string.IsNullOrEmpty(unencryptedKey))\n        {\n            return unencryptedKey;\n        }\n\n        var userData = Encoding.UTF8.GetBytes(unencryptedKey);\n\n        var protectedData = _dataProtector.Protect(userData);\n        return Convert.ToBase64String(protectedData).TrimEnd('=');\n    }\n\n    protected (string? Key, bool ExtractedSuccessfully) Unprotect(string? encryptedRequestKey)\n    {\n        if (string.IsNullOrEmpty(encryptedRequestKey))\n        {\n            return (Key: null, ExtractedSuccessfully: true);\n        }\n\n        try\n        {\n            var keyBytes = Convert.FromBase64String(Pad(encryptedRequestKey));\n\n            var decryptedKeyBytes = _dataProtector.Unprotect(keyBytes);\n            if (decryptedKeyBytes is null)\n            {\n                Log.RequestAffinityKeyDecryptionFailed(Logger, null);\n                return (Key: null, ExtractedSuccessfully: false);\n            }\n\n            return (Key: Encoding.UTF8.GetString(decryptedKeyBytes), ExtractedSuccessfully: true);\n        }\n        catch (Exception ex)\n        {\n            Log.RequestAffinityKeyDecryptionFailed(Logger, ex);\n            return (Key: null, ExtractedSuccessfully: false);\n        }\n    }\n\n    private static string Pad(string text)\n    {\n        var padding = 3 - ((text.Length + 3) % 4);\n        if (padding == 0)\n        {\n            return text;\n        }\n        return text + new string('=', padding);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/BaseHashCookieSessionAffinityPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\ninternal abstract class BaseHashCookieSessionAffinityPolicy : ISessionAffinityPolicy\n{\n    private static readonly object AffinityKeyId = new();\n    private readonly ILogger _logger;\n    private readonly TimeProvider _timeProvider;\n\n    public BaseHashCookieSessionAffinityPolicy(TimeProvider timeProvider, ILogger logger)\n    {\n        ArgumentNullException.ThrowIfNull(timeProvider);\n        ArgumentNullException.ThrowIfNull(logger);\n\n        _timeProvider = timeProvider;\n        _logger = logger;\n    }\n\n    public abstract string Name { get; }\n\n    public void AffinitizeResponse(HttpContext context, ClusterState cluster, SessionAffinityConfig config, DestinationState destination)\n    {\n        if (!config.Enabled.GetValueOrDefault())\n        {\n            throw new InvalidOperationException(\"Session affinity is disabled for cluster.\");\n        }\n\n        if (context.RequestAborted.IsCancellationRequested)\n        {\n            // Avoid wasting time if the client is already gone.\n            return;\n        }\n\n        // Affinity key is set on the response only if it's a new affinity.\n        if (!context.Items.ContainsKey(AffinityKeyId))\n        {\n            var affinityKey = GetDestinationHash(destination);\n            var affinityCookieOptions = AffinityHelpers.CreateCookieOptions(config.Cookie, context.Request.IsHttps, _timeProvider);\n\n            // CodeQL [SM02373] - Whether CookieOptions.Secure is used depends on YARP configuration, and session affinity may be used in non-HTTPS setups. Hash-based affinity policies do not intend to provide privacy protection. See https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/session-affinity#key-protection.\n            context.Response.Cookies.Append(config.AffinityKeyName, affinityKey, affinityCookieOptions);\n        }\n    }\n\n    public AffinityResult FindAffinitizedDestinations(HttpContext context, ClusterState cluster, SessionAffinityConfig config, IReadOnlyList<DestinationState> destinations)\n    {\n        if (!config.Enabled.GetValueOrDefault())\n        {\n            throw new InvalidOperationException($\"Session affinity is disabled for cluster {cluster.ClusterId}.\");\n        }\n\n        var affinityHash = context.Request.Cookies[config.AffinityKeyName];\n        if (affinityHash is null)\n        {\n            return new(null, AffinityStatus.AffinityKeyNotSet);\n        }\n\n        foreach (var d in destinations)\n        {\n            var hashValue = GetDestinationHash(d);\n\n            if (affinityHash == hashValue)\n            {\n                context.Items[AffinityKeyId] = affinityHash;\n                return new(d, AffinityStatus.OK);\n            }\n        }\n\n        if (destinations.Count == 0)\n        {\n            Log.AffinityCannotBeEstablishedBecauseNoDestinationsFound(_logger, cluster.ClusterId);\n        }\n        else\n        {\n            Log.DestinationMatchingToAffinityKeyNotFound(_logger, cluster.ClusterId);\n        }\n\n        return new(null, AffinityStatus.DestinationNotFound);\n    }\n\n    protected abstract string GetDestinationHash(DestinationState d);\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/CookieSessionAffinityPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\ninternal sealed class CookieSessionAffinityPolicy : BaseEncryptedSessionAffinityPolicy<string>\n{\n    private readonly TimeProvider _timeProvider;\n\n    public CookieSessionAffinityPolicy(\n        IDataProtectionProvider dataProtectionProvider,\n        TimeProvider timeProvider,\n        ILogger<CookieSessionAffinityPolicy> logger)\n        : base(dataProtectionProvider, logger)\n    {\n        ArgumentNullException.ThrowIfNull(timeProvider);\n        _timeProvider = timeProvider;\n    }\n\n    public override string Name => SessionAffinityConstants.Policies.Cookie;\n\n    protected override string GetDestinationAffinityKey(DestinationState destination)\n    {\n        return destination.DestinationId;\n    }\n\n    protected override (string? Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config)\n    {\n        var encryptedRequestKey = context.Request.Cookies.TryGetValue(config.AffinityKeyName, out var keyInCookie) ? keyInCookie : null;\n        return Unprotect(encryptedRequestKey);\n    }\n\n    protected override void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config, string unencryptedKey)\n    {\n        var affinityCookieOptions = AffinityHelpers.CreateCookieOptions(config.Cookie, context.Request.IsHttps, _timeProvider);\n\n        // CodeQL [SM02373] - Whether CookieOptions.Secure is used depends on YARP configuration, and session affinity may be used in non-HTTPS setups. Cookie values are encrypted using ASP.NET DataProtection. See https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/session-affinity#key-protection.\n        context.Response.Cookies.Append(config.AffinityKeyName, Protect(unencryptedKey), affinityCookieOptions);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/CustomHeaderSessionAffinityPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Primitives;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\ninternal sealed class CustomHeaderSessionAffinityPolicy : BaseEncryptedSessionAffinityPolicy<string>\n{\n    public CustomHeaderSessionAffinityPolicy(\n        IDataProtectionProvider dataProtectionProvider,\n        ILogger<CustomHeaderSessionAffinityPolicy> logger)\n        : base(dataProtectionProvider, logger)\n    { }\n\n    public override string Name => SessionAffinityConstants.Policies.CustomHeader;\n\n    protected override string GetDestinationAffinityKey(DestinationState destination)\n    {\n        return destination.DestinationId;\n    }\n\n    protected override (string? Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config)\n    {\n        var customHeaderName = config.AffinityKeyName;\n        var keyHeaderValues = context.Request.Headers[customHeaderName];\n\n        if (StringValues.IsNullOrEmpty(keyHeaderValues))\n        {\n            // It means affinity key is not defined that is a successful case\n            return (Key: null, ExtractedSuccessfully: true);\n        }\n\n        if (keyHeaderValues.Count > 1)\n        {\n            // Multiple values is an ambiguous case which is considered a key extraction failure\n            Log.RequestAffinityHeaderHasMultipleValues(Logger, customHeaderName, keyHeaderValues.Count);\n            return (Key: null, ExtractedSuccessfully: false);\n        }\n\n        return Unprotect(keyHeaderValues[0]);\n    }\n\n    protected override void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config, string unencryptedKey)\n    {\n        context.Response.Headers.Append(config.AffinityKeyName, Protect(unencryptedKey));\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, string, int, Exception?> _requestAffinityHeaderHasMultipleValues = LoggerMessage.Define<string, int>(\n            LogLevel.Error,\n            EventIds.RequestAffinityHeaderHasMultipleValues,\n            \"The request affinity header `{headerName}` has `{valueCount}` values.\");\n\n        public static void RequestAffinityHeaderHasMultipleValues(ILogger logger, string headerName, int valueCount)\n        {\n            _requestAffinityHeaderHasMultipleValues(logger, headerName, valueCount, null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/HashCookieSessionAffinityPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.IO.Hashing;\nusing System.Runtime.CompilerServices;\nusing System.Text;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\ninternal sealed class HashCookieSessionAffinityPolicy : BaseHashCookieSessionAffinityPolicy\n{\n    private readonly ConditionalWeakTable<DestinationState, string> _hashes = new();\n\n    public HashCookieSessionAffinityPolicy(\n        TimeProvider timeProvider,\n        ILogger<HashCookieSessionAffinityPolicy> logger)\n        : base(timeProvider, logger) { }\n\n    public override string Name => SessionAffinityConstants.Policies.HashCookie;\n\n    protected override string GetDestinationHash(DestinationState d)\n    {\n        return _hashes.GetValue(d, static d =>\n        {\n            // Stable format across instances\n            var destinationIdBytes = Encoding.Unicode.GetBytes(d.DestinationId.ToUpperInvariant());\n            var hashBytes = XxHash64.Hash(destinationIdBytes);\n            return Convert.ToHexString(hashBytes).ToLowerInvariant();\n        });\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/IAffinityFailurePolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\n/// <summary>\n/// Affinity failures handling policy.\n/// </summary>\npublic interface IAffinityFailurePolicy\n{\n    /// <summary>\n    ///  A unique identifier for this failure policy. This will be referenced from config.\n    /// </summary>\n    string Name { get; }\n\n    /// <summary>\n    /// Handles affinity failures. This method assumes the full control on <see cref=\"HttpContext\"/>\n    /// and can change it in any way.\n    /// </summary>\n    /// <param name=\"context\">Current request's context.</param>\n    /// <param name=\"cluster\">The associated cluster for the request.</param>\n    /// <param name=\"affinityStatus\">Affinity resolution status.</param>\n    /// <returns>\n    /// 'true' if the failure is considered recoverable and the request processing can proceed.\n    /// Otherwise, 'false' indicating that an error response has been generated and the request's processing must be terminated.\n    /// </returns>\n    Task<bool> Handle(HttpContext context, ClusterState cluster, AffinityStatus affinityStatus);\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/ISessionAffinityPolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\n/// <summary>\n/// Provides session affinity for load-balanced clusters.\n/// </summary>\npublic interface ISessionAffinityPolicy\n{\n    /// <summary>\n    ///  A unique identifier for this session affinity implementation. This will be referenced from config.\n    /// </summary>\n    string Name { get; }\n\n    /// <summary>\n    /// Finds <see cref=\"DestinationState\"/> to which the current request is affinitized by the affinity key.\n    /// </summary>\n    /// <param name=\"context\">Current request's context.</param>\n    /// <param name=\"cluster\">Current request's cluster.</param>\n    /// <param name=\"config\">Affinity config.</param>\n    /// <param name=\"destinations\"><see cref=\"DestinationState\"/>s available for the request.</param>\n    /// <returns><see cref=\"AffinityResult\"/> carrying the found affinitized destinations if any and the <see cref=\"AffinityStatus\"/>.</returns>\n    AffinityResult FindAffinitizedDestinations(HttpContext context, ClusterState cluster, SessionAffinityConfig config, IReadOnlyList<DestinationState> destinations);\n\n    /// <summary>\n    /// Finds <see cref=\"DestinationState\"/> to which the current request is affinitized by the affinity key.\n    /// </summary>\n    /// <param name=\"context\">Current request's context.</param>\n    /// <param name=\"cluster\">Current request's cluster.</param>\n    /// <param name=\"config\">Affinity config.</param>\n    /// <param name=\"destinations\"><see cref=\"DestinationState\"/>s available for the request.</param>\n    /// <param name=\"cancellationToken\">The token to monitor for cancellation requests.</param>\n    /// <returns><see cref=\"AffinityResult\"/> carrying the found affinitized destinations if any and the <see cref=\"AffinityStatus\"/>.</returns>\n    ValueTask<AffinityResult> FindAffinitizedDestinationsAsync(HttpContext context, ClusterState cluster, SessionAffinityConfig config, IReadOnlyList<DestinationState> destinations, CancellationToken cancellationToken)\n    {\n        cancellationToken.ThrowIfCancellationRequested();\n\n        return new ValueTask<AffinityResult>(FindAffinitizedDestinations(context, cluster, config, destinations));\n    }\n\n    /// <summary>\n    /// Affinitize the current response to the given <see cref=\"DestinationState\"/> by setting the affinity key extracted from <see cref=\"DestinationState\"/>.\n    /// </summary>\n    /// <param name=\"context\">Current request's context.</param>\n    /// <param name=\"cluster\">Current request's cluster.</param>\n    /// <param name=\"config\">Affinity config.</param>\n    /// <param name=\"destination\"><see cref=\"DestinationState\"/> to which request is to be affinitized.</param>\n    void AffinitizeResponse(HttpContext context, ClusterState cluster, SessionAffinityConfig config, DestinationState destination);\n\n    /// <summary>\n    /// Affinitize the current response to the given <see cref=\"DestinationState\"/> by setting the affinity key extracted from <see cref=\"DestinationState\"/>.\n    /// </summary>\n    /// <param name=\"context\">Current request's context.</param>\n    /// <param name=\"cluster\">Current request's cluster.</param>\n    /// <param name=\"config\">Affinity config.</param>\n    /// <param name=\"destination\"><see cref=\"DestinationState\"/> to which request is to be affinitized.</param>\n    /// <param name=\"cancellationToken\">The token to monitor for cancellation requests.</param>\n    ValueTask AffinitizeResponseAsync(HttpContext context, ClusterState cluster, SessionAffinityConfig config, DestinationState destination, CancellationToken cancellationToken)\n    {\n        AffinitizeResponse(context, cluster, config, destination);\n        return default;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/Log.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\ninternal static class Log\n{\n    private static readonly Action<ILogger, string, Exception?> _affinityCannotBeEstablishedBecauseNoDestinationsFound = LoggerMessage.Define<string>(\n        LogLevel.Warning,\n        EventIds.AffinityCannotBeEstablishedBecauseNoDestinationsFoundOnCluster,\n        \"The request affinity cannot be established because no destinations are found on cluster `{clusterId}`.\");\n\n    private static readonly Action<ILogger, Exception?> _requestAffinityKeyDecryptionFailed = LoggerMessage.Define(\n        LogLevel.Error,\n        EventIds.RequestAffinityKeyDecryptionFailed,\n        \"The request affinity key decryption failed.\");\n\n    private static readonly Action<ILogger, string, Exception?> _destinationMatchingToAffinityKeyNotFound = LoggerMessage.Define<string>(\n        LogLevel.Warning,\n        EventIds.DestinationMatchingToAffinityKeyNotFound,\n        \"Destination matching to the request affinity key is not found on cluster `{clusterId}`. Configured failure policy will be applied.\");\n\n    public static void AffinityCannotBeEstablishedBecauseNoDestinationsFound(ILogger logger, string clusterId)\n    {\n        _affinityCannotBeEstablishedBecauseNoDestinationsFound(logger, clusterId, null);\n    }\n\n    public static void RequestAffinityKeyDecryptionFailed(ILogger logger, Exception? ex)\n    {\n        _requestAffinityKeyDecryptionFailed(logger, ex);\n    }\n\n    public static void DestinationMatchingToAffinityKeyNotFound(ILogger logger, string clusterId)\n    {\n        _destinationMatchingToAffinityKeyNotFound(logger, clusterId, null);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/RedistributeAffinityFailurePolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\ninternal sealed class RedistributeAffinityFailurePolicy : IAffinityFailurePolicy\n{\n    public string Name => SessionAffinityConstants.FailurePolicies.Redistribute;\n\n    public Task<bool> Handle(HttpContext context, ClusterState cluster, AffinityStatus affinityStatus)\n    {\n        if (affinityStatus == AffinityStatus.OK\n            || affinityStatus == AffinityStatus.AffinityKeyNotSet)\n        {\n            throw new InvalidOperationException($\"{nameof(RedistributeAffinityFailurePolicy)} is called to handle a successful request's affinity status {affinityStatus}.\");\n        }\n\n        // Available destinations list have not been changed in the context,\n        // so simply allow processing to proceed to load balancing.\n        return TaskUtilities.TrueTask;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/Return503ErrorAffinityFailurePolicy.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\ninternal sealed class Return503ErrorAffinityFailurePolicy : IAffinityFailurePolicy\n{\n    public string Name => SessionAffinityConstants.FailurePolicies.Return503Error;\n\n    public Task<bool> Handle(HttpContext context, ClusterState cluster, AffinityStatus affinityStatus)\n    {\n        if (affinityStatus == AffinityStatus.OK\n            || affinityStatus == AffinityStatus.AffinityKeyNotSet)\n        {\n            throw new InvalidOperationException($\"{nameof(Return503ErrorAffinityFailurePolicy)} is called to handle a successful request's affinity status {affinityStatus}.\");\n        }\n\n        context.Response.StatusCode = 503;\n        return TaskUtilities.FalseTask;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/SessionAffinityConstants.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\n/// <summary>\n/// Names of built-in session affinity services.\n/// </summary>\npublic static class SessionAffinityConstants\n{\n    public static class Policies\n    {\n        public static string Cookie => nameof(Cookie);\n\n        public static string HashCookie => nameof(HashCookie);\n\n        public static string ArrCookie => nameof(ArrCookie);\n\n        public static string CustomHeader => nameof(CustomHeader);\n    }\n\n    public static class FailurePolicies\n    {\n        public static string Redistribute => nameof(Redistribute);\n\n        public static string Return503Error => nameof(Return503Error);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/SessionAffinity/SessionAffinityMiddleware.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.SessionAffinity;\n\n/// <summary>\n/// Looks up an affinitized <see cref=\"DestinationState\"/> matching the request's affinity key if any is set\n/// </summary>\ninternal sealed class SessionAffinityMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly FrozenDictionary<string, ISessionAffinityPolicy> _sessionAffinityPolicies;\n    private readonly FrozenDictionary<string, IAffinityFailurePolicy> _affinityFailurePolicies;\n    private readonly ILogger _logger;\n\n    public SessionAffinityMiddleware(\n        RequestDelegate next,\n        IEnumerable<ISessionAffinityPolicy> sessionAffinityPolicies,\n        IEnumerable<IAffinityFailurePolicy> affinityFailurePolicies,\n        ILogger<SessionAffinityMiddleware> logger)\n    {\n        ArgumentNullException.ThrowIfNull(next);\n        ArgumentNullException.ThrowIfNull(logger);\n        ArgumentNullException.ThrowIfNull(sessionAffinityPolicies);\n        ArgumentNullException.ThrowIfNull(affinityFailurePolicies);\n\n        _next = next;\n        _logger = logger;\n        _sessionAffinityPolicies = sessionAffinityPolicies.ToDictionaryByUniqueId(p => p.Name);\n        _affinityFailurePolicies = affinityFailurePolicies.ToDictionaryByUniqueId(p => p.Name);\n    }\n\n    public Task Invoke(HttpContext context)\n    {\n        var proxyFeature = context.GetReverseProxyFeature();\n\n        var config = proxyFeature.Cluster.Config.SessionAffinity;\n\n        if (config is null || !config.Enabled.GetValueOrDefault())\n        {\n            return _next(context);\n        }\n\n        return InvokeInternal(context, proxyFeature, config);\n    }\n\n    private async Task InvokeInternal(HttpContext context, IReverseProxyFeature proxyFeature, SessionAffinityConfig config)\n    {\n        var destinations = proxyFeature.AvailableDestinations;\n        var cluster = proxyFeature.Route.Cluster!;\n\n        var policy = _sessionAffinityPolicies.GetRequiredServiceById(config.Policy, SessionAffinityConstants.Policies.HashCookie);\n        var affinityResult = await policy.FindAffinitizedDestinationsAsync(context, cluster, config, destinations, context.RequestAborted);\n\n        // Used for Distributed Tracing as part of Open Telemetry, null if there are no listeners\n        var activity = context.GetYarpActivity();\n        activity?.SetTag(\"proxy.session_affinity.policy\", policy.Name);\n\n        switch (affinityResult.Status)\n        {\n            case AffinityStatus.OK:\n                proxyFeature.AvailableDestinations = affinityResult.Destinations!;\n                activity?.SetTag(\"proxy.session_affinity.status\", \"success\");\n                break;\n            case AffinityStatus.AffinityKeyNotSet:\n                // Nothing to do so just continue processing\n                break;\n            case AffinityStatus.AffinityKeyExtractionFailed:\n            case AffinityStatus.DestinationNotFound:\n\n                var failurePolicy = _affinityFailurePolicies.GetRequiredServiceById(config.FailurePolicy, SessionAffinityConstants.FailurePolicies.Redistribute);\n                var keepProcessing = await failurePolicy.Handle(context, proxyFeature.Route.Cluster!, affinityResult.Status);\n\n                if (!keepProcessing)\n                {\n                    // Policy reported the failure is unrecoverable and took the full responsibility for its handling,\n                    // so we simply stop processing.\n                    Log.AffinityResolutionFailedForCluster(_logger, cluster.ClusterId);\n                    activity?.SetTag(\"proxy.session_affinity.status\", \"failed\");\n                    return;\n                }\n\n                Log.AffinityResolutionFailureWasHandledProcessingWillBeContinued(_logger, cluster.ClusterId, failurePolicy.Name);\n                activity?.SetTag(\"proxy.session_affinity.status\", \"recovered\");\n\n                break;\n            default:\n                throw new NotSupportedException($\"Affinity status '{affinityResult.Status}' is not supported.\");\n        }\n\n        await _next(context);\n    }\n\n    private static class Log\n    {\n        private static readonly Action<ILogger, string, Exception?> _affinityResolutionFailedForCluster = LoggerMessage.Define<string>(\n            LogLevel.Warning,\n            EventIds.AffinityResolutionFailedForCluster,\n            \"Affinity resolution failed for cluster '{clusterId}'.\");\n\n        private static readonly Action<ILogger, string, string, Exception?> _affinityResolutionFailureWasHandledProcessingWillBeContinued = LoggerMessage.Define<string, string>(\n            LogLevel.Debug,\n            EventIds.AffinityResolutionFailureWasHandledProcessingWillBeContinued,\n            \"Affinity resolution failure for cluster '{clusterId}' was handled successfully by the policy '{policyName}'. Request processing will be continued.\");\n\n        public static void AffinityResolutionFailedForCluster(ILogger logger, string clusterId)\n        {\n            _affinityResolutionFailedForCluster(logger, clusterId, null);\n        }\n\n        public static void AffinityResolutionFailureWasHandledProcessingWillBeContinued(ILogger logger, string clusterId, string policyName)\n        {\n            _affinityResolutionFailureWasHandledProcessingWillBeContinued(logger, clusterId, policyName, null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/Builder/ActionTransformProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Transforms.Builder;\n\ninternal sealed class ActionTransformProvider : ITransformProvider\n{\n    private readonly Action<TransformBuilderContext> _action;\n\n    public ActionTransformProvider(Action<TransformBuilderContext> action)\n    {\n        ArgumentNullException.ThrowIfNull(action);\n        _action = action;\n    }\n\n    public void Apply(TransformBuilderContext transformBuildContext)\n    {\n        _action(transformBuildContext);\n    }\n\n    public void ValidateRoute(TransformRouteValidationContext context)\n    {\n    }\n\n    public void ValidateCluster(TransformClusterValidationContext context)\n    {\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/Builder/ITransformBuilder.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Transforms.Builder;\n\n/// <summary>\n/// Validates and builds request and response transforms for a given route.\n/// </summary>\npublic interface ITransformBuilder\n{\n    /// <summary>\n    /// Validates that each transform for the given route is known and has the expected parameters. All transforms are validated\n    /// so all errors can be reported.\n    /// </summary>\n    IReadOnlyList<Exception> ValidateRoute(RouteConfig route);\n\n    /// <summary>\n    /// Validates that any cluster data needed for transforms is valid.\n    /// </summary>\n    IReadOnlyList<Exception> ValidateCluster(ClusterConfig cluster);\n\n    /// <summary>\n    /// Builds the transforms for the given route into executable rules.\n    /// </summary>\n    HttpTransformer Build(RouteConfig route, ClusterConfig? cluster);\n\n    HttpTransformer Create(Action<TransformBuilderContext> action);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/Builder/ITransformFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\n\nnamespace Yarp.ReverseProxy.Transforms.Builder;\n\n/// <summary>\n/// Validates and builds transforms from the given parameters\n/// </summary>\npublic interface ITransformFactory\n{\n    /// <summary>\n    /// Checks if the given transform values match a known transform, and if those values have any errors.\n    /// </summary>\n    /// <param name=\"context\">The context to add any generated errors to.</param>\n    /// <param name=\"transformValues\">The transform values to validate.</param>\n    /// <returns>True if this factory matches the given transform, otherwise false.</returns>\n    bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues);\n\n    /// <summary>\n    /// Checks if the given transform values match a known transform, and if so, generates a transform and\n    /// adds it to the context. This can throw if the transform values are invalid.\n    /// </summary>\n    /// <param name=\"context\">The context to add any generated transforms to.</param>\n    /// <param name=\"transformValues\">The transform values to use as input.</param>\n    /// <returns>True if this factory matches the given transform, otherwise false.</returns>\n    bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/Builder/ITransformProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Transforms.Builder;\n\n/// <summary>\n/// Enables the implementor to inspect each route and conditionally add transforms.\n/// </summary>\npublic interface ITransformProvider\n{\n    /// <summary>\n    /// Validates any route data needed for transforms.\n    /// </summary>\n    /// <param name=\"context\">The context to add any generated errors to.</param>\n    void ValidateRoute(TransformRouteValidationContext context);\n\n    /// <summary>\n    /// Validates any cluster data needed for transforms.\n    /// </summary>\n    /// <param name=\"context\">The context to add any generated errors to.</param>\n    void ValidateCluster(TransformClusterValidationContext context);\n\n    /// <summary>\n    /// Inspect the given route and conditionally add transforms.\n    /// This is called for every route, each time that route is built.\n    /// </summary>\n    /// <param name=\"context\">The context to add any generated transforms to.</param>\n    void Apply(TransformBuilderContext context);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/Builder/StructuredTransformer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Transforms.Builder;\n\n/// <summary>\n/// Transforms for a given route.\n/// </summary>\ninternal sealed class StructuredTransformer : HttpTransformer\n{\n    /// <summary>\n    /// Creates a new <see cref=\"StructuredTransformer\"/> instance.\n    /// </summary>\n    internal StructuredTransformer(bool? copyRequestHeaders, bool? copyResponseHeaders, bool? copyResponseTrailers,\n        IList<RequestTransform> requestTransforms,\n        IList<ResponseTransform> responseTransforms,\n        IList<ResponseTrailersTransform> responseTrailerTransforms)\n    {\n        ArgumentNullException.ThrowIfNull(requestTransforms);\n        ArgumentNullException.ThrowIfNull(responseTransforms);\n        ArgumentNullException.ThrowIfNull(responseTrailerTransforms);\n\n        ShouldCopyRequestHeaders = copyRequestHeaders;\n        ShouldCopyResponseHeaders = copyResponseHeaders;\n        ShouldCopyResponseTrailers = copyResponseTrailers;\n        RequestTransforms = requestTransforms.ToArray();\n        ResponseTransforms = responseTransforms.ToArray();\n        ResponseTrailerTransforms = responseTrailerTransforms.ToArray();\n    }\n\n    /// <summary>\n    /// Indicates if all request headers should be copied to the proxy request before applying transforms.\n    /// </summary>\n    internal bool? ShouldCopyRequestHeaders { get; }\n\n    /// <summary>\n    /// Indicates if all response headers should be copied to the client response before applying transforms.\n    /// </summary>\n    internal bool? ShouldCopyResponseHeaders { get; }\n\n    /// <summary>\n    /// Indicates if all response trailers should be copied to the client response before applying transforms.\n    /// </summary>\n    internal bool? ShouldCopyResponseTrailers { get; }\n\n    /// <summary>\n    /// Request transforms.\n    /// </summary>\n    internal RequestTransform[] RequestTransforms { get; }\n\n    /// <summary>\n    /// Response header transforms.\n    /// </summary>\n    internal ResponseTransform[] ResponseTransforms { get; }\n\n    /// <summary>\n    /// Response trailer transforms.\n    /// </summary>\n    internal ResponseTrailersTransform[] ResponseTrailerTransforms { get; }\n\n#pragma warning disable CS0672 // We're overriding the obsolete overloads to preserve backwards compatibility.\n    public override ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix) =>\n        TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, CancellationToken.None);\n\n    public override ValueTask<bool> TransformResponseAsync(HttpContext httpContext, HttpResponseMessage? proxyResponse) =>\n        TransformResponseAsync(httpContext, proxyResponse, CancellationToken.None);\n\n    public override ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse) =>\n        TransformResponseTrailersAsync(httpContext, proxyResponse, CancellationToken.None);\n#pragma warning restore\n\n    public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken)\n    {\n        if (ShouldCopyRequestHeaders.GetValueOrDefault(true))\n        {\n            // We own the base implementation and know it doesn't make use of the cancellation token.\n            // We're intentionally calling the overload without it to avoid it calling back into this derived implementation, causing a stack overflow.\n\n#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods\n#pragma warning disable CS0618 // Type or member is obsolete\n            await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix);\n#pragma warning restore CS0618\n#pragma warning restore CA2016\n        }\n\n        if (RequestTransforms.Length == 0)\n        {\n            return;\n        }\n\n        var transformContext = new RequestTransformContext()\n        {\n            DestinationPrefix = destinationPrefix,\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            Path = httpContext.Request.Path,\n            HeadersCopied = ShouldCopyRequestHeaders.GetValueOrDefault(true),\n            CancellationToken = cancellationToken,\n        };\n\n        foreach (var requestTransform in RequestTransforms)\n        {\n            await requestTransform.ApplyAsync(transformContext);\n\n            // The transform generated a response, do not apply further transforms and do not forward.\n            if (RequestUtilities.IsResponseSet(httpContext.Response))\n            {\n                return;\n            }\n        }\n\n        // Allow a transform to directly set a custom RequestUri.\n        if (proxyRequest.RequestUri is null)\n        {\n            var queryString = transformContext.MaybeQuery?.QueryString ?? httpContext.Request.QueryString;\n\n            proxyRequest.RequestUri = RequestUtilities.MakeDestinationAddress(\n                transformContext.DestinationPrefix, transformContext.Path, queryString);\n        }\n    }\n\n    public override async ValueTask<bool> TransformResponseAsync(HttpContext httpContext, HttpResponseMessage? proxyResponse, CancellationToken cancellationToken)\n    {\n        if (ShouldCopyResponseHeaders.GetValueOrDefault(true))\n        {\n            // We own the base implementation and know it doesn't make use of the cancellation token.\n            // We're intentionally calling the overload without it to avoid it calling back into this derived implementation, causing a stack overflow.\n\n#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods\n#pragma warning disable CS0618 // Type or member is obsolete\n            await base.TransformResponseAsync(httpContext, proxyResponse);\n#pragma warning restore CS0618\n#pragma warning restore CA2016\n        }\n\n        if (ResponseTransforms.Length == 0)\n        {\n            return true;\n        }\n\n        var transformContext = new ResponseTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = ShouldCopyResponseHeaders.GetValueOrDefault(true),\n            CancellationToken = cancellationToken,\n        };\n\n        foreach (var responseTransform in ResponseTransforms)\n        {\n            await responseTransform.ApplyAsync(transformContext);\n        }\n\n        return !transformContext.SuppressResponseBody;\n    }\n\n    public override async ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken)\n    {\n        if (ShouldCopyResponseTrailers.GetValueOrDefault(true))\n        {\n            // We own the base implementation and know it doesn't make use of the cancellation token.\n            // We're intentionally calling the overload without it to avoid it calling back into this derived implementation, causing a stack overflow.\n\n#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods\n#pragma warning disable CS0618 // Type or member is obsolete\n            await base.TransformResponseTrailersAsync(httpContext, proxyResponse);\n#pragma warning restore CS0618\n#pragma warning restore CA2016\n        }\n\n        if (ResponseTrailerTransforms.Length == 0)\n        {\n            return;\n        }\n\n        // Only run the transforms if trailers are actually supported by the client response.\n        var responseTrailersFeature = httpContext.Features.Get<IHttpResponseTrailersFeature>();\n        var outgoingTrailers = responseTrailersFeature?.Trailers;\n        if (outgoingTrailers is not null && !outgoingTrailers.IsReadOnly)\n        {\n            var transformContext = new ResponseTrailersTransformContext()\n            {\n                HttpContext = httpContext,\n                ProxyResponse = proxyResponse,\n                HeadersCopied = ShouldCopyResponseTrailers.GetValueOrDefault(true),\n                CancellationToken = cancellationToken,\n            };\n\n            foreach (var responseTrailerTransform in ResponseTrailerTransforms)\n            {\n                await responseTrailerTransform.ApplyAsync(transformContext);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/Builder/TransformBuilder.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Transforms.Builder;\n\n/// <summary>\n/// Validates and builds request and response transforms for a given route.\n/// </summary>\ninternal sealed class TransformBuilder : ITransformBuilder\n{\n    private readonly IServiceProvider _services;\n    private readonly List<ITransformFactory> _factories;\n    private readonly List<ITransformProvider> _providers;\n\n    /// <summary>\n    /// Creates a new <see cref=\"TransformBuilder\"/>\n    /// </summary>\n    public TransformBuilder(IServiceProvider services, IEnumerable<ITransformFactory> factories, IEnumerable<ITransformProvider> providers)\n    {\n        ArgumentNullException.ThrowIfNull(services);\n        ArgumentNullException.ThrowIfNull(factories);\n        ArgumentNullException.ThrowIfNull(providers);\n\n        _services = services;\n        _factories = factories.ToList();\n        _providers = providers.ToList();\n    }\n\n    /// <inheritdoc/>\n    public IReadOnlyList<Exception> ValidateRoute(RouteConfig route)\n    {\n        var context = new TransformRouteValidationContext()\n        {\n            Services = _services,\n            Route = route,\n        };\n\n        var rawTransforms = route?.Transforms;\n        if (rawTransforms?.Count > 0)\n        {\n            foreach (var rawTransform in rawTransforms)\n            {\n                var handled = false;\n                foreach (var factory in _factories)\n                {\n                    if (factory.Validate(context, rawTransform))\n                    {\n                        handled = true;\n                        break;\n                    }\n                }\n\n                if (!handled)\n                {\n                    context.Errors.Add(new ArgumentException($\"Unknown transform: {string.Join(';', rawTransform.Keys)}\"));\n                }\n            }\n        }\n\n        // Let the app add any more validation it wants.\n        foreach (var transformProvider in _providers)\n        {\n            transformProvider.ValidateRoute(context);\n        }\n\n        // We promise not to modify the list after we return it.\n        return (IReadOnlyList<Exception>)context.Errors;\n    }\n\n    /// <inheritdoc/>\n    public IReadOnlyList<Exception> ValidateCluster(ClusterConfig cluster)\n    {\n        var context = new TransformClusterValidationContext()\n        {\n            Services = _services,\n            Cluster = cluster,\n        };\n\n        // Let the app add any more validation it wants.\n        foreach (var transformProvider in _providers)\n        {\n            transformProvider.ValidateCluster(context);\n        }\n\n        // We promise not to modify the list after we return it.\n        return (IReadOnlyList<Exception>)context.Errors;\n    }\n\n    /// <inheritdoc/>\n    public HttpTransformer Build(RouteConfig route, ClusterConfig? cluster)\n    {\n        return BuildInternal(route, cluster);\n    }\n\n    // This is separate from Build for testing purposes.\n    internal StructuredTransformer BuildInternal(RouteConfig route, ClusterConfig? cluster)\n    {\n        var rawTransforms = route.Transforms;\n\n        var context = new TransformBuilderContext\n        {\n            Services = _services,\n            Route = route,\n            Cluster = cluster,\n        };\n\n        if (rawTransforms?.Count > 0)\n        {\n            foreach (var rawTransform in rawTransforms)\n            {\n                var handled = false;\n                foreach (var factory in _factories)\n                {\n                    if (factory.Build(context, rawTransform))\n                    {\n                        handled = true;\n                        break;\n                    }\n                }\n\n                if (!handled)\n                {\n                    throw new ArgumentException($\"Unknown transform: {string.Join(';', rawTransform.Keys)}\");\n                }\n            }\n        }\n\n        // Let the app add any more transforms it wants.\n        foreach (var transformProvider in _providers)\n        {\n            transformProvider.Apply(context);\n        }\n\n        return CreateTransformer(context);\n    }\n\n    public HttpTransformer Create(Action<TransformBuilderContext> action)\n    {\n        return CreateInternal(action);\n    }\n\n    internal StructuredTransformer CreateInternal(Action<TransformBuilderContext> action)\n    {\n        var context = new TransformBuilderContext\n        {\n            Services = _services,\n        };\n\n        action(context);\n\n        return CreateTransformer(context);\n    }\n\n    internal static StructuredTransformer CreateTransformer(TransformBuilderContext context)\n    {\n        // RequestHeaderOriginalHostKey defaults to false, and CopyRequestHeaders defaults to true.\n        // If RequestHeaderOriginalHostKey was not specified then we need to make sure the transform gets\n        // added anyway to remove the original host and to observe hosts specified in DestinationConfig.\n        if (!context.RequestTransforms.Any(item => item is RequestHeaderOriginalHostTransform))\n        {\n            context.AddOriginalHost(false);\n        }\n\n        // Add default forwarders only if they haven't already been added or disabled.\n        if (context.UseDefaultForwarders.GetValueOrDefault(true))\n        {\n            context.AddXForwarded();\n        }\n\n        return new StructuredTransformer(\n            context.CopyRequestHeaders,\n            context.CopyResponseHeaders,\n            context.CopyResponseTrailers,\n            context.RequestTransforms,\n            context.ResponseTransforms,\n            context.ResponseTrailersTransforms);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/Builder/TransformBuilderContext.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Transforms.Builder;\n\n/// <summary>\n/// State used when building transforms for the given route.\n/// </summary>\npublic class TransformBuilderContext\n{\n    /// <summary>\n    /// Application services that can be used to construct transforms.\n    /// </summary>\n    public IServiceProvider Services { get; init; } = default!;\n\n    /// <summary>\n    /// The route these transforms will be associated with.\n    /// </summary>\n    public RouteConfig Route { get; init; } = default!;\n\n    /// <summary>\n    /// The cluster config used by the route.\n    /// This may be null if the route is not currently paired with a cluster.\n    /// </summary>\n    public ClusterConfig? Cluster { get; init; }\n\n    /// <summary>\n    /// Indicates if request headers should all be copied to the proxy request before transforms are applied.\n    /// </summary>\n    public bool? CopyRequestHeaders { get; set; }\n\n    /// <summary>\n    /// Indicates if response headers should all be copied to the client response before transforms are applied.\n    /// </summary>\n    public bool? CopyResponseHeaders { get; set; }\n\n    /// <summary>\n    /// Indicates if response trailers should all be copied to the client response before transforms are applied.\n    /// </summary>\n    public bool? CopyResponseTrailers { get; set; }\n\n    /// <summary>\n    /// Indicates if default x-forwarded-* transforms should be added to this route. Disable this if you do not want\n    /// x-forwarded-* headers or have configured your own.\n    /// </summary>\n    public bool? UseDefaultForwarders { get; set; }\n\n    /// <summary>\n    /// Add request transforms here for the given route.\n    /// </summary>\n    public IList<RequestTransform> RequestTransforms { get; } = new List<RequestTransform>();\n\n    /// <summary>\n    /// Add response transforms here for the given route.\n    /// </summary>\n    public IList<ResponseTransform> ResponseTransforms { get; } = new List<ResponseTransform>();\n\n    /// <summary>\n    /// Add response trailers transforms here for the given route.\n    /// </summary>\n    public IList<ResponseTrailersTransform> ResponseTrailersTransforms { get; } = new List<ResponseTrailersTransform>();\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/Builder/TransformClusterValidationContext.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Transforms.Builder;\n\n/// <summary>\n/// State used when validating transforms for the given cluster.\n/// </summary>\npublic class TransformClusterValidationContext\n{\n    /// <summary>\n    /// Application services that can be used to validate transforms.\n    /// </summary>\n    public IServiceProvider Services { get; init; } = default!;\n\n    /// <summary>\n    /// The cluster configuration that may be used when creating transforms.\n    /// </summary>\n    public ClusterConfig Cluster { get; init; } = default!;\n\n    /// <summary>\n    /// The accumulated list of validation errors for this cluster.\n    /// Add validation errors here.\n    /// </summary>\n    public IList<Exception> Errors { get; } = new List<Exception>();\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/Builder/TransformHelpers.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\n\nnamespace Yarp.ReverseProxy.Transforms.Builder;\n\npublic static class TransformHelpers\n{\n    public static void TryCheckTooManyParameters(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> rawTransform, int expected)\n    {\n        if (rawTransform.Count > expected)\n        {\n            context.Errors.Add(new InvalidOperationException(\"The transform contains more parameters than expected: \" + string.Join(';', rawTransform.Keys)));\n        }\n    }\n\n    public static void CheckTooManyParameters(IReadOnlyDictionary<string, string> rawTransform, int expected)\n    {\n        if (rawTransform.Count > expected)\n        {\n            throw new InvalidOperationException(\"The transform contains more parameters than expected: \" + string.Join(';', rawTransform.Keys));\n        }\n    }\n\n    internal static void RemoveAllXForwardedHeaders(TransformBuilderContext context, string prefix)\n    {\n        context.AddXForwardedFor(prefix + ForwardedTransformFactory.ForKey, ForwardedTransformActions.Remove);\n        context.AddXForwardedPrefix(prefix + ForwardedTransformFactory.PrefixKey, ForwardedTransformActions.Remove);\n        context.AddXForwardedHost(prefix + ForwardedTransformFactory.HostKey, ForwardedTransformActions.Remove);\n        context.AddXForwardedProto(prefix + ForwardedTransformFactory.ProtoKey, ForwardedTransformActions.Remove);\n    }\n\n    internal static void RemoveForwardedHeader(TransformBuilderContext context)\n    {\n        context.RequestTransforms.Add(RequestHeaderForwardedTransform.RemoveTransform);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/Builder/TransformRouteValidationContext.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Transforms.Builder;\n\n/// <summary>\n/// State used when validating transforms for the given route.\n/// </summary>\npublic class TransformRouteValidationContext\n{\n    /// <summary>\n    /// Application services that can be used to validate transforms.\n    /// </summary>\n    public IServiceProvider Services { get; init; } = default!;\n\n    /// <summary>\n    /// The route these transforms are associated with.\n    /// </summary>\n    public RouteConfig Route { get; init; } = default!;\n\n    /// <summary>\n    /// The accumulated list of validation errors for this route.\n    /// Add transform validation errors here.\n    /// </summary>\n    public IList<Exception> Errors { get; } = new List<Exception>();\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ForwardedTransformActions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Transforms;\n\npublic enum ForwardedTransformActions\n{\n    Off = 0,\n    Set,\n    Append,\n    Remove\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ForwardedTransformExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Extensions for adding forwarded header transforms.\n/// </summary>\npublic static class ForwardedTransformExtensions\n{\n    /// <summary>\n    /// Clones the route and adds the transform which will add X-Forwarded-* headers.\n    /// </summary>\n    public static RouteConfig WithTransformXForwarded(\n        this RouteConfig route,\n        string headerPrefix = \"X-Forwarded-\",\n        ForwardedTransformActions xDefault = ForwardedTransformActions.Set,\n        ForwardedTransformActions? xFor = null,\n        ForwardedTransformActions? xHost = null,\n        ForwardedTransformActions? xProto = null,\n        ForwardedTransformActions? xPrefix = null)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[ForwardedTransformFactory.XForwardedKey] = xDefault.ToString();\n\n            if (xFor is not null)\n            {\n                transform[ForwardedTransformFactory.ForKey] = xFor.Value.ToString();\n            }\n\n            if (xPrefix is not null)\n            {\n                transform[ForwardedTransformFactory.PrefixKey] = xPrefix.Value.ToString();\n            }\n\n            if (xHost is not null)\n            {\n                transform[ForwardedTransformFactory.HostKey] = xHost.Value.ToString();\n            }\n\n            if (xProto is not null)\n            {\n                transform[ForwardedTransformFactory.ProtoKey] = xProto.Value.ToString();\n            }\n\n            transform[ForwardedTransformFactory.HeaderPrefixKey] = headerPrefix;\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform which will add X-Forwarded-For request header.\n    /// </summary>\n    public static TransformBuilderContext AddXForwardedFor(this TransformBuilderContext context, string headerName = \"X-Forwarded-For\", ForwardedTransformActions action = ForwardedTransformActions.Set)\n    {\n        context.UseDefaultForwarders = false;\n        if (action == ForwardedTransformActions.Off)\n        {\n            return context;\n        }\n        context.RequestTransforms.Add(new RequestHeaderXForwardedForTransform(headerName, action));\n        return context;\n    }\n\n    /// <summary>\n    /// Adds the transform which will add X-Forwarded-Host request header.\n    /// </summary>\n    public static TransformBuilderContext AddXForwardedHost(this TransformBuilderContext context, string headerName = \"X-Forwarded-Host\", ForwardedTransformActions action = ForwardedTransformActions.Set)\n    {\n        context.UseDefaultForwarders = false;\n        if (action == ForwardedTransformActions.Off)\n        {\n            return context;\n        }\n        context.RequestTransforms.Add(new RequestHeaderXForwardedHostTransform(headerName, action));\n        return context;\n    }\n\n    /// <summary>\n    /// Adds the transform which will add X-Forwarded-Proto request header.\n    /// </summary>\n    public static TransformBuilderContext AddXForwardedProto(this TransformBuilderContext context, string headerName = \"X-Forwarded-Proto\", ForwardedTransformActions action = ForwardedTransformActions.Set)\n    {\n        context.UseDefaultForwarders = false;\n        if (action == ForwardedTransformActions.Off)\n        {\n            return context;\n        }\n        context.RequestTransforms.Add(new RequestHeaderXForwardedProtoTransform(headerName, action));\n        return context;\n    }\n\n    /// <summary>\n    /// Adds the transform which will add X-Forwarded-Prefix request header.\n    /// </summary>\n    public static TransformBuilderContext AddXForwardedPrefix(this TransformBuilderContext context, string headerName = \"X-Forwarded-Prefix\", ForwardedTransformActions action = ForwardedTransformActions.Set)\n    {\n        context.UseDefaultForwarders = false;\n        if (action == ForwardedTransformActions.Off)\n        {\n            return context;\n        }\n        context.RequestTransforms.Add(new RequestHeaderXForwardedPrefixTransform(headerName, action));\n        return context;\n    }\n\n    /// <summary>\n    /// Adds the transform which will add X-Forwarded-* request headers.\n    /// </summary>\n    /// <remarks>\n    /// Also optionally removes the <c>Forwarded</c> header when enabled.\n    /// </remarks>\n    public static TransformBuilderContext AddXForwarded(this TransformBuilderContext context, ForwardedTransformActions action = ForwardedTransformActions.Set, bool removeForwardedHeader = true)\n    {\n        context.AddXForwardedFor(action: action);\n        context.AddXForwardedPrefix(action: action);\n        context.AddXForwardedHost(action: action);\n        context.AddXForwardedProto(action: action);\n\n        if (removeForwardedHeader)\n        {\n            // Remove the Forwarded header when an X-Forwarded transform is enabled\n            TransformHelpers.RemoveForwardedHeader(context);\n        }\n\n        return context;\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will add the Forwarded header as defined by [RFC 7239](https://tools.ietf.org/html/rfc7239).\n    /// </summary>\n    public static RouteConfig WithTransformForwarded(this RouteConfig route, bool useHost = true, bool useProto = true,\n        NodeFormat forFormat = NodeFormat.Random, NodeFormat byFormat = NodeFormat.Random, ForwardedTransformActions action = ForwardedTransformActions.Set)\n    {\n        var headers = new List<string>();\n\n        if (forFormat != NodeFormat.None)\n        {\n            headers.Add(ForwardedTransformFactory.ForKey);\n        }\n\n        if (byFormat != NodeFormat.None)\n        {\n            headers.Add(ForwardedTransformFactory.ByKey);\n        }\n\n        if (useHost)\n        {\n            headers.Add(ForwardedTransformFactory.HostKey);\n        }\n\n        if (useProto)\n        {\n            headers.Add(ForwardedTransformFactory.ProtoKey);\n        }\n\n        return route.WithTransform(transform =>\n        {\n            transform[ForwardedTransformFactory.ForwardedKey] = string.Join(',', headers);\n            transform[ForwardedTransformFactory.ActionKey] = action.ToString();\n\n            if (forFormat != NodeFormat.None)\n            {\n                transform.Add(ForwardedTransformFactory.ForFormatKey, forFormat.ToString());\n            }\n\n            if (byFormat != NodeFormat.None)\n            {\n                transform.Add(ForwardedTransformFactory.ByFormatKey, byFormat.ToString());\n            }\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform which will add the Forwarded header as defined by [RFC 7239](https://tools.ietf.org/html/rfc7239).\n    /// </summary>\n    /// <remarks>\n    /// Also optionally removes the <c>X-Forwarded</c> headers when enabled.\n    /// </remarks>\n    public static TransformBuilderContext AddForwarded(this TransformBuilderContext context,\n        bool useHost = true, bool useProto = true, NodeFormat forFormat = NodeFormat.Random,\n        NodeFormat byFormat = NodeFormat.Random, ForwardedTransformActions action = ForwardedTransformActions.Set, bool removeAllXForwardedHeaders = true)\n    {\n        context.UseDefaultForwarders = false;\n\n        if (action == ForwardedTransformActions.Off)\n        {\n            return context;\n        }\n\n        if (byFormat != NodeFormat.None || forFormat != NodeFormat.None || useHost || useProto)\n        {\n            var random = context.Services.GetRequiredService<IRandomFactory>();\n            context.RequestTransforms.Add(new RequestHeaderForwardedTransform(random,\n                forFormat, byFormat, useHost, useProto, action));\n\n            if (removeAllXForwardedHeaders)\n            {\n                // Remove the X-Forwarded headers when a Forwarded transform is enabled\n                TransformHelpers.RemoveAllXForwardedHeaders(context, ForwardedTransformFactory.DefaultXForwardedPrefix);\n            }\n        }\n        return context;\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will set the given header with the Base64 encoded client certificate.\n    /// </summary>\n    public static RouteConfig WithTransformClientCertHeader(this RouteConfig route, string headerName)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[ForwardedTransformFactory.ClientCertKey] = headerName;\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform which will set the given header with the Base64 encoded client certificate.\n    /// </summary>\n    public static TransformBuilderContext AddClientCertHeader(this TransformBuilderContext context, string headerName)\n    {\n        context.RequestTransforms.Add(new RequestHeaderClientCertTransform(headerName));\n        return context;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ForwardedTransformFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Transforms.Builder;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\ninternal sealed class ForwardedTransformFactory : ITransformFactory\n{\n    internal const string XForwardedKey = \"X-Forwarded\";\n    internal const string DefaultXForwardedPrefix = \"X-Forwarded-\";\n    internal const string ForwardedKey = \"Forwarded\";\n    internal const string ActionKey = \"Action\";\n    internal const string HeaderPrefixKey = \"HeaderPrefix\";\n    internal const string ForKey = \"For\";\n    internal const string ByKey = \"By\";\n    internal const string HostKey = \"Host\";\n    internal const string ProtoKey = \"Proto\";\n    internal const string PrefixKey = \"Prefix\";\n    internal const string ForFormatKey = \"ForFormat\";\n    internal const string ByFormatKey = \"ByFormat\";\n    internal const string ClientCertKey = \"ClientCert\";\n\n    private readonly IRandomFactory _randomFactory;\n\n    public ForwardedTransformFactory(IRandomFactory randomFactory)\n    {\n        ArgumentNullException.ThrowIfNull(randomFactory);\n        _randomFactory = randomFactory;\n    }\n\n    public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(XForwardedKey, out var headerValue))\n        {\n            var xExpected = 1;\n\n            ValidateAction(context, XForwardedKey, headerValue);\n\n            if (transformValues.TryGetValue(HeaderPrefixKey, out _))\n            {\n                xExpected++;\n            }\n\n            if (transformValues.TryGetValue(ForKey, out headerValue))\n            {\n                xExpected++;\n                ValidateAction(context, ForKey, headerValue);\n            }\n\n            if (transformValues.TryGetValue(PrefixKey, out headerValue))\n            {\n                xExpected++;\n                ValidateAction(context, PrefixKey, headerValue);\n            }\n\n            if (transformValues.TryGetValue(HostKey, out headerValue))\n            {\n                xExpected++;\n                ValidateAction(context, HostKey, headerValue);\n            }\n\n            if (transformValues.TryGetValue(ProtoKey, out headerValue))\n            {\n                xExpected++;\n                ValidateAction(context, ProtoKey, headerValue);\n            }\n\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, xExpected);\n        }\n        else if (transformValues.TryGetValue(ForwardedKey, out var forwardedHeader))\n        {\n            var expected = 1;\n\n            if (transformValues.TryGetValue(ActionKey, out headerValue))\n            {\n                expected++;\n                ValidateAction(context, ForwardedKey + \":\" + ActionKey, headerValue);\n            }\n\n            var enumValues = \"Random,RandomAndPort,Unknown,UnknownAndPort,Ip,IpAndPort\";\n            if (transformValues.TryGetValue(ForFormatKey, out var forFormat))\n            {\n                expected++;\n                if (!Enum.TryParse<NodeFormat>(forFormat, ignoreCase: true, out var _))\n                {\n                    context.Errors.Add(new ArgumentException($\"Unexpected value for Forwarded:ForFormat: {forFormat}. Expected: {enumValues}\"));\n                }\n            }\n\n            if (transformValues.TryGetValue(ByFormatKey, out var byFormat))\n            {\n                expected++;\n                if (!Enum.TryParse<NodeFormat>(byFormat, ignoreCase: true, out var _))\n                {\n                    context.Errors.Add(new ArgumentException($\"Unexpected value for Forwarded:ByFormat: {byFormat}. Expected: {enumValues}\"));\n                }\n            }\n\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected);\n\n            // for, host, proto, by\n            var tokens = forwardedHeader.Split([',', ' '], StringSplitOptions.RemoveEmptyEntries);\n\n            foreach (var token in tokens)\n            {\n                if (!string.Equals(token, ByKey, StringComparison.OrdinalIgnoreCase)\n                    && !string.Equals(token, HostKey, StringComparison.OrdinalIgnoreCase)\n                    && !string.Equals(token, ProtoKey, StringComparison.OrdinalIgnoreCase)\n                    && !string.Equals(token, ForKey, StringComparison.OrdinalIgnoreCase))\n                {\n                    context.Errors.Add(new ArgumentException($\"Unexpected value for X-Forwarded: {token}. Expected 'for', 'host', 'proto', or 'by'\"));\n                }\n            }\n        }\n        else if (transformValues.TryGetValue(ClientCertKey, out _))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n        }\n        else\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(XForwardedKey, out var headerValue))\n        {\n            var xExpected = 1;\n\n            var defaultXAction = Enum.Parse<ForwardedTransformActions>(headerValue);\n\n            var prefix = DefaultXForwardedPrefix;\n            if (transformValues.TryGetValue(HeaderPrefixKey, out var prefixValue))\n            {\n                xExpected++;\n                prefix = prefixValue;\n            }\n\n            var xForAction = defaultXAction;\n            if (transformValues.TryGetValue(ForKey, out headerValue))\n            {\n                xExpected++;\n                xForAction = Enum.Parse<ForwardedTransformActions>(headerValue);\n            }\n\n            var xPrefixAction = defaultXAction;\n            if (transformValues.TryGetValue(PrefixKey, out headerValue))\n            {\n                xExpected++;\n                xPrefixAction = Enum.Parse<ForwardedTransformActions>(headerValue);\n            }\n\n            var xHostAction = defaultXAction;\n            if (transformValues.TryGetValue(HostKey, out headerValue))\n            {\n                xExpected++;\n                xHostAction = Enum.Parse<ForwardedTransformActions>(headerValue);\n            }\n\n            var xProtoAction = defaultXAction;\n            if (transformValues.TryGetValue(ProtoKey, out headerValue))\n            {\n                xExpected++;\n                xProtoAction = Enum.Parse<ForwardedTransformActions>(headerValue);\n            }\n\n            TransformHelpers.CheckTooManyParameters(transformValues, xExpected);\n\n            context.AddXForwardedFor(prefix + ForKey, xForAction);\n            context.AddXForwardedPrefix(prefix + PrefixKey, xPrefixAction);\n            context.AddXForwardedHost(prefix + HostKey, xHostAction);\n            context.AddXForwardedProto(prefix + ProtoKey, xProtoAction);\n\n            if (xForAction != ForwardedTransformActions.Off || xPrefixAction != ForwardedTransformActions.Off\n                || xHostAction != ForwardedTransformActions.Off || xProtoAction != ForwardedTransformActions.Off)\n            {\n                // Remove the Forwarded header when an X-Forwarded transform is enabled\n                TransformHelpers.RemoveForwardedHeader(context);\n            }\n        }\n        else if (transformValues.TryGetValue(ForwardedKey, out var forwardedHeader))\n        {\n            var useHost = false;\n            var useProto = false;\n            var useFor = false;\n            var useBy = false;\n            var forFormat = NodeFormat.None;\n            var byFormat = NodeFormat.None;\n\n            // for, host, proto, Prefix\n            var tokens = forwardedHeader.Split([',', ' '], StringSplitOptions.RemoveEmptyEntries);\n\n            foreach (var token in tokens)\n            {\n                if (string.Equals(token, ForKey, StringComparison.OrdinalIgnoreCase))\n                {\n                    useFor = true;\n                    forFormat = NodeFormat.Random; // RFC Default\n                }\n                else if (string.Equals(token, ByKey, StringComparison.OrdinalIgnoreCase))\n                {\n                    useBy = true;\n                    byFormat = NodeFormat.Random; // RFC Default\n                }\n                else if (string.Equals(token, HostKey, StringComparison.OrdinalIgnoreCase))\n                {\n                    useHost = true;\n                }\n                else if (string.Equals(token, ProtoKey, StringComparison.OrdinalIgnoreCase))\n                {\n                    useProto = true;\n                }\n                else\n                {\n                    throw new ArgumentException($\"Unexpected value for Forwarded: {token}. Expected 'for', 'host', 'proto', or 'by'\");\n                }\n            }\n\n            var expected = 1;\n\n            var headerAction = ForwardedTransformActions.Set;\n            if (transformValues.TryGetValue(ActionKey, out headerValue))\n            {\n                expected++;\n                headerAction = Enum.Parse<ForwardedTransformActions>(headerValue);\n            }\n\n            if (useFor && transformValues.TryGetValue(ForFormatKey, out var forFormatString))\n            {\n                expected++;\n                forFormat = Enum.Parse<NodeFormat>(forFormatString, ignoreCase: true);\n            }\n\n            if (useBy && transformValues.TryGetValue(ByFormatKey, out var byFormatString))\n            {\n                expected++;\n                byFormat = Enum.Parse<NodeFormat>(byFormatString, ignoreCase: true);\n            }\n\n            TransformHelpers.CheckTooManyParameters(transformValues, expected);\n\n            context.UseDefaultForwarders = false;\n            if (headerAction != ForwardedTransformActions.Off && (useBy || useFor || useHost || useProto))\n            {\n                // Not using the extension to avoid resolving the random factory each time.\n                context.RequestTransforms.Add(new RequestHeaderForwardedTransform(_randomFactory, forFormat, byFormat, useHost, useProto, headerAction));\n\n                // Remove the X-Forwarded headers when a Forwarded transform is enabled\n                TransformHelpers.RemoveAllXForwardedHeaders(context, DefaultXForwardedPrefix);\n            }\n        }\n        else if (transformValues.TryGetValue(ClientCertKey, out var clientCertHeader))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            context.AddClientCertHeader(clientCertHeader);\n        }\n        else\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    private static void ValidateAction(TransformRouteValidationContext context, string key, string? headerValue)\n    {\n        if (!Enum.TryParse<ForwardedTransformActions>(headerValue, out var _))\n        {\n            context.Errors.Add(new ArgumentException($\"Unexpected value for {key}: {headerValue}. Expected one of {nameof(ForwardedTransformActions)}\"));\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/HttpMethodChangeTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Replaces the HTTP method if it matches.\n/// </summary>\npublic class HttpMethodChangeTransform : RequestTransform\n{\n    /// <summary>\n    /// Creates a new transform.\n    /// </summary>\n    /// <param name=\"fromMethod\">The method to match.</param>\n    /// <param name=\"toMethod\">The method to it change to.</param>\n    public HttpMethodChangeTransform(string fromMethod, string toMethod)\n    {\n        if (string.IsNullOrEmpty(fromMethod))\n        {\n            throw new ArgumentException($\"'{nameof(fromMethod)}' cannot be null or empty.\", nameof(fromMethod));\n        }\n\n        if (string.IsNullOrEmpty(toMethod))\n        {\n            throw new ArgumentException($\"'{nameof(toMethod)}' cannot be null or empty.\", nameof(toMethod));\n        }\n\n        FromMethod = GetCanonicalizedValue(fromMethod);\n        ToMethod = GetCanonicalizedValue(toMethod);\n    }\n\n    internal HttpMethod FromMethod { get; }\n\n    internal HttpMethod ToMethod { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        if (FromMethod.Equals(context.ProxyRequest.Method))\n        {\n            context.ProxyRequest.Method = ToMethod;\n        }\n\n        return default;\n    }\n\n    private static HttpMethod GetCanonicalizedValue(string method)\n    {\n        return method switch\n        {\n            string _ when HttpMethods.IsGet(method) => HttpMethod.Get,\n            string _ when HttpMethods.IsPost(method) => HttpMethod.Post,\n            string _ when HttpMethods.IsPut(method) => HttpMethod.Put,\n            string _ when HttpMethods.IsDelete(method) => HttpMethod.Delete,\n            string _ when HttpMethods.IsOptions(method) => HttpMethod.Options,\n            string _ when HttpMethods.IsHead(method) => HttpMethod.Head,\n            string _ when HttpMethods.IsPatch(method) => HttpMethod.Patch,\n            string _ when HttpMethods.IsTrace(method) => HttpMethod.Trace,\n            string _ => new HttpMethod(method),\n        };\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/HttpMethodTransformExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Extensions for modifying the request method.\n/// </summary>\npublic static class HttpMethodTransformExtensions\n{\n    /// <summary>\n    /// Clones the route and adds the transform that will replace the HTTP method if it matches.\n    /// </summary>\n    public static RouteConfig WithTransformHttpMethodChange(this RouteConfig route, string fromHttpMethod, string toHttpMethod)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[HttpMethodTransformFactory.HttpMethodChangeKey] = fromHttpMethod;\n            transform[HttpMethodTransformFactory.SetKey] = toHttpMethod;\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform that will replace the HTTP method if it matches.\n    /// </summary>\n    public static TransformBuilderContext AddHttpMethodChange(this TransformBuilderContext context, string fromHttpMethod, string toHttpMethod)\n    {\n        context.RequestTransforms.Add(new HttpMethodChangeTransform(fromHttpMethod, toHttpMethod));\n        return context;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/HttpMethodTransformFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\ninternal sealed class HttpMethodTransformFactory : ITransformFactory\n{\n    internal const string HttpMethodChangeKey = \"HttpMethodChange\";\n    internal const string SetKey = \"Set\";\n\n    public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(HttpMethodChangeKey, out var _))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 2);\n            if (!transformValues.TryGetValue(SetKey, out var _))\n            {\n                context.Errors.Add(new ArgumentException($\"Unexpected parameters for HttpMethod: {string.Join(';', transformValues.Keys)}. Expected 'Set'\"));\n            }\n        }\n        else\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(HttpMethodChangeKey, out var fromHttpMethod))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 2);\n            if (transformValues.TryGetValue(SetKey, out var toHttpMethod))\n            {\n                context.AddHttpMethodChange(fromHttpMethod, toHttpMethod);\n            }\n        }\n        else\n        {\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/NodeFormat.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// For use with <see cref=\"RequestHeaderForwardedTransform\"/>.\n/// </summary>\npublic enum NodeFormat\n{\n    None,\n    Random,\n    RandomAndPort,\n    RandomAndRandomPort,\n    Unknown,\n    UnknownAndPort,\n    UnknownAndRandomPort,\n    Ip,\n    IpAndPort,\n    IpAndRandomPort,\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/PathRouteValuesTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.AspNetCore.Routing.Patterns;\nusing Microsoft.AspNetCore.Routing.Template;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Generates a new request path by plugging matched route parameters into the given pattern.\n/// </summary>\npublic class PathRouteValuesTransform : RequestTransform\n{\n    private readonly TemplateBinderFactory _binderFactory;\n\n    /// <summary>\n    /// Creates a new transform.\n    /// </summary>\n    /// <param name=\"pattern\">The pattern used to create the new request path.</param>\n    /// <param name=\"binderFactory\">The factory used to bind route parameters to the given path pattern.</param>\n    public PathRouteValuesTransform(\n        [StringSyntax(\"Route\")] string pattern, TemplateBinderFactory binderFactory)\n    {\n        ArgumentNullException.ThrowIfNull(pattern);\n        ArgumentNullException.ThrowIfNull(binderFactory);\n        _binderFactory = binderFactory;\n        Pattern = RoutePatternFactory.Parse(pattern);\n    }\n\n    internal RoutePattern Pattern { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        // TemplateBinder.BindValues will modify the RouteValueDictionary\n        // We make a copy so that the original request is not modified by the transform\n        var routeValues = context.HttpContext.Request.RouteValues;\n        var routeValuesCopy = new RouteValueDictionary();\n\n        // Only copy route values used in the pattern, otherwise they'll be added as query parameters.\n        foreach (var pattern in Pattern.Parameters)\n        {\n            if (routeValues.TryGetValue(pattern.Name, out var value))\n            {\n                routeValuesCopy[pattern.Name] = value;\n            }\n        }\n\n        var binder = _binderFactory.Create(Pattern);\n        context.Path = binder.BindValues(acceptedValues: routeValuesCopy);\n\n        return default;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/PathStringTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Modifies the proxy request Path with the given value.\n/// </summary>\npublic class PathStringTransform : RequestTransform\n{\n    /// <summary>\n    /// Creates a new transform.\n    /// </summary>\n    /// <param name=\"mode\">A <see cref=\"PathTransformMode\"/> indicating how the given value should update the existing path.</param>\n    /// <param name=\"value\">The path value used to update the existing value.</param>\n    public PathStringTransform(PathTransformMode mode, PathString value)\n    {\n        ArgumentNullException.ThrowIfNull(value.Value);\n\n        Mode = mode;\n        Value = value;\n    }\n\n    internal PathString Value { get; }\n\n    internal PathTransformMode Mode { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        switch (Mode)\n        {\n            case PathTransformMode.Set:\n                context.Path = Value;\n                break;\n            case PathTransformMode.Prefix:\n                context.Path = Value + context.Path;\n                break;\n            case PathTransformMode.RemovePrefix:\n                context.Path = context.Path.StartsWithSegments(Value, out var remainder) ? remainder : context.Path;\n                break;\n            default:\n                throw new NotImplementedException(Mode.ToString());\n        }\n\n        return default;\n    }\n\n    public enum PathTransformMode\n    {\n        Set,\n        Prefix,\n        RemovePrefix,\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/PathTransformExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics.CodeAnalysis;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing.Template;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Extensions for adding path transforms.\n/// </summary>\npublic static class PathTransformExtensions\n{\n    /// <summary>\n    /// Clones the route and adds the transform which sets the request path with the given value.\n    /// </summary>\n    public static RouteConfig WithTransformPathSet(this RouteConfig route, PathString path)\n    {\n        ArgumentNullException.ThrowIfNull(path.Value);\n\n        return route.WithTransform(transform =>\n        {\n            transform[PathTransformFactory.PathSetKey] = path.Value;\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform which sets the request path with the given value.\n    /// </summary>\n    public static TransformBuilderContext AddPathSet(this TransformBuilderContext context, PathString path)\n    {\n        context.RequestTransforms.Add(new PathStringTransform(PathStringTransform.PathTransformMode.Set, path));\n        return context;\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will prefix the request path with the given value.\n    /// </summary>\n    public static RouteConfig WithTransformPathPrefix(this RouteConfig route, PathString prefix)\n    {\n        ArgumentNullException.ThrowIfNull(prefix.Value);\n\n        return route.WithTransform(transform =>\n        {\n            transform[PathTransformFactory.PathPrefixKey] = prefix.Value;\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform which will prefix the request path with the given value.\n    /// </summary>\n    public static TransformBuilderContext AddPathPrefix(this TransformBuilderContext context, PathString prefix)\n    {\n        context.RequestTransforms.Add(new PathStringTransform(PathStringTransform.PathTransformMode.Prefix, prefix));\n        return context;\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will remove the matching prefix from the request path.\n    /// </summary>\n    public static RouteConfig WithTransformPathRemovePrefix(this RouteConfig route, PathString prefix)\n    {\n        ArgumentNullException.ThrowIfNull(prefix.Value);\n\n        return route.WithTransform(transform =>\n        {\n            transform[PathTransformFactory.PathRemovePrefixKey] = prefix.Value;\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform which will remove the matching prefix from the request path.\n    /// </summary>\n    public static TransformBuilderContext AddPathRemovePrefix(this TransformBuilderContext context, PathString prefix)\n    {\n        context.RequestTransforms.Add(new PathStringTransform(PathStringTransform.PathTransformMode.RemovePrefix, prefix));\n        return context;\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will set the request path with the given value.\n    /// </summary>\n    public static RouteConfig WithTransformPathRouteValues(this RouteConfig route, [StringSyntax(\"Route\")] PathString pattern)\n    {\n        ArgumentNullException.ThrowIfNull(pattern.Value);\n\n        return route.WithTransform(transform =>\n        {\n            transform[PathTransformFactory.PathPatternKey] = pattern.Value;\n        });\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will set the request path with the given value.\n    /// </summary>\n    public static TransformBuilderContext AddPathRouteValues(this TransformBuilderContext context, [StringSyntax(\"Route\")] PathString pattern)\n    {\n        ArgumentNullException.ThrowIfNull(pattern.Value);\n\n        var binder = context.Services.GetRequiredService<TemplateBinderFactory>();\n        context.RequestTransforms.Add(new PathRouteValuesTransform(pattern.Value, binder));\n        return context;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/PathTransformFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing.Template;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\ninternal sealed class PathTransformFactory : ITransformFactory\n{\n    internal const string PathSetKey = \"PathSet\";\n    internal const string PathPrefixKey = \"PathPrefix\";\n    internal const string PathRemovePrefixKey = \"PathRemovePrefix\";\n    internal const string PathPatternKey = \"PathPattern\";\n\n    private readonly TemplateBinderFactory _binderFactory;\n\n    public PathTransformFactory(TemplateBinderFactory binderFactory)\n    {\n        ArgumentNullException.ThrowIfNull(binderFactory);\n        _binderFactory = binderFactory;\n    }\n\n    public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(PathSetKey, out var pathSet))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n            CheckPathNotNull(context, PathSetKey, pathSet);\n        }\n        else if (transformValues.TryGetValue(PathPrefixKey, out var pathPrefix))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n            CheckPathNotNull(context, PathPrefixKey, pathPrefix);\n        }\n        else if (transformValues.TryGetValue(PathRemovePrefixKey, out var pathRemovePrefix))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n            CheckPathNotNull(context, PathRemovePrefixKey, pathRemovePrefix);\n        }\n        else if (transformValues.TryGetValue(PathPatternKey, out var pathPattern))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n            CheckPathNotNull(context, PathPatternKey, pathPattern);\n            // TODO: Validate the pattern format. Does it build?\n        }\n        else\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    private static void CheckPathNotNull(TransformRouteValidationContext context, string fieldName, string? path)\n    {\n        if (path is null)\n        {\n            context.Errors.Add(new ArgumentNullException(fieldName));\n        }\n    }\n\n    public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(PathSetKey, out var pathSet))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            var path = MakePathString(pathSet);\n            context.AddPathSet(path);\n        }\n        else if (transformValues.TryGetValue(PathPrefixKey, out var pathPrefix))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            var path = MakePathString(pathPrefix);\n            context.AddPathPrefix(path);\n        }\n        else if (transformValues.TryGetValue(PathRemovePrefixKey, out var pathRemovePrefix))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            var path = MakePathString(pathRemovePrefix);\n            context.AddPathRemovePrefix(path);\n        }\n        else if (transformValues.TryGetValue(PathPatternKey, out var pathPattern))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            var path = MakePathString(pathPattern);\n            // We don't use the extension here because we want to avoid doing a DI lookup for the binder every time.\n            context.RequestTransforms.Add(new PathRouteValuesTransform(path.Value!, _binderFactory));\n        }\n        else\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    private static PathString MakePathString(string path)\n    {\n        ArgumentNullException.ThrowIfNull(path);\n        if (!path.StartsWith('/'))\n        {\n            path = \"/\" + path;\n        }\n        return new PathString(path);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/QueryParameterFromRouteTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\npublic class QueryParameterRouteTransform : QueryParameterTransform\n{\n    public QueryParameterRouteTransform(QueryStringTransformMode mode, string key, string routeValueKey)\n        : base(mode, key)\n    {\n        if (string.IsNullOrEmpty(key))\n        {\n            throw new ArgumentException($\"'{nameof(key)}' cannot be null or empty.\", nameof(key));\n        }\n\n        if (string.IsNullOrEmpty(routeValueKey))\n        {\n            throw new ArgumentException($\"'{nameof(routeValueKey)}' cannot be null or empty.\", nameof(routeValueKey));\n        }\n\n        RouteValueKey = routeValueKey;\n    }\n\n    internal string RouteValueKey { get; }\n\n    /// <inheritdoc/>\n    protected override string? GetValue(RequestTransformContext context)\n    {\n        var routeValues = context.HttpContext.Request.RouteValues;\n        if (!routeValues.TryGetValue(RouteValueKey, out var value))\n        {\n            return null;\n        }\n\n        return value?.ToString();\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/QueryParameterFromStaticTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\npublic class QueryParameterFromStaticTransform : QueryParameterTransform\n{\n    public QueryParameterFromStaticTransform(QueryStringTransformMode mode, string key, string value)\n        : base(mode, key)\n    {\n        if (string.IsNullOrEmpty(key))\n        {\n            throw new ArgumentException($\"'{nameof(key)}' cannot be null or empty.\", nameof(key));\n        }\n\n        ArgumentNullException.ThrowIfNull(value);\n\n        Value = value;\n    }\n\n    internal string Value { get; }\n\n    /// <inheritdoc/>\n    protected override string GetValue(RequestTransformContext context)\n    {\n        return Value;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/QueryParameterRemoveTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// A request transform that removes the given query parameter.\n/// </summary>\npublic class QueryParameterRemoveTransform : RequestTransform\n{\n    public QueryParameterRemoveTransform(string key)\n    {\n        if (string.IsNullOrEmpty(key))\n        {\n            throw new ArgumentException($\"'{nameof(key)}' cannot be null or empty.\", nameof(key));\n        }\n\n        Key = key;\n    }\n\n    internal string Key { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        context.Query.Collection.Remove(Key);\n\n        return default;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/QueryParameterTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\npublic abstract class QueryParameterTransform : RequestTransform\n{\n    public QueryParameterTransform(QueryStringTransformMode mode, string key)\n    {\n        if (string.IsNullOrEmpty(key))\n        {\n            throw new ArgumentException($\"'{nameof(key)}' cannot be null or empty.\", nameof(key));\n        }\n\n        Mode = mode;\n        Key = key;\n    }\n\n    internal QueryStringTransformMode Mode { get; }\n\n    internal string Key { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        var value = GetValue(context);\n        if (value is not null)\n        {\n            switch (Mode)\n            {\n                case QueryStringTransformMode.Append:\n                    StringValues newValue = value;\n                    if (context.Query.Collection.TryGetValue(Key, out var currentValue))\n                    {\n                        newValue = StringValues.Concat(currentValue, value);\n                    }\n                    context.Query.Collection[Key] = newValue;\n                    break;\n                case QueryStringTransformMode.Set:\n                    context.Query.Collection[Key] = value;\n                    break;\n                default:\n                    throw new NotImplementedException(Mode.ToString());\n            }\n        }\n\n        return default;\n    }\n\n    protected abstract string? GetValue(RequestTransformContext context);\n}\n\npublic enum QueryStringTransformMode\n{\n    Append,\n    Set\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/QueryTransformContext.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Extensions;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Transform state for use with <see cref=\"RequestTransform\"/>\n/// </summary>\npublic class QueryTransformContext\n{\n    private readonly HttpRequest _request;\n    private readonly QueryString _originalQueryString;\n    private Dictionary<string, StringValues>? _modifiedQueryParameters;\n\n    public QueryTransformContext(HttpRequest request)\n    {\n        ArgumentNullException.ThrowIfNull(request);\n        _request = request;\n        _originalQueryString = request.QueryString;\n        _modifiedQueryParameters = null;\n    }\n\n    public QueryString QueryString\n    {\n        get\n        {\n            if (_modifiedQueryParameters is null)\n            {\n                return _originalQueryString;\n            }\n\n            return new QueryBuilder(_modifiedQueryParameters).ToQueryString();\n        }\n    }\n\n    public IDictionary<string, StringValues> Collection\n    {\n        get\n        {\n            if (_modifiedQueryParameters is null)\n            {\n                _modifiedQueryParameters = new Dictionary<string, StringValues>(_request.Query, StringComparer.OrdinalIgnoreCase);\n            }\n\n            return _modifiedQueryParameters;\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/QueryTransformExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Extensions for adding query transforms.\n/// </summary>\npublic static class QueryTransformExtensions\n{\n    /// <summary>\n    /// Clones the route and adds the transform that will append or set the query parameter from the given value.\n    /// </summary>\n    public static RouteConfig WithTransformQueryValue(this RouteConfig route, string queryKey, string value, bool append = true)\n    {\n        var type = append ? QueryTransformFactory.AppendKey : QueryTransformFactory.SetKey;\n        return route.WithTransform(transform =>\n        {\n            transform[QueryTransformFactory.QueryValueParameterKey] = queryKey;\n            transform[type] = value;\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform that will append or set the query parameter from the given value.\n    /// </summary>\n    public static TransformBuilderContext AddQueryValue(this TransformBuilderContext context, string queryKey, string value, bool append = true)\n    {\n        context.RequestTransforms.Add(new QueryParameterFromStaticTransform(\n            append ? QueryStringTransformMode.Append : QueryStringTransformMode.Set,\n            queryKey, value));\n        return context;\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform that will append or set the query parameter from a route value.\n    /// </summary>\n    public static RouteConfig WithTransformQueryRouteValue(this RouteConfig route, string queryKey, string routeValueKey, bool append = true)\n    {\n        var type = append ? QueryTransformFactory.AppendKey : QueryTransformFactory.SetKey;\n        return route.WithTransform(transform =>\n        {\n            transform[QueryTransformFactory.QueryRouteParameterKey] = queryKey;\n            transform[type] = routeValueKey;\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform that will append or set the query parameter from a route value.\n    /// </summary>\n    public static TransformBuilderContext AddQueryRouteValue(this TransformBuilderContext context, string queryKey, string routeValueKey, bool append = true)\n    {\n        context.RequestTransforms.Add(new QueryParameterRouteTransform(\n            append ? QueryStringTransformMode.Append : QueryStringTransformMode.Set,\n            queryKey, routeValueKey));\n        return context;\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform that will remove the given query key.\n    /// </summary>\n    public static RouteConfig WithTransformQueryRemoveKey(this RouteConfig route, string queryKey)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[QueryTransformFactory.QueryRemoveParameterKey] = queryKey;\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform that will remove the given query key.\n    /// </summary>\n    public static TransformBuilderContext AddQueryRemoveKey(this TransformBuilderContext context, string queryKey)\n    {\n        context.RequestTransforms.Add(new QueryParameterRemoveTransform(queryKey));\n        return context;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/QueryTransformFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\ninternal sealed class QueryTransformFactory : ITransformFactory\n{\n    internal const string QueryValueParameterKey = \"QueryValueParameter\";\n    internal const string QueryRouteParameterKey = \"QueryRouteParameter\";\n    internal const string QueryRemoveParameterKey = \"QueryRemoveParameter\";\n    internal const string AppendKey = \"Append\";\n    internal const string SetKey = \"Set\";\n\n    public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(QueryValueParameterKey, out var queryValueParameter))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2);\n            if (!transformValues.TryGetValue(AppendKey, out var _) && !transformValues.TryGetValue(SetKey, out var _))\n            {\n                context.Errors.Add(new ArgumentException($\"Unexpected parameters for QueryValueParameter: {string.Join(';', transformValues.Keys)}. Expected 'Append' or 'Set'.\"));\n            }\n        }\n        else if (transformValues.TryGetValue(QueryRouteParameterKey, out var queryRouteParameter))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2);\n            if (!transformValues.TryGetValue(AppendKey, out var _) && !transformValues.TryGetValue(SetKey, out var _))\n            {\n                context.Errors.Add(new ArgumentException($\"Unexpected parameters for QueryRouteParameter: {string.Join(';', transformValues.Keys)}. Expected 'Append' or 'Set'.\"));\n            }\n        }\n        else if (transformValues.TryGetValue(QueryRemoveParameterKey, out var removeQueryParameter))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n        }\n        else\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(QueryValueParameterKey, out var queryValueParameter))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 2);\n            if (transformValues.TryGetValue(AppendKey, out var appendValue))\n            {\n                context.AddQueryValue(queryValueParameter, appendValue, append: true);\n            }\n            else if (transformValues.TryGetValue(SetKey, out var setValue))\n            {\n                context.AddQueryValue(queryValueParameter, setValue, append: false);\n            }\n            else\n            {\n                throw new NotSupportedException(string.Join(\";\", transformValues.Keys));\n            }\n        }\n        else if (transformValues.TryGetValue(QueryRouteParameterKey, out var queryRouteParameter))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 2);\n            if (transformValues.TryGetValue(AppendKey, out var routeValueKeyAppend))\n            {\n                context.AddQueryRouteValue(queryRouteParameter, routeValueKeyAppend, append: true);\n            }\n            else if (transformValues.TryGetValue(SetKey, out var routeValueKeySet))\n            {\n                context.AddQueryRouteValue(queryRouteParameter, routeValueKeySet, append: false);\n            }\n            else\n            {\n                throw new NotSupportedException(string.Join(\";\", transformValues.Keys));\n            }\n        }\n        else if (transformValues.TryGetValue(QueryRemoveParameterKey, out var removeQueryParameter))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            context.AddQueryRemoveKey(removeQueryParameter);\n        }\n        else\n        {\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestFuncTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// A request transform that runs the given Func.\n/// </summary>\npublic class RequestFuncTransform : RequestTransform\n{\n    private readonly Func<RequestTransformContext, ValueTask> _func;\n\n    public RequestFuncTransform(Func<RequestTransformContext, ValueTask> func)\n    {\n        ArgumentNullException.ThrowIfNull(func);\n        _func = func;\n    }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        return _func(context);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeaderClientCertTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Base64 encodes the client certificate (if any) and sets it as the header value.\n/// </summary>\npublic class RequestHeaderClientCertTransform : RequestTransform\n{\n    public RequestHeaderClientCertTransform(string headerName)\n    {\n        if (string.IsNullOrEmpty(headerName))\n        {\n            throw new ArgumentException($\"'{nameof(headerName)}' cannot be null or empty.\", nameof(headerName));\n        }\n\n        HeaderName = headerName;\n    }\n\n    internal string HeaderName { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        RemoveHeader(context, HeaderName);\n\n        var clientCert = context.HttpContext.Connection.ClientCertificate;\n        if (clientCert is not null)\n        {\n            var encoded = Convert.ToBase64String(clientCert.RawData);\n            AddHeader(context, HeaderName, encoded);\n        }\n\n        return default;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeaderForwardedTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Net;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// An implementation of the Forwarded header as defined in https://tools.ietf.org/html/rfc7239.\n/// </summary>\npublic class RequestHeaderForwardedTransform : RequestTransform\n{\n    internal static readonly RequestHeaderForwardedTransform RemoveTransform =\n        new RequestHeaderForwardedTransform(new NullRandomFactory(), NodeFormat.Random, NodeFormat.Random, false, false, ForwardedTransformActions.Remove);\n\n    private const string ForwardedHeaderName = \"Forwarded\";\n    // obfnode = \"_\" 1*( ALPHA / DIGIT / \".\" / \"_\" / \"-\")\n    private const string ObfChars = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-\";\n\n    private readonly IRandomFactory _randomFactory;\n\n    public RequestHeaderForwardedTransform(IRandomFactory randomFactory, NodeFormat forFormat, NodeFormat byFormat, bool host, bool proto, ForwardedTransformActions action)\n    {\n        ArgumentNullException.ThrowIfNull(randomFactory);\n        _randomFactory = randomFactory;\n        ForFormat = forFormat;\n        ByFormat = byFormat;\n        HostEnabled = host;\n        ProtoEnabled = proto;\n        Debug.Assert(action != ForwardedTransformActions.Off);\n        TransformAction = action;\n    }\n\n    internal NodeFormat ForFormat { get; }\n\n    internal NodeFormat ByFormat { get; }\n\n    internal bool HostEnabled { get; }\n\n    internal bool ProtoEnabled { get; }\n\n    internal ForwardedTransformActions TransformAction { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        var httpContext = context.HttpContext;\n\n        switch (TransformAction)\n        {\n            case ForwardedTransformActions.Set:\n                RemoveHeader(context, ForwardedHeaderName);\n                AddHeader(context, ForwardedHeaderName, GetHeaderValue(httpContext));\n                break;\n            case ForwardedTransformActions.Append:\n                var existingValues = TakeHeader(context, ForwardedHeaderName);\n                var values = StringValues.Concat(existingValues, GetHeaderValue(httpContext));\n                AddHeader(context, ForwardedHeaderName, values);\n                break;\n            case ForwardedTransformActions.Remove:\n                RemoveHeader(context, ForwardedHeaderName);\n                break;\n            default:\n                throw new NotImplementedException(TransformAction.ToString());\n        }\n\n        return default;\n    }\n\n    private string GetHeaderValue(HttpContext httpContext)\n    {\n        var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);\n        AppendProto(httpContext, ref builder);\n        AppendHost(httpContext, ref builder);\n        AppendFor(httpContext, ref builder);\n        AppendBy(httpContext, ref builder);\n        return builder.ToString();\n    }\n\n    private void AppendProto(HttpContext context, ref ValueStringBuilder builder)\n    {\n        if (ProtoEnabled)\n        {\n            // Always first doesn't need to check for ';'\n            builder.Append(\"proto=\");\n            builder.Append(context.Request.Scheme);\n        }\n    }\n\n    private void AppendHost(HttpContext context, ref ValueStringBuilder builder)\n    {\n        if (HostEnabled)\n        {\n            if (builder.Length > 0)\n            {\n                builder.Append(';');\n            }\n            // Quoted because of the ':' when there's a port.\n            builder.Append(\"host=\\\"\");\n            builder.Append(context.Request.Host.ToUriComponent());\n            builder.Append('\"');\n        }\n    }\n\n    private void AppendFor(HttpContext context, ref ValueStringBuilder builder)\n    {\n        if (ForFormat > NodeFormat.None)\n        {\n            if (builder.Length > 0)\n            {\n                builder.Append(';');\n            }\n            builder.Append(\"for=\");\n            AppendNode(context.Connection.RemoteIpAddress, context.Connection.RemotePort, ForFormat, ref builder);\n        }\n    }\n\n    private void AppendBy(HttpContext context, ref ValueStringBuilder builder)\n    {\n        if (ByFormat > NodeFormat.None)\n        {\n            if (builder.Length > 0)\n            {\n                builder.Append(';');\n            }\n            builder.Append(\"by=\");\n            AppendNode(context.Connection.LocalIpAddress, context.Connection.LocalPort, ByFormat, ref builder);\n        }\n    }\n\n    // https://tools.ietf.org/html/rfc7239#section-6\n    private void AppendNode(IPAddress? ipAddress, int port, NodeFormat format, ref ValueStringBuilder builder)\n    {\n        // Prefer IPv4 formatting\n        if (ipAddress is { IsIPv4MappedToIPv6: true })\n        {\n            ipAddress = ipAddress.MapToIPv4();\n        }\n\n        // \"It is important to note that an IPv6 address and any nodename with\n        // node-port specified MUST be quoted, since \":\" is not an allowed\n        // character in \"token\".\"\n        var addPort = port != 0 && (format == NodeFormat.IpAndPort || format == NodeFormat.UnknownAndPort || format == NodeFormat.RandomAndPort);\n        var addRandomPort = (format == NodeFormat.IpAndRandomPort || format == NodeFormat.UnknownAndRandomPort || format == NodeFormat.RandomAndRandomPort);\n        var ipv6 = (format == NodeFormat.Ip || format == NodeFormat.IpAndPort || format == NodeFormat.IpAndRandomPort)\n            && ipAddress is not null && ipAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6;\n        var quote = addPort || addRandomPort || ipv6;\n\n        if (quote)\n        {\n            builder.Append('\"');\n        }\n\n        switch (format)\n        {\n            case NodeFormat.Ip:\n            case NodeFormat.IpAndPort:\n            case NodeFormat.IpAndRandomPort:\n                if (ipAddress is not null)\n                {\n                    if (ipv6)\n                    {\n                        builder.Append('[');\n                    }\n                    builder.Append(ipAddress.ToString());\n                    if (ipv6)\n                    {\n                        builder.Append(']');\n                    }\n                    break;\n                }\n                // This primarily happens in test environments that don't use real connections.\n                goto case NodeFormat.Unknown;\n            case NodeFormat.Unknown:\n            case NodeFormat.UnknownAndPort:\n            case NodeFormat.UnknownAndRandomPort:\n                builder.Append(\"unknown\");\n                break;\n            case NodeFormat.Random:\n            case NodeFormat.RandomAndPort:\n            case NodeFormat.RandomAndRandomPort:\n                AppendRandom(ref builder);\n                break;\n            default:\n                throw new NotImplementedException(format.ToString());\n        }\n\n        if (addPort)\n        {\n            builder.Append(':');\n            builder.Append(port);\n        }\n        else if (addRandomPort)\n        {\n            builder.Append(':');\n            AppendRandom(ref builder);\n        }\n\n        if (quote)\n        {\n            builder.Append('\"');\n        }\n    }\n\n    // https://tools.ietf.org/html/rfc7239#section-6.3\n    private void AppendRandom(ref ValueStringBuilder builder)\n    {\n        var random = _randomFactory.CreateRandomInstance();\n        builder.Append('_');\n        // This length is arbitrary.\n        for (var i = 0; i < 9; i++)\n        {\n            builder.Append(ObfChars[random.Next(ObfChars.Length)]);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeaderOriginalHostTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.Net.Http.Headers;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// A transform used to include or suppress the original request host header.\n/// </summary>\npublic class RequestHeaderOriginalHostTransform : RequestTransform\n{\n    public static readonly RequestHeaderOriginalHostTransform OriginalHost = new(true);\n\n    public static readonly RequestHeaderOriginalHostTransform SuppressHost = new(false);\n\n    /// <summary>\n    /// Creates a new <see cref=\"RequestHeaderOriginalHostTransform\"/>.\n    /// </summary>\n    /// <param name=\"useOriginalHost\">True if the original request host header should be used,\n    /// false otherwise.</param>\n    private RequestHeaderOriginalHostTransform(bool useOriginalHost)\n    {\n        UseOriginalHost = useOriginalHost;\n    }\n\n    internal bool UseOriginalHost { get; }\n\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        var destinationConfigHost = context.HttpContext.Features.Get<IReverseProxyFeature>()?.ProxiedDestination?.Model.Config?.Host;\n        var originalHost = context.HttpContext.Request.Host.Value is { Length: > 0 } host ? host : null;\n        var existingHost = RequestUtilities.TryGetValues(context.ProxyRequest.Headers, HeaderNames.Host, out var currentHost) ? currentHost.ToString() : null;\n\n        if (UseOriginalHost)\n        {\n            if (!context.HeadersCopied && existingHost is null)\n            {\n                // Propagate the host if the transform pipeline didn't already override it.\n                // If there was no original host specified, allow the destination config host to flow through.\n                context.ProxyRequest.Headers.TryAddWithoutValidation(HeaderNames.Host, originalHost ?? destinationConfigHost);\n            }\n        }\n        else if (existingHost is null || string.Equals(originalHost, existingHost, StringComparison.Ordinal))\n        {\n            // Use the host from destination configuration (which may be null) if either:\n            // * there is no host header set, or\n            // * the original host header is being suppressed and has not been modified by the transform pipeline\n            context.ProxyRequest.Headers.Host = destinationConfigHost;\n        }\n\n        return default;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeaderRemoveTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Removes a request header.\n/// </summary>\npublic class RequestHeaderRemoveTransform : RequestTransform\n{\n    public RequestHeaderRemoveTransform(string headerName)\n    {\n        if (string.IsNullOrEmpty(headerName))\n        {\n            throw new ArgumentException($\"'{nameof(headerName)}' cannot be null or empty.\", nameof(headerName));\n        }\n\n        HeaderName = headerName;\n    }\n\n    internal string HeaderName { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        RemoveHeader(context, HeaderName);\n\n        return default;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeaderRouteValueTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\npublic class RequestHeaderRouteValueTransform : RequestHeaderTransform\n{\n    public RequestHeaderRouteValueTransform(string headerName, string routeValueKey, bool append)\n        : base(headerName, append)\n    {\n        if (string.IsNullOrEmpty(headerName))\n        {\n            throw new ArgumentException($\"'{nameof(headerName)}' cannot be null or empty.\", nameof(headerName));\n        }\n\n        if (string.IsNullOrEmpty(routeValueKey))\n        {\n            throw new ArgumentException($\"'{nameof(routeValueKey)}' cannot be null or empty.\", nameof(routeValueKey));\n        }\n\n        RouteValueKey = routeValueKey;\n    }\n\n    internal string RouteValueKey { get; }\n\n    protected override string? GetValue(RequestTransformContext context)\n    {\n        var routeValues = context.HttpContext.Request.RouteValues;\n        if (!routeValues.TryGetValue(RouteValueKey, out var value))\n        {\n            return null;\n        }\n\n        return value?.ToString();\n    }\n}\n\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeaderTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\npublic abstract class RequestHeaderTransform : RequestTransform\n{\n    protected RequestHeaderTransform(string headerName, bool append)\n    {\n        if (string.IsNullOrEmpty(headerName))\n        {\n            throw new ArgumentException($\"'{nameof(headerName)}' cannot be null or empty.\", nameof(headerName));\n        }\n\n        Append = append;\n        HeaderName = headerName;\n    }\n\n    internal bool Append { get; }\n    internal string HeaderName { get; }\n\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        var value = GetValue(context);\n        if (value is null)\n        {\n            return default;\n        }\n\n        if (Append)\n        {\n            var existingValues = TakeHeader(context, HeaderName);\n            var newValue = StringValues.Concat(existingValues, value);\n            AddHeader(context, HeaderName, newValue);\n        }\n        else\n        {\n            RemoveHeader(context, HeaderName);\n            AddHeader(context, HeaderName, value);\n        }\n\n        return default;\n    }\n\n    protected abstract string? GetValue(RequestTransformContext context);\n}\n\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeaderValueTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Sets or appends simple request header values.\n/// </summary>\npublic class RequestHeaderValueTransform : RequestHeaderTransform\n{\n    public RequestHeaderValueTransform(string headerName, string value, bool append) : base(headerName, append)\n    {\n        ArgumentException.ThrowIfNullOrEmpty(headerName);\n        ArgumentNullException.ThrowIfNull(value);\n        Value = value;\n    }\n\n    internal string Value { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        return base.ApplyAsync(context);\n    }\n\n    /// <inheritdoc/>\n    protected override string GetValue(RequestTransformContext context)\n    {\n        return Value;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeaderXForwardedForTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Sets or appends the X-Forwarded-For header with the previous client's IP address.\n/// </summary>\npublic class RequestHeaderXForwardedForTransform : RequestTransform\n{\n    /// <summary>\n    /// Creates a new transform.\n    /// </summary>\n    /// <param name=\"headerName\">The header name.</param>\n    /// <param name=\"action\">Action to applied to the header.</param>\n    public RequestHeaderXForwardedForTransform(string headerName, ForwardedTransformActions action)\n    {\n        if (string.IsNullOrEmpty(headerName))\n        {\n            throw new ArgumentException($\"'{nameof(headerName)}' cannot be null or empty.\", nameof(headerName));\n        }\n\n        HeaderName = headerName;\n        Debug.Assert(action != ForwardedTransformActions.Off);\n        TransformAction = action;\n    }\n\n    internal string HeaderName { get; }\n\n    internal ForwardedTransformActions TransformAction { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        string? remoteIp = null;\n        var remoteIpAddress = context.HttpContext.Connection.RemoteIpAddress;\n        if (remoteIpAddress is not null)\n        {\n            remoteIp = remoteIpAddress.IsIPv4MappedToIPv6 ?\n                remoteIpAddress.MapToIPv4().ToString() :\n                remoteIpAddress.ToString();\n        }\n\n        switch (TransformAction)\n        {\n            case ForwardedTransformActions.Set:\n                RemoveHeader(context, HeaderName);\n                if (remoteIp is not null)\n                {\n                    AddHeader(context, HeaderName, remoteIp);\n                }\n                break;\n            case ForwardedTransformActions.Append:\n                Append(context, remoteIp);\n                break;\n            case ForwardedTransformActions.Remove:\n                RemoveHeader(context, HeaderName);\n                break;\n            default:\n                throw new NotImplementedException(TransformAction.ToString());\n        }\n\n        return default;\n    }\n\n    private void Append(RequestTransformContext context, string? remoteIp)\n    {\n        var existingValues = TakeHeader(context, HeaderName);\n        if (remoteIp is null)\n        {\n            if (!string.IsNullOrEmpty(existingValues))\n            {\n                AddHeader(context, HeaderName, existingValues);\n            }\n        }\n        else\n        {\n            var values = StringValues.Concat(existingValues, remoteIp);\n            AddHeader(context, HeaderName, values);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeaderXForwardedHostTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Sets or appends the X-Forwarded-Host header with the request's original Host header.\n/// </summary>\npublic class RequestHeaderXForwardedHostTransform : RequestTransform\n{\n    /// <summary>\n    /// Creates a new transform.\n    /// </summary>\n    /// <param name=\"headerName\">The header name.</param>\n    /// <param name=\"action\">Action to applied to the header.</param>\n    public RequestHeaderXForwardedHostTransform(string headerName, ForwardedTransformActions action)\n    {\n        ArgumentException.ThrowIfNullOrEmpty(headerName);\n\n        HeaderName = headerName;\n        Debug.Assert(action != ForwardedTransformActions.Off);\n        TransformAction = action;\n    }\n\n    internal string HeaderName { get; }\n    internal ForwardedTransformActions TransformAction { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        var host = context.HttpContext.Request.Host;\n\n        switch (TransformAction)\n        {\n            case ForwardedTransformActions.Set:\n                RemoveHeader(context, HeaderName);\n                if (host.HasValue)\n                {\n                    AddHeader(context, HeaderName, host.ToUriComponent());\n                }\n                break;\n            case ForwardedTransformActions.Append:\n                Append(context, host);\n                break;\n            case ForwardedTransformActions.Remove:\n                RemoveHeader(context, HeaderName);\n                break;\n            default:\n                throw new NotImplementedException(TransformAction.ToString());\n        }\n\n        return default;\n    }\n\n    private void Append(RequestTransformContext context, Microsoft.AspNetCore.Http.HostString host)\n    {\n        var existingValues = TakeHeader(context, HeaderName);\n        if (!host.HasValue)\n        {\n            if (!string.IsNullOrEmpty(existingValues))\n            {\n                AddHeader(context, HeaderName, existingValues);\n            }\n        }\n        else\n        {\n            var values = StringValues.Concat(existingValues, host.ToUriComponent());\n            AddHeader(context, HeaderName, values);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeaderXForwardedPrefixTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Sets or appends the X-Forwarded-Prefix header with the request's original PathBase.\n/// </summary>\npublic class RequestHeaderXForwardedPrefixTransform : RequestTransform\n{\n    public RequestHeaderXForwardedPrefixTransform(string headerName, ForwardedTransformActions action)\n    {\n        if (string.IsNullOrEmpty(headerName))\n        {\n            throw new ArgumentException($\"'{nameof(headerName)}' cannot be null or empty.\", nameof(headerName));\n        }\n\n        HeaderName = headerName;\n        Debug.Assert(action != ForwardedTransformActions.Off);\n        TransformAction = action;\n    }\n\n    internal string HeaderName { get; }\n    internal ForwardedTransformActions TransformAction { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        var pathBase = context.HttpContext.Request.PathBase;\n\n        switch (TransformAction)\n        {\n            case ForwardedTransformActions.Set:\n                RemoveHeader(context, HeaderName);\n                if (pathBase.HasValue)\n                {\n                    AddHeader(context, HeaderName, pathBase.ToUriComponent());\n                }\n                break;\n            case ForwardedTransformActions.Append:\n                Append(context, pathBase);\n                break;\n            case ForwardedTransformActions.Remove:\n                RemoveHeader(context, HeaderName);\n                break;\n            default:\n                throw new NotImplementedException(TransformAction.ToString());\n        }\n\n        return default;\n    }\n\n    private void Append(RequestTransformContext context, Microsoft.AspNetCore.Http.PathString pathBase)\n    {\n        var existingValues = TakeHeader(context, HeaderName);\n        if (!pathBase.HasValue)\n        {\n            if (!string.IsNullOrEmpty(existingValues))\n            {\n                AddHeader(context, HeaderName, existingValues);\n            }\n        }\n        else\n        {\n            var values = StringValues.Concat(existingValues, pathBase.ToUriComponent());\n            AddHeader(context, HeaderName, values);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeaderXForwardedProtoTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Sets or appends the X-Forwarded-Proto header with the request's original url scheme.\n/// </summary>\npublic class RequestHeaderXForwardedProtoTransform : RequestTransform\n{\n    /// <summary>\n    /// Creates a new transform.\n    /// </summary>\n    /// <param name=\"headerName\">The header name.</param>\n    /// <param name=\"action\">Action to applied to the header.</param>\n    public RequestHeaderXForwardedProtoTransform(string headerName, ForwardedTransformActions action)\n    {\n        if (string.IsNullOrEmpty(headerName))\n        {\n            throw new ArgumentException($\"'{nameof(headerName)}' cannot be null or empty.\", nameof(headerName));\n        }\n\n        HeaderName = headerName;\n        Debug.Assert(action != ForwardedTransformActions.Off);\n        TransformAction = action;\n    }\n\n    internal string HeaderName { get; }\n    internal ForwardedTransformActions TransformAction { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        var scheme = context.HttpContext.Request.Scheme;\n\n        switch (TransformAction)\n        {\n            case ForwardedTransformActions.Set:\n                RemoveHeader(context, HeaderName);\n                AddHeader(context, HeaderName, scheme);\n                break;\n            case ForwardedTransformActions.Append:\n                var existingValues = TakeHeader(context, HeaderName);\n                var values = StringValues.Concat(existingValues, scheme);\n                AddHeader(context, HeaderName, values);\n                break;\n            case ForwardedTransformActions.Remove:\n                RemoveHeader(context, HeaderName);\n                break;\n            default:\n                throw new NotImplementedException(TransformAction.ToString());\n        }\n\n        return default;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeadersAllowedTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Copies only allowed request headers.\n/// </summary>\npublic class RequestHeadersAllowedTransform : RequestTransform\n{\n    public RequestHeadersAllowedTransform(string[] allowedHeaders)\n    {\n        ArgumentNullException.ThrowIfNull(allowedHeaders);\n\n        AllowedHeaders = allowedHeaders;\n        AllowedHeadersSet = new HashSet<string>(allowedHeaders, StringComparer.OrdinalIgnoreCase).ToFrozenSet(StringComparer.OrdinalIgnoreCase);\n    }\n\n    internal string[] AllowedHeaders { get; }\n\n    private FrozenSet<string> AllowedHeadersSet { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(RequestTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        Debug.Assert(!context.HeadersCopied);\n\n        foreach (var header in context.HttpContext.Request.Headers)\n        {\n            var headerName = header.Key;\n            var headerValue = header.Value;\n            if (!StringValues.IsNullOrEmpty(headerValue)\n                && AllowedHeadersSet.Contains(headerName))\n            {\n                AddHeader(context, headerName, headerValue);\n            }\n        }\n\n        context.HeadersCopied = true;\n\n        return default;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeadersTransformExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Extensions for adding request header transforms.\n/// </summary>\npublic static class RequestHeadersTransformExtensions\n{\n    /// <summary>\n    /// Clones the route and adds the transform which will enable or suppress copying request headers to the proxy request.\n    /// </summary>\n    public static RouteConfig WithTransformCopyRequestHeaders(this RouteConfig route, bool copy = true)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[RequestHeadersTransformFactory.RequestHeadersCopyKey] = copy ? bool.TrueString : bool.FalseString;\n        });\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will copy the incoming request Host header to the proxy request.\n    /// </summary>\n    public static RouteConfig WithTransformUseOriginalHostHeader(this RouteConfig route, bool useOriginal = true)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[RequestHeadersTransformFactory.RequestHeaderOriginalHostKey] = useOriginal ? bool.TrueString : bool.FalseString;\n        });\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will append or set the request header.\n    /// </summary>\n    public static RouteConfig WithTransformRequestHeader(this RouteConfig route, string headerName, string value, bool append = true)\n    {\n        var type = append ? RequestHeadersTransformFactory.AppendKey : RequestHeadersTransformFactory.SetKey;\n        return route.WithTransform(transform =>\n        {\n            transform[RequestHeadersTransformFactory.RequestHeaderKey] = headerName;\n            transform[type] = value;\n        });\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will append or set the request header from a route value.\n    /// </summary>\n    public static RouteConfig WithTransformRequestHeaderRouteValue(this RouteConfig route, string headerName, string routeValueKey, bool append = true)\n    {\n        var type = append ? RequestHeadersTransformFactory.AppendKey : RequestHeadersTransformFactory.SetKey;\n        return route.WithTransform(transform =>\n        {\n            transform[RequestHeadersTransformFactory.RequestHeaderRouteValueKey] = headerName;\n            transform[type] = routeValueKey;\n        });\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will remove the request header.\n    /// </summary>\n    public static RouteConfig WithTransformRequestHeaderRemove(this RouteConfig route, string headerName)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[RequestHeadersTransformFactory.RequestHeaderRemoveKey] = headerName;\n        });\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will only copy the allowed request headers. Other transforms\n    /// that modify or append to existing headers may be affected if not included in the allow list.\n    /// </summary>\n    public static RouteConfig WithTransformRequestHeadersAllowed(this RouteConfig route, params string[] allowedHeaders)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[RequestHeadersTransformFactory.RequestHeadersAllowedKey] = string.Join(';', allowedHeaders);\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform which will append or set the request header.\n    /// </summary>\n    public static TransformBuilderContext AddRequestHeader(this TransformBuilderContext context, string headerName, string value, bool append = true)\n    {\n        context.RequestTransforms.Add(new RequestHeaderValueTransform(headerName, value, append));\n        return context;\n    }\n\n    /// <summary>\n    /// Adds the transform which will append or set the request header from a route value.\n    /// </summary>\n    public static TransformBuilderContext AddRequestHeaderRouteValue(this TransformBuilderContext context, string headerName, string routeValueKey, bool append = true)\n    {\n        context.RequestTransforms.Add(new RequestHeaderRouteValueTransform(headerName, routeValueKey, append));\n        return context;\n    }\n\n    /// <summary>\n    /// Adds the transform which will remove the request header.\n    /// </summary>\n    public static TransformBuilderContext AddRequestHeaderRemove(this TransformBuilderContext context, string headerName)\n    {\n        context.RequestTransforms.Add(new RequestHeaderRemoveTransform(headerName));\n        return context;\n    }\n\n    /// <summary>\n    /// Adds the transform which will only copy the allowed request headers. Other transforms\n    /// that modify or append to existing headers may be affected if not included in the allow list.\n    /// </summary>\n    public static TransformBuilderContext AddRequestHeadersAllowed(this TransformBuilderContext context, params string[] allowedHeaders)\n    {\n        context.CopyRequestHeaders = false;\n        context.RequestTransforms.Add(new RequestHeadersAllowedTransform(allowedHeaders));\n        return context;\n    }\n\n    /// <summary>\n    /// Adds the transform which will copy or remove the original host header.\n    /// </summary>\n    public static TransformBuilderContext AddOriginalHost(this TransformBuilderContext context, bool useOriginal = true)\n    {\n        if (useOriginal)\n        {\n            context.RequestTransforms.Add(RequestHeaderOriginalHostTransform.OriginalHost);\n        }\n        else\n        {\n            context.RequestTransforms.Add(RequestHeaderOriginalHostTransform.SuppressHost);\n        }\n        return context;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestHeadersTransformFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\ninternal sealed class RequestHeadersTransformFactory : ITransformFactory\n{\n    internal const string RequestHeadersCopyKey = \"RequestHeadersCopy\";\n    internal const string RequestHeaderOriginalHostKey = \"RequestHeaderOriginalHost\";\n    internal const string RequestHeaderKey = \"RequestHeader\";\n    internal const string RequestHeaderRouteValueKey = \"RequestHeaderRouteValue\";\n    internal const string RequestHeaderRemoveKey = \"RequestHeaderRemove\";\n    internal const string RequestHeadersAllowedKey = \"RequestHeadersAllowed\";\n    internal const string AppendKey = \"Append\";\n    internal const string SetKey = \"Set\";\n\n    public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(RequestHeadersCopyKey, out var copyHeaders))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n            if (!bool.TryParse(copyHeaders, out var _))\n            {\n                context.Errors.Add(new ArgumentException($\"Unexpected value for RequestHeaderCopy: {copyHeaders}. Expected 'true' or 'false'\"));\n            }\n        }\n        else if (transformValues.TryGetValue(RequestHeaderOriginalHostKey, out var originalHost))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n            if (!bool.TryParse(originalHost, out var _))\n            {\n                context.Errors.Add(new ArgumentException($\"Unexpected value for RequestHeaderOriginalHost: {originalHost}. Expected 'true' or 'false'\"));\n            }\n        }\n        else if (transformValues.TryGetValue(RequestHeaderKey, out var _))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2);\n            if (!transformValues.TryGetValue(SetKey, out var _) && !transformValues.TryGetValue(AppendKey, out var _))\n            {\n                context.Errors.Add(new ArgumentException($\"Unexpected parameters for RequestHeader: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'\"));\n            }\n        }\n        else if (transformValues.TryGetValue(RequestHeaderRouteValueKey, out var _))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2);\n            if (!transformValues.TryGetValue(AppendKey, out _) && !transformValues.TryGetValue(SetKey, out _))\n            {\n                context.Errors.Add(new ArgumentException($\"Unexpected parameters for RequestHeaderFromRoute: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'.\"));\n            }\n        }\n        else if (transformValues.TryGetValue(RequestHeaderRemoveKey, out var _))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n        }\n        else if (transformValues.TryGetValue(RequestHeadersAllowedKey, out var _))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n        }\n        else\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(RequestHeadersCopyKey, out var copyHeaders))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            context.CopyRequestHeaders = bool.Parse(copyHeaders);\n        }\n        else if (transformValues.TryGetValue(RequestHeaderOriginalHostKey, out var originalHost))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            context.AddOriginalHost(bool.Parse(originalHost));\n        }\n        else if (transformValues.TryGetValue(RequestHeaderKey, out var headerName))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 2);\n            if (transformValues.TryGetValue(SetKey, out var setValue))\n            {\n                context.AddRequestHeader(headerName, setValue, append: false);\n            }\n            else if (transformValues.TryGetValue(AppendKey, out var appendValue))\n            {\n                context.AddRequestHeader(headerName, appendValue, append: true);\n            }\n            else\n            {\n                throw new ArgumentException($\"Unexpected parameters for RequestHeader: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'\");\n            }\n        }\n        else if (transformValues.TryGetValue(RequestHeaderRouteValueKey, out var headerNameFromRoute))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 2);\n            if (transformValues.TryGetValue(AppendKey, out var routeValueKeyAppend))\n            {\n                context.AddRequestHeaderRouteValue(headerNameFromRoute, routeValueKeyAppend, append: true);\n            }\n            else if (transformValues.TryGetValue(SetKey, out var routeValueKeySet))\n            {\n                context.AddRequestHeaderRouteValue(headerNameFromRoute, routeValueKeySet, append: false);\n            }\n            else\n            {\n                throw new NotSupportedException(string.Join(\";\", transformValues.Keys));\n            }\n        }\n        else if (transformValues.TryGetValue(RequestHeaderRemoveKey, out var removeHeaderName))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            context.AddRequestHeaderRemove(removeHeaderName);\n        }\n        else if (transformValues.TryGetValue(RequestHeadersAllowedKey, out var allowedHeaders))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            var headersList = allowedHeaders.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);\n            context.AddRequestHeadersAllowed(headersList);\n        }\n        else\n        {\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Primitives;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// The base class for request transforms.\n/// </summary>\npublic abstract class RequestTransform\n{\n    /// <summary>\n    /// Transforms any of the available fields before building the outgoing request.\n    /// </summary>\n    public abstract ValueTask ApplyAsync(RequestTransformContext context);\n\n    /// <summary>\n    /// Removes and returns the current header value by first checking the HttpRequestMessage,\n    /// then the HttpContent, and falling back to the HttpContext only if\n    /// <see cref=\"RequestTransformContext.HeadersCopied\"/> is not set.\n    /// This ordering allows multiple transforms to mutate the same header.\n    /// </summary>\n    /// <param name=\"context\">The transform context.</param>\n    /// <param name=\"headerName\">The name of the header to take.</param>\n    /// <returns>The requested header value, or StringValues.Empty if none.</returns>\n    public static StringValues TakeHeader(RequestTransformContext context, string headerName)\n    {\n        if (string.IsNullOrEmpty(headerName))\n        {\n            throw new ArgumentException($\"'{nameof(headerName)}' cannot be null or empty.\", nameof(headerName));\n        }\n\n        var proxyRequest = context.ProxyRequest;\n\n        if (RequestUtilities.TryGetValues(proxyRequest.Headers, headerName, out var existingValues))\n        {\n            proxyRequest.Headers.Remove(headerName);\n        }\n        else if (proxyRequest.Content is { } content && RequestUtilities.TryGetValues(content.Headers, headerName, out existingValues))\n        {\n            content.Headers.Remove(headerName);\n        }\n        else if (!context.HeadersCopied)\n        {\n            existingValues = context.HttpContext.Request.Headers[headerName];\n        }\n\n        return existingValues;\n    }\n\n    /// <summary>\n    /// Adds the given header to the HttpRequestMessage or HttpContent where applicable.\n    /// </summary>\n    public static void AddHeader(RequestTransformContext context, string headerName, StringValues values)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n        ArgumentException.ThrowIfNullOrEmpty(headerName);\n\n        RequestUtilities.AddHeader(context.ProxyRequest, headerName, values);\n    }\n\n    /// <summary>\n    /// Removes the given header from the HttpRequestMessage or HttpContent where applicable.\n    /// </summary>\n    public static void RemoveHeader(RequestTransformContext context, string headerName)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n        ArgumentException.ThrowIfNullOrEmpty(headerName);\n\n        RequestUtilities.RemoveHeader(context.ProxyRequest, headerName);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RequestTransformContext.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing System.Threading;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Transform state for use with <see cref=\"RequestTransform\"/>\n/// </summary>\npublic class RequestTransformContext\n{\n    /// <summary>\n    /// The current request context.\n    /// </summary>\n    public HttpContext HttpContext { get; init; } = default!;\n\n    /// <summary>\n    /// The outgoing proxy request. All field are initialized except for the 'RequestUri' and optionally headers.\n    /// If no value is provided then the 'RequestUri' will be initialized using the updated 'DestinationPrefix',\n    /// 'Path', and 'Query' properties after the transforms have run. The headers will be copied later when\n    /// applying header transforms.\n    /// </summary>\n    public HttpRequestMessage ProxyRequest { get; init; } = default!;\n\n    /// <summary>\n    /// Gets or sets if the request headers have been copied from the HttpRequest to the HttpRequestMessage and HttpContent.\n    /// Transforms use this when searching for the current value of a header they should operate on.\n    /// </summary>\n    public bool HeadersCopied { get; set; }\n\n    /// <summary>\n    /// The path to use for the proxy request.\n    /// </summary>\n    /// <remarks>\n    /// This will be prefixed by any PathBase specified for the destination server.\n    /// </remarks>\n    public PathString Path { get; set; }\n\n    internal QueryTransformContext? MaybeQuery { get; private set; }\n\n    /// <summary>\n    /// The query used for the proxy request.\n    /// </summary>\n    public QueryTransformContext Query\n    {\n        get => MaybeQuery ??= new QueryTransformContext(HttpContext.Request);\n        set => MaybeQuery = value;\n    }\n\n    /// <summary>\n    /// The URI prefix for the proxy request. This includes the scheme and host and can optionally include a\n    /// port and path base. The 'Path' and 'Query' properties will be appended to this after the transforms have run.\n    /// Changing this value can have side effects on load balancing and health checks.\n    /// </summary>\n    public string DestinationPrefix { get; set; } = default!;\n\n    /// <summary>\n    /// A <see cref=\"CancellationToken\"/> indicating that the request is being aborted.\n    /// </summary>\n    public CancellationToken CancellationToken { get; set; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseCondition.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Specifies the conditions under which a response transform will run.\n/// </summary>\npublic enum ResponseCondition\n{\n    /// <summary>\n    /// The transform runs for all conditions.\n    /// </summary>\n    Always,\n\n    /// <summary>\n    /// The transform only runs if there is a successful response with a status code less than 400.\n    /// </summary>\n    Success,\n\n    /// <summary>\n    /// The transform only runs if there is no response or a response with a 400+ status code.\n    /// </summary>\n    Failure\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseFuncTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// A response transform that runs the given Func.\n/// </summary>\npublic class ResponseFuncTransform : ResponseTransform\n{\n    private readonly Func<ResponseTransformContext, ValueTask> _func;\n\n    public ResponseFuncTransform(Func<ResponseTransformContext, ValueTask> func)\n    {\n        ArgumentNullException.ThrowIfNull(func);\n        _func = func;\n    }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(ResponseTransformContext context)\n    {\n        return _func(context);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseHeaderRemoveTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Removes a response header.\n/// </summary>\npublic class ResponseHeaderRemoveTransform : ResponseTransform\n{\n    public ResponseHeaderRemoveTransform(string headerName, ResponseCondition condition)\n    {\n        if (string.IsNullOrEmpty(headerName))\n        {\n            throw new ArgumentException($\"'{nameof(headerName)}' cannot be null or empty.\", nameof(headerName));\n        }\n\n        HeaderName = headerName;\n        Condition = condition;\n    }\n\n    internal string HeaderName { get; }\n\n    internal ResponseCondition Condition { get; }\n\n    // Assumes the response status code has been set on the HttpContext already.\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(ResponseTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        if (Condition == ResponseCondition.Always\n            || Success(context) == (Condition == ResponseCondition.Success))\n        {\n            context.HttpContext.Response.Headers.Remove(HeaderName);\n        }\n\n        return default;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseHeaderValueTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Sets or appends simple response header values.\n/// </summary>\npublic class ResponseHeaderValueTransform : ResponseTransform\n{\n    public ResponseHeaderValueTransform(string headerName, string value, bool append, ResponseCondition condition)\n    {\n        if (string.IsNullOrEmpty(headerName))\n        {\n            throw new ArgumentException($\"'{nameof(headerName)}' cannot be null or empty.\", nameof(headerName));\n        }\n\n        HeaderName = headerName;\n        ArgumentNullException.ThrowIfNull(value);\n        Value = value;\n        Append = append;\n        Condition = condition;\n    }\n\n    internal ResponseCondition Condition { get; }\n\n    internal bool Append { get; }\n\n    internal string HeaderName { get; }\n\n    internal string Value { get; }\n\n    // Assumes the response status code has been set on the HttpContext already.\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(ResponseTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        if (Condition == ResponseCondition.Always\n            || Success(context) == (Condition == ResponseCondition.Success))\n        {\n            if (Append)\n            {\n                var existingHeader = TakeHeader(context, HeaderName);\n                var value = StringValues.Concat(existingHeader, Value);\n                SetHeader(context, HeaderName, value);\n            }\n            else\n            {\n                SetHeader(context, HeaderName, Value);\n            }\n        }\n\n        return default;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseHeadersAllowedTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Net.Http.Headers;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Copies only allowed response headers.\n/// </summary>\npublic class ResponseHeadersAllowedTransform : ResponseTransform\n{\n    public ResponseHeadersAllowedTransform(string[] allowedHeaders)\n    {\n        ArgumentNullException.ThrowIfNull(allowedHeaders);\n\n        AllowedHeaders = allowedHeaders;\n        AllowedHeadersSet = new HashSet<string>(allowedHeaders, StringComparer.OrdinalIgnoreCase).ToFrozenSet(StringComparer.OrdinalIgnoreCase);\n    }\n\n    internal string[] AllowedHeaders { get; }\n\n    private FrozenSet<string> AllowedHeadersSet { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(ResponseTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        if (context.ProxyResponse is null)\n        {\n            return default;\n        }\n\n        Debug.Assert(!context.HeadersCopied);\n\n        // See https://github.com/dotnet/yarp/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/HttpTransformer.cs#L67-L77\n        var responseHeaders = context.HttpContext.Response.Headers;\n        CopyResponseHeaders(context.ProxyResponse.Headers, responseHeaders);\n        if (context.ProxyResponse.Content is not null)\n        {\n            CopyResponseHeaders(context.ProxyResponse.Content.Headers, responseHeaders);\n        }\n\n        context.HeadersCopied = true;\n\n        return default;\n    }\n\n    // See https://github.com/dotnet/yarp/blob/main/src/ReverseProxy/Forwarder/HttpTransformer.cs#:~:text=void-,CopyResponseHeaders\n    private void CopyResponseHeaders(HttpHeaders source, IHeaderDictionary destination)\n    {\n        foreach (var header in source.NonValidated)\n        {\n            var headerName = header.Key;\n            if (!AllowedHeadersSet.Contains(headerName))\n            {\n                continue;\n            }\n\n            destination[headerName] = RequestUtilities.Concat(destination[headerName], header.Value);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseTrailerRemoveTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http.Features;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Removes a response trailer.\n/// </summary>\npublic class ResponseTrailerRemoveTransform : ResponseTrailersTransform\n{\n    public ResponseTrailerRemoveTransform(string headerName, ResponseCondition condition)\n    {\n        if (string.IsNullOrEmpty(headerName))\n        {\n            throw new ArgumentException($\"'{nameof(headerName)}' cannot be null or empty.\", nameof(headerName));\n        }\n\n        HeaderName = headerName;\n        Condition = condition;\n    }\n\n    internal string HeaderName { get; }\n\n    internal ResponseCondition Condition { get; }\n\n    // Assumes the response status code has been set on the HttpContext already.\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(ResponseTrailersTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        Debug.Assert(context.ProxyResponse is not null);\n\n        if (Condition == ResponseCondition.Always\n            || Success(context) == (Condition == ResponseCondition.Success))\n        {\n            var responseTrailersFeature = context.HttpContext.Features.Get<IHttpResponseTrailersFeature>();\n            var responseTrailers = responseTrailersFeature?.Trailers;\n            // Support should have already been checked by the caller.\n            Debug.Assert(responseTrailers is not null);\n            Debug.Assert(!responseTrailers.IsReadOnly);\n\n            responseTrailers.Remove(HeaderName);\n        }\n\n        return default;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseTrailerValueTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Sets or appends simple response trailer values.\n/// </summary>\npublic class ResponseTrailerValueTransform : ResponseTrailersTransform\n{\n    public ResponseTrailerValueTransform(string headerName, string value, bool append, ResponseCondition condition)\n    {\n        if (string.IsNullOrEmpty(headerName))\n        {\n            throw new ArgumentException($\"'{nameof(headerName)}' cannot be null or empty.\", nameof(headerName));\n        }\n\n        HeaderName = headerName;\n        ArgumentNullException.ThrowIfNull(value);\n        Value = value;\n        Append = append;\n        Condition = condition;\n    }\n\n    internal ResponseCondition Condition { get; }\n\n    internal bool Append { get; }\n\n    internal string HeaderName { get; }\n\n    internal string Value { get; }\n\n    // Assumes the response status code has been set on the HttpContext already.\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(ResponseTrailersTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        if (Condition == ResponseCondition.Always\n            || Success(context) == (Condition == ResponseCondition.Success))\n        {\n            if (Append)\n            {\n                var existingHeader = TakeHeader(context, HeaderName);\n                var value = StringValues.Concat(existingHeader, Value);\n                SetHeader(context, HeaderName, value);\n            }\n            else\n            {\n                SetHeader(context, HeaderName, Value);\n            }\n        }\n\n        return default;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseTrailersAllowedTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Net.Http.Headers;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Copies only allowed response trailers.\n/// </summary>\npublic class ResponseTrailersAllowedTransform : ResponseTrailersTransform\n{\n    public ResponseTrailersAllowedTransform(string[] allowedHeaders)\n    {\n        ArgumentNullException.ThrowIfNull(allowedHeaders);\n\n        AllowedHeaders = allowedHeaders;\n        AllowedHeadersSet = new HashSet<string>(allowedHeaders, StringComparer.OrdinalIgnoreCase).ToFrozenSet(StringComparer.OrdinalIgnoreCase);\n    }\n\n    internal string[] AllowedHeaders { get; }\n\n    private FrozenSet<string> AllowedHeadersSet { get; }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(ResponseTrailersTransformContext context)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        Debug.Assert(context.ProxyResponse is not null);\n        Debug.Assert(!context.HeadersCopied);\n\n        // See https://github.com/dotnet/yarp/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/HttpTransformer.cs#L85-L99\n        // NOTE: Deliberately not using `context.Response.SupportsTrailers()`, `context.Response.AppendTrailer(...)`\n        // because they lookup `IHttpResponseTrailersFeature` for every call. Here we do it just once instead.\n        var responseTrailersFeature = context.HttpContext.Features.Get<IHttpResponseTrailersFeature>();\n        var outgoingTrailers = responseTrailersFeature?.Trailers;\n        if (outgoingTrailers is not null && !outgoingTrailers.IsReadOnly)\n        {\n            // Note that trailers, if any, should already have been declared in Proxy's response\n            CopyResponseHeaders(context.ProxyResponse.TrailingHeaders, outgoingTrailers);\n        }\n\n        context.HeadersCopied = true;\n\n        return default;\n    }\n\n    // See https://github.com/dotnet/yarp/blob/main/src/ReverseProxy/Forwarder/HttpTransformer.cs#:~:text=void-,CopyResponseHeaders\n    private void CopyResponseHeaders(HttpHeaders source, IHeaderDictionary destination)\n    {\n        foreach (var header in source.NonValidated)\n        {\n            var headerName = header.Key;\n            if (!AllowedHeadersSet.Contains(headerName))\n            {\n                continue;\n            }\n\n            destination[headerName] = RequestUtilities.Concat(destination[headerName], header.Value);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseTrailersFuncTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// A response trailers transform that runs the given Func.\n/// </summary>\npublic class ResponseTrailersFuncTransform : ResponseTrailersTransform\n{\n    private readonly Func<ResponseTrailersTransformContext, ValueTask> _func;\n\n    public ResponseTrailersFuncTransform(Func<ResponseTrailersTransformContext, ValueTask> func)\n    {\n        ArgumentNullException.ThrowIfNull(func);\n        _func = func;\n    }\n\n    /// <inheritdoc/>\n    public override ValueTask ApplyAsync(ResponseTrailersTransformContext context)\n    {\n        return _func(context);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseTrailersTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.Extensions.Primitives;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Transforms for response trailers.\n/// </summary>\npublic abstract class ResponseTrailersTransform\n{\n    /// <summary>\n    /// Transforms the given response trailers. The trailers will have (optionally) already been\n    /// copied to the <see cref=\"HttpResponse\"/> and any changes should be made there.\n    /// </summary>\n    public abstract ValueTask ApplyAsync(ResponseTrailersTransformContext context);\n\n    /// <summary>\n    /// Removes and returns the current trailer value by first checking the HttpResponse\n    /// and falling back to the value from HttpResponseMessage only if\n    /// <see cref=\"ResponseTrailersTransformContext.HeadersCopied\"/> is not set.\n    /// This ordering allows multiple transforms to mutate the same header.\n    /// </summary>\n    /// <param name=\"context\">The transform context.</param>\n    /// <param name=\"headerName\">The name of the header to take.</param>\n    /// <returns>The response header value, or StringValues.Empty if none.</returns>\n    public static StringValues TakeHeader(ResponseTrailersTransformContext context, string headerName)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n        ArgumentException.ThrowIfNullOrEmpty(headerName);\n\n        Debug.Assert(context.ProxyResponse is not null);\n\n        var responseTrailersFeature = context.HttpContext.Features.Get<IHttpResponseTrailersFeature>();\n        var responseTrailers = responseTrailersFeature?.Trailers;\n        // Support should have already been checked by the caller.\n        Debug.Assert(responseTrailers is not null);\n        Debug.Assert(!responseTrailers.IsReadOnly);\n\n        if (responseTrailers.TryGetValue(headerName, out var existingValues))\n        {\n            responseTrailers.Remove(headerName);\n        }\n        else if (!context.HeadersCopied)\n        {\n            RequestUtilities.TryGetValues(context.ProxyResponse.TrailingHeaders, headerName, out existingValues);\n        }\n\n        return existingValues;\n    }\n\n    /// <summary>\n    /// Sets the given trailer on the HttpResponse.\n    /// </summary>\n    public static void SetHeader(ResponseTrailersTransformContext context, string headerName, StringValues values)\n    {\n        var responseTrailersFeature = context.HttpContext.Features.Get<IHttpResponseTrailersFeature>();\n        var responseTrailers = responseTrailersFeature?.Trailers;\n        // Support should have already been checked by the caller.\n        Debug.Assert(responseTrailers is not null);\n        Debug.Assert(!responseTrailers.IsReadOnly);\n\n        responseTrailers[headerName] = values;\n    }\n\n    internal static bool Success(ResponseTrailersTransformContext context)\n    {\n        // TODO: How complex should this get? Compare with http://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header\n        return context.HttpContext.Response.StatusCode < 400;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseTrailersTransformContext.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing System.Threading;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Transform state for use with <see cref=\"ResponseTrailersTransform\"/>\n/// </summary>\npublic class ResponseTrailersTransformContext\n{\n    /// <summary>\n    /// The current request context.\n    /// </summary>\n    public HttpContext HttpContext { get; init; } = default!;\n\n    /// <summary>\n    /// The incoming proxy response.\n    /// </summary>\n    public HttpResponseMessage ProxyResponse { get; init; } = default!;\n\n    /// <summary>\n    /// Gets or sets if the response trailers have been copied from the HttpResponseMessage\n    /// to the HttpResponse. Transforms use this when searching for the current value of a header they\n    /// should operate on.\n    /// </summary>\n    public bool HeadersCopied { get; set; }\n\n    /// <summary>\n    /// A <see cref=\"CancellationToken\"/> indicating that the request is being aborted.\n    /// </summary>\n    public CancellationToken CancellationToken { get; set; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseTransform.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Transforms for responses.\n/// </summary>\npublic abstract class ResponseTransform\n{\n    /// <summary>\n    /// Transforms the given response. The status and headers will have (optionally) already been\n    /// copied to the <see cref=\"HttpResponse\"/> and any changes should be made there.\n    /// </summary>\n    public abstract ValueTask ApplyAsync(ResponseTransformContext context);\n\n    /// <summary>\n    /// Removes and returns the current header value by first checking the HttpResponse\n    /// and falling back to the value from HttpResponseMessage or HttpContent only if\n    /// <see cref=\"ResponseTransformContext.HeadersCopied\"/> is not set.\n    /// This ordering allows multiple transforms to mutate the same header.\n    /// </summary>\n    /// <param name=\"context\">The transform context.</param>\n    /// <param name=\"headerName\">The name of the header to take.</param>\n    /// <returns>The response header value, or StringValues.Empty if none.</returns>\n    public static StringValues TakeHeader(ResponseTransformContext context, string headerName)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n\n        if (string.IsNullOrEmpty(headerName))\n        {\n            throw new ArgumentException($\"'{nameof(headerName)}' cannot be null or empty.\", nameof(headerName));\n        }\n\n        if (context.HttpContext.Response.Headers.TryGetValue(headerName, out var existingValues))\n        {\n            context.HttpContext.Response.Headers.Remove(headerName);\n        }\n        else if (context.ProxyResponse is { } proxyResponse && !context.HeadersCopied)\n        {\n            if (!RequestUtilities.TryGetValues(proxyResponse.Headers, headerName, out existingValues))\n            {\n                RequestUtilities.TryGetValues(proxyResponse.Content.Headers, headerName, out existingValues);\n            }\n        }\n\n        return existingValues;\n    }\n\n    /// <summary>\n    /// Sets the given header on the HttpResponse.\n    /// </summary>\n    public static void SetHeader(ResponseTransformContext context, string headerName, StringValues values)\n    {\n        context.HttpContext.Response.Headers[headerName] = values;\n    }\n\n    internal static bool Success(ResponseTransformContext context)\n    {\n        // TODO: How complex should this get? Compare with http://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header\n        return context.HttpContext.Response.StatusCode < 400;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseTransformContext.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing System.Threading;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Transform state for use with <see cref=\"ResponseTransform\"/>\n/// </summary>\npublic class ResponseTransformContext\n{\n    /// <summary>\n    /// The current request context.\n    /// </summary>\n    public HttpContext HttpContext { get; init; } = default!;\n\n    /// <summary>\n    /// The proxy response. This can be null if the destination did not respond.\n    /// When null, check <see cref=\"HttpContext.Features\"/> Get&lt;IForwarderErrorFeature&gt;() method \n    /// or <see cref=\"HttpContextFeaturesExtensions.GetForwarderErrorFeature\"/> method\n    /// for details about the error via the <see cref=\"IForwarderErrorFeature\"/>.\n    /// </summary>\n    public HttpResponseMessage? ProxyResponse { get; init; }\n\n    /// <summary>\n    /// Gets or sets if the response headers have been copied from the HttpResponseMessage and HttpContent\n    /// to the HttpResponse. Transforms use this when searching for the current value of a header they\n    /// should operate on.\n    /// </summary>\n    public bool HeadersCopied { get; set; }\n\n    /// <summary>\n    /// Set to true if the proxy should exclude the body and trailing headers when proxying this response.\n    /// Defaults to false.\n    /// </summary>\n    public bool SuppressResponseBody { get; set; }\n\n    /// <summary>\n    /// A <see cref=\"CancellationToken\"/> indicating that the request is being aborted.\n    /// </summary>\n    public CancellationToken CancellationToken { get; set; }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseTransformExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Extensions for adding response header and trailer transforms.\n/// </summary>\npublic static class ResponseTransformExtensions\n{\n    /// <summary>\n    /// Clones the route and adds the transform which will enable or suppress copying response headers to the client response.\n    /// </summary>\n    public static RouteConfig WithTransformCopyResponseHeaders(this RouteConfig route, bool copy = true)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[ResponseTransformFactory.ResponseHeadersCopyKey] = copy ? bool.TrueString : bool.FalseString;\n        });\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will enable or suppress copying response trailers to the client response.\n    /// </summary>\n    public static RouteConfig WithTransformCopyResponseTrailers(this RouteConfig route, bool copy = true)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[ResponseTransformFactory.ResponseTrailersCopyKey] = copy ? bool.TrueString : bool.FalseString;\n        });\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will append or set the response header.\n    /// </summary>\n    public static RouteConfig WithTransformResponseHeader(this RouteConfig route, string headerName, string value, bool append = true, ResponseCondition condition = ResponseCondition.Success)\n    {\n        var type = append ? ResponseTransformFactory.AppendKey : ResponseTransformFactory.SetKey;\n        return route.WithTransform(transform =>\n        {\n            transform[ResponseTransformFactory.ResponseHeaderKey] = headerName;\n            transform[type] = value;\n            transform[ResponseTransformFactory.WhenKey] = condition.ToString();\n        });\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will remove the response header.\n    /// </summary>\n    public static RouteConfig WithTransformResponseHeaderRemove(this RouteConfig route, string headerName, ResponseCondition condition = ResponseCondition.Success)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[ResponseTransformFactory.ResponseHeaderRemoveKey] = headerName;\n            transform[ResponseTransformFactory.WhenKey] = condition.ToString();\n        });\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will only copy the allowed response headers. Other transforms\n    /// that modify or append to existing headers may be affected if not included in the allow list.\n    /// </summary>\n    public static RouteConfig WithTransformResponseHeadersAllowed(this RouteConfig route, params string[] allowedHeaders)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[ResponseTransformFactory.ResponseHeadersAllowedKey] = string.Join(';', allowedHeaders);\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform which will append or set the response header.\n    /// </summary>\n    public static TransformBuilderContext AddResponseHeader(this TransformBuilderContext context, string headerName, string value, bool append = true, ResponseCondition condition = ResponseCondition.Success)\n    {\n        context.ResponseTransforms.Add(new ResponseHeaderValueTransform(headerName, value, append, condition));\n        return context;\n    }\n\n    /// <summary>\n    /// Adds the transform which will remove the response header.\n    /// </summary>\n    public static TransformBuilderContext AddResponseHeaderRemove(this TransformBuilderContext context, string headerName, ResponseCondition condition = ResponseCondition.Success)\n    {\n        context.ResponseTransforms.Add(new ResponseHeaderRemoveTransform(headerName, condition));\n        return context;\n    }\n\n    /// <summary>\n    /// Adds the transform which will only copy the allowed response headers. Other transforms\n    /// that modify or append to existing headers may be affected if not included in the allow list.\n    /// </summary>\n    public static TransformBuilderContext AddResponseHeadersAllowed(this TransformBuilderContext context, params string[] allowedHeaders)\n    {\n        context.CopyResponseHeaders = false;\n        context.ResponseTransforms.Add(new ResponseHeadersAllowedTransform(allowedHeaders));\n        return context;\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will append or set the response trailer.\n    /// </summary>\n    public static RouteConfig WithTransformResponseTrailer(this RouteConfig route, string headerName, string value, bool append = true, ResponseCondition condition = ResponseCondition.Success)\n    {\n        var type = append ? ResponseTransformFactory.AppendKey : ResponseTransformFactory.SetKey;\n        return route.WithTransform(transform =>\n        {\n            transform[ResponseTransformFactory.ResponseTrailerKey] = headerName;\n            transform[type] = value;\n            transform[ResponseTransformFactory.WhenKey] = condition.ToString();\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform which will append or set the response trailer.\n    /// </summary>\n    public static TransformBuilderContext AddResponseTrailer(this TransformBuilderContext context, string headerName, string value, bool append = true, ResponseCondition condition = ResponseCondition.Success)\n    {\n        context.ResponseTrailersTransforms.Add(new ResponseTrailerValueTransform(headerName, value, append, condition));\n        return context;\n    }\n\n    /// <summary>\n    /// Adds the transform which will remove the response trailer.\n    /// </summary>\n    public static TransformBuilderContext AddResponseTrailerRemove(this TransformBuilderContext context, string headerName, ResponseCondition condition = ResponseCondition.Success)\n    {\n        context.ResponseTrailersTransforms.Add(new ResponseTrailerRemoveTransform(headerName, condition));\n        return context;\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will remove the response trailer.\n    /// </summary>\n    public static RouteConfig WithTransformResponseTrailerRemove(this RouteConfig route, string headerName, ResponseCondition condition = ResponseCondition.Success)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[ResponseTransformFactory.ResponseTrailerRemoveKey] = headerName;\n            transform[ResponseTransformFactory.WhenKey] = condition.ToString();\n        });\n    }\n\n    /// <summary>\n    /// Clones the route and adds the transform which will only copy the allowed response trailers. Other transforms\n    /// that modify or append to existing trailers may be affected if not included in the allow list.\n    /// </summary>\n    public static RouteConfig WithTransformResponseTrailersAllowed(this RouteConfig route, params string[] allowedHeaders)\n    {\n        return route.WithTransform(transform =>\n        {\n            transform[ResponseTransformFactory.ResponseTrailersAllowedKey] = string.Join(';', allowedHeaders);\n        });\n    }\n\n    /// <summary>\n    /// Adds the transform which will only copy the allowed response trailers. Other transforms\n    /// that modify or append to existing trailers may be affected if not included in the allow list.\n    /// </summary>\n    public static TransformBuilderContext AddResponseTrailersAllowed(this TransformBuilderContext context, params string[] allowedHeaders)\n    {\n        context.CopyResponseTrailers = false;\n        context.ResponseTrailersTransforms.Add(new ResponseTrailersAllowedTransform(allowedHeaders));\n        return context;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/ResponseTransformFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\ninternal sealed class ResponseTransformFactory : ITransformFactory\n{\n    internal const string ResponseHeadersCopyKey = \"ResponseHeadersCopy\";\n    internal const string ResponseTrailersCopyKey = \"ResponseTrailersCopy\";\n    internal const string ResponseHeaderKey = \"ResponseHeader\";\n    internal const string ResponseTrailerKey = \"ResponseTrailer\";\n    internal const string ResponseHeaderRemoveKey = \"ResponseHeaderRemove\";\n    internal const string ResponseTrailerRemoveKey = \"ResponseTrailerRemove\";\n    internal const string ResponseHeadersAllowedKey = \"ResponseHeadersAllowed\";\n    internal const string ResponseTrailersAllowedKey = \"ResponseTrailersAllowed\";\n    internal const string WhenKey = \"When\";\n    internal const string AppendKey = \"Append\";\n    internal const string SetKey = \"Set\";\n\n    public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(ResponseHeadersCopyKey, out var copyHeaders))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n            if (!bool.TryParse(copyHeaders, out var _))\n            {\n                context.Errors.Add(new ArgumentException($\"Unexpected value for ResponseHeadersCopy: {copyHeaders}. Expected 'true' or 'false'\"));\n            }\n        }\n        else if (transformValues.TryGetValue(ResponseTrailersCopyKey, out copyHeaders))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n            if (!bool.TryParse(copyHeaders, out var _))\n            {\n                context.Errors.Add(new ArgumentException($\"Unexpected value for ResponseTrailersCopy: {copyHeaders}. Expected 'true' or 'false'\"));\n            }\n        }\n        else if (transformValues.TryGetValue(ResponseHeaderKey, out var _))\n        {\n            if (transformValues.TryGetValue(WhenKey, out var whenValue))\n            {\n                TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 3);\n                if (!Enum.TryParse<ResponseCondition>(whenValue, ignoreCase: true, out var _))\n                {\n                    context.Errors.Add(new ArgumentException($\"Unexpected value for ResponseHeader:When: {whenValue}. Expected 'Always', 'Success', or 'Failure'\"));\n                }\n            }\n            else\n            {\n                TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2);\n            }\n\n            if (!transformValues.TryGetValue(SetKey, out var _) && !transformValues.TryGetValue(AppendKey, out var _))\n            {\n                context.Errors.Add(new ArgumentException($\"Unexpected parameters for ResponseHeader: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'\"));\n            }\n        }\n        else if (transformValues.TryGetValue(ResponseTrailerKey, out var _))\n        {\n            if (transformValues.TryGetValue(WhenKey, out var whenValue))\n            {\n                TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 3);\n                if (!Enum.TryParse<ResponseCondition>(whenValue, ignoreCase: true, out var _))\n                {\n                    context.Errors.Add(new ArgumentException($\"Unexpected value for ResponseTrailer:When: {whenValue}. Expected 'Always', 'Success', or 'Failure'\"));\n                }\n            }\n            else\n            {\n                TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2);\n            }\n\n            if (!transformValues.TryGetValue(SetKey, out var _) && !transformValues.TryGetValue(AppendKey, out var _))\n            {\n                context.Errors.Add(new ArgumentException($\"Unexpected parameters for ResponseTrailer: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'\"));\n            }\n        }\n        else if (transformValues.TryGetValue(ResponseHeaderRemoveKey, out var _))\n        {\n            if (transformValues.TryGetValue(WhenKey, out var whenValue))\n            {\n                TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2);\n                if (!Enum.TryParse<ResponseCondition>(whenValue, ignoreCase: true, out var _))\n                {\n                    context.Errors.Add(new ArgumentException($\"Unexpected value for ResponseHeaderRemove:When: {whenValue}. Expected 'Always', 'Success', or 'Failure'\"));\n                }\n            }\n            else\n            {\n                TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n            }\n        }\n        else if (transformValues.TryGetValue(ResponseTrailerRemoveKey, out var _))\n        {\n            if (transformValues.TryGetValue(WhenKey, out var whenValue))\n            {\n                TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 2);\n                if (!Enum.TryParse<ResponseCondition>(whenValue, ignoreCase: true, out var _))\n                {\n                    context.Errors.Add(new ArgumentException($\"Unexpected value for ResponseTrailerRemove:When: {whenValue}. Expected 'Always', 'Success', or 'Failure'\"));\n                }\n            }\n            else\n            {\n                TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n            }\n        }\n        else if (transformValues.TryGetValue(ResponseHeadersAllowedKey, out var _))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n        }\n        else if (transformValues.TryGetValue(ResponseTrailersAllowedKey, out var _))\n        {\n            TransformHelpers.TryCheckTooManyParameters(context, transformValues, expected: 1);\n        }\n        else\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(ResponseHeadersCopyKey, out var copyHeaders))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            context.CopyResponseHeaders = bool.Parse(copyHeaders);\n        }\n        else if (transformValues.TryGetValue(ResponseTrailersCopyKey, out copyHeaders))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            context.CopyResponseTrailers = bool.Parse(copyHeaders);\n        }\n        else if (transformValues.TryGetValue(ResponseHeaderKey, out var responseHeaderName))\n        {\n            var condition = ResponseCondition.Success;\n            if (transformValues.TryGetValue(WhenKey, out var whenValue))\n            {\n                TransformHelpers.CheckTooManyParameters(transformValues, expected: 3);\n                condition = Enum.Parse<ResponseCondition>(whenValue, ignoreCase: true);\n            }\n            else\n            {\n                TransformHelpers.CheckTooManyParameters(transformValues, expected: 2);\n            }\n\n            if (transformValues.TryGetValue(SetKey, out var setValue))\n            {\n                context.AddResponseHeader(responseHeaderName, setValue, append: false, condition);\n            }\n            else if (transformValues.TryGetValue(AppendKey, out var appendValue))\n            {\n                context.AddResponseHeader(responseHeaderName, appendValue, append: true, condition);\n            }\n            else\n            {\n                throw new ArgumentException($\"Unexpected parameters for ResponseHeader: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'\");\n            }\n        }\n        else if (transformValues.TryGetValue(ResponseTrailerKey, out var responseTrailerName))\n        {\n            var condition = ResponseCondition.Success;\n            if (transformValues.TryGetValue(WhenKey, out var whenValue))\n            {\n                TransformHelpers.CheckTooManyParameters(transformValues, expected: 3);\n                condition = Enum.Parse<ResponseCondition>(whenValue, ignoreCase: true);\n            }\n            else\n            {\n                TransformHelpers.CheckTooManyParameters(transformValues, expected: 2);\n            }\n\n            if (transformValues.TryGetValue(SetKey, out var setValue))\n            {\n                context.AddResponseTrailer(responseTrailerName, setValue, append: false, condition);\n            }\n            else if (transformValues.TryGetValue(AppendKey, out var appendValue))\n            {\n                context.AddResponseTrailer(responseTrailerName, appendValue, append: true, condition);\n            }\n            else\n            {\n                throw new ArgumentException($\"Unexpected parameters for ResponseTrailer: {string.Join(';', transformValues.Keys)}. Expected 'Set' or 'Append'\");\n            }\n        }\n        else if (transformValues.TryGetValue(ResponseHeaderRemoveKey, out var removeResponseHeaderName))\n        {\n            var condition = ResponseCondition.Success;\n            if (transformValues.TryGetValue(WhenKey, out var whenValue))\n            {\n                TransformHelpers.CheckTooManyParameters(transformValues, expected: 2);\n                condition = Enum.Parse<ResponseCondition>(whenValue, ignoreCase: true);\n            }\n            else\n            {\n                TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            }\n\n            context.AddResponseHeaderRemove(removeResponseHeaderName, condition);\n        }\n        else if (transformValues.TryGetValue(ResponseTrailerRemoveKey, out var removeResponseTrailerName))\n        {\n            var condition = ResponseCondition.Success;\n            if (transformValues.TryGetValue(WhenKey, out var whenValue))\n            {\n                TransformHelpers.CheckTooManyParameters(transformValues, expected: 2);\n                condition = Enum.Parse<ResponseCondition>(whenValue, ignoreCase: true);\n            }\n            else\n            {\n                TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            }\n\n            context.AddResponseTrailerRemove(removeResponseTrailerName, condition);\n        }\n        else if (transformValues.TryGetValue(ResponseHeadersAllowedKey, out var allowedHeaders))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            var headersList = allowedHeaders.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);\n            context.AddResponseHeadersAllowed(headersList);\n        }\n        else if (transformValues.TryGetValue(ResponseTrailersAllowedKey, out var allowedTrailers))\n        {\n            TransformHelpers.CheckTooManyParameters(transformValues, expected: 1);\n            var headersList = allowedTrailers.Split(';', options: StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);\n            context.AddResponseTrailersAllowed(headersList);\n        }\n        else\n        {\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/RouteConfigTransformExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Extensions for adding transforms to <see cref=\"RouteConfig\"/>.\n/// </summary>\npublic static class RouteConfigTransformExtensions\n{\n    /// <summary>\n    /// Clones the <see cref=\"RouteConfig\"/> and adds the transform.\n    /// </summary>\n    /// <returns>The cloned route with the new transform.</returns>\n    public static RouteConfig WithTransform(this RouteConfig route, Action<IDictionary<string, string>> createTransform)\n    {\n        ArgumentNullException.ThrowIfNull(createTransform);\n\n        List<IReadOnlyDictionary<string, string>> transforms;\n        if (route.Transforms is null)\n        {\n            transforms = new List<IReadOnlyDictionary<string, string>>();\n        }\n        else\n        {\n            transforms = new List<IReadOnlyDictionary<string, string>>(route.Transforms.Count + 1);\n            transforms.AddRange(route.Transforms);\n        }\n\n        var transform = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);\n        createTransform(transform);\n        transforms.Add(transform);\n\n        return route with { Transforms = transforms };\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Transforms/TransformBuilderContextFuncExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms;\n\n/// <summary>\n/// Extension methods for <see cref=\"TransformBuilderContext\"/>.\n/// </summary>\npublic static class TransformBuilderContextFuncExtensions\n{\n    /// <summary>\n    /// Adds a transform Func that runs on each request for the given route.\n    /// </summary>\n    public static TransformBuilderContext AddRequestTransform(this TransformBuilderContext context, Func<RequestTransformContext, ValueTask> func)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n        ArgumentNullException.ThrowIfNull(func);\n\n        context.RequestTransforms.Add(new RequestFuncTransform(func));\n        return context;\n    }\n\n    /// <summary>\n    /// Adds a transform Func that runs on each response for the given route.\n    /// </summary>\n    public static TransformBuilderContext AddResponseTransform(this TransformBuilderContext context, Func<ResponseTransformContext, ValueTask> func)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n        ArgumentNullException.ThrowIfNull(func);\n\n        context.ResponseTransforms.Add(new ResponseFuncTransform(func));\n        return context;\n    }\n\n    /// <summary>\n    /// Adds a transform Func that runs on each response for the given route.\n    /// </summary>\n    public static TransformBuilderContext AddResponseTrailersTransform(this TransformBuilderContext context, Func<ResponseTrailersTransformContext, ValueTask> func)\n    {\n        ArgumentNullException.ThrowIfNull(context);\n        ArgumentNullException.ThrowIfNull(func);\n\n        context.ResponseTrailersTransforms.Add(new ResponseTrailersFuncTransform(func));\n        return context;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/ActivityCancellationTokenSource.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Concurrent;\nusing System.Diagnostics;\nusing System.Threading;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\ninternal sealed class ActivityCancellationTokenSource : CancellationTokenSource\n{\n    // Avoid paying the cost of updating the timeout timer if doing so won't meaningfully affect\n    // the overall timeout duration (default is 100s). This is a trade-off between precision and performance.\n    // The exact value is somewhat arbitrary, but should be large enough to avoid most timer updates.\n    private const int TimeoutResolutionMs = 20;\n\n    private const int MaxQueueSize = 1024;\n    private static readonly ConcurrentQueue<ActivityCancellationTokenSource> _sharedSources = new();\n    private static int _count;\n\n    private static readonly Action<object?> _linkedTokenCancelDelegate = static s =>\n    {\n        var cts = (ActivityCancellationTokenSource)s!;\n\n        // If a cancellation was triggered by a timeout or manual call to Cancel, it's possible that this will\n        // cascade into other tokens firing. Avoid incorrectly marking CancelledByLinkedToken in such cases.\n        if (!cts.IsCancellationRequested)\n        {\n            cts.CancelledByLinkedToken = true;\n            cts.Cancel(throwOnFirstException: false);\n        }\n    };\n\n    private int _activityTimeoutMs;\n    private uint _lastTimeoutTicks;\n    private CancellationTokenRegistration _linkedRegistration1;\n    private CancellationTokenRegistration _linkedRegistration2;\n\n    private ActivityCancellationTokenSource() { }\n\n    public bool CancelledByLinkedToken { get; private set; }\n\n    private void StartTimeout()\n    {\n        _lastTimeoutTicks = (uint)Environment.TickCount;\n        CancelAfter(_activityTimeoutMs);\n    }\n\n    public void ResetTimeout()\n    {\n        var currentMs = (uint)Environment.TickCount;\n        var elapsedMs = currentMs - _lastTimeoutTicks;\n\n        if (elapsedMs > TimeoutResolutionMs)\n        {\n            _lastTimeoutTicks = currentMs;\n            CancelAfter(_activityTimeoutMs);\n        }\n    }\n\n    public static ActivityCancellationTokenSource Rent(TimeSpan activityTimeout, CancellationToken linkedToken1 = default, CancellationToken linkedToken2 = default)\n    {\n        if (_sharedSources.TryDequeue(out var cts))\n        {\n            Interlocked.Decrement(ref _count);\n        }\n        else\n        {\n            cts = new ActivityCancellationTokenSource();\n        }\n\n        cts._activityTimeoutMs = (int)activityTimeout.TotalMilliseconds;\n        cts._linkedRegistration1 = linkedToken1.UnsafeRegister(_linkedTokenCancelDelegate, cts);\n        cts._linkedRegistration2 = linkedToken2.UnsafeRegister(_linkedTokenCancelDelegate, cts);\n        cts.StartTimeout();\n\n        return cts;\n    }\n\n    public void Return()\n    {\n        _linkedRegistration1.Dispose();\n        _linkedRegistration1 = default;\n        _linkedRegistration2.Dispose();\n        _linkedRegistration2 = default;\n\n        if (TryReset())\n        {\n            Debug.Assert(!CancelledByLinkedToken);\n\n            if (Interlocked.Increment(ref _count) <= MaxQueueSize)\n            {\n                _sharedSources.Enqueue(this);\n                return;\n            }\n\n            Interlocked.Decrement(ref _count);\n        }\n\n        Dispose();\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/AtomicCounter.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\ninternal sealed class AtomicCounter\n{\n    private int _value;\n\n    /// <summary>\n    /// Gets the current value of the counter.\n    /// </summary>\n    public int Value\n    {\n        get => Volatile.Read(ref _value);\n        set => Volatile.Write(ref _value, value);\n    }\n\n    /// <summary>\n    /// Atomically increments the counter value by 1.\n    /// </summary>\n    public int Increment()\n    {\n        return Interlocked.Increment(ref _value);\n    }\n\n    /// <summary>\n    /// Atomically decrements the counter value by 1.\n    /// </summary>\n    public int Decrement()\n    {\n        return Interlocked.Decrement(ref _value);\n    }\n\n    /// <summary>\n    /// Atomically resets the counter value to 0.\n    /// </summary>\n    public void Reset()\n    {\n        Interlocked.Exchange(ref _value, 0);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/CaseInsensitiveEqualHelper.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\ninternal static class CaseInsensitiveEqualHelper\n{\n    internal static bool Equals(IReadOnlyList<string>? list1, IReadOnlyList<string>? list2)\n    {\n        return CollectionEqualityHelper.Equals(list1, list2, StringComparer.OrdinalIgnoreCase);\n    }\n\n    internal static int GetHashCode(IReadOnlyList<string>? values)\n    {\n        return CollectionEqualityHelper.GetHashCode(values, StringComparer.OrdinalIgnoreCase);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/CaseSensitiveEqualHelper.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\ninternal static class CaseSensitiveEqualHelper\n{\n    internal static bool Equals(IReadOnlyList<string>? list1, IReadOnlyList<string>? list2)\n    {\n        return CollectionEqualityHelper.Equals(list1, list2, StringComparer.Ordinal);\n    }\n\n    internal static bool Equals(IReadOnlyDictionary<string, string>? dictionary1, IReadOnlyDictionary<string, string>? dictionary2)\n    {\n        return CollectionEqualityHelper.Equals(dictionary1, dictionary2, StringComparer.Ordinal);\n    }\n\n    internal static bool Equals(IReadOnlyList<IReadOnlyDictionary<string, string>>? dictionaryList1, IReadOnlyList<IReadOnlyDictionary<string, string>>? dictionaryList2)\n    {\n        return CollectionEqualityHelper.Equals(dictionaryList1, dictionaryList2, StringComparer.Ordinal);\n    }\n\n    internal static int GetHashCode(IReadOnlyList<string>? values)\n    {\n        return CollectionEqualityHelper.GetHashCode(values, StringComparer.Ordinal);\n    }\n\n    internal static int GetHashCode(IReadOnlyDictionary<string, string>? dictionary)\n    {\n        return CollectionEqualityHelper.GetHashCode(dictionary, StringComparer.Ordinal);\n    }\n\n    internal static int GetHashCode(IReadOnlyList<IReadOnlyDictionary<string, string>>? dictionaryList)\n    {\n        return CollectionEqualityHelper.GetHashCode(dictionaryList);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/CollectionEqualityHelper.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\ninternal static class CollectionEqualityHelper\n{\n    public static bool Equals<T>(IReadOnlyList<T>? list1, IReadOnlyList<T>? list2, IEqualityComparer<T>? valueComparer = null)\n    {\n        if (ReferenceEquals(list1, list2))\n        {\n            return true;\n        }\n\n        if (list1 is null || list2 is null)\n        {\n            return false;\n        }\n\n        if (list1.Count != list2.Count)\n        {\n            return false;\n        }\n\n        valueComparer ??= EqualityComparer<T>.Default;\n\n        for (var i = 0; i < list1.Count; i++)\n        {\n            if (!valueComparer.Equals(list1[i], list2[i]))\n            {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    public static bool Equals<T>(IReadOnlyDictionary<string, T>? dictionary1, IReadOnlyDictionary<string, T>? dictionary2, IEqualityComparer<T>? valueComparer = null)\n    {\n        if (ReferenceEquals(dictionary1, dictionary2))\n        {\n            return true;\n        }\n\n        if (dictionary1 is null || dictionary2 is null)\n        {\n            return false;\n        }\n\n        if (dictionary1.Count != dictionary2.Count)\n        {\n            return false;\n        }\n\n        if (dictionary1.Count == 0)\n        {\n            return true;\n        }\n\n        valueComparer ??= EqualityComparer<T>.Default;\n\n        foreach (var (key, value1) in dictionary1)\n        {\n            if (dictionary2.TryGetValue(key, out var value2))\n            {\n                if (!valueComparer.Equals(value1, value2))\n                {\n                    return false;\n                }\n            }\n            else\n            {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    public static bool Equals<T>(IReadOnlyList<IReadOnlyDictionary<string, T>>? dictionaryList1, IReadOnlyList<IReadOnlyDictionary<string, T>>? dictionaryList2, IEqualityComparer<T>? valueComparer = null)\n    {\n        if (ReferenceEquals(dictionaryList1, dictionaryList2))\n        {\n            return true;\n        }\n\n        if (dictionaryList1 is null || dictionaryList2 is null)\n        {\n            return false;\n        }\n\n        if (dictionaryList1.Count != dictionaryList2.Count)\n        {\n            return false;\n        }\n\n        for (var i = 0; i < dictionaryList1.Count; i++)\n        {\n            if (!Equals(dictionaryList1[i], dictionaryList2[i], valueComparer))\n            {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    public static int GetHashCode<T>(IReadOnlyList<T>? values, IEqualityComparer<T>? valueComparer = null)\n    {\n        if (values is null)\n        {\n            return 0;\n        }\n\n        valueComparer ??= EqualityComparer<T>.Default;\n\n        var hashCode = new HashCode();\n        foreach (var value in values)\n        {\n            hashCode.Add(value, valueComparer);\n        }\n        return hashCode.ToHashCode();\n    }\n\n    public static int GetHashCode<T>(IReadOnlyDictionary<string, T>? dictionary, IEqualityComparer<T>? valueComparer = null)\n    {\n        if (dictionary is null)\n        {\n            return 0;\n        }\n\n        if (dictionary.Count == 0)\n        {\n            return 42;\n        }\n\n        // We don't know what comparer the dictionary was created with, so we assume it's Ordinal/OrdinalIgnoreCase\n        // If a culture-sensitive comparer was used, this may result in GetHashCode returning different values for \"equal\" strings\n        // If that comes up as a realistic scenario, we can consider ignoring keys in the future\n        var keyComparer = StringComparer.OrdinalIgnoreCase;\n        valueComparer ??= EqualityComparer<T>.Default;\n\n        // Dictionaries are unordered collections and HashCode uses an order-sensitive algorithm (xxHash), so we have to sort the elements\n        var keys = dictionary.Keys.ToArray();\n        Array.Sort(keys, keyComparer);\n\n        var hashCode = new HashCode();\n        foreach (var key in keys)\n        {\n            hashCode.Add(key, keyComparer);\n            hashCode.Add(dictionary[key], valueComparer);\n        }\n        return hashCode.ToHashCode();\n    }\n\n    public static int GetHashCode<T>(IReadOnlyList<IReadOnlyDictionary<string, T>>? dictionaryList, IEqualityComparer<T>? valueComparer = null)\n    {\n        if (dictionaryList is null)\n        {\n            return 0;\n        }\n\n        var hashCode = new HashCode();\n        foreach (var dictionary in dictionaryList)\n        {\n            hashCode.Add(GetHashCode(dictionary, valueComparer));\n        }\n        return hashCode.ToHashCode();\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/ConcurrentDictionaryExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\ninternal static class ConcurrentDictionaryExtensions\n{\n    public static bool Contains<TKey, TValue>(this ConcurrentDictionary<TKey, TValue> dictionary, KeyValuePair<TKey, TValue> item)\n        where TKey : notnull\n    {\n        return ((ICollection<KeyValuePair<TKey, TValue>>)dictionary).Contains(item);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/DelegatingStream.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\n// Taken from https://github.com/dotnet/runtime/blob/00f37bc13b4edbba1afca9e98d74432a94f5192f/src/libraries/Common/src/System/IO/DelegatingStream.cs\n// Forwards all calls to an inner stream except where overridden in a derived class.\ninternal abstract class DelegatingStream : Stream\n{\n    private readonly Stream _innerStream;\n\n    #region Properties\n\n    public override bool CanRead\n    {\n        get { return _innerStream.CanRead; }\n    }\n\n    public override bool CanSeek\n    {\n        get { return _innerStream.CanSeek; }\n    }\n\n    public override bool CanWrite\n    {\n        get { return _innerStream.CanWrite; }\n    }\n\n    public override long Length\n    {\n        get { return _innerStream.Length; }\n    }\n\n    public override long Position\n    {\n        get { return _innerStream.Position; }\n        set { _innerStream.Position = value; }\n    }\n\n    public override int ReadTimeout\n    {\n        get { return _innerStream.ReadTimeout; }\n        set { _innerStream.ReadTimeout = value; }\n    }\n\n    public override bool CanTimeout\n    {\n        get { return _innerStream.CanTimeout; }\n    }\n\n    public override int WriteTimeout\n    {\n        get { return _innerStream.WriteTimeout; }\n        set { _innerStream.WriteTimeout = value; }\n    }\n\n    #endregion Properties\n\n    protected DelegatingStream(Stream innerStream)\n    {\n        Debug.Assert(innerStream is not null);\n        _innerStream = innerStream;\n    }\n\n    protected override void Dispose(bool disposing)\n    {\n        if (disposing)\n        {\n            _innerStream.Dispose();\n        }\n        base.Dispose(disposing);\n    }\n\n    public override ValueTask DisposeAsync()\n    {\n        return _innerStream.DisposeAsync();\n    }\n\n    #region Read\n\n    public override long Seek(long offset, SeekOrigin origin)\n    {\n        return _innerStream.Seek(offset, origin);\n    }\n\n    public override int Read(byte[] buffer, int offset, int count)\n    {\n        return _innerStream.Read(buffer, offset, count);\n    }\n\n    public override int Read(Span<byte> buffer)\n    {\n        return _innerStream.Read(buffer);\n    }\n\n    public override int ReadByte()\n    {\n        return _innerStream.ReadByte();\n    }\n\n    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)\n    {\n        return _innerStream.ReadAsync(buffer, offset, count, cancellationToken);\n    }\n\n    public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)\n    {\n        return _innerStream.ReadAsync(buffer, cancellationToken);\n    }\n\n    public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)\n    {\n        return _innerStream.BeginRead(buffer, offset, count, callback!, state);\n    }\n\n    public override int EndRead(IAsyncResult asyncResult)\n    {\n        return _innerStream.EndRead(asyncResult);\n    }\n\n    #endregion Read\n\n    #region Write\n\n    public override void Flush()\n    {\n        _innerStream.Flush();\n    }\n\n    public override Task FlushAsync(CancellationToken cancellationToken)\n    {\n        return _innerStream.FlushAsync(cancellationToken);\n    }\n\n    public override void SetLength(long value)\n    {\n        _innerStream.SetLength(value);\n    }\n\n    public override void Write(byte[] buffer, int offset, int count)\n    {\n        _innerStream.Write(buffer, offset, count);\n    }\n\n    public override void Write(ReadOnlySpan<byte> buffer)\n    {\n        _innerStream.Write(buffer);\n    }\n\n    public override void WriteByte(byte value)\n    {\n        _innerStream.WriteByte(value);\n    }\n\n    public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)\n    {\n        return _innerStream.WriteAsync(buffer, offset, count, cancellationToken);\n    }\n\n    public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)\n    {\n        return _innerStream.WriteAsync(buffer, cancellationToken);\n    }\n\n    public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)\n    {\n        return _innerStream.BeginWrite(buffer, offset, count, callback!, state);\n    }\n\n    public override void EndWrite(IAsyncResult asyncResult)\n    {\n        _innerStream.EndWrite(asyncResult);\n    }\n\n    public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)\n    {\n        return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken);\n    }\n    #endregion Write\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/EventIds.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.ReverseProxy;\n\ninternal static class EventIds\n{\n    public static readonly EventId LoadData = new EventId(1, \"ApplyProxyConfig\");\n    public static readonly EventId ErrorSignalingChange = new EventId(2, \"ApplyProxyConfigFailed\");\n    public static readonly EventId NoClusterFound = new EventId(4, \"NoClusterFound\");\n    public static readonly EventId NoAvailableDestinations = new EventId(7, \"NoAvailableDestinations\");\n    public static readonly EventId MultipleDestinationsAvailable = new EventId(8, \"MultipleDestinationsAvailable\");\n    public static readonly EventId Forwarding = new EventId(9, \"Forwarding\");\n    public static readonly EventId ExplicitActiveCheckOfAllClustersHealthFailed = new EventId(10, \"ExplicitActiveCheckOfAllClustersHealthFailed\");\n    public static readonly EventId ActiveHealthProbingFailedOnCluster = new EventId(11, \"ActiveHealthProbingFailedOnCluster\");\n    public static readonly EventId ErrorOccurredDuringActiveHealthProbingShutdownOnCluster = new EventId(12, \"ErrorOccurredDuringActiveHealthProbingShutdownOnCluster\");\n    public static readonly EventId ActiveHealthProbeConstructionFailedOnCluster = new EventId(13, \"ActiveHealthProbeConstructionFailedOnCluster\");\n    public static readonly EventId StartingActiveHealthProbingOnCluster = new EventId(14, \"StartingActiveHealthProbingOnCluster\");\n    public static readonly EventId StoppedActiveHealthProbingOnCluster = new EventId(15, \"StoppedActiveHealthProbingOnCluster\");\n    public static readonly EventId DestinationProbingCompleted = new EventId(16, \"DestinationActiveProbingCompleted\");\n    public static readonly EventId DestinationProbingFailed = new EventId(17, \"DestinationActiveProbingFailed\");\n    public static readonly EventId SendingHealthProbeToEndpointOfDestination = new EventId(18, \"SendingHealthProbeToEndpointOfDestination\");\n    public static readonly EventId UnhealthyDestinationIsScheduledForReactivation = new EventId(19, \"UnhealthyDestinationIsScheduledForReactivation\");\n    public static readonly EventId PassiveDestinationHealthResetToUnknownState = new EventId(20, \"PassiveDestinationHealthResetToUnknownState\");\n    public static readonly EventId ClusterAdded = new EventId(21, \"ClusterAdded\");\n    public static readonly EventId ClusterChanged = new EventId(22, \"ClusterChanged\");\n    public static readonly EventId ClusterRemoved = new EventId(23, \"ClusterRemoved\");\n    public static readonly EventId DestinationAdded = new EventId(24, \"EndpointAdded\");\n    public static readonly EventId DestinationChanged = new EventId(25, \"EndpointChanged\");\n    public static readonly EventId DestinationRemoved = new EventId(26, \"EndpointRemoved\");\n    public static readonly EventId RouteAdded = new EventId(27, \"RouteAdded\");\n    public static readonly EventId RouteChanged = new EventId(28, \"RouteChanged\");\n    public static readonly EventId RouteRemoved = new EventId(29, \"RouteRemoved\");\n    public static readonly EventId HttpDowngradeDetected = new EventId(30, \"HttpDowngradeDetected\");\n    public static readonly EventId OperationStarted = new EventId(31, \"OperationStarted\");\n    public static readonly EventId OperationEnded = new EventId(32, \"OperationEnded\");\n    public static readonly EventId OperationFailed = new EventId(33, \"OperationFailed\");\n    public static readonly EventId AffinityResolutionFailedForCluster = new EventId(34, \"AffinityResolutionFailedForCluster\");\n    public static readonly EventId MultipleDestinationsOnClusterToEstablishRequestAffinity = new EventId(35, \"MultipleDestinationsOnClusterToEstablishRequestAffinity\");\n    public static readonly EventId AffinityCannotBeEstablishedBecauseNoDestinationsFoundOnCluster = new EventId(36, \"AffinityCannotBeEstablishedBecauseNoDestinationsFoundOnCluster\");\n    public static readonly EventId NoDestinationOnClusterToEstablishRequestAffinity = new EventId(37, \"NoDestinationOnClusterToEstablishRequestAffinity\");\n    public static readonly EventId RequestAffinityKeyDecryptionFailed = new EventId(38, \"RequestAffinityKeyDecryptionFailed\");\n    public static readonly EventId DestinationMatchingToAffinityKeyNotFound = new EventId(39, \"DestinationMatchingToAffinityKeyNotFound\");\n    public static readonly EventId RequestAffinityHeaderHasMultipleValues = new EventId(40, \"RequestAffinityHeaderHasMultipleValues\");\n    public static readonly EventId AffinityResolutionFailureWasHandledProcessingWillBeContinued = new EventId(41, \"AffinityResolutionFailureWasHandledProcessingWillBeContinued\");\n    public static readonly EventId ClusterConfigException = new EventId(42, \"ClusterConfigException\");\n    public static readonly EventId ErrorReloadingConfig = new EventId(43, \"ErrorReloadingConfig\");\n    public static readonly EventId ErrorApplyingConfig = new EventId(44, \"ErrorApplyingConfig\");\n    public static readonly EventId ClientCreated = new EventId(45, \"ClientCreated\");\n    public static readonly EventId ClientReused = new EventId(46, \"ClientReused\");\n    public static readonly EventId ConfigurationDataConversionFailed = new EventId(47, \"ConfigurationDataConversionFailed\");\n    public static readonly EventId ForwardingError = new EventId(48, \"ForwardingError\");\n    public static readonly EventId ActiveDestinationHealthStateIsSetToUnhealthy = new EventId(49, \"ActiveDestinationHealthStateIsSetToUnhealthy\");\n    public static readonly EventId ActiveDestinationHealthStateIsSet = new EventId(50, \"ActiveDestinationHealthStateIsSet\");\n    public static readonly EventId DelegationQueueInitializationFailed = new EventId(51, \"DelegationQueueInitializationFailed\");\n    public static readonly EventId DelegationQueueNotFound = new EventId(52, \"DelegationQueueNotFound\");\n    public static readonly EventId DelegationQueueNotInitialized = new EventId(53, \"DelegationQueueNotInitialized\");\n    public static readonly EventId DelegatingRequest = new EventId(54, \"DelegatingRequest\");\n    public static readonly EventId DelegationFailed = new EventId(55, \"DelegationFailed\");\n    public static readonly EventId ResponseReceived = new EventId(56, \"ResponseReceived\");\n    public static readonly EventId DelegationQueueReset = new EventId(57, \"DelegationQueueReset\");\n    public static readonly EventId Http10RequestVersionDetected = new EventId(58, \"Http10RequestVersionDetected\");\n    public static readonly EventId NotForwarding = new EventId(59, \"NotForwarding\");\n    public static readonly EventId MaxRequestBodySizeSet = new EventId(60, \"MaxRequestBodySizeSet\");\n    public static readonly EventId RetryingWebSocketDowngradeNoConnect = new EventId(61, \"RetryingWebSocketDowngradeNoConnect\");\n    public static readonly EventId RetryingWebSocketDowngradeNoHttp2 = new EventId(62, \"RetryingWebSocketDowngradeNoHttp2\");\n    public static readonly EventId InvalidSecWebSocketKeyHeader = new EventId(63, \"InvalidSecWebSocketKeyHeader\");\n    public static readonly EventId TimeoutNotApplied = new(64, nameof(TimeoutNotApplied));\n    public static readonly EventId DelegationQueueNoLongerExists = new(65, nameof(DelegationQueueNoLongerExists));\n    public static readonly EventId ForwardingRequestCancelled = new(66, nameof(ForwardingRequestCancelled));\n    public static readonly EventId DelegationQueueDisposed = new(67, nameof(DelegationQueueDisposed));\n    public static readonly EventId ActiveHealthProbeCancelledOnDestination = new(68, nameof(ActiveHealthProbeCancelledOnDestination));\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/IClock.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\n/// <summary>\n/// Abstraction over monotonic time providers\n/// (Environment.TickCount64, Stopwatch.GetTimestamp, as opposed to DateTime).\n/// </summary>\n[Obsolete(\"For testing only. Use TimeProvider instead.\")]\npublic interface IClock\n{\n    /// <summary>\n    /// Gets the current time in UTC as a <see cref=\"DateTimeOffset\"/>.\n    /// </summary>\n    /// <returns></returns>\n    DateTimeOffset GetUtcNow();\n\n    /// <summary>\n    /// Gets a value that indicates the current tick count measured as milliseconds from an arbitrary reference time.\n    /// The default implementation leverages <see cref=\"Environment.TickCount64\"/>.\n    /// This is generally more efficient than <see cref=\"GetStopwatchTime\"/>, but provides less precision.\n    /// </summary>\n    long TickCount { get; }\n\n    /// <summary>\n    /// Gets a precise time measurement using <see cref=\"System.Diagnostics.Stopwatch\"/> as the time source.\n    /// </summary>\n    /// <returns>The time measurement.</returns>\n    TimeSpan GetStopwatchTime();\n\n    /// <summary>\n    /// Creates a cancellable task that completes after a specified time interval.\n    /// This is equivalent to <see cref=\"Task.Delay(TimeSpan, CancellationToken)\"/>,\n    /// and facilitates unit tests that use virtual time.\n    /// </summary>\n    /// <param name=\"delay\">The time span to wait before completing the returned task, or TimeSpan.FromMilliseconds(-1) to wait indefinitely.</param>\n    /// <param name=\"cancellationToken\">A cancellation token to observe while waiting for the task to complete.</param>\n    /// <returns>A task that represents the time delay.</returns>\n    Task Delay(TimeSpan delay, CancellationToken cancellationToken);\n\n    /// <summary>\n    /// Creates a cancellable task that completes after a specified time interval.\n    /// This is equivalent to <see cref=\"Task.Delay(int, CancellationToken)\"/>,\n    /// and facilitates unit tests that use virtual time.\n    /// </summary>\n    /// <param name=\"millisecondsDelay\">The number of milliseconds to wait before completing the returned task, or -1 to wait indefinitely.</param>\n    /// <param name=\"cancellationToken\">A cancellation token to observe while waiting for the task to complete.</param>\n    /// <returns>A task that represents the time delay.</returns>\n    Task Delay(int millisecondsDelay, CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/IRandomFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\n/// <summary>\n/// Factory for creating random class. This factory let us able to inject random class into other class.\n/// So that we can mock the random class for unit test.\n/// </summary>\npublic interface IRandomFactory\n{\n    /// <summary>\n    /// Create a instance of random class.\n    /// </summary>\n    Random CreateRandomInstance();\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/NullRandomFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\ninternal sealed class NullRandomFactory : IRandomFactory\n{\n    public Random CreateRandomInstance()\n    {\n        throw new NotImplementedException();\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/Observability.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Diagnostics;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\ninternal static class Observability\n{\n    public static readonly ActivitySource YarpActivitySource = new ActivitySource(\"Yarp.ReverseProxy\");\n\n    public static Activity? GetYarpActivity(this HttpContext context)\n    {\n        return context.Features[typeof(YarpActivity)] as Activity;\n    }\n\n    public static void SetYarpActivity(this HttpContext context, Activity? activity)\n    {\n        if (activity is not null)\n        {\n            context.Features[typeof(YarpActivity)] = activity;\n        }\n    }\n\n    public static void AddError(this Activity activity, string message, string description)\n    {\n        if (activity is not null)\n        {\n            var tagsCollection = new ActivityTagsCollection\n            {\n                { \"error\", message },\n                { \"description\", description }\n            };\n\n            activity.AddEvent(new ActivityEvent(\"Error\", default, tagsCollection));\n        }\n    }\n\n    private sealed class YarpActivity\n    {\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/ParsedMetadataEntry.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\ninternal sealed class ParsedMetadataEntry<T>\n{\n    private readonly Parser _parser;\n    private readonly string _metadataName;\n    private readonly ClusterState _cluster;\n    // Use a volatile field of a reference Tuple<T1, T2> type to ensure atomicity during concurrent access.\n    private volatile Tuple<string?, T>? _value;\n\n    public delegate bool Parser(string stringValue, out T parsedValue);\n\n    public ParsedMetadataEntry(Parser parser, ClusterState cluster, string metadataName)\n    {\n        ArgumentNullException.ThrowIfNull(parser);\n        ArgumentNullException.ThrowIfNull(cluster);\n        ArgumentNullException.ThrowIfNull(metadataName);\n\n        _parser = parser;\n        _cluster = cluster;\n        _metadataName = metadataName;\n    }\n\n    public T GetParsedOrDefault(T defaultValue)\n    {\n        var currentValue = _value;\n        if (_cluster.Model.Config.Metadata is not null && _cluster.Model.Config.Metadata.TryGetValue(_metadataName, out var stringValue))\n        {\n            if (currentValue is null || currentValue.Item1 != stringValue)\n            {\n                _value = Tuple.Create<string?, T>(stringValue, _parser(stringValue, out var parsedValue) ? parsedValue : defaultValue);\n            }\n        }\n        else if (currentValue is null || currentValue.Item1 is not null)\n        {\n            _value = Tuple.Create<string?, T>(null, defaultValue);\n        }\n\n        return _value!.Item2;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/RandomFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\n/// <inheritdoc/>\ninternal sealed class RandomFactory : IRandomFactory\n{\n    /// <inheritdoc/>\n    public Random CreateRandomInstance()\n    {\n        return Random.Shared;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/ServiceLookupHelper.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Frozen;\nusing System.Collections.Generic;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\ninternal static class ServiceLookupHelper\n{\n    public static FrozenDictionary<string, T> ToDictionaryByUniqueId<T>(this IEnumerable<T> services, Func<T, string> idSelector)\n    {\n        ArgumentNullException.ThrowIfNull(services);\n\n        var result = new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);\n\n        foreach (var service in services)\n        {\n            if (!result.TryAdd(idSelector(service), service))\n            {\n                throw new ArgumentException($\"More than one {typeof(T)} found with the same identifier.\", nameof(services));\n            }\n        }\n\n        return result.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);\n    }\n\n    public static T GetRequiredServiceById<T>(this FrozenDictionary<string, T> services, string? id, string defaultId)\n    {\n        var lookup = id;\n        if (string.IsNullOrEmpty(lookup))\n        {\n            lookup = defaultId;\n        }\n\n        if (!services.TryGetValue(lookup, out var result))\n        {\n            throw new ArgumentException($\"No {typeof(T)} was found for the id '{lookup}'.\", nameof(id));\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/SkipLocalsInit.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\n// Used to indicate to the compiler that the .locals init flag should not be set in method headers.\n[module: System.Runtime.CompilerServices.SkipLocalsInit]\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/TaskUtilities.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\ninternal static class TaskUtilities\n{\n    internal static readonly Task<bool> TrueTask = Task.FromResult(true);\n    internal static readonly Task<bool> FalseTask = Task.FromResult(false);\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/TlsFrameHelper.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n// See the LICENSE file in the project root for more information.\n\nusing System;\nusing System.Diagnostics;\nusing System.Buffers.Binary;\nusing System.Globalization;\nusing System.Net.Security;\nusing System.Security.Authentication;\nusing System.Text;\n\nnamespace Yarp.ReverseProxy.Utilities.Tls;\n\n// SSL3/TLS protocol frames definitions.\npublic enum TlsContentType : byte\n{\n    ChangeCipherSpec = 20,\n    Alert = 21,\n    Handshake = 22,\n    AppData = 23\n}\n\npublic enum TlsHandshakeType : byte\n{\n    HelloRequest = 0,\n    ClientHello = 1,\n    ServerHello = 2,\n    NewSessionTicket = 4,\n    EndOfEarlyData = 5,\n    EncryptedExtensions = 8,\n    Certificate = 11,\n    ServerKeyExchange = 12,\n    CertificateRequest = 13,\n    ServerHelloDone = 14,\n    CertificateVerify = 15,\n    ClientKeyExchange = 16,\n    Finished = 20,\n    KeyUpdate = 24,\n    MessageHash = 254\n}\n\npublic enum TlsAlertLevel : byte\n{\n    Warning = 1,\n    Fatal = 2,\n}\n\npublic enum TlsAlertDescription : byte\n{\n    CloseNotify = 0, // warning\n    UnexpectedMessage = 10, // error\n    BadRecordMac = 20, // error\n    DecryptionFailed = 21, // reserved\n    RecordOverflow = 22, // error\n    DecompressionFail = 30, // error\n    HandshakeFailure = 40, // error\n    BadCertificate = 42, // warning or error\n    UnsupportedCert = 43, // warning or error\n    CertificateRevoked = 44, // warning or error\n    CertificateExpired = 45, // warning or error\n    CertificateUnknown = 46, // warning or error\n    IllegalParameter = 47, // error\n    UnknownCA = 48, // error\n    AccessDenied = 49, // error\n    DecodeError = 50, // error\n    DecryptError = 51, // error\n    ExportRestriction = 60, // reserved\n    ProtocolVersion = 70, // error\n    InsufficientSecurity = 71, // error\n    InternalError = 80, // error\n    UserCanceled = 90, // warning or error\n    NoRenegotiation = 100, // warning\n    UnsupportedExt = 110, // error\n}\n\npublic enum ExtensionType : ushort\n{\n    ServerName = 0,\n    MaximumFragmentLength = 1,\n    ClientCertificateUrl = 2,\n    TrustedCaKeys = 3,\n    TruncatedHmac = 4,\n    CertificateStatusRequest = 5,\n    ApplicationProtocols = 16,\n    SupportedVersions = 43\n}\n\npublic struct TlsFrameHeader\n{\n    public TlsContentType Type;\n    public SslProtocols Version;\n    public int Length;\n\n    public override string ToString() => $\"{Version}:{Type}[{Length}]\";\n}\n\npublic static class TlsFrameHelper\n{\n    public const int HeaderSize = 5;\n\n    [Flags]\n    public enum ProcessingOptions\n    {\n        ServerName = 0x1,\n        ApplicationProtocol = 0x2,\n        Versions = 0x4,\n        CipherSuites = 0x8,\n        All = 0x7FFFFFFF,\n    }\n\n    [Flags]\n    public enum ApplicationProtocolInfo\n    {\n        None = 0,\n        Http11 = 1,\n        Http2 = 2,\n        Other = 128\n    }\n\n    public enum ParsingStatus\n    {\n        Ok = 0,\n        IncompleteFrame = 1,\n        InvalidFrame = 2,\n        UnsupportedFrame = 3,\n    }\n\n    public struct TlsFrameInfo\n    {\n        internal TlsCipherSuite[]? _ciphers;\n        public TlsFrameHeader Header;\n        public TlsHandshakeType HandshakeType;\n        public SslProtocols SupportedVersions;\n        public string TargetName;\n        public ApplicationProtocolInfo ApplicationProtocols;\n        public TlsAlertDescription AlertDescription;\n        public ParsingStatus ParsingStatus;\n        public ReadOnlyMemory<TlsCipherSuite> TlsCipherSuites\n        {\n            get\n            {\n                return _ciphers is null ? ReadOnlyMemory<TlsCipherSuite>.Empty : new ReadOnlyMemory<TlsCipherSuite>(_ciphers);\n            }\n        }\n\n        public override string ToString()\n        {\n            if (Header.Type == TlsContentType.Handshake)\n            {\n                if (HandshakeType == TlsHandshakeType.ClientHello)\n                {\n                    return $\"{Header.Version}:{HandshakeType}[{Header.Length}] TargetName='{TargetName}' SupportedVersion='{SupportedVersions}' ApplicationProtocols='{ApplicationProtocols}'\";\n                }\n                else if (HandshakeType == TlsHandshakeType.ServerHello)\n                {\n                    return $\"{Header.Version}:{HandshakeType}[{Header.Length}] SupportedVersion='{SupportedVersions}' ApplicationProtocols='{ApplicationProtocols}'\";\n                }\n                else\n                {\n                    return $\"{Header.Version}:{HandshakeType}[{Header.Length}] SupportedVersion='{SupportedVersions}'\";\n                }\n            }\n            else\n            {\n                return $\"{Header.Version}:{Header.Type}[{Header.Length}]\";\n            }\n        }\n    }\n\n    public delegate bool HelloExtensionCallback(ref TlsFrameInfo info, ExtensionType type, ReadOnlySpan<byte> extensionsData);\n\n    private static readonly byte[] s_protocolMismatch13 = new byte[] { (byte)TlsContentType.Alert, 3, 4, 0, 2, 2, 70 };\n    private static readonly byte[] s_protocolMismatch12 = new byte[] { (byte)TlsContentType.Alert, 3, 3, 0, 2, 2, 70 };\n    private static readonly byte[] s_protocolMismatch11 = new byte[] { (byte)TlsContentType.Alert, 3, 2, 0, 2, 2, 70 };\n    private static readonly byte[] s_protocolMismatch10 = new byte[] { (byte)TlsContentType.Alert, 3, 1, 0, 2, 2, 70 };\n    private static readonly byte[] s_protocolMismatch30 = new byte[] { (byte)TlsContentType.Alert, 3, 0, 0, 2, 2, 40 };\n\n    private const int UInt24Size = 3;\n    private const int RandomSize = 32;\n    private const int OpaqueType1LengthSize = sizeof(byte);\n    private const int OpaqueType2LengthSize = sizeof(ushort);\n    private const int ProtocolVersionMajorOffset = 0;\n    private const int ProtocolVersionMinorOffset = 1;\n    private const int ProtocolVersionSize = 2;\n    private const int ProtocolVersionTlsMajorValue = 3;\n\n    // Per spec \"AllowUnassigned flag MUST be set\". See comment above DecodeString() for more details.\n    private static readonly IdnMapping s_idnMapping = new IdnMapping() { AllowUnassigned = true };\n    private static readonly Encoding s_encoding = Encoding.GetEncoding(\"utf-8\", new EncoderExceptionFallback(), new DecoderExceptionFallback());\n\n    public static bool TryGetFrameHeader(ReadOnlySpan<byte> frame, ref TlsFrameHeader header)\n    {\n        var result = frame.Length > 4;\n\n        if (frame.Length >= 1)\n        {\n            header.Type = (TlsContentType)frame[0];\n\n            if (frame.Length > 4)\n            {\n                // SSLv3, TLS or later\n                if (frame[1] == 3)\n                {\n                    header.Length = ((frame[3] << 8) | frame[4]);\n                    header.Version = TlsMinorVersionToProtocol(frame[2]);\n                    return true;\n                }\n                // May be SSL3/TLS frame wrapped in unified header.\n                else if (frame[2] == (byte)TlsHandshakeType.ClientHello &&\n                        frame[3] == 3) // SSL3 or above\n                {\n                    int length;\n                    if ((frame[0] & 0x80) != 0)\n                    {\n                        // Two bytes\n                        length = (((frame[0] & 0x7f) << 8) | frame[1]) + 2;\n                    }\n                    else\n                    {\n                        // Three bytes\n                        length = (((frame[0] & 0x3f) << 8) | frame[1]) + 3;\n                    }\n\n                    // max frame for SSLv2 is 32767.\n                    // However, we expect something reasonable for initial HELLO\n                    // We don't have enough logic to verify full validity,\n                    // the limits below are guesses.\n                    if (length > 20 && length < 1000)\n                    {\n#pragma warning disable CS0618 // Ssl2 and Ssl3 are obsolete\n                        header.Version = SslProtocols.Ssl2;\n#pragma warning restore CS0618\n                        header.Length = length;\n                        header.Type = TlsContentType.Handshake;\n                        return true;\n                    }\n                }\n            }\n        }\n\n        header.Length = -1;\n        header.Version = SslProtocols.None;\n\n        return result;\n    }\n\n    // Returns frame size e.g. header + content\n    public static int GetFrameSize(ReadOnlySpan<byte> frame)\n    {\n        if (frame.Length < 5 || frame[1] < 3)\n        {\n            return -1;\n        }\n\n        return ((frame[3] << 8) | frame[4]) + HeaderSize;\n    }\n\n    // This function will try to parse TLS hello frame and fill details in provided info structure.\n    // If frame was fully processed without any error, function returns true.\n    // Otherwise, it returns false and info may have partial data.\n    // It is OK to call it again if more data becomes available.\n    // It is also possible to limit what information is processed.\n    // If callback delegate is provided, it will be called on ALL extensions.\n    public static bool TryGetFrameInfo(ReadOnlySpan<byte> frame, ref TlsFrameInfo info, ProcessingOptions options = ProcessingOptions.All, HelloExtensionCallback? callback = null)\n    {\n        const int HandshakeTypeOffset = 5;\n        if (frame.Length < HeaderSize)\n        {\n            info.ParsingStatus = ParsingStatus.IncompleteFrame;\n            return false;\n        }\n\n        // This will not fail since we have enough data.\n        var gotHeader = TryGetFrameHeader(frame, ref info.Header);\n        Debug.Assert(gotHeader);\n\n        info.SupportedVersions = info.Header.Version;\n#pragma warning disable CS0618 // Ssl2 and Ssl3 are obsolete\n        if (info.Header.Version == SslProtocols.Ssl2)\n        {\n            // This is safe. We would not get here if the length is too small.\n            info.SupportedVersions |= TlsMinorVersionToProtocol(frame[4]);\n            // We only recognize Unified ClientHello at the moment.\n            // This is needed to trigger certificate selection callback in SslStream.\n            info.HandshakeType = TlsHandshakeType.ClientHello;\n            // There is no more parsing for old protocols.\n            return true;\n        }\n#pragma warning restore CS0618\n\n        if (info.Header.Type == TlsContentType.Alert)\n        {\n            TlsAlertLevel level = default;\n            TlsAlertDescription description = default;\n            if (TryGetAlertInfo(frame, ref level, ref description))\n            {\n                info.AlertDescription = description;\n                info.ParsingStatus = ParsingStatus.Ok;\n                return true;\n            }\n\n            info.ParsingStatus = ParsingStatus.IncompleteFrame;\n            return false;\n        }\n\n        if (info.Header.Type != TlsContentType.Handshake)\n        {\n            info.ParsingStatus = ParsingStatus.UnsupportedFrame;\n            return false;\n        }\n\n        if (frame.Length <= HandshakeTypeOffset)\n        {\n            info.ParsingStatus = ParsingStatus.IncompleteFrame;\n            return false;\n        }\n\n        info.HandshakeType = (TlsHandshakeType)frame[HandshakeTypeOffset];\n        // Check if we have full frame.\n        var isComplete = frame.Length >= HeaderSize + info.Header.Length;\n        info.ParsingStatus = isComplete ? ParsingStatus.Ok : ParsingStatus.IncompleteFrame;\n\n#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete\n        if (((int)info.Header.Version >= (int)SslProtocols.Tls) &&\n#pragma warning restore SYSLIB0039\n            (info.HandshakeType == TlsHandshakeType.ClientHello || info.HandshakeType == TlsHandshakeType.ServerHello))\n        {\n            if (!TryParseHelloFrame(frame.Slice(HeaderSize, Math.Min(info.Header.Length, frame.Length - HeaderSize)), ref info, options, callback))\n            {\n                isComplete = false;\n            }\n        }\n\n        return isComplete;\n    }\n\n    // This is similar to TryGetFrameInfo, but it will only process SNI.\n    // It returns TargetName as string or NULL if SNI is missing or parsing error happened.\n    public static string? GetServerName(ReadOnlySpan<byte> frame)\n    {\n        TlsFrameInfo info = default;\n        if (!TryGetFrameInfo(frame, ref info, ProcessingOptions.ServerName))\n        {\n            return null;\n        }\n\n        return info.TargetName;\n    }\n\n    // This function will parse the TLS Alert message, and return the alert level and description.\n    public static bool TryGetAlertInfo(ReadOnlySpan<byte> frame, ref TlsAlertLevel level, ref TlsAlertDescription description)\n    {\n        if (frame.Length < 7 || frame[0] != (byte)TlsContentType.Alert)\n        {\n            return false;\n        }\n\n        level = (TlsAlertLevel)frame[5];\n        description = (TlsAlertDescription)frame[6];\n\n        return true;\n    }\n\n    private static byte[] CreateProtocolVersionAlert(SslProtocols version) =>\n        version switch\n        {\n            SslProtocols.Tls13 => s_protocolMismatch13,\n            SslProtocols.Tls12 => s_protocolMismatch12,\n#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete\n            SslProtocols.Tls11 => s_protocolMismatch11,\n            SslProtocols.Tls => s_protocolMismatch10,\n#pragma warning restore SYSLIB0039\n#pragma warning disable 0618\n            SslProtocols.Ssl3 => s_protocolMismatch30,\n#pragma warning restore 0618\n            _ => Array.Empty<byte>(),\n        };\n\n    public static byte[] CreateAlertFrame(SslProtocols version, TlsAlertDescription reason)\n    {\n        if (reason == TlsAlertDescription.ProtocolVersion)\n        {\n            return CreateProtocolVersionAlert(version);\n        }\n#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete\n        else if ((int)version > (int)SslProtocols.Tls)\n        {\n            // Create TLS1.2 alert\n            var buffer = new byte[] { (byte)TlsContentType.Alert, 3, 3, 0, 2, 2, (byte)reason };\n            switch (version)\n            {\n                case SslProtocols.Tls13:\n                    buffer[2] = 4;\n                    break;\n                case SslProtocols.Tls11:\n                    buffer[2] = 2;\n                    break;\n                case SslProtocols.Tls:\n                    buffer[2] = 1;\n                    break;\n            }\n#pragma warning restore SYSLIB0039\n            return buffer;\n        }\n\n        return Array.Empty<byte>();\n    }\n\n    private static bool TryParseHelloFrame(ReadOnlySpan<byte> sslHandshake, ref TlsFrameInfo info, ProcessingOptions options, HelloExtensionCallback? callback)\n    {\n        // https://tools.ietf.org/html/rfc6101#section-5.6\n        // struct {\n        //     HandshakeType msg_type;    /* handshake type */\n        //     uint24 length;             /* bytes in message */\n        //     select (HandshakeType) {\n        //         ...\n        //         case client_hello: ClientHello;\n        //         case server_hello: ServerHello;\n        //         ...\n        //     } body;\n        // } Handshake;\n        const int HandshakeTypeOffset = 0;\n        const int HelloLengthOffset = HandshakeTypeOffset + sizeof(TlsHandshakeType);\n        const int HelloOffset = HelloLengthOffset + UInt24Size;\n        const int HandshakeHeaderLength = 4;   // Type and Handshake length\n        const int MinimalHandshakeLength = 44; // Version, Random, SessionID and Cipher length with at least one cipher\n\n        if (info.Header.Length - HandshakeHeaderLength < MinimalHandshakeLength)\n        {\n            info.ParsingStatus = ParsingStatus.InvalidFrame;\n            return false;\n        }\n\n        if (sslHandshake.Length < HelloOffset + 3)\n        {\n            info.ParsingStatus = ParsingStatus.IncompleteFrame;\n            return false;\n        }\n\n        if ((TlsHandshakeType)sslHandshake[HandshakeTypeOffset] != TlsHandshakeType.ClientHello &&\n             (TlsHandshakeType)sslHandshake[HandshakeTypeOffset] != TlsHandshakeType.ServerHello)\n        {\n            info.ParsingStatus = ParsingStatus.UnsupportedFrame;\n            return false;\n        }\n\n        var helloLength = ReadUInt24BigEndian(sslHandshake.Slice(HelloLengthOffset));\n        if (helloLength < MinimalHandshakeLength || helloLength > info.Header.Length - HandshakeHeaderLength)\n        {\n            info.ParsingStatus = ParsingStatus.InvalidFrame;\n            return false;\n        }\n\n        var helloData = sslHandshake.Slice(HelloOffset);\n        if (helloData.Length < helloLength)\n        {\n            info.ParsingStatus = ParsingStatus.IncompleteFrame;\n            return false;\n        }\n\n        // ProtocolVersion may be different from frame header.\n        if (helloData[ProtocolVersionMajorOffset] == ProtocolVersionTlsMajorValue)\n        {\n            info.SupportedVersions |= TlsMinorVersionToProtocol(helloData[ProtocolVersionMinorOffset]);\n        }\n\n        return (TlsHandshakeType)sslHandshake[HandshakeTypeOffset] == TlsHandshakeType.ClientHello ?\n                    TryParseClientHello(helloData.Slice(0, helloLength), ref info, options, callback) :\n                    TryParseServerHello(helloData.Slice(0, helloLength), ref info, options, callback);\n    }\n\n    private static bool TryParseClientHello(ReadOnlySpan<byte> clientHello, ref TlsFrameInfo info, ProcessingOptions options, HelloExtensionCallback? callback)\n    {\n        // Basic structure: https://tools.ietf.org/html/rfc6101#section-5.6.1.2\n        // Extended structure: https://tools.ietf.org/html/rfc3546#section-2.1\n        // struct {\n        //     ProtocolVersion client_version; // 2x uint8\n        //     Random random; // 32 bytes\n        //     SessionID session_id; // opaque type\n        //     CipherSuite cipher_suites<2..2^16-1>; // opaque type\n        //     CompressionMethod compression_methods<1..2^8-1>; // opaque type\n        //     Extension client_hello_extension_list<0..2^16-1>;\n        // } ClientHello;\n\n        var p = SkipBytes(clientHello, ProtocolVersionSize + RandomSize);\n\n        // Skip SessionID (max size 32 => size fits in 1 byte)\n        p = SkipOpaqueType1(p);\n\n        if (options.HasFlag(ProcessingOptions.CipherSuites))\n        {\n            TryGetCipherSuites(p, ref info);\n        }\n        // Skip cipher suites (max size 2^16-1 => size fits in 2 bytes)\n        p = SkipOpaqueType2(p);\n\n        // Skip compression methods (max size 2^8-1 => size fits in 1 byte)\n        p = SkipOpaqueType1(p);\n\n        // no extension\n        if (p.IsEmpty)\n        {\n            return true;\n        }\n\n        // client_hello_extension_list (max size 2^16-1 => size fits in 2 bytes)\n        int extensionListLength = BinaryPrimitives.ReadUInt16BigEndian(p);\n        p = SkipBytes(p, sizeof(ushort));\n        if (extensionListLength != p.Length)\n        {\n            return false;\n        }\n\n        return TryParseHelloExtensions(p, ref info, options, callback);\n    }\n\n    private static bool TryParseServerHello(ReadOnlySpan<byte> serverHello, ref TlsFrameInfo info, ProcessingOptions options, HelloExtensionCallback? callback)\n    {\n        // Basic structure: https://tools.ietf.org/html/rfc6101#section-5.6.1.3\n        // Extended structure: https://tools.ietf.org/html/rfc3546#section-2.2\n        // struct {\n        //   ProtocolVersion server_version;\n        //   Random random;\n        //   SessionID session_id;\n        //   CipherSuite cipher_suite;\n        //   CompressionMethod compression_method;\n        //   Extension server_hello_extension_list<0..2^16-1>;\n        // }\n        // ServerHello;\n        const int CipherSuiteLength = 2;\n        const int CompressionMethodLength = 1;\n\n        var p = SkipBytes(serverHello, ProtocolVersionSize + RandomSize);\n        // Skip SessionID (max size 32 => size fits in 1 byte)\n        p = SkipOpaqueType1(p);\n        p = SkipBytes(p, CipherSuiteLength + CompressionMethodLength);\n\n        // is invalid structure or no extensions?\n        if (p.IsEmpty)\n        {\n            return false;\n        }\n\n        // client_hello_extension_list (max size 2^16-1 => size fits in 2 bytes)\n        int extensionListLength = BinaryPrimitives.ReadUInt16BigEndian(p);\n        p = SkipBytes(p, sizeof(ushort));\n        if (extensionListLength != p.Length)\n        {\n            return false;\n        }\n\n        return TryParseHelloExtensions(p, ref info, options, callback);\n    }\n\n    // This is common for ClientHello and ServerHello.\n    private static bool TryParseHelloExtensions(ReadOnlySpan<byte> extensions, ref TlsFrameInfo info, ProcessingOptions options, HelloExtensionCallback? callback)\n    {\n        const int ExtensionHeader = 4;\n        var isComplete = true;\n\n        while (extensions.Length >= ExtensionHeader)\n        {\n            var extensionType = (ExtensionType)BinaryPrimitives.ReadUInt16BigEndian(extensions);\n            extensions = SkipBytes(extensions, sizeof(ushort));\n\n            var extensionLength = BinaryPrimitives.ReadUInt16BigEndian(extensions);\n            extensions = SkipBytes(extensions, sizeof(ushort));\n            if (extensions.Length < extensionLength)\n            {\n                isComplete = false;\n                break;\n            }\n\n            var extensionData = extensions.Slice(0, extensionLength);\n\n            if (extensionType == ExtensionType.ServerName && options.HasFlag(ProcessingOptions.ServerName))\n            {\n                if (!TryGetSniFromServerNameList(extensionData, out var sni))\n                {\n                    return false;\n                }\n\n                info.TargetName = sni!;\n            }\n            else if (extensionType == ExtensionType.SupportedVersions && options.HasFlag(ProcessingOptions.Versions))\n            {\n                if (!TryGetSupportedVersionsFromExtension(extensionData, out var versions))\n                {\n                    return false;\n                }\n\n                info.SupportedVersions |= versions;\n            }\n            else if (extensionType == ExtensionType.ApplicationProtocols && options.HasFlag(ProcessingOptions.ApplicationProtocol))\n            {\n                if (!TryGetApplicationProtocolsFromExtension(extensionData, out var alpn))\n                {\n                    return false;\n                }\n\n                info.ApplicationProtocols |= alpn;\n            }\n\n            callback?.Invoke(ref info, extensionType, extensionData);\n            extensions = extensions.Slice(extensionLength);\n        }\n\n        return isComplete;\n    }\n\n    private static bool TryGetSniFromServerNameList(ReadOnlySpan<byte> serverNameListExtension, out string? sni)\n    {\n        // https://tools.ietf.org/html/rfc3546#section-3.1\n        // struct {\n        //     ServerName server_name_list<1..2^16-1>\n        // } ServerNameList;\n        // ServerNameList is an opaque type (length of sufficient size for max data length is prepended)\n        const int ServerNameListOffset = sizeof(ushort);\n        sni = null;\n\n        if (serverNameListExtension.Length < ServerNameListOffset)\n        {\n            return false;\n        }\n\n        int serverNameListLength = BinaryPrimitives.ReadUInt16BigEndian(serverNameListExtension);\n        var serverNameList = serverNameListExtension.Slice(ServerNameListOffset);\n\n        if (serverNameListLength != serverNameList.Length)\n        {\n            return false;\n        }\n\n        var serverName = serverNameList.Slice(0, serverNameListLength);\n\n        sni = GetSniFromServerName(serverName, out var invalid);\n        return invalid == false;\n    }\n\n    private static string? GetSniFromServerName(ReadOnlySpan<byte> serverName, out bool invalid)\n    {\n        // https://tools.ietf.org/html/rfc3546#section-3.1\n        // struct {\n        //     NameType name_type;\n        //     select (name_type) {\n        //         case host_name: HostName;\n        //     } name;\n        // } ServerName;\n        // ServerName is an opaque type (length of sufficient size for max data length is prepended)\n        const int NameTypeOffset = 0;\n        const int HostNameStructOffset = NameTypeOffset + sizeof(NameType);\n        if (serverName.Length < HostNameStructOffset)\n        {\n            invalid = true;\n            return null;\n        }\n\n        // Following can underflow but it is ok due to equality check below\n        var nameType = (NameType)serverName[NameTypeOffset];\n        var hostNameStruct = serverName.Slice(HostNameStructOffset);\n        if (nameType != NameType.HostName)\n        {\n            invalid = true;\n            return null;\n        }\n\n        return GetSniFromHostNameStruct(hostNameStruct, out invalid);\n    }\n\n    private static string? GetSniFromHostNameStruct(ReadOnlySpan<byte> hostNameStruct, out bool invalid)\n    {\n        // https://tools.ietf.org/html/rfc3546#section-3.1\n        // HostName is an opaque type (length of sufficient size for max data length is prepended)\n        const int HostNameLengthOffset = 0;\n        const int HostNameOffset = HostNameLengthOffset + sizeof(ushort);\n\n        int hostNameLength = BinaryPrimitives.ReadUInt16BigEndian(hostNameStruct);\n        var hostName = hostNameStruct.Slice(HostNameOffset);\n        if (hostNameLength != hostName.Length)\n        {\n            invalid = true;\n            return null;\n        }\n\n        invalid = false;\n        return DecodeString(hostName);\n    }\n\n    private static bool TryGetSupportedVersionsFromExtension(ReadOnlySpan<byte> extensionData, out SslProtocols protocols)\n    {\n        // https://tools.ietf.org/html/rfc8446#section-4.2.1\n        // struct {\n        // select(Handshake.msg_type) {\n        //  case client_hello:\n        //    ProtocolVersion versions<2..254 >;\n        //\n        //  case server_hello: /* and HelloRetryRequest */\n        //    ProtocolVersion selected_version;\n        // };\n        const int VersionListLengthOffset = 0;\n        const int VersionListNameOffset = VersionListLengthOffset + sizeof(byte);\n        const int VersionLength = 2;\n\n        protocols = SslProtocols.None;\n\n        var supportedVersionLength = extensionData[VersionListLengthOffset];\n        extensionData = extensionData.Slice(VersionListNameOffset);\n\n        if (extensionData.Length != supportedVersionLength)\n        {\n            return false;\n        }\n\n        // Get list of protocols we support. Ignore the rest.\n        while (extensionData.Length >= VersionLength)\n        {\n            if (extensionData[ProtocolVersionMajorOffset] == ProtocolVersionTlsMajorValue)\n            {\n                protocols |= TlsMinorVersionToProtocol(extensionData[ProtocolVersionMinorOffset]);\n            }\n\n            extensionData = extensionData.Slice(VersionLength);\n        }\n\n        return true;\n    }\n\n    private static bool TryGetApplicationProtocolsFromExtension(ReadOnlySpan<byte> extensionData, out ApplicationProtocolInfo alpn)\n    {\n        // https://tools.ietf.org/html/rfc7301#section-3.1\n        // opaque ProtocolName<1..2 ^ 8 - 1 >;\n        //\n        // struct {\n        //   ProtocolName protocol_name_list<2..2^16-1>\n        // }\n        // ProtocolNameList;\n        const int AlpnListLengthOffset = 0;\n        const int AlpnListOffset = AlpnListLengthOffset + sizeof(short);\n\n        alpn = ApplicationProtocolInfo.None;\n\n        if (extensionData.Length < AlpnListOffset)\n        {\n            return false;\n        }\n\n        int AlpnListLength = BinaryPrimitives.ReadUInt16BigEndian(extensionData);\n        var alpnList = extensionData.Slice(AlpnListOffset);\n        if (AlpnListLength != alpnList.Length)\n        {\n            return false;\n        }\n\n        while (!alpnList.IsEmpty)\n        {\n            var protocolLength = alpnList[0];\n            if (alpnList.Length < protocolLength + 1)\n            {\n                return false;\n            }\n\n            var protocol = alpnList.Slice(1, protocolLength);\n            if (protocolLength == 2)\n            {\n                if (protocol.SequenceEqual(SslApplicationProtocol.Http2.Protocol.Span))\n                {\n                    alpn |= ApplicationProtocolInfo.Http2;\n                }\n                else\n                {\n                    alpn |= ApplicationProtocolInfo.Other;\n                }\n            }\n            else if (protocolLength == SslApplicationProtocol.Http11.Protocol.Length &&\n                     protocol.SequenceEqual(SslApplicationProtocol.Http11.Protocol.Span))\n            {\n                alpn |= ApplicationProtocolInfo.Http11;\n            }\n            else\n            {\n                alpn |= ApplicationProtocolInfo.Other;\n            }\n\n            alpnList = alpnList.Slice(protocolLength + 1);\n        }\n\n        return true;\n    }\n\n    private static bool TryGetCipherSuites(ReadOnlySpan<byte> bytes, ref TlsFrameInfo info)\n    {\n        if (bytes.Length < OpaqueType2LengthSize)\n        {\n            return false;\n        }\n\n        var length = BinaryPrimitives.ReadUInt16BigEndian(bytes);\n        if (bytes.Length < OpaqueType2LengthSize + length)\n        {\n            return false;\n        }\n\n        bytes = bytes.Slice(OpaqueType2LengthSize, length);\n        var count = length / 2;\n\n        info._ciphers = new TlsCipherSuite[count];\n        for (var i = 0; i < count; i++)\n        {\n            info._ciphers[i] = (TlsCipherSuite)BinaryPrimitives.ReadUInt16BigEndian(bytes.Slice(i * 2, 2));\n        }\n\n        return true;\n    }\n\n    private static SslProtocols TlsMinorVersionToProtocol(byte value)\n    {\n        return value switch\n        {\n            4 => SslProtocols.Tls13,\n            3 => SslProtocols.Tls12,\n#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete\n            2 => SslProtocols.Tls11,\n            1 => SslProtocols.Tls,\n#pragma warning restore SYSLIB0039\n#pragma warning disable 0618\n            0 => SslProtocols.Ssl3,\n#pragma warning restore 0618\n            _ => SslProtocols.None,\n        };\n    }\n\n    private static string? DecodeString(ReadOnlySpan<byte> bytes)\n    {\n        // https://tools.ietf.org/html/rfc3546#section-3.1\n        // Per spec:\n        //   If the hostname labels contain only US-ASCII characters, then the\n        //   client MUST ensure that labels are separated only by the byte 0x2E,\n        //   representing the dot character U+002E (requirement 1 in section 3.1\n        //   of [IDNA] notwithstanding). If the server needs to match the HostName\n        //   against names that contain non-US-ASCII characters, it MUST perform\n        //   the conversion operation described in section 4 of [IDNA], treating\n        //   the HostName as a \"query string\" (i.e. the AllowUnassigned flag MUST\n        //   be set). Note that IDNA allows labels to be separated by any of the\n        //   Unicode characters U+002E, U+3002, U+FF0E, and U+FF61, therefore\n        //   servers MUST accept any of these characters as a label separator.  If\n        //   the server only needs to match the HostName against names containing\n        //   exclusively ASCII characters, it MUST compare ASCII names case-\n        //   insensitively.\n\n        string idnEncodedString;\n        try\n        {\n            idnEncodedString = s_encoding.GetString(bytes);\n        }\n        catch (DecoderFallbackException)\n        {\n            return null;\n        }\n\n        try\n        {\n            return s_idnMapping.GetUnicode(idnEncodedString);\n        }\n        catch (ArgumentException)\n        {\n            // client has not done IDN mapping\n            return idnEncodedString;\n        }\n    }\n\n    private static int ReadUInt24BigEndian(ReadOnlySpan<byte> bytes)\n    {\n        return (bytes[0] << 16) | (bytes[1] << 8) | bytes[2];\n    }\n\n    private static ReadOnlySpan<byte> SkipBytes(ReadOnlySpan<byte> bytes, int numberOfBytesToSkip)\n    {\n        return (numberOfBytesToSkip < bytes.Length) ? bytes.Slice(numberOfBytesToSkip) : ReadOnlySpan<byte>.Empty;\n    }\n\n    // Opaque type is of structure:\n    //   - length (minimum number of bytes to hold the max value)\n    //   - data (length bytes)\n    // We will only use opaque types which are of max size: 255 (length = 1) or 2^16-1 (length = 2).\n    // We will call them SkipOpaqueType`length`\n    private static ReadOnlySpan<byte> SkipOpaqueType1(ReadOnlySpan<byte> bytes)\n    {\n        if (bytes.Length < OpaqueType1LengthSize)\n        {\n            return ReadOnlySpan<byte>.Empty;\n        }\n\n        var length = bytes[0];\n        var totalBytes = OpaqueType1LengthSize + length;\n\n        return SkipBytes(bytes, totalBytes);\n    }\n\n    private static ReadOnlySpan<byte> SkipOpaqueType2(ReadOnlySpan<byte> bytes)\n    {\n        if (bytes.Length < OpaqueType2LengthSize)\n        {\n            return ReadOnlySpan<byte>.Empty;\n        }\n\n        var length = BinaryPrimitives.ReadUInt16BigEndian(bytes);\n        var totalBytes = OpaqueType2LengthSize + length;\n\n        return SkipBytes(bytes, totalBytes);\n    }\n\n    private enum NameType : byte\n    {\n        HostName = 0x00\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/ValueStopwatch.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\n/// <summary>\n/// Value-type replacement for <see cref=\"Stopwatch\"/> which avoids allocations.\n/// </summary>\n/// <remarks>\n/// Inspired on <seealso href=\"https://github.com/dotnet/extensions/blob/master/src/Shared/src/ValueStopwatch/ValueStopwatch.cs\"/>.\n/// </remarks>\ninternal struct ValueStopwatch\n{\n    private static readonly double _timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;\n\n    private readonly long _startTimestamp;\n\n    private ValueStopwatch(long startTimestamp)\n    {\n        _startTimestamp = startTimestamp;\n    }\n\n    /// <summary>\n    /// Gets the time elapsed since the stopwatch was created with <see cref=\"StartNew\"/>.\n    /// </summary>\n    public TimeSpan Elapsed\n    {\n        get\n        {\n            // Start timestamp can't be zero in an initialized ValueStopwatch. It would have to be literally the first thing executed when the machine boots to be 0.\n            // So it being 0 is a clear indication of default(ValueStopwatch)\n            if (_startTimestamp == 0)\n            {\n                throw new InvalidOperationException(\"An uninitialized, or 'default', ValueStopwatch cannot be used to get elapsed time.\");\n            }\n\n            var end = Stopwatch.GetTimestamp();\n            var timestampDelta = end - _startTimestamp;\n            var ticks = (long)(_timestampToTicks * timestampDelta);\n            return new TimeSpan(ticks);\n        }\n    }\n\n    /// <summary>\n    /// Creates a new <see cref=\"ValueStopwatch\"/> that is ready to be used.\n    /// </summary>\n    public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp());\n}\n"
  },
  {
    "path": "src/ReverseProxy/Utilities/ValueStringBuilder.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Buffers;\nusing System.Diagnostics;\nusing System.Globalization;\nusing System.Runtime.CompilerServices;\n\nnamespace Yarp.ReverseProxy.Utilities;\n\n// Adapted from https://github.com/dotnet/runtime/blob/82fee2692b3954ba8903fa4764f1f4e36a26341a/src/libraries/Common/src/System/Text/ValueStringBuilder.cs\ninternal ref partial struct ValueStringBuilder\n{\n    public const int StackallocThreshold = 512;\n\n    private char[]? _arrayToReturnToPool;\n    private Span<char> _chars;\n    private int _pos;\n\n    public ValueStringBuilder(Span<char> initialBuffer)\n    {\n        _arrayToReturnToPool = null;\n        _chars = initialBuffer;\n        _pos = 0;\n    }\n\n    public int Length\n    {\n        get => _pos;\n        set\n        {\n            Debug.Assert(value >= 0);\n            Debug.Assert(value <= _chars.Length);\n            _pos = value;\n        }\n    }\n\n    public override string ToString()\n    {\n        var s = _chars.Slice(0, _pos).ToString();\n        Dispose();\n        return s;\n    }\n\n    [MethodImpl(MethodImplOptions.AggressiveInlining)]\n    public void Append(char c)\n    {\n        var pos = _pos;\n        var chars = _chars;\n        if ((uint)pos < (uint)chars.Length)\n        {\n            chars[pos] = c;\n            _pos = pos + 1;\n        }\n        else\n        {\n            GrowAndAppend(c);\n        }\n    }\n\n    [MethodImpl(MethodImplOptions.AggressiveInlining)]\n    public void Append(string s)\n    {\n        if (s is null)\n        {\n            return;\n        }\n\n        var pos = _pos;\n        if (pos > _chars.Length - s.Length)\n        {\n            Grow(s.Length);\n        }\n\n        s.CopyTo(_chars.Slice(pos));\n        _pos += s.Length;\n    }\n\n    public void Append(ReadOnlySpan<char> value)\n    {\n        var pos = _pos;\n        if (pos > _chars.Length - value.Length)\n        {\n            Grow(value.Length);\n        }\n\n        value.CopyTo(_chars.Slice(_pos));\n        _pos += value.Length;\n    }\n\n    [MethodImpl(MethodImplOptions.AggressiveInlining)]\n    public void Append(int i)\n    {\n        var pos = _pos;\n        if (i.TryFormat(_chars.Slice(pos), out var charsWritten, default, null))\n        {\n            _pos = pos + charsWritten;\n        }\n        else\n        {\n            Append(i.ToString(CultureInfo.InvariantCulture));\n        }\n    }\n\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    private void GrowAndAppend(char c)\n    {\n        Grow(1);\n        Append(c);\n    }\n\n    /// <summary>\n    /// Resize the internal buffer either by doubling current buffer size or\n    /// by adding <paramref name=\"additionalCapacityBeyondPos\"/> to\n    /// <see cref=\"_pos\"/> whichever is greater.\n    /// </summary>\n    /// <param name=\"additionalCapacityBeyondPos\">\n    /// Number of chars requested beyond current position.\n    /// </param>\n    [MethodImpl(MethodImplOptions.NoInlining)]\n    private void Grow(int additionalCapacityBeyondPos)\n    {\n        Debug.Assert(additionalCapacityBeyondPos > 0);\n        Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, \"Grow called incorrectly, no resize is needed.\");\n\n        const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength\n\n        // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try\n        // to double the size if possible, bounding the doubling to not go beyond the max array length.\n        var newCapacity = (int)Math.Max(\n            (uint)(_pos + additionalCapacityBeyondPos),\n            Math.Min((uint)_chars.Length * 2, ArrayMaxLength));\n\n        // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative.\n        // This could also go negative if the actual required length wraps around.\n        var poolArray = ArrayPool<char>.Shared.Rent(newCapacity);\n\n        _chars.Slice(0, _pos).CopyTo(poolArray);\n\n        var toReturn = _arrayToReturnToPool;\n        _chars = _arrayToReturnToPool = poolArray;\n        if (toReturn is not null)\n        {\n            ArrayPool<char>.Shared.Return(toReturn);\n        }\n    }\n\n    [MethodImpl(MethodImplOptions.AggressiveInlining)]\n    public void Dispose()\n    {\n        var toReturn = _arrayToReturnToPool;\n        this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again\n        if (toReturn is not null)\n        {\n            ArrayPool<char>.Shared.Return(toReturn);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/WebSocketsTelemetry/HttpConnectFeatureWrapper.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\n\nnamespace Yarp.ReverseProxy.WebSocketsTelemetry;\n\ninternal sealed class HttpConnectFeatureWrapper : IHttpExtendedConnectFeature\n{\n    private readonly TimeProvider _timeProvider;\n\n    public HttpContext HttpContext { get; private set; }\n\n    public IHttpExtendedConnectFeature InnerConnectFeature { get; private set; }\n\n    public WebSocketsTelemetryStream? TelemetryStream { get; private set; }\n\n    public bool IsExtendedConnect => InnerConnectFeature.IsExtendedConnect;\n\n    public string? Protocol => InnerConnectFeature.Protocol;\n\n    public HttpConnectFeatureWrapper(TimeProvider timeProvider, HttpContext httpContext, IHttpExtendedConnectFeature connectFeature)\n    {\n        ArgumentNullException.ThrowIfNull(timeProvider);\n        ArgumentNullException.ThrowIfNull(httpContext);\n        ArgumentNullException.ThrowIfNull(connectFeature);\n\n        _timeProvider = timeProvider;\n        HttpContext = httpContext;\n        InnerConnectFeature = connectFeature;\n    }\n\n    public async ValueTask<Stream> AcceptAsync()\n    {\n        Debug.Assert(TelemetryStream is null);\n        var opaqueTransport = await InnerConnectFeature.AcceptAsync();\n        TelemetryStream = new WebSocketsTelemetryStream(_timeProvider, opaqueTransport);\n        return TelemetryStream;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/WebSocketsTelemetry/HttpUpgradeFeatureWrapper.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.Net.Http.Headers;\n\nnamespace Yarp.ReverseProxy.WebSocketsTelemetry;\n\ninternal sealed class HttpUpgradeFeatureWrapper : IHttpUpgradeFeature\n{\n    private readonly TimeProvider _timeProvider;\n\n    public HttpContext HttpContext { get; private set; }\n\n    public IHttpUpgradeFeature InnerUpgradeFeature { get; private set; }\n\n    public WebSocketsTelemetryStream? TelemetryStream { get; private set; }\n\n    public bool IsUpgradableRequest => InnerUpgradeFeature.IsUpgradableRequest;\n\n    public HttpUpgradeFeatureWrapper(TimeProvider timeProvider, HttpContext httpContext, IHttpUpgradeFeature upgradeFeature)\n    {\n        ArgumentNullException.ThrowIfNull(timeProvider);\n        ArgumentNullException.ThrowIfNull(httpContext);\n        ArgumentNullException.ThrowIfNull(upgradeFeature);\n\n        _timeProvider = timeProvider;\n        HttpContext = httpContext;\n        InnerUpgradeFeature = upgradeFeature;\n    }\n\n    public async Task<Stream> UpgradeAsync()\n    {\n        Debug.Assert(TelemetryStream is null);\n        var opaqueTransport = await InnerUpgradeFeature.UpgradeAsync();\n\n        if (HttpContext.Response.Headers.TryGetValue(HeaderNames.Upgrade, out var upgradeValues) &&\n            upgradeValues.Count == 1 &&\n            string.Equals(\"WebSocket\", upgradeValues.ToString(), StringComparison.OrdinalIgnoreCase))\n        {\n            TelemetryStream = new WebSocketsTelemetryStream(_timeProvider, opaqueTransport);\n        }\n\n        return TelemetryStream ?? opaqueTransport;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/WebSocketsTelemetry/WebSocketCloseReason.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.ReverseProxy.WebSocketsTelemetry;\n\ninternal enum WebSocketCloseReason : int\n{\n    Unknown,\n    ClientGracefulClose,\n    ServerGracefulClose,\n    ClientDisconnect,\n    ServerDisconnect,\n    ActivityTimeout,\n}\n"
  },
  {
    "path": "src/ReverseProxy/WebSocketsTelemetry/WebSocketsParser.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\n\nnamespace Yarp.ReverseProxy.WebSocketsTelemetry;\n\ninternal unsafe struct WebSocketsParser\n{\n    private const int MaskLength = 4;\n    private const int MinHeaderSize = 2;\n    private const int MaxHeaderSize = MinHeaderSize + MaskLength + sizeof(ulong);\n\n    private fixed byte _leftoverBuffer[MaxHeaderSize - 1];\n    private readonly byte _minHeaderSize;\n    private byte _leftover;\n    private ulong _bytesToSkip;\n    private long _closeTime;\n    private readonly TimeProvider _timeProvider;\n\n    public long MessageCount { get; private set; }\n\n    public DateTime? CloseTime => _closeTime == 0 ? null : new DateTime(_closeTime, DateTimeKind.Utc);\n\n    public WebSocketsParser(TimeProvider timeProvider, bool isServer)\n    {\n        _minHeaderSize = (byte)(MinHeaderSize + (isServer ? MaskLength : 0));\n        _leftover = 0;\n        _bytesToSkip = 0;\n        _closeTime = 0;\n        _timeProvider = timeProvider;\n        MessageCount = 0;\n    }\n\n    // The WebSocket Protocol: https://datatracker.ietf.org/doc/html/rfc6455#section-5.2\n    //  0                   1                   2                   3\n    //  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\n    // +-+-+-+-+-------+-+-------------+-------------------------------+\n    // |F|R|R|R| opcode|M| Payload len |    Extended payload length    |\n    // |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |\n    // |N|V|V|V|       |S|             |   (if payload len==126/127)   |\n    // | |1|2|3|       |K|             |                               |\n    // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +\n    // |     Extended payload length continued, if payload len == 127  |\n    // + - - - - - - - - - - - - - - - +-------------------------------+\n    // |                               |Masking-key, if MASK set to 1  |\n    // +-------------------------------+-------------------------------+\n    // | Masking-key (continued)       |          Payload Data         |\n    // +-------------------------------- - - - - - - - - - - - - - - - +\n    // :                     Payload Data continued ...                :\n    // +---------------------------------------------------------------+\n    //\n    // The header can be 2-10 bytes long, followed by a 4 byte mask if the message was sent by the client.\n    // We have to read the first 2 bytes to know how long the frame header will be.\n    // Since the buffer may not contain the full frame, we make use of a leftoverBuffer\n    // where we store leftover bytes that don't represent a complete frame header.\n    // On the next call to Consume, we interpret the leftover bytes as the beginning of the frame.\n    // As we are not interested in the actual payload data, we skip over (payload length + mask length) bytes after each header.\n    public void Consume(ReadOnlySpan<byte> buffer)\n    {\n        int leftover = _leftover;\n        var bytesToSkip = _bytesToSkip;\n\n        while (true)\n        {\n            var toSkip = Math.Min(bytesToSkip, (ulong)buffer.Length);\n            buffer = buffer.Slice((int)toSkip);\n            bytesToSkip -= toSkip;\n\n            var available = leftover + buffer.Length;\n            int headerSize = _minHeaderSize;\n\n            if (available < headerSize)\n            {\n                break;\n            }\n\n            var length = (leftover > 1 ? _leftoverBuffer[1] : buffer[1 - leftover]) & 0x7FUL;\n\n            if (length > 125)\n            {\n                // The actual length will be encoded in 2 or 8 bytes, based on whether the length was 126 or 127\n                var lengthBytes = 2 << (((int)length & 1) << 1);\n                headerSize += lengthBytes;\n                Debug.Assert(leftover < headerSize);\n\n                if (available < headerSize)\n                {\n                    break;\n                }\n\n                lengthBytes += MinHeaderSize;\n\n                length = 0;\n                for (var i = MinHeaderSize; i < lengthBytes; i++)\n                {\n                    length <<= 8;\n                    length |= i < leftover ? _leftoverBuffer[i] : buffer[i - leftover];\n                }\n            }\n\n            Debug.Assert(leftover < headerSize);\n            bytesToSkip = length;\n\n            const int NonReservedBitsMask = 0b_1000_1111;\n            var header = (leftover > 0 ? _leftoverBuffer[0] : buffer[0]) & NonReservedBitsMask;\n\n            // Don't count control frames under MessageCount\n            if ((uint)(header - 0x80) <= 0x02)\n            {\n                // Has FIN (0x80) and is a Continuation (0x00) / Text (0x01) / Binary (0x02) opcode\n                MessageCount++;\n            }\n            else if ((header & 0xF) == 0x8) // CLOSE\n            {\n                if (_closeTime == 0)\n                {\n                    _closeTime = _timeProvider.GetUtcNow().Ticks;\n                }\n            }\n\n            // Advance the buffer by the number of bytes read for the header,\n            // accounting for any bytes we may have read from the leftoverBuffer\n            buffer = buffer.Slice(headerSize - leftover);\n            leftover = 0;\n        }\n\n        Debug.Assert(bytesToSkip == 0 || buffer.Length == 0);\n        _bytesToSkip = bytesToSkip;\n\n        Debug.Assert(leftover + buffer.Length < MaxHeaderSize);\n        for (var i = 0; i < buffer.Length; i++, leftover++)\n        {\n            _leftoverBuffer[leftover] = buffer[i];\n        }\n\n        _leftover = (byte)leftover;\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/WebSocketsTelemetry/WebSocketsTelemetry.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Diagnostics.CodeAnalysis;\nusing System.Diagnostics.Tracing;\n\nnamespace Yarp.ReverseProxy.WebSocketsTelemetry;\n\n[EventSource(Name = \"Yarp.ReverseProxy.WebSockets\")]\ninternal sealed class WebSocketsTelemetry : EventSource\n{\n    public static readonly WebSocketsTelemetry Log = new();\n\n    [UnconditionalSuppressMessage(\"ReflectionAnalysis\", \"IL2026:RequiresUnreferencedCode\",\n        Justification = \"Parameters to this method are primitive and are trimmer safe.\")]\n    [Event(1, Level = EventLevel.Informational)]\n    public void WebSocketClosed(long establishedTime, WebSocketCloseReason closeReason, long messagesRead, long messagesWritten)\n    {\n        if (IsEnabled(EventLevel.Informational, EventKeywords.All))\n        {\n            WriteEvent(eventId: 1, establishedTime, closeReason, messagesRead, messagesWritten);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/WebSocketsTelemetry/WebSocketsTelemetryExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.ReverseProxy.WebSocketsTelemetry;\n\nnamespace Microsoft.AspNetCore.Builder;\n\n/// <summary>\n/// <see cref=\"IApplicationBuilder\"/> extension methods to add the <see cref=\"WebSocketsTelemetryMiddleware\"/>.\n/// </summary>\npublic static class WebSocketsTelemetryExtensions\n{\n    /// <summary>\n    /// Adds a <see cref=\"WebSocketsTelemetryMiddleware\"/> to the request pipeline.\n    /// Must be added before <see cref=\"WebSockets.WebSocketMiddleware\"/>.\n    /// </summary>\n    public static IApplicationBuilder UseWebSocketsTelemetry(this IApplicationBuilder app)\n    {\n        return app.Use(next =>\n        {\n            // UseWebSocketsTelemetry may be used independently of the rest of YARP.\n            // Avoid exposing another extension method (AddWebSocketsTelemetry) just because of TimeProvider.\n            var timeProvider = app.ApplicationServices.GetService<TimeProvider>() ?? TimeProvider.System;\n            return new WebSocketsTelemetryMiddleware(next, timeProvider).InvokeAsync;\n        });\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/WebSocketsTelemetry/WebSocketsTelemetryMiddleware.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\n\nnamespace Yarp.ReverseProxy.WebSocketsTelemetry;\n\ninternal sealed class WebSocketsTelemetryMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly TimeProvider _timeProvider;\n\n    public WebSocketsTelemetryMiddleware(RequestDelegate next, TimeProvider timeProvider)\n    {\n        ArgumentNullException.ThrowIfNull(next);\n        ArgumentNullException.ThrowIfNull(timeProvider);\n\n        _next = next;\n        _timeProvider = timeProvider;\n    }\n\n    public Task InvokeAsync(HttpContext context)\n    {\n        if (WebSocketsTelemetry.Log.IsEnabled())\n        {\n            if (context.Features.Get<IHttpUpgradeFeature>() is { IsUpgradableRequest: true } upgradeFeature)\n            {\n                var upgradeWrapper = new HttpUpgradeFeatureWrapper(_timeProvider, context, upgradeFeature);\n                return InvokeAsyncCore(upgradeWrapper, _next);\n            }\n            else if (context.Features.Get<IHttpExtendedConnectFeature>() is { IsExtendedConnect: true } connectFeature\n                && string.Equals(\"websocket\", connectFeature.Protocol, StringComparison.OrdinalIgnoreCase))\n            {\n                var connectWrapper = new HttpConnectFeatureWrapper(_timeProvider, context, connectFeature);\n                return InvokeAsyncCore(connectWrapper, _next);\n            }\n        }\n\n        return _next(context);\n    }\n\n    private static async Task InvokeAsyncCore(HttpUpgradeFeatureWrapper upgradeWrapper, RequestDelegate next)\n    {\n        upgradeWrapper.HttpContext.Features.Set<IHttpUpgradeFeature>(upgradeWrapper);\n\n        try\n        {\n            await next(upgradeWrapper.HttpContext);\n        }\n        finally\n        {\n            if (upgradeWrapper.TelemetryStream is { } telemetryStream)\n            {\n                WebSocketsTelemetry.Log.WebSocketClosed(\n                    telemetryStream.EstablishedTime.Ticks,\n                    telemetryStream.GetCloseReason(upgradeWrapper.HttpContext),\n                    telemetryStream.MessagesRead,\n                    telemetryStream.MessagesWritten);\n            }\n\n            upgradeWrapper.HttpContext.Features.Set(upgradeWrapper.InnerUpgradeFeature);\n        }\n    }\n\n    private static async Task InvokeAsyncCore(HttpConnectFeatureWrapper connectWrapper, RequestDelegate next)\n    {\n        connectWrapper.HttpContext.Features.Set<IHttpExtendedConnectFeature>(connectWrapper);\n\n        try\n        {\n            await next(connectWrapper.HttpContext);\n        }\n        finally\n        {\n            if (connectWrapper.TelemetryStream is { } telemetryStream)\n            {\n                WebSocketsTelemetry.Log.WebSocketClosed(\n                    telemetryStream.EstablishedTime.Ticks,\n                    telemetryStream.GetCloseReason(connectWrapper.HttpContext),\n                    telemetryStream.MessagesRead,\n                    telemetryStream.MessagesWritten);\n            }\n\n            connectWrapper.HttpContext.Features.Set(connectWrapper.InnerConnectFeature);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/WebSocketsTelemetry/WebSocketsTelemetryStream.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.IO;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.WebSocketsTelemetry;\n\ninternal sealed class WebSocketsTelemetryStream : DelegatingStream\n{\n    private WebSocketsParser _readParser, _writeParser;\n\n    public DateTime EstablishedTime { get; }\n    public long MessagesRead => _readParser.MessageCount;\n    public long MessagesWritten => _writeParser.MessageCount;\n\n    public WebSocketsTelemetryStream(TimeProvider timeProvider, Stream innerStream)\n        : base(innerStream)\n    {\n        EstablishedTime = timeProvider.GetUtcNow().UtcDateTime;\n        _readParser = new WebSocketsParser(timeProvider, isServer: true);\n        _writeParser = new WebSocketsParser(timeProvider, isServer: false);\n    }\n\n    public WebSocketCloseReason GetCloseReason(HttpContext context)\n    {\n        var clientCloseTime = _readParser.CloseTime;\n        var serverCloseTime = _writeParser.CloseTime;\n\n        // Mutual, graceful WebSocket close. We report whichever one we saw first.\n        if (clientCloseTime.HasValue && serverCloseTime.HasValue)\n        {\n            return clientCloseTime.Value < serverCloseTime.Value ? WebSocketCloseReason.ClientGracefulClose : WebSocketCloseReason.ServerGracefulClose;\n        }\n\n        // One side sent a WebSocket close, but we never saw a response from the other side\n        // It is possible an error occurred, but we saw a graceful close first, so that is the initiator\n        if (clientCloseTime.HasValue)\n        {\n            return WebSocketCloseReason.ClientGracefulClose;\n        }\n        if (serverCloseTime.HasValue)\n        {\n            return WebSocketCloseReason.ServerGracefulClose;\n        }\n\n        return context.Features.Get<IForwarderErrorFeature>()?.Error switch\n        {\n            // Either side disconnected without sending a WebSocket close\n            ForwarderError.UpgradeRequestClient => WebSocketCloseReason.ClientDisconnect,\n            ForwarderError.UpgradeRequestCanceled => WebSocketCloseReason.ClientDisconnect,\n            ForwarderError.UpgradeResponseClient => WebSocketCloseReason.ClientDisconnect,\n            ForwarderError.UpgradeResponseCanceled => WebSocketCloseReason.ClientDisconnect,\n            ForwarderError.UpgradeRequestDestination => WebSocketCloseReason.ServerDisconnect,\n            ForwarderError.UpgradeResponseDestination => WebSocketCloseReason.ServerDisconnect,\n\n            // Activity Timeout\n            ForwarderError.UpgradeActivityTimeout => WebSocketCloseReason.ActivityTimeout,\n\n            // Both sides gracefully closed the underlying connection without sending a WebSocket close,\n            // or the server closed the connection and we canceled the client and suppressed the errors.\n            null => WebSocketCloseReason.ServerDisconnect,\n\n            // We are not expecting any other error from HttpForwarder after a successful connection upgrade\n            // Technically, a user could overwrite the IForwarderErrorFeature, in which case we don't know what's going on\n            _ => WebSocketCloseReason.Unknown\n        };\n    }\n\n    public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)\n    {\n        var readTask = base.ReadAsync(buffer, cancellationToken);\n\n        if (buffer.Length == 0)\n        {\n            return readTask;\n        }\n\n        if (readTask.IsCompletedSuccessfully)\n        {\n            var read = readTask.GetAwaiter().GetResult();\n            _readParser.Consume(buffer.Span.Slice(0, read));\n            return new ValueTask<int>(read);\n        }\n\n        return Core(buffer, readTask);\n\n        async ValueTask<int> Core(Memory<byte> buffer, ValueTask<int> readTask)\n        {\n            var read = await readTask;\n            _readParser.Consume(buffer.Span.Slice(0, read));\n            return read;\n        }\n    }\n\n    public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)\n    {\n        _writeParser.Consume(buffer.Span);\n        return base.WriteAsync(buffer, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/ReverseProxy/Yarp.ReverseProxy.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <Description>Reverse proxy toolkit for building fast proxy servers in .NET using the infrastructure from ASP.NET and .NET</Description>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <OutputType>Library</OutputType>\n    <RootNamespace>Yarp.ReverseProxy</RootNamespace>\n    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>\n    <Nullable>enable</Nullable>\n    <IsAotCompatible>true</IsAotCompatible>\n    <PackageReadmeFile>README.md</PackageReadmeFile>\n    <PackageTags>yarp;dotnet;reverse-proxy;aspnetcore</PackageTags>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"System.IO.Hashing\" Version=\"$(SystemIOHashingVersion)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <InternalsVisibleTo Include=\"DynamicProxyGenAssembly2\" Key=\"$(MoqPublicKey)\" />\n    <InternalsVisibleTo Include=\"Yarp.ReverseProxy.Tests\" />\n    <InternalsVisibleTo Include=\"Yarp.ReverseProxy.FunctionalTests\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Include=\"README.md\" Pack=\"true\" PackagePath=\"\\\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <FrameworkReference Include=\"Microsoft.AspNetCore.App\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/TelemetryConsumption/EventListenerService.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics.Tracing;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.Telemetry.Consumption;\n\ninternal abstract class EventListenerService<TService, TTelemetryConsumer, TMetrics> : EventListener, IHostedService\n    where TMetrics : class, new()\n{\n    protected abstract string EventSourceName { get; }\n    protected abstract int NumberOfMetrics { get; }\n    protected abstract void OnEvent(TTelemetryConsumer[] consumers, EventWrittenEventArgs eventData);\n    protected abstract bool TrySaveMetric(TMetrics metrics, string name, double value);\n\n    private readonly ILogger<TService> _logger;\n    private readonly TTelemetryConsumer[]? _telemetryConsumers;\n    private readonly IMetricsConsumer<TMetrics>[]? _metricsConsumers;\n\n    private int _metricsCount;\n    private TMetrics? _previousMetrics;\n    private TMetrics? _currentMetrics;\n\n    private EventSource? _eventSource;\n    private readonly object _syncObject = new();\n    private readonly bool _initialized;\n\n    public EventListenerService(\n        ILogger<TService> logger,\n        IEnumerable<TTelemetryConsumer> telemetryConsumers,\n        IEnumerable<IMetricsConsumer<TMetrics>> metricsConsumers)\n    {\n        ArgumentNullException.ThrowIfNull(logger);\n        ArgumentNullException.ThrowIfNull(telemetryConsumers);\n        ArgumentNullException.ThrowIfNull(metricsConsumers);\n\n        _logger = logger;\n        _telemetryConsumers = telemetryConsumers.ToArray();\n        _metricsConsumers = metricsConsumers.ToArray();\n\n        if (_telemetryConsumers.Any(s => s is null) || metricsConsumers.Any(c => c is null))\n        {\n            throw new ArgumentException(\"A consumer may not be null\",\n                _telemetryConsumers.Any(s => s is null) ? nameof(telemetryConsumers) : nameof(metricsConsumers));\n        }\n\n        if (_telemetryConsumers.Length == 0)\n        {\n            _telemetryConsumers = null;\n        }\n\n        if (_metricsConsumers.Length == 0)\n        {\n            _metricsConsumers = null;\n        }\n\n        lock (_syncObject)\n        {\n            if (_eventSource is EventSource eventSource)\n            {\n                EnableEventSource(eventSource);\n            }\n\n            _initialized = true;\n        }\n    }\n\n    protected override void OnEventSourceCreated(EventSource eventSource)\n    {\n        if (eventSource.Name == EventSourceName)\n        {\n            lock (_syncObject)\n            {\n                _eventSource = eventSource;\n\n                if (_initialized)\n                {\n                    // Ctor already finished - enable the EventSource here\n                    EnableEventSource(eventSource);\n                }\n            }\n        }\n    }\n\n    private void EnableEventSource(EventSource eventSource)\n    {\n        var enableEvents = _telemetryConsumers is not null;\n        var enableMetrics = _metricsConsumers is not null;\n\n        if (!enableEvents && !enableMetrics)\n        {\n            return;\n        }\n\n        var eventLevel = enableEvents ? EventLevel.Informational : EventLevel.Critical;\n        var arguments = enableMetrics ? new Dictionary<string, string?> { { \"EventCounterIntervalSec\", MetricsOptions.Interval.TotalSeconds.ToString() } } : null;\n\n        EnableEvents(eventSource, eventLevel, EventKeywords.None, arguments);\n    }\n\n    protected sealed override void OnEventWritten(EventWrittenEventArgs eventData)\n    {\n        if (eventData.EventId <= 0)\n        {\n            OnNonUserEvent(eventData);\n        }\n        else if (_telemetryConsumers is TTelemetryConsumer[] consumers)\n        {\n            OnEvent(consumers, eventData);\n        }\n    }\n\n    private void OnNonUserEvent(EventWrittenEventArgs eventData)\n    {\n        if (eventData.EventId == -1)\n        {\n            if (!ReferenceEquals(eventData.EventSource, _eventSource))\n            {\n                // Workaround for https://github.com/dotnet/runtime/issues/31927\n                // EventCounters are published to all EventListeners, regardless of\n                // which EventSource providers a listener is enabled for.\n                return;\n            }\n\n            // Throwing an exception here would crash the process\n            if (eventData.EventName != \"EventCounters\" ||\n                eventData.Payload?.Count != 1 ||\n                eventData.Payload[0] is not IDictionary<string, object> counters ||\n                !counters.TryGetValue(\"Name\", out var nameObject) ||\n                nameObject is not string name ||\n                !(counters.TryGetValue(\"Mean\", out var valueObj) || counters.TryGetValue(\"Increment\", out valueObj)) ||\n                valueObj is not double value)\n            {\n                _logger.LogDebug(\"Failed to parse EventCounters event from {EventSourceName}\", EventSourceName);\n                return;\n            }\n\n            var metrics = _currentMetrics ??= new();\n\n            if (!TrySaveMetric(metrics, name, value))\n            {\n                return;\n            }\n\n            if (++_metricsCount == NumberOfMetrics)\n            {\n                _metricsCount = 0;\n\n                var previous = _previousMetrics;\n                _previousMetrics = metrics;\n                _currentMetrics = null;\n\n                if (previous is null)\n                {\n                    return;\n                }\n\n                if (_metricsConsumers is IMetricsConsumer<TMetrics>[] consumers)\n                {\n                    foreach (var consumer in consumers)\n                    {\n                        try\n                        {\n                            consumer.OnMetrics(previous, metrics);\n                        }\n                        catch (Exception ex)\n                        {\n                            _logger.LogError(ex, \"Uncaught exception occurred while processing metrics for EventSource {EventSourceName}\", EventSourceName);\n                        }\n                    }\n                }\n            }\n        }\n        else if (eventData.EventId == 0)\n        {\n            _logger.LogError(\"Received an error message from EventSource {EventSourceName}: {Message}\", EventSourceName, eventData.Message);\n        }\n        else\n        {\n            _logger.LogDebug(\"Received an unknown event from EventSource {EventSourceName}: {EventId}\", EventSourceName, eventData.EventId);\n        }\n    }\n\n    public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;\n\n    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/Forwarder/ForwarderEventListenerService.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Diagnostics;\nusing System.Diagnostics.Tracing;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.Telemetry.Consumption;\n\ninternal sealed class ForwarderEventListenerService : EventListenerService<ForwarderEventListenerService, IForwarderTelemetryConsumer, ForwarderMetrics>\n{\n    protected override string EventSourceName => \"Yarp.ReverseProxy\";\n\n    protected override int NumberOfMetrics => 4;\n\n    public ForwarderEventListenerService(ILogger<ForwarderEventListenerService> logger, IEnumerable<IForwarderTelemetryConsumer> telemetryConsumers, IEnumerable<IMetricsConsumer<ForwarderMetrics>> metricsConsumers)\n        : base(logger, telemetryConsumers, metricsConsumers)\n    { }\n\n    protected override void OnEvent(IForwarderTelemetryConsumer[] consumers, EventWrittenEventArgs eventData)\n    {\n#pragma warning disable IDE0007 // Use implicit type\n        // Explicit type here to drop the object? signature of payload elements\n        ReadOnlyCollection<object> payload = eventData.Payload!;\n#pragma warning restore IDE0007 // Use implicit type\n\n        switch (eventData.EventId)\n        {\n            case 1:\n                Debug.Assert(eventData.EventName == \"ForwarderStart\" && payload.Count == 1);\n                {\n                    var destinationPrefix = (string)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnForwarderStart(eventData.TimeStamp, destinationPrefix);\n                    }\n                }\n                break;\n\n            case 2:\n                Debug.Assert(eventData.EventName == \"ForwarderStop\" && payload.Count == 1);\n                {\n                    var statusCode = (int)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnForwarderStop(eventData.TimeStamp, statusCode);\n                    }\n                }\n                break;\n\n            case 3:\n                Debug.Assert(eventData.EventName == \"ForwarderFailed\" && payload.Count == 1);\n                {\n                    var error = (ForwarderError)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnForwarderFailed(eventData.TimeStamp, error);\n                    }\n                }\n                break;\n\n            case 4:\n                Debug.Assert(eventData.EventName == \"ForwarderStage\" && payload.Count == 1);\n                {\n                    var proxyStage = (ForwarderStage)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnForwarderStage(eventData.TimeStamp, proxyStage);\n                    }\n                }\n                break;\n\n            case 5:\n                Debug.Assert(eventData.EventName == \"ContentTransferring\" && payload.Count == 5);\n                {\n                    var isRequest = (bool)payload[0];\n                    var contentLength = (long)payload[1];\n                    var iops = (long)payload[2];\n                    var readTime = new TimeSpan((long)payload[3]);\n                    var writeTime = new TimeSpan((long)payload[4]);\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnContentTransferring(eventData.TimeStamp, isRequest, contentLength, iops, readTime, writeTime);\n                    }\n                }\n                break;\n\n            case 6:\n                Debug.Assert(eventData.EventName == \"ContentTransferred\" && payload.Count == 6);\n                {\n                    var isRequest = (bool)payload[0];\n                    var contentLength = (long)payload[1];\n                    var iops = (long)payload[2];\n                    var readTime = new TimeSpan((long)payload[3]);\n                    var writeTime = new TimeSpan((long)payload[4]);\n                    var firstReadTime = new TimeSpan((long)payload[5]);\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnContentTransferred(eventData.TimeStamp, isRequest, contentLength, iops, readTime, writeTime, firstReadTime);\n                    }\n                }\n                break;\n\n            case 7:\n                Debug.Assert(eventData.EventName == \"ForwarderInvoke\" && payload.Count == 3);\n                {\n                    var clusterId = (string)payload[0];\n                    var routeId = (string)payload[1];\n                    var destinationId = (string)payload[2];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnForwarderInvoke(eventData.TimeStamp, clusterId, routeId, destinationId);\n                    }\n                }\n                break;\n        }\n    }\n\n    protected override bool TrySaveMetric(ForwarderMetrics metrics, string name, double value)\n    {\n        var longValue = (long)value;\n\n        switch (name)\n        {\n            case \"requests-started\":\n                metrics.RequestsStarted = longValue;\n                break;\n\n            case \"requests-started-rate\":\n                metrics.RequestsStartedRate = longValue;\n                break;\n\n            case \"requests-failed\":\n                metrics.RequestsFailed = longValue;\n                break;\n\n            case \"current-requests\":\n                metrics.CurrentRequests = longValue;\n                break;\n\n            default:\n                return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/Forwarder/ForwarderMetrics.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// Represents metrics reported by the Yarp.ReverseProxy event counters.\n/// </summary>\npublic sealed class ForwarderMetrics\n{\n    public ForwarderMetrics() => Timestamp = DateTime.UtcNow;\n\n    /// <summary>\n    /// Timestamp of when this <see cref=\"ForwarderMetrics\"/> instance was created.\n    /// </summary>\n    public DateTime Timestamp { get; internal set; }\n\n    /// <summary>\n    /// Number of proxy requests started since telemetry was enabled.\n    /// </summary>\n    public long RequestsStarted { get; internal set; }\n\n    /// <summary>\n    /// Number of proxy requests started in the last metrics interval.\n    /// </summary>\n    public long RequestsStartedRate { get; internal set; }\n\n    /// <summary>\n    /// Number of proxy requests that failed since telemetry was enabled.\n    /// </summary>\n    public long RequestsFailed { get; internal set; }\n\n    /// <summary>\n    /// Number of active proxy requests that have started but not yet completed or failed.\n    /// </summary>\n    public long CurrentRequests { get; internal set; }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/Forwarder/ForwarderStage.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// Stages of forwarding a request.\n/// </summary>\npublic enum ForwarderStage : int\n{\n    SendAsyncStart = 1,\n    SendAsyncStop,\n    RequestContentTransferStart,\n    ResponseContentTransferStart,\n    ResponseUpgrade,\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/Forwarder/IForwarderTelemetryConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// A consumer of Yarp.ReverseProxy EventSource events.\n/// </summary>\npublic interface IForwarderTelemetryConsumer\n{\n    /// <summary>\n    /// Called before forwarding a request.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"destinationPrefix\"></param>\n    void OnForwarderStart(DateTime timestamp, string destinationPrefix) { }\n\n    /// <summary>\n    /// Called after forwarding a request.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"statusCode\">The status code returned in the response.</param>\n    void OnForwarderStop(DateTime timestamp, int statusCode) { }\n\n    /// <summary>\n    /// Called before <see cref=\"OnForwarderStop(DateTime, int)\"/> if forwarding the request failed.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"error\"><see cref=\"ForwarderError\"/> information for the forwarding failure.</param>\n    void OnForwarderFailed(DateTime timestamp, ForwarderError error) { }\n\n    /// <summary>\n    /// Called when reaching a given stage of forwarding a request.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"stage\">Stage of the forwarding operation.</param>\n    void OnForwarderStage(DateTime timestamp, ForwarderStage stage) { }\n\n    /// <summary>\n    /// Called periodically while a content transfer is active.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"isRequest\">Indicates whether we are transferring the content from the client to the backend or vice-versa.</param>\n    /// <param name=\"contentLength\">Number of bytes transferred.</param>\n    /// <param name=\"iops\">Number of read/write pairs performed.</param>\n    /// <param name=\"readTime\">Time spent reading from the source.</param>\n    /// <param name=\"writeTime\">Time spent writing to the destination.</param>\n    void OnContentTransferring(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime) { }\n\n    /// <summary>\n    /// Called after transferring the request or response content.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"isRequest\">Indicates whether we transferred the content from the client to the backend or vice-versa.</param>\n    /// <param name=\"contentLength\">Number of bytes transferred.</param>\n    /// <param name=\"iops\">Number of read/write pairs performed.</param>\n    /// <param name=\"readTime\">Time spent reading from the source.</param>\n    /// <param name=\"writeTime\">Time spent writing to the destination.</param>\n    /// <param name=\"firstReadTime\">Time spent on the first read of the source.</param>\n    void OnContentTransferred(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime, TimeSpan firstReadTime) { }\n\n    /// <summary>\n    /// Called before forwarding a request.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"clusterId\">Cluster ID</param>\n    /// <param name=\"routeId\">Route ID</param>\n    /// <param name=\"destinationId\">Destination ID</param>\n    void OnForwarderInvoke(DateTime timestamp, string clusterId, string routeId, string destinationId) { }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/Http/HttpEventListenerService.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Diagnostics;\nusing System.Diagnostics.Tracing;\nusing System.Net.Http;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.Telemetry.Consumption;\n\ninternal sealed class HttpEventListenerService : EventListenerService<HttpEventListenerService, IHttpTelemetryConsumer, HttpMetrics>\n{\n    protected override string EventSourceName => \"System.Net.Http\";\n\n    protected override int NumberOfMetrics => 11;\n\n    public HttpEventListenerService(ILogger<HttpEventListenerService> logger, IEnumerable<IHttpTelemetryConsumer> telemetryConsumers, IEnumerable<IMetricsConsumer<HttpMetrics>> metricsConsumers)\n        : base(logger, telemetryConsumers, metricsConsumers)\n    { }\n\n    protected override void OnEvent(IHttpTelemetryConsumer[] consumers, EventWrittenEventArgs eventData)\n    {\n#pragma warning disable IDE0007 // Use implicit type\n        // Explicit type here to drop the object? signature of payload elements\n        ReadOnlyCollection<object> payload = eventData.Payload!;\n#pragma warning restore IDE0007 // Use implicit type\n\n        switch (eventData.EventId)\n        {\n            case 1:\n                Debug.Assert(eventData.EventName == \"RequestStart\" && payload.Count == 7);\n                {\n                    var scheme = (string)payload[0];\n                    var host = (string)payload[1];\n                    var port = (int)payload[2];\n                    var pathAndQuery = (string)payload[3];\n                    var versionMajor = (int)(byte)payload[4];\n                    var versionMinor = (int)(byte)payload[5];\n                    var versionPolicy = (HttpVersionPolicy)payload[6];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnRequestStart(eventData.TimeStamp, scheme, host, port, pathAndQuery, versionMajor, versionMinor, versionPolicy);\n                    }\n                }\n                break;\n\n            case 2:\n                Debug.Assert(eventData.EventName == \"RequestStop\" && payload.Count == (eventData.Version == 0 ? 0 : 1));\n                {\n                    var statusCode = (int)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnRequestStop(eventData.TimeStamp, statusCode);\n                    }\n                }\n                break;\n\n            case 3:\n                Debug.Assert(eventData.EventName == \"RequestFailed\" && payload.Count == (eventData.Version == 0 ? 0 : 1));\n                {\n                    var exceptionMessage = (string)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnRequestFailed(eventData.TimeStamp, exceptionMessage);\n                    }\n                }\n                break;\n\n            case 4:\n                Debug.Assert(eventData.EventName == \"ConnectionEstablished\" && payload.Count == (eventData.Version == 0 ? 2 : 7));\n                {\n                    var versionMajor = (int)(byte)payload[0];\n                    var versionMinor = (int)(byte)payload[1];\n                    var connectionId = (long)payload[2];\n                    var scheme = (string)payload[3];\n                    var host = (string)payload[4];\n                    var port = (int)payload[5];\n                    var remoteAddress = (string?)payload[6];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnConnectionEstablished(eventData.TimeStamp, versionMajor, versionMinor, connectionId, scheme, host, port, remoteAddress);\n                    }\n                }\n                break;\n\n            case 5:\n                Debug.Assert(eventData.EventName == \"ConnectionClosed\" && payload.Count == (eventData.Version == 0 ? 2 : 3));\n                {\n                    var versionMajor = (int)(byte)payload[0];\n                    var versionMinor = (int)(byte)payload[1];\n                    var connectionId = (long)payload[2];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnConnectionClosed(eventData.TimeStamp, versionMajor, versionMinor, connectionId);\n                    }\n                }\n                break;\n\n            case 6:\n                Debug.Assert(eventData.EventName == \"RequestLeftQueue\" && payload.Count == 3);\n                {\n                    var timeOnQueue = TimeSpan.FromMilliseconds((double)payload[0]);\n                    var versionMajor = (int)(byte)payload[1];\n                    var versionMinor = (int)(byte)payload[2];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnRequestLeftQueue(eventData.TimeStamp, timeOnQueue, versionMajor, versionMinor);\n                    }\n                }\n                break;\n\n            case 7:\n                Debug.Assert(eventData.EventName == \"RequestHeadersStart\" && payload.Count == (eventData.Version == 0 ? 0 : 1));\n                {\n                    var connectionId = (long)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnRequestHeadersStart(eventData.TimeStamp, connectionId);\n                    }\n                }\n                break;\n\n            case 8:\n                Debug.Assert(eventData.EventName == \"RequestHeadersStop\" && payload.Count == 0);\n                {\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnRequestHeadersStop(eventData.TimeStamp);\n                    }\n                }\n                break;\n\n            case 9:\n                Debug.Assert(eventData.EventName == \"RequestContentStart\" && payload.Count == 0);\n                {\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnRequestContentStart(eventData.TimeStamp);\n                    }\n                }\n                break;\n\n            case 10:\n                Debug.Assert(eventData.EventName == \"RequestContentStop\" && payload.Count == 1);\n                {\n                    var contentLength = (long)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnRequestContentStop(eventData.TimeStamp, contentLength);\n                    }\n                }\n                break;\n\n            case 11:\n                Debug.Assert(eventData.EventName == \"ResponseHeadersStart\" && payload.Count == 0);\n                {\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnResponseHeadersStart(eventData.TimeStamp);\n                    }\n                }\n                break;\n\n            case 12:\n                Debug.Assert(eventData.EventName == \"ResponseHeadersStop\" && payload.Count == (eventData.Version == 0 ? 0 : 1));\n                {\n                    var statusCode = (int)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnResponseHeadersStop(eventData.TimeStamp, statusCode);\n                    }\n                }\n                break;\n\n            case 13:\n                Debug.Assert(eventData.EventName == \"ResponseContentStart\" && payload.Count == 0);\n                {\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnResponseContentStart(eventData.TimeStamp);\n                    }\n                }\n                break;\n\n            case 14:\n                Debug.Assert(eventData.EventName == \"ResponseContentStop\" && payload.Count == 0);\n                {\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnResponseContentStop(eventData.TimeStamp);\n                    }\n                }\n                break;\n\n            case 15:\n                Debug.Assert(eventData.EventName == \"RequestFailedDetailed\" && payload.Count == 1);\n                // This event is more expensive to collect and requires an opt-in keyword.\n                // We should only see it if a different EventListener opted in (potentially from a different process).\n                break;\n\n            case 16:\n                Debug.Assert(eventData.EventName == \"Redirect\" && payload.Count == 1);\n                {\n                    var redirectUri = (string)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnRedirect(eventData.TimeStamp, redirectUri);\n                    }\n                }\n                break;\n        }\n    }\n\n    protected override bool TrySaveMetric(HttpMetrics metrics, string name, double value)\n    {\n        switch (name)\n        {\n            case \"requests-started\":\n                metrics.RequestsStarted = (long)value;\n                break;\n\n            case \"requests-started-rate\":\n                metrics.RequestsStartedRate = (long)value;\n                break;\n\n            case \"requests-failed\":\n                metrics.RequestsFailed = (long)value;\n                break;\n\n            case \"requests-failed-rate\":\n                metrics.RequestsFailedRate = (long)value;\n                break;\n\n            case \"current-requests\":\n                metrics.CurrentRequests = (long)value;\n                break;\n\n            case \"http11-connections-current-total\":\n                metrics.CurrentHttp11Connections = (long)value;\n                break;\n\n            case \"http20-connections-current-total\":\n                metrics.CurrentHttp20Connections = (long)value;\n                break;\n\n            case \"http11-requests-queue-duration\":\n                metrics.Http11RequestsQueueDuration = TimeSpan.FromMilliseconds(value);\n                break;\n\n            case \"http20-requests-queue-duration\":\n                metrics.Http20RequestsQueueDuration = TimeSpan.FromMilliseconds(value);\n                break;\n\n            case \"http30-connections-current-total\":\n                metrics.CurrentHttp30Connections = (long)value;\n                break;\n\n            case \"http30-requests-queue-duration\":\n                metrics.Http30RequestsQueueDuration = TimeSpan.FromMilliseconds(value);\n                break;\n\n            default:\n                return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/Http/HttpMetrics.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// Represents metrics reported by the System.Net.Http event counters.\n/// </summary>\npublic sealed class HttpMetrics\n{\n    public HttpMetrics() => Timestamp = DateTime.UtcNow;\n\n    /// <summary>\n    /// Timestamp of when this <see cref=\"KestrelMetrics\"/> instance was created.\n    /// </summary>\n    public DateTime Timestamp { get; internal set; }\n\n    /// <summary>\n    /// Number of HTTP requests started since telemetry was enabled.\n    /// </summary>\n    public long RequestsStarted { get; internal set; }\n\n    /// <summary>\n    /// Number of HTTP requests started in the last metrics interval.\n    /// </summary>\n    public long RequestsStartedRate { get; internal set; }\n\n    /// <summary>\n    /// Number of HTTP requests that failed since telemetry was enabled.\n    /// </summary>\n    public long RequestsFailed { get; internal set; }\n\n    /// <summary>\n    /// Number of HTTP requests that failed in the last metrics interval.\n    /// </summary>\n    public long RequestsFailedRate { get; internal set; }\n\n    /// <summary>\n    /// Number of active HTTP requests that have started but not yet completed or failed.\n    /// </summary>\n    public long CurrentRequests { get; internal set; }\n\n    /// <summary>\n    /// Number of currently open HTTP 1.1 connections.\n    /// </summary>\n    public long CurrentHttp11Connections { get; internal set; }\n\n    /// <summary>\n    /// Number of currently open HTTP 2.0 connections.\n    /// </summary>\n    public long CurrentHttp20Connections { get; internal set; }\n\n    /// <summary>\n    /// Average time spent on queue for HTTP 1.1 requests that hit the MaxConnectionsPerServer limit in the last metrics interval.\n    /// </summary>\n    public TimeSpan Http11RequestsQueueDuration { get; internal set; }\n\n    /// <summary>\n    /// Average time spent on queue for HTTP 2.0 requests that hit the MAX_CONCURRENT_STREAMS limit on the connection in the last metrics interval.\n    /// </summary>\n    public TimeSpan Http20RequestsQueueDuration { get; internal set; }\n\n    /// <summary>\n    /// Number of currently open HTTP 3.0 connections.\n    /// </summary>\n    public long CurrentHttp30Connections { get; internal set; }\n\n    /// <summary>\n    /// Average time spent on queue for HTTP 3.0 requests in the last metrics interval.\n    /// </summary>\n    public TimeSpan Http30RequestsQueueDuration { get; internal set; }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/Http/IHttpTelemetryConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// A consumer of System.Net.Http EventSource events.\n/// </summary>\npublic interface IHttpTelemetryConsumer\n{\n    /// <summary>\n    /// Called before an HTTP request.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"scheme\">Scheme of the request Uri.</param>\n    /// <param name=\"host\">Host of the request Uri.</param>\n    /// <param name=\"port\">Port of the request Uri.</param>\n    /// <param name=\"pathAndQuery\">Path and query of the request Uri.</param>\n    /// <param name=\"versionMajor\">Major component of the request's HTTP version.</param>\n    /// <param name=\"versionMinor\">Minor component of the request's HTTP version.</param>\n    /// <param name=\"versionPolicy\"><see cref=\"HttpVersionPolicy\"/> of the request.</param>\n    void OnRequestStart(DateTime timestamp, string scheme, string host, int port, string pathAndQuery, int versionMajor, int versionMinor, HttpVersionPolicy versionPolicy) { }\n\n    /// <summary>\n    /// Called after an HTTP request.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    void OnRequestStop(DateTime timestamp) { }\n\n    /// <summary>\n    /// Called before <see cref=\"OnRequestStop(DateTime)\"/> if the request failed.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    void OnRequestFailed(DateTime timestamp) { }\n\n    /// <summary>\n    /// Called when a new HTTP connection is established.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"versionMajor\">Major component of the connection's HTTP version.</param>\n    /// <param name=\"versionMinor\">Minor component of the connection's HTTP version.</param>\n    void OnConnectionEstablished(DateTime timestamp, int versionMajor, int versionMinor) { }\n\n    /// <summary>\n    /// Called when a new HTTP connection is closed.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"versionMajor\">Major component of the connection's HTTP version.</param>\n    /// <param name=\"versionMinor\">Minor component of the connection's HTTP version.</param>\n    void OnConnectionClosed(DateTime timestamp, int versionMajor, int versionMinor) { }\n\n    /// <summary>\n    /// Called when a request that hit the MaxConnectionsPerServer or MAX_CONCURRENT_STREAMS limit leaves the queue.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"timeOnQueue\">Time spent on queue.</param>\n    /// <param name=\"versionMajor\">Major component of the request's HTTP version.</param>\n    /// <param name=\"versionMinor\">Minor component of the request's HTTP version.</param>\n    void OnRequestLeftQueue(DateTime timestamp, TimeSpan timeOnQueue, int versionMajor, int versionMinor) { }\n\n    /// <summary>\n    /// Called before sending the request headers.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    void OnRequestHeadersStart(DateTime timestamp) { }\n\n    /// <summary>\n    /// Called after sending the request headers.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    void OnRequestHeadersStop(DateTime timestamp) { }\n\n    /// <summary>\n    /// Called before sending the request content.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    void OnRequestContentStart(DateTime timestamp) { }\n\n    /// <summary>\n    /// Called after sending the request content.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"contentLength\"></param>\n    void OnRequestContentStop(DateTime timestamp, long contentLength) { }\n\n    /// <summary>\n    /// Called before reading the response headers.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    void OnResponseHeadersStart(DateTime timestamp) { }\n\n    /// <summary>\n    /// Called after reading all response headers.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    void OnResponseHeadersStop(DateTime timestamp) { }\n\n    /// <summary>\n    /// Called when <see cref=\"HttpClient\"/> starts buffering the response content.\n    /// This event WILL NOT be called for requests made by YARP, as they are not buffered.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    void OnResponseContentStart(DateTime timestamp) { }\n\n    /// <summary>\n    /// Called when <see cref=\"HttpClient\"/> stops buffering the response content.\n    /// This event WILL NOT be called for requests made by YARP, as they are not buffered.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    void OnResponseContentStop(DateTime timestamp) { }\n\n    /// <summary>\n    /// Called after an HTTP request.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"statusCode\">The status code returned by the server. -1 if no response was received.</param>\n    void OnRequestStop(DateTime timestamp, int statusCode) =>\n        OnRequestStop(timestamp);\n\n    /// <summary>\n    /// Called before <see cref=\"OnRequestStop(DateTime)\"/> if the request failed.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"exceptionMessage\">A message that describes the exception associated with this request failure.</param>\n    void OnRequestFailed(DateTime timestamp, string exceptionMessage) =>\n        OnRequestFailed(timestamp);\n\n    /// <summary>\n    /// Called when a new HTTP connection is established.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"versionMajor\">Major component of the connection's HTTP version.</param>\n    /// <param name=\"versionMinor\">Minor component of the connection's HTTP version.</param>\n    /// <param name=\"connectionId\">ID of the connection that was established, unique for this process.</param>\n    /// <param name=\"scheme\">Scheme the connection was established with.</param>\n    /// <param name=\"host\">Host the connection was established to.</param>\n    /// <param name=\"port\">Port the connection was established to.</param>\n    /// <param name=\"remoteAddress\">The remote address this connection was established to, if available.</param>\n    void OnConnectionEstablished(DateTime timestamp, int versionMajor, int versionMinor, long connectionId, string scheme, string host, int port, string? remoteAddress) =>\n        OnConnectionEstablished(timestamp, versionMajor, versionMinor);\n\n    /// <summary>\n    /// Called when a new HTTP connection is closed.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"versionMajor\">Major component of the connection's HTTP version.</param>\n    /// <param name=\"versionMinor\">Minor component of the connection's HTTP version.</param>\n    /// <param name=\"connectionId\">ID of the connection that was closed.</param>\n    void OnConnectionClosed(DateTime timestamp, int versionMajor, int versionMinor, long connectionId) =>\n        OnConnectionClosed(timestamp, versionMajor, versionMinor);\n\n    /// <summary>\n    /// Called before sending the request headers.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"connectionId\">ID of the connection we are sending this request on.</param>\n    void OnRequestHeadersStart(DateTime timestamp, long connectionId) =>\n        OnRequestHeadersStart(timestamp);\n\n    /// <summary>\n    /// Called after reading all response headers.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"statusCode\">The status code returned by the server.</param>\n    void OnResponseHeadersStop(DateTime timestamp, int statusCode) =>\n        OnResponseHeadersStop(timestamp);\n\n    /// <summary>\n    /// Called before a request is redirected if <see cref=\"SocketsHttpHandler.AllowAutoRedirect\"/> is enabled.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"redirectUri\">The uri the request is being redirected to.</param>\n    void OnRedirect(DateTime timestamp, string redirectUri) { }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/IMetricsConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// A consumer of <typeparamref name=\"TMetrics\"/>.\n/// </summary>\npublic interface IMetricsConsumer<TMetrics>\n{\n    /// <summary>\n    /// Processes <typeparamref name=\"TMetrics\"/> from the last event counter interval.\n    /// </summary>\n    /// <param name=\"previous\"><typeparamref name=\"TMetrics\"/> collected in the previous interval.</param>\n    /// <param name=\"current\"><typeparamref name=\"TMetrics\"/> collected in the last interval.</param>\n    void OnMetrics(TMetrics previous, TMetrics current);\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/Kestrel/IKestrelTelemetryConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// A consumer of Microsoft-AspNetCore-Server-Kestrel EventSource events.\n/// </summary>\npublic interface IKestrelTelemetryConsumer\n{\n    /// <summary>\n    /// Called at the start of a connection.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"connectionId\">ID of the connection.</param>\n    /// <param name=\"localEndPoint\">Local endpoint for the connection.</param>\n    /// <param name=\"remoteEndPoint\">Remote endpoint for the connection.</param>\n    void OnConnectionStart(DateTime timestamp, string connectionId, string? localEndPoint, string? remoteEndPoint) { }\n\n    /// <summary>\n    /// Called at the end of a connection.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"connectionId\">ID of the connection.</param>\n    void OnConnectionStop(DateTime timestamp, string connectionId) { }\n\n    /// <summary>\n    /// Called at the start of a request.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"connectionId\">ID of the connection.</param>\n    /// <param name=\"requestId\">ID of the request.</param>\n    /// <param name=\"httpVersion\">HTTP version of the request.</param>\n    /// <param name=\"path\">Path of the request.</param>\n    /// <param name=\"method\">HTTP method of the request.</param>\n    void OnRequestStart(DateTime timestamp, string connectionId, string requestId, string httpVersion, string path, string method) { }\n\n    /// <summary>\n    /// Called at the end of a request.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"connectionId\">ID of the connection.</param>\n    /// <param name=\"requestId\">ID of the request.</param>\n    /// <param name=\"httpVersion\">HTTP version of the request.</param>\n    /// <param name=\"path\">Path of the request.</param>\n    /// <param name=\"method\">HTTP method of the request.</param>\n    void OnRequestStop(DateTime timestamp, string connectionId, string requestId, string httpVersion, string path, string method) { }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/Kestrel/KestrelEventListenerService.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Diagnostics;\nusing System.Diagnostics.Tracing;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.Telemetry.Consumption;\n\ninternal sealed class KestrelEventListenerService : EventListenerService<KestrelEventListenerService, IKestrelTelemetryConsumer, KestrelMetrics>\n{\n    protected override string EventSourceName => \"Microsoft-AspNetCore-Server-Kestrel\";\n\n    protected override int NumberOfMetrics => 10;\n\n    public KestrelEventListenerService(ILogger<KestrelEventListenerService> logger, IEnumerable<IKestrelTelemetryConsumer> telemetryConsumers, IEnumerable<IMetricsConsumer<KestrelMetrics>> metricsConsumers)\n        : base(logger, telemetryConsumers, metricsConsumers)\n    { }\n\n    protected override void OnEvent(IKestrelTelemetryConsumer[] consumers, EventWrittenEventArgs eventData)\n    {\n#pragma warning disable IDE0007 // Use implicit type\n        // Explicit type here to drop the object? signature of payload elements\n        ReadOnlyCollection<object> payload = eventData.Payload!;\n#pragma warning restore IDE0007 // Use implicit type\n\n        switch (eventData.EventId)\n        {\n            case 1:\n                Debug.Assert(eventData.EventName == \"ConnectionStart\" && payload.Count == 3);\n                {\n                    var connectionId = (string)payload[0];\n                    var localEndPoint = (string?)payload[1];\n                    var remoteEndPoint = (string?)payload[2];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnConnectionStart(eventData.TimeStamp, connectionId, localEndPoint, remoteEndPoint);\n                    }\n                }\n                break;\n\n            case 2:\n                Debug.Assert(eventData.EventName == \"ConnectionStop\" && payload.Count == 1);\n                {\n                    var connectionId = (string)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnConnectionStop(eventData.TimeStamp, connectionId);\n                    }\n                }\n                break;\n\n            case 3:\n                Debug.Assert(eventData.EventName == \"RequestStart\" && payload.Count == 5);\n                {\n                    var connectionId = (string)payload[0];\n                    var requestId = (string)payload[1];\n                    var httpVersion = (string)payload[2];\n                    var path = (string)payload[3];\n                    var method = (string)payload[4];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnRequestStart(eventData.TimeStamp, connectionId, requestId, httpVersion, path, method);\n                    }\n                }\n                break;\n\n            case 4:\n                Debug.Assert(eventData.EventName == \"RequestStop\" && payload.Count == 5);\n                {\n                    var connectionId = (string)payload[0];\n                    var requestId = (string)payload[1];\n                    var httpVersion = (string)payload[2];\n                    var path = (string)payload[3];\n                    var method = (string)payload[4];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnRequestStop(eventData.TimeStamp, connectionId, requestId, httpVersion, path, method);\n                    }\n                }\n                break;\n        }\n    }\n\n    protected override bool TrySaveMetric(KestrelMetrics metrics, string name, double value)\n    {\n        var longValue = (long)value;\n\n        switch (name)\n        {\n            case \"connections-per-second\":\n                metrics.ConnectionRate = longValue;\n                break;\n\n            case \"total-connections\":\n                metrics.TotalConnections = longValue;\n                break;\n\n            case \"tls-handshakes-per-second\":\n                metrics.TlsHandshakeRate = longValue;\n                break;\n\n            case \"total-tls-handshakes\":\n                metrics.TotalTlsHandshakes = longValue;\n                break;\n\n            case \"current-tls-handshakes\":\n                metrics.CurrentTlsHandshakes = longValue;\n                break;\n\n            case \"failed-tls-handshakes\":\n                metrics.FailedTlsHandshakes = longValue;\n                break;\n\n            case \"current-connections\":\n                metrics.CurrentConnections = longValue;\n                break;\n\n            case \"connection-queue-length\":\n                metrics.ConnectionQueueLength = longValue;\n                break;\n\n            case \"request-queue-length\":\n                metrics.RequestQueueLength = longValue;\n                break;\n\n            case \"current-upgraded-requests\":\n                metrics.CurrentUpgradedRequests = longValue;\n                break;\n\n            default:\n                return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/Kestrel/KestrelMetrics.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// Represents metrics reported by the Microsoft-AspNetCore-Server-Kestrel event counters.\n/// </summary>\npublic sealed class KestrelMetrics\n{\n    public KestrelMetrics() => Timestamp = DateTime.UtcNow;\n\n    /// <summary>\n    /// Timestamp of when this <see cref=\"KestrelMetrics\"/> instance was created.\n    /// </summary>\n    public DateTime Timestamp { get; internal set; }\n\n    /// <summary>\n    /// Number of connections opened in the last metrics interval.\n    /// </summary>\n    public long ConnectionRate { get; internal set; }\n\n    /// <summary>\n    /// Number of connections opened since telemetry was enabled.\n    /// </summary>\n    public long TotalConnections { get; internal set; }\n\n    /// <summary>\n    /// Number of TLS handshakes started in the last metrics interval.\n    /// </summary>\n    public long TlsHandshakeRate { get; internal set; }\n\n    /// <summary>\n    /// Number of TLS handshakes started since telemetry was enabled.\n    /// </summary>\n    public long TotalTlsHandshakes { get; internal set; }\n\n    /// <summary>\n    /// Number of active TLS handshakes that have started but not yet completed or failed.\n    /// </summary>\n    public long CurrentTlsHandshakes { get; internal set; }\n\n    /// <summary>\n    /// Number of TLS handshakes that failed since telemetry was enabled.\n    /// </summary>\n    public long FailedTlsHandshakes { get; internal set; }\n\n    /// <summary>\n    /// Number of currently open connections.\n    /// </summary>\n    public long CurrentConnections { get; internal set; }\n\n    /// <summary>\n    /// Number of connections on the queue.\n    /// </summary>\n    public long ConnectionQueueLength { get; internal set; }\n\n    /// <summary>\n    /// Number of requests on the queue.\n    /// </summary>\n    public long RequestQueueLength { get; internal set; }\n\n    /// <summary>\n    /// Number of currently upgraded requests (number of webSocket connections).\n    /// </summary>\n    public long CurrentUpgradedRequests { get; internal set; }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/MetricsOptions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.Telemetry.Consumption;\n\ninternal static class MetricsOptions\n{\n    // TODO: Should this be publicly configurable? It's currently only visible to tests to reduce execution time\n    public static TimeSpan Interval { get; set; } = TimeSpan.FromSeconds(1);\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/NameResolution/INameResolutionTelemetryConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// A consumer of System.Net.NameResolution EventSource events.\n/// </summary>\npublic interface INameResolutionTelemetryConsumer\n{\n    /// <summary>\n    /// Called before a name resolution.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"hostNameOrAddress\">Host name or address we are resolving.</param>\n    void OnResolutionStart(DateTime timestamp, string hostNameOrAddress) { }\n\n    /// <summary>\n    /// Called after a name resolution.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    void OnResolutionStop(DateTime timestamp) { }\n\n    /// <summary>\n    /// Called before <see cref=\"OnResolutionStop(DateTime)\"/> if the name resolution failed.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    void OnResolutionFailed(DateTime timestamp) { }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/NameResolution/NameResolutionEventListenerService.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Diagnostics;\nusing System.Diagnostics.Tracing;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.Telemetry.Consumption;\n\ninternal sealed class NameResolutionEventListenerService : EventListenerService<NameResolutionEventListenerService, INameResolutionTelemetryConsumer, NameResolutionMetrics>\n{\n    protected override string EventSourceName => \"System.Net.NameResolution\";\n\n    protected override int NumberOfMetrics => 3;\n\n    public NameResolutionEventListenerService(ILogger<NameResolutionEventListenerService> logger, IEnumerable<INameResolutionTelemetryConsumer> telemetryConsumers, IEnumerable<IMetricsConsumer<NameResolutionMetrics>> metricsConsumers)\n        : base(logger, telemetryConsumers, metricsConsumers)\n    { }\n\n    protected override void OnEvent(INameResolutionTelemetryConsumer[] consumers, EventWrittenEventArgs eventData)\n    {\n#pragma warning disable IDE0007 // Use implicit type\n        // Explicit type here to drop the object? signature of payload elements\n        ReadOnlyCollection<object> payload = eventData.Payload!;\n#pragma warning restore IDE0007 // Use implicit type\n\n        switch (eventData.EventId)\n        {\n            case 1:\n                Debug.Assert(eventData.EventName == \"ResolutionStart\" && payload.Count == 1);\n                {\n                    var hostNameOrAddress = (string)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnResolutionStart(eventData.TimeStamp, hostNameOrAddress);\n                    }\n                }\n                break;\n\n            case 2:\n                Debug.Assert(eventData.EventName == \"ResolutionStop\" && payload.Count == 0);\n                {\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnResolutionStop(eventData.TimeStamp);\n                    }\n                }\n                break;\n\n            case 3:\n                Debug.Assert(eventData.EventName == \"ResolutionFailed\" && payload.Count == 0);\n                {\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnResolutionFailed(eventData.TimeStamp);\n                    }\n                }\n                break;\n        }\n    }\n\n    protected override bool TrySaveMetric(NameResolutionMetrics metrics, string name, double value)\n    {\n        switch (name)\n        {\n            case \"dns-lookups-requested\":\n                metrics.DnsLookupsRequested = (long)value;\n                break;\n\n            case \"dns-lookups-duration\":\n                metrics.AverageLookupDuration = TimeSpan.FromMilliseconds(value);\n                break;\n\n            case \"current-dns-lookups\":\n                metrics.CurrentDnsLookups = (long)value;\n                break;\n\n            default:\n                return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/NameResolution/NameResolutionMetrics.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// Represents metrics reported by the System.Net.NameResolution event counters.\n/// </summary>\npublic sealed class NameResolutionMetrics\n{\n    public NameResolutionMetrics() => Timestamp = DateTime.UtcNow;\n\n    /// <summary>\n    /// Timestamp of when this <see cref=\"NameResolutionMetrics\"/> instance was created.\n    /// </summary>\n    public DateTime Timestamp { get; internal set; }\n\n    /// <summary>\n    /// Number of DNS lookups requested since telemetry was enabled.\n    /// </summary>\n    public long DnsLookupsRequested { get; internal set; }\n\n    /// <summary>\n    /// Average DNS lookup duration in the last metrics interval.\n    /// </summary>\n    public TimeSpan AverageLookupDuration { get; internal set; }\n\n    /// <summary>\n    /// Number of DNS lookups that have started but not yet completed or failed.\n    /// </summary>\n    public long CurrentDnsLookups { get; internal set; }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/NetSecurity/INetSecurityTelemetryConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Security.Authentication;\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// A consumer of System.Net.Security EventSource events.\n/// </summary>\npublic interface INetSecurityTelemetryConsumer\n{\n    /// <summary>\n    /// Called before a handshake.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"isServer\">Indicates whether we are authenticating as the server.</param>\n    /// <param name=\"targetHost\">Name of the host we are authenticating with.</param>\n    void OnHandshakeStart(DateTime timestamp, bool isServer, string targetHost) { }\n\n    /// <summary>\n    /// Called after a handshake.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"protocol\">The protocol established by the handshake.</param>\n    void OnHandshakeStop(DateTime timestamp, SslProtocols protocol) { }\n\n    /// <summary>\n    /// Called before <see cref=\"OnHandshakeStop(DateTime, SslProtocols)\"/> if the handshake failed.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"isServer\">Indicates whether we were authenticating as the server.</param>\n    /// <param name=\"elapsed\">Time elapsed since the start of the handshake.</param>\n    /// <param name=\"exceptionMessage\">Exception information for the handshake failure.</param>\n    void OnHandshakeFailed(DateTime timestamp, bool isServer, TimeSpan elapsed, string exceptionMessage) { }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/NetSecurity/NetSecurityEventListenerService.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Diagnostics;\nusing System.Diagnostics.Tracing;\nusing System.Security.Authentication;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.Telemetry.Consumption;\n\ninternal sealed class NetSecurityEventListenerService : EventListenerService<NetSecurityEventListenerService, INetSecurityTelemetryConsumer, NetSecurityMetrics>\n{\n    protected override string EventSourceName => \"System.Net.Security\";\n\n    protected override int NumberOfMetrics => 14;\n\n    public NetSecurityEventListenerService(ILogger<NetSecurityEventListenerService> logger, IEnumerable<INetSecurityTelemetryConsumer> telemetryConsumers, IEnumerable<IMetricsConsumer<NetSecurityMetrics>> metricsConsumers)\n        : base(logger, telemetryConsumers, metricsConsumers)\n    { }\n\n    protected override void OnEvent(INetSecurityTelemetryConsumer[] consumers, EventWrittenEventArgs eventData)\n    {\n#pragma warning disable IDE0007 // Use implicit type\n        // Explicit type here to drop the object? signature of payload elements\n        ReadOnlyCollection<object> payload = eventData.Payload!;\n#pragma warning restore IDE0007 // Use implicit type\n\n        switch (eventData.EventId)\n        {\n            case 1:\n                Debug.Assert(eventData.EventName == \"HandshakeStart\" && payload.Count == 2);\n                {\n                    var isServer = (bool)payload[0];\n                    var targetHost = (string)payload[1];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnHandshakeStart(eventData.TimeStamp, isServer, targetHost);\n                    }\n                }\n                break;\n\n            case 2:\n                Debug.Assert(eventData.EventName == \"HandshakeStop\" && payload.Count == 1);\n                {\n                    var protocol = (SslProtocols)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnHandshakeStop(eventData.TimeStamp, protocol);\n                    }\n                }\n                break;\n\n            case 3:\n                Debug.Assert(eventData.EventName == \"HandshakeFailed\" && payload.Count == 3);\n                {\n                    var isServer = (bool)payload[0];\n                    var elapsed = TimeSpan.FromMilliseconds((double)payload[1]);\n                    var exceptionMessage = (string)payload[2];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnHandshakeFailed(eventData.TimeStamp, isServer, elapsed, exceptionMessage);\n                    }\n                }\n                break;\n        }\n    }\n\n    protected override bool TrySaveMetric(NetSecurityMetrics metrics, string name, double value)\n    {\n        switch (name)\n        {\n            case \"tls-handshake-rate\":\n                metrics.TlsHandshakeRate = (long)value;\n                break;\n\n            case \"total-tls-handshakes\":\n                metrics.TotalTlsHandshakes = (long)value;\n                break;\n\n            case \"current-tls-handshakes\":\n                metrics.CurrentTlsHandshakes = (long)value;\n                break;\n\n            case \"failed-tls-handshakes\":\n                metrics.FailedTlsHandshakes = (long)value;\n                break;\n\n            case \"all-tls-sessions-open\":\n                metrics.TlsSessionsOpen = (long)value;\n                break;\n\n            case \"tls10-sessions-open\":\n                metrics.Tls10SessionsOpen = (long)value;\n                break;\n\n            case \"tls11-sessions-open\":\n                metrics.Tls11SessionsOpen = (long)value;\n                break;\n\n            case \"tls12-sessions-open\":\n                metrics.Tls12SessionsOpen = (long)value;\n                break;\n\n            case \"tls13-sessions-open\":\n                metrics.Tls13SessionsOpen = (long)value;\n                break;\n\n            case \"all-tls-handshake-duration\":\n                metrics.TlsHandshakeDuration = TimeSpan.FromMilliseconds(value);\n                break;\n\n            case \"tls10-handshake-duration\":\n                metrics.Tls10HandshakeDuration = TimeSpan.FromMilliseconds(value);\n                break;\n\n            case \"tls11-handshake-duration\":\n                metrics.Tls11HandshakeDuration = TimeSpan.FromMilliseconds(value);\n                break;\n\n            case \"tls12-handshake-duration\":\n                metrics.Tls12HandshakeDuration = TimeSpan.FromMilliseconds(value);\n                break;\n\n            case \"tls13-handshake-duration\":\n                metrics.Tls13HandshakeDuration = TimeSpan.FromMilliseconds(value);\n                break;\n\n            default:\n                return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/NetSecurity/NetSecurityMetrics.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// Represents metrics reported by the System.Net.Security event counters.\n/// </summary>\npublic sealed class NetSecurityMetrics\n{\n    public NetSecurityMetrics() => Timestamp = DateTime.UtcNow;\n\n    /// <summary>\n    /// Timestamp of when this <see cref=\"NetSecurityMetrics\"/> instance was created.\n    /// </summary>\n    public DateTime Timestamp { get; internal set; }\n\n    /// <summary>\n    /// Number of TLS handshakes completed in the last metrics interval.\n    /// </summary>\n    public long TlsHandshakeRate { get; internal set; }\n\n    /// <summary>\n    /// Number of TLS handshakes completed since telemetry was enabled.\n    /// </summary>\n    public long TotalTlsHandshakes { get; internal set; }\n\n    /// <summary>\n    /// Number of active TLS handshakes that have started but not yet completed or failed.\n    /// </summary>\n    public long CurrentTlsHandshakes { get; internal set; }\n\n    /// <summary>\n    /// Number of TLS handshakes that failed since telemetry was enabled.\n    /// </summary>\n    public long FailedTlsHandshakes { get; internal set; }\n\n    /// <summary>\n    /// Number of currently open TLS sessions.\n    /// </summary>\n    public long TlsSessionsOpen { get; internal set; }\n\n    /// <summary>\n    /// Number of currently open TLS 1.0 sessions.\n    /// </summary>\n    public long Tls10SessionsOpen { get; internal set; }\n\n    /// <summary>\n    /// Number of currently open TLS 1.1 sessions.\n    /// </summary>\n    public long Tls11SessionsOpen { get; internal set; }\n\n    /// <summary>\n    /// Number of currently open TLS 1.2 sessions.\n    /// </summary>\n    public long Tls12SessionsOpen { get; internal set; }\n\n    /// <summary>\n    /// Number of currently open TLS 1.3 sessions.\n    /// </summary>\n    public long Tls13SessionsOpen { get; internal set; }\n\n    /// <summary>\n    /// Average duration of all TLS handshakes completed in the last metrics interval.\n    /// </summary>\n    public TimeSpan TlsHandshakeDuration { get; internal set; }\n\n    /// <summary>\n    /// Average duration of all TLS 1.0 handshakes completed in the last metrics interval.\n    /// </summary>\n    public TimeSpan Tls10HandshakeDuration { get; internal set; }\n\n    /// <summary>\n    /// Average duration of all TLS 1.1 handshakes completed in the last metrics interval.\n    /// </summary>\n    public TimeSpan Tls11HandshakeDuration { get; internal set; }\n\n    /// <summary>\n    /// Average duration of all TLS 1.2 handshakes completed in the last metrics interval.\n    /// </summary>\n    public TimeSpan Tls12HandshakeDuration { get; internal set; }\n\n    /// <summary>\n    /// Average duration of all TLS 1.3 handshakes completed in the last metrics interval.\n    /// </summary>\n    public TimeSpan Tls13HandshakeDuration { get; internal set; }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/README.md",
    "content": "YARP (Yet Another Reverse Proxy) is a highly customizable reverse proxy built using .NET. This package extends the base Yarp.ReverseProxy implementation to enable consuming AspNetCore, HttpClient, and YARP telemetry in process, allowing you to live monitor the performance and export the data as needed.\n\nTo learn more see the docs at https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/diagnosing-yarp-issues#using-telemetry-events and the GitHub repo at https://github.com/dotnet/yarp.\n"
  },
  {
    "path": "src/TelemetryConsumption/Sockets/ISocketsTelemetryConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Sockets;\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// A consumer of System.Net.Sockets EventSource events.\n/// </summary>\npublic interface ISocketsTelemetryConsumer\n{\n    /// <summary>\n    /// Called before a Socket connect.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"address\">Socket address we are connecting to.</param>\n    void OnConnectStart(DateTime timestamp, string address) { }\n\n    /// <summary>\n    /// Called after a Socket connect.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    void OnConnectStop(DateTime timestamp) { }\n\n    /// <summary>\n    /// Called before <see cref=\"OnConnectStop(DateTime)\"/> if the connect failed.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"error\"><see cref=\"SocketError\"/> information for the connect failure.</param>\n    /// <param name=\"exceptionMessage\">Exception information for the connect failure.</param>\n    void OnConnectFailed(DateTime timestamp, SocketError error, string exceptionMessage) { }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/Sockets/SocketsEventListenerService.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Diagnostics;\nusing System.Diagnostics.Tracing;\nusing System.Net.Sockets;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.Telemetry.Consumption;\n\ninternal sealed class SocketsEventListenerService : EventListenerService<SocketsEventListenerService, ISocketsTelemetryConsumer, SocketsMetrics>\n{\n    protected override string EventSourceName => \"System.Net.Sockets\";\n\n    protected override int NumberOfMetrics => 7;\n\n    public SocketsEventListenerService(ILogger<SocketsEventListenerService> logger, IEnumerable<ISocketsTelemetryConsumer> telemetryConsumers, IEnumerable<IMetricsConsumer<SocketsMetrics>> metricsConsumers)\n        : base(logger, telemetryConsumers, metricsConsumers)\n    { }\n\n    protected override void OnEvent(ISocketsTelemetryConsumer[] consumers, EventWrittenEventArgs eventData)\n    {\n#pragma warning disable IDE0007 // Use implicit type\n        // Explicit type here to drop the object? signature of payload elements\n        ReadOnlyCollection<object> payload = eventData.Payload!;\n#pragma warning restore IDE0007 // Use implicit type\n\n        switch (eventData.EventId)\n        {\n            case 1:\n                Debug.Assert(eventData.EventName == \"ConnectStart\" && payload.Count == 1);\n                {\n                    var address = (string)payload[0];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnConnectStart(eventData.TimeStamp, address);\n                    }\n                }\n                break;\n\n            case 2:\n                Debug.Assert(eventData.EventName == \"ConnectStop\" && payload.Count == 0);\n                {\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnConnectStop(eventData.TimeStamp);\n                    }\n                }\n                break;\n\n            case 3:\n                Debug.Assert(eventData.EventName == \"ConnectFailed\" && payload.Count == 2);\n                {\n                    var error = (SocketError)payload[0];\n                    var exceptionMessage = (string)payload[1];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnConnectFailed(eventData.TimeStamp, error, exceptionMessage);\n                    }\n                }\n                break;\n        }\n    }\n\n    protected override bool TrySaveMetric(SocketsMetrics metrics, string name, double value)\n    {\n        var longValue = (long)value;\n\n        switch (name)\n        {\n            case \"outgoing-connections-established\":\n                metrics.OutgoingConnectionsEstablished = longValue;\n                break;\n\n            case \"incoming-connections-established\":\n                metrics.IncomingConnectionsEstablished = longValue;\n                break;\n\n            case \"bytes-received\":\n                metrics.BytesReceived = longValue;\n                break;\n\n            case \"bytes-sent\":\n                metrics.BytesSent = longValue;\n                break;\n\n            case \"datagrams-received\":\n                metrics.DatagramsReceived = longValue;\n                break;\n\n            case \"datagrams-sent\":\n                metrics.DatagramsSent = longValue;\n                break;\n\n            case \"current-outgoing-connect-attempts\":\n                metrics.CurrentOutgoingConnectAttempts = longValue;\n                break;\n\n            default:\n                return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/Sockets/SocketsMetrics.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// Represents metrics reported by the System.Net.Sockets event counters.\n/// </summary>\npublic sealed class SocketsMetrics\n{\n    public SocketsMetrics() => Timestamp = DateTime.UtcNow;\n\n    /// <summary>\n    /// Timestamp of when this <see cref=\"SocketsMetrics\"/> instance was created.\n    /// </summary>\n    public DateTime Timestamp { get; internal set; }\n\n    /// <summary>\n    /// Number of outgoing (Connect) Socket connections established since telemetry was enabled.\n    /// </summary>\n    public long OutgoingConnectionsEstablished { get; internal set; }\n\n    /// <summary>\n    /// Number of incoming (Accept) Socket connections established since telemetry was enabled.\n    /// </summary>\n    public long IncomingConnectionsEstablished { get; internal set; }\n\n    /// <summary>\n    /// Number of bytes received since telemetry was enabled.\n    /// </summary>\n    public long BytesReceived { get; internal set; }\n\n    /// <summary>\n    /// Number of bytes sent since telemetry was enabled.\n    /// </summary>\n    public long BytesSent { get; internal set; }\n\n    /// <summary>\n    /// Number of datagrams received since telemetry was enabled.\n    /// </summary>\n    public long DatagramsReceived { get; internal set; }\n\n    /// <summary>\n    /// Number of datagrams sent since telemetry was enabled.\n    /// </summary>\n    public long DatagramsSent { get; internal set; }\n\n    /// <summary>\n    /// Number of outgoing (Connect) Socket connection attempts that are currently in progress.\n    /// </summary>\n    public long CurrentOutgoingConnectAttempts { get; internal set; }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/TelemetryConsumptionExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics.CodeAnalysis;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Yarp.Telemetry.Consumption;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\npublic static class TelemetryConsumptionExtensions\n{\n    /// <summary>\n    /// Registers all telemetry listeners (Forwarder, Kestrel, Http, NameResolution, NetSecurity, Sockets and WebSockets).\n    /// </summary>\n    public static IServiceCollection AddTelemetryListeners(this IServiceCollection services)\n    {\n        services.AddHostedService<WebSocketsEventListenerService>();\n        services.AddHostedService<ForwarderEventListenerService>();\n        services.AddHostedService<KestrelEventListenerService>();\n        services.AddHostedService<HttpEventListenerService>();\n        services.AddHostedService<NameResolutionEventListenerService>();\n        services.AddHostedService<NetSecurityEventListenerService>();\n        services.AddHostedService<SocketsEventListenerService>();\n        return services;\n    }\n\n    /// <summary>\n    /// Registers a consumer singleton for every I*TelemetryConsumer interface it implements.\n    /// </summary>\n    public static IServiceCollection AddTelemetryConsumer(this IServiceCollection services, object consumer)\n    {\n        var implementsAny = false;\n\n        if (consumer is IWebSocketsTelemetryConsumer webSocketsTelemetryConsumer)\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton(webSocketsTelemetryConsumer));\n            implementsAny = true;\n        }\n\n        if (consumer is IForwarderTelemetryConsumer forwarderTelemetryConsumer)\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton(forwarderTelemetryConsumer));\n            implementsAny = true;\n        }\n\n        if (consumer is IKestrelTelemetryConsumer kestrelTelemetryConsumer)\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton(kestrelTelemetryConsumer));\n            implementsAny = true;\n        }\n\n        if (consumer is IHttpTelemetryConsumer httpTelemetryConsumer)\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton(httpTelemetryConsumer));\n            implementsAny = true;\n        }\n\n        if (consumer is INameResolutionTelemetryConsumer nameResolutionTelemetryConsumer)\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton(nameResolutionTelemetryConsumer));\n            implementsAny = true;\n        }\n\n        if (consumer is INetSecurityTelemetryConsumer netSecurityTelemetryConsumer)\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton(netSecurityTelemetryConsumer));\n            implementsAny = true;\n        }\n\n        if (consumer is ISocketsTelemetryConsumer socketsTelemetryConsumer)\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton(socketsTelemetryConsumer));\n            implementsAny = true;\n        }\n\n        if (!implementsAny)\n        {\n            throw new ArgumentException(\"The consumer must implement at least one I*TelemetryConsumer interface.\", nameof(consumer));\n        }\n\n        services.AddTelemetryListeners();\n\n        return services;\n    }\n\n    /// <summary>\n    /// Registers a <typeparamref name=\"TConsumer\"/> singleton for every I*TelemetryConsumer interface it implements.\n    /// </summary>\n    public static IServiceCollection AddTelemetryConsumer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TConsumer>(this IServiceCollection services)\n        where TConsumer : class\n    {\n        var implementsAny = false;\n\n        if (typeof(IWebSocketsTelemetryConsumer).IsAssignableFrom(typeof(TConsumer)))\n        {\n            services.AddSingleton(services => (IWebSocketsTelemetryConsumer)services.GetRequiredService<TConsumer>());\n            implementsAny = true;\n        }\n\n        if (typeof(IForwarderTelemetryConsumer).IsAssignableFrom(typeof(TConsumer)))\n        {\n            services.AddSingleton(services => (IForwarderTelemetryConsumer)services.GetRequiredService<TConsumer>());\n            implementsAny = true;\n        }\n\n        if (typeof(IKestrelTelemetryConsumer).IsAssignableFrom(typeof(TConsumer)))\n        {\n            services.AddSingleton(services => (IKestrelTelemetryConsumer)services.GetRequiredService<TConsumer>());\n            implementsAny = true;\n        }\n\n        if (typeof(IHttpTelemetryConsumer).IsAssignableFrom(typeof(TConsumer)))\n        {\n            services.AddSingleton(services => (IHttpTelemetryConsumer)services.GetRequiredService<TConsumer>());\n            implementsAny = true;\n        }\n\n        if (typeof(INameResolutionTelemetryConsumer).IsAssignableFrom(typeof(TConsumer)))\n        {\n            services.AddSingleton(services => (INameResolutionTelemetryConsumer)services.GetRequiredService<TConsumer>());\n            implementsAny = true;\n        }\n\n        if (typeof(INetSecurityTelemetryConsumer).IsAssignableFrom(typeof(TConsumer)))\n        {\n            services.AddSingleton(services => (INetSecurityTelemetryConsumer)services.GetRequiredService<TConsumer>());\n            implementsAny = true;\n        }\n\n        if (typeof(ISocketsTelemetryConsumer).IsAssignableFrom(typeof(TConsumer)))\n        {\n            services.AddSingleton(services => (ISocketsTelemetryConsumer)services.GetRequiredService<TConsumer>());\n            implementsAny = true;\n        }\n\n        if (!implementsAny)\n        {\n            throw new ArgumentException(\"TConsumer must implement at least one I*TelemetryConsumer interface.\", nameof(TConsumer));\n        }\n\n        services.TryAddSingleton<TConsumer>();\n\n        services.AddTelemetryListeners();\n\n        return services;\n    }\n\n    /// <summary>\n    /// Registers a consumer singleton for every IMetricsConsumer interface it implements.\n    /// </summary>\n    public static IServiceCollection AddMetricsConsumer(this IServiceCollection services, object consumer)\n    {\n        var implementsAny = false;\n\n        if (consumer is IMetricsConsumer<ForwarderMetrics> forwarderMetricsConsumer)\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton(forwarderMetricsConsumer));\n            implementsAny = true;\n        }\n\n        if (consumer is IMetricsConsumer<KestrelMetrics> kestrelMetricsConsumer)\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton(kestrelMetricsConsumer));\n            implementsAny = true;\n        }\n\n        if (consumer is IMetricsConsumer<HttpMetrics> httpMetricsConsumer)\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton(httpMetricsConsumer));\n            implementsAny = true;\n        }\n\n        if (consumer is IMetricsConsumer<NameResolutionMetrics> nameResolutionMetricsConsumer)\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton(nameResolutionMetricsConsumer));\n            implementsAny = true;\n        }\n\n        if (consumer is IMetricsConsumer<NetSecurityMetrics> netSecurityMetricsConsumer)\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton(netSecurityMetricsConsumer));\n            implementsAny = true;\n        }\n\n        if (consumer is IMetricsConsumer<SocketsMetrics> socketsMetricsConsumer)\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton(socketsMetricsConsumer));\n            implementsAny = true;\n        }\n\n        if (!implementsAny)\n        {\n            throw new ArgumentException(\"The consumer must implement at least one IMetricsConsumer interface.\", nameof(consumer));\n        }\n\n        services.AddTelemetryListeners();\n\n        return services;\n    }\n\n    /// <summary>\n    /// Registers a consumer singleton for every IMetricsConsumer interface it implements.\n    /// </summary>\n    public static IServiceCollection AddMetricsConsumer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TConsumer>(this IServiceCollection services)\n        where TConsumer : class\n    {\n        var implementsAny = false;\n\n        if (typeof(IMetricsConsumer<ForwarderMetrics>).IsAssignableFrom(typeof(TConsumer)))\n        {\n            services.AddSingleton(services => (IMetricsConsumer<ForwarderMetrics>)services.GetRequiredService<TConsumer>());\n            implementsAny = true;\n        }\n\n        if (typeof(IMetricsConsumer<KestrelMetrics>).IsAssignableFrom(typeof(TConsumer)))\n        {\n            services.AddSingleton(services => (IMetricsConsumer<KestrelMetrics>)services.GetRequiredService<TConsumer>());\n            implementsAny = true;\n        }\n\n        if (typeof(IMetricsConsumer<HttpMetrics>).IsAssignableFrom(typeof(TConsumer)))\n        {\n            services.AddSingleton(services => (IMetricsConsumer<HttpMetrics>)services.GetRequiredService<TConsumer>());\n            implementsAny = true;\n        }\n\n        if (typeof(IMetricsConsumer<NameResolutionMetrics>).IsAssignableFrom(typeof(TConsumer)))\n        {\n            services.AddSingleton(services => (IMetricsConsumer<NameResolutionMetrics>)services.GetRequiredService<TConsumer>());\n            implementsAny = true;\n        }\n\n        if (typeof(IMetricsConsumer<NetSecurityMetrics>).IsAssignableFrom(typeof(TConsumer)))\n        {\n            services.AddSingleton(services => (IMetricsConsumer<NetSecurityMetrics>)services.GetRequiredService<TConsumer>());\n            implementsAny = true;\n        }\n\n        if (typeof(IMetricsConsumer<SocketsMetrics>).IsAssignableFrom(typeof(TConsumer)))\n        {\n            services.AddSingleton(services => (IMetricsConsumer<SocketsMetrics>)services.GetRequiredService<TConsumer>());\n            implementsAny = true;\n        }\n\n        if (!implementsAny)\n        {\n            throw new ArgumentException(\"TConsumer must implement at least one IMetricsConsumer interface.\", nameof(TConsumer));\n        }\n\n        services.TryAddSingleton<TConsumer>();\n\n        services.AddTelemetryListeners();\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/WebSockets/IWebSocketsTelemetryConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// A consumer of Yarp.ReverseProxy.WebSockets EventSource events.\n/// </summary>\npublic interface IWebSocketsTelemetryConsumer\n{\n    /// <summary>\n    /// Called when a WebSockets connection is closed.\n    /// </summary>\n    /// <param name=\"timestamp\">Timestamp when the event was fired.</param>\n    /// <param name=\"establishedTime\">Timestamp when the connection upgrade completed.</param>\n    /// <param name=\"closeReason\">The reason the WebSocket connection closed.</param>\n    /// <param name=\"messagesRead\">Messages read by the destination server.</param>\n    /// <param name=\"messagesWritten\">Messages sent by the destination server.</param>\n    void OnWebSocketClosed(DateTime timestamp, DateTime establishedTime, WebSocketCloseReason closeReason, long messagesRead, long messagesWritten);\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/WebSockets/WebSocketCloseReason.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.Telemetry.Consumption;\n\n/// <summary>\n/// The reason the WebSocket connection closed.\n/// </summary>\npublic enum WebSocketCloseReason : int\n{\n    Unknown,\n    ClientGracefulClose,\n    ServerGracefulClose,\n    ClientDisconnect,\n    ServerDisconnect,\n    ActivityTimeout,\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/WebSockets/WebSocketsEventListenerService.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Diagnostics;\nusing System.Diagnostics.Tracing;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.Telemetry.Consumption;\n\ninternal sealed class WebSocketsMetrics { }\n\ninternal sealed class WebSocketsEventListenerService : EventListenerService<WebSocketsEventListenerService, IWebSocketsTelemetryConsumer, WebSocketsMetrics>\n{\n    protected override string EventSourceName => \"Yarp.ReverseProxy.WebSockets\";\n\n    protected override int NumberOfMetrics => 0;\n\n    public WebSocketsEventListenerService(ILogger<WebSocketsEventListenerService> logger, IEnumerable<IWebSocketsTelemetryConsumer> telemetryConsumers, IEnumerable<IMetricsConsumer<WebSocketsMetrics>> metricsConsumers)\n        : base(logger, telemetryConsumers, metricsConsumers)\n    { }\n\n    protected override void OnEvent(IWebSocketsTelemetryConsumer[] consumers, EventWrittenEventArgs eventData)\n    {\n#pragma warning disable IDE0007 // Use implicit type\n        // Explicit type here to drop the object? signature of payload elements\n        ReadOnlyCollection<object> payload = eventData.Payload!;\n#pragma warning restore IDE0007 // Use implicit type\n\n        switch (eventData.EventId)\n        {\n            case 1:\n                Debug.Assert(eventData.EventName == \"WebSocketClosed\" && payload.Count == 4);\n                {\n                    var establishedTime = new DateTime((long)payload[0]);\n                    var closeReason = (WebSocketCloseReason)payload[1];\n                    var messagesRead = (long)payload[2];\n                    var messagesWritten = (long)payload[3];\n                    foreach (var consumer in consumers)\n                    {\n                        consumer.OnWebSocketClosed(eventData.TimeStamp, establishedTime, closeReason, messagesRead, messagesWritten);\n                    }\n                }\n                break;\n        }\n    }\n\n    protected override bool TrySaveMetric(WebSocketsMetrics metrics, string name, double value)\n    {\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/TelemetryConsumption/Yarp.Telemetry.Consumption.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <Description>Yarp.ReverseProxy extension package for in-process telemetry consumption</Description>\n    <TargetFrameworks>$(ReleaseTFMs)</TargetFrameworks>\n    <OutputType>Library</OutputType>\n    <RootNamespace>Yarp.Telemetry.Consumption</RootNamespace>\n    <Nullable>enable</Nullable>\n    <IsAotCompatible>true</IsAotCompatible>\n    <PackageReadmeFile>README.md</PackageReadmeFile>\n    <PackageTags>yarp;dotnet;reverse-proxy;aspnetcore;telemetry</PackageTags>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <InternalsVisibleTo Include=\"Yarp.ReverseProxy.FunctionalTests\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Include=\"README.md\" Pack=\"true\" PackagePath=\"\\\"/>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "startvs.cmd",
    "content": "@ECHO OFF\nSETLOCAL\n\n:: This command launches a Visual Studio solution with environment variables required to use a local version of the .NET Core SDK.\n\n:: This tells .NET Core to use the same dotnet.exe that build scripts use\nSET DOTNET_ROOT=%~dp0.dotnet\nSET DOTNET_ROOT(x86)=%~dp0.dotnet\\x86\n\n:: This tells .NET Core not to go looking for .NET Core in other places\nSET DOTNET_MULTILEVEL_LOOKUP=0\n\n:: Put our local dotnet.exe on PATH first so Visual Studio knows which one to use\nSET PATH=%DOTNET_ROOT%;%PATH%\n\nSET sln=%~1\n\nIF \"%sln%\"==\"\" (\n    echo Solution not specified, using YARP.slnx\n    SET sln=%~dp0YARP.slnx\n)\n\nIF NOT EXIST \"%DOTNET_ROOT%\\dotnet.exe\" (\n    echo .NET Core has not yet been installed. Run `%~dp0restore.cmd` to install tools\n    exit /b 1\n)\n\nstart \"\" \"%sln%\"\n"
  },
  {
    "path": "test/Directory.Build.props",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project>\n  <!-- Recurse up. -->\n  <Import Project=\"$(MSBuildThisFileDirectory)..\\Directory.Build.props\" />\n\n  <PropertyGroup>\n    <IsTestProject>true</IsTestProject>\n    <TestRunnerName>XUnitV3</TestRunnerName>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "test/Kubernetes.Tests/Certificates/CertificateHelperTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing Microsoft.Extensions.Logging;\nusing Moq;\nusing Xunit;\nusing Yarp.Kubernetes.Tests;\n\nnamespace Yarp.Kubernetes.Controller.Certificates.Tests;\n\npublic class CertificateHelperTests\n{\n    private readonly Mock<ILogger<CertificateHelper>> _mockLogger;\n    private readonly ICertificateHelper _certificateHelper;\n    private readonly byte[] _pemPrivateKey;\n    private readonly byte[] _pemCert;\n    private readonly byte[] _derPrivateKey;\n    private readonly byte[] _derCert;\n\n    public CertificateHelperTests()\n    {\n        _mockLogger = new Mock<ILogger<CertificateHelper>>();\n\n        _certificateHelper = new CertificateHelper(_mockLogger.Object);\n\n        _pemCert = ReadManifestData(\".Certificates.cert.pem\");\n        _pemPrivateKey = ReadManifestData(\".Certificates.key.pem\");\n        _derCert = ReadManifestData(\".Certificates.cert.der\");\n        _derPrivateKey = ReadManifestData(\".Certificates.key.der\");\n    }\n\n    [Theory]\n    [InlineData(true, true, true)]\n    [InlineData(false, true, false)]\n    [InlineData(true, false, false)]\n    [InlineData(false, false, false)]\n    public void CertificateConversionFromPem(bool loadCert, bool loadKey, bool expectCert)\n    {\n        // Arrange\n        var cert = loadCert ? _pemCert : (byte[])null;\n        var key = loadKey ? _pemPrivateKey : (byte[])null;\n\n        var secret = KubeResourceGenerator.CreateSecret(\"yarp-ingress-tls\", \"default\", cert, key);\n        var namespacedName = NamespacedName.From(secret);\n\n        // Act\n        var actualCertificate = _certificateHelper.ConvertCertificate(namespacedName, secret);\n\n        // Assert\n        if (expectCert)\n        {\n            Assert.NotNull(actualCertificate);\n        }\n        else\n        {\n            Assert.Null(actualCertificate);\n        }\n    }\n\n    [Theory]\n    [InlineData(true, true, true)]\n    [InlineData(false, true, false)]\n    [InlineData(true, false, false)]\n    [InlineData(false, false, false)]\n    public void CertificateConversionFromDer(bool loadCert, bool loadKey, bool expectCert)\n    {\n        // Arrange\n        var cert = loadCert ? _derCert : (byte[])null;\n        var key = loadKey ? _derPrivateKey : (byte[])null;\n\n        var secret = KubeResourceGenerator.CreateSecret(\"yarp-ingress-tls\", \"default\", cert, key);\n        var namespacedName = NamespacedName.From(secret);\n\n        // Act\n        var actualCertificate = _certificateHelper.ConvertCertificate(namespacedName, secret);\n\n        // Assert\n        if (expectCert)\n        {\n            Assert.NotNull(actualCertificate);\n        }\n        else\n        {\n            Assert.Null(actualCertificate);\n        }\n    }\n\n    private static byte[] ReadManifestData(string ending)\n    {\n        var assembly = typeof(CertificateHelperTests).Assembly;\n        var resourceName = assembly.GetManifestResourceNames().Single(str => str.EndsWith(ending));\n        var manifestStream = assembly.GetManifestResourceStream(resourceName);\n\n        using var reader = new StreamReader(manifestStream);\n        return Encoding.UTF8.GetBytes(reader.ReadToEnd());\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Certificates/cert.der",
    "content": "MIIDSDCCAjACCQCTqER3kAB0EDANBgkqhkiG9w0BAQQFADBmMRMwEQYDVQQKEwpN\neSBDb21wYW55MRAwDgYDVQQHEwdNeSBUb3duMRwwGgYDVQQIExNTdGF0ZSBvciBQ\ncm92aWRlbmNlMQswCQYDVQQGEwJVUzESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTIy\nMDUyMjA0MDI0M1oXDTMyMDUxOTA0MDI0M1owZjETMBEGA1UEChMKTXkgQ29tcGFu\neTEQMA4GA1UEBxMHTXkgVG93bjEcMBoGA1UECBMTU3RhdGUgb3IgUHJvdmlkZW5j\nZTELMAkGA1UEBhMCVVMxEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAL40UpmJAyiKjO0A5sFF0GjYTBNsJB4xeep2z70f\n41A2414lhBwlL4EEMTr9XFh8p91w2uIDKbeASXzZg5AMWVEX9W+dBfZUvrIo4fTL\ne67iV1LbWb4DD8PGHO+M7ZS7ENnOQdhcoM5zTf3I9RgVlsPMXt1IKrBRovjD+C7c\nUT6UEZYYcRjp3Z4DiYjBPsci0YOlknPwsJf4tsVddN8Mb4PS4Zq6MA/2Ce48GMSH\niX6WPy2pL+jjNo9AkUw2rD1mCfqDvVSoeI/4PlvmtcnwcSDBKY97ZhGAf5s243EE\nKBfRwrKher5ZQDr8QhePkq5NehPcxDU+x1NHLnMIVJnga7sCAwEAATANBgkqhkiG\n9w0BAQQFAAOCAQEAgAPecGaSvaoNOnU9njQgxfxVcAIVLPboN4h99Tvalry9ra+i\nLsdPyiUJB7tmWcDBkZnLoOFEHkrns27KckWKDZwY3SMTT1osQdcLP2CgJLWPX2U2\nWYjOOhYRMPCBAI40isPFvv+5T4KbIllAFwPSi3gG+3NCAka1YZq3m/8iR70zhI/B\ntfmjzQeUsVM4D5Ts2NjLPMVKcwSsZixmGkEZpUDinhuwHAbli86URt1MpsHATK+W\nA2R7/h+p+j5AX7LBXdUz+h/cbP5ENElrrk1H/82G8D56Beis5womOjYoYDTBy9oO\nZvi8b11fgIjeakv7MyVOhf/ZkiMlOQK5R3P92g==\n"
  },
  {
    "path": "test/Kubernetes.Tests/Certificates/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDSDCCAjACCQCTqER3kAB0EDANBgkqhkiG9w0BAQQFADBmMRMwEQYDVQQKEwpN\neSBDb21wYW55MRAwDgYDVQQHEwdNeSBUb3duMRwwGgYDVQQIExNTdGF0ZSBvciBQ\ncm92aWRlbmNlMQswCQYDVQQGEwJVUzESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTIy\nMDUyMjA0MDI0M1oXDTMyMDUxOTA0MDI0M1owZjETMBEGA1UEChMKTXkgQ29tcGFu\neTEQMA4GA1UEBxMHTXkgVG93bjEcMBoGA1UECBMTU3RhdGUgb3IgUHJvdmlkZW5j\nZTELMAkGA1UEBhMCVVMxEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAL40UpmJAyiKjO0A5sFF0GjYTBNsJB4xeep2z70f\n41A2414lhBwlL4EEMTr9XFh8p91w2uIDKbeASXzZg5AMWVEX9W+dBfZUvrIo4fTL\ne67iV1LbWb4DD8PGHO+M7ZS7ENnOQdhcoM5zTf3I9RgVlsPMXt1IKrBRovjD+C7c\nUT6UEZYYcRjp3Z4DiYjBPsci0YOlknPwsJf4tsVddN8Mb4PS4Zq6MA/2Ce48GMSH\niX6WPy2pL+jjNo9AkUw2rD1mCfqDvVSoeI/4PlvmtcnwcSDBKY97ZhGAf5s243EE\nKBfRwrKher5ZQDr8QhePkq5NehPcxDU+x1NHLnMIVJnga7sCAwEAATANBgkqhkiG\n9w0BAQQFAAOCAQEAgAPecGaSvaoNOnU9njQgxfxVcAIVLPboN4h99Tvalry9ra+i\nLsdPyiUJB7tmWcDBkZnLoOFEHkrns27KckWKDZwY3SMTT1osQdcLP2CgJLWPX2U2\nWYjOOhYRMPCBAI40isPFvv+5T4KbIllAFwPSi3gG+3NCAka1YZq3m/8iR70zhI/B\ntfmjzQeUsVM4D5Ts2NjLPMVKcwSsZixmGkEZpUDinhuwHAbli86URt1MpsHATK+W\nA2R7/h+p+j5AX7LBXdUz+h/cbP5ENElrrk1H/82G8D56Beis5womOjYoYDTBy9oO\nZvi8b11fgIjeakv7MyVOhf/ZkiMlOQK5R3P92g==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/Kubernetes.Tests/Certificates/key.der",
    "content": "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+NFKZiQMoiozt\nAObBRdBo2EwTbCQeMXnqds+9H+NQNuNeJYQcJS+BBDE6/VxYfKfdcNriAym3gEl8\n2YOQDFlRF/VvnQX2VL6yKOH0y3uu4ldS21m+Aw/DxhzvjO2UuxDZzkHYXKDOc039\nyPUYFZbDzF7dSCqwUaL4w/gu3FE+lBGWGHEY6d2eA4mIwT7HItGDpZJz8LCX+LbF\nXXTfDG+D0uGaujAP9gnuPBjEh4l+lj8tqS/o4zaPQJFMNqw9Zgn6g71UqHiP+D5b\n5rXJ8HEgwSmPe2YRgH+bNuNxBCgX0cKyoXq+WUA6/EIXj5KuTXoT3MQ1PsdTRy5z\nCFSZ4Gu7AgMBAAECggEBAIoTA49PZgqNIaZ/DARrwNILipZi34lHk2BAZae+OU6m\nucFDbLbdy7FVsMNI3zuhKl7XKR3++86pAy/t2tK8FC6JPPOMQqLCfDhq8zS4bo3S\n419Tur70DAKrk5/WZzWb4qyqTSRagaW9EHXV5w5Xxb1XY9oxJEQgzzTVVhn2d8f7\nhcXJW6MyVuFnpAJcGbp9fwl2pGoIdG2BXzWq1ri9Gkgh9sTk1+CEHOLw/2d+QS8n\nK+PnP7LK8q6AI+VhXQPAFTyCfUnSRvtEufGU/wvA0B54kpqGVB4RObwEtSEuK0vq\nrfnN59TTtrDaFeiRtgWX1gUovZIP8iWALN1So+1oRWECgYEA7iiJCxP27+UxzmvP\nZ96bCx+latvEg08vOLdlbdbDLgTrv4GA/TLnbv7+Y0EXRjVIeTxfS+d8LA7fuqUp\nyHQcvd6Xv3jh+a6IF0H/0ongkZxTbuti3bE+WyTHS357MhW1AvPSp8b3BL1wx2Q/\nLCO7Du6kdIz3sS60y+qYEECkppECgYEAzHQdDOsKJwk+9QdNQwZuZpwEr6S3YVsz\nrUA7eHXT4Wak43WXCe2wkKl2DoifS1hIU9KGW9x0BzS2QQVZv+I/hohUF9YQKCnJ\ncuxBYbA8D/iycvoDpqyV76LiwwXRniRNm94luTQjLODFcck42aeoKkcZkjMWhaUj\nRWiPFR6Vy4sCgYBVYxEneKP7hNgjo0G8gvJhvZnoQx0k2xoaIp7qD6rw7/C6O6tM\nnJifkisQ2QCIOohed0fPhhJeFYMffyII7aB0br9HdgbHJ01B0XbwPGDYtAyx4xES\nXP73XKtbpOB0p5W7lkG7x7k+6NDrnESOBc2GYAd0hio0S4Ok7NpSUWr3EQKBgDwK\nhVV72Llp/7EZedkLFHTRsJacOrY+gEiKqmxPve9do2Kg78Acq1NwUJkoCg+oV2U2\nV/q2HOTY2AT0O00cdidd9cQiOxBwZRZ4xyKXDKxsDouxXE0gNc/v98Pp+4sDgj8Z\n194xr3rIb3Ng8m8Iy1vPEXVbx2tr+ZWyhQJgvwDNAoGATprpcJOqWemD9d4sTA8T\nURs1ihJIcUEVzBSv/mNgw0szs15qFers3rQJgPpS9yjm4oiFNsGT4Cc7bPDRYyb3\n90njZTRseMLAm2snMrv34I7saW9AGNotrA5LA5axbBiYmJ9NX75fNNlYMZeKxC/B\nRO+LjL/JqRyj4ymPMFfzUTo=\n"
  },
  {
    "path": "test/Kubernetes.Tests/Certificates/key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+NFKZiQMoiozt\nAObBRdBo2EwTbCQeMXnqds+9H+NQNuNeJYQcJS+BBDE6/VxYfKfdcNriAym3gEl8\n2YOQDFlRF/VvnQX2VL6yKOH0y3uu4ldS21m+Aw/DxhzvjO2UuxDZzkHYXKDOc039\nyPUYFZbDzF7dSCqwUaL4w/gu3FE+lBGWGHEY6d2eA4mIwT7HItGDpZJz8LCX+LbF\nXXTfDG+D0uGaujAP9gnuPBjEh4l+lj8tqS/o4zaPQJFMNqw9Zgn6g71UqHiP+D5b\n5rXJ8HEgwSmPe2YRgH+bNuNxBCgX0cKyoXq+WUA6/EIXj5KuTXoT3MQ1PsdTRy5z\nCFSZ4Gu7AgMBAAECggEBAIoTA49PZgqNIaZ/DARrwNILipZi34lHk2BAZae+OU6m\nucFDbLbdy7FVsMNI3zuhKl7XKR3++86pAy/t2tK8FC6JPPOMQqLCfDhq8zS4bo3S\n419Tur70DAKrk5/WZzWb4qyqTSRagaW9EHXV5w5Xxb1XY9oxJEQgzzTVVhn2d8f7\nhcXJW6MyVuFnpAJcGbp9fwl2pGoIdG2BXzWq1ri9Gkgh9sTk1+CEHOLw/2d+QS8n\nK+PnP7LK8q6AI+VhXQPAFTyCfUnSRvtEufGU/wvA0B54kpqGVB4RObwEtSEuK0vq\nrfnN59TTtrDaFeiRtgWX1gUovZIP8iWALN1So+1oRWECgYEA7iiJCxP27+UxzmvP\nZ96bCx+latvEg08vOLdlbdbDLgTrv4GA/TLnbv7+Y0EXRjVIeTxfS+d8LA7fuqUp\nyHQcvd6Xv3jh+a6IF0H/0ongkZxTbuti3bE+WyTHS357MhW1AvPSp8b3BL1wx2Q/\nLCO7Du6kdIz3sS60y+qYEECkppECgYEAzHQdDOsKJwk+9QdNQwZuZpwEr6S3YVsz\nrUA7eHXT4Wak43WXCe2wkKl2DoifS1hIU9KGW9x0BzS2QQVZv+I/hohUF9YQKCnJ\ncuxBYbA8D/iycvoDpqyV76LiwwXRniRNm94luTQjLODFcck42aeoKkcZkjMWhaUj\nRWiPFR6Vy4sCgYBVYxEneKP7hNgjo0G8gvJhvZnoQx0k2xoaIp7qD6rw7/C6O6tM\nnJifkisQ2QCIOohed0fPhhJeFYMffyII7aB0br9HdgbHJ01B0XbwPGDYtAyx4xES\nXP73XKtbpOB0p5W7lkG7x7k+6NDrnESOBc2GYAd0hio0S4Ok7NpSUWr3EQKBgDwK\nhVV72Llp/7EZedkLFHTRsJacOrY+gEiKqmxPve9do2Kg78Acq1NwUJkoCg+oV2U2\nV/q2HOTY2AT0O00cdidd9cQiOxBwZRZ4xyKXDKxsDouxXE0gNc/v98Pp+4sDgj8Z\n194xr3rIb3Ng8m8Iy1vPEXVbx2tr+ZWyhQJgvwDNAoGATprpcJOqWemD9d4sTA8T\nURs1ihJIcUEVzBSv/mNgw0szs15qFers3rQJgPpS9yjm4oiFNsGT4Cc7bPDRYyb3\n90njZTRseMLAm2snMrv34I7saW9AGNotrA5LA5axbBiYmJ9NX75fNNlYMZeKxC/B\nRO+LjL/JqRyj4ymPMFfzUTo=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "test/Kubernetes.Tests/Client/ResourceInformerTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.Models;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\nusing Yarp.Kubernetes.Tests.TestCluster;\nusing Yarp.Kubernetes.Tests.Utils;\n\nnamespace Yarp.Kubernetes.Controller.Client.Tests;\n\npublic class ResourceInformerTests\n{\n    private static (TResources Resources, TShouldBe ShouldBe) LoadTestResource<TResources, TShouldBe>(string name)\n    {\n        var resourcesYaml = File.ReadAllText(Path.Combine(\"testassets/resource-informer\", name, \"resources.yaml\"));\n        var shouldBeYaml = File.ReadAllText(Path.Combine(\"testassets/resource-informer\", name, \"shouldbe.yaml\"));\n\n        var resources = ResourceSerializers.DeserializeYaml<TResources>(resourcesYaml);\n        var shouldBe = ResourceSerializers.DeserializeYaml<TShouldBe>(shouldBeYaml);\n\n        return (resources, shouldBe);\n    }\n\n    [Fact]\n    public async Task ResourcesAreListedWhenReadyAsyncIsComplete()\n    {\n        using var cancellation = new CancellationTokenSource(Debugger.IsAttached ? TimeSpan.FromMinutes(5) : TimeSpan.FromSeconds(5));\n\n        var (resources, shouldBe) = LoadTestResource<V1Pod[], NamespacedName[]>(nameof(ResourcesAreListedWhenReadyAsyncIsComplete));\n\n        using var clusterHost = new TestClusterHostBuilder()\n            .UseInitialResources(resources)\n            .Build();\n\n        using var testHost = new HostBuilder()\n            .ConfigureServices((context, services) =>\n            {\n                services.AddKubernetesReverseProxy(context.Configuration);\n                services.RegisterResourceInformer<V1Pod, V1PodResourceInformer>();\n                services.Configure<KubernetesClientOptions>(options =>\n                {\n                    options.Configuration = KubernetesClientConfiguration.BuildConfigFromConfigObject(clusterHost.KubeConfig);\n                });\n            })\n            .Build();\n\n        var informer = testHost.Services.GetRequiredService<IResourceInformer<V1Pod>>();\n        var pods = new Dictionary<NamespacedName, V1Pod>();\n\n        informer.StartWatching();\n        using var registration = informer.Register((eventType, pod) =>\n        {\n            pods[NamespacedName.From(pod)] = pod;\n        });\n\n        await clusterHost.StartAsync(cancellation.Token);\n        await testHost.StartAsync(cancellation.Token);\n\n        await registration.ReadyAsync(cancellation.Token);\n\n        Assert.Equal(shouldBe, pods.Keys);\n    }\n\n    [Fact]\n    public async Task ResourcesWithApiGroupAreListed()\n    {\n        using var cancellation = new CancellationTokenSource(Debugger.IsAttached ? TimeSpan.FromMinutes(5) : TimeSpan.FromSeconds(5));\n\n        var (resources, shouldBe) = LoadTestResource<V1Deployment[], NamespacedName[]>(nameof(ResourcesWithApiGroupAreListed));\n\n        using var clusterHost = new TestClusterHostBuilder()\n            .UseInitialResources(resources)\n            .Build();\n\n        using var testHost = new HostBuilder()\n            .ConfigureServices((context, services) =>\n            {\n                services.AddKubernetesReverseProxy(context.Configuration);\n                services.RegisterResourceInformer<V1Deployment, V1DeploymentResourceInformer>();\n                services.Configure<KubernetesClientOptions>(options =>\n                {\n                    options.Configuration = KubernetesClientConfiguration.BuildConfigFromConfigObject(clusterHost.KubeConfig);\n                });\n            })\n            .Build();\n\n        var informer = testHost.Services.GetRequiredService<IResourceInformer<V1Deployment>>();\n        var deployments = new Dictionary<NamespacedName, V1Deployment>();\n\n        informer.StartWatching();\n        using var registration = informer.Register((eventType, deployment) =>\n        {\n            deployments[NamespacedName.From(deployment)] = deployment;\n        });\n\n        await clusterHost.StartAsync(cancellation.Token);\n        await testHost.StartAsync(cancellation.Token);\n\n        await registration.ReadyAsync(cancellation.Token);\n\n        Assert.Equal(shouldBe, deployments.Keys);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Client/SyncResourceInformer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.Models;\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.Immutable;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Client.Tests;\n\ninternal class SyncResourceInformer<TResource> : IResourceInformer<TResource>\n    where TResource : class, IKubernetesObject<V1ObjectMeta>, new()\n{\n    private readonly object _sync = new();\n    private ImmutableList<Registration> _registrations = ImmutableList<Registration>.Empty;\n\n    public void PublishUpdate(WatchEventType eventType, TResource resource)\n    {\n        List<ResourceInformerCallback<TResource>> callbacks;\n        lock (_sync)\n        {\n            callbacks = _registrations.Select(x => x.Callback).ToList();\n        }\n\n        callbacks.ForEach(x => x.Invoke(eventType, resource));\n    }\n\n    public Task ReadyAsync(CancellationToken cancellationToken)\n    {\n        return Task.CompletedTask;\n    }\n\n    public IResourceInformerRegistration Register(ResourceInformerCallback<TResource> callback)\n    {\n        return new Registration(this, callback);\n    }\n\n    public IResourceInformerRegistration Register(ResourceInformerCallback<IKubernetesObject<V1ObjectMeta>> callback)\n    {\n        return new Registration(this, (eventType, resource) => callback(eventType, resource));\n    }\n\n    public Task StartAsync(CancellationToken cancellationToken)\n    {\n        return Task.CompletedTask;\n    }\n\n    public void StartWatching()\n    {\n    }\n\n    public Task StopAsync(CancellationToken cancellationToken)\n    {\n        return Task.CompletedTask;\n    }\n\n    internal class Registration : IResourceInformerRegistration\n    {\n        private bool _disposedValue;\n\n        public Registration(SyncResourceInformer<TResource> resourceInformer, ResourceInformerCallback<TResource> callback)\n        {\n            ResourceInformer = resourceInformer;\n            Callback = callback;\n            lock (resourceInformer._sync)\n            {\n                resourceInformer._registrations = resourceInformer._registrations.Add(this);\n            }\n        }\n\n        ~Registration()\n        {\n            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method\n            Dispose(disposing: false);\n        }\n\n        public SyncResourceInformer<TResource> ResourceInformer { get; }\n        public ResourceInformerCallback<TResource> Callback { get; }\n\n        public Task ReadyAsync(CancellationToken cancellationToken) => ResourceInformer.ReadyAsync(cancellationToken);\n\n        protected virtual void Dispose(bool disposing)\n        {\n            if (!_disposedValue)\n            {\n                lock (ResourceInformer._sync)\n                {\n                    ResourceInformer._registrations = ResourceInformer._registrations.Remove(this);\n                }\n                _disposedValue = true;\n            }\n        }\n\n        public void Dispose()\n        {\n            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method\n            Dispose(disposing: true);\n            GC.SuppressFinalize(this);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Client/V1DeploymentResourceInformer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing k8s;\nusing k8s.Autorest;\nusing k8s.Models;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Client.Tests;\n\ninternal class V1DeploymentResourceInformer : ResourceInformer<V1Deployment, V1DeploymentList>\n{\n    public V1DeploymentResourceInformer(\n        IKubernetes client,\n        ResourceSelector<V1Deployment> selector,\n        IHostApplicationLifetime hostApplicationLifetime,\n        ILogger<V1DeploymentResourceInformer> logger)\n        : base(client, selector, hostApplicationLifetime, logger)\n    {\n    }\n\n    protected override Task<HttpOperationResponse<V1DeploymentList>> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector<V1Deployment> resourceSelector = null, CancellationToken cancellationToken = default)\n    {\n        return Client.AppsV1.ListDeploymentForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken);\n    }\n\n    protected override Watcher<V1Deployment> WatchResourceListAsync(string resourceVersion = null, ResourceSelector<V1Deployment> resourceSelector = null, Action<WatchEventType, V1Deployment> onEvent = null, Action<Exception> onError = null, Action onClosed = null)\n    {\n        return Client.AppsV1.WatchListDeploymentForAllNamespaces(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Client/V1PodResourceInformer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing k8s;\nusing k8s.Autorest;\nusing k8s.Models;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Controller.Client.Tests;\n\ninternal class V1PodResourceInformer : ResourceInformer<V1Pod, V1PodList>\n{\n    public V1PodResourceInformer(\n        IKubernetes client,\n        ResourceSelector<V1Pod> selector,\n        IHostApplicationLifetime hostApplicationLifetime,\n        ILogger<V1PodResourceInformer> logger)\n        : base(client, selector, hostApplicationLifetime, logger)\n    {\n    }\n\n    protected override Task<HttpOperationResponse<V1PodList>> RetrieveResourceListAsync(string resourceVersion = null, ResourceSelector<V1Pod> resourceSelector = null, CancellationToken cancellationToken = default)\n    {\n        return Client.CoreV1.ListPodForAllNamespacesWithHttpMessagesAsync(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, cancellationToken: cancellationToken);\n    }\n\n    protected override Watcher<V1Pod> WatchResourceListAsync(string resourceVersion = null, ResourceSelector<V1Pod> resourceSelector = null, Action<WatchEventType, V1Pod> onEvent = null, Action<Exception> onError = null, Action onClosed = null)\n    {\n        return Client.CoreV1.WatchListPodForAllNamespaces(resourceVersion: resourceVersion, fieldSelector: resourceSelector?.FieldSelector, onEvent: onEvent, onError: onError, onClosed: onClosed);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Hosting/BackgroundHostedServiceTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Hosting.Server;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\nusing Yarp.Kubernetes.Tests.Hosting.Fakes;\n\nnamespace Yarp.Kubernetes.Controller.Hosting.Tests;\n\npublic class BackgroundHostedServiceTests\n{\n    [Fact]\n    public async Task StartAndStopUnderHosting()\n    {\n        var latches = new TestLatches();\n\n        using var host = new HostBuilder()\n            .ConfigureServices((hbc, services) =>\n            {\n                services.AddSingleton<IHostedService, FakeBackgroundHostedService>();\n                services.AddSingleton(latches);\n            })\n            .Build();\n\n        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));\n        await host.StartAsync(cts.Token);\n        await latches.RunEnter.WhenSignalAsync(cts.Token);\n        latches.RunResult.Signal();\n        await latches.RunExit.WhenSignalAsync(cts.Token);\n        await host.StopAsync(cts.Token);\n    }\n\n    [Fact]\n    public async Task StartAndStopUnderWebHost()\n    {\n        var latches = new TestLatches();\n\n        using var host = new WebHostBuilder()\n            .ConfigureServices((hbc, services) =>\n            {\n                services.AddSingleton<IServer, FakeServer>();\n                services.AddSingleton<IHostedService, FakeBackgroundHostedService>();\n                services.AddSingleton(latches);\n            })\n            .Configure(app => { })\n            .Build();\n\n        // TODO: figure out why the hosting takes so long to unwind naturally\n        // and increase this safety cancellation up from 3 seconds\n        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));\n\n        var runTask = host.RunAsync(cts.Token);\n\n        await latches.RunEnter.WhenSignalAsync(cts.Token);\n        latches.RunResult.Signal();\n        await latches.RunExit.WhenSignalAsync(cts.Token);\n\n        await runTask;\n    }\n\n    [Fact]\n    public async Task IfRunAsyncThrowsItComesBackFromHost()\n    {\n        var context = new TestLatches();\n\n        using var host = new WebHostBuilder()\n            .ConfigureServices((hbc, services) =>\n            {\n                services.AddSingleton<IServer, FakeServer>();\n                services.AddSingleton<IHostedService, FakeBackgroundHostedService>();\n                services.AddSingleton(context);\n            })\n            .Configure(app => { })\n            .Build();\n\n        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));\n\n        var runTask = host.RunAsync(cts.Token);\n\n#pragma warning disable CA1303 // Do not pass literals as localized parameters\n        context.RunResult.Throw(new ApplicationException(\"Unwind\"));\n#pragma warning restore CA1303 // Do not pass literals as localized parameters\n\n        var ex = await Assert.ThrowsAsync<AggregateException>(() => runTask);\n\n        Assert.Equal(\"Unwind\", Assert.Single(ex.Flatten().InnerExceptions).Message);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Hosting/Fakes/FakeBackgroundHostedService.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Yarp.Kubernetes.Controller.Hosting;\n\nnamespace Yarp.Kubernetes.Tests.Hosting.Fakes;\n\npublic class FakeBackgroundHostedService : BackgroundHostedService\n{\n    private readonly TestLatches _context;\n\n    public FakeBackgroundHostedService(\n        TestLatches context,\n        IHostApplicationLifetime hostApplicationLifetime,\n        ILogger<FakeBackgroundHostedService> logger)\n        : base(hostApplicationLifetime, logger)\n    {\n        _context = context;\n    }\n\n    public override async Task RunAsync(CancellationToken cancellationToken)\n    {\n        try\n        {\n            _context.RunEnter.Signal();\n            await _context.RunResult.WhenSignalAsync(cancellationToken).ConfigureAwait(false);\n        }\n        finally\n        {\n            _context.RunExit.Signal();\n        }\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Hosting/Fakes/FakeServer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Hosting.Server;\nusing Microsoft.AspNetCore.Http.Features;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Tests.Hosting.Fakes;\n\npublic sealed class FakeServer : IServer\n{\n    public IFeatureCollection Features { get; } = new FeatureCollection();\n\n    public void Dispose()\n    {\n    }\n\n    public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)\n    {\n        return Task.CompletedTask;\n    }\n\n    public Task StopAsync(CancellationToken cancellationToken)\n    {\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Hosting/Fakes/TestLatch.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Tests.Hosting.Fakes;\n\npublic class TestLatch\n{\n    private readonly TaskCompletionSource<bool> _completion = new TaskCompletionSource<bool>();\n\n    public void Signal()\n    {\n        _completion.SetResult(false);\n    }\n    public void Throw(ApplicationException exception)\n    {\n        _completion.SetException(exception);\n    }\n\n    public async Task WhenSignalAsync(CancellationToken cancellationToken)\n    {\n        var task = await Task.WhenAny(\n            _completion.Task,\n            Task.Delay(TimeSpan.FromSeconds(90), cancellationToken))\n            .ConfigureAwait(false);\n\n        await task.ConfigureAwait(false);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Hosting/Fakes/TestLatches.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nnamespace Yarp.Kubernetes.Tests.Hosting.Fakes;\n\npublic class TestLatches\n{\n    public TestLatch RunEnter { get; } = new TestLatch();\n    public TestLatch RunResult { get; } = new TestLatch();\n    public TestLatch RunExit { get; } = new TestLatch();\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/IngressCacheTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\nusing k8s;\nusing k8s.Models;\nusing Microsoft.Extensions.Options;\nusing Moq;\nusing Xunit;\nusing Yarp.Kubernetes.Controller;\nusing Yarp.Kubernetes.Controller.Caching;\nusing Yarp.Kubernetes.Controller.Certificates;\nusing Yarp.Kubernetes.Tests.Utils;\n\nnamespace Yarp.Kubernetes.Tests;\n\npublic class IngressCacheTests\n{\n    private readonly Mock<IOptions<YarpOptions>> _mockOptions;\n    private readonly Mock<IServerCertificateSelector> _certificateSelector;\n    private readonly Mock<ICertificateHelper> _certificateHelper;\n    private readonly IngressCache _cacheUnderTest;\n    private readonly X509Certificate2 _localhostCertificate;\n\n    public IngressCacheTests(ITestOutputHelper output)\n    {\n        var logger = new TestLogger<IngressCache>(output);\n        _mockOptions = new Mock<IOptions<YarpOptions>>();\n        _certificateSelector = new Mock<IServerCertificateSelector>();\n        _certificateHelper = new Mock<ICertificateHelper>();\n\n        _mockOptions.SetupGet(o => o.Value).Returns(new YarpOptions { ControllerClass = \"microsoft.com/ingress-yarp\", DefaultSslCertificate = \"default/yarp-ingress-tls\" });\n\n        _cacheUnderTest = new IngressCache(_mockOptions.Object, _certificateSelector.Object, _certificateHelper.Object, logger);\n\n        // Generate a certificate for testing\n        var ecdsa = ECDsa.Create();\n        var req = new CertificateRequest(\"cn=localhost\", ecdsa, HashAlgorithmName.SHA256);\n        _localhostCertificate = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5));\n    }\n\n    [Theory]\n    [InlineData(\"yarp\", \"microsoft.com/ingress-yarp\", false, 1)]\n    [InlineData(\"yarp\", \"microsoft.com/ingress-yarp\", true, 1)]\n    [InlineData(\"yarp-internal\", \"microsoft.com/ingress-yarp-internal\", false, 0)]\n    [InlineData(\"yarp-internal\", \"microsoft.com/ingress-yarp-internal\", true, 0)]\n    [InlineData(null, null, false, 0)]\n    public void IngressWithClassAnnotationTests(string ingressClassName, string controllerName, bool? isDefault, int expectedIngressCount)\n    {\n        // Arrange\n        if (controllerName is not null)\n        {\n            var ingressClass = KubeResourceGenerator.CreateIngressClass(ingressClassName, controllerName, isDefault);\n            _cacheUnderTest.Update(WatchEventType.Added, ingressClass);\n        }\n\n        var ingress = KubeResourceGenerator.CreateIngress(\"ingress-with-class\", \"ns-test\", \"yarp\");\n\n        // Act\n        _cacheUnderTest.Update(WatchEventType.Added, ingress);\n\n        // Assert\n        var ingresses = _cacheUnderTest.GetIngresses().ToArray();\n\n        Assert.Equal(expectedIngressCount, ingresses.Length);\n    }\n\n    [Theory]\n    [InlineData(\"yarp\", \"microsoft.com/ingress-yarp\", true, 1)]\n    [InlineData(\"yarp\", \"microsoft.com/ingress-yarp\", false, 0)]\n    [InlineData(\"yarp-internal\", \"microsoft.com/ingress-yarp-internal\", false, 0)]\n    [InlineData(\"yarp-internal\", \"microsoft.com/ingress-yarp-internal\", true, 0)]\n    [InlineData(null, null, false, 0)]\n    public void IngressWithoutClassAnnotationTests(string ingressClassName, string controllerName, bool? isDefault, int expectedIngressCount)\n    {\n        // Arrange\n        if (controllerName is not null)\n        {\n            var ingressClass = KubeResourceGenerator.CreateIngressClass(ingressClassName, controllerName, isDefault);\n            _cacheUnderTest.Update(WatchEventType.Added, ingressClass);\n        }\n\n        var ingress = KubeResourceGenerator.CreateIngress(\"ingress-without-class\", \"ns-test\", null);\n\n        // Act\n        _cacheUnderTest.Update(WatchEventType.Added, ingress);\n\n        // Assert\n        var ingresses = _cacheUnderTest.GetIngresses().ToArray();\n\n        Assert.Equal(expectedIngressCount, ingresses.Length);\n    }\n\n    [Fact]\n    public void IngressModifiedToRemoveClass()\n    {\n        // Arrange\n        var ingressClass = KubeResourceGenerator.CreateIngressClass(\"yarp\", \"microsoft.com/ingress-yarp\", false);\n        _cacheUnderTest.Update(WatchEventType.Added, ingressClass);\n\n        var ingress = KubeResourceGenerator.CreateIngress(\"ingress-with-class\", \"ns-test\", \"yarp\");\n        _cacheUnderTest.Update(WatchEventType.Added, ingress);\n\n        // Act\n        ingress.Spec.IngressClassName = null;\n        _cacheUnderTest.Update(WatchEventType.Modified, ingress);\n\n        // Assert\n        var ingresses = _cacheUnderTest.GetIngresses().ToArray();\n        Assert.Empty(ingresses);\n    }\n\n    [Fact]\n    public void IngressClassModifiedToAddDefault()\n    {\n        // Arrange\n        var ingressClass = KubeResourceGenerator.CreateIngressClass(\"yarp\", \"microsoft.com/ingress-yarp\", false);\n        var ingress = KubeResourceGenerator.CreateIngress(\"ingress-with-class\", \"ns-test\", \"yarp\");\n\n        _cacheUnderTest.Update(WatchEventType.Added, ingressClass);\n\n        // Act\n        ingressClass.Metadata.Annotations.Add(\"ingressclass.kubernetes.io/is-default-class\", \"true\");\n\n        _cacheUnderTest.Update(WatchEventType.Modified, ingressClass);\n        _cacheUnderTest.Update(WatchEventType.Added, ingress);\n\n        // Assert\n        var ingresses = _cacheUnderTest.GetIngresses().ToArray();\n        Assert.Single(ingresses);\n    }\n\n    [Fact]\n    public void IngressClassDeleted()\n    {\n        // Arrange\n        var ingressClass = KubeResourceGenerator.CreateIngressClass(\"yarp\", \"microsoft.com/ingress-yarp\", true);\n        var ingress = KubeResourceGenerator.CreateIngress(\"ingress-with-class\", \"ns-test\", \"yarp\");\n\n        _cacheUnderTest.Update(WatchEventType.Added, ingressClass);\n\n        // Act\n        _cacheUnderTest.Update(WatchEventType.Deleted, ingressClass);\n        _cacheUnderTest.Update(WatchEventType.Added, ingress);\n\n        // Assert\n        var ingresses = _cacheUnderTest.GetIngresses().ToArray();\n        Assert.Empty(ingresses);\n    }\n\n    [Fact]\n    public void SecretNotMatchDefaultNameIgnored()\n    {\n        // Arrange\n        var secret = KubeResourceGenerator.CreateSecret(\"yarp\", \"not-my-tls\");\n\n        // Act\n        _cacheUnderTest.Update(WatchEventType.Added, secret);\n\n        // Assert\n        _certificateHelper.Verify(h => h.ConvertCertificate(It.IsAny<NamespacedName>(), It.IsAny<V1Secret>()), Times.Never);\n        _certificateSelector.Verify(s => s.AddCertificate(It.IsAny<NamespacedName>(), It.IsAny<X509Certificate2>()), Times.Never);\n    }\n\n    [Fact]\n    public void SecretMatchDefaultNameAdded()\n    {\n        // Arrange\n        var secret = KubeResourceGenerator.CreateSecret(\"yarp-ingress-tls\", \"default\");\n        _certificateHelper\n            .Setup(h => h.ConvertCertificate(It.Is<NamespacedName>(n => n.Name == \"yarp-ingress-tls\" && n.Namespace == \"default\"), It.Is<V1Secret>(s => s == secret)))\n            .Returns(_localhostCertificate);\n\n        // Act\n        _cacheUnderTest.Update(WatchEventType.Added, secret);\n\n        // Assert\n        _certificateHelper.Verify(h => h.ConvertCertificate(It.Is<NamespacedName>(n => n.Name == \"yarp-ingress-tls\" && n.Namespace == \"default\"), It.Is<V1Secret>(s => s == secret)), Times.Once);\n        _certificateSelector.Verify(s => s.AddCertificate(It.Is<NamespacedName>(n => n.Name == \"yarp-ingress-tls\" && n.Namespace == \"default\"), It.Is<X509Certificate2>(c => c == _localhostCertificate)), Times.Once);\n    }\n\n    [Fact]\n    public void SecretMatchDefaultNameModified()\n    {\n        // Arrange\n        var secret = KubeResourceGenerator.CreateSecret(\"yarp-ingress-tls\", \"default\");\n        _certificateHelper\n            .Setup(h => h.ConvertCertificate(It.Is<NamespacedName>(n => n.Name == \"yarp-ingress-tls\" && n.Namespace == \"default\"), It.Is<V1Secret>(s => s == secret)))\n            .Returns(_localhostCertificate);\n\n        // Act\n        _cacheUnderTest.Update(WatchEventType.Modified, secret);\n\n        // Assert\n        _certificateHelper.Verify(h => h.ConvertCertificate(It.Is<NamespacedName>(n => n.Name == \"yarp-ingress-tls\" && n.Namespace == \"default\"), It.Is<V1Secret>(s => s == secret)), Times.Once);\n        _certificateSelector.Verify(s => s.AddCertificate(It.Is<NamespacedName>(n => n.Name == \"yarp-ingress-tls\" && n.Namespace == \"default\"), It.Is<X509Certificate2>(c => c == _localhostCertificate)), Times.Once);\n    }\n\n    [Fact]\n    public void SecretMatchDefaultNameRemoved()\n    {\n        // Arrange\n        var secret = KubeResourceGenerator.CreateSecret(\"yarp-ingress-tls\", \"default\");\n        _certificateHelper\n            .Setup(h => h.ConvertCertificate(It.Is<NamespacedName>(n => n.Name == \"yarp-ingress-tls\" && n.Namespace == \"default\"), It.Is<V1Secret>(s => s == secret)))\n            .Returns(_localhostCertificate);\n\n        // Act\n        _cacheUnderTest.Update(WatchEventType.Deleted, secret);\n\n        // Assert\n        _certificateHelper.Verify(h => h.ConvertCertificate(It.Is<NamespacedName>(n => n.Name == \"yarp-ingress-tls\" && n.Namespace == \"default\"), It.Is<V1Secret>(s => s == secret)), Times.Once);\n        _certificateSelector.Verify(s => s.RemoveCertificate(It.Is<NamespacedName>(n => n.Name == \"yarp-ingress-tls\" && n.Namespace == \"default\")), Times.Once);\n    }\n\n    [Fact]\n    public void SecretMatchDefaultNameCantConvertNotAdded()\n    {\n        // Arrange\n        var secret = KubeResourceGenerator.CreateSecret(\"yarp-ingress-tls\", \"default\");\n        _certificateHelper\n            .Setup(h => h.ConvertCertificate(It.Is<NamespacedName>(n => n.Name == \"yarp-ingress-tls\" && n.Namespace == \"default\"), It.Is<V1Secret>(s => s == secret)))\n            .Returns((X509Certificate2)null);\n\n        // Act\n        _cacheUnderTest.Update(WatchEventType.Added, secret);\n\n        // Assert\n        _certificateHelper.Verify(h => h.ConvertCertificate(It.Is<NamespacedName>(n => n.Name == \"yarp-ingress-tls\" && n.Namespace == \"default\"), It.Is<V1Secret>(s => s == secret)), Times.Once);\n        _certificateSelector.Verify(s => s.AddCertificate(It.IsAny<NamespacedName>(), It.IsAny<X509Certificate2>()), Times.Never);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/IngressControllerTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.Models;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Options;\nusing Moq;\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\nusing Yarp.Kubernetes.Controller;\nusing Yarp.Kubernetes.Controller.Caching;\nusing Yarp.Kubernetes.Controller.Client.Tests;\nusing Yarp.Kubernetes.Controller.Services;\nusing Yarp.Kubernetes.Tests.Utils;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.Kubernetes.Tests;\n\npublic class IngressControllerTests\n{\n    private readonly Mock<ICache> _mockCache = new();\n    private readonly Mock<IReconciler> _mockReconciler = new();\n    private readonly SyncResourceInformer<V1Ingress> _ingressInformer = new();\n    private readonly SyncResourceInformer<V1Service> _serviceInformer = new();\n    private readonly SyncResourceInformer<V1Endpoints> _endpointsInformer = new();\n    private readonly SyncResourceInformer<V1IngressClass> _ingressClassInformer = new();\n    private readonly SyncResourceInformer<V1Secret> _secretInformer = new();\n    private readonly Mock<IHostApplicationLifetime> _mockHostApplicationLifetime = new();\n    private readonly Mock<IOptions<YarpOptions>> _mockOptions = new();\n    private readonly IngressController _controller;\n\n    public IngressControllerTests(ITestOutputHelper output)\n    {\n        var optionsNoWatchSecrets = new YarpOptions()\n        {\n            ServerCertificates = false,\n        };\n\n        _mockOptions.Setup(o => o.Value).Returns(optionsNoWatchSecrets);\n\n        var logger = new TestLogger<IngressController>(output);\n        _controller = new IngressController(_mockCache.Object, _mockReconciler.Object, _ingressInformer, _serviceInformer, _endpointsInformer, _ingressClassInformer, _secretInformer, _mockHostApplicationLifetime.Object, _mockOptions.Object, logger);\n    }\n\n    [Fact]\n    public async Task ReconciliationContinuesOnReconcilerError()\n    {\n        _mockCache.Setup(x => x.Update(It.IsAny<WatchEventType>(), It.IsAny<V1Ingress>())).Returns(true);\n\n        var awaiter = new SemaphoreSlim(0, 1);\n        _mockReconciler\n            .SetupSequence(x => x.ProcessAsync(It.IsAny<CancellationToken>()))\n                .Returns(() => { awaiter.Release(); return Task.CompletedTask; })\n                .Returns(() => { awaiter.Release(); return Task.CompletedTask; })\n                .Returns(() => { awaiter.Release(); return Task.FromException(new Exception(\"reconicliation failed\")); })\n                .Returns(() => { awaiter.Release(); return Task.CompletedTask; });\n\n        await _controller.StartAsync(CancellationToken.None).DefaultTimeout();\n        await awaiter.WaitAsync().DefaultTimeout();\n        _mockReconciler.Verify(x => x.ProcessAsync(It.IsAny<CancellationToken>()), Times.Exactly(1));\n\n        _ingressInformer.PublishUpdate(WatchEventType.Added, new V1Ingress());\n        await awaiter.WaitAsync().DefaultTimeout();\n        _mockReconciler.Verify(x => x.ProcessAsync(It.IsAny<CancellationToken>()), Times.Exactly(2));\n\n        _ingressInformer.PublishUpdate(WatchEventType.Added, new V1Ingress());\n        await awaiter.WaitAsync().DefaultTimeout();\n        _mockReconciler.Verify(x => x.ProcessAsync(It.IsAny<CancellationToken>()), Times.AtLeast(3));\n        await awaiter.WaitAsync().DefaultTimeout();\n        _mockReconciler.Verify(x => x.ProcessAsync(It.IsAny<CancellationToken>()), Times.AtLeast(4));\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/IngressConversionTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing k8s;\nusing k8s.Models;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Moq;\nusing Newtonsoft.Json;\nusing Newtonsoft.Json.Converters;\nusing Newtonsoft.Json.Linq;\nusing Xunit;\nusing Yarp.Kubernetes.Controller;\nusing Yarp.Kubernetes.Controller.Caching;\nusing Yarp.Kubernetes.Controller.Certificates;\nusing Yarp.Kubernetes.Controller.Converters;\nusing JsonSerializer = System.Text.Json.JsonSerializer;\n\nnamespace Yarp.Kubernetes.Tests;\n\npublic class IngressConversionTests\n{\n    public IngressConversionTests()\n    {\n        JsonConvert.DefaultSettings = () => new JsonSerializerSettings()\n        {\n            NullValueHandling = NullValueHandling.Ignore,\n            Converters = { new StringEnumConverter() }\n        };\n    }\n\n    [Theory]\n    [InlineData(\"basic-ingress\")]\n    [InlineData(\"multiple-endpoints-ports\")]\n    [InlineData(\"multiple-endpoints-same-port\")]\n    [InlineData(\"https\")]\n    [InlineData(\"https-service-port-protocol\")]\n    [InlineData(\"exact-match\")]\n    [InlineData(\"annotations\")]\n    [InlineData(\"mapped-port\")]\n    [InlineData(\"port-mismatch\")]\n    [InlineData(\"hostname-routing\")]\n    [InlineData(\"multiple-hosts\")]\n    [InlineData(\"multiple-ingresses\")]\n    [InlineData(\"multiple-ingresses-one-svc\")]\n    [InlineData(\"multiple-namespaces\")]\n    [InlineData(\"route-metadata\")]\n    [InlineData(\"route-queryparameters\")]\n    [InlineData(\"route-headers\")]\n    [InlineData(\"route-order\")]\n    [InlineData(\"route-methods\")]\n    [InlineData(\"missing-svc\")]\n    [InlineData(\"port-diff-name\")]\n    [InlineData(\"external-name-ingress\")]\n    public async Task ParsingTests(string name)\n    {\n        var ingressClass = KubeResourceGenerator.CreateIngressClass(\"yarp\", \"microsoft.com/ingress-yarp\", true);\n        var cache = await GetKubernetesInfo(name, ingressClass);\n        var configContext = new YarpConfigContext();\n        var ingresses = cache.GetIngresses().ToArray();\n\n        foreach (var ingress in ingresses)\n        {\n            if (cache.TryGetReconcileData(new NamespacedName(ingress.Metadata.NamespaceProperty, ingress.Metadata.Name), out var data))\n            {\n                var ingressContext = new YarpIngressContext(ingress, data.ServiceList, data.EndpointsList);\n                YarpParser.ConvertFromKubernetesIngress(ingressContext, configContext);\n            }\n        }\n        var options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } };\n        VerifyClusters(JsonSerializer.Serialize(configContext.BuildClusterConfig(), options), name);\n        VerifyRoutes(JsonSerializer.Serialize(configContext.Routes, options), name);\n    }\n\n    private static void VerifyClusters(string clusterJson, string name)\n    {\n        VerifyJson(clusterJson, name, \"clusters.json\");\n    }\n\n    private static void VerifyRoutes(string routesJson, string name)\n    {\n        VerifyJson(routesJson, name, \"routes.json\");\n    }\n\n    private static string StripNullProperties(string json)\n    {\n        using var reader = new JsonTextReader(new StringReader(json));\n        var sb = new StringBuilder();\n        using var sw = new StringWriter(sb);\n        using var writer = new JsonTextWriter(sw);\n        while (reader.Read())\n        {\n            var token = reader.TokenType;\n            var value = reader.Value;\n            if (reader.TokenType == JsonToken.PropertyName)\n            {\n                reader.Read();\n                if (reader.TokenType == JsonToken.Null)\n                {\n                    continue;\n                }\n                writer.WriteToken(token, value);\n            }\n            writer.WriteToken(reader.TokenType, reader.Value);\n        }\n\n        return sb.ToString();\n    }\n\n    private static void VerifyJson(string json, string name, string fileName)\n    {\n        var other = File.ReadAllText(Path.Combine(\"testassets\", name, fileName));\n        json = StripNullProperties(json);\n        other = StripNullProperties(other);\n\n        var actual = JToken.Parse(json);\n        var jOther = JToken.Parse(other);\n\n        Assert.True(JToken.DeepEquals(actual, jOther), $\"Expected: {jOther}\\nActual: {actual}\");\n    }\n\n    private async Task<ICache> GetKubernetesInfo(string name, V1IngressClass ingressClass)\n    {\n        var mockLogger = new Mock<ILogger<IngressCache>>();\n        var mockOptions = new Mock<IOptions<YarpOptions>>();\n        var certificateSelector = new Mock<IServerCertificateSelector>();\n        var loggerHelper = new Mock<ILogger<CertificateHelper>>();\n        var certificateHelper = new CertificateHelper(loggerHelper.Object);\n\n        mockOptions.SetupGet(o => o.Value).Returns(new YarpOptions { ControllerClass = \"microsoft.com/ingress-yarp\" });\n\n        var cache = new IngressCache(mockOptions.Object, certificateSelector.Object, certificateHelper, mockLogger.Object);\n\n        var typeMap = new Dictionary<string, Type>();\n        typeMap.Add(\"networking.k8s.io/v1/Ingress\", typeof(V1Ingress));\n        typeMap.Add(\"v1/Service\", typeof(V1Service));\n        typeMap.Add(\"v1/Endpoints\", typeof(V1Endpoints));\n\n        if (ingressClass is not null)\n        {\n            cache.Update(WatchEventType.Added, ingressClass);\n        }\n\n        var kubeObjects = await KubernetesYaml.LoadAllFromFileAsync(Path.Combine(\"testassets\", name, \"ingress.yaml\"), typeMap).ConfigureAwait(false);\n        foreach (var obj in kubeObjects)\n        {\n            if (obj is V1Ingress ingress)\n            {\n                cache.Update(WatchEventType.Added, ingress);\n            }\n            else if (obj is V1Service service)\n            {\n                cache.Update(WatchEventType.Added, service);\n            }\n            else if (obj is V1Endpoints endpoints)\n            {\n                cache.Update(WatchEventType.Added, endpoints);\n            }\n        }\n\n        return cache;\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/KubeResourceGenerator.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Globalization;\nusing k8s.Models;\n\nnamespace Yarp.Kubernetes.Tests;\n\ninternal static class KubeResourceGenerator\n{\n    public static V1IngressClass CreateIngressClass(string name, string controller, bool? isDefaultClass)\n    {\n        var ingressClass = new V1IngressClass\n        {\n            Spec = new V1IngressClassSpec\n            {\n                Controller = controller,\n            },\n            Metadata = new V1ObjectMeta\n            {\n                Name = name,\n                Annotations = new Dictionary<string, string>(),\n            },\n        };\n\n        if (isDefaultClass.HasValue && isDefaultClass.Value)\n        {\n            ingressClass.Metadata.Annotations.Add(\"ingressclass.kubernetes.io/is-default-class\", isDefaultClass.Value.ToString(CultureInfo.InvariantCulture));\n        }\n\n        return ingressClass;\n    }\n\n    public static V1Ingress CreateIngress(string name, string namespaceName, string ingressClassName)\n    {\n        var ingress = new V1Ingress\n        {\n            Spec = new V1IngressSpec(),\n            Metadata = new V1ObjectMeta\n            {\n                Name = name,\n                NamespaceProperty = namespaceName,\n            }\n        };\n\n        if (!string.IsNullOrEmpty(ingressClassName))\n        {\n            ingress.Spec.IngressClassName = ingressClassName;\n        }\n\n        return ingress;\n    }\n\n    public static V1Secret CreateSecret(string name, string namespaceName, byte[] publicData = null, byte[] privateData = null)\n    {\n        var secret = new V1Secret\n        {\n            Metadata = new V1ObjectMeta\n            {\n                Name = name,\n                NamespaceProperty = namespaceName,\n            }\n        };\n\n        if (publicData != null)\n        {\n            if (secret.Data == null)\n            {\n                secret.Data = new Dictionary<string, byte[]>();\n            }\n\n            secret.Data[\"tls.crt\"] = publicData;\n        }\n\n        if (privateData != null)\n        {\n            if (secret.Data == null)\n            {\n                secret.Data = new Dictionary<string, byte[]>();\n            }\n\n            secret.Data[\"tls.key\"] = privateData;\n        }\n\n        return secret;\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Management/KubernetesCoreExtensionsTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing Microsoft.Extensions.DependencyInjection;\nusing Xunit;\n\nnamespace Yarp.Kubernetes.Controller.Management.Tests;\n\npublic class KubernetesCoreExtensionsTests\n{\n    [Fact]\n    public void KubernetesClientIsAdded()\n    {\n        var services = new ServiceCollection();\n\n        services.AddKubernetesCore();\n\n        var serviceProvider = services.BuildServiceProvider();\n        Assert.NotNull(serviceProvider.GetService<IKubernetes>());\n    }\n\n    [Fact]\n    public void ExistingClientIsNotReplaced()\n    {\n        using var client = new k8s.Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig());\n        var services = new ServiceCollection();\n\n        services.AddSingleton<IKubernetes>(client);\n        services.AddKubernetesCore();\n\n        var serviceProvider = services.BuildServiceProvider();\n        Assert.Same(client, serviceProvider.GetService<IKubernetes>());\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/NamespacedNameTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s.Models;\nusing System.Collections.Generic;\nusing Xunit;\n\nnamespace Yarp.Kubernetes.Controller.Tests;\n\npublic class NamespacedNameTests\n{\n    [Fact]\n    public void WorksAsDictionaryKey()\n    {\n        var dictionary = new Dictionary<NamespacedName, string>();\n        var name1 = new NamespacedName(\"ns\", \"n1\");\n        var name2 = new NamespacedName(\"ns\", \"n2\");\n        var name3 = new NamespacedName(\"ns\", \"n3\");\n\n        dictionary[name1] = \"one\";\n        dictionary[name1] = \"one again\";\n        dictionary[name2] = \"two\";\n\n        Assert.Contains(new KeyValuePair<NamespacedName, string>(name1, \"one again\"), dictionary);\n        Assert.Contains(new KeyValuePair<NamespacedName, string>(name2, \"two\"), dictionary);\n        Assert.DoesNotContain(name3, dictionary.Keys);\n    }\n\n    [Theory]\n    [InlineData(\"ns\", \"n1\", \"ns\", \"n1\", true)]\n    [InlineData(\"ns\", \"n1\", \"ns\", \"n2\", false)]\n    [InlineData(\"ns\", \"n1\", \"ns-x\", \"n1\", false)]\n    [InlineData(null, \"n1\", null, \"n1\", true)]\n    [InlineData(null, \"n1\", null, \"n2\", false)]\n    public void EqualityAndInequality(\n        string namespace1,\n        string name1,\n        string namespace2,\n        string name2,\n        bool shouldBeEqual)\n    {\n        var namespacedName1 = new NamespacedName(namespace1, name1);\n        var namespacedName2 = new NamespacedName(namespace2, name2);\n\n        var areEqual = namespacedName1 == namespacedName2;\n        var areNotEqual = namespacedName1 != namespacedName2;\n#pragma warning disable CS1718 // Comparison made to same variable\n        var sameEqual1 = namespacedName1 == namespacedName1;\n        var sameNotEqual1 = namespacedName1 != namespacedName1;\n        var sameEqual2 = namespacedName2 == namespacedName2;\n        var sameNotEqual2 = namespacedName2 != namespacedName2;\n#pragma warning restore CS1718 // Comparison made to same variable\n\n        Assert.NotEqual(areNotEqual, areEqual);\n        Assert.Equal(shouldBeEqual, areEqual);\n        Assert.True(sameEqual1);\n        Assert.False(sameNotEqual1);\n        Assert.True(sameEqual2);\n        Assert.False(sameNotEqual2);\n    }\n\n    [Fact]\n    public void NamespaceAndNameFromResource()\n    {\n        var resource = new V1ConfigMap\n        {\n            ApiVersion = V1ConfigMap.KubeApiVersion,\n            Kind = V1ConfigMap.KubeKind,\n            Metadata = new V1ObjectMeta {Name = \"the-name\", NamespaceProperty = \"the-namespace\"}\n        };\n\n        var nn = NamespacedName.From(resource);\n\n        Assert.Equal(\"the-name\", nn.Name);\n        Assert.Equal(\"the-namespace\", nn.Namespace);\n    }\n\n    [Fact]\n    public void JustNameFromClusterResource()\n    {\n        var resource = new V1ClusterRole\n        {\n            ApiVersion = V1ClusterRole.KubeApiVersion,\n            Kind = V1ClusterRole.KubeKind,\n            Metadata = new V1ObjectMeta { Name = \"the-name\" }\n        };\n\n        var nn = NamespacedName.From(resource);\n\n        Assert.Equal(\"the-name\", nn.Name);\n        Assert.Null(nn.Namespace);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Queues/WorkQueueTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Yarp.Kubernetes.Controller.Queues.Tests;\n\npublic class WorkQueueTests\n{\n    public CancellationTokenSource Cancellation { get; set; } = new CancellationTokenSource(TimeSpan.FromSeconds(5));\n\n    [Fact]\n    public async Task NormalUsageIsAddGetDone()\n    {\n        using IWorkQueue<string> queue = new WorkQueue<string>();\n\n        Assert.Equal(0, queue.Len());\n        queue.Add(\"one\");\n        Assert.Equal(1, queue.Len());\n        queue.Add(\"two\");\n        Assert.Equal(2, queue.Len());\n        var (item1, shutdown1) = await queue.GetAsync(Cancellation.Token);\n        Assert.Equal(1, queue.Len());\n        queue.Done(item1);\n        Assert.Equal(1, queue.Len());\n        var (item2, shutdown2) = await queue.GetAsync(Cancellation.Token);\n        Assert.Equal(0, queue.Len());\n        queue.Done(item2);\n        Assert.Equal(0, queue.Len());\n\n        Assert.Equal(\"one\", item1);\n        Assert.False(shutdown1);\n        Assert.Equal(\"two\", item2);\n        Assert.False(shutdown2);\n    }\n\n    [Fact]\n    public void AddingSameItemAgainHasNoEffect()\n    {\n        using IWorkQueue<string> queue = new WorkQueue<string>();\n\n        var len1 = queue.Len();\n        queue.Add(\"one\");\n        var len2 = queue.Len();\n        queue.Add(\"one\");\n        var len3 = queue.Len();\n\n        Assert.Equal(0, len1);\n        Assert.Equal(1, len2);\n        Assert.Equal(1, len3);\n    }\n\n    [Fact]\n    public async Task CallingAddWhileItemIsBeingProcessedHasNoEffect()\n    {\n        using IWorkQueue<string> queue = new WorkQueue<string>();\n\n        var lenOriginal = queue.Len();\n        queue.Add(\"one\");\n        var lenAfterAdd = queue.Len();\n        var (item1, _) = await queue.GetAsync(Cancellation.Token);\n        var lenAfterGet = queue.Len();\n        queue.Add(\"one\");\n        var lenAfterAddAgain = queue.Len();\n\n        Assert.Equal(\"one\", item1);\n        Assert.Equal(0, lenOriginal);\n        Assert.Equal(1, lenAfterAdd);\n        Assert.Equal(0, lenAfterGet);\n        Assert.Equal(0, lenAfterAddAgain);\n\n        Assert.Equal(0, queue.Len());\n    }\n\n    [Fact]\n    public async Task ItemCanBeAddedAgainAfterDoneIsCalled()\n    {\n        using IWorkQueue<string> queue = new WorkQueue<string>();\n\n        var lenOriginal = queue.Len();\n        queue.Add(\"one\");\n        var lenAfterAdd = queue.Len();\n        var (item1, _) = await queue.GetAsync(Cancellation.Token);\n        var lenAfterGet = queue.Len();\n        queue.Done(item1);\n        var lenAfterDone = queue.Len();\n        queue.Add(\"one\");\n        var lenAfterAddAgain = queue.Len();\n\n        Assert.Equal(\"one\", item1);\n        Assert.Equal(0, lenOriginal);\n        Assert.Equal(1, lenAfterAdd);\n        Assert.Equal(0, lenAfterGet);\n        Assert.Equal(0, lenAfterDone);\n        Assert.Equal(1, lenAfterAddAgain);\n\n        Assert.Equal(1, queue.Len());\n    }\n\n    [Fact]\n    public async Task IfAddWasCalledDuringProcessingThenItemIsRequeuedByDone()\n    {\n        using IWorkQueue<string> queue = new WorkQueue<string>();\n\n        var lenOriginal = queue.Len();\n        queue.Add(\"one\");\n        var lenAfterAdd = queue.Len();\n        var (item1, _) = await queue.GetAsync(Cancellation.Token);\n        var lenAfterGet = queue.Len();\n        queue.Add(\"one\");\n        var lenAfterAddAgain = queue.Len();\n        queue.Done(item1);\n        var lenAfterDone = queue.Len();\n        var (item2, _) = await queue.GetAsync(Cancellation.Token);\n        var lenAfterGetAgain = queue.Len();\n\n        Assert.Equal(\"one\", item1);\n        Assert.Equal(\"one\", item2);\n        Assert.Equal(0, lenOriginal);\n        Assert.Equal(1, lenAfterAdd);\n        Assert.Equal(0, lenAfterGet);\n        Assert.Equal(0, lenAfterAddAgain);\n        Assert.Equal(1, lenAfterDone);\n        Assert.Equal(0, lenAfterGetAgain);\n\n        Assert.Equal(0, queue.Len());\n    }\n\n    [Fact]\n    public async Task GetCompletesOnceAddIsCalled()\n    {\n        using IWorkQueue<string> queue = new WorkQueue<string>();\n\n        var getTask = queue.GetAsync(Cancellation.Token);\n        Assert.Equal(0, queue.Len());\n        Assert.False(getTask.IsCompleted);\n\n        queue.Add(\"one\");\n        var (item1, _) = await getTask;\n        Assert.Equal(0, queue.Len());\n        Assert.True(getTask.IsCompleted);\n\n        Assert.Equal(\"one\", item1);\n        Assert.Equal(0, queue.Len());\n    }\n\n    [Fact]\n    public async Task GetReturnsShutdownTrueAfterShutdownIsCalled()\n    {\n        using IWorkQueue<string> queue = new WorkQueue<string>();\n\n        var getTask = queue.GetAsync(Cancellation.Token);\n        Assert.Equal(0, queue.Len());\n        Assert.False(getTask.IsCompleted);\n\n        queue.ShutDown();\n        var (item1, shutdown1) = await getTask;\n        Assert.Equal(0, queue.Len());\n        Assert.True(getTask.IsCompleted);\n\n        Assert.True(shutdown1);\n        Assert.Equal(0, queue.Len());\n    }\n\n    [Fact]\n    public void ShuttingDownReturnsTrueAfterShutdownIsCalled()\n    {\n        using IWorkQueue<string> queue = new WorkQueue<string>();\n\n        var shuttingDownBefore = queue.ShuttingDown();\n        queue.ShutDown();\n        var shuttingDownAfter = queue.ShuttingDown();\n\n        Assert.False(shuttingDownBefore);\n        Assert.True(shuttingDownAfter);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Rate/LimitTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Xunit;\n\nnamespace Yarp.Kubernetes.Controller.Rate.Tests;\n\npublic class LimitTests\n{\n    [Theory]\n    [InlineData(15, 1, 15)]\n    [InlineData(15, 120, 1800)]\n    [InlineData(15, .1, 1.5)]\n    [InlineData(300, 2, 600)]\n    public void TokensFromDuration(double perSecond, double durationSeconds, double tokens)\n    {\n        var limit = new Limit(perSecond);\n\n        var tokensFromDuration = limit.TokensFromDuration(TimeSpan.FromSeconds(durationSeconds));\n\n        Assert.Equal(tokens, tokensFromDuration);\n    }\n\n    [Theory]\n    [InlineData(15, 1, 15)]\n    [InlineData(15, 120, 1800)]\n    [InlineData(15, .1, 1.5)]\n    [InlineData(300, 2, 600)]\n    public void DurationFromTokens(double perSecond, double durationSeconds, double tokens)\n    {\n        var limit = new Limit(perSecond);\n\n        var durationFromTokens = limit.DurationFromTokens(tokens);\n\n        Assert.Equal(TimeSpan.FromSeconds(durationSeconds), durationFromTokens);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Rate/LimiterTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Polly;\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\nusing Xunit.Sdk;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.Kubernetes.Controller.Rate.Tests;\n\npublic class LimiterTests\n{\n    private readonly DateTimeOffset _startTime = new DateTimeOffset(2020, 10, 14, 12, 34, 56, TimeSpan.Zero);\n\n    [Fact]\n    public void FirstTokenIsAvailable()\n    {\n        var timeProvider = new TestTimeProvider(_startTime);\n        var limiter = new Limiter(new Limit(10), 1, timeProvider);\n\n        var allowed = limiter.Allow();\n\n        Assert.True(allowed);\n    }\n\n    [Theory]\n    [InlineData(5)]\n    [InlineData(1)]\n    [InlineData(300)]\n    public void AsManyAsBurstTokensAreAvailableRightAway(int burst)\n    {\n        var timeProvider = new TestTimeProvider(_startTime);\n        var limiter = new Limiter(new Limit(10), burst, timeProvider);\n\n        var allowed = new List<bool>();\n        foreach (var index in Enumerable.Range(1, burst))\n        {\n            allowed.Add(limiter.Allow());\n        }\n        var notAllowed = limiter.Allow();\n\n        Assert.All(allowed, item => Assert.True(item));\n        Assert.False(notAllowed);\n    }\n\n    [Fact]\n    public void TokensBecomeAvailableAtLimitPerSecondRate()\n    {\n        var timeProvider = new TestTimeProvider(_startTime);\n        var limiter = new Limiter(new Limit(10), 50, timeProvider);\n\n        var initiallyAllowed = limiter.AllowN(timeProvider.GetUtcNow(), 50);\n        var thenNotAllowed1 = limiter.Allow();\n\n        timeProvider.Advance(TimeSpan.FromMilliseconds(100));\n        var oneTokenAvailable = limiter.Allow();\n        var thenNotAllowed2 = limiter.Allow();\n\n        timeProvider.Advance(TimeSpan.FromMilliseconds(200));\n        var twoTokensAvailable1 = limiter.Allow();\n        var twoTokensAvailable2 = limiter.Allow();\n        var thenNotAllowed3 = limiter.Allow();\n\n        Assert.True(initiallyAllowed);\n        Assert.False(thenNotAllowed1);\n        Assert.True(oneTokenAvailable);\n        Assert.False(thenNotAllowed2);\n        Assert.True(twoTokensAvailable1);\n        Assert.True(twoTokensAvailable2);\n        Assert.False(thenNotAllowed3);\n    }\n\n    [Fact]\n    public void ReserveTellsYouHowLongToWait()\n    {\n        var timeProvider = new TestTimeProvider(_startTime);\n        var limiter = new Limiter(new Limit(10), 50, timeProvider);\n\n        var initiallyAllowed = limiter.AllowN(timeProvider.GetUtcNow(), 50);\n        var thenNotAllowed1 = limiter.Allow();\n\n        var reserveOne = limiter.Reserve();\n        var delayOne = reserveOne.Delay();\n\n        var reserveTwoMore = limiter.Reserve(timeProvider.GetUtcNow(), 2);\n        var delayTwoMore = reserveTwoMore.Delay();\n\n        timeProvider.Advance(TimeSpan.FromMilliseconds(450));\n\n        var reserveAlreadyAvailable = limiter.Reserve();\n        var delayAlreadyAvailable = reserveAlreadyAvailable.Delay();\n\n        var reserveHalfAvailable = limiter.Reserve();\n        var delayHalfAvailable = reserveHalfAvailable.Delay();\n\n        Assert.True(initiallyAllowed);\n        Assert.False(thenNotAllowed1);\n        Assert.True(reserveOne.Ok);\n        Assert.Equal(TimeSpan.FromMilliseconds(100), delayOne);\n        Assert.True(reserveTwoMore.Ok);\n        Assert.Equal(TimeSpan.FromMilliseconds(300), delayTwoMore);\n        Assert.True(reserveAlreadyAvailable.Ok);\n        Assert.Equal(TimeSpan.Zero, delayAlreadyAvailable);\n        Assert.True(reserveHalfAvailable.Ok);\n        Assert.Equal(TimeSpan.FromMilliseconds(50), delayHalfAvailable);\n    }\n\n    [Fact(Skip = \"https://github.com/dotnet/yarp/issues/1357\")]\n    public async Task WaitAsyncCausesPauseLikeReserve()\n    {\n        var limiter = new Limiter(new Limit(10), 5);\n        using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5));\n\n        while (cancellation.IsCancellationRequested == false)\n        {\n            var task = limiter.WaitAsync(cancellation.Token);\n            if (!task.IsCompleted)\n            {\n                await task;\n                break;\n            }\n        }\n\n        var delayOne = new Stopwatch();\n        delayOne.Start();\n        await limiter.WaitAsync(cancellation.Token);\n        delayOne.Stop();\n\n        var delayTwoMore = new Stopwatch();\n        delayTwoMore.Start();\n        await limiter.WaitAsync(2, cancellation.Token);\n        delayTwoMore.Stop();\n\n        await Task.Delay(TimeSpan.FromMilliseconds(150));\n\n        var delayAlreadyAvailable = new Stopwatch();\n        delayAlreadyAvailable.Start();\n        await limiter.WaitAsync(cancellation.Token);\n        delayAlreadyAvailable.Stop();\n\n        var delayHalfAvailable = new Stopwatch();\n        delayHalfAvailable.Start();\n        await limiter.WaitAsync(cancellation.Token);\n        delayHalfAvailable.Stop();\n\n        Assert.InRange(delayOne.Elapsed, TimeSpan.FromMilliseconds(75), TimeSpan.FromMilliseconds(125));\n        Assert.InRange(delayTwoMore.Elapsed, TimeSpan.FromMilliseconds(175), TimeSpan.FromMilliseconds(225));\n        Assert.InRange(delayAlreadyAvailable.Elapsed, TimeSpan.Zero, TimeSpan.FromMilliseconds(5));\n        Assert.InRange(delayHalfAvailable.Elapsed, TimeSpan.FromMilliseconds(25), TimeSpan.FromMilliseconds(75));\n    }\n\n    [Fact(Skip = \"https://github.com/dotnet/yarp/issues/1357\")]\n    public async Task ManyWaitsStackUp()\n    {\n        await Policy\n            .Handle<InRangeException>()\n            .RetryAsync(3)\n            .ExecuteAsync(async () =>\n            {\n                var limiter = new Limiter(new Limit(10), 5);\n                using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5));\n\n                while (cancellation.IsCancellationRequested == false)\n                {\n                    var task = limiter.WaitAsync(cancellation.Token);\n                    if (!task.IsCompleted)\n                    {\n                        await task;\n                        break;\n                    }\n                }\n\n                var delayOne = new Stopwatch();\n                delayOne.Start();\n\n                var delayTwo = new Stopwatch();\n                delayTwo.Start();\n\n                var delayThree = new Stopwatch();\n                delayThree.Start();\n\n                var waits = new List<Task>\n                {\n                    limiter.WaitAsync(cancellation.Token),\n                    limiter.WaitAsync(cancellation.Token),\n                    limiter.WaitAsync(cancellation.Token),\n                };\n\n                var taskOne = await Task.WhenAny(waits);\n                await taskOne;\n                delayOne.Stop();\n                waits.Remove(taskOne);\n\n                var taskTwo = await Task.WhenAny(waits);\n                await taskTwo;\n                delayTwo.Stop();\n                waits.Remove(taskTwo);\n\n                var taskThree = await Task.WhenAny(waits);\n                await taskThree;\n                delayThree.Stop();\n                waits.Remove(taskThree);\n\n                Assert.InRange(delayOne.Elapsed, TimeSpan.FromMilliseconds(75), TimeSpan.FromMilliseconds(125));\n                Assert.InRange(delayTwo.Elapsed, TimeSpan.FromMilliseconds(175), TimeSpan.FromMilliseconds(225));\n                Assert.InRange(delayThree.Elapsed, TimeSpan.FromMilliseconds(275), TimeSpan.FromMilliseconds(325));\n            });\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Rate/ReservationTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Xunit;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.Kubernetes.Controller.Rate.Tests;\n\npublic class ReservationTests\n{\n    private readonly DateTimeOffset _startTime = new DateTimeOffset(2020, 10, 14, 12, 34, 56, TimeSpan.Zero);\n\n    [Fact]\n    public void NotOkayAlwaysReturnsMaxValueDelay()\n    {\n        var timeProvider = new TestTimeProvider(_startTime);\n        var reservation = new Reservation(\n            timeProvider: timeProvider,\n            limiter: default,\n            ok: false);\n\n        var delay1 = reservation.Delay();\n        var delayFrom1 = reservation.DelayFrom(timeProvider.GetUtcNow());\n        timeProvider.Advance(TimeSpan.FromMinutes(3));\n        var delay2 = reservation.Delay();\n        var delayFrom2 = reservation.DelayFrom(timeProvider.GetUtcNow());\n\n        Assert.Equal(TimeSpan.MaxValue, delay1);\n        Assert.Equal(TimeSpan.MaxValue, delayFrom1);\n        Assert.Equal(TimeSpan.MaxValue, delay2);\n        Assert.Equal(TimeSpan.MaxValue, delayFrom2);\n    }\n\n    [Fact]\n    public void DelayIsZeroWhenTimeToActIsNowOrEarlier()\n    {\n        var timeProvider = new TestTimeProvider(_startTime);\n        var reservation = new Reservation(\n            timeProvider: timeProvider,\n            limiter: default,\n            ok: true,\n            timeToAct: timeProvider.GetUtcNow(),\n            limit: default);\n\n        var delay1 = reservation.Delay();\n        var delayFrom1 = reservation.DelayFrom(timeProvider.GetUtcNow());\n        timeProvider.Advance(TimeSpan.FromMinutes(3));\n        var delay2 = reservation.Delay();\n        var delayFrom2 = reservation.DelayFrom(timeProvider.GetUtcNow());\n\n        Assert.Equal(TimeSpan.Zero, delay1);\n        Assert.Equal(TimeSpan.Zero, delayFrom1);\n        Assert.Equal(TimeSpan.Zero, delay2);\n        Assert.Equal(TimeSpan.Zero, delayFrom2);\n    }\n\n    [Fact]\n    public void DelayGetsSmallerAsTimePasses()\n    {\n        var timeProvider = new TestTimeProvider(_startTime);\n        var reservation = new Reservation(\n            timeProvider: timeProvider,\n            limiter: default,\n            ok: true,\n            timeToAct: timeProvider.GetUtcNow().Add(TimeSpan.FromMinutes(5)),\n            limit: default);\n\n        var delay1 = reservation.Delay();\n        timeProvider.Advance(TimeSpan.FromMinutes(3));\n        var delay2 = reservation.Delay();\n        timeProvider.Advance(TimeSpan.FromMinutes(3));\n        var delay3 = reservation.Delay();\n\n        Assert.Equal(TimeSpan.FromMinutes(5), delay1);\n        Assert.Equal(TimeSpan.FromMinutes(2), delay2);\n        Assert.Equal(TimeSpan.Zero, delay3);\n    }\n\n    [Fact]\n    public void DelayFromNotChangedByTimePassing()\n    {\n        var timeProvider = new TestTimeProvider(_startTime);\n        var reservation = new Reservation(\n            timeProvider: timeProvider,\n            limiter: default,\n            ok: true,\n            timeToAct: timeProvider.GetUtcNow().Add(TimeSpan.FromMinutes(5)),\n            limit: default);\n\n        var twoMinutesPast = timeProvider.GetUtcNow().Subtract(TimeSpan.FromMinutes(2));\n        var twoMinutesFuture = timeProvider.GetUtcNow().Add(TimeSpan.FromMinutes(2));\n\n        var delay1 = reservation.DelayFrom(timeProvider.GetUtcNow());\n        var delayPast1 = reservation.DelayFrom(twoMinutesPast);\n        var delayFuture1 = reservation.DelayFrom(twoMinutesFuture);\n        timeProvider.Advance(TimeSpan.FromMinutes(3));\n        var delay2 = reservation.DelayFrom(timeProvider.GetUtcNow());\n        var delayPast2 = reservation.DelayFrom(twoMinutesPast);\n        var delayFuture2 = reservation.DelayFrom(twoMinutesFuture);\n\n        Assert.Equal(TimeSpan.FromMinutes(5), delay1);\n        Assert.Equal(TimeSpan.FromMinutes(7), delayPast1);\n        Assert.Equal(TimeSpan.FromMinutes(3), delayFuture1);\n        Assert.Equal(TimeSpan.FromMinutes(2), delay2);\n        Assert.Equal(TimeSpan.FromMinutes(7), delayPast2);\n        Assert.Equal(TimeSpan.FromMinutes(3), delayFuture2);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/ReconcilerTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Moq;\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\nusing Yarp.Kubernetes.Controller;\nusing Yarp.Kubernetes.Controller.Client;\nusing Yarp.Kubernetes.Controller.Caching;\nusing Yarp.Kubernetes.Controller.Configuration;\nusing Yarp.Kubernetes.Controller.Services;\nusing Yarp.Kubernetes.Tests.Utils;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.Kubernetes.Tests;\n\npublic class ReconcilerTests\n{\n    private readonly Mock<ICache> _mockCache = new();\n    private readonly Mock<IUpdateConfig> _mockUpdateConfig = new();\n    private readonly Mock<IIngressResourceStatusUpdater> _mockIngressResourceStatusUpdater = new();\n    private readonly Reconciler _reconciler;\n\n    public ReconcilerTests(ITestOutputHelper output)\n    {\n        var logger = new TestLogger<Reconciler>(output);\n        _reconciler = new Reconciler(_mockCache.Object, _mockUpdateConfig.Object, _mockIngressResourceStatusUpdater.Object, logger);\n    }\n\n    [Fact]\n    public async Task ReconcilerDoesNotStopOnInvalidIngress()\n    {\n        _mockCache\n            .Setup(x => x.GetIngresses())\n            .Returns(new[]\n                {\n                    new IngressData(KubeResourceGenerator.CreateIngress(\"bad-ingress\", \"default\", \"yarp\")),\n                    new IngressData(KubeResourceGenerator.CreateIngress(\"good-ingress\", \"default\", \"yarp\"))\n                });\n\n        _mockCache\n            .Setup(x => x.TryGetReconcileData(It.IsAny<NamespacedName>(), out It.Ref<ReconcileData>.IsAny))\n            .Returns(true);\n\n        _mockCache\n            .Setup(x => x.TryGetReconcileData(new NamespacedName(\"default\", \"bad-ingress\"), out It.Ref<ReconcileData>.IsAny))\n            .Throws(new Exception(\"poison ingress\"));\n\n        await _reconciler.ProcessAsync(CancellationToken.None);\n        _mockUpdateConfig.Verify(x => x.UpdateAsync(It.IsAny<IReadOnlyList<RouteConfig>>(), It.IsAny<IReadOnlyList<ClusterConfig>>(), It.IsAny<CancellationToken>()));\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/TestCluster/Controllers/ResourceApiController.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s.Models;\nusing Microsoft.AspNetCore.Mvc;\nusing System.Threading.Tasks;\nusing Yarp.Kubernetes.Tests.TestCluster.Models;\n\nnamespace Yarp.Kubernetes.Tests.TestCluster;\n\n[Route(\"api/{version}/{plural}\")]\npublic class ResourceApiController : ControllerBase\n{\n    private readonly ITestCluster _testCluster;\n\n    public ResourceApiController(ITestCluster testCluster)\n    {\n        _testCluster = testCluster;\n    }\n\n    [FromRoute]\n    public string Version { get; set; }\n\n    [FromRoute]\n    public string Plural { get; set; }\n\n    [HttpGet]\n    public async Task<IActionResult> ListAsync(ListParameters parameters)\n    {\n        var list = await _testCluster.ListResourcesAsync(string.Empty, Version, Plural, parameters);\n\n        var result = new KubernetesList<ResourceObject>(\n            apiVersion: Version,\n            kind: \"PodList\",\n            metadata: new V1ListMeta\n            {\n                ContinueProperty = list.Continue, RemainingItemCount = null, ResourceVersion = list.ResourceVersion\n            },\n            items: list.Items);\n\n        return new ObjectResult(result);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/TestCluster/Controllers/ResourceApiGroupController.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s.Models;\nusing Microsoft.AspNetCore.Mvc;\nusing System.Threading.Tasks;\nusing Yarp.Kubernetes.Tests.TestCluster.Models;\n\nnamespace Yarp.Kubernetes.Tests.TestCluster;\n\n[Route(\"apis/{group}/{version}/{plural}\")]\npublic class ResourceApiGroupController : ControllerBase\n{\n    private readonly ITestCluster _testCluster;\n\n    public ResourceApiGroupController(ITestCluster testCluster)\n    {\n        _testCluster = testCluster;\n    }\n\n    [FromRoute]\n    public string Group { get; set; }\n\n    [FromRoute]\n    public string Version { get; set; }\n\n    [FromRoute]\n    public string Plural { get; set; }\n\n    [HttpGet]\n    public async Task<IActionResult> ListAsync(ListParameters parameters)\n    {\n        var list = await _testCluster.ListResourcesAsync(Group, Version, Plural, parameters);\n\n        var result = new KubernetesList<ResourceObject>(\n            apiVersion: $\"{Group}/{Version}\",\n            kind: \"DeploymentList\",\n            metadata: new V1ListMeta\n            {\n                ContinueProperty = list.Continue, RemainingItemCount = null, ResourceVersion = list.ResourceVersion\n            },\n            items: list.Items);\n\n        return new ObjectResult(result);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/TestCluster/ITestCluster.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Http;\nusing System.Threading.Tasks;\nusing Yarp.Kubernetes.Tests.TestCluster.Models;\n\nnamespace Yarp.Kubernetes.Tests.TestCluster;\n\npublic interface ITestCluster\n{\n    Task UnhandledRequest(HttpContext context);\n\n    Task<ListResult> ListResourcesAsync(string group, string version, string plural, ListParameters parameters);\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/TestCluster/ITestClusterHost.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.KubeConfigModels;\nusing Microsoft.Extensions.Hosting;\n\nnamespace Yarp.Kubernetes.Tests.TestCluster;\n\npublic interface ITestClusterHost : IHost\n{\n    K8SConfiguration KubeConfig { get; }\n\n    IKubernetes Client { get; }\n\n    ITestCluster Cluster { get; }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/TestCluster/Models/ListParameters.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Yarp.Kubernetes.Tests.TestCluster.Models;\n\npublic class ListParameters\n{\n    [FromQuery]\n    public string Continue { get; set; }\n\n    [FromQuery]\n    public string FieldSelector { get; set; }\n\n    [FromQuery]\n    public string LabelSelector { get; set; }\n\n    [FromQuery]\n    public int? Limit { get; set; }\n\n    [FromQuery]\n    public string ResourceVersion { get; set; }\n\n    [FromQuery]\n    public int? TimeoutSeconds { get; set; }\n\n    [FromQuery]\n    public bool? Watch { get; set; }\n\n    [FromQuery]\n    public string Pretty { get; set; }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/TestCluster/Models/ListResult.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\n\nnamespace Yarp.Kubernetes.Tests.TestCluster.Models;\n\npublic class ListResult\n{\n    public string Continue { get; set; }\n\n    public string ResourceVersion { get; set; }\n\n    public IList<ResourceObject> Items { get; set; }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/TestCluster/Models/ResourceObject.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.Models;\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace Yarp.Kubernetes.Tests.TestCluster.Models;\n\npublic class ResourceObject : IKubernetesObject<V1ObjectMeta>\n{\n    [JsonPropertyName(\"apiVersion\")]\n    public string ApiVersion { get; set; }\n\n    [JsonPropertyName(\"kind\")]\n    public string Kind { get; set; }\n\n    [JsonPropertyName(\"metadata\")]\n    public V1ObjectMeta Metadata { get; set; }\n\n    [JsonExtensionData]\n    public IDictionary<string, object> AdditionalData { get; set; }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/TestCluster/TestCluster.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Options;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Yarp.Kubernetes.Tests.TestCluster.Models;\nusing Yarp.Kubernetes.Tests.Utils;\n\nnamespace Yarp.Kubernetes.Tests.TestCluster;\n\npublic class TestCluster : ITestCluster\n{\n    public IList<ResourceObject> Resources { get; } = new List<ResourceObject>();\n\n    public TestCluster(IOptions<TestClusterOptions> options)\n    {\n        ArgumentNullException.ThrowIfNull(options);\n\n        foreach (var resource in options.Value.InitialResources)\n        {\n            Resources.Add(ResourceSerializers.Convert<ResourceObject>(resource));\n        }\n    }\n\n    public virtual Task UnhandledRequest(HttpContext context)\n    {\n        throw new NotImplementedException();\n    }\n\n    public virtual Task<ListResult> ListResourcesAsync(string group, string version, string plural, ListParameters parameters)\n    {\n        ArgumentException.ThrowIfNullOrEmpty(version);\n        ArgumentException.ThrowIfNullOrEmpty(plural);\n        ArgumentNullException.ThrowIfNull(parameters);\n\n        return Task.FromResult(new ListResult\n        {\n            ResourceVersion = parameters.ResourceVersion,\n            Continue = null,\n            Items = Resources.ToArray(),\n        });\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/TestCluster/TestClusterHost.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.KubeConfigModels;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Kubernetes.Tests.TestCluster;\n\npublic class TestClusterHost : ITestClusterHost\n{\n    private readonly IHost _host;\n    private bool _disposedValue;\n\n    public TestClusterHost(IHost host, K8SConfiguration kubeConfig, IKubernetes client)\n    {\n        _host = host;\n        KubeConfig = kubeConfig;\n        Client = client;\n    }\n\n    public IServiceProvider Services => _host.Services;\n\n    public ITestCluster Cluster => _host.Services.GetRequiredService<ITestCluster>();\n\n    public K8SConfiguration KubeConfig { get; }\n\n    public IKubernetes Client { get; }\n\n    public Task StartAsync(CancellationToken cancellationToken = default) => _host.StartAsync(cancellationToken);\n\n    public Task StopAsync(CancellationToken cancellationToken = default) => _host.StartAsync(cancellationToken);\n\n    protected virtual void Dispose(bool disposing)\n    {\n        if (!_disposedValue)\n        {\n            if (disposing)\n            {\n                _host.Dispose();\n            }\n\n            _disposedValue = true;\n        }\n    }\n\n    public void Dispose()\n    {\n        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method\n        Dispose(disposing: true);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/TestCluster/TestClusterHostBuilder.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.KubeConfigModels;\nusing k8s.Models;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing System;\nusing System.Net;\nusing System.Net.Sockets;\n\nnamespace Yarp.Kubernetes.Tests.TestCluster;\n\npublic class TestClusterHostBuilder\n{\n    private readonly IHostBuilder _hostBuilder = new HostBuilder();\n\n    public ITestClusterHost Build()\n    {\n        if (string.IsNullOrEmpty(ServerUrl))\n        {\n            ServerUrl = $\"http://{IPAddress.Loopback}:{AvailablePort()}\";\n        }\n\n        _hostBuilder.ConfigureWebHostDefaults(web =>\n        {\n            web\n                .UseStartup<TestClusterStartup>()\n                .UseUrls(ServerUrl);\n        });\n\n        var host = _hostBuilder.Build();\n\n        var kubeConfig = new K8SConfiguration\n        {\n            ApiVersion = \"v1\",\n            Kind = \"Config\",\n            CurrentContext = \"test-context\",\n            Contexts = new[]\n            {\n                new Context\n                {\n                    Name = \"test-context\",\n                    ContextDetails = new ContextDetails\n                    {\n                        Namespace = \"test-namespace\",\n                        Cluster = \"test-cluster\",\n                        User = \"test-user\",\n                    }\n                }\n            },\n            Clusters = new[]\n            {\n                new Cluster\n                {\n                    Name = \"test-cluster\",\n                    ClusterEndpoint = new ClusterEndpoint\n                    {\n                        Server = ServerUrl,\n                    }\n                }\n            },\n            Users = new[]\n            {\n                new User\n                {\n                    Name = \"test-user\",\n                    UserCredentials = new UserCredentials\n                    {\n                        Token = \"test-token\",\n                    }\n                }\n            },\n        };\n\n        var clientConfiguration = KubernetesClientConfiguration.BuildConfigFromConfigObject(kubeConfig);\n\n        var client = new k8s.Kubernetes(clientConfiguration);\n\n        return new TestClusterHost(host, kubeConfig, client);\n    }\n\n    public TestClusterHostBuilder UseInitialResources(params IKubernetesObject<V1ObjectMeta>[] resources)\n    {\n        return ConfigureServices(services =>\n        {\n            services.Configure<TestClusterOptions>(options =>\n            {\n                foreach (var resource in resources)\n                {\n                    options.InitialResources.Add(resource);\n                }\n            });\n        });\n    }\n\n    public TestClusterHostBuilder ConfigureServices(Action<IServiceCollection> configureDelegate)\n    {\n        _hostBuilder.ConfigureServices(configureDelegate);\n        return this;\n    }\n\n    public TestClusterHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)\n    {\n        _hostBuilder.ConfigureServices(configureDelegate);\n        return this;\n    }\n\n    public TestClusterHostBuilder Configure(Action<TestClusterOptions> configureOptions)\n    {\n        _hostBuilder.ConfigureServices(services => Configure(configureOptions));\n        return this;\n    }\n\n    private static int AvailablePort()\n    {\n        using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);\n        socket.Bind(new IPEndPoint(IPAddress.Any, 0));\n        return ((IPEndPoint)socket.LocalEndPoint).Port;\n    }\n\n    public string ServerUrl { get; set; }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/TestCluster/TestClusterOptions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing k8s;\nusing k8s.Models;\nusing System.Collections.Generic;\n\nnamespace Yarp.Kubernetes.Tests.TestCluster;\n\npublic class TestClusterOptions\n{\n    public IList<IKubernetesObject<V1ObjectMeta>> InitialResources { get; } = new List<IKubernetesObject<V1ObjectMeta>>();\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/TestCluster/TestClusterStartup.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\n\nnamespace Yarp.Kubernetes.Tests.TestCluster;\n\npublic class TestClusterStartup\n{\n#pragma warning disable CA1822 // Mark members as static\n    public void ConfigureServices(IServiceCollection services)\n#pragma warning restore CA1822 // Mark members as static\n    {\n        // services.AddTransient<ResourceManager>();\n        services.AddControllers();\n        services.AddSingleton<ITestCluster, TestCluster>();\n    }\n\n#pragma warning disable CA1822 // Mark members as static\n    public void Configure(IApplicationBuilder app, ITestCluster cluster)\n#pragma warning restore CA1822 // Mark members as static\n    {\n        if (app is null)\n        {\n            throw new System.ArgumentNullException(nameof(app));\n        }\n\n        if (cluster is null)\n        {\n            throw new System.ArgumentNullException(nameof(cluster));\n        }\n\n        app.Use(next => async context =>\n        {\n            // This is a no-op, but very convenient for setting a breakpoint to see per-request details.\n            await next(context);\n        });\n        app.UseRouting();\n        app.UseEndpoints(endpoints => endpoints.MapControllers());\n        app.Run(cluster.UnhandledRequest);\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Utils/ResourceSerializers.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Text.Json;\nusing YamlDotNet.Core.Events;\nusing YamlDotNet.Serialization;\n\nnamespace Yarp.Kubernetes.Tests.Utils;\n\n/// <summary>\n/// Class ResourceSerializers implements the resource serializers interface.\n/// Implements the <see cref=\"IResourceSerializers\" />.\n/// </summary>\n/// <seealso cref=\"IResourceSerializers\" />\npublic static class ResourceSerializers\n{\n    private static readonly IDeserializer _yamlDeserializer = new DeserializerBuilder()\n            .WithNodeTypeResolver(new NonStringScalarTypeResolver())\n            .Build();\n\n    private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true };\n\n    public static T DeserializeYaml<T>(string yaml)\n    {\n        var resource = _yamlDeserializer.Deserialize<object>(yaml);\n\n        return Convert<T>(resource);\n    }\n\n    public static TResource Convert<TResource>(object resource)\n    {\n        var json = JsonSerializer.Serialize(resource, _jsonOptions);\n\n        return JsonSerializer.Deserialize<TResource>(json, _jsonOptions);\n    }\n\n    private class NonStringScalarTypeResolver : INodeTypeResolver\n    {\n        bool INodeTypeResolver.Resolve(NodeEvent nodeEvent, ref Type currentType)\n        {\n            if (currentType == typeof(object) && nodeEvent is Scalar)\n            {\n                var scalar = nodeEvent as Scalar;\n                if (scalar.IsPlainImplicit)\n                {\n                    // TODO: should use the correct boolean parser (which accepts yes/no) instead of bool.tryparse\n                    if (bool.TryParse(scalar.Value, out var _))\n                    {\n                        currentType = typeof(bool);\n                        return true;\n                    }\n\n                    if (int.TryParse(scalar.Value, out var _))\n                    {\n                        currentType = typeof(int);\n                        return true;\n                    }\n\n                    if (double.TryParse(scalar.Value, out var _))\n                    {\n                        currentType = typeof(double);\n                        return true;\n                    }\n                }\n            }\n\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Utils/TestLogger.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.Extensions.Logging;\nusing Xunit;\n\nnamespace Yarp.Kubernetes.Tests.Utils;\n\npublic class TestLogger<T> : ILogger<T>\n{\n    private readonly ITestOutputHelper _output;\n    private readonly LogLevel _minLogLevel;\n\n    public TestLogger(ITestOutputHelper output, LogLevel minLogLevel = LogLevel.Debug)\n    {\n        _output = output;\n        _minLogLevel = minLogLevel;\n    }\n\n    public IDisposable BeginScope<TState>(TState state)\n    {\n        return null;\n    }\n\n    public bool IsEnabled(LogLevel logLevel)\n    {\n        return logLevel != LogLevel.None && logLevel >= _minLogLevel;\n    }\n\n    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)\n    {\n        _output.WriteLine(formatter.Invoke(state, exception));\n    }\n}\n"
  },
  {
    "path": "test/Kubernetes.Tests/Yarp.Kubernetes.Tests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(TestTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <NoWarn>$(NoWarn);CS8002</NoWarn>\n    <RootNamespace>Yarp.Kubernetes</RootNamespace>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Kubernetes.Controller\\Yarp.Kubernetes.Controller.csproj\" />\n    <ProjectReference Include=\"..\\Tests.Common\\Yarp.Tests.Common.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Moq\" Version=\"$(MoqVersion)\" />\n    <PackageReference Include=\"Polly\" Version=\"$(PollyVersion)\" />\n    <PackageReference Include=\"Newtonsoft.Json\" Version=\"$(NewtonsoftJsonVersion)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Remove=\"Client\\*.yaml\" />\n    <None Remove=\"Certificates\\cert.der\" />\n    <None Remove=\"Certificates\\cert.pem\" />\n    <None Remove=\"Certificates\\key.der\" />\n    <None Remove=\"Certificates\\key.pem\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <EmbeddedResource Include=\"Client\\*.yaml\" />\n    <EmbeddedResource Include=\"Certificates\\cert.der\" />\n    <EmbeddedResource Include=\"Certificates\\cert.pem\" />\n    <EmbeddedResource Include=\"Certificates\\key.der\" />\n    <EmbeddedResource Include=\"Certificates\\key.pem\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Content Include=\"testassets\\**\\*\" CopyToOutputDirectory=\"PreserveNewest\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Compile Include=\"..\\ReverseProxy.Tests\\Common\\TaskExtensions.cs\" Link=\"Common\\TaskExtensions.cs\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/annotations/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"frontend.default:80\",\n    \"LoadBalancingPolicy\": \"Random\",\n    \"SessionAffinity\": {\n      \"Enabled\": true,\n      \"Policy\": \"Cookie\",\n      \"FailurePolicy\": \"Redistribute\",\n      \"AffinityKeyName\": \"Key1\",\n      \"Cookie\": {\n        \"Domain\": \"localhost\",\n        \"Expiration\": null,\n        \"HttpOnly\": true,\n        \"IsEssential\": true,\n        \"MaxAge\": null,\n        \"Path\": \"mypath\",\n        \"SameSite\": \"Strict\",\n        \"SecurePolicy\": \"Always\"\n      }\n    },\n    \"HealthCheck\": {\n      \"Active\": {\n        \"Enabled\": true,\n        \"Interval\": \"00:00:10\",\n        \"Timeout\": \"00:00:10\",\n        \"Policy\": \"ConsecutiveFailures\",\n        \"Path\": \"/api/health\"\n      }\n    },\n    \"HttpClient\": {\n      \"SslProtocols\": \"Ssl3\",\n      \"MaxConnectionsPerServer\": 2,\n      \"ActivityContextHeaders\": null,\n      \"WebProxy\": null,\n      \"DangerousAcceptAnyServerCertificate\": true\n    },\n    \"HttpRequest\": {\n      \"ActivityTimeout\": \"00:01:00\",\n      \"Version\": \"2.0\",\n      \"VersionPolicy\": \"RequestVersionExact\",\n      \"AllowResponseBuffering\": false\n    },\n    \"Destinations\": {\n      \"http://10.244.2.38:80\": {\n        \"Address\": \"http://10.244.2.38:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/annotations/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: minimal-ingress\n  namespace: default\n  annotations:\n    yarp.ingress.kubernetes.io/authorization-policy: authzpolicy\n    yarp.ingress.kubernetes.io/rate-limiter-policy: ratelimiterpolicy\n    yarp.ingress.kubernetes.io/cors-policy: corspolicy\n    yarp.ingress.kubernetes.io/load-balancing: Random\n    yarp.ingress.kubernetes.io/health-check: |\n      Active:\n        Enabled: true\n        Interval: '00:00:10'\n        Timeout: '00:00:10'\n        Policy: ConsecutiveFailures\n        Path: \"/api/health\"\n\n    yarp.ingress.kubernetes.io/http-client: |\n      SslProtocols: Ssl3\n      MaxConnectionsPerServer: 2\n      DangerousAcceptAnyServerCertificate: true\n\n    yarp.ingress.kubernetes.io/http-request: |\n      ActivityTimeout: '00:01:00'\n      Version: '2.0'\n      VersionPolicy: 'RequestVersionExact'\n      AllowResponseBuffering: false\n\n    yarp.ingress.kubernetes.io/session-affinity: |\n      Enabled: true\n      Policy: Cookie\n      FailurePolicy: Redistribute\n      AffinityKeyName: Key1\n      Cookie:\n        Domain: localhost\n        Expiration:\n        HttpOnly: true\n        IsEssential: true\n        MaxAge:\n        Path: mypath\n        SameSite: Strict\n        SecurePolicy: Always\n\n    yarp.ingress.kubernetes.io/transforms: |\n      - PathPrefix: \"/apis\"\n      - RequestHeader: header1\n        Append: bar\nspec:\n  rules:\n    - http:\n        paths:\n          - path: /foo\n            pathType: Prefix\n            backend:\n              service:\n                name: frontend\n                port:\n                  number: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: default\nspec:\n  selector:\n    app: frontend\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: default\nsubsets:\n  - addresses:\n      - ip: 10.244.2.38\n    ports:\n      - name: http\n        port: 80\n        protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/annotations/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"minimal-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"frontend.default:80\",\n    \"AuthorizationPolicy\": \"authzpolicy\",\n    \"RateLimiterPolicy\": \"ratelimiterpolicy\",\n    \"CorsPolicy\": \"corspolicy\",\n    \"Metadata\": null,\n    \"Transforms\": [\n      { \"PathPrefix\": \"/apis\" },\n      {\n        \"RequestHeader\": \"header1\",\n        \"Append\": \"bar\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/basic-ingress/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"frontend.default:80\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.38:80\": {\n        \"Address\": \"http://10.244.2.38:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/basic-ingress/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: minimal-ingress\n  namespace: default\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: frontend\n            port:\n              number: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: default\nspec:\n  selector:\n    app: frontend\n  ports:\n  - name: http\n    port: 80\n    targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.38\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/basic-ingress/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"minimal-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"frontend.default:80\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/exact-match/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"frontend.default:80\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.38:80\": {\n        \"Address\": \"http://10.244.2.38:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/exact-match/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: minimal-ingress\n  namespace: default\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: ExactMatch\n        backend:\n          service:\n            name: frontend\n            port:\n              number: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: default\nspec:\n  selector:\n    app: frontend\n  ports:\n  - name: http\n    port: 80\n    targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.38\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/exact-match/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"minimal-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"frontend.default:80\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/external-name-ingress/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"external-service.default:443\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"https://external-service.example.com:443\": {\n        \"Address\": \"https://external-service.example.com:443\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/external-name-ingress/ingress.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: external-service\n  namespace: default\nspec:\n  type: ExternalName\n  externalName: external-service.example.com\n--- \napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: external-ingress\n  namespace: default\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: external-service\n            port:\n              number: 443\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/external-name-ingress/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"external-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"external-service.default:443\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/hostname-routing/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"frontend.foo:80\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.38:80\": {\n        \"Address\": \"http://10.244.2.38:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/hostname-routing/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: hostname-routing\n  namespace: foo\nspec:\n  rules:\n  - host: foo.bar.com\n    http:\n      paths:\n      - path: /\n        pathType: Prefix\n        backend:\n          service:\n            name: frontend\n            port:\n              number: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: foo\nspec:\n  selector:\n    app: frontend\n  ports:\n  - name: http\n    port: 80\n    targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: foo\nsubsets:\n  - addresses:\n    - ip: 10.244.2.38\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/hostname-routing/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"hostname-routing.foo:foo.bar.com/\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [ \"foo.bar.com\" ],\n      \"Path\": \"/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"frontend.foo:80\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/https/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"frontend.default:443\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"https://10.244.2.38:443\": {\n        \"Address\": \"https://10.244.2.38:443\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/https/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: minimal-ingress\n  namespace: default\n  annotations:\n    yarp.ingress.kubernetes.io/backend-protocol: https\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: frontend\n            port:\n              number: 443\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: default\nspec:\n  selector:\n    app: frontend\n  ports:\n  - name: https\n    port: 443\n    targetPort: 443\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.38\n    ports:\n    - name: https\n      port: 443\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/https/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"minimal-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"frontend.default:443\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/https-service-port-protocol/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"frontend.default:443\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"https://10.244.2.38:443\": {\n        \"Address\": \"https://10.244.2.38:443\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  },\n  {\n    \"ClusterId\": \"frontend.default:https\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"https://10.244.2.38:443\": {\n        \"Address\": \"https://10.244.2.38:443\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  },\n  {\n    \"ClusterId\": \"frontend.default:80\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.38:80\": {\n        \"Address\": \"http://10.244.2.38:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/https-service-port-protocol/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: minimal-ingress\n  namespace: default\nspec:\n  rules:\n    - http:\n        paths:\n          - path: /foo\n            pathType: Prefix\n            backend:\n              service:\n                name: frontend\n                port:\n                  number: 443\n          - path: /fee\n            pathType: Prefix\n            backend:\n              service:\n                name: frontend\n                port:\n                  name: https\n          - path: /faa\n            pathType: Prefix\n            backend:\n              service:\n                name: frontend\n                port:\n                  number: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: default\nspec:\n  selector:\n    app: frontend\n  ports:\n    - name: https\n      port: 443\n      targetPort: 443\n    - name: http\n      port: 80\n      targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: default\nsubsets:\n  - addresses:\n      - ip: 10.244.2.38\n    ports:\n      - name: https\n        port: 443\n        protocol: TCP\n      - name: http\n        port: 80\n        protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/https-service-port-protocol/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"minimal-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"frontend.default:443\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  },\n  {\n    \"RouteId\": \"minimal-ingress.default:/fee\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/fee/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"frontend.default:https\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  },\n  {\n    \"RouteId\": \"minimal-ingress.default:/faa\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/faa/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"frontend.default:80\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/ingress-class-not-set/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: the-ingress\n  namespace: default\nspec:\n  rules:\n    - http:\n        paths:\n          - path: /\n            pathType: ImplementationSpecific\n            backend:\n              service:\n                name: the-service\n                port: \n                  name: http\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: the-service\n  namespace: default\nspec:\n  selector:\n    app: repro-1\n  type: ClusterIP\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: the-service\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.1.11\n    - ip: 10.244.1.12\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/ingress-class-set/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: the-ingress\n  namespace: default\nspec:\n  ingressClassName: yarp\n  rules:\n    - http:\n        paths:\n          - path: /\n            pathType: ImplementationSpecific\n            backend:\n              service:\n                name: the-service\n                port: \n                  name: http\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: the-service\n  namespace: default\nspec:\n  selector:\n    app: repro-1\n  type: ClusterIP\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: the-service\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.1.11\n    - ip: 10.244.1.12\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/mapped-port/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"backend.default:5011\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.33:80\": {\n        \"Address\": \"http://10.244.2.33:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/mapped-port/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: mapped-port\n  namespace: default\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: backend\n            port:\n              number: 5011\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: backend\n  namespace: default\nspec:\n  selector:\n    app: backend\n  ports:\n  - port: 5011\n    targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: backend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.33\n    ports:\n    - port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/mapped-port/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"mapped-port.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"backend.default:5011\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/missing-svc/clusters.json",
    "content": "[\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/missing-svc/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: missing-svc\n  namespace: default\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: backend\n            port:\n              number: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: default\nspec:\n  selector:\n    app: frontend\n  ports:\n  - name: http\n    port: 80\n    targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.38\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/missing-svc/routes.json",
    "content": "[\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-endpoints-ports/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"frontend.default:80\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.38:80\": {\n        \"Address\": \"http://10.244.2.38:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-endpoints-ports/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: minimal-ingress\n  namespace: default\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: frontend\n            port:\n              number: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: default\nspec:\n  selector:\n    app: frontend\n  ports:\n  - name: http\n    port: 80\n    targetPort: 80\n  - name: https\n    port: 443\n    targetPort: 443\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.38\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n    - name: https\n      port: 443\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-endpoints-ports/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"minimal-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"frontend.default:80\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-endpoints-same-port/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"frontend.default:80\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.38:80\": {\n        \"Address\": \"http://10.244.2.38:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-endpoints-same-port/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: minimal-ingress\n  namespace: default\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: frontend\n            port:\n              number: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: default\nspec:\n  selector:\n    app: frontend\n  ports:\n  - name: http\n    port: 80\n    targetPort: 80\n  - name: other_http\n    port: 8080\n    targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.38\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n    - name: other_http\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-endpoints-same-port/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"minimal-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"frontend.default:80\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-hosts/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"repro-1-service.foo:http\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.1.11:80\": {\n        \"Address\": \"http://10.244.1.11:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  },\n  {\n    \"ClusterId\": \"repro-2-service.foo:http\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.22:80\": {\n        \"Address\": \"http://10.244.2.22:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  },\n  {\n    \"ClusterId\": \"repro-3-service.foo:http\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.3.33:80\": {\n        \"Address\": \"http://10.244.3.33:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-hosts/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: repro-1-ingress\n  namespace: foo\nspec:\n  rules:\n    - host: 'subdomain1.example.com'\n      http:\n        paths:\n          - path: /\n            pathType: Prefix\n            backend:\n              service:\n                name: repro-1-service\n                port:\n                  name: http\n    - host: 'subdomain2.example.com'\n      http:\n        paths:\n          - path: /\n            pathType: Prefix\n            backend:\n              service:\n                name: repro-2-service\n                port:\n                  name: http\n    - http:\n        paths:\n          - path: /\n            pathType: Prefix\n            backend:\n              service:\n                name: repro-3-service\n                port:\n                  name: http\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: repro-1-service\n  namespace: foo\nspec:\n  selector:\n    app: repro-1\n  type: ClusterIP\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: repro-1-service\n  namespace: foo\nsubsets:\n  - addresses:\n    - ip: 10.244.1.11\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: repro-2-service\n  namespace: foo\nspec:\n  selector:\n    app: repro-2\n  type: ClusterIP\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: repro-2-service\n  namespace: foo\nsubsets:\n  - addresses:\n    - ip: 10.244.2.22\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: repro-3-service\n  namespace: foo\nspec:\n  selector:\n    app: repro-2\n  type: ClusterIP\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: repro-3-service\n  namespace: foo\nsubsets:\n  - addresses:\n    - ip: 10.244.3.33\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-hosts/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"repro-1-ingress.foo:subdomain1.example.com/\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [ \"subdomain1.example.com\" ],\n      \"Path\": \"/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"repro-1-service.foo:http\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  },\n  {\n    \"RouteId\": \"repro-1-ingress.foo:subdomain2.example.com/\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [ \"subdomain2.example.com\" ],\n      \"Path\": \"/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"repro-2-service.foo:http\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  },\n  {\n    \"RouteId\": \"repro-1-ingress.foo:/\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"repro-3-service.foo:http\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-ingresses/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"repro-1-service.foo:http\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.1.11:80\": {\n        \"Address\": \"http://10.244.1.11:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      },\n      \"http://10.244.1.12:80\": {\n        \"Address\": \"http://10.244.1.12:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  },\n  {\n    \"ClusterId\": \"repro-2-service.foo:http\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.22:80\": {\n        \"Address\": \"http://10.244.2.22:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      },\n      \"http://10.244.2.23:80\": {\n        \"Address\": \"http://10.244.2.23:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-ingresses/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: repro-1-ingress\n  namespace: foo\nspec:\n  rules:\n    - host: 'subdomain1.example.com'\n      http:\n        paths:\n          - path: /\n            pathType: ImplementationSpecific\n            backend:\n              service:\n                name: repro-1-service\n                port: \n                  name: http\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: repro-1-service\n  namespace: foo\nspec:\n  selector:\n    app: repro-1\n  type: ClusterIP\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: repro-1-service\n  namespace: foo\nsubsets:\n  - addresses:\n    - ip: 10.244.1.11\n    - ip: 10.244.1.12\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n---\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: repro-2-ingress\n  namespace: foo\nspec:\n  rules:\n    - host: 'subdomain2.example.com'\n      http:\n        paths:\n          - path: /\n            pathType: ImplementationSpecific\n            backend:\n              service:\n                name: repro-2-service\n                port: \n                  name: http\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: repro-2-service\n  namespace: foo\nspec:\n  selector:\n    app: repro-2\n  type: ClusterIP\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: repro-2-service\n  namespace: foo\nsubsets:\n  - addresses:\n    - ip: 10.244.2.22\n    - ip: 10.244.2.23\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-ingresses/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"repro-1-ingress.foo:subdomain1.example.com/\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [ \"subdomain1.example.com\" ],\n      \"Path\": \"/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"repro-1-service.foo:http\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  },\n  {\n    \"RouteId\": \"repro-2-ingress.foo:subdomain2.example.com/\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [ \"subdomain2.example.com\" ],\n      \"Path\": \"/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"repro-2-service.foo:http\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-ingresses-one-svc/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"repro-service.foo:http\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.1.11:80\": {\n        \"Address\": \"http://10.244.1.11:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      },\n      \"http://10.244.1.12:80\": {\n        \"Address\": \"http://10.244.1.12:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-ingresses-one-svc/ingress.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: repro-service\n  namespace: foo\nspec:\n  selector:\n    app: repro-1\n  type: ClusterIP\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: repro-service\n  namespace: foo\nsubsets:\n  - addresses:\n    - ip: 10.244.1.11\n    - ip: 10.244.1.12\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n---\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: repro-1-ingress\n  namespace: foo\nspec:\n  rules:\n    - host: 'subdomain1.example.com'\n      http:\n        paths:\n          - path: /\n            pathType: ImplementationSpecific\n            backend:\n              service:\n                name: repro-service\n                port: \n                  name: http\n---\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: repro-2-ingress\n  namespace: foo\nspec:\n  rules:\n    - host: 'subdomain2.example.com'\n      http:\n        paths:\n          - path: /\n            pathType: ImplementationSpecific\n            backend:\n              service:\n                name: repro-service\n                port: \n                  name: http\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-ingresses-one-svc/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"repro-1-ingress.foo:subdomain1.example.com/\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [ \"subdomain1.example.com\" ],\n      \"Path\": \"/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"repro-service.foo:http\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  },\n  {\n    \"RouteId\": \"repro-2-ingress.foo:subdomain2.example.com/\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [ \"subdomain2.example.com\" ],\n      \"Path\": \"/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"repro-service.foo:http\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-namespaces/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"the-service.ns-one:http\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.1.11:80\": {\n        \"Address\": \"http://10.244.1.11:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      },\n      \"http://10.244.1.12:80\": {\n        \"Address\": \"http://10.244.1.12:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  },\n  {\n    \"ClusterId\": \"the-service.ns-two:http\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.22:80\": {\n        \"Address\": \"http://10.244.2.22:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      },\n      \"http://10.244.2.23:80\": {\n        \"Address\": \"http://10.244.2.23:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-namespaces/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: the-ingress\n  namespace: ns-one\nspec:\n  rules:\n    - host: 'subdomain1.example.com'\n      http:\n        paths:\n          - path: /\n            pathType: ImplementationSpecific\n            backend:\n              service:\n                name: the-service\n                port: \n                  name: http\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: the-service\n  namespace: ns-one\nspec:\n  selector:\n    app: repro-1\n  type: ClusterIP\n  ports:\n    - name: http\n      port: 80\n      targetPort: 80\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: the-service\n  namespace: ns-one\nsubsets:\n  - addresses:\n    - ip: 10.244.1.11\n    - ip: 10.244.1.12\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n---\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: the-ingress\n  namespace: ns-two\nspec:\n  rules:\n    - host: 'subdomain2.example.com'\n      http:\n        paths:\n          - path: /\n            pathType: ImplementationSpecific\n            backend:\n              service:\n                name: the-service\n                port: \n                  name: http\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: the-service\n  namespace: ns-two\nspec:\n  selector:\n    app: repro-2\n  type: ClusterIP\n  ports:\n    - name: http\n      port: 80\n      targetPort: http\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: the-service\n  namespace: ns-two\nsubsets:\n  - addresses:\n    - ip: 10.244.2.22\n    - ip: 10.244.2.23\n    ports:\n    - name: http\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/multiple-namespaces/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"the-ingress.ns-one:subdomain1.example.com/\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [ \"subdomain1.example.com\" ],\n      \"Path\": \"/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"the-service.ns-one:http\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  },\n  {\n    \"RouteId\": \"the-ingress.ns-two:subdomain2.example.com/\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [ \"subdomain2.example.com\" ],\n      \"Path\": \"/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"the-service.ns-two:http\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/port-diff-name/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"backend.default:88\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.33:80\": {\n        \"Address\": \"http://10.244.2.33:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/port-diff-name/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: port-diff-name\n  namespace: default\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: backend\n            port:\n              number: 88\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: backend\n  namespace: default\nspec:\n  selector:\n    app: backend\n  ports:\n  - port: 88\n    name: my-http\n    targetPort: http\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: backend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.33\n    ports:\n    - port: 80\n      protocol: TCP\n      name: my-http\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/port-diff-name/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"port-diff-name.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"backend.default:88\",\n    \"AuthorizationPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/port-mismatch/clusters.json",
    "content": "[\n]\n\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/port-mismatch/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: port-mismatch\n  namespace: default\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: backend\n            port:\n              number: 5011\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: backend\n  namespace: default\nspec:\n  selector:\n    app: backend\n  ports:\n  - port: 80\n    targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: backend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.33\n    ports:\n    - port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/port-mismatch/routes.json",
    "content": "[\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/resource-informer/ResourcesAreListedWhenReadyAsyncIsComplete/resources.yaml",
    "content": "﻿# pods\n- apiVersion: v1\n  kind: Pod\n  metadata:\n    name: pod-1\n    namespace: the-namespace\n  spec:\n    containers:\n      - name: test\n        image: test\n- apiVersion: v1\n  kind: Pod\n  metadata:\n    name: pod-2\n    namespace: the-namespace\n  spec:\n    containers:\n      - name: test\n        image: test"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/resource-informer/ResourcesAreListedWhenReadyAsyncIsComplete/shouldbe.yaml",
    "content": "- namespace: the-namespace\n  name: pod-1\n- namespace: the-namespace\n  name: pod-2"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/resource-informer/ResourcesWithApiGroupAreListed/resources.yaml",
    "content": "﻿# pods\n- apiVersion: apps/v1\n  kind: Deployment\n  metadata:\n    name: deployment-1\n    namespace: the-namespace\n  spec:\n    template:\n      spec:\n        containers:\n        - name: test\n          image: test\n- apiVersion: apps/v1\n  kind: Deployment\n  metadata:\n    name: deployment-2\n    namespace: the-namespace\n  spec:\n    template:\n      spec:\n        containers:\n        - name: test\n          image: test"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/resource-informer/ResourcesWithApiGroupAreListed/shouldbe.yaml",
    "content": "- namespace: the-namespace\n  name: deployment-1\n- namespace: the-namespace\n  name: deployment-2"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-headers/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"frontend.default:80\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.38:80\": {\n        \"Address\": \"http://10.244.2.38:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-headers/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: minimal-ingress\n  namespace: default\n  annotations:\n    yarp.ingress.kubernetes.io/route-metadata: |\n      foo: bar\n      another-key: another-value\n    yarp.ingress.kubernetes.io/route-headers: |\n      - Name: the-header-key\n        Values: \n        - the-header-value\n        Mode: Contains\n        IsCaseSensitive: false\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: frontend\n            port:\n              number: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: default\nspec:\n  selector:\n    app: frontend\n  ports:\n  - name: https\n    port: 80\n    targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.38\n    ports:\n    - name: https\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-headers/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"minimal-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": [\n        {\n          \"Name\": \"the-header-key\",\n          \"Values\": [ \"the-header-value\" ],\n          \"Mode\": \"Contains\",\n          \"IsCaseSensitive\": false\n        }\n      ],\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"frontend.default:80\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": {\n      \"foo\": \"bar\",\n      \"another-key\": \"another-value\"\n    },\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-metadata/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"frontend.default:80\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.38:80\": {\n        \"Address\": \"http://10.244.2.38:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-metadata/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: minimal-ingress\n  namespace: default\n  annotations:\n    yarp.ingress.kubernetes.io/route-metadata: |\n      foo: bar\n      another-key: another-value\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: frontend\n            port:\n              number: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: default\nspec:\n  selector:\n    app: frontend\n  ports:\n  - name: https\n    port: 80\n    targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.38\n    ports:\n    - name: https\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-metadata/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"minimal-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": null,\n    \"ClusterId\": \"frontend.default:80\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": {\n      \"foo\": \"bar\",\n      \"another-key\": \"another-value\"\n    },\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-methods/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"frontend.default:80\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.38:80\": {\n        \"Address\": \"http://10.244.2.38:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-methods/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: first-ingress\n  namespace: default\n  annotations:\n    yarp.ingress.kubernetes.io/route-methods: |\n      - GET\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: frontend\n            port:\n              number: 80\n---\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: second-ingress\n  namespace: default\n  annotations:\n    yarp.ingress.kubernetes.io/route-methods: |\n      - POST\n      - PUT\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: frontend\n            port:\n              number: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: default\nspec:\n  selector:\n    app: frontend\n  ports:\n  - name: https\n    port: 80\n    targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.38\n    ports:\n    - name: https\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-methods/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"first-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": [\"GET\"],\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"ClusterId\": \"frontend.default:80\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  },\n  {\n    \"RouteId\": \"second-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": [\"POST\", \"PUT\"],\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"ClusterId\": \"frontend.default:80\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-order/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"frontend.default:80\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.38:80\": {\n        \"Address\": \"http://10.244.2.38:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-order/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: minimal-ingress\n  namespace: default\n  annotations:\n    yarp.ingress.kubernetes.io/route-order: '10'\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: frontend\n            port:\n              number: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: default\nspec:\n  selector:\n    app: frontend\n  ports:\n  - name: https\n    port: 80\n    targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.38\n    ports:\n    - name: https\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-order/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"minimal-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": null\n    },\n    \"Order\": 10,\n    \"ClusterId\": \"frontend.default:80\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": null,\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-queryparameters/clusters.json",
    "content": "[\n  {\n    \"ClusterId\": \"frontend.default:80\",\n    \"LoadBalancingPolicy\": null,\n    \"SessionAffinity\": null,\n    \"HealthCheck\": null,\n    \"HttpClient\": null,\n    \"HttpRequest\": null,\n    \"Destinations\": {\n      \"http://10.244.2.38:80\": {\n        \"Address\": \"http://10.244.2.38:80\",\n        \"Health\": null,\n        \"Metadata\": null\n      }\n    },\n    \"Metadata\": null\n  }\n]\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-queryparameters/ingress.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: minimal-ingress\n  namespace: default\n  annotations:\n    yarp.ingress.kubernetes.io/route-metadata: |\n      foo: bar\n      another-key: another-value\n    yarp.ingress.kubernetes.io/route-queryparameters: |\n      - Name: the-queryparameter-key\n        Values: \n        - the-queryparameter-value\n        Mode: Contains\n        IsCaseSensitive: false\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /foo\n        pathType: Prefix\n        backend:\n          service:\n            name: frontend\n            port:\n              number: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: frontend\n  namespace: default\nspec:\n  selector:\n    app: frontend\n  ports:\n  - name: https\n    port: 80\n    targetPort: 80\n  type: ClusterIP\n---\napiVersion: v1\nkind: Endpoints\nmetadata:\n  name: frontend\n  namespace: default\nsubsets:\n  - addresses:\n    - ip: 10.244.2.38\n    ports:\n    - name: https\n      port: 80\n      protocol: TCP\n"
  },
  {
    "path": "test/Kubernetes.Tests/testassets/route-queryparameters/routes.json",
    "content": "[\n  {\n    \"RouteId\": \"minimal-ingress.default:/foo\",\n    \"Match\": {\n      \"Methods\": null,\n      \"Hosts\": [],\n      \"Path\": \"/foo/{**catch-all}\",\n      \"Headers\": null,\n      \"QueryParameters\": [\n        {\n          \"Name\": \"the-queryparameter-key\",\n          \"Values\": [ \"the-queryparameter-value\" ],\n          \"Mode\": \"Contains\",\n          \"IsCaseSensitive\": false\n        }\n      ]\n    },\n    \"Order\": null,\n    \"ClusterId\": \"frontend.default:80\",\n    \"AuthorizationPolicy\": null,\n    \"RateLimiterPolicy\": null,\n    \"CorsPolicy\": null,\n    \"Metadata\": {\n      \"foo\": \"bar\",\n      \"another-key\": \"another-value\"\n    },\n    \"Transforms\": null\n  }\n]\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/Common/Helpers.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Hosting.Server;\nusing Microsoft.AspNetCore.Hosting.Server.Features;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.DependencyInjection;\nusing System.Linq;\n\nnamespace Yarp.ReverseProxy;\n\npublic static class Helpers\n{\n    public static string GetAddress(this IHost server)\n    {\n        return server.Services.GetService<IServer>().Features.Get<IServerAddressesFeature>().Addresses.First();\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/Common/HttpSysTestEnvironment.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Server.HttpSys;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Common;\n\npublic class HttpSysTestEnvironment\n{\n    private readonly Action<IServiceCollection> _configureDestinationServices;\n    private readonly Action<HttpSysOptions> _configureDestinationHttpSysOptions;\n    private readonly Action<IApplicationBuilder> _configureDestinationApp;\n    private readonly Action<IServiceCollection> _configureProxyServices;\n    private readonly Action<IReverseProxyBuilder> _configureProxy;\n    private readonly Action<IApplicationBuilder> _configureProxyApp;\n    private readonly Action<IReverseProxyApplicationBuilder> _configureProxyPipeline;\n    private readonly Func<ClusterConfig, RouteConfig, (ClusterConfig Cluster, RouteConfig Route)> _configTransformer;\n\n    public string ClusterId { get; set; } = \"cluster1\";\n\n    public HttpSysTestEnvironment(\n        Action<IServiceCollection> configureDestinationServices,\n        Action<HttpSysOptions> configureDestinationHttpSysOptions,\n        Action<IApplicationBuilder> configureDestinationApp,\n        Action<IServiceCollection> configureProxyServices,\n        Action<IReverseProxyBuilder> configureProxy,\n        Action<IApplicationBuilder> configureProxyApp,\n        Action<IReverseProxyApplicationBuilder> configureProxyPipeline,\n        Func<ClusterConfig, RouteConfig, (ClusterConfig Cluster, RouteConfig Route)> configTransformer = null)\n    {\n        _configureDestinationServices = configureDestinationServices;\n        _configureDestinationHttpSysOptions = configureDestinationHttpSysOptions;\n        _configureDestinationApp = configureDestinationApp;\n        _configureProxy = configureProxy;\n        _configureProxyApp = configureProxyApp;\n        _configureProxyPipeline = configureProxyPipeline;\n        _configureProxyServices = configureProxyServices ?? (_ => { });\n        _configTransformer = configTransformer ?? ((ClusterConfig c, RouteConfig r) => (c, r));\n    }\n\n    public async Task Invoke(Func<string, Task> clientFunc, CancellationToken cancellationToken = default)\n    {\n        using var destination = CreateHttpSysHost(_configureDestinationServices, _configureDestinationHttpSysOptions, _configureDestinationApp);\n        await destination.StartAsync(cancellationToken);\n\n        using var proxy = CreateHttpSysProxy(\n            ClusterId,\n            destination.GetAddress(),\n            _configureProxyServices,\n            _configureProxy,\n            _configureProxyApp,\n            _configureProxyPipeline,\n            _configTransformer);\n        await proxy.StartAsync(cancellationToken);\n\n        try\n        {\n            await clientFunc(proxy.GetAddress());\n        }\n        finally\n        {\n            await proxy.StopAsync(cancellationToken);\n            await destination.StopAsync(cancellationToken);\n        }\n    }\n\n    private static IHost CreateHttpSysProxy(\n        string clusterId,\n        string destinationAddress,\n        Action<IServiceCollection> configureServices,\n        Action<IReverseProxyBuilder> configureProxy,\n        Action<IApplicationBuilder> configureProxyApp,\n        Action<IReverseProxyApplicationBuilder> configureProxyPipeline,\n        Func<ClusterConfig, RouteConfig, (ClusterConfig Cluster, RouteConfig Route)> configTransformer)\n    {\n        return CreateHttpSysHost(\n            services =>\n            {\n                configureServices(services);\n\n                var route = new RouteConfig\n                {\n                    RouteId = \"route1\",\n                    ClusterId = clusterId,\n                    Match = new RouteMatch { Path = \"/{**catchall}\" }\n                };\n\n                var cluster = new ClusterConfig\n                {\n                    ClusterId = clusterId,\n                    Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n                    {\n                        { \"destination1\",  new DestinationConfig() { Address = destinationAddress } }\n                    },\n                };\n                (cluster, route) = configTransformer(cluster, route);\n                var proxyBuilder = services.AddReverseProxy().LoadFromMemory(new[] { route }, new[] { cluster });\n                configureProxy(proxyBuilder);\n            },\n            httpSysOptions => { },\n            app =>\n            {\n                configureProxyApp(app);\n                app.UseRouting();\n                app.UseEndpoints(builder =>\n                {\n                    if (configureProxyPipeline != null)\n                    {\n                        builder.MapReverseProxy(configureProxyPipeline);\n                    }\n                    else\n                    {\n                        builder.MapReverseProxy();\n                    }\n                });\n            });\n    }\n\n    private static IHost CreateHttpSysHost(\n        Action<IServiceCollection> configureServices,\n        Action<HttpSysOptions> configureHttpSys,\n        Action<IApplicationBuilder> configureApp)\n    {\n        return CreateHost(webHostBuilder =>\n        {\n            Debug.Assert(OperatingSystem.IsWindows());\n            webHostBuilder\n               .ConfigureServices(configureServices)\n               .UseHttpSys(options =>\n               {\n                   options.UrlPrefixes.Add(UrlPrefix.Create(\"http\", \"localhost\", \"0\", \"/\"));\n                   configureHttpSys(options);\n               })\n               .Configure(configureApp);\n        });\n    }\n\n    private static IHost CreateHost(Action<IWebHostBuilder> configureWebHost)\n    {\n        return new HostBuilder()\n            .ConfigureAppConfiguration(config =>\n            {\n                config.AddInMemoryCollection(new Dictionary<string, string>()\n                {\n                    { \"Logging:LogLevel:Microsoft.AspNetCore.Hosting.Diagnostics\", \"Information\" }\n                });\n            })\n            .ConfigureLogging((hostingContext, loggingBuilder) =>\n            {\n                loggingBuilder.AddConfiguration(hostingContext.Configuration.GetSection(\"Logging\"));\n                loggingBuilder.AddEventSourceLogger();\n            })\n            .ConfigureWebHost(configureWebHost)\n            .Build();\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/Common/TestEnvironment.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Server.HttpSys;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Common;\n\npublic class TestEnvironment\n{\n    public ITestOutputHelper TestOutput { get; set; }\n\n    public HttpProtocols ProxyProtocol { get; set; } = HttpProtocols.Http1AndHttp2;\n\n    public bool UseHttpsOnProxy { get; set; }\n\n    public Encoding HeaderEncoding { get; set; }\n\n    public Action<IServiceCollection> ConfigureProxyServices { get; set; } = _ => { };\n\n    public Action<IReverseProxyBuilder> ConfigureProxy { get; set; } = _ => { };\n\n    public Action<IApplicationBuilder> ConfigureProxyApp { get; set; } = _ => { };\n\n    public string ClusterId { get; set; } = \"cluster1\";\n\n    public Func<ClusterConfig, RouteConfig, (ClusterConfig Cluster, RouteConfig Route)> ConfigTransformer { get; set; } = (a, b) => (a, b);\n\n    public Version DestinationHttpVersion { get; set; }\n\n    public HttpVersionPolicy? DestinationHttpVersionPolicy { get; set; }\n\n    public HttpProtocols DestinationProtocol { get; set; } = HttpProtocols.Http1AndHttp2;\n\n    public bool UseHttpsOnDestination { get; set; }\n\n    public bool UseHttpSysOnDestination { get; set; }\n\n    public Action<IServiceCollection> ConfigureDestinationServices { get; set; } = _ => { };\n\n    public Action<IApplicationBuilder> ConfigureDestinationApp { get; set; } = _ => { };\n\n    public TestEnvironment() { }\n\n    public TestEnvironment(RequestDelegate destinationGetDelegate)\n    {\n        ConfigureDestinationApp = destinationApp =>\n        {\n            destinationApp.Run(destinationGetDelegate);\n        };\n    }\n\n    public async Task Invoke(Func<string, Task> clientFunc, CancellationToken cancellationToken = default)\n    {\n        using var destination = CreateHost(DestinationProtocol, UseHttpsOnDestination, HeaderEncoding,\n            ConfigureDestinationServices, ConfigureDestinationApp, UseHttpSysOnDestination);\n        await destination.StartAsync(cancellationToken);\n\n        Exception proxyException = null;\n        using var proxy = CreateProxy(destination.GetAddress(), ex => proxyException = ex);\n        await proxy.StartAsync(cancellationToken);\n\n        try\n        {\n            await clientFunc(proxy.GetAddress());\n        }\n        finally\n        {\n            await proxy.StopAsync(cancellationToken);\n            await destination.StopAsync(cancellationToken);\n        }\n\n        Assert.Null(proxyException);\n    }\n\n    public IHost CreateProxy(string destinationAddress, Action<Exception> onProxyException = null)\n    {\n        return CreateHost(ProxyProtocol, UseHttpsOnProxy, HeaderEncoding,\n            services =>\n            {\n                ConfigureProxyServices(services);\n\n                var route = new RouteConfig\n                {\n                    RouteId = \"route1\",\n                    ClusterId = ClusterId,\n                    Match = new RouteMatch { Path = \"/{**catchall}\" }\n                };\n\n                var cluster = new ClusterConfig\n                {\n                    ClusterId = ClusterId,\n                    Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n                    {\n                        { \"destination1\",  new DestinationConfig() { Address = destinationAddress } }\n                    },\n                    HttpClient = new HttpClientConfig\n                    {\n                        DangerousAcceptAnyServerCertificate = UseHttpsOnDestination,\n                        RequestHeaderEncoding = HeaderEncoding?.WebName,\n                    },\n                    HttpRequest = new Forwarder.ForwarderRequestConfig\n                    {\n                        Version = DestinationHttpVersion,\n                        VersionPolicy = DestinationHttpVersionPolicy,\n                    }\n                };\n                (cluster, route) = ConfigTransformer(cluster, route);\n                var proxyBuilder = services.AddReverseProxy().LoadFromMemory(new[] { route }, new[] { cluster });\n                ConfigureProxy(proxyBuilder);\n            },\n            app =>\n            {\n                app.Use(async (context, next) =>\n                {\n                    try\n                    {\n                        await next();\n                    }\n                    catch (Exception ex)\n                    {\n                        onProxyException?.Invoke(ex);\n                        throw;\n                    }\n                });\n\n                ConfigureProxyApp(app);\n                app.UseRouting();\n                app.UseEndpoints(builder =>\n                {\n                    builder.MapReverseProxy();\n                });\n            });\n    }\n\n    private IHost CreateHost(HttpProtocols protocols, bool useHttps, Encoding requestHeaderEncoding,\n        Action<IServiceCollection> configureServices, Action<IApplicationBuilder> configureApp, bool useHttpSys = false)\n    {\n        return new HostBuilder()\n            .ConfigureAppConfiguration(config =>\n            {\n                config.AddInMemoryCollection(new Dictionary<string, string>()\n                {\n                    { \"Logging:LogLevel:Yarp\", \"Trace\" },\n                    { \"Logging:LogLevel:Microsoft\", \"Trace\" },\n                    { \"Logging:LogLevel:Microsoft.AspNetCore.Hosting.Diagnostics\", \"Information\" }\n                });\n            })\n            .ConfigureLogging((hostingContext, loggingBuilder) =>\n            {\n                loggingBuilder.AddConfiguration(hostingContext.Configuration.GetSection(\"Logging\"));\n                loggingBuilder.AddEventSourceLogger();\n                if (TestOutput != null)\n                {\n                    loggingBuilder.Services.AddSingleton<ILoggerProvider>(new TestLoggerProvider(TestOutput));\n                }\n            })\n            .ConfigureWebHost(webHostBuilder =>\n            {\n                webHostBuilder\n                    .ConfigureServices(configureServices)\n                    .UseKestrel(kestrel =>\n                    {\n                        if (requestHeaderEncoding is not null)\n                        {\n                            kestrel.RequestHeaderEncodingSelector = _ => requestHeaderEncoding;\n                        }\n                        kestrel.Listen(IPAddress.Loopback, 0, listenOptions =>\n                        {\n                            listenOptions.Protocols = protocols;\n                            if (useHttps)\n                            {\n                                listenOptions.UseHttps(TestResources.GetTestCertificate());\n                            }\n                            listenOptions.UseConnectionLogging();\n                        });\n                    })\n                    .Configure(configureApp);\n                if (useHttpSys)\n                {\n#pragma warning disable CA1416 // Validate platform compatibility\n                    webHostBuilder.UseHttpSys(httpSys =>\n                    {\n                        if (useHttps)\n                        {\n                            httpSys.UrlPrefixes.Add(\"https://localhost:\" + FindHttpSysHttpsPortAsync(TestOutput).Result);\n                        }\n                        else\n                        {\n                            httpSys.UrlPrefixes.Add(\"http://localhost:0\");\n                        }\n                    });\n#pragma warning restore CA1416 // Validate platform compatibility\n                }\n            }).Build();\n    }\n\n    private const int BaseHttpsPort = 44300;\n    private const int MaxHttpsPort = 44399;\n    private static int NextHttpsPort = BaseHttpsPort;\n    private static readonly SemaphoreSlim PortLock = new SemaphoreSlim(1);\n\n    internal static async Task<int> FindHttpSysHttpsPortAsync(ITestOutputHelper output)\n    {\n        await PortLock.WaitAsync();\n        try\n        {\n            while (NextHttpsPort < MaxHttpsPort)\n            {\n                var port = NextHttpsPort++;\n                using var host = new HostBuilder()\n                    .ConfigureAppConfiguration(config =>\n                    {\n                        config.AddInMemoryCollection(new Dictionary<string, string>()\n                        {\n                            { \"Logging:LogLevel:Microsoft\", \"Trace\" },\n                        });\n                    })\n                    .ConfigureLogging((hostingContext, loggingBuilder) =>\n                    {\n                        loggingBuilder.AddConfiguration(hostingContext.Configuration.GetSection(\"Logging\"));\n                        loggingBuilder.AddEventSourceLogger();\n                        loggingBuilder.AddXunit(output);\n                    })\n                    .ConfigureWebHost(webHostBuilder =>\n                    {\n#pragma warning disable CA1416 // Validate platform compatibility\n                        webHostBuilder.UseHttpSys(httpSys =>\n                        {\n                            httpSys.UrlPrefixes.Add(\"https://localhost:\" + port);\n                        });\n                        webHostBuilder.Configure(app => { });\n#pragma warning restore CA1416 // Validate platform compatibility\n                    }).Build();\n\n                try\n                {\n                    await host.StartAsync();\n                    await host.StopAsync();\n                    return port;\n                }\n                catch (HttpSysException)\n                {\n                }\n            }\n            NextHttpsPort = BaseHttpsPort;\n        }\n        finally\n        {\n            PortLock.Release();\n        }\n        throw new Exception(\"Failed to locate a free port.\");\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/Common/TestUrlHelper.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.ReverseProxy;\n\npublic static class TestUrlHelper\n{\n    public static string GetTestUrl()\n    {\n        return BuildTestUri().ToString();\n    }\n\n    public static Uri BuildTestUri()\n    {\n        return BuildTestUri(Uri.UriSchemeHttp);\n    }\n\n    internal static Uri BuildTestUri(string scheme)\n    {\n        // Most functional tests use this codepath and should directly bind to dynamic port \"0\" and scrape\n        // the assigned port from the status message, which should be 100% reliable since the port is bound\n        // once and never released.  Binding to dynamic port \"0\" on \"localhost\" (both IPv4 and IPv6) is not\n        // supported, so the port is only bound on \"127.0.0.1\" (IPv4).  If a test explicitly requires IPv6,\n        // it should provide a hint URL with \"localhost\" (IPv4 and IPv6) or \"[::1]\" (IPv6-only).\n        return new UriBuilder(scheme, \"127.0.0.1\", 0).Uri;\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/DistributedTracingTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Text;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\nusing Yarp.ReverseProxy.Common;\n\nnamespace Yarp.ReverseProxy;\n\npublic class DistributedTracingTests\n{\n    // These constants depend on the default behavior of DistributedContextPropagator\n    private const string Baggage = \"Correlation-Context\";\n    private const string TraceParent = \"traceparent\";\n    private const string TraceState = \"tracestate\";\n    private const string RequestId = \"Request-Id\";\n\n    [Theory]\n    [InlineData(ActivityIdFormat.W3C)]\n    [InlineData(ActivityIdFormat.Hierarchical)]\n    public async Task DistributedTracing_Works(ActivityIdFormat idFormat)\n    {\n        var proxyHeaders = new HeaderDictionary();\n        var downstreamHeaders = new HeaderDictionary();\n\n        var test = new TestEnvironment(\n            async context =>\n            {\n                foreach (var header in context.Request.Headers)\n                {\n                    downstreamHeaders.Add(header);\n                }\n                await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(\"Hello\"));\n            })\n        {\n            ConfigureProxyApp = proxyApp =>\n            {\n                proxyApp.Use(next => context =>\n                {\n                    foreach (var header in context.Request.Headers)\n                    {\n                        proxyHeaders.Add(header);\n                    }\n                    return next(context);\n                });\n            },\n        };\n\n        var clientActivity = new Activity(\"Foo\");\n        clientActivity.SetIdFormat(idFormat);\n        clientActivity.TraceStateString = \"Bar\";\n        clientActivity.AddBaggage(\"One\", \"1\");\n        clientActivity.AddBaggage(\"Two\", \"2\");\n        clientActivity.Start();\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new HttpClient();\n            Assert.Equal(\"Hello\", await client.GetStringAsync(uri));\n        });\n\n        Assert.NotEmpty(proxyHeaders);\n        Assert.NotEmpty(downstreamHeaders);\n\n        ValidateActivities(idFormat, clientActivity, proxyHeaders, downstreamHeaders);\n    }\n\n    private static void ValidateActivities(ActivityIdFormat idFormat, Activity client, HeaderDictionary proxy, HeaderDictionary downstream)\n    {\n        var baggage = string.Join(\", \", client.Baggage.Select(pair => $\"{pair.Key}={pair.Value}\"));\n        Assert.Equal(baggage, proxy[Baggage]);\n        Assert.Equal(baggage, downstream[Baggage]);\n\n        if (idFormat == ActivityIdFormat.W3C)\n        {\n            Assert.True(ActivityContext.TryParse(proxy[TraceParent], proxy[TraceState], out var proxyContext));\n            Assert.True(ActivityContext.TryParse(downstream[TraceParent], downstream[TraceState], out var downstreamContext));\n            Assert.Equal(client.TraceStateString, proxyContext.TraceState);\n            Assert.Equal(client.TraceStateString, downstreamContext.TraceState);\n            var proxyTraceId = proxyContext.TraceId.ToHexString();\n            var proxySpanId = proxyContext.SpanId.ToHexString();\n            var downstreamTraceId = downstreamContext.TraceId.ToHexString();\n            var downstreamSpanId = downstreamContext.SpanId.ToHexString();\n            Assert.Equal(client.TraceId.ToHexString(), proxyTraceId);\n            Assert.Equal(client.TraceId.ToHexString(), downstreamTraceId);\n            Assert.NotEqual(proxySpanId, downstreamSpanId);\n        }\n        else\n        {\n            var proxyId = proxy[RequestId].ToString();\n            var downstreamId = downstream[RequestId].ToString();\n            Assert.StartsWith(client.Id, proxyId);\n            Assert.StartsWith(proxyId, downstreamId);\n            Assert.NotEqual(proxyId, downstreamId);\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/Expect100ContinueTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Runtime.InteropServices;\nusing System.Text;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Primitives;\nusing Microsoft.Net.Http.Headers;\nusing Xunit;\nusing Yarp.ReverseProxy.Common;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy;\n\npublic class Expect100ContinueTests\n{\n    // HTTP/2 over TLS is not supported on macOS due to missing ALPN support.\n    // See https://github.com/dotnet/runtime/issues/27727\n    public static bool Http2OverTlsSupported => !RuntimeInformation.IsOSPlatform(OSPlatform.OSX);\n\n    [Theory(Skip = \"Condition not met\", SkipUnless = nameof(Http2OverTlsSupported))]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, true, 200)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, false, 200)]\n    [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, true, 200)]\n    [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, false, 200)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, true, 400)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, false, 400)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, true, 400)]\n    [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, true, 400)]\n    public async Task PostExpect100_BodyNotUploadedIfFailed(HttpProtocols proxyProtocol, HttpProtocols destProtocol, bool useContentLength, int destResponseCode)\n    {\n        var headerTcs = new TaskCompletionSource<StringValues>(TaskCreationOptions.RunContinuationsAsynchronously);\n        var bodyTcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var contentString = new string('a', 1024 * 1024 * 10);\n        var test = new TestEnvironment(\n            async context =>\n            {\n                if ((context.Request.Protocol == \"HTTP/1.1\" && destProtocol != HttpProtocols.Http1)\n                    || (context.Request.Protocol == \"HTTP/2.0\" && destProtocol != HttpProtocols.Http2))\n                {\n                    headerTcs.SetException(new Exception($\"Unexpected request protocol {context.Request.Protocol}\"));\n                    return;\n                }\n                else if (context.Request.Headers.TryGetValue(HeaderNames.Expect, out var expectHeader))\n                {\n                    headerTcs.SetResult(expectHeader);\n                }\n                else\n                {\n                    headerTcs.SetException(new Exception(\"Missing 'Expect' header in request\"));\n                    return;\n                }\n\n                if (destResponseCode == 200)\n                {\n                    // 100 response code is sent automatically on reading Body.\n                    await ReadContent(context, bodyTcs, Encoding.UTF8.GetByteCount(contentString));\n                }\n\n                context.Response.StatusCode = destResponseCode;\n            })\n        {\n            ProxyProtocol = proxyProtocol,\n            UseHttpsOnDestination = true,\n            UseHttpsOnProxy = true,\n            ConfigTransformer = (c, r) =>\n            {\n                c = c with\n                {\n                    HttpRequest = new ForwarderRequestConfig\n                    {\n                        Version = destProtocol == HttpProtocols.Http2 ? HttpVersion.Version20 : HttpVersion.Version11,\n                    }\n                };\n                return (c, r);\n            },\n            ConfigureProxy = proxyBuilder =>\n            {\n                proxyBuilder.Services.AddSingleton<IForwarderHttpClientFactory, TestForwarderHttpClientFactory>();\n            },\n        };\n\n        await test.Invoke(async uri =>\n        {\n            await ProcessHttpRequest(new Uri(uri), proxyProtocol, contentString, useContentLength, destResponseCode, false, destResponseCode == 200);\n\n            Assert.True(headerTcs.Task.IsCompleted);\n            var expectHeader = await headerTcs.Task;\n            var expectValue = Assert.Single(expectHeader);\n            Assert.Equal(\"100-continue\", expectValue);\n\n            if (destResponseCode == 200)\n            {\n                Assert.True(bodyTcs.Task.IsCompleted);\n                var actualString = await bodyTcs.Task;\n                Assert.Equal(contentString, actualString);\n            }\n            else\n            {\n                Assert.False(bodyTcs.Task.IsCompleted);\n            }\n        });\n    }\n\n    [Theory(Skip = \"Condition not met\", SkipUnless = nameof(Http2OverTlsSupported))]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, true, true, 200)]\n    [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, true, true, 200)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, true, true, 200)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, false, false, 400)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, false, true, 400)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, true, false, 400)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, true, true, 400)]\n    [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, false, false, 400)]\n    [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, false, true, 400)]\n    [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, true, false, 400)]\n    [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, true, true, 400)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, false, false, 400)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, false, true, 400)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, true, false, 400)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, true, true, 400)]\n    public async Task PostExpect100_ResponseWithPayload(HttpProtocols proxyProtocol, HttpProtocols destProtocol, bool useContentLength, bool cancelResponse, int responseCode)\n    {\n        var requestBodyTcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var contentString = new string('a', 1024 * 1024 * 10);\n        var test = new TestEnvironment(\n            async context =>\n            {\n                await ReadContent(context, requestBodyTcs, Encoding.UTF8.GetByteCount(contentString));\n\n                context.Response.StatusCode = responseCode;\n\n                var responseBody = Encoding.UTF8.GetBytes(contentString + \"Response\");\n                if (useContentLength)\n                {\n                    context.Response.Headers.ContentLength = responseBody.Length;\n                }\n\n                if (cancelResponse)\n                {\n                    await context.Response.BodyWriter.WriteAsync(responseBody.AsMemory(0, responseBody.Length / 2));\n                    context.Abort();\n                }\n                else\n                {\n                    await context.Response.Body.WriteAsync(responseBody.AsMemory());\n                }\n            })\n        {\n            ProxyProtocol = proxyProtocol,\n            UseHttpsOnDestination = true,\n            UseHttpsOnProxy = true,\n            ConfigTransformer = (c, r) =>\n            {\n                c = c with\n                {\n                    HttpRequest = new ForwarderRequestConfig\n                    {\n                        Version = destProtocol == HttpProtocols.Http2 ? HttpVersion.Version20 : HttpVersion.Version11,\n                    }\n                };\n                return (c, r);\n            },\n            ConfigureProxy = proxyBuilder =>\n            {\n                proxyBuilder.Services.AddSingleton<IForwarderHttpClientFactory, TestForwarderHttpClientFactory>();\n            },\n        };\n\n        await test.Invoke(async uri =>\n        {\n            await ProcessHttpRequest(new Uri(uri), proxyProtocol, contentString, useContentLength, responseCode, cancelResponse, true, async response =>\n            {\n                Assert.Equal(responseCode, (int)response.StatusCode);\n\n                var actualResponse = await response.Content.ReadAsStringAsync();\n                Assert.Equal(contentString + \"Response\", actualResponse);\n            });\n        });\n    }\n\n    [Theory(Skip = \"Condition not met\", SkipUnless = nameof(Http2OverTlsSupported))]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, false)]\n    [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, false)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, false)]\n    [InlineData(HttpProtocols.Http2, HttpProtocols.Http1, false)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http1, true)]\n    [InlineData(HttpProtocols.Http2, HttpProtocols.Http2, true)]\n    [InlineData(HttpProtocols.Http1, HttpProtocols.Http2, true)]\n    [InlineData(HttpProtocols.Http2, HttpProtocols.Http1, true)]\n    public async Task PostExpect100_SkipRequestBodyWithUnsuccessfulResponseCode(HttpProtocols proxyProtocol, HttpProtocols destProtocol, bool useContentLength)\n    {\n        var requestBodyTcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var contentString = new string('a', 1024 * 1024 * 10);\n        var test = new TestEnvironment(\n            async context =>\n            {\n                context.Response.StatusCode = 400;\n\n                var responseBody = Encoding.UTF8.GetBytes(contentString + \"Response\");\n                if (useContentLength)\n                {\n                    context.Response.Headers.ContentLength = responseBody.Length;\n                }\n\n                await context.Response.Body.WriteAsync(responseBody.AsMemory());\n            })\n        {\n            ProxyProtocol = proxyProtocol,\n            UseHttpsOnDestination = true,\n            UseHttpsOnProxy = true,\n            ConfigTransformer = (c, r) =>\n            {\n                c = c with\n                {\n                    HttpRequest = new ForwarderRequestConfig\n                    {\n                        Version = destProtocol == HttpProtocols.Http2 ? HttpVersion.Version20 : HttpVersion.Version11,\n                    }\n                };\n                return (c, r);\n            },\n            ConfigureProxy = proxyBuilder =>\n            {\n                proxyBuilder.Services.AddSingleton<IForwarderHttpClientFactory, TestForwarderHttpClientFactory>();\n            },\n        };\n\n        await test.Invoke(async uri =>\n        {\n            await ProcessHttpRequest(new Uri(uri), proxyProtocol, contentString, useContentLength, 400, cancelResponse: false, contentRead: false, async response =>\n            {\n                Assert.Equal(400, (int)response.StatusCode);\n\n                var actualResponse = await response.Content.ReadAsStringAsync();\n                Assert.Equal(contentString + \"Response\", actualResponse);\n            });\n        });\n    }\n\n    private static async Task ReadContent(Microsoft.AspNetCore.Http.HttpContext context, TaskCompletionSource<string> bodyTcs, int byteCount)\n    {\n        try\n        {\n            var buffer = new byte[byteCount];\n            var readCount = 0;\n            var totalReadCount = 0;\n            do\n            {\n                readCount = await context.Request.Body.ReadAsync(buffer, totalReadCount, buffer.Length - totalReadCount);\n                totalReadCount += readCount;\n            } while (readCount != 0);\n\n            var actualString = Encoding.UTF8.GetString(buffer);\n            bodyTcs.SetResult(actualString);\n        }\n        catch (Exception e)\n        {\n            bodyTcs.SetException(e);\n        }\n    }\n\n    private async Task ProcessHttpRequest(\n        Uri proxyHostUri,\n        HttpProtocols protocol,\n        string contentString,\n        bool useContentLength,\n        int expectedCode,\n        bool cancelResponse,\n        bool contentRead,\n        Func<HttpResponseMessage, Task> responseAction = null)\n    {\n        using var handler = new SocketsHttpHandler() { Expect100ContinueTimeout = TimeSpan.FromSeconds(60) };\n        handler.UseProxy = false;\n        handler.AllowAutoRedirect = false;\n        handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };\n        using var client = new HttpClient(handler);\n        using var message = new HttpRequestMessage(HttpMethod.Post, proxyHostUri);\n        message.Version = protocol == HttpProtocols.Http2 ? HttpVersion.Version20 : HttpVersion.Version11;\n        message.Headers.ExpectContinue = true;\n\n        var content = Encoding.UTF8.GetBytes(contentString);\n        using var contentStream = new MemoryStream(content);\n        message.Content = new StreamContent(contentStream);\n        if (useContentLength)\n        {\n            message.Content.Headers.ContentLength = content.Length;\n        }\n        else\n        {\n            message.Headers.TransferEncodingChunked = true;\n        }\n\n        if (!cancelResponse)\n        {\n            using var response = await client.SendAsync(message);\n\n            Assert.Equal(expectedCode, (int)response.StatusCode);\n            if (contentRead)\n            {\n                Assert.Equal(content.Length, contentStream.Position);\n            }\n            else\n            {\n                Assert.Equal(0, contentStream.Position);\n            }\n\n            if (responseAction is not null)\n            {\n                await responseAction(response);\n            }\n        }\n        else\n        {\n            var exception = await Assert.ThrowsAsync<HttpRequestException>(() => client.SendAsync(message));\n            Assert.IsAssignableFrom<IOException>(exception.InnerException);\n            Assert.Equal(content.Length, contentStream.Position);\n        }\n    }\n\n    private class TestForwarderHttpClientFactory : ForwarderHttpClientFactory\n    {\n        protected override void ConfigureHandler(ForwarderHttpClientContext context, SocketsHttpHandler handler)\n        {\n            base.ConfigureHandler(context, handler);\n            handler.Expect100ContinueTimeout = TimeSpan.FromSeconds(60);\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/HeaderTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Sockets;\nusing System.Text;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Microsoft.Extensions.Primitives;\nusing Microsoft.Net.Http.Headers;\nusing Xunit;\nusing Yarp.ReverseProxy.Common;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy;\n\npublic class HeaderTests\n{\n    [Fact]\n    public async Task ProxyAsync_EmptyRequestHeader_Proxied()\n    {\n        var refererReceived = new TaskCompletionSource<StringValues>(TaskCreationOptions.RunContinuationsAsynchronously);\n        var customReceived = new TaskCompletionSource<StringValues>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        IForwarderErrorFeature proxyError = null;\n        Exception unhandledError = null;\n\n        var test = new TestEnvironment(\n            context =>\n            {\n                if (context.Request.Headers.TryGetValue(HeaderNames.Referer, out var header))\n                {\n                    refererReceived.SetResult(header);\n                }\n                else\n                {\n                    refererReceived.SetException(new Exception($\"Missing '{HeaderNames.Referer}' header in request\"));\n                }\n\n                if (context.Request.Headers.TryGetValue(\"custom\", out header))\n                {\n                    customReceived.SetResult(header);\n                }\n                else\n                {\n                    customReceived.SetException(new Exception($\"Missing 'custom' header in request\"));\n                }\n\n                return Task.CompletedTask;\n            })\n        {\n            ProxyProtocol = HttpProtocols.Http1,\n            ConfigureProxyApp = proxyApp =>\n            {\n                proxyApp.Use(async (context, next) =>\n                {\n                    try\n                    {\n                        Assert.True(context.Request.Headers.TryGetValue(HeaderNames.Referer, out var header));\n                        var value = Assert.Single(header);\n                        Assert.True(StringValues.IsNullOrEmpty(value));\n\n                        Assert.True(context.Request.Headers.TryGetValue(\"custom\", out header));\n                        value = Assert.Single(header);\n                        Assert.True(StringValues.IsNullOrEmpty(value));\n\n                        await next();\n                        proxyError = context.Features.Get<IForwarderErrorFeature>();\n                    }\n                    catch (Exception ex)\n                    {\n                        unhandledError = ex;\n                        throw;\n                    }\n                });\n            },\n        };\n\n        await test.Invoke(async proxyUri =>\n        {\n            var proxyHostUri = new Uri(proxyUri);\n\n            using var tcpClient = new TcpClient(proxyHostUri.Host, proxyHostUri.Port);\n            await using var stream = tcpClient.GetStream();\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"GET / HTTP/1.1\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Host: {proxyHostUri.Host}:{proxyHostUri.Port}\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Content-Length: 0\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Connection: close\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Referer: \\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"custom: \\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"\\r\\n\"));\n            var buffer = new byte[4096];\n            var responseBuilder = new StringBuilder();\n            while (true)\n            {\n                var count = await stream.ReadAsync(buffer);\n                if (count == 0)\n                {\n                    break;\n                }\n                responseBuilder.Append(Encoding.ASCII.GetString(buffer, 0, count));\n            }\n            var response = responseBuilder.ToString();\n\n            Assert.Null(proxyError);\n            Assert.Null(unhandledError);\n\n            Assert.StartsWith(\"HTTP/1.1 200 OK\", response);\n\n            Assert.True(refererReceived.Task.IsCompleted);\n            var refererHeader = await refererReceived.Task;\n            var referer = Assert.Single(refererHeader);\n            Assert.True(StringValues.IsNullOrEmpty(referer));\n\n            Assert.True(customReceived.Task.IsCompleted);\n            var customHeader = await customReceived.Task;\n            var custom = Assert.Single(customHeader);\n            Assert.True(StringValues.IsNullOrEmpty(custom));\n        });\n    }\n\n    [Fact]\n    public async Task ProxyAsync_EmptyResponseHeader_Proxied()\n    {\n        IForwarderErrorFeature proxyError = null;\n        Exception unhandledError = null;\n\n        var test = new TestEnvironment(\n            context =>\n            {\n                context.Response.Headers[HeaderNames.WWWAuthenticate] = \"\";\n                context.Response.Headers[\"custom\"] = \"\";\n                return Task.CompletedTask;\n            })\n        {\n            ProxyProtocol = HttpProtocols.Http1,\n            ConfigureProxyApp = proxyApp =>\n            {\n                proxyApp.Use(async (context, next) =>\n                {\n                    try\n                    {\n                        await next();\n\n                        Assert.True(context.Response.Headers.TryGetValue(HeaderNames.WWWAuthenticate, out var header));\n                        var value = Assert.Single(header);\n                        Assert.True(StringValues.IsNullOrEmpty(value));\n\n                        Assert.True(context.Response.Headers.TryGetValue(\"custom\", out header));\n                        value = Assert.Single(header);\n                        Assert.True(StringValues.IsNullOrEmpty(value));\n\n                        proxyError = context.Features.Get<IForwarderErrorFeature>();\n                    }\n                    catch (Exception ex)\n                    {\n                        unhandledError = ex;\n                        throw;\n                    }\n                });\n            },\n        };\n\n        await test.Invoke(async proxyUri =>\n        {\n            var proxyHostUri = new Uri(proxyUri);\n\n            using var tcpClient = new TcpClient(proxyHostUri.Host, proxyHostUri.Port);\n            await using var stream = tcpClient.GetStream();\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"GET / HTTP/1.1\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Host: {proxyHostUri.Host}:{proxyHostUri.Port}\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Content-Length: 0\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Connection: close\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"\\r\\n\"));\n            var buffer = new byte[4096];\n            var responseBuilder = new StringBuilder();\n            while (true)\n            {\n                var count = await stream.ReadAsync(buffer);\n                if (count == 0)\n                {\n                    break;\n                }\n                responseBuilder.Append(Encoding.ASCII.GetString(buffer, 0, count));\n            }\n            var response = responseBuilder.ToString();\n\n            Assert.Null(proxyError);\n            Assert.Null(unhandledError);\n\n            var lines = response.Split(\"\\r\\n\");\n            Assert.Equal(\"HTTP/1.1 200 OK\", lines[0]);\n            // Order varies across versions.\n            // Assert.Equal(\"Content-Length: 0\", lines[1]);\n            // Assert.Equal(\"Connection: close\", lines[2]);\n            // Assert.StartsWith(\"Date: \", lines[3]);\n            // Assert.Equal(\"Server: Kestrel\", lines[4]);\n            Assert.Equal(\"WWW-Authenticate: \", lines[5]);\n            Assert.Equal(\"custom: \", lines[6]);\n            Assert.Equal(\"\", lines[7]);\n        });\n    }\n\n    [Theory]\n    [InlineData(\"http://www.ěščřžýáíé.com\", \"utf-8\")]\n    [InlineData(\"http://www.çáéôîèñøæ.com\", \"iso-8859-1\")]\n    public async Task ProxyAsync_RequestWithEncodedHeaderValue(string headerValue, string encodingName)\n    {\n        var encoding = Encoding.GetEncoding(encodingName);\n        var tcs = new TaskCompletionSource<StringValues>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        IForwarderErrorFeature proxyError = null;\n        Exception unhandledError = null;\n\n        var test = new TestEnvironment(\n            context =>\n            {\n                if (context.Request.Headers.TryGetValue(HeaderNames.Referer, out var header))\n                {\n                    tcs.SetResult(header);\n                }\n                else\n                {\n                    tcs.SetException(new Exception($\"Missing '{HeaderNames.Referer}' header in request\"));\n                }\n                return Task.CompletedTask;\n            })\n        {\n            ProxyProtocol = HttpProtocols.Http1,\n            HeaderEncoding = encoding,\n            ConfigureProxyApp = proxyApp =>\n            {\n                proxyApp.Use(async (context, next) =>\n                {\n                    try\n                    {\n                        Assert.True(context.Request.Headers.TryGetValue(HeaderNames.Referer, out var header));\n                        var value = Assert.Single(header);\n                        Assert.Equal(headerValue, value);\n\n                        await next();\n                        proxyError = context.Features.Get<IForwarderErrorFeature>();\n                    }\n                    catch (Exception ex)\n                    {\n                        unhandledError = ex;\n                        throw;\n                    }\n                });\n            },\n        };\n\n        await test.Invoke(async proxyUri =>\n        {\n            var proxyHostUri = new Uri(proxyUri);\n\n            using var tcpClient = new TcpClient(proxyHostUri.Host, proxyHostUri.Port);\n            await using var stream = tcpClient.GetStream();\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"GET / HTTP/1.1\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Host: {proxyHostUri.Host}:{proxyHostUri.Port}\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Content-Length: 0\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Connection: close\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Referer: \"));\n            await stream.WriteAsync(encoding.GetBytes(headerValue));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"\\r\\n\"));\n            var buffer = new byte[4096];\n            var responseBuilder = new StringBuilder();\n            while (true)\n            {\n                var count = await stream.ReadAsync(buffer);\n                if (count == 0)\n                {\n                    break;\n                }\n                responseBuilder.Append(Encoding.ASCII.GetString(buffer, 0, count));\n            }\n            var response = responseBuilder.ToString();\n\n            Assert.Null(proxyError);\n            Assert.Null(unhandledError);\n\n            Assert.StartsWith(\"HTTP/1.1 200 OK\", response);\n\n            Assert.True(tcs.Task.IsCompleted);\n            var refererHeader = await tcs.Task;\n            var referer = Assert.Single(refererHeader);\n            Assert.Equal(headerValue, referer);\n        });\n    }\n\n    [Theory]\n    [InlineData(\"http://www.ěščřžýáíé.com\", \"utf-8\")]\n    [InlineData(\"http://www.çáéôîèñøæ.com\", \"iso-8859-1\")]\n    public async Task ProxyAsync_ResponseWithEncodedHeaderValue(string headerValue, string encodingName)\n    {\n        var encoding = Encoding.GetEncoding(encodingName);\n\n        var tcpListener = new TcpListener(IPAddress.Loopback, 0);\n        tcpListener.Start();\n        var destinationTask = Task.Run(async () =>\n        {\n            using var tcpClient = await tcpListener.AcceptTcpClientAsync();\n            await using var stream = tcpClient.GetStream();\n            var buffer = new byte[4096];\n            var requestBuilder = new StringBuilder();\n            while (true)\n            {\n                var count = await stream.ReadAsync(buffer);\n                if (count == 0)\n                {\n                    break;\n                }\n\n                requestBuilder.Append(encoding.GetString(buffer, 0, count));\n\n                // End of the request\n                if (requestBuilder.Length >= 4 &&\n                    requestBuilder[^4] == '\\r' && requestBuilder[^3] == '\\n' &&\n                    requestBuilder[^2] == '\\r' && requestBuilder[^1] == '\\n')\n                {\n                    break;\n                }\n            }\n\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"HTTP/1.1 200 OK\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Content-Length: 0\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Connection: close\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Test-Extra: pingu\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Location: \"));\n            await stream.WriteAsync(encoding.GetBytes(headerValue));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"\\r\\n\"));\n        });\n\n        IForwarderErrorFeature proxyError = null;\n        Exception unhandledError = null;\n\n        var proxyTest = new TestEnvironment()\n        {\n            ProxyProtocol = HttpProtocols.Http1,\n            HeaderEncoding = encoding,\n            ClusterId = \"cluster1\",\n            ConfigureProxyApp = proxyApp =>\n            {\n                proxyApp.Use(async (context, next) =>\n                {\n                    try\n                    {\n                        await next();\n                        proxyError = context.Features.Get<IForwarderErrorFeature>();\n                    }\n                    catch (Exception ex)\n                    {\n                        unhandledError = ex;\n                        throw;\n                    }\n                });\n            },\n        };\n\n        using var proxy = proxyTest.CreateProxy($\"http://{tcpListener.LocalEndpoint}\");\n\n        await proxy.StartAsync();\n\n        try\n        {\n            using var httpClient = new HttpClient();\n            using var response = await httpClient.GetAsync(proxy.GetAddress());\n\n            Assert.NotNull(proxyError);\n            Assert.Equal(ForwarderError.ResponseHeaders, proxyError.Error);\n            var ioe = Assert.IsType<InvalidOperationException>(proxyError.Exception);\n            Assert.StartsWith(\"Invalid non-ASCII or control character in header: \", ioe.Message);\n            Assert.Null(unhandledError);\n\n            Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode);\n\n            Assert.False(response.Headers.TryGetValues(HeaderNames.Location, out _));\n            Assert.False(response.Headers.TryGetValues(\"Test-Extra\", out _));\n\n            await destinationTask;\n        }\n        finally\n        {\n            await proxy.StopAsync();\n            tcpListener.Stop();\n        }\n    }\n\n    [Fact]\n    public async Task ContentLengthAndTransferEncoding_ContentLengthRemoved()\n    {\n        var proxyTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n        var appTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var test = new TestEnvironment(\n            context =>\n            {\n                try\n                {\n                    Assert.Null(context.Request.ContentLength);\n                    Assert.Equal(\"chunked\", context.Request.Headers[HeaderNames.TransferEncoding]);\n                    appTcs.SetResult(0);\n                }\n                catch (Exception ex)\n                {\n                    appTcs.SetException(ex);\n                }\n                return Task.CompletedTask;\n            })\n        {\n            ProxyProtocol = HttpProtocols.Http1,\n            ConfigureProxyApp = proxyApp =>\n            {\n                proxyApp.Use(async (context, next) =>\n                {\n                    try\n                    {\n                        // Removed by the server\n                        Assert.Null(context.Request.ContentLength);\n                        // Set it just to make sure YARP removes it\n                        context.Request.ContentLength = 11;\n                        Assert.Equal(\"chunked\", context.Request.Headers[HeaderNames.TransferEncoding]);\n                        proxyTcs.SetResult(0);\n                    }\n                    catch (Exception ex)\n                    {\n                        proxyTcs.SetException(ex);\n                    }\n\n                    await next();\n                });\n            },\n        };\n\n        await test.Invoke(async proxyUri =>\n        {\n            var proxyHostUri = new Uri(proxyUri);\n\n            using var tcpClient = new TcpClient(proxyHostUri.Host, proxyHostUri.Port);\n            await using var stream = tcpClient.GetStream();\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"GET / HTTP/1.1\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Host: {proxyHostUri.Host}:{proxyHostUri.Port}\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Content-Length: 11\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Transfer-Encoding: chunked\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Connection: close\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"b\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Hello World\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"0\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"\\r\\n\"));\n            var buffer = new byte[4096];\n            var responseBuilder = new StringBuilder();\n            while (true)\n            {\n                var count = await stream.ReadAsync(buffer);\n                if (count == 0)\n                {\n                    break;\n                }\n                responseBuilder.Append(Encoding.ASCII.GetString(buffer, 0, count));\n            }\n            var response = responseBuilder.ToString();\n\n            await proxyTcs.Task;\n            await appTcs.Task;\n\n            Assert.StartsWith(\"HTTP/1.1 200 OK\", response);\n        });\n    }\n\n    [Theory]\n    [MemberData(nameof(RequestMultiHeadersData))]\n    public async Task MultiValueRequestHeaders(string headerName, string[] values, string expectedValues)\n    {\n        var proxyTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n        var appTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var test = new TestEnvironment(\n            context =>\n            {\n                try\n                {\n                    Assert.True(context.Request.Headers.TryGetValue(headerName, out var headerValues));\n                    Assert.Single(headerValues);\n                    Assert.Equal(expectedValues, headerValues);\n                    appTcs.SetResult(0);\n                }\n                catch (Exception ex)\n                {\n                    appTcs.SetException(ex);\n                }\n                return Task.CompletedTask;\n            })\n        {\n            ProxyProtocol = HttpProtocols.Http1,\n            ConfigureProxyApp = proxyApp =>\n            {\n                proxyApp.Use(async (context, next) =>\n                {\n                    try\n                    {\n                        Assert.True(context.Request.Headers.TryGetValue(headerName, out var headerValues));\n                        Assert.Equal(values.Length, headerValues.Count);\n                        for (var i = 0; i < values.Length; ++i)\n                        {\n                            Assert.Equal(values[i], headerValues[i]);\n                        }\n                        proxyTcs.SetResult(0);\n                    }\n                    catch (Exception ex)\n                    {\n                        proxyTcs.SetException(ex);\n                    }\n\n                    await next();\n                });\n            },\n        };\n\n        await test.Invoke(async proxyUri =>\n        {\n            var proxyHostUri = new Uri(proxyUri);\n\n            using var tcpClient = new TcpClient(proxyHostUri.Host, proxyHostUri.Port);\n            await using var stream = tcpClient.GetStream();\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"GET / HTTP/1.1\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Host: {proxyHostUri.Host}:{proxyHostUri.Port}\\r\\n\"));\n\n            foreach (var value in values)\n            {\n                await stream.WriteAsync(Encoding.ASCII.GetBytes($\"{headerName}: {value}\\r\\n\"));\n            }\n\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Connection: close\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"\\r\\n\"));\n            var response = await new StreamReader(stream).ReadToEndAsync();\n\n            await proxyTcs.Task;\n            await appTcs.Task;\n\n            Assert.StartsWith(\"HTTP/1.1 200 OK\", response);\n        });\n    }\n    public static IEnumerable<string> RequestMultiHeaderNames()\n    {\n        var headers = new[]\n        {\n            HeaderNames.Accept,\n            HeaderNames.AcceptCharset,\n            HeaderNames.AcceptEncoding,\n            HeaderNames.AcceptLanguage,\n            HeaderNames.Via\n        };\n\n        foreach (var header in headers)\n        {\n            yield return header;\n        }\n    }\n\n    public static IEnumerable<string[]> MultiValues()\n    {\n        var values = new string[][] {\n            new[] { \"testA=A_Value\", \"testB=B_Value\", \"testC=C_Value\" },\n            new[] { \"testA=A_Value, testB=B_Value\", \"testC=C_Value\" },\n            new[] { \"testA=A_Value, testB=B_Value, testC=C_Value\" },\n        };\n\n        foreach (var value in values)\n        {\n            yield return value;\n        }\n    }\n\n    public static IEnumerable<object[]> RequestMultiHeadersData()\n    {\n        foreach (var header in RequestMultiHeaderNames())\n        {\n            foreach (var value in MultiValues())\n            {\n                yield return new object[] { header, value, string.Join(\", \", value).TrimEnd() };\n            }\n        }\n\n        // Special separator \";\" for Cookie header\n        foreach (var value in MultiValues())\n        {\n            yield return new object[] { HeaderNames.Cookie, value, string.Join(\"; \", value).TrimEnd() };\n        }\n    }\n\n    public static IEnumerable<object[]> ResponseMultiHeadersData()\n    {\n        foreach (var header in ResponseMultiHeaderNames())\n        {\n            foreach (var value in MultiValues())\n            {\n                yield return new object[] { header, value, value };\n            }\n        }\n    }\n\n    public static IEnumerable<string> ResponseMultiHeaderNames()\n    {\n        var headers = new[]\n        {\n            HeaderNames.AcceptRanges,\n            HeaderNames.Allow,\n            HeaderNames.ContentEncoding,\n            HeaderNames.ContentLanguage,\n            HeaderNames.ContentRange,\n            HeaderNames.ContentType,\n            HeaderNames.SetCookie,\n            HeaderNames.Via,\n            HeaderNames.Warning,\n            HeaderNames.WWWAuthenticate\n        };\n\n        foreach (var header in headers)\n        {\n            yield return header;\n        }\n    }\n\n    [Theory]\n    [MemberData(nameof(ResponseMultiHeadersData))]\n    public async Task MultiValueResponseHeaders(string headerName, string[] values, string[] expectedValues)\n    {\n        IForwarderErrorFeature proxyError = null;\n        Exception unhandledError = null;\n\n        var test = new TestEnvironment(\n            context =>\n            {\n                Assert.True(context.Response.Headers.TryAdd(headerName, values));\n                return Task.CompletedTask;\n            })\n        {\n            ProxyProtocol = HttpProtocols.Http1,\n            ConfigureProxyApp = proxyApp =>\n            {\n                proxyApp.Use(async (context, next) =>\n                {\n                    try\n                    {\n                        await next();\n\n                        Assert.True(context.Response.Headers.TryGetValue(headerName, out var header));\n                        Assert.Equal(values.Length, header.Count);\n                        for (var i = 0; i < values.Length; ++i)\n                        {\n                            Assert.Equal(values[i], header[i]);\n                        }\n\n                        proxyError = context.Features.Get<IForwarderErrorFeature>();\n                    }\n                    catch (Exception ex)\n                    {\n                        unhandledError = ex;\n                        throw;\n                    }\n                });\n            },\n        };\n\n        await test.Invoke(async proxyUri =>\n        {\n            var proxyHostUri = new Uri(proxyUri);\n\n            using var tcpClient = new TcpClient(proxyHostUri.Host, proxyHostUri.Port);\n            await using var stream = tcpClient.GetStream();\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"GET / HTTP/1.1\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Host: {proxyHostUri.Host}:{proxyHostUri.Port}\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Content-Length: 0\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes($\"Connection: close\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"\\r\\n\"));\n            await stream.WriteAsync(Encoding.ASCII.GetBytes(\"\\r\\n\"));\n            var response = await new StreamReader(stream).ReadToEndAsync();\n\n            Assert.Null(proxyError);\n            Assert.Null(unhandledError);\n\n            var lines = response.Split(\"\\r\\n\");\n            Assert.Equal(\"HTTP/1.1 200 OK\", lines[0]);\n            foreach (var expected in expectedValues)\n            {\n                Assert.Contains($\"{headerName}: {expected}\", lines);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/HttpForwarderCancellationTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Runtime.InteropServices;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http.Features;\nusing Xunit;\nusing Yarp.ReverseProxy.Common;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy;\n\npublic class HttpForwarderCancellationTests\n{\n    // HTTP/2 over TLS is not supported on macOS due to missing ALPN support.\n    // See https://github.com/dotnet/runtime/issues/27727\n    public static bool Http2OverTlsSupported => !RuntimeInformation.IsOSPlatform(OSPlatform.OSX);\n\n    [Fact(Skip = \"Condition not met\", SkipUnless = nameof(Http2OverTlsSupported))]\n    public async Task ServerSendsHttp2Reset_ReadToClientIsCanceled()\n    {\n        var readAsyncCalled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var test = new TestEnvironment(\n            async context =>\n            {\n                Assert.Equal(\"HTTP/2\", context.Request.Protocol);\n\n                await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(\"Hello\"));\n                await context.Response.CompleteAsync();\n\n                await readAsyncCalled.Task;\n\n                var resetFeature = context.Features.Get<IHttpResetFeature>();\n                Assert.NotNull(resetFeature);\n                resetFeature.Reset(0); // NO_ERROR\n            })\n        {\n            UseHttpsOnDestination = true,\n            UseHttpsOnProxy = true,\n            ConfigureProxyApp = proxyApp =>\n            {\n                proxyApp.Use(next => context =>\n                {\n                    context.Request.Body = new ReadDelegatingStream(context.Request.Body, async (memory, cancellation) =>\n                    {\n                        Assert.False(cancellation.IsCancellationRequested);\n                        readAsyncCalled.SetResult();\n\n                        var startTime = DateTime.UtcNow;\n                        while (DateTime.UtcNow.Subtract(startTime) < TimeSpan.FromSeconds(10))\n                        {\n                            cancellation.ThrowIfCancellationRequested();\n                            await Task.Delay(10, cancellation);\n                        }\n\n                        throw new InvalidOperationException(\"Cancellation was not requested\");\n                    });\n\n                    return next(context);\n                });\n            },\n        };\n\n        await test.Invoke(async uri =>\n        {\n            var content = new InfiniteHttpContent();\n\n            var request = new HttpRequestMessage(HttpMethod.Post, uri)\n            {\n                Version = HttpVersion.Version20,\n                Content = content\n            };\n\n            using var client = new HttpClient(new HttpClientHandler\n            {\n                ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator\n            });\n\n            using var response = await client.SendAsync(request);\n\n            response.EnsureSuccessStatusCode();\n\n            var responseString = await response.Content.ReadAsStringAsync();\n            Assert.Equal(\"Hello\", responseString);\n\n            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => content.Completion.Task);\n        });\n    }\n\n    private sealed class InfiniteHttpContent : HttpContent\n    {\n        public TaskCompletionSource Completion { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)\n        {\n            throw new NotImplementedException();\n        }\n\n        protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken)\n        {\n            var buffer = new byte[1024];\n            new Random(42).NextBytes(buffer);\n\n            while (true)\n            {\n                try\n                {\n                    await stream.WriteAsync(buffer, cancellationToken);\n                }\n                catch (Exception ex)\n                {\n                    Completion.SetException(ex);\n                    return;\n                }\n            }\n        }\n\n        protected override bool TryComputeLength(out long length)\n        {\n            length = -1;\n            return false;\n        }\n    }\n\n    private sealed class ReadDelegatingStream : DelegatingStream\n    {\n        private readonly Func<Memory<byte>, CancellationToken, ValueTask<int>> _readAsync;\n\n        public ReadDelegatingStream(Stream stream, Func<Memory<byte>, CancellationToken, ValueTask<int>> readAsync)\n            : base(stream)\n        {\n            _readAsync = readAsync;\n        }\n\n        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            return _readAsync(buffer, cancellationToken);\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/HttpProxyCookieTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Microsoft.Extensions.Primitives;\nusing Microsoft.Net.Http.Headers;\nusing Xunit;\nusing Yarp.ReverseProxy.Common;\n\nnamespace Yarp.ReverseProxy;\n\npublic abstract class HttpProxyCookieTests\n{\n    public const string CookieAKey = \"testA\";\n    public const string CookieAValue = \"A_Cookie\";\n    public const string CookieBKey = \"testB\";\n    public const string CookieBValue = \"B_Cookie\";\n\n    public static readonly string CookieA = $\"{CookieAKey}={CookieAValue}\";\n    public static readonly string CookieB = $\"{CookieBKey}={CookieBValue}\";\n    public static readonly string Cookies = $\"{CookieA}; {CookieB}\";\n\n    public abstract HttpProtocols HttpProtocol { get; }\n    public abstract Task ProcessHttpRequest(Uri proxyHostUri);\n\n    [Fact]\n    public async Task ProxyAsync_RequestWithCookieHeaders()\n    {\n        var tcs = new TaskCompletionSource<StringValues>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var test = new TestEnvironment(\n            context =>\n            {\n                if (context.Request.Headers.TryGetValue(HeaderNames.Cookie, out var cookieHeaders))\n                {\n                    tcs.SetResult(cookieHeaders);\n                }\n                else\n                {\n                    tcs.SetException(new Exception(\"Missing 'Cookie' header in request\"));\n                }\n                return Task.CompletedTask;\n            })\n        {\n            ProxyProtocol = HttpProtocol,\n            ConfigureProxyApp = proxyApp =>\n            {\n                proxyApp.UseMiddleware<CheckCookieHeaderMiddleware>();\n            },\n        };\n\n        await test.Invoke(async uri =>\n        {\n            await ProcessHttpRequest(new Uri(uri));\n\n            Assert.True(tcs.Task.IsCompleted);\n            var cookieHeaders = await tcs.Task;\n            var cookies = Assert.Single(cookieHeaders);\n            Assert.Equal(Cookies, cookies);\n        });\n    }\n\n    private class CheckCookieHeaderMiddleware\n    {\n        private readonly RequestDelegate _next;\n\n        public CheckCookieHeaderMiddleware(RequestDelegate next)\n        {\n            _next = next;\n        }\n\n        public async Task Invoke(HttpContext context)\n        {\n            // Ensure that CookieA is the first and CookieB the last.\n            Assert.True(context.Request.Headers.TryGetValue(HeaderNames.Cookie, out var headerValues));\n\n            if (context.Request.Protocol is \"HTTP/1.1\" or \"HTTP/2\")\n            {\n                Assert.Single(headerValues);\n                Assert.Equal(Cookies, headerValues);\n            }\n            else\n            {\n                Assert.Fail($\"Unexpected HTTP protocol '{context.Request.Protocol}'\");\n            }\n\n            await _next.Invoke(context);\n        }\n    }\n}\n\npublic class HttpProxyCookieTests_Http1 : HttpProxyCookieTests\n{\n    public override HttpProtocols HttpProtocol => HttpProtocols.Http1;\n\n    public override async Task ProcessHttpRequest(Uri proxyHostUri)\n    {\n        using var client = new HttpClient();\n        using var message = new HttpRequestMessage(HttpMethod.Get, proxyHostUri);\n        message.Headers.Add(HeaderNames.Cookie, Cookies);\n        using var response = await client.SendAsync(message);\n        response.EnsureSuccessStatusCode();\n    }\n}\n\npublic class HttpProxyCookieTests_Http2 : HttpProxyCookieTests\n{\n    public override HttpProtocols HttpProtocol => HttpProtocols.Http2;\n\n    // HttpClient for H/2 will use different header frames for cookies from a container and message headers.\n    // It will first send message header cookie and than the container one and we expect them in the order of cookieA;cookieB.\n    public override async Task ProcessHttpRequest(Uri proxyHostUri)\n    {\n        using var handler = new HttpClientHandler();\n        handler.CookieContainer.Add(new System.Net.Cookie(CookieBKey, CookieBValue, path: \"/\", domain: proxyHostUri.Host));\n        using var client = new HttpClient(handler);\n        using var message = new HttpRequestMessage(HttpMethod.Get, proxyHostUri);\n        message.Version = HttpVersion.Version20;\n        message.VersionPolicy = HttpVersionPolicy.RequestVersionExact;\n        message.Headers.Add(HeaderNames.Cookie, CookieA);\n        using var response = await client.SendAsync(message);\n        response.EnsureSuccessStatusCode();\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/HttpSysDelegationTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Runtime.CompilerServices;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Xunit;\nusing Yarp.ReverseProxy.Common;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Delegation;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy;\n\npublic partial class HttpSysDelegationTests\n{\n    [HttpSysDelegationFact]\n    public async Task RequestDelegated()\n    {\n        IHttpSysDelegator delegator = null;\n        IForwarderErrorFeature proxyError = null;\n        Exception unhandledError = null;\n        var expectedRepsone = \"Hello World!\";\n        var queueName = nameof(HttpSysDelegationTests) + Random.Shared.Next().ToString(\"x8\");\n        string urlPrefix = null;\n\n        var test = new HttpSysTestEnvironment(\n            destinationServices => { },\n            destinationHttpSysOptions => destinationHttpSysOptions.RequestQueueName = queueName,\n            destinationApp => destinationApp.Run(context => context.Response.WriteAsync(expectedRepsone)),\n            proxyServices => { },\n            proxyBuilder => { },\n            proxyApp =>\n            {\n                delegator = proxyApp.ApplicationServices.GetRequiredService<IHttpSysDelegator>();\n                proxyApp.Use(async (context, next) =>\n                {\n                    try\n                    {\n                        await next();\n                        proxyError = context.Features.Get<IForwarderErrorFeature>();\n                    }\n                    catch (Exception ex)\n                    {\n                        unhandledError = ex;\n                        throw;\n                    }\n                });\n            },\n            proxyPipeline =>\n            {\n                proxyPipeline.UseHttpSysDelegation();\n            },\n            (cluster, route) =>\n            {\n                urlPrefix = cluster.Destinations.First().Value.Address;\n                var destination = new DestinationConfig()\n                {\n                    Address = urlPrefix,\n                    Metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n                    {\n                        { DelegationExtensions.HttpSysDelegationQueueMetadataKey, queueName },\n                    },\n                };\n\n                cluster = new ClusterConfig\n                {\n                    ClusterId = cluster.ClusterId,\n                    Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n                    {\n                        { \"destination1\",  destination },\n                    },\n                };\n\n                return (cluster, route);\n            });\n\n        await test.Invoke(async proxyUri =>\n        {\n            using var httpClient = new HttpClient();\n            var response = await httpClient.GetStringAsync(proxyUri);\n\n            Assert.Null(proxyError);\n            Assert.Null(unhandledError);\n            Assert.Equal(expectedRepsone, response);\n\n            Assert.NotNull(delegator);\n            delegator.ResetQueue(queueName, urlPrefix);\n\n            response = await httpClient.GetStringAsync(proxyUri);\n\n            Assert.Null(proxyError);\n            Assert.Null(unhandledError);\n            Assert.Equal(expectedRepsone, response);\n        });\n    }\n\n    private class HttpSysDelegationFactAttribute : FactAttribute\n    {\n        public HttpSysDelegationFactAttribute([CallerFilePath] string sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = -1)\n            : base(sourceFilePath, sourceLineNumber)\n        {\n            // Htty.sys delegation was added to Windows in the 21H2 release but back ported through RS5 (1809)\n            if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 1809))\n            {\n                Skip = \"Http.sys tests are only supported on Windows versions >= 10.0.1809\";\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/PassiveHealthCheckTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.DependencyInjection;\nusing Xunit;\nusing Yarp.ReverseProxy.Common;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Health;\n\nnamespace Yarp.ReverseProxy;\n\npublic class PassiveHealthCheckTests\n{\n    private sealed class MockHttpClientFactory : IForwarderHttpClientFactory\n    {\n        private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsync;\n\n        public MockHttpClientFactory(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsync)\n        {\n            _sendAsync = sendAsync;\n        }\n\n        public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context)\n        {\n            return new HttpMessageInvoker(new MockHandler(_sendAsync));\n        }\n\n        private sealed class MockHandler : HttpMessageHandler\n        {\n            private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsync;\n\n            public MockHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsync)\n            {\n                _sendAsync = sendAsync;\n            }\n\n            protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n            {\n                return await _sendAsync(request, cancellationToken);\n            }\n        }\n    }\n\n    [Fact]\n    public async Task PassiveHealthChecksEnabled_MultipleDestinationFailures_ProxyReturnsServiceUnavailable()\n    {\n        var destinationReached = false;\n        IProxyStateLookup lookup = null;\n        string clusterId = null;\n\n        var test = new TestEnvironment(\n            context =>\n            {\n                destinationReached = true;\n                throw new InvalidOperationException();\n            })\n        {\n            ConfigTransformer = (c, r) =>\n            {\n                c = c with\n                {\n                    HealthCheck = new HealthCheckConfig\n                    {\n                        AvailableDestinationsPolicy = HealthCheckConstants.AvailableDestinations.HealthyAndUnknown,\n                        Passive = new PassiveHealthCheckConfig\n                        {\n                            Enabled = true\n                        }\n                    }\n                };\n\n                clusterId = c.ClusterId;\n\n                return (c, r);\n            },\n            ConfigureProxy = proxyBuilder => proxyBuilder.Services.AddSingleton<IForwarderHttpClientFactory>(new MockHttpClientFactory((_, _) => throw new IOException())),\n            ConfigureProxyApp = proxyApp =>\n            {\n                lookup = proxyApp.ApplicationServices.GetRequiredService<IProxyStateLookup>();\n            },\n        };\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new HttpClient();\n\n            for (var i = 0; i < 10; i++)\n            {\n                using var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, uri));\n                Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode);\n            }\n\n            Assert.NotNull(lookup);\n            Assert.NotNull(clusterId);\n\n            // The destination list will be updated asynchronously in the background.\n            // Wait until that update takes effect.\n            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));\n\n            while (true)\n            {\n                Assert.True(lookup.TryGetCluster(clusterId, out var cluster));\n                Assert.Single(cluster.DestinationsState.AllDestinations);\n\n                if (cluster.DestinationsState.AvailableDestinations.Count == 0)\n                {\n                    break;\n                }\n\n                await Task.Delay(10, cts.Token);\n            }\n\n            for (var i = 0; i < 42; i++)\n            {\n                using var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, uri));\n                Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);\n            }\n        });\n\n        Assert.False(destinationReached);\n    }\n\n    [Fact]\n    public async Task PassiveHealthChecksEnabled_IncompleteClientRequests_ProxyHealthIsUnaffected()\n    {\n        var destinationReached = false;\n\n        var shouldThrow = true;\n        var requestStartedTcs = new TaskCompletionSource<byte>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var proxySendAsync = async (HttpRequestMessage request, CancellationToken ct) =>\n        {\n            requestStartedTcs.SetResult(0);\n\n            if (shouldThrow)\n            {\n                await Task.Delay(-1, ct);\n\n                throw new OperationCanceledException(ct);\n            }\n            else\n            {\n                return new HttpResponseMessage((HttpStatusCode)418)\n                {\n                    Content = new StringContent(\"Hello world\")\n                };\n            }\n        };\n\n        var test = new TestEnvironment(\n            context =>\n            {\n                destinationReached = true;\n                throw new InvalidOperationException();\n            })\n        {\n            ConfigTransformer = (c, r) =>\n            {\n                c = c with\n                {\n                    HealthCheck = new HealthCheckConfig\n                    {\n                        Passive = new PassiveHealthCheckConfig\n                        {\n                            Enabled = true\n                        }\n                    }\n                };\n\n                return (c, r);\n            },\n            ConfigureProxy = proxyBuilder => proxyBuilder.Services.AddSingleton<IForwarderHttpClientFactory>(new MockHttpClientFactory(proxySendAsync)),\n        };\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new HttpClient();\n            for (var i = 0; i < 42; i++)\n            {\n                using var cts = new CancellationTokenSource();\n                _ = requestStartedTcs.Task.ContinueWith(_ => cts.Cancel());\n\n                try\n                {\n                    await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, uri), cts.Token);\n                    Assert.True(false);\n                }\n                catch { }\n\n                requestStartedTcs = new TaskCompletionSource<byte>(TaskCreationOptions.RunContinuationsAsynchronously);\n            }\n\n            shouldThrow = false;\n\n            using var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, uri));\n\n            Assert.Equal(418, (int)response.StatusCode);\n            Assert.Equal(\"Hello world\", await response.Content.ReadAsStringAsync());\n        });\n\n        Assert.False(destinationReached);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/TelemetryConsumptionTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Sockets;\nusing System.Security.Authentication;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Xunit;\nusing Yarp.ReverseProxy.Common;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.Telemetry.Consumption;\n\nnamespace Yarp.ReverseProxy;\n\npublic class TelemetryConsumptionTests\n{\n    public enum RegistrationApproach\n    {\n        WithInstanceHelper,\n        WithGenericHelper,\n        Manual\n    }\n\n    private static void RegisterTelemetryConsumers(IServiceCollection services, RegistrationApproach approach)\n    {\n        if (approach == RegistrationApproach.WithInstanceHelper)\n        {\n            services.AddTelemetryConsumer(new TelemetryConsumer());\n            services.AddTelemetryConsumer(new SecondTelemetryConsumer());\n        }\n        else if (approach == RegistrationApproach.WithGenericHelper)\n        {\n            services.AddTelemetryConsumer<TelemetryConsumer>();\n            services.AddTelemetryConsumer<SecondTelemetryConsumer>();\n        }\n        else if (approach == RegistrationApproach.Manual)\n        {\n            services.AddSingleton<TelemetryConsumer>();\n            services.AddSingleton(services => (IForwarderTelemetryConsumer)services.GetRequiredService<TelemetryConsumer>());\n            services.AddSingleton(services => (IKestrelTelemetryConsumer)services.GetRequiredService<TelemetryConsumer>());\n            services.AddSingleton(services => (IHttpTelemetryConsumer)services.GetRequiredService<TelemetryConsumer>());\n            services.AddSingleton(services => (INameResolutionTelemetryConsumer)services.GetRequiredService<TelemetryConsumer>());\n            services.AddSingleton(services => (INetSecurityTelemetryConsumer)services.GetRequiredService<TelemetryConsumer>());\n            services.AddSingleton(services => (ISocketsTelemetryConsumer)services.GetRequiredService<TelemetryConsumer>());\n\n            services.AddSingleton<SecondTelemetryConsumer>();\n            services.AddSingleton(services => (IForwarderTelemetryConsumer)services.GetRequiredService<SecondTelemetryConsumer>());\n            services.AddSingleton(services => (IKestrelTelemetryConsumer)services.GetRequiredService<SecondTelemetryConsumer>());\n            services.AddSingleton(services => (IHttpTelemetryConsumer)services.GetRequiredService<SecondTelemetryConsumer>());\n            services.AddSingleton(services => (INameResolutionTelemetryConsumer)services.GetRequiredService<SecondTelemetryConsumer>());\n            services.AddSingleton(services => (INetSecurityTelemetryConsumer)services.GetRequiredService<SecondTelemetryConsumer>());\n            services.AddSingleton(services => (ISocketsTelemetryConsumer)services.GetRequiredService<SecondTelemetryConsumer>());\n\n            services.AddTelemetryListeners();\n        }\n    }\n\n    private static void RegisterMetricsConsumers(IServiceCollection services, RegistrationApproach approach)\n    {\n        if (approach == RegistrationApproach.WithInstanceHelper)\n        {\n            services.AddMetricsConsumer(new MetricsConsumer());\n        }\n        else if (approach == RegistrationApproach.WithGenericHelper)\n        {\n            services.AddMetricsConsumer<MetricsConsumer>();\n        }\n        else if (approach == RegistrationApproach.Manual)\n        {\n            services.AddSingleton<MetricsConsumer>();\n            services.AddSingleton(services => (IMetricsConsumer<ForwarderMetrics>)services.GetRequiredService<MetricsConsumer>());\n            services.AddSingleton(services => (IMetricsConsumer<KestrelMetrics>)services.GetRequiredService<MetricsConsumer>());\n            services.AddSingleton(services => (IMetricsConsumer<HttpMetrics>)services.GetRequiredService<MetricsConsumer>());\n            services.AddSingleton(services => (IMetricsConsumer<NameResolutionMetrics>)services.GetRequiredService<MetricsConsumer>());\n            services.AddSingleton(services => (IMetricsConsumer<NetSecurityMetrics>)services.GetRequiredService<MetricsConsumer>());\n            services.AddSingleton(services => (IMetricsConsumer<SocketsMetrics>)services.GetRequiredService<MetricsConsumer>());\n\n            services.AddTelemetryListeners();\n        }\n    }\n\n    private static void VerifyStages(string[] expected, List<(string Stage, DateTime Timestamp)> stages)\n    {\n        Assert.Equal(expected, stages.Select(s => s.Stage).ToArray());\n\n        for (var i = 1; i < stages.Count; i++)\n        {\n            Assert.True(stages[i - 1].Timestamp <= stages[i].Timestamp);\n        }\n    }\n\n    [Theory]\n    [InlineData(RegistrationApproach.WithInstanceHelper)]\n    [InlineData(RegistrationApproach.WithGenericHelper)]\n    [InlineData(RegistrationApproach.Manual)]\n    public async Task TelemetryConsumptionWorks(RegistrationApproach registrationApproach)\n    {\n        var useHttpsOnDestination = !OperatingSystem.IsMacOS();\n\n        var test = new TestEnvironment(\n            async context => await context.Response.WriteAsync(\"Foo\"))\n        {\n            UseHttpsOnDestination = useHttpsOnDestination,\n            ClusterId = Guid.NewGuid().ToString(),\n            ConfigureProxy = proxyBuilder => RegisterTelemetryConsumers(proxyBuilder.Services, registrationApproach),\n        };\n\n        await test.Invoke(async uri =>\n        {\n            using var httpClient = new HttpClient();\n            await httpClient.GetStringAsync(uri);\n        });\n\n        var expected = new[]\n        {\n            \"OnConnectionStart-Kestrel\",\n            \"OnRequestStart-Kestrel\",\n            \"OnForwarderInvoke\",\n            \"OnForwarderStart\",\n            \"OnForwarderStage-SendAsyncStart\",\n            \"OnRequestStart\",\n            \"OnConnectStart\",\n            \"OnConnectStop\",\n            \"OnHandshakeStart\",\n            \"OnHandshakeStop\",\n            \"OnConnectionEstablished\",\n            \"OnRequestLeftQueue\",\n            \"OnRequestHeadersStart\",\n            \"OnRequestHeadersStop\",\n            \"OnResponseHeadersStart\",\n            \"OnResponseHeadersStop\",\n            \"OnRequestStop\",\n            \"OnForwarderStage-SendAsyncStop\",\n            \"OnForwarderStage-ResponseContentTransferStart\",\n            \"OnContentTransferred\",\n            \"OnForwarderStop\",\n            \"OnRequestStop-Kestrel\",\n            \"OnConnectionStop-Kestrel\",\n        };\n\n        if (!useHttpsOnDestination)\n        {\n            expected = expected.Where(s => !s.Contains(\"OnHandshake\", StringComparison.Ordinal)).ToArray();\n        }\n\n        foreach (var consumerType in new[] { typeof(TelemetryConsumer), typeof(SecondTelemetryConsumer) })\n        {\n            Assert.True(TelemetryConsumer.PerClusterTelemetry.TryGetValue((test.ClusterId, consumerType), out var stages));\n            VerifyStages(expected, stages);\n        }\n    }\n\n    [Theory]\n    [InlineData(RegistrationApproach.WithInstanceHelper)]\n    [InlineData(RegistrationApproach.WithGenericHelper)]\n    [InlineData(RegistrationApproach.Manual)]\n    public async Task NonProxyTelemetryConsumptionWorks(RegistrationApproach registrationApproach)\n    {\n        var redirected = false;\n\n        var test = new TestEnvironment(\n            async context =>\n            {\n                if (redirected)\n                {\n                    await context.Response.WriteAsync(\"Foo\");\n                }\n                else\n                {\n                    context.Response.Redirect(\"/foo\");\n                    redirected = true;\n                }\n            })\n        {\n            UseHttpsOnDestination = true,\n            ConfigureProxy = proxyBuilder => RegisterTelemetryConsumers(proxyBuilder.Services, registrationApproach),\n        };\n        var path = $\"/{Guid.NewGuid()}\";\n\n        await test.Invoke(async uri =>\n        {\n            using var httpClient = new HttpClient();\n            await httpClient.GetStringAsync($\"{uri.TrimEnd('/')}{path}\");\n        });\n\n        var expected = new[]\n        {\n            \"OnRequestStart\",\n            \"OnConnectStart\",\n            \"OnConnectStop\",\n            \"OnConnectionEstablished\",\n            \"OnRequestLeftQueue\",\n            \"OnRequestHeadersStart\",\n            \"OnRequestHeadersStop\",\n            \"OnResponseHeadersStart\",\n            \"OnResponseHeadersStop\",\n            \"OnRedirect\",\n            \"OnRequestHeadersStart\",\n            \"OnRequestHeadersStop\",\n            \"OnResponseHeadersStart\",\n            \"OnResponseHeadersStop\",\n            \"OnResponseContentStart\",\n            \"OnResponseContentStop\",\n            \"OnRequestStop\",\n        };\n\n        foreach (var consumerType in new[] { typeof(TelemetryConsumer), typeof(SecondTelemetryConsumer) })\n        {\n            Assert.True(TelemetryConsumer.PerPathAndQueryTelemetry.TryGetValue((path, consumerType), out var stages));\n            VerifyStages(expected, stages);\n        }\n    }\n\n    private class SecondTelemetryConsumer : TelemetryConsumer { }\n\n    private class TelemetryConsumer :\n        IForwarderTelemetryConsumer,\n        IKestrelTelemetryConsumer,\n        IHttpTelemetryConsumer,\n        INameResolutionTelemetryConsumer,\n        INetSecurityTelemetryConsumer,\n        ISocketsTelemetryConsumer\n    {\n        public static readonly ConcurrentDictionary<(string, Type), List<(string Stage, DateTime Timestamp)>> PerClusterTelemetry = new();\n        public static readonly ConcurrentDictionary<(string, Type), List<(string Stage, DateTime Timestamp)>> PerPathAndQueryTelemetry = new();\n\n        private readonly AsyncLocal<List<(string Stage, DateTime Timestamp)>> _stages = new();\n\n        private void AddStage(string stage, DateTime timestamp)\n        {\n            var stages = _stages.Value ??= new List<(string Stage, DateTime Timestamp)>();\n\n            lock (stages)\n            {\n                stages.Add((stage, timestamp));\n            }\n        }\n\n        public void OnForwarderStart(DateTime timestamp, string destinationPrefix) => AddStage(nameof(OnForwarderStart), timestamp);\n        public void OnForwarderStop(DateTime timestamp, int statusCode) => AddStage(nameof(OnForwarderStop), timestamp);\n        public void OnForwarderFailed(DateTime timestamp, ForwarderError error) => AddStage(nameof(OnForwarderFailed), timestamp);\n        public void OnForwarderStage(DateTime timestamp, Telemetry.Consumption.ForwarderStage stage) => AddStage($\"{nameof(OnForwarderStage)}-{stage}\", timestamp);\n        public void OnContentTransferring(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime) => AddStage(nameof(OnContentTransferring), timestamp);\n        public void OnContentTransferred(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime, TimeSpan firstReadTime) => AddStage(nameof(OnContentTransferred), timestamp);\n        public void OnForwarderInvoke(DateTime timestamp, string clusterId, string routeId, string destinationId)\n        {\n            AddStage(nameof(OnForwarderInvoke), timestamp);\n            PerClusterTelemetry.TryAdd((clusterId, GetType()), _stages.Value);\n        }\n        public void OnRequestStart(DateTime timestamp, string scheme, string host, int port, string pathAndQuery, int versionMajor, int versionMinor, HttpVersionPolicy versionPolicy)\n        {\n            AddStage(nameof(OnRequestStart), timestamp);\n            PerPathAndQueryTelemetry.TryAdd((pathAndQuery, GetType()), _stages.Value);\n        }\n        public void OnRequestStop(DateTime timestamp) => AddStage(nameof(OnRequestStop), timestamp);\n        public void OnRequestFailed(DateTime timestamp) => AddStage(nameof(OnRequestFailed), timestamp);\n        public void OnConnectionEstablished(DateTime timestamp, int versionMajor, int versionMinor) => AddStage(nameof(OnConnectionEstablished), timestamp);\n        public void OnRequestLeftQueue(DateTime timestamp, TimeSpan timeOnQueue, int versionMajor, int versionMinor) => AddStage(nameof(OnRequestLeftQueue), timestamp);\n        public void OnRequestHeadersStart(DateTime timestamp) => AddStage(nameof(OnRequestHeadersStart), timestamp);\n        public void OnRequestHeadersStop(DateTime timestamp) => AddStage(nameof(OnRequestHeadersStop), timestamp);\n        public void OnRequestContentStart(DateTime timestamp) => AddStage(nameof(OnRequestContentStart), timestamp);\n        public void OnRequestContentStop(DateTime timestamp, long contentLength) => AddStage(nameof(OnRequestContentStop), timestamp);\n        public void OnResponseHeadersStart(DateTime timestamp) => AddStage(nameof(OnResponseHeadersStart), timestamp);\n        public void OnResponseHeadersStop(DateTime timestamp) => AddStage(nameof(OnResponseHeadersStop), timestamp);\n        public void OnResponseContentStart(DateTime timestamp) => AddStage(nameof(OnResponseContentStart), timestamp);\n        public void OnResponseContentStop(DateTime timestamp) => AddStage(nameof(OnResponseContentStop), timestamp);\n        public void OnResolutionStart(DateTime timestamp, string hostNameOrAddress) => AddStage(nameof(OnResolutionStart), timestamp);\n        public void OnResolutionStop(DateTime timestamp) => AddStage(nameof(OnResolutionStop), timestamp);\n        public void OnResolutionFailed(DateTime timestamp) => AddStage(nameof(OnResolutionFailed), timestamp);\n        public void OnHandshakeStart(DateTime timestamp, bool isServer, string targetHost) => AddStage(nameof(OnHandshakeStart), timestamp);\n        public void OnHandshakeStop(DateTime timestamp, SslProtocols protocol) => AddStage(nameof(OnHandshakeStop), timestamp);\n        public void OnHandshakeFailed(DateTime timestamp, bool isServer, TimeSpan elapsed, string exceptionMessage) => AddStage(nameof(OnHandshakeFailed), timestamp);\n        public void OnConnectStart(DateTime timestamp, string address) => AddStage(nameof(OnConnectStart), timestamp);\n        public void OnConnectStop(DateTime timestamp) => AddStage(nameof(OnConnectStop), timestamp);\n        public void OnConnectFailed(DateTime timestamp, SocketError error, string exceptionMessage) => AddStage(nameof(OnConnectFailed), timestamp);\n        public void OnConnectionStart(DateTime timestamp, string connectionId, string localEndPoint, string remoteEndPoint) => AddStage($\"{nameof(OnConnectionStart)}-Kestrel\", timestamp);\n        public void OnRequestStart(DateTime timestamp, string connectionId, string requestId, string httpVersion, string path, string method) => AddStage($\"{nameof(OnRequestStart)}-Kestrel\", timestamp);\n        public void OnRequestStop(DateTime timestamp, string connectionId, string requestId, string httpVersion, string path, string method) => AddStage($\"{nameof(OnRequestStop)}-Kestrel\", timestamp);\n        public void OnConnectionStop(DateTime timestamp, string connectionId) => AddStage($\"{nameof(OnConnectionStop)}-Kestrel\", timestamp);\n        public void OnRedirect(DateTime timestamp, string redirectUri) => AddStage(nameof(OnRedirect), timestamp);\n    }\n\n    [Theory]\n    [InlineData(RegistrationApproach.WithInstanceHelper)]\n    [InlineData(RegistrationApproach.WithGenericHelper)]\n    [InlineData(RegistrationApproach.Manual)]\n    public async Task MetricsConsumptionWorks(RegistrationApproach registrationApproach)\n    {\n        MetricsOptions.Interval = TimeSpan.FromMilliseconds(10);\n\n        var test = new TestEnvironment(\n            async context => await context.Response.WriteAsync(\"Foo\"))\n        {\n            UseHttpsOnDestination = true,\n            ConfigureProxy = proxyBuilder => RegisterMetricsConsumers(proxyBuilder.Services, registrationApproach),\n        };\n        var consumerBox = new MetricsConsumer.MetricsConsumerBox();\n        MetricsConsumer.ScopeInstance.Value = consumerBox;\n        MetricsConsumer consumer = null;\n\n        await test.Invoke(async uri =>\n        {\n            var httpClient = new HttpClient();\n            await httpClient.GetStringAsync(uri);\n\n            consumer = consumerBox.Instance;\n\n            try\n            {\n                // Do some arbitrary DNS work to get metrics, since we're connecting to localhost\n                _ = await Dns.GetHostAddressesAsync(\"microsoft.com\");\n            }\n            catch { }\n\n            await Task.WhenAll(\n                WaitAsync(() => consumer.ProxyMetrics.LastOrDefault()?.RequestsStarted > 0, nameof(ForwarderMetrics)),\n                WaitAsync(() => consumer.KestrelMetrics.LastOrDefault()?.TotalConnections > 0, nameof(KestrelMetrics)),\n                WaitAsync(() => consumer.HttpMetrics.LastOrDefault()?.RequestsStarted > 0, nameof(HttpMetrics)),\n                WaitAsync(() => consumer.SocketsMetrics.LastOrDefault()?.OutgoingConnectionsEstablished > 0, nameof(SocketsMetrics)),\n                WaitAsync(() => consumer.NetSecurityMetrics.LastOrDefault()?.TotalTlsHandshakes > 0, nameof(NetSecurityMetrics)),\n                WaitAsync(() => consumer.NameResolutionMetrics.LastOrDefault()?.DnsLookupsRequested > 0, nameof(NameResolutionMetrics)));\n        });\n\n        VerifyTimestamp(consumer.ProxyMetrics.Last().Timestamp);\n        VerifyTimestamp(consumer.KestrelMetrics.Last().Timestamp);\n        VerifyTimestamp(consumer.HttpMetrics.Last().Timestamp);\n        VerifyTimestamp(consumer.SocketsMetrics.Last().Timestamp);\n        VerifyTimestamp(consumer.NetSecurityMetrics.Last().Timestamp);\n        VerifyTimestamp(consumer.NameResolutionMetrics.Last().Timestamp);\n\n        static void VerifyTimestamp(DateTime timestamp)\n        {\n            var now = DateTime.UtcNow;\n            Assert.InRange(timestamp, now.Subtract(TimeSpan.FromSeconds(10)), now.AddSeconds(10));\n        }\n\n        static async Task WaitAsync(Func<bool> condition, string name)\n        {\n            var stopwatch = Stopwatch.StartNew();\n            while (!condition())\n            {\n                if (stopwatch.Elapsed > TimeSpan.FromSeconds(10))\n                {\n                    throw new TimeoutException($\"Timed out waiting for {name}\");\n                }\n                await Task.Delay(10);\n            }\n        }\n    }\n\n    private sealed class MetricsConsumer :\n        IMetricsConsumer<ForwarderMetrics>,\n        IMetricsConsumer<KestrelMetrics>,\n        IMetricsConsumer<HttpMetrics>,\n        IMetricsConsumer<NameResolutionMetrics>,\n        IMetricsConsumer<NetSecurityMetrics>,\n        IMetricsConsumer<SocketsMetrics>\n    {\n        public sealed class MetricsConsumerBox\n        {\n            public MetricsConsumer Instance;\n        }\n\n        public static readonly AsyncLocal<MetricsConsumerBox> ScopeInstance = new();\n\n        public readonly ConcurrentQueue<ForwarderMetrics> ProxyMetrics = new();\n        public readonly ConcurrentQueue<KestrelMetrics> KestrelMetrics = new();\n        public readonly ConcurrentQueue<HttpMetrics> HttpMetrics = new();\n        public readonly ConcurrentQueue<SocketsMetrics> SocketsMetrics = new();\n        public readonly ConcurrentQueue<NetSecurityMetrics> NetSecurityMetrics = new();\n        public readonly ConcurrentQueue<NameResolutionMetrics> NameResolutionMetrics = new();\n\n        public MetricsConsumer()\n        {\n            ScopeInstance.Value.Instance = this;\n        }\n\n        public void OnMetrics(ForwarderMetrics previous, ForwarderMetrics current) => ProxyMetrics.Enqueue(current);\n        public void OnMetrics(KestrelMetrics previous, KestrelMetrics current) => KestrelMetrics.Enqueue(current);\n        public void OnMetrics(SocketsMetrics previous, SocketsMetrics current) => SocketsMetrics.Enqueue(current);\n        public void OnMetrics(NetSecurityMetrics previous, NetSecurityMetrics current) => NetSecurityMetrics.Enqueue(current);\n        public void OnMetrics(NameResolutionMetrics previous, NameResolutionMetrics current) => NameResolutionMetrics.Enqueue(current);\n        public void OnMetrics(HttpMetrics previous, HttpMetrics current) => HttpMetrics.Enqueue(current);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/TelemetryEnumTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy;\n\npublic class TelemetryEnumTests\n{\n    [Theory]\n    [InlineData(typeof(Telemetry.Consumption.ForwarderStage), typeof(Forwarder.ForwarderStage))]\n    [InlineData(typeof(Telemetry.Consumption.WebSocketCloseReason), typeof(WebSocketsTelemetry.WebSocketCloseReason))]\n    public void ExposedEnumsMatchInternalCopies(Type publicEnum, Type internalEnum)\n    {\n        Assert.Equal(internalEnum.GetEnumNames(), publicEnum.GetEnumNames());\n        Assert.Equal(internalEnum.GetEnumValues().Cast<int>().ToArray(), publicEnum.GetEnumValues().Cast<int>().ToArray());\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/WebSocketTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.WebSockets;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Net.Http.Headers;\nusing Xunit;\nusing Yarp.ReverseProxy.Common;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Transforms;\n\nnamespace Yarp.ReverseProxy;\n\npublic class WebSocketTests\n{\n    private readonly ITestOutputHelper _output;\n\n    public WebSocketTests(ITestOutputHelper output)\n    {\n        _output = output;\n    }\n\n    public static IEnumerable<object[]> WebSocketVersionNegotiation_TestData()\n    {\n        foreach (Version incomingVersion in new[] { HttpVersion.Version11, HttpVersion.Version20 })\n        {\n            foreach (HttpVersionPolicy versionPolicy in Enum.GetValues<HttpVersionPolicy>())\n            {\n                foreach (Version destinationVersion in new[] { HttpVersion.Version11, HttpVersion.Version20, HttpVersion.Version30 })\n                {\n                    foreach (HttpProtocols destinationProtocols in new[] { HttpProtocols.Http1, HttpProtocols.Http2, HttpProtocols.Http1AndHttp2 })\n                    {\n                        foreach (bool useHttpsOnDestination in new[] { true, false })\n                        {\n                            (int version, bool canDowngrade) = (destinationVersion.Major, versionPolicy, useHttpsOnDestination) switch\n                            {\n                                (1, HttpVersionPolicy.RequestVersionOrHigher, true) => (2, true),\n                                (1, _, _) => (1, false),\n                                (2, HttpVersionPolicy.RequestVersionOrLower, true) => (2, true),\n                                (2, HttpVersionPolicy.RequestVersionOrLower, false) => (1, false),\n                                (2, _, _) => (2, false),\n                                (3, HttpVersionPolicy.RequestVersionOrLower, true) => (2, true),\n                                (3, HttpVersionPolicy.RequestVersionOrLower, false) => (1, false),\n                                (3, _, _) => (-1, false), // RequestCreation error\n                                _ => throw new Exception()\n                            };\n\n                            ForwarderError? expectedProxyError = version == -1 ? ForwarderError.RequestCreation : null;\n                            bool e2eWillFail = expectedProxyError.HasValue;\n\n                            if (version == 2 && destinationProtocols == HttpProtocols.Http1)\n                            {\n                                // ALPN rejects HTTP/2.\n                                if (canDowngrade)\n                                {\n                                    Debug.Assert(useHttpsOnDestination);\n                                    version = 1;\n                                }\n                                else\n                                {\n                                    e2eWillFail = true;\n                                    expectedProxyError = ForwarderError.Request;\n                                }\n                            }\n\n                            if (version == 1 && destinationProtocols == HttpProtocols.Http2)\n                            {\n                                // ALPN rejects HTTP/1.1, or the server sends back an error response when not using TLS.\n                                e2eWillFail = true;\n\n                                // An error response is just a bad status code, not a failed request from the proxy's perspective.\n                                if (useHttpsOnDestination)\n                                {\n                                    expectedProxyError = ForwarderError.Request;\n                                }\n                            }\n\n                            if (version == 2 && destinationProtocols == HttpProtocols.Http1AndHttp2 && !useHttpsOnDestination)\n                            {\n                                // No ALPN, Kestrel doesn't know whether to use HTTP/1.1 or HTTP/2, defaulting to HTTP/1.1.\n                                // YARP will see an 'HTTP_1_1_REQUIRED' error and return a 502.\n                                Debug.Assert(!canDowngrade);\n                                e2eWillFail = true;\n                                expectedProxyError = ForwarderError.Request;\n                            }\n\n                            string expectedVersion = version == 1 ? \"HTTP/1.1\" : \"HTTP/2\";\n\n                            yield return new object[] { incomingVersion, versionPolicy, destinationVersion, destinationProtocols, useHttpsOnDestination, expectedVersion, expectedProxyError, e2eWillFail };\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    [Theory]\n    [MemberData(nameof(WebSocketVersionNegotiation_TestData))]\n    public async Task WebSocketVersionNegotiation(Version incomingVersion, HttpVersionPolicy versionPolicy, Version requestedDestinationVersion, HttpProtocols destinationProtocols, bool useHttpsOnDestination,\n        string expectedVersion, ForwarderError? expectedProxyError, bool e2eWillFail)\n    {\n        using var cts = CreateTimer();\n\n        var test = CreateTestEnvironment();\n        test.ProxyProtocol = incomingVersion.Major == 1 ? HttpProtocols.Http1 : HttpProtocols.Http2;\n        test.DestinationProtocol = destinationProtocols;\n        test.DestinationHttpVersion = requestedDestinationVersion;\n        test.DestinationHttpVersionPolicy = versionPolicy;\n        test.UseHttpsOnDestination = useHttpsOnDestination;\n\n        int proxyRequests = 0;\n        ForwarderError? error = null;\n\n        test.ConfigureProxyApp = builder =>\n        {\n            builder.Use(async (context, next) =>\n            {\n                proxyRequests++;\n                await next(context);\n\n                error = context.Features.Get<IForwarderErrorFeature>()?.Error;\n            });\n        };\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new ClientWebSocket();\n            client.Options.HttpVersion = incomingVersion;\n            client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;\n\n            if (e2eWillFail)\n            {\n                var ex = await Assert.ThrowsAsync<WebSocketException>(() => SendWebSocketRequestAsync(client, uri, expectedVersion, cts.Token));\n                Assert.IsNotType<TaskCanceledException>(ex.InnerException);\n            }\n            else\n            {\n                await SendWebSocketRequestAsync(client, uri, expectedVersion, cts.Token);\n            }\n        }, cts.Token);\n\n        Assert.Equal(1, proxyRequests);\n        Assert.Equal(expectedProxyError, error);\n    }\n\n    [Theory]\n    [InlineData(WebSocketMessageType.Binary)]\n    [InlineData(WebSocketMessageType.Text)]\n    public async Task WebSocketMessageTypes(WebSocketMessageType messageType)\n    {\n        using var cts = CreateTimer();\n\n        var test = CreateTestEnvironment();\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new ClientWebSocket();\n            var webSocketsTarget = uri.Replace(\"https://\", \"wss://\").Replace(\"http://\", \"ws://\");\n            var targetUri = new Uri(new Uri(webSocketsTarget, UriKind.Absolute), \"websockets\");\n            await client.ConnectAsync(targetUri, cts.Token);\n\n            var buffer = new byte[1024];\n            var textToSend = $\"Hello World!\";\n            var numBytes = Encoding.UTF8.GetBytes(textToSend, buffer.AsSpan());\n            await client.SendAsync(new ArraySegment<byte>(buffer, 0, numBytes),\n                messageType,\n                endOfMessage: true,\n                cts.Token);\n\n            var message = await client.ReceiveAsync(buffer, cts.Token);\n\n            Assert.Equal(messageType, message.MessageType);\n            Assert.True(message.EndOfMessage);\n\n            var text = Encoding.UTF8.GetString(buffer.AsSpan(0, message.Count));\n            Assert.Equal(textToSend, text);\n\n            await client.CloseAsync(WebSocketCloseStatus.NormalClosure, \"Bye\", cts.Token);\n            Assert.Equal(WebSocketCloseStatus.NormalClosure, client.CloseStatus);\n            Assert.Equal(\"Bye\", client.CloseStatusDescription);\n        }, cts.Token);\n    }\n\n    [Fact]\n    public async Task RawUpgradeTest()\n    {\n        using var cts = CreateTimer();\n\n        var test = CreateTestEnvironment();\n\n        await test.Invoke(async uri =>\n        {\n            using var client = WebSocketTests.CreateInvoker();\n            var targetUri = new Uri(new Uri(uri, UriKind.Absolute), \"rawupgrade\");\n            using var request = new HttpRequestMessage(HttpMethod.Get, targetUri);\n\n            // TODO: https://github.com/dotnet/yarp/issues/255 Until this is fixed the \"Upgrade: WebSocket\" header is required.\n            request.Headers.TryAddWithoutValidation(\"Upgrade\", \"WebSocket\");\n\n            request.Headers.TryAddWithoutValidation(\"Connection\", \"upgrade\");\n            request.Version = new Version(1, 1);\n\n            var response = await client.SendAsync(request, cts.Token);\n\n            Assert.Equal(HttpStatusCode.SwitchingProtocols, response.StatusCode);\n\n            using var rawStream = await response.Content.ReadAsStreamAsync(cts.Token);\n\n            var buffer = new byte[5];\n            for (var i = 0; i <= 255; i++)\n            {\n                buffer[0] = (byte)i;\n                await rawStream.WriteAsync(buffer, 0, buffer.Length, cts.Token);\n                var read = await rawStream.ReadAsync(buffer, cts.Token);\n\n                Assert.Equal(buffer.Length, read);\n                Assert.Equal(i, buffer[0]);\n            }\n\n            await rawStream.WriteAsync(Encoding.UTF8.GetBytes(\"close\"));\n            while (await rawStream.ReadAsync(buffer, cts.Token) != 0) { }\n            rawStream.Dispose();\n        }, cts.Token);\n    }\n\n    [Fact]\n    // https://github.com/dotnet/yarp/issues/255 IIS claims all requests are upgradeable.\n    public async Task FalseUpgradeTest()\n    {\n        using var cts = CreateTimer();\n\n        var test = CreateTestEnvironment(forceUpgradable: true);\n\n        await test.Invoke(async uri =>\n        {\n            using var client = WebSocketTests.CreateInvoker();\n            var targetUri = new Uri(new Uri(uri, UriKind.Absolute), \"post\");\n            using var request = new HttpRequestMessage(HttpMethod.Post, targetUri);\n            request.Content = new StringContent(\"Hello World\");\n            request.Version = new Version(1, 1);\n\n            var response = await client.SendAsync(request, cts.Token);\n\n            Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n            Assert.Equal(\"Hello World\", await response.Content.ReadAsStringAsync(cts.Token));\n        }, cts.Token);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task WebSocket11_To_11(bool useHttps)\n    {\n        using var cts = CreateTimer();\n\n        var test = CreateTestEnvironment();\n        test.ProxyProtocol = HttpProtocols.Http1;\n        test.DestinationProtocol = HttpProtocols.Http1;\n        test.DestinationHttpVersion = HttpVersion.Version11;\n        test.UseHttpsOnProxy = useHttps;\n        test.UseHttpsOnDestination = useHttps;\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new ClientWebSocket();\n            await SendWebSocketRequestAsync(client, uri, \"HTTP/1.1\", cts.Token);\n        }, cts.Token);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task WebSocket20_To_20(bool useHttps)\n    {\n        using var cts = CreateTimer();\n\n        var test = CreateTestEnvironment();\n        test.ProxyProtocol = HttpProtocols.Http2;\n        test.DestinationProtocol = HttpProtocols.Http2;\n        test.DestinationHttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;\n        test.UseHttpsOnProxy = useHttps;\n        test.UseHttpsOnDestination = useHttps;\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new ClientWebSocket();\n            client.Options.HttpVersion = HttpVersion.Version20;\n            client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;\n            await SendWebSocketRequestAsync(client, uri, \"HTTP/2\", cts.Token);\n        }, cts.Token);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task WebSocket20_To_11(bool useHttps)\n    {\n        using var cts = CreateTimer();\n\n        var test = CreateTestEnvironment();\n        test.ProxyProtocol = HttpProtocols.Http2;\n        test.DestinationProtocol = HttpProtocols.Http1;\n        test.DestinationHttpVersion = HttpVersion.Version11;\n        test.UseHttpsOnProxy = useHttps;\n        test.UseHttpsOnDestination = useHttps;\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new ClientWebSocket();\n            client.Options.HttpVersion = HttpVersion.Version20;\n            client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;\n            await SendWebSocketRequestAsync(client, uri, \"HTTP/1.1\", cts.Token);\n        }, cts.Token);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task WebSocket11_To_20(bool useHttps)\n    {\n        using var cts = CreateTimer();\n\n        var test = CreateTestEnvironment();\n        test.ProxyProtocol = HttpProtocols.Http1;\n        test.DestinationProtocol = HttpProtocols.Http2;\n        test.DestinationHttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;\n        test.UseHttpsOnProxy = useHttps;\n        test.UseHttpsOnDestination = useHttps;\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new ClientWebSocket();\n            client.Options.HttpVersion = HttpVersion.Version11;\n            client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;\n            await SendWebSocketRequestAsync(client, uri, \"HTTP/2\", cts.Token);\n        }, cts.Token);\n    }\n\n    [Fact]\n    public async Task WebSocketFallbackFromH2()\n    {\n        using var cts = CreateTimer();\n\n        var test = CreateTestEnvironment();\n        test.ProxyProtocol = HttpProtocols.Http1;\n        // The destination doesn't support HTTP/2, as determined by ALPN\n        test.DestinationProtocol = HttpProtocols.Http1;\n        test.DestinationHttpVersion = HttpVersion.Version20;\n        test.UseHttpsOnDestination = true;\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new ClientWebSocket();\n            await SendWebSocketRequestAsync(client, uri, \"HTTP/1.1\", cts.Token);\n        }, cts.Token);\n    }\n\n    [Fact]\n    public async Task WebSocketFallbackFromH2_FailureInSecondRequestTransform_TreatedAsRequestCreationFailure()\n    {\n        using var cts = CreateTimer();\n\n        ForwarderError? error = null;\n\n        var test = new TestEnvironment()\n        {\n            TestOutput = _output,\n            ProxyProtocol = HttpProtocols.Http1,\n            // The destination doesn't support HTTP/2, as determined by ALPN\n            DestinationProtocol = HttpProtocols.Http1,\n            DestinationHttpVersion = HttpVersion.Version20,\n            UseHttpsOnDestination = true,\n            ConfigureProxy = builder =>\n            {\n                builder.AddTransforms(transforms =>\n                {\n                    transforms.AddRequestTransform(context =>\n                    {\n                        if (context.ProxyRequest.Version.Major == 1)\n                        {\n                            // This is the second (downgrade) request.\n                            throw new Exception(\"Foo\");\n                        }\n\n                        return default;\n                    });\n                });\n            },\n            ConfigureProxyApp = builder =>\n            {\n                builder.Use(async (context, next) =>\n                {\n                    await next(context);\n\n                    error = context.Features.Get<IForwarderErrorFeature>()?.Error;\n                });\n            },\n        };\n\n        await test.Invoke(async uri =>\n        {\n            try\n            {\n                using var client = new ClientWebSocket();\n                await SendWebSocketRequestAsync(client, uri, \"HTTP/1.1\", cts.Token);\n            }\n            catch { }\n        }, cts.Token);\n\n        Assert.Equal(ForwarderError.RequestCreation, error);\n    }\n\n    // [Fact]\n    [Fact(Skip = \"Manual test only, the CI doesn't always have the IIS Express test cert installed.\")]\n    public async Task WebSocketFallbackFromH2WS()\n    {\n        if (!OperatingSystem.IsWindows())\n        {\n            // This test relies on Windows/HttpSys\n            return;\n        }\n\n        using var cts = CreateTimer();\n\n        var test = CreateTestEnvironment();\n        test.ProxyProtocol = HttpProtocols.Http1;\n        // The destination supports HTTP/2, but not H2WS\n        test.UseHttpSysOnDestination = true;\n        test.UseHttpsOnDestination = true;\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new ClientWebSocket();\n            await SendWebSocketRequestAsync(client, uri, \"HTTP/1.1\", cts.Token);\n        }, cts.Token);\n    }\n\n    [Theory]\n    [InlineData(HttpVersionPolicy.RequestVersionExact, true)]\n    [InlineData(HttpVersionPolicy.RequestVersionExact, false)]\n    [InlineData(HttpVersionPolicy.RequestVersionOrHigher, true)]\n    public async Task WebSocketCantFallbackFromH2(HttpVersionPolicy policy, bool useHttps)\n    {\n        using var cts = CreateTimer();\n\n        var test = CreateTestEnvironment();\n        test.ProxyProtocol = HttpProtocols.Http1;\n        test.DestinationProtocol = HttpProtocols.Http1;\n        test.DestinationHttpVersion = HttpVersion.Version20;\n        test.DestinationHttpVersionPolicy = policy;\n        test.UseHttpsOnDestination = useHttps;\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new ClientWebSocket();\n            var webSocketsTarget = uri.Replace(\"https://\", \"wss://\").Replace(\"http://\", \"ws://\");\n            var targetUri = new Uri(new Uri(webSocketsTarget, UriKind.Absolute), \"websockets\");\n            using var invoker = CreateInvoker();\n            var wse = await Assert.ThrowsAsync<WebSocketException>(() => client.ConnectAsync(targetUri, invoker, cts.Token));\n            Assert.Equal(\"The server returned status code '502' when status code '101' was expected.\", wse.Message);\n        }, cts.Token);\n    }\n\n    [Theory]\n    [InlineData(HttpProtocols.Http1)] // Checked by destination\n    [InlineData(HttpProtocols.Http2)] // Checked by proxy\n    public async Task InvalidKeyHeader_400(HttpProtocols destinationProtocol)\n    {\n        using var cts = CreateTimer();\n\n        var test = CreateTestEnvironment();\n        test.ProxyProtocol = HttpProtocols.Http1;\n        test.DestinationProtocol = destinationProtocol;\n        test.DestinationHttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;\n        test.DestinationHttpVersion = destinationProtocol == HttpProtocols.Http1 ? HttpVersion.Version11 : HttpVersion.Version20;\n\n        test.ConfigureProxyApp = builder =>\n        {\n            builder.Use(async (context, next) =>\n            {\n                context.Request.Headers[HeaderNames.SecWebSocketKey] = \"ThisIsAnIncorrectKeyHeaderLongerThan24Bytes\";\n\n                var logs = TestLogger.Collect();\n                await next(context);\n\n                if (destinationProtocol == HttpProtocols.Http1)\n                {\n                    Assert.DoesNotContain(logs, log => log.EventId == EventIds.InvalidSecWebSocketKeyHeader);\n                }\n                else\n                {\n                    Assert.Contains(logs, log => log.EventId == EventIds.InvalidSecWebSocketKeyHeader);\n                }\n            });\n        };\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new ClientWebSocket();\n            client.Options.CollectHttpResponseDetails = true;\n            var webSocketsTarget = uri.Replace(\"https://\", \"wss://\").Replace(\"http://\", \"ws://\");\n            var targetUri = new Uri(new Uri(webSocketsTarget, UriKind.Absolute), \"websockets\");\n            client.Options.RemoteCertificateValidationCallback = (_, _, _, _) => true;\n            var wse = await Assert.ThrowsAsync<WebSocketException>(() => client.ConnectAsync(targetUri, cts.Token));\n            Assert.Equal(\"The server returned status code '400' when status code '101' was expected.\", wse.Message);\n            Assert.Equal(HttpStatusCode.BadRequest, client.HttpStatusCode);\n            // TODO: Assert the version https://github.com/dotnet/runtime/issues/75353\n        }, cts.Token);\n    }\n\n    [Fact]\n    public async Task WebSocket20_To_11_WithWellFormedKeyHeader_OriginalKeyIsUsed()\n    {\n        using var cts = CreateTimer();\n\n        var clientKey = ProtocolHelper.CreateSecWebSocketKey();\n\n        var test = CreateTestEnvironment();\n        test.ProxyProtocol = HttpProtocols.Http2;\n        test.DestinationProtocol = HttpProtocols.Http1;\n\n        var originalDestinationApp = test.ConfigureDestinationApp;\n        test.ConfigureDestinationApp = app =>\n        {\n            app.Use((context, next) =>\n            {\n                Assert.True(context.Request.Headers.TryGetValue(HeaderNames.SecWebSocketKey, out var key));\n                Assert.Equal(clientKey, key);\n                return next(context);\n            });\n            originalDestinationApp(app);\n        };\n\n        await test.Invoke(async uri =>\n        {\n            using var client = new ClientWebSocket();\n            client.Options.HttpVersion = HttpVersion.Version20;\n            client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;\n\n            client.Options.SetRequestHeader(HeaderNames.SecWebSocketKey, clientKey);\n\n            await SendWebSocketRequestAsync(client, uri, \"HTTP/1.1\", cts.Token);\n        }, cts.Token);\n    }\n\n    [Fact]\n    public async Task WebSocket20_To_11_WithInvalidKeyHeader_RequestRejected()\n    {\n        using var cts = CreateTimer();\n\n        var test = CreateTestEnvironment();\n        test.ProxyProtocol = HttpProtocols.Http2;\n        test.DestinationProtocol = HttpProtocols.Http1;\n\n        test.ConfigureProxyApp = builder =>\n        {\n            builder.Use(async (context, next) =>\n            {\n                var logs = TestLogger.Collect();\n                await next(context);\n                Assert.Contains(logs, log => log.EventId == EventIds.InvalidSecWebSocketKeyHeader);\n            });\n        };\n\n        await test.Invoke(async uri =>\n        {\n            var webSocketsTarget = uri.Replace(\"http://\", \"ws://\");\n            var targetUri = new Uri(new Uri(webSocketsTarget, UriKind.Absolute), \"websockets\");\n\n            using var client = new ClientWebSocket();\n            client.Options.HttpVersion = HttpVersion.Version20;\n            client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;\n            client.Options.CollectHttpResponseDetails = true;\n\n            client.Options.SetRequestHeader(HeaderNames.SecWebSocketKey, \"Foo\");\n\n            using var invoker = CreateInvoker();\n            var wse = await Assert.ThrowsAsync<WebSocketException>(() => client.ConnectAsync(targetUri, invoker, cts.Token));\n            Assert.Equal(\"The server returned status code '400' when status code '200' was expected.\", wse.Message);\n            Assert.Equal(HttpStatusCode.BadRequest, client.HttpStatusCode);\n        }, cts.Token);\n    }\n\n    private async Task SendWebSocketRequestAsync(ClientWebSocket client, string uri, string destinationProtocol, CancellationToken token)\n    {\n        var webSocketsTarget = uri.Replace(\"https://\", \"wss://\").Replace(\"http://\", \"ws://\");\n        var targetUri = new Uri(new Uri(webSocketsTarget, UriKind.Absolute), \"websocketversion\");\n        using var invoker = CreateInvoker();\n        await client.ConnectAsync(targetUri, invoker, token);\n        _output.WriteLine(\"Client connected.\");\n\n        var buffer = new byte[1024];\n        var textToSend = $\"Hello World!\";\n        var numBytes = Encoding.UTF8.GetBytes(textToSend, buffer);\n        await client.SendAsync(buffer.AsMemory(0, numBytes),\n            WebSocketMessageType.Text,\n            endOfMessage: true,\n            token);\n        _output.WriteLine($\"Client sent {numBytes}.\");\n\n        var message = await client.ReceiveAsync(buffer, token);\n        _output.WriteLine($\"Client received {message.Count}.\");\n\n        Assert.Equal(WebSocketMessageType.Text, message.MessageType);\n        Assert.True(message.EndOfMessage);\n\n        var text = Encoding.UTF8.GetString(buffer.AsSpan(0, message.Count));\n        Assert.Equal(destinationProtocol, text);\n\n        _output.WriteLine($\"Client sending Close.\");\n        await client.CloseAsync(WebSocketCloseStatus.NormalClosure, \"Bye\", token);\n        Assert.Equal(WebSocketCloseStatus.NormalClosure, client.CloseStatus);\n        Assert.Equal(\"Bye\", client.CloseStatusDescription);\n        _output.WriteLine($\"Client Closed.\");\n    }\n\n    private TestEnvironment CreateTestEnvironment(bool forceUpgradable = false)\n    {\n        return new TestEnvironment()\n        {\n            TestOutput = _output,\n            ConfigureDestinationServices = destinationServices =>\n            {\n                destinationServices.AddRouting();\n            },\n            ConfigureDestinationApp = destinationApp =>\n            {\n                destinationApp.UseWebSockets();\n                destinationApp.UseRouting();\n                destinationApp.UseEndpoints(builder =>\n                {\n                    builder.Map(\"/websockets\", WebSocket);\n                    builder.Map(\"/websocketVersion\", WebSocketVersion);\n                    builder.Map(\"/rawupgrade\", RawUpgrade);\n                    builder.Map(\"/post\", Post);\n                });\n            },\n            ConfigureProxyApp = proxyApp =>\n            {\n                // Mimic the IIS issue https://github.com/dotnet/yarp/issues/255\n                proxyApp.Use((context, next) =>\n                {\n                    if (forceUpgradable && !(context.Features.Get<IHttpUpgradeFeature>()?.IsUpgradableRequest == true))\n                    {\n                        context.Features.Set<IHttpUpgradeFeature>(new AlwaysUpgradeFeature());\n                    }\n                    return next();\n                });\n            },\n        };\n\n        static async Task WebSocket(HttpContext httpContext)\n        {\n            var logger = httpContext.RequestServices.GetRequiredService<ILogger<WebSocketTests>>();\n            if (!httpContext.WebSockets.IsWebSocketRequest)\n            {\n                httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;\n                logger.LogInformation(\"Non-WebSocket request refused.\");\n                return;\n            }\n\n            using var webSocket = await httpContext.WebSockets.AcceptWebSocketAsync();\n            logger.LogInformation(\"WebSocket accepted.\");\n\n            var buffer = new byte[1024];\n            while (true)\n            {\n                var message = await webSocket.ReceiveAsync(buffer, httpContext.RequestAborted);\n                if (message.MessageType == WebSocketMessageType.Close)\n                {\n                    logger.LogInformation(\"WebSocket Close received {status}.\", message.CloseStatus);\n                    await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, message.CloseStatusDescription, httpContext.RequestAborted);\n                    logger.LogInformation(\"WebSocket Close sent {status}.\", WebSocketCloseStatus.NormalClosure);\n                    return;\n                }\n\n                logger.LogInformation(\"WebSocket received {count} bytes.\", message.Count);\n\n                await webSocket.SendAsync(buffer[0..message.Count],\n                    message.MessageType,\n                    message.EndOfMessage,\n                    httpContext.RequestAborted);\n\n                logger.LogInformation(\"WebSocket sent {count} bytes.\", message.Count);\n            }\n        }\n\n        static async Task WebSocketVersion(HttpContext httpContext)\n        {\n            var logger = httpContext.RequestServices.GetRequiredService<ILogger<WebSocketTests>>();\n            if (!httpContext.WebSockets.IsWebSocketRequest)\n            {\n                httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;\n                logger.LogInformation(\"Non-WebSocket request refused.\");\n                return;\n            }\n\n            using var webSocket = await httpContext.WebSockets.AcceptWebSocketAsync();\n            logger.LogInformation(\"WebSocket accepted.\");\n\n            var buffer = new byte[1024];\n            while (true)\n            {\n                var message = await webSocket.ReceiveAsync(buffer, httpContext.RequestAborted);\n                if (message.MessageType == WebSocketMessageType.Close)\n                {\n                    logger.LogInformation(\"WebSocket Close received {status}.\", message.CloseStatus);\n                    await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, message.CloseStatusDescription, httpContext.RequestAborted);\n                    logger.LogInformation(\"WebSocket Close sent {status}.\", WebSocketCloseStatus.NormalClosure);\n                    return;\n                }\n\n                logger.LogInformation(\"WebSocket received {count} bytes.\", message.Count);\n\n                await webSocket.SendAsync(Encoding.ASCII.GetBytes(httpContext.Request.Protocol),\n                    WebSocketMessageType.Text,\n                    endOfMessage: true,\n                    httpContext.RequestAborted);\n\n                logger.LogInformation(\"WebSocket sent {count} bytes.\", httpContext.Request.Protocol.Length);\n            }\n        }\n\n        static async Task RawUpgrade(HttpContext httpContext)\n        {\n            var upgradeFeature = httpContext.Features.Get<IHttpUpgradeFeature>();\n            if (upgradeFeature is null || !upgradeFeature.IsUpgradableRequest)\n            {\n                httpContext.Response.StatusCode = StatusCodes.Status426UpgradeRequired;\n                return;\n            }\n\n            await using var stream = await upgradeFeature.UpgradeAsync();\n            var buffer = new byte[5];\n            int read;\n            while ((read = await stream.ReadAsync(buffer, httpContext.RequestAborted)) != 0)\n            {\n                await stream.WriteAsync(buffer, 0, read, httpContext.RequestAborted);\n\n                if (string.Equals(\"close\", Encoding.UTF8.GetString(buffer, 0, read), StringComparison.Ordinal))\n                {\n                    break;\n                }\n            }\n        }\n\n        static async Task Post(HttpContext httpContext)\n        {\n            var body = await new StreamReader(httpContext.Request.Body).ReadToEndAsync();\n            await httpContext.Response.WriteAsync(body);\n        }\n    }\n\n    private static CancellationTokenSource CreateTimer()\n    {\n        if (Debugger.IsAttached)\n        {\n            return new CancellationTokenSource();\n        }\n        return new CancellationTokenSource(TimeSpan.FromSeconds(15));\n    }\n\n    private static HttpMessageInvoker CreateInvoker()\n    {\n        var handler = new SocketsHttpHandler\n        {\n            AllowAutoRedirect = false,\n            AutomaticDecompression = DecompressionMethods.None,\n            UseCookies = false,\n            UseProxy = false\n        };\n        handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true;\n        return new HttpMessageInvoker(handler);\n    }\n\n    private class AlwaysUpgradeFeature : IHttpUpgradeFeature\n    {\n        public bool IsUpgradableRequest => true;\n\n        public Task<Stream> UpgradeAsync()\n        {\n            throw new InvalidOperationException(\"This wasn't supposed to get called.\");\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/WebSocketsTelemetryTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\n#nullable enable\n\nusing System;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.WebSockets;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Server.Kestrel.Core;\nusing Microsoft.Extensions.DependencyInjection;\nusing Xunit;\nusing Yarp.ReverseProxy.Common;\nusing Yarp.Telemetry.Consumption;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy;\n\npublic class WebSocketsTelemetryTests\n{\n    private readonly ITestOutputHelper _output;\n\n    public WebSocketsTelemetryTests(ITestOutputHelper output)\n    {\n        _output = output;\n    }\n\n    [Fact]\n    public async Task NoWebSocketsUpgrade_NoTelemetryWritten()\n    {\n        var telemetry = await TestAsync(\n            async uri =>\n            {\n                using var client = new HttpClient();\n                await client.GetStringAsync(uri);\n            },\n            (context, webSocket) => throw new InvalidOperationException(\"Shouldn't be reached\"));\n\n        Assert.Null(telemetry);\n    }\n\n    [Theory]\n    [InlineData(0, 0, 42)]\n    [InlineData(0, 1, 42)]\n    [InlineData(1, 0, 42)]\n    [InlineData(23, 29, 0)]\n    [InlineData(17, 19, 1)]\n    [InlineData(11, 13, 100)]\n    [InlineData(5, 7, 1_000)]\n    [InlineData(2, 3, 100_000)]\n    public async Task MessagesExchanged_CorrectNumberReported(int read, int written, int messageSize)\n    {\n        var telemetry = await TestAsync(\n            async uri =>\n            {\n                using var client = new ClientWebSocket();\n                await client.ConnectAsync(uri, CancellationToken.None);\n                var webSocket = new WebSocketAdapter(client);\n\n                await Task.WhenAll(\n                    SendMessagesAndCloseAsync(webSocket, read, messageSize),\n                    ReceiveAllMessagesAsync(webSocket));\n            },\n            async (context, webSocket) =>\n            {\n                await Task.WhenAll(\n                    SendMessagesAndCloseAsync(webSocket, written, messageSize),\n                    ReceiveAllMessagesAsync(webSocket));\n            },\n            new TestTimeProvider(new TimeSpan(42)));\n\n        Assert.NotNull(telemetry);\n        Assert.Equal(42, telemetry!.EstablishedTime.Ticks);\n        Assert.Contains(telemetry.CloseReason, new[] { WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerGracefulClose });\n        Assert.Equal(read, telemetry!.MessagesRead);\n        Assert.Equal(written, telemetry.MessagesWritten);\n    }\n\n    [Fact]\n    public async Task Http2WebSocketsWork()\n    {\n        var read = 11;\n        var written = 13;\n        var messageSize = 100;\n        var telemetry = await TestAsync(\n            async uri =>\n            {\n                using var invoker = CreateInvoker();\n                using var client = new ClientWebSocket();\n                client.Options.HttpVersion = HttpVersion.Version20;\n                client.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;\n                await client.ConnectAsync(uri, invoker, CancellationToken.None);\n                var webSocket = new WebSocketAdapter(client);\n\n                await Task.WhenAll(\n                    SendMessagesAndCloseAsync(webSocket, read, messageSize),\n                    ReceiveAllMessagesAsync(webSocket));\n            },\n            async (context, webSocket) =>\n            {\n                await Task.WhenAll(\n                    SendMessagesAndCloseAsync(webSocket, written, messageSize),\n                    ReceiveAllMessagesAsync(webSocket));\n            },\n            new TestTimeProvider(new TimeSpan(42)),\n            http2Proxy: true);\n\n        Assert.NotNull(telemetry);\n        Assert.Equal(42, telemetry!.EstablishedTime.Ticks);\n        Assert.Contains(telemetry.CloseReason, new[] { WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerGracefulClose });\n        Assert.Equal(read, telemetry!.MessagesRead);\n        Assert.Equal(written, telemetry.MessagesWritten);\n    }\n\n    public enum Behavior\n    {\n        ClosesConnection = 1,\n        SendsClose_WaitsForClose = 2,\n        SendsClose_ClosesConnection = 4 | ClosesConnection,\n        WaitsForClose_SendsClose = 8,\n        WaitsForClose_ClosesConnection = 16 | ClosesConnection,\n    }\n\n    [Theory]\n    // Both sides close the connection - race between which is noticed first\n    [InlineData(Behavior.ClosesConnection, Behavior.ClosesConnection, WebSocketCloseReason.Unknown, WebSocketCloseReason.ClientDisconnect, WebSocketCloseReason.ServerDisconnect)]\n    // One side sends a graceful close\n    [InlineData(Behavior.SendsClose_ClosesConnection, Behavior.WaitsForClose_ClosesConnection, WebSocketCloseReason.ClientGracefulClose)]\n    [InlineData(Behavior.SendsClose_WaitsForClose, Behavior.WaitsForClose_ClosesConnection, WebSocketCloseReason.ClientGracefulClose)]\n    [InlineData(Behavior.WaitsForClose_ClosesConnection, Behavior.SendsClose_ClosesConnection, WebSocketCloseReason.ServerGracefulClose)]\n    [InlineData(Behavior.WaitsForClose_ClosesConnection, Behavior.SendsClose_WaitsForClose, WebSocketCloseReason.ServerGracefulClose)]\n    // One side sends a graceful close while the other disconnects - race between which is noticed first\n    [InlineData(Behavior.SendsClose_WaitsForClose, Behavior.ClosesConnection, WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerDisconnect)]\n    [InlineData(Behavior.SendsClose_ClosesConnection, Behavior.ClosesConnection, WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerDisconnect)]\n    [InlineData(Behavior.ClosesConnection, Behavior.SendsClose_ClosesConnection, WebSocketCloseReason.ServerGracefulClose, WebSocketCloseReason.ClientDisconnect)]\n    [InlineData(Behavior.ClosesConnection, Behavior.SendsClose_WaitsForClose, WebSocketCloseReason.ServerGracefulClose, WebSocketCloseReason.ClientDisconnect)]\n    // One side closes the connection while the other is waiting for messages\n    [InlineData(Behavior.ClosesConnection, Behavior.WaitsForClose_SendsClose, WebSocketCloseReason.ClientDisconnect)]\n    [InlineData(Behavior.ClosesConnection, Behavior.WaitsForClose_ClosesConnection, WebSocketCloseReason.ClientDisconnect)]\n    [InlineData(Behavior.WaitsForClose_SendsClose, Behavior.ClosesConnection, WebSocketCloseReason.ServerDisconnect)]\n    [InlineData(Behavior.WaitsForClose_ClosesConnection, Behavior.ClosesConnection, WebSocketCloseReason.ServerDisconnect)]\n    // Graceful, mutual close - other side closes as a reaction to receiving close\n    [InlineData(Behavior.SendsClose_WaitsForClose, Behavior.WaitsForClose_SendsClose, WebSocketCloseReason.ClientGracefulClose)]\n    [InlineData(Behavior.SendsClose_ClosesConnection, Behavior.WaitsForClose_SendsClose, WebSocketCloseReason.ClientGracefulClose)]\n    [InlineData(Behavior.WaitsForClose_SendsClose, Behavior.SendsClose_WaitsForClose, WebSocketCloseReason.ServerGracefulClose)]\n    [InlineData(Behavior.WaitsForClose_SendsClose, Behavior.SendsClose_ClosesConnection, WebSocketCloseReason.ServerGracefulClose)]\n    // Graceful, mutual close - both sides close at the same time - race between which is noticed first\n    [InlineData(Behavior.SendsClose_WaitsForClose, Behavior.SendsClose_WaitsForClose, WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerGracefulClose)]\n    [InlineData(Behavior.SendsClose_WaitsForClose, Behavior.SendsClose_ClosesConnection, WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerGracefulClose)]\n    [InlineData(Behavior.SendsClose_ClosesConnection, Behavior.SendsClose_WaitsForClose, WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerGracefulClose)]\n    [InlineData(Behavior.SendsClose_ClosesConnection, Behavior.SendsClose_ClosesConnection, WebSocketCloseReason.ClientGracefulClose, WebSocketCloseReason.ServerGracefulClose)]\n    public async Task ConnectionClosed_BlameAttributedCorrectly(Behavior clientBehavior, Behavior serverBehavior, params WebSocketCloseReason[] expectedReasons)\n    {\n        var serverSawClose = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var telemetry = await TestAsync(\n            async uri =>\n            {\n                using var client = new ClientWebSocket();\n\n                // Keep sending messages from the client in order to observe a server disconnect sooner\n                client.Options.KeepAliveInterval = TimeSpan.FromMilliseconds(10);\n\n                await client.ConnectAsync(uri, CancellationToken.None);\n                var webSocket = new WebSocketAdapter(client);\n\n                try\n                {\n                    await ProcessAsync(webSocket, clientBehavior, client: client);\n                }\n                catch (Exception ex)\n                {\n                    _output.WriteLine($\"Ignored client exception: {ex}\");\n\n                    Assert.True(serverBehavior.HasFlag(Behavior.ClosesConnection));\n                }\n            },\n            async (context, webSocket) =>\n            {\n                try\n                {\n                    await ProcessAsync(webSocket, serverBehavior, context: context);\n                }\n                catch (Exception ex)\n                {\n                    _output.WriteLine($\"Ignored destination exception: {ex}\");\n\n                    Assert.True(clientBehavior.HasFlag(Behavior.ClosesConnection));\n                }\n            });\n\n        Assert.NotNull(telemetry);\n        Assert.Contains(telemetry!.CloseReason, expectedReasons);\n\n        async Task ProcessAsync(WebSocketAdapter webSocket, Behavior behavior, ClientWebSocket? client = null, HttpContext? context = null)\n        {\n            if (behavior == Behavior.SendsClose_WaitsForClose ||\n                behavior == Behavior.SendsClose_ClosesConnection)\n            {\n                await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, \"Bye\");\n            }\n\n            if (behavior == Behavior.SendsClose_WaitsForClose ||\n                behavior == Behavior.WaitsForClose_SendsClose ||\n                behavior == Behavior.WaitsForClose_ClosesConnection)\n            {\n                await ReceiveAllMessagesAsync(webSocket);\n\n                if (context is not null)\n                {\n                    serverSawClose.SetResult();\n                }\n            }\n\n            if (behavior == Behavior.WaitsForClose_SendsClose)\n            {\n                await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, \"Bye\");\n            }\n\n            if (behavior.HasFlag(Behavior.ClosesConnection))\n            {\n                if (client is not null &&\n                    behavior is Behavior.SendsClose_ClosesConnection &&\n                    serverBehavior is Behavior.WaitsForClose_SendsClose or Behavior.WaitsForClose_ClosesConnection)\n                {\n                    // If we're sending a close message and expect the server to receive it, wait before killing the connection.\n                    await serverSawClose.Task.WaitAsync(TimeSpan.FromMinutes(1));\n                }\n\n                client?.Abort();\n\n                if (context is not null)\n                {\n                    await context.Response.Body.FlushAsync();\n                    context.Abort();\n                }\n            }\n        }\n    }\n\n    [Theory]\n    [InlineData(100, 200, WebSocketCloseReason.ClientGracefulClose)]\n    [InlineData(200, 100, WebSocketCloseReason.ServerGracefulClose)]\n    [InlineData(100, 100, WebSocketCloseReason.ServerGracefulClose)] // Implementation detail\n    public async Task ConnectionClosed_BlameReliesOnCloseTimes(long clientCloseTime, long serverCloseTime, WebSocketCloseReason expectedCloseReason)\n    {\n        var timeProvider = new TestTimeProvider(new TimeSpan(1));\n\n        var telemetry = await TestAsync(\n            async uri =>\n            {\n                using var client = new ClientWebSocket();\n                await client.ConnectAsync(uri, CancellationToken.None);\n                var webSocket = new WebSocketAdapter(client);\n\n                await ProcessAsync(webSocket, timeProvider, clientCloseTime, sendCloseFirst: clientCloseTime <= serverCloseTime);\n            },\n            async (context, webSocket) =>\n            {\n                await ProcessAsync(webSocket, timeProvider, serverCloseTime, sendCloseFirst: serverCloseTime < clientCloseTime);\n            },\n            timeProvider);\n\n        Assert.NotNull(telemetry);\n        Assert.Equal(1, telemetry!.EstablishedTime.Ticks);\n        Assert.Equal(expectedCloseReason, telemetry.CloseReason);\n\n        static async Task ProcessAsync(WebSocketAdapter webSocket, TestTimeProvider timeProvider, long closeTime, bool sendCloseFirst)\n        {\n            await SendAndAcknowledgeMessageAsync(webSocket);\n\n            var receiveTask = ReceiveAllMessagesAsync(webSocket);\n\n            if (!sendCloseFirst)\n            {\n                await receiveTask;\n            }\n\n            lock (timeProvider)\n            {\n                timeProvider.AdvanceTo(TimeSpan.FromTicks(closeTime));\n            }\n\n            await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, \"Bye\", CancellationToken.None);\n\n            await receiveTask;\n        }\n    }\n\n    private static async Task SendAndAcknowledgeMessageAsync(WebSocketAdapter webSocket)\n    {\n        var receiveBuffer = new byte[10];\n\n        var sendTask = webSocket.SendAsync(\"Hello\"u8.ToArray(), WebSocketMessageType.Text, endOfMessage: true).AsTask();\n        var receiveTask = webSocket.ReceiveAsync(receiveBuffer).AsTask();\n\n        await Task.WhenAll(sendTask, receiveTask);\n\n        Assert.Equal(\"Hello\", Encoding.UTF8.GetString(receiveBuffer[..(await receiveTask).Count]));\n    }\n\n    private static async Task ReceiveAllMessagesAsync(WebSocketAdapter webSocket)\n    {\n        Memory<byte> buffer = new byte[1024];\n\n        while (true)\n        {\n            var result = await webSocket.ReceiveAsync(buffer);\n\n            if (result.MessageType == WebSocketMessageType.Close)\n            {\n                break;\n            }\n        }\n    }\n\n    private static async Task SendMessagesAndCloseAsync(WebSocketAdapter webSocket, int messageCount, int messageSize)\n    {\n        var rng = new Random(42);\n        var buffer = new byte[1024];\n\n        for (var i = 0; i < messageCount; i++)\n        {\n            var remaining = messageSize;\n\n            while (remaining > 1)\n            {\n                var chunkSize = Math.Min(buffer.Length, remaining - 1);\n                remaining -= chunkSize;\n                var chunk = buffer.AsMemory(0, chunkSize);\n                rng.NextBytes(chunk.Span);\n                await webSocket.SendAsync(chunk, WebSocketMessageType.Binary, endOfMessage: false);\n            }\n\n            await webSocket.SendAsync(buffer.AsMemory(0, remaining), WebSocketMessageType.Binary, endOfMessage: true);\n        }\n\n        await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, \"Bye\", CancellationToken.None);\n    }\n\n    private class WebSocketAdapter\n    {\n        private readonly ClientWebSocket? _client;\n        private readonly WebSocket? _server;\n\n        public WebSocketAdapter(ClientWebSocket? client = null, WebSocket? server = null)\n        {\n            Assert.True(client is null ^ server is null);\n            _client = client;\n            _server = server;\n        }\n\n        public ValueTask<ValueWebSocketReceiveResult> ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            return _client is not null\n                ? _client.ReceiveAsync(buffer, cancellationToken)\n                : _server!.ReceiveAsync(buffer, cancellationToken);\n        }\n\n        public ValueTask SendAsync(ReadOnlyMemory<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken = default)\n        {\n            return _client is not null\n                ? _client.SendAsync(buffer, messageType, endOfMessage, cancellationToken)\n                : _server!.SendAsync(buffer, messageType, endOfMessage, cancellationToken);\n        }\n\n        public Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken = default)\n        {\n            return _client is not null\n                ? _client.CloseOutputAsync(closeStatus, statusDescription, cancellationToken)\n                : _server!.CloseOutputAsync(closeStatus, statusDescription, cancellationToken);\n        }\n    }\n\n    private static async Task<WebSocketsTelemetry?> TestAsync(Func<Uri, Task> requestDelegate, Func<HttpContext, WebSocketAdapter, Task> destinationDelegate, TimeProvider? timeProvider = null, bool http2Proxy = false)\n    {\n        var telemetryConsumer = new TelemetryConsumer();\n\n        var test = new TestEnvironment()\n        {\n            ConfigureDestinationApp = destinationApp =>\n            {\n                destinationApp.UseWebSockets();\n\n                destinationApp.Run(async context =>\n                {\n                    if (context.WebSockets.IsWebSocketRequest)\n                    {\n                        var webSocket = await context.WebSockets.AcceptWebSocketAsync();\n\n                        await destinationDelegate(context, new WebSocketAdapter(server: webSocket));\n                    }\n                });\n            },\n            ConfigureProxyServices = proxyServices =>\n            {\n                if (timeProvider is not null)\n                {\n                    proxyServices.AddSingleton(timeProvider);\n                }\n            },\n            ConfigureProxy = proxyBuilder =>\n            {\n                proxyBuilder.Services.AddTelemetryConsumer(telemetryConsumer);\n            },\n            ConfigureProxyApp = proxyApp =>\n            {\n                proxyApp.UseWebSocketsTelemetry();\n            },\n        };\n\n        if (http2Proxy)\n        {\n            test.ProxyProtocol = HttpProtocols.Http2;\n        }\n\n        await test.Invoke(async uri =>\n        {\n            var webSocketsTarget = uri.Replace(\"https://\", \"wss://\").Replace(\"http://\", \"ws://\");\n            var webSocketsUri = new Uri(webSocketsTarget, UriKind.Absolute);\n\n            await requestDelegate(webSocketsUri);\n        });\n\n        return telemetryConsumer.Telemetry;\n    }\n\n    private record WebSocketsTelemetry(DateTime Timestamp, DateTime EstablishedTime, WebSocketCloseReason CloseReason, long MessagesRead, long MessagesWritten);\n\n    private class TelemetryConsumer : IWebSocketsTelemetryConsumer\n    {\n        public WebSocketsTelemetry? Telemetry { get; private set; }\n\n        public void OnWebSocketClosed(DateTime timestamp, DateTime establishedTime, WebSocketCloseReason closeReason, long messagesRead, long messagesWritten)\n        {\n            Telemetry = new WebSocketsTelemetry(timestamp, establishedTime, closeReason, messagesRead, messagesWritten);\n        }\n    }\n\n    private static HttpMessageInvoker CreateInvoker()\n    {\n        var handler = new SocketsHttpHandler\n        {\n            AllowAutoRedirect = false,\n            AutomaticDecompression = DecompressionMethods.None,\n            UseCookies = false,\n            UseProxy = false\n        };\n        handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true;\n        return new HttpMessageInvoker(handler);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.FunctionalTests/Yarp.ReverseProxy.FunctionalTests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(TestTFMs)</TargetFrameworks>\n    <NoWarn>$(NoWarn);SYSLIB0057</NoWarn>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>Yarp.ReverseProxy</RootNamespace>\n\n    <!--\n      Arcade test detection looks for known suffixes like '.Tests' or '.UnitTests'\n      so we need to explicitly specify that this is a test project.\n      -->\n    <IsUnitTestProject>true</IsUnitTestProject>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Content Include=\"..\\TestCertificates\\testCert.pfx\" LinkBase=\"TestCertificates\" CopyToOutputDirectory=\"PreserveNewest\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\TelemetryConsumption\\Yarp.Telemetry.Consumption.csproj\" />\n    <ProjectReference Include=\"..\\Tests.Common\\Yarp.Tests.Common.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Compile Include=\"..\\ReverseProxy.Tests\\Common\\TestResources.cs\" Link=\"Common\\TestResources.cs\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.DotNet.XUnitV3Extensions\" Version=\"$(MicrosoftDotNetXUnitV3ExtensionsPackageVersion)\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Common/EventAssertExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics.Tracing;\nusing System.Linq;\nusing Xunit;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.Tests.Common;\n\ninternal static class EventAssertExtensions\n{\n    public static (ForwarderStage Stage, DateTime TimeStamp)[] GetProxyStages(this List<EventWrittenEventArgs> events)\n    {\n        return events\n            .Where(e => e.EventName == \"ForwarderStage\")\n            .Select(e =>\n            {\n                var stage = (ForwarderStage)Assert.Single(e.Payload);\n                Assert.InRange(stage, ForwarderStage.SendAsyncStart, ForwarderStage.ResponseUpgrade);\n                return (stage, e.TimeStamp);\n            })\n            .ToArray();\n    }\n\n    public static void AssertContainProxyStages(this List<EventWrittenEventArgs> events, bool hasRequestContent = true, bool upgrade = false, bool hasResponseContent = true)\n    {\n        var stages = new List<ForwarderStage>()\n        {\n            ForwarderStage.SendAsyncStart,\n            ForwarderStage.SendAsyncStop,\n        };\n\n        if (hasRequestContent)\n        {\n            stages.Add(ForwarderStage.RequestContentTransferStart);\n        }\n\n        if (upgrade)\n        {\n            stages.Add(ForwarderStage.ResponseUpgrade);\n        }\n\n        if (hasResponseContent)\n        {\n            stages.Add(ForwarderStage.ResponseContentTransferStart);\n        }\n\n        events.AssertContainProxyStages(stages.ToArray());\n    }\n\n    public static void AssertContainProxyStages(this List<EventWrittenEventArgs> events, ForwarderStage[] expectedStages)\n    {\n        var proxyStages = events.GetProxyStages()\n            .Select(s => s.Stage)\n            .ToArray();\n\n        var presentStages = proxyStages.ToHashSet();\n\n        Assert.Equal(presentStages.Count, proxyStages.Length);\n\n        foreach (var expectedStage in expectedStages)\n        {\n            Assert.Contains(expectedStage, presentStages);\n        }\n\n        presentStages.RemoveWhere(s => expectedStages.Contains(s));\n\n        Assert.Empty(presentStages);\n    }\n}\n\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Common/HttpContentExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.IO;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Tests.Common;\n\ninternal static class HttpContentExtensions\n{\n    public static Task CopyToWithCancellationAsync(this HttpContent httpContent, Stream stream)\n    {\n        // StreamCopyHttpContent assumes that the cancellation token passed to it can always be canceled.\n        // This is the case for real callers, so we insert a dummy CTS in tests to allow us to keep the debug assertion.\n        return httpContent.CopyToAsync(stream, new CancellationTokenSource().Token);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Common/MockHttpHandler.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Tests.Common;\n\ninternal class MockHttpHandler : HttpMessageHandler\n{\n    private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _func;\n\n    public MockHttpHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> func)\n    {\n        ArgumentNullException.ThrowIfNull(func);\n        _func = func;\n    }\n\n    public static HttpMessageInvoker CreateClient(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> func)\n    {\n        var handler = new MockHttpHandler(func);\n        return new HttpMessageInvoker(handler);\n    }\n\n    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        return _func(request, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Common/TaskExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Runtime.CompilerServices;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Yarp.Tests.Common;\n\n/// <summary>\n/// Extensions for the <see cref=\"Task\"/> class.\n/// </summary>\ninternal static class TaskExtensions\n{\n    public static TimeSpan DefaultTimeoutTimeSpan { get; } = TimeSpan.FromSeconds(5);\n\n    public static Task<T> DefaultTimeout<T>(this ValueTask<T> task)\n    {\n        return task.AsTask().TimeoutAfter(DefaultTimeoutTimeSpan);\n    }\n\n    public static Task DefaultTimeout(this ValueTask task)\n    {\n        return task.AsTask().TimeoutAfter(DefaultTimeoutTimeSpan);\n    }\n\n    public static Task<T> DefaultTimeout<T>(this Task<T> task)\n    {\n        return task.TimeoutAfter(DefaultTimeoutTimeSpan);\n    }\n\n    public static Task DefaultTimeout(this Task task)\n    {\n        return task.TimeoutAfter(DefaultTimeoutTimeSpan);\n    }\n\n    private static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout,\n        [CallerFilePath] string filePath = null,\n        [CallerLineNumber] int lineNumber = default)\n    {\n        // Don't create a timer if the task is already completed\n        // or the debugger is attached\n        if (task.IsCompleted || Debugger.IsAttached)\n        {\n            return await task;\n        }\n\n        try\n        {\n            return await task.WaitAsync(timeout);\n        }\n        catch (TimeoutException ex) when (ex.Source == typeof(TaskExtensions).Namespace)\n        {\n            throw new TimeoutException(CreateMessage(timeout, filePath, lineNumber));\n        }\n    }\n\n    private static async Task TimeoutAfter(this Task task, TimeSpan timeout,\n        [CallerFilePath] string filePath = null,\n        [CallerLineNumber] int lineNumber = default)\n    {\n        // Don't create a timer if the task is already completed\n        // or the debugger is attached\n        if (task.IsCompleted || Debugger.IsAttached)\n        {\n            await task;\n            return;\n        }\n\n        var cts = new CancellationTokenSource();\n        if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)))\n        {\n            cts.Cancel();\n            await task;\n        }\n        else\n        {\n            throw new TimeoutException(CreateMessage(timeout, filePath, lineNumber));\n        }\n    }\n\n    private static string CreateMessage(TimeSpan timeout, string filePath, int lineNumber)\n        => string.IsNullOrEmpty(filePath)\n        ? $\"The operation timed out after reaching the limit of {timeout.TotalMilliseconds}ms.\"\n        : $\"The operation at {filePath}:{lineNumber} timed out after reaching the limit of {timeout.TotalMilliseconds}ms.\";\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Common/TestEventListener.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Diagnostics.Tracing;\nusing System.Threading;\n\nnamespace Yarp.Tests.Common;\n\ninternal static class TestEventListener\n{\n    private static readonly AsyncLocal<List<EventWrittenEventArgs>> _eventsAsyncLocal = new();\n#pragma warning disable IDE0052 // Remove unread private members\n    private static readonly InternalEventListener _listener = new();\n#pragma warning restore IDE0052\n\n    public static List<EventWrittenEventArgs> Collect() => _eventsAsyncLocal.Value ??= [];\n\n    private sealed class InternalEventListener : EventListener\n    {\n        protected override void OnEventSourceCreated(EventSource eventSource)\n        {\n            if (eventSource.Name == \"Yarp.ReverseProxy\")\n            {\n                EnableEvents(eventSource, EventLevel.LogAlways, EventKeywords.All);\n            }\n        }\n\n        protected override void OnEventWritten(EventWrittenEventArgs eventData) =>\n            _eventsAsyncLocal.Value?.Add(eventData);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Common/TestResources.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Runtime.InteropServices;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Threading;\nusing Xunit;\n\nnamespace Yarp.Tests.Common;\n\npublic static class TestResources\n{\n    private const int MutexTimeout = 120 * 1000;\n    private static readonly Mutex importPfxMutex = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?\n        new Mutex(initiallyOwned: false, \"Global\\\\Yarp.ReverseProxy.Tests.Certificates.LoadPfxCertificate\") :\n        null;\n\n    public static X509Certificate2 GetTestCertificate(string certName = \"testCert.pfx\")\n    {\n        // On Windows, applications should not import PFX files in parallel to avoid a known system-level\n        // race condition bug in native code which can cause crashes/corruption of the certificate state.\n        if (importPfxMutex is not null)\n        {\n            Assert.True(importPfxMutex.WaitOne(MutexTimeout), \"Cannot acquire the global certificate mutex.\");\n        }\n\n        try\n        {\n            return new X509Certificate2(GetCertPath(certName), \"testPassword\");\n        }\n        finally\n        {\n            importPfxMutex?.ReleaseMutex();\n        }\n    }\n\n    public static IWebProxy GetTestWebProxy(string address = \"http://localhost:8080\", bool? bypassOnLocal = null, bool? useDefaultCredentials = null)\n    {\n        var webProxy = new WebProxy(new System.Uri(address));\n\n        if (bypassOnLocal is not null)\n        {\n            webProxy.BypassProxyOnLocal = bypassOnLocal.Value;\n        }\n\n        if (useDefaultCredentials is not null)\n        {\n            webProxy.UseDefaultCredentials = useDefaultCredentials.Value;\n        }\n\n        return webProxy;\n    }\n\n    public static string GetCertPath(string fileName)\n    {\n        if (fileName is null)\n        {\n            return null;\n        }\n\n        var basePath = Path.Combine(Directory.GetCurrentDirectory(), \"TestCertificates\");\n        return Path.Combine(basePath, fileName);\n    }\n\n    public static IEnumerable<(string Name, string[] Values)> ParseNameAndValues(string names, string values) =>\n        names.Split(\"; \").Zip(values.Split(\", \")).GroupBy(p => p.First, (k, g) => (Name: k, Values: g.Select(i => i.Second).ToArray()));\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Common/TestTrailersFeature.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Yarp.Tests.Common;\n\ninternal sealed class TestTrailersFeature : IHttpResponseTrailersFeature\n{\n    public IHeaderDictionary Trailers { get; set; } = new HeaderDictionary();\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/ActiveHealthCheckConfigTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Configuration.Tests;\n\npublic class ActiveHealthCheckConfigTests\n{\n    [Fact]\n    public void Equals_Same_Value_Returns_True()\n    {\n        var options1 = new ActiveHealthCheckConfig\n        {\n            Enabled = true,\n            Interval = TimeSpan.FromSeconds(2),\n            Timeout = TimeSpan.FromSeconds(1),\n            Policy = \"Any5xxResponse\",\n            Path = \"/a\",\n        };\n\n        var options2 = new ActiveHealthCheckConfig\n        {\n            Enabled = true,\n            Interval = TimeSpan.FromSeconds(2),\n            Timeout = TimeSpan.FromSeconds(1),\n            Policy = \"any5xxResponse\",\n            Path = \"/a\",\n        };\n\n        var equals = options1.Equals(options2);\n\n        Assert.True(equals);\n        Assert.Equal(options1.GetHashCode(), options2.GetHashCode());\n    }\n\n    [Fact]\n    public void Equals_Different_Value_Returns_False()\n    {\n        var options1 = new ActiveHealthCheckConfig\n        {\n            Enabled = true,\n            Interval = TimeSpan.FromSeconds(2),\n            Timeout = TimeSpan.FromSeconds(1),\n            Policy = \"Any5xxResponse\",\n            Path = \"/a\",\n        };\n\n        var options2 = new ActiveHealthCheckConfig\n        {\n            Enabled = false,\n            Interval = TimeSpan.FromSeconds(4),\n            Timeout = TimeSpan.FromSeconds(2),\n            Policy = \"AnyFailure\",\n            Path = \"/b\",\n        };\n\n        var equals = options1.Equals(options2);\n\n        Assert.False(equals);\n    }\n\n    [Fact]\n    public void Equals_DifferingQueries_Returns_False()\n    {\n        var options1 = new ActiveHealthCheckConfig\n        {\n            Query = \"?key=value1\"\n        };\n\n        var options2 = new ActiveHealthCheckConfig\n        {\n            Query = \"?key=value2\"\n        };\n\n        var equals = options1.Equals(options2);\n\n        Assert.False(equals);\n    }\n\n    [Fact]\n    public void Equals_Second_Null_Returns_False()\n    {\n        var options1 = new ActiveHealthCheckConfig();\n\n        var equals = options1.Equals(null);\n\n        Assert.False(equals);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/ClusterConfigTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Net.Http;\nusing System.Security.Authentication;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Xunit;\nusing Yarp.ReverseProxy.LoadBalancing;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Configuration.Tests;\n\npublic class ClusterConfigTests\n{\n    [Fact]\n    public void Equals_Same_Value_Returns_True()\n    {\n        var config1 = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                {\n                    \"destinationA\",\n                    new DestinationConfig\n                    {\n                        Address = \"https://localhost:10000/destA\",\n                        Health = \"https://localhost:20000/destA\",\n                        Metadata = new Dictionary<string, string> { { \"destA-K1\", \"destA-V1\" }, { \"destA-K2\", \"destA-V2\" } }\n                    }\n                },\n                {\n                    \"destinationB\",\n                    new DestinationConfig\n                    {\n                        Address = \"https://localhost:10000/destB\",\n                        Health = \"https://localhost:20000/destB\",\n                        Metadata = new Dictionary<string, string> { { \"destB-K1\", \"destB-V1\" }, { \"destB-K2\", \"destB-V2\" } }\n                    }\n                }\n            },\n            HealthCheck = new HealthCheckConfig\n            {\n                Passive = new PassiveHealthCheckConfig\n                {\n                    Enabled = true,\n                    Policy = \"FailureRate\",\n                    ReactivationPeriod = TimeSpan.FromMinutes(5)\n                },\n                Active = new ActiveHealthCheckConfig\n                {\n                    Enabled = true,\n                    Interval = TimeSpan.FromSeconds(4),\n                    Timeout = TimeSpan.FromSeconds(6),\n                    Policy = \"Any5xxResponse\",\n                    Path = \"healthCheckPath\"\n                }\n            },\n            LoadBalancingPolicy = LoadBalancingPolicies.Random,\n            SessionAffinity = new SessionAffinityConfig\n            {\n                Enabled = true,\n                FailurePolicy = \"Return503Error\",\n                Policy = \"Cookie\",\n                AffinityKeyName = \"Key1\",\n                Cookie = new SessionAffinityCookieConfig\n                {\n                    Domain = \"localhost\",\n                    Expiration = TimeSpan.FromHours(3),\n                    HttpOnly = true,\n                    IsEssential = true,\n                    MaxAge = TimeSpan.FromDays(1),\n                    Path = \"mypath\",\n                    SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict,\n                    SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest\n                }\n            },\n            HttpClient = new HttpClientConfig\n            {\n                SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12,\n                MaxConnectionsPerServer = 10,\n                DangerousAcceptAnyServerCertificate = true,\n                RequestHeaderEncoding = Encoding.UTF8.WebName,\n                ResponseHeaderEncoding = Encoding.UTF8.WebName\n            },\n            HttpRequest = new ForwarderRequestConfig\n            {\n                ActivityTimeout = TimeSpan.FromSeconds(60),\n                Version = Version.Parse(\"1.0\"),\n                VersionPolicy = HttpVersionPolicy.RequestVersionExact,\n            },\n            Metadata = new Dictionary<string, string> { { \"cluster1-K1\", \"cluster1-V1\" }, { \"cluster1-K2\", \"cluster1-V2\" } }\n        };\n\n        var config2 = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                {\n                    \"destinationA\",\n                    new DestinationConfig\n                    {\n                        Address = \"https://localhost:10000/destA\",\n                        Health = \"https://localhost:20000/destA\",\n                        Metadata = new Dictionary<string, string> { { \"destA-K1\", \"destA-V1\" }, { \"destA-K2\", \"destA-V2\" } }\n                    }\n                },\n                {\n                    \"destinationB\",\n                    new DestinationConfig\n                    {\n                        Address = \"https://localhost:10000/destB\",\n                        Health = \"https://localhost:20000/destB\",\n                        Metadata = new Dictionary<string, string> { { \"destB-K1\", \"destB-V1\" }, { \"destB-K2\", \"destB-V2\" } }\n                    }\n                }\n            },\n            HealthCheck = new HealthCheckConfig\n            {\n                Passive = new PassiveHealthCheckConfig\n                {\n                    Enabled = true,\n                    Policy = \"FailureRate\",\n                    ReactivationPeriod = TimeSpan.FromMinutes(5)\n                },\n                Active = new ActiveHealthCheckConfig\n                {\n                    Enabled = true,\n                    Interval = TimeSpan.FromSeconds(4),\n                    Timeout = TimeSpan.FromSeconds(6),\n                    Policy = \"Any5xxResponse\",\n                    Path = \"healthCheckPath\"\n                }\n            },\n            LoadBalancingPolicy = LoadBalancingPolicies.Random,\n            SessionAffinity = new SessionAffinityConfig\n            {\n                Enabled = true,\n                FailurePolicy = \"Return503Error\",\n                Policy = \"Cookie\",\n                AffinityKeyName = \"Key1\",\n                Cookie = new SessionAffinityCookieConfig\n                {\n                    Domain = \"localhost\",\n                    Expiration = TimeSpan.FromHours(3),\n                    HttpOnly = true,\n                    IsEssential = true,\n                    MaxAge = TimeSpan.FromDays(1),\n                    Path = \"mypath\",\n                    SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict,\n                    SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest\n                }\n            },\n            HttpClient = new HttpClientConfig\n            {\n                SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12,\n                MaxConnectionsPerServer = 10,\n                DangerousAcceptAnyServerCertificate = true,\n                RequestHeaderEncoding = Encoding.UTF8.WebName,\n                ResponseHeaderEncoding = Encoding.UTF8.WebName\n            },\n            HttpRequest = new ForwarderRequestConfig\n            {\n                ActivityTimeout = TimeSpan.FromSeconds(60),\n                Version = Version.Parse(\"1.0\"),\n                VersionPolicy = HttpVersionPolicy.RequestVersionExact,\n            },\n            Metadata = new Dictionary<string, string> { { \"cluster1-K1\", \"cluster1-V1\" }, { \"cluster1-K2\", \"cluster1-V2\" } }\n        };\n\n        var config3 = config1 with { }; // Clone\n\n        Assert.True(config1.Equals(config2));\n        Assert.True(config1.Equals(config3));\n        Assert.Equal(config1.GetHashCode(), config2.GetHashCode());\n        Assert.Equal(config1.GetHashCode(), config3.GetHashCode());\n    }\n\n    [Fact]\n    public void Equals_Different_Value_Returns_False()\n    {\n        var config1 = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                {\n                    \"destinationA\",\n                    new DestinationConfig\n                    {\n                        Address = \"https://localhost:10000/destA\",\n                        Health = \"https://localhost:20000/destA\",\n                        Metadata = new Dictionary<string, string> { { \"destA-K1\", \"destA-V1\" }, { \"destA-K2\", \"destA-V2\" } }\n                    }\n                },\n                {\n                    \"destinationB\",\n                    new DestinationConfig\n                    {\n                        Address = \"https://localhost:10000/destB\",\n                        Health = \"https://localhost:20000/destB\",\n                        Metadata = new Dictionary<string, string> { { \"destB-K1\", \"destB-V1\" }, { \"destB-K2\", \"destB-V2\" } }\n                    }\n                }\n            },\n            HealthCheck = new HealthCheckConfig\n            {\n                Passive = new PassiveHealthCheckConfig\n                {\n                    Enabled = true,\n                    Policy = \"FailureRate\",\n                    ReactivationPeriod = TimeSpan.FromMinutes(5)\n                },\n                Active = new ActiveHealthCheckConfig\n                {\n                    Enabled = true,\n                    Interval = TimeSpan.FromSeconds(4),\n                    Timeout = TimeSpan.FromSeconds(6),\n                    Policy = \"Any5xxResponse\",\n                    Path = \"healthCheckPath\"\n                }\n            },\n            LoadBalancingPolicy = LoadBalancingPolicies.Random,\n            SessionAffinity = new SessionAffinityConfig\n            {\n                Enabled = true,\n                FailurePolicy = \"Return503Error\",\n                Policy = \"Cookie\",\n                AffinityKeyName = \"Key1\",\n                Cookie = new SessionAffinityCookieConfig\n                {\n                    Domain = \"localhost\",\n                    Expiration = TimeSpan.FromHours(3),\n                    HttpOnly = true,\n                    IsEssential = true,\n                    MaxAge = TimeSpan.FromDays(1),\n                    Path = \"mypath\",\n                    SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict,\n                    SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest\n                }\n            },\n            HttpClient = new HttpClientConfig\n            {\n                SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12,\n                MaxConnectionsPerServer = 10,\n                DangerousAcceptAnyServerCertificate = true,\n            },\n            HttpRequest = new ForwarderRequestConfig\n            {\n                ActivityTimeout = TimeSpan.FromSeconds(60),\n                Version = Version.Parse(\"1.0\"),\n                VersionPolicy = HttpVersionPolicy.RequestVersionExact,\n            },\n            Metadata = new Dictionary<string, string> { { \"cluster1-K1\", \"cluster1-V1\" }, { \"cluster1-K2\", \"cluster1-V2\" } }\n        };\n\n        Assert.False(config1.Equals(config1 with { ClusterId = \"different\" }));\n        Assert.False(config1.Equals(config1 with { Destinations = new Dictionary<string, DestinationConfig>() }));\n        Assert.False(config1.Equals(config1 with { HealthCheck = new HealthCheckConfig() }));\n        Assert.False(config1.Equals(config1 with { LoadBalancingPolicy = \"different\" }));\n        Assert.False(config1.Equals(config1 with\n        {\n            SessionAffinity = new SessionAffinityConfig\n            {\n                Enabled = true,\n                FailurePolicy = \"Return503Error\",\n                Policy = \"Cookie\",\n                AffinityKeyName = \"Key1\",\n                Cookie = new SessionAffinityCookieConfig\n                {\n                    Domain = \"localhost\",\n                    Expiration = TimeSpan.FromHours(3),\n                    HttpOnly = true,\n                    IsEssential = true,\n                    MaxAge = TimeSpan.FromDays(1),\n                    Path = \"newpath\",\n                    SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict,\n                    SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest\n                }\n            }\n        }));\n        Assert.False(config1.Equals(config1 with\n        {\n            HttpClient = new HttpClientConfig\n            {\n                SslProtocols = SslProtocols.Tls12,\n                MaxConnectionsPerServer = 10,\n                DangerousAcceptAnyServerCertificate = true,\n            }\n        }));\n        Assert.False(config1.Equals(config1 with { HttpRequest = new ForwarderRequestConfig() { } }));\n        Assert.False(config1.Equals(config1 with { Metadata = null }));\n    }\n\n    [Fact]\n    public void Equals_Second_Null_Returns_False()\n    {\n        var config1 = new ClusterConfig();\n\n        var equals = config1.Equals(null);\n\n        Assert.False(equals);\n    }\n\n    [Fact]\n    public void Cluster_CanBeJsonSerialized()\n    {\n        var cluster1 = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            LoadBalancingPolicy = LoadBalancingPolicies.Random,\n            SessionAffinity = new SessionAffinityConfig\n            {\n                Enabled = true,\n                FailurePolicy = \"Return503Error\",\n                Policy = \"Cookie\",\n                AffinityKeyName = \"Key1\",\n                Cookie = new SessionAffinityCookieConfig\n                {\n                    Domain = \"domain\",\n                    Expiration = TimeSpan.FromDays(1),\n                    HttpOnly = true,\n                    IsEssential = true,\n                    MaxAge = TimeSpan.FromHours(1),\n                    Path = \"/\",\n                    SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified,\n                    SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.None\n                }\n            },\n            HealthCheck = new HealthCheckConfig\n            {\n                Passive = new PassiveHealthCheckConfig\n                {\n                    Enabled = true,\n                    Policy = \"FailureRate\",\n                    ReactivationPeriod = TimeSpan.FromMinutes(5)\n                },\n                Active = new ActiveHealthCheckConfig\n                {\n                    Enabled = true,\n                    Interval = TimeSpan.FromSeconds(4),\n                    Timeout = TimeSpan.FromSeconds(6),\n                    Policy = \"Any5xxResponse\",\n                    Path = \"healthCheckPath\"\n                }\n            },\n            HttpClient = new HttpClientConfig\n            {\n                EnableMultipleHttp2Connections = true,\n                SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12,\n                MaxConnectionsPerServer = 10,\n                DangerousAcceptAnyServerCertificate = true,\n                WebProxy = new WebProxyConfig\n                {\n                    Address = new Uri(\"http://proxy\"),\n                    BypassOnLocal = false,\n                    UseDefaultCredentials = false,\n                }\n            },\n            HttpRequest = new ForwarderRequestConfig\n            {\n                ActivityTimeout = TimeSpan.FromSeconds(60),\n                Version = Version.Parse(\"1.0\"),\n                VersionPolicy = HttpVersionPolicy.RequestVersionExact,\n            },\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                {\n                    \"destinationA\",\n                    new DestinationConfig\n                    {\n                        Address = \"https://localhost:10000/destA\",\n                        Health = \"https://localhost:20000/destA\",\n                        Metadata = new Dictionary<string, string> { { \"destA-K1\", \"destA-V1\" }, { \"destA-K2\", \"destA-V2\" } }\n                    }\n                },\n                {\n                    \"destinationB\",\n                    new DestinationConfig\n                    {\n                        Address = \"https://localhost:10000/destB\",\n                        Health = \"https://localhost:20000/destB\",\n                        Metadata = new Dictionary<string, string> { { \"destB-K1\", \"destB-V1\" }, { \"destB-K2\", \"destB-V2\" } }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string> { { \"cluster1-K1\", \"cluster1-V1\" }, { \"cluster1-K2\", \"cluster1-V2\" } }\n        };\n\n        var json = JsonSerializer.Serialize(cluster1);\n        var cluster2 = JsonSerializer.Deserialize<ClusterConfig>(json);\n\n        Assert.Equal(cluster1.Destinations, cluster2.Destinations);\n        Assert.Equal(cluster1.HealthCheck.Active, cluster2.HealthCheck.Active);\n        Assert.Equal(cluster1.HealthCheck.Passive, cluster2.HealthCheck.Passive);\n        Assert.Equal(cluster1.HealthCheck, cluster2.HealthCheck);\n        Assert.Equal(cluster1.HttpClient, cluster2.HttpClient);\n        Assert.Equal(cluster1.HttpRequest, cluster2.HttpRequest);\n        Assert.Equal(cluster1.Metadata, cluster2.Metadata);\n        Assert.Equal(cluster1.SessionAffinity, cluster2.SessionAffinity);\n        Assert.Equal(cluster1, cluster2);\n    }\n\n    public class TimeSpanConverter : JsonConverter<TimeSpan>\n    {\n        public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n        {\n            return TimeSpan.Parse(reader.GetString(), CultureInfo.InvariantCulture);\n        }\n\n        public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)\n        {\n            writer.WriteStringValue(value.ToString(format: null, CultureInfo.InvariantCulture));\n        }\n    }\n\n    public class VersionConverter : JsonConverter<Version>\n    {\n        public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n        {\n            var versionString = reader.GetString();\n            return Version.Parse(versionString);\n        }\n\n        public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options)\n        {\n            writer.WriteStringValue(value.ToString());\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/ConfigProvider/ConfigurationConfigProviderTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Security.Authentication;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Nodes;\nusing System.Threading.Tasks;\nusing Json.Schema;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Logging;\nusing Moq;\nusing Xunit;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.LoadBalancing;\n\nnamespace Yarp.ReverseProxy.Configuration.ConfigProvider.Tests;\n\npublic class ConfigurationConfigProviderTests\n{\n    private static readonly JsonSchema s_yarpSchema = JsonSchema.FromText(File.ReadAllText(\"ConfigurationSchema.json\"));\n    private static readonly EvaluationOptions s_schemaOptions = new()\n    {\n        OutputFormat = OutputFormat.List,\n        RequireFormatValidation = true\n    };\n\n    #region JSON test configuration\n\n    private readonly ConfigurationSnapshot _validConfigurationData = new ConfigurationSnapshot()\n    {\n        Clusters =\n        {\n            {\n                new ClusterConfig\n                {\n                    ClusterId = \"cluster1\",\n                    Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n                    {\n                        {\n                            \"destinationA\",\n                            new DestinationConfig\n                            {\n                                Address = \"https://localhost:10000/destA\",\n                                Health = \"https://localhost:20000/destA\",\n                                Metadata = new Dictionary<string, string> { { \"destA-K1\", \"destA-V1\" }, { \"destA-K2\", \"destA-V2\" } },\n                                Host = \"localhost\"\n                            }\n                        },\n                        {\n                            \"destinationB\",\n                            new DestinationConfig\n                            {\n                                Address = \"https://localhost:10000/destB\",\n                                Health = \"https://localhost:20000/destB\",\n                                Metadata = new Dictionary<string, string> { { \"destB-K1\", \"destB-V1\" }, { \"destB-K2\", \"destB-V2\" }, { \"foo\", \"42\" }, { \"bar\", \"True\" } },\n                                Host = \"localhost\"\n                            }\n                        }\n                    },\n                    HealthCheck = new HealthCheckConfig\n                    {\n                        Passive = new PassiveHealthCheckConfig\n                        {\n                            Enabled = true,\n                            Policy = \"FailureRate\",\n                            ReactivationPeriod = TimeSpan.FromMinutes(5)\n                        },\n                        Active = new ActiveHealthCheckConfig\n                        {\n                            Enabled = true,\n                            Interval = TimeSpan.FromSeconds(4),\n                            Timeout = TimeSpan.FromSeconds(6),\n                            Policy = \"Any5xxResponse\",\n                            Path = \"healthCheckPath\",\n                            Query = \"?key=value\"\n                        },\n                        AvailableDestinationsPolicy = \"HealthyOrPanic\"\n                    },\n                    LoadBalancingPolicy = LoadBalancingPolicies.Random,\n                    SessionAffinity = new SessionAffinityConfig\n                    {\n                        Enabled = true,\n                        FailurePolicy = \"Return503Error\",\n                        Policy = \"Cookie\",\n                        AffinityKeyName = \"Key1\",\n                        Cookie = new SessionAffinityCookieConfig\n                        {\n                            Domain = \"localhost\",\n                            Expiration = TimeSpan.FromHours(3),\n                            HttpOnly = true,\n                            IsEssential = true,\n                            MaxAge = TimeSpan.FromDays(1),\n                            Path = \"mypath\",\n                            SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict,\n                            SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.None\n                        }\n                    },\n                    HttpClient = new HttpClientConfig\n                    {\n                        SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12,\n                        MaxConnectionsPerServer = 10,\n                        DangerousAcceptAnyServerCertificate = true,\n                        EnableMultipleHttp2Connections = true,\n                    },\n                    HttpRequest = new ForwarderRequestConfig()\n                    {\n                        ActivityTimeout = TimeSpan.FromSeconds(60),\n                        Version = Version.Parse(\"1.0\"),\n                        VersionPolicy = HttpVersionPolicy.RequestVersionExact,\n                        AllowResponseBuffering = true\n                    },\n                    Metadata = new Dictionary<string, string> { { \"cluster1-K1\", \"cluster1-V1\" }, { \"cluster1-K2\", \"cluster1-V2\" } }\n                }\n            },\n            {\n                new ClusterConfig\n                {\n                    ClusterId = \"cluster2\",\n                    Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n                    {\n                        { \"destinationC\", new DestinationConfig { Address = \"https://localhost:10001/destC\", Host = \"localhost\" } },\n                        { \"destinationD\", new DestinationConfig { Address = \"https://localhost:10000/destB\", Host = \"remotehost\" } }\n                    },\n                    LoadBalancingPolicy = LoadBalancingPolicies.RoundRobin\n                }\n            }\n        },\n        Routes =\n        {\n            new RouteConfig\n            {\n                RouteId = \"routeA\",\n                ClusterId = \"cluster1\",\n                AuthorizationPolicy = \"Default\",\n                RateLimiterPolicy = \"Default\",\n                TimeoutPolicy = \"Default\",\n                Timeout = TimeSpan.Zero,\n                CorsPolicy = \"Default\",\n                Order = -1,\n                MaxRequestBodySize = -1,\n                Match = new RouteMatch\n                {\n                    Hosts = new List<string> { \"host-A\" },\n                    Methods = new List<string> { \"GET\", \"POST\", \"DELETE\" },\n                    Path = \"/apis/entities\",\n                    Headers = new[]\n                    {\n                        new RouteHeader\n                        {\n                            Name = \"header1\",\n                            Values = new[] { \"value1\" },\n                            IsCaseSensitive = true,\n                            Mode = HeaderMatchMode.HeaderPrefix\n                        }\n                    },\n                    QueryParameters = new[]\n                    {\n                        new RouteQueryParameter\n                        {\n                            Name = \"queryparam1\",\n                            Values = new[] { \"value1\" },\n                            IsCaseSensitive = true,\n                            Mode = QueryParameterMatchMode.Contains\n                        }\n                    }\n                },\n                Transforms = new[]\n                {\n                    new Dictionary<string, string>\n                    {\n                        { \"RequestHeadersCopy\", \"true\" }\n                    },\n                    new Dictionary<string, string>\n                    {\n                        { \"PathRemovePrefix\", \"/apis\" },\n                    },\n                    new Dictionary<string, string>\n                    {\n                        { \"PathPrefix\", \"/apis\" }\n                    },\n                    new Dictionary<string, string>\n                    {\n                        { \"RequestHeader\", \"header1\" },\n                        { \"Append\", \"foo\" }\n                    }\n                },\n                Metadata = new Dictionary<string, string> { { \"routeA-K1\", \"routeA-V1\" }, { \"routeA-K2\", \"routeA-V2\" } }\n            },\n            new RouteConfig\n            {\n                RouteId = \"routeB\",\n                ClusterId = \"cluster2\",\n                Order = 2,\n                MaxRequestBodySize = 1,\n                Match = new RouteMatch\n                {\n                    Hosts = new List<string> { \"host-B\" },\n                    Methods = new List<string> { \"GET\" },\n                    Path = \"/apis/users\",\n                    Headers = new[]\n                    {\n                        new RouteHeader\n                        {\n                            Name = \"header2\",\n                            Values = new[] { \"value2\" },\n                            IsCaseSensitive = false,\n                            Mode = HeaderMatchMode.ExactHeader\n                        }\n                    },\n                    QueryParameters = new[]\n                    {\n                        new RouteQueryParameter\n                        {\n                            Name = \"queryparam2\",\n                            Values = new[] { \"value2\" },\n                            IsCaseSensitive = true,\n                            Mode = QueryParameterMatchMode.Contains\n                        }\n                    }\n                }\n            }\n        }\n    };\n\n    private const string ValidJsonConfig =\n        \"\"\"\n        {\n            \"Clusters\": {\n                \"cluster1\": {\n                    \"LoadBalancingPolicy\": \"Random\",\n                    \"SessionAffinity\": {\n                        \"Enabled\": true,\n                        \"Policy\": \"Cookie\",\n                        \"FailurePolicy\": \"Return503Error\",\n                        \"AffinityKeyName\": \"Key1\",\n                        \"Cookie\": {\n                            \"Domain\": \"localhost\",\n                            \"Expiration\": \"03:0:00\",\n                            \"HttpOnly\": true,\n                            \"IsEssential\": \"True\",\n                            \"MaxAge\": \"1.00:00:0\",\n                            \"Path\": \"mypath\",\n                            \"SameSite\": \"Strict\",\n                            \"SecurePolicy\": \"None\"\n                        }\n                    },\n                    \"HealthCheck\": {\n                        \"Passive\": {\n                            \"Enabled\": true,\n                            \"Policy\": \"FailureRate\",\n                            \"ReactivationPeriod\": \"0:05:00\"\n                        },\n                        \"Active\": {\n                            \"Enabled\": true,\n                            \"Interval\": \"00:00:04\",\n                            \"Timeout\": \"00:00:06\",\n                            \"Policy\": \"Any5xxResponse\",\n                            \"Path\": \"healthCheckPath\",\n                            \"Query\": \"?key=value\"\n                        },\n                        \"AvailableDestinationsPolicy\": \"HealthyOrPanic\"\n                    },\n                    \"HttpClient\": {\n                        \"SslProtocols\": [\n                            \"Tls11\",\n                            \"Tls12\"\n                        ],\n                        \"DangerousAcceptAnyServerCertificate\": true,\n                        \"MaxConnectionsPerServer\": 10,\n                        \"EnableMultipleHttp2Connections\": true,\n                        \"RequestHeaderEncoding\": \"utf-8\",\n                        \"ResponseHeaderEncoding\": \"utf-8\",\n                        \"WebProxy\": {\n                            \"Address\": \"http://localhost:8080\",\n                            \"BypassOnLocal\": true,\n                            \"UseDefaultCredentials\": true\n                        }\n                    },\n                    \"HttpRequest\": {\n                        \"ActivityTimeout\": \"00:01:00\",\n                        \"Version\": \"1\",\n                        \"VersionPolicy\": \"RequestVersionExact\",\n                        \"AllowResponseBuffering\": true\n                    },\n                    \"Destinations\": {\n                        \"destinationA\": {\n                            \"Address\": \"https://localhost:10000/destA\",\n                            \"Health\": \"https://localhost:20000/destA\",\n                            \"Host\": \"localhost\",\n                            \"Metadata\": {\n                                \"destA-K1\": \"destA-V1\",\n                                \"destA-K2\": \"destA-V2\"\n                            }\n                        },\n                        \"destinationB\": {\n                            \"Address\": \"https://localhost:10000/destB\",\n                            \"Health\": \"https://localhost:20000/destB\",\n                            \"Host\": \"localhost\",\n                            \"Metadata\": {\n                                \"destB-K1\": \"destB-V1\",\n                                \"destB-K2\": \"destB-V2\",\n                                \"foo\": 42,\n                                \"bar\": true\n                            }\n                        }\n                    },\n                    \"Metadata\": {\n                        \"cluster1-K1\": \"cluster1-V1\",\n                        \"cluster1-K2\": \"cluster1-V2\"\n                    }\n                },\n                \"cluster2\": {\n                    \"LoadBalancingPolicy\": \"RoundRobin\",\n                    \"SessionAffinity\": null,\n                    \"HealthCheck\": null,\n                    \"HttpClient\": null,\n                    \"Destinations\": {\n                        \"destinationC\": {\n                            \"Address\": \"https://localhost:10001/destC\",\n                            \"Host\": \"localhost\",\n                            \"Metadata\": null\n                        },\n                        \"destinationD\": {\n                            \"Address\": \"https://localhost:10000/destB\",\n                            \"Host\": \"remotehost\",\n                            \"Metadata\": null\n                        }\n                    },\n                    \"Metadata\": null\n                }\n            },\n            \"Routes\": {\n                \"routeA\" : {\n                    \"Match\": {\n                        \"Methods\": [\n                            \"GET\",\n                            \"POST\",\n                            \"DELETE\"\n                        ],\n                        \"Hosts\": [\n                            \"host-A\"\n                        ],\n                        \"Path\": \"/apis/entities\",\n                        \"Headers\": [\n                          {\n                            \"Name\": \"header1\",\n                            \"Values\": [ \"value1\" ],\n                            \"IsCaseSensitive\": true,\n                            \"Mode\": \"HeaderPrefix\"\n                          }\n                        ],\n                        \"QueryParameters\": [\n                          {\n                            \"Name\": \"queryparam1\",\n                            \"Values\": [ \"value1\" ],\n                            \"IsCaseSensitive\": \"true\",\n                            \"Mode\": \"Contains\"\n                          }\n                        ]\n                    },\n                    \"Order\": -1,\n                    \"MaxRequestBodySize\": -1,\n                    \"ClusterId\": \"cluster1\",\n                    \"AuthorizationPolicy\": \"Default\",\n                    \"RateLimiterPolicy\": \"Default\",\n                    \"OutputCachePolicy\": \"Default\",\n                    \"CorsPolicy\": \"Default\",\n                    \"TimeoutPolicy\": \"Default\",\n                    \"Timeout\": \"00:00:01\",\n                    \"Metadata\": {\n                        \"routeA-K1\": \"routeA-V1\",\n                        \"routeA-K2\": \"routeA-V2\"\n                    },\n                    \"Transforms\": [\n                        {\n                            \"RequestHeadersCopy\": true\n                        },\n                        {\n                            \"PathRemovePrefix\": \"/apis\"\n                        },\n                        {\n                            \"PathPrefix\": \"/apis\"\n                        },\n                        {\n                            \"RequestHeader\": \"header1\",\n                            \"Append\": \"foo\"\n                        }\n                    ]\n                },\n                \"routeB\" : {\n                    \"Match\": {\n                        \"Methods\": [\n                            \"GET\"\n                        ],\n                        \"Hosts\": [\n                            \"host-B\"\n                        ],\n                        \"Path\": \"/apis/users\",\n                        \"Headers\": [\n                          {\n                            \"Name\": \"header2\",\n                            \"Values\": [ \"value2\" ]\n                          }\n                        ],\n                        \"QueryParameters\": [\n                          {\n                            \"Name\": \"queryparam2\",\n                            \"Values\": [ \"value2\" ],\n                            \"IsCaseSensitive\": true,\n                            \"Mode\": \"Contains\"\n                          }\n                        ]\n                    },\n                    \"Order\": 2,\n                    \"MaxRequestBodySize\": 1,\n                    \"ClusterId\": \"cluster2\",\n                    \"AuthorizationPolicy\": null,\n                    \"RateLimiterPolicy\": null,\n                    \"OutputCachePolicy\": null,\n                    \"CorsPolicy\": null,\n                    \"Metadata\": null,\n                    \"Transforms\": null\n                }\n            }\n        }\n        \"\"\";\n\n    #endregion\n\n    private static ConfigurationConfigProvider CreateProvider(string json)\n    {\n        var builder = new ConfigurationBuilder();\n        var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));\n        var configuration = builder.AddJsonStream(stream).Build();\n        var logger = new Mock<ILogger<ConfigurationConfigProvider>>();\n\n        return new ConfigurationConfigProvider(logger.Object, configuration);\n    }\n\n    [Fact]\n    public void GetConfig_ValidSerializedConfiguration_ConvertToAbstractionsSuccessfully()\n    {\n        var provider = CreateProvider(ValidJsonConfig);\n        var abstractConfig = provider.GetConfig();\n\n        VerifyValidAbstractConfig(_validConfigurationData, abstractConfig);\n    }\n\n    [Fact]\n    public void GetConfig_ValidConfiguration_AllAbstractionsPropertiesAreSet()\n    {\n        var provider = CreateProvider(ValidJsonConfig);\n        var abstractConfig = (ConfigurationSnapshot)provider.GetConfig();\n\n        var abstractionsNamespace = typeof(ClusterConfig).Namespace;\n        // Removed incompletely filled out instances.\n        abstractConfig.Clusters = abstractConfig.Clusters.Where(c => c.ClusterId == \"cluster1\").ToList();\n        abstractConfig.Routes = abstractConfig.Routes.Where(r => r.RouteId == \"routeA\").ToList();\n\n        VerifyAllPropertiesAreSet(abstractConfig);\n\n        void VerifyFullyInitialized(object obj, string name)\n        {\n            switch (obj)\n            {\n                case null:\n                    Assert.Fail($\"Property {name} is not initialized.\");\n                    break;\n                case Enum m:\n                    Assert.NotEqual(0, (int)(object)m);\n                    break;\n                case string str:\n                    Assert.NotEmpty(str);\n                    break;\n                case ValueType v:\n                    var equals = Equals(Activator.CreateInstance(v.GetType()), v);\n                    Assert.False(equals, $\"Property {name} is not initialized.\");\n                    if (v.GetType().Namespace == abstractionsNamespace)\n                    {\n                        VerifyAllPropertiesAreSet(v);\n                    }\n                    break;\n                case IDictionary d:\n                    Assert.NotEmpty(d);\n                    foreach (var value in d.Values)\n                    {\n                        VerifyFullyInitialized(value, name);\n                    }\n                    break;\n                case IEnumerable e:\n                    Assert.NotEmpty(e);\n                    foreach (var item in e)\n                    {\n                        VerifyFullyInitialized(item, name);\n                    }\n\n                    var type = e.GetType();\n                    if (!type.IsArray && type.Namespace == abstractionsNamespace)\n                    {\n                        VerifyAllPropertiesAreSet(e);\n                    }\n                    break;\n                case object o:\n                    if (o.GetType().Namespace == abstractionsNamespace)\n                    {\n                        VerifyAllPropertiesAreSet(o);\n                    }\n                    break;\n            }\n        }\n\n        void VerifyAllPropertiesAreSet(object obj)\n        {\n            var properties = obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public).Cast<PropertyInfo>();\n            foreach (var property in properties)\n            {\n                VerifyFullyInitialized(property.GetValue(obj), $\"{property.DeclaringType.Name}.{property.Name}\");\n            }\n        }\n    }\n\n    private void VerifyValidAbstractConfig(IProxyConfig validConfig, IProxyConfig abstractConfig)\n    {\n        Assert.NotNull(abstractConfig);\n        Assert.Equal(2, abstractConfig.Clusters.Count);\n\n        var cluster1 = validConfig.Clusters.First(c => c.ClusterId == \"cluster1\");\n        Assert.Single(abstractConfig.Clusters, c => c.ClusterId == \"cluster1\");\n        var abstractCluster1 = abstractConfig.Clusters.Single(c => c.ClusterId == \"cluster1\");\n        Assert.Equal(cluster1.Destinations[\"destinationA\"].Address, abstractCluster1.Destinations[\"destinationA\"].Address);\n        Assert.Equal(cluster1.Destinations[\"destinationA\"].Health, abstractCluster1.Destinations[\"destinationA\"].Health);\n        Assert.Equal(cluster1.Destinations[\"destinationA\"].Metadata, abstractCluster1.Destinations[\"destinationA\"].Metadata);\n        Assert.Equal(cluster1.Destinations[\"destinationA\"].Host, abstractCluster1.Destinations[\"destinationA\"].Host);\n        Assert.Equal(cluster1.Destinations[\"destinationB\"].Address, abstractCluster1.Destinations[\"destinationB\"].Address);\n        Assert.Equal(cluster1.Destinations[\"destinationB\"].Health, abstractCluster1.Destinations[\"destinationB\"].Health);\n        Assert.Equal(cluster1.Destinations[\"destinationB\"].Metadata, abstractCluster1.Destinations[\"destinationB\"].Metadata);\n        Assert.Equal(cluster1.Destinations[\"destinationB\"].Host, abstractCluster1.Destinations[\"destinationB\"].Host);\n        Assert.Equal(cluster1.HealthCheck.AvailableDestinationsPolicy, abstractCluster1.HealthCheck.AvailableDestinationsPolicy);\n        Assert.Equal(cluster1.HealthCheck.Passive.Enabled, abstractCluster1.HealthCheck.Passive.Enabled);\n        Assert.Equal(cluster1.HealthCheck.Passive.Policy, abstractCluster1.HealthCheck.Passive.Policy);\n        Assert.Equal(cluster1.HealthCheck.Passive.ReactivationPeriod, abstractCluster1.HealthCheck.Passive.ReactivationPeriod);\n        Assert.Equal(cluster1.HealthCheck.Active.Enabled, abstractCluster1.HealthCheck.Active.Enabled);\n        Assert.Equal(cluster1.HealthCheck.Active.Interval, abstractCluster1.HealthCheck.Active.Interval);\n        Assert.Equal(cluster1.HealthCheck.Active.Timeout, abstractCluster1.HealthCheck.Active.Timeout);\n        Assert.Equal(cluster1.HealthCheck.Active.Policy, abstractCluster1.HealthCheck.Active.Policy);\n        Assert.Equal(cluster1.HealthCheck.Active.Path, abstractCluster1.HealthCheck.Active.Path);\n        Assert.Equal(cluster1.HealthCheck.Active.Query, abstractCluster1.HealthCheck.Active.Query);\n        Assert.Equal(LoadBalancingPolicies.Random, abstractCluster1.LoadBalancingPolicy);\n        Assert.Equal(cluster1.SessionAffinity.Enabled, abstractCluster1.SessionAffinity.Enabled);\n        Assert.Equal(cluster1.SessionAffinity.FailurePolicy, abstractCluster1.SessionAffinity.FailurePolicy);\n        Assert.Equal(cluster1.SessionAffinity.Policy, abstractCluster1.SessionAffinity.Policy);\n        Assert.Equal(cluster1.SessionAffinity.AffinityKeyName, abstractCluster1.SessionAffinity.AffinityKeyName);\n        Assert.Equal(cluster1.SessionAffinity.Cookie.Domain, abstractCluster1.SessionAffinity.Cookie.Domain);\n        Assert.Equal(cluster1.SessionAffinity.Cookie.Expiration, abstractCluster1.SessionAffinity.Cookie.Expiration);\n        Assert.Equal(cluster1.SessionAffinity.Cookie.HttpOnly, abstractCluster1.SessionAffinity.Cookie.HttpOnly);\n        Assert.Equal(cluster1.SessionAffinity.Cookie.IsEssential, abstractCluster1.SessionAffinity.Cookie.IsEssential);\n        Assert.Equal(cluster1.SessionAffinity.Cookie.MaxAge, abstractCluster1.SessionAffinity.Cookie.MaxAge);\n        Assert.Equal(cluster1.SessionAffinity.Cookie.Path, abstractCluster1.SessionAffinity.Cookie.Path);\n        Assert.Equal(cluster1.SessionAffinity.Cookie.SameSite, abstractCluster1.SessionAffinity.Cookie.SameSite);\n        Assert.Equal(cluster1.SessionAffinity.Cookie.SecurePolicy, abstractCluster1.SessionAffinity.Cookie.SecurePolicy);\n        Assert.Equal(cluster1.HttpClient.MaxConnectionsPerServer, abstractCluster1.HttpClient.MaxConnectionsPerServer);\n        Assert.Equal(cluster1.HttpClient.EnableMultipleHttp2Connections, abstractCluster1.HttpClient.EnableMultipleHttp2Connections);\n        Assert.Equal(Encoding.UTF8.WebName, abstractCluster1.HttpClient.RequestHeaderEncoding);\n        Assert.Equal(Encoding.UTF8.WebName, abstractCluster1.HttpClient.ResponseHeaderEncoding);\n        Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, abstractCluster1.HttpClient.SslProtocols);\n        Assert.Equal(cluster1.HttpRequest.ActivityTimeout, abstractCluster1.HttpRequest.ActivityTimeout);\n        Assert.Equal(HttpVersion.Version10, abstractCluster1.HttpRequest.Version);\n        Assert.Equal(cluster1.HttpRequest.VersionPolicy, abstractCluster1.HttpRequest.VersionPolicy);\n        Assert.Equal(cluster1.HttpRequest.AllowResponseBuffering, abstractCluster1.HttpRequest.AllowResponseBuffering);\n        Assert.Equal(cluster1.HttpClient.DangerousAcceptAnyServerCertificate, abstractCluster1.HttpClient.DangerousAcceptAnyServerCertificate);\n        Assert.Equal(cluster1.Metadata, abstractCluster1.Metadata);\n\n        var cluster2 = validConfig.Clusters.First(c => c.ClusterId == \"cluster2\");\n        Assert.Single(abstractConfig.Clusters, c => c.ClusterId == \"cluster2\");\n        var abstractCluster2 = abstractConfig.Clusters.Single(c => c.ClusterId == \"cluster2\");\n        Assert.Equal(cluster2.Destinations[\"destinationC\"].Address, abstractCluster2.Destinations[\"destinationC\"].Address);\n        Assert.Equal(cluster2.Destinations[\"destinationC\"].Metadata, abstractCluster2.Destinations[\"destinationC\"].Metadata);\n        Assert.Equal(cluster2.Destinations[\"destinationC\"].Host, abstractCluster2.Destinations[\"destinationC\"].Host);\n        Assert.Equal(cluster2.Destinations[\"destinationD\"].Address, abstractCluster2.Destinations[\"destinationD\"].Address);\n        Assert.Equal(cluster2.Destinations[\"destinationD\"].Metadata, abstractCluster2.Destinations[\"destinationD\"].Metadata);\n        Assert.Equal(cluster2.Destinations[\"destinationD\"].Host, abstractCluster2.Destinations[\"destinationD\"].Host);\n        Assert.Equal(LoadBalancingPolicies.RoundRobin, abstractCluster2.LoadBalancingPolicy);\n\n        Assert.Equal(2, abstractConfig.Routes.Count);\n\n        VerifyRoute(validConfig, abstractConfig, \"routeA\");\n        VerifyRoute(validConfig, abstractConfig, \"routeB\");\n    }\n\n    private void VerifyRoute(IProxyConfig validConfig, IProxyConfig abstractConfig, string routeId)\n    {\n        var route = validConfig.Routes.Single(c => c.RouteId == routeId);\n        Assert.Single(abstractConfig.Routes, r => r.RouteId == routeId);\n        var abstractRoute = abstractConfig.Routes.Single(c => c.RouteId == routeId);\n        Assert.Equal(route.ClusterId, abstractRoute.ClusterId);\n        Assert.Equal(route.Order, abstractRoute.Order);\n        Assert.Equal(route.MaxRequestBodySize, abstractRoute.MaxRequestBodySize);\n        Assert.Equal(route.Match.Hosts, abstractRoute.Match.Hosts);\n        Assert.Equal(route.Match.Methods, abstractRoute.Match.Methods);\n        Assert.Equal(route.Match.Path, abstractRoute.Match.Path);\n        var header = route.Match.Headers.Single();\n        var expectedHeader = abstractRoute.Match.Headers.Single();\n        Assert.Equal(header.Name, expectedHeader.Name);\n        Assert.Equal(header.Mode, expectedHeader.Mode);\n        Assert.Equal(header.IsCaseSensitive, expectedHeader.IsCaseSensitive);\n\n        var queryparam = route.Match.QueryParameters.Single();\n        var expectedQueryParam = abstractRoute.Match.QueryParameters.Single();\n        Assert.Equal(queryparam.Name, expectedQueryParam.Name);\n        Assert.Equal(queryparam.Mode, expectedQueryParam.Mode);\n        Assert.Equal(queryparam.IsCaseSensitive, expectedQueryParam.IsCaseSensitive);\n\n        if (route.Transforms is null)\n        {\n            Assert.Null(abstractRoute.Transforms);\n        }\n        else\n        {\n            Assert.NotNull(abstractRoute.Transforms);\n            Assert.Equal(route.Transforms.Count, abstractRoute.Transforms.Count);\n\n            for (var i = 0; i < route.Transforms.Count; i++)\n            {\n                var transform = route.Transforms[i];\n                var expectedTransform = abstractRoute.Transforms[i];\n\n                Assert.Equal(transform.Count, expectedTransform.Count);\n                foreach (var kv in transform)\n                {\n                    Assert.True(expectedTransform.TryGetValue(kv.Key, out var value));\n\n                    if (value == \"True\")\n                    {\n                        value = \"true\";\n                    }\n\n                    Assert.Equal(kv.Value, value);\n                }\n            }\n        }\n    }\n\n    [Fact]\n    public void ValidateSchema_ValidInput()\n    {\n        var results = s_yarpSchema.Evaluate(JsonNode.Parse(\n            $$\"\"\"\n            {\n              \"ReverseProxy\": {{ValidJsonConfig}}\n            }\n            \"\"\"),\n            s_schemaOptions);\n\n        var errors = results.Details\n            .Where(d => d.HasErrors)\n            .SelectMany(d => d.Errors!.Select(error => $\"Path:${d.InstanceLocation} {error.Key}:{error.Value}\"))\n            .ToArray();\n\n        Assert.True(results.IsValid);\n        Assert.False(results.HasErrors);\n        Assert.True(results.Details.Count > 300);\n    }\n\n    [Fact]\n    public async Task ValidateSchema_Samples()\n    {\n        string repoRoot = Path.Combine(Environment.CurrentDirectory, \"../../../../../\");\n        repoRoot = Path.GetFullPath(repoRoot);\n\n        foreach (string file in Directory.EnumerateFiles(repoRoot, \"*.json\", SearchOption.AllDirectories))\n        {\n            if (file.Contains(\"\\\\obj\\\\\", StringComparison.Ordinal) || file.Contains(\"/obj/\", StringComparison.Ordinal))\n            {\n                continue;\n            }\n\n            if (file.Contains(\"appsettings\", StringComparison.OrdinalIgnoreCase))\n            {\n                var contents = await File.ReadAllTextAsync(file);\n                var document = JsonDocument.Parse(contents, new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip });\n                var results = s_yarpSchema.Evaluate(document.RootElement, s_schemaOptions);\n\n                if (results.IsValid)\n                {\n                    Assert.False(results.HasErrors);\n\n                    if (contents.Contains(\"\\\"ReverseProxy\\\"\", StringComparison.OrdinalIgnoreCase))\n                    {\n                        Assert.True(results.Details.Count > 5, $\"No details for '{file}'\");\n                    }\n                }\n                else\n                {\n                    Assert.Fail($\"Json errors reported for '{file}'\");\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/ConfigProvider/ConfigurationReadingExtensionsTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Xunit;\n\nnamespace Microsoft.Extensions.Configuration.Tests;\n\npublic class ConfigurationReadingExtensionsTests\n{\n    [Fact]\n    public void ReadInt32_NegativeNumber()\n    {\n        var configuration = new ConfigurationBuilder()\n            .AddInMemoryCollection(new Dictionary<string, string>\n            {\n                [\"Key\"] = \"-1\"\n            })\n            .Build();\n\n        var number = configuration.ReadInt32(\"Key\");\n\n        Assert.Equal(-1, number);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/ConfigValidatorTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Cors.Infrastructure;\nusing Microsoft.AspNetCore.RateLimiting;\nusing Microsoft.Extensions.DependencyInjection;\nusing Moq;\nusing Xunit;\nusing Yarp.ReverseProxy.Health;\nusing Yarp.ReverseProxy.LoadBalancing;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Configuration.Tests;\n\npublic class ConfigValidatorTests\n{\n    private IServiceProvider CreateServices(Action<IServiceCollection> configure = null)\n    {\n        var services = new ServiceCollection();\n        services.AddReverseProxy();\n        var passivePolicy = new Mock<IPassiveHealthCheckPolicy>();\n        passivePolicy.SetupGet(p => p.Name).Returns(\"passive0\");\n        services.AddSingleton(passivePolicy.Object);\n        var availableDestinationsPolicy = new Mock<IAvailableDestinationsPolicy>();\n        availableDestinationsPolicy.SetupGet(p => p.Name).Returns(\"availableDestinations0\");\n        services.AddSingleton(availableDestinationsPolicy.Object);\n        services.AddOptions();\n        services.AddLogging();\n        services.AddRouting();\n        configure?.Invoke(services);\n        return services.BuildServiceProvider();\n    }\n\n    [Fact]\n    public void Constructor_Works()\n    {\n        var services = CreateServices();\n        services.GetRequiredService<IConfigValidator>();\n    }\n\n    [Theory]\n    [InlineData(\"example.com\", \"/a/\", null)]\n    [InlineData(\"example.com:80\", \"/a/\", null)]\n    [InlineData(\"\\u00FCnicode\", \"/a/\", null)]\n    [InlineData(\"\\u00FCnicode:443\", \"/a/\", null)]\n    [InlineData(\"example.com\", \"/a/**\", null)]\n    [InlineData(\"example.com\", \"/a/**\", \"GET\")]\n    [InlineData(null, \"/a/\", null)]\n    [InlineData(null, \"/a/**\", \"GET\")]\n    [InlineData(\"example.com\", null, \"get\")]\n    [InlineData(\"example.com\", null, \"gEt,put\")]\n    [InlineData(\"example.com\", null, \"gEt,put,POST,traCE,PATCH,DELETE,Head\")]\n    [InlineData(\"example.com,example2.com\", null, \"get\")]\n    [InlineData(\"*.example.com\", null, null)]\n    [InlineData(\"a-b.example.com\", null, null)]\n    [InlineData(\"a-b.b-c.example.com\", null, null)]\n    public async Task Accepts_ValidRules(string host, string path, string methods)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Hosts = host?.Split(\",\") ?? Array.Empty<string>(),\n                Path = path,\n                Methods = methods?.Split(\",\"),\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [InlineData(\"\")]\n    [InlineData(null)]\n    public async Task Rejects_MissingRouteId(string routeId)\n    {\n        var route = new RouteConfig { RouteId = routeId };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Equals(\"Missing Route Id.\"));\n    }\n\n    [Fact]\n    public async Task Rejects_MissingMatch()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Equals(\"Route 'route1' did not set any match criteria, it requires Hosts or Path specified. Set the Path to '/{**catchall}' to match all requests.\"));\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(\"xn--nicode-2ya\")]\n    [InlineData(\"Xn--nicode-2ya\")]\n    public async Task Rejects_InvalidHost(string host)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Hosts = new[] { host }\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Contains(\"host name\"));\n    }\n\n    [Theory]\n    [InlineData(null, null)]\n    [InlineData(null, \"\")]\n    [InlineData(\"\", null)]\n    [InlineData(\",\", null)]\n    [InlineData(\"\", \"\")]\n    public async Task Rejects_MissingHostAndPath(string host, string path)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch\n            {\n                Hosts = host?.Split(\",\"),\n                Path = path\n            },\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Equals(\"Route 'route1' requires Hosts or Path specified. Set the Path to '/{**catchall}' to match all requests.\"));\n    }\n\n    [Theory]\n    [InlineData(\"/{***a}\")]\n    [InlineData(\"/{\")]\n    [InlineData(\"/}\")]\n    [InlineData(\"/{ab/c}\")]\n    public async Task Rejects_InvalidPath(string path)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch()\n            {\n                Path = path,\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Equals($\"Invalid path '{path}' for route 'route1'.\"));\n    }\n\n    [Theory]\n    [InlineData(\"\")]\n    [InlineData(\"gett\")]\n    public async Task Rejects_InvalidMethod(string methods)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Methods = methods.Split(\",\"),\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Equals($\"Unsupported HTTP method '{methods}' has been set for route 'route1'.\"));\n    }\n\n    [Theory]\n    [InlineData(\"get,GET\")]\n    [InlineData(\"get,post,get\")]\n    public async Task Rejects_DuplicateMethod(string methods)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Methods = methods.Split(\",\"),\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.StartsWith(\"Duplicate HTTP method\"));\n    }\n\n    [Fact]\n    public async Task Accepts_RouteHeader()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Path = \"/\",\n                Headers = new[]\n                {\n                    new RouteHeader()\n                    {\n                        Name = \"header1\",\n                        Values = new[] { \"value1\" },\n                    }\n                },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task Accepts_RouteQueryParameter()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Path = \"/\",\n                QueryParameters = new[]\n                {\n                    new RouteQueryParameter()\n                    {\n                        Name = \"queryparam1\",\n                        Values = new[] { \"value1\" },\n                    }\n                },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task Accepts_RouteHeader_ExistsWithNoValue()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Path = \"/\",\n                Headers = new[]\n                {\n                    new RouteHeader()\n                    {\n                        Name = \"header1\",\n                        Mode = HeaderMatchMode.Exists\n                    }\n                },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task Accepts_RouteHeader_NotExistsWithNoValue()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Path = \"/\",\n                Headers = new[]\n                {\n                    new RouteHeader()\n                    {\n                        Name = \"header1\",\n                        Mode = HeaderMatchMode.NotExists\n                    }\n                },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task Accepts_RouteQueryParameter_ExistsWithNoValue()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Path = \"/\",\n                QueryParameters = new[]\n                {\n                    new RouteQueryParameter()\n                    {\n                        Name = \"queryparam1\",\n                        Mode = QueryParameterMatchMode.Exists\n                    }\n                },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task Rejects_NullRouteHeader()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Path = \"/\",\n                Headers = new RouteHeader[] { null },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        var ex = Assert.Single(result);\n        Assert.Contains(\"A null route header has been set for route\", ex.Message);\n    }\n\n    [Fact]\n    public async Task Rejects_NullRouteQueryParameter()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Path = \"/\",\n                QueryParameters = new RouteQueryParameter[] { null },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        var ex = Assert.Single(result);\n        Assert.Contains(\"A null route query parameter has been set for route\", ex.Message);\n    }\n\n    [Theory]\n    [InlineData(\"\", \"v1\", HeaderMatchMode.ExactHeader, \"A null or empty route header name has been set for route\")]\n    [InlineData(\"h1\", null, HeaderMatchMode.ExactHeader, \"No header values were set on route header\")]\n    [InlineData(\"h1\", \"v1\", HeaderMatchMode.Exists, \"Header values were set when using mode 'Exists'\")]\n    [InlineData(\"h1\", \"v1\", HeaderMatchMode.NotExists, \"Header values were set when using mode 'NotExists'\")]\n    public async Task Rejects_InvalidRouteHeader(string name, string value, HeaderMatchMode mode, string error)\n    {\n        var routeHeader = new RouteHeader()\n        {\n            Name = name,\n            Mode = mode,\n            Values = value is null ? null : new[] { value },\n        };\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Path = \"/\",\n                Headers = new[] { routeHeader },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        var ex = Assert.Single(result);\n        Assert.Contains(error, ex.Message);\n    }\n\n    [Theory]\n    [InlineData(\"\", \"v1\", QueryParameterMatchMode.Exact, \"A null or empty route query parameter name has been set for route\")]\n    [InlineData(\"h1\", null, QueryParameterMatchMode.Exact, \"No query parameter values were set on route query parameter\")]\n    [InlineData(\"h1\", \"v1\", QueryParameterMatchMode.Exists, \"Query parameter values where set when using mode 'Exists'\")]\n    public async Task Rejects_InvalidRouteQueryParameter(string name, string value, QueryParameterMatchMode mode, string error)\n    {\n        var routeQueryParameter = new RouteQueryParameter()\n        {\n            Name = name,\n            Mode = mode,\n            Values = value is null ? null : new[] { value },\n        };\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Path = \"/\",\n                QueryParameters = new[] { routeQueryParameter },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        var ex = Assert.Single(result);\n        Assert.Contains(error, ex.Message);\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(\"defaulT\")]\n    public async Task Accepts_ReservedAuthorizationPolicy(string policy)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            AuthorizationPolicy = policy,\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"localhost\" },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task Accepts_CustomAuthorizationPolicy()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            AuthorizationPolicy = \"custom\",\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"localhost\" },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices(services =>\n        {\n            services.AddAuthorization(options =>\n            {\n                options.AddPolicy(\"custom\", builder => builder.RequireAuthenticatedUser());\n            });\n        });\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task Rejects_UnknownAuthorizationPolicy()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            AuthorizationPolicy = \"unknown\",\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch(),\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Equals(\"Authorization policy 'unknown' not found for route 'route1'.\"));\n    }\n\n    [Theory]\n    [InlineData(\"Default\")]\n    [InlineData(\"Anonymous\")]\n    public async Task Rejects_ReservedAuthorizationPolicyIsUsed(string authorizationPolicy)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            AuthorizationPolicy = authorizationPolicy,\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch(),\n        };\n\n        var services = CreateServices(serviceCollection =>\n        {\n            serviceCollection.AddAuthorization(options =>\n            {\n                options.AddPolicy(authorizationPolicy, builder =>\n                {\n                    builder.RequireAuthenticatedUser();\n                });\n            });\n        });\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Equals($\"The application has registered an authorization policy named '{authorizationPolicy}' that conflicts with the reserved authorization policy name used on this route. The registered policy name needs to be changed for this route to function.\"));\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(\"disAble\")]\n    public async Task Accepts_ReservedTimeoutPolicy(string policy)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            TimeoutPolicy = policy,\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"localhost\" },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task Accepts_CustomTimeoutPolicy()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            TimeoutPolicy = \"custom\",\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"localhost\" },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices(services =>\n        {\n            services.AddRequestTimeouts(options =>\n            {\n                options.AddPolicy(\"custom\", TimeSpan.FromSeconds(1));\n            });\n        });\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task Accepts_CustomTimeout()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Timeout = TimeSpan.FromSeconds(1),\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"localhost\" },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task Rejects_UnknownTimeoutPolicy()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            TimeoutPolicy = \"unknown\",\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch(),\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Equals(\"Timeout policy 'unknown' not found for route 'route1'.\"));\n    }\n\n    [Theory]\n    [InlineData(0)]\n    [InlineData(-1)]\n    public async Task Rejects_InvalidTimeouts(int timeout)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Timeout = TimeSpan.FromMilliseconds(timeout),\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch(),\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Equals($\"The Timeout value '{TimeSpan.FromMilliseconds(timeout)}' is invalid for route 'route1'. The Timeout must be greater than zero milliseconds.\"));\n    }\n\n    [Fact]\n    public async Task Rejects_TimeoutWithTimeoutPolicy()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            TimeoutPolicy = \"unknown\",\n            Timeout = TimeSpan.FromSeconds(1),\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch(),\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Equals(\"Timeout policy 'unknown' not found for route 'route1'.\"));\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(\"defaulT\")]\n    [InlineData(\"disAble\")]\n    public async Task Accepts_ReservedCorsPolicy(string policy)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            CorsPolicy = policy,\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"localhost\" },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task Accepts_CustomCorsPolicy()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            CorsPolicy = \"custom\",\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"localhost\" },\n            },\n            ClusterId = \"cluster1\",\n        };\n\n        var services = CreateServices(services =>\n        {\n            services.AddCors(options =>\n            {\n                options.AddPolicy(\"custom\", new CorsPolicy());\n            });\n        });\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task Rejects_UnknownCorsPolicy()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            CorsPolicy = \"unknown\",\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch(),\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Equals(\"CORS policy 'unknown' not found for route 'route1'.\"));\n    }\n\n    [Theory]\n    [InlineData(\"Default\")]\n    [InlineData(\"Disable\")]\n    public async Task Rejects_ReservedCorsPolicyIsUsed(string corsPolicy)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            CorsPolicy = corsPolicy,\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"localhost\" },\n            },\n        };\n\n        var services = CreateServices(serviceCollection =>\n        {\n            serviceCollection.AddCors(options =>\n            {\n                options.AddPolicy(corsPolicy, builder =>\n                {\n\n                });\n            });\n        });\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Equals($\"The application has registered a CORS policy named '{corsPolicy}' that conflicts with the reserved CORS policy name used on this route. The registered policy name needs to be changed for this route to function.\"));\n    }\n\n    [Theory]\n    [InlineData(\"Default\")]\n    [InlineData(\"Disable\")]\n    public async Task Accepts_BuiltInRateLimiterPolicy(string rateLimiterPolicy)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"localhost\" },\n            },\n            ClusterId = \"cluster1\",\n            RateLimiterPolicy = rateLimiterPolicy\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [InlineData(\"Default\")]\n    [InlineData(\"Disable\")]\n    public async Task Reports_BuildInRateLimiterPolicyNameConflict(string rateLimiterPolicy)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"localhost\" },\n            },\n            ClusterId = \"cluster1\",\n            RateLimiterPolicy = rateLimiterPolicy\n        };\n\n        var services = CreateServices(s =>\n        {\n            s.AddRateLimiter(o => o.AddConcurrencyLimiter(rateLimiterPolicy, c => { }));\n        });\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Contains($\"The application has registered a RateLimiter policy named '{rateLimiterPolicy}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function.\"));\n    }\n\n    [Theory]\n    [InlineData(\"NotAPolicy\")]\n    public async Task Rejects_InvalidRateLimiterPolicy(string rateLimiterPolicy)\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"localhost\" },\n            },\n            ClusterId = \"cluster1\",\n            RateLimiterPolicy = rateLimiterPolicy\n        };\n\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var result = await validator.ValidateRouteAsync(route);\n\n        Assert.NotEmpty(result);\n        Assert.Contains(result, err => err.Message.Contains($\"RateLimiter policy '{rateLimiterPolicy}' not found\"));\n    }\n\n    [Fact]\n    public async Task EmptyCluster_Works()\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Empty(errors);\n    }\n\n    [Fact]\n    public async Task DestinationAddress_Works()\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase) {\n                { \"destination1\", new DestinationConfig { Address = \"https://localhost:1234\" } }\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Empty(errors);\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    public async Task DestinationAddressInvalid_Fails(string address)\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase) {\n                { \"destination1\", new DestinationConfig { Address = address } }\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        var ex = Assert.Single(errors);\n        Assert.Equal(\"No address found for destination 'destination1' on cluster 'cluster1'.\", ex.Message);\n    }\n\n    [Fact]\n    public async Task LoadBalancingPolicy_KnownPolicy_Works()\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            LoadBalancingPolicy = LoadBalancingPolicies.RoundRobin\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Empty(errors);\n    }\n\n    [Fact]\n    public async Task LoadBalancingPolicy_UnknownPolicy_Fails()\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            LoadBalancingPolicy = \"MyCustomPolicy\"\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        var ex = Assert.Single(errors);\n        Assert.Equal(\"No matching ILoadBalancingPolicy found for the load balancing policy 'MyCustomPolicy' set on the cluster 'cluster1'.\", ex.Message);\n    }\n\n    [Fact]\n    public async Task EnableSessionAffinity_Works()\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            SessionAffinity = new SessionAffinityConfig\n            {\n                Enabled = true,\n                AffinityKeyName = \"SomeKey\"\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Empty(errors);\n    }\n\n    [Fact]\n    public async Task EnableSessionAffinity_InvalidPolicy_Fails()\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            SessionAffinity = new SessionAffinityConfig\n            {\n                Enabled = true,\n                FailurePolicy = \"Invalid\",\n                AffinityKeyName = \"SomeKey\"\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        var ex = Assert.Single(errors);\n        Assert.Equal(\"No matching IAffinityFailurePolicy found for the affinity failure policy name 'Invalid' set on the cluster 'cluster1'.\", ex.Message);\n    }\n\n    [Theory]\n    [InlineData(\"\")]\n    [InlineData(null)]\n    public async Task EnableSessionAffinity_AffinityIsNotSet_Fails(string key)\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            SessionAffinity = new SessionAffinityConfig\n            {\n                Enabled = true,\n                AffinityKeyName = key\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        var ex = Assert.Single(errors);\n        Assert.Equal(\"Affinity key name set on the cluster 'cluster1' must not be null.\", ex.Message);\n    }\n\n    [Fact]\n    public async Task Accepts_RequestVersion_Null()\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            HttpRequest = new ForwarderRequestConfig\n            {\n                Version = null,\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Empty(errors);\n    }\n\n    [Theory]\n    [InlineData(1, 0)]\n    [InlineData(1, 1)]\n    [InlineData(2, 0)]\n    public async Task Accepts_RequestVersion(int major, int minor)\n    {\n        var version = new Version(major, minor);\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            HttpRequest = new ForwarderRequestConfig\n            {\n                Version = version,\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Empty(errors);\n    }\n\n    [Theory]\n    [InlineData(1, 9)]\n    [InlineData(2, 5)]\n    [InlineData(3, 1)]\n    public async Task Rejects_RequestVersion(int major, int minor)\n    {\n        var version = new Version(major, minor);\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            HttpRequest = new ForwarderRequestConfig\n            {\n                Version = version,\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Single(errors);\n        Assert.Equal($\"Outgoing request version '{cluster.HttpRequest.Version}' is not any of supported HTTP versions (1.0, 1.1, 2 and 3).\", errors[0].Message);\n        Assert.IsType<ArgumentException>(errors[0]);\n    }\n\n    [Theory]\n    [InlineData(null, null, null, null)]\n    [InlineData(null, null, null, \"\")]\n    [InlineData(null, null, null, \"ConsecutiveFailures\")]\n    [InlineData(25, null, null, \"ConsecutiveFailures\")]\n    [InlineData(25, 10, null, \"ConsecutiveFailures\")]\n    [InlineData(25, 10, \"/api/health\", \"ConsecutiveFailures\")]\n    public async Task EnableActiveHealthCheck_Works(int? interval, int? timeout, string path, string policy)\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            HealthCheck = new HealthCheckConfig\n            {\n                Active = new ActiveHealthCheckConfig\n                {\n                    Enabled = true,\n                    Interval = interval is not null ? TimeSpan.FromSeconds(interval.Value) : (TimeSpan?)null,\n                    Path = path,\n                    Policy = policy,\n                    Timeout = timeout is not null ? TimeSpan.FromSeconds(timeout.Value) : (TimeSpan?)null\n                }\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Empty(errors);\n    }\n\n    [Theory]\n    [InlineData(-1, null, \"ConsecutiveFailures\", \"Destination probing interval\")]\n    [InlineData(null, -1, \"ConsecutiveFailures\", \"Destination probing timeout\")]\n    [InlineData(null, null, \"NonExistingPolicy\", \"No matching IActiveHealthCheckPolicy found for the active health check policy\")]\n    public async Task EnableActiveHealthCheck_InvalidParameter_ErrorReturned(int? interval, int? timeout, string policy, string expectedError)\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            HealthCheck = new HealthCheckConfig\n            {\n                Active = new ActiveHealthCheckConfig\n                {\n                    Enabled = true,\n                    Interval = interval is not null ? TimeSpan.FromSeconds(interval.Value) : (TimeSpan?)null,\n                    Policy = policy,\n                    Timeout = timeout is not null ? TimeSpan.FromSeconds(timeout.Value) : (TimeSpan?)null\n                }\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Single(errors);\n        Assert.Contains(expectedError, errors[0].Message);\n        Assert.IsType<ArgumentException>(errors[0]);\n    }\n\n    [Theory]\n    [InlineData(null, null)]\n    [InlineData(null, \"\")]\n    [InlineData(null, \"passive0\")]\n    [InlineData(25, \"passive0\")]\n    public async Task EnablePassiveHealthCheck_Works(int? reactivationPeriod, string policy)\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            HealthCheck = new HealthCheckConfig\n            {\n                Passive = new PassiveHealthCheckConfig\n                {\n                    Enabled = true,\n                    Policy = policy,\n                    ReactivationPeriod = reactivationPeriod is not null ? TimeSpan.FromSeconds(reactivationPeriod.Value) : (TimeSpan?)null\n                }\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Empty(errors);\n    }\n\n    [Theory]\n    [InlineData(-1, \"passive0\", \"Unhealthy destination reactivation period\")]\n    [InlineData(1, \"NonExistingPolicy\", \"No matching IPassiveHealthCheckPolicy found for the passive health check policy\")]\n    public async Task EnablePassiveHealthCheck_InvalidParameter_ErrorReturned(int? reactivationPeriod, string policy, string expectedError)\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            HealthCheck = new HealthCheckConfig\n            {\n                Passive = new PassiveHealthCheckConfig\n                {\n                    Enabled = true,\n                    Policy = policy,\n                    ReactivationPeriod = reactivationPeriod is not null ? TimeSpan.FromSeconds(reactivationPeriod.Value) : (TimeSpan?)null\n                }\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Single(errors);\n        Assert.Contains(expectedError, errors[0].Message);\n        Assert.IsType<ArgumentException>(errors[0]);\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(\"availableDestinations0\")]\n    public async Task SetAvailableDestinationsPolicy_Works(string policy)\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            HealthCheck = new HealthCheckConfig\n            {\n                AvailableDestinationsPolicy = policy\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Empty(errors);\n    }\n\n    [Fact]\n    public async Task SetAvailableDestinationsPolicy_Invalid()\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n        const string policy = \"Unknown1\";\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            HealthCheck = new HealthCheckConfig\n            {\n                AvailableDestinationsPolicy = policy\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        const string expectedError = \"No matching IAvailableDestinationsPolicy found for the available destinations policy 'Unknown1' set on the cluster.\";\n        Assert.Single(errors);\n        Assert.Contains(expectedError, errors[0].Message);\n        Assert.IsType<ArgumentException>(errors[0]);\n    }\n\n    [Fact]\n    public async Task HttpClient_RequestHeaderEncoding_Valid()\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            HttpClient = new HttpClientConfig\n            {\n                RequestHeaderEncoding = \"utf-8\"\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Empty(errors);\n    }\n\n    [Fact]\n    public async Task HttpClient_RequestHeaderEncoding_Invalid()\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            HttpClient = new HttpClientConfig\n            {\n                RequestHeaderEncoding = \"base64\"\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Single(errors);\n        Assert.Equal(\"Invalid request header encoding 'base64'.\", errors[0].Message);\n    }\n\n    [Fact]\n    public async Task HttpClient_ResponseHeaderEncoding_Valid()\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            HttpClient = new HttpClientConfig\n            {\n                ResponseHeaderEncoding = \"utf-8\"\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Empty(errors);\n    }\n\n    [Fact]\n    public async Task HttpClient_ResponseHeaderEncoding_Invalid()\n    {\n        var services = CreateServices();\n        var validator = services.GetRequiredService<IConfigValidator>();\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            HttpClient = new HttpClientConfig\n            {\n                ResponseHeaderEncoding = \"base64\"\n            }\n        };\n\n        var errors = await validator.ValidateClusterAsync(cluster);\n\n        Assert.Single(errors);\n        Assert.Equal(\"Invalid response header encoding 'base64'.\", errors[0].Message);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/DestinationConfigTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Configuration.Tests;\n\npublic class DestinationConfigTests\n{\n    [Fact]\n    public void Equals_Same_Value_Returns_True()\n    {\n        var options1 = new DestinationConfig\n        {\n            Address = \"https://localhost:10000/destA\",\n            Health = \"https://localhost:20000/destA\",\n            Metadata = new Dictionary<string, string> { { \"destA-K1\", \"destA-V1\" }, { \"destA-K2\", \"destA-V2\" } }\n        };\n\n        var options2 = new DestinationConfig\n        {\n            Address = \"https://localhost:10000/DestA\",\n            Health = \"https://localhost:20000/DestA\",\n            Metadata = new Dictionary<string, string> { { \"destA-K1\", \"destA-V1\" }, { \"destA-K2\", \"destA-V2\" } }\n        };\n\n        var options3 = options1 with { }; // Clone\n\n        Assert.True(options1.Equals(options2));\n        Assert.True(options1.Equals(options3));\n        Assert.Equal(options1.GetHashCode(), options2.GetHashCode());\n        Assert.Equal(options1.GetHashCode(), options3.GetHashCode());\n    }\n\n    [Fact]\n    public void Equals_Different_Value_Returns_False()\n    {\n\n        var options1 = new DestinationConfig\n        {\n            Address = \"https://localhost:10000/destA\",\n            Health = \"https://localhost:20000/destA\",\n            Metadata = new Dictionary<string, string> { { \"destA-K1\", \"destA-V1\" }, { \"destA-K2\", \"destA-V2\" } }\n        };\n\n        Assert.False(options1.Equals(options1 with { Address = \"different\" }));\n        Assert.False(options1.Equals(options1 with { Health = null }));\n        Assert.False(options1.Equals(options1 with\n        {\n            Metadata = new Dictionary<string, string>\n            {\n                { \"destB-K1\", \"destB-V1\" }, { \"destB-K2\", \"destB-V2\" }\n            }\n        }));\n    }\n\n    [Fact]\n    public void Equals_Second_Null_Returns_False()\n    {\n        var options1 = new DestinationConfig();\n\n        var equals = options1.Equals(null);\n\n        Assert.False(equals);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/HealthCheckConfigTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Configuration.Tests;\n\npublic class HealthCheckConfigTests\n{\n    [Fact]\n    public void Equals_Same_Value_Returns_True()\n    {\n        var options1 = new HealthCheckConfig\n        {\n            Active = new ActiveHealthCheckConfig\n            {\n                Enabled = true,\n                Interval = TimeSpan.FromSeconds(2),\n                Timeout = TimeSpan.FromSeconds(1),\n                Policy = \"Any5xxResponse\",\n                Path = \"/a\",\n            },\n            Passive = new PassiveHealthCheckConfig\n            {\n                Enabled = true,\n                Policy = \"Passive\",\n                ReactivationPeriod = TimeSpan.FromSeconds(5),\n            }\n        };\n\n        var options2 = new HealthCheckConfig\n        {\n            Active = new ActiveHealthCheckConfig\n            {\n                Enabled = true,\n                Interval = TimeSpan.FromSeconds(2),\n                Timeout = TimeSpan.FromSeconds(1),\n                Policy = \"any5xxResponse\",\n                Path = \"/a\",\n            },\n            Passive = new PassiveHealthCheckConfig\n            {\n                Enabled = true,\n                Policy = \"passive\",\n                ReactivationPeriod = TimeSpan.FromSeconds(5),\n            }\n        };\n\n        var equals = options1.Equals(options2);\n\n        Assert.True(equals);\n        Assert.Equal(options1.GetHashCode(), options2.GetHashCode());\n    }\n\n    [Fact]\n    public void Equals_Different_Value_Returns_False()\n    {\n        var options1 = new HealthCheckConfig\n        {\n            Active = new ActiveHealthCheckConfig\n            {\n                Enabled = true,\n                Interval = TimeSpan.FromSeconds(2),\n                Timeout = TimeSpan.FromSeconds(1),\n                Policy = \"Any5xxResponse\",\n                Path = \"/a\",\n            },\n            Passive = new PassiveHealthCheckConfig\n            {\n                Enabled = true,\n                Policy = \"Passive\",\n                ReactivationPeriod = TimeSpan.FromSeconds(5),\n            }\n        };\n\n        var options2 = new HealthCheckConfig\n        {\n            Active = new ActiveHealthCheckConfig\n            {\n                Enabled = true,\n                Interval = TimeSpan.FromSeconds(2),\n                Timeout = TimeSpan.FromSeconds(1),\n                Policy = \"Different\",\n                Path = \"/a\",\n            },\n            Passive = new PassiveHealthCheckConfig\n            {\n                Enabled = true,\n                Policy = \"Passive\",\n                ReactivationPeriod = TimeSpan.FromSeconds(5),\n            }\n        };\n\n        var equals = options1.Equals(options2);\n\n        Assert.False(equals);\n    }\n\n    [Fact]\n    public void Equals_Second_Null_Returns_False()\n    {\n        var options1 = new HealthCheckConfig();\n\n        var equals = options1.Equals(null);\n\n        Assert.False(equals);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/HttpClientConfigTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Security.Authentication;\nusing System.Text;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Configuration.Tests;\n\npublic class HttpClientConfigTests\n{\n    [Fact]\n    public void Equals_Same_Value_Returns_True()\n    {\n        var options1 = new HttpClientConfig\n        {\n            SslProtocols = SslProtocols.Tls11,\n            DangerousAcceptAnyServerCertificate = false,\n            MaxConnectionsPerServer = 20,\n            WebProxy = new WebProxyConfig() { Address = new Uri(\"http://localhost:8080\"), BypassOnLocal = true, UseDefaultCredentials = true },\n            RequestHeaderEncoding = Encoding.UTF8.WebName,\n            ResponseHeaderEncoding = Encoding.UTF8.WebName,\n        };\n\n        var options2 = new HttpClientConfig\n        {\n            SslProtocols = SslProtocols.Tls11,\n            DangerousAcceptAnyServerCertificate = false,\n            MaxConnectionsPerServer = 20,\n            WebProxy = new WebProxyConfig() { Address = new Uri(\"http://localhost:8080\"), BypassOnLocal = true, UseDefaultCredentials = true },\n            RequestHeaderEncoding = Encoding.UTF8.WebName,\n            ResponseHeaderEncoding = Encoding.UTF8.WebName,\n        };\n\n        var equals = options1.Equals(options2);\n\n        Assert.True(equals);\n        Assert.True(options1 == options2);\n        Assert.False(options1 != options2);\n        Assert.Equal(options1.GetHashCode(), options2.GetHashCode());\n    }\n\n    [Fact]\n    public void Equals_Different_Value_Returns_False()\n    {\n        var options1 = new HttpClientConfig\n        {\n            SslProtocols = SslProtocols.Tls11,\n            DangerousAcceptAnyServerCertificate = false,\n            MaxConnectionsPerServer = 20,\n            RequestHeaderEncoding = Encoding.UTF8.WebName,\n        };\n\n        var options2 = new HttpClientConfig\n        {\n            SslProtocols = SslProtocols.Tls12,\n            DangerousAcceptAnyServerCertificate = true,\n            MaxConnectionsPerServer = 20,\n            RequestHeaderEncoding = Encoding.Latin1.WebName,\n        };\n\n        var equals = options1.Equals(options2);\n\n        Assert.False(equals);\n    }\n\n    [Fact]\n    public void Equals_Same_WebProxyAddress_Returns_True()\n    {\n        var options1 = new HttpClientConfig\n        {\n            WebProxy = new WebProxyConfig() { Address = new Uri(\"http://localhost:8080\"), BypassOnLocal = true, UseDefaultCredentials = true }\n        };\n\n        var options2 = new HttpClientConfig\n        {\n            WebProxy = new WebProxyConfig() { Address = new Uri(\"http://localhost:8080\"), BypassOnLocal = true, UseDefaultCredentials = true }\n        };\n\n        var equals = options1.Equals(options2);\n\n        Assert.True(equals);\n        Assert.True(options1 == options2);\n        Assert.False(options1 != options2);\n    }\n\n    [Fact]\n    public void Equals_Different_WebProxyAddress_Returns_False()\n    {\n        var options1 = new HttpClientConfig\n        {\n            WebProxy = new WebProxyConfig() { Address = new Uri(\"http://localhost:8080\"), BypassOnLocal = true, UseDefaultCredentials = true }\n        };\n\n        var options2 = new HttpClientConfig\n        {\n            WebProxy = new WebProxyConfig() { Address = new Uri(\"http://localhost:9999\"), BypassOnLocal = true, UseDefaultCredentials = true }\n        };\n\n        var equals = options1.Equals(options2);\n\n        Assert.False(equals);\n        Assert.True(options1 != options2);\n        Assert.False(options1 == options2);\n    }\n\n    [Fact]\n    public void Equals_Second_Null_Returns_False()\n    {\n        var options1 = new HttpClientConfig\n        {\n            SslProtocols = SslProtocols.Tls11,\n            DangerousAcceptAnyServerCertificate = false,\n            MaxConnectionsPerServer = 20\n        };\n\n        var equals = options1.Equals(null);\n\n        Assert.False(equals);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/PassiveHealthCheckConfigTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Configuration.Tests;\n\npublic class PassiveHealthCheckConfigTests\n{\n    [Fact]\n    public void Equals_Same_Value_Returns_True()\n    {\n        var options1 = new PassiveHealthCheckConfig\n        {\n            Enabled = true,\n            Policy = \"Passive\",\n            ReactivationPeriod = TimeSpan.FromSeconds(5),\n        };\n\n        var options2 = new PassiveHealthCheckConfig\n        {\n            Enabled = true,\n            Policy = \"passive\",\n            ReactivationPeriod = TimeSpan.FromSeconds(5),\n        };\n\n        var equals = options1.Equals(options2);\n\n        Assert.True(equals);\n        Assert.Equal(options1.GetHashCode(), options2.GetHashCode());\n    }\n\n    [Fact]\n    public void Equals_Different_Value_Returns_False()\n    {\n        var options1 = new PassiveHealthCheckConfig\n        {\n            Enabled = true,\n            Policy = \"Passive\",\n            ReactivationPeriod = TimeSpan.FromSeconds(5),\n        };\n\n        var options2 = new PassiveHealthCheckConfig\n        {\n            Enabled = false,\n            Policy = \"Passive\",\n            ReactivationPeriod = TimeSpan.FromSeconds(1),\n        };\n\n        var equals = options1.Equals(options2);\n\n        Assert.False(equals);\n    }\n\n    [Fact]\n    public void Equals_Second_Null_Returns_False()\n    {\n        var options1 = new PassiveHealthCheckConfig();\n\n        var equals = options1.Equals(null);\n\n        Assert.False(equals);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/RouteConfigTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Configuration.Tests;\n\npublic class RouteConfigTests\n{\n    [Fact]\n    public void Equals_Positive()\n    {\n        var a = new RouteConfig()\n        {\n            AuthorizationPolicy = \"a\",\n            RateLimiterPolicy = \"rl\",\n            TimeoutPolicy = \"t\",\n            Timeout = TimeSpan.FromSeconds(1),\n            ClusterId = \"c\",\n            CorsPolicy = \"co\",\n            Match = new RouteMatch()\n            {\n                Headers = new[]\n                {\n                    new RouteHeader()\n                    {\n                        Name = \"Hi\",\n                        Values = new[] { \"v1\", \"v2\" },\n                        IsCaseSensitive = true,\n                        Mode = HeaderMatchMode.HeaderPrefix,\n                    }\n                },\n                Hosts = new[] { \"foo:90\" },\n                Methods = new[] { \"GET\", \"POST\" },\n                Path = \"/p\",\n            },\n            Metadata = new Dictionary<string, string>()\n            {\n                { \"m\", \"m1\" }\n            },\n            Order = 1,\n            RouteId = \"R\",\n        };\n        var b = new RouteConfig()\n        {\n            AuthorizationPolicy = \"A\",\n            RateLimiterPolicy = \"RL\",\n            TimeoutPolicy = \"T\",\n            Timeout = TimeSpan.FromSeconds(1),\n            ClusterId = \"C\",\n            CorsPolicy = \"Co\",\n            Match = new RouteMatch()\n            {\n                Headers = new[]\n                {\n                    new RouteHeader()\n                    {\n                        Name = \"hi\",\n                        Values = new[] { \"v1\", \"v2\" },\n                        IsCaseSensitive = true,\n                        Mode = HeaderMatchMode.HeaderPrefix,\n                    }\n                },\n                Hosts = new[] { \"foo:90\" },\n                Methods = new[] { \"GET\", \"POST\" },\n                Path = \"/P\"\n            },\n            Metadata = new Dictionary<string, string>()\n            {\n                { \"m\", \"m1\" }\n            },\n            Order = 1,\n            RouteId = \"r\",\n        };\n        var c = b with { }; // Clone\n\n        Assert.True(a.Equals(b));\n        Assert.True(a.Equals(c));\n        Assert.Equal(a.GetHashCode(), b.GetHashCode());\n        Assert.Equal(a.GetHashCode(), c.GetHashCode());\n    }\n\n    [Fact]\n    public void Equals_Negative()\n    {\n        var a = new RouteConfig()\n        {\n            AuthorizationPolicy = \"a\",\n            RateLimiterPolicy = \"rl\",\n            TimeoutPolicy = \"t\",\n            Timeout = TimeSpan.FromSeconds(1),\n            ClusterId = \"c\",\n            CorsPolicy = \"co\",\n            Match = new RouteMatch()\n            {\n                Headers = new[]\n                {\n                    new RouteHeader()\n                    {\n                        Name = \"Hi\",\n                        Values = new[] { \"v1\", \"v2\" },\n                        IsCaseSensitive = true,\n                        Mode = HeaderMatchMode.HeaderPrefix,\n                    }\n                },\n                Hosts = new[] { \"foo:90\" },\n                Methods = new[] { \"GET\", \"POST\" },\n                Path = \"/p\",\n            },\n            Metadata = new Dictionary<string, string>()\n            {\n                { \"m\", \"m1\" }\n            },\n            Order = 1,\n            RouteId = \"R\",\n        };\n        var b = a with { AuthorizationPolicy = \"b\" };\n        var c = a with { ClusterId = \"d\" };\n        var d = a with { CorsPolicy = \"p\" };\n        var e = a with { Match = new RouteMatch() };\n        var f = a with { Metadata = new Dictionary<string, string>() { { \"f\", \"f1\" } } };\n        var g = a with { Order = null };\n        var h = a with { RouteId = \"h\" };\n        var i = a with { RateLimiterPolicy = \"i\" };\n        var j = a with { TimeoutPolicy = \"j\" };\n        var k = a with { Timeout = TimeSpan.FromSeconds(107) };\n\n        Assert.False(a.Equals(b));\n        Assert.False(a.Equals(c));\n        Assert.False(a.Equals(d));\n        Assert.False(a.Equals(e));\n        Assert.False(a.Equals(f));\n        Assert.False(a.Equals(g));\n        Assert.False(a.Equals(h));\n        Assert.False(a.Equals(i));\n        Assert.False(a.Equals(j));\n        Assert.False(a.Equals(k));\n    }\n\n    [Fact]\n    public void Equals_Null_False()\n    {\n        Assert.False(new RouteConfig().Equals(null));\n    }\n\n    [Fact]\n    public void RouteConfig_CanBeJsonSerialized()\n    {\n        var route1 = new RouteConfig()\n        {\n            AuthorizationPolicy = \"a\",\n            RateLimiterPolicy = \"rl\",\n            ClusterId = \"c\",\n            CorsPolicy = \"co\",\n            Match = new RouteMatch()\n            {\n                Headers = new[]\n                {\n                    new RouteHeader()\n                    {\n                        Name = \"Hi\",\n                        Values = new[] { \"v1\", \"v2\" },\n                        IsCaseSensitive = true,\n                        Mode = HeaderMatchMode.HeaderPrefix,\n                    }\n                },\n                Hosts = new[] { \"foo:90\" },\n                Methods = new[] { \"GET\", \"POST\" },\n                Path = \"/p\",\n            },\n            Metadata = new Dictionary<string, string>()\n            {\n                { \"m\", \"m1\" }\n            },\n            Transforms = new[]\n            {\n                new Dictionary<string, string>\n                {\n                    { \"key\", \"value\" },\n                    { \"key1\", \"\" }\n                }\n            },\n            Order = 1,\n            RouteId = \"R\",\n        };\n\n        var json = JsonSerializer.Serialize(route1);\n        var route2 = JsonSerializer.Deserialize<RouteConfig>(json);\n\n        Assert.Equal(route1, route2);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/RouteHeaderTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Configuration.Tests;\n\npublic class RouteHeaderTests\n{\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void Equals_Positive(bool isCaseSensitive)\n    {\n        var a = new RouteHeader()\n        {\n            Name = \"foo\",\n            Mode = HeaderMatchMode.Exists,\n            Values = new[] { \"v1\", \"v2\" },\n            IsCaseSensitive = isCaseSensitive,\n        };\n        var b = new RouteHeader()\n        {\n            Name = \"Foo\",\n            Mode = HeaderMatchMode.Exists,\n            Values = new[] { \"v1\", \"v2\" },\n            IsCaseSensitive = isCaseSensitive,\n        };\n        var c = a with { }; // Clone\n        Assert.True(a.Equals(b));\n        Assert.True(a.Equals(c));\n        Assert.Equal(a.GetHashCode(), b.GetHashCode());\n        Assert.Equal(a.GetHashCode(), c.GetHashCode());\n    }\n\n    [Fact]\n    public void Equals_Negative()\n    {\n        var a = new RouteHeader()\n        {\n            Name = \"foo\",\n            Mode = HeaderMatchMode.Exists,\n            Values = new[] { \"v1\", \"v2\" },\n            IsCaseSensitive = true,\n        };\n        var b = a with { Name = \"bar\" };\n        var c = a with { Mode = HeaderMatchMode.ExactHeader };\n        var d = a with { Values = new[] { \"v1\", \"v3\" } };\n        var e = a with { IsCaseSensitive = false };\n        Assert.False(a.Equals(b));\n        Assert.False(a.Equals(c));\n        Assert.False(a.Equals(d));\n        Assert.False(a.Equals(e));\n    }\n\n    [Fact]\n    public void Equals_Null_False()\n    {\n        Assert.False(new RouteHeader().Equals(null));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/RouteMatchTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Configuration.Tests;\n\npublic class RouteMatchTests\n{\n    [Fact]\n    public void Equals_Positive()\n    {\n        var a = new RouteMatch()\n        {\n            Headers = new[]\n            {\n                new RouteHeader()\n                {\n                    Name = \"Hi\",\n                    Values = new[] { \"v1\", \"v2\" },\n                    IsCaseSensitive = true,\n                    Mode = HeaderMatchMode.HeaderPrefix,\n                }\n            },\n            Hosts = new[] { \"foo:90\" },\n            Methods = new[] { \"GET\", \"POST\" },\n            Path = \"/p\",\n        };\n        var b = new RouteMatch()\n        {\n            Headers = new[]\n            {\n                new RouteHeader()\n                {\n                    Name = \"hi\",\n                    Values = new[] { \"v1\", \"v2\" },\n                    IsCaseSensitive = true,\n                    Mode = HeaderMatchMode.HeaderPrefix,\n                }\n            },\n            Hosts = new[] { \"foo:90\" },\n            Methods = new[] { \"GET\", \"POST\" },\n            Path = \"/P\",\n        };\n        var c = b with { }; // Clone\n\n        Assert.True(a.Equals(b));\n        Assert.True(a.Equals(c));\n        Assert.Equal(a.GetHashCode(), b.GetHashCode());\n        Assert.Equal(a.GetHashCode(), c.GetHashCode());\n    }\n\n    [Fact]\n    public void Equals_Negative()\n    {\n        var a = new RouteMatch()\n        {\n            Headers = new[]\n            {\n                new RouteHeader()\n                {\n                    Name = \"Hi\",\n                    Values = new[] { \"v1\", \"v2\" },\n                    IsCaseSensitive = true,\n                    Mode = HeaderMatchMode.HeaderPrefix,\n                }\n            },\n            Hosts = new[] { \"foo:90\" },\n            Methods = new[] { \"GET\", \"POST\" },\n            Path = \"/p\",\n        };\n        var b = a with\n        {\n            Headers = new[]\n            {\n                new RouteHeader()\n                {\n                    Name = \"Bye\",\n                    Values = new[] { \"v1\", \"v2\" },\n                    IsCaseSensitive = true,\n                    Mode = HeaderMatchMode.HeaderPrefix,\n                }\n            }\n        };\n        var c = a with { Hosts = new[] { \"bar:90\" } };\n        var d = a with { Methods = new[] { \"PUT\", \"POST\" } };\n        var e = a with { Path = \"/z\" };\n\n        Assert.False(a.Equals(b));\n        Assert.False(a.Equals(c));\n        Assert.False(a.Equals(d));\n        Assert.False(a.Equals(e));\n    }\n\n    [Fact]\n    public void Equals_Null_False()\n    {\n        Assert.False(new RouteMatch().Equals(null));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/RouteQueryParameterTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Configuration.Tests;\n\npublic class RouteQueryParameterTests\n{\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void Equals_Positive(bool isCaseSensitive)\n    {\n        var a = new RouteQueryParameter()\n        {\n            Name = \"foo\",\n            Mode = QueryParameterMatchMode.Exists,\n            Values = new[] { \"v1\", \"v2\" },\n            IsCaseSensitive = isCaseSensitive,\n        };\n        var b = new RouteQueryParameter()\n        {\n            Name = \"Foo\",\n            Mode = QueryParameterMatchMode.Exists,\n            Values = new[] { \"v1\", \"v2\" },\n            IsCaseSensitive = isCaseSensitive,\n        };\n        var c = a with { }; // Clone\n        Assert.True(a.Equals(b));\n        Assert.True(a.Equals(c));\n        Assert.Equal(a.GetHashCode(), b.GetHashCode());\n        Assert.Equal(a.GetHashCode(), c.GetHashCode());\n    }\n\n    [Fact]\n    public void Equals_Negative()\n    {\n        var a = new RouteQueryParameter()\n        {\n            Name = \"foo\",\n            Mode = QueryParameterMatchMode.Exists,\n            Values = new[] { \"v1\", \"v2\" },\n            IsCaseSensitive = true,\n        };\n        var b = a with { Name = \"bar\" };\n        var c = a with { Mode = QueryParameterMatchMode.Exact };\n        var d = a with { Values = new[] { \"v1\", \"v3\" } };\n        var e = a with { IsCaseSensitive = false };\n        Assert.False(a.Equals(b));\n        Assert.False(a.Equals(c));\n        Assert.False(a.Equals(d));\n        Assert.False(a.Equals(e));\n    }\n\n    [Fact]\n    public void Equals_Null_False()\n    {\n        Assert.False(new RouteQueryParameter().Equals(null));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/SessionAffinityConfigTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Configuration.Tests;\n\npublic class SessionAffinityConfigTests\n{\n    [Fact]\n    public void Equals_Same_Value_Returns_True()\n    {\n        var options1 = new SessionAffinityConfig\n        {\n            Enabled = true,\n            FailurePolicy = \"policy1\",\n            Policy = \"policy1\",\n            AffinityKeyName = \"Key1\"\n        };\n\n        var options2 = new SessionAffinityConfig\n        {\n            Enabled = true,\n            FailurePolicy = \"Policy1\",\n            Policy = \"Policy1\",\n            AffinityKeyName = \"Key1\"\n        };\n\n        var equals = options1.Equals(options2);\n\n        Assert.True(equals);\n        Assert.Equal(options1.GetHashCode(), options2.GetHashCode());\n    }\n\n    [Fact]\n    public void Equals_Different_Value_Returns_False()\n    {\n        var options1 = new SessionAffinityConfig\n        {\n            Enabled = true,\n            FailurePolicy = \"policy1\",\n            Policy = \"policy1\",\n            AffinityKeyName = \"Key1\"\n        };\n\n        var options2 = new SessionAffinityConfig\n        {\n            Enabled = false,\n            FailurePolicy = \"policy2\",\n            Policy = \"policy2\",\n            AffinityKeyName = \"Key1\"\n        };\n\n        var equals = options1.Equals(options2);\n\n        Assert.False(equals);\n    }\n\n    [Fact]\n    public void Equals_Second_Null_Returns_False()\n    {\n        var options1 = new SessionAffinityConfig\n        {\n            Enabled = true,\n            FailurePolicy = \"policy1\",\n            Policy = \"policy1\",\n            AffinityKeyName = \"Key1\"\n        };\n\n        var equals = options1.Equals(null);\n\n        Assert.False(equals);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/YarpOutputCachePolicyProviderTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.DependencyInjection;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\npublic class YarpOutputCachePolicyProviderTests\n{\n    [Fact]\n    public async Task GetPolicyAsync_Works()\n    {\n        var services = new ServiceCollection();\n\n        services.AddOutputCache(options =>\n        {\n            options.AddPolicy(\"customPolicy\", opt =>\n            {\n                opt.Expire(TimeSpan.FromSeconds(12));\n                opt.SetVaryByHost(true);\n            });\n        });\n\n        services.AddReverseProxy();\n        var provider = services.BuildServiceProvider();\n        var outputCachePolicyProvider = provider.GetRequiredService<IYarpOutputCachePolicyProvider>();\n        Assert.Null(await outputCachePolicyProvider.GetPolicyAsync(\"anotherPolicy\"));\n        Assert.NotNull(await outputCachePolicyProvider.GetPolicyAsync(\"customPolicy\"));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Configuration/YarpRateLimiterPolicyProviderTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing System.Threading.RateLimiting;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.RateLimiting;\nusing Microsoft.Extensions.DependencyInjection;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Configuration;\n\npublic class YarpRateLimiterPolicyProviderTests\n{\n    [Fact]\n    public async Task GetPolicyAsync_Works()\n    {\n        var services = new ServiceCollection();\n\n        services.AddRateLimiter(options =>\n        {\n            options.AddFixedWindowLimiter(\"customPolicy\", opt =>\n            {\n                opt.PermitLimit = 4;\n                opt.Window = TimeSpan.FromSeconds(12);\n                opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;\n                opt.QueueLimit = 2;\n            });\n        });\n\n        services.AddReverseProxy();\n        var provider = services.BuildServiceProvider();\n        var rateLimiterPolicyProvider = provider.GetRequiredService<IYarpRateLimiterPolicyProvider>();\n        Assert.Null(await rateLimiterPolicyProvider.GetPolicyAsync(\"anotherPolicy\"));\n        Assert.NotNull(await rateLimiterPolicyProvider.GetPolicyAsync(\"customPolicy\"));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Delegation/HttpSysDelegatorMiddlewareTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Server.HttpSys;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Utilities;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Delegation;\n\npublic class HttpSysDelegatorMiddlewareTests : TestAutoMockBase\n{\n    private readonly HttpSysDelegatorMiddleware _sut;\n    private readonly RequestDelegate _next;\n    private readonly DefaultHttpContext _context;\n    private readonly ReverseProxyFeature _proxyFeature;\n    private readonly List<DestinationState> _availableDestinations;\n\n    private Action _nextCallback;\n    private bool _nextCalled;\n\n    public HttpSysDelegatorMiddlewareTests()\n    {\n        _context = new DefaultHttpContext();\n        _availableDestinations = new List<DestinationState>();\n        _proxyFeature = new ReverseProxyFeature\n        {\n            AvailableDestinations = _availableDestinations,\n            Cluster = new ClusterModel(new ClusterConfig(), new HttpMessageInvoker(Mock<HttpMessageHandler>().Object)),\n        };\n        _context.Features.Set<IReverseProxyFeature>(_proxyFeature);\n        _context.Features.Set(Mock<IHttpSysRequestDelegationFeature>().Object);\n\n        Mock<IHttpSysRequestDelegationFeature>()\n            .SetupGet(p => p.CanDelegate)\n            .Returns(true);\n\n        _next = context =>\n        {\n            _nextCalled = true;\n            _nextCallback?.Invoke();\n            return Task.CompletedTask;\n        };\n        Provide(_next);\n\n        _sut = Create<HttpSysDelegatorMiddleware>();\n    }\n\n    [Fact]\n    public async Task SingleDelegationDestination_VerifyProxiedDestinationSetAndNextNotCalled()\n    {\n        var destination = CreateDestination(\"dest1\", \"queue1\");\n        _availableDestinations.Add(destination);\n\n        await _sut.Invoke(_context);\n\n        Assert.Same(destination, _proxyFeature.ProxiedDestination);\n        Assert.False(_nextCalled);\n    }\n\n    [Fact]\n    public async Task NoDestinations_VerifyNextInvoked()\n    {\n        await _sut.Invoke(_context);\n\n        Assert.True(_nextCalled);\n    }\n\n    [Fact]\n    public async Task NoDelegationDestinations_VerifyNextInvoked()\n    {\n        _availableDestinations.Add(CreateDestination(\"dest1\", queueName: null));\n\n        await _sut.Invoke(_context);\n\n        Assert.True(_nextCalled);\n    }\n\n    [Fact]\n    public async Task MultipleDestinations_OneDelegationAndOneProxyDestination_ProxyChosen_VerifyNextInvokedWithSingleProxyDestination()\n    {\n        var destination1 = CreateDestination(\"dest1\", \"queue1\");\n        var destination2 = CreateDestination(\"dest2\", queueName: null);\n        _availableDestinations.Add(destination1);\n        _availableDestinations.Add(destination2);\n        SetupRandomToReturn(1); // return \"dest2\"\n\n        _nextCallback = () =>\n        {\n            Assert.Single(_proxyFeature.AvailableDestinations);\n            Assert.Same(destination2, _proxyFeature.AvailableDestinations[0]);\n        };\n\n        await _sut.Invoke(_context);\n\n        Assert.True(_nextCalled);\n    }\n\n    [Fact]\n    public async Task MultipleDestinations_OneDelegationAndOneProxyDestination_DelegationChosen_VerifyProxiedDestinationSetAndNextNotCalled()\n    {\n        var destination1 = CreateDestination(\"dest1\", \"queue1\");\n        var destination2 = CreateDestination(\"dest2\", queueName: null);\n        _availableDestinations.Add(destination1);\n        _availableDestinations.Add(destination2);\n        SetupRandomToReturn(0); // return \"dest1\"\n\n        await _sut.Invoke(_context);\n\n        Assert.Same(destination1, _proxyFeature.ProxiedDestination);\n        Assert.False(_nextCalled);\n    }\n\n    private static DestinationState CreateDestination(string id, string queueName = null)\n    {\n        var metadata = new Dictionary<string, string>();\n        if (queueName != null)\n        {\n            metadata.Add(DelegationExtensions.HttpSysDelegationQueueMetadataKey, queueName);\n        }\n\n        var config = new DestinationConfig()\n        {\n            Address = \"http://*:80\",\n            Metadata = metadata,\n        };\n\n        return new DestinationState(id)\n        {\n            Model = new DestinationModel(config),\n        };\n    }\n\n    private void SetupRandomToReturn(int value)\n    {\n        Mock<IRandomFactory>().Setup(m => m.CreateRandomInstance()).Returns(new TestRandom(value));\n    }\n\n    private class TestRandom : Random\n    {\n        private readonly int _next;\n\n        public TestRandom(int next)\n        {\n            _next = next;\n        }\n\n        public override int Next(int maxValue)\n        {\n            return _next;\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Delegation/HttpSysDelegatorTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Hosting.Server;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.AspNetCore.Server.HttpSys;\nusing Moq;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Delegation;\n\npublic class HttpSysDelegatorTests : TestAutoMockBase\n{\n    private readonly HttpSysDelegator _delegator;\n    private readonly IClusterChangeListener _changeListener;\n    private readonly DefaultHttpContext _context;\n\n    public HttpSysDelegatorTests()\n    {\n        Mock<IFeatureCollection>()\n            .Setup(m => m.Get<IServerDelegationFeature>())\n            .Returns(Mock<IServerDelegationFeature>().Object);\n        Mock<IServer>()\n            .SetupGet(p => p.Features)\n            .Returns(Mock<IFeatureCollection>().Object);\n\n        _delegator = Create<HttpSysDelegator>();\n        _changeListener = _delegator;\n\n        _context = new DefaultHttpContext();\n        _context.Features.Set(Mock<IHttpSysRequestDelegationFeature>().Object);\n\n        SetupCanDelegate(true);\n    }\n\n    [Fact]\n    public void DelegateRequest_VerifyRequestDelegated()\n    {\n        var destination = CreateDestination(\"dest1\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination);\n\n        _changeListener.OnClusterAdded(cluster);\n\n        DelegateRequest(destination);\n\n        VerifyRequestDelegated();\n    }\n\n    [Fact]\n    public void DelegateRequest_DestinationRemoved_VerifyRequestDelegated()\n    {\n        var destination = CreateDestination(\"dest1\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination);\n\n        _changeListener.OnClusterAdded(cluster);\n        _changeListener.OnClusterRemoved(cluster);\n\n        DelegateRequest(destination);\n\n        VerifyRequestDelegated();\n    }\n\n    [Fact]\n    public void DelegateRequest_DestinationChanged_VerifyRequestDelegated()\n    {\n        var destination = CreateDestination(\"dest1\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination);\n\n        _changeListener.OnClusterAdded(cluster);\n        destination.Model = CreateDestinationModel(\"queue2\");\n        _changeListener.OnClusterChanged(cluster);\n\n        DelegateRequest(destination);\n\n        VerifyRequestDelegated();\n    }\n\n    [Fact]\n    public void DelegateRequest_NoDelegationFeature_VerifyThrows()\n    {\n        var destination = CreateDestination(\"dest1\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination);\n\n        _changeListener.OnClusterAdded(cluster);\n        _context.Features.Set<IHttpSysRequestDelegationFeature>(null);\n\n        Assert.ThrowsAny<InvalidOperationException>(() => DelegateRequest(destination));\n    }\n\n    [Fact]\n    public void DelegateRequest_CanNotDelegate_VerifyThrows()\n    {\n        var destination = CreateDestination(\"dest1\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination);\n\n        _changeListener.OnClusterAdded(cluster);\n        SetupCanDelegate(false);\n\n        Assert.ThrowsAny<InvalidOperationException>(() => DelegateRequest(destination));\n    }\n\n    [Fact]\n    public void DelegateRequest_DelegationRuleNotFound_Verify503StatusAndErrorFeatureSet()\n    {\n        var destination = CreateDestination(\"dest1\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination);\n\n        DelegateRequest(CreateDestination(\"dest1\", \"invalidQueue\"));\n\n        VerifyNoAvailableDestinationsError();\n    }\n\n    [Fact]\n    public void DelegateRequest_CreateRuleFailed_Verify503StatusAndErrorFeatureSet()\n    {\n        var destination = CreateDestination(\"dest1\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination);\n        Mock<IServerDelegationFeature>()\n            .Setup(x => x.CreateDelegationRule(It.IsAny<string>(), It.IsAny<string>()))\n            .Throws<Exception>()\n            .Verifiable();\n\n        _changeListener.OnClusterAdded(cluster);\n\n        DelegateRequest(destination);\n\n        VerifyNoAvailableDestinationsError();\n    }\n\n    [Fact]\n    public void DelegateRequest_DelegationFails_Verify503StatusAndErrorFeatureSet()\n    {\n        var destination = CreateDestination(\"dest1\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination);\n        Mock<IHttpSysRequestDelegationFeature>()\n            .Setup(x => x.DelegateRequest(It.IsAny<DelegationRule>()))\n            .Throws<Exception>()\n            .Verifiable();\n\n        _changeListener.OnClusterAdded(cluster);\n\n        DelegateRequest(destination);\n\n        VerifyDelegationFailedError();\n    }\n\n    [Fact]\n    public void ResetQueue()\n    {\n        var destination = CreateDestination(\"dest1\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination);\n\n        _changeListener.OnClusterAdded(cluster);\n\n        ResetDelegatorQueue(destination);\n    }\n\n    [Fact]\n    public void OnClusterAdded_SingleDelegationDestination_RuleCreated()\n    {\n        var destination = CreateDestination(\"dest1\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination);\n\n        _changeListener.OnClusterAdded(cluster);\n\n        VerifyDelegationRuleCreated(destination);\n    }\n\n    [Fact]\n    public void OnClusterAdd_MultipleDelegationDestination_VerifyRulesCreated()\n    {\n        var destination1 = CreateDestination(\"dest1\", \"queue1\");\n        var destination2 = CreateDestination(\"dest2\", \"queue2\");\n        var cluster = CreateCluster(\"cluster1\", destination1, destination2);\n\n        _changeListener.OnClusterAdded(cluster);\n\n        VerifyDelegationRuleCreated(destination1);\n        VerifyDelegationRuleCreated(destination2);\n    }\n\n    [Fact]\n    public void OnClusterAdd_MultipleDelegationDestinationWithSameQueue_VerifyRuleCreated()\n    {\n        var destination1 = CreateDestination(\"dest1\", \"queue1\");\n        var destination2 = CreateDestination(\"dest2\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination1, destination2);\n\n        _changeListener.OnClusterAdded(cluster);\n\n        VerifyDelegationRuleCreated(destination1);\n    }\n\n    [Fact]\n    public void OnClusterAdd_NoDelegationDestinations_VerifyRuleNotCreated()\n    {\n        var destination = CreateDestination(\"dest1\", queueName: null);\n        var cluster = CreateCluster(\"cluster1\", destination);\n\n        _changeListener.OnClusterAdded(cluster);\n\n        VerifyDelegationRuleNotCreated(destination);\n    }\n\n    [Fact]\n    public void OnClusterChanged_RuleExists_NewRuleAdded_VerifyNewRuleCreated()\n    {\n        var destination1 = CreateDestination(\"dest1\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination1);\n\n        _changeListener.OnClusterAdded(cluster);\n        Mock<IServerDelegationFeature>().Reset();\n\n        var destination2 = CreateDestination(\"dest2\", \"queue2\");\n        cluster = CreateCluster(cluster.ClusterId, destination1, destination2);\n\n        _changeListener.OnClusterAdded(cluster);\n\n        VerifyDelegationRuleNotCreated(destination1);\n        VerifyDelegationRuleCreated(destination2);\n    }\n\n    [Fact]\n    public void OnClusterChanged_RuleExists_NoNewRuleAdded_VerifyRuleNotCreated()\n    {\n        var destination1 = CreateDestination(\"dest1\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination1);\n\n        _changeListener.OnClusterAdded(cluster);\n        Mock<IServerDelegationFeature>().Reset();\n\n        var destination2 = CreateDestination(\"dest2\", queueName: null);\n        cluster = CreateCluster(cluster.ClusterId, destination1, destination2);\n\n        _changeListener.OnClusterAdded(cluster);\n\n        VerifyDelegationRuleNotCreated(destination1);\n        VerifyDelegationRuleNotCreated(destination2);\n    }\n\n    [Fact]\n    public void OnClusterChanged_NoRuleExists_NewRuleAdded_VerifyNewRuleCreated()\n    {\n        var destination = CreateDestination(\"dest1\", \"queue1\");\n        var cluster = CreateCluster(\"cluster1\", destination);\n\n        _changeListener.OnClusterAdded(cluster);\n\n        VerifyDelegationRuleCreated(destination);\n    }\n\n    [Fact]\n    public void OnClusterChanged_NoRuleExists_NoNewRuleAdded_VerifyRuleNotCreated()\n    {\n        var destination = CreateDestination(\"dest1\", queueName: null);\n        var cluster = CreateCluster(\"cluster1\", destination);\n\n        _changeListener.OnClusterAdded(cluster);\n\n        VerifyDelegationRuleNotCreated(destination);\n    }\n\n    private void SetupCanDelegate(bool canDelegate)\n    {\n        Mock<IHttpSysRequestDelegationFeature>()\n            .SetupGet(p => p.CanDelegate)\n            .Returns(canDelegate);\n    }\n\n    private void DelegateRequest(DestinationState destination)\n    {\n        _delegator.DelegateRequest(_context, destination);\n    }\n\n    private void ResetDelegatorQueue(DestinationState destination)\n    {\n        _delegator.ResetQueue(destination.GetHttpSysDelegationQueue(), destination.Model.Config.Address);\n    }\n\n    private void VerifyRequestDelegated()\n    {\n        Mock<IHttpSysRequestDelegationFeature>()\n            .Verify(m => m.DelegateRequest(It.IsAny<DelegationRule>()), Times.Once());\n        var errorFeature = _context.Features.Get<IForwarderErrorFeature>();\n        Assert.Null(errorFeature);\n    }\n\n    private void VerifyNoAvailableDestinationsError()\n    {\n        Assert.Equal(StatusCodes.Status503ServiceUnavailable, _context.Response.StatusCode);\n        var errorFeature = _context.Features.Get<IForwarderErrorFeature>();\n        Assert.NotNull(errorFeature);\n        Assert.Equal(ForwarderError.NoAvailableDestinations, errorFeature.Error);\n    }\n\n    private void VerifyDelegationFailedError()\n    {\n        Assert.Equal(StatusCodes.Status503ServiceUnavailable, _context.Response.StatusCode);\n        var errorFeature = _context.Features.Get<IForwarderErrorFeature>();\n        Assert.NotNull(errorFeature);\n        Assert.Equal(ForwarderError.Request, errorFeature.Error);\n        Assert.NotNull(errorFeature.Exception);\n    }\n\n    private void VerifyDelegationRuleCreated(DestinationState destination)\n    {\n        Mock<IServerDelegationFeature>()\n            .Verify(m => m.CreateDelegationRule(destination.GetHttpSysDelegationQueue(), destination.Model.Config.Address), Times.Once());\n    }\n\n    private void VerifyDelegationRuleNotCreated(DestinationState destination)\n    {\n        Mock<IServerDelegationFeature>()\n            .Verify(m => m.CreateDelegationRule(destination.GetHttpSysDelegationQueue(), destination.Model.Config.Address), Times.Never());\n    }\n\n    private static ClusterState CreateCluster(string id, params DestinationState[] destinations)\n    {\n        var cluster = new ClusterState(id);\n        foreach (var destination in destinations)\n        {\n            cluster.Destinations.TryAdd(destination.DestinationId, destination);\n        }\n\n        return cluster;\n    }\n\n    private static DestinationModel CreateDestinationModel(string queueName)\n    {\n        var metadata = new Dictionary<string, string>();\n        if (queueName != null)\n        {\n            metadata.Add(DelegationExtensions.HttpSysDelegationQueueMetadataKey, queueName);\n        }\n\n        var config = new DestinationConfig()\n        {\n            Address = \"http://*:80/\",\n            Metadata = metadata,\n        };\n\n        return new DestinationModel(config);\n    }\n\n    private static DestinationState CreateDestination(string id, string queueName = null)\n    {\n        return new DestinationState(id)\n        {\n            Model = CreateDestinationModel(queueName),\n        };\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Forwarder/ForwarderHttpClientFactoryTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Security.Authentication;\nusing System.Text;\nusing Microsoft.Extensions.Logging;\nusing Xunit;\nusing Yarp.Tests.Common;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Forwarder.Tests;\n\npublic class ForwarderHttpClientFactoryTests : TestAutoMockBase\n{\n    [Fact]\n    public void Constructor_Works()\n    {\n        new ForwarderHttpClientFactory(Mock<ILogger<ForwarderHttpClientFactory>>().Object);\n    }\n\n    [Fact]\n    public void CreateClient_Works()\n    {\n        var factory = new ForwarderHttpClientFactory(Mock<ILogger<ForwarderHttpClientFactory>>().Object);\n\n        var actual1 = factory.CreateClient(new ForwarderHttpClientContext()\n        {\n            NewConfig = HttpClientConfig.Empty,\n            OldConfig = HttpClientConfig.Empty\n        });\n        var actual2 = factory.CreateClient(new ForwarderHttpClientContext()\n        {\n            NewConfig = HttpClientConfig.Empty,\n            OldConfig = HttpClientConfig.Empty\n        });\n\n        Assert.NotNull(actual1);\n        Assert.NotNull(actual2);\n        Assert.NotSame(actual2, actual1);\n    }\n\n    [Fact]\n    public void CreateClient_ApplySslProtocols_Success()\n    {\n        var factory = new ForwarderHttpClientFactory(Mock<ILogger<ForwarderHttpClientFactory>>().Object);\n        var options = new HttpClientConfig\n        {\n            SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,\n        };\n        var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options });\n\n        var handler = GetHandler(client);\n\n        Assert.NotNull(handler);\n        Assert.Equal(SslProtocols.Tls12 | SslProtocols.Tls13, handler.SslOptions.EnabledSslProtocols);\n        VerifyDefaultValues(handler, \"SslProtocols\");\n    }\n\n    [Fact]\n    public void CreateClient_ApplyDangerousAcceptAnyServerCertificate_Success()\n    {\n        var factory = new ForwarderHttpClientFactory(Mock<ILogger<ForwarderHttpClientFactory>>().Object);\n        var options = new HttpClientConfig { DangerousAcceptAnyServerCertificate = true };\n        var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options });\n\n        var handler = GetHandler(client);\n\n        Assert.NotNull(handler);\n        Assert.NotNull(handler.SslOptions.RemoteCertificateValidationCallback);\n        Assert.True(handler.SslOptions.RemoteCertificateValidationCallback(default, default, default, default));\n        VerifyDefaultValues(handler, \"DangerousAcceptAnyServerCertificate\");\n    }\n\n    [Fact]\n    public void CreateClient_ApplyMaxConnectionsPerServer_Success()\n    {\n        var factory = new ForwarderHttpClientFactory(Mock<ILogger<ForwarderHttpClientFactory>>().Object);\n        var options = new HttpClientConfig { MaxConnectionsPerServer = 22 };\n        var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options });\n\n        var handler = GetHandler(client);\n\n        Assert.NotNull(handler);\n        Assert.Equal(22, handler.MaxConnectionsPerServer);\n        VerifyDefaultValues(handler, \"MaxConnectionsPerServer\");\n    }\n\n    [Fact]\n    public void CreateClient_ApplyWebProxy_Success()\n    {\n        var factory = new ForwarderHttpClientFactory(Mock<ILogger<ForwarderHttpClientFactory>>().Object);\n        var options = new HttpClientConfig\n        {\n            WebProxy = new WebProxyConfig()\n            {\n                Address = new Uri(\"http://localhost:8080\"),\n                BypassOnLocal = true,\n                UseDefaultCredentials = true\n            }\n        };\n        var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options });\n\n        var handler = GetHandler(client);\n\n        Assert.NotNull(handler);\n        Assert.NotNull(handler.Proxy);\n        Assert.True(handler.UseProxy);\n        VerifyDefaultValues(handler, \"WebProxy\");\n    }\n\n    [Fact]\n    public void CreateClient_ApplyRequestHeaderEncoding_Success()\n    {\n        var factory = new ForwarderHttpClientFactory(Mock<ILogger<ForwarderHttpClientFactory>>().Object);\n        var options = new HttpClientConfig\n        {\n            RequestHeaderEncoding = Encoding.Latin1.WebName\n        };\n        var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options });\n\n        var handler = GetHandler(client);\n\n        Assert.NotNull(handler);\n        Assert.NotNull(handler.RequestHeaderEncodingSelector);\n        Assert.Equal(Encoding.Latin1, handler.RequestHeaderEncodingSelector(default, default));\n        VerifyDefaultValues(handler, nameof(SocketsHttpHandler.RequestHeaderEncodingSelector));\n    }\n\n    [Fact]\n    public void CreateClient_ApplyResponseHeaderEncoding_Success()\n    {\n        var factory = new ForwarderHttpClientFactory(Mock<ILogger<ForwarderHttpClientFactory>>().Object);\n        var options = new HttpClientConfig\n        {\n            ResponseHeaderEncoding = Encoding.Latin1.WebName\n        };\n        var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options });\n\n        var handler = GetHandler(client);\n\n        Assert.NotNull(handler);\n        Assert.NotNull(handler.ResponseHeaderEncodingSelector);\n        Assert.Equal(Encoding.Latin1, handler.ResponseHeaderEncodingSelector(default, default));\n        VerifyDefaultValues(handler, nameof(SocketsHttpHandler.ResponseHeaderEncodingSelector));\n    }\n\n    [Fact]\n    public void CreateClient_OldClientExistsNoConfigChange_ReturnsOldInstance()\n    {\n        var factory = new ForwarderHttpClientFactory(Mock<ILogger<ForwarderHttpClientFactory>>().Object);\n        var oldClient = new HttpMessageInvoker(new SocketsHttpHandler());\n        var oldOptions = new HttpClientConfig\n        {\n            SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12,\n            DangerousAcceptAnyServerCertificate = true,\n            MaxConnectionsPerServer = 10,\n            RequestHeaderEncoding = Encoding.Latin1.WebName,\n        };\n        var newOptions = oldOptions with { }; // Clone\n        var oldMetadata = new Dictionary<string, string> { { \"key1\", \"value1\" }, { \"key2\", \"value2\" } };\n        var newMetadata = new Dictionary<string, string> { { \"key1\", \"value1\" }, { \"key2\", \"value2\" } };\n        var context = new ForwarderHttpClientContext { ClusterId = \"cluster1\", OldConfig = oldOptions, OldMetadata = oldMetadata, OldClient = oldClient, NewConfig = newOptions, NewMetadata = newMetadata };\n\n        var actualClient = factory.CreateClient(context);\n\n        Assert.Equal(newOptions, oldOptions);\n        Assert.Same(oldClient, actualClient);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void CreateClient_ApplyEnableMultipleHttp2Connections_Success(bool enableMultipleHttp2Connections)\n    {\n        var factory = new ForwarderHttpClientFactory(Mock<ILogger<ForwarderHttpClientFactory>>().Object);\n        var options = new HttpClientConfig { EnableMultipleHttp2Connections = enableMultipleHttp2Connections };\n        var client = factory.CreateClient(new ForwarderHttpClientContext { NewConfig = options });\n\n        var handler = GetHandler(client);\n\n        Assert.Equal(enableMultipleHttp2Connections, handler.EnableMultipleHttp2Connections);\n    }\n\n    [Theory]\n    [MemberData(nameof(GetChangedHttpClientOptions))]\n    public void CreateClient_OldClientExistsHttpClientOptionsChanged_ReturnsNewInstance(HttpClientConfig oldOptions, HttpClientConfig newOptions)\n    {\n        var factory = new ForwarderHttpClientFactory(Mock<ILogger<ForwarderHttpClientFactory>>().Object);\n        var oldClient = new HttpMessageInvoker(new SocketsHttpHandler());\n        var context = new ForwarderHttpClientContext { ClusterId = \"cluster1\", OldConfig = oldOptions, OldClient = oldClient, NewConfig = newOptions };\n\n        var actualClient = factory.CreateClient(context);\n\n        Assert.NotEqual(newOptions, oldOptions);\n        Assert.NotSame(oldClient, actualClient);\n    }\n\n    public static IEnumerable<object[]> GetChangedHttpClientOptions()\n    {\n        return new[]\n        {\n            new object[] {\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = null,\n                },\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = null,\n                },\n            },\n            new object[] {\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = null,\n                },\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = false,\n                    MaxConnectionsPerServer = null,\n                },\n            },\n            new object[] {\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = null,\n                },\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = false,\n                    MaxConnectionsPerServer = null,\n                },\n            },\n            new object[] {\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = null,\n                },\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = false,\n                    MaxConnectionsPerServer = null,\n                },\n            },\n            new object[] {\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = null,\n                },\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 10,\n                },\n            },\n            new object[] {\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 10,\n                },\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = null,\n                },\n            },\n            new object[] {\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 10,\n                },\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 20,\n                },\n            },\n            new object[] {\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 10,\n                    EnableMultipleHttp2Connections = true\n                },\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 10,\n                    EnableMultipleHttp2Connections = false\n                },\n            },\n            new object[] {\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 10,\n                },\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 10,\n                    RequestHeaderEncoding = Encoding.UTF8.WebName,\n                },\n            },\n            new object[] {\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 10,\n                    RequestHeaderEncoding = Encoding.UTF8.WebName,\n                },\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 10,\n                    RequestHeaderEncoding = Encoding.Latin1.WebName,\n                },\n            },\n            new object[] {\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 10,\n                    RequestHeaderEncoding = Encoding.UTF8.WebName,\n                },\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 10,\n                    RequestHeaderEncoding = Encoding.UTF8.WebName,\n                    ResponseHeaderEncoding = Encoding.UTF8.WebName,\n                },\n            },\n            new object[] {\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 10,\n                    RequestHeaderEncoding = Encoding.UTF8.WebName,\n                    ResponseHeaderEncoding = Encoding.Latin1.WebName,\n                },\n                new HttpClientConfig\n                {\n                    SslProtocols = SslProtocols.Tls11,\n                    DangerousAcceptAnyServerCertificate = true,\n                    MaxConnectionsPerServer = 10,\n                    RequestHeaderEncoding = Encoding.UTF8.WebName,\n                    ResponseHeaderEncoding = Encoding.UTF8.WebName,\n                },\n            }\n        };\n    }\n\n    public static SocketsHttpHandler GetHandler(HttpMessageInvoker client)\n    {\n        var handlerFieldInfo = typeof(HttpMessageInvoker).GetFields(BindingFlags.Instance | BindingFlags.NonPublic).Single(f => f.Name == \"_handler\");\n        var handler = handlerFieldInfo.GetValue(client);\n        return (SocketsHttpHandler)handler;\n    }\n\n    private void VerifyDefaultValues(SocketsHttpHandler actualHandler, params string[] skippedExtractors)\n    {\n        var skippedSet = new HashSet<string>(skippedExtractors);\n        var defaultHandler = new SocketsHttpHandler();\n        foreach (var extractor in GetAllExtractors().Where(e => !skippedSet.Contains(e.name)).Select(e => e.extractor))\n        {\n            Assert.Equal(extractor(defaultHandler), extractor(actualHandler));\n        }\n    }\n\n    private (string name, Func<SocketsHttpHandler, object> extractor)[] GetAllExtractors()\n    {\n        return new (string name, Func<SocketsHttpHandler, object> extractor)[] {\n            (\"SslProtocols\", h => h.SslOptions.EnabledSslProtocols),\n            (\"DangerousAcceptAnyServerCertificate\", h => h.SslOptions.RemoteCertificateValidationCallback),\n            (\"ClientCertificate\", h => h.SslOptions.ClientCertificates),\n            (\"MaxConnectionsPerServer\", h => h.MaxConnectionsPerServer),\n            (\"WebProxy\", h => h.Proxy)\n        };\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Forwarder/ForwarderMiddlewareTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Moq;\nusing Xunit;\nusing Yarp.Tests.Common;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Forwarder.Tests;\n\npublic class ForwarderMiddlewareTests : TestAutoMockBase\n{\n    [Fact]\n    public void Constructor_Works()\n    {\n        Create<ForwarderMiddleware>();\n    }\n\n    [Fact]\n    public async Task Invoke_Works()\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Scheme = \"https\";\n        httpContext.Request.Host = new HostString(\"example.com\");\n        httpContext.Request.Path = \"/api/test\";\n        httpContext.Request.QueryString = new QueryString(\"?a=b&c=d\");\n\n        var httpClient = new HttpMessageInvoker(new Mock<HttpMessageHandler>().Object);\n        var httpRequestOptions = new ForwarderRequestConfig\n        {\n            ActivityTimeout = TimeSpan.FromSeconds(60),\n            Version = HttpVersion.Version11,\n            VersionPolicy = HttpVersionPolicy.RequestVersionExact,\n        };\n        var cluster1 = new ClusterState(clusterId: \"cluster1\");\n        var clusterModel = new ClusterModel(new ClusterConfig() { HttpRequest = httpRequestOptions },\n            httpClient);\n        var destination1 = cluster1.Destinations.GetOrAdd(\n            \"destination1\",\n            id => new DestinationState(id)\n            {\n                Model = new DestinationModel(new DestinationConfig { Address = \"https://localhost:123/a/b/\" })\n            });\n        var routeConfig = new RouteModel(\n            config: new RouteConfig() { RouteId = \"Route-1\" },\n            cluster: cluster1,\n            transformer: HttpTransformer.Default);\n\n        httpContext.Features.Set<IReverseProxyFeature>(\n            new ReverseProxyFeature()\n            {\n                AvailableDestinations = new List<DestinationState>() { destination1 }.AsReadOnly(),\n                Cluster = clusterModel,\n                Route = routeConfig,\n            });\n        httpContext.Features.Set(cluster1);\n\n        var tcs1 = new TaskCompletionSource<bool>();\n        var tcs2 = new TaskCompletionSource<bool>();\n        Mock<IHttpForwarder>()\n            .Setup(h => h.SendAsync(\n                httpContext,\n                It.Is<string>(uri => uri == \"https://localhost:123/a/b/\"),\n                httpClient,\n                It.Is<ForwarderRequestConfig>(requestOptions =>\n                    requestOptions.ActivityTimeout == httpRequestOptions.ActivityTimeout\n                    && requestOptions.Version == httpRequestOptions.Version\n                    && requestOptions.VersionPolicy == httpRequestOptions.VersionPolicy),\n                It.IsAny<HttpTransformer>()))\n            .Returns(\n                async () =>\n                {\n                    tcs1.TrySetResult(true);\n                    await tcs2.Task;\n                    return ForwarderError.None;\n                })\n            .Verifiable();\n\n        var sut = Create<ForwarderMiddleware>();\n\n        Assert.Equal(0, cluster1.ConcurrencyCounter.Value);\n        Assert.Equal(0, destination1.ConcurrentRequestCount);\n\n        var task = sut.Invoke(httpContext);\n        if (task.IsFaulted)\n        {\n            // Something went wrong, don't hang the test.\n            await task;\n        }\n\n        Mock<IHttpForwarder>().Verify();\n\n        await tcs1.Task; // Wait until we get to the proxying step.\n        Assert.Equal(1, cluster1.ConcurrencyCounter.Value);\n        Assert.Equal(1, destination1.ConcurrentRequestCount);\n\n        Assert.Same(destination1, httpContext.GetReverseProxyFeature().ProxiedDestination);\n\n        tcs2.TrySetResult(true);\n        await task;\n        Assert.Equal(0, cluster1.ConcurrencyCounter.Value);\n        Assert.Equal(0, destination1.ConcurrentRequestCount);\n\n        var invoke = Assert.Single(events, e => e.EventName == \"ForwarderInvoke\");\n        Assert.Equal(3, invoke.Payload.Count);\n        Assert.Equal(cluster1.ClusterId, (string)invoke.Payload[0]);\n        Assert.Equal(routeConfig.Config.RouteId, (string)invoke.Payload[1]);\n        Assert.Equal(destination1.DestinationId, (string)invoke.Payload[2]);\n    }\n\n    [Fact]\n    public async Task NoDestinations_503()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Scheme = \"https\";\n        httpContext.Request.Host = new HostString(\"example.com\");\n\n        var httpClient = new HttpMessageInvoker(new Mock<HttpMessageHandler>().Object);\n        var cluster1 = new ClusterState(clusterId: \"cluster1\");\n        var clusterModel = new ClusterModel(new ClusterConfig(), httpClient);\n        var routeConfig = new RouteModel(\n            config: new RouteConfig(),\n            cluster: cluster1,\n            transformer: HttpTransformer.Default);\n        httpContext.Features.Set<IReverseProxyFeature>(\n            new ReverseProxyFeature()\n            {\n                AvailableDestinations = Array.Empty<DestinationState>(),\n                Cluster = clusterModel,\n                Route = routeConfig,\n            });\n\n        Mock<IHttpForwarder>()\n            .Setup(h => h.SendAsync(\n                httpContext,\n                It.IsAny<string>(),\n                httpClient,\n                It.IsAny<ForwarderRequestConfig>(),\n                It.IsAny<HttpTransformer>()))\n            .Returns(() => throw new NotImplementedException());\n\n        var sut = Create<ForwarderMiddleware>();\n\n        Assert.Equal(0, cluster1.ConcurrencyCounter.Value);\n\n        await sut.Invoke(httpContext);\n        Assert.Equal(0, cluster1.ConcurrencyCounter.Value);\n\n        Mock<IHttpForwarder>().Verify();\n        Assert.Equal(StatusCodes.Status503ServiceUnavailable, httpContext.Response.StatusCode);\n        var errorFeature = httpContext.Features.Get<IForwarderErrorFeature>();\n        Assert.Equal(ForwarderError.NoAvailableDestinations, errorFeature?.Error);\n        Assert.Null(errorFeature.Exception);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Forwarder/HttpForwarderTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics.Tracing;\nusing System.IO;\nusing System.IO.Pipelines;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.AspNetCore.WebUtilities;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Primitives;\nusing Microsoft.Net.Http.Headers;\nusing Moq;\nusing Xunit;\nusing Yarp.ReverseProxy.Common;\nusing Yarp.ReverseProxy.Transforms;\nusing Yarp.ReverseProxy.Transforms.Builder.Tests;\nusing Yarp.ReverseProxy.Utilities;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Forwarder.Tests;\n\npublic class HttpForwarderTests\n{\n    private readonly ITestOutputHelper _output;\n\n    public HttpForwarderTests(ITestOutputHelper output)\n    {\n        _output = output;\n    }\n\n    private IHttpForwarder CreateProxy()\n    {\n        var services = new ServiceCollection();\n        services.AddLogging(b =>\n        {\n            b.SetMinimumLevel(LogLevel.Trace);\n            b.Services.AddSingleton<ILoggerProvider>(new TestLoggerProvider(_output));\n        });\n        services.AddHttpForwarder();\n        var provider = services.BuildServiceProvider();\n        return provider.GetRequiredService<IHttpForwarder>();\n    }\n\n    [Fact]\n    public void Constructor_Works()\n    {\n        Assert.NotNull(CreateProxy());\n    }\n\n    // Tests normal (as opposed to upgradeable) request proxying.\n    [Fact]\n    public async Task NormalRequest_Works()\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Scheme = \"http\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Path = \"/path/base/dropped\";\n        httpContext.Request.Path = \"/api/test\";\n        httpContext.Request.QueryString = new QueryString(\"?a=b&c=d\");\n        httpContext.Request.Headers[\":authority\"] = \"example.com:3456\";\n        httpContext.Request.Headers[\"x-ms-request-test\"] = \"request\";\n        httpContext.Request.Headers[\"Content-Language\"] = \"requestLanguage\";\n\n        var requestBody = \"request content\";\n        httpContext.Request.Headers[\"Content-Length\"] = requestBody.Length.ToString();\n        httpContext.Request.Body = StringToStream(requestBody);\n        httpContext.Connection.RemoteIpAddress = IPAddress.Loopback;\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/a/b/\";\n        var targetUri = \"https://localhost:123/a/b/api/test?a=b&c=d\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                await Task.Yield();\n\n                Assert.Equal(new Version(2, 0), request.Version);\n                Assert.Equal(HttpMethod.Post, request.Method);\n                Assert.Equal(targetUri, request.RequestUri.AbsoluteUri);\n                Assert.Contains(\"request\", request.Headers.GetValues(\"x-ms-request-test\"));\n                Assert.Null(request.Headers.Host);\n                Assert.False(request.Headers.TryGetValues(\":authority\", out var value));\n\n                Assert.NotNull(request.Content);\n                Assert.Contains(\"requestLanguage\", request.Content.Headers.GetValues(\"Content-Language\"));\n\n                var capturedRequestContent = new MemoryStream();\n\n                // Use CopyToAsync as this is what HttpClient and friends use internally\n                await request.Content.CopyToWithCancellationAsync(capturedRequestContent);\n                capturedRequestContent.Position = 0;\n                var capturedContentText = StreamToString(capturedRequestContent);\n                Assert.Equal(\"request content\", capturedContentText);\n\n                var response = new HttpResponseMessage((HttpStatusCode)234);\n                response.ReasonPhrase = \"Test Reason Phrase\";\n                response.Headers.TryAddWithoutValidation(\"x-ms-response-test\", \"response\");\n                response.Content = new StreamContent(StringToStream(\"response content\"));\n                response.Content.Headers.TryAddWithoutValidation(\"Content-Language\", \"responseLanguage\");\n                return response;\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        Assert.Equal(234, httpContext.Response.StatusCode);\n        var reasonPhrase = httpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase;\n        Assert.Equal(\"Test Reason Phrase\", reasonPhrase);\n        Assert.Contains(\"response\", httpContext.Response.Headers[\"x-ms-response-test\"].ToArray());\n        Assert.Contains(\"responseLanguage\", httpContext.Response.Headers[\"Content-Language\"].ToArray());\n\n        proxyResponseStream.Position = 0;\n        var proxyResponseText = StreamToString(proxyResponseStream);\n        Assert.Equal(\"response content\", proxyResponseText);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages();\n    }\n\n    [Fact]\n    public async Task NormalRequestWithTransforms_Works()\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Protocol = \"http/2\";\n        httpContext.Request.Scheme = \"http\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Path = \"/path/base/dropped\";\n        httpContext.Request.Path = \"/api/test\";\n        httpContext.Request.QueryString = new QueryString(\"?a=b&c=d\");\n        httpContext.Request.Headers[\":authority\"] = \"example.com:3456\";\n        httpContext.Request.Headers[\"x-ms-request-test\"] = \"request\";\n        httpContext.Request.Headers[\"Content-Language\"] = \"requestLanguage\";\n        httpContext.Request.Body = StringToStream(\"request content\");\n        httpContext.Connection.RemoteIpAddress = IPAddress.Loopback;\n        httpContext.Features.Set<IHttpResponseTrailersFeature>(new TestTrailersFeature());\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/a/b/\";\n        Uri originalRequestUri = null;\n        var transforms = new DelegateHttpTransforms()\n        {\n            OnRequest = (context, request, destination) =>\n            {\n                originalRequestUri = request.RequestUri;\n                request.RequestUri = new Uri(destination + \"prefix\"\n                    + context.Request.Path + context.Request.QueryString);\n                request.Headers.Remove(\"transformHeader\");\n                request.Headers.TryAddWithoutValidation(\"transformHeader\", \"value\");\n                request.Headers.TryAddWithoutValidation(\"x-ms-request-test\", \"transformValue\");\n                request.Headers.Host = null;\n                return Task.CompletedTask;\n            },\n            OnResponse = (context, response) =>\n            {\n                context.Response.Headers[\"transformHeader\"] = \"value\";\n                context.Response.Headers.Append(\"x-ms-response-test\", \"value\");\n                return new(true);\n            },\n            OnResponseTrailers = (context, response) =>\n            {\n                context.Response.AppendTrailer(\"trailerTransform\", \"value\");\n                return Task.CompletedTask;\n            }\n        };\n\n        var targetUri = \"https://localhost:123/a/b/prefix/api/test?a=b&c=d\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                await Task.Yield();\n\n                Assert.Equal(new Version(2, 0), request.Version);\n                Assert.Equal(HttpMethod.Post, request.Method);\n                Assert.Equal(targetUri, request.RequestUri.AbsoluteUri);\n                Assert.Equal(new[] { \"value\" }, request.Headers.GetValues(\"transformHeader\"));\n                Assert.Equal(new[] { \"request\", \"transformValue\" }, request.Headers.GetValues(\"x-ms-request-test\"));\n                Assert.Null(request.Headers.Host);\n                Assert.False(request.Headers.TryGetValues(\":authority\", out var value));\n\n                Assert.NotNull(request.Content);\n                Assert.Contains(\"requestLanguage\", request.Content.Headers.GetValues(\"Content-Language\"));\n\n                var capturedRequestContent = new MemoryStream();\n\n                // Use CopyToAsync as this is what HttpClient and friends use internally\n                await request.Content.CopyToWithCancellationAsync(capturedRequestContent);\n                capturedRequestContent.Position = 0;\n                var capturedContentText = StreamToString(capturedRequestContent);\n                Assert.Equal(\"request content\", capturedContentText);\n\n                var response = new HttpResponseMessage((HttpStatusCode)234);\n                response.ReasonPhrase = \"Test Reason Phrase\";\n                response.Headers.TryAddWithoutValidation(\"x-ms-response-test\", \"response\");\n                response.Content = new StreamContent(StringToStream(\"response content\"));\n                response.Content.Headers.TryAddWithoutValidation(\"Content-Language\", \"responseLanguage\");\n                return response;\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms);\n\n        Assert.Null(originalRequestUri); // Should only be set by the transformer\n        Assert.Equal(ForwarderError.None, proxyError);\n        Assert.Equal(234, httpContext.Response.StatusCode);\n        var reasonPhrase = httpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase;\n        Assert.Null(reasonPhrase); // We don't set the ReasonPhrase for HTTP/2+\n        Assert.Equal(new[] { \"response\", \"value\" }, httpContext.Response.Headers[\"x-ms-response-test\"].ToArray());\n        Assert.Contains(\"responseLanguage\", httpContext.Response.Headers[\"Content-Language\"].ToArray());\n        Assert.Contains(\"value\", httpContext.Response.Headers[\"transformHeader\"].ToArray());\n        Assert.Equal(new[] { \"value\" }, httpContext.Features.Get<IHttpResponseTrailersFeature>().Trailers?[\"trailerTransform\"].ToArray());\n\n        proxyResponseStream.Position = 0;\n        var proxyResponseText = StreamToString(proxyResponseStream);\n        Assert.Equal(\"response content\", proxyResponseText);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages();\n    }\n\n    [Fact]\n    public async Task NormalRequestWithCopyRequestHeadersDisabled_Works()\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Scheme = \"http\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.PathBase = \"/api\";\n        httpContext.Request.Path = \"/test\";\n        httpContext.Request.QueryString = new QueryString(\"?a=b&c=d\");\n        httpContext.Request.Headers[\":authority\"] = \"example.com:3456\";\n        httpContext.Request.Headers[\"x-ms-request-test\"] = \"request\";\n        httpContext.Request.Headers[\"Content-Language\"] = \"requestLanguage\";\n        httpContext.Request.Headers[\"Transfer-Encoding\"] = \"chunked\";\n        httpContext.Request.Body = StringToStream(\"request content\");\n        httpContext.Connection.RemoteIpAddress = IPAddress.Loopback;\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/a/b/\";\n        var transforms = new DelegateHttpTransforms()\n        {\n            CopyRequestHeaders = false,\n            OnRequest = (context, request, destination) =>\n            {\n                request.Headers.TryAddWithoutValidation(\"x-ms-request-test\", \"transformValue\");\n                return Task.CompletedTask;\n            }\n        };\n        var targetUri = \"https://localhost:123/a/b/test?a=b&c=d\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                await Task.Yield();\n\n                Assert.Equal(new Version(2, 0), request.Version);\n                Assert.Equal(HttpMethod.Post, request.Method);\n                Assert.Equal(targetUri, request.RequestUri.AbsoluteUri);\n                Assert.Equal(new[] { \"transformValue\" }, request.Headers.GetValues(\"x-ms-request-test\"));\n                Assert.Null(request.Headers.Host);\n                Assert.False(request.Headers.TryGetValues(\":authority\", out var _));\n\n                Assert.NotNull(request.Content);\n                Assert.False(request.Content.Headers.TryGetValues(\"Content-Language\", out var _));\n\n                var capturedRequestContent = new MemoryStream();\n\n                // Use CopyToAsync as this is what HttpClient and friends use internally\n                await request.Content.CopyToWithCancellationAsync(capturedRequestContent);\n                capturedRequestContent.Position = 0;\n                var capturedContentText = StreamToString(capturedRequestContent);\n                Assert.Equal(\"request content\", capturedContentText);\n\n                var response = new HttpResponseMessage((HttpStatusCode)234);\n                response.ReasonPhrase = \"Test Reason Phrase\";\n                response.Headers.TryAddWithoutValidation(\"x-ms-response-test\", \"response\");\n                response.Content = new StreamContent(StringToStream(\"response content\"));\n                response.Content.Headers.TryAddWithoutValidation(\"Content-Language\", \"responseLanguage\");\n                return response;\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms);\n\n        Assert.Equal(ForwarderError.None, proxyError);\n        Assert.Equal(234, httpContext.Response.StatusCode);\n        var reasonPhrase = httpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase;\n        Assert.Equal(\"Test Reason Phrase\", reasonPhrase);\n        Assert.Contains(\"response\", httpContext.Response.Headers[\"x-ms-response-test\"].ToArray());\n        Assert.Contains(\"responseLanguage\", httpContext.Response.Headers[\"Content-Language\"].ToArray());\n\n        proxyResponseStream.Position = 0;\n        var proxyResponseText = StreamToString(proxyResponseStream);\n        Assert.Equal(\"response content\", proxyResponseText);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages();\n    }\n\n    [Fact]\n    public async Task TransformRequestAsync_ReplaceBody()\n    {\n        var events = TestEventListener.Collect();\n\n        var replaced = \"should be replaced\";\n        var replacing = \"request content\";\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Protocol = \"HTTP/2\";\n        httpContext.Request.Body = StringToStream(replaced);\n\n        var destinationPrefix = \"https://localhost/\";\n\n        var transforms = new DelegateHttpTransforms()\n        {\n            CopyRequestHeaders = true,\n            OnRequest = (context, request, destination) =>\n            {\n                context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(replacing));\n                return Task.CompletedTask;\n            }\n        };\n\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                await Task.Yield();\n\n                Assert.Equal(new Version(2, 0), request.Version);\n                Assert.Equal(HttpMethod.Post, request.Method);\n\n                Assert.NotNull(request.Content);\n\n                var capturedRequestContent = new MemoryStream();\n                // Use CopyToAsync as this is what HttpClient and friends use internally\n                await request.Content.CopyToWithCancellationAsync(capturedRequestContent);\n                capturedRequestContent.Position = 0;\n                var capturedContentText = StreamToString(capturedRequestContent);\n                Assert.Equal(replacing, capturedContentText);\n\n                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms);\n\n        Assert.Equal(ForwarderError.None, proxyError);\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n        var resultStream = (MemoryStream)httpContext.Request.Body;\n        Assert.Equal(Encoding.UTF8.GetBytes(replacing), resultStream.ToArray());\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages();\n    }\n\n    [Fact]\n    public async Task TransformRequestAsync_SetsStatus_ShortCircuits()\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Protocol = \"HTTP/2\";\n\n        var destinationPrefix = \"https://localhost/\";\n\n        var transforms = new DelegateHttpTransforms()\n        {\n            CopyRequestHeaders = true,\n            OnRequest = (context, request, destination) =>\n            {\n                context.Response.StatusCode = 401;\n                return Task.CompletedTask;\n            }\n        };\n\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                throw new NotImplementedException();\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms);\n\n        Assert.Equal(ForwarderError.None, proxyError);\n        Assert.Equal(StatusCodes.Status401Unauthorized, httpContext.Response.StatusCode);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(Array.Empty<ForwarderStage>());\n    }\n\n    [Fact]\n    public async Task TransformRequestAsync_StartsResponse_ShortCircuits()\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        var responseBody = new TestResponseBody();\n        httpContext.Features.Set<IHttpResponseFeature>(responseBody);\n        httpContext.Features.Set<IHttpResponseBodyFeature>(responseBody);\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Protocol = \"HTTP/2\";\n\n        var destinationPrefix = \"https://localhost/\";\n\n        var transforms = new DelegateHttpTransforms()\n        {\n            CopyRequestHeaders = true,\n            OnRequest = (context, request, destination) =>\n            {\n                return context.Response.StartAsync();\n            }\n        };\n\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                throw new NotImplementedException();\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms);\n\n        Assert.Equal(ForwarderError.None, proxyError);\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n        Assert.True(httpContext.Response.HasStarted);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(Array.Empty<ForwarderStage>());\n    }\n\n    [Fact]\n    public async Task TransformRequestAsync_WritesToResponse_ShortCircuits()\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        var resultStream = new MemoryStream();\n        var responseBody = new TestResponseBody(resultStream);\n        httpContext.Features.Set<IHttpResponseFeature>(responseBody);\n        httpContext.Features.Set<IHttpResponseBodyFeature>(responseBody);\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Protocol = \"HTTP/2\";\n\n        var destinationPrefix = \"https://localhost/\";\n\n        var transforms = new DelegateHttpTransforms()\n        {\n            CopyRequestHeaders = true,\n            OnRequest = (context, request, destination) =>\n            {\n                return context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(\"Hello World\")).AsTask();\n            }\n        };\n\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                throw new NotImplementedException();\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms);\n\n        Assert.Equal(ForwarderError.None, proxyError);\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n        Assert.True(httpContext.Response.HasStarted);\n        Assert.Equal(\"Hello World\", Encoding.UTF8.GetString(resultStream.ToArray()));\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(Array.Empty<ForwarderStage>());\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task TransformRequestAsync_ModifiesRequestContent_Throws(bool originalHasBody)\n    {\n        var events = TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new TestBodyDetector { CanHaveBody = originalHasBody });\n\n        var destinationPrefix = \"https://localhost/foo\";\n\n        var transforms = new DelegateHttpTransforms()\n        {\n            CopyRequestHeaders = true,\n            OnRequest = (context, request, destination) =>\n            {\n                request.Content = new StringContent(\"modified content\");\n                return Task.CompletedTask;\n            }\n        };\n\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                throw new NotImplementedException();\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transforms);\n\n        AssertErrorInfo<InvalidOperationException>(ForwarderError.RequestCreation, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix);\n        Assert.Empty(events.GetProxyStages());\n    }\n\n    // Tests proxying an upgradeable request.\n    [Theory]\n    [InlineData(\"WebSocket\")]\n    [InlineData(\"SPDY/3.1\")]\n    public async Task UpgradableRequest_Works(string upgradeHeader)\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Scheme = \"http\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Path = \"/api/test\";\n        httpContext.Request.QueryString = new QueryString(\"?a=b&c=d\");\n        httpContext.Request.Headers[\":authority\"] = \"example.com:3456\";\n        httpContext.Request.Headers[\"x-ms-request-test\"] = \"request\";\n        httpContext.Connection.RemoteIpAddress = IPAddress.Loopback;\n        httpContext.Request.Headers.Upgrade = upgradeHeader;\n\n        var downstreamStream = new DuplexStream(\n            readStream: StringToStream(\"request content\"),\n            writeStream: new MemoryStream());\n        DuplexStream upstreamStream = null;\n\n        var upgradeFeatureMock = new Mock<IHttpUpgradeFeature>();\n        upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true);\n        upgradeFeatureMock.Setup(u => u.UpgradeAsync()).ReturnsAsync(downstreamStream);\n        httpContext.Features.Set(upgradeFeatureMock.Object);\n\n        var destinationPrefix = \"https://localhost:123/a/b/\";\n        var targetUri = \"https://localhost:123/a/b/api/test?a=b&c=d\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                await Task.Yield();\n\n                Assert.Equal(new Version(1, 1), request.Version);\n                Assert.Equal(HttpMethod.Get, request.Method);\n                Assert.Equal(targetUri, request.RequestUri.AbsoluteUri);\n                Assert.Contains(\"request\", request.Headers.GetValues(\"x-ms-request-test\"));\n                Assert.Null(request.Headers.Host);\n                Assert.False(request.Headers.TryGetValues(\":authority\", out var value));\n\n                Assert.Null(request.Content);\n\n                var response = new HttpResponseMessage(HttpStatusCode.SwitchingProtocols);\n                response.Headers.TryAddWithoutValidation(\"x-ms-response-test\", \"response\");\n                upstreamStream = new DuplexStream(\n                    readStream: StringToStream(\"response content\"),\n                    writeStream: new MemoryStream());\n                response.Content = new RawStreamContent(upstreamStream);\n                return response;\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig()\n        {\n            Version = HttpVersion.Version11,\n        });\n\n        Assert.Equal(StatusCodes.Status101SwitchingProtocols, httpContext.Response.StatusCode);\n        Assert.Contains(\"response\", httpContext.Response.Headers[\"x-ms-response-test\"].ToArray());\n\n        downstreamStream.WriteStream.Position = 0;\n        var returnedToDownstream = StreamToString(downstreamStream.WriteStream);\n        Assert.Equal(\"response content\", returnedToDownstream);\n\n        Assert.NotNull(upstreamStream);\n        upstreamStream.WriteStream.Position = 0;\n        var sentToUpstream = StreamToString(upstreamStream.WriteStream);\n        Assert.Equal(\"request content\", sentToUpstream);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(upgrade: true);\n    }\n\n    // Tests proxying an upgradeable request where the destination refused to upgrade.\n    // We should still proxy back the response.\n    [Fact]\n    public async Task UpgradableRequestFailsToUpgrade_ProxiesResponse()\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Scheme = \"https\";\n        httpContext.Request.Host = new HostString(\"example.com\");\n        httpContext.Request.Path = \"/api/test\";\n        httpContext.Request.QueryString = new QueryString(\"?a=b&c=d\");\n        httpContext.Request.Headers[\":host\"] = \"example.com\";\n        httpContext.Request.Headers[\"x-ms-request-test\"] = \"request\";\n        httpContext.Request.Headers.Upgrade = \"WebSocket\";\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var upgradeFeatureMock = new Mock<IHttpUpgradeFeature>(MockBehavior.Strict);\n        upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true);\n        httpContext.Features.Set(upgradeFeatureMock.Object);\n\n        var destinationPrefix = \"https://localhost:123/a/b/\";\n        var targetUri = \"https://localhost:123/a/b/api/test?a=b&c=d\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                await Task.Yield();\n\n                Assert.Equal(new Version(1, 1), request.Version);\n                Assert.Equal(HttpMethod.Get, request.Method);\n                Assert.Equal(targetUri, request.RequestUri.AbsoluteUri);\n                Assert.Contains(\"request\", request.Headers.GetValues(\"x-ms-request-test\"));\n\n                Assert.Null(request.Content);\n\n                var response = new HttpResponseMessage((HttpStatusCode)234);\n                response.ReasonPhrase = \"Test Reason Phrase\";\n                response.Headers.TryAddWithoutValidation(\"x-ms-response-test\", \"response\");\n                response.Content = new StreamContent(StringToStream(\"response content\"));\n                response.Content.Headers.TryAddWithoutValidation(\"Content-Language\", \"responseLanguage\");\n                return response;\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig()\n        {\n            Version = HttpVersion.Version11,\n        });\n\n        Assert.Equal(234, httpContext.Response.StatusCode);\n        var reasonPhrase = httpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase;\n        Assert.Equal(\"Test Reason Phrase\", reasonPhrase);\n        Assert.Contains(\"response\", httpContext.Response.Headers[\"x-ms-response-test\"].ToArray());\n        Assert.Contains(\"responseLanguage\", httpContext.Response.Headers[\"Content-Language\"].ToArray());\n\n        proxyResponseStream.Position = 0;\n        var proxyResponseText = StreamToString(proxyResponseStream);\n        Assert.Equal(\"response content\", proxyResponseText);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(hasRequestContent: false, upgrade: false);\n    }\n\n    [Fact]\n    public async Task UpgradableSpdyRequest_DisallowedByVersionPolicy_Fails()\n    {\n        var events = TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Headers.Upgrade = \"SPDY/3.1\";\n\n        var upgradeFeatureMock = new Mock<IHttpUpgradeFeature>();\n        upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true);\n        httpContext.Features.Set(upgradeFeatureMock.Object);\n\n        var destinationPrefix = \"https://localhost:123/a/b/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient((_, _) => throw new InvalidOperationException(\"Unreachable\"));\n        var requestConfig = new ForwarderRequestConfig\n        {\n            Version = HttpVersion.Version20,\n            VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher\n        };\n\n        var error = await sut.SendAsync(httpContext, destinationPrefix, client, requestConfig);\n\n        var ex = AssertErrorInfo<HttpRequestException>(ForwarderError.RequestCreation, StatusCodes.Status502BadGateway, error, httpContext, destinationPrefix);\n        Assert.Contains(\"SPDY requests require HTTP/1.1 support\", ex.Message);\n\n        // Error thrown before sending the request.\n        events.AssertContainProxyStages([]);\n    }\n\n    [Fact]\n    public async Task UpgradableRequest_CancelsIfIdle()\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Scheme = \"http\";\n        httpContext.Request.Path = \"/api/test\";\n        httpContext.Connection.RemoteIpAddress = IPAddress.Loopback;\n        httpContext.Request.Headers.Upgrade = \"WebSocket\";\n\n        var idleTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var downstreamStream = new DuplexStream(\n            readStream: new StallStream(ct =>\n            {\n                ct.Register(() => idleTcs.TrySetCanceled());\n                return idleTcs.Task;\n            }),\n            writeStream: new MemoryStream());\n        DuplexStream upstreamStream = null;\n\n        var upgradeFeatureMock = new Mock<IHttpUpgradeFeature>();\n        upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true);\n        upgradeFeatureMock.Setup(u => u.UpgradeAsync()).ReturnsAsync(downstreamStream);\n        httpContext.Features.Set(upgradeFeatureMock.Object);\n\n        var destinationPrefix = \"https://localhost:123/a/b/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                await Task.Yield();\n\n                Assert.Equal(new Version(1, 1), request.Version);\n                Assert.Equal(HttpMethod.Get, request.Method);\n\n                Assert.Null(request.Content);\n\n                var response = new HttpResponseMessage(HttpStatusCode.SwitchingProtocols);\n                upstreamStream = new DuplexStream(\n                    readStream: new StallStream(ct =>\n                    {\n                        ct.Register(() => idleTcs.TrySetCanceled());\n                        return idleTcs.Task;\n                    }),\n                    writeStream: new MemoryStream());\n                response.Content = new RawStreamContent(upstreamStream);\n                return response;\n            });\n\n        var result = await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig\n        {\n            Version = HttpVersion.Version11,\n            ActivityTimeout = TimeSpan.FromSeconds(1),\n        }).DefaultTimeout();\n\n        Assert.Equal(StatusCodes.Status101SwitchingProtocols, httpContext.Response.StatusCode);\n\n        Assert.Equal(ForwarderError.UpgradeActivityTimeout, result);\n\n        events.AssertContainProxyStages(upgrade: true);\n    }\n\n    [Theory]\n    [InlineData(\"TRACE\", \"HTTP/1.1\", \"\")]\n    [InlineData(\"TRACE\", \"HTTP/2\", \"\")]\n    [InlineData(\"GET\", \"HTTP/1.1\", \"\")]\n    [InlineData(\"GET\", \"HTTP/2\", \"\")]\n    [InlineData(\"GET\", \"HTTP/1.1\", \"Content-Length:0\")]\n    [InlineData(\"HEAD\", \"HTTP/1.1\", \"\")]\n    [InlineData(\"POST\", \"HTTP/1.1\", \"\")]\n    [InlineData(\"POST\", \"HTTP/1.1\", \"Content-Length:0\")]\n    [InlineData(\"POST\", \"HTTP/1.1\", \"Content-Length:0;Content-Type:text/plain\")]\n    [InlineData(\"POST\", \"HTTP/2\", \"Content-Length:0\")]\n    [InlineData(\"POST\", \"HTTP/2\", \"Content-Length:0;Content-Type:text/plain\")]\n    [InlineData(\"PATCH\", \"HTTP/1.1\", \"\")]\n    [InlineData(\"DELETE\", \"HTTP/1.1\", \"\")]\n    [InlineData(\"Unknown\", \"HTTP/1.1\", \"\")]\n    // [InlineData(\"CONNECT\", \"HTTP/1.1\", \"\")] Blocked in HttpUtilities.GetHttpMethod\n    [InlineData(\"GET\", \"HTTP/1.1\", \"Allow:Foo\")]\n    [InlineData(\"GET\", \"HTTP/1.1\", \"Content-Disposition:Foo\")]\n    [InlineData(\"GET\", \"HTTP/1.1\", \"Content-Encoding:Foo\")]\n    [InlineData(\"GET\", \"HTTP/1.1\", \"Content-Language:Foo\")]\n    [InlineData(\"GET\", \"HTTP/1.1\", \"Content-Location:Foo\")]\n    [InlineData(\"GET\", \"HTTP/1.1\", \"Content-MD5:Foo\")]\n    [InlineData(\"GET\", \"HTTP/1.1\", \"Content-Range:Foo\")]\n    [InlineData(\"GET\", \"HTTP/1.1\", \"Content-Type:Foo\")]\n    [InlineData(\"GET\", \"HTTP/1.1\", \"Expires:Foo\")]\n    [InlineData(\"GET\", \"HTTP/1.1\", \"Last-Modified:Foo\")]\n    public async Task RequestWithoutBodies_NoHttpContent(string method, string protocol, string headerList)\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = method;\n        httpContext.Request.Protocol = protocol;\n\n        var headers = headerList\n            .Split(';', StringSplitOptions.RemoveEmptyEntries)\n            .Select(header => (Key: header.Split(':')[0], Value: header.Split(':')[1]))\n            .ToArray();\n\n        foreach (var (key, value) in headers)\n        {\n            httpContext.Request.Headers[key] = value;\n        }\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                Assert.Equal(new Version(2, 0), request.Version);\n                Assert.Equal(method, request.Method.Method, StringComparer.OrdinalIgnoreCase);\n\n                // When adding content-specific headers, we will inject an EmptyHttpContent if no other content is present\n                if (headers.Any())\n                {\n                    Assert.NotNull(request.Content);\n                    Assert.IsType<EmptyHttpContent>(request.Content);\n                    Assert.Empty(await request.Content.ReadAsByteArrayAsync(cancellationToken));\n\n                    foreach (var (key, value) in headers)\n                    {\n                        Assert.True(request.Content.Headers.TryGetValues(key, out var values));\n                        Assert.Equal(value, Assert.Single(values));\n                    }\n\n                    // If a custom content is injected, so is a \"Content-Length: 0\" header\n                    Assert.True(request.Content.Headers.TryGetValues(HeaderNames.ContentLength, out var contentLength));\n                    Assert.Equal(\"0\", Assert.Single(contentLength));\n                }\n                else\n                {\n                    Assert.Null(request.Content);\n                }\n\n                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(hasRequestContent: false);\n    }\n\n    [Theory]\n    [InlineData(\"POST\", \"HTTP/2\", \"\", \"\")]\n    [InlineData(\"PATCH\", \"HTTP/2\", \"\", \"\")]\n    [InlineData(\"UNKNOWN\", \"HTTP/2\", \"\", \"\")]\n    [InlineData(\"UNKNOWN\", \"HTTP/1.1\", \"Content-Length:10\", \"aaaaaaaaaa\")]\n    [InlineData(\"UNKNOWN\", \"HTTP/1.1\", \"transfer-encoding:Chunked\", \"\")]\n    [InlineData(\"GET\", \"HTTP/1.1\", \"Content-Length:10\", \"aaaaaaaaaa\")]\n    [InlineData(\"GET\", \"HTTP/2\", \"Content-Length:10\", \"aaaaaaaaaa\")]\n    [InlineData(\"HEAD\", \"HTTP/1.1\", \"transfer-encoding:Chunked\", \"\")]\n    [InlineData(\"HEAD\", \"HTTP/2\", \"transfer-encoding:Chunked\", \"\")]\n    public async Task RequestWithBodies_HasHttpContent(string method, string protocol, string headers, string body)\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = method;\n        httpContext.Request.Protocol = protocol;\n        foreach (var header in headers.Split(';', StringSplitOptions.RemoveEmptyEntries))\n        {\n            var parts = header.Split(':');\n            var key = parts[0];\n            var value = parts[1];\n            httpContext.Request.Headers[key] = value;\n        }\n        httpContext.Request.Body = StringToStream(body);\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                Assert.Equal(new Version(2, 0), request.Version);\n                Assert.Equal(method, request.Method.Method, StringComparer.OrdinalIgnoreCase);\n\n                Assert.NotNull(request.Content);\n\n                // Must consume the body\n                await request.Content.CopyToWithCancellationAsync(Stream.Null);\n\n                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages();\n    }\n\n    [Theory]\n    [InlineData(\"1.1\", 1, \"\")]\n    [InlineData(\"1.1\", 1, \"aa\")]\n    [InlineData(\"1.1\", 2, \"a\")]\n    [InlineData(\"2.0\", 1, \"\")]\n    [InlineData(\"2.0\", 1, \"aa\")]\n    [InlineData(\"2.0\", 2, \"a\")]\n    public async Task RequestWithBodies_WrongContentLength(string version, long contentLength, string body)\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.ContentLength = contentLength;\n        httpContext.Request.Body = StringToStream(body);\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                Assert.Equal(new Version(version), request.Version);\n\n                Assert.NotNull(request.Content);\n\n                // Must throw\n                try\n                {\n                    await request.Content.CopyToWithCancellationAsync(Stream.Null);\n                }\n                catch (HttpRequestException ex)\n                {\n                    Assert.Contains(\"Content-Length\", ex.InnerException.InnerException.Message);\n                    throw;\n                }\n\n                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n            });\n\n        var options = new ForwarderRequestConfig\n        {\n            Version = new Version(version),\n        };\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, options);\n\n        AssertErrorInfoAndStages<AggregateException>(ForwarderError.RequestBodyClient, StatusCodes.Status400BadRequest, proxyError, httpContext, destinationPrefix, ForwarderStage.RequestContentTransferStart);\n    }\n\n    [Fact]\n    public async Task RequestWithBodies_WithoutContentLength()\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Protocol = \"HTTP/2\";\n        httpContext.Request.Body = StringToStream(\"request content\");\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                Assert.Equal(new Version(2, 0), request.Version);\n\n                Assert.NotNull(request.Content);\n\n                // Must consume the body\n                await request.Content.CopyToWithCancellationAsync(Stream.Null);\n\n                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages();\n    }\n\n    [Fact]\n    public async Task BodyDetectionFeatureSaysNo_NoHttpContent()\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = HttpMethods.Post;\n        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new TestBodyDetector() { CanHaveBody = false });\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                Assert.Equal(new Version(2, 0), request.Version);\n\n                Assert.Null(request.Content);\n\n                var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n                return Task.FromResult(response);\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(hasRequestContent: false);\n    }\n\n    [Fact]\n    public async Task BodyDetectionFeatureSaysYes_HasHttpContent()\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = HttpMethods.Get;\n        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new TestBodyDetector() { CanHaveBody = true });\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                Assert.Equal(new Version(2, 0), request.Version);\n\n                Assert.NotNull(request.Content);\n\n                // Must consume the body\n                await request.Content.CopyToWithCancellationAsync(Stream.Null);\n\n                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages();\n    }\n\n    private class TestBodyDetector : IHttpRequestBodyDetectionFeature\n    {\n        public bool CanHaveBody { get; set; }\n    }\n\n    [Theory]\n    [InlineData(\"testA=A_Value, testB=B_Value, testC=C_Value\")]\n    public async Task RequestWithCookieHeaders(params string[] cookies)\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Headers[HeaderNames.Cookie] = cookies;\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                // \"testA=A_Cookie; testB=B_Cookie; testC=C_Cookie\"\n                var expectedCookieString = string.Join(\"; \", cookies);\n\n                Assert.Equal(new Version(2, 0), request.Version);\n                Assert.Equal(\"GET\", request.Method.Method, StringComparer.OrdinalIgnoreCase);\n                Assert.Null(request.Content);\n                Assert.True(request.Headers.TryGetValues(HeaderNames.Cookie, out var cookieHeaders));\n                Assert.NotNull(cookieHeaders);\n                var cookie = Assert.Single(cookieHeaders);\n                Assert.Equal(expectedCookieString, cookie);\n\n                var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n                return Task.FromResult(response);\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        Assert.Null(httpContext.Features.Get<IForwarderErrorFeature>());\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(hasRequestContent: false);\n    }\n\n    [Theory]\n    [MemberData(nameof(RequestMultiHeadersData))]\n    public async Task RequestWithMultiHeaders(string version, string headerName, string[] headers)\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Headers[headerName] = headers;\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                Assert.Equal(new Version(version), request.Version);\n                Assert.Equal(\"GET\", request.Method.Method, StringComparer.OrdinalIgnoreCase);\n                IEnumerable<string> sentHeaders;\n                if (headerName.StartsWith(\"Content\"))\n                {\n                    Assert.True(request.Content.Headers.TryGetValues(headerName, out sentHeaders));\n                }\n                else\n                {\n                    Assert.True(request.Headers.TryGetValues(headerName, out sentHeaders));\n                }\n\n                Assert.NotNull(sentHeaders);\n                AreEqualIgnoringEmptyStrings(sentHeaders, headers);\n\n                var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n                return Task.FromResult(response);\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = new Version(version) });\n\n        Assert.Null(httpContext.Features.Get<IForwarderErrorFeature>());\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(hasRequestContent: false);\n    }\n\n    [Theory]\n    [MemberData(nameof(RequestEmptyMultiHeadersData))]\n    public async Task RequestWithEmptyMultiHeaders(string version, string headerName, string[] headers)\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Headers[headerName] = headers;\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                Assert.Equal(new Version(version), request.Version);\n                Assert.Equal(\"GET\", request.Method.Method, StringComparer.OrdinalIgnoreCase);\n                HeaderStringValues sentHeaders;\n                if (headerName.StartsWith(\"Content\"))\n                {\n                    Assert.True(request.Content.Headers.NonValidated.TryGetValues(headerName, out sentHeaders));\n                }\n                else\n                {\n                    Assert.True(request.Headers.NonValidated.TryGetValues(headerName, out sentHeaders));\n                }\n                Assert.Equal(sentHeaders, headers);\n\n                var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n                return Task.FromResult(response);\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = new Version(version) });\n\n        Assert.Null(httpContext.Features.Get<IForwarderErrorFeature>());\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(hasRequestContent: false);\n    }\n\n    internal static void AreEqualIgnoringEmptyStrings(IEnumerable<string> left, IEnumerable<string> right)\n    => Assert.Equal(left.Where(s => !string.IsNullOrEmpty(s)), right.Where(s => !string.IsNullOrEmpty(s)));\n\n    public static IEnumerable<string> RequestMultiHeaderNames()\n    {\n        var headers = new[]\n        {\n            HeaderNames.Accept,\n            HeaderNames.AcceptCharset,\n            HeaderNames.AcceptEncoding,\n            HeaderNames.AcceptLanguage,\n            HeaderNames.ContentEncoding,\n            HeaderNames.ContentLanguage,\n            HeaderNames.ContentType,\n            HeaderNames.Via\n        };\n\n        foreach (var header in headers)\n        {\n            yield return header;\n        }\n    }\n\n    public static IEnumerable<string> ResponseMultiHeaderNames()\n    {\n        var headers = new[]\n        {\n            HeaderNames.AcceptRanges,\n            HeaderNames.Allow,\n            HeaderNames.ContentEncoding,\n            HeaderNames.ContentLanguage,\n            HeaderNames.ContentRange,\n            HeaderNames.ContentType,\n            HeaderNames.SetCookie,\n            HeaderNames.Via,\n            HeaderNames.Warning,\n            HeaderNames.WWWAuthenticate\n        };\n\n        foreach (var header in headers)\n        {\n            yield return header;\n        }\n    }\n\n    public static IEnumerable<string[]> MultiValues()\n    {\n        var values = new string[][] {\n            new[] { \"testA=A_Value\", \"testB=B_Value\", \"testC=C_Value\" },\n            new[] { \"testA=A_Value, testB=B_Value\", \"testC=C_Value\" },\n            new[] { \"testA=A_Value\", \"\",  \"testB=B_Value, testC=C_Value\" },\n            new[] { \"testA=A_Value, testB=B_Value, testC=C_Value\" }\n        };\n\n        foreach (var value in values)\n        {\n            yield return value;\n        }\n    }\n\n    public static IEnumerable<object[]> RequestMultiHeadersData()\n    {\n        foreach (var header in RequestMultiHeaderNames())\n        {\n            foreach (var value in MultiValues())\n            {\n                foreach (var version in new[] { \"1.1\", \"2.0\" })\n                {\n                    yield return new object[] { version, header, value };\n                }\n            }\n        }\n    }\n\n    public static IEnumerable<object[]> ResponseMultiHeadersData()\n    {\n        foreach (var header in ResponseMultiHeaderNames())\n        {\n            foreach (var version in new[] { \"1.1\", \"2.0\" })\n            {\n                foreach (var value in MultiValues())\n                {\n                    yield return new object[] { version, header, value };\n                }\n                yield return new object[] { version, header, new[] { \"\", \"\" } };\n            }\n        }\n    }\n\n    public static IEnumerable<object[]> RequestEmptyMultiHeadersData()\n    {\n        foreach (var header in RequestMultiHeaderNames())\n        {\n            foreach (var version in new[] { \"1.1\", \"2.0\" })\n            {\n                yield return new object[] { version, header, new[] { \"\", \"\" } };\n            }\n        }\n    }\n\n    public static IEnumerable<object[]> ResponseEmptyMultiHeadersData()\n    {\n        foreach (var header in ResponseMultiHeaderNames())\n        {\n            foreach (var version in new[] { \"1.1\", \"2.0\" })\n            {\n                yield return new object[] { version, header, new[] { \"\", \"\" } };\n            }\n        }\n    }\n\n    [Fact]\n    public async Task OptionsWithVersion()\n    {\n        var events = TestEventListener.Collect();\n\n        // Use any non-default value\n        var version = new Version(3, 0);\n        var versionPolicy = HttpVersionPolicy.RequestVersionExact;\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                Assert.Equal(version, request.Version);\n                Assert.Equal(versionPolicy, request.VersionPolicy);\n                Assert.Equal(\"GET\", request.Method.Method, StringComparer.OrdinalIgnoreCase);\n                Assert.Null(request.Content);\n\n                var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n                return Task.FromResult(response);\n            });\n\n        var options = new ForwarderRequestConfig\n        {\n            Version = version,\n            VersionPolicy = versionPolicy,\n        };\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, options);\n\n        Assert.Equal(ForwarderError.None, proxyError);\n        Assert.Null(httpContext.Features.Get<IForwarderErrorFeature>());\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(hasRequestContent: false);\n    }\n\n    [Fact]\n    public async Task OptionsWithVersion_Transformed()\n    {\n        var events = TestEventListener.Collect();\n\n        // Use any non-default value\n        var version = new Version(0, 9);\n        var transformedVersion = new Version(3, 0);\n        var versionPolicy = HttpVersionPolicy.RequestVersionExact;\n        var transformedVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                Assert.Equal(transformedVersion, request.Version);\n                Assert.Equal(transformedVersionPolicy, request.VersionPolicy);\n                Assert.Equal(\"GET\", request.Method.Method, StringComparer.OrdinalIgnoreCase);\n                Assert.Null(request.Content);\n\n                var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n                return Task.FromResult(response);\n            });\n\n        var transforms = new DelegateHttpTransforms()\n        {\n            CopyRequestHeaders = false,\n            OnRequest = (context, request, destination) =>\n            {\n                Assert.Equal(version, request.Version);\n                request.Version = transformedVersion;\n                Assert.Equal(versionPolicy, request.VersionPolicy);\n                request.VersionPolicy = transformedVersionPolicy;\n                return Task.CompletedTask;\n            }\n        };\n\n        var requestOptions = new ForwarderRequestConfig\n        {\n            Version = version,\n            VersionPolicy = versionPolicy,\n        };\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, requestOptions, transforms);\n\n        Assert.Equal(ForwarderError.None, proxyError);\n        Assert.Null(httpContext.Features.Get<IForwarderErrorFeature>());\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(hasRequestContent: false);\n    }\n\n    [Fact]\n    public async Task UnableToConnect_Returns502()\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                throw new HttpRequestException(\"No connection could be made because the target machine actively refused it.\");\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfoAndStages<HttpRequestException>(ForwarderError.Request, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix);\n        Assert.Equal(0, proxyResponseStream.Length);\n    }\n\n    [Fact]\n    public async Task UnableToConnectWithBody_Returns502()\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Body = new MemoryStream(new byte[1]);\n        httpContext.Request.ContentLength = 1;\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                throw new HttpRequestException(\"No connection could be made because the target machine actively refused it.\");\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfoAndStages<HttpRequestException>(ForwarderError.Request, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix);\n        Assert.Equal(0, proxyResponseStream.Length);\n    }\n\n    [Fact]\n    public async Task RequestTimedOut_Returns504()\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                cancellationToken.WaitHandle.WaitOne();\n                cancellationToken.ThrowIfCancellationRequested();\n                return Task.FromResult(new HttpResponseMessage());\n            });\n\n        // Time out immediately\n        var requestOptions = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromTicks(1) };\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, requestOptions);\n\n        AssertErrorInfoAndStages<OperationCanceledException>(ForwarderError.RequestTimedOut, StatusCodes.Status504GatewayTimeout, proxyError, httpContext, destinationPrefix);\n        Assert.Equal(0, proxyResponseStream.Length);\n    }\n\n    [Fact]\n    public async Task RequestConnectTimedOut_Returns504()\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://microsoft.com:123/\"; // Port that doesn't accept connections\n        var sut = CreateProxy();\n\n        using var client = new HttpMessageInvoker(new SocketsHttpHandler\n        {\n            // Time out immediately\n            ConnectTimeout = TimeSpan.FromTicks(1)\n        });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfoAndStages<OperationCanceledException>(ForwarderError.RequestTimedOut, StatusCodes.Status504GatewayTimeout, proxyError, httpContext, destinationPrefix);\n        Assert.Equal(0, proxyResponseStream.Length);\n    }\n\n    [Fact]\n    public async Task RequestCanceled_Returns400()\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.RequestAborted = new CancellationToken(canceled: true);\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                cancellationToken.ThrowIfCancellationRequested();\n                return Task.FromResult(new HttpResponseMessage());\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfoAndStages<OperationCanceledException>(ForwarderError.RequestCanceled, StatusCodes.Status400BadRequest, proxyError, httpContext, destinationPrefix);\n        Assert.Equal(0, proxyResponseStream.Length);\n    }\n\n    [Fact]\n    public async Task RequestWithBodyTimedOut_Returns504()\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Body = new MemoryStream(new byte[1]);\n        httpContext.Request.ContentLength = 1;\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                cancellationToken.WaitHandle.WaitOne();\n                cancellationToken.ThrowIfCancellationRequested();\n                return Task.FromResult(new HttpResponseMessage());\n            });\n\n        // Time out immediately\n        var requestOptions = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromTicks(1) };\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, requestOptions);\n\n        AssertErrorInfoAndStages<OperationCanceledException>(ForwarderError.RequestTimedOut, StatusCodes.Status504GatewayTimeout, proxyError, httpContext, destinationPrefix);\n        Assert.Equal(0, proxyResponseStream.Length);\n    }\n\n    [Fact]\n    public async Task RequestWithBody_KeptAliveByActivity()\n    {\n        var events = TestEventListener.Collect();\n\n        var reads = 0;\n        var expectedReads = 6;\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Body = new CallbackReadStream(async (memory, ct) =>\n        {\n            if (memory.Length == 0 || reads >= expectedReads)\n            {\n                return 0;\n            }\n            reads++;\n            await Task.Delay(TimeSpan.FromMilliseconds(250), ct);\n            memory.Span[0] = (byte)'a';\n            return 1;\n        });\n        httpContext.Request.ContentLength = expectedReads;\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                Assert.Equal(new Version(2, 0), request.Version);\n                Assert.Equal(\"POST\", request.Method.Method, StringComparer.OrdinalIgnoreCase);\n\n                Assert.NotNull(request.Content);\n\n                // Must consume the body\n                var body = new MemoryStream();\n                await request.Content.CopyToWithCancellationAsync(body);\n\n                Assert.Equal(expectedReads, body.Length);\n\n                cancellationToken.ThrowIfCancellationRequested();\n\n                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n            });\n\n        var requestOptions = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(1) };\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, requestOptions);\n\n        Assert.Equal(ForwarderError.None, proxyError);\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n        Assert.Equal(0, proxyResponseStream.Length);\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages([ForwarderStage.SendAsyncStart, ForwarderStage.SendAsyncStop,\n            ForwarderStage.RequestContentTransferStart, ForwarderStage.ResponseContentTransferStart]);\n    }\n\n    [Fact]\n    public async Task RequestWithBodyCanceled_Returns400()\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Body = new MemoryStream(new byte[1]);\n        httpContext.Request.ContentLength = 1;\n        httpContext.RequestAborted = new CancellationToken(canceled: true);\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                cancellationToken.ThrowIfCancellationRequested();\n                return Task.FromResult(new HttpResponseMessage());\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfoAndStages<OperationCanceledException>(ForwarderError.RequestCanceled, StatusCodes.Status400BadRequest, proxyError, httpContext, destinationPrefix);\n        Assert.Equal(0, proxyResponseStream.Length);\n    }\n\n    [Fact]\n    public async Task RequestBodyClientErrorBeforeResponseError_Returns400()\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Body = new ThrowStream(throwOnFirstRead: true);\n        httpContext.Request.ContentLength = 1;\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                // Should throw.\n                await request.Content.CopyToWithCancellationAsync(Stream.Null);\n                return new HttpResponseMessage();\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfoAndStages<AggregateException>(ForwarderError.RequestBodyClient, StatusCodes.Status400BadRequest, proxyError, httpContext, destinationPrefix, ForwarderStage.RequestContentTransferStart);\n        Assert.Equal(0, proxyResponseStream.Length);\n    }\n\n    [Theory]\n    [InlineData(StatusCodes.Status413PayloadTooLarge)]\n    public async Task NonGenericRequestBodyClientErrorCode_ReturnsNonGenericClientErrorCode(int statusCode)\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.ContentLength = 1;\n        httpContext.Request.Body = new ThrowBadHttpRequestExceptionStream(statusCode);\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (request, _) =>\n            {\n                Assert.NotNull(request.Content);\n\n                await request.Content.CopyToWithCancellationAsync(Stream.Null);\n\n                return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty<byte>()) };\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfoAndStages<AggregateException>(ForwarderError.RequestBodyClient, statusCode, proxyError, httpContext, destinationPrefix, ForwarderStage.RequestContentTransferStart);\n    }\n\n    [Fact]\n    public async Task RequestBodyDestinationErrorBeforeResponseError_Returns502()\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Body = new MemoryStream(new byte[1]);\n        httpContext.Request.ContentLength = 1;\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                // Doesn't throw for destination errors\n                await request.Content.CopyToWithCancellationAsync(new ThrowStream());\n                throw new HttpRequestException();\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfoAndStages<AggregateException>(ForwarderError.RequestBodyDestination, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix, ForwarderStage.RequestContentTransferStart);\n        Assert.Equal(0, proxyResponseStream.Length);\n    }\n\n    [Fact]\n    public async Task RequestBodyCanceledBeforeResponseError_Returns502()\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Body = new MemoryStream(new byte[1]);\n        httpContext.Request.ContentLength = 1;\n        httpContext.RequestAborted = new CancellationToken(canceled: true);\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                // should throw\n                try\n                {\n                    await request.Content.CopyToWithCancellationAsync(new MemoryStream());\n                }\n                catch (OperationCanceledException) { }\n                throw new HttpRequestException();\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfoAndStages<AggregateException>(ForwarderError.RequestBodyCanceled, StatusCodes.Status400BadRequest, proxyError, httpContext, destinationPrefix);\n        Assert.Equal(0, proxyResponseStream.Length);\n    }\n\n    [Fact]\n    public async Task ResponseBodySuppressedByTransform_ReturnsStatusCodeAndHeaders()\n    {\n        var events = TestEventListener.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                var message = new HttpResponseMessage()\n                {\n                    StatusCode = HttpStatusCode.UnprocessableEntity,\n                    Content = new StreamContent(new ThrowStream(throwOnFirstRead: true))\n                };\n                message.Headers.AcceptRanges.Add(\"bytes\");\n                return Task.FromResult(message);\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, new DelegateHttpTransforms()\n        {\n            OnResponse = (context, proxyResponse) =>\n            {\n                Assert.Equal(HttpStatusCode.UnprocessableEntity, proxyResponse.StatusCode);\n                Assert.Equal(StatusCodes.Status422UnprocessableEntity, context.Response.StatusCode);\n                return new(false);\n            }\n        });\n\n        Assert.Equal(ForwarderError.None, proxyError);\n        Assert.Equal(StatusCodes.Status422UnprocessableEntity, httpContext.Response.StatusCode);\n        Assert.Equal(0, proxyResponseStream.Length);\n        Assert.Equal(\"bytes\", httpContext.Response.Headers[HeaderNames.AcceptRanges]);\n        Assert.Null(httpContext.Features.Get<IForwarderErrorFeature>());\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(hasRequestContent: false, hasResponseContent: false);\n    }\n\n    [Fact]\n    public async Task ResponseBodyDestinationErrorFirstRead_Returns502()\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                var message = new HttpResponseMessage()\n                {\n                    Content = new StreamContent(new ThrowStream(throwOnFirstRead: true))\n                };\n                message.Headers.AcceptRanges.Add(\"bytes\");\n                return Task.FromResult(message);\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfoAndStages<IOException>(ForwarderError.ResponseBodyDestination, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix, ForwarderStage.SendAsyncStop, ForwarderStage.ResponseContentTransferStart);\n        Assert.Equal(0, proxyResponseStream.Length);\n        Assert.Empty(httpContext.Response.Headers);\n    }\n\n    [Fact]\n    public async Task ResponseBodyDestinationErrorSecondRead_Aborted()\n    {\n        var events = TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        var responseBody = new TestResponseBody();\n        httpContext.Features.Set<IHttpResponseFeature>(responseBody);\n        httpContext.Features.Set<IHttpResponseBodyFeature>(responseBody);\n        httpContext.Features.Set<IHttpRequestLifetimeFeature>(responseBody);\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                var message = new HttpResponseMessage()\n                {\n                    Content = new StreamContent(new ThrowStream(throwOnFirstRead: false))\n                };\n                message.Headers.AcceptRanges.Add(\"bytes\");\n                return Task.FromResult(message);\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfo<IOException>(ForwarderError.ResponseBodyDestination, StatusCodes.Status200OK, proxyError, httpContext, destinationPrefix);\n\n        Assert.Equal(1, responseBody.InnerStream.Length);\n        Assert.True(responseBody.Aborted);\n        Assert.Equal(\"bytes\", httpContext.Response.Headers[HeaderNames.AcceptRanges]);\n\n        events.AssertContainProxyStages(hasRequestContent: false);\n    }\n\n    [Fact]\n    public async Task ResponseBodyClientError_Aborted()\n    {\n        var events = TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        var responseBody = new TestResponseBody(new ThrowStream());\n        httpContext.Features.Set<IHttpResponseFeature>(responseBody);\n        httpContext.Features.Set<IHttpResponseBodyFeature>(responseBody);\n        httpContext.Features.Set<IHttpRequestLifetimeFeature>(responseBody);\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                var message = new HttpResponseMessage()\n                {\n                    Content = new StreamContent(new MemoryStream(new byte[1]))\n                };\n                message.Headers.AcceptRanges.Add(\"bytes\");\n                return Task.FromResult(message);\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfo<IOException>(ForwarderError.ResponseBodyClient, StatusCodes.Status200OK, proxyError, httpContext, destinationPrefix);\n\n        Assert.True(responseBody.Aborted);\n        Assert.Equal(\"bytes\", httpContext.Response.Headers[HeaderNames.AcceptRanges]);\n\n        events.AssertContainProxyStages(hasRequestContent: false);\n    }\n\n    [Fact]\n    public async Task ResponseBodyCancelled_502()\n    {\n        var events = TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        var responseBody = new TestResponseBody();\n        httpContext.Features.Set<IHttpResponseFeature>(responseBody);\n        httpContext.Features.Set<IHttpResponseBodyFeature>(responseBody);\n        httpContext.Features.Set<IHttpRequestLifetimeFeature>(responseBody);\n        httpContext.RequestAborted = new CancellationToken(canceled: true);\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                var message = new HttpResponseMessage()\n                {\n                    Content = new StreamContent(new MemoryStream(new byte[1]))\n                };\n                message.Headers.AcceptRanges.Add(\"bytes\");\n                return Task.FromResult(message);\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfo<TaskCanceledException>(ForwarderError.ResponseBodyCanceled, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix);\n\n        Assert.False(responseBody.Aborted);\n        Assert.Empty(httpContext.Response.Headers);\n\n        events.AssertContainProxyStages(hasRequestContent: false);\n    }\n\n    [Fact]\n    public async Task ResponseBodyCancelledAfterStart_Aborted()\n    {\n        var events = TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        var responseBody = new TestResponseBody();\n        httpContext.Features.Set<IHttpResponseFeature>(responseBody);\n        httpContext.Features.Set<IHttpResponseBodyFeature>(responseBody);\n        httpContext.Features.Set<IHttpRequestLifetimeFeature>(responseBody);\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var cts = new CancellationTokenSource();\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                var message = new HttpResponseMessage()\n                {\n                    Content = new StreamContent(new CallbackReadStream((_, _) =>\n                    {\n                        responseBody.HasStarted = true;\n                        cts.Cancel();\n                        cts.Token.ThrowIfCancellationRequested();\n                        throw new NotImplementedException();\n                    }))\n                };\n                message.Headers.AcceptRanges.Add(\"bytes\");\n                return Task.FromResult(message);\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty,\n            HttpTransformer.Empty, cts.Token);\n\n        AssertErrorInfo<OperationCanceledException>(ForwarderError.ResponseBodyCanceled, StatusCodes.Status200OK, proxyError, httpContext, destinationPrefix);\n\n        Assert.True(responseBody.Aborted);\n        Assert.Equal(\"bytes\", httpContext.Response.Headers[HeaderNames.AcceptRanges]);\n\n        events.AssertContainProxyStages(hasRequestContent: false);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    [InlineData(null)]\n    public async Task ResponseBodyDisableBuffering_Success(bool? enableBuffering)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        var responseBody = new TestResponseBody();\n        httpContext.Features.Set<IHttpResponseFeature>(responseBody);\n        httpContext.Features.Set<IHttpResponseBodyFeature>(responseBody);\n        httpContext.Features.Set<IHttpRequestLifetimeFeature>(responseBody);\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                var message = new HttpResponseMessage()\n                {\n                    Content = new StreamContent(new MemoryStream(new byte[1]))\n                };\n                message.Headers.AcceptRanges.Add(\"bytes\");\n                return Task.FromResult(message);\n            });\n\n        var requestConfig = ForwarderRequestConfig.Empty with { AllowResponseBuffering = enableBuffering };\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, requestConfig, HttpTransformer.Default);\n\n        Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);\n        Assert.Equal(enableBuffering != true, responseBody.BufferingDisabled);\n    }\n\n    [Fact]\n    public async Task RequestBodyCanceledAfterResponse_Reported()\n    {\n        var events = TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var waitTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Body = new StallStream(waitTcs.Task);\n        httpContext.Request.ContentLength = 1;\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        using var longTokenSource = new CancellationTokenSource();\n        httpContext.RequestAborted = longTokenSource.Token;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                // Background copy\n                _ = request.Content.CopyToWithCancellationAsync(new MemoryStream());\n                // Make sure the request isn't canceled until the response finishes copying.\n                return Task.FromResult(new HttpResponseMessage()\n                {\n                    Content = new StreamContent(new OnCompletedReadStream(() =>\n                    {\n                        longTokenSource.Cancel();\n                        waitTcs.SetResult(0);\n                    }))\n                });\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfo<OperationCanceledException>(ForwarderError.RequestBodyCanceled, StatusCodes.Status200OK, proxyError, httpContext, destinationPrefix);\n        Assert.Equal(0, proxyResponseStream.Length);\n\n        events.AssertContainProxyStages();\n    }\n\n    [Fact]\n    public async Task RequestBodyClientErrorAfterResponse_Reported()\n    {\n        var events = TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var waitTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Body = new StallStream(waitTcs.Task);\n        httpContext.Request.ContentLength = 1;\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                // Background copy\n                _ = request.Content.CopyToWithCancellationAsync(new MemoryStream());\n                // Make sure the request isn't canceled until the response finishes copying.\n                return Task.FromResult(new HttpResponseMessage()\n                {\n                    Content = new StreamContent(new OnCompletedReadStream(() => waitTcs.SetResult(0)))\n                });\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfo<IOException>(ForwarderError.RequestBodyClient, StatusCodes.Status200OK, proxyError, httpContext, destinationPrefix);\n        Assert.Equal(0, proxyResponseStream.Length);\n\n        events.AssertContainProxyStages();\n    }\n\n    [Fact]\n    public async Task RequestBodyDestinationErrorAfterResponse_Reported()\n    {\n        var events = TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var waitTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Body = new MemoryStream(new byte[1]);\n        httpContext.Request.ContentLength = 1;\n\n        var proxyResponseStream = new MemoryStream();\n        httpContext.Response.Body = proxyResponseStream;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                // Background copy\n                _ = request.Content.CopyToWithCancellationAsync(new StallStream(waitTcs.Task));\n                // Make sure the request isn't canceled until the response finishes copying.\n                return Task.FromResult(new HttpResponseMessage()\n                {\n                    Content = new StreamContent(new OnCompletedReadStream(() => waitTcs.SetResult(0)))\n                });\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client);\n\n        AssertErrorInfo<IOException>(ForwarderError.RequestBodyDestination, StatusCodes.Status200OK, proxyError, httpContext, destinationPrefix);\n        Assert.Equal(0, proxyResponseStream.Length);\n\n        events.AssertContainProxyStages();\n    }\n\n    [Fact]\n    public async Task UpgradableRequest_RequestBodyCopyError_CancelsResponseBody()\n    {\n        var events = TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Scheme = \"http\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Headers.Upgrade = \"WebSocket\";\n\n        var downstreamStream = new DuplexStream(\n            readStream: new ThrowStream(),\n            writeStream: new MemoryStream());\n        DuplexStream upstreamStream = null;\n\n        var upgradeFeatureMock = new Mock<IHttpUpgradeFeature>();\n        upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true);\n        upgradeFeatureMock.Setup(u => u.UpgradeAsync()).ReturnsAsync(downstreamStream);\n        httpContext.Features.Set(upgradeFeatureMock.Object);\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                Assert.Equal(new Version(1, 1), request.Version);\n                Assert.Equal(HttpMethod.Get, request.Method);\n\n                Assert.Null(request.Content);\n\n                var response = new HttpResponseMessage(HttpStatusCode.SwitchingProtocols);\n                upstreamStream = new DuplexStream(\n                    readStream: new StallStream(ct =>\n                    {\n                        var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n                        ct.Register(() => tcs.SetResult(0));\n                        return tcs.Task.DefaultTimeout();\n                    }),\n                    writeStream: new MemoryStream());\n                response.Content = new RawStreamContent(upstreamStream);\n                return Task.FromResult(response);\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig()\n        {\n            Version = HttpVersion.Version11,\n        });\n\n        AssertErrorInfo<IOException>(ForwarderError.UpgradeRequestClient, StatusCodes.Status101SwitchingProtocols, proxyError, httpContext, destinationPrefix);\n\n        events.AssertContainProxyStages(upgrade: true);\n    }\n\n    [Fact]\n    public async Task UpgradableRequest_ResponseBodyCopyError_CancelsRequestBody()\n    {\n        var events = TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Scheme = \"http\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.Request.Headers.Upgrade = \"WebSocket\";\n\n        var downstreamStream = new DuplexStream(\n            readStream: new StallStream(ct =>\n            {\n                var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n                ct.Register(() => tcs.SetResult(0));\n                return tcs.Task.DefaultTimeout();\n            }),\n            writeStream: new MemoryStream());\n        DuplexStream upstreamStream = null;\n\n        var upgradeFeatureMock = new Mock<IHttpUpgradeFeature>();\n        upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true);\n        upgradeFeatureMock.Setup(u => u.UpgradeAsync()).ReturnsAsync(downstreamStream);\n        httpContext.Features.Set(upgradeFeatureMock.Object);\n\n        var destinationPrefix = \"https://localhost/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                Assert.Equal(new Version(1, 1), request.Version);\n                Assert.Equal(HttpMethod.Get, request.Method);\n\n                Assert.Null(request.Content);\n\n                var response = new HttpResponseMessage(HttpStatusCode.SwitchingProtocols);\n                upstreamStream = new DuplexStream(\n                    readStream: new ThrowStream(),\n                    writeStream: new MemoryStream());\n                response.Content = new RawStreamContent(upstreamStream);\n                return Task.FromResult(response);\n            });\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig()\n        {\n            Version = HttpVersion.Version11,\n        });\n\n        AssertErrorInfo<IOException>(ForwarderError.UpgradeResponseDestination, StatusCodes.Status101SwitchingProtocols, proxyError, httpContext, destinationPrefix);\n\n        events.AssertContainProxyStages(upgrade: true);\n    }\n\n    [Fact]\n    public async Task WithHttpClient_Fails()\n    {\n        var httpClient = new HttpClient();\n        var httpContext = new DefaultHttpContext();\n        var destinationPrefix = \"\";\n        var transforms = HttpTransformer.Default;\n        var requestOptions = ForwarderRequestConfig.Empty;\n        var proxy = CreateProxy();\n\n        await Assert.ThrowsAsync<ArgumentException>(async () => await proxy.SendAsync(httpContext,\n            destinationPrefix, httpClient, requestOptions, transforms));\n    }\n\n    [Theory]\n    [InlineData(\"HTTP/1.1\", \"1.1\")]\n    [InlineData(\"HTTP/2\", \"2.0\")]\n    public async Task Expect100ContinueWithFailedResponse_ReturnResponse(string fromProtocol, string toProtocol)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"POST\";\n        httpContext.Request.Protocol = fromProtocol;\n        httpContext.Request.Headers[HeaderNames.Expect] = \"100-continue\";\n        var content = Encoding.UTF8.GetBytes(new string('a', 1024 * 1024 * 10));\n        httpContext.Request.Headers[HeaderNames.ContentLength] = content.Length.ToString();\n        using var contentStream = new MemoryStream(content);\n        httpContext.Request.Body = contentStream;\n\n        var destinationPrefix = \"https://localhost:123/a/b/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                await Task.Yield();\n                Assert.NotNull(request.Content);\n                return new HttpResponseMessage(HttpStatusCode.Conflict);\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = Version.Parse(toProtocol) });\n\n        Assert.Equal(0, contentStream.Position);\n        Assert.Equal((int)HttpStatusCode.Conflict, httpContext.Response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(\"1.1\", false, \"Connection: upgrade; Upgrade: test123\", null, \"Connection; Upgrade\")]\n    [InlineData(\"1.1\", false, \"Connection: keep-alive; Keep-Alive: timeout=100\", null, \"Connection; Keep-Alive\")]\n    [InlineData(\"1.1\", true, \"Connection: upgrade; Upgrade: websocket\", \"Connection: upgrade; Upgrade: websocket\", null)]\n    [InlineData(\"1.1\", true, \"Connection: upgrade, keep-alive; Upgrade: websocket; Keep-Alive: timeout=100\", \"Connection: upgrade; Upgrade: websocket\", \"Keep-Alive\")]\n    [InlineData(\"1.1\", true, \"Connection: keep-alive; Upgrade: websocket; Keep-Alive: timeout=100\", null, \"Connection; Upgrade; Keep-Alive\")]\n    [InlineData(\"1.1\", true, \"Foo: bar; Upgrade: websocket\", \"Foo: bar\", \"Upgrade\")]\n    [InlineData(\"1.1\", true, \"Foo: bar; Connection: upgrade\", \"Foo: bar\", \"Connection\")]\n    [InlineData(\"1.1\", false, \"Foo: bar\", \"Foo: bar\", null)]\n    [InlineData(\"2.0\", false, \"Connection: keep-alive; Keep-Alive: timeout=100\", null, \"Connection; Keep-Alive\")]\n    [InlineData(\"2.0\", false, \"Connection: upgrade; Upgrade: websocket\", null, \"Connection; Upgrade\")]\n    [InlineData(\"2.0\", false, \"Foo: bar\", \"Foo: bar\", null)]\n    public async Task ResponseToNonUpgradeableRequest_RemoveAllConnectionHeaders(string protocol, bool upgrade, string responseHeadersList, string preservedHeadersList, string removedHeadersList)\n    {\n        var events = TestEventListener.Collect();\n\n        var responseHeaders = responseHeadersList.Split(\"; \");\n        var preservedHeaders = preservedHeadersList?.Split(\"; \") ?? Enumerable.Empty<string>();\n        var removedHeaders = removedHeadersList?.Split(\"; \") ?? Enumerable.Empty<string>();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n\n        if (upgrade)\n        {\n            var upgradeFeature = new Mock<IHttpUpgradeFeature>();\n            upgradeFeature.SetupGet(f => f.IsUpgradableRequest).Returns(true);\n            upgradeFeature.Setup(f => f.UpgradeAsync()).ReturnsAsync(httpContext.Request.Body);\n            httpContext.Features.Set(upgradeFeature.Object);\n            httpContext.Request.Headers[HeaderNames.Upgrade] = \"WebSocket\";\n        }\n\n        var destinationPrefix = \"https://localhost:123/a/b/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                await Task.Yield();\n\n                var response = new HttpResponseMessage(upgrade ? HttpStatusCode.SwitchingProtocols : HttpStatusCode.OK);\n                response.Content = new StringContent(\"Foo\");\n\n                foreach (var header in responseHeaders)\n                {\n                    (var headerName, var headerValues) = GetHeaderNameAndValues(header);\n                    response.Headers.TryAddWithoutValidation(headerName, headerValues);\n                }\n\n                return response;\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = Version.Parse(protocol) });\n\n        Assert.Equal(upgrade ? (int)HttpStatusCode.SwitchingProtocols : (int)HttpStatusCode.OK, httpContext.Response.StatusCode);\n\n        foreach (var preservedHeader in preservedHeaders)\n        {\n            (var headerName, var expectedValues) = GetHeaderNameAndValues(preservedHeader);\n            var actualValues = httpContext.Response.Headers[headerName];\n            Assert.Equal(expectedValues, actualValues);\n        }\n\n        foreach (var removedHeaderName in removedHeaders)\n        {\n            Assert.False(httpContext.Response.Headers.TryGetValue(removedHeaderName, out _));\n        }\n\n        AssertProxyStartStop(events, destinationPrefix, httpContext.Response.StatusCode);\n        events.AssertContainProxyStages(hasRequestContent: upgrade, upgrade);\n    }\n\n    [Theory]\n    [InlineData(\"1.1\", false, \"Connection: upgrade; Upgrade: test123\", null, \"Connection; Upgrade\")]\n    [InlineData(\"1.1\", false, \"Connection: keep-alive; Keep-Alive: timeout=100\", null, \"Connection; Keep-Alive\")]\n    [InlineData(\"1.1\", true, \"Connection: upgrade; Upgrade: websocket\", \"Connection: upgrade; Upgrade: websocket\", null)]\n    [InlineData(\"1.1\", true, \"Connection: upgrade; Upgrade: SPDY/\", \"Connection: upgrade; Upgrade: SPDY/\", null)]\n    [InlineData(\"1.1\", true, \"Connection: upgrade, keep-alive; Upgrade: websocket; Keep-Alive: timeout=100\", \"Connection: upgrade; Upgrade: websocket\", \"Keep-Alive\")]\n    [InlineData(\"1.1\", false, \"Foo: bar\", \"Foo: bar\", null)]\n    [InlineData(\"2.0\", false, \"Connection: keep-alive; Keep-Alive: timeout=100\", null, \"Connection; Keep-Alive\")]\n    [InlineData(\"2.0\", false, \"Connection: upgrade; Upgrade: websocket\", null, \"Connection; Upgrade\")]\n    [InlineData(\"2.0\", false, \"Foo: bar\", \"Foo: bar\", null)]\n    public async Task NonUpgradableRequest_RemoveAllConnectionHeaders(string protocol, bool upgrade, string addHeadersList, string preservedHeadersList, string removedHeadersList)\n    {\n        var addHeaders = addHeadersList.Split(\"; \").Select(GetHeaderNameAndValues);\n        var preservedHeaders = (preservedHeadersList?.Split(\"; \") ?? Enumerable.Empty<string>()).Select(GetHeaderNameAndValues);\n        var removedHeaders = removedHeadersList?.Split(\"; \") ?? Enumerable.Empty<string>();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n\n        if (upgrade)\n        {\n            var upgradeFeature = new Mock<IHttpUpgradeFeature>();\n            upgradeFeature.SetupGet(f => f.IsUpgradableRequest).Returns(true);\n            httpContext.Features.Set(upgradeFeature.Object);\n        }\n\n        foreach (var (name, value) in addHeaders)\n        {\n            httpContext.Request.Headers[name] = value;\n        }\n\n        var destinationPrefix = \"https://localhost:123/a/b/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                await Task.Yield();\n\n                foreach (var (name, value) in preservedHeaders)\n                {\n                    var actualValues = string.Join(\", \", request.Headers.GetValues(name));\n                    Assert.Equal(value, actualValues);\n                }\n\n                foreach (var removedHeaderName in removedHeaders)\n                {\n                    Assert.False(request.Headers.TryGetValues(removedHeaderName, out _));\n                }\n\n                var response = new HttpResponseMessage(HttpStatusCode.OK);\n                return response;\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = Version.Parse(protocol) });\n\n        Assert.Equal((int)HttpStatusCode.OK, httpContext.Response.StatusCode);\n    }\n\n    [Theory]\n    [MemberData(nameof(GetProhibitedHeaders))]\n    [MemberData(nameof(GetHeadersWithNewLines))]\n    public async Task Request_RemoveProhibitedHeaders(string protocol, string prohibitedHeadersList)\n    {\n        const string PreservedHeaderName = \"Foo\";\n        const string PreservedHeaderValue = \"bar\";\n        var prohibitedHeaders = prohibitedHeadersList.Split(\"; \").Select(GetHeaderNameAndValues);\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n\n        foreach (var (name, value) in prohibitedHeaders)\n        {\n            httpContext.Request.Headers[name] = value;\n        }\n        httpContext.Request.Headers[PreservedHeaderName] = PreservedHeaderValue;\n\n        var destinationPrefix = \"https://localhost:123/a/b/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                await Task.Yield();\n\n                Assert.Equal(PreservedHeaderValue, string.Join(\", \", request.Headers.GetValues(PreservedHeaderName)));\n\n                foreach (var (name, _) in prohibitedHeaders)\n                {\n                    Assert.False(request.Headers.TryGetValues(name, out _));\n                }\n\n                var response = new HttpResponseMessage(HttpStatusCode.OK);\n                return response;\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = Version.Parse(protocol) });\n\n        Assert.Equal((int)HttpStatusCode.OK, httpContext.Response.StatusCode);\n    }\n\n    [Theory]\n    [MemberData(nameof(ResponseMultiHeadersData))]\n    public async Task ResponseWithMultiHeaders(string version, string headerName, string[] headers)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n\n        var destinationPrefix = \"https://localhost:123/a/b/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                await Task.Yield();\n\n                var response = new HttpResponseMessage(HttpStatusCode.OK);\n\n                if (!response.Headers.TryAddWithoutValidation(headerName, headers))\n                {\n                    Assert.True(response.Content.Headers.TryAddWithoutValidation(headerName, headers));\n                }\n\n                return response;\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = new Version(version) });\n\n        Assert.Equal((int)HttpStatusCode.OK, httpContext.Response.StatusCode);\n        Assert.True(httpContext.Response.Headers.TryGetValue(headerName, out var sentHeaders));\n        Assert.True(sentHeaders.Equals(headers));\n    }\n\n    [Theory]\n    [MemberData(nameof(GetProhibitedHeaders))]\n    public async Task Response_RemoveProhibitedHeaders(string protocol, string prohibitedHeadersList)\n    {\n        const string PreservedHeaderName = \"Foo\";\n        const string PreservedHeaderValue = \"bar\";\n        var prohibitedHeaders = prohibitedHeadersList.Split(\"; \").Select(GetHeaderNameAndValues);\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n\n        var destinationPrefix = \"https://localhost:123/a/b/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            async (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                await Task.Yield();\n\n                var response = new HttpResponseMessage(HttpStatusCode.OK);\n\n                foreach (var (name, value) in prohibitedHeaders)\n                {\n                    response.Headers.TryAddWithoutValidation(name, value);\n                }\n                response.Headers.TryAddWithoutValidation(PreservedHeaderName, PreservedHeaderValue);\n\n                return response;\n            });\n\n        await sut.SendAsync(httpContext, destinationPrefix, client, new ForwarderRequestConfig { Version = Version.Parse(protocol) });\n\n        Assert.Equal((int)HttpStatusCode.OK, httpContext.Response.StatusCode);\n        Assert.Equal(PreservedHeaderValue, string.Join(\", \", httpContext.Response.Headers[PreservedHeaderName]));\n\n        foreach (var (name, _) in prohibitedHeaders)\n        {\n            Assert.False(httpContext.Response.Headers.TryGetValue(name, out _));\n        }\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task RequestFailure_ResponseTransformsAreCalled(bool failureInRequestTransform)\n    {\n        var events = TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                throw new Exception();\n            });\n\n        var responseTransformWithNullResponseCalled = false;\n\n        var transformer = new DelegateHttpTransforms\n        {\n            OnRequest = (context, request, prefix) =>\n            {\n                if (failureInRequestTransform)\n                {\n                    throw new Exception();\n                }\n\n                return Task.CompletedTask;\n            },\n            OnResponse = (context, response) =>\n            {\n                if (response is null)\n                {\n                    responseTransformWithNullResponseCalled = true;\n                }\n\n                return new ValueTask<bool>(true);\n            }\n        };\n\n        var proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transformer);\n\n        Assert.True(responseTransformWithNullResponseCalled);\n\n        var expectedError = failureInRequestTransform\n            ? ForwarderError.RequestCreation\n            : ForwarderError.Request;\n\n        AssertErrorInfo<Exception>(expectedError, StatusCodes.Status502BadGateway, proxyError, httpContext, destinationPrefix);\n\n        events.AssertContainProxyStages(failureInRequestTransform ? [] : [ForwarderStage.SendAsyncStart]);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task RequestFailure_CancellationExceptionInResponseTransformIsIgnored(bool throwOce)\n    {\n        TestEventListener.Collect();\n        TestLogger.Collect();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                throw new Exception();\n            });\n\n        var responseTransformWithNullResponseCalled = false;\n\n        var transformer = new DelegateHttpTransforms\n        {\n            OnResponse = (context, response) =>\n            {\n                if (response is null)\n                {\n                    responseTransformWithNullResponseCalled = true;\n\n                    throw throwOce\n                        ? new OperationCanceledException(\"Foo\")\n                        : new InvalidOperationException(\"Bar\");\n                }\n\n                return new ValueTask<bool>(true);\n            }\n        };\n\n        var proxyError = ForwarderError.None;\n        Exception exceptionThrownBySendAsync = null;\n\n        try\n        {\n            proxyError = await sut.SendAsync(httpContext, destinationPrefix, client, ForwarderRequestConfig.Empty, transformer);\n        }\n        catch (Exception ex)\n        {\n            exceptionThrownBySendAsync = ex;\n        }\n\n        Assert.True(responseTransformWithNullResponseCalled);\n\n        if (throwOce)\n        {\n            Assert.Null(exceptionThrownBySendAsync);\n            Assert.Equal(ForwarderError.Request, proxyError);\n        }\n        else\n        {\n            Assert.NotNull(exceptionThrownBySendAsync);\n        }\n\n        AssertErrorInfoAndStages<Exception>(ForwarderError.Request, StatusCodes.Status502BadGateway, ForwarderError.Request, httpContext, destinationPrefix);\n    }\n\n    public enum CancellationScenario\n    {\n        RequestAborted,\n        ActivityTimeout,\n        ManualCancellationToken,\n    }\n\n    [Theory]\n    [InlineData(CancellationScenario.RequestAborted)]\n    [InlineData(CancellationScenario.ActivityTimeout)]\n    [InlineData(CancellationScenario.ManualCancellationToken)]\n    public async Task ForwarderCancellations_CancellationsAreVisibleInTransforms(CancellationScenario cancellationScenario)\n    {\n        var events = TestEventListener.Collect();\n        TestLogger.Collect();\n\n        using var requestAbortedCts = new CancellationTokenSource();\n        using var parameterCts = new CancellationTokenSource();\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Method = \"GET\";\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n        httpContext.RequestAborted = requestAbortedCts.Token;\n\n        var destinationPrefix = \"https://localhost:123/\";\n        var sut = CreateProxy();\n        var client = MockHttpHandler.CreateClient(\n            (HttpRequestMessage request, CancellationToken cancellationToken) =>\n            {\n                throw new InvalidOperationException();\n            });\n\n        var requestConfig = new ForwarderRequestConfig();\n\n        if (cancellationScenario == CancellationScenario.ActivityTimeout)\n        {\n            requestConfig = requestConfig with { ActivityTimeout = TimeSpan.FromMilliseconds(42) };\n        }\n\n        var ctWasAlreadyCancelled = false;\n        var inTheTransformsTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var transformer = TransformBuilderTests.CreateTransformBuilder().CreateInternal(context =>\n        {\n            context.AddRequestTransform(async context =>\n            {\n                ctWasAlreadyCancelled = context.CancellationToken.IsCancellationRequested;\n\n                inTheTransformsTcs.SetResult();\n\n                await Task.Delay(-1, context.CancellationToken);\n            });\n        });\n\n        var proxyTask = sut.SendAsync(httpContext, destinationPrefix, client, requestConfig, transformer, parameterCts.Token);\n\n        await inTheTransformsTcs.Task;\n\n        if (cancellationScenario == CancellationScenario.RequestAborted)\n        {\n            requestAbortedCts.Cancel();\n        }\n        else if (cancellationScenario == CancellationScenario.ManualCancellationToken)\n        {\n            parameterCts.Cancel();\n        }\n\n        var proxyError = await proxyTask;\n\n        if (cancellationScenario != CancellationScenario.ManualCancellationToken)\n        {\n            Assert.False(ctWasAlreadyCancelled);\n        }\n\n        var expectedError = cancellationScenario == CancellationScenario.ActivityTimeout\n            ? ForwarderError.RequestTimedOut\n            : ForwarderError.RequestCanceled;\n\n        var expectedStatusCode = cancellationScenario switch\n        {\n            CancellationScenario.ActivityTimeout => StatusCodes.Status504GatewayTimeout,\n            CancellationScenario.RequestAborted => StatusCodes.Status400BadRequest,\n            CancellationScenario.ManualCancellationToken => StatusCodes.Status502BadGateway,\n            _ => throw new NotImplementedException(cancellationScenario.ToString()),\n        };\n\n        AssertErrorInfo<TaskCanceledException>(expectedError, expectedStatusCode, proxyError, httpContext, destinationPrefix);\n\n        events.AssertContainProxyStages([]);\n    }\n\n    public static IEnumerable<object[]> GetProhibitedHeaders()\n    {\n        var headers = new[]\n        {\n            \"Connection: close\",\n            \"Upgrade: test123\",\n            \"Transfer-Encoding: deflate\",\n            \"Keep-Alive: timeout=100\",\n            \"Proxy-Connection: value\",\n            \"Proxy-Authenticate: value\",\n            \"Proxy-Authentication-Info: value\",\n            \"Proxy-Authorization: value\",\n            \"Proxy-Features: value\",\n            \"Proxy-Instruction: value\",\n            \"Security-Scheme: value\",\n            \"ALPN: value\",\n            \"Close: value\",\n            \"TE: value\",\n            \"HTTP2-Settings: value\",\n            \"Upgrade-Insecure-Requests: value\",\n            \"Alt-Svc: value\",\n        };\n\n        foreach (var header in headers)\n        {\n            yield return new object[] { \"1.1\", header };\n            yield return new object[] { \"2.0\", header };\n        }\n    }\n\n    public static IEnumerable<object[]> GetHeadersWithNewLines()\n    {\n        var headers = new[]\n        {\n            \"valid-name-1: \\rfoo\",\n            \"valid-name-2: bar\\n\",\n            \"valid-name-3: foo\\r\\nbar\",\n            \"valid-name-4: foo\\r\\n bar\",\n        };\n\n        foreach (var header in headers)\n        {\n            yield return new object[] { \"1.1\", header };\n            yield return new object[] { \"2.0\", header };\n        }\n    }\n\n    private static TException AssertErrorInfoAndStages<TException>(\n        ForwarderError expectedError, int expectedStatusCode,\n        ForwarderError error, HttpContext context, string destinationPrefix,\n        params ForwarderStage[] otherStages)\n        where TException : Exception\n    {\n        TException exception = AssertErrorInfo<TException>(expectedError, expectedStatusCode, error, context, destinationPrefix);\n\n        TestEventListener.Collect().AssertContainProxyStages([ForwarderStage.SendAsyncStart, .. otherStages]);\n\n        return exception;\n    }\n\n    private static TException AssertErrorInfo<TException>(\n        ForwarderError expectedError, int expectedStatusCode,\n        ForwarderError error, HttpContext context, string destinationPrefix)\n        where TException : Exception\n    {\n        Assert.Equal(expectedError, error);\n        Assert.Equal(expectedStatusCode, context.Response.StatusCode);\n        var errorFeature = context.Features.Get<IForwarderErrorFeature>();\n        Assert.NotNull(errorFeature);\n        Assert.Equal(error, errorFeature.Error);\n        Assert.IsAssignableFrom<TException>(errorFeature.Exception);\n\n        var errorIsRequestCancelled = error is\n            ForwarderError.RequestCanceled or\n            ForwarderError.RequestBodyCanceled or\n            ForwarderError.ResponseBodyCanceled or\n            ForwarderError.UpgradeRequestCanceled or\n            ForwarderError.UpgradeResponseCanceled;\n\n        var expectedId = errorIsRequestCancelled\n            ? EventIds.ForwardingRequestCancelled\n            : EventIds.ForwardingError;\n\n        var log = Assert.Single(TestLogger.Collect(), l => l.EventId == expectedId);\n        Assert.Equal(typeof(HttpForwarder).FullName, log.CategoryName);\n        Assert.Contains(error.ToString(), log.Message);\n        Assert.NotNull(log.Exception);\n\n        AssertProxyStartFailedStop(TestEventListener.Collect(), destinationPrefix, context.Response.StatusCode, errorFeature.Error);\n\n        return (TException)errorFeature.Exception;\n    }\n\n    private static void AssertProxyStartStop(List<EventWrittenEventArgs> events, string destinationPrefix, int statusCode)\n    {\n        AssertProxyStartFailedStop(events, destinationPrefix, statusCode, error: null);\n    }\n\n    private static void AssertProxyStartFailedStop(List<EventWrittenEventArgs> events, string destinationPrefix, int statusCode, ForwarderError? error)\n    {\n        var start = Assert.Single(events, e => e.EventName == \"ForwarderStart\");\n        var prefixActual = (string)Assert.Single(start.Payload);\n        Assert.Equal(destinationPrefix, prefixActual);\n\n        var stop = Assert.Single(events, e => e.EventName == \"ForwarderStop\");\n        var statusActual = (int)Assert.Single(stop.Payload);\n        Assert.Equal(statusCode, statusActual);\n        Assert.True(start.TimeStamp <= stop.TimeStamp);\n\n        if (error is null)\n        {\n            Assert.DoesNotContain(events, e => e.EventName == \"ForwarderFailed\");\n        }\n        else\n        {\n            var failed = Assert.Single(events, e => e.EventName == \"ForwarderFailed\");\n            var errorActual = (ForwarderError)Assert.Single(failed.Payload);\n            Assert.Equal(error.Value, errorActual);\n            Assert.True(start.TimeStamp <= failed.TimeStamp);\n            Assert.True(failed.TimeStamp <= stop.TimeStamp);\n        }\n    }\n\n    private static MemoryStream StringToStream(string text)\n    {\n        var stream = new MemoryStream(Encoding.UTF8.GetBytes(text));\n        return stream;\n    }\n\n    private static string StreamToString(Stream stream)\n    {\n        using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true);\n        return reader.ReadToEnd();\n    }\n\n    private static (string Name, string Values) GetHeaderNameAndValues(string fullHeader)\n    {\n        var headerNameEnd = fullHeader.IndexOf(\": \");\n        return (fullHeader[..headerNameEnd], fullHeader[(headerNameEnd + 2)..]);\n    }\n\n    private class DuplexStream : Stream\n    {\n        public DuplexStream(Stream readStream, Stream writeStream)\n        {\n            ArgumentNullException.ThrowIfNull(readStream);\n            ArgumentNullException.ThrowIfNull(writeStream);\n            ReadStream = readStream;\n            WriteStream = writeStream;\n        }\n\n        public Stream ReadStream { get; }\n\n        public Stream WriteStream { get; }\n\n        public override bool CanRead => true;\n\n        public override bool CanSeek => false;\n\n        public override bool CanWrite => true;\n\n        public override long Length => throw new NotImplementedException();\n\n        public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }\n\n        public override int Read(byte[] buffer, int offset, int count)\n        {\n            return ReadStream.Read(buffer, offset, count);\n        }\n\n        public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)\n        {\n            return ReadStream.ReadAsync(buffer, offset, count, cancellationToken);\n        }\n\n        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            return ReadStream.ReadAsync(buffer, cancellationToken);\n        }\n\n        public override void Write(byte[] buffer, int offset, int count)\n        {\n            WriteStream.Write(buffer, offset, count);\n        }\n\n        public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)\n        {\n            return WriteStream.WriteAsync(buffer, offset, count, cancellationToken);\n        }\n\n        public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            return WriteStream.WriteAsync(buffer, cancellationToken);\n        }\n\n        public override void Flush()\n        {\n        }\n\n        public override long Seek(long offset, SeekOrigin origin)\n        {\n            throw new NotImplementedException();\n        }\n\n        public override void SetLength(long value)\n        {\n            throw new NotImplementedException();\n        }\n    }\n\n    /// <summary>\n    /// Replacement for <see cref=\"StreamContent\"/> which just returns the raw stream,\n    /// whereas <see cref=\"StreamContent\"/> wraps it in a read-only stream.\n    /// We need to return the raw internal stream to test full duplex proxying.\n    /// </summary>\n    private class RawStreamContent : HttpContent\n    {\n        private readonly Stream _stream;\n\n        public RawStreamContent(Stream stream)\n        {\n            ArgumentNullException.ThrowIfNull(stream);\n            _stream = stream;\n        }\n\n        protected override Task<Stream> CreateContentReadStreamAsync()\n        {\n            return Task.FromResult(_stream);\n        }\n\n        protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)\n        {\n            throw new NotImplementedException();\n        }\n\n        protected override bool TryComputeLength(out long length)\n        {\n            throw new NotImplementedException();\n        }\n    }\n\n    private class ThrowStream : DelegatingStream\n    {\n        private bool _firstRead = true;\n\n        public ThrowStream(bool throwOnFirstRead = true)\n            : base(Stream.Null)\n        {\n            ThrowOnFirstRead = throwOnFirstRead;\n        }\n\n        public bool ThrowOnFirstRead { get; }\n\n        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            if (buffer.Length == 0)\n            {\n                return new ValueTask<int>(0);\n            }\n\n            cancellationToken.ThrowIfCancellationRequested();\n            if (_firstRead && !ThrowOnFirstRead)\n            {\n                _firstRead = false;\n                return new ValueTask<int>(1);\n            }\n            throw new IOException(\"Fake connection issue\");\n        }\n\n        public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            cancellationToken.ThrowIfCancellationRequested();\n            throw new IOException(\"Fake connection issue\");\n        }\n    }\n\n    private class StallStream : DelegatingStream\n    {\n        public StallStream(Task until)\n            : this(_ => until)\n        { }\n\n        public StallStream(Func<CancellationToken, Task> onStallAction)\n            : base(Stream.Null)\n        {\n            OnStallAction = onStallAction;\n        }\n\n        public Func<CancellationToken, Task> OnStallAction { get; }\n\n        public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            await OnStallAction(cancellationToken);\n            cancellationToken.ThrowIfCancellationRequested();\n            throw new IOException();\n        }\n\n        public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            await OnStallAction(cancellationToken);\n            cancellationToken.ThrowIfCancellationRequested();\n            throw new IOException();\n        }\n    }\n\n    private class CallbackReadStream : DelegatingStream\n    {\n        public CallbackReadStream(Func<Memory<byte>, CancellationToken, ValueTask<int>> onReadAsync)\n            : base(Stream.Null)\n        {\n            OnReadAsync = onReadAsync;\n        }\n\n        public Func<Memory<byte>, CancellationToken, ValueTask<int>> OnReadAsync { get; }\n\n        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            return OnReadAsync(buffer, cancellationToken);\n        }\n\n        public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            throw new IOException();\n        }\n    }\n\n    private class TestResponseBody : DelegatingStream, IHttpResponseBodyFeature, IHttpResponseFeature, IHttpRequestLifetimeFeature\n    {\n        public TestResponseBody()\n            : this(new MemoryStream())\n        { }\n\n        public TestResponseBody(Stream innerStream)\n            : base(innerStream)\n        {\n            InnerStream = innerStream;\n        }\n\n        public Stream InnerStream { get; }\n\n        public bool Aborted { get; private set; }\n\n        public Stream Stream => this;\n\n        public PipeWriter Writer => throw new NotImplementedException();\n\n        public bool BufferingDisabled { get; set; }\n\n        public int StatusCode { get; set; } = 200;\n        public string ReasonPhrase { get; set; }\n        public IHeaderDictionary Headers { get; set; } = new HeaderDictionary();\n        public Stream Body { get => this; set => throw new NotImplementedException(); }\n        public bool HasStarted { get; set; }\n        public CancellationToken RequestAborted { get; set; }\n\n        public void Abort()\n        {\n            Aborted = true;\n        }\n\n        public Task CompleteAsync()\n        {\n            throw new NotImplementedException();\n        }\n\n        public void DisableBuffering()\n        {\n            BufferingDisabled = true;\n        }\n\n        public void OnCompleted(Func<object, Task> callback, object state)\n        {\n            throw new NotImplementedException();\n        }\n\n        public void OnStarting(Func<object, Task> callback, object state)\n        {\n            throw new NotImplementedException();\n        }\n\n        public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default)\n        {\n            throw new NotImplementedException();\n        }\n\n        public Task StartAsync(CancellationToken cancellationToken = default)\n        {\n            OnStart();\n            return Task.CompletedTask;\n        }\n\n        public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            OnStart();\n            return base.WriteAsync(buffer, cancellationToken);\n        }\n\n        private void OnStart()\n        {\n            if (!HasStarted)\n            {\n                HasStarted = true;\n            }\n        }\n    }\n\n    private class OnCompletedReadStream : DelegatingStream\n    {\n        public OnCompletedReadStream(Action onCompleted)\n            : base(Stream.Null)\n        {\n            OnCompleted = onCompleted;\n        }\n\n        public Action OnCompleted { get; }\n\n        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            if (buffer.Length != 0)\n            {\n                OnCompleted();\n            }\n            return new ValueTask<int>(0);\n        }\n    }\n\n    private class DelegateHttpTransforms : HttpTransformer\n    {\n        public bool CopyRequestHeaders { get; set; } = true;\n\n        public Func<HttpContext, HttpRequestMessage, string, Task> OnRequest { get; set; } = (_, _, _) => Task.CompletedTask;\n        public Func<HttpContext, HttpResponseMessage, ValueTask<bool>> OnResponse { get; set; } = (_, _) => new(true);\n        public Func<HttpContext, HttpResponseMessage, Task> OnResponseTrailers { get; set; } = (_, _) => Task.CompletedTask;\n\n        public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken)\n        {\n            if (CopyRequestHeaders)\n            {\n                await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken);\n            }\n\n            await OnRequest(httpContext, proxyRequest, destinationPrefix);\n        }\n\n        public override async ValueTask<bool> TransformResponseAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken)\n        {\n            await base.TransformResponseAsync(httpContext, proxyResponse, cancellationToken);\n\n            return await OnResponse(httpContext, proxyResponse);\n        }\n\n        public override async ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken)\n        {\n            await base.TransformResponseTrailersAsync(httpContext, proxyResponse, cancellationToken);\n\n            await OnResponseTrailers(httpContext, proxyResponse);\n        }\n    }\n\n    private class ThrowBadHttpRequestExceptionStream : DelegatingStream\n    {\n        public ThrowBadHttpRequestExceptionStream(int statusCode)\n            : base(Stream.Null)\n        {\n            StatusCode = statusCode;\n        }\n\n        private int StatusCode { get; }\n\n        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            if (buffer.Length == 0)\n            {\n                return new ValueTask<int>(0);\n            }\n\n            cancellationToken.ThrowIfCancellationRequested();\n\n            throw new BadHttpRequestException(ReasonPhrases.GetReasonPhrase(StatusCode), StatusCode);\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Forwarder/HttpTransformerTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.Net.Http.Headers;\nusing Xunit;\nusing Yarp.ReverseProxy.Transforms;\nusing Yarp.ReverseProxy.Transforms.Builder.Tests;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Forwarder.Tests;\n\npublic class HttpTransformerTests\n{\n    private static readonly string[] RestrictedHeaders = new[]\n    {\n        HeaderNames.Connection,\n        HeaderNames.TransferEncoding,\n        HeaderNames.KeepAlive,\n        HeaderNames.Upgrade,\n        HeaderNames.ProxyConnection,\n        HeaderNames.ProxyAuthenticate,\n        \"Proxy-Authentication-Info\",\n        HeaderNames.ProxyAuthorization,\n        \"Proxy-Features\",\n        \"Proxy-Instruction\",\n        \"Security-Scheme\",\n        \"ALPN\",\n        \"Close\",\n        \"HTTP2-Settings\",\n        HeaderNames.UpgradeInsecureRequests,\n        HeaderNames.TE,\n        HeaderNames.AltSvc,\n    };\n\n    [Fact]\n    public async Task TransformRequestAsync_RemovesRestrictedHeaders()\n    {\n        var transformer = HttpTransformer.Default;\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage(HttpMethod.Get, \"https://localhost\");\n\n        foreach (var header in RestrictedHeaders)\n        {\n            httpContext.Request.Headers[header] = \"value\";\n        }\n\n        await transformer.TransformRequestAsync(httpContext, proxyRequest, \"prefix\", CancellationToken.None);\n\n        foreach (var header in RestrictedHeaders)\n        {\n            Assert.False(proxyRequest.Headers.Contains(header));\n        }\n\n        Assert.Null(proxyRequest.Content);\n    }\n\n    [Fact]\n    public async Task TransformRequestAsync_KeepOriginalHost()\n    {\n        var transformer = HttpTransformer.Empty;\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage(HttpMethod.Get, \"https://localhost\");\n\n        httpContext.Request.Host = new HostString(\"example.com:3456\");\n\n        await transformer.TransformRequestAsync(httpContext, proxyRequest, \"prefix\", CancellationToken.None);\n\n        Assert.Equal(\"example.com:3456\", proxyRequest.Headers.Host);\n    }\n\n    [Fact]\n    public async Task TransformRequestAsync_TETrailers_Copied()\n    {\n        var transformer = HttpTransformer.Default;\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Protocol = \"HTTP/2\";\n        var proxyRequest = new HttpRequestMessage(HttpMethod.Get, \"https://localhost\");\n\n        httpContext.Request.Headers[HeaderNames.TE] = \"traiLers\";\n\n        await transformer.TransformRequestAsync(httpContext, proxyRequest, \"prefix\", CancellationToken.None);\n\n        Assert.True(proxyRequest.Headers.TryGetValues(HeaderNames.TE, out var values));\n        var value = Assert.Single(values);\n        Assert.Equal(\"traiLers\", value);\n\n        Assert.Null(proxyRequest.Content);\n    }\n\n    [Fact]\n    public async Task TransformRequestAsync_ContentLengthAndTransferEncoding_ContentLengthRemoved()\n    {\n        var transformer = HttpTransformer.Default;\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage(HttpMethod.Get, \"https://localhost\")\n        {\n            Content = new ByteArrayContent(Array.Empty<byte>())\n        };\n\n        httpContext.Request.Headers[HeaderNames.TransferEncoding] = \"chUnked\";\n        httpContext.Request.Headers[HeaderNames.ContentLength] = \"10\";\n\n        await transformer.TransformRequestAsync(httpContext, proxyRequest, \"prefix\", CancellationToken.None);\n\n        Assert.False(proxyRequest.Content.Headers.TryGetValues(HeaderNames.ContentLength, out var _));\n        // Transfer-Encoding is on the restricted list and removed. HttpClient will re-add it if required.\n        Assert.False(proxyRequest.Headers.TryGetValues(HeaderNames.TransferEncoding, out var _));\n    }\n\n    [Fact]\n    public async Task TransformRequestAsync_SetDestinationPrefix()\n    {\n        const string updatedDestinationPrefix = \"https://contoso.com\";\n        var transformer = TransformBuilderTests.CreateTransformBuilder().CreateInternal(context =>\n        {\n            context.AddRequestTransform(transformContext =>\n            {\n                transformContext.DestinationPrefix = updatedDestinationPrefix;\n                return ValueTask.CompletedTask;\n            });\n        });\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage(HttpMethod.Get, requestUri: (string)null)\n        {\n            Content = new ByteArrayContent(Array.Empty<byte>()),\n        };\n\n        await transformer.TransformRequestAsync(httpContext, proxyRequest, \"https://localhost\", CancellationToken.None);\n        Assert.Equal(new Uri(updatedDestinationPrefix), proxyRequest.RequestUri);\n    }\n\n    [Theory]\n    [InlineData(HttpStatusCode.Continue)]\n    [InlineData(HttpStatusCode.SwitchingProtocols)]\n    [InlineData(HttpStatusCode.NoContent)]\n    [InlineData(HttpStatusCode.ResetContent)]\n    public async Task TransformResponseAsync_ContentLength0OnBodylessStatusCode_ContentLengthRemoved(HttpStatusCode statusCode)\n    {\n        var transformer = HttpTransformer.Default;\n        var httpContext = new DefaultHttpContext();\n\n        var proxyResponse = new HttpResponseMessage(statusCode)\n        {\n            Content = new ByteArrayContent(Array.Empty<byte>())\n        };\n\n        Assert.Equal(0, proxyResponse.Content.Headers.ContentLength);\n        await transformer.TransformResponseAsync(httpContext, proxyResponse, CancellationToken.None);\n        Assert.False(httpContext.Response.Headers.ContainsKey(HeaderNames.ContentLength));\n    }\n\n    [Fact]\n    public async Task TransformResponseAsync_RemovesRestrictedHeaders()\n    {\n        var transformer = HttpTransformer.Default;\n        var httpContext = new DefaultHttpContext();\n        var proxyResponse = new HttpResponseMessage()\n        {\n            Content = new ByteArrayContent(Array.Empty<byte>())\n        };\n\n        foreach (var header in RestrictedHeaders)\n        {\n            if (!proxyResponse.Headers.TryAddWithoutValidation(header, \"value\"))\n            {\n                Assert.True(proxyResponse.Content.Headers.TryAddWithoutValidation(header, \"value\"));\n            }\n        }\n\n        await transformer.TransformResponseAsync(httpContext, proxyResponse, CancellationToken.None);\n\n        foreach (var header in RestrictedHeaders)\n        {\n            Assert.False(httpContext.Response.Headers.ContainsKey(header));\n        }\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task TransformResponseAsync_StrictTransportSecurity_CopiedIfNotPresent(bool alreadyPresent)\n    {\n        var transformer = HttpTransformer.Default;\n        var httpContext = new DefaultHttpContext();\n        var proxyResponse = new HttpResponseMessage()\n        {\n            Content = new ByteArrayContent(Array.Empty<byte>())\n        };\n\n        if (alreadyPresent)\n        {\n            httpContext.Response.Headers.StrictTransportSecurity = \"max-age=31536000; includeSubDomains\";\n        }\n\n        Assert.True(proxyResponse.Headers.TryAddWithoutValidation(HeaderNames.StrictTransportSecurity, \"max-age=31000; preload\"));\n\n        await transformer.TransformResponseAsync(httpContext, proxyResponse, CancellationToken.None);\n\n        var result = httpContext.Response.Headers.StrictTransportSecurity;\n        if (alreadyPresent)\n        {\n            Assert.Equal(\"max-age=31536000; includeSubDomains\", result);\n        }\n        else\n        {\n            Assert.Equal(\"max-age=31000; preload\", result);\n        }\n    }\n\n    [Fact]\n    public async Task TransformResponseAsync_ContentLengthAndTransferEncoding_ContentLengthRemoved()\n    {\n        var transformer = HttpTransformer.Default;\n        var httpContext = new DefaultHttpContext();\n        var proxyResponse = new HttpResponseMessage()\n        {\n            Content = new ByteArrayContent(new byte[10])\n        };\n\n        proxyResponse.Headers.TransferEncodingChunked = true;\n        Assert.Equal(10, proxyResponse.Content.Headers.ContentLength);\n\n        await transformer.TransformResponseAsync(httpContext, proxyResponse, CancellationToken.None);\n\n        Assert.False(httpContext.Response.Headers.ContainsKey(HeaderNames.ContentLength));\n        // Transfer-Encoding is on the restricted list and removed. HttpClient will re-add it if required.\n        Assert.False(httpContext.Response.Headers.ContainsKey(HeaderNames.TransferEncoding));\n    }\n\n    [Fact]\n    public async Task TransformResponseTrailersAsync_RemovesRestrictedHeaders()\n    {\n        var transformer = HttpTransformer.Default;\n        var httpContext = new DefaultHttpContext();\n        var trailersFeature = new TestTrailersFeature();\n        httpContext.Features.Set<IHttpResponseTrailersFeature>(trailersFeature);\n        var proxyResponse = new HttpResponseMessage();\n\n        foreach (var header in RestrictedHeaders)\n        {\n            Assert.True(proxyResponse.TrailingHeaders.TryAddWithoutValidation(header, \"value\"));\n        }\n\n        await transformer.TransformResponseTrailersAsync(httpContext, proxyResponse, CancellationToken.None);\n\n        foreach (var header in RestrictedHeaders)\n        {\n            Assert.False(trailersFeature.Trailers.ContainsKey(header));\n        }\n    }\n\n    public enum ImplementationType\n    {\n        StructuredTransformer,\n        DerivedWithoutCT,\n        DerivedWithCT,\n    }\n\n    public static IEnumerable<object[]> ImplementationTypes_MemberData() =>\n        Enum.GetValues<ImplementationType>().Select(i => new object[] { i });\n\n    [Theory]\n    [MemberData(nameof(ImplementationTypes_MemberData))]\n    public async Task DerivedImplementation_TransformRequestAsync_DerivedImplementationCalled(ImplementationType implementationType)\n    {\n        var implementationCalled = 0;\n\n        var transformer = GetTransformerImplementation(implementationType, () => implementationCalled++);\n\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage();\n        var destinationPrefix = \"http://destinationhost:9090/path\";\n\n        using var cts = new CancellationTokenSource();\n        await transformer.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cts.Token);\n\n        Assert.Equal(1, implementationCalled);\n    }\n\n    [Theory]\n    [MemberData(nameof(ImplementationTypes_MemberData))]\n    public async Task DerivedImplementation_TransformResponseAsync_DerivedImplementationCalled(ImplementationType implementationType)\n    {\n        var implementationCalled = 0;\n\n        var transformer = GetTransformerImplementation(implementationType, () => implementationCalled++);\n\n        var httpContext = new DefaultHttpContext();\n        var proxyResponse = new HttpResponseMessage();\n\n        using var cts = new CancellationTokenSource();\n        await transformer.TransformResponseAsync(httpContext, proxyResponse, cts.Token);\n\n        Assert.Equal(1, implementationCalled);\n    }\n\n    [Theory]\n    [MemberData(nameof(ImplementationTypes_MemberData))]\n    public async Task DerivedImplementation_TransformResponseTrailersAsync_DerivedImplementationCalled(ImplementationType implementationType)\n    {\n        var implementationCalled = 0;\n\n        var transformer = GetTransformerImplementation(implementationType, () => implementationCalled++);\n\n        var httpContext = new DefaultHttpContext();\n        var proxyResponse = new HttpResponseMessage();\n\n        httpContext.Features.Set<IHttpResponseTrailersFeature>(new TestTrailersFeature());\n\n        using var cts = new CancellationTokenSource();\n        await transformer.TransformResponseTrailersAsync(httpContext, proxyResponse, cts.Token);\n\n        Assert.Equal(1, implementationCalled);\n    }\n\n    private static HttpTransformer GetTransformerImplementation(ImplementationType implementationType, Action callback)\n    {\n        return implementationType switch\n        {\n            ImplementationType.StructuredTransformer => TransformBuilderTests.CreateTransformBuilder().CreateInternal(context =>\n            {\n                context.AddRequestTransform(context =>\n                {\n                    callback();\n                    return default;\n                });\n                context.AddResponseTransform(context =>\n                {\n                    callback();\n                    return default;\n                });\n                context.AddResponseTrailersTransform(context =>\n                {\n                    callback();\n                    return default;\n                });\n            }),\n            ImplementationType.DerivedWithoutCT => new DerivedTransformerWithoutCT { Callback = callback },\n            ImplementationType.DerivedWithCT => new DerivedTransformerWithCT { Callback = callback },\n            _ => throw new InvalidOperationException(implementationType.ToString())\n        };\n    }\n\n    private sealed class DerivedTransformerWithoutCT : HttpTransformer\n    {\n        public Action Callback { get; set; }\n\n#pragma warning disable CS0672 // We're intentionally testing the obsolete overloads\n#pragma warning disable CS0618\n        public override ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix)\n        {\n            Callback();\n\n            return base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix);\n        }\n\n        public override ValueTask<bool> TransformResponseAsync(HttpContext httpContext, HttpResponseMessage proxyResponse)\n        {\n            Callback();\n            return base.TransformResponseAsync(httpContext, proxyResponse);\n        }\n\n        public override ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse)\n        {\n            Callback();\n            return base.TransformResponseTrailersAsync(httpContext, proxyResponse);\n        }\n#pragma warning restore CS0618\n#pragma warning restore CS0672\n    }\n\n    private sealed class DerivedTransformerWithCT : HttpTransformer\n    {\n        public Action Callback { get; set; }\n\n        public override ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken)\n        {\n            Callback();\n            return base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken);\n        }\n\n        public override ValueTask<bool> TransformResponseAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken)\n        {\n            Callback();\n            return base.TransformResponseAsync(httpContext, proxyResponse, cancellationToken);\n        }\n\n        public override ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken)\n        {\n            Callback();\n            return base.TransformResponseTrailersAsync(httpContext, proxyResponse, cancellationToken);\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Forwarder/RequestUtilitiesTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Forwarder.Tests;\n\npublic class RequestUtilitiesTests\n{\n    [Fact]\n    public void GetHttpMethod_Get_Works()\n    {\n        Assert.Same(HttpMethod.Get, RequestUtilities.GetHttpMethod(\"GET\"));\n    }\n\n    [Fact]\n    public void GetHttpMethod_Post_Works()\n    {\n        Assert.Same(HttpMethod.Post, RequestUtilities.GetHttpMethod(\"POST\"));\n    }\n\n    [Fact]\n    public void GetHttpMethod_Put_Works()\n    {\n        Assert.Same(HttpMethod.Put, RequestUtilities.GetHttpMethod(\"PUT\"));\n    }\n\n    [Fact]\n    public void GetHttpMethod_Delete_Works()\n    {\n        Assert.Same(HttpMethod.Delete, RequestUtilities.GetHttpMethod(\"DELETE\"));\n    }\n\n    [Fact]\n    public void GetHttpMethod_Options_Works()\n    {\n        Assert.Same(HttpMethod.Options, RequestUtilities.GetHttpMethod(\"OPTIONS\"));\n    }\n\n    [Fact]\n    public void GetHttpMethod_Head_Works()\n    {\n        Assert.Same(HttpMethod.Head, RequestUtilities.GetHttpMethod(\"HEAD\"));\n    }\n\n    [Fact]\n    public void GetHttpMethod_Patch_Works()\n    {\n        Assert.Same(HttpMethod.Patch, RequestUtilities.GetHttpMethod(\"PATCH\"));\n    }\n\n    [Fact]\n    public void GetHttpMethod_Trace_Works()\n    {\n        Assert.Same(HttpMethod.Trace, RequestUtilities.GetHttpMethod(\"TRACE\"));\n    }\n\n    [Fact]\n    public void GetHttpMethod_Unknown_Works()\n    {\n        Assert.Same(\"Unknown\", RequestUtilities.GetHttpMethod(\"Unknown\").Method);\n    }\n\n    [Fact]\n    public void GetHttpMethod_Connect_Throws()\n    {\n        Assert.Throws<NotSupportedException>(() => RequestUtilities.GetHttpMethod(\"CONNECT\"));\n    }\n\n    [Theory]\n    [InlineData(\" GET\")]\n    [InlineData(\"GET \")]\n    [InlineData(\"G;ET\")]\n    public void GetHttpMethod_Invalid_Throws(string method)\n    {\n        Assert.Throws<FormatException>(() => RequestUtilities.GetHttpMethod(method));\n    }\n\n    [Theory]\n    [InlineData(\"http://localhost\", \"\", \"\", \"http://localhost/\")]\n    [InlineData(\"http://localhost/\", \"\", \"\", \"http://localhost/\")]\n    [InlineData(\"http://localhost\", \"/\", \"\", \"http://localhost/\")]\n    [InlineData(\"http://localhost/\", \"/\", \"\", \"http://localhost/\")]\n    [InlineData(\"http://localhost\", \"\", \"?query\", \"http://localhost/?query\")]\n    [InlineData(\"http://localhost\", \"/path\", \"?query\", \"http://localhost/path?query\")]\n    [InlineData(\"http://localhost\", \"/path/\", \"?query\", \"http://localhost/path/?query\")]\n    [InlineData(\"http://localhost/\", \"/path\", \"?query\", \"http://localhost/path?query\")]\n    [InlineData(\"http://localhost/base\", \"\", \"\", \"http://localhost/base\")]\n    [InlineData(\"http://localhost/base\", \"\", \"?query\", \"http://localhost/base?query\")]\n    [InlineData(\"http://localhost/base\", \"/path\", \"?query\", \"http://localhost/base/path?query\")]\n    [InlineData(\"http://localhost/base/\", \"/path\", \"?query\", \"http://localhost/base/path?query\")]\n    [InlineData(\"http://localhost/base/\", \"/path/\", \"?query\", \"http://localhost/base/path/?query\")]\n    [InlineData(\"http://localhost/base/\", \"/path/你好\", \"?query%E4%BD%A0%E5%A5%BD\", \"http://localhost/base/path/%E4%BD%A0%E5%A5%BD?query%E4%BD%A0%E5%A5%BD\")]\n    // pchar         = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n    // unreserved    = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n    // sub-delims    = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n    [InlineData(\"http://localhost/base/\", \"/path/!$&'()*+,;=:@/%?#[]\", \"?query\", \"http://localhost/base/path/!$&'()*+,;=:@/%25%3F%23%5B%5D?query\")]\n    // PathString should be fully un-escaped to start with and QueryString should be fully escaped.\n    [InlineData(\"http://localhost/base/\", \"/path/%2F%20\", \"?query%20\", \"http://localhost/base/path/%252F%2520?query%20\")]\n    public void MakeDestinationAddress(string destinationPrefix, string path, string query, string expected)\n    {\n        var uri = RequestUtilities.MakeDestinationAddress(destinationPrefix, new PathString(path), new QueryString(query));\n        Assert.Equal(expected, uri.AbsoluteUri);\n    }\n\n    // https://datatracker.ietf.org/doc/html/rfc3986/#appendix-A\n    // pchar         = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n    // pct-encoded   = \"%\" HEXDIG HEXDIG\n    // unreserved    = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n    // reserved      = gen-delims / sub-delims\n    // gen-delims    = \":\" / \"/\" / \"?\" / \"#\" / \"[\" / \"]\" / \"@\"\n    // sub-delims    = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\n    [Fact]\n    public void ValidPathCharacters()\n    {\n        var valids = new char[]\n        {\n            '!', '$', '&', '\\'', '(', ')', '*', '+', ',', '-', '.', '/',\n            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',\n            ':', ';', '=', '@',\n            'A', 'B', 'C', 'D', 'E', 'F','G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',\n            '_',\n            'a', 'b', 'c', 'd', 'e', 'f','g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',\n            '~'\n        };\n\n        foreach (var c in valids)\n        {\n            Assert.Equal($\"/{c}\", RequestUtilities.EncodePath($\"/{c}\"));\n        }\n    }\n\n    [Fact]\n    public void InvalidPathCharacters()\n    {\n        var invalids = new char[]\n        {\n            // Controls\n            (char)0x00, (char)0x01, (char)0x02, (char)0x03, (char)0x04, (char)0x05, (char)0x06, (char)0x00, (char)0x07, (char)0x08, (char)0x09, (char)0x0A, (char)0x0B, (char)0x0C, (char)0x0D, (char)0x0E, (char)0x0F,\n            (char)0x10, (char)0x11, (char)0x02, (char)0x13, (char)0x14, (char)0x15, (char)0x16, (char)0x10, (char)0x17, (char)0x18, (char)0x19, (char)0x1A, (char)0x1B, (char)0x1C, (char)0x1D, (char)0x1E, (char)0x1F,\n            ' ', '\"', '#', '%', '<', '>', '?', '[', '\\\\', ']', '^', '`', '{', '|', '}'\n        };\n\n        foreach (var c in invalids)\n        {\n            Assert.Equal($\"/%{(int)c:X2}\", RequestUtilities.EncodePath($\"/{c}\"));\n        }\n    }\n\n    [Theory]\n    [InlineData(null, \"a\", \"a\")]\n    [InlineData(\"a\", \"\", \"a;\")]\n    [InlineData(\"\", \"a\", \";a\")]\n    [InlineData(\"a\", \"b\", \"a;b\")]\n    [InlineData(null, \"a;b\", \"a;b\")]\n    [InlineData(\"a;b\", \"\", \"a;b;\")]\n    [InlineData(\"\", \"a;b\", \";a;b\")]\n    [InlineData(\"a;b\", \"c\", \"a;b;c\")]\n    [InlineData(\"a\", \"b;c\", \"a;b;c\")]\n    [InlineData(\"a;b\", \"c;d\", \"a;b;c;d\")]\n    [InlineData(\"a\", \"b c\", \"a;b c\")]\n    [InlineData(\"a b\", \"c\", \"a b;c\")]\n    public void Concat(string stringValues, string inputHeaderStringValues, string expectedOutput)\n    {\n        var request = new HttpRequestMessage();\n        foreach (var value in inputHeaderStringValues.Split(';'))\n        {\n            request.Headers.TryAddWithoutValidation(\"foo\", value);\n        }\n        request.Headers.TryAddWithoutValidation(\"bar\", inputHeaderStringValues.Split(';'));\n\n        var headerStringValues = request.Headers.NonValidated[\"foo\"];\n        var actualValues = RequestUtilities.Concat(stringValues?.Split(';'), headerStringValues);\n        Assert.Equal(expectedOutput.Split(';'), actualValues);\n\n        headerStringValues = request.Headers.NonValidated[\"bar\"];\n        actualValues = RequestUtilities.Concat(stringValues?.Split(';'), headerStringValues);\n        Assert.Equal(expectedOutput.Split(';'), actualValues);\n    }\n\n    [Theory]\n    [InlineData(\"a\")]\n    [InlineData(\"a b\")]\n    [InlineData(\"a\", \"b\")]\n    [InlineData(\"a\", \"b c\", \"d\")]\n    [InlineData(\"\")]\n    [InlineData(\"\", \"\")]\n    [InlineData(\"a\", \"\")]\n    [InlineData(\"\", \"a\")]\n    [InlineData(\"\", \"a\", \"b\")]\n    [InlineData(\"\", \"a\", \"\")]\n    [InlineData(\"a\", \"\", \"b\")]\n    public void TryGetValues(params string[] headerValues)\n    {\n        var request = new HttpRequestMessage();\n        foreach (var value in headerValues)\n        {\n            request.Headers.TryAddWithoutValidation(\"foo\", value);\n        }\n        request.Headers.TryAddWithoutValidation(\"bar\", headerValues);\n\n        Assert.True(RequestUtilities.TryGetValues(request.Headers, \"foo\", out var actualValues));\n        Assert.Equal(headerValues, actualValues);\n\n        Assert.True(RequestUtilities.TryGetValues(request.Headers, \"bar\", out actualValues));\n        Assert.Equal(headerValues, actualValues);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Forwarder/ReverseProxyServiceCollectionTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Forwarder;\n\npublic class ReverseProxyServiceCollectionTests\n{\n\n    [Fact]\n    public void ConfigureHttpClient_Works()\n    {\n        new ServiceCollection()\n            .AddReverseProxy()\n            .ConfigureHttpClient((_, _) => { });\n    }\n\n    [Fact]\n    public void ConfigureHttpClient_ThrowIfCustomServiceAdded()\n    {\n        Assert.Throws<InvalidOperationException>(() =>\n        {\n            new ServiceCollection()\n                .AddSingleton<IForwarderHttpClientFactory, CustomForwarderHttpClientFactory>()\n                .AddReverseProxy()\n                .ConfigureHttpClient((_, _) => { });\n        });\n    }\n\n    private class CustomForwarderHttpClientFactory : IForwarderHttpClientFactory\n    {\n        public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context)\n        {\n            throw new NotImplementedException();\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Forwarder/StreamCopierTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics.Tracing;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\nusing Yarp.Tests.Common;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Forwarder.Tests;\n\npublic class StreamCopierTests : TestAutoMockBase\n{\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task CopyAsync_Works(bool isRequest)\n    {\n        var events = TestEventListener.Collect();\n\n        const int SourceSize = (128 * 1024) - 3;\n        var sourceBytes = Enumerable.Range(0, SourceSize).Select(i => (byte)(i % 256)).ToArray();\n        var source = new MemoryStream(sourceBytes);\n        var destination = new MemoryStream();\n\n        using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);\n        await StreamCopier.CopyAsync(isRequest, source, destination, SourceSize, TimeProvider.System, cts, cts.Token);\n\n        Assert.False(cts.Token.IsCancellationRequested);\n\n        Assert.Equal(sourceBytes, destination.ToArray());\n\n        AssertContentTransferred(events, isRequest, SourceSize);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task SourceThrows_Reported(bool isRequest)\n    {\n        var events = TestEventListener.Collect();\n\n        var timeProvider = new TestTimeProvider();\n        var sourceWaitTime = TimeSpan.FromMilliseconds(12345);\n        var source = new SlowStream(new ThrowStream(), timeProvider, sourceWaitTime);\n        var destination = new MemoryStream();\n\n        using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);\n        var (result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, StreamCopier.UnknownLength, timeProvider, cts, cts.Token);\n        Assert.Equal(StreamCopyResult.InputError, result);\n        Assert.IsAssignableFrom<IOException>(error);\n\n        AssertContentTransferred(events, isRequest,\n            contentLength: 0,\n            iops: 1,\n            firstReadTime: sourceWaitTime,\n            readTime: sourceWaitTime,\n            writeTime: TimeSpan.Zero);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task DestinationThrows_Reported(bool isRequest)\n    {\n        var events = TestEventListener.Collect();\n\n        const int SourceSize = 10;\n        const int BytesPerRead = 3;\n\n        var timeProvider = new TestTimeProvider();\n        var sourceWaitTime = TimeSpan.FromMilliseconds(12345);\n        var destinationWaitTime = TimeSpan.FromMilliseconds(42);\n        var source = new SlowStream(new MemoryStream(new byte[SourceSize]), timeProvider, sourceWaitTime) { MaxBytesPerRead = BytesPerRead };\n        var destination = new SlowStream(new ThrowStream(), timeProvider, destinationWaitTime);\n\n        using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);\n        var (result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, SourceSize, timeProvider, cts, cts.Token);\n        Assert.Equal(StreamCopyResult.OutputError, result);\n        Assert.IsAssignableFrom<IOException>(error);\n\n        AssertContentTransferred(events, isRequest,\n            contentLength: BytesPerRead,\n            iops: 1,\n            firstReadTime: sourceWaitTime,\n            readTime: sourceWaitTime,\n            writeTime: destinationWaitTime);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task Cancelled_Reported(bool isRequest)\n    {\n        var events = TestEventListener.Collect();\n\n        var source = new MemoryStream(new byte[10]);\n        var destination = new MemoryStream();\n\n        var requestCts = new CancellationTokenSource();\n        using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), requestCts.Token);\n        requestCts.Cancel();\n        var (result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, StreamCopier.UnknownLength, new TestTimeProvider(), cts, cts.Token);\n        Assert.Equal(StreamCopyResult.Canceled, result);\n        Assert.IsAssignableFrom<OperationCanceledException>(error);\n\n        AssertContentTransferred(events, isRequest,\n            contentLength: 0,\n            iops: 1,\n            firstReadTime: TimeSpan.Zero,\n            readTime: TimeSpan.Zero,\n            writeTime: TimeSpan.Zero);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task SlowStreams_TelemetryReportsCorrectTime(bool isRequest)\n    {\n        var events = TestEventListener.Collect();\n\n        const int SourceSize = 3;\n        var sourceBytes = new byte[SourceSize];\n        var source = new MemoryStream(sourceBytes);\n        var destination = new MemoryStream();\n\n        var timeProvider = new TestTimeProvider();\n        var sourceWaitTime = TimeSpan.FromMilliseconds(12345);\n        var destinationWaitTime = TimeSpan.FromMilliseconds(42);\n\n        using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);\n        await StreamCopier.CopyAsync(\n            isRequest,\n            new SlowStream(source, timeProvider, sourceWaitTime),\n            new SlowStream(destination, timeProvider, destinationWaitTime),\n            SourceSize,\n            timeProvider,\n            cts,\n            cts.Token);\n\n        Assert.Equal(sourceBytes, destination.ToArray());\n\n        AssertContentTransferred(events, isRequest, SourceSize,\n            iops: SourceSize + 1,\n            firstReadTime: sourceWaitTime,\n            readTime: (SourceSize + 1) * sourceWaitTime,\n            writeTime: SourceSize * destinationWaitTime);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task LongContentTransfer_TelemetryReportsTransferringEvents(bool isRequest)\n    {\n        var events = TestEventListener.Collect();\n\n        const int SourceSize = 123;\n        var sourceBytes = new byte[SourceSize];\n        var source = new MemoryStream(sourceBytes);\n        var destination = new MemoryStream();\n\n        var timeProvider = new TestTimeProvider();\n        var sourceWaitTime = TimeSpan.FromMilliseconds(789); // Every second read triggers ContentTransferring\n        var destinationWaitTime = TimeSpan.FromMilliseconds(42);\n\n        const int BytesPerRead = 3;\n        var contentReads = (int)Math.Ceiling((double)SourceSize / BytesPerRead);\n\n        using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);\n        await StreamCopier.CopyAsync(\n            isRequest,\n            new SlowStream(source, timeProvider, sourceWaitTime) { MaxBytesPerRead = BytesPerRead },\n            new SlowStream(destination, timeProvider, destinationWaitTime),\n            SourceSize,\n            timeProvider,\n            cts,\n            cts.Token);\n\n        Assert.Equal(sourceBytes, destination.ToArray());\n\n        AssertContentTransferred(events, isRequest, SourceSize,\n            iops: contentReads + 1,\n            firstReadTime: sourceWaitTime,\n            readTime: (contentReads + 1) * sourceWaitTime,\n            writeTime: contentReads * destinationWaitTime);\n\n        var transferringEvents = events.Where(e => e.EventName == \"ContentTransferring\").ToArray();\n        Assert.Equal(contentReads / 2, transferringEvents.Length);\n\n        for (var i = 0; i < transferringEvents.Length; i++)\n        {\n            var payload = transferringEvents[i].Payload;\n            Assert.Equal(5, payload.Count);\n\n            Assert.Equal(isRequest, (bool)payload[0]);\n\n            var contentLength = (long)payload[1];\n\n            var iops = (long)payload[2];\n            Assert.Equal((i + 1) * 2, iops);\n\n            if (contentLength % BytesPerRead == 0)\n            {\n                Assert.Equal(iops * BytesPerRead, contentLength);\n            }\n            else\n            {\n                Assert.Equal(transferringEvents.Length - 1, i);\n                Assert.Equal(SourceSize, contentLength);\n            }\n\n            var readTime = new TimeSpan((long)payload[3]);\n            Assert.Equal(iops * sourceWaitTime, readTime, new ApproximateTimeSpanComparer());\n\n            var writeTime = new TimeSpan((long)payload[4]);\n            Assert.Equal(iops * destinationWaitTime, writeTime, new ApproximateTimeSpanComparer());\n        }\n    }\n\n    private static void AssertContentTransferred(\n        List<EventWrittenEventArgs> events,\n        bool isRequest,\n        long contentLength,\n        long? iops = null,\n        TimeSpan? firstReadTime = null,\n        TimeSpan? readTime = null,\n        TimeSpan? writeTime = null)\n    {\n        var contentTransferred = Assert.Single(events, e => e.EventName == \"ContentTransferred\");\n        var payload = contentTransferred.Payload;\n        Assert.Equal(6, payload.Count);\n\n        Assert.Equal(isRequest, (bool)payload[0]);\n        Assert.Equal(contentLength, (long)payload[1]);\n\n        var actualIops = (long)payload[2];\n        if (iops.HasValue)\n        {\n            Assert.Equal(iops.Value, actualIops);\n        }\n        else\n        {\n            Assert.InRange(actualIops, 1, contentLength + 1);\n        }\n\n        if (readTime.HasValue)\n        {\n            Assert.Equal(readTime.Value, new TimeSpan((long)payload[3]), new ApproximateTimeSpanComparer());\n        }\n\n        if (writeTime.HasValue)\n        {\n            Assert.Equal(writeTime.Value, new TimeSpan((long)payload[4]), new ApproximateTimeSpanComparer());\n        }\n\n        if (firstReadTime.HasValue)\n        {\n            Assert.Equal(firstReadTime.Value, new TimeSpan((long)payload[5]), new ApproximateTimeSpanComparer());\n\n            if (readTime.HasValue)\n            {\n                Assert.True(firstReadTime.Value <= readTime.Value);\n            }\n        }\n\n        var stages = events.GetProxyStages();\n\n        var startStage = isRequest ? ForwarderStage.RequestContentTransferStart : ForwarderStage.ResponseContentTransferStart;\n        var startTime = Assert.Single(stages, s => s.Stage == startStage).TimeStamp;\n\n        Assert.True(startTime <= contentTransferred.TimeStamp);\n    }\n\n    private class ThrowStream : DelegatingStream\n    {\n        public ThrowStream()\n            : base(Stream.Null)\n        { }\n\n        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            throw new IOException(\"Fake connection issue\");\n        }\n\n        public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            throw new IOException(\"Fake connection issue\");\n        }\n    }\n\n    private class SlowStream : DelegatingStream\n    {\n        private readonly TimeSpan _waitTime;\n        private readonly TestTimeProvider _timeProvider;\n\n        public int MaxBytesPerRead { get; set; } = 1;\n\n        public SlowStream(Stream innerStream, TestTimeProvider timeProvider, TimeSpan waitTime)\n            : base(innerStream)\n        {\n            _timeProvider = timeProvider;\n            _waitTime = waitTime;\n        }\n\n        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            if (buffer.Length == 0)\n            {\n                return new ValueTask<int>(0);\n            }\n\n            _timeProvider.Advance(_waitTime);\n            return base.ReadAsync(buffer.Slice(0, Math.Min(buffer.Length, MaxBytesPerRead)), cancellationToken);\n        }\n\n        public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            _timeProvider.Advance(_waitTime);\n            return base.WriteAsync(buffer, cancellationToken);\n        }\n    }\n\n    private class ApproximateTimeSpanComparer : IEqualityComparer<TimeSpan>\n    {\n        public TimeSpan Precision { get; set; } = TimeSpan.FromMilliseconds(0.1);\n\n        public bool Equals(TimeSpan x, TimeSpan y) => x > y\n            ? x - y <= Precision\n            : y - x <= Precision;\n\n        public int GetHashCode(TimeSpan obj) => 42;\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Forwarder/StreamCopyHttpContentTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.IO;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Reflection;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing Moq;\nusing Xunit;\nusing Yarp.ReverseProxy.Utilities;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Forwarder.Tests;\n\npublic class StreamCopyHttpContentTests\n{\n    private static StreamCopyHttpContent CreateContent(HttpContext context = null, bool isStreamingRequest = false, TimeProvider timeProvider = null, ActivityCancellationTokenSource contentCancellation = null)\n    {\n        context ??= new DefaultHttpContext();\n        timeProvider ??= TimeProvider.System;\n        contentCancellation ??= ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);\n        return new StreamCopyHttpContent(context, isStreamingRequest, timeProvider, NullLogger.Instance, contentCancellation);\n    }\n\n    [Fact]\n    public async Task CopyToAsync_InvokesStreamCopier()\n    {\n        const int SourceSize = (128 * 1024) - 3;\n\n        var sourceBytes = Enumerable.Range(0, SourceSize).Select(i => (byte)(i % 256)).ToArray();\n        var context = new DefaultHttpContext();\n        context.Request.Body = new MemoryStream(sourceBytes);\n        var destination = new MemoryStream();\n\n        var sut = CreateContent(context);\n\n        Assert.False(sut.ConsumptionTask.IsCompleted);\n        Assert.False(sut.Started);\n        await sut.CopyToWithCancellationAsync(destination);\n\n        Assert.True(sut.Started);\n        Assert.True(sut.ConsumptionTask.IsCompleted);\n        Assert.Equal(sourceBytes, destination.ToArray());\n    }\n\n    [Theory]\n    [InlineData(false)] // we expect to always flush at least once to trigger sending request headers\n    [InlineData(true)]\n    public async Task CopyToAsync_AutoFlushing(bool autoFlush)\n    {\n        // Must be same as StreamCopier constant.\n        const int DefaultBufferSize = 65536;\n        const int SourceSize = (128 * 1024) - 3;\n\n        var expectedFlushes = 0;\n        if (autoFlush)\n        {\n            // How many buffers is needed to send the source rounded up.\n            expectedFlushes = (SourceSize - 1) / DefaultBufferSize + 1;\n        }\n        // Explicit flush after headers are sent.\n        expectedFlushes++;\n\n        var sourceBytes = Enumerable.Range(0, SourceSize).Select(i => (byte)(i % 256)).ToArray();\n        var context = new DefaultHttpContext();\n        context.Request.Body = new MemoryStream(sourceBytes);\n        var destination = new MemoryStream();\n        var flushCountingDestination = new FlushCountingStream(destination);\n\n        var sut = CreateContent(context, autoFlush);\n\n        Assert.False(sut.ConsumptionTask.IsCompleted);\n        Assert.False(sut.Started);\n        await sut.CopyToWithCancellationAsync(flushCountingDestination);\n\n        Assert.True(sut.Started);\n        Assert.True(sut.ConsumptionTask.IsCompleted);\n        Assert.Equal(sourceBytes, destination.ToArray());\n        Assert.Equal(expectedFlushes, flushCountingDestination.NumFlushes);\n    }\n\n    [Fact]\n    public async Task CopyToAsync_AsyncSequencing()\n    {\n        var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n        var source = new Mock<Stream>();\n        source.Setup(s => s.ReadAsync(It.IsAny<Memory<byte>>(), It.IsAny<CancellationToken>())).Returns(() => new ValueTask<int>(tcs.Task));\n        var context = new DefaultHttpContext();\n        context.Request.Body = source.Object;\n        var destination = new MemoryStream();\n\n        var sut = CreateContent(context);\n\n        Assert.False(sut.ConsumptionTask.IsCompleted);\n        Assert.False(sut.Started);\n        var task = sut.CopyToWithCancellationAsync(destination);\n\n        Assert.True(sut.Started); // This should happen synchronously\n        Assert.False(sut.ConsumptionTask.IsCompleted); // This cannot happen until the tcs releases it\n\n        tcs.TrySetResult(0);\n        await task;\n        Assert.True(sut.ConsumptionTask.IsCompleted);\n    }\n\n    [Fact]\n    public Task ReadAsStreamAsync_Throws()\n    {\n        var sut = CreateContent();\n\n        Func<Task> func = () => sut.ReadAsStreamAsync();\n\n        return Assert.ThrowsAsync<NotImplementedException>(func);\n    }\n\n    [Fact]\n    public void AllowDuplex_ReturnsTrue()\n    {\n        var sut = CreateContent();\n\n        // This is an internal property that HttpClient and friends use internally and which must be true\n        // to support duplex channels. This test helps detect regressions or changes in undocumented behavior\n        // in .NET Core, and it passes as of .NET Core 3.1.\n        var allowDuplexProperty = typeof(HttpContent).GetProperty(\"AllowDuplex\", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);\n        Assert.NotNull(allowDuplexProperty);\n        var allowDuplex = (bool)allowDuplexProperty.GetValue(sut);\n        Assert.True(allowDuplex);\n    }\n\n    [Fact]\n    public async Task SerializeToStreamAsync_RespectsContentCancellation()\n    {\n        var tcs = new TaskCompletionSource<byte>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var source = new ReadDelegatingStream(new MemoryStream(), async (buffer, cancellation) =>\n        {\n            if (buffer.Length == 0)\n            {\n                return 0;\n            }\n\n            Assert.False(cancellation.IsCancellationRequested);\n            await tcs.Task;\n            Assert.True(cancellation.IsCancellationRequested);\n            return 0;\n        });\n\n        var context = new DefaultHttpContext();\n        context.Request.Body = source;\n\n        using var contentCts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);\n\n        var sut = CreateContent(context, contentCancellation: contentCts);\n\n        var copyToTask = sut.CopyToWithCancellationAsync(new MemoryStream());\n        contentCts.Cancel();\n        tcs.SetResult(0);\n\n        await copyToTask;\n    }\n\n    [Fact]\n    public async Task SerializeToStreamAsync_CanBeCanceledExternally()\n    {\n        var tcs = new TaskCompletionSource<byte>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var source = new ReadDelegatingStream(new MemoryStream(), async (buffer, cancellation) =>\n        {\n            if (buffer.Length == 0)\n            {\n                return 0;\n            }\n\n            Assert.False(cancellation.IsCancellationRequested);\n            await tcs.Task;\n            Assert.True(cancellation.IsCancellationRequested);\n            return 0;\n        });\n\n        var context = new DefaultHttpContext();\n        context.Request.Body = source;\n\n        var sut = CreateContent(context);\n\n        using var cts = new CancellationTokenSource();\n        var copyToTask = sut.CopyToAsync(new MemoryStream(), cts.Token);\n        cts.Cancel();\n        tcs.SetResult(0);\n\n        await copyToTask;\n    }\n\n    private class FlushCountingStream : DelegatingStream\n    {\n        public FlushCountingStream(Stream stream)\n            : base(stream)\n        { }\n\n        public int NumFlushes { get; private set; }\n\n        public override async Task FlushAsync(CancellationToken cancellationToken)\n        {\n            await base.FlushAsync(cancellationToken);\n            NumFlushes++;\n        }\n    }\n\n    private sealed class ReadDelegatingStream : DelegatingStream\n    {\n        private readonly Func<Memory<byte>, CancellationToken, ValueTask<int>> _readAsync;\n\n        public ReadDelegatingStream(Stream stream, Func<Memory<byte>, CancellationToken, ValueTask<int>> readAsync)\n            : base(stream)\n        {\n            _readAsync = readAsync;\n        }\n\n        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)\n        {\n            return _readAsync(buffer, cancellationToken);\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Health/ActiveHealthCheckMonitorTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Text.RegularExpressions;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Moq;\nusing Xunit;\nusing Yarp.Tests.Common;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health.Tests;\n\npublic class ActiveHealthCheckMonitorTests\n{\n    private readonly TimeSpan Interval0 = TimeSpan.FromSeconds(10);\n    private readonly TimeSpan Interval1 = TimeSpan.FromSeconds(20);\n\n    [Fact]\n    public async Task CheckHealthAsync_ActiveHealthCheckIsEnabledForCluster_SendProbe()\n    {\n        var policy0 = new Mock<IActiveHealthCheckPolicy>();\n        policy0.SetupGet(p => p.Name).Returns(\"policy0\");\n        var policy1 = new Mock<IActiveHealthCheckPolicy>();\n        policy1.SetupGet(p => p.Name).Returns(\"policy1\");\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) });\n        var clusters = new List<ClusterState>();\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy0.Object, policy1.Object }, new DefaultProbingRequestFactory(), new Mock<TimeProvider>().Object, GetLogger());\n\n        var httpClient0 = GetHttpClient();\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\", true, httpClient0.Object);\n        clusters.Add(cluster0);\n        var httpClient1 = GetHttpClient();\n        var cluster1 = GetClusterInfo(\"cluster1\", \"policy0\", false, httpClient1.Object);\n        clusters.Add(cluster1);\n        var httpClient2 = GetHttpClient();\n        var cluster2 = GetClusterInfo(\"cluster2\", \"policy1\", true, httpClient2.Object);\n        clusters.Add(cluster2);\n\n        Assert.False(monitor.InitialProbeCompleted);\n\n        await monitor.CheckHealthAsync(clusters);\n\n        Assert.True(monitor.InitialProbeCompleted);\n\n        VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { (\"https://localhost:20000/cluster0/api/health/\", 1), (\"https://localhost:20001/cluster0/api/health/\", 1) });\n\n        httpClient1.Verify(c => c.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()), Times.Never);\n\n        VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { (\"https://localhost:20000/cluster2/api/health/\", 1), (\"https://localhost:20001/cluster2/api/health/\", 1) });\n    }\n\n    [Theory]\n    [InlineData(false)] // Test old API (CreateRequest with Models) via default implementation\n    [InlineData(true)]  // Test new API (CreateRequestAsync with State)\n    public async Task CheckHealthAsync_CustomUserAgentSpecified_UserAgentUnchanged(bool overrideAsyncMethod)\n    {\n        var policy = new Mock<IActiveHealthCheckPolicy>();\n        policy.SetupGet(p => p.Name).Returns(\"policy\");\n\n        var requestFactory = new Mock<IProbingRequestFactory>();\n\n        HttpRequestMessage CreateCustomRequest()\n        {\n            var request = new HttpRequestMessage(HttpMethod.Get, \"https://localhost:20000/cluster/api/health/\");\n            request.Headers.UserAgent.ParseAdd(\"FooBar/9001\");\n            return request;\n        }\n\n        if (overrideAsyncMethod)\n        {\n            requestFactory.Setup(p => p.CreateRequestAsync(It.IsAny<ClusterState>(), It.IsAny<DestinationState>(), It.IsAny<CancellationToken>()))\n                .Returns(() => ValueTask.FromResult(CreateCustomRequest()));\n        }\n        else\n        {\n            // Test the old API - the default implementation of CreateRequestAsync should call this\n            requestFactory.Setup(p => p.CreateRequest(It.IsAny<ClusterModel>(), It.IsAny<DestinationModel>()))\n                .Returns(CreateCustomRequest);\n\n            // Use default interface implementation for CreateRequestAsync\n            requestFactory.CallBase = true;\n        }\n\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions());\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, requestFactory.Object, new Mock<TimeProvider>().Object, GetLogger());\n\n        var httpClient = GetHttpClient();\n        var cluster = GetClusterInfo(\"cluster\", \"policy\", true, httpClient.Object, destinationCount: 1);\n\n        await monitor.CheckHealthAsync(new[] { cluster });\n\n        VerifySentProbeAndResult(cluster, httpClient, policy, new[] { (\"https://localhost:20000/cluster/api/health/\", 1) }, userAgent: @\"^FooBar\\/9001$\");\n    }\n\n    [Fact]\n    public async Task CheckHealthAsync_FactoryCancelledExternally_ProbePassedToPolicyWithException()\n    {\n        var policy = new Mock<IActiveHealthCheckPolicy>();\n        policy.SetupGet(p => p.Name).Returns(\"policy\");\n\n        var externalCts = new CancellationTokenSource();\n        var requestFactory = new Mock<IProbingRequestFactory>();\n\n        // First destination: factory throws OperationCanceledException (external cancellation)\n        // Second destination: succeeds normally\n        var callCount = 0;\n        requestFactory.Setup(p => p.CreateRequestAsync(It.IsAny<ClusterState>(), It.IsAny<DestinationState>(), It.IsAny<CancellationToken>()))\n            .Returns<ClusterState, DestinationState, CancellationToken>((cluster, destination, ct) =>\n            {\n                callCount++;\n                if (callCount == 1)\n                {\n                    // Simulate external cancellation (not timeout) - throw without the timeout CTS being cancelled\n                    throw new OperationCanceledException(externalCts.Token);\n                }\n\n                return ValueTask.FromResult(new HttpRequestMessage(HttpMethod.Get, $\"https://localhost:20000/{destination.DestinationId}/health/\"));\n            });\n\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultTimeout = TimeSpan.FromSeconds(30) });\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, requestFactory.Object, new Mock<TimeProvider>().Object, GetLogger());\n\n        var httpClient = GetHttpClient();\n        var cluster = GetClusterInfo(\"cluster\", \"policy\", true, httpClient.Object, destinationCount: 2);\n\n        await monitor.CheckHealthAsync(new[] { cluster });\n\n        // Policy should receive 2 results: one with exception (cancelled), one successful\n        policy.Verify(\n            p => p.ProbingCompleted(\n                cluster,\n                It.Is<IReadOnlyList<DestinationProbingResult>>(r =>\n                    r.Count == 2 &&\n                    r.Any(x => x.Exception is OperationCanceledException) &&\n                    r.Any(x => x.Response != null && x.Response.StatusCode == HttpStatusCode.OK)))\n            , Times.Once);\n        policy.Verify(p => p.Name);\n        policy.VerifyNoOtherCalls();\n    }\n\n    [Fact]\n    public async Task CheckHealthAsync_SendAsyncCancelledExternally_ProbePassedToPolicyWithException()\n    {\n        var policy = new Mock<IActiveHealthCheckPolicy>();\n        policy.SetupGet(p => p.Name).Returns(\"policy\");\n\n        var externalCts = new CancellationTokenSource();\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultTimeout = TimeSpan.FromSeconds(30) });\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock<TimeProvider>().Object, GetLogger());\n\n        // First destination: SendAsync throws OperationCanceledException (external cancellation)\n        // Second destination: succeeds normally\n        var callCount = 0;\n        var httpClient = new Mock<HttpMessageInvoker>(() => new HttpMessageInvoker(new Mock<HttpMessageHandler>().Object));\n        httpClient.Setup(c => c.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))\n            .Returns<HttpRequestMessage, CancellationToken>((request, ct) =>\n            {\n                callCount++;\n                if (callCount == 1)\n                {\n                    // Simulate external cancellation (not timeout) - throw without the timeout CTS being cancelled\n                    throw new OperationCanceledException(externalCts.Token);\n                }\n\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Version = request.Version });\n            });\n\n        var cluster = GetClusterInfo(\"cluster\", \"policy\", true, httpClient.Object, destinationCount: 2);\n\n        await monitor.CheckHealthAsync(new[] { cluster });\n\n        // Policy should receive 2 results: one with exception (cancelled), one successful\n        policy.Verify(\n            p => p.ProbingCompleted(\n                cluster,\n                It.Is<IReadOnlyList<DestinationProbingResult>>(r =>\n                    r.Count == 2 &&\n                    r.Any(x => x.Exception is OperationCanceledException) &&\n                    r.Any(x => x.Response != null && x.Response.StatusCode == HttpStatusCode.OK)))\n            , Times.Once);\n        policy.Verify(p => p.Name);\n        policy.VerifyNoOtherCalls();\n    }\n\n    [Fact]\n    public async Task CheckHealthAsync_TimeoutCancellation_TreatedAsError()\n    {\n        var policy = new Mock<IActiveHealthCheckPolicy>();\n        policy.SetupGet(p => p.Name).Returns(\"policy\");\n\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultTimeout = TimeSpan.FromMilliseconds(1) });\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock<TimeProvider>().Object, GetLogger());\n\n        var tcs = new TaskCompletionSource<HttpResponseMessage>();\n        var httpClient = new Mock<HttpMessageInvoker>(() => new HttpMessageInvoker(new Mock<HttpMessageHandler>().Object));\n        httpClient.Setup(c => c.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))\n            .Returns<HttpRequestMessage, CancellationToken>((request, ct) =>\n            {\n                // Register callback to cancel the TCS when timeout occurs\n                ct.Register(() => tcs.TrySetCanceled(ct));\n                return tcs.Task;\n            });\n\n        var cluster = GetClusterInfo(\"cluster\", \"policy\", true, httpClient.Object, destinationCount: 1);\n\n        await monitor.CheckHealthAsync(new[] { cluster });\n\n        // Policy should receive 1 result with an exception (timeout is an error, not skipped)\n        policy.Verify(\n            p => p.ProbingCompleted(\n                cluster,\n                It.Is<IReadOnlyList<DestinationProbingResult>>(r =>\n                    r.Count == 1 &&\n                    r[0].Response == null &&\n                    r[0].Exception is OperationCanceledException)),\n            Times.Once);\n        policy.Verify(p => p.Name);\n        policy.VerifyNoOtherCalls();\n    }\n\n    [Fact]\n    public async Task ProbeCluster_ProbingTimerFired_SendProbesAndReceiveResponses()\n    {\n        var policy0 = new Mock<IActiveHealthCheckPolicy>();\n        policy0.SetupGet(p => p.Name).Returns(\"policy0\");\n        var policy1 = new Mock<IActiveHealthCheckPolicy>();\n        policy1.SetupGet(p => p.Name).Returns(\"policy1\");\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) });\n        var timeProvider = new TestTimeProvider();\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy0.Object, policy1.Object }, new DefaultProbingRequestFactory(), timeProvider, GetLogger());\n\n        var httpClient0 = GetHttpClient();\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\", true, httpClient0.Object, Interval0);\n        monitor.OnClusterAdded(cluster0);\n        var httpClient2 = GetHttpClient();\n        var cluster2 = GetClusterInfo(\"cluster2\", \"policy1\", true, httpClient2.Object, Interval1);\n        monitor.OnClusterAdded(cluster2);\n\n        Assert.False(monitor.InitialProbeCompleted);\n\n        await monitor.CheckHealthAsync(Array.Empty<ClusterState>());\n\n        Assert.True(monitor.InitialProbeCompleted);\n\n        timeProvider.FireAllTimers();\n\n        Assert.Equal(2, timeProvider.TimerCount);\n        timeProvider.VerifyTimer(0, Interval0);\n        timeProvider.VerifyTimer(1, Interval1);\n        VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { (\"https://localhost:20000/cluster0/api/health/\", 1), (\"https://localhost:20001/cluster0/api/health/\", 1) }, policyCallTimes: 1);\n        VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { (\"https://localhost:20000/cluster2/api/health/\", 1), (\"https://localhost:20001/cluster2/api/health/\", 1) }, policyCallTimes: 1);\n\n        GC.KeepAlive(monitor); // The timer does not keep a strong reference to the scheduler\n    }\n\n    [Fact]\n    public async Task ProbeCluster_ClusterRemoved_StopSendingProbes()\n    {\n        var policy0 = new Mock<IActiveHealthCheckPolicy>();\n        policy0.SetupGet(p => p.Name).Returns(\"policy0\");\n        var policy1 = new Mock<IActiveHealthCheckPolicy>();\n        policy1.SetupGet(p => p.Name).Returns(\"policy1\");\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) });\n        var timeProvider = new TestTimeProvider();\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy0.Object, policy1.Object }, new DefaultProbingRequestFactory(), timeProvider, GetLogger());\n\n        var httpClient0 = GetHttpClient();\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\", true, httpClient0.Object, interval: Interval0);\n        monitor.OnClusterAdded(cluster0);\n        var httpClient2 = GetHttpClient();\n        var cluster2 = GetClusterInfo(\"cluster2\", \"policy1\", true, httpClient2.Object, interval: Interval1);\n        monitor.OnClusterAdded(cluster2);\n\n        Assert.False(monitor.InitialProbeCompleted);\n\n        await monitor.CheckHealthAsync(Array.Empty<ClusterState>());\n\n        Assert.True(monitor.InitialProbeCompleted);\n\n        timeProvider.FireAllTimers();\n\n        Assert.Equal(2, timeProvider.TimerCount);\n        timeProvider.VerifyTimer(0, Interval0);\n        timeProvider.VerifyTimer(1, Interval1);\n        VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { (\"https://localhost:20000/cluster0/api/health/\", 1), (\"https://localhost:20001/cluster0/api/health/\", 1) }, policyCallTimes: 1);\n        VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { (\"https://localhost:20000/cluster2/api/health/\", 1), (\"https://localhost:20001/cluster2/api/health/\", 1) }, policyCallTimes: 1);\n\n        monitor.OnClusterRemoved(cluster2);\n\n        timeProvider.FireTimer(0);\n\n        timeProvider.AssertTimerDisposed(1);\n\n        VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { (\"https://localhost:20000/cluster0/api/health/\", 2), (\"https://localhost:20001/cluster0/api/health/\", 2) }, policyCallTimes: 2);\n        VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { (\"https://localhost:20000/cluster2/api/health/\", 1), (\"https://localhost:20001/cluster2/api/health/\", 1) }, policyCallTimes: 1);\n\n        GC.KeepAlive(monitor); // The timer does not keep a strong reference to the scheduler\n    }\n\n    [Fact]\n    public async Task ProbeCluster_ClusterAdded_StartSendingProbes()\n    {\n        var policy0 = new Mock<IActiveHealthCheckPolicy>();\n        policy0.SetupGet(p => p.Name).Returns(\"policy0\");\n        var policy1 = new Mock<IActiveHealthCheckPolicy>();\n        policy1.SetupGet(p => p.Name).Returns(\"policy1\");\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) });\n        var timeProvider = new TestTimeProvider();\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy0.Object, policy1.Object }, new DefaultProbingRequestFactory(), timeProvider, GetLogger());\n\n        var httpClient0 = GetHttpClient();\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\", true, httpClient0.Object, interval: Interval0);\n        monitor.OnClusterAdded(cluster0);\n\n        Assert.False(monitor.InitialProbeCompleted);\n\n        await monitor.CheckHealthAsync(Array.Empty<ClusterState>());\n\n        Assert.True(monitor.InitialProbeCompleted);\n\n        timeProvider.FireAllTimers();\n\n        Assert.Equal(1, timeProvider.TimerCount);\n        timeProvider.VerifyTimer(0, Interval0);\n        VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { (\"https://localhost:20000/cluster0/api/health/\", 1), (\"https://localhost:20001/cluster0/api/health/\", 1) }, policyCallTimes: 1);\n\n        var httpClient2 = GetHttpClient();\n        var cluster2 = GetClusterInfo(\"cluster2\", \"policy1\", true, httpClient2.Object, interval: Interval1);\n        monitor.OnClusterAdded(cluster2);\n\n        timeProvider.FireAllTimers();\n\n        Assert.Equal(2, timeProvider.TimerCount);\n        timeProvider.VerifyTimer(0, Interval0);\n        timeProvider.VerifyTimer(1, Interval1);\n        VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { (\"https://localhost:20000/cluster0/api/health/\", 2), (\"https://localhost:20001/cluster0/api/health/\", 2) }, policyCallTimes: 2);\n        VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { (\"https://localhost:20000/cluster2/api/health/\", 1), (\"https://localhost:20001/cluster2/api/health/\", 1) }, policyCallTimes: 1);\n\n        GC.KeepAlive(monitor); // The timer does not keep a strong reference to the scheduler\n    }\n\n    [Fact]\n    public async Task ProbeCluster_ClusterChanged_SendProbesToNewHealthEndpoint()\n    {\n        var policy0 = new Mock<IActiveHealthCheckPolicy>();\n        policy0.SetupGet(p => p.Name).Returns(\"policy0\");\n        var policy1 = new Mock<IActiveHealthCheckPolicy>();\n        policy1.SetupGet(p => p.Name).Returns(\"policy1\");\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) });\n        var timeProvider = new TestTimeProvider();\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy0.Object, policy1.Object }, new DefaultProbingRequestFactory(), timeProvider, GetLogger());\n\n        var httpClient0 = GetHttpClient();\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\", true, httpClient0.Object, interval: Interval0);\n        monitor.OnClusterAdded(cluster0);\n        var httpClient2 = GetHttpClient();\n        var cluster2 = GetClusterInfo(\"cluster2\", \"policy1\", true, httpClient2.Object, interval: Interval1);\n        monitor.OnClusterAdded(cluster2);\n\n        Assert.False(monitor.InitialProbeCompleted);\n\n        await monitor.CheckHealthAsync(Array.Empty<ClusterState>());\n\n        Assert.True(monitor.InitialProbeCompleted);\n\n        timeProvider.FireAllTimers();\n\n        Assert.Equal(2, timeProvider.TimerCount);\n        timeProvider.VerifyTimer(0, Interval0);\n        timeProvider.VerifyTimer(1, Interval1);\n        VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { (\"https://localhost:20000/cluster0/api/health/\", 1), (\"https://localhost:20001/cluster0/api/health/\", 1) }, policyCallTimes: 1);\n        VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { (\"https://localhost:20000/cluster2/api/health/\", 1), (\"https://localhost:20001/cluster2/api/health/\", 1) }, policyCallTimes: 1);\n\n        foreach (var destination in cluster2.Destinations.Values)\n        {\n            var d = cluster2.Destinations.GetOrAdd(destination.DestinationId, id => new DestinationState(id));\n            d.Model = new DestinationModel(new DestinationConfig { Address = destination.Model.Config.Address });\n        }\n\n        monitor.OnClusterChanged(cluster2);\n\n        timeProvider.FireAllTimers();\n\n        Assert.Equal(2, timeProvider.TimerCount);\n        timeProvider.VerifyTimer(0, Interval0);\n        timeProvider.VerifyTimer(1, Interval1);\n        VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { (\"https://localhost:20000/cluster0/api/health/\", 2), (\"https://localhost:20001/cluster0/api/health/\", 2) }, policyCallTimes: 2);\n        VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { (\"https://localhost:10000/cluster2/api/health/\", 1), (\"https://localhost:10001/cluster2/api/health/\", 1) }, policyCallTimes: 2);\n\n        GC.KeepAlive(monitor); // The timer does not keep a strong reference to the scheduler\n    }\n\n    [Fact]\n    public async Task ProbeCluster_ClusterChanged_StopSendingProbes()\n    {\n        var policy0 = new Mock<IActiveHealthCheckPolicy>();\n        policy0.SetupGet(p => p.Name).Returns(\"policy0\");\n        var policy1 = new Mock<IActiveHealthCheckPolicy>();\n        policy1.SetupGet(p => p.Name).Returns(\"policy1\");\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) });\n        var timeProvider = new TestTimeProvider();\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy0.Object, policy1.Object }, new DefaultProbingRequestFactory(), timeProvider, GetLogger());\n\n        var httpClient0 = GetHttpClient();\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\", true, httpClient0.Object, interval: Interval0);\n        monitor.OnClusterAdded(cluster0);\n        var httpClient2 = GetHttpClient();\n        var cluster2 = GetClusterInfo(\"cluster2\", \"policy1\", true, httpClient2.Object, interval: Interval1);\n        monitor.OnClusterAdded(cluster2);\n\n        Assert.False(monitor.InitialProbeCompleted);\n\n        await monitor.CheckHealthAsync(Array.Empty<ClusterState>());\n\n        Assert.True(monitor.InitialProbeCompleted);\n\n        timeProvider.FireAllTimers();\n\n        Assert.Equal(2, timeProvider.TimerCount);\n        timeProvider.VerifyTimer(0, Interval0);\n        timeProvider.VerifyTimer(1, Interval1);\n        VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { (\"https://localhost:20000/cluster0/api/health/\", 1), (\"https://localhost:20001/cluster0/api/health/\", 1) }, policyCallTimes: 1);\n        VerifySentProbeAndResult(cluster2, httpClient2, policy1, new[] { (\"https://localhost:20000/cluster2/api/health/\", 1), (\"https://localhost:20001/cluster2/api/health/\", 1) }, policyCallTimes: 1);\n\n        var healthCheckConfig = new HealthCheckConfig\n        {\n            Passive = new PassiveHealthCheckConfig\n            {\n                Enabled = true,\n                Policy = \"passive0\",\n            },\n            Active = new ActiveHealthCheckConfig\n            {\n                Policy = cluster2.Model.Config.HealthCheck.Active.Policy,\n            }\n        };\n        cluster2.Model = new ClusterModel(new ClusterConfig { ClusterId = cluster2.ClusterId, HealthCheck = healthCheckConfig },\n            cluster2.Model.HttpClient);\n\n        monitor.OnClusterChanged(cluster2);\n\n        timeProvider.FireTimer(0);\n\n        timeProvider.AssertTimerDisposed(1);\n        VerifySentProbeAndResult(cluster0, httpClient0, policy0, new[] { (\"https://localhost:20000/cluster0/api/health/\", 2), (\"https://localhost:20001/cluster0/api/health/\", 2) }, policyCallTimes: 2);\n\n        GC.KeepAlive(monitor); // The timer does not keep a strong reference to the scheduler\n    }\n\n    [Fact]\n    public async Task ProbeCluster_UnsuccessfulResponseReceivedOrExceptionThrown_ReportItToPolicy()\n    {\n        var policy = new Mock<IActiveHealthCheckPolicy>();\n        policy.SetupGet(p => p.Name).Returns(\"policy0\");\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) });\n        var clusters = new List<ClusterState>();\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock<TimeProvider>().Object, GetLogger());\n\n        var httpClient = new Mock<HttpMessageInvoker>(() => new HttpMessageInvoker(new Mock<HttpMessageHandler>().Object));\n        httpClient.Setup(c => c.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))\n            .Returns((HttpRequestMessage m, CancellationToken t) => GetResponse(m, t));\n        var cluster = GetClusterInfo(\"cluster0\", \"policy0\", true, httpClient.Object, destinationCount: 3);\n        clusters.Add(cluster);\n\n        Assert.False(monitor.InitialProbeCompleted);\n\n        await monitor.CheckHealthAsync(clusters);\n\n        Assert.True(monitor.InitialProbeCompleted);\n\n        policy.Verify(\n            p => p.ProbingCompleted(\n                cluster,\n                It.Is<IReadOnlyList<DestinationProbingResult>>(\n                    r => r.Count == 3\n                    && r.Single(i => i.Destination.DestinationId == \"destination0\").Response.StatusCode == HttpStatusCode.InternalServerError\n                    && r.Single(i => i.Destination.DestinationId == \"destination0\").Exception == null\n                    && r.Single(i => i.Destination.DestinationId == \"destination1\").Response == null\n                    && r.Single(i => i.Destination.DestinationId == \"destination1\").Exception.GetType() == typeof(InvalidOperationException)\n                    && r.Single(i => i.Destination.DestinationId == \"destination2\").Response.StatusCode == HttpStatusCode.OK\n                    && r.Single(i => i.Destination.DestinationId == \"destination2\").Exception == null)),\n            Times.Once);\n        policy.Verify(p => p.Name);\n        policy.VerifyNoOtherCalls();\n\n        GC.KeepAlive(monitor); // The timer does not keep a strong reference to the scheduler\n\n        async Task<HttpResponseMessage> GetResponse(HttpRequestMessage m, CancellationToken t)\n        {\n            return await Task.Run(() =>\n            {\n                switch (m.RequestUri.AbsoluteUri)\n                {\n                    case \"https://localhost:20000/cluster0/api/health/\":\n                        return new HttpResponseMessage(HttpStatusCode.InternalServerError) { Version = m.Version };\n                    case \"https://localhost:20001/cluster0/api/health/\":\n                        throw new InvalidOperationException();\n                    default:\n                        return new HttpResponseMessage(HttpStatusCode.OK) { Version = m.Version };\n                }\n            });\n        }\n    }\n\n    [Fact]\n    public async Task ForceCheckAll_PolicyThrowsException_SkipItAndSetIsFullyInitializedFlag()\n    {\n        var policy = new Mock<IActiveHealthCheckPolicy>();\n        policy.SetupGet(p => p.Name).Returns(\"policy0\");\n        policy.Setup(p => p.ProbingCompleted(It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationProbingResult>>())).Throws<InvalidOperationException>();\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromSeconds(5) });\n        var clusters = new List<ClusterState>();\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock<TimeProvider>().Object, GetLogger());\n\n        var httpClient = GetHttpClient();\n        var cluster = GetClusterInfo(\"cluster0\", \"policy0\", true, httpClient.Object);\n        clusters.Add(cluster);\n\n        Assert.False(monitor.InitialProbeCompleted);\n\n        await monitor.CheckHealthAsync(clusters);\n\n        Assert.True(monitor.InitialProbeCompleted);\n\n        policy.Verify(p => p.ProbingCompleted(It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationProbingResult>>()), Times.Once);\n        policy.Verify(p => p.Name);\n        policy.VerifyNoOtherCalls();\n    }\n\n    [Theory]\n    [InlineData(HttpStatusCode.OK, HttpStatusCode.OK, HttpStatusCode.OK)]\n    [InlineData(HttpStatusCode.InternalServerError, HttpStatusCode.OK, HttpStatusCode.OK)]\n    [InlineData(HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError)]\n    [InlineData(HttpStatusCode.OK, HttpStatusCode.InternalServerError, HttpStatusCode.OK)]\n    [InlineData(HttpStatusCode.BadRequest, HttpStatusCode.OK, HttpStatusCode.OK)]\n    [InlineData(HttpStatusCode.OK, HttpStatusCode.OK, HttpStatusCode.BadRequest)]\n    public async Task InitialDestinationsProbed_TrueAfterTheFirstProbe_AllReturns(HttpStatusCode firstResult, HttpStatusCode secondResult, HttpStatusCode thirdResult)\n    {\n        var policy = new Mock<IActiveHealthCheckPolicy>();\n        policy.SetupGet(p => p.Name).Returns(\"policy0\");\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = Timeout.InfiniteTimeSpan });\n        var clusters = new List<ClusterState>();\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock<TimeProvider>().Object, GetLogger());\n\n        var tcs0 = new TaskCompletionSource<HttpResponseMessage>();\n        var httpClient0 = GetHttpClient(tcs0.Task);\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\", true, httpClient0.Object, destinationCount: 1);\n        clusters.Add(cluster0);\n        var tcs1 = new TaskCompletionSource<HttpResponseMessage>();\n        var httpClient1 = GetHttpClient(tcs1.Task);\n        var cluster1 = GetClusterInfo(\"cluster1\", \"policy0\", true, httpClient1.Object, destinationCount: 1);\n        clusters.Add(cluster1);\n        var tcs2 = new TaskCompletionSource<HttpResponseMessage>();\n        var httpClient2 = GetHttpClient(tcs2.Task);\n        var cluster2 = GetClusterInfo(\"cluster2\", \"policy0\", true, httpClient2.Object, destinationCount: 1);\n        clusters.Add(cluster2);\n\n        Assert.False(monitor.InitialProbeCompleted);\n\n        var healthCheckTask = monitor.CheckHealthAsync(clusters);\n\n        Assert.False(healthCheckTask.IsCompleted);\n        Assert.False(monitor.InitialProbeCompleted);\n\n        tcs0.SetResult(new HttpResponseMessage(firstResult));\n\n        Assert.False(healthCheckTask.IsCompleted);\n        Assert.False(monitor.InitialProbeCompleted);\n\n        tcs1.SetResult(new HttpResponseMessage(secondResult));\n\n        Assert.False(healthCheckTask.IsCompleted);\n        Assert.False(monitor.InitialProbeCompleted);\n\n        tcs2.SetResult(new HttpResponseMessage(thirdResult));\n\n        await healthCheckTask;\n\n        Assert.True(monitor.InitialProbeCompleted);\n\n        policy.Verify(p => p.ProbingCompleted(It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationProbingResult>>()), Times.Exactly(3));\n        policy.Verify(p => p.Name);\n        policy.VerifyNoOtherCalls();\n    }\n\n    [Fact]\n    public async Task InitialDestinationsProbed_TrueAfterTheFirstProbe_OneTimesOut()\n    {\n        var policy = new Mock<IActiveHealthCheckPolicy>();\n        policy.SetupGet(p => p.Name).Returns(\"policy0\");\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromMilliseconds(1) });\n        var clusters = new List<ClusterState>();\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock<TimeProvider>().Object, GetLogger());\n\n        var assertsCompletedMre = new ManualResetEventSlim(false);\n\n        var tcs0 = new TaskCompletionSource<HttpResponseMessage>(TaskCreationOptions.RunContinuationsAsynchronously);\n        var httpClient0 = GetHttpClient(tcs0.Task);\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\", true, httpClient0.Object, destinationCount: 1);\n        clusters.Add(cluster0);\n        var tcs1 = new TaskCompletionSource<HttpResponseMessage>(TaskCreationOptions.RunContinuationsAsynchronously);\n        var httpClient1 = GetHttpClient(tcs1.Task, () =>\n        {\n            assertsCompletedMre.Wait();\n            tcs1.SetCanceled();\n        });\n        var cluster1 = GetClusterInfo(\"cluster1\", \"policy0\", true, httpClient1.Object, destinationCount: 1);\n        clusters.Add(cluster1);\n\n        Assert.False(monitor.InitialProbeCompleted);\n\n        var healthCheckTask = monitor.CheckHealthAsync(clusters);\n\n        Assert.False(healthCheckTask.IsCompleted);\n        Assert.False(monitor.InitialProbeCompleted);\n\n        tcs0.SetResult(new HttpResponseMessage(HttpStatusCode.OK));\n\n        Assert.False(healthCheckTask.IsCompleted);\n        Assert.False(monitor.InitialProbeCompleted);\n\n        assertsCompletedMre.Set();\n\n        // Never set result to the second destination for it to time out.\n\n        await healthCheckTask;\n\n        Assert.True(monitor.InitialProbeCompleted);\n\n        policy.Verify(p => p.ProbingCompleted(It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationProbingResult>>()), Times.Exactly(2));\n        policy.Verify(p => p.Name);\n        policy.VerifyNoOtherCalls();\n    }\n\n    [Fact]\n    public async Task InitialDestinationsProbed_TrueAfterTheFirstProbe_AllTimeOut()\n    {\n        var policy = new Mock<IActiveHealthCheckPolicy>();\n        policy.SetupGet(p => p.Name).Returns(\"policy0\");\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = TimeSpan.FromMilliseconds(1) });\n        var clusters = new List<ClusterState>();\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock<TimeProvider>().Object, GetLogger());\n\n        var tcs0 = new TaskCompletionSource<HttpResponseMessage>();\n        var httpClient0 = GetHttpClient(tcs0.Task, () => tcs0.SetCanceled());\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\", true, httpClient0.Object, destinationCount: 1);\n        clusters.Add(cluster0);\n        var tcs1 = new TaskCompletionSource<HttpResponseMessage>();\n        var httpClient1 = GetHttpClient(tcs1.Task, () => tcs1.SetCanceled());\n        var cluster1 = GetClusterInfo(\"cluster1\", \"policy0\", true, httpClient1.Object, destinationCount: 1);\n        clusters.Add(cluster1);\n\n        Assert.False(monitor.InitialProbeCompleted);\n\n        var healthCheckTask = monitor.CheckHealthAsync(clusters);\n\n        // Never set results to the either of the destination for them to time out.\n\n        await healthCheckTask;\n\n        Assert.True(monitor.InitialProbeCompleted);\n\n        policy.Verify(p => p.ProbingCompleted(It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationProbingResult>>()), Times.Exactly(2));\n        policy.Verify(p => p.Name);\n        policy.VerifyNoOtherCalls();\n    }\n\n    [Fact]\n    public async Task InitialDestinationsProbed_TrueAfterTheFirstProbe_OneThrows()\n    {\n        var policy = new Mock<IActiveHealthCheckPolicy>();\n        policy.SetupGet(p => p.Name).Returns(\"policy0\");\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = Timeout.InfiniteTimeSpan });\n        var clusters = new List<ClusterState>();\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock<TimeProvider>().Object, GetLogger());\n\n        var tcs0 = new TaskCompletionSource<HttpResponseMessage>();\n        var httpClient0 = GetHttpClient(tcs0.Task);\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\", true, httpClient0.Object, destinationCount: 1);\n        clusters.Add(cluster0);\n        var tcs1 = new TaskCompletionSource<HttpResponseMessage>();\n        var httpClient1 = GetHttpClient(tcs1.Task);\n        var cluster1 = GetClusterInfo(\"cluster1\", \"policy0\", true, httpClient1.Object, destinationCount: 1);\n        clusters.Add(cluster1);\n\n        Assert.False(monitor.InitialProbeCompleted);\n\n        var healthCheckTask = monitor.CheckHealthAsync(clusters);\n\n        Assert.False(healthCheckTask.IsCompleted);\n        Assert.False(monitor.InitialProbeCompleted);\n\n        tcs0.SetException(new Exception());\n\n        Assert.False(healthCheckTask.IsCompleted);\n        Assert.False(monitor.InitialProbeCompleted);\n\n        tcs1.SetResult(new HttpResponseMessage(HttpStatusCode.OK));\n\n        await healthCheckTask;\n\n        Assert.True(monitor.InitialProbeCompleted);\n\n        policy.Verify(p => p.ProbingCompleted(It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationProbingResult>>()), Times.Exactly(2));\n        policy.Verify(p => p.Name);\n        policy.VerifyNoOtherCalls();\n    }\n\n    [Fact]\n    public async Task InitialDestinationsProbed_TrueAfterTheFirstProbe_AllThrow()\n    {\n        var policy = new Mock<IActiveHealthCheckPolicy>();\n        policy.SetupGet(p => p.Name).Returns(\"policy0\");\n        var options = Options.Create(new ActiveHealthCheckMonitorOptions { DefaultInterval = TimeSpan.FromSeconds(60), DefaultTimeout = Timeout.InfiniteTimeSpan });\n        var clusters = new List<ClusterState>();\n        var monitor = new ActiveHealthCheckMonitor(options, new[] { policy.Object }, new DefaultProbingRequestFactory(), new Mock<TimeProvider>().Object, GetLogger());\n\n        var tcs0 = new TaskCompletionSource<HttpResponseMessage>();\n        var httpClient0 = GetHttpClient(tcs0.Task);\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\", true, httpClient0.Object, destinationCount: 1);\n        clusters.Add(cluster0);\n        var tcs1 = new TaskCompletionSource<HttpResponseMessage>();\n        var httpClient1 = GetHttpClient(tcs1.Task);\n        var cluster1 = GetClusterInfo(\"cluster1\", \"policy0\", true, httpClient1.Object, destinationCount: 1);\n        clusters.Add(cluster1);\n\n        Assert.False(monitor.InitialProbeCompleted);\n\n        var healthCheckTask = monitor.CheckHealthAsync(clusters);\n\n        Assert.False(healthCheckTask.IsCompleted);\n        Assert.False(monitor.InitialProbeCompleted);\n\n        tcs0.SetException(new Exception());\n\n        Assert.False(healthCheckTask.IsCompleted);\n        Assert.False(monitor.InitialProbeCompleted);\n\n        tcs1.SetException(new Exception());\n\n        await healthCheckTask;\n\n        Assert.True(monitor.InitialProbeCompleted);\n\n        policy.Verify(p => p.ProbingCompleted(It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationProbingResult>>()), Times.Exactly(2));\n        policy.Verify(p => p.Name);\n        policy.VerifyNoOtherCalls();\n    }\n\n    private static void VerifySentProbeAndResult(ClusterState cluster, Mock<HttpMessageInvoker> httpClient, Mock<IActiveHealthCheckPolicy> policy, (string RequestUri, int Times)[] probes, int policyCallTimes = 1, string userAgent = @\"^YARP\\/\\S+? \\([^\\)]+?\\)$\")\n    {\n        foreach (var probe in probes)\n        {\n            httpClient.Verify(\n                c => c.SendAsync(\n                    It.Is<HttpRequestMessage>(m => m.RequestUri.AbsoluteUri == probe.RequestUri && Regex.IsMatch(m.Headers.UserAgent.ToString(), userAgent)),\n                    It.IsAny<CancellationToken>()),\n                Times.Exactly(probe.Times));\n        }\n        httpClient.VerifyNoOtherCalls();\n        policy.Verify(\n            p => p.ProbingCompleted(\n                cluster,\n                It.Is<IReadOnlyList<DestinationProbingResult>>(r => cluster.Destinations.Values.All(d => r.Any(i => i.Destination == d && i.Response.StatusCode == HttpStatusCode.OK)))),\n            Times.Exactly(policyCallTimes));\n        policy.Verify(p => p.Name);\n        policy.VerifyNoOtherCalls();\n    }\n\n    private ClusterState GetClusterInfo(string id, string policy, bool activeCheckEnabled, HttpMessageInvoker httpClient, TimeSpan? interval = null, TimeSpan? timeout = null, int destinationCount = 2)\n    {\n        var clusterModel = new ClusterModel(\n            new ClusterConfig\n            {\n                ClusterId = id,\n                HealthCheck = new HealthCheckConfig\n                {\n                    Active = new ActiveHealthCheckConfig\n                    {\n                        Enabled = activeCheckEnabled,\n                        Interval = interval,\n                        Timeout = timeout,\n                        Policy = policy,\n                        Path = \"/api/health/\",\n                    }\n                }\n            },\n            httpClient);\n        var clusterState = new ClusterState(id);\n        clusterState.Model = clusterModel;\n        for (var i = 0; i < destinationCount; i++)\n        {\n            var destinationModel = new DestinationModel(new DestinationConfig { Address = $\"https://localhost:1000{i}/{id}/\", Health = $\"https://localhost:2000{i}/{id}/\" });\n            var destinationId = $\"destination{i}\";\n            clusterState.Destinations.GetOrAdd(destinationId, id => new DestinationState(id)\n            {\n                Model = destinationModel\n            });\n        }\n        clusterState.DestinationsState = new ClusterDestinationsState(clusterState.Destinations.Values.ToList(), clusterState.Destinations.Values.ToList());\n\n        return clusterState;\n    }\n\n    private Mock<HttpMessageInvoker> GetHttpClient(Task<HttpResponseMessage> task = null, Action cancellation = null)\n    {\n        var httpClient = new Mock<HttpMessageInvoker>(() => new HttpMessageInvoker(new Mock<HttpMessageHandler>().Object));\n        httpClient.Setup(c => c.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))\n            .Returns((HttpRequestMessage m, CancellationToken c) =>\n            {\n                if (cancellation is not null)\n                {\n                    c.Register(_ => cancellation(), null);\n                }\n\n                return task ?? Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Version = m.Version });\n            });\n        return httpClient;\n    }\n\n    private static ILogger<ActiveHealthCheckMonitor> GetLogger()\n    {\n        return new Mock<ILogger<ActiveHealthCheckMonitor>>().Object;\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Health/ClusterDestinationsUpdaterTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing Moq;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health.Tests;\n\npublic class ClusterDestinationsUpdaterTests\n{\n    [Fact]\n    public void UpdateAllDestinations_UseDestinationsCollectionAsSource()\n    {\n        var cluster = GetCluster(\"policy1\");\n        var destination0 = cluster.Destinations.GetOrAdd(\"d0\", id => new DestinationState(id));\n        var destination1 = cluster.Destinations.GetOrAdd(\"d1\", id => new DestinationState(id));\n        var destination2 = cluster.Destinations.GetOrAdd(\"d2\", id => new DestinationState(id));\n        var expectedAll = new[] { destination0, destination1, destination2 };\n        var expectedAvailable = new[] { destination0, destination2 };\n        var policy0 = new StubPolicy(\"policy0\", destination1);\n        var policy1 = new StubPolicy(\"policy1\", destination1);\n        var updater = new ClusterDestinationsUpdater(new[] { policy0, policy1 });\n\n        updater.UpdateAllDestinations(cluster);\n\n        AssertEquals(expectedAll, cluster.DestinationsState.AllDestinations);\n        AssertEquals(expectedAvailable, cluster.DestinationsState.AvailableDestinations);\n\n        Assert.False(policy0.IsCalled);\n        Assert.Null(policy0.TakenDestinations);\n        Assert.True(policy1.IsCalled);\n        AssertEquals(expectedAll, policy1.TakenDestinations);\n    }\n\n    [Fact]\n    public void UpdateAvailableDestinations_UseAllDestinationsAsSource()\n    {\n        var cluster = GetCluster(\"policy1\");\n        var allDestinations = new[] { new DestinationState(\"d0\"), new DestinationState(\"d1\"), new DestinationState(\"d2\") };\n        cluster.DestinationsState = new ClusterDestinationsState(allDestinations, new[] { allDestinations[0], allDestinations[1] });\n        var expectedAvailable = new[] { allDestinations[0], allDestinations[2] };\n        var policy0 = new StubPolicy(\"policy0\", allDestinations[1]);\n        var policy1 = new StubPolicy(\"policy1\", allDestinations[1]);\n        var updater = new ClusterDestinationsUpdater(new[] { policy0, policy1 });\n\n        updater.UpdateAvailableDestinations(cluster);\n\n        Assert.Empty(cluster.Destinations);\n        AssertEquals(allDestinations, cluster.DestinationsState.AllDestinations);\n        AssertEquals(expectedAvailable, cluster.DestinationsState.AvailableDestinations);\n\n        Assert.False(policy0.IsCalled);\n        Assert.Null(policy0.TakenDestinations);\n        Assert.True(policy1.IsCalled);\n        AssertEquals(allDestinations, policy1.TakenDestinations);\n    }\n\n    private static void AssertEquals(IEnumerable<DestinationState> actual, IEnumerable<DestinationState> expected)\n    {\n        Assert.Equal(actual.OrderBy(d => d.DestinationId).Select(d => d.DestinationId), expected.OrderBy(d => d.DestinationId).Select(d => d.DestinationId));\n    }\n\n    private static ClusterState GetCluster(string policyName)\n    {\n        var cluster = new ClusterState(\"cluster1\")\n        {\n            Model = new ClusterModel(\n                new ClusterConfig\n                {\n                    ClusterId = \"cluster1\",\n                    HealthCheck = new HealthCheckConfig { AvailableDestinationsPolicy = policyName }\n                },\n            httpClient: new HttpMessageInvoker(new Mock<HttpMessageHandler>().Object))\n        };\n\n        return cluster;\n    }\n\n    private class StubPolicy : IAvailableDestinationsPolicy\n    {\n        private readonly DestinationState _skipDestination;\n\n        public bool IsCalled { get; private set; }\n\n        public IReadOnlyList<DestinationState> TakenDestinations { get; private set; }\n\n        public StubPolicy(string name, DestinationState skipDestination)\n        {\n            Name = name;\n            _skipDestination = skipDestination;\n        }\n\n        public string Name { get; }\n\n        public IReadOnlyList<DestinationState> GetAvailableDestinations(ClusterConfig config, IReadOnlyList<DestinationState> allDestinations)\n        {\n            IsCalled = true;\n            TakenDestinations = allDestinations;\n            return allDestinations.Where(p => p != _skipDestination).ToArray();\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Health/ConsecutiveFailuresHealthPolicyTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing Microsoft.Extensions.Options;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health.Tests;\n\npublic class ConsecutiveFailuresHealthPolicyTests\n{\n    [Fact]\n    public void ProbingCompleted_FailureThresholdExceeded_MarkDestinationUnhealthy()\n    {\n        var options = Options.Create(new ConsecutiveFailuresHealthPolicyOptions { DefaultThreshold = 2 });\n        var policy = new ConsecutiveFailuresHealthPolicy(options, new DestinationHealthUpdaterStub());\n        var cluster0 = GetClusterInfo(\"cluster0\", destinationCount: 2);\n        var cluster1 = GetClusterInfo(\"cluster1\", destinationCount: 2, failureThreshold: 3);\n\n        var probingResults0 = new[] {\n            new DestinationProbingResult(cluster0.Destinations.Values.First(), new HttpResponseMessage(HttpStatusCode.InternalServerError), null),\n            new DestinationProbingResult(cluster0.Destinations.Values.Skip(1).First(), new HttpResponseMessage(HttpStatusCode.OK), null)\n        };\n        var probingResults1 = new[] {\n            new DestinationProbingResult(cluster1.Destinations.Values.First(), new HttpResponseMessage(HttpStatusCode.OK), null),\n            new DestinationProbingResult(cluster1.Destinations.Values.Skip(1).First(), null, new InvalidOperationException())\n        };\n\n        Assert.Equal(HealthCheckConstants.ActivePolicy.ConsecutiveFailures, policy.Name);\n\n        // Initial state\n        Assert.All(cluster0.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Active));\n        Assert.All(cluster1.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Active));\n\n        // First probing attempt\n        policy.ProbingCompleted(cluster0, probingResults0);\n        Assert.Equal(DestinationHealth.Unknown, cluster0.Destinations.Values.First().Health.Active);\n        Assert.Equal(DestinationHealth.Healthy, cluster0.Destinations.Values.Skip(1).First().Health.Active);\n        policy.ProbingCompleted(cluster1, probingResults1);\n        Assert.Equal(DestinationHealth.Healthy, cluster1.Destinations.Values.First().Health.Active);\n        Assert.Equal(DestinationHealth.Unknown, cluster1.Destinations.Values.Skip(1).First().Health.Active);\n\n        // Second probing attempt\n        policy.ProbingCompleted(cluster0, probingResults0);\n        Assert.Equal(DestinationHealth.Unhealthy, cluster0.Destinations.Values.First().Health.Active);\n        Assert.Equal(DestinationHealth.Healthy, cluster0.Destinations.Values.Skip(1).First().Health.Active);\n        policy.ProbingCompleted(cluster1, probingResults1);\n        Assert.Equal(DestinationHealth.Healthy, cluster1.Destinations.Values.First().Health.Active);\n        Assert.Equal(DestinationHealth.Unknown, cluster1.Destinations.Values.Skip(1).First().Health.Active);\n\n        // Third probing attempt\n        policy.ProbingCompleted(cluster0, probingResults0);\n        Assert.Equal(DestinationHealth.Unhealthy, cluster0.Destinations.Values.First().Health.Active);\n        Assert.Equal(DestinationHealth.Healthy, cluster0.Destinations.Values.Skip(1).First().Health.Active);\n        policy.ProbingCompleted(cluster1, probingResults1);\n        Assert.Equal(DestinationHealth.Healthy, cluster1.Destinations.Values.First().Health.Active);\n        Assert.Equal(DestinationHealth.Unhealthy, cluster1.Destinations.Values.Skip(1).First().Health.Active);\n\n        Assert.All(cluster0.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Passive));\n        Assert.All(cluster1.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Passive));\n    }\n\n    [Fact]\n    public void ProbingCompleted_SuccessfulResponse_MarkDestinationHealthy()\n    {\n        var options = Options.Create(new ConsecutiveFailuresHealthPolicyOptions { DefaultThreshold = 2 });\n        var policy = new ConsecutiveFailuresHealthPolicy(options, new DestinationHealthUpdaterStub());\n        var cluster = GetClusterInfo(\"cluster0\", destinationCount: 2);\n\n        var probingResults = new[] {\n            new DestinationProbingResult(cluster.Destinations.Values.First(), new HttpResponseMessage(HttpStatusCode.InternalServerError), null),\n            new DestinationProbingResult(cluster.Destinations.Values.Skip(1).First(), new HttpResponseMessage(HttpStatusCode.OK), null)\n        };\n\n        for (var i = 0; i < 2; i++)\n        {\n            policy.ProbingCompleted(cluster, probingResults);\n        }\n\n        Assert.Equal(DestinationHealth.Unhealthy, cluster.Destinations.Values.First().Health.Active);\n        Assert.Equal(DestinationHealth.Healthy, cluster.Destinations.Values.Skip(1).First().Health.Active);\n\n        policy.ProbingCompleted(cluster, new[] { new DestinationProbingResult(cluster.Destinations.Values.First(), new HttpResponseMessage(HttpStatusCode.OK), null) });\n\n        Assert.Equal(DestinationHealth.Healthy, cluster.Destinations.Values.First().Health.Active);\n        Assert.Equal(DestinationHealth.Healthy, cluster.Destinations.Values.Skip(1).First().Health.Active);\n\n        Assert.All(cluster.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Passive));\n    }\n\n    [Fact]\n    public void ProbingCompleted_EmptyProbingResultList_DoNothing()\n    {\n        var options = Options.Create(new ConsecutiveFailuresHealthPolicyOptions { DefaultThreshold = 2 });\n        var policy = new ConsecutiveFailuresHealthPolicy(options, new DestinationHealthUpdaterStub());\n        var cluster = GetClusterInfo(\"cluster0\", destinationCount: 2);\n\n        var probingResults = new[] {\n            new DestinationProbingResult(cluster.Destinations.Values.First(), new HttpResponseMessage(HttpStatusCode.InternalServerError), null),\n            new DestinationProbingResult(cluster.Destinations.Values.Skip(1).First(), new HttpResponseMessage(HttpStatusCode.OK), null)\n        };\n\n        for (var i = 0; i < 2; i++)\n        {\n            policy.ProbingCompleted(cluster, probingResults);\n        }\n\n        Assert.Equal(DestinationHealth.Unhealthy, cluster.Destinations.Values.First().Health.Active);\n        Assert.Equal(DestinationHealth.Healthy, cluster.Destinations.Values.Skip(1).First().Health.Active);\n\n        policy.ProbingCompleted(cluster, Array.Empty<DestinationProbingResult>());\n\n        Assert.Equal(DestinationHealth.Unhealthy, cluster.Destinations.Values.First().Health.Active);\n        Assert.Equal(DestinationHealth.Healthy, cluster.Destinations.Values.Skip(1).First().Health.Active);\n    }\n\n    private ClusterState GetClusterInfo(string id, int destinationCount, int? failureThreshold = null)\n    {\n        var metadata = failureThreshold is not null\n            ? new Dictionary<string, string> { { ConsecutiveFailuresHealthPolicyOptions.ThresholdMetadataName, failureThreshold.ToString() } }\n            : null;\n        var clusterModel = new ClusterModel(\n            new ClusterConfig\n            {\n                ClusterId = id,\n                HealthCheck = new HealthCheckConfig()\n                {\n                    Active = new ActiveHealthCheckConfig\n                    {\n                        Enabled = true,\n                        Policy = \"policy\",\n                        Path = \"/api/health/\",\n                    },\n                },\n                Metadata = metadata,\n            },\n            new HttpMessageInvoker(new HttpClientHandler()));\n        var clusterState = new ClusterState(id);\n        clusterState.Model = clusterModel;\n        for (var i = 0; i < destinationCount; i++)\n        {\n            var destinationModel = new DestinationModel(new DestinationConfig { Address = $\"https://localhost:1000{i}/{id}/\", Health = $\"https://localhost:2000{i}/{id}/\" });\n            var destinationId = $\"destination{i}\";\n            clusterState.Destinations.GetOrAdd(destinationId, id => new DestinationState(id)\n            {\n                Model = destinationModel\n            });\n        }\n\n        clusterState.DestinationsState = new ClusterDestinationsState(clusterState.Destinations.Values.ToList(), clusterState.Destinations.Values.ToList());\n\n        return clusterState;\n    }\n\n    private class DestinationHealthUpdaterStub : IDestinationHealthUpdater\n    {\n        public void SetActive(ClusterState cluster, IEnumerable<NewActiveDestinationHealth> newHealthStates)\n        {\n            foreach (var newHealthState in newHealthStates)\n            {\n                newHealthState.Destination.Health.Active = newHealthState.NewActiveHealth;\n            }\n\n            var destinations = cluster.Destinations.Values.ToList();\n            cluster.DestinationsState = new ClusterDestinationsState(destinations, destinations);\n        }\n\n        public void SetPassive(ClusterState cluster, DestinationState destination, DestinationHealth newHealth, TimeSpan reactivationPeriod)\n        {\n            throw new NotImplementedException();\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Health/DefaultProbingRequestFactoryTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net;\nusing System.Net.Http;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Health.Tests;\n\npublic class DefaultProbingRequestFactoryTests\n{\n    [Theory]\n    [InlineData(\"https://localhost:10000/\", null, null, null, \"https://localhost:10000/\")]\n    [InlineData(\"https://localhost:10000/\", \"https://localhost:20000/\", null, null, \"https://localhost:20000/\")]\n    [InlineData(\"https://localhost:10000/\", null, \"/api/health/\", null, \"https://localhost:10000/api/health/\")]\n    [InlineData(\"https://localhost:10000/\", \"https://localhost:20000/\", \"/api/health/\", null, \"https://localhost:20000/api/health/\")]\n    [InlineData(\"https://localhost:10000/api\", \"https://localhost:20000/\", \"/health/\", null, \"https://localhost:20000/health/\")]\n    [InlineData(\"https://localhost:10000/\", \"https://localhost:20000/api\", \"/health/\", null, \"https://localhost:20000/api/health/\")]\n    [InlineData(\"https://localhost:10000/\", null, null, \"?key=value\", \"https://localhost:10000/?key=value\")]\n    [InlineData(\"https://localhost:10000/\", \"https://localhost:20000/\", null, \"?key=value\", \"https://localhost:20000/?key=value\")]\n    [InlineData(\"https://localhost:10000/\", null, \"/api/health/\", \"?key=value\", \"https://localhost:10000/api/health/?key=value\")]\n    [InlineData(\"https://localhost:10000/\", \"https://localhost:20000/\", \"/api/health/\", \"?key=value\", \"https://localhost:20000/api/health/?key=value\")]\n    [InlineData(\"https://localhost:10000/api\", \"https://localhost:20000/\", \"/health/\", \"?key=value\", \"https://localhost:20000/health/?key=value\")]\n    [InlineData(\"https://localhost:10000/\", \"https://localhost:20000/api\", \"/health/\", \"?key=value\", \"https://localhost:20000/api/health/?key=value\")]\n    [InlineData(\"https://localhost:10000/\", \"https://localhost:20000/api\", \"/health?foo=bar\", \"?key=value\", \"https://localhost:20000/api/health%3Ffoo=bar?key=value\")]\n    public void CreateRequest_HealthEndpointIsNotDefined_UseDestinationAddress(string address, string health, string healthPath, string query, string expectedRequestUri)\n    {\n        var clusterModel = GetClusterConfig(\"cluster0\",\n            new ActiveHealthCheckConfig()\n            {\n                Enabled = true,\n                Policy = \"policy\",\n                Path = healthPath,\n                Query = query,\n            }, HttpVersion.Version20);\n        var destinationModel = new DestinationModel(new DestinationConfig { Address = address, Health = health });\n        var factory = new DefaultProbingRequestFactory();\n\n        var request = factory.CreateRequest(clusterModel, destinationModel);\n\n        Assert.Equal(expectedRequestUri, request.RequestUri.AbsoluteUri);\n    }\n\n    [Theory]\n    [InlineData(\"1.0\")]\n    [InlineData(null)]\n    public void CreateRequest_RequestVersionProperties(string versionString)\n    {\n        var version = versionString is not null ? Version.Parse(versionString) : null;\n        var clusterModel = GetClusterConfig(\"cluster0\",\n            new ActiveHealthCheckConfig()\n            {\n                Enabled = true,\n                Policy = \"policy\",\n            }, version, HttpVersionPolicy.RequestVersionExact);\n        var destinationModel = new DestinationModel(new DestinationConfig { Address = \"https://localhost:10000/\" });\n        var factory = new DefaultProbingRequestFactory();\n\n        var request = factory.CreateRequest(clusterModel, destinationModel);\n\n        Assert.Equal(version ?? HttpVersion.Version20, request.Version);\n        Assert.Equal(HttpVersionPolicy.RequestVersionExact, request.VersionPolicy);\n    }\n\n    private ClusterModel GetClusterConfig(string id, ActiveHealthCheckConfig healthCheckOptions, Version version, HttpVersionPolicy versionPolicy = HttpVersionPolicy.RequestVersionExact)\n    {\n        return new ClusterModel(\n            new ClusterConfig\n            {\n                ClusterId = id,\n                HealthCheck = new HealthCheckConfig()\n                {\n                    Active = healthCheckOptions,\n                },\n                HttpRequest = new ForwarderRequestConfig\n                {\n                    ActivityTimeout = TimeSpan.FromSeconds(60),\n                    Version = version,\n                    VersionPolicy = versionPolicy,\n                }\n            },\n            new HttpMessageInvoker(new HttpClientHandler()));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Health/DestinationHealthUpdaterTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Logging;\nusing Moq;\nusing Xunit;\nusing Yarp.Tests.Common;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health.Tests;\n\npublic class DestinationHealthUpdaterTests\n{\n    [Fact]\n    public async Task SetPassiveAsync_DestinationBecameUnhealthy_SetUnhealthyAndScheduleReactivation()\n    {\n        var destination = new DestinationState(\"destination0\");\n        destination.Health.Active = DestinationHealth.Healthy;\n        destination.Health.Passive = DestinationHealth.Healthy;\n        var cluster = CreateCluster(passive: true, active: false, destination);\n        var timeProvider = new TestTimeProvider();\n        var updater = new DestinationHealthUpdater(timeProvider, GetClusterUpdater(), new Mock<ILogger<DestinationHealthUpdater>>().Object);\n\n        await updater.SetPassiveAsync(cluster, destination, DestinationHealth.Unhealthy, TimeSpan.FromSeconds(2));\n\n        timeProvider.VerifyTimer(0, TimeSpan.FromSeconds(2));\n        Assert.Empty(cluster.DestinationsState.AvailableDestinations);\n        Assert.Equal(DestinationHealth.Healthy, destination.Health.Active);\n        Assert.Equal(DestinationHealth.Unhealthy, destination.Health.Passive);\n\n        timeProvider.FireAllTimers();\n        GC.KeepAlive(updater); // The timer does not keep a strong reference to the scheduler\n\n        Assert.Equal(DestinationHealth.Healthy, destination.Health.Active);\n        Assert.Equal(DestinationHealth.Unknown, destination.Health.Passive);\n        Assert.Single(cluster.DestinationsState.AvailableDestinations);\n        Assert.Same(destination, cluster.DestinationsState.AvailableDestinations[0]);\n        timeProvider.AssertTimerDisposed(0);\n    }\n\n    [Fact]\n    public async Task SetPassiveAsync_DestinationBecameHealthy_SetNewState()\n    {\n        var destination = new DestinationState(\"destination0\");\n        destination.Health.Active = DestinationHealth.Healthy;\n        destination.Health.Passive = DestinationHealth.Unhealthy;\n        var cluster = CreateCluster(passive: true, active: false, destination);\n        var timeProvider = new TestTimeProvider();\n        var updater = new DestinationHealthUpdater(timeProvider, GetClusterUpdater(), new Mock<ILogger<DestinationHealthUpdater>>().Object);\n\n        await updater.SetPassiveAsync(cluster, destination, DestinationHealth.Healthy, TimeSpan.FromSeconds(2));\n\n        Assert.Equal(0, timeProvider.TimerCount);\n        Assert.Equal(DestinationHealth.Healthy, destination.Health.Active);\n        Assert.Equal(DestinationHealth.Healthy, destination.Health.Passive);\n        Assert.Single(cluster.DestinationsState.AvailableDestinations);\n        Assert.Same(destination, cluster.DestinationsState.AvailableDestinations[0]);\n    }\n\n    [Theory]\n    [InlineData(DestinationHealth.Unhealthy)]\n    [InlineData(DestinationHealth.Healthy)]\n    [InlineData(DestinationHealth.Unknown)]\n    public async Task SetPassiveAsync_HealthSateIsNotChanged_DoNothing(DestinationHealth health)\n    {\n        var destination = new DestinationState(\"destination0\");\n        destination.Health.Active = DestinationHealth.Healthy;\n        destination.Health.Passive = health;\n        var cluster = CreateCluster(passive: true, active: false, destination);\n        var timeProvider = new TestTimeProvider();\n        var updater = new DestinationHealthUpdater(timeProvider, GetClusterUpdater(), new Mock<ILogger<DestinationHealthUpdater>>().Object);\n\n        await updater.SetPassiveAsync(cluster, destination, health, TimeSpan.FromSeconds(2));\n\n        Assert.Equal(0, timeProvider.TimerCount);\n        Assert.Equal(DestinationHealth.Healthy, destination.Health.Active);\n        Assert.Equal(health, destination.Health.Passive);\n    }\n\n    [Fact]\n    public void SetActive_ChangedAndUnchangedHealthStates_SetChangedStates()\n    {\n        var destination0 = new DestinationState(\"destination0\");\n        destination0.Health.Active = DestinationHealth.Healthy;\n        destination0.Health.Passive = DestinationHealth.Healthy;\n        var destination1 = new DestinationState(\"destination1\");\n        destination1.Health.Active = DestinationHealth.Healthy;\n        destination1.Health.Passive = DestinationHealth.Healthy;\n        var destination2 = new DestinationState(\"destination2\");\n        destination2.Health.Active = DestinationHealth.Unhealthy;\n        destination2.Health.Passive = DestinationHealth.Healthy;\n        var destination3 = new DestinationState(\"destination3\");\n        destination3.Health.Active = DestinationHealth.Unhealthy;\n        destination3.Health.Passive = DestinationHealth.Healthy;\n        var cluster = CreateCluster(passive: false, active: true, destination0, destination1, destination2, destination3);\n        var updater = new DestinationHealthUpdater(new Mock<TimeProvider>().Object, GetClusterUpdater(), new Mock<ILogger<DestinationHealthUpdater>>().Object);\n\n        var newHealthStates = new[] {\n            new NewActiveDestinationHealth(destination0, DestinationHealth.Unhealthy), new NewActiveDestinationHealth(destination1, DestinationHealth.Healthy),\n            new NewActiveDestinationHealth(destination2, DestinationHealth.Unhealthy), new NewActiveDestinationHealth(destination3, DestinationHealth.Healthy)\n        };\n        updater.SetActive(cluster, newHealthStates);\n\n        foreach (var newHealthState in newHealthStates)\n        {\n            Assert.Equal(newHealthState.NewActiveHealth, newHealthState.Destination.Health.Active);\n            Assert.Equal(DestinationHealth.Healthy, newHealthState.Destination.Health.Passive);\n        }\n\n        Assert.Equal(2, cluster.DestinationsState.AvailableDestinations.Count);\n        Assert.Contains(cluster.DestinationsState.AvailableDestinations, d => d == destination1);\n        Assert.Contains(cluster.DestinationsState.AvailableDestinations, d => d == destination3);\n    }\n\n    private static ClusterState CreateCluster(bool passive, bool active, params DestinationState[] destinations)\n    {\n        var cluster = new ClusterState(\"cluster0\");\n        cluster.Model = new ClusterModel(\n            new ClusterConfig\n            {\n                ClusterId = cluster.ClusterId,\n                HealthCheck = new HealthCheckConfig()\n                {\n                    Passive = new PassiveHealthCheckConfig()\n                    {\n                        Policy = \"policy0\",\n                        Enabled = passive,\n                    },\n                    Active = new ActiveHealthCheckConfig()\n                    {\n                        Enabled = active,\n                        Policy = \"policy1\",\n                    },\n                },\n            },\n            new HttpMessageInvoker(new HttpClientHandler()));\n\n        foreach (var destination in destinations)\n        {\n            cluster.Destinations.TryAdd(destination.DestinationId, destination);\n        }\n\n        cluster.DestinationsState = new ClusterDestinationsState(destinations, destinations);\n\n        return cluster;\n    }\n\n    private IClusterDestinationsUpdater GetClusterUpdater()\n    {\n        var result = new Mock<IClusterDestinationsUpdater>(MockBehavior.Strict);\n        result.Setup(u => u.UpdateAvailableDestinations(It.IsAny<ClusterState>())).Callback((ClusterState c) =>\n        {\n            var availableDestinations = c.Destinations.Values\n                .Where(d => d.Health.Active != DestinationHealth.Unhealthy && d.Health.Passive != DestinationHealth.Unhealthy)\n                .ToList();\n            c.DestinationsState = new ClusterDestinationsState(c.DestinationsState.AllDestinations, availableDestinations);\n        });\n        return result.Object;\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Health/EntityActionSchedulerTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Health.Tests;\n\n// It uses a real TimerFactory to verify scheduling work E2E.\npublic class EntityActionSchedulerTests\n{\n    private readonly TimeSpan Period0 = TimeSpan.FromSeconds(20);\n    private readonly TimeSpan Period1 = TimeSpan.FromSeconds(10);\n\n    [Fact]\n    public void Schedule_AutoStartEnabledRunOnceDisabled_StartsAutomaticallyAndRunsIndefinitely()\n    {\n        var entity0 = new Entity { Id = \"entity0\" };\n        var entity1 = new Entity { Id = \"entity1\" };\n        var timeProvider = new TestTimeProvider();\n        Entity lastInvokedEntity = null;\n        using var scheduler = new EntityActionScheduler<Entity>(e =>\n        {\n            lastInvokedEntity = e;\n            return Task.CompletedTask;\n        }, autoStart: true, runOnce: false, timeProvider);\n\n        scheduler.ScheduleEntity(entity0, TimeSpan.FromMilliseconds(20000));\n        scheduler.ScheduleEntity(entity1, TimeSpan.FromMilliseconds(10000));\n\n        VerifyEntities(scheduler, entity0, entity1);\n        Assert.Equal(2, timeProvider.TimerCount);\n        timeProvider.VerifyTimer(0, Period0);\n        timeProvider.VerifyTimer(1, Period1);\n\n        timeProvider.FireTimer(1);\n        Assert.Same(entity1, lastInvokedEntity);\n\n        timeProvider.FireTimer(0);\n        Assert.Same(entity0, lastInvokedEntity);\n\n        timeProvider.FireTimer(1);\n        Assert.Same(entity1, lastInvokedEntity);\n\n        timeProvider.FireTimer(0);\n        Assert.Same(entity0, lastInvokedEntity);\n\n        VerifyEntities(scheduler, entity0, entity1);\n        Assert.Equal(2, timeProvider.TimerCount);\n        timeProvider.VerifyTimer(0, Period0);\n        timeProvider.VerifyTimer(1, Period1);\n    }\n\n    [Fact]\n    public void Schedule_AutoStartDisabledRunOnceEnabled_StartsManuallyAndRunsEachRegistrationOnlyOnce()\n    {\n        var entity0 = new Entity { Id = \"entity0\" };\n        var entity1 = new Entity { Id = \"entity1\" };\n        Entity lastInvokedEntity = null;\n        var timeProvider = new TestTimeProvider();\n        using var scheduler = new EntityActionScheduler<Entity>(e =>\n        {\n            lastInvokedEntity = e;\n            return Task.CompletedTask;\n        }, autoStart: false, runOnce: true, timeProvider);\n\n        scheduler.ScheduleEntity(entity0, Period0);\n        scheduler.ScheduleEntity(entity1, Period1);\n        Assert.Equal(2, timeProvider.TimerCount);\n        timeProvider.VerifyTimer(0, Timeout.InfiniteTimeSpan);\n        timeProvider.VerifyTimer(1, Timeout.InfiniteTimeSpan);\n\n        scheduler.Start();\n\n        VerifyEntities(scheduler, entity0, entity1);\n        Assert.Equal(2, timeProvider.TimerCount);\n        timeProvider.VerifyTimer(0, Period0);\n        timeProvider.VerifyTimer(1, Period1);\n\n        timeProvider.FireTimer(1);\n\n        Assert.Same(entity1, lastInvokedEntity);\n\n        VerifyEntities(scheduler, entity0);\n\n        timeProvider.FireTimer(0);\n\n        Assert.Same(entity0, lastInvokedEntity);\n\n        Assert.False(scheduler.IsScheduled(entity0));\n        Assert.False(scheduler.IsScheduled(entity1));\n        timeProvider.AssertTimerDisposed(0);\n        timeProvider.AssertTimerDisposed(1);\n    }\n\n    [Fact]\n    public void Unschedule_EntityUnscheduledBeforeFirstCall_CallbackNotInvoked()\n    {\n        var entity0 = new Entity { Id = \"entity0\" };\n        var entity1 = new Entity { Id = \"entity1\" };\n        Entity lastInvokedEntity = null;\n        var timeProvider = new TestTimeProvider();\n        using var scheduler = new EntityActionScheduler<Entity>(e =>\n        {\n            lastInvokedEntity = e;\n            return Task.CompletedTask;\n        }, autoStart: false, runOnce: false, timeProvider);\n\n        scheduler.ScheduleEntity(entity0, Period0);\n        scheduler.ScheduleEntity(entity1, Period1);\n\n        VerifyEntities(scheduler, entity0, entity1);\n        Assert.Equal(2, timeProvider.TimerCount);\n        timeProvider.VerifyTimer(0, Timeout.InfiniteTimeSpan);\n        timeProvider.VerifyTimer(1, Timeout.InfiniteTimeSpan);\n\n        scheduler.UnscheduleEntity(entity1);\n        VerifyEntities(scheduler, entity0);\n        timeProvider.AssertTimerDisposed(1);\n\n        scheduler.Start();\n\n        timeProvider.VerifyTimer(0, Period0);\n\n        timeProvider.FireTimer(0);\n\n        Assert.Same(entity0, lastInvokedEntity);\n\n        VerifyEntities(scheduler, entity0);\n    }\n\n    [Fact]\n    public void Unschedule_EntityUnscheduledAfterFirstCall_CallbackInvokedOnlyOnce()\n    {\n        var entity0 = new Entity { Id = \"entity0\" };\n        var entity1 = new Entity { Id = \"entity1\" };\n        Entity lastInvokedEntity = null;\n        var timeProvider = new TestTimeProvider();\n        using var scheduler = new EntityActionScheduler<Entity>(e =>\n        {\n            lastInvokedEntity = e;\n            return Task.CompletedTask;\n        }, autoStart: true, runOnce: false, timeProvider);\n\n        scheduler.ScheduleEntity(entity0, Period0);\n        scheduler.ScheduleEntity(entity1, Period1);\n\n        VerifyEntities(scheduler, entity0, entity1);\n\n        timeProvider.FireTimer(1);\n\n        Assert.Same(entity1, lastInvokedEntity);\n\n        timeProvider.FireTimer(0);\n\n        Assert.Same(entity0, lastInvokedEntity);\n\n        scheduler.UnscheduleEntity(entity1);\n        VerifyEntities(scheduler, entity0);\n        timeProvider.AssertTimerDisposed(1);\n\n        timeProvider.FireTimer(0);\n\n        Assert.Same(entity0, lastInvokedEntity);\n\n        VerifyEntities(scheduler, entity0);\n    }\n\n    [Fact]\n    public void ChangePeriod_PeriodChangedTimerNotStarted_PeriodChangedBeforeFirstCall()\n    {\n        var entity = new Entity { Id = \"entity0\" };\n        Entity lastInvokedEntity = null;\n        var timeProvider = new TestTimeProvider();\n        using var scheduler = new EntityActionScheduler<Entity>(e =>\n        {\n            lastInvokedEntity = e;\n            return Task.CompletedTask;\n        }, autoStart: false, runOnce: false, timeProvider);\n\n        scheduler.ScheduleEntity(entity, Period0);\n        timeProvider.VerifyTimer(0, Timeout.InfiniteTimeSpan);\n\n        var newPeriod = Period1;\n        scheduler.ChangePeriod(entity, newPeriod);\n        timeProvider.VerifyTimer(0, Timeout.InfiniteTimeSpan);\n\n        scheduler.Start();\n\n        timeProvider.VerifyTimer(0, Period1);\n\n        timeProvider.FireTimer(0);\n\n        Assert.Same(entity, lastInvokedEntity);\n    }\n\n    [Fact]\n    public void ChangePeriod_TimerStartedPeriodChangedAfterFirstCall_PeriodChangedBeforeNextCall()\n    {\n        var entity = new Entity { Id = \"entity0\" };\n        Entity lastInvokedEntity = null;\n        var timeProvider = new TestTimeProvider();\n        using var scheduler = new EntityActionScheduler<Entity>(e =>\n        {\n            lastInvokedEntity = e;\n            return Task.CompletedTask;\n        }, autoStart: true, runOnce: false, timeProvider);\n\n        scheduler.ScheduleEntity(entity, Period0);\n        timeProvider.VerifyTimer(0, Period0);\n\n        timeProvider.FireTimer(0);\n\n        var newPeriod = Period1;\n        scheduler.ChangePeriod(entity, newPeriod);\n\n        timeProvider.VerifyTimer(0, Period1);\n        Assert.Same(entity, lastInvokedEntity);\n    }\n\n    private void VerifyEntities(EntityActionScheduler<Entity> scheduler, params Entity[] entities)\n    {\n        var actualCount = 0;\n        foreach (var entity in entities)\n        {\n            Assert.True(scheduler.IsScheduled(entity));\n            actualCount++;\n        }\n        Assert.Equal(entities.Length, actualCount);\n    }\n\n    private class Entity\n    {\n        public string Id { get; set; }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Health/HealthyAndUnknownDestinationsPolicyTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health.Tests;\n\npublic class HealthyAndUnknownDestinationsPolicyTests\n{\n    [Fact]\n    public void GetAvailableDestinations_HealthChecksEnabled_FilterOutUnhealthy()\n    {\n        var cluster = new ClusterConfig()\n        {\n            ClusterId = \"cluster1\",\n            HealthCheck = new HealthCheckConfig\n            {\n                Active = new ActiveHealthCheckConfig { Enabled = true },\n                Passive = new PassiveHealthCheckConfig { Enabled = true }\n            }\n        };\n\n        var allDestinations = new[]\n        {\n            new DestinationState(\"d1\") { Health = { Active = DestinationHealth.Healthy } },\n            new DestinationState(\"d2\") { Health = { Active = DestinationHealth.Unhealthy } },\n            new DestinationState(\"d3\") { Health = { Active = DestinationHealth.Unhealthy, Passive = DestinationHealth.Healthy } },\n            new DestinationState(\"d4\") { Health = { Passive = DestinationHealth.Unhealthy } },\n            new DestinationState(\"d5\") { Health = { Passive = DestinationHealth.Healthy } },\n            new DestinationState(\"d6\") { Health = { Active = DestinationHealth.Healthy, Passive = DestinationHealth.Unhealthy } },\n            new DestinationState(\"d7\") { Health = { Active = DestinationHealth.Unhealthy, Passive = DestinationHealth.Unhealthy } },\n            new DestinationState(\"d8\")\n        };\n        var policy = new HealthyAndUnknownDestinationsPolicy();\n\n        var availableDestinations = policy.GetAvailableDestinations(cluster, allDestinations);\n\n        Assert.Equal(3, availableDestinations.Count);\n        Assert.Same(allDestinations[0], availableDestinations[0]);\n        Assert.Same(allDestinations[4], availableDestinations[1]);\n        Assert.Same(allDestinations[7], availableDestinations[2]);\n    }\n\n    [Theory]\n    [MemberData(nameof(GetDisabledHealthChecksCases))]\n    public void GetAvailableDestinations_HealthChecksDisabled_ReturnAll(HealthCheckConfig config)\n    {\n        var cluster = new ClusterConfig() { ClusterId = \"cluster1\", HealthCheck = config };\n        var allDestinations = new[]\n        {\n            new DestinationState(\"d1\") { Health = { Active = DestinationHealth.Healthy } },\n            new DestinationState(\"d2\") { Health = { Active = DestinationHealth.Unhealthy, Passive = DestinationHealth.Healthy } },\n            new DestinationState(\"d3\") { Health = { Passive = DestinationHealth.Healthy } },\n            new DestinationState(\"d4\"),\n            new DestinationState(\"d5\") { Health = { Active = DestinationHealth.Healthy, Passive = DestinationHealth.Unhealthy } },\n            new DestinationState(\"d6\") { Health = { Active = DestinationHealth.Unhealthy, Passive = DestinationHealth.Unhealthy } }\n        };\n        var policy = new HealthyAndUnknownDestinationsPolicy();\n\n        var availableDestinations = policy.GetAvailableDestinations(cluster, allDestinations);\n\n        Assert.Equal(6, availableDestinations.Count);\n        Assert.Same(allDestinations[0], availableDestinations[0]);\n        Assert.Same(allDestinations[1], availableDestinations[1]);\n        Assert.Same(allDestinations[2], availableDestinations[2]);\n        Assert.Same(allDestinations[3], availableDestinations[3]);\n        Assert.Same(allDestinations[4], availableDestinations[4]);\n        Assert.Same(allDestinations[5], availableDestinations[5]);\n    }\n\n    [Theory]\n    [InlineData(true, DestinationHealth.Unhealthy, true, DestinationHealth.Healthy, false)]\n    [InlineData(false, DestinationHealth.Unhealthy, true, DestinationHealth.Healthy, true)]\n    [InlineData(true, DestinationHealth.Healthy, true, DestinationHealth.Unhealthy, false)]\n    [InlineData(true, DestinationHealth.Healthy, false, DestinationHealth.Unhealthy, true)]\n    [InlineData(false, DestinationHealth.Unhealthy, false, DestinationHealth.Unhealthy, true)]\n    [InlineData(true, DestinationHealth.Unhealthy, true, DestinationHealth.Unhealthy, false)]\n    public void GetAvailableDestinations_OneHealthCheckDisabled_UseUnknownState(bool activeEnabled, DestinationHealth active, bool passiveEnabled, DestinationHealth passive, bool isAvailable)\n    {\n        var cluster = new ClusterConfig()\n        {\n            ClusterId = \"cluster1\",\n            HealthCheck = new HealthCheckConfig\n            {\n                Active = new ActiveHealthCheckConfig { Enabled = activeEnabled },\n                Passive = new PassiveHealthCheckConfig { Enabled = passiveEnabled }\n            }\n        };\n\n        var policy = new HealthyAndUnknownDestinationsPolicy();\n\n        var destination = new DestinationState(\"d0\") { Health = { Active = active, Passive = passive } };\n        var availableDestinations = policy.GetAvailableDestinations(cluster, new[] { destination });\n\n        if (isAvailable)\n        {\n            Assert.Single(availableDestinations, destination);\n        }\n        else\n        {\n            Assert.Empty(availableDestinations);\n        }\n    }\n\n    public static IEnumerable<object[]> GetDisabledHealthChecksCases()\n    {\n        yield return new[] { new HealthCheckConfig() };\n        yield return new[] { (object)null };\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Health/HealthyOrPanicDestinationsPolicyTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Health.Tests;\n\npublic class HealthyOrPanicDestinationsPolicyTests\n{\n    [Fact]\n    public void GetAvailableDestinations_SomeDestinationsAreHealthy_ReturnOnlyHealthy()\n    {\n        var cluster = GetClusterConfig();\n\n        var allDestinations = new[]\n        {\n            new DestinationState(\"d1\") { Health = { Active = DestinationHealth.Healthy } },\n            new DestinationState(\"d2\") { Health = { Active = DestinationHealth.Unhealthy } },\n            new DestinationState(\"d2\") { Health = { Passive = DestinationHealth.Healthy } },\n            new DestinationState(\"d4\")\n        };\n        var policy = new HealthyOrPanicDestinationsPolicy();\n\n        var availableDestinations = policy.GetAvailableDestinations(cluster, allDestinations);\n\n        Assert.Equal(3, availableDestinations.Count);\n        Assert.Same(allDestinations[0], availableDestinations[0]);\n        Assert.Same(allDestinations[2], availableDestinations[1]);\n        Assert.Same(allDestinations[3], availableDestinations[2]);\n    }\n\n    [Fact]\n    public void GetAvailableDestinations_AllDestinationsAreUnhealthy_ReturnAll()\n    {\n        var cluster = GetClusterConfig();\n\n        var allDestinations = new[]\n        {\n            new DestinationState(\"d1\") { Health = { Active = DestinationHealth.Unhealthy } },\n            new DestinationState(\"d2\") { Health = { Passive = DestinationHealth.Unhealthy } },\n            new DestinationState(\"d2\") { Health = { Active = DestinationHealth.Unhealthy, Passive = DestinationHealth.Healthy } },\n            new DestinationState(\"d4\")  { Health = { Active = DestinationHealth.Unhealthy, Passive = DestinationHealth.Unhealthy } }\n        };\n        var policy = new HealthyOrPanicDestinationsPolicy();\n\n        var availableDestinations = policy.GetAvailableDestinations(cluster, allDestinations);\n\n        Assert.Equal(4, availableDestinations.Count);\n        Assert.Same(allDestinations[0], availableDestinations[0]);\n        Assert.Same(allDestinations[1], availableDestinations[1]);\n        Assert.Same(allDestinations[2], availableDestinations[2]);\n        Assert.Same(allDestinations[3], availableDestinations[3]);\n    }\n\n    private static ClusterConfig GetClusterConfig()\n    {\n        return new ClusterConfig()\n        {\n            ClusterId = \"cluster1\",\n            HealthCheck = new HealthCheckConfig\n            {\n                Active = new ActiveHealthCheckConfig { Enabled = true },\n                Passive = new PassiveHealthCheckConfig { Enabled = true }\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Health/PassiveHealthCheckMiddlewareTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Moq;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Health.Tests;\n\npublic class PassiveHealthCheckMiddlewareTests\n{\n    [Fact]\n    public async Task Invoke_PassiveHealthCheckIsEnabled_CallPolicy()\n    {\n        var policies = new[] { GetPolicy(\"policy0\"), GetPolicy(\"policy1\") };\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\");\n        var cluster1 = GetClusterInfo(\"cluster1\", \"policy1\");\n        var nextInvoked = false;\n        var middleware = new PassiveHealthCheckMiddleware(c =>\n        {\n            nextInvoked = true;\n            return Task.CompletedTask;\n        }, policies.Select(p => p.Object));\n\n        var context0 = GetContext(cluster0, selectedDestination: 1, error: null);\n        await middleware.Invoke(context0);\n\n        Assert.True(nextInvoked);\n        policies[0].Verify(p => p.RequestProxied(context0, cluster0, cluster0.DestinationsState.AllDestinations[1]), Times.Once);\n        policies[0].VerifyGet(p => p.Name, Times.Once);\n        policies[0].VerifyNoOtherCalls();\n        policies[1].VerifyGet(p => p.Name, Times.Once);\n        policies[1].VerifyNoOtherCalls();\n\n        nextInvoked = false;\n\n        var error = new ForwarderErrorFeature(ForwarderError.Request, null);\n        var context1 = GetContext(cluster1, selectedDestination: 0, error);\n        await middleware.Invoke(context1);\n\n        Assert.True(nextInvoked);\n        policies[1].Verify(p => p.RequestProxied(context1, cluster1, cluster1.DestinationsState.AllDestinations[0]), Times.Once);\n        policies[1].VerifyNoOtherCalls();\n        policies[0].VerifyNoOtherCalls();\n    }\n\n    [Fact]\n    public async Task Invoke_PassiveHealthCheckIsDisabled_DoNothing()\n    {\n        var policies = new[] { GetPolicy(\"policy0\"), GetPolicy(\"policy1\") };\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\", enabled: false);\n        var nextInvoked = false;\n        var middleware = new PassiveHealthCheckMiddleware(c =>\n        {\n            nextInvoked = true;\n            return Task.CompletedTask;\n        }, policies.Select(p => p.Object));\n\n        var context0 = GetContext(cluster0, selectedDestination: 0, error: null);\n        await middleware.Invoke(context0);\n\n        Assert.True(nextInvoked);\n        policies[0].VerifyGet(p => p.Name, Times.Once);\n        policies[0].VerifyNoOtherCalls();\n        policies[1].VerifyGet(p => p.Name, Times.Once);\n        policies[1].VerifyNoOtherCalls();\n    }\n\n    [Fact]\n    public async Task Invoke_PassiveHealthCheckIsEnabledButNoDestinationSelected_DoNothing()\n    {\n        var policies = new[] { GetPolicy(\"policy0\"), GetPolicy(\"policy1\") };\n        var cluster0 = GetClusterInfo(\"cluster0\", \"policy0\");\n        var nextInvoked = false;\n        var middleware = new PassiveHealthCheckMiddleware(c =>\n        {\n            nextInvoked = true;\n            return Task.CompletedTask;\n        }, policies.Select(p => p.Object));\n\n        var context0 = GetContext(cluster0, selectedDestination: 1, error: null);\n        context0.GetReverseProxyFeature().ProxiedDestination = null;\n        await middleware.Invoke(context0);\n\n        Assert.True(nextInvoked);\n        policies[0].VerifyGet(p => p.Name, Times.Once);\n        policies[0].VerifyNoOtherCalls();\n        policies[1].VerifyGet(p => p.Name, Times.Once);\n        policies[1].VerifyNoOtherCalls();\n    }\n\n    private HttpContext GetContext(ClusterState cluster, int selectedDestination, IForwarderErrorFeature error)\n    {\n        var context = new DefaultHttpContext();\n        context.Features.Set(GetProxyFeature(cluster, cluster.DestinationsState.AllDestinations[selectedDestination]));\n        context.Features.Set(error);\n        return context;\n    }\n\n    private Mock<IPassiveHealthCheckPolicy> GetPolicy(string name)\n    {\n        var policy = new Mock<IPassiveHealthCheckPolicy>();\n        policy.SetupGet(p => p.Name).Returns(name);\n        return policy;\n    }\n\n    private IReverseProxyFeature GetProxyFeature(ClusterState clusterState, DestinationState destination)\n    {\n        return new ReverseProxyFeature()\n        {\n            ProxiedDestination = destination,\n            Cluster = clusterState.Model,\n            Route = new RouteModel(new RouteConfig(), clusterState, HttpTransformer.Default),\n        };\n    }\n\n    private ClusterState GetClusterInfo(string id, string policy, bool enabled = true)\n    {\n        var clusterModel = new ClusterModel(\n            new ClusterConfig\n            {\n                ClusterId = id,\n                HealthCheck = new HealthCheckConfig\n                {\n                    Passive = new PassiveHealthCheckConfig\n                    {\n                        Enabled = enabled,\n                        Policy = policy,\n                    }\n                }\n            },\n            new HttpMessageInvoker(new HttpClientHandler()));\n        var clusterState = new ClusterState(id);\n        clusterState.Model = clusterModel;\n        clusterState.Destinations.GetOrAdd(\"destination0\", id => new DestinationState(id));\n        clusterState.Destinations.GetOrAdd(\"destination1\", id => new DestinationState(id));\n\n        clusterState.DestinationsState = new ClusterDestinationsState(clusterState.Destinations.Values.ToList(), clusterState.Destinations.Values.ToList());\n\n        return clusterState;\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Health/TransportFailureRateHealthPolicyTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.Linq;\nusing System.Net.Http;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Options;\nusing Moq;\nusing Xunit;\nusing Yarp.Tests.Common;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Health.Tests;\n\npublic class TransportFailureRateHealthPolicyTests\n{\n    [Fact]\n    public void RequestProxied_FailureRateLimitExceeded_MarkDestinationUnhealthy()\n    {\n        var options = Options.Create(\n            new TransportFailureRateHealthPolicyOptions { DefaultFailureRateLimit = 0.5, DetectionWindowSize = TimeSpan.FromSeconds(30), MinimalTotalCountThreshold = 1 });\n        var timeProvider = new TestTimeProvider(TimeSpan.FromMilliseconds(10000));\n        var healthUpdater = new Mock<IDestinationHealthUpdater>();\n        var policy = new TransportFailureRateHealthPolicy(options, timeProvider, healthUpdater.Object);\n        Assert.Equal(HealthCheckConstants.PassivePolicy.TransportFailureRate, policy.Name);\n\n        var reactivationPeriod0 = TimeSpan.FromSeconds(60);\n        var reactivationPeriod1 = TimeSpan.FromSeconds(100);\n        var cluster0 = GetClusterInfo(\"cluster0\", destinationCount: 2);\n        var cluster1 = GetClusterInfo(\"cluster1\", destinationCount: 2, failureRateLimit: 0.61, reactivationPeriod1);\n\n        // Initial state\n        Assert.All(cluster0.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Passive));\n        Assert.All(cluster1.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Passive));\n\n        // Successful requests\n        for (var i = 0; i < 3; i++)\n        {\n            policy.RequestProxied(new DefaultHttpContext(), cluster0, cluster0.Destinations.Values.First());\n            policy.RequestProxied(new DefaultHttpContext(), cluster0, cluster0.Destinations.Values.Skip(1).First());\n            policy.RequestProxied(new DefaultHttpContext(), cluster1, cluster1.Destinations.Values.First());\n            policy.RequestProxied(new DefaultHttpContext(), cluster1, cluster1.Destinations.Values.Skip(1).First());\n            timeProvider.Advance(TimeSpan.FromMilliseconds(4000));\n        }\n\n        healthUpdater.Verify(u => u.SetPassive(cluster0, cluster0.Destinations.Values.First(), DestinationHealth.Healthy, reactivationPeriod0), Times.Exactly(3));\n        healthUpdater.Verify(u => u.SetPassive(cluster0, cluster0.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod0), Times.Exactly(3));\n        healthUpdater.Verify(u => u.SetPassive(cluster1, cluster1.Destinations.Values.First(), DestinationHealth.Healthy, reactivationPeriod1), Times.Exactly(3));\n        healthUpdater.Verify(u => u.SetPassive(cluster1, cluster1.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod1), Times.Exactly(3));\n        healthUpdater.VerifyNoOtherCalls();\n\n        // Failed requests\n        for (var i = 0; i < 3; i++)\n        {\n            policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster0, cluster0.Destinations.Values.Skip(1).First());\n            policy.RequestProxied(GetFailedRequestContext(ForwarderError.Request), cluster1, cluster1.Destinations.Values.First());\n            timeProvider.Advance(TimeSpan.FromMilliseconds(4000));\n        }\n\n        healthUpdater.Verify(u => u.SetPassive(cluster0, cluster0.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod0), Times.Exactly(5));\n        healthUpdater.Verify(u => u.SetPassive(cluster0, cluster0.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, reactivationPeriod0), Times.Once);\n        healthUpdater.Verify(u => u.SetPassive(cluster1, cluster1.Destinations.Values.First(), DestinationHealth.Healthy, reactivationPeriod1), Times.Exactly(6));\n        healthUpdater.VerifyNoOtherCalls();\n\n        // Two more failed requests\n        policy.RequestProxied(GetFailedRequestContext(ForwarderError.Request), cluster1, cluster1.Destinations.Values.First());\n        // End of the detection window\n        timeProvider.Advance(TimeSpan.FromMilliseconds(6000));\n        policy.RequestProxied(GetFailedRequestContext(ForwarderError.Request), cluster1, cluster1.Destinations.Values.First());\n\n        healthUpdater.Verify(u => u.SetPassive(cluster1, cluster1.Destinations.Values.First(), DestinationHealth.Healthy, reactivationPeriod1), Times.Exactly(7));\n        healthUpdater.Verify(u => u.SetPassive(cluster1, cluster1.Destinations.Values.First(), DestinationHealth.Unhealthy, reactivationPeriod1), Times.Once);\n        healthUpdater.VerifyNoOtherCalls();\n    }\n\n    [Fact]\n    public void RequestProxied_FailureMovedOutOfDetectionWindow_MarkDestinationHealthy()\n    {\n        var options = Options.Create(\n            new TransportFailureRateHealthPolicyOptions { DefaultFailureRateLimit = 0.5, DetectionWindowSize = TimeSpan.FromSeconds(30), MinimalTotalCountThreshold = 1 });\n        var timeProvider = new TestTimeProvider(TimeSpan.FromMilliseconds(10000));\n        var healthUpdater = new Mock<IDestinationHealthUpdater>();\n        var policy = new TransportFailureRateHealthPolicy(options, timeProvider, healthUpdater.Object);\n\n        var cluster = GetClusterInfo(\"cluster0\", destinationCount: 2);\n\n        // Initial failed requests\n        for (var i = 0; i < 2; i++)\n        {\n            policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First());\n            timeProvider.Advance(TimeSpan.FromMilliseconds(1000));\n        }\n\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, TimeSpan.FromSeconds(60)), Times.Exactly(2));\n        healthUpdater.VerifyNoOtherCalls();\n\n        // Successful requests\n        for (var i = 0; i < 4; i++)\n        {\n            policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.First());\n            policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.Skip(1).First());\n            timeProvider.Advance(TimeSpan.FromMilliseconds(5000));\n        }\n\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.First(), DestinationHealth.Healthy, TimeSpan.FromSeconds(60)), Times.Exactly(4));\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, TimeSpan.FromSeconds(60)), Times.Exactly(2));\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, TimeSpan.FromSeconds(60)), Times.Exactly(4));\n        healthUpdater.VerifyNoOtherCalls();\n\n        // Failed requests\n        for (var i = 0; i < 2; i++)\n        {\n            policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First());\n            timeProvider.Advance(TimeSpan.FromMilliseconds(1));\n        }\n\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, TimeSpan.FromSeconds(60)), Times.Exactly(3));\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, TimeSpan.FromSeconds(60)), Times.Exactly(5));\n        healthUpdater.VerifyNoOtherCalls();\n\n        // Shift the detection window to the future\n        timeProvider.Advance(TimeSpan.FromMilliseconds(10998));\n\n        // New successful requests\n        for (var i = 0; i < 2; i++)\n        {\n            policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.Skip(1).First());\n            timeProvider.Advance(TimeSpan.FromMilliseconds(1));\n        }\n\n        // New failed request, but 2 oldest failures have moved out of the detection window\n        policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First());\n\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, TimeSpan.FromSeconds(60)), Times.Exactly(6));\n        healthUpdater.VerifyNoOtherCalls();\n    }\n\n    [Fact]\n    public void RequestProxied_FailedAndReactivationLessDetection_UseDetectionPeriodForReactivation()\n    {\n        var options = Options.Create(\n            new TransportFailureRateHealthPolicyOptions { DefaultFailureRateLimit = 0.5, DetectionWindowSize = TimeSpan.FromSeconds(30), MinimalTotalCountThreshold = 1 });\n        var timeProvider = new TestTimeProvider(TimeSpan.FromMilliseconds(10000));\n        var healthUpdater = new Mock<IDestinationHealthUpdater>();\n        var policy = new TransportFailureRateHealthPolicy(options, timeProvider, healthUpdater.Object);\n\n        var cluster = GetClusterInfo(\"cluster0\", destinationCount: 2, reactivationPeriod: TimeSpan.FromSeconds(10));\n\n        // Initial failed requests\n        for (var i = 0; i < 2; i++)\n        {\n            policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First());\n            timeProvider.Advance(TimeSpan.FromMilliseconds(1000));\n        }\n\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, TimeSpan.FromSeconds(30)), Times.Exactly(2));\n        healthUpdater.VerifyNoOtherCalls();\n\n        // Simulate a reactivation\n        timeProvider.Advance(TimeSpan.FromMilliseconds(31000));\n        cluster.Destinations.Values.Skip(1).First().Health.Passive = DestinationHealth.Unknown;\n\n        // One successful request to the reactivated destination\n        policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.Skip(1).First());\n        timeProvider.Advance(TimeSpan.FromMilliseconds(100));\n\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, TimeSpan.FromSeconds(30)), Times.Exactly(1));\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, TimeSpan.FromSeconds(30)), Times.Exactly(2));\n        healthUpdater.VerifyNoOtherCalls();\n    }\n\n    [Fact]\n    public void RequestProxied_MultipleConcurrentRequests_MarkDestinationUnhealthyAndHealthyAgain()\n    {\n        var options = Options.Create(\n            new TransportFailureRateHealthPolicyOptions { DefaultFailureRateLimit = 0.5, DetectionWindowSize = TimeSpan.FromSeconds(30), MinimalTotalCountThreshold = 1 });\n        var timeProvider = new TestTimeProvider(TimeSpan.FromMilliseconds(10000));\n        var healthUpdater = new Mock<IDestinationHealthUpdater>();\n        var reactivationPeriod = TimeSpan.FromSeconds(40);\n        var policy = new TransportFailureRateHealthPolicy(options, timeProvider, healthUpdater.Object);\n\n        var cluster = GetClusterInfo(\"cluster0\", destinationCount: 2, reactivationPeriod: reactivationPeriod);\n\n        // Initial state\n        Assert.All(cluster.Destinations.Values, d => Assert.Equal(DestinationHealth.Unknown, d.Health.Passive));\n\n        // Initial successful requests\n        for (var i = 0; i < 2; i++)\n        {\n            policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.Skip(1).First());\n        }\n\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod), Times.Exactly(2));\n        healthUpdater.VerifyNoOtherCalls();\n\n        // Concurrent failed requests.\n        // They are 'concurrent' because the timeProvider is not updated.\n        for (var i = 0; i < 2; i++)\n        {\n            policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First());\n        }\n\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod), Times.Exactly(3));\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, reactivationPeriod), Times.Once);\n        healthUpdater.VerifyNoOtherCalls();\n\n        // More successful requests\n        for (var i = 0; i < 2; i++)\n        {\n            policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.Skip(1).First());\n            timeProvider.Advance(TimeSpan.FromMilliseconds(100));\n        }\n\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod), Times.Exactly(5));\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, reactivationPeriod), Times.Once);\n        healthUpdater.VerifyNoOtherCalls();\n\n        // More failed requests\n        for (var i = 0; i < 2; i++)\n        {\n            policy.RequestProxied(GetFailedRequestContext(ForwarderError.RequestTimedOut), cluster, cluster.Destinations.Values.Skip(1).First());\n            timeProvider.Advance(TimeSpan.FromMilliseconds(100));\n        }\n\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Healthy, reactivationPeriod), Times.Exactly(6));\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.Skip(1).First(), DestinationHealth.Unhealthy, reactivationPeriod), Times.Exactly(2));\n        healthUpdater.VerifyNoOtherCalls();\n\n        policy.RequestProxied(new DefaultHttpContext(), cluster, cluster.Destinations.Values.First());\n\n        healthUpdater.Verify(u => u.SetPassive(cluster, cluster.Destinations.Values.First(), DestinationHealth.Healthy, reactivationPeriod), Times.Once);\n        healthUpdater.VerifyNoOtherCalls();\n    }\n\n    private HttpContext GetFailedRequestContext(ForwarderError error)\n    {\n        var errorFeature = new ForwarderErrorFeature(error, null);\n        var context = new DefaultHttpContext();\n        context.Features.Set<IForwarderErrorFeature>(errorFeature);\n        return context;\n    }\n\n    private ClusterState GetClusterInfo(string id, int destinationCount, double? failureRateLimit = null, TimeSpan? reactivationPeriod = null)\n    {\n        var metadata = failureRateLimit is not null\n            ? new Dictionary<string, string> { { TransportFailureRateHealthPolicyOptions.FailureRateLimitMetadataName, failureRateLimit?.ToString(CultureInfo.InvariantCulture) } }\n            : null;\n        var clusterModel = new ClusterModel(\n            new ClusterConfig\n            {\n                ClusterId = id,\n                HealthCheck = new HealthCheckConfig\n                {\n                    Passive = new PassiveHealthCheckConfig\n                    {\n                        Enabled = true,\n                        Policy = \"policy\",\n                        ReactivationPeriod = reactivationPeriod,\n                    }\n                },\n                Metadata = metadata,\n            },\n            new HttpMessageInvoker(new HttpClientHandler()));\n        var clusterState = new ClusterState(id);\n        clusterState.Model = clusterModel;\n        for (var i = 0; i < destinationCount; i++)\n        {\n            var destinationModel = new DestinationModel(new DestinationConfig { Address = $\"https://localhost:1000{i}/{id}/\", Health = $\"https://localhost:2000{i}/{id}/\" });\n            var destinationId = $\"destination{i}\";\n            clusterState.Destinations.GetOrAdd(destinationId, id => new DestinationState(id)\n            {\n                Model = destinationModel\n            });\n        }\n\n        return clusterState;\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Limits/LimitsMiddlewareTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.Limits.Tests;\n\npublic class LimitsMiddlewareTests\n{\n    [Fact]\n    public void Constructor_Works()\n    {\n        CreateMiddleware();\n    }\n\n    [Fact]\n    public async Task MissingFeature_NoOps()\n    {\n        var context = CreateContext(10, null);\n\n        var sut = CreateMiddleware();\n\n        await sut.Invoke(context);\n    }\n\n    [Theory]\n    [InlineData(true, null, null, null)]\n    [InlineData(true, 10L, null, 10L)]\n    [InlineData(true, null, 10L, null)]\n    [InlineData(true, 10L, 11L, 10L)]\n    [InlineData(false, null, null, null)]\n    [InlineData(false, 10L, null, 10L)]\n    [InlineData(false, null, 10L, 10L)]\n    [InlineData(false, null, -1L, null)]\n    [InlineData(false, 10L, -1L, null)]\n    [InlineData(false, 10L, 11L, 11L)]\n    public async Task Invoke_CombinationsWork(bool readOnly, long? serverLimit, long? routeLimit, long? expected)\n    {\n        var feature = new FakeBodySizeFeature() { IsReadOnly = readOnly, MaxRequestBodySize = serverLimit };\n        var context = CreateContext(routeLimit, feature);\n\n        var sut = CreateMiddleware();\n\n        await sut.Invoke(context);\n\n        Assert.Equal(expected, feature.MaxRequestBodySize);\n    }\n\n    private static LimitsMiddleware CreateMiddleware()\n    {\n        return new LimitsMiddleware(\n            _ => Task.CompletedTask,\n            NullLogger<LimitsMiddleware>.Instance);\n    }\n\n    private static HttpContext CreateContext(long? bodySizeLimit, IHttpMaxRequestBodySizeFeature feature)\n    {\n        var cluster = new ClusterState(\"cluster1\")\n        {\n            Model = new ClusterModel(new ClusterConfig(),\n                new HttpMessageInvoker(new HttpClientHandler()))\n        };\n\n        var context = new DefaultHttpContext();\n\n        var route = new RouteModel(new RouteConfig() { MaxRequestBodySize = bodySizeLimit }, cluster, HttpTransformer.Default);\n        context.Features.Set<IReverseProxyFeature>(\n            new ReverseProxyFeature()\n            {\n                Route = route,\n                Cluster = cluster.Model\n            });\n        context.Features.Set(cluster);\n\n        var endpoint = new Endpoint(default, new EndpointMetadataCollection(route), string.Empty);\n        context.SetEndpoint(endpoint);\n\n        context.Features.Set(feature);\n\n        return context;\n    }\n\n    private class FakeBodySizeFeature : IHttpMaxRequestBodySizeFeature\n    {\n        public bool IsReadOnly { get; set; }\n\n        public long? MaxRequestBodySize { get; set; }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/LoadBalancing/LoadBalancerMiddlewareTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\nusing Moq;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.LoadBalancing.Tests;\n\npublic class LoadBalancerMiddlewareTests\n{\n    private static LoadBalancingMiddleware CreateMiddleware(RequestDelegate next, params ILoadBalancingPolicy[] loadBalancingPolicies)\n    {\n        var logger = new Mock<ILogger<LoadBalancingMiddleware>>();\n        logger\n            .Setup(l => l.IsEnabled(It.IsAny<LogLevel>()))\n            .Returns(true);\n\n        return new LoadBalancingMiddleware(\n            next,\n            logger.Object,\n            loadBalancingPolicies);\n    }\n\n    [Fact]\n    public void Constructor_Works()\n    {\n        CreateMiddleware(_ => Task.CompletedTask);\n    }\n\n    [Fact]\n    public async Task PickDestination_UnsupportedPolicy_Throws()\n    {\n        const string PolicyName = \"NonExistentPolicy\";\n        var context = CreateContext(PolicyName, new[]\n        {\n            new DestinationState(\"destination1\"),\n            new DestinationState(\"destination2\")\n        });\n\n        var sut = CreateMiddleware(_ => Task.CompletedTask);\n\n        var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await sut.Invoke(context));\n        Assert.Equal($\"No {typeof(ILoadBalancingPolicy)} was found for the id '{PolicyName}'. (Parameter 'id')\", ex.Message);\n    }\n\n    [Fact]\n    public async Task PickDestination_SingleDestinations_ShortCircuit()\n    {\n        var context = CreateContext(LoadBalancingPolicies.FirstAlphabetical, new[]\n        {\n            new DestinationState(\"destination1\")\n        });\n\n        var sut = CreateMiddleware(_ => Task.CompletedTask);\n\n        await sut.Invoke(context);\n\n        var feature = context.Features.Get<IReverseProxyFeature>();\n        Assert.NotNull(feature);\n        Assert.NotNull(feature.AvailableDestinations);\n        Assert.Single(feature.AvailableDestinations);\n        Assert.Same(\"destination1\", feature.AvailableDestinations[0].DestinationId);\n\n        Assert.Equal(200, context.Response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Invoke_Works()\n    {\n        // Selects the alphabetically first available destination.\n        var context = CreateContext(LoadBalancingPolicies.FirstAlphabetical, new[]\n        {\n            new DestinationState(\"destination2\"),\n            new DestinationState(\"destination1\"),\n        });\n\n        var sut = CreateMiddleware(_ => Task.CompletedTask, new FirstLoadBalancingPolicy());\n\n        await sut.Invoke(context);\n\n        var feature = context.Features.Get<IReverseProxyFeature>();\n        Assert.NotNull(feature);\n        Assert.NotNull(feature.AvailableDestinations);\n        Assert.Single(feature.AvailableDestinations);\n        Assert.Same(\"destination1\", feature.AvailableDestinations[0].DestinationId);\n\n        Assert.Equal(200, context.Response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Invoke_WithoutDestinations_503()\n    {\n        var context = CreateContext(LoadBalancingPolicies.FirstAlphabetical, Array.Empty<DestinationState>());\n\n        var sut = CreateMiddleware(context =>\n        {\n            context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;\n            return Task.CompletedTask;\n        });\n\n        await sut.Invoke(context);\n\n        var feature = context.Features.Get<IReverseProxyFeature>();\n        Assert.NotNull(feature);\n        Assert.NotNull(feature.AvailableDestinations);\n        Assert.Empty(feature.AvailableDestinations);\n\n        Assert.Equal(503, context.Response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Invoke_ServiceReturnsNoResults_FallThrough()\n    {\n        const string PolicyName = \"CustomPolicy\";\n\n        var context = CreateContext(PolicyName, new[]\n        {\n            new DestinationState(\"destination1\"),\n            new DestinationState(\"destination2\")\n        });\n\n        var policy = new Mock<ILoadBalancingPolicy>();\n        policy\n            .Setup(p => p.Name)\n            .Returns(PolicyName);\n        policy\n            .Setup(p => p.PickDestination(It.IsAny<HttpContext>(), It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationState>>()))\n            .Returns((DestinationState)null);\n\n        var sut = CreateMiddleware(context =>\n        {\n            context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;\n            return Task.CompletedTask;\n        },\n        policy.Object);\n\n        await sut.Invoke(context);\n\n        var feature = context.Features.Get<IReverseProxyFeature>();\n        Assert.NotNull(feature);\n        Assert.NotNull(feature.AvailableDestinations);\n        Assert.Empty(feature.AvailableDestinations);\n\n        Assert.Equal(503, context.Response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Invoke_NoPolicySpecified_DefaultsToPowerOfTwoChoices()\n    {\n        var destinations = new[]\n        {\n            new DestinationState(\"destination1\"),\n            new DestinationState(\"destination2\")\n        };\n        var context = CreateContext(loadBalancingPolicy: null, destinations);\n\n        var policy = new Mock<ILoadBalancingPolicy>();\n        policy\n            .Setup(p => p.Name)\n            .Returns(LoadBalancingPolicies.PowerOfTwoChoices);\n        policy\n            .Setup(p => p.PickDestination(It.IsAny<HttpContext>(), It.IsAny<ClusterState>(), It.IsAny<IReadOnlyList<DestinationState>>()))\n            .Returns((DestinationState)destinations[0]);\n\n        var sut = CreateMiddleware(_ => Task.CompletedTask, policy.Object);\n\n        await sut.Invoke(context);\n\n        policy.Verify(p => p.PickDestination(context, It.IsAny<ClusterState>(), destinations), Times.Once);\n    }\n\n    private static HttpContext CreateContext(string loadBalancingPolicy, IReadOnlyList<DestinationState> destinations)\n    {\n        var cluster = new ClusterState(\"cluster1\")\n        {\n            Model = new ClusterModel(new ClusterConfig { LoadBalancingPolicy = loadBalancingPolicy },\n                new HttpMessageInvoker(new HttpClientHandler()))\n        };\n\n        var context = new DefaultHttpContext();\n\n        var route = new RouteModel(new RouteConfig(), cluster, HttpTransformer.Default);\n        context.Features.Set<IReverseProxyFeature>(\n            new ReverseProxyFeature()\n            {\n                AvailableDestinations = destinations,\n                Route = route,\n                Cluster = cluster.Model\n            });\n        context.Features.Set(cluster);\n\n        var endpoint = new Endpoint(default, new EndpointMetadataCollection(route), string.Empty);\n        context.SetEndpoint(endpoint);\n\n        return context;\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/LoadBalancing/LoadBalancingPoliciesTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\nusing Yarp.Tests.Common;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.LoadBalancing.Tests;\n\npublic class LoadBalancingPoliciesTests : TestAutoMockBase\n{\n    public LoadBalancingPoliciesTests()\n    {\n        RandomFactory = new TestRandomFactory() { Instance = RandomInstance };\n        Provide<IRandomFactory>(RandomFactory);\n    }\n\n    private TestRandom RandomInstance { get; set; } = new TestRandom();\n\n    private TestRandomFactory RandomFactory { get; set; }\n\n    [Fact]\n    public void PickDestination_FirstWithDestinations_Works()\n    {\n        var destinations = new[]\n        {\n            new DestinationState(\"d1\"),\n            new DestinationState(\"d2\"),\n            new DestinationState(\"d3\")\n        };\n\n        var context = new DefaultHttpContext();\n        var loadBalancer = Create<FirstLoadBalancingPolicy>();\n\n        for (var i = 0; i < 10; i++)\n        {\n            var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations);\n            Assert.Same(destinations[0], result);\n            result.ConcurrentRequestCount++;\n        }\n    }\n\n    [Fact]\n    public void PickDestination_Random_Works()\n    {\n        var destinations = new[]\n        {\n            new DestinationState(\"d1\"),\n            new DestinationState(\"d2\"),\n            new DestinationState(\"d3\")\n        };\n\n        const int Iterations = 10;\n        var random = new Random(42);\n        RandomInstance.Sequence = Enumerable.Range(0, Iterations).Select(_ => random.Next(destinations.Length)).ToArray();\n\n        var context = new DefaultHttpContext();\n        var loadBalancer = Create<RandomLoadBalancingPolicy>();\n\n        for (var i = 0; i < Iterations; i++)\n        {\n            var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations);\n            Assert.Same(destinations[RandomInstance.Sequence[i]], result);\n            result.ConcurrentRequestCount++;\n        }\n    }\n\n    [Fact]\n    public void PickDestination_PowerOfTwoChoices_SkipBusiestConnection()\n    {\n        var destinations = new[]\n        {\n            new DestinationState(\"d1\"),\n            new DestinationState(\"d2\"),\n            new DestinationState(\"d3\")\n        };\n        destinations[0].ConcurrentRequestCount++;\n\n        const int Iterations = 100;\n        var random = new Random(42);\n        RandomInstance.Sequence = Enumerable.Range(0, Iterations * Iterations).Select(_ => random.Next(destinations.Length)).ToArray();\n\n        var context = new DefaultHttpContext();\n        var loadBalancer = Create<PowerOfTwoChoicesLoadBalancingPolicy>();\n\n        for (var i = 0; i < Iterations; i++)\n        {\n            var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations);\n\n            var groupByLoad = destinations.GroupBy(d => d.ConcurrentRequestCount);\n            var busiestGroup = groupByLoad.OrderByDescending(g => g.Key).First();\n            if (busiestGroup.Count() == 1)\n            {\n                Assert.True(result.ConcurrentRequestCount < busiestGroup.Key);\n            }\n\n            result.ConcurrentRequestCount++;\n        }\n    }\n\n    [Fact]\n    public void PickDestination_PowerOfTwoChoices_LeastLoaded()\n    {\n        var destinations = new[]\n        {\n            new DestinationState(\"d1\") {ConcurrentRequestCount = 1000},\n            new DestinationState(\"d2\")\n        };\n\n        const int Iterations = 100;\n        var random = new Random(42);\n        RandomInstance.Sequence = Enumerable.Range(0, Iterations * Iterations).Select(_ => random.Next(destinations.Length)).ToArray();\n\n        var context = new DefaultHttpContext();\n        var loadBalancer = Create<PowerOfTwoChoicesLoadBalancingPolicy>();\n\n        for (var i = 0; i < Iterations; i++)\n        {\n            var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations);\n            Assert.Same(destinations[1], result);\n        }\n    }\n\n    [Fact]\n    public void PickDestination_LeastRequests_Works()\n    {\n        var destinations = new[]\n        {\n            new DestinationState(\"d1\"),\n            new DestinationState(\"d2\"),\n            new DestinationState(\"d3\")\n        };\n        destinations[0].ConcurrentRequestCount++;\n\n        var context = new DefaultHttpContext();\n        var loadBalancer = Create<LeastRequestsLoadBalancingPolicy>();\n\n        for (var i = 0; i < 10; i++)\n        {\n            var result = loadBalancer.PickDestination(context, cluster: null, availableDestinations: destinations);\n            Assert.Same(destinations.OrderBy(d => d.ConcurrentRequestCount).First(), result);\n            result.ConcurrentRequestCount++;\n        }\n    }\n\n    [Fact]\n    public void PickDestination_RoundRobin_Works()\n    {\n        var destinations = new[]\n        {\n            new DestinationState(\"d1\"),\n            new DestinationState(\"d2\"),\n            new DestinationState(\"d3\")\n        };\n        destinations[0].ConcurrentRequestCount++;\n\n        var context = new DefaultHttpContext();\n\n        var cluster = new ClusterState(\"cluster1\");\n        var routeConfig = new RouteModel(new RouteConfig(), cluster, HttpTransformer.Default);\n        var feature = new ReverseProxyFeature()\n        {\n            Route = routeConfig,\n        };\n        context.Features.Set<IReverseProxyFeature>(feature);\n\n        var loadBalancer = Create<RoundRobinLoadBalancingPolicy>();\n\n        for (var i = 0; i < 10; i++)\n        {\n            var result = loadBalancer.PickDestination(context, cluster, availableDestinations: destinations);\n            Assert.Same(destinations[i % destinations.Length], result);\n            result.ConcurrentRequestCount++;\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Management/ProxyConfigManagerTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Sockets;\nusing System.Security.Authentication;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Channels;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Hosting.Server;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Microsoft.Extensions.Primitives;\nusing Moq;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Forwarder.Tests;\nusing Yarp.ReverseProxy.Health;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Routing;\nusing Yarp.ReverseProxy.ServiceDiscovery;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Management.Tests;\n\npublic class ProxyConfigManagerTests\n{\n    private static IServiceProvider CreateServices(\n        List<RouteConfig> routes,\n        List<ClusterConfig> clusters,\n        Action<IReverseProxyBuilder> configureProxy = null,\n        IEnumerable<IConfigChangeListener> configListeners = null,\n        IDestinationResolver destinationResolver = null)\n    {\n        var serviceCollection = new ServiceCollection();\n        serviceCollection.AddLogging();\n        serviceCollection.AddRouting();\n        var proxyBuilder = serviceCollection.AddReverseProxy().LoadFromMemory(routes, clusters);\n        serviceCollection.TryAddSingleton(new Mock<IServer>().Object);\n        serviceCollection.TryAddSingleton(new Mock<IWebHostEnvironment>().Object);\n        var activeHealthPolicy = new Mock<IActiveHealthCheckPolicy>();\n        activeHealthPolicy.SetupGet(p => p.Name).Returns(\"activePolicyA\");\n        serviceCollection.AddSingleton(activeHealthPolicy.Object);\n        configureProxy?.Invoke(proxyBuilder);\n        if (configListeners is not null)\n        {\n            foreach (var configListener in configListeners)\n            {\n                serviceCollection.AddSingleton(configListener);\n            }\n        }\n\n        if (destinationResolver is not null)\n        {\n            serviceCollection.AddSingleton(destinationResolver);\n        }\n\n        var services = serviceCollection.BuildServiceProvider();\n        var routeBuilder = services.GetRequiredService<ProxyEndpointFactory>();\n        routeBuilder.SetProxyPipeline(context => Task.CompletedTask);\n        return services;\n    }\n\n    private static IServiceProvider CreateServices(\n        IEnumerable<IProxyConfigProvider> configProviders,\n        Action<IReverseProxyBuilder> configureProxy = null,\n        IEnumerable<IConfigChangeListener> configListeners = null)\n    {\n        var serviceCollection = new ServiceCollection();\n        serviceCollection.AddLogging();\n        serviceCollection.AddRouting();\n        var proxyBuilder = serviceCollection.AddReverseProxy();\n        foreach (var configProvider in configProviders)\n        {\n            serviceCollection.AddSingleton(configProvider);\n        }\n        serviceCollection.TryAddSingleton(new Mock<IServer>().Object);\n        serviceCollection.TryAddSingleton(new Mock<IWebHostEnvironment>().Object);\n        var activeHealthPolicy = new Mock<IActiveHealthCheckPolicy>();\n        activeHealthPolicy.SetupGet(p => p.Name).Returns(\"activePolicyA\");\n        serviceCollection.AddSingleton(activeHealthPolicy.Object);\n        configureProxy?.Invoke(proxyBuilder);\n        if (configListeners is not null)\n        {\n            foreach (var configListener in configListeners)\n            {\n                serviceCollection.AddSingleton(configListener);\n            }\n        }\n        var services = serviceCollection.BuildServiceProvider();\n        var routeBuilder = services.GetRequiredService<ProxyEndpointFactory>();\n        routeBuilder.SetProxyPipeline(context => Task.CompletedTask);\n        return services;\n    }\n\n    [Fact]\n    public void Constructor_Works()\n    {\n        var services = CreateServices(new List<RouteConfig>(), new List<ClusterConfig>());\n        _ = services.GetRequiredService<ProxyConfigManager>();\n    }\n\n    [Fact]\n    public async Task NullRoutes_StartsEmpty()\n    {\n        var services = CreateServices(null, new List<ClusterConfig>());\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n        var dataSource = await manager.InitialLoadAsync();\n        Assert.NotNull(dataSource);\n        var endpoints = dataSource.Endpoints;\n        Assert.Empty(endpoints);\n    }\n\n    [Fact]\n    public async Task NullClusters_StartsEmpty()\n    {\n        var services = CreateServices(new List<RouteConfig>(), null);\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n        var dataSource = await manager.InitialLoadAsync();\n        Assert.NotNull(dataSource);\n        var endpoints = dataSource.Endpoints;\n        Assert.Empty(endpoints);\n    }\n\n    [Fact]\n    public async Task Endpoints_StartsEmpty()\n    {\n        var services = CreateServices(new List<RouteConfig>(), new List<ClusterConfig>());\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n        var dataSource = await manager.InitialLoadAsync();\n        Assert.NotNull(dataSource);\n        var endpoints = dataSource.Endpoints;\n        Assert.Empty(endpoints);\n    }\n\n    [Fact]\n    public async Task Lookup_StartsEmpty()\n    {\n        var services = CreateServices(new List<RouteConfig>(), new List<ClusterConfig>());\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n        var lookup = services.GetRequiredService<IProxyStateLookup>();\n        await manager.InitialLoadAsync();\n\n        Assert.Empty(lookup.GetRoutes());\n        Assert.Empty(lookup.GetClusters());\n        Assert.False(lookup.TryGetRoute(\"route1\", out var _));\n        Assert.False(lookup.TryGetCluster(\"cluster1\", out var _));\n    }\n\n    [Fact]\n    public async Task GetChangeToken_InitialValue()\n    {\n        var services = CreateServices(new List<RouteConfig>(), new List<ClusterConfig>());\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n        var dataSource = await manager.InitialLoadAsync();\n        Assert.NotNull(dataSource);\n        var changeToken = dataSource.GetChangeToken();\n        Assert.NotNull(changeToken);\n        Assert.True(changeToken.ActiveChangeCallbacks);\n        Assert.False(changeToken.HasChanged);\n    }\n\n    [Fact]\n    public async Task BuildConfig_OneClusterOneDestinationOneRoute_Works()\n    {\n        const string TestAddress = \"https://localhost:123/\";\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d1\", new DestinationConfig { Address = TestAddress } }\n            }\n        };\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch { Path = \"/\" }\n        };\n\n        var services = CreateServices(new List<RouteConfig>() { route }, new List<ClusterConfig>() { cluster });\n\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n        var lookup = services.GetRequiredService<IProxyStateLookup>();\n        var dataSource = await manager.InitialLoadAsync();\n\n        Assert.NotNull(dataSource);\n        var endpoints = dataSource.Endpoints;\n        var endpoint = Assert.Single(endpoints);\n        var routeConfig = endpoint.Metadata.GetMetadata<RouteModel>();\n        Assert.NotNull(routeConfig);\n        Assert.Equal(\"route1\", routeConfig.Config.RouteId);\n        Assert.True(lookup.TryGetRoute(\"route1\", out var routeModel));\n        Assert.Equal(route, routeModel.Config);\n        routeModel = Assert.Single(lookup.GetRoutes());\n        Assert.Equal(route, routeModel.Config);\n\n        var clusterState = routeConfig.Cluster;\n        Assert.NotNull(clusterState);\n\n        Assert.Equal(\"cluster1\", clusterState.ClusterId);\n        Assert.NotNull(clusterState.Destinations);\n        Assert.NotNull(clusterState.Model);\n        Assert.NotNull(clusterState.Model.HttpClient);\n        Assert.Same(clusterState, routeConfig.Cluster);\n        Assert.True(lookup.TryGetCluster(\"cluster1\", out clusterState));\n        Assert.Equal(cluster, clusterState.Model.Config);\n        clusterState = Assert.Single(lookup.GetClusters());\n        Assert.Equal(cluster, clusterState.Model.Config);\n\n        var actualDestinations = clusterState.Destinations.Values;\n        var destination = Assert.Single(actualDestinations);\n        Assert.Equal(\"d1\", destination.DestinationId);\n        Assert.NotNull(destination.Model);\n        Assert.Equal(TestAddress, destination.Model.Config.Address);\n    }\n\n    [Fact]\n    public async Task BuildConfig_DuplicateRouteIds_Throws()\n    {\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch { Path = \"/\" }\n        };\n\n        var services = CreateServices(new List<RouteConfig> { route, route }, new List<ClusterConfig>());\n\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => manager.InitialLoadAsync());\n        Assert.Contains(\"Duplicate route 'route1'\", ex.ToString());\n    }\n\n    [Fact]\n    public async Task BuildConfig_DuplicateClusterIds_Throws()\n    {\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\"\n        };\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch { Path = \"/\" }\n        };\n\n        var services = CreateServices(new List<RouteConfig> { route }, new List<ClusterConfig> { cluster, cluster });\n\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => manager.InitialLoadAsync());\n        Assert.Contains(\"Duplicate cluster 'cluster1'\", ex.ToString());\n    }\n\n    [Fact]\n    public async Task BuildConfig_TwoDistinctConfigs_Works()\n    {\n        const string TestAddress = \"https://localhost:123/\";\n\n        var cluster1 = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d1\", new DestinationConfig { Address = TestAddress } }\n            }\n        };\n        var route1 = new RouteConfig\n        {\n            RouteId = \"route1\",\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch { Path = \"/\" }\n        };\n\n        var cluster2 = new ClusterConfig\n        {\n            ClusterId = \"cluster2\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d2\", new DestinationConfig { Address = TestAddress } }\n            }\n        };\n        var route2 = new RouteConfig\n        {\n            RouteId = \"route2\",\n            ClusterId = \"cluster2\",\n            Match = new RouteMatch { Path = \"/\" }\n        };\n\n        var config1 = new InMemoryConfigProvider(new List<RouteConfig>() { route1 }, new List<ClusterConfig>() { cluster1 });\n        var config2 = new InMemoryConfigProvider(new List<RouteConfig>() { route2 }, new List<ClusterConfig>() { cluster2 });\n\n        var services = CreateServices(new[] { config1, config2 });\n\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n        var dataSource = await manager.InitialLoadAsync();\n\n        Assert.NotNull(dataSource);\n        var endpoints = dataSource.Endpoints;\n        Assert.Equal(2, endpoints.Count);\n\n        // The order is unstable because routes are stored in a dictionary.\n        var routeConfig = endpoints.Single(e => string.Equals(e.DisplayName, \"route1\")).Metadata.GetMetadata<RouteModel>();\n        Assert.NotNull(routeConfig);\n        Assert.Equal(\"route1\", routeConfig.Config.RouteId);\n\n        var clusterState = routeConfig.Cluster;\n        Assert.NotNull(clusterState);\n\n        Assert.Equal(\"cluster1\", clusterState.ClusterId);\n        Assert.NotNull(clusterState.Destinations);\n        Assert.NotNull(clusterState.Model);\n        Assert.NotNull(clusterState.Model.HttpClient);\n        Assert.Same(clusterState, routeConfig.Cluster);\n\n        var actualDestinations = clusterState.Destinations.Values;\n        var destination = Assert.Single(actualDestinations);\n        Assert.Equal(\"d1\", destination.DestinationId);\n        Assert.NotNull(destination.Model);\n        Assert.Equal(TestAddress, destination.Model.Config.Address);\n\n        routeConfig = endpoints.Single(e => string.Equals(e.DisplayName, \"route2\")).Metadata.GetMetadata<RouteModel>();\n        Assert.NotNull(routeConfig);\n        Assert.Equal(\"route2\", routeConfig.Config.RouteId);\n\n        clusterState = routeConfig.Cluster;\n        Assert.NotNull(clusterState);\n\n        Assert.Equal(\"cluster2\", clusterState.ClusterId);\n        Assert.NotNull(clusterState.Destinations);\n        Assert.NotNull(clusterState.Model);\n        Assert.NotNull(clusterState.Model.HttpClient);\n        Assert.Same(clusterState, routeConfig.Cluster);\n\n        actualDestinations = clusterState.Destinations.Values;\n        destination = Assert.Single(actualDestinations);\n        Assert.Equal(\"d2\", destination.DestinationId);\n        Assert.NotNull(destination.Model);\n        Assert.Equal(TestAddress, destination.Model.Config.Address);\n    }\n\n    [Fact]\n    public async Task BuildConfig_TwoOverlappingConfigs_Works()\n    {\n        const string TestAddress = \"https://localhost:123/\";\n\n        var cluster1 = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d1\", new DestinationConfig { Address = TestAddress } }\n            }\n        };\n        var cluster2 = new ClusterConfig\n        {\n            ClusterId = \"cluster2\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d2\", new DestinationConfig { Address = TestAddress } }\n            }\n        };\n\n        var route1 = new RouteConfig\n        {\n            RouteId = \"route1\",\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch { Path = \"/\" }\n        };\n        var route2 = new RouteConfig\n        {\n            RouteId = \"route2\",\n            ClusterId = \"cluster2\",\n            Match = new RouteMatch { Path = \"/\" }\n        };\n\n        var config1 = new InMemoryConfigProvider(new List<RouteConfig>() { route2 }, new List<ClusterConfig>() { cluster1 });\n        var config2 = new InMemoryConfigProvider(new List<RouteConfig>() { route1 }, new List<ClusterConfig>() { cluster2 });\n\n        var services = CreateServices(new[] { config1, config2 });\n\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n        var dataSource = await manager.InitialLoadAsync();\n\n        Assert.NotNull(dataSource);\n        var endpoints = dataSource.Endpoints;\n        Assert.Equal(2, endpoints.Count);\n\n        // The order is unstable because routes are stored in a dictionary.\n        var routeConfig = endpoints.Single(e => string.Equals(e.DisplayName, \"route1\")).Metadata.GetMetadata<RouteModel>();\n        Assert.NotNull(routeConfig);\n        Assert.Equal(\"route1\", routeConfig.Config.RouteId);\n\n        var clusterState = routeConfig.Cluster;\n        Assert.NotNull(clusterState);\n\n        Assert.Equal(\"cluster1\", clusterState.ClusterId);\n        Assert.NotNull(clusterState.Destinations);\n        Assert.NotNull(clusterState.Model);\n        Assert.NotNull(clusterState.Model.HttpClient);\n        Assert.Same(clusterState, routeConfig.Cluster);\n\n        var actualDestinations = clusterState.Destinations.Values;\n        var destination = Assert.Single(actualDestinations);\n        Assert.Equal(\"d1\", destination.DestinationId);\n        Assert.NotNull(destination.Model);\n        Assert.Equal(TestAddress, destination.Model.Config.Address);\n\n        routeConfig = endpoints.Single(e => string.Equals(e.DisplayName, \"route2\")).Metadata.GetMetadata<RouteModel>();\n        Assert.NotNull(routeConfig);\n        Assert.Equal(\"route2\", routeConfig.Config.RouteId);\n\n        clusterState = routeConfig.Cluster;\n        Assert.NotNull(clusterState);\n\n        Assert.Equal(\"cluster2\", clusterState.ClusterId);\n        Assert.NotNull(clusterState.Destinations);\n        Assert.NotNull(clusterState.Model);\n        Assert.NotNull(clusterState.Model.HttpClient);\n        Assert.Same(clusterState, routeConfig.Cluster);\n\n        actualDestinations = clusterState.Destinations.Values;\n        destination = Assert.Single(actualDestinations);\n        Assert.Equal(\"d2\", destination.DestinationId);\n        Assert.NotNull(destination.Model);\n        Assert.Equal(TestAddress, destination.Model.Config.Address);\n    }\n\n    private class FakeConfigChangeListener : IConfigChangeListener\n    {\n        public bool? HasApplyingSucceeded { get; private set; }\n        public bool DidAtLeastOneErrorOccurWhileLoading { get; private set; }\n        public string[] EventuallyLoaded;\n        public string[] SuccessfullyApplied;\n        public string[] FailedApplied;\n\n        public FakeConfigChangeListener()\n        {\n            Reset();\n        }\n\n        public void Reset()\n        {\n            DidAtLeastOneErrorOccurWhileLoading = false;\n            HasApplyingSucceeded = null;\n            EventuallyLoaded = Array.Empty<string>();\n            SuccessfullyApplied = Array.Empty<string>();\n            FailedApplied = Array.Empty<string>();\n        }\n\n        public void ConfigurationLoadingFailed(IProxyConfigProvider configProvider, Exception ex)\n        {\n            DidAtLeastOneErrorOccurWhileLoading = true;\n        }\n\n        public void ConfigurationLoaded(IReadOnlyList<IProxyConfig> proxyConfigs)\n        {\n            EventuallyLoaded = proxyConfigs.Select(c => c.RevisionId).ToArray();\n        }\n\n        public void ConfigurationApplyingFailed(IReadOnlyList<IProxyConfig> proxyConfigs, Exception ex)\n        {\n            HasApplyingSucceeded = false;\n            FailedApplied = proxyConfigs.Select(c => c.RevisionId).ToArray();\n        }\n\n        public void ConfigurationApplied(IReadOnlyList<IProxyConfig> proxyConfigs)\n        {\n            HasApplyingSucceeded = true;\n            SuccessfullyApplied = proxyConfigs.Select(c => c.RevisionId).ToArray();\n        }\n    }\n\n    private class ConfigChangeListenerCounter : IConfigChangeListener\n    {\n        public int NumberOfLoadedConfigurations { get; private set; }\n        public int NumberOfFailedConfigurationLoads { get; private set; }\n        public int NumberOfAppliedConfigurations { get; private set; }\n        public int NumberOfFailedConfigurationApplications { get; private set; }\n\n        public ConfigChangeListenerCounter()\n        {\n            Reset();\n        }\n\n        public void Reset()\n        {\n            NumberOfLoadedConfigurations = 0;\n            NumberOfFailedConfigurationLoads = 0;\n            NumberOfAppliedConfigurations = 0;\n            NumberOfFailedConfigurationApplications = 0;\n        }\n\n        public void ConfigurationLoadingFailed(IProxyConfigProvider configProvider, Exception ex)\n        {\n            NumberOfFailedConfigurationLoads++;\n        }\n\n        public void ConfigurationLoaded(IReadOnlyList<IProxyConfig> proxyConfigs)\n        {\n            NumberOfLoadedConfigurations += proxyConfigs.Count;\n        }\n\n        public void ConfigurationApplyingFailed(IReadOnlyList<IProxyConfig> proxyConfigs, Exception ex)\n        {\n            NumberOfFailedConfigurationApplications += proxyConfigs.Count;\n        }\n\n        public void ConfigurationApplied(IReadOnlyList<IProxyConfig> proxyConfigs)\n        {\n            NumberOfAppliedConfigurations += proxyConfigs.Count;\n        }\n    }\n\n    private class InMemoryConfig : IProxyConfig\n    {\n        private readonly CancellationTokenSource _cts = new CancellationTokenSource();\n\n        public InMemoryConfig(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters, string revisionId)\n        {\n            RevisionId = revisionId;\n            Routes = routes;\n            Clusters = clusters;\n            ChangeToken = new CancellationChangeToken(_cts.Token);\n        }\n\n        public string RevisionId { get; }\n\n        public IReadOnlyList<RouteConfig> Routes { get; }\n\n        public IReadOnlyList<ClusterConfig> Clusters { get; }\n\n        public IChangeToken ChangeToken { get; }\n\n        internal void SignalChange()\n        {\n            _cts.Cancel();\n        }\n    }\n\n    private class OnDemandFailingInMemoryConfigProvider : IProxyConfigProvider\n    {\n        private volatile InMemoryConfig _config;\n\n        public OnDemandFailingInMemoryConfigProvider(\n            InMemoryConfig config)\n        {\n            _config = config;\n        }\n\n        public OnDemandFailingInMemoryConfigProvider(\n            IReadOnlyList<RouteConfig> routes,\n            IReadOnlyList<ClusterConfig> clusters,\n            string revisionId) : this(new InMemoryConfig(routes, clusters, revisionId))\n        {\n        }\n\n        public IProxyConfig GetConfig()\n        {\n            if (ShouldConfigLoadingFail)\n            {\n                return null;\n            }\n\n            return _config;\n        }\n\n        public void Update(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters, string revisionId)\n        {\n            Update(new InMemoryConfig(routes, clusters, revisionId));\n        }\n\n        public void Update(InMemoryConfig config)\n        {\n            var oldConfig = Interlocked.Exchange(ref _config, config);\n            oldConfig.SignalChange();\n        }\n\n        public bool ShouldConfigLoadingFail { get; set; }\n    }\n\n    [Fact]\n    public async Task BuildConfig_CanBeNotifiedOfProxyConfigSuccessfulAndFailedLoading()\n    {\n        var configProviderA = new OnDemandFailingInMemoryConfigProvider(new List<RouteConfig>() { }, new List<ClusterConfig>() { }, \"A1\");\n        var configProviderB = new OnDemandFailingInMemoryConfigProvider(new List<RouteConfig>() { }, new List<ClusterConfig>() { }, \"B1\");\n\n        var configChangeListenerCounter = new ConfigChangeListenerCounter();\n        var fakeConfigChangeListener = new FakeConfigChangeListener();\n\n        var services = CreateServices(new[] { configProviderA, configProviderB }, null, new IConfigChangeListener[] { fakeConfigChangeListener, configChangeListenerCounter });\n\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n        await manager.InitialLoadAsync();\n\n        Assert.Equal(2, configChangeListenerCounter.NumberOfLoadedConfigurations);\n        Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationLoads);\n        Assert.Equal(2, configChangeListenerCounter.NumberOfAppliedConfigurations);\n        Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationApplications);\n\n        Assert.False(fakeConfigChangeListener.DidAtLeastOneErrorOccurWhileLoading);\n        Assert.Equal(new[] { \"A1\", \"B1\" }, fakeConfigChangeListener.EventuallyLoaded);\n        Assert.True(fakeConfigChangeListener.HasApplyingSucceeded);\n        Assert.Equal(new[] { \"A1\", \"B1\" }, fakeConfigChangeListener.SuccessfullyApplied);\n        Assert.Empty(fakeConfigChangeListener.FailedApplied);\n\n        const string TestAddress = \"https://localhost:123/\";\n\n        var cluster1 = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d1\", new DestinationConfig { Address = TestAddress } }\n            }\n        };\n        var cluster2 = new ClusterConfig\n        {\n            ClusterId = \"cluster2\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d2\", new DestinationConfig { Address = TestAddress } }\n            }\n        };\n\n        var route1 = new RouteConfig\n        {\n            RouteId = \"route1\",\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch { Path = \"/\" }\n        };\n        var route2 = new RouteConfig\n        {\n            RouteId = \"route2\",\n            ClusterId = \"cluster2\",\n            Match = new RouteMatch { Path = \"/\" }\n        };\n\n        fakeConfigChangeListener.Reset();\n        configChangeListenerCounter.Reset();\n\n        configProviderA.Update(new List<RouteConfig>() { route1 }, new List<ClusterConfig>() { cluster1 }, \"A2\");\n\n        Assert.Equal(2, configChangeListenerCounter.NumberOfLoadedConfigurations);\n        Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationLoads);\n        Assert.Equal(2, configChangeListenerCounter.NumberOfAppliedConfigurations);\n        Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationApplications);\n\n        Assert.False(fakeConfigChangeListener.DidAtLeastOneErrorOccurWhileLoading);\n        Assert.Equal(new[] { \"A2\", \"B1\" }, fakeConfigChangeListener.EventuallyLoaded);\n        Assert.True(fakeConfigChangeListener.HasApplyingSucceeded);\n        Assert.Equal(new[] { \"A2\", \"B1\" }, fakeConfigChangeListener.SuccessfullyApplied);\n        Assert.Empty(fakeConfigChangeListener.FailedApplied);\n\n        configProviderB.ShouldConfigLoadingFail = true;\n\n        fakeConfigChangeListener.Reset();\n        configChangeListenerCounter.Reset();\n\n        configProviderB.Update(new List<RouteConfig>() { route2 }, new List<ClusterConfig>() { cluster2 }, \"B2\");\n\n        Assert.Equal(2, configChangeListenerCounter.NumberOfLoadedConfigurations);\n        Assert.Equal(1, configChangeListenerCounter.NumberOfFailedConfigurationLoads);\n        Assert.Equal(2, configChangeListenerCounter.NumberOfAppliedConfigurations);\n        Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationApplications);\n\n        Assert.True(fakeConfigChangeListener.DidAtLeastOneErrorOccurWhileLoading);\n        Assert.Equal(new[] { \"A2\", \"B1\" }, fakeConfigChangeListener.EventuallyLoaded);\n        Assert.True(fakeConfigChangeListener.HasApplyingSucceeded);\n        Assert.Equal(new[] { \"A2\", \"B1\" }, fakeConfigChangeListener.SuccessfullyApplied);\n        Assert.Empty(fakeConfigChangeListener.FailedApplied);\n    }\n\n    [Fact]\n    public async Task BuildConfig_CanBeNotifiedOfProxyConfigSuccessfulAndFailedUpdating()\n    {\n        var configProviderA = new InMemoryConfigProvider(new List<RouteConfig>() { }, new List<ClusterConfig>() { }, \"A1\");\n        var configProviderB = new InMemoryConfigProvider(new List<RouteConfig>() { }, new List<ClusterConfig>() { }, \"B1\");\n\n        var configChangeListenerCounter = new ConfigChangeListenerCounter();\n        var fakeConfigChangeListener = new FakeConfigChangeListener();\n\n        var services = CreateServices(new[] { configProviderA, configProviderB }, null, new IConfigChangeListener[] { fakeConfigChangeListener, configChangeListenerCounter });\n\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n        await manager.InitialLoadAsync();\n\n        Assert.Equal(2, configChangeListenerCounter.NumberOfLoadedConfigurations);\n        Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationLoads);\n        Assert.Equal(2, configChangeListenerCounter.NumberOfAppliedConfigurations);\n        Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationApplications);\n\n        Assert.False(fakeConfigChangeListener.DidAtLeastOneErrorOccurWhileLoading);\n        Assert.Equal(new[] { \"A1\", \"B1\" }, fakeConfigChangeListener.EventuallyLoaded);\n        Assert.True(fakeConfigChangeListener.HasApplyingSucceeded);\n        Assert.Equal(new[] { \"A1\", \"B1\" }, fakeConfigChangeListener.SuccessfullyApplied);\n        Assert.Empty(fakeConfigChangeListener.FailedApplied);\n\n        const string TestAddress = \"https://localhost:123/\";\n\n        var cluster1 = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d1\", new DestinationConfig { Address = TestAddress } }\n            }\n        };\n        var cluster2 = new ClusterConfig\n        {\n            ClusterId = \"cluster2\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d2\", new DestinationConfig { Address = TestAddress } }\n            }\n        };\n\n        var route1 = new RouteConfig\n        {\n            RouteId = \"route1\",\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch { Path = \"/\" }\n        };\n        var route2 = new RouteConfig\n        {\n            RouteId = \"route2\",\n            ClusterId = \"cluster2\",\n            // Missing Match here will be caught by the analysis\n        };\n\n        fakeConfigChangeListener.Reset();\n        configChangeListenerCounter.Reset();\n\n        configProviderA.Update(new List<RouteConfig>() { route1 }, new List<ClusterConfig>() { cluster1 }, \"A2\");\n\n        Assert.Equal(2, configChangeListenerCounter.NumberOfLoadedConfigurations);\n        Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationLoads);\n        Assert.Equal(2, configChangeListenerCounter.NumberOfAppliedConfigurations);\n        Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationApplications);\n\n        Assert.False(fakeConfigChangeListener.DidAtLeastOneErrorOccurWhileLoading);\n        Assert.Equal(new[] { \"A2\", \"B1\" }, fakeConfigChangeListener.EventuallyLoaded);\n        Assert.True(fakeConfigChangeListener.HasApplyingSucceeded);\n        Assert.Equal(new[] { \"A2\", \"B1\" }, fakeConfigChangeListener.SuccessfullyApplied);\n        Assert.Empty(fakeConfigChangeListener.FailedApplied);\n\n        fakeConfigChangeListener.Reset();\n        configChangeListenerCounter.Reset();\n\n        configProviderB.Update(new List<RouteConfig>() { route2 }, new List<ClusterConfig>() { cluster2 }, \"B2\");\n\n        Assert.Equal(2, configChangeListenerCounter.NumberOfLoadedConfigurations);\n        Assert.Equal(0, configChangeListenerCounter.NumberOfFailedConfigurationLoads);\n        Assert.Equal(0, configChangeListenerCounter.NumberOfAppliedConfigurations);\n        Assert.Equal(2, configChangeListenerCounter.NumberOfFailedConfigurationApplications);\n\n        Assert.False(fakeConfigChangeListener.DidAtLeastOneErrorOccurWhileLoading);\n        Assert.Equal(new[] { \"A2\", \"B2\" }, fakeConfigChangeListener.EventuallyLoaded);\n        Assert.False(fakeConfigChangeListener.HasApplyingSucceeded);\n        Assert.Empty(fakeConfigChangeListener.SuccessfullyApplied);\n        Assert.Equal(new[] { \"A2\", \"B2\" }, fakeConfigChangeListener.FailedApplied);\n    }\n\n    public class DummyProxyConfig : IProxyConfig\n    {\n        public IReadOnlyList<RouteConfig> Routes => throw new NotImplementedException();\n        public IReadOnlyList<ClusterConfig> Clusters => throw new NotImplementedException();\n        public IChangeToken ChangeToken => throw new NotImplementedException();\n    }\n\n    [Fact]\n    public void IProxyConfigDerivedTypes_RevisionIdIsAutomaticallySet()\n    {\n        IProxyConfig config = new DummyProxyConfig();\n        Assert.NotNull(config.RevisionId);\n        Assert.NotEmpty(config.RevisionId);\n        Assert.Same(config.RevisionId, config.RevisionId);\n    }\n\n    [Fact]\n    public async Task InitialLoadAsync_ProxyHttpClientOptionsSet_CreateAndSetHttpClient()\n    {\n        const string TestAddress = \"https://localhost:123/\";\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d1\", new DestinationConfig { Address = TestAddress } }\n            },\n            HttpClient = new HttpClientConfig\n            {\n                SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12,\n                MaxConnectionsPerServer = 10,\n                RequestHeaderEncoding = Encoding.UTF8.WebName,\n                ResponseHeaderEncoding = Encoding.UTF8.WebName,\n            },\n            HealthCheck = new HealthCheckConfig\n            {\n                Active = new ActiveHealthCheckConfig { Enabled = true }\n            }\n        };\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch { Path = \"/\" }\n        };\n\n        var services = CreateServices(new List<RouteConfig>() { route }, new List<ClusterConfig>() { cluster });\n\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n        var dataSource = await manager.InitialLoadAsync();\n\n        Assert.NotNull(dataSource);\n        var endpoint = Assert.Single(dataSource.Endpoints);\n        var routeConfig = endpoint.Metadata.GetMetadata<RouteModel>();\n        var clusterState = routeConfig.Cluster;\n        Assert.Equal(\"cluster1\", clusterState.ClusterId);\n        var clusterModel = clusterState.Model;\n        Assert.NotNull(clusterModel.HttpClient);\n        Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, clusterModel.Config.HttpClient.SslProtocols);\n        Assert.Equal(10, clusterModel.Config.HttpClient.MaxConnectionsPerServer);\n        Assert.Equal(Encoding.UTF8.WebName, clusterModel.Config.HttpClient.RequestHeaderEncoding);\n        Assert.Equal(Encoding.UTF8.WebName, clusterModel.Config.HttpClient.ResponseHeaderEncoding);\n\n        var handler = ForwarderHttpClientFactoryTests.GetHandler(clusterModel.HttpClient);\n        Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, handler.SslOptions.EnabledSslProtocols);\n        Assert.Equal(10, handler.MaxConnectionsPerServer);\n        Assert.Equal(Encoding.UTF8, handler.RequestHeaderEncodingSelector(default, default));\n        var activeMonitor = (ActiveHealthCheckMonitor)services.GetRequiredService<IActiveHealthCheckMonitor>();\n        Assert.True(activeMonitor.Scheduler.IsScheduled(clusterState));\n    }\n\n    [Fact]\n    public async Task GetChangeToken_SignalsChange()\n    {\n        var services = CreateServices(new List<RouteConfig>(), new List<ClusterConfig>());\n        var inMemoryConfig = (InMemoryConfigProvider)services.GetRequiredService<IProxyConfigProvider>();\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n        var dataSource = await configManager.InitialLoadAsync();\n        _ = configManager.Endpoints; // Lazily creates endpoints the first time, activates change notifications.\n\n        var signaled1 = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n        var signaled2 = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n        IReadOnlyList<Endpoint> readEndpoints1 = null;\n        IReadOnlyList<Endpoint> readEndpoints2 = null;\n\n        var changeToken1 = dataSource.GetChangeToken();\n        changeToken1.RegisterChangeCallback(\n            _ =>\n            {\n                readEndpoints1 = dataSource.Endpoints;\n                signaled1.SetResult(1);\n            }, null);\n\n        // updating should signal the current change token\n        Assert.False(signaled1.Task.IsCompleted);\n        inMemoryConfig.Update(new List<RouteConfig>() { new RouteConfig() { RouteId = \"r1\", Match = new RouteMatch { Path = \"/\" } } }, new List<ClusterConfig>());\n        await signaled1.Task.DefaultTimeout();\n\n        var changeToken2 = dataSource.GetChangeToken();\n        changeToken2.RegisterChangeCallback(\n            _ =>\n            {\n                readEndpoints2 = dataSource.Endpoints;\n                signaled2.SetResult(1);\n            }, null);\n\n        // updating again should only signal the new change token\n        Assert.False(signaled2.Task.IsCompleted);\n        inMemoryConfig.Update(new List<RouteConfig>() { new RouteConfig() { RouteId = \"r2\", Match = new RouteMatch { Path = \"/\" } } }, new List<ClusterConfig>());\n        await signaled2.Task.DefaultTimeout();\n\n        Assert.NotNull(readEndpoints1);\n        Assert.NotNull(readEndpoints2);\n    }\n\n    [Fact]\n    public async Task GetChangeToken_MultipleConfigs_SignalsChange()\n    {\n        var config1 = new InMemoryConfigProvider(new List<RouteConfig>(), new List<ClusterConfig>());\n        var config2 = new InMemoryConfigProvider(new List<RouteConfig>(), new List<ClusterConfig>());\n        var services = CreateServices(new[] { config1, config2 });\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n        var dataSource = await configManager.InitialLoadAsync();\n        _ = configManager.Endpoints; // Lazily creates endpoints the first time, activates change notifications.\n\n        var signaled1 = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n        var signaled2 = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n        IReadOnlyList<Endpoint> readEndpoints1 = null;\n        IReadOnlyList<Endpoint> readEndpoints2 = null;\n\n        var changeToken1 = dataSource.GetChangeToken();\n        changeToken1.RegisterChangeCallback(\n            _ =>\n            {\n                readEndpoints1 = dataSource.Endpoints;\n                signaled1.SetResult(1);\n            }, null);\n\n        // updating should signal the current change token\n        Assert.False(signaled1.Task.IsCompleted);\n        config1.Update(new List<RouteConfig>() { new RouteConfig() { RouteId = \"r1\", Match = new RouteMatch { Path = \"/\" } } }, new List<ClusterConfig>());\n        await signaled1.Task.DefaultTimeout();\n\n        var changeToken2 = dataSource.GetChangeToken();\n        changeToken2.RegisterChangeCallback(\n            _ =>\n            {\n                readEndpoints2 = dataSource.Endpoints;\n                signaled2.SetResult(1);\n            }, null);\n\n        // updating again should only signal the new change token\n        Assert.False(signaled2.Task.IsCompleted);\n        config2.Update(new List<RouteConfig>() { new RouteConfig() { RouteId = \"r2\", Match = new RouteMatch { Path = \"/\" } } }, new List<ClusterConfig>());\n        await signaled2.Task.DefaultTimeout();\n\n        var endpoint = Assert.Single(readEndpoints1);\n        Assert.Equal(\"r1\", endpoint.DisplayName);\n\n        Assert.NotNull(readEndpoints2);\n        Assert.Equal(2, readEndpoints2.Count);\n        // Ordering is unstable due to dictionary storage.\n        readEndpoints2.Single(e => string.Equals(e.DisplayName, \"r1\"));\n        readEndpoints2.Single(e => string.Equals(e.DisplayName, \"r2\"));\n    }\n\n    [Fact]\n    public async Task ChangeConfig_ActiveHealthCheckIsEnabled_RunInitialCheck()\n    {\n        var endpoints = new List<RouteConfig>() { new RouteConfig() { RouteId = \"r1\", ClusterId = \"c1\", Match = new RouteMatch { Path = \"/\" } } };\n        var clusters = new List<ClusterConfig>() { new ClusterConfig { ClusterId = \"c1\" } };\n        var services = CreateServices(endpoints, clusters);\n        var inMemoryConfig = (InMemoryConfigProvider)services.GetRequiredService<IProxyConfigProvider>();\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n        var dataSource = await configManager.InitialLoadAsync();\n\n        var endpoint = Assert.Single(dataSource.Endpoints);\n        var routeConfig = endpoint.Metadata.GetMetadata<RouteModel>();\n        var clusterState = routeConfig.Cluster;\n        var activeMonitor = (ActiveHealthCheckMonitor)services.GetRequiredService<IActiveHealthCheckMonitor>();\n        Assert.False(activeMonitor.Scheduler.IsScheduled(clusterState));\n\n        var signaled = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);\n\n        var changeToken = dataSource.GetChangeToken();\n        changeToken.RegisterChangeCallback(\n            _ =>\n            {\n                signaled.SetResult(1);\n            }, null);\n\n        // updating should signal the current change token\n        Assert.False(signaled.Task.IsCompleted);\n        inMemoryConfig.Update(\n            endpoints,\n            new List<ClusterConfig>()\n            {\n                new ClusterConfig\n                {\n                    ClusterId = \"c1\",\n                    HealthCheck = new HealthCheckConfig { Active = new ActiveHealthCheckConfig { Enabled = true } }\n                }\n            });\n        await signaled.Task.DefaultTimeout();\n\n        Assert.True(activeMonitor.Scheduler.IsScheduled(clusterState));\n    }\n\n    [Fact]\n    public async Task ChangeConfig_DestinationChange_IsReflectedOnRouteConfiguration()\n    {\n        var endpoints = new List<RouteConfig>() { new RouteConfig() { RouteId = \"r1\", ClusterId = \"c1\", Match = new RouteMatch { Path = \"/\" } } };\n        var clusters = new List<ClusterConfig>()\n        {\n            new ClusterConfig\n            {\n                ClusterId = \"c1\",\n                Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n                {\n                    { \"d1\", new DestinationConfig() { Address = \"http://d1\" } }\n                }\n            }\n        };\n        var services = CreateServices(endpoints, clusters);\n        var inMemoryConfig = (InMemoryConfigProvider)services.GetRequiredService<IProxyConfigProvider>();\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n        var dataSource = await configManager.InitialLoadAsync();\n\n        var endpoint = Assert.Single(dataSource.Endpoints);\n        var routeConfig = endpoint.Metadata.GetMetadata<RouteModel>();\n        Assert.Equal(\"http://d1\", Assert.Single(routeConfig.Cluster.Destinations).Value.Model.Config.Address);\n        Assert.Equal(1, routeConfig.Cluster.Revision);\n\n        inMemoryConfig.Update(\n            endpoints,\n            new List<ClusterConfig>()\n            {\n                new ClusterConfig\n                {\n                    ClusterId = \"c1\",\n                    Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n                    {\n                        { \"d1\", new DestinationConfig() { Address = \"http://d1-v2\" } }\n                    }\n                }\n            });\n\n        var destinationConfig = Assert.Single(routeConfig.Cluster.Destinations).Value.Model.Config;\n        Assert.Equal(\"http://d1-v2\", destinationConfig.Address);\n\n        Assert.Same(destinationConfig, Assert.Single(routeConfig.Cluster.DestinationsState.AllDestinations).Model.Config);\n        Assert.Same(destinationConfig, Assert.Single(routeConfig.Cluster.Model.Config.Destinations).Value);\n\n        // Destination changes do not affect this property\n        Assert.Equal(1, routeConfig.Cluster.Revision);\n    }\n\n    [Fact]\n    public async Task LoadAsync_RequestVersionValidationError_Throws()\n    {\n        const string TestAddress = \"https://localhost:123/\";\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d1\", new DestinationConfig { Address = TestAddress } }\n            },\n            HttpRequest = new ForwarderRequestConfig() { Version = new Version(1, 2) }\n        };\n\n        var services = CreateServices(new List<RouteConfig>(), new List<ClusterConfig>() { cluster });\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n\n        var ioEx = await Assert.ThrowsAsync<InvalidOperationException>(() => configManager.InitialLoadAsync());\n        Assert.Equal(\"Unable to load or apply the proxy configuration.\", ioEx.Message);\n        var agex = Assert.IsType<AggregateException>(ioEx.InnerException);\n\n        Assert.Single(agex.InnerExceptions);\n        var argex = Assert.IsType<ArgumentException>(agex.InnerExceptions.First());\n        Assert.StartsWith(\"Outgoing request version\", argex.Message);\n    }\n\n    [Fact]\n    public async Task LoadAsync_RouteValidationError_Throws()\n    {\n        var routeName = \"route1\";\n        var route1 = new RouteConfig { RouteId = routeName, Match = new RouteMatch { Hosts = null }, ClusterId = \"cluster1\" };\n        var services = CreateServices(new List<RouteConfig>() { route1 }, new List<ClusterConfig>());\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n\n        var ioEx = await Assert.ThrowsAsync<InvalidOperationException>(() => configManager.InitialLoadAsync());\n        Assert.Equal(\"Unable to load or apply the proxy configuration.\", ioEx.Message);\n        var agex = Assert.IsType<AggregateException>(ioEx.InnerException);\n\n        Assert.Single(agex.InnerExceptions);\n        var argex = Assert.IsType<ArgumentException>(agex.InnerExceptions.First());\n        Assert.StartsWith($\"Route '{routeName}' requires Hosts or Path specified\", argex.Message);\n    }\n\n    [Fact]\n    public async Task LoadAsync_MultipleSourcesWithValidationErrors_Throws()\n    {\n        var route1 = new RouteConfig { RouteId = \"route1\", Match = new RouteMatch { Hosts = null }, ClusterId = \"cluster1\" };\n        var provider1 = new InMemoryConfigProvider(new List<RouteConfig>() { route1 }, new List<ClusterConfig>());\n        var cluster1 = new ClusterConfig { ClusterId = \"cluster1\", HttpClient = new HttpClientConfig { MaxConnectionsPerServer = -1 } };\n        var provider2 = new InMemoryConfigProvider(new List<RouteConfig>(), new List<ClusterConfig>() { cluster1 });\n        var services = CreateServices(new[] { provider1, provider2 });\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n\n        var ioEx = await Assert.ThrowsAsync<InvalidOperationException>(() => configManager.InitialLoadAsync());\n        Assert.Equal(\"Unable to load or apply the proxy configuration.\", ioEx.Message);\n        var agex = Assert.IsType<AggregateException>(ioEx.InnerException);\n\n        Assert.Equal(2, agex.InnerExceptions.Count);\n        var argex = Assert.IsType<ArgumentException>(agex.InnerExceptions.First());\n        Assert.StartsWith($\"Route 'route1' requires Hosts or Path specified\", argex.Message);\n        argex = Assert.IsType<ArgumentException>(agex.InnerExceptions.Skip(1).First());\n        Assert.StartsWith($\"Max connections per server limit set on the cluster 'cluster1' must be positive.\", argex.Message);\n    }\n\n    [Fact]\n    public async Task LoadAsync_ConfigFilterRouteActions_CanFixBrokenRoute()\n    {\n        var route1 = new RouteConfig { RouteId = \"route1\", Match = new RouteMatch { Hosts = null }, Order = 1, ClusterId = \"cluster1\" };\n        var services = CreateServices(new List<RouteConfig>() { route1 }, new List<ClusterConfig>(), proxyBuilder =>\n        {\n            proxyBuilder.AddConfigFilter<FixRouteHostFilter>();\n        });\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n\n        var dataSource = await configManager.InitialLoadAsync();\n        var endpoints = dataSource.Endpoints;\n\n        Assert.Single(endpoints);\n        var endpoint = endpoints.Single();\n        Assert.Same(route1.RouteId, endpoint.DisplayName);\n        var hostMetadata = endpoint.Metadata.GetMetadata<HostAttribute>();\n        Assert.NotNull(hostMetadata);\n        var host = Assert.Single(hostMetadata.Hosts);\n        Assert.Equal(\"example.com\", host);\n    }\n\n    private class FixRouteHostFilter : IProxyConfigFilter\n    {\n        public ValueTask<ClusterConfig> ConfigureClusterAsync(ClusterConfig cluster, CancellationToken cancel)\n        {\n            return new ValueTask<ClusterConfig>(cluster);\n        }\n\n        public ValueTask<RouteConfig> ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel)\n        {\n            return new ValueTask<RouteConfig>(route with\n            {\n                Match = route.Match with { Hosts = new[] { \"example.com\" } }\n            });\n        }\n    }\n\n    private class ClusterAndRouteFilter : IProxyConfigFilter\n    {\n        public ValueTask<ClusterConfig> ConfigureClusterAsync(ClusterConfig cluster, CancellationToken cancel)\n        {\n            return new ValueTask<ClusterConfig>(cluster with\n            {\n                HealthCheck = new HealthCheckConfig()\n                {\n                    Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(12), Policy = \"activePolicyA\" }\n                }\n            });\n        }\n\n        public ValueTask<RouteConfig> ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel)\n        {\n            string order;\n            if (cluster is not null)\n            {\n                order = cluster.Metadata[\"Order\"];\n            }\n            else\n            {\n                order = \"12\";\n            }\n\n            return new ValueTask<RouteConfig>(route with { Order = int.Parse(order) });\n        }\n    }\n\n    [Fact]\n    public async Task LoadAsync_ConfigFilterConfiguresCluster_Works()\n    {\n        var route1 = new RouteConfig\n        {\n            RouteId = \"route1\",\n            ClusterId = \"cluster1\",\n            Match = new RouteMatch { Path = \"/\" }\n        };\n        var route2 = new RouteConfig\n        {\n            RouteId = \"route2\",\n            ClusterId = \"cluster2\",\n            Match = new RouteMatch { Path = \"/\" }\n        };\n        var cluster = new ClusterConfig()\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d1\", new DestinationConfig() { Address = \"http://localhost\" } }\n            },\n            Metadata = new Dictionary<string, string>\n            {\n                [\"Order\"] = \"47\"\n            }\n        };\n        var services = CreateServices(new List<RouteConfig>() { route1, route2 }, new List<ClusterConfig>() { cluster }, proxyBuilder =>\n        {\n            proxyBuilder.AddConfigFilter<ClusterAndRouteFilter>();\n        });\n        var manager = services.GetRequiredService<ProxyConfigManager>();\n        var dataSource = await manager.InitialLoadAsync();\n\n        Assert.NotNull(dataSource);\n        Assert.Equal(2, dataSource.Endpoints.Count);\n\n        var endpoint1 = Assert.Single(dataSource.Endpoints, e => e.DisplayName == \"route1\");\n        var routeConfig1 = endpoint1.Metadata.GetMetadata<RouteModel>();\n        Assert.Equal(47, routeConfig1.Config.Order);\n        var clusterState1 = routeConfig1.Cluster;\n        Assert.NotNull(clusterState1);\n        Assert.True(clusterState1.Model.Config.HealthCheck.Active.Enabled);\n        Assert.Equal(TimeSpan.FromSeconds(12), clusterState1.Model.Config.HealthCheck.Active.Interval);\n        var destination = Assert.Single(clusterState1.DestinationsState.AllDestinations);\n        Assert.Equal(\"http://localhost\", destination.Model.Config.Address);\n\n        var endpoint2 = Assert.Single(dataSource.Endpoints, e => e.DisplayName == \"route2\");\n        var routeConfig2 = endpoint2.Metadata.GetMetadata<RouteModel>();\n        Assert.Equal(12, routeConfig2.Config.Order);\n    }\n\n    private class ClusterAndRouteThrows : IProxyConfigFilter\n    {\n        public ValueTask<ClusterConfig> ConfigureClusterAsync(ClusterConfig cluster, CancellationToken cancel)\n        {\n            throw new NotFiniteNumberException(\"Test exception\");\n        }\n\n        public ValueTask<RouteConfig> ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel)\n        {\n            throw new NotFiniteNumberException(\"Test exception\");\n        }\n    }\n\n    [Fact]\n    public async Task LoadAsync_ConfigFilterClusterActionThrows_Throws()\n    {\n        var cluster = new ClusterConfig()\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d1\", new DestinationConfig() { Address = \"http://localhost\" } }\n            }\n        };\n        var services = CreateServices(new List<RouteConfig>(), new List<ClusterConfig>() { cluster }, proxyBuilder =>\n        {\n            proxyBuilder.AddConfigFilter<ClusterAndRouteThrows>();\n            proxyBuilder.AddConfigFilter<ClusterAndRouteThrows>();\n        });\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n\n        var ioEx = await Assert.ThrowsAsync<InvalidOperationException>(() => configManager.InitialLoadAsync());\n        Assert.Equal(\"Unable to load or apply the proxy configuration.\", ioEx.Message);\n        var agex = Assert.IsType<AggregateException>(ioEx.InnerException);\n\n        Assert.Single(agex.InnerExceptions);\n        Assert.IsType<NotFiniteNumberException>(agex.InnerExceptions.First().InnerException);\n    }\n\n    [Fact]\n    public async Task LoadAsync_ConfigFilterRouteActionThrows_Throws()\n    {\n        var route1 = new RouteConfig { RouteId = \"route1\", Match = new RouteMatch { Hosts = new[] { \"example.com\" } }, Order = 1, ClusterId = \"cluster1\" };\n        var route2 = new RouteConfig { RouteId = \"route2\", Match = new RouteMatch { Hosts = new[] { \"example2.com\" } }, Order = 1, ClusterId = \"cluster2\" };\n        var services = CreateServices(new List<RouteConfig>() { route1, route2 }, new List<ClusterConfig>(), proxyBuilder =>\n        {\n            proxyBuilder.AddConfigFilter<ClusterAndRouteThrows>();\n            proxyBuilder.AddConfigFilter<ClusterAndRouteThrows>();\n        });\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n\n        var ioEx = await Assert.ThrowsAsync<InvalidOperationException>(() => configManager.InitialLoadAsync());\n        Assert.Equal(\"Unable to load or apply the proxy configuration.\", ioEx.Message);\n        var agex = Assert.IsType<AggregateException>(ioEx.InnerException);\n\n        Assert.Equal(2, agex.InnerExceptions.Count);\n        Assert.IsType<NotFiniteNumberException>(agex.InnerExceptions.First().InnerException);\n        Assert.IsType<NotFiniteNumberException>(agex.InnerExceptions.Skip(1).First().InnerException);\n    }\n\n    private class FakeDestinationResolver : IDestinationResolver\n    {\n        private readonly Func<IReadOnlyDictionary<string, DestinationConfig>, CancellationToken, ValueTask<ResolvedDestinationCollection>> _delegate;\n\n        public FakeDestinationResolver(\n            Func<IReadOnlyDictionary<string, DestinationConfig>, CancellationToken, ValueTask<ResolvedDestinationCollection>> @delegate)\n        {\n            _delegate = @delegate;\n        }\n\n        public ValueTask<ResolvedDestinationCollection> ResolveDestinationsAsync(IReadOnlyDictionary<string, DestinationConfig> destinations, CancellationToken cancellationToken)\n            => _delegate(destinations, cancellationToken);\n    }\n\n    private class TestConfigChangeListener : IConfigChangeListener\n    {\n        private readonly bool _includeLoad;\n        private readonly bool _includeApply;\n\n        public Channel<ConfigChangeListenerEvent> Events { get; } = Channel.CreateUnbounded<ConfigChangeListenerEvent>();\n\n        public TestConfigChangeListener(bool includeLoad = true, bool includeApply = true)\n        {\n            _includeLoad = includeLoad;\n            _includeApply = includeApply;\n        }\n\n        public void ConfigurationApplied(IReadOnlyList<IProxyConfig> proxyConfigs)\n        {\n            if (!_includeApply)\n            {\n                return;\n            }\n\n            Assert.True(Events.Writer.TryWrite(new ConfigurationAppliedEvent(proxyConfigs)));\n        }\n\n        public void ConfigurationApplyingFailed(IReadOnlyList<IProxyConfig> proxyConfigs, Exception exception)\n        {\n            if (!_includeApply)\n            {\n                return;\n            }\n\n            Assert.True(Events.Writer.TryWrite(new ConfigurationApplyingFailedEvent(proxyConfigs, exception)));\n        }\n\n        public void ConfigurationLoaded(IReadOnlyList<IProxyConfig> proxyConfigs)\n        {\n            if (!_includeLoad)\n            {\n                return;\n            }\n\n            Assert.True(Events.Writer.TryWrite(new ConfigurationLoadedEvent(proxyConfigs)));\n        }\n\n        public void ConfigurationLoadingFailed(IProxyConfigProvider configProvider, Exception exception)\n        {\n            if (!_includeLoad)\n            {\n                return;\n            }\n\n            Assert.True(Events.Writer.TryWrite(new ConfigurationLoadingFailedEvent(configProvider, exception)));\n        }\n\n        public record ConfigChangeListenerEvent { };\n        public record ConfigurationAppliedEvent(IReadOnlyList<IProxyConfig> ProxyConfigs) : ConfigChangeListenerEvent;\n        public record ConfigurationApplyingFailedEvent(IReadOnlyList<IProxyConfig> ProxyConfigs, Exception exception) : ConfigChangeListenerEvent;\n        public record ConfigurationLoadedEvent(IReadOnlyList<IProxyConfig> ProxyConfigs) : ConfigChangeListenerEvent;\n        public record ConfigurationLoadingFailedEvent(IProxyConfigProvider ConfigProvider, Exception Exception) : ConfigChangeListenerEvent;\n    }\n\n    [Fact]\n    public async Task LoadAsync_DestinationResolver_Initial_ThrowsSync()\n    {\n        var throwResolver = new FakeDestinationResolver((destinations, cancellation) => throw new InvalidOperationException(\"Throwing!\"));\n\n        var cluster = new ClusterConfig()\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d1\", new DestinationConfig() { Address = \"http://localhost\" } }\n            }\n        };\n        var services = CreateServices(\n            new List<RouteConfig>(),\n            new List<ClusterConfig>() { cluster },\n            destinationResolver: throwResolver);\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n\n        var ioEx = await Assert.ThrowsAsync<InvalidOperationException>(() => configManager.InitialLoadAsync());\n        Assert.Equal(\"Unable to load or apply the proxy configuration.\", ioEx.Message);\n\n        var innerExc = Assert.IsType<InvalidOperationException>(ioEx.InnerException);\n        Assert.Equal(\"Throwing!\", innerExc.Message);\n    }\n\n    [Fact]\n    public async Task LoadAsync_DestinationResolver_Initial_ThrowsAsync()\n    {\n        var throwResolver = new FakeDestinationResolver((destinations, cancellation) => ValueTask.FromException<ResolvedDestinationCollection>(new InvalidOperationException(\"Throwing!\")));\n\n        var cluster = new ClusterConfig()\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d1\", new DestinationConfig() { Address = \"http://localhost\" } }\n            }\n        };\n        var services = CreateServices(new List<RouteConfig>(), new List<ClusterConfig>() { cluster }, destinationResolver: throwResolver);\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n\n        var ioEx = await Assert.ThrowsAsync<InvalidOperationException>(() => configManager.InitialLoadAsync());\n        Assert.Equal(\"Unable to load or apply the proxy configuration.\", ioEx.Message);\n\n        var innerExc1 = Assert.IsType<InvalidOperationException>(ioEx.InnerException);\n        Assert.Equal(\"Error resolving destinations for cluster cluster1\", innerExc1.Message);\n        var innerExc2 = Assert.IsType<InvalidOperationException>(innerExc1.InnerException);\n        Assert.Equal(\"Throwing!\", innerExc2.Message);\n    }\n\n    [Fact]\n    public async Task LoadAsync_DestinationResolver_Successful()\n    {\n        var destinationsToExpand = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n        {\n            { \"d1\", new DestinationConfig() { Address = \"http://localhost\" } }\n        };\n\n        var syncExpandResolver = new FakeDestinationResolver((destinations, cancellation) =>\n        {\n            var expandedDestinations = new Dictionary<string, DestinationConfig>();\n\n            foreach (var destKvp in destinations)\n            {\n                expandedDestinations[$\"{destKvp.Key}-1\"] = new DestinationConfig { Address = \"http://127.0.0.1:8080\" };\n                expandedDestinations[$\"{destKvp.Key}-2\"] = new DestinationConfig { Address = \"http://127.1.1.1:8080\" };\n            }\n\n            var result = new ResolvedDestinationCollection(expandedDestinations, null);\n            return new(result);\n        });\n\n        var cluster1 = new ClusterConfig()\n        {\n            ClusterId = \"cluster1\",\n            Destinations = destinationsToExpand\n        };\n\n        var services = CreateServices(new List<RouteConfig>(), new List<ClusterConfig>() { cluster1 }, destinationResolver: syncExpandResolver);\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n\n        await configManager.InitialLoadAsync();\n\n        Assert.True(configManager.TryGetCluster(cluster1.ClusterId, out var cluster));\n\n        var expectedDestinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n        {\n            { \"d1-1\", new DestinationConfig() { Address = \"http://127.0.0.1:8080\" } },\n            { \"d1-2\", new DestinationConfig() { Address = \"http://127.1.1.1:8080\" } }\n        };\n\n        var actualDestinations = cluster.Destinations.ToDictionary(static k => k.Key, static v => v.Value.Model.Config);\n        Assert.Equal(expectedDestinations, actualDestinations);\n    }\n\n    [Fact]\n    public async Task LoadAsync_DestinationResolver_Dns()\n    {\n        var destinationsToExpand = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n        {\n            { \"d1\", new DestinationConfig() { Address = \"http://localhost/a/b/c\", Health = \"http://localhost/healthz\" } },\n            { \"d2\", new DestinationConfig() { Address = \"http://localhost:8080/a/b/c\", Health = \"http://localhost:8080/healthz\"} },\n            { \"d3\", new DestinationConfig() { Address = \"https://localhost/a/b/c\", Health = \"https://localhost/healthz\" } },\n            { \"d4\", new DestinationConfig() { Address = \"https://localhost:8443/a/b/c\", Health = \"https://localhost:8443/healthz\", Host = \"overriddenhost\" } }\n        };\n\n        var cluster1 = new ClusterConfig()\n        {\n            ClusterId = \"cluster1\",\n            Destinations = destinationsToExpand\n        };\n\n        var services = CreateServices(\n            new List<RouteConfig>(),\n            new List<ClusterConfig>() { cluster1 },\n            configureProxy: rp => rp.AddDnsDestinationResolver(o => o.AddressFamily = AddressFamily.InterNetwork));\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n\n        await configManager.InitialLoadAsync();\n\n        Assert.True(configManager.TryGetCluster(cluster1.ClusterId, out var cluster));\n\n        var expectedDestinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n        {\n            { \"d1[127.0.0.1]\", new DestinationConfig() { Address = \"http://127.0.0.1/a/b/c\", Health = \"http://127.0.0.1/healthz\", Host = \"localhost\" } },\n            { \"d2[127.0.0.1]\", new DestinationConfig() { Address = \"http://127.0.0.1:8080/a/b/c\", Health = \"http://127.0.0.1:8080/healthz\", Host = \"localhost:8080\" } },\n            { \"d3[127.0.0.1]\", new DestinationConfig() { Address = \"https://127.0.0.1/a/b/c\", Health = \"https://127.0.0.1/healthz\", Host = \"localhost\" } },\n            { \"d4[127.0.0.1]\", new DestinationConfig() { Address = \"https://127.0.0.1:8443/a/b/c\", Health = \"https://127.0.0.1:8443/healthz\", Host = \"overriddenhost\" } }\n        };\n\n        var actualDestinations = cluster.Destinations.ToDictionary(static k => k.Key, static v => v.Value.Model.Config);\n        Assert.Equal(expectedDestinations, actualDestinations);\n    }\n\n    [Fact]\n    public async Task LoadAsync_DestinationResolver_ReloadResolution()\n    {\n        var configListener = new TestConfigChangeListener(includeApply: false);\n        var destinationsToExpand = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n        {\n            { \"d1\", new DestinationConfig() { Address = \"http://localhost\" } }\n        };\n\n        var cts = new[] { new CancellationTokenSource() };\n        var signaled = new[] { 0 };\n        var syncExpandResolver = new FakeDestinationResolver((destinations, cancellation) =>\n        {\n            signaled[0]++;\n            var expandedDestinations = new Dictionary<string, DestinationConfig>();\n\n            foreach (var destKvp in destinations)\n            {\n                expandedDestinations[$\"{destKvp.Key}-1\"] = new DestinationConfig { Address = $\"http://127.0.0.1:8080/{signaled[0]}\" };\n                expandedDestinations[$\"{destKvp.Key}-2\"] = new DestinationConfig { Address = $\"http://127.1.1.1:8080/{signaled[0]}\" };\n            }\n\n            var result = new ResolvedDestinationCollection(expandedDestinations, new CancellationChangeToken(cts[0].Token));\n            return new(result);\n        });\n\n        var cluster1 = new ClusterConfig()\n        {\n            ClusterId = \"cluster1\",\n            Destinations = destinationsToExpand\n        };\n\n        var services = CreateServices(\n            new List<RouteConfig>(),\n            new List<ClusterConfig>() { cluster1 },\n            configListeners: new[] { configListener },\n            destinationResolver: syncExpandResolver);\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n\n        await configManager.InitialLoadAsync();\n        var configEvent = await configListener.Events.Reader.ReadAsync();\n        var configLoadEvent = Assert.IsType<TestConfigChangeListener.ConfigurationLoadedEvent>(configEvent);\n\n        Assert.True(configManager.TryGetCluster(cluster1.ClusterId, out var cluster));\n\n        var expectedDestinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n        {\n            { \"d1-1\", new DestinationConfig() { Address = \"http://127.0.0.1:8080/1\" } },\n            { \"d1-2\", new DestinationConfig() { Address = \"http://127.1.1.1:8080/1\" } }\n        };\n\n        var actualDestinations = cluster.Destinations.ToDictionary(static k => k.Key, static v => v.Value.Model.Config);\n        Assert.Equal(expectedDestinations, actualDestinations);\n\n        // Trigger the change token and wait for a subsequent load\n        var initialCts = cts[0];\n        cts[0] = new();\n        initialCts.Cancel();\n\n        configEvent = await configListener.Events.Reader.ReadAsync();\n        configLoadEvent = Assert.IsType<TestConfigChangeListener.ConfigurationLoadedEvent>(configEvent);\n\n        Assert.True(configManager.TryGetCluster(cluster1.ClusterId, out cluster));\n\n        expectedDestinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n        {\n            { \"d1-1\", new DestinationConfig() { Address = \"http://127.0.0.1:8080/2\" } },\n            { \"d1-2\", new DestinationConfig() { Address = \"http://127.1.1.1:8080/2\" } }\n        };\n\n        actualDestinations = cluster.Destinations.ToDictionary(static k => k.Key, static v => v.Value.Model.Config);\n        Assert.Equal(expectedDestinations, actualDestinations);\n    }\n\n    [Fact]\n    public async Task LoadAsync_DestinationResolver_Reload_ThrowsSync()\n    {\n        var configListener = new TestConfigChangeListener(includeApply: false);\n        var cts = new CancellationTokenSource();\n        var syncThrowResolver = new FakeDestinationResolver((destinations, cancellation) =>\n        {\n            if (cts.IsCancellationRequested)\n            {\n                throw new InvalidOperationException(\"Throwing!\");\n            }\n            else\n            {\n                return new(new ResolvedDestinationCollection(destinations, new CancellationChangeToken(cts.Token)));\n            }\n        });\n        var cluster = new ClusterConfig()\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d1\", new DestinationConfig() { Address = \"http://localhost\" } }\n            }\n        };\n        var services = CreateServices(\n            new List<RouteConfig>(),\n            new List<ClusterConfig>() { cluster },\n            configListeners: new[] { configListener },\n            destinationResolver: syncThrowResolver);\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n        await configManager.InitialLoadAsync();\n\n        // Read the successful load event\n        Assert.IsType<TestConfigChangeListener.ConfigurationLoadedEvent>(await configListener.Events.Reader.ReadAsync());\n\n        // Trigger invalidation\n        cts.Cancel();\n\n        // Read the failure event\n        var configLoadException = Assert.IsType<TestConfigChangeListener.ConfigurationLoadingFailedEvent>(await configListener.Events.Reader.ReadAsync());\n        var ex = configLoadException.Exception;\n        Assert.Equal(\"Throwing!\", ex.Message);\n    }\n\n    [Fact]\n    public async Task LoadAsync_DestinationResolver_Reload_ThrowsAsync()\n    {\n        var configListener = new TestConfigChangeListener(includeApply: false);\n        var cts = new CancellationTokenSource();\n        var syncThrowResolver = new FakeDestinationResolver(async (destinations, cancellation) =>\n        {\n            await Task.Yield();\n\n            if (cts.IsCancellationRequested)\n            {\n                throw new InvalidOperationException(\"Throwing!\");\n            }\n            else\n            {\n                return new ResolvedDestinationCollection(destinations, new CancellationChangeToken(cts.Token));\n            }\n        });\n        var cluster = new ClusterConfig()\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n            {\n                { \"d1\", new DestinationConfig() { Address = \"http://localhost\" } }\n            }\n        };\n        var services = CreateServices(\n            new List<RouteConfig>(),\n            new List<ClusterConfig>() { cluster },\n            configListeners: new[] { configListener },\n            destinationResolver: syncThrowResolver);\n        var configManager = services.GetRequiredService<ProxyConfigManager>();\n        await configManager.InitialLoadAsync();\n\n        // Read the successful load event\n        Assert.IsType<TestConfigChangeListener.ConfigurationLoadedEvent>(await configListener.Events.Reader.ReadAsync());\n\n        // Trigger invalidation\n        cts.Cancel();\n\n        // Read the failure event\n        var configLoadException = Assert.IsType<TestConfigChangeListener.ConfigurationLoadingFailedEvent>(await configListener.Events.Reader.ReadAsync());\n        var innerExc1 = Assert.IsType<InvalidOperationException>(configLoadException.Exception);\n        Assert.Equal(\"Error resolving destinations for cluster cluster1\", innerExc1.Message);\n        var innerExc2 = Assert.IsType<InvalidOperationException>(innerExc1.InnerException);\n        Assert.Equal(\"Throwing!\", innerExc2.Message);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Model/DestinationStateTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Model.Tests;\n\npublic class DestinationStateTests\n{\n    [Fact]\n    public void DestinationInfoEnumerator()\n    {\n        var destinationInfo = new DestinationState(\"destination1\");\n        var list = new List<DestinationState>();\n\n        foreach (var item in destinationInfo)\n        {\n            list.Add(item);\n        }\n\n        var first = Assert.Single(list);\n        Assert.Same(destinationInfo, first);\n    }\n\n    [Fact]\n    public void DestinationInfoReadOnlyList()\n    {\n        var destinationInfo = new DestinationState(\"destination2\");\n\n        IReadOnlyList<DestinationState> list = destinationInfo;\n\n        Assert.Single(list);\n        Assert.Same(destinationInfo, list[0]);\n        Assert.Throws<IndexOutOfRangeException>(() => list[1]);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Model/HttpContextFeaturesExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Model.Tests;\n\npublic class HttpContextFeaturesExtensions\n{\n    [Fact]\n    public void ReassignProxyRequest_Success()\n    {\n        var client = new HttpMessageInvoker(new SocketsHttpHandler());\n        var context = new DefaultHttpContext();\n        var d1 = new DestinationState(\"d1\");\n        var d2 = new DestinationState(\"d2\");\n        var cc1 = new ClusterConfig() { ClusterId = \"c1\" };\n        var cm1 = new ClusterModel(cc1, client);\n        var cs1 = new ClusterState(\"c1\") { Model = cm1 };\n        var r1 = new RouteModel(new RouteConfig() { RouteId = \"r1\" }, cs1, HttpTransformer.Empty);\n        var feature = new ReverseProxyFeature()\n        {\n            AllDestinations = d1,\n            AvailableDestinations = d1,\n            Cluster = cm1,\n            Route = r1,\n            ProxiedDestination = d1,\n        };\n\n        context.Features.Set<IReverseProxyFeature>(feature);\n\n        var cc2 = new ClusterConfig() { ClusterId = \"cc2\" };\n        var cm2 = new ClusterModel(cc2, client);\n        var cs2 = new ClusterState(\"cs2\")\n        {\n            DestinationsState = new ClusterDestinationsState(d2, d2),\n            Model = cm2,\n        };\n        context.ReassignProxyRequest(cs2);\n\n        var newFeature = context.GetReverseProxyFeature();\n        Assert.NotSame(feature, newFeature);\n        Assert.Same(d2, newFeature.AllDestinations);\n        Assert.Same(d2, newFeature.AvailableDestinations);\n        Assert.Same(d1, newFeature.ProxiedDestination); // Copied unmodified.\n        Assert.Same(cm2, newFeature.Cluster);\n        Assert.Same(r1, newFeature.Route);\n\n        // Begin testing ReassignProxyRequest(route, cluster) overload\n        var r2 = new RouteModel(new RouteConfig() { RouteId = \"r2\" }, cs2, HttpTransformer.Empty);\n        context.ReassignProxyRequest(r2, cs2);\n\n        var newFeatureOverload = context.GetReverseProxyFeature();\n        Assert.NotSame(newFeature, newFeatureOverload);\n        Assert.Same(d2, newFeatureOverload.AllDestinations); // Unmodified\n        Assert.Same(d2, newFeatureOverload.AvailableDestinations); // Unmodified\n        Assert.Same(d1, newFeatureOverload.ProxiedDestination); // Unmodified\n        Assert.Same(cm2, newFeatureOverload.Cluster); // Unmodified\n        Assert.Same(r2, newFeatureOverload.Route); // Asset route update\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Model/ProxyPipelineInitializerMiddlewareTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Timeouts;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.AspNetCore.Routing.Patterns;\nusing Moq;\nusing Xunit;\nusing Yarp.Tests.Common;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Forwarder;\nusing System.Diagnostics;\n\nnamespace Yarp.ReverseProxy.Model.Tests;\n\npublic class ProxyPipelineInitializerMiddlewareTests : TestAutoMockBase\n{\n    public ProxyPipelineInitializerMiddlewareTests()\n    {\n        Provide<RequestDelegate>(context =>\n        {\n            context.Response.StatusCode = StatusCodes.Status418ImATeapot;\n            return Task.CompletedTask;\n        });\n    }\n\n    [Fact]\n    public void Constructor_Works()\n    {\n        Create<ProxyPipelineInitializerMiddleware>();\n    }\n\n    [Fact]\n    public async Task Invoke_SetsFeatures()\n    {\n        var httpClient = new HttpMessageInvoker(new Mock<HttpMessageHandler>().Object);\n        var cluster1 = new ClusterState(clusterId: \"cluster1\");\n        cluster1.Model = new ClusterModel(new ClusterConfig(), httpClient);\n        var destination1 = cluster1.Destinations.GetOrAdd(\n            \"destination1\",\n            id => new DestinationState(id) { Model = new DestinationModel(new DestinationConfig { Address = \"https://localhost:123/a/b/\" }) });\n        cluster1.DestinationsState = new ClusterDestinationsState(new[] { destination1 }, new[] { destination1 });\n\n        var aspNetCoreEndpoints = new List<Endpoint>();\n        var routeConfig = new RouteModel(\n            config: new RouteConfig(),\n            cluster1,\n            HttpTransformer.Default);\n        var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig);\n        aspNetCoreEndpoints.Add(aspNetCoreEndpoint);\n        var httpContext = new DefaultHttpContext();\n        httpContext.SetEndpoint(aspNetCoreEndpoint);\n\n        var sut = Create<ProxyPipelineInitializerMiddleware>();\n\n        await sut.Invoke(httpContext);\n\n        var proxyFeature = httpContext.GetReverseProxyFeature();\n        Assert.NotNull(proxyFeature);\n        Assert.NotNull(proxyFeature.AvailableDestinations);\n        Assert.Single(proxyFeature.AvailableDestinations);\n        Assert.Same(destination1, proxyFeature.AvailableDestinations[0]);\n        Assert.Same(cluster1.Model, proxyFeature.Cluster);\n\n        Assert.Equal(StatusCodes.Status418ImATeapot, httpContext.Response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Invoke_NoHealthyEndpoints_CallsNext()\n    {\n        var httpClient = new HttpMessageInvoker(new Mock<HttpMessageHandler>().Object);\n        var cluster1 = new ClusterState(clusterId: \"cluster1\");\n        cluster1.Model = new ClusterModel(\n            new ClusterConfig()\n            {\n                HealthCheck = new HealthCheckConfig\n                {\n                    Active = new ActiveHealthCheckConfig\n                    {\n                        Enabled = true,\n                        Timeout = Timeout.InfiniteTimeSpan,\n                        Interval = Timeout.InfiniteTimeSpan,\n                        Policy = \"Any5xxResponse\",\n                    }\n                }\n            },\n            httpClient);\n        var destination1 = cluster1.Destinations.GetOrAdd(\n            \"destination1\",\n            id => new DestinationState(id)\n            {\n                Model = new DestinationModel(new DestinationConfig { Address = \"https://localhost:123/a/b/\" }),\n                Health = { Active = DestinationHealth.Unhealthy },\n            });\n        cluster1.DestinationsState = new ClusterDestinationsState(new[] { destination1 }, Array.Empty<DestinationState>());\n\n        var aspNetCoreEndpoints = new List<Endpoint>();\n        var routeConfig = new RouteModel(\n            config: new RouteConfig(),\n            cluster: cluster1,\n            transformer: HttpTransformer.Default);\n        var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig);\n        aspNetCoreEndpoints.Add(aspNetCoreEndpoint);\n        var httpContext = new DefaultHttpContext();\n        httpContext.SetEndpoint(aspNetCoreEndpoint);\n\n        var sut = Create<ProxyPipelineInitializerMiddleware>();\n\n        await sut.Invoke(httpContext);\n\n        var feature = httpContext.Features.Get<IReverseProxyFeature>();\n        Assert.NotNull(feature);\n        Assert.Single(feature.AllDestinations, destination1);\n        Assert.Empty(feature.AvailableDestinations);\n\n        Assert.Equal(StatusCodes.Status418ImATeapot, httpContext.Response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(1)]\n    [InlineData(Timeout.Infinite)]\n    public async Task Invoke_MissingTimeoutMiddleware_RefuseRequest(int timeoutMs)\n    {\n        var httpClient = new HttpMessageInvoker(new Mock<HttpMessageHandler>().Object);\n        var cluster1 = new ClusterState(clusterId: \"cluster1\")\n        {\n            Model = new ClusterModel(new ClusterConfig(), httpClient)\n        };\n\n        var aspNetCoreEndpoints = new List<Endpoint>();\n        var routeConfig = new RouteModel(\n            config: new RouteConfig(),\n            cluster: cluster1,\n            transformer: HttpTransformer.Default);\n        var aspNetCoreEndpoint = CreateAspNetCoreEndpoint(routeConfig,\n            builder =>\n            {\n                builder.Metadata.Add(new RequestTimeoutAttribute(timeoutMs));\n            });\n        aspNetCoreEndpoints.Add(aspNetCoreEndpoint);\n        var httpContext = new DefaultHttpContext();\n        httpContext.SetEndpoint(aspNetCoreEndpoint);\n\n        var sut = Create<ProxyPipelineInitializerMiddleware>();\n\n        if (timeoutMs == Timeout.Infinite || Debugger.IsAttached)\n        {\n            // If the timeout was infinite or the debugger is attached, we shouldn't refuse the request.\n            await sut.Invoke(httpContext);\n        }\n        else\n        {\n            await Assert.ThrowsAsync<InvalidOperationException>(() => sut.Invoke(httpContext));\n        }\n    }\n\n    private static Endpoint CreateAspNetCoreEndpoint(RouteModel routeConfig, Action<RouteEndpointBuilder> configure = null)\n    {\n        var endpointBuilder = new RouteEndpointBuilder(\n            requestDelegate: httpContext => Task.CompletedTask,\n            routePattern: RoutePatternFactory.Parse(\"/\"),\n            order: 0);\n        endpointBuilder.Metadata.Add(routeConfig);\n        configure?.Invoke(endpointBuilder);\n        return endpointBuilder.Build();\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Routing/HeaderMatcherPolicyTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.AspNetCore.Routing.Matching;\nusing Microsoft.AspNetCore.Routing.Patterns;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Routing.Tests;\n\npublic class HeaderMatcherPolicyTests\n{\n    [Fact]\n    public void Comparer_SortOrder_SingleRuleEqual()\n    {\n        // Most specific to least\n        var endpoints = new[]\n        {\n            (0, CreateEndpoint(\"header\", new[] { \"abc\" }, HeaderMatchMode.ExactHeader, isCaseSensitive: true)),\n\n            (0, CreateEndpoint(\"header\", new[] { \"abc\" }, HeaderMatchMode.ExactHeader)),\n            (0, CreateEndpoint(\"header\", new[] { \"abc\", \"def\" }, HeaderMatchMode.ExactHeader)),\n            (0, CreateEndpoint(\"header2\", new[] { \"abc\", \"def\" }, HeaderMatchMode.ExactHeader)),\n\n            (0, CreateEndpoint(\"header\", new[] { \"abc\" }, HeaderMatchMode.HeaderPrefix, isCaseSensitive: true)),\n\n            (0, CreateEndpoint(\"header\", new[] { \"abc\" }, HeaderMatchMode.HeaderPrefix)),\n            (0, CreateEndpoint(\"header\", new[] { \"abc\", \"def\" }, HeaderMatchMode.HeaderPrefix)),\n            (0, CreateEndpoint(\"header2\", new[] { \"abc\", \"def\" }, HeaderMatchMode.HeaderPrefix)),\n\n            (0, CreateEndpoint(\"header\", Array.Empty<string>(), HeaderMatchMode.Exists, isCaseSensitive: true)),\n            (0, CreateEndpoint(\"header\", Array.Empty<string>(), HeaderMatchMode.Exists)),\n            (0, CreateEndpoint(\"header\", Array.Empty<string>(), HeaderMatchMode.Exists, isCaseSensitive: true)),\n            (0, CreateEndpoint(\"header\", Array.Empty<string>(), HeaderMatchMode.Exists)),\n        };\n        var sut = new HeaderMatcherPolicy();\n\n        for (var i = 0; i < endpoints.Length; i++)\n        {\n            for (var j = 0; j < endpoints.Length; j++)\n            {\n                var a = endpoints[i];\n                var b = endpoints[j];\n\n                var actual = sut.Comparer.Compare(a.Item2, b.Item2);\n                var expected =\n                    a.Item1 < b.Item1 ? -1 :\n                    a.Item1 > b.Item1 ? 1 : 0;\n                if (actual != expected)\n                {\n                    Assert.Fail($\"Error comparing [{i}] to [{j}], expected {expected}, found {actual}.\");\n                }\n            }\n        }\n    }\n\n    [Fact]\n    public void Comparer_MultipleHeaders_SortOrder()\n    {\n        // Most specific to least\n        var endpoints = new[]\n        {\n            (0, CreateEndpoint(new[]\n            {\n                new HeaderMatcher(\"header\", Array.Empty<string>(), HeaderMatchMode.Exists, isCaseSensitive: true),\n                new HeaderMatcher(\"header\", new[] { \"abc\" }, HeaderMatchMode.HeaderPrefix, isCaseSensitive: true),\n                new HeaderMatcher(\"header\", new[] { \"cbcabc\" }, HeaderMatchMode.Contains, isCaseSensitive: true),\n                new HeaderMatcher(\"header\", new[] { \"abc\" }, HeaderMatchMode.ExactHeader, isCaseSensitive: true)\n            })),\n\n            (1, CreateEndpoint(new[]\n            {\n                new HeaderMatcher(\"header\", new[] { \"cbcabc\" }, HeaderMatchMode.Contains, isCaseSensitive: true),\n                new HeaderMatcher(\"header\", new[] { \"abc\" }, HeaderMatchMode.ExactHeader, isCaseSensitive: true)\n            })),\n            (1, CreateEndpoint(new[]\n            {\n                new HeaderMatcher(\"header\", Array.Empty<string>(), HeaderMatchMode.Exists, isCaseSensitive: true),\n                new HeaderMatcher(\"header\", new[] { \"abc\" }, HeaderMatchMode.ExactHeader, isCaseSensitive: true)\n            })),\n\n            (2, CreateEndpoint(\"header\", new[] { \"abc\" })),\n\n            (3, CreateEndpoint(Array.Empty<HeaderMatcher>())),\n        };\n        var sut = new HeaderMatcherPolicy();\n\n        for (var i = 0; i < endpoints.Length; i++)\n        {\n            for (var j = 0; j < endpoints.Length; j++)\n            {\n                var a = endpoints[i];\n                var b = endpoints[j];\n\n                var actual = sut.Comparer.Compare(a.Item2, b.Item2);\n                var expected =\n                    a.Item1 < b.Item1 ? -1 :\n                    a.Item1 > b.Item1 ? 1 : 0;\n                if (actual != expected)\n                {\n                    Assert.Fail($\"Error comparing [{i}] to [{j}], expected {expected}, found {actual}.\");\n                }\n            }\n        }\n    }\n\n    [Fact]\n    public void AppliesToEndpoints_AppliesScenarios()\n    {\n        var scenarios = new[]\n        {\n            CreateEndpoint(\"org-id\", Array.Empty<string>(), HeaderMatchMode.Exists),\n            CreateEndpoint(\"org-id\", new[] { \"abc\" }),\n            CreateEndpoint(\"org-id\", new[] { \"abc\", \"def\" }),\n            CreateEndpoint(\"org-id\", Array.Empty<string>(), HeaderMatchMode.Exists, isDynamic: true),\n            CreateEndpoint(\"org-id\", new[] { \"abc\" }, isDynamic: true),\n            CreateEndpoint(\"org-id\", null, HeaderMatchMode.Exists, isDynamic: true),\n            CreateEndpoint(new[]\n            {\n                new HeaderMatcher(\"header\", Array.Empty<string>(), HeaderMatchMode.Exists, isCaseSensitive: true),\n                new HeaderMatcher(\"header\", new[] { \"abc\" }, HeaderMatchMode.ExactHeader, isCaseSensitive: true)\n            })\n        };\n        var sut = new HeaderMatcherPolicy();\n        var endpointSelectorPolicy = (IEndpointSelectorPolicy)sut;\n\n        for (var i = 0; i < scenarios.Length; i++)\n        {\n            var result = endpointSelectorPolicy.AppliesToEndpoints(new[] { scenarios[i] });\n            Assert.True(result, $\"scenario {i}\");\n        }\n    }\n\n    [Fact]\n    public void AppliesToEndpoints_NoMetadata_DoesNotApply()\n    {\n        var endpoint = CreateEndpoint(Array.Empty<HeaderMatcher>());\n\n        var sut = new HeaderMatcherPolicy();\n        var endpointSelectorPolicy = (IEndpointSelectorPolicy)sut;\n\n        var result = endpointSelectorPolicy.AppliesToEndpoints(new[] { endpoint });\n        Assert.False(result);\n    }\n\n    [Theory]\n    [InlineData(null, HeaderMatchMode.Exists, false)]\n    [InlineData(\"\", HeaderMatchMode.Exists, false)]\n    [InlineData(\"abc\", HeaderMatchMode.Exists, true)]\n    [InlineData(null, HeaderMatchMode.NotExists, true)]\n    [InlineData(\"\", HeaderMatchMode.NotExists, true)]\n    [InlineData(\"abc\", HeaderMatchMode.NotExists, false)]\n    public async Task ApplyAsync_MatchingScenarios_AnyHeaderValue(string incomingHeaderValue, HeaderMatchMode mode, bool shouldMatch)\n    {\n        var context = new DefaultHttpContext();\n        if (incomingHeaderValue is not null)\n        {\n            context.Request.Headers[\"org-id\"] = incomingHeaderValue;\n        }\n\n        var endpoint = CreateEndpoint(\"org-id\", Array.Empty<string>(), mode);\n        var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]);\n        var sut = new HeaderMatcherPolicy();\n\n        await sut.ApplyAsync(context, candidates);\n\n        Assert.Equal(shouldMatch, candidates.IsValidCandidate(0));\n    }\n\n    [Theory]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, null, false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"aBC\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"abcd\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"ab\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \"\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \"abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \"aBC\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \"abcd\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \"ab\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \";\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"ab;c\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \";abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \";abC\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"abc;\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"Abc;\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"abc;def\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"ABC;DEF\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"def;abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"abc;aBc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"def;ab c\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"bcd;efg\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"\\\"abc\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"abc\\\"\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"\\\"abc\\\"\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \" \\\"abc\\\"\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"\\\"abc\\\" \", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"\\\"abc\\\", \", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"\\\"ab\\\", \\\"abc\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"\\\"ab\\\", \\\"abc\\\"\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"ab\\\", \\\"abc\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"ab\\\"\\\",\\\"abc\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"ab\\\"\\\",\\\"abc\\\"\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"\\\"\\\"ab\\\"\\\"c\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"\\\"\\\"ab\\\"\\\"c,\\\"abc,\\\"\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, false, \"\\\"\\\"ab\\\"\\\"c,\\\"abc,\\\",abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \";\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \"ab;c\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \";abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \"abc;\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \"abc;def\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \"abc;abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \"def;abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \"def;abC\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \"def;ab c\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.ExactHeader, true, \"bcd;efg\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, false, \"\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, false, \"abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, false, \"aBC\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, false, \"abcd\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, false, \"ab\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"aBC\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"abcd\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"aBCd\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"ab\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, false, \";\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, false, \"abc;\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, false, \";aBc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, false, \"abd;abC\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, false, \"abd;abe\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"abc;\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \";abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"abd;abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"abd;abe\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"ab\\\"c\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"ab\\\"c\\\"\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"\\\"abc\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"\\\"abc\\\"\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \" \\\"abc\\\"\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \" \\\"abc\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"\\\"abc\\\" \", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"ab,abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"ab, abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"\\\"ab, abc\\\"\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"\\\"ab\\\", abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"\\\"ab\\\", abc\\\"\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"\\\"ab\\\"\\\"\\\"\\\", abc\\\"\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.HeaderPrefix, true, \"\\\"ab\\\"\\\"\\\"\\\"\\\", abc\\\"\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \"\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \"ababc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \"zaBCz\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \"dcbaabcd\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \"ababab\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, true, \"\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, true, \"abcc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, true, \"aaaBC\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, true, \"bbabcdb\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, true, \"aBCcba\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, true, \"baab\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \";\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \"ababc;\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \";ababc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \"ab;cd\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \"ab;cd;abcd\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \"abc;abc;def\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \"\\\"abc\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \"abc\\\"\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \"\\\"abc\\\"\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.Contains, false, \"ab\\\"c\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, false, null, true)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, false, \"\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, false, \"ababc\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, false, \"zaBCz\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, false, \"dcbaabcd\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, false, \"ababab\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, true, null, true)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, true, \"\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, true, \"abcc\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, true, \"aaaBC\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, true, \"bbabcdb\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, true, \"aBCcba\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, true, \"baab\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, false, \";abc\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, false, \"abc;\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, false, \"ab;cd\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, false, \"ababc;abc\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, false, \"abc;def\", false)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, false, \"ab\\\"c\", true)]\n    [InlineData(\"abc\", HeaderMatchMode.NotContains, false, \"\\\"abc\\\"\", false)]\n    public async Task ApplyAsync_MatchingScenarios_OneHeaderValue(\n        string headerValue,\n        HeaderMatchMode headerValueMatchMode,\n        bool isCaseSensitive,\n        string incomingHeaderValues,\n        bool shouldMatch)\n    {\n        var context = new DefaultHttpContext();\n        if (incomingHeaderValues is not null)\n        {\n            context.Request.Headers[\"org-id\"] = incomingHeaderValues.Split(';');\n        }\n\n        var endpoint = CreateEndpoint(\"org-id\", new[] { headerValue }, headerValueMatchMode, isCaseSensitive);\n        var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]);\n        var sut = new HeaderMatcherPolicy();\n\n        await sut.ApplyAsync(context, candidates);\n\n        Assert.Equal(shouldMatch, candidates.IsValidCandidate(0));\n    }\n\n    [Theory]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, false, null, false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, false, \"\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, false, \"abc\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, false, \"aBc\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, false, \"abcd\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, false, \"def\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, false, \"deF\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, false, \"defg\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, null, false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"abc\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"aBC\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"aBCd\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"def\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"DEFg\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"dEf\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \";\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"abc;a\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"a;abc\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"abc;def\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"ab;def\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"ab;cdef\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"ab;\\\"def\\\"\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"\\\"abc,def\\\"\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \"\\\"abc\\\",def\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.ExactHeader, true, \" \\\"abc\\\",def\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, false, null, false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, false, \"\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, false, \"abc\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, false, \"aBc\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, false, \"abcd\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, false, \"abcD\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, false, \"def\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, false, \"deF\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, false, \"defg\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, false, \"defG\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, false, \"aabc\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, null, false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"abc\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"aBC\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"aBCd\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"def\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"DEFg\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"aabc\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \";\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"ab;cde;fgh\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"abcd;e\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"abcd;defg\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"Abcd;defg\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"Abcd;Defg\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"a;defg\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"abcd;\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \";def\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \" \\\"abc\\\",def\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"ab, \\\"def\\\"\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"ab, def\\\"\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"ab, \\\"def\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"\\\"\\\"ab\\\",def\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"\\\"\\\"ab\\\",def\\\"\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.HeaderPrefix, true, \"\\\"\\\"ab\\\"\\\",def\\\"\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, false, null, false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, false, \"\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, false, \"aabc\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, false, \"baBc\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, false, \"ababcd\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, false, \"dcabcD\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, false, \"fdeff\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, false, \"edeF\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, false, \"adefg\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, false, \"abdefG\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, false, \"ddaabc\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, false, \"abcdef\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, null, false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"cabca\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"aBCa\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"CaBCdd\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"DEFdef\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"defDEFg\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"bbaabc\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \";\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"cabca;\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \";cabca\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"ab;cd;ef\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"aBCa;deFg\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"aBCa;defg\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"abcd;d\", true)]\n    [InlineData(\"abc\", \"ABC\", HeaderMatchMode.Contains, true, \"abc;d\", true)]\n    [InlineData(\"abc\", \"ABC\", HeaderMatchMode.Contains, true, \"ABC;d\", true)]\n    [InlineData(\"abc\", \"ABC\", HeaderMatchMode.Contains, true, \"abC;d\", false)]\n    [InlineData(\"abc\", \"ABC\", HeaderMatchMode.Contains, true, \"abcABC;d\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"\\\"abc, def\\\"\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"\\\"abc\\\", def\\\"\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.Contains, true, \"ab\\\"cde\\\"f\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, false, null, true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, false, \"\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, false, \"aabc\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, false, \"baBc\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, false, \"ababcd\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, false, \"dcabcD\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, false, \"def\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, false, \"ghi\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, null, true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"cabca\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"aBCa\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"CaBCdd\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"DEFdef\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"DEFg\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"bbaabc\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"defG\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"bbaabc;\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \";bbaabc\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"ab;cd;ef\", true)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"a;defg\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"ab;cdef\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"abc;def\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"Abc;cdef\", false)]\n    [InlineData(\"abc\", \"def\", HeaderMatchMode.NotContains, true, \"Abc;cdEf\", true)]\n    public async Task ApplyAsync_MatchingScenarios_TwoHeaderValues(\n        string header1Value,\n        string header2Value,\n        HeaderMatchMode headerValueMatchMode,\n        bool isCaseSensitive,\n        string incomingHeaderValues,\n        bool shouldMatch)\n    {\n        var context = new DefaultHttpContext();\n        if (incomingHeaderValues is not null)\n        {\n            context.Request.Headers[\"org-id\"] = incomingHeaderValues.Split(';');\n        }\n\n        var endpoint = CreateEndpoint(\"org-id\", new[] { header1Value, header2Value }, headerValueMatchMode, isCaseSensitive);\n\n        var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]);\n        var sut = new HeaderMatcherPolicy();\n\n        await sut.ApplyAsync(context, candidates);\n\n        Assert.Equal(shouldMatch, candidates.IsValidCandidate(0));\n    }\n\n    [Theory]\n    [InlineData(HeaderMatchMode.Contains, true, false)]\n    [InlineData(HeaderMatchMode.Contains, false, false)]\n    [InlineData(HeaderMatchMode.NotContains, true, true)]\n    [InlineData(HeaderMatchMode.NotContains, false, true)]\n    [InlineData(HeaderMatchMode.HeaderPrefix, true, false)]\n    [InlineData(HeaderMatchMode.HeaderPrefix, false, false)]\n    [InlineData(HeaderMatchMode.ExactHeader, true, false)]\n    [InlineData(HeaderMatchMode.ExactHeader, false, false)]\n    [InlineData(HeaderMatchMode.NotExists, true, true)]\n    [InlineData(HeaderMatchMode.NotExists, false, true)]\n    [InlineData(HeaderMatchMode.Exists, true, false)]\n    [InlineData(HeaderMatchMode.Exists, false, false)]\n    public async Task ApplyAsync_MatchingScenarios_MissingHeader(\n        HeaderMatchMode headerValueMatchMode,\n        bool isCaseSensitive,\n        bool shouldMatch)\n    {\n        var context = new DefaultHttpContext();\n\n        var headerValues = new[] { \"bar\" };\n        if (headerValueMatchMode == HeaderMatchMode.Exists\n            || headerValueMatchMode == HeaderMatchMode.NotExists)\n        {\n            headerValues = null;\n        }\n\n        var endpoint = CreateEndpoint(\"foo\", headerValues, headerValueMatchMode, isCaseSensitive);\n        var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]);\n        var sut = new HeaderMatcherPolicy();\n\n        await sut.ApplyAsync(context, candidates);\n\n        Assert.Equal(shouldMatch, candidates.IsValidCandidate(0));\n    }\n\n    [Theory]\n    [InlineData(\"Foo\", \"abc\", HeaderMatchMode.ExactHeader, \"ab, abc\", true)]\n    [InlineData(\"Foo\", \"abc\", HeaderMatchMode.ExactHeader, \"ab; abc\", false)]\n    [InlineData(\"Cookie\", \"abc\", HeaderMatchMode.ExactHeader, \"ab, abc\", false)]\n    [InlineData(\"Cookie\", \"abc\", HeaderMatchMode.ExactHeader, \"ab; abc\", true)]\n    [InlineData(\"Set-Cookie\", \"abc\", HeaderMatchMode.ExactHeader, \"ab, abc\", true)]\n    [InlineData(\"Set-Cookie\", \"abc\", HeaderMatchMode.ExactHeader, \"ab; abc\", false)]\n    [InlineData(\"Cookie\", \"abc\", HeaderMatchMode.ExactHeader, \"\\\"ab\\\"; abc\", true)]\n    [InlineData(\"Cookie\", \"abc\", HeaderMatchMode.ExactHeader, \"ab; \\\"abc\\\"\", true)]\n    [InlineData(\"Cookie\", \"abc\", HeaderMatchMode.ExactHeader, \"\\\"ab\\\"; \\\"abc\\\"\", true)]\n    [InlineData(\"Cookie\", \"abc\", HeaderMatchMode.ExactHeader, \"abc;\", true)]\n    [InlineData(\"Cookie\", \"abc\", HeaderMatchMode.ExactHeader, \" abc;\", true)]\n    [InlineData(\"Cookie\", \"abc\", HeaderMatchMode.ExactHeader, \" \\\"abc\\\";\", true)]\n    [InlineData(\"Cookie\", \"abc\", HeaderMatchMode.ExactHeader, \"\\\"abc;\\\"\", false)]\n    [InlineData(\"Cookie\", \"abc\", HeaderMatchMode.ExactHeader, \"\\\"abc;\\\" \\\"abc\\\"\", false)]\n    public async Task ApplyAsync_Cookie_UsesDifferentSeparator(\n        string headerName,\n        string headerValue,\n        HeaderMatchMode headerValueMatchMode,\n        string incomingHeaderValue,\n        bool shouldMatch)\n    {\n        var context = new DefaultHttpContext();\n        context.Request.Headers[headerName] = incomingHeaderValue;\n\n        var endpoint = CreateEndpoint(headerName, new[] { headerValue }, headerValueMatchMode, true);\n        var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]);\n        var sut = new HeaderMatcherPolicy();\n\n        await sut.ApplyAsync(context, candidates);\n\n        Assert.Equal(shouldMatch, candidates.IsValidCandidate(0));\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, true, false)]\n    [InlineData(true, false, false)]\n    [InlineData(true, true, true)]\n    public async Task ApplyAsync_MultipleRules_RequiresAllHeaders(bool sendHeader1, bool sendHeader2, bool shouldMatch)\n    {\n        var endpoint = CreateEndpoint(new[]\n        {\n            new HeaderMatcher(\"header1\", new[] { \"value1\" }, HeaderMatchMode.ExactHeader, isCaseSensitive: false),\n            new HeaderMatcher(\"header2\", new[] { \"value2\" }, HeaderMatchMode.ExactHeader, isCaseSensitive: false)\n        });\n\n        var context = new DefaultHttpContext();\n        if (sendHeader1)\n        {\n            context.Request.Headers[\"header1\"] = \"value1\";\n        }\n        if (sendHeader2)\n        {\n            context.Request.Headers[\"header2\"] = \"value2\";\n        }\n\n        var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]);\n        var sut = new HeaderMatcherPolicy();\n\n        await sut.ApplyAsync(context, candidates);\n\n        Assert.Equal(shouldMatch, candidates.IsValidCandidate(0));\n    }\n\n    private static Endpoint CreateEndpoint(\n        string headerName,\n        string[] headerValues,\n        HeaderMatchMode mode = HeaderMatchMode.ExactHeader,\n        bool isCaseSensitive = false,\n        bool isDynamic = false)\n    {\n        return CreateEndpoint(new[] { new HeaderMatcher(headerName, headerValues, mode, isCaseSensitive) }, isDynamic);\n    }\n\n    private static Endpoint CreateEndpoint(IReadOnlyList<HeaderMatcher> matchers, bool isDynamic = false)\n    {\n        var builder = new RouteEndpointBuilder(_ => Task.CompletedTask, RoutePatternFactory.Parse(\"/\"), 0);\n        builder.Metadata.Add(new HeaderMetadata(matchers));\n        if (isDynamic)\n        {\n            builder.Metadata.Add(new DynamicEndpointMetadata());\n        }\n\n        return builder.Build();\n    }\n\n    private class DynamicEndpointMetadata : IDynamicEndpointMetadata\n    {\n        public bool IsDynamic => true;\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Routing/ProxyEndpointFactoryTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Cors;\nusing Microsoft.AspNetCore.Cors.Infrastructure;\nusing Microsoft.AspNetCore.Http.Timeouts;\nusing Microsoft.AspNetCore.RateLimiting;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.AspNetCore.Routing.Patterns;\nusing Microsoft.Extensions.DependencyInjection;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Routing.Tests;\n\npublic class ProxyEndpointFactoryTests\n{\n    private IServiceProvider CreateServices()\n    {\n        var serviceCollection = new ServiceCollection();\n        serviceCollection.AddSingleton<ProxyEndpointFactory, ProxyEndpointFactory>();\n        return serviceCollection.BuildServiceProvider();\n    }\n\n    [Fact]\n    public void Constructor_Works()\n    {\n        var services = CreateServices();\n        _ = services.GetRequiredService<ProxyEndpointFactory>();\n    }\n\n    [Fact]\n    public void AddEndpoint_HostAndPath_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"example.com\" },\n                Path = \"/a\",\n            },\n            Order = 12,\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.Same(cluster, routeConfig.Cluster);\n        Assert.Equal(\"route1\", routeEndpoint.DisplayName);\n        Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata<RouteModel>());\n        Assert.Equal(\"/a\", routeEndpoint.RoutePattern.RawText);\n        Assert.Equal(12, routeEndpoint.Order);\n        Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision));\n\n        var hostMetadata = routeEndpoint.Metadata.GetMetadata<HostAttribute>();\n        Assert.NotNull(hostMetadata);\n        Assert.Single(hostMetadata.Hosts);\n        Assert.Equal(\"example.com\", hostMetadata.Hosts[0]);\n    }\n\n    private (RouteEndpoint routeEndpoint, RouteModel routeConfig) CreateEndpoint(ProxyEndpointFactory factory, RouteState routeState, RouteConfig routeConfig, ClusterState clusterState)\n    {\n        routeState.ClusterRevision = clusterState.Revision;\n        var routeModel = new RouteModel(routeConfig, clusterState, HttpTransformer.Default);\n\n        var endpoint = factory.CreateEndpoint(routeModel, Array.Empty<Action<EndpointBuilder>>());\n\n        var routeEndpoint = Assert.IsType<RouteEndpoint>(endpoint);\n\n        return (routeEndpoint, routeModel);\n    }\n\n    [Fact]\n    public void AddEndpoint_JustHost_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"example.com\" },\n            },\n            Order = 12,\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.Same(cluster, routeConfig.Cluster);\n        Assert.Equal(\"route1\", routeEndpoint.DisplayName);\n        Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata<RouteModel>());\n        Assert.Equal(\"/{**catchall}\", routeEndpoint.RoutePattern.RawText);\n        Assert.Equal(12, routeEndpoint.Order);\n        Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision));\n\n        var hostMetadata = routeEndpoint.Metadata.GetMetadata<HostAttribute>();\n        Assert.NotNull(hostMetadata);\n        Assert.Single(hostMetadata.Hosts);\n        Assert.Equal(\"example.com\", hostMetadata.Hosts[0]);\n    }\n\n    [Fact]\n    public void AddEndpoint_JustHostWithWildcard_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Hosts = new[] { \"*.example.com\" },\n            },\n            Order = 12,\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.Same(cluster, routeConfig.Cluster);\n        Assert.Equal(\"route1\", routeEndpoint.DisplayName);\n        Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata<RouteModel>());\n        Assert.Equal(\"/{**catchall}\", routeEndpoint.RoutePattern.RawText);\n        Assert.Equal(12, routeEndpoint.Order);\n        Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision));\n\n        var hostMetadata = routeEndpoint.Metadata.GetMetadata<HostAttribute>();\n        Assert.NotNull(hostMetadata);\n        Assert.Single(hostMetadata.Hosts);\n        Assert.Equal(\"*.example.com\", hostMetadata.Hosts[0]);\n    }\n\n    [Fact]\n    public void AddEndpoint_JustPath_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Path = \"/a\",\n            },\n            Order = 12,\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.Same(cluster, routeConfig.Cluster);\n        Assert.Equal(\"route1\", routeEndpoint.DisplayName);\n        Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata<RouteModel>());\n        Assert.Equal(\"/a\", routeEndpoint.RoutePattern.RawText);\n        Assert.Equal(12, routeEndpoint.Order);\n        Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision));\n\n        var hostMetadata = routeEndpoint.Metadata.GetMetadata<HostAttribute>();\n        Assert.Null(hostMetadata);\n    }\n\n    [Fact]\n    public void AddEndpoint_NullMatchers_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Order = 12,\n            Match = new RouteMatch()\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.Same(cluster, routeConfig.Cluster);\n        Assert.Equal(\"route1\", routeEndpoint.DisplayName);\n        Assert.Same(routeConfig, routeEndpoint.Metadata.GetMetadata<RouteModel>());\n        Assert.Equal(\"/{**catchall}\", routeEndpoint.RoutePattern.RawText);\n        Assert.Equal(12, routeEndpoint.Order);\n        Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision));\n\n        var hostMetadata = routeEndpoint.Metadata.GetMetadata<HostAttribute>();\n        Assert.Null(hostMetadata);\n    }\n\n    [Fact]\n    public void AddEndpoint_InvalidPath_BubblesOutException()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Path = \"/{invalid\",\n            },\n            Order = 12,\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        Action action = () => CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.Throws<RoutePatternException>(action);\n    }\n\n    [Fact]\n    public void AddEndpoint_DefaultAuth_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            AuthorizationPolicy = \"defaulT\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        var attribute = Assert.IsType<AuthorizeAttribute>(routeEndpoint.Metadata.GetMetadata<IAuthorizeData>());\n        Assert.Null(attribute.Policy);\n    }\n\n    [Fact]\n    public void AddEndpoint_AnonymousAuth_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            AuthorizationPolicy = \"AnonymouS\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.IsType<AllowAnonymousAttribute>(routeEndpoint.Metadata.GetMetadata<IAllowAnonymous>());\n    }\n\n    [Fact]\n    public void AddEndpoint_CustomAuth_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            AuthorizationPolicy = \"custom\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        var attribute = Assert.IsType<AuthorizeAttribute>(routeEndpoint.Metadata.GetMetadata<IAuthorizeData>());\n        Assert.Equal(\"custom\", attribute.Policy);\n    }\n\n    [Fact]\n    public void AddEndpoint_NoAuth_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<IAuthorizeData>());\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<IAllowAnonymous>());\n    }\n\n    [Fact]\n    public void AddEndpoint_DefaultRateLimiter_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            RateLimiterPolicy = \"defaulT\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<EnableRateLimitingAttribute>());\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<DisableRateLimitingAttribute>());\n    }\n\n    [Fact]\n    public void AddEndpoint_CustomRateLimiter_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            RateLimiterPolicy = \"custom\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        var attribute = routeEndpoint.Metadata.GetMetadata<EnableRateLimitingAttribute>();\n        Assert.NotNull(attribute);\n        Assert.Equal(\"custom\", attribute.PolicyName);\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<DisableRateLimitingAttribute>());\n    }\n\n    [Fact]\n    public void AddEndpoint_DisableRateLimiter_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            RateLimiterPolicy = \"disAble\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.NotNull(routeEndpoint.Metadata.GetMetadata<DisableRateLimitingAttribute>());\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<EnableRateLimitingAttribute>());\n    }\n\n    [Fact]\n    public void AddEndpoint_NoRateLimiter_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<EnableRateLimitingAttribute>());\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<DisableRateLimitingAttribute>());\n    }\n\n    [Fact]\n    public void AddEndpoint_CustomTimeoutPolicy_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            TimeoutPolicy = \"custom\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        var attribute = routeEndpoint.Metadata.GetMetadata<RequestTimeoutAttribute>();\n        Assert.NotNull(attribute);\n        Assert.Equal(\"custom\", attribute.PolicyName);\n        Assert.Null(attribute.Timeout);\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<DisableRequestTimeoutAttribute>());\n    }\n\n    [Fact]\n    public void AddEndpoint_CustomTimeout_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Timeout = TimeSpan.FromSeconds(5),\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        var attribute = routeEndpoint.Metadata.GetMetadata<RequestTimeoutAttribute>();\n        Assert.NotNull(attribute);\n        Assert.Null(attribute.PolicyName);\n        Assert.Equal(TimeSpan.FromSeconds(5), attribute.Timeout);\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<DisableRequestTimeoutAttribute>());\n    }\n\n    [Fact]\n    public void AddEndpoint_DisableTimeoutPolicy_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            TimeoutPolicy = \"disAble\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.NotNull(routeEndpoint.Metadata.GetMetadata<DisableRequestTimeoutAttribute>());\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<RequestTimeoutAttribute>());\n    }\n\n    [Fact]\n    public void AddEndpoint_NoTimeoutPolicy_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<RequestTimeoutAttribute>());\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<DisableRequestTimeoutAttribute>());\n    }\n\n    [Fact]\n    public void AddEndpoint_DefaultCors_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            CorsPolicy = \"defaulT\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        var attribute = Assert.IsType<EnableCorsAttribute>(routeEndpoint.Metadata.GetMetadata<IEnableCorsAttribute>());\n        Assert.Null(attribute.PolicyName);\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<IDisableCorsAttribute>());\n    }\n\n    [Fact]\n    public void AddEndpoint_CustomCors_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            CorsPolicy = \"custom\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        var attribute = Assert.IsType<EnableCorsAttribute>(routeEndpoint.Metadata.GetMetadata<IEnableCorsAttribute>());\n        Assert.Equal(\"custom\", attribute.PolicyName);\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<IDisableCorsAttribute>());\n    }\n\n    [Fact]\n    public void AddEndpoint_DisableCors_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            CorsPolicy = \"disAble\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.IsType<DisableCorsAttribute>(routeEndpoint.Metadata.GetMetadata<IDisableCorsAttribute>());\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<IEnableCorsAttribute>());\n    }\n\n    [Fact]\n    public void AddEndpoint_NoCors_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Order = 12,\n            Match = new RouteMatch(),\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<IEnableCorsAttribute>());\n        Assert.Null(routeEndpoint.Metadata.GetMetadata<IDisableCorsAttribute>());\n    }\n\n    [Fact]\n    public void BuildEndpoints_Header_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Path = \"/\",\n                Headers = new[]\n                {\n                    new RouteHeader()\n                    {\n                        Name = \"header1\",\n                        Values = new[] { \"value1\" },\n                        Mode = HeaderMatchMode.HeaderPrefix,\n                        IsCaseSensitive = true,\n                    }\n                }\n            },\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.Same(cluster, routeConfig.Cluster);\n        Assert.Equal(\"route1\", routeEndpoint.DisplayName);\n        var headerMetadata = routeEndpoint.Metadata.GetMetadata<IHeaderMetadata>();\n        Assert.NotNull(headerMetadata);\n        var matchers = headerMetadata.Matchers;\n        Assert.Single(matchers);\n        var matcher = matchers.Single();\n        Assert.Equal(\"header1\", matcher.Name);\n        Assert.Equal(new[] { \"value1\" }, matcher.Values);\n        Assert.Equal(HeaderMatchMode.HeaderPrefix, matcher.Mode);\n        Assert.Equal(StringComparison.Ordinal, matcher.Comparison);\n\n        Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision));\n    }\n\n    [Fact]\n    public void BuildEndpoints_Headers_Works()\n    {\n        var services = CreateServices();\n        var factory = services.GetRequiredService<ProxyEndpointFactory>();\n        factory.SetProxyPipeline(context => Task.CompletedTask);\n\n        var route = new RouteConfig\n        {\n            RouteId = \"route1\",\n            Match = new RouteMatch\n            {\n                Path = \"/\",\n                Headers = new[]\n                {\n                    new RouteHeader()\n                    {\n                        Name = \"header1\",\n                        Values = new[] { \"value1\" },\n                        Mode = HeaderMatchMode.HeaderPrefix,\n                        IsCaseSensitive = true,\n                    },\n                    new RouteHeader()\n                    {\n                        Name = \"header2\",\n                        Mode = HeaderMatchMode.Exists,\n                    }\n                }\n            },\n        };\n        var cluster = new ClusterState(\"cluster1\");\n        var routeState = new RouteState(\"route1\");\n\n        var (routeEndpoint, routeConfig) = CreateEndpoint(factory, routeState, route, cluster);\n\n        Assert.Same(cluster, routeConfig.Cluster);\n        Assert.Equal(\"route1\", routeEndpoint.DisplayName);\n        var metadata = routeEndpoint.Metadata.GetMetadata<IHeaderMetadata>();\n        Assert.Equal(2, metadata.Matchers.Length);\n\n        var firstMetadata = metadata.Matchers.First();\n        Assert.NotNull(firstMetadata);\n        Assert.Equal(\"header1\", firstMetadata.Name);\n        Assert.Equal(new[] { \"value1\" }, firstMetadata.Values);\n        Assert.Equal(HeaderMatchMode.HeaderPrefix, firstMetadata.Mode);\n        Assert.Equal(StringComparison.Ordinal, firstMetadata.Comparison);\n\n        var secondMetadata = metadata.Matchers.Skip(1).Single();\n        Assert.NotNull(secondMetadata);\n        Assert.Equal(\"header2\", secondMetadata.Name);\n        Assert.Same(Array.Empty<string>(), secondMetadata.Values);\n        Assert.Equal(HeaderMatchMode.Exists, secondMetadata.Mode);\n        Assert.Equal(StringComparison.OrdinalIgnoreCase, secondMetadata.Comparison);\n\n        Assert.False(routeConfig.HasConfigChanged(route, cluster, routeState.ClusterRevision));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Routing/QueryMatcherPolicyTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.AspNetCore.Routing.Matching;\nusing Microsoft.AspNetCore.Routing.Patterns;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Routing.Tests;\n\npublic class QueryParameterMatcherPolicyTests\n{\n    [Fact]\n    public void Comparer_SortOrder_SingleRuleEqual()\n    {\n        // Most specific to least\n        var endpoints = new[]\n        {\n            (0, CreateEndpoint(\"queryparam\", new[] { \"abc\" }, QueryParameterMatchMode.Exact, isCaseSensitive: true)),\n\n            (0, CreateEndpoint(\"queryparam\", new[] { \"abc\" }, QueryParameterMatchMode.Exact)),\n            (0, CreateEndpoint(\"queryparam\", new[] { \"abc\", \"def\" }, QueryParameterMatchMode.Exact)),\n            (0, CreateEndpoint(\"queryparam2\", new[] { \"abc\", \"def\" }, QueryParameterMatchMode.Exact)),\n\n            (0, CreateEndpoint(\"queryparam\", new[] { \"abc\" }, QueryParameterMatchMode.Contains, isCaseSensitive: true)),\n\n            (0, CreateEndpoint(\"queryparam\", new[] { \"abc\" }, QueryParameterMatchMode.Contains)),\n            (0, CreateEndpoint(\"queryparam\", new[] { \"abc\", \"def\" }, QueryParameterMatchMode.Contains)),\n            (0, CreateEndpoint(\"queryparam2\", new[] { \"abc\", \"def\" }, QueryParameterMatchMode.Contains)),\n\n            (0, CreateEndpoint(\"queryparam\", new[] { \"abc\" }, QueryParameterMatchMode.Prefix)),\n            (0, CreateEndpoint(\"queryparam\", new[] { \"abc\", \"def\" }, QueryParameterMatchMode.Prefix)),\n            (0, CreateEndpoint(\"queryparam2\", new[] { \"abc\", \"def\" }, QueryParameterMatchMode.Prefix)),\n\n            (0, CreateEndpoint(\"queryparam\", Array.Empty<string>(), QueryParameterMatchMode.Exists, isCaseSensitive: true)),\n            (0, CreateEndpoint(\"queryparam\", Array.Empty<string>(), QueryParameterMatchMode.Exists)),\n            (0, CreateEndpoint(\"queryparam\", Array.Empty<string>(), QueryParameterMatchMode.Exists, isCaseSensitive: true)),\n            (0, CreateEndpoint(\"queryparam\", Array.Empty<string>(), QueryParameterMatchMode.Exists)),\n        };\n        var sut = new QueryParameterMatcherPolicy();\n\n        for (var i = 0; i < endpoints.Length; i++)\n        {\n            for (var j = 0; j < endpoints.Length; j++)\n            {\n                var a = endpoints[i];\n                var b = endpoints[j];\n\n                var actual = sut.Comparer.Compare(a.Item2, b.Item2);\n                var expected =\n                    a.Item1 < b.Item1 ? -1 :\n                    a.Item1 > b.Item1 ? 1 : 0;\n                if (actual != expected)\n                {\n                    Assert.Fail($\"Error comparing [{i}] to [{j}], expected {expected}, found {actual}.\");\n                }\n            }\n        }\n    }\n\n    [Fact]\n    public void Comparer_MultipleQueryParameters_SortOrder()\n    {\n        // Most specific to least\n        var endpoints = new[]\n        {\n            (0, CreateEndpoint(new[]\n            {\n                new QueryParameterMatcher(\"queryparam\", Array.Empty<string>(), QueryParameterMatchMode.Exists, isCaseSensitive: true),\n                new QueryParameterMatcher(\"queryparam\", new[] { \"abc\" }, QueryParameterMatchMode.Prefix, isCaseSensitive: true),\n                new QueryParameterMatcher(\"queryparam\", new[] { \"abc\" }, QueryParameterMatchMode.Contains, isCaseSensitive: true),\n                new QueryParameterMatcher(\"queryparam\", new[] { \"abc\" }, QueryParameterMatchMode.Exact, isCaseSensitive: true)\n            })),\n\n            (1, CreateEndpoint(new[]\n            {\n                new QueryParameterMatcher(\"queryparam\", new[] { \"abc\" }, QueryParameterMatchMode.Contains, isCaseSensitive: true),\n                new QueryParameterMatcher(\"queryparam\", new[] { \"abc\" }, QueryParameterMatchMode.Exact, isCaseSensitive: true)\n            })),\n            (1, CreateEndpoint(new[]\n            {\n                new QueryParameterMatcher(\"queryparam\", Array.Empty<string>(), QueryParameterMatchMode.Exists, isCaseSensitive: true),\n                new QueryParameterMatcher(\"queryparam\", new[] { \"abc\" }, QueryParameterMatchMode.Exact, isCaseSensitive: true)\n            })),\n\n            (2, CreateEndpoint(\"queryparam\", new[] { \"abc\" })),\n\n            (3, CreateEndpoint(Array.Empty<QueryParameterMatcher>())),\n        };\n        var sut = new QueryParameterMatcherPolicy();\n\n        for (var i = 0; i < endpoints.Length; i++)\n        {\n            for (var j = 0; j < endpoints.Length; j++)\n            {\n                var a = endpoints[i];\n                var b = endpoints[j];\n\n                var actual = sut.Comparer.Compare(a.Item2, b.Item2);\n                var expected =\n                    a.Item1 < b.Item1 ? -1 :\n                    a.Item1 > b.Item1 ? 1 : 0;\n                if (actual != expected)\n                {\n                    Assert.Fail($\"Error comparing [{i}] to [{j}], expected {expected}, found {actual}.\");\n                }\n            }\n        }\n    }\n\n    [Fact]\n    public void AppliesToEndpoints_AppliesScenarios()\n    {\n        var scenarios = new[]\n        {\n            CreateEndpoint(\"org-id\", Array.Empty<string>(), QueryParameterMatchMode.Exists),\n            CreateEndpoint(\"org-id\", new[] { \"abc\" }),\n            CreateEndpoint(\"org-id\", new[] { \"abc\", \"def\" }),\n            CreateEndpoint(\"org-id\", Array.Empty<string>(), QueryParameterMatchMode.Exists, isDynamic: true),\n            CreateEndpoint(\"org-id\", new[] { \"abc\" }, isDynamic: true),\n            CreateEndpoint(\"org-id\", null, QueryParameterMatchMode.Exists, isDynamic: true),\n            CreateEndpoint(new[]\n            {\n                new QueryParameterMatcher(\"queryParam\", Array.Empty<string>(), QueryParameterMatchMode.Exists, isCaseSensitive: true),\n                new QueryParameterMatcher(\"queryParam\", new[] { \"abc\" }, QueryParameterMatchMode.Exact, isCaseSensitive: true)\n            })\n        };\n        var sut = new QueryParameterMatcherPolicy();\n        var endpointSelectorPolicy = (IEndpointSelectorPolicy)sut;\n\n        for (var i = 0; i < scenarios.Length; i++)\n        {\n            var result = endpointSelectorPolicy.AppliesToEndpoints(new[] { scenarios[i] });\n            Assert.True(result, $\"scenario {i}\");\n        }\n    }\n\n    [Fact]\n    public void AppliesToEndpoints_NoMetadata_DoesNotApply()\n    {\n        var endpoint = CreateEndpoint(Array.Empty<QueryParameterMatcher>());\n\n        var sut = new QueryParameterMatcherPolicy();\n        var endpointSelectorPolicy = (IEndpointSelectorPolicy)sut;\n\n        var result = endpointSelectorPolicy.AppliesToEndpoints(new[] { endpoint });\n        Assert.False(result);\n    }\n\n    [Theory]\n    [InlineData(null, false)]\n    [InlineData(\"\", false)]\n    [InlineData(\"abc\", true)]\n    public async Task ApplyAsync_MatchingScenarios_AnyQueryParamValue(string incomingQueryParamValue, bool shouldMatch)\n    {\n        var context = new DefaultHttpContext();\n        if (incomingQueryParamValue is not null)\n        {\n            var queryStr = \"?org-id=\" + incomingQueryParamValue;\n            context.Request.QueryString = new QueryString(queryStr);\n        }\n\n        var endpoint = CreateEndpoint(\"org-id\", Array.Empty<string>(), QueryParameterMatchMode.Exists);\n        var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]);\n        var sut = new QueryParameterMatcherPolicy();\n\n        await sut.ApplyAsync(context, candidates);\n\n        Assert.Equal(shouldMatch, candidates.IsValidCandidate(0));\n    }\n\n    [Theory]\n    [InlineData(\"abc\", QueryParameterMatchMode.Exact, false, null, false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Exact, false, \"\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Exact, false, \"abc\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Exact, false, \"aBC\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Exact, false, \"abcd\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Exact, false, \"ab\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Exact, true, \"\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Exact, true, \"abc\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Exact, true, \"aBC\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Exact, true, \"abcd\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Exact, true, \"ab\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Exact, true, \"ab;cd\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Exact, true, \"a;abc\", true)]\n    [InlineData(\"val ue\", QueryParameterMatchMode.Contains, false, \"val%20ue\", true)]\n    [InlineData(\"value\", QueryParameterMatchMode.Contains, false, \"val%20ue\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Contains, false, \"\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Contains, false, \"aabc\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Contains, false, \"zaBCz\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Contains, false, \"sabcd\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Contains, false, \"aaab\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Contains, true, \"\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Contains, true, \"abcaa\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Contains, true, \"cbcaBC\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Contains, true, \"ababcd\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Contains, true, \"aaaBCd\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Contains, true, \"baba\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Prefix, false, \"\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Prefix, false, \"abc\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Prefix, false, \"aBC\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Prefix, false, \"abcd\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Prefix, false, \"ab\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Prefix, true, \"\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Prefix, true, \"abc\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Prefix, true, \"aBC\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Prefix, true, \"abcd\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Prefix, true, \"aBCd\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.Prefix, true, \"ab\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.NotContains, false, \"\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.NotContains, false, \"aabc\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.NotContains, false, \"zaBCz\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.NotContains, false, \"sabcd\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.NotContains, false, \"aaab\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.NotContains, true, \"\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.NotContains, true, \"abcaa\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.NotContains, true, \"cbcaBC\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.NotContains, true, \"ababcd\", false)]\n    [InlineData(\"abc\", QueryParameterMatchMode.NotContains, true, \"aaaBCd\", true)]\n    [InlineData(\"abc\", QueryParameterMatchMode.NotContains, true, \"baba\", true)]\n    public async Task ApplyAsync_MatchingScenarios_OneQueryParamValue(\n        string queryParamValue,\n        QueryParameterMatchMode queryParamValueMatchMode,\n        bool isCaseSensitive,\n        string incomingQueryParamValue,\n        bool shouldMatch)\n    {\n        var context = new DefaultHttpContext();\n        if (incomingQueryParamValue is not null)\n        {\n            var queryStr = \"?org-id=\" + string.Join(\"&org-id=\", incomingQueryParamValue?.Split(';') ?? new[] { \"\" });\n            context.Request.QueryString = new QueryString(queryStr);\n        }\n\n        var endpoint = CreateEndpoint(\"org-id\", new[] { queryParamValue }, queryParamValueMatchMode, isCaseSensitive);\n        var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]);\n        var sut = new QueryParameterMatcherPolicy();\n\n        await sut.ApplyAsync(context, candidates);\n\n        Assert.Equal(shouldMatch, candidates.IsValidCandidate(0));\n    }\n\n    [Theory]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, false, null, false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, false, \"\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, false, \"abc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, false, \"aBc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, false, \"abcd\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, false, \"def\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, false, \"deF\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, false, \"defg\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, null, false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, \"\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, \"abc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, \"aBC\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, \"aBCd\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, \"def\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, \"DEFg\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, \"dEf\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, \";\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, \"abc;a\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, \"a;abc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, \"abc;def\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, \"ab;def\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Exact, true, \"ab;cdef\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, false, null, false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, false, \"\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, false, \"abc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, false, \"aBc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, false, \"abcd\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, false, \"abcD\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, false, \"def\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, false, \"deF\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, false, \"defg\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, false, \"defG\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, false, \"abcA\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, false, \"aabc\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, null, false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"abc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"aBC\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"aBCd\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"def\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"DEFg\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"abcc\", true)]\n    [InlineData(\"val ue\", \"def\", QueryParameterMatchMode.Contains, false, \"val%20ue&aabb\", true)]\n    [InlineData(\"value\", \"def\", QueryParameterMatchMode.Contains, false, \"val%20ue&aabb\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"aabc\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \";\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"ab;cde;fgh\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"abcd;e\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"abcd;defg\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"Abcd;defg\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"Abcd;Defg\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"a;defg\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \"abcd;\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Prefix, true, \";def\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, false, null, false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, false, \"\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, false, \"aaaabc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, false, \"aBc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, false, \"aabcdd\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, false, \"ddabcD\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, false, \"dedef\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, false, \"adeFF\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, false, \"degdefg\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, false, \"efgdefG\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, false, \"AAabc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, null, false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"abc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"aBCcba\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"abaBCd\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"efdeff\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"FDEDEFg\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"aabc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, false, \"aaa\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, false, \"Abc\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, false, \"def\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, false, \"\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"cabca\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"aBCa\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"CaBCdd\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"DEFdef\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"defDEFg\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"bbaabc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \";\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"cabca;\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \";cabca\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"ab;cd;ef\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"aBCa;deFg\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"aBCa;defg\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.Contains, true, \"abcd;d\", true)]\n    [InlineData(\"abc\", \"ABC\", QueryParameterMatchMode.Contains, true, \"abc;d\", true)]\n    [InlineData(\"abc\", \"ABC\", QueryParameterMatchMode.Contains, true, \"ABC;d\", true)]\n    [InlineData(\"abc\", \"ABC\", QueryParameterMatchMode.Contains, true, \"abC;d\", false)]\n    [InlineData(\"abc\", \"ABC\", QueryParameterMatchMode.Contains, true, \"abcABC;d\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, false, null, true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"aaa\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"Abc\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"def\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, false, \"aabc\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, false, \"baBc\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, false, \"ababcd\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, false, \"dcabcD\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, false, \"ghi\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, null, true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"cabca\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"aBCa\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"CaBCdd\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"DEFdef\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"DEFg\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"bbaabc\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"defG\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"bbaabc;\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \";bbaabc\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"ab;cd;ef\", true)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"a;defg\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"ab;cdef\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"abc;def\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"Abc;cdef\", false)]\n    [InlineData(\"abc\", \"def\", QueryParameterMatchMode.NotContains, true, \"Abc;cdEf\", true)]\n    public async Task ApplyAsync_MatchingScenarios_TwoQueryParamValues(\n        string queryParam1Value,\n        string queryParam2Value,\n        QueryParameterMatchMode queryParamValueMatchMode,\n        bool isCaseSensitive,\n        string incomingQueryParamValue,\n        bool shouldMatch)\n    {\n        var context = new DefaultHttpContext();\n        if (incomingQueryParamValue is not null)\n        {\n            var queryStr = \"?org-id=\" + string.Join(\"&org-id=\", incomingQueryParamValue?.Split(';') ?? new[] { \"\" });\n            context.Request.QueryString = new QueryString(queryStr);\n        }\n\n        var endpoint = CreateEndpoint(\"org-id\", new[] { queryParam1Value, queryParam2Value }, queryParamValueMatchMode, isCaseSensitive);\n\n        var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]);\n        var sut = new QueryParameterMatcherPolicy();\n\n        await sut.ApplyAsync(context, candidates);\n\n        Assert.Equal(shouldMatch, candidates.IsValidCandidate(0));\n    }\n\n    [Theory]\n    [InlineData(QueryParameterMatchMode.NotContains, true, true)]\n    [InlineData(QueryParameterMatchMode.NotContains, false, true)]\n    [InlineData(QueryParameterMatchMode.Exists, true, false)]\n    [InlineData(QueryParameterMatchMode.Exists, false, false)]\n    [InlineData(QueryParameterMatchMode.Contains, true, false)]\n    [InlineData(QueryParameterMatchMode.Contains, false, false)]\n    [InlineData(QueryParameterMatchMode.Exact, true, false)]\n    [InlineData(QueryParameterMatchMode.Exact, false, false)]\n    [InlineData(QueryParameterMatchMode.Prefix, true, false)]\n    [InlineData(QueryParameterMatchMode.Prefix, false, false)]\n    public async Task ApplyAsync_MatchingScenarios_MissingParam(\n        QueryParameterMatchMode queryParamValueMatchMode,\n        bool isCaseSensitive,\n        bool shouldMatch)\n    {\n        var context = new DefaultHttpContext();\n\n        var queryParamValues = new[] { \"bar\" };\n        if (queryParamValueMatchMode == QueryParameterMatchMode.Exists)\n        {\n            queryParamValues = null;\n        }\n\n        var endpoint = CreateEndpoint(\"foo\", queryParamValues, queryParamValueMatchMode, isCaseSensitive);\n        var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]);\n        var sut = new QueryParameterMatcherPolicy();\n\n        await sut.ApplyAsync(context, candidates);\n\n        Assert.Equal(shouldMatch, candidates.IsValidCandidate(0));\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, true, false)]\n    [InlineData(true, false, false)]\n    [InlineData(true, true, true)]\n    public async Task ApplyAsync_MultipleRules_RequiresAllQueryParameter(bool sendQueryParam1, bool sendQueryParam2, bool shouldMatch)\n    {\n        var endpoint = CreateEndpoint(new[]\n        {\n            new QueryParameterMatcher(\"queryParam1\", new[] { \"value1\" }, QueryParameterMatchMode.Exact, isCaseSensitive: false),\n            new QueryParameterMatcher(\"queryParam2\", new[] { \"value2\" }, QueryParameterMatchMode.Exact, isCaseSensitive: false)\n        });\n\n        var context = new DefaultHttpContext();\n        var queryStr = new List<string>();\n        if (sendQueryParam1)\n        {\n            queryStr.Add(\"queryParam1=value1\");\n        }\n        if (sendQueryParam2)\n        {\n            queryStr.Add(\"queryParam2=value2\");\n        }\n\n        context.Request.QueryString = new QueryString(\"?\" + string.Join(\"&\", queryStr));\n        var candidates = new CandidateSet(new[] { endpoint }, new RouteValueDictionary[1], new int[1]);\n        var sut = new QueryParameterMatcherPolicy();\n\n        await sut.ApplyAsync(context, candidates);\n\n        Assert.Equal(shouldMatch, candidates.IsValidCandidate(0));\n    }\n\n    private static Endpoint CreateEndpoint(\n        string queryParamName,\n        string[] queryParamValues,\n        QueryParameterMatchMode mode = QueryParameterMatchMode.Exact,\n        bool isCaseSensitive = false,\n        bool isDynamic = false)\n    {\n        return CreateEndpoint(new[] { new QueryParameterMatcher(queryParamName, queryParamValues, mode, isCaseSensitive) }, isDynamic);\n    }\n\n    private static Endpoint CreateEndpoint(IReadOnlyList<QueryParameterMatcher> matchers, bool isDynamic = false)\n    {\n        var builder = new RouteEndpointBuilder(_ => Task.CompletedTask, RoutePatternFactory.Parse(\"/\"), 0);\n        builder.Metadata.Add(new QueryParameterMetadata(matchers));\n        if (isDynamic)\n        {\n            builder.Metadata.Add(new DynamicEndpointMetadata());\n        }\n\n        return builder.Build();\n    }\n\n    private class DynamicEndpointMetadata : IDynamicEndpointMetadata\n    {\n        public bool IsDynamic => true;\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Routing/ReverseProxyConventionBuilderTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.AspNetCore.Routing.Patterns;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Microsoft.AspNetCore.Builder.Tests;\n\npublic class ReverseProxyConventionBuilderTests\n{\n    [Fact]\n    public void ReverseProxyConventionBuilder_Configure_Works()\n    {\n        var configured = false;\n\n        var conventions = new List<Action<EndpointBuilder>>();\n        var builder = new ReverseProxyConventionBuilder(conventions);\n\n        builder.ConfigureEndpoints(builder =>\n        {\n            configured = true;\n        });\n\n        var routeConfig = new RouteConfig();\n        var cluster = new ClusterConfig();\n        var endpointBuilder = CreateEndpointBuilder(routeConfig, cluster);\n\n        var action = Assert.Single(conventions);\n        action(endpointBuilder);\n\n        Assert.True(configured);\n    }\n\n    [Fact]\n    public void ReverseProxyConventionBuilder_ConfigureWithProxy_Works()\n    {\n        var configured = false;\n\n        var conventions = new List<Action<EndpointBuilder>>();\n        var builder = new ReverseProxyConventionBuilder(conventions);\n\n        builder.ConfigureEndpoints((builder, proxy) =>\n        {\n            configured = true;\n        });\n\n        var routeConfig = new RouteConfig();\n        var cluster = new ClusterConfig();\n        var endpointBuilder = CreateEndpointBuilder(routeConfig, cluster);\n\n        var action = Assert.Single(conventions);\n        action(endpointBuilder);\n\n        Assert.True(configured);\n    }\n\n    [Fact]\n    public void ReverseProxyConventionBuilder_ConfigureWithProxyAndCluster_Works()\n    {\n        var configured = false;\n\n        var conventions = new List<Action<EndpointBuilder>>();\n        var builder = new ReverseProxyConventionBuilder(conventions);\n\n        builder.ConfigureEndpoints((builder, proxy, cluster) =>\n        {\n            configured = true;\n        });\n\n        var routeConfig = new RouteConfig();\n        var cluster = new ClusterConfig();\n        var endpointBuilder = CreateEndpointBuilder(routeConfig, cluster);\n\n        var action = Assert.Single(conventions);\n        action(endpointBuilder);\n\n        Assert.True(configured);\n    }\n\n    private static RouteEndpointBuilder CreateEndpointBuilder(RouteConfig routeConfig, ClusterConfig cluster)\n    {\n        var endpointBuilder = new RouteEndpointBuilder(context => Task.CompletedTask, RoutePatternFactory.Parse(\"\"), 0);\n        var routeModel = new RouteModel(\n            routeConfig,\n            new ClusterState(\"cluster-1\")\n            {\n                Model = new ClusterModel(cluster, new HttpMessageInvoker(new HttpClientHandler()))\n            },\n            HttpTransformer.Default);\n\n        endpointBuilder.Metadata.Add(routeModel);\n\n        return endpointBuilder;\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Routing/RoutingTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.TestHost;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace Yarp.ReverseProxy.Routing.Tests;\n\npublic class RoutingTests\n{\n    [Fact]\n    public async Task PathRouting_Works()\n    {\n        var routes = new[]\n        {\n            new RouteConfig()\n            {\n                RouteId = \"route1\",\n                ClusterId = \"cluster1\",\n                Match = new RouteMatch { Path = \"/api/{**catchall}\" }\n            }\n        };\n\n        using var host = await CreateHostAsync(routes);\n        var client = host.GetTestClient();\n\n        // Positive\n        var response = await client.GetAsync(\"/api/extra\");\n        response.EnsureSuccessStatusCode();\n        Assert.Equal(\"route1\", response.Headers.GetValues(\"route\").SingleOrDefault());\n\n        // Negative\n        response = await client.GetAsync(\"/\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task HostRouting_Works()\n    {\n        var routes = new[]\n        {\n            new RouteConfig()\n            {\n                RouteId = \"route1\",\n                ClusterId = \"cluster1\",\n                Match = new RouteMatch { Hosts = new[] { \"*.example.com\" } }\n            }\n        };\n\n        using var host = await CreateHostAsync(routes);\n        var client = host.GetTestClient();\n\n        // Positive\n        var response = await client.GetAsync(\"http://foo.example.com/\");\n        response.EnsureSuccessStatusCode();\n        Assert.Equal(\"route1\", response.Headers.GetValues(\"route\").SingleOrDefault());\n\n        // Negative\n        response = await client.GetAsync(\"http://example.com\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task HeaderRouting_OneHeader_Works()\n    {\n        var routes = new[]\n        {\n            new RouteConfig()\n            {\n                RouteId = \"route1\",\n                ClusterId = \"cluster1\",\n                Match = new RouteMatch\n                {\n                    Path = \"/{**catchall}\",\n                    Headers = new[]\n                    {\n                        new RouteHeader()\n                        {\n                            Name = \"header1\",\n                            Values = new[] { \"value1\" },\n                        }\n                    }\n                }\n            }\n        };\n\n        using var host = await CreateHostAsync(routes);\n        var client = host.GetTestClient();\n\n        // Positive\n        var request = new HttpRequestMessage();\n        request.Headers.Add(\"header1\", \"value1\");\n        var response = await client.SendAsync(request);\n        response.EnsureSuccessStatusCode();\n        Assert.Equal(\"route1\", response.Headers.GetValues(\"route\").SingleOrDefault());\n\n        // Negative\n        response = await client.GetAsync(\"/\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n\n        request = new HttpRequestMessage();\n        request.Headers.Add(\"header2\", \"value1\");\n        response = await client.SendAsync(request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n\n        request = new HttpRequestMessage();\n        request.Headers.Add(\"header1\", \"v\");\n        response = await client.SendAsync(request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n\n        request = new HttpRequestMessage();\n        request.Headers.Add(\"header1\", (string)null);\n        response = await client.SendAsync(request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task HeaderRouting_MultipleHeaders_Works()\n    {\n        var routes = new[]\n        {\n            new RouteConfig()\n            {\n                RouteId = \"route1\",\n                ClusterId = \"cluster1\",\n                Match = new RouteMatch\n                {\n                    Path = \"/{**catchall}\",\n                    Headers = new[]\n                    {\n                        new RouteHeader()\n                        {\n                            Name = \"header1\",\n                            Values = new[] { \"value1\" },\n                        }\n                    }\n                }\n            },\n            new RouteConfig()\n            {\n                RouteId = \"route2\",\n                ClusterId = \"cluster1\",\n                Match = new RouteMatch\n                {\n                    Path = \"/{**catchall}\",\n                    Headers = new[]\n                    {\n                        new RouteHeader()\n                        {\n                            Name = \"header2\",\n                            Values = new[] { \"value2\" },\n                        }\n                    }\n                }\n            },\n            new RouteConfig()\n            {\n                RouteId = \"route3\",\n                ClusterId = \"cluster1\",\n                Match = new RouteMatch\n                {\n                    Path = \"/{**catchall}\",\n                    Headers = new[]\n                    {\n                        new RouteHeader()\n                        {\n                            Name = \"header1\",\n                            Values = new[] { \"value1\" },\n                        },\n                        new RouteHeader()\n                        {\n                            Name = \"header2\",\n                            Values = new[] { \"value2\" },\n                        }\n                    }\n                }\n            }\n        };\n\n        using var host = await CreateHostAsync(routes);\n        var client = host.GetTestClient();\n\n        // Check for the most specific match\n        var request = new HttpRequestMessage();\n        request.Headers.Add(\"header1\", \"value1\");\n        var response = await client.SendAsync(request);\n        response.EnsureSuccessStatusCode();\n        Assert.Equal(\"route1\", response.Headers.GetValues(\"route\").SingleOrDefault());\n\n        request = new HttpRequestMessage();\n        request.Headers.Add(\"header1\", \"value1\");\n        request.Headers.Add(\"header2\", \"value3\");\n        response = await client.SendAsync(request);\n        response.EnsureSuccessStatusCode();\n        Assert.Equal(\"route1\", response.Headers.GetValues(\"route\").SingleOrDefault());\n\n        request = new HttpRequestMessage();\n        request.Headers.Add(\"header2\", \"value2\");\n        response = await client.SendAsync(request);\n        response.EnsureSuccessStatusCode();\n        Assert.Equal(\"route2\", response.Headers.GetValues(\"route\").SingleOrDefault());\n\n        request = new HttpRequestMessage();\n        request.Headers.Add(\"header1\", \"value3\");\n        request.Headers.Add(\"header2\", \"value2\");\n        response = await client.SendAsync(request);\n        response.EnsureSuccessStatusCode();\n        Assert.Equal(\"route2\", response.Headers.GetValues(\"route\").SingleOrDefault());\n\n        request = new HttpRequestMessage();\n        request.Headers.Add(\"header1\", \"value1\");\n        request.Headers.Add(\"header2\", \"value2\");\n        response = await client.SendAsync(request);\n        response.EnsureSuccessStatusCode();\n        Assert.Equal(\"route3\", response.Headers.GetValues(\"route\").SingleOrDefault());\n\n        // Negative\n        response = await client.GetAsync(\"/\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n\n        request = new HttpRequestMessage();\n        request.Headers.Add(\"header1\", \"value2\");\n        request.Headers.Add(\"header2\", \"value1\");\n        response = await client.SendAsync(request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Precedence_PathMethodHostHeaders()\n    {\n        var routes = new[]\n        {\n            new RouteConfig()\n            {\n                RouteId = \"route1\",\n                ClusterId = \"cluster1\",\n                Match = new RouteMatch { Path = \"/route1\" }\n            },\n            new RouteConfig()\n            {\n                RouteId = \"route2\",\n                ClusterId = \"cluster1\",\n                Match = new RouteMatch\n                {\n                    Path = \"/{**catchall}\",\n                    Methods = new[] { \"GET\" },\n                }\n            },\n            new RouteConfig()\n            {\n                RouteId = \"route3\",\n                ClusterId = \"cluster1\",\n                Match = new RouteMatch\n                {\n                    Hosts = new[] { \"localhost\" }\n                }\n            },\n            new RouteConfig()\n            {\n                RouteId = \"route4\",\n                ClusterId = \"cluster1\",\n                Match = new RouteMatch\n                {\n                    Path = \"/{**catchall}\",\n                    Headers = new[]\n                    {\n                        new RouteHeader()\n                        {\n                            Name = \"header1\",\n                            Values = new[] { \"value1\" },\n                        },\n                    }\n                }\n            }\n        };\n\n        using var host = await CreateHostAsync(routes);\n        var client = host.GetTestClient();\n\n        // Check for the highest priority match\n\n        // Path\n        var request = new HttpRequestMessage(HttpMethod.Get, \"http://localhost/route1\");\n        request.Headers.Add(\"header1\", \"value1\");\n        var response = await client.SendAsync(request);\n        response.EnsureSuccessStatusCode();\n        Assert.Equal(\"route1\", response.Headers.GetValues(\"route\").SingleOrDefault());\n\n        // Method\n        request = new HttpRequestMessage(HttpMethod.Get, \"http://localhost/\");\n        request.Headers.Add(\"header1\", \"value1\");\n        response = await client.SendAsync(request);\n        response.EnsureSuccessStatusCode();\n        Assert.Equal(\"route2\", response.Headers.GetValues(\"route\").SingleOrDefault());\n\n        // Host\n        request = new HttpRequestMessage(HttpMethod.Post, \"http://localhost/\");\n        request.Headers.Add(\"header1\", \"value1\");\n        response = await client.SendAsync(request);\n        response.EnsureSuccessStatusCode();\n        Assert.Equal(\"route3\", response.Headers.GetValues(\"route\").SingleOrDefault());\n\n        // Header\n        request = new HttpRequestMessage(HttpMethod.Post, \"http://example/\");\n        request.Headers.Add(\"header1\", \"value1\");\n        response = await client.SendAsync(request);\n        response.EnsureSuccessStatusCode();\n        Assert.Equal(\"route4\", response.Headers.GetValues(\"route\").SingleOrDefault());\n    }\n\n    public static Task<IHost> CreateHostAsync(IReadOnlyList<RouteConfig> routes)\n    {\n        var clusters = new[]\n        {\n            new ClusterConfig()\n            {\n                ClusterId = \"cluster1\",\n                Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n                {\n                    { \"d1\", new DestinationConfig() { Address = \"http://localhost/\" }  }\n                }\n            }\n        };\n\n        return new HostBuilder()\n            .ConfigureWebHost(webHost =>\n            {\n                webHost.UseTestServer();\n                webHost.ConfigureServices(services =>\n                {\n                    services.AddReverseProxy()\n                        .LoadFromMemory(routes, clusters);\n                });\n                webHost.Configure(appBuilder =>\n                {\n                    appBuilder.UseRouting();\n                    appBuilder.UseEndpoints(endpoints =>\n                    {\n                        endpoints.MapReverseProxy(proxyApp =>\n                        {\n                            proxyApp.Run(context =>\n                            {\n                                var endpoint = context.GetEndpoint();\n                                context.Response.Headers[\"route\"] = endpoint.DisplayName;\n                                return Task.CompletedTask;\n                            });\n                        });\n                    });\n                });\n            }).StartAsync();\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/SessionAffinity/AffinitizeTransformProviderTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing Moq;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.SessionAffinity.Tests;\n\npublic class AffinitizeTransformProviderTests\n{\n    [Fact]\n    public void EnableSessionAffinity_AddsTransform()\n    {\n        var affinityPolicy = new Mock<ISessionAffinityPolicy>(MockBehavior.Strict);\n        affinityPolicy.SetupGet(p => p.Name).Returns(\"Policy\");\n\n        var transformProvider = new AffinitizeTransformProvider(new[] { affinityPolicy.Object });\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            SessionAffinity = new SessionAffinityConfig\n            {\n                Enabled = true,\n                Policy = \"Policy\",\n                AffinityKeyName = \"Key1\"\n            }\n        };\n\n        var validationContext = new TransformClusterValidationContext()\n        {\n            Cluster = cluster,\n        };\n        transformProvider.ValidateCluster(validationContext);\n\n        Assert.Empty(validationContext.Errors);\n\n        var builderContext = new TransformBuilderContext()\n        {\n            Cluster = cluster,\n        };\n        transformProvider.Apply(builderContext);\n\n        Assert.IsType<AffinitizeTransform>(builderContext.ResponseTransforms.Single());\n    }\n\n    [Fact]\n    public void EnableSession_InvalidMode_Fails()\n    {\n        var affinityPolicy = new Mock<ISessionAffinityPolicy>(MockBehavior.Strict);\n        affinityPolicy.SetupGet(p => p.Name).Returns(\"Policy\");\n\n        var transformProvider = new AffinitizeTransformProvider(new[] { affinityPolicy.Object });\n\n        var cluster = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            SessionAffinity = new SessionAffinityConfig\n            {\n                Enabled = true,\n                Policy = \"Invalid\",\n                AffinityKeyName = \"Key1\"\n            }\n        };\n\n        var validationContext = new TransformClusterValidationContext()\n        {\n            Cluster = cluster,\n        };\n        transformProvider.ValidateCluster(validationContext);\n\n        var ex = Assert.Single(validationContext.Errors);\n        Assert.Equal(\"No matching ISessionAffinityPolicy found for the session affinity policy 'Invalid' set on the cluster 'cluster1'.\", ex.Message);\n\n        var builderContext = new TransformBuilderContext()\n        {\n            Cluster = cluster,\n        };\n\n        ex = Assert.Throws<ArgumentException>(() => transformProvider.Apply(builderContext));\n        Assert.Equal($\"No {typeof(ISessionAffinityPolicy).FullName} was found for the id 'Invalid'. (Parameter 'id')\", ex.Message);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/SessionAffinity/AffinitizeTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Moq;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Transforms;\n\nnamespace Yarp.ReverseProxy.SessionAffinity.Tests;\n\npublic class AffinitizeTransformTests\n{\n    [Fact]\n    public async Task ApplyAsync_InvokeAffinitizeRequest()\n    {\n        var cluster = GetCluster();\n        var destination = cluster.Destinations.Values.First();\n        var provider = new Mock<ISessionAffinityPolicy>(MockBehavior.Strict);\n        provider\n            .Setup(p => p.AffinitizeResponseAsync(\n                It.IsAny<HttpContext>(),\n                It.IsAny<ClusterState>(),\n                It.IsNotNull<SessionAffinityConfig>(),\n                It.IsAny<DestinationState>(),\n                It.IsAny<CancellationToken>()))\n            .Returns(new ValueTask());\n\n        var transform = new AffinitizeTransform(provider.Object);\n\n        var context = new DefaultHttpContext();\n        context.Features.Set<IReverseProxyFeature>(new ReverseProxyFeature()\n        {\n            Cluster = cluster.Model,\n            Route = new RouteModel(new RouteConfig(), cluster, HttpTransformer.Default),\n            ProxiedDestination = destination,\n        });\n\n        var transformContext = new ResponseTransformContext()\n        {\n            HttpContext = context,\n        };\n        await transform.ApplyAsync(transformContext);\n\n        provider.Verify();\n    }\n\n    internal ClusterState GetCluster()\n    {\n        var cluster = new ClusterState(\"cluster-1\");\n        cluster.Destinations.GetOrAdd(\"dest-A\", id => new DestinationState(id));\n        cluster.Model = new ClusterModel(new ClusterConfig\n        {\n            SessionAffinity = new SessionAffinityConfig\n            {\n                Enabled = true,\n                Policy = \"Policy-B\",\n                FailurePolicy = \"Policy-1\",\n                AffinityKeyName = \"Key1\"\n            }\n        },\n        new HttpMessageInvoker(new Mock<HttpMessageHandler>().Object));\n\n        var destinations = cluster.Destinations.Values.ToList();\n        cluster.DestinationsState = new ClusterDestinationsState(destinations, destinations);\n        return cluster;\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/SessionAffinity/AffinityTestHelper.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Text;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.Extensions.Logging;\nusing Moq;\n\nnamespace Yarp.ReverseProxy.SessionAffinity.Tests;\n\npublic static class AffinityTestHelper\n{\n    public static Mock<ILogger<T>> GetLogger<T>()\n    {\n        var result = new Mock<ILogger<T>>();\n        result.Setup(l => l.IsEnabled(It.IsAny<LogLevel>())).Returns(true);\n        return result;\n    }\n\n    public static Mock<IDataProtector> GetDataProtector()\n    {\n        var protector = new Mock<IDataProtector>();\n        protector.Setup(p => p.CreateProtector(It.IsAny<string>())).Returns(protector.Object);\n        protector.Setup(p => p.Protect(It.IsAny<byte[]>())).Returns((byte[] b) => b);\n        protector.Setup(p => p.Unprotect(It.IsAny<byte[]>())).Returns((byte[] b) => b);\n        return protector;\n    }\n\n    public static string ToUTF8BytesInBase64(this string text)\n    {\n        return Convert.ToBase64String(Encoding.UTF8.GetBytes(text));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/SessionAffinity/ArrCookieSessionAffinityPolicyTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Security.Cryptography;\nusing System.Text;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.SessionAffinity.Tests;\n\npublic class ArrCookieSessionAffinityPolicyTests\n{\n    private readonly SessionAffinityConfig _config = new()\n    {\n        Enabled = true,\n        Policy = \"ArrCookie\",\n        FailurePolicy = \"Return503Error\",\n        AffinityKeyName = \"My.Affinity\",\n        Cookie = new SessionAffinityCookieConfig\n        {\n            Domain = \"mydomain.my\",\n            HttpOnly = false,\n            IsEssential = true,\n            MaxAge = TimeSpan.FromHours(1),\n            Path = \"/some\",\n            SameSite = SameSiteMode.Lax,\n            SecurePolicy = CookieSecurePolicy.Always,\n        }\n    };\n    private readonly IReadOnlyList<DestinationState> _destinations = new[] { new DestinationState(\"dest-A\"), new DestinationState(\"dest-B\"), new DestinationState(\"dest-C\") };\n\n    [Fact]\n    public void FindAffinitizedDestination_AffinityKeyIsNotSetOnRequest_ReturnKeyNotSet()\n    {\n        var policy = new ArrCookieSessionAffinityPolicy(\n            new TestTimeProvider(),\n            NullLogger<ArrCookieSessionAffinityPolicy>.Instance);\n\n        Assert.Equal(SessionAffinityConstants.Policies.ArrCookie, policy.Name);\n\n        var context = new DefaultHttpContext();\n        context.Request.Headers[\"Cookie\"] = new[] { $\"Some-Cookie=ZZZ\" };\n        var cluster = new ClusterState(\"cluster\");\n\n        var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations);\n\n        Assert.Equal(AffinityStatus.AffinityKeyNotSet, affinityResult.Status);\n        Assert.Null(affinityResult.Destinations);\n    }\n\n    [Fact]\n    public void FindAffinitizedDestination_AffinityKeyIsSetOnRequest_Success()\n    {\n        var policy = new ArrCookieSessionAffinityPolicy(\n            new TestTimeProvider(),\n            NullLogger<ArrCookieSessionAffinityPolicy>.Instance);\n        var context = new DefaultHttpContext();\n        var affinitizedDestination = _destinations[1];\n        context.Request.Headers[\"Cookie\"] = GetCookieWithAffinity(affinitizedDestination);\n        var cluster = new ClusterState(\"cluster\");\n\n        var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations);\n\n        Assert.Equal(AffinityStatus.OK, affinityResult.Status);\n        Assert.Single(affinityResult.Destinations);\n        Assert.Same(affinitizedDestination, affinityResult.Destinations[0]);\n    }\n\n    [Fact]\n    public void AffinitizedRequest_CustomConfigAffinityKeyIsNotExtracted_SetKeyOnResponse()\n    {\n        var policy = new ArrCookieSessionAffinityPolicy(\n            new TestTimeProvider(),\n            NullLogger<ArrCookieSessionAffinityPolicy>.Instance);\n        var context = new DefaultHttpContext();\n\n        policy.AffinitizeResponse(context, new ClusterState(\"cluster\"), _config, _destinations[1]);\n\n        var affinityCookieHeader = context.Response.Headers[\"Set-Cookie\"];\n        Assert.Equal(\"My.Affinity=920A160FA519353932B655488361A944531650016793761EE7224DE632863B13; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax\",\n            affinityCookieHeader);\n    }\n\n    [Fact]\n    public void AffinitizeRequest_CookieConfigSpecified_UseIt()\n    {\n        var policy = new ArrCookieSessionAffinityPolicy(\n            new TestTimeProvider(),\n            NullLogger<ArrCookieSessionAffinityPolicy>.Instance);\n        var context = new DefaultHttpContext();\n\n        policy.AffinitizeResponse(context, new ClusterState(\"cluster\"), _config, _destinations[1]);\n\n        var affinityCookieHeader = context.Response.Headers[\"Set-Cookie\"];\n        Assert.Equal(\"My.Affinity=920A160FA519353932B655488361A944531650016793761EE7224DE632863B13; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax\",\n            affinityCookieHeader);\n    }\n\n    [Fact]\n    public void AffinitizedRequest_AffinityKeyIsExtracted_DoNothing()\n    {\n        var policy = new ArrCookieSessionAffinityPolicy(\n            new TestTimeProvider(),\n            NullLogger<ArrCookieSessionAffinityPolicy>.Instance);\n        var context = new DefaultHttpContext();\n        var affinitizedDestination = _destinations[0];\n        context.Request.Headers[\"Cookie\"] = GetCookieWithAffinity(affinitizedDestination);\n        var cluster = new ClusterState(\"cluster\");\n\n        var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations);\n\n        Assert.Equal(AffinityStatus.OK, affinityResult.Status);\n\n        policy.AffinitizeResponse(context, cluster, _config, affinitizedDestination);\n\n        Assert.False(context.Response.Headers.ContainsKey(\"Cookie\"));\n    }\n\n    private string[] GetCookieWithAffinity(DestinationState affinitizedDestination)\n    {\n        var destinationIdBytes = Encoding.Unicode.GetBytes(affinitizedDestination.DestinationId.ToLowerInvariant());\n        var hashBytes = SHA256.HashData(destinationIdBytes);\n        var value = Convert.ToHexString(hashBytes);\n        return new[] { $\"Some-Cookie=ZZZ\", $\"{_config.AffinityKeyName}={value}\" };\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/SessionAffinity/BaseSessionAffinityPolicyTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\nusing Moq;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.SessionAffinity.Tests;\n\npublic class BaseSesstionAffinityPolicyTests\n{\n    private const string InvalidKeyNull = \"!invalid key - null!\";\n    private const string InvalidKeyThrow = \"!invalid key - throw!\";\n    private const string KeyName = \"StubAffinityKey\";\n    private readonly SessionAffinityConfig _defaultOptions = new SessionAffinityConfig\n    {\n        Enabled = true,\n        Policy = \"Stub\",\n        FailurePolicy = \"Return503Error\",\n        AffinityKeyName = \"StubAffinityKey\"\n    };\n\n    [Theory]\n    [MemberData(nameof(FindAffinitizedDestinationsCases))]\n    public void Request_FindAffinitizedDestinations(\n        HttpContext context,\n        DestinationState[] allDestinations,\n        AffinityStatus expectedStatus,\n        DestinationState expectedDestination,\n        byte[] expectedEncryptedKey,\n        bool unprotectCalled,\n        LogLevel? expectedLogLevel,\n        EventId expectedEventId)\n    {\n        var dataProtector = GetDataProtector();\n        var logger = AffinityTestHelper.GetLogger<BaseEncryptedSessionAffinityPolicy<string>>();\n        var provider = new ProviderStub(dataProtector.Object, logger.Object);\n        var cluster = new ClusterState(\"cluster\");\n        var affinityResult = provider.FindAffinitizedDestinations(context, cluster, _defaultOptions, allDestinations);\n\n        if (unprotectCalled)\n        {\n            dataProtector.Verify(p => p.Unprotect(It.Is<byte[]>(b => b.SequenceEqual(expectedEncryptedKey))), Times.Once);\n        }\n\n        Assert.Equal(expectedStatus, affinityResult.Status);\n        Assert.Same(expectedDestination, affinityResult.Destinations?.FirstOrDefault());\n\n        if (expectedLogLevel is not null)\n        {\n            logger.Verify(\n                l => l.Log(expectedLogLevel.Value, expectedEventId, It.IsAny<It.IsAnyType>(), It.IsAny<Exception>(), (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),\n                Times.Once);\n        }\n\n        if (expectedDestination is not null)\n        {\n            Assert.Single(affinityResult.Destinations);\n        }\n        else\n        {\n            Assert.Null(affinityResult.Destinations);\n        }\n    }\n\n    [Fact]\n    public void FindAffinitizedDestination_AffinityDisabledOnCluster_ReturnsAffinityDisabled()\n    {\n        var provider = new ProviderStub(GetDataProtector().Object, AffinityTestHelper.GetLogger<BaseEncryptedSessionAffinityPolicy<string>>().Object);\n        var options = new SessionAffinityConfig\n        {\n            Enabled = false,\n            Policy = _defaultOptions.Policy,\n            FailurePolicy = _defaultOptions.FailurePolicy,\n            AffinityKeyName = _defaultOptions.AffinityKeyName\n        };\n        var cluster = new ClusterState(\"cluster\");\n        Assert.Throws<InvalidOperationException>(() => provider.FindAffinitizedDestinations(new DefaultHttpContext(), cluster, options, new[] { new DestinationState(\"1\") }));\n    }\n\n    [Fact]\n    public void AffinitizeRequest_AffinityDisabled_DoNothing()\n    {\n        var dataProtector = GetDataProtector();\n        var provider = new ProviderStub(dataProtector.Object, AffinityTestHelper.GetLogger<BaseEncryptedSessionAffinityPolicy<string>>().Object);\n        Assert.Throws<InvalidOperationException>(() => provider.AffinitizeResponse(new DefaultHttpContext(), new ClusterState(\"cluster\"), new SessionAffinityConfig(), new DestinationState(\"id\")));\n    }\n\n    [Fact]\n    public void AffinitizeRequest_RequestIsAffinitized_DoNothing()\n    {\n        var dataProtector = GetDataProtector();\n        var provider = new ProviderStub(dataProtector.Object, AffinityTestHelper.GetLogger<BaseEncryptedSessionAffinityPolicy<string>>().Object);\n        var context = new DefaultHttpContext();\n        provider.DirectlySetExtractedKeyOnContext(context, \"ExtractedKey\");\n        provider.AffinitizeResponse(context, new ClusterState(\"cluster\"), _defaultOptions, new DestinationState(\"id\"));\n        Assert.Null(provider.LastSetEncryptedKey);\n        dataProtector.Verify(p => p.Protect(It.IsAny<byte[]>()), Times.Never);\n    }\n\n    [Fact]\n    public void AffinitizeRequest_RequestIsNotAffinitized_SetAffinityKey()\n    {\n        var dataProtector = GetDataProtector();\n        var provider = new ProviderStub(dataProtector.Object, AffinityTestHelper.GetLogger<BaseEncryptedSessionAffinityPolicy<string>>().Object);\n        var destination = new DestinationState(\"dest-A\");\n        provider.AffinitizeResponse(new DefaultHttpContext(), new ClusterState(\"cluster\"), _defaultOptions, destination);\n        Assert.Equal(\"ZGVzdC1B\", provider.LastSetEncryptedKey);\n        var keyBytes = Encoding.UTF8.GetBytes(destination.DestinationId);\n        dataProtector.Verify(p => p.Protect(It.Is<byte[]>(b => b.SequenceEqual(keyBytes))), Times.Once);\n    }\n\n    [Fact]\n    public void Ctor_MandatoryArgumentIsNull_Throw()\n    {\n        Assert.Throws<ArgumentNullException>(() => new ProviderStub(null, new Mock<ILogger>().Object));\n        // CreateDataProtector will return null\n        Assert.Throws<ArgumentNullException>(() => new ProviderStub(new Mock<IDataProtector>().Object, new Mock<ILogger>().Object));\n        Assert.Throws<ArgumentNullException>(() => new ProviderStub(GetDataProtector().Object, null));\n    }\n\n    public static IEnumerable<object[]> FindAffinitizedDestinationsCases()\n    {\n        var destinations = new[] { new DestinationState(\"dest-A\"), new DestinationState(\"dest-B\"), new DestinationState(\"dest-C\") };\n        yield return new object[] { GetHttpContext(new[] { (\"SomeKey\", \"SomeValue\") }), destinations, AffinityStatus.AffinityKeyNotSet, null, null, false, null, null };\n        yield return new object[] { GetHttpContext(new[] { (KeyName, \"dest-B\") }), destinations, AffinityStatus.OK, destinations[1], Encoding.UTF8.GetBytes(\"dest-B\"), true, null, null };\n        yield return new object[] { GetHttpContext(new[] { (KeyName, \"dest-Z\") }), destinations, AffinityStatus.DestinationNotFound, null, Encoding.UTF8.GetBytes(\"dest-Z\"), true, LogLevel.Warning, EventIds.DestinationMatchingToAffinityKeyNotFound };\n        yield return new object[] { GetHttpContext(new[] { (KeyName, \"dest-B\") }), Array.Empty<DestinationState>(), AffinityStatus.DestinationNotFound, null, Encoding.UTF8.GetBytes(\"dest-B\"), true, LogLevel.Warning, EventIds.AffinityCannotBeEstablishedBecauseNoDestinationsFoundOnCluster };\n        yield return new object[] { GetHttpContext(new[] { (KeyName, \"/////\") }, false), destinations, AffinityStatus.AffinityKeyExtractionFailed, null, Encoding.UTF8.GetBytes(InvalidKeyNull), false, LogLevel.Error, EventIds.RequestAffinityKeyDecryptionFailed };\n        yield return new object[] { GetHttpContext(new[] { (KeyName, InvalidKeyNull) }), destinations, AffinityStatus.AffinityKeyExtractionFailed, null, Encoding.UTF8.GetBytes(InvalidKeyNull), true, LogLevel.Error, EventIds.RequestAffinityKeyDecryptionFailed };\n        yield return new object[] { GetHttpContext(new[] { (KeyName, InvalidKeyThrow) }), destinations, AffinityStatus.AffinityKeyExtractionFailed, null, Encoding.UTF8.GetBytes(InvalidKeyThrow), true, LogLevel.Error, EventIds.RequestAffinityKeyDecryptionFailed };\n    }\n\n    private static HttpContext GetHttpContext((string Key, string Value)[] items, bool encodeToBase64 = true)\n    {\n        var context = new DefaultHttpContext\n        {\n            Items = items.ToDictionary(i => (object)i.Key, i => encodeToBase64 ? (object)Convert.ToBase64String(Encoding.UTF8.GetBytes(i.Value)) : i.Value)\n        };\n        return context;\n    }\n\n    private Mock<IDataProtector> GetDataProtector()\n    {\n        var result = new Mock<IDataProtector>();\n        var nullBytes = Encoding.UTF8.GetBytes(InvalidKeyNull);\n        var throwBytes = Encoding.UTF8.GetBytes(InvalidKeyThrow);\n        result.Setup(p => p.Protect(It.IsAny<byte[]>())).Returns((byte[] k) => k);\n        result.Setup(p => p.Unprotect(It.IsAny<byte[]>())).Returns((byte[] k) => k);\n        result.Setup(p => p.Unprotect(It.Is<byte[]>(b => b.SequenceEqual(nullBytes)))).Returns((byte[])null);\n        result.Setup(p => p.Unprotect(It.Is<byte[]>(b => b.SequenceEqual(throwBytes)))).Throws<InvalidOperationException>();\n        result.Setup(p => p.CreateProtector(It.IsAny<string>())).Returns(result.Object);\n        return result;\n    }\n\n    private class ProviderStub : BaseEncryptedSessionAffinityPolicy<string>\n    {\n        public static readonly string KeyNameSetting = \"AffinityKeyName\";\n\n        public ProviderStub(IDataProtectionProvider dataProtectionProvider, ILogger logger)\n            : base(dataProtectionProvider, logger)\n        { }\n\n        public override string Name => \"Stub\";\n\n        public string LastSetEncryptedKey { get; private set; }\n\n        public void DirectlySetExtractedKeyOnContext(HttpContext context, string key)\n        {\n            context.Items[AffinityKeyId] = key;\n        }\n\n        protected override string GetDestinationAffinityKey(DestinationState destination)\n        {\n            return destination.DestinationId;\n        }\n\n        protected override (string Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig options)\n        {\n            Assert.Equal(Name, options.Policy);\n            // HttpContext.Items is used here to store the request affinity key for simplicity.\n            // In real world scenario, a provider will extract it from request (e.g. header, cookie, etc.)\n            var encryptedKey = context.Items.TryGetValue(options.AffinityKeyName, out var requestKey) ? requestKey : null;\n            return Unprotect((string)encryptedKey);\n        }\n\n        protected override void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig options, string unencryptedKey)\n        {\n            var encryptedKey = Protect(unencryptedKey);\n            context.Items[options.AffinityKeyName] = encryptedKey;\n            LastSetEncryptedKey = encryptedKey;\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/SessionAffinity/CookieSessionAffinityPolicyTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\nusing Yarp.Tests.Common;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.SessionAffinity.Tests;\n\npublic class CookieSessionAffinityPolicyTests\n{\n    private readonly SessionAffinityConfig _config = new SessionAffinityConfig\n    {\n        Enabled = true,\n        Policy = \"Cookie\",\n        FailurePolicy = \"Return503Error\",\n        AffinityKeyName = \"My.Affinity\",\n        Cookie = new SessionAffinityCookieConfig\n        {\n            Domain = \"mydomain.my\",\n            HttpOnly = false,\n            IsEssential = true,\n            MaxAge = TimeSpan.FromHours(1),\n            Path = \"/some\",\n            SameSite = SameSiteMode.Lax,\n            SecurePolicy = CookieSecurePolicy.Always,\n        }\n    };\n    private readonly IReadOnlyList<DestinationState> _destinations = new[] { new DestinationState(\"dest-A\"), new DestinationState(\"dest-B\"), new DestinationState(\"dest-C\") };\n\n    [Fact]\n    public void FindAffinitizedDestination_AffinityKeyIsNotSetOnRequest_ReturnKeyNotSet()\n    {\n        var policy = new CookieSessionAffinityPolicy(\n            AffinityTestHelper.GetDataProtector().Object,\n            new TestTimeProvider(),\n            AffinityTestHelper.GetLogger<CookieSessionAffinityPolicy>().Object);\n\n        Assert.Equal(SessionAffinityConstants.Policies.Cookie, policy.Name);\n\n        var context = new DefaultHttpContext();\n        context.Request.Headers[\"Cookie\"] = new[] { $\"Some-Cookie=ZZZ\" };\n        var cluster = new ClusterState(\"cluster\");\n\n        var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations);\n\n        Assert.Equal(AffinityStatus.AffinityKeyNotSet, affinityResult.Status);\n        Assert.Null(affinityResult.Destinations);\n    }\n\n    [Fact]\n    public void FindAffinitizedDestination_AffinityKeyIsSetOnRequest_Success()\n    {\n        var policy = new CookieSessionAffinityPolicy(\n            AffinityTestHelper.GetDataProtector().Object,\n            new TestTimeProvider(),\n            AffinityTestHelper.GetLogger<CookieSessionAffinityPolicy>().Object);\n        var context = new DefaultHttpContext();\n        var affinitizedDestination = _destinations[1];\n        context.Request.Headers[\"Cookie\"] = GetCookieWithAffinity(affinitizedDestination);\n        var cluster = new ClusterState(\"cluster\");\n\n        var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations);\n\n        Assert.Equal(AffinityStatus.OK, affinityResult.Status);\n        Assert.Single(affinityResult.Destinations);\n        Assert.Same(affinitizedDestination, affinityResult.Destinations[0]);\n    }\n\n    [Fact]\n    public void AffinitizedRequest_CustomConfigAffinityKeyIsNotExtracted_SetKeyOnResponse()\n    {\n        var policy = new CookieSessionAffinityPolicy(\n            AffinityTestHelper.GetDataProtector().Object,\n            new TestTimeProvider(),\n            AffinityTestHelper.GetLogger<CookieSessionAffinityPolicy>().Object);\n        var context = new DefaultHttpContext();\n\n        policy.AffinitizeResponse(context, new ClusterState(\"cluster\"), _config, _destinations[1]);\n\n        var affinityCookieHeader = context.Response.Headers[\"Set-Cookie\"];\n        Assert.Equal(\"My.Affinity=ZGVzdC1C; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax\", affinityCookieHeader);\n    }\n\n    [Fact]\n    public void AffinitizeRequest_CookieConfigSpecified_UseIt()\n    {\n        var policy = new CookieSessionAffinityPolicy(\n            AffinityTestHelper.GetDataProtector().Object,\n            new TestTimeProvider(),\n            AffinityTestHelper.GetLogger<CookieSessionAffinityPolicy>().Object);\n        var context = new DefaultHttpContext();\n\n        policy.AffinitizeResponse(context, new ClusterState(\"cluster\"), _config, _destinations[1]);\n\n        var affinityCookieHeader = context.Response.Headers[\"Set-Cookie\"];\n        Assert.Equal(\"My.Affinity=ZGVzdC1C; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax\", affinityCookieHeader);\n    }\n\n    [Fact]\n    public void AffinitizedRequest_AffinityKeyIsExtracted_DoNothing()\n    {\n        var policy = new CookieSessionAffinityPolicy(\n            AffinityTestHelper.GetDataProtector().Object,\n            new TestTimeProvider(),\n            AffinityTestHelper.GetLogger<CookieSessionAffinityPolicy>().Object);\n        var context = new DefaultHttpContext();\n        var affinitizedDestination = _destinations[0];\n        context.Request.Headers[\"Cookie\"] = GetCookieWithAffinity(affinitizedDestination);\n        var cluster = new ClusterState(\"cluster\");\n\n        var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations);\n\n        Assert.Equal(AffinityStatus.OK, affinityResult.Status);\n\n        policy.AffinitizeResponse(context, cluster, _config, affinitizedDestination);\n\n        Assert.False(context.Response.Headers.ContainsKey(\"Cookie\"));\n    }\n\n    private string[] GetCookieWithAffinity(DestinationState affinitizedDestination)\n    {\n        return new[] { $\"Some-Cookie=ZZZ\", $\"{_config.AffinityKeyName}={affinitizedDestination.DestinationId.ToUTF8BytesInBase64()}\" };\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/SessionAffinity/CustomHeaderSessionAffinityPolicyTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\n\nnamespace Yarp.ReverseProxy.SessionAffinity.Tests;\n\npublic class CustomHeaderSessionAffinityPolicyTests\n{\n    private const string AffinityHeaderName = \"X-MyAffinity\";\n    private readonly SessionAffinityConfig _defaultOptions = new SessionAffinityConfig\n    {\n        Enabled = true,\n        Policy = \"Cookie\",\n        FailurePolicy = \"Return503Error\",\n        AffinityKeyName = AffinityHeaderName\n    };\n    private readonly IReadOnlyList<DestinationState> _destinations = new[] { new DestinationState(\"dest-A\"), new DestinationState(\"dest-B\"), new DestinationState(\"dest-C\") };\n\n    [Fact]\n    public void FindAffinitizedDestination_AffinityKeyIsNotSetOnRequest_ReturnKeyNotSet()\n    {\n        var policy = new CustomHeaderSessionAffinityPolicy(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger<CustomHeaderSessionAffinityPolicy>().Object);\n\n        Assert.Equal(SessionAffinityConstants.Policies.CustomHeader, policy.Name);\n\n        var context = new DefaultHttpContext();\n        context.Request.Headers[\"SomeHeader\"] = new[] { \"SomeValue\" };\n        var cluster = new ClusterState(\"cluster\");\n\n        var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _defaultOptions, _destinations);\n\n        Assert.Equal(AffinityStatus.AffinityKeyNotSet, affinityResult.Status);\n        Assert.Null(affinityResult.Destinations);\n    }\n\n    [Fact]\n    public void FindAffinitizedDestination_AffinityKeyIsSetOnRequest_Success()\n    {\n        var policy = new CustomHeaderSessionAffinityPolicy(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger<CustomHeaderSessionAffinityPolicy>().Object);\n        var context = new DefaultHttpContext();\n        context.Request.Headers[\"SomeHeader\"] = new[] { \"SomeValue\" };\n        var affinitizedDestination = _destinations[1];\n        context.Request.Headers[AffinityHeaderName] = new[] { affinitizedDestination.DestinationId.ToUTF8BytesInBase64() };\n        var cluster = new ClusterState(\"cluster\");\n\n        var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _defaultOptions, _destinations);\n\n        Assert.Equal(AffinityStatus.OK, affinityResult.Status);\n        Assert.Single(affinityResult.Destinations);\n        Assert.Same(affinitizedDestination, affinityResult.Destinations[0]);\n    }\n\n    [Fact]\n    public void AffinitizedRequest_AffinityKeyIsNotExtracted_SetKeyOnResponse()\n    {\n        var policy = new CustomHeaderSessionAffinityPolicy(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger<CustomHeaderSessionAffinityPolicy>().Object);\n        var context = new DefaultHttpContext();\n        var chosenDestination = _destinations[1];\n        var expectedAffinityHeaderValue = chosenDestination.DestinationId.ToUTF8BytesInBase64();\n\n        policy.AffinitizeResponse(context, new ClusterState(\"cluster\"), _defaultOptions, chosenDestination);\n\n        Assert.True(context.Response.Headers.ContainsKey(AffinityHeaderName));\n        Assert.Equal(expectedAffinityHeaderValue, context.Response.Headers[AffinityHeaderName]);\n    }\n\n    [Fact]\n    public void AffinitizedRequest_AffinityKeyIsExtracted_DoNothing()\n    {\n        var policy = new CustomHeaderSessionAffinityPolicy(AffinityTestHelper.GetDataProtector().Object, AffinityTestHelper.GetLogger<CustomHeaderSessionAffinityPolicy>().Object);\n        var context = new DefaultHttpContext();\n        context.Request.Headers[\"SomeHeader\"] = new[] { \"SomeValue\" };\n        var affinitizedDestination = _destinations[1];\n        context.Request.Headers[AffinityHeaderName] = new[] { affinitizedDestination.DestinationId.ToUTF8BytesInBase64() };\n        var cluster = new ClusterState(\"cluster\");\n\n        var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _defaultOptions, _destinations);\n\n        Assert.Equal(AffinityStatus.OK, affinityResult.Status);\n\n        policy.AffinitizeResponse(context, cluster, _defaultOptions, affinitizedDestination);\n\n        Assert.False(context.Response.Headers.ContainsKey(AffinityHeaderName));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/SessionAffinity/HashCookieSessionAffinityPolicyTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO.Hashing;\nusing System.Text;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.SessionAffinity.Tests;\n\npublic class HashCookieSessionAffinityPolicyTests\n{\n    private readonly SessionAffinityConfig _config = new()\n    {\n        Enabled = true,\n        Policy = \"HashCookie\",\n        FailurePolicy = \"Return503Error\",\n        AffinityKeyName = \"My.Affinity\",\n        Cookie = new SessionAffinityCookieConfig\n        {\n            Domain = \"mydomain.my\",\n            HttpOnly = false,\n            IsEssential = true,\n            MaxAge = TimeSpan.FromHours(1),\n            Path = \"/some\",\n            SameSite = SameSiteMode.Lax,\n            SecurePolicy = CookieSecurePolicy.Always,\n        }\n    };\n    private readonly IReadOnlyList<DestinationState> _destinations = new[] { new DestinationState(\"dest-A\"), new DestinationState(\"dest-B\"), new DestinationState(\"dest-C\") };\n\n    [Fact]\n    public void FindAffinitizedDestination_AffinityKeyIsNotSetOnRequest_ReturnKeyNotSet()\n    {\n        var policy = new HashCookieSessionAffinityPolicy(\n            new TestTimeProvider(),\n            NullLogger<HashCookieSessionAffinityPolicy>.Instance);\n\n        Assert.Equal(SessionAffinityConstants.Policies.HashCookie, policy.Name);\n\n        var context = new DefaultHttpContext();\n        context.Request.Headers[\"Cookie\"] = new[] { $\"Some-Cookie=ZZZ\" };\n        var cluster = new ClusterState(\"cluster\");\n\n        var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations);\n\n        Assert.Equal(AffinityStatus.AffinityKeyNotSet, affinityResult.Status);\n        Assert.Null(affinityResult.Destinations);\n    }\n\n    [Fact]\n    public void FindAffinitizedDestination_AffinityKeyIsSetOnRequest_Success()\n    {\n        var policy = new HashCookieSessionAffinityPolicy(\n            new TestTimeProvider(),\n            NullLogger<HashCookieSessionAffinityPolicy>.Instance);\n        var context = new DefaultHttpContext();\n        var affinitizedDestination = _destinations[1];\n        context.Request.Headers[\"Cookie\"] = GetCookieWithAffinity(affinitizedDestination);\n        var cluster = new ClusterState(\"cluster\");\n\n        var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations);\n\n        Assert.Equal(AffinityStatus.OK, affinityResult.Status);\n        Assert.Single(affinityResult.Destinations);\n        Assert.Same(affinitizedDestination, affinityResult.Destinations[0]);\n    }\n\n    [Fact]\n    public void AffinitizedRequest_CustomConfigAffinityKeyIsNotExtracted_SetKeyOnResponse()\n    {\n        var policy = new HashCookieSessionAffinityPolicy(\n            new TestTimeProvider(),\n            NullLogger<HashCookieSessionAffinityPolicy>.Instance);\n        var context = new DefaultHttpContext();\n\n        policy.AffinitizeResponse(context, new ClusterState(\"cluster\"), _config, _destinations[1]);\n\n        var affinityCookieHeader = context.Response.Headers[\"Set-Cookie\"];\n        Assert.Equal(\"My.Affinity=53c079ed4c377b0d; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax\",\n            affinityCookieHeader);\n    }\n\n    [Fact]\n    public void AffinitizeRequest_CookieConfigSpecified_UseIt()\n    {\n        var policy = new HashCookieSessionAffinityPolicy(\n            new TestTimeProvider(),\n            NullLogger<HashCookieSessionAffinityPolicy>.Instance);\n        var context = new DefaultHttpContext();\n\n        policy.AffinitizeResponse(context, new ClusterState(\"cluster\"), _config, _destinations[1]);\n\n        var affinityCookieHeader = context.Response.Headers[\"Set-Cookie\"];\n        Assert.Equal(\"My.Affinity=53c079ed4c377b0d; max-age=3600; domain=mydomain.my; path=/some; secure; samesite=lax\",\n            affinityCookieHeader);\n    }\n\n    [Fact]\n    public void AffinitizedRequest_AffinityKeyIsExtracted_DoNothing()\n    {\n        var policy = new HashCookieSessionAffinityPolicy(\n            new TestTimeProvider(),\n            NullLogger<HashCookieSessionAffinityPolicy>.Instance);\n        var context = new DefaultHttpContext();\n        var affinitizedDestination = _destinations[0];\n        context.Request.Headers[\"Cookie\"] = GetCookieWithAffinity(affinitizedDestination);\n        var cluster = new ClusterState(\"cluster\");\n\n        var affinityResult = policy.FindAffinitizedDestinations(context, cluster, _config, _destinations);\n\n        Assert.Equal(AffinityStatus.OK, affinityResult.Status);\n\n        policy.AffinitizeResponse(context, cluster, _config, affinitizedDestination);\n\n        Assert.False(context.Response.Headers.ContainsKey(\"Cookie\"));\n    }\n\n    private string[] GetCookieWithAffinity(DestinationState affinitizedDestination)\n    {\n        var destinationIdBytes = Encoding.Unicode.GetBytes(affinitizedDestination.DestinationId.ToUpperInvariant());\n        var hashBytes = XxHash64.Hash(destinationIdBytes);\n        var value = Convert.ToHexString(hashBytes).ToLowerInvariant();\n        return new[] { $\"Some-Cookie=ZZZ\", $\"{_config.AffinityKeyName}={value}\" };\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/SessionAffinity/RedistributeAffinityFailurePolicyTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.SessionAffinity.Tests;\n\npublic class RedistributeAffinityFailurePolicyTests\n{\n    [Theory]\n    [InlineData(AffinityStatus.AffinityKeyExtractionFailed)]\n    [InlineData(AffinityStatus.DestinationNotFound)]\n    public async Task Handle_FailedAffinityStatus_ReturnTrue(AffinityStatus status)\n    {\n        var policy = new RedistributeAffinityFailurePolicy();\n\n        Assert.Equal(SessionAffinityConstants.FailurePolicies.Redistribute, policy.Name);\n        Assert.True(await policy.Handle(new DefaultHttpContext(), cluster: null, affinityStatus: status));\n    }\n\n    [Theory]\n    [InlineData(AffinityStatus.OK)]\n    [InlineData(AffinityStatus.AffinityKeyNotSet)]\n    public async Task Handle_SuccessfulAffinityStatus_Throw(AffinityStatus status)\n    {\n        var policy = new RedistributeAffinityFailurePolicy();\n        var context = new DefaultHttpContext();\n\n        await Assert.ThrowsAsync<InvalidOperationException>(() => policy.Handle(context, cluster: null, affinityStatus: status));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/SessionAffinity/Return503ErrorAffinityFailurePolicyTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.SessionAffinity.Tests;\n\npublic class Return503ErrorAffinityFailurePolicyTests\n{\n    [Theory]\n    [InlineData(AffinityStatus.DestinationNotFound)]\n    [InlineData(AffinityStatus.AffinityKeyExtractionFailed)]\n    public async Task Handle_FaultyAffinityStatus_RespondWith503(AffinityStatus status)\n    {\n        var policy = new Return503ErrorAffinityFailurePolicy();\n        var context = new DefaultHttpContext();\n\n        Assert.Equal(SessionAffinityConstants.FailurePolicies.Return503Error, policy.Name);\n\n        Assert.False(await policy.Handle(context, cluster: null, affinityStatus: status));\n        Assert.Equal(503, context.Response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(AffinityStatus.OK)]\n    [InlineData(AffinityStatus.AffinityKeyNotSet)]\n    public async Task Handle_SuccessfulAffinityStatus_Throw(AffinityStatus status)\n    {\n        var policy = new Return503ErrorAffinityFailurePolicy();\n        var context = new DefaultHttpContext();\n\n        await Assert.ThrowsAsync<InvalidOperationException>(() => policy.Handle(context, cluster: null, affinityStatus: status));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/SessionAffinity/SessionAffinityMiddlewareTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\nusing Moq;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Forwarder;\nusing System.Threading;\n\nnamespace Yarp.ReverseProxy.SessionAffinity.Tests;\n\npublic class SessionAffinityMiddlewareTests\n{\n    protected const string AffinitizedDestinationName = \"dest-B\";\n    protected readonly ClusterModel ClusterConfig = new ClusterModel(new ClusterConfig\n    {\n        ClusterId = \"cluster-1\",\n        SessionAffinity = new SessionAffinityConfig\n        {\n            Enabled = true,\n            Policy = \"Policy-B\",\n            FailurePolicy = \"Policy-1\",\n            AffinityKeyName = \"Key1\"\n        }\n    },\n        new HttpMessageInvoker(new Mock<HttpMessageHandler>().Object));\n\n    [Theory]\n    [InlineData(AffinityStatus.AffinityKeyNotSet, null)]\n    [InlineData(AffinityStatus.OK, AffinitizedDestinationName)]\n    public async Task Invoke_SuccessfulFlow_CallNext(AffinityStatus status, string foundDestinationId)\n    {\n        var cluster = GetCluster();\n        var endpoint = GetEndpoint(cluster);\n        DestinationState foundDestination = null;\n        if (foundDestinationId is not null)\n        {\n            cluster.Destinations.TryGetValue(foundDestinationId, out foundDestination);\n        }\n        var invokedMode = string.Empty;\n        const string expectedMode = \"Policy-B\";\n        var policies = RegisterAffinityPolicies(\n            true,\n            cluster.Destinations.Values.ToList(),\n            (\"Policy-A\", AffinityStatus.DestinationNotFound, (DestinationState)null, (Action<ISessionAffinityPolicy>)(p => throw new InvalidOperationException($\"Policy {p.Name} call is not expected.\"))),\n            (expectedMode, status, foundDestination, p => invokedMode = p.Name));\n        var nextInvoked = false;\n        var middleware = new SessionAffinityMiddleware(c =>\n            {\n                nextInvoked = true;\n                return Task.CompletedTask;\n            },\n            policies.Select(p => p.Object), Array.Empty<IAffinityFailurePolicy>(),\n            new Mock<ILogger<SessionAffinityMiddleware>>().Object);\n        var context = new DefaultHttpContext();\n        context.SetEndpoint(endpoint);\n        var destinationFeature = GetReverseProxyFeature(cluster);\n        context.Features.Set(destinationFeature);\n\n        await middleware.Invoke(context);\n\n        Assert.Equal(expectedMode, invokedMode);\n        Assert.True(nextInvoked);\n        policies[0].VerifyGet(p => p.Name, Times.Once);\n        policies[0].VerifyNoOtherCalls();\n        policies[1].VerifyAll();\n\n        if (foundDestinationId is not null)\n        {\n            Assert.Single(destinationFeature.AvailableDestinations);\n            Assert.Equal(foundDestinationId, destinationFeature.AvailableDestinations[0].DestinationId);\n        }\n        else\n        {\n            Assert.True(cluster.Destinations.Values.SequenceEqual(destinationFeature.AvailableDestinations));\n        }\n    }\n\n    [Theory]\n    [InlineData(AffinityStatus.DestinationNotFound, true)]\n    [InlineData(AffinityStatus.DestinationNotFound, false)]\n    [InlineData(AffinityStatus.AffinityKeyExtractionFailed, true)]\n    [InlineData(AffinityStatus.AffinityKeyExtractionFailed, false)]\n    public async Task Invoke_ErrorFlow_CallFailurePolicy(AffinityStatus affinityStatus, bool keepProcessing)\n    {\n        var cluster = GetCluster();\n        var endpoint = GetEndpoint(cluster);\n        var policies = RegisterAffinityPolicies(true, cluster.Destinations.Values.ToList(), (\"Policy-B\", affinityStatus, null, _ => { }));\n        var invokedPolicy = string.Empty;\n        const string expectedPolicy = \"Policy-1\";\n        var failurePolicies = RegisterFailurePolicies(\n            affinityStatus,\n            (\"Policy-0\", false, p => throw new InvalidOperationException($\"Policy {p.Name} call is not expected.\")),\n            (expectedPolicy, keepProcessing, p => invokedPolicy = p.Name));\n        var nextInvoked = false;\n        var logger = AffinityTestHelper.GetLogger<SessionAffinityMiddleware>();\n        var middleware = new SessionAffinityMiddleware(c =>\n            {\n                nextInvoked = true;\n                return Task.CompletedTask;\n            },\n            policies.Select(p => p.Object), failurePolicies.Select(p => p.Object),\n            logger.Object);\n        var context = new DefaultHttpContext();\n        var destinationFeature = GetReverseProxyFeature(cluster);\n\n        context.SetEndpoint(endpoint);\n        context.Features.Set(destinationFeature);\n\n        await middleware.Invoke(context);\n\n        Assert.Equal(expectedPolicy, invokedPolicy);\n        Assert.Equal(keepProcessing, nextInvoked);\n        failurePolicies[0].VerifyGet(p => p.Name, Times.Once);\n        failurePolicies[0].VerifyNoOtherCalls();\n        failurePolicies[1].VerifyAll();\n        if (!keepProcessing)\n        {\n            logger.Verify(\n                l => l.Log(LogLevel.Warning, EventIds.AffinityResolutionFailedForCluster, It.IsAny<It.IsAnyType>(), null, (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),\n                Times.Once);\n        }\n    }\n\n    internal ClusterState GetCluster()\n    {\n        var cluster = new ClusterState(\"cluster-1\");\n        var destinationManager = cluster.Destinations;\n        destinationManager.GetOrAdd(\"dest-A\", id => new DestinationState(id));\n        destinationManager.GetOrAdd(AffinitizedDestinationName, id => new DestinationState(id));\n        destinationManager.GetOrAdd(\"dest-C\", id => new DestinationState(id));\n        cluster.Model = ClusterConfig;\n        cluster.DestinationsState = new ClusterDestinationsState(destinationManager.Values.ToList(), destinationManager.Values.ToList());\n        return cluster;\n    }\n\n    internal IReadOnlyList<Mock<ISessionAffinityPolicy>> RegisterAffinityPolicies(\n        bool lookupMiddlewareTest,\n        IReadOnlyList<DestinationState> expectedDestinations,\n        params (string Mode, AffinityStatus? Status, DestinationState Destinations, Action<ISessionAffinityPolicy> Callback)[] prototypes)\n    {\n        var result = new List<Mock<ISessionAffinityPolicy>>();\n        foreach (var (mode, status, destinations, callback) in prototypes)\n        {\n            var policy = new Mock<ISessionAffinityPolicy>(MockBehavior.Strict);\n            policy.SetupGet(p => p.Name).Returns(mode);\n            if (lookupMiddlewareTest)\n            {\n                policy.Setup(p => p.FindAffinitizedDestinationsAsync(\n                    It.IsAny<HttpContext>(),\n                    It.IsAny<ClusterState>(),\n                    ClusterConfig.Config.SessionAffinity,\n                    expectedDestinations,\n                    It.IsAny<CancellationToken>()))\n                .Returns(new ValueTask<AffinityResult>(new AffinityResult(destinations, status.Value)))\n                .Callback(() => callback(policy.Object));\n            }\n            else\n            {\n                policy.Setup(p => p.AffinitizeResponseAsync(\n                    It.IsAny<HttpContext>(),\n                    It.IsAny<ClusterState>(),\n                    ClusterConfig.Config.SessionAffinity,\n                    expectedDestinations[0],\n                    It.IsAny<CancellationToken>()))\n                .Returns(new ValueTask())\n                .Callback(() => callback(policy.Object));\n            }\n            result.Add(policy);\n        }\n        return result.AsReadOnly();\n    }\n\n    internal IReadOnlyList<Mock<IAffinityFailurePolicy>> RegisterFailurePolicies(AffinityStatus expectedStatus, params (string Name, bool Handled, Action<IAffinityFailurePolicy> Callback)[] prototypes)\n    {\n        var result = new List<Mock<IAffinityFailurePolicy>>();\n        foreach (var (name, handled, callback) in prototypes)\n        {\n            var policy = new Mock<IAffinityFailurePolicy>(MockBehavior.Strict);\n            policy.SetupGet(p => p.Name).Returns(name);\n            policy.Setup(p => p.Handle(It.IsAny<HttpContext>(), It.IsAny<ClusterState>(), expectedStatus))\n                .ReturnsAsync(handled)\n                .Callback(() => callback(policy.Object));\n            result.Add(policy);\n        }\n        return result.AsReadOnly();\n    }\n\n    internal IReverseProxyFeature GetReverseProxyFeature(ClusterState cluster)\n    {\n        return new ReverseProxyFeature()\n        {\n            AvailableDestinations = cluster.Destinations.Values.ToList(),\n            Route = new RouteModel(new RouteConfig(), cluster: cluster, HttpTransformer.Default),\n            Cluster = cluster.Model,\n        };\n    }\n\n    internal Endpoint GetEndpoint(ClusterState cluster)\n    {\n        var routeConfig = new RouteConfig();\n        var routeModel = new RouteModel(routeConfig, cluster, HttpTransformer.Default);\n        var endpoint = new Endpoint(default, new EndpointMetadataCollection(routeModel), string.Empty);\n        return endpoint;\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/Builder/TransformBuilderTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.AspNetCore.HttpOverrides;\nusing Microsoft.Extensions.DependencyInjection;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Transforms.Builder.Tests;\n\npublic class TransformBuilderTests\n{\n    [Fact]\n    public void CreateBuilder_Success()\n    {\n        CreateTransformBuilder();\n    }\n\n    [Fact]\n    public void NullTransforms_AddsDefaults()\n    {\n        NullOrEmptyTransforms_AddsDefaults(null);\n    }\n\n    [Fact]\n    public void EmptyTransforms_AddsDefaults()\n    {\n        NullOrEmptyTransforms_AddsDefaults(new List<IReadOnlyDictionary<string, string>>());\n    }\n\n    private void NullOrEmptyTransforms_AddsDefaults(IReadOnlyList<IReadOnlyDictionary<string, string>> transforms)\n    {\n        var transformBuilder = CreateTransformBuilder();\n\n        var route = new RouteConfig { Transforms = transforms };\n        var errors = transformBuilder.ValidateRoute(route);\n        Assert.Empty(errors);\n\n        var results = transformBuilder.BuildInternal(route, new ClusterConfig());\n        Assert.NotNull(results);\n        Assert.Null(results.ShouldCopyRequestHeaders);\n        Assert.Null(results.ShouldCopyResponseHeaders);\n        Assert.Null(results.ShouldCopyResponseTrailers);\n        Assert.Empty(results.ResponseTransforms);\n        Assert.Empty(results.ResponseTrailerTransforms);\n\n        Assert.Equal(6, results.RequestTransforms.Length);\n        var hostTransform = Assert.Single(results.RequestTransforms.OfType<RequestHeaderOriginalHostTransform>());\n        Assert.False(hostTransform.UseOriginalHost);\n        var forTransform = Assert.Single(results.RequestTransforms.OfType<RequestHeaderXForwardedForTransform>());\n        Assert.Equal(ForwardedHeadersDefaults.XForwardedForHeaderName, forTransform.HeaderName);\n        var xHostTransform = Assert.Single(results.RequestTransforms.OfType<RequestHeaderXForwardedHostTransform>());\n        Assert.Equal(ForwardedHeadersDefaults.XForwardedHostHeaderName, xHostTransform.HeaderName);\n        var prefixTransform = Assert.Single(results.RequestTransforms.OfType<RequestHeaderXForwardedPrefixTransform>());\n        Assert.Equal(\"X-Forwarded-Prefix\", prefixTransform.HeaderName);\n        var protoTransform = Assert.Single(results.RequestTransforms.OfType<RequestHeaderXForwardedProtoTransform>());\n        Assert.Equal(ForwardedHeadersDefaults.XForwardedProtoHeaderName, protoTransform.HeaderName);\n\n        var removeForwardedTransform = Assert.Single(results.RequestTransforms.OfType<RequestHeaderForwardedTransform>());\n        Assert.Equal(ForwardedTransformActions.Remove, removeForwardedTransform.TransformAction);\n    }\n\n    [Fact]\n    public void CreateTransforms_ExecutesAction()\n    {\n        var transformBuilder = CreateTransformBuilder();\n\n        var executed = false;\n        var results = transformBuilder.CreateInternal(_ =>\n        {\n            executed = true;\n        });\n\n        Assert.True(executed);\n    }\n\n    [Fact]\n    public void CreateTransforms_AddsDefaults()\n    {\n        var transformBuilder = CreateTransformBuilder();\n\n        var results = transformBuilder.CreateInternal(_ => { });\n        Assert.NotNull(results);\n        Assert.Null(results.ShouldCopyRequestHeaders);\n        Assert.Null(results.ShouldCopyResponseHeaders);\n        Assert.Null(results.ShouldCopyResponseTrailers);\n        Assert.Empty(results.ResponseTransforms);\n        Assert.Empty(results.ResponseTrailerTransforms);\n\n        Assert.Equal(6, results.RequestTransforms.Length);\n        var hostTransform = Assert.Single(results.RequestTransforms.OfType<RequestHeaderOriginalHostTransform>());\n        Assert.False(hostTransform.UseOriginalHost);\n        var forTransform = Assert.Single(results.RequestTransforms.OfType<RequestHeaderXForwardedForTransform>());\n        Assert.Equal(ForwardedHeadersDefaults.XForwardedForHeaderName, forTransform.HeaderName);\n        var xHostTransform = Assert.Single(results.RequestTransforms.OfType<RequestHeaderXForwardedHostTransform>());\n        Assert.Equal(ForwardedHeadersDefaults.XForwardedHostHeaderName, xHostTransform.HeaderName);\n        var prefixTransform = Assert.Single(results.RequestTransforms.OfType<RequestHeaderXForwardedPrefixTransform>());\n        Assert.Equal(\"X-Forwarded-Prefix\", prefixTransform.HeaderName);\n        var protoTransform = Assert.Single(results.RequestTransforms.OfType<RequestHeaderXForwardedProtoTransform>());\n        Assert.Equal(ForwardedHeadersDefaults.XForwardedProtoHeaderName, protoTransform.HeaderName);\n\n        var removeForwardedTransform = Assert.Single(results.RequestTransforms.OfType<RequestHeaderForwardedTransform>());\n        Assert.Equal(ForwardedTransformActions.Remove, removeForwardedTransform.TransformAction);\n    }\n\n    [Fact]\n    public void EmptyTransform_Error()\n    {\n        var transformBuilder = CreateTransformBuilder();\n        var transforms = new[]\n        {\n            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase), // Empty\n        };\n\n        var route = new RouteConfig() { Transforms = transforms };\n        var errors = transformBuilder.ValidateRoute(route);\n        var error = Assert.Single(errors);\n        Assert.Equal(\"Unknown transform: \", error.Message);\n\n        var nie = Assert.Throws<ArgumentException>(() => transformBuilder.BuildInternal(route, new ClusterConfig()));\n        Assert.Equal(\"Unknown transform: \", nie.Message);\n    }\n\n    [Fact]\n    public void UnknownTransforms_Error()\n    {\n        var transformBuilder = CreateTransformBuilder();\n        var transforms = new[]\n        {\n            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) // Unknown transform\n            {\n                {  \"string1\", \"value1\" },\n                {  \"string2\", \"value2\" }\n            },\n            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) // Unknown transform\n            {\n                {  \"string3\", \"value3\" },\n                {  \"string4\", \"value4\" }\n            },\n        };\n\n        var route = new RouteConfig() { Transforms = transforms };\n        var errors = transformBuilder.ValidateRoute(route);\n        //All errors reported\n        Assert.Equal(2, errors.Count);\n        Assert.Equal(\"Unknown transform: string1;string2\", errors.First().Message);\n        Assert.Equal(\"Unknown transform: string3;string4\", errors.Skip(1).First().Message);\n        var ex = Assert.Throws<ArgumentException>(() => transformBuilder.BuildInternal(route, new ClusterConfig()));\n        // First error reported\n        Assert.Equal(\"Unknown transform: string1;string2\", ex.Message);\n    }\n\n    [Fact]\n    public void CallsTransformFactories()\n    {\n        var factory1 = new TestTransformFactory(\"1\");\n        var factory2 = new TestTransformFactory(\"2\");\n        var factory3 = new TestTransformFactory(\"3\");\n        var builder = new TransformBuilder(new ServiceCollection().BuildServiceProvider(),\n            new[] { factory1, factory2, factory3 }, Array.Empty<ITransformProvider>());\n\n        var route = new RouteConfig().WithTransform(transform =>\n        {\n            transform[\"2\"] = \"B\";\n        });\n        var errors = builder.ValidateRoute(route);\n        Assert.Empty(errors);\n        Assert.Equal(1, factory1.ValidationCalls);\n        Assert.Equal(1, factory2.ValidationCalls);\n        Assert.Equal(0, factory3.ValidationCalls);\n\n        var transforms = builder.BuildInternal(route, new ClusterConfig());\n        Assert.Equal(1, factory1.BuildCalls);\n        Assert.Equal(1, factory2.BuildCalls);\n        Assert.Equal(0, factory3.BuildCalls);\n\n        Assert.Single(transforms.ResponseTrailerTransforms);\n    }\n\n    [Fact]\n    public void CallsTransformProviders()\n    {\n        var provider1 = new TestTransformProvider();\n        var provider2 = new TestTransformProvider();\n        var provider3 = new TestTransformProvider();\n        var builder = new TransformBuilder(new ServiceCollection().BuildServiceProvider(),\n            Array.Empty<ITransformFactory>(), new[] { provider1, provider2, provider3 });\n\n        var route = new RouteConfig();\n        var errors = builder.ValidateRoute(route);\n        Assert.Empty(errors);\n        Assert.Equal(1, provider1.ValidateRouteCalls);\n        Assert.Equal(1, provider2.ValidateRouteCalls);\n        Assert.Equal(1, provider3.ValidateRouteCalls);\n\n        var cluster = new ClusterConfig();\n        errors = builder.ValidateCluster(cluster);\n        Assert.Empty(errors);\n        Assert.Equal(1, provider1.ValidateClusterCalls);\n        Assert.Equal(1, provider2.ValidateClusterCalls);\n        Assert.Equal(1, provider3.ValidateClusterCalls);\n\n        var transforms = builder.BuildInternal(route, cluster);\n        Assert.Equal(1, provider1.ApplyCalls);\n        Assert.Equal(1, provider2.ApplyCalls);\n        Assert.Equal(1, provider3.ApplyCalls);\n\n        Assert.Equal(3, transforms.ResponseTrailerTransforms.Length);\n    }\n\n    [Fact]\n    public void DefaultsCanBeDisabled()\n    {\n        var transformBuilder = CreateTransformBuilder();\n        var transforms = new[]\n        {\n            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n            {\n                {  \"RequestHeadersCopy\", \"false\" }\n            },\n            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n            {\n                {  \"X-Forwarded\", \"Off\" }\n            },\n        };\n\n        var route = new RouteConfig() { Transforms = transforms };\n        var errors = transformBuilder.ValidateRoute(route);\n        Assert.Empty(errors);\n\n        var results = transformBuilder.BuildInternal(route, new ClusterConfig());\n        Assert.NotNull(results);\n        Assert.False(results.ShouldCopyRequestHeaders);\n        Assert.Single(results.RequestTransforms);\n        Assert.Empty(results.ResponseTransforms);\n        Assert.Empty(results.ResponseTrailerTransforms);\n    }\n\n    [Theory]\n    [InlineData(null, null, false)]\n    [InlineData(null, true, false)]\n    [InlineData(null, false, false)]\n    [InlineData(true, null, false)]\n    [InlineData(false, null, false)]\n    [InlineData(true, true, false)]\n    [InlineData(true, false, false)]\n    [InlineData(false, true, false)]\n    [InlineData(false, false, false)]\n    [InlineData(null, null, true)]\n    [InlineData(null, true, true)]\n    [InlineData(null, false, true)]\n    [InlineData(true, null, true)]\n    [InlineData(false, null, true)]\n    [InlineData(true, true, true)]\n    [InlineData(true, false, true)]\n    [InlineData(false, true, true)]\n    [InlineData(false, false, true)]\n    public async Task UseOriginalHost(bool? useOriginalHost, bool? copyHeaders, bool hasDestinationHost)\n    {\n        var transformBuilder = CreateTransformBuilder();\n        var transforms = new List<Dictionary<string, string>>();\n        // Disable default forwarders\n        transforms.Add(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n        {\n            {  \"X-Forwarded\", \"Off\" }\n        });\n        if (useOriginalHost.HasValue)\n        {\n            transforms.Add(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n            {\n                {  \"RequestHeaderOriginalHost\", useOriginalHost.ToString() }\n            });\n        }\n        if (copyHeaders.HasValue)\n        {\n            transforms.Add(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n            {\n                {  \"RequestHeadersCopy\", copyHeaders.ToString() }\n            });\n        }\n\n        var route = new RouteConfig() { Transforms = transforms };\n        var errors = transformBuilder.ValidateRoute(route);\n        Assert.Empty(errors);\n\n        var destinationHost = hasDestinationHost ? \"d1-host\" : null;\n        var clusterConfig = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>\n            {\n                [\"d1\"] = new DestinationConfig\n                {\n                    Address = \"https://localhost\",\n                    Host = destinationHost\n                }\n            }\n        };\n        var results = transformBuilder.BuildInternal(route, clusterConfig);\n        Assert.NotNull(results);\n        Assert.Equal(copyHeaders, results.ShouldCopyRequestHeaders);\n        Assert.Empty(results.ResponseTransforms);\n        Assert.Empty(results.ResponseTrailerTransforms);\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Features.Set<IReverseProxyFeature>(new ReverseProxyFeature\n        {\n            ProxiedDestination = new DestinationState(\"d1\") { Model = new(clusterConfig.Destinations.Single().Value) }\n        });\n        httpContext.Request.Host = new HostString(\"StartHost\");\n        var proxyRequest = new HttpRequestMessage();\n        var destinationPrefix = \"http://destinationhost:9090/path\";\n        await results.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, CancellationToken.None);\n\n        // We expect the host to be flowed as long as it is being explicitly flowed or it wasn't suppressed and headers are being copied.\n        if (useOriginalHost.GetValueOrDefault(false))\n        {\n            Assert.Equal(\"StartHost\", proxyRequest.Headers.Host);\n        }\n        else if (destinationHost is not null)\n        {\n            // Otherwise, fall back to the destination config host, which will be null if it's not set.\n            Assert.Equal(destinationHost, proxyRequest.Headers.Host);\n        }\n        else\n        {\n            // Otherwise, the host should be null\n            Assert.Null(proxyRequest.Headers.Host);\n        }\n    }\n\n    [Theory]\n    [InlineData(null, null)]\n    [InlineData(null, true)]\n    [InlineData(null, false)]\n    [InlineData(true, null)]\n    [InlineData(false, null)]\n    [InlineData(true, true)]\n    [InlineData(true, false)]\n    [InlineData(false, true)]\n    [InlineData(false, false)]\n    // https://github.com/dotnet/yarp/issues/859\n    // Verify that a custom host works no matter what combination of\n    // useOriginalHost and copyHeaders is used.\n    public async Task UseCustomHost(bool? useOriginalHost, bool? copyHeaders)\n    {\n        var transformBuilder = CreateTransformBuilder();\n        var transforms = new List<Dictionary<string, string>>();\n        // Disable default forwarders\n        transforms.Add(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n        {\n            {  \"X-Forwarded\", \"Off\" }\n        });\n        transforms.Add(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n        {\n            {  \"RequestHeader\", \"Host\" },\n            {  \"Set\", \"CustomHost\" }\n        });\n        if (useOriginalHost.HasValue)\n        {\n            transforms.Add(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n            {\n                {  \"RequestHeaderOriginalHost\", useOriginalHost.ToString() }\n            });\n        }\n        if (copyHeaders.HasValue)\n        {\n            transforms.Add(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n            {\n                {  \"RequestHeadersCopy\", copyHeaders.ToString() }\n            });\n        }\n\n        var route = new RouteConfig() { Transforms = transforms };\n        var errors = transformBuilder.ValidateRoute(route);\n        Assert.Empty(errors);\n\n        var clusterConfig = new ClusterConfig\n        {\n            ClusterId = \"cluster1\",\n            Destinations = new Dictionary<string, DestinationConfig>\n            {\n                [\"d1\"] = new DestinationConfig\n                {\n                    Address = \"https://localhost\",\n                    Host = \"d1-host\"\n                }\n            }\n        };\n        var results = transformBuilder.BuildInternal(route, clusterConfig);\n        Assert.Equal(copyHeaders, results.ShouldCopyRequestHeaders);\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Features.Set<IReverseProxyFeature>(new ReverseProxyFeature\n        {\n            ProxiedDestination = new DestinationState(\"d1\") { Model = new(clusterConfig.Destinations.Single().Value) }\n        });\n        httpContext.Request.Host = new HostString(\"StartHost\");\n        var proxyRequest = new HttpRequestMessage();\n        var destinationPrefix = \"http://destinationhost:9090/path\";\n\n        await results.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, CancellationToken.None);\n\n        Assert.Equal(\"CustomHost\", proxyRequest.Headers.Host);\n    }\n\n    [Fact]\n    public void DefaultsCanBeOverriddenByForwarded()\n    {\n        var transformBuilder = CreateTransformBuilder();\n        var transforms = new[]\n        {\n            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n            {\n                {  \"RequestHeadersCopy\", \"false\" }\n            },\n            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n            {\n                {  \"Forwarded\", \"proto\" }\n            },\n        };\n\n        var route = new RouteConfig() { Transforms = transforms };\n        var errors = transformBuilder.ValidateRoute(route);\n        Assert.Empty(errors);\n\n        var results = transformBuilder.BuildInternal(route, new ClusterConfig());\n        Assert.Equal(6, results.RequestTransforms.Length);\n        Assert.All(\n            results.RequestTransforms.Skip(1).SkipLast(1).Select(t => (dynamic)t),\n            t =>\n            {\n                Assert.StartsWith(\"X-Forwarded-\", t.HeaderName);\n                Assert.Equal(ForwardedTransformActions.Remove, t.TransformAction);\n            });\n        var transform = results.RequestTransforms[0];\n        var forwardedTransform = Assert.IsType<RequestHeaderForwardedTransform>(transform);\n        Assert.True(forwardedTransform.ProtoEnabled);\n    }\n\n    [Fact]\n    public async Task CallerCallsOverloadsWithoutCT_AllTransformsAreCalled()\n    {\n        var requestTransformsCalled = 0;\n        var responseTransformsCalled = 0;\n        var responseTrailerTransformsCalled = 0;\n\n        var transformer = CreateTransformBuilder().CreateInternal(context =>\n        {\n            context.AddRequestTransform(context =>\n            {\n                requestTransformsCalled++;\n                return default;\n            });\n            context.AddResponseTransform(context =>\n            {\n                responseTransformsCalled++;\n                return default;\n            });\n            context.AddResponseTrailersTransform(context =>\n            {\n                responseTrailerTransformsCalled++;\n                return default;\n            });\n        });\n\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage();\n        var proxyResponse = new HttpResponseMessage();\n        var destinationPrefix = \"http://destinationhost:9090/path\";\n\n        httpContext.Features.Set<IHttpResponseTrailersFeature>(new TestTrailersFeature());\n\n#pragma warning disable CS0618 // We're intentionally testing the obsolete overloads\n        await transformer.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix);\n        await transformer.TransformResponseAsync(httpContext, proxyResponse);\n        await transformer.TransformResponseTrailersAsync(httpContext, proxyResponse);\n#pragma warning restore CS0618\n\n        Assert.Equal(1, requestTransformsCalled);\n        Assert.Equal(1, responseTransformsCalled);\n        Assert.Equal(1, responseTrailerTransformsCalled);\n    }\n\n    internal static TransformBuilder CreateTransformBuilder()\n    {\n        var serviceCollection = new ServiceCollection();\n        serviceCollection.AddLogging();\n        serviceCollection.AddReverseProxy();\n        using var services = serviceCollection.BuildServiceProvider();\n        return (TransformBuilder)services.GetRequiredService<ITransformBuilder>();\n    }\n\n    private class TestTransformFactory : ITransformFactory\n    {\n        private readonly string _v;\n\n        public int ValidationCalls { get; set; }\n        public int BuildCalls { get; set; }\n\n        public TestTransformFactory(string v)\n        {\n            _v = v;\n        }\n\n        public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)\n        {\n            Assert.NotNull(context.Services);\n            Assert.NotNull(context.Route);\n            Assert.NotNull(context.Errors);\n            ValidationCalls++;\n            return transformValues.TryGetValue(_v, out var _);\n        }\n\n        public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)\n        {\n            Assert.NotNull(context.Services);\n            Assert.NotNull(context.Route);\n            BuildCalls++;\n            if (transformValues.TryGetValue(_v, out var _))\n            {\n                context.AddResponseTrailersTransform(context => default);\n                return true;\n            }\n\n            return false;\n        }\n    }\n\n    private class TestTransformProvider : ITransformProvider\n    {\n        public int ValidateRouteCalls { get; set; }\n        public int ValidateClusterCalls { get; set; }\n        public int ApplyCalls { get; set; }\n\n        public void ValidateRoute(TransformRouteValidationContext context)\n        {\n            Assert.NotNull(context.Services);\n            Assert.NotNull(context.Route);\n            Assert.NotNull(context.Errors);\n            ValidateRouteCalls++;\n        }\n\n        public void ValidateCluster(TransformClusterValidationContext context)\n        {\n            Assert.NotNull(context.Services);\n            Assert.NotNull(context.Cluster);\n            Assert.NotNull(context.Errors);\n            ValidateClusterCalls++;\n        }\n\n        public void Apply(TransformBuilderContext context)\n        {\n            Assert.NotNull(context.Services);\n            Assert.NotNull(context.Route);\n            Assert.NotNull(context.Cluster);\n            ApplyCalls++;\n            context.AddResponseTrailer(\"key\", \"value\");\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/DestinationPrefixTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class DestinationPrefixTransformTests\n{\n    [Fact]\n    public async Task UpdateDestinationPrefix()\n    {\n        const string newDestinationPrefix = \"http://localhost:8080\";\n        var context = new RequestTransformContext()\n        {\n            DestinationPrefix = \"http://contoso.com:5000\"\n        };\n        var transform = new DestinationPrefixTransform(newDestinationPrefix);\n        await transform.ApplyAsync(context);\n    }\n\n    private class DestinationPrefixTransform(string newDestinationPrefix) : RequestTransform\n    {\n        public override ValueTask ApplyAsync(RequestTransformContext context)\n        {\n            context.DestinationPrefix = newDestinationPrefix;\n            return ValueTask.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/ForwardedTransformExtensionsTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Microsoft.Extensions.DependencyInjection;\nusing Xunit;\nusing Yarp.Tests.Common;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class ForwardedTransformExtensionsTests : TransformExtensionsTestsBase\n{\n    private readonly ForwardedTransformFactory _factory = new ForwardedTransformFactory(new TestRandomFactory());\n\n    [Theory]\n    [InlineData(ForwardedTransformActions.Set, null, null, null, null)]\n    [InlineData(ForwardedTransformActions.Append, ForwardedTransformActions.Set, null, null, null)]\n    [InlineData(ForwardedTransformActions.Append, null, ForwardedTransformActions.Set, null, null)]\n    [InlineData(ForwardedTransformActions.Append, null, null, ForwardedTransformActions.Set, null)]\n    [InlineData(ForwardedTransformActions.Append, null, null, null, ForwardedTransformActions.Set)]\n    [InlineData(ForwardedTransformActions.Append, ForwardedTransformActions.Off, null, null, null)]\n    [InlineData(ForwardedTransformActions.Append, null, ForwardedTransformActions.Off, null, null)]\n    [InlineData(ForwardedTransformActions.Append, null, null, ForwardedTransformActions.Off, null)]\n    [InlineData(ForwardedTransformActions.Append, null, null, null, ForwardedTransformActions.Off)]\n    [InlineData(ForwardedTransformActions.Set, ForwardedTransformActions.Append, ForwardedTransformActions.Remove, ForwardedTransformActions.Off, ForwardedTransformActions.Remove)]\n    public void WithTransformXForwarded(\n        ForwardedTransformActions xDefault,\n        ForwardedTransformActions? xFor,\n        ForwardedTransformActions? xHost,\n        ForwardedTransformActions? xProto,\n        ForwardedTransformActions? xPrefix)\n    {\n        var routeConfig = new RouteConfig();\n        var prefix = \"prefix-\";\n        routeConfig = routeConfig.WithTransformXForwarded(prefix, xDefault, xFor, xHost, xProto, xPrefix);\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        if (xFor != ForwardedTransformActions.Off)\n        {\n            ValidateXForwardedTransform(\"For\", prefix, xFor ?? xDefault, builderContext.RequestTransforms.OfType<RequestHeaderXForwardedForTransform>().Single());\n        }\n        else\n        {\n            Assert.Empty(builderContext.RequestTransforms.OfType<RequestHeaderXForwardedForTransform>());\n        }\n\n        if (xHost != ForwardedTransformActions.Off)\n        {\n            ValidateXForwardedTransform(\"Host\", prefix, xHost ?? xDefault, builderContext.RequestTransforms.OfType<RequestHeaderXForwardedHostTransform>().Single());\n        }\n        else\n        {\n            Assert.Empty(builderContext.RequestTransforms.OfType<RequestHeaderXForwardedHostTransform>());\n        }\n\n        if (xProto != ForwardedTransformActions.Off)\n        {\n            ValidateXForwardedTransform(\"Proto\", prefix, xProto ?? xDefault, builderContext.RequestTransforms.OfType<RequestHeaderXForwardedProtoTransform>().Single());\n        }\n        else\n        {\n            Assert.Empty(builderContext.RequestTransforms.OfType<RequestHeaderXForwardedProtoTransform>());\n        }\n\n        if (xPrefix != ForwardedTransformActions.Off)\n        {\n            ValidateXForwardedTransform(\"Prefix\", prefix, xPrefix ?? xDefault, builderContext.RequestTransforms.OfType<RequestHeaderXForwardedPrefixTransform>().Single());\n        }\n        else\n        {\n            Assert.Empty(builderContext.RequestTransforms.OfType<RequestHeaderXForwardedPrefixTransform>());\n        }\n    }\n\n    [Theory]\n    [MemberData(nameof(GetAddXForwardedCases))]\n    public void AddXForwarded(Func<TransformBuilderContext, string, ForwardedTransformActions, TransformBuilderContext> addFunc,\n        string transformName, ForwardedTransformActions action)\n    {\n        var builderContext = CreateBuilderContext();\n        addFunc(builderContext, \"prefix-\" + transformName, action);\n\n        ValidateXForwarded(builderContext, transformName, \"prefix-\", action);\n    }\n\n    public static IEnumerable<object[]> GetAddXForwardedCases()\n    {\n        var actions = (ForwardedTransformActions[])Enum.GetValues(typeof(ForwardedTransformActions));\n        var addTransformFuncs = new (Func<TransformBuilderContext, string, ForwardedTransformActions, TransformBuilderContext>, string)[]\n        {\n            (ForwardedTransformExtensions.AddXForwardedFor, \"For\"), (ForwardedTransformExtensions.AddXForwardedPrefix, \"Prefix\"),\n            (ForwardedTransformExtensions.AddXForwardedHost, \"Host\"), (ForwardedTransformExtensions.AddXForwardedProto, \"Proto\")\n        };\n\n        return addTransformFuncs.Join(actions, _ => true, _ => true, (t, a) => new object[] { t.Item1, t.Item2, a });\n    }\n\n    private static void ValidateXForwarded(TransformBuilderContext builderContext, string transformName, string headerPrefix, ForwardedTransformActions action)\n    {\n        Assert.False(builderContext.UseDefaultForwarders);\n\n        if (action == ForwardedTransformActions.Off)\n        {\n            Assert.Empty(builderContext.RequestTransforms);\n        }\n        else\n        {\n            var transform = Assert.Single(builderContext.RequestTransforms);\n            ValidateXForwardedTransform(transformName, headerPrefix, action, transform);\n        }\n    }\n\n    private static void ValidateXForwardedTransform(string transformName, string headerPrefix, ForwardedTransformActions action, RequestTransform transform)\n    {\n        Assert.Equal($\"RequestHeaderXForwarded{transformName}Transform\", transform.GetType().Name);\n        Assert.Equal(headerPrefix + transformName, ((dynamic)transform).HeaderName);\n        Assert.Equal(action, ((dynamic)transform).TransformAction);\n    }\n\n    [Theory]\n    [InlineData(NodeFormat.Random, true, true, NodeFormat.Random, ForwardedTransformActions.Append)]\n    [InlineData(NodeFormat.RandomAndPort, true, true, NodeFormat.Random, ForwardedTransformActions.Set)]\n    [InlineData(NodeFormat.None, false, false, NodeFormat.None, ForwardedTransformActions.Append)]\n    [InlineData(NodeFormat.None, false, false, NodeFormat.None, ForwardedTransformActions.Set)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.Random, ForwardedTransformActions.Append)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.Random, ForwardedTransformActions.Set)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.None, ForwardedTransformActions.Set)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.RandomAndPort, ForwardedTransformActions.Set)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.Unknown, ForwardedTransformActions.Set)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.UnknownAndPort, ForwardedTransformActions.Set)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.Ip, ForwardedTransformActions.Set)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.IpAndPort, ForwardedTransformActions.Set)]\n    public void WithTransformForwarded(NodeFormat forFormat, bool useHost, bool useProto, NodeFormat byFormat, ForwardedTransformActions action)\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformForwarded(useHost, useProto, forFormat, byFormat, action);\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory, CreateServices());\n\n        ValidateForwarded(builderContext, useHost, useProto, forFormat, byFormat, action, true);\n    }\n\n    [Theory]\n    [InlineData(NodeFormat.Random, true, true, NodeFormat.Random, ForwardedTransformActions.Append, true)]\n    [InlineData(NodeFormat.Random, true, true, NodeFormat.Random, ForwardedTransformActions.Append, false)]\n    [InlineData(NodeFormat.RandomAndPort, true, true, NodeFormat.Random, ForwardedTransformActions.Set, true)]\n    [InlineData(NodeFormat.RandomAndPort, true, true, NodeFormat.Random, ForwardedTransformActions.Set, false)]\n    [InlineData(NodeFormat.None, false, false, NodeFormat.None, ForwardedTransformActions.Append, true)]\n    [InlineData(NodeFormat.None, false, false, NodeFormat.None, ForwardedTransformActions.Append, false)]\n    [InlineData(NodeFormat.None, false, false, NodeFormat.None, ForwardedTransformActions.Set, true)]\n    [InlineData(NodeFormat.None, false, false, NodeFormat.None, ForwardedTransformActions.Set, false)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.Random, ForwardedTransformActions.Append, true)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.Random, ForwardedTransformActions.Append, false)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.Random, ForwardedTransformActions.Set, true)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.Random, ForwardedTransformActions.Set, false)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.None, ForwardedTransformActions.Set, true)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.None, ForwardedTransformActions.Set, false)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.RandomAndPort, ForwardedTransformActions.Set, true)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.RandomAndPort, ForwardedTransformActions.Set, false)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.Unknown, ForwardedTransformActions.Set, true)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.Unknown, ForwardedTransformActions.Set, false)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.UnknownAndPort, ForwardedTransformActions.Set, true)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.UnknownAndPort, ForwardedTransformActions.Set, false)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.Ip, ForwardedTransformActions.Set, true)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.Ip, ForwardedTransformActions.Set, false)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.IpAndPort, ForwardedTransformActions.Set, true)]\n    [InlineData(NodeFormat.None, false, true, NodeFormat.IpAndPort, ForwardedTransformActions.Set, false)]\n    public void AddForwarded(NodeFormat forFormat, bool useHost, bool useProto, NodeFormat byFormat, ForwardedTransformActions action, bool removeAllXForwardedHeaders)\n    {\n        var builderContext = CreateBuilderContext(services: CreateServices());\n        builderContext.AddForwarded(useHost, useProto, forFormat, byFormat, action, removeAllXForwardedHeaders);\n\n        ValidateForwarded(builderContext, useHost, useProto, forFormat, byFormat, action, removeAllXForwardedHeaders);\n    }\n\n    private static void ValidateForwarded(TransformBuilderContext builderContext, bool useHost, bool useProto,\n        NodeFormat forFormat, NodeFormat byFormat, ForwardedTransformActions action, bool removeAllXForwardedHeaders)\n    {\n        Assert.False(builderContext.UseDefaultForwarders);\n\n        if (byFormat != NodeFormat.None || forFormat != NodeFormat.None || useHost || useProto)\n        {\n            if (removeAllXForwardedHeaders)\n            {\n                Assert.Equal(5, builderContext.RequestTransforms.Count);\n                Assert.All(\n                    builderContext.RequestTransforms.Skip(1).Select(t => (dynamic)t),\n                    t =>\n                    {\n                        Assert.StartsWith(\"X-Forwarded-\", t.HeaderName);\n                        Assert.Equal(ForwardedTransformActions.Remove, t.TransformAction);\n                    });\n            }\n            else\n            {\n                Assert.Equal(1, builderContext.RequestTransforms.Count);\n                var xForwardedTransforms = builderContext.RequestTransforms.Skip(1).Cast<dynamic>().Where(requestTransform =>\n                    requestTransform.HeaderName.ToLowerInvariant().StartsWith(\"x-forwarded\")).ToList();\n                Assert.Empty(xForwardedTransforms);\n            }\n\n            var transform = builderContext.RequestTransforms[0];\n            var requestHeaderForwardedTransform = Assert.IsType<RequestHeaderForwardedTransform>(transform);\n            Assert.Equal(action, requestHeaderForwardedTransform.TransformAction);\n            Assert.Equal(useHost, requestHeaderForwardedTransform.HostEnabled);\n            Assert.Equal(useProto, requestHeaderForwardedTransform.ProtoEnabled);\n            Assert.Equal(byFormat, requestHeaderForwardedTransform.ByFormat);\n            Assert.Equal(forFormat, requestHeaderForwardedTransform.ForFormat);\n        }\n        else\n        {\n            Assert.Empty(builderContext.RequestTransforms);\n        }\n    }\n\n    [Fact]\n    public void WithTransformClientCertHeader()\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformClientCertHeader(\"name\");\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        var transform = Assert.Single(builderContext.RequestTransforms);\n        var certTransform = Assert.IsType<RequestHeaderClientCertTransform>(transform);\n        Assert.Equal(\"name\", certTransform.HeaderName);\n    }\n\n    [Fact]\n    public void AddClientCertHeader()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddClientCertHeader(\"name\");\n\n        var transform = Assert.Single(builderContext.RequestTransforms);\n        var certTransform = Assert.IsType<RequestHeaderClientCertTransform>(transform);\n        Assert.Equal(\"name\", certTransform.HeaderName);\n    }\n\n    private static IServiceProvider CreateServices()\n    {\n        var collection = new ServiceCollection();\n        collection.AddSingleton<IRandomFactory, TestRandomFactory>();\n        return collection.BuildServiceProvider();\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/HttpMethodChangeTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class HttpMethodChangeTransformTests\n{\n    [Theory]\n    [InlineData(\"PUT\", \"POST\", \"PUT\", \"POST\")]\n    [InlineData(\"PUT\", \"POST\", \"POST\", \"POST\")]\n    [InlineData(\"PUT\", \"POST\", \"GET\", \"GET\")]\n    public async Task HttpMethodChange_Works(string fromMethod, string toMethod, string requestMethod, string expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        var request = new HttpRequestMessage() { Method = new HttpMethod(requestMethod) };\n        var context = new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = request,\n        };\n        var transform = new HttpMethodChangeTransform(fromMethod, toMethod);\n        await transform.ApplyAsync(context);\n        Assert.Equal(expected, request.Method.Method);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/HttpMethodTransformExtensionsTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class HttpMethodTransformExtensionsTests : TransformExtensionsTestsBase\n{\n    private readonly HttpMethodTransformFactory _factory = new();\n\n    [Fact]\n    public void WithTransformHttpMethodChange()\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformHttpMethodChange(HttpMethods.Put, HttpMethods.Post);\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        ValidateHttpMethod(builderContext);\n    }\n\n    [Fact]\n    public void AddHttpMethodChange()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddHttpMethodChange(HttpMethods.Put, HttpMethods.Post);\n\n        ValidateHttpMethod(builderContext);\n    }\n\n    private static void ValidateHttpMethod(TransformBuilderContext builderContext)\n    {\n        var requestTransform = Assert.Single(builderContext.RequestTransforms);\n        var httpMethodTransform = Assert.IsType<HttpMethodChangeTransform>(requestTransform);\n        Assert.Equal(HttpMethod.Put, httpMethodTransform.FromMethod);\n        Assert.Equal(HttpMethod.Post, httpMethodTransform.ToMethod);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/PathRouteValuesTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.AspNetCore.Routing.Template;\nusing Microsoft.Extensions.DependencyInjection;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class PathRouteValuesTransformTests\n{\n    [Theory]\n    [InlineData(\"/{a}/{b}/{c}\", \"/6/7/8\")]\n    [InlineData(\"/{a}/foo/{b}/{c}/{d}\", \"/6/foo/7/8\")] // Unknown value (d) dropped\n    [InlineData(\"/{a}/foo/{b}\", \"/6/foo/7\")] // Extra values (c) dropped\n    public async Task ReplacesPatternWithRouteValues(string transformValue, string expected)\n    {\n        var serviceCollection = new ServiceCollection();\n        serviceCollection.AddOptions();\n        serviceCollection.AddRouting();\n        using var services = serviceCollection.BuildServiceProvider();\n\n        var routeValues = new Dictionary<string, object>\n        {\n            { \"a\", \"6\" },\n            { \"b\", \"7\" },\n            { \"c\", \"8\" },\n        };\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.RouteValues = new RouteValueDictionary(routeValues);\n        var context = new RequestTransformContext()\n        {\n            Path = \"/\",\n            HttpContext = httpContext\n        };\n        var transform = new PathRouteValuesTransform(transformValue, services.GetRequiredService<TemplateBinderFactory>());\n        await transform.ApplyAsync(context);\n        Assert.Equal(expected, context.Path);\n\n        // The transform should not modify the original request's route values\n        Assert.Equal(routeValues, httpContext.Request.RouteValues);\n    }\n\n    [Fact]\n    public async Task RouteValuesWithSlashesNotEncoded()\n    {\n        var serviceCollection = new ServiceCollection();\n        serviceCollection.AddOptions();\n        serviceCollection.AddRouting();\n        using var services = serviceCollection.BuildServiceProvider();\n\n        var routeValues = new Dictionary<string, object>\n        {\n            { \"a\", \"abc\" },\n            { \"b\", \"def\" },\n            { \"remainder\", \"klm/nop/qrs\" },\n        };\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.RouteValues = new RouteValueDictionary(routeValues);\n        var context = new RequestTransformContext()\n        {\n            Path = \"/\",\n            HttpContext = httpContext\n        };\n        var transform = new PathRouteValuesTransform(\"/{a}/{b}/{**remainder}\", services.GetRequiredService<TemplateBinderFactory>());\n        await transform.ApplyAsync(context);\n        Assert.Equal(\"/abc/def/klm/nop/qrs\", context.Path.Value);\n\n        // The transform should not modify the original request's route values\n        Assert.Equal(routeValues, httpContext.Request.RouteValues);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/PathStringTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class PathStringTransformTests\n{\n    [Theory]\n    [InlineData(\"/foo\", \"Set\", \"/value\", \"/value\")]\n    [InlineData(\"/foo\", \"Set\", \"\", \"\")]\n    [InlineData(\"/foo\", \"Prefix\", \"/value\", \"/value/foo\")]\n    [InlineData(\"/value/foo\", \"RemovePrefix\", \"/value\", \"/foo\")]\n    public async Task Set_Path_Success(string initialValue, string modeString, string transformValue, string expected)\n    {\n        // We can't put an internal type in a public test API parameter.\n        var mode = Enum.Parse<PathStringTransform.PathTransformMode>(modeString);\n        var context = new RequestTransformContext() { Path = initialValue };\n        var transform = new PathStringTransform(mode, transformValue);\n        await transform.ApplyAsync(context);\n        Assert.Equal(expected, context.Path);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/PathTransformExtensionsTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing.Template;\nusing Microsoft.Extensions.DependencyInjection;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class PathTransformExtensionsTests : TransformExtensionsTestsBase\n{\n    private readonly PathTransformFactory _factory;\n\n    public PathTransformExtensionsTests()\n    {\n        var serviceCollection = new ServiceCollection();\n        serviceCollection.AddOptions();\n        serviceCollection.AddRouting();\n        var services = serviceCollection.BuildServiceProvider();\n\n        _factory = new PathTransformFactory(services.GetRequiredService<TemplateBinderFactory>());\n    }\n\n    [Fact]\n    public void WithTransformPathSet()\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformPathSet(new PathString(\"/path#\"));\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        ValidatePathSet(builderContext);\n    }\n\n    [Fact]\n    public void AddPathSet()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddPathSet(new PathString(\"/path#\"));\n\n        ValidatePathSet(builderContext);\n    }\n\n    private static void ValidatePathSet(TransformBuilderContext builderContext)\n    {\n        var requestTransform = Assert.Single(builderContext.RequestTransforms);\n        var pathStringTransform = Assert.IsType<PathStringTransform>(requestTransform);\n        Assert.Equal(PathStringTransform.PathTransformMode.Set, pathStringTransform.Mode);\n        Assert.Equal(\"/path#\", pathStringTransform.Value.Value);\n    }\n\n    [Fact]\n    public void WithTransformPathRemovePrefix()\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformPathRemovePrefix(new PathString(\"/path#\"));\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        ValidatePathRemovePrefix(builderContext);\n    }\n\n    [Fact]\n    public void AddPathRemovePrefix()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddPathRemovePrefix(new PathString(\"/path#\"));\n\n        ValidatePathRemovePrefix(builderContext);\n    }\n\n    private static void ValidatePathRemovePrefix(TransformBuilderContext builderContext)\n    {\n        var requestTransform = Assert.Single(builderContext.RequestTransforms);\n        var pathStringTransform = Assert.IsType<PathStringTransform>(requestTransform);\n        Assert.Equal(PathStringTransform.PathTransformMode.RemovePrefix, pathStringTransform.Mode);\n        Assert.Equal(\"/path#\", pathStringTransform.Value.Value);\n    }\n\n    [Fact]\n    public void WithTransformPathPrefix()\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformPathPrefix(new PathString(\"/path#\"));\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        ValidatePathPrefix(builderContext);\n    }\n\n    [Fact]\n    public void AddPathPrefix()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddPathPrefix(new PathString(\"/path#\"));\n\n        ValidatePathPrefix(builderContext);\n    }\n\n    private static void ValidatePathPrefix(TransformBuilderContext builderContext)\n    {\n        var requestTransform = Assert.Single(builderContext.RequestTransforms);\n        var pathStringTransform = Assert.IsType<PathStringTransform>(requestTransform);\n        Assert.Equal(PathStringTransform.PathTransformMode.Prefix, pathStringTransform.Mode);\n        Assert.Equal(\"/path#\", pathStringTransform.Value.Value);\n    }\n\n    [Fact]\n    public void WithTransformPathRouteValues()\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformPathRouteValues(new PathString(\"/path#\"));\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        ValidatePathRouteValues(builderContext);\n    }\n\n    [Fact]\n    public void AddPathRouteValues()\n    {\n        var serviceCollection = new ServiceCollection();\n        serviceCollection.AddOptions();\n        serviceCollection.AddRouting();\n        var services = serviceCollection.BuildServiceProvider();\n\n        var builderContext = CreateBuilderContext(services: services);\n        builderContext.AddPathRouteValues(new PathString(\"/path#\"));\n\n        ValidatePathRouteValues(builderContext);\n    }\n\n    private static void ValidatePathRouteValues(TransformBuilderContext builderContext)\n    {\n        var requestTransform = Assert.Single(builderContext.RequestTransforms);\n        var pathRouteValuesTransform = Assert.IsType<PathRouteValuesTransform>(requestTransform);\n        Assert.Equal(\"/path#\", pathRouteValuesTransform.Pattern.RawText);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/QueryParameterFromRouteTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.AspNetCore.Routing.Template;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class QueryParameterFromRouteTransformTests\n{\n    [Theory]\n    [InlineData(\"/{a}/{b}/{c}\", \"a\", \"?z=6\")]\n    [InlineData(\"/{a}/{b}/{c}\", \"c\", \"?z=8\")]\n    [InlineData(\"/{a}/{*remainder}\", \"remainder\", \"?z=7%2F8\")]\n    public async Task Append_AddsQueryParameterWithRouteValue(string pattern, string routeValueKey, string expected)\n    {\n        const string path = \"/6/7/8\";\n\n        var routeValues = new RouteValueDictionary();\n        var templateMatcher = new TemplateMatcher(TemplateParser.Parse(pattern), new RouteValueDictionary());\n        templateMatcher.TryMatch(path, routeValues);\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.RouteValues = routeValues;\n        var context = new RequestTransformContext()\n        {\n            Path = path,\n            Query = new QueryTransformContext(httpContext.Request),\n            HttpContext = httpContext\n        };\n        var transform = new QueryParameterRouteTransform(QueryStringTransformMode.Append, \"z\", routeValueKey);\n        await transform.ApplyAsync(context);\n        Assert.Equal(expected, context.Query.QueryString.Value);\n    }\n\n    [Fact]\n    public void Append_IgnoresExistingQueryParameter()\n    {\n        const string path = \"/6/7/8\";\n\n        var routeValues = new RouteValueDictionary();\n        var templateMatcher = new TemplateMatcher(TemplateParser.Parse(\"/{a}/{b}/{c}\"), new RouteValueDictionary());\n        templateMatcher.TryMatch(path, routeValues);\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.RouteValues = routeValues;\n        httpContext.Request.QueryString = new QueryString(\"?z=1\");\n        var context = new RequestTransformContext()\n        {\n            Path = path,\n            Query = new QueryTransformContext(httpContext.Request),\n            HttpContext = httpContext\n        };\n        var transform = new QueryParameterRouteTransform(QueryStringTransformMode.Append, \"z\", \"a\");\n        transform.ApplyAsync(context);\n        Assert.Equal(\"?z=1&z=6\", context.Query.QueryString.Value);\n    }\n\n    [Fact]\n    public void Set_OverwritesExistingQueryParameter()\n    {\n        const string path = \"/6/7/8\";\n\n        var routeValues = new RouteValueDictionary();\n        var templateMatcher = new TemplateMatcher(TemplateParser.Parse(\"/{a}/{b}/{c}\"), new RouteValueDictionary());\n        templateMatcher.TryMatch(path, routeValues);\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.RouteValues = routeValues;\n        httpContext.Request.QueryString = new QueryString(\"?z=1\");\n        var context = new RequestTransformContext()\n        {\n            Path = path,\n            Query = new QueryTransformContext(httpContext.Request),\n            HttpContext = httpContext\n        };\n        var transform = new QueryParameterRouteTransform(QueryStringTransformMode.Set, \"z\", \"a\");\n        transform.ApplyAsync(context);\n        Assert.Equal(\"?z=6\", context.Query.QueryString.Value);\n    }\n\n    [Fact]\n    public void Set_AddsNewQueryParameter()\n    {\n        const string path = \"/6/7/8\";\n\n        var routeValues = new RouteValueDictionary();\n        var templateMatcher = new TemplateMatcher(TemplateParser.Parse(\"/{a}/{b}/{c}\"), new RouteValueDictionary());\n        templateMatcher.TryMatch(path, routeValues);\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.RouteValues = routeValues;\n        var context = new RequestTransformContext()\n        {\n            Path = path,\n            Query = new QueryTransformContext(httpContext.Request),\n            HttpContext = httpContext\n        };\n        var transform = new QueryParameterRouteTransform(QueryStringTransformMode.Set, \"z\", \"a\");\n        transform.ApplyAsync(context);\n        Assert.Equal(\"?z=6\", context.Query.QueryString.Value);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/QueryParameterFromStaticTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class QueryParameterFromStaticTransformTests\n{\n    [Fact]\n    public async Task Append_AddsQueryStringParameterWithStaticValue()\n    {\n        var httpContext = new DefaultHttpContext();\n        var context = new RequestTransformContext()\n        {\n            Query = new QueryTransformContext(httpContext.Request),\n            HttpContext = httpContext\n        };\n        var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Append, \"z\", \"foo\");\n        await transform.ApplyAsync(context);\n        Assert.Equal(\"?z=foo\", context.Query.QueryString.Value);\n    }\n\n    [Fact]\n    public async Task Append_IgnoresExistingQueryStringParameter()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.QueryString = new QueryString(\"?z=1\");\n        var context = new RequestTransformContext()\n        {\n            Query = new QueryTransformContext(httpContext.Request),\n            HttpContext = httpContext\n        };\n        var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Append, \"z\", \"foo\");\n        await transform.ApplyAsync(context);\n        Assert.Equal(\"?z=1&z=foo\", context.Query.QueryString.Value);\n    }\n\n    [Fact]\n    public async Task Set_OverwritesExistingQueryStringParameter()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.QueryString = new QueryString(\"?z=1\");\n        var context = new RequestTransformContext()\n        {\n            Query = new QueryTransformContext(httpContext.Request),\n            HttpContext = httpContext\n        };\n        var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Set, \"z\", \"foo\");\n        await transform.ApplyAsync(context);\n        Assert.Equal(\"?z=foo\", context.Query.QueryString.Value);\n    }\n\n    [Fact]\n    public async Task Set_AddsNewQueryStringParameter()\n    {\n        var httpContext = new DefaultHttpContext();\n        var context = new RequestTransformContext()\n        {\n            Query = new QueryTransformContext(httpContext.Request),\n            HttpContext = httpContext\n        };\n        var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Set, \"z\", \"foo\");\n        await transform.ApplyAsync(context);\n        Assert.Equal(\"?z=foo\", context.Query.QueryString.Value);\n    }\n\n    [Fact]\n    public async Task Set_AddsNewEmptyQueryStringParameter()\n    {\n        var httpContext = new DefaultHttpContext();\n        var context = new RequestTransformContext()\n        {\n            Query = new QueryTransformContext(httpContext.Request),\n            HttpContext = httpContext\n        };\n        var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Set, \"z\", \"\");\n        await transform.ApplyAsync(context);\n        Assert.Equal(\"?z=\", context.Query.QueryString.Value);\n    }\n\n    [Fact]\n    public async Task Set_OverwritesExistingParamWithEmptyValue()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.QueryString = new QueryString(\"?z=foo\");\n        var context = new RequestTransformContext()\n        {\n            Query = new QueryTransformContext(httpContext.Request),\n            HttpContext = httpContext\n        };\n        var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Set, \"z\", \"\");\n        await transform.ApplyAsync(context);\n        Assert.Equal(\"?z=\", context.Query.QueryString.Value);\n    }\n\n    [Fact]\n    public async Task Set_AddNewEmptyParamToExistingQueryStringParameter()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.QueryString = new QueryString(\"?x=1\");\n        var context = new RequestTransformContext()\n        {\n            Query = new QueryTransformContext(httpContext.Request),\n            HttpContext = httpContext\n        };\n        var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Set, \"z\", \"\");\n        await transform.ApplyAsync(context);\n        Assert.Equal(\"?x=1&z=\", context.Query.QueryString.Value);\n    }\n\n    [Fact]\n    public async Task Append_AddsNewEmptyQueryStringParameter()\n    {\n        var httpContext = new DefaultHttpContext();\n        var context = new RequestTransformContext()\n        {\n            Query = new QueryTransformContext(httpContext.Request),\n            HttpContext = httpContext\n        };\n        var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Append, \"z\", \"\");\n        await transform.ApplyAsync(context);\n        Assert.Equal(\"?z=\", context.Query.QueryString.Value);\n    }\n\n    [Fact]\n    public async Task Append_AppendsEmptyValueToExistingParam()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.QueryString = new QueryString(\"?z=foo\");\n        var context = new RequestTransformContext()\n        {\n            Query = new QueryTransformContext(httpContext.Request),\n            HttpContext = httpContext\n        };\n        var transform = new QueryParameterFromStaticTransform(QueryStringTransformMode.Append, \"z\", \"\");\n        await transform.ApplyAsync(context);\n        Assert.Equal(\"?z=foo&z=\", context.Query.QueryString.Value);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/QueryParameterRemoveTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class QueryParameterRemoveTransformTests\n{\n    [Fact]\n    public async Task RemovesExistingQueryParameter()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.QueryString = new QueryString(\"?z=1\");\n        var context = new RequestTransformContext()\n        {\n            Query = new QueryTransformContext(httpContext.Request)\n        };\n        var transform = new QueryParameterRemoveTransform(\"z\");\n        await transform.ApplyAsync(context);\n        Assert.False(context.Query.QueryString.HasValue);\n    }\n\n    [Fact]\n    public async Task LeavesOtherQueryParameters()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.QueryString = new QueryString(\"?z=1&a=2\");\n        var context = new RequestTransformContext()\n        {\n            Query = new QueryTransformContext(httpContext.Request),\n        };\n        var transform = new QueryParameterRemoveTransform(\"z\");\n        await transform.ApplyAsync(context);\n        Assert.Equal(\"?a=2\", context.Query.QueryString.Value);\n    }\n\n    [Fact]\n    public async Task DoesNotFailOnNonExistingQueryParameter()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.QueryString = new QueryString(\"?z=1\");\n        var context = new RequestTransformContext()\n        {\n            Query = new QueryTransformContext(httpContext.Request),\n        };\n        var transform = new QueryParameterRemoveTransform(\"a\");\n        await transform.ApplyAsync(context);\n        Assert.Equal(\"?z=1\", context.Query.QueryString.Value);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/QueryTransformContextTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class QueryTransformContextTests\n{\n    [Fact]\n    public void Collection_TryGetValue_CaseInsensitive()\n    {\n        var httpContext = new DefaultHttpContext { Request = { QueryString = new QueryString(\"?z=1\") } };\n        var queryTransformContext = new QueryTransformContext(httpContext.Request);\n        queryTransformContext.Collection.TryGetValue(\"Z\", out var result);\n        Assert.Equal(\"1\", result);\n    }\n\n    [Fact]\n    public void Collection_RemoveKey_CaseInsensitive()\n    {\n        var httpContext = new DefaultHttpContext { Request = { QueryString = new QueryString(\"?z=1\") } };\n        var queryTransformContext = new QueryTransformContext(httpContext.Request);\n        queryTransformContext.Collection.Remove(\"Z\");\n        Assert.False(queryTransformContext.QueryString.HasValue);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/QueryTransformExtensionsTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class QueryTransformExtensionsTests : TransformExtensionsTestsBase\n{\n    private readonly QueryTransformFactory _factory = new();\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void WithTransformQueryRouteValue(bool append)\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformQueryRouteValue(\"key\", \"value\", append);\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        ValidateQueryRouteParameter(append, builderContext);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void AddQueryRouteValue(bool append)\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddQueryRouteValue(\"key\", \"value\", append);\n\n        ValidateQueryRouteParameter(append, builderContext);\n    }\n\n    private static void ValidateQueryRouteParameter(bool append, TransformBuilderContext builderContext)\n    {\n        var requestTransform = Assert.Single(builderContext.RequestTransforms);\n        var queryParameterRouteTransform = Assert.IsType<QueryParameterRouteTransform>(requestTransform);\n        Assert.Equal(\"key\", queryParameterRouteTransform.Key);\n        Assert.Equal(\"value\", queryParameterRouteTransform.RouteValueKey);\n        var expectedMode = append ? QueryStringTransformMode.Append : QueryStringTransformMode.Set;\n        Assert.Equal(expectedMode, queryParameterRouteTransform.Mode);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void WithTransformQueryValue(bool append)\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformQueryValue(\"key\", \"value\", append);\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        ValidateQueryValue(append, builderContext);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void AddQueryValue(bool append)\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddQueryValue(\"key\", \"value\", append);\n\n        ValidateQueryValue(append, builderContext);\n    }\n\n    private static void ValidateQueryValue(bool append, TransformBuilderContext builderContext)\n    {\n        var requestTransform = Assert.Single(builderContext.RequestTransforms);\n        var queryParameterFromStaticTransform = Assert.IsType<QueryParameterFromStaticTransform>(requestTransform);\n        Assert.Equal(\"key\", queryParameterFromStaticTransform.Key);\n        Assert.Equal(\"value\", queryParameterFromStaticTransform.Value);\n        var expectedMode = append ? QueryStringTransformMode.Append : QueryStringTransformMode.Set;\n        Assert.Equal(expectedMode, queryParameterFromStaticTransform.Mode);\n    }\n\n    [Fact]\n    public void WithTransformQueryRemoveKey()\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformQueryRemoveKey(\"key\");\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        ValidateQueryRemoveKey(builderContext);\n    }\n\n    [Fact]\n    public void AddQueryRemoveKey()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddQueryRemoveKey(\"key\");\n\n        ValidateQueryRemoveKey(builderContext);\n    }\n\n    private static void ValidateQueryRemoveKey(TransformBuilderContext builderContext)\n    {\n        var requestTransform = Assert.Single(builderContext.RequestTransforms);\n        var removeQueryParameterTransform = Assert.IsType<QueryParameterRemoveTransform>(requestTransform);\n        Assert.Equal(\"key\", removeQueryParameterTransform.Key);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/RequestHeaderClientCertTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.IO;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class RequestHeaderClientCertTransformTests\n{\n    [Fact]\n    public async Task NoCert_NoOp()\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage();\n        var transform = new RequestHeaderClientCertTransform(\"Name\");\n        await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest });\n        Assert.Empty(proxyRequest.Headers);\n    }\n\n    [Fact]\n    public async Task Cert_Encoded()\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage();\n        httpContext.Connection.ClientCertificate = Certificates.SelfSignedValidWithClientEku;\n        var transform = new RequestHeaderClientCertTransform(\"Name\");\n        await transform.ApplyAsync(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest });\n        var expected = Convert.ToBase64String(Certificates.SelfSignedValidWithClientEku.RawData);\n        Assert.Equal(expected, proxyRequest.Headers.GetValues(\"Name\").Single());\n    }\n\n    [Fact]\n    public async Task ExistingHeader_NoCert_RemovesHeader()\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"Name\", \"OtherValue\");\n        var transform = new RequestHeaderClientCertTransform(\"Name\");\n        await transform.ApplyAsync(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true\n        });\n        Assert.Empty(proxyRequest.Headers);\n    }\n\n    [Fact]\n    public async Task ExistingHeader_Replaced()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Connection.ClientCertificate = Certificates.SelfSignedValidWithClientEku;\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"Name\", \"OtherValue\");\n        var transform = new RequestHeaderClientCertTransform(\"Name\");\n        await transform.ApplyAsync(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true\n        });\n        var expected = Convert.ToBase64String(Certificates.SelfSignedValidWithClientEku.RawData);\n        Assert.Equal(expected, proxyRequest.Headers.GetValues(\"Name\").Single());\n    }\n\n    private static class Certificates\n    {\n        public static X509Certificate2 SelfSignedValidWithClientEku { get; private set; } =\n            new X509Certificate2(GetFullyQualifiedFilePath(\"validSelfSignedClientEkuCertificate.cer\"));\n\n        private static string GetFullyQualifiedFilePath(string filename)\n        {\n            var filePath = Path.Combine(AppContext.BaseDirectory, filename);\n            if (!File.Exists(filePath))\n            {\n                throw new FileNotFoundException(filePath);\n            }\n            return filePath;\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/RequestHeaderForwardedTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class RequestHeaderForwardedTransformTests\n{\n    [Theory]\n    // Using \"|\" to represent multi-line headers\n    [InlineData(\"\", \"https\", ForwardedTransformActions.Set, \"proto=https\")]\n    [InlineData(\"\", \"https\", ForwardedTransformActions.Append, \"proto=https\")]\n    [InlineData(\"\", \"https\", ForwardedTransformActions.Remove, \"\")]\n    [InlineData(\"existing,Header\", \"https\", ForwardedTransformActions.Set, \"proto=https\")]\n    [InlineData(\"existing|Header\", \"https\", ForwardedTransformActions.Set, \"proto=https\")]\n    [InlineData(\"existing,Header\", \"https\", ForwardedTransformActions.Append, \"existing,Header|proto=https\")]\n    [InlineData(\"existing|Header\", \"https\", ForwardedTransformActions.Append, \"existing|Header|proto=https\")]\n    [InlineData(\"existing,Header\", \"https\", ForwardedTransformActions.Remove, \"\")]\n    [InlineData(\"existing|Header\", \"https\", ForwardedTransformActions.Remove, \"\")]\n    public async Task Proto_Added(string startValue, string scheme, ForwardedTransformActions action, string expected)\n    {\n        var randomFactory = new TestRandomFactory();\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Scheme = scheme;\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"Forwarded\", startValue.Split(\"|\", StringSplitOptions.RemoveEmptyEntries));\n        var transform = new RequestHeaderForwardedTransform(randomFactory, forFormat: NodeFormat.None,\n            byFormat: NodeFormat.None, host: false, proto: true, action);\n        await transform.ApplyAsync(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true,\n        });\n        if (string.IsNullOrEmpty(expected))\n        {\n            Assert.False(proxyRequest.Headers.TryGetValues(\"Forwarded\", out _));\n        }\n        else\n        {\n            Assert.Equal(expected.Split(\"|\", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues(\"Forwarded\"));\n        }\n    }\n\n    [Theory]\n    // Using \"|\" to represent multi-line headers\n    [InlineData(\"\", \"myHost\", ForwardedTransformActions.Set, \"host=\\\"myHost\\\"\")]\n    [InlineData(\"\", \"myHost\", ForwardedTransformActions.Append, \"host=\\\"myHost\\\"\")]\n    [InlineData(\"\", \"ho本st\", ForwardedTransformActions.Set, \"host=\\\"xn--host-6j1i\\\"\")]\n    [InlineData(\"\", \"myHost:80\", ForwardedTransformActions.Set, \"host=\\\"myHost:80\\\"\")]\n    [InlineData(\"\", \"ho本st:80\", ForwardedTransformActions.Set, \"host=\\\"xn--host-6j1i:80\\\"\")]\n    [InlineData(\"existing,Header\", \"myHost\", ForwardedTransformActions.Set, \"host=\\\"myHost\\\"\")]\n    [InlineData(\"existing|Header\", \"myHost\", ForwardedTransformActions.Set, \"host=\\\"myHost\\\"\")]\n    [InlineData(\"existing|Header\", \"myHost:80\", ForwardedTransformActions.Set, \"host=\\\"myHost:80\\\"\")]\n    [InlineData(\"existing,Header\", \"myHost\", ForwardedTransformActions.Append, \"existing,Header|host=\\\"myHost\\\"\")]\n    [InlineData(\"existing|Header\", \"myHost\", ForwardedTransformActions.Append, \"existing|Header|host=\\\"myHost\\\"\")]\n    [InlineData(\"existing|Header\", \"myHost:80\", ForwardedTransformActions.Append, \"existing|Header|host=\\\"myHost:80\\\"\")]\n    public async Task Host_Added(string startValue, string host, ForwardedTransformActions action, string expected)\n    {\n        var randomFactory = new TestRandomFactory();\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Host = new HostString(host);\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"Forwarded\", startValue.Split(\"|\", StringSplitOptions.RemoveEmptyEntries));\n        var transform = new RequestHeaderForwardedTransform(randomFactory, forFormat: NodeFormat.None,\n            byFormat: NodeFormat.None, host: true, proto: false, action);\n        await transform.ApplyAsync(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true,\n        });\n        Assert.Equal(expected.Split(\"|\", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues(\"Forwarded\"));\n    }\n\n    [Theory]\n    // Using \"|\" to represent multi-line headers\n    [InlineData(\"\", \"\", 2, NodeFormat.Ip, ForwardedTransformActions.Set, \"for=unknown\")] // Missing IP falls back to Unknown\n    [InlineData(\"\", \"\", 0, NodeFormat.IpAndPort, ForwardedTransformActions.Append, \"for=unknown\")] // Missing port excluded\n    [InlineData(\"\", \"\", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, \"for=\\\"unknown:2\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.Unknown, ForwardedTransformActions.Set, \"for=unknown\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.UnknownAndPort, ForwardedTransformActions.Append, \"for=\\\"unknown:2\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.UnknownAndRandomPort, ForwardedTransformActions.Append, \"for=\\\"unknown:_abcdefghi\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.Ip, ForwardedTransformActions.Set, \"for=\\\"[::1]\\\"\")]\n    [InlineData(\"\", \"::1\", 0, NodeFormat.IpAndPort, ForwardedTransformActions.Append, \"for=\\\"[::1]\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, \"for=\\\"[::1]:2\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, \"for=\\\"[::1]:_abcdefghi\\\"\")]\n    [InlineData(\"\", \"127.0.0.1\", 2, NodeFormat.Ip, ForwardedTransformActions.Set, \"for=127.0.0.1\")]\n    [InlineData(\"\", \"127.0.0.1\", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, \"for=\\\"127.0.0.1:2\\\"\")]\n    [InlineData(\"\", \"127.0.0.1\", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, \"for=\\\"127.0.0.1:_abcdefghi\\\"\")]\n    [InlineData(\"\", \"::ffff:127.0.0.1\", 2, NodeFormat.Ip, ForwardedTransformActions.Set, \"for=127.0.0.1\")]\n    [InlineData(\"\", \"::ffff:127.0.0.1\", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, \"for=\\\"127.0.0.1:2\\\"\")]\n    [InlineData(\"\", \"::ffff:127.0.0.1\", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, \"for=\\\"127.0.0.1:_abcdefghi\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.Random, ForwardedTransformActions.Set, \"for=_abcdefghi\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.RandomAndPort, ForwardedTransformActions.Append, \"for=\\\"_abcdefghi:2\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.RandomAndRandomPort, ForwardedTransformActions.Append, \"for=\\\"_abcdefghi:_jklmnopqr\\\"\")]\n    [InlineData(\"existing,header\", \"::1\", 2, NodeFormat.Random, ForwardedTransformActions.Set, \"for=_abcdefghi\")]\n    [InlineData(\"existing,header\", \"::1\", 2, NodeFormat.RandomAndPort, ForwardedTransformActions.Append, \"existing,header|for=\\\"_abcdefghi:2\\\"\")]\n    [InlineData(\"existing|header\", \"::1\", 2, NodeFormat.RandomAndPort, ForwardedTransformActions.Append, \"existing|header|for=\\\"_abcdefghi:2\\\"\")]\n    [InlineData(\"existing,header\", \"::1\", 2, NodeFormat.RandomAndRandomPort, ForwardedTransformActions.Append, \"existing,header|for=\\\"_abcdefghi:_jklmnopqr\\\"\")]\n    [InlineData(\"existing|header\", \"::1\", 2, NodeFormat.RandomAndRandomPort, ForwardedTransformActions.Append, \"existing|header|for=\\\"_abcdefghi:_jklmnopqr\\\"\")]\n    public async Task For_Added(string startValue, string ip, int port, NodeFormat format, ForwardedTransformActions action, string expected)\n    {\n        var randomFactory = new TestRandomFactory();\n        randomFactory.Instance = new TestRandom() { Sequence = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 } };\n        var httpContext = new DefaultHttpContext();\n        httpContext.Connection.RemoteIpAddress = string.IsNullOrEmpty(ip) ? null : IPAddress.Parse(ip);\n        httpContext.Connection.RemotePort = port;\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"Forwarded\", startValue.Split(\"|\", StringSplitOptions.RemoveEmptyEntries));\n        var transform = new RequestHeaderForwardedTransform(randomFactory, forFormat: format,\n            byFormat: NodeFormat.None, host: false, proto: false, action);\n        await transform.ApplyAsync(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true,\n        });\n        Assert.Equal(expected.Split(\"|\", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues(\"Forwarded\"));\n    }\n\n    [Theory]\n    // Using \"|\" to represent multi-line headers\n    [InlineData(\"\", \"\", 2, NodeFormat.Ip, ForwardedTransformActions.Set, \"by=unknown\")] // Missing IP falls back to Unknown\n    [InlineData(\"\", \"\", 0, NodeFormat.IpAndPort, ForwardedTransformActions.Append, \"by=unknown\")] // Missing port excluded\n    [InlineData(\"\", \"\", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, \"by=\\\"unknown:2\\\"\")]\n    [InlineData(\"\", \"\", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, \"by=\\\"unknown:_abcdefghi\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.Unknown, ForwardedTransformActions.Set, \"by=unknown\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.UnknownAndPort, ForwardedTransformActions.Append, \"by=\\\"unknown:2\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.UnknownAndRandomPort, ForwardedTransformActions.Append, \"by=\\\"unknown:_abcdefghi\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.Ip, ForwardedTransformActions.Set, \"by=\\\"[::1]\\\"\")]\n    [InlineData(\"\", \"::1\", 0, NodeFormat.IpAndPort, ForwardedTransformActions.Append, \"by=\\\"[::1]\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, \"by=\\\"[::1]:2\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, \"by=\\\"[::1]:_abcdefghi\\\"\")]\n    [InlineData(\"\", \"127.0.0.1\", 2, NodeFormat.Ip, ForwardedTransformActions.Set, \"by=127.0.0.1\")]\n    [InlineData(\"\", \"127.0.0.1\", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, \"by=\\\"127.0.0.1:2\\\"\")]\n    [InlineData(\"\", \"127.0.0.1\", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, \"by=\\\"127.0.0.1:_abcdefghi\\\"\")]\n    [InlineData(\"\", \"::ffff:127.0.0.1\", 2, NodeFormat.Ip, ForwardedTransformActions.Set, \"by=127.0.0.1\")]\n    [InlineData(\"\", \"::ffff:127.0.0.1\", 2, NodeFormat.IpAndPort, ForwardedTransformActions.Append, \"by=\\\"127.0.0.1:2\\\"\")]\n    [InlineData(\"\", \"::ffff:127.0.0.1\", 2, NodeFormat.IpAndRandomPort, ForwardedTransformActions.Append, \"by=\\\"127.0.0.1:_abcdefghi\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.Random, ForwardedTransformActions.Set, \"by=_abcdefghi\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.RandomAndPort, ForwardedTransformActions.Append, \"by=\\\"_abcdefghi:2\\\"\")]\n    [InlineData(\"\", \"::1\", 2, NodeFormat.RandomAndRandomPort, ForwardedTransformActions.Append, \"by=\\\"_abcdefghi:_jklmnopqr\\\"\")]\n    [InlineData(\"existing,header\", \"::1\", 2, NodeFormat.Random, ForwardedTransformActions.Set, \"by=_abcdefghi\")]\n    [InlineData(\"existing,header\", \"::1\", 2, NodeFormat.RandomAndPort, ForwardedTransformActions.Append, \"existing,header|by=\\\"_abcdefghi:2\\\"\")]\n    [InlineData(\"existing|header\", \"::1\", 2, NodeFormat.RandomAndPort, ForwardedTransformActions.Append, \"existing|header|by=\\\"_abcdefghi:2\\\"\")]\n    [InlineData(\"existing,header\", \"::1\", 2, NodeFormat.RandomAndRandomPort, ForwardedTransformActions.Append, \"existing,header|by=\\\"_abcdefghi:_jklmnopqr\\\"\")]\n    [InlineData(\"existing|header\", \"::1\", 2, NodeFormat.RandomAndRandomPort, ForwardedTransformActions.Append, \"existing|header|by=\\\"_abcdefghi:_jklmnopqr\\\"\")]\n    public async Task By_Added(string startValue, string ip, int port, NodeFormat format, ForwardedTransformActions action, string expected)\n    {\n        var randomFactory = new TestRandomFactory();\n        randomFactory.Instance = new TestRandom() { Sequence = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 } };\n        var httpContext = new DefaultHttpContext();\n        httpContext.Connection.LocalIpAddress = string.IsNullOrEmpty(ip) ? null : IPAddress.Parse(ip);\n        httpContext.Connection.LocalPort = port;\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"Forwarded\", startValue.Split(\"|\", StringSplitOptions.RemoveEmptyEntries));\n        var transform = new RequestHeaderForwardedTransform(randomFactory, forFormat: NodeFormat.None,\n            byFormat: format, host: false, proto: false, action);\n        await transform.ApplyAsync(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true,\n        });\n        Assert.Equal(expected.Split(\"|\", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues(\"Forwarded\"));\n    }\n\n    [Theory]\n    // Using \"|\" to represent multi-line headers\n    [InlineData(\"\", ForwardedTransformActions.Set, \"proto=https;host=\\\"myHost:80\\\";for=\\\"[::1]:10\\\";by=_abcdefghi\")]\n    [InlineData(\"\", ForwardedTransformActions.Append, \"proto=https;host=\\\"myHost:80\\\";for=\\\"[::1]:10\\\";by=_abcdefghi\")]\n    [InlineData(\"otherHeader\", ForwardedTransformActions.Set, \"proto=https;host=\\\"myHost:80\\\";for=\\\"[::1]:10\\\";by=_abcdefghi\")]\n    [InlineData(\"otherHeader\", ForwardedTransformActions.Append, \"otherHeader|proto=https;host=\\\"myHost:80\\\";for=\\\"[::1]:10\\\";by=_abcdefghi\")]\n    public async Task AllValues_Added(string startValue, ForwardedTransformActions action, string expected)\n    {\n        var randomFactory = new TestRandomFactory();\n        randomFactory.Instance = new TestRandom() { Sequence = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 } };\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Scheme = \"https\";\n        httpContext.Request.Host = new HostString(\"myHost\", 80);\n        httpContext.Connection.RemoteIpAddress = IPAddress.IPv6Loopback;\n        httpContext.Connection.RemotePort = 10;\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"Forwarded\", startValue.Split(\"|\", StringSplitOptions.RemoveEmptyEntries));\n        var transform = new RequestHeaderForwardedTransform(randomFactory,\n            forFormat: NodeFormat.IpAndPort,\n            byFormat: NodeFormat.Random,\n            host: true, proto: true, action);\n        await transform.ApplyAsync(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true,\n        });\n        Assert.Equal(expected.Split(\"|\", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues(\"Forwarded\"));\n    }\n\n    internal class TestRandomFactory : IRandomFactory\n    {\n        internal TestRandom Instance { get; set; }\n\n        public Random CreateRandomInstance()\n        {\n            return Instance;\n        }\n    }\n\n    public class TestRandom : Random\n    {\n        public int[] Sequence { get; set; }\n        public int Offset { get; set; }\n\n        public override int Next(int maxValue)\n        {\n            return Sequence[Offset++];\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/RequestHeaderRemoveTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class RequestHeaderRemoveTransformTests\n{\n    [Theory]\n    [InlineData(\"header1\", \"value1\", \"header1\", \"\")]\n    [InlineData(\"header1\", \"value1\", \"headerX\", \"header1\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", \"header2\", \"header1; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", \"headerX\", \"header1; header2; header3\")]\n    [InlineData(\"header1; header2; header2; header3\", \"value1, value2-1, value2-2, value3\", \"header2\", \"header1; header3\")]\n    public async Task RemoveHeader_Success(string names, string values, string removedHeader, string expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage();\n        foreach (var pair in TestResources.ParseNameAndValues(names, values))\n        {\n            proxyRequest.Headers.Add(pair.Name, pair.Values);\n        }\n\n        var transform = new RequestHeaderRemoveTransform(removedHeader);\n        await transform.ApplyAsync(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true,\n        });\n\n        var expectedHeaders = expected.Split(\"; \", StringSplitOptions.RemoveEmptyEntries);\n        Assert.Equal(expectedHeaders, proxyRequest.Headers.Select(h => h.Key));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/RequestHeaderRouteValueTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.AspNetCore.Routing.Template;\nusing Xunit;\nusing Yarp.ReverseProxy.Transforms;\n\nnamespace Yarp.ReverseProxy.Tests.Transforms;\n\npublic class RequestHeaderRouteValueTransformTests\n{\n    [Theory]\n    [InlineData(\"defaultHeader\", \"value\", \"/{a}/{b}/{c}\", \"a\", \"value;6\", true)]\n    [InlineData(\"defaultHeader\", \"value\", \"/{a}/{b}/{c}\", \"notInRoute\", \"value\", true)]\n    [InlineData(\"defaultHeader\", \"value\", \"/{a}/{b}/{c}\", \"notInRoute\", \"value\", false)]\n    [InlineData(\"defaultHeader\", \"value\", \"/{a}/{b}/{c}\", \"a\", \"6\", false)]\n    [InlineData(\"h1\", \"value\", \"/{a}/{b}/{c}\", \"a\", \"6\", false)]\n    [InlineData(\"h1\", \"value\", \"/{a}/{b}/{c}\", \"b\", \"7\", false)]\n    [InlineData(\"h1\", \"value\", \"/{a}/{*remainder}\", \"remainder\", \"7/8\", false)]\n    public async Task AddsRequestHeaderRouteValue_SetHeader(string headerName, string defaultHeaderStartValue, string pattern, string routeValueKey, string expected, bool append)\n    {\n        // Arrange\n        const string path = \"/6/7/8\";\n\n        var routeValues = new RouteValueDictionary();\n        var templateMatcher = new TemplateMatcher(TemplateParser.Parse(pattern), new RouteValueDictionary());\n        templateMatcher.TryMatch(path, routeValues);\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.RouteValues = routeValues;\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"defaultHeader\", defaultHeaderStartValue.Split(\";\", StringSplitOptions.RemoveEmptyEntries));\n\n        var context = new RequestTransformContext()\n        {\n            Path = path,\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true\n        };\n\n        // Act\n        var transform = new RequestHeaderRouteValueTransform(headerName, routeValueKey, append);\n        await transform.ApplyAsync(context);\n\n        // Assert\n        Assert.Equal(expected.Split(\";\", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues(headerName));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/RequestHeaderValueTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class RequestHeaderValueTransformTests\n{\n    [Theory]\n    // Using \";\" to represent multi-line headers\n    [InlineData(\"\", \"new,value\", false, \"new,value\")]\n    [InlineData(\"\", \"new,value\", true, \"new,value\")]\n    [InlineData(\"start\", \"new,value\", false, \"new,value\")]\n    [InlineData(\"start,value\", \"new,value\", false, \"new,value\")]\n    [InlineData(\"start;value\", \"new,value\", false, \"new,value\")]\n    [InlineData(\"start\", \"new,value\", true, \"start;new,value\")]\n    [InlineData(\"start,value\", \"new,value\", true, \"start,value;new,value\")]\n    [InlineData(\"start;value\", \"new,value\", true, \"start;value;new,value\")]\n    public async Task AddHeader_Success(string startValue, string value, bool append, string expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"name\", startValue.Split(\";\", StringSplitOptions.RemoveEmptyEntries));\n        var transform = new RequestHeaderValueTransform(\"name\", value, append);\n        await transform.ApplyAsync(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true,\n        });\n        Assert.Equal(expected.Split(\";\", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues(\"name\"));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/RequestHeaderXForwardedForTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class RequestHeaderXForwardedForTransformTests\n{\n    [Theory]\n    // Using \";\" to represent multi-line headers\n    [InlineData(\"\", \"\", ForwardedTransformActions.Set, \"\")]\n    [InlineData(\"\", \"\", ForwardedTransformActions.Append, \"\")]\n    [InlineData(\"\", \"\", ForwardedTransformActions.Remove, \"\")]\n    [InlineData(\"\", \"::1\", ForwardedTransformActions.Set, \"::1\")]\n    [InlineData(\"\", \"127.0.0.1\", ForwardedTransformActions.Set, \"127.0.0.1\")]\n    [InlineData(\"\", \"::ffff:127.0.0.1\", ForwardedTransformActions.Set, \"127.0.0.1\")]\n    [InlineData(\"\", \"127.0.0.1\", ForwardedTransformActions.Append, \"127.0.0.1\")]\n    [InlineData(\"\", \"::ffff:127.0.0.1\", ForwardedTransformActions.Append, \"127.0.0.1\")]\n    [InlineData(\"\", \"127.0.0.1\", ForwardedTransformActions.Remove, \"\")]\n    [InlineData(\"existing,Header\", \"\", ForwardedTransformActions.Set, \"\")]\n    [InlineData(\"existing;Header\", \"\", ForwardedTransformActions.Set, \"\")]\n    [InlineData(\"existing,Header\", \"\", ForwardedTransformActions.Append, \"existing,Header\")]\n    [InlineData(\"existing;Header\", \"\", ForwardedTransformActions.Append, \"existing;Header\")]\n    [InlineData(\"existing;Header\", \"\", ForwardedTransformActions.Remove, \"\")]\n    [InlineData(\"existing,Header\", \"127.0.0.1\", ForwardedTransformActions.Set, \"127.0.0.1\")]\n    [InlineData(\"existing;Header\", \"127.0.0.1\", ForwardedTransformActions.Set, \"127.0.0.1\")]\n    [InlineData(\"existing,Header\", \"127.0.0.1\", ForwardedTransformActions.Append, \"existing,Header;127.0.0.1\")]\n    [InlineData(\"existing;Header\", \"127.0.0.1\", ForwardedTransformActions.Append, \"existing;Header;127.0.0.1\")]\n    [InlineData(\"existing;Header\", \"127.0.0.1\", ForwardedTransformActions.Remove, \"\")]\n    public async Task RemoteIp_Added(string startValue, string remoteIp, ForwardedTransformActions action, string expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Connection.RemoteIpAddress = string.IsNullOrEmpty(remoteIp) ? null : IPAddress.Parse(remoteIp);\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"name\", startValue.Split(\";\", StringSplitOptions.RemoveEmptyEntries));\n        var transform = new RequestHeaderXForwardedForTransform(\"name\", action);\n        await transform.ApplyAsync(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true,\n        });\n        if (string.IsNullOrEmpty(expected))\n        {\n            Assert.False(proxyRequest.Headers.TryGetValues(\"name\", out var _));\n        }\n        else\n        {\n            Assert.Equal(expected.Split(\";\", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues(\"name\"));\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/RequestHeaderXForwardedHostTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class RequestHeaderXForwardedHostTransformTests\n{\n    [Theory]\n    // Using \";\" to represent multi-line headers\n    [InlineData(\"\", \"\", ForwardedTransformActions.Set, \"\")]\n    [InlineData(\"\", \"\", ForwardedTransformActions.Append, \"\")]\n    [InlineData(\"\", \"\", ForwardedTransformActions.Remove, \"\")]\n    [InlineData(\"\", \"host\", ForwardedTransformActions.Set, \"host\")]\n    [InlineData(\"\", \"host:80\", ForwardedTransformActions.Append, \"host:80\")]\n    [InlineData(\"\", \"host:80\", ForwardedTransformActions.Remove, \"\")]\n    [InlineData(\"\", \"ho本st\", ForwardedTransformActions.Set, \"xn--host-6j1i\")]\n    [InlineData(\"\", \"::1\", ForwardedTransformActions.Set, \"::1\")]\n    [InlineData(\"\", \"[::1]:80\", ForwardedTransformActions.Set, \"[::1]:80\")]\n    [InlineData(\"existing,Header\", \"\", ForwardedTransformActions.Set, \"\")]\n    [InlineData(\"existing;Header\", \"\", ForwardedTransformActions.Set, \"\")]\n    [InlineData(\"existing,Header\", \"\", ForwardedTransformActions.Append, \"existing,Header\")]\n    [InlineData(\"existing;Header\", \"\", ForwardedTransformActions.Append, \"existing;Header\")]\n    [InlineData(\"existing;Header\", \"\", ForwardedTransformActions.Remove, \"\")]\n    [InlineData(\"existing,Header\", \"host\", ForwardedTransformActions.Set, \"host\")]\n    [InlineData(\"existing;Header\", \"host\", ForwardedTransformActions.Set, \"host\")]\n    [InlineData(\"existing,Header\", \"host:80\", ForwardedTransformActions.Append, \"existing,Header;host:80\")]\n    [InlineData(\"existing;Header\", \"host\", ForwardedTransformActions.Append, \"existing;Header;host\")]\n    [InlineData(\"existing;Header\", \"host\", ForwardedTransformActions.Remove, \"\")]\n    public async Task Host_Added(string startValue, string host, ForwardedTransformActions action, string expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Host = string.IsNullOrEmpty(host) ? new HostString() : new HostString(host);\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"name\", startValue.Split(\";\", StringSplitOptions.RemoveEmptyEntries));\n        var transform = new RequestHeaderXForwardedHostTransform(\"name\", action);\n        await transform.ApplyAsync(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true,\n        });\n        if (string.IsNullOrEmpty(expected))\n        {\n            Assert.False(proxyRequest.Headers.TryGetValues(\"name\", out var _));\n        }\n        else\n        {\n            Assert.Equal(expected.Split(\";\", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues(\"name\"));\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/RequestHeaderXForwardedPrefixTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class RequestHeaderXForwardedPrefixTransformTests\n{\n    [Theory]\n    // Using \";\" to represent multi-line headers\n    [InlineData(\"\", \"\", ForwardedTransformActions.Set, \"\")]\n    [InlineData(\"\", \"\", ForwardedTransformActions.Append, \"\")]\n    [InlineData(\"\", \"\", ForwardedTransformActions.Remove, \"\")]\n    [InlineData(\"\", \"/\", ForwardedTransformActions.Set, \"/\")]\n    [InlineData(\"\", \"/\", ForwardedTransformActions.Append, \"/\")]\n    [InlineData(\"\", \"/base\", ForwardedTransformActions.Set, \"/base\")]\n    [InlineData(\"\", \"/base\", ForwardedTransformActions.Append, \"/base\")]\n    [InlineData(\"\", \"/base\", ForwardedTransformActions.Remove, \"\")]\n    [InlineData(\"\", \"/base/value\", ForwardedTransformActions.Set, \"/base/value\")]\n    [InlineData(\"\", \"/base/value\", ForwardedTransformActions.Append, \"/base/value\")]\n    [InlineData(\"\", \"/base本\", ForwardedTransformActions.Set, \"/base%E6%9C%AC\")]\n    [InlineData(\"existing,Header\", \"\", ForwardedTransformActions.Set, \"\")]\n    [InlineData(\"existing;Header\", \"\", ForwardedTransformActions.Set, \"\")]\n    [InlineData(\"existing,Header\", \"\", ForwardedTransformActions.Append, \"existing,Header\")]\n    [InlineData(\"existing;Header\", \"\", ForwardedTransformActions.Append, \"existing;Header\")]\n    [InlineData(\"existing;Header\", \"\", ForwardedTransformActions.Remove, \"\")]\n    [InlineData(\"existing,Header\", \"/base\", ForwardedTransformActions.Set, \"/base\")]\n    [InlineData(\"existing;Header\", \"/base\", ForwardedTransformActions.Set, \"/base\")]\n    [InlineData(\"existing,Header\", \"/base\", ForwardedTransformActions.Append, \"existing,Header;/base\")]\n    [InlineData(\"existing;Header\", \"/base\", ForwardedTransformActions.Append, \"existing;Header;/base\")]\n    [InlineData(\"existing;Header\", \"/base\", ForwardedTransformActions.Remove, \"\")]\n    public async Task PathBase_Added(string startValue, string pathBase, ForwardedTransformActions action, string expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.PathBase = string.IsNullOrEmpty(pathBase) ? new PathString() : new PathString(pathBase);\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"name\", startValue.Split(\";\", StringSplitOptions.RemoveEmptyEntries));\n        var transform = new RequestHeaderXForwardedPrefixTransform(\"name\", action);\n        await transform.ApplyAsync(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true,\n        });\n        if (string.IsNullOrEmpty(expected))\n        {\n            Assert.False(proxyRequest.Headers.TryGetValues(\"name\", out var _));\n        }\n        else\n        {\n            Assert.Equal(expected.Split(\";\", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues(\"name\"));\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/RequestHeaderXForwardedProtoTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class RequestHeaderXForwardedProtoTransformTests\n{\n    [Theory]\n    // Using \";\" to represent multi-line headers\n    [InlineData(\"\", \"http\", ForwardedTransformActions.Set, \"http\")]\n    [InlineData(\"\", \"http\", ForwardedTransformActions.Append, \"http\")]\n    [InlineData(\"\", \"http\", ForwardedTransformActions.Remove, \"\")]\n    [InlineData(\"existing,Header\", \"http\", ForwardedTransformActions.Set, \"http\")]\n    [InlineData(\"existing;Header\", \"http\", ForwardedTransformActions.Set, \"http\")]\n    [InlineData(\"existing,Header\", \"http\", ForwardedTransformActions.Append, \"existing,Header;http\")]\n    [InlineData(\"existing;Header\", \"http\", ForwardedTransformActions.Append, \"existing;Header;http\")]\n    [InlineData(\"existing;Header\", \"http\", ForwardedTransformActions.Remove, \"\")]\n    public async Task Scheme_Added(string startValue, string scheme, ForwardedTransformActions action, string expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Scheme = scheme;\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"name\", startValue.Split(\";\", StringSplitOptions.RemoveEmptyEntries));\n        var transform = new RequestHeaderXForwardedProtoTransform(\"name\", action);\n        await transform.ApplyAsync(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true,\n        });\n        if (string.IsNullOrEmpty(expected))\n        {\n            Assert.False(proxyRequest.Headers.TryGetValues(\"name\", out var _));\n        }\n        else\n        {\n            Assert.Equal(expected.Split(\";\", StringSplitOptions.RemoveEmptyEntries), proxyRequest.Headers.GetValues(\"name\"));\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/RequestHeadersAllowedTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Net.Http.Headers;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class RequestHeadersAllowedTransformTests\n{\n    [Theory]\n    [InlineData(\"\", 0)]\n    [InlineData(\"header1\", 1)]\n    [InlineData(\"header1;header2\", 2)]\n    [InlineData(\"header1;header2;header3\", 3)]\n    [InlineData(\"header1;header2;header2;header3\", 3)]\n    public async Task AllowedHeaders_Copied(string names, int expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage();\n        httpContext.Request.Headers[\"header1\"] = \"value1\";\n        httpContext.Request.Headers[\"header2\"] = \"value2\";\n        httpContext.Request.Headers[\"header3\"] = \"value3\";\n        httpContext.Request.Headers[\"header4\"] = \"value4\";\n        httpContext.Request.Headers[\"header5\"] = \"value5\";\n        httpContext.Request.Headers.ContentLength = 0;\n\n        var allowed = names.Split(';');\n        var transform = new RequestHeadersAllowedTransform(allowed);\n        var transformContext = new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = false,\n        };\n        await transform.ApplyAsync(transformContext);\n\n        Assert.True(transformContext.HeadersCopied);\n\n        Assert.Equal(expected, proxyRequest.Headers.Count());\n        foreach (var header in proxyRequest.Headers)\n        {\n            Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase);\n        }\n    }\n\n    [Theory]\n    [InlineData(\"\", 0)]\n    [InlineData(\"Allow\", 1)]\n    [InlineData(\"content-disposition;header2\", 1)]\n    [InlineData(\"content-length;content-Location;Content-Type\", 3)]\n    [InlineData(\"Allow;Content-Disposition;Content-Encoding;Content-Language;Content-Location;Content-MD5;Content-Range;Content-Type;Expires;Last-Modified;Content-Length\", 11)]\n    public async Task ContentHeaders_CopiedIfAllowed(string names, int expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage();\n        httpContext.Request.Headers[HeaderNames.Allow] = \"value1\";\n        httpContext.Request.Headers[HeaderNames.ContentDisposition] = \"value2\";\n        httpContext.Request.Headers[HeaderNames.ContentEncoding] = \"value3\";\n        httpContext.Request.Headers[HeaderNames.ContentLanguage] = \"value4\";\n        httpContext.Request.Headers[HeaderNames.ContentLocation] = \"value5\";\n        httpContext.Request.Headers[HeaderNames.ContentMD5] = \"value6\";\n        httpContext.Request.Headers[HeaderNames.ContentRange] = \"value7\";\n        httpContext.Request.Headers[HeaderNames.ContentType] = \"value8\";\n        httpContext.Request.Headers[HeaderNames.Expires] = \"value9\";\n        httpContext.Request.Headers[HeaderNames.LastModified] = \"value10\";\n        httpContext.Request.Headers.ContentLength = 0;\n\n        var allowed = names.Split(';');\n        var transform = new RequestHeadersAllowedTransform(allowed);\n        var transformContext = new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = false,\n        };\n        await transform.ApplyAsync(transformContext);\n\n        Assert.True(transformContext.HeadersCopied);\n\n        Assert.Empty(proxyRequest.Headers);\n        var content = proxyRequest.Content;\n        if (expected == 0)\n        {\n            Assert.Null(content);\n            return;\n        }\n\n        Assert.Equal(expected, content.Headers.Count());\n        foreach (var header in content.Headers)\n        {\n            Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase);\n        }\n    }\n\n    [Theory]\n    [InlineData(\"\", 0)]\n    [InlineData(\"connection\", 1)]\n    [InlineData(\"Transfer-Encoding;Keep-Alive\", 2)]\n    // See https://github.com/dotnet/yarp/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/RequestUtilities.cs#L61-L83\n    public async Task RestrictedHeaders_CopiedIfAllowed(string names, int expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage();\n        httpContext.Request.Headers[HeaderNames.Connection] = \"value1\";\n        httpContext.Request.Headers[HeaderNames.TransferEncoding] = \"value2\";\n        httpContext.Request.Headers[HeaderNames.KeepAlive] = \"value3\";\n\n        var allowed = names.Split(';');\n        var transform = new RequestHeadersAllowedTransform(allowed);\n        var transformContext = new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = false,\n        };\n        await transform.ApplyAsync(transformContext);\n\n        Assert.True(transformContext.HeadersCopied);\n\n        Assert.Equal(expected, proxyRequest.Headers.Count());\n        foreach (var header in proxyRequest.Headers)\n        {\n            Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase);\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/RequestHeadersTransformExtensionsTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Linq;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class RequestHeadersTransformExtensionsTests : TransformExtensionsTestsBase\n{\n    private readonly RequestHeadersTransformFactory _factory = new();\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void WithTransformCopyRequestHeaders(bool copy)\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformCopyRequestHeaders(copy);\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        Assert.Equal(copy, builderContext.CopyRequestHeaders);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void WithTransformUseOriginalHostHeader(bool useOriginal)\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformUseOriginalHostHeader(useOriginal);\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        var transform = Assert.Single(builderContext.RequestTransforms);\n        var hostTransform = Assert.IsType<RequestHeaderOriginalHostTransform>(transform);\n\n        Assert.Equal(useOriginal, hostTransform.UseOriginalHost);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void WithTransformRequestHeader(bool append)\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformRequestHeader(\"name\", \"value\", append);\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        ValidateRequestHeader(append, builderContext);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void WithTransformRequestHeaderRouteValue(bool append)\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformRequestHeaderRouteValue(\"key\", \"value\", append);\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        ValidateHeaderRouteParameter(append, builderContext);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void AddRequestHeader(bool append)\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddRequestHeader(\"name\", \"value\", append);\n\n        ValidateRequestHeader(append, builderContext);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public void AddRequestHeaderRouteValue(bool append)\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddRequestHeaderRouteValue(\"key\", \"value\", append);\n\n        ValidateHeaderRouteParameter(append, builderContext);\n    }\n\n    [Fact]\n    public void WithTransformRequestHeaderRemove()\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformRequestHeaderRemove(\"MyHeader\");\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n        var transform = Assert.Single(builderContext.RequestTransforms) as RequestHeaderRemoveTransform;\n        Assert.Equal(\"MyHeader\", transform.HeaderName);\n    }\n\n    [Fact]\n    public void AddRequestHeaderRemove()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddRequestHeaderRemove(\"MyHeader\");\n\n        var transform = Assert.Single(builderContext.RequestTransforms) as RequestHeaderRemoveTransform;\n        Assert.Equal(\"MyHeader\", transform.HeaderName);\n    }\n\n    [Fact]\n    public void WithTransformRequestHeadersAllowed()\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformRequestHeadersAllowed(\"header1\", \"Header2\");\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n        var transform = Assert.Single(builderContext.RequestTransforms) as RequestHeadersAllowedTransform;\n        Assert.Equal(new[] { \"header1\", \"Header2\" }, transform.AllowedHeaders);\n        Assert.False(builderContext.CopyRequestHeaders);\n    }\n\n    [Fact]\n    public void AddRequestHeadersAllowed()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddRequestHeadersAllowed(\"header1\", \"Header2\");\n\n        var transform = Assert.Single(builderContext.RequestTransforms) as RequestHeadersAllowedTransform;\n        Assert.Equal(new[] { \"header1\", \"Header2\" }, transform.AllowedHeaders);\n        Assert.False(builderContext.CopyRequestHeaders);\n    }\n\n    private static void ValidateRequestHeader(bool append, TransformBuilderContext builderContext)\n    {\n        var requestHeaderValueTransform = Assert.Single(builderContext.RequestTransforms.OfType<RequestHeaderValueTransform>(), t => t.HeaderName == \"name\");\n        Assert.Equal(\"value\", requestHeaderValueTransform.Value);\n        Assert.Equal(append, requestHeaderValueTransform.Append);\n    }\n\n    private static void ValidateHeaderRouteParameter(bool append, TransformBuilderContext builderContext)\n    {\n        var requestTransform = Assert.Single(builderContext.RequestTransforms);\n        var requestHeaderFromRouteTransform = Assert.IsType<RequestHeaderRouteValueTransform>(requestTransform);\n        Assert.Equal(\"key\", requestHeaderFromRouteTransform.HeaderName);\n        Assert.Equal(\"value\", requestHeaderFromRouteTransform.RouteValueKey);\n        var expectedMode = append;\n        Assert.Equal(expectedMode, requestHeaderFromRouteTransform.Append);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/RequestTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Linq;\nusing System.Net.Http;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing Microsoft.Net.Http.Headers;\nusing Xunit;\nusing Yarp.ReverseProxy.Forwarder;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class RequestTransformTests\n{\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void TakeHeader_RemovesAndReturnsProxyRequestHeader(bool copiedHeaders)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Headers[\"name\"] = \"value0\";\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Headers.Add(\"Name\", \"value1\");\n        proxyRequest.Content = new StringContent(\"hello world\");\n        proxyRequest.Content.Headers.Add(\"Name\", \"value2\");\n        var result = RequestTransform.TakeHeader(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = copiedHeaders,\n        }, \"name\");\n        Assert.Equal(\"value1\", result);\n        Assert.False(proxyRequest.Headers.TryGetValues(\"name\", out var _));\n        Assert.Equal(new[] { \"value2\" }, proxyRequest.Content.Headers.GetValues(\"name\"));\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void TakeHeaderFromContent_RemovesAndReturnsProxyContentHeader(bool copiedHeaders)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.ContentType = \"value0\";\n        var proxyRequest = new HttpRequestMessage();\n        proxyRequest.Content = new StringContent(\"hello world\");\n        var result = RequestTransform.TakeHeader(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = copiedHeaders,\n        }, HeaderNames.ContentType);\n        Assert.Equal(\"text/plain; charset=utf-8\", result);\n        Assert.False(proxyRequest.Content.Headers.TryGetValues(HeaderNames.ContentType, out var _));\n    }\n\n    [Fact]\n    public void TakeHeader_HeadersNotCopied_ReturnsHttpRequestHeader()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Headers[\"name\"] = \"value0\";\n        var proxyRequest = new HttpRequestMessage();\n        var result = RequestTransform.TakeHeader(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = false,\n        }, \"name\");\n        Assert.Equal(\"value0\", result);\n    }\n\n    [Fact]\n    public void TakeHeader_HeadersCopied_ReturnsNothing()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.Headers[\"name\"] = \"value0\";\n        var proxyRequest = new HttpRequestMessage();\n        var result = RequestTransform.TakeHeader(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest,\n            HeadersCopied = true,\n        }, \"name\");\n        Assert.Equal(StringValues.Empty, result);\n    }\n\n    [Theory]\n    [InlineData(\"header1\", \"header1\", \"\")]\n    [InlineData(\"header1\", \"headerX\", \"header1\")]\n    [InlineData(\"header1; header2; header3\", \"header2\", \"header1; header3\")]\n    [InlineData(\"header1\", \"Content-Encoding\", \"header1\")]\n    [InlineData(\"header1; Content-Encoding\", \"Content-Encoding\", \"header1\")]\n    [InlineData(\"header1; Content-Encoding\", \"header1\", \"Content-Encoding\")]\n    [InlineData(\"header1; Content-Encoding\", \"Content-Type\", \"header1; Content-Encoding\")]\n    [InlineData(\"header1; Content-Encoding\", \"headerX\", \"header1; Content-Encoding\")]\n    [InlineData(\"header1; Content-Encoding; Accept-Encoding\", \"header1\", \"Content-Encoding; Accept-Encoding\")]\n    [InlineData(\"header1; Content-Encoding; Accept-Encoding\", \"Content-Encoding\", \"header1; Accept-Encoding\")]\n    [InlineData(\"header1; Content-Encoding; Accept-Encoding\", \"Accept-Encoding\", \"header1; Content-Encoding\")]\n    [InlineData(\"header1; Content-Encoding; Accept-Encoding\", \"headerX\", \"header1; Content-Encoding; Accept-Encoding\")]\n    public void RemoveHeader_RemovesProxyRequestHeader(string names, string removedHeader, string expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyRequest = new HttpRequestMessage()\n        {\n            Content = new EmptyHttpContent()\n        };\n\n        foreach (var name in names.Split(\"; \"))\n        {\n            httpContext.Request.Headers[name] = \"value0\";\n            RequestUtilities.AddHeader(proxyRequest, name, \"value1\");\n        }\n\n        RequestTransform.RemoveHeader(new RequestTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyRequest = proxyRequest\n        }, removedHeader);\n\n        foreach (var name in names.Split(\"; \"))\n        {\n            Assert.True(httpContext.Request.Headers.TryGetValue(name, out var value));\n            Assert.Equal(\"value0\", value);\n        }\n\n        var expectedHeaders = expected.Split(\"; \", System.StringSplitOptions.RemoveEmptyEntries).OrderBy(h => h);\n        var remainingHeaders = proxyRequest.Headers.Union(proxyRequest.Content.Headers).OrderBy(h => h.Key);\n        Assert.Equal(expectedHeaders, remainingHeaders.Select(h => h.Key));\n        Assert.All(remainingHeaders, h => Assert.Equal(\"value1\", Assert.Single(h.Value)));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/ResponseHeaderRemoveTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class ResponseHeaderRemoveTransformTests\n{\n    [Theory]\n    [InlineData(\"header1\", \"value1\", 200, ResponseCondition.Success, \"header1\", \"\")]\n    [InlineData(\"header1\", \"value1\", 404, ResponseCondition.Success, \"header1\", \"header1\")]\n    [InlineData(\"header1\", \"value1\", 200, ResponseCondition.Failure, \"header1\", \"header1\")]\n    [InlineData(\"header1\", \"value1\", 404, ResponseCondition.Failure, \"header1\", \"\")]\n    [InlineData(\"header1\", \"value1\", 200, ResponseCondition.Always, \"header1\", \"\")]\n    [InlineData(\"header1\", \"value1\", 404, ResponseCondition.Always, \"header1\", \"\")]\n    [InlineData(\"header1\", \"value1\", 200, ResponseCondition.Success, \"headerX\", \"header1\")]\n    [InlineData(\"header1\", \"value1\", 404, ResponseCondition.Success, \"headerX\", \"header1\")]\n    [InlineData(\"header1\", \"value1\", 200, ResponseCondition.Always, \"headerX\", \"header1\")]\n    [InlineData(\"header1\", \"value1\", 404, ResponseCondition.Always, \"headerX\", \"header1\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 200, ResponseCondition.Success, \"header2\", \"header1; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 404, ResponseCondition.Success, \"header2\", \"header1; header2; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 200, ResponseCondition.Always, \"header2\", \"header1; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 404, ResponseCondition.Always, \"header2\", \"header1; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 200, ResponseCondition.Success, \"headerX\", \"header1; header2; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 404, ResponseCondition.Success, \"headerX\", \"header1; header2; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 200, ResponseCondition.Always, \"headerX\", \"header1; header2; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 404, ResponseCondition.Always, \"headerX\", \"header1; header2; header3\")]\n    [InlineData(\"header1; header2; header2; header3\", \"value1, value2-1, value2-2, value3\", 200, ResponseCondition.Success, \"header2\", \"header1; header3\")]\n    [InlineData(\"header1; header2; header2; header3\", \"value1, value2-1, value2-2, value3\", 404, ResponseCondition.Success, \"header2\", \"header1; header2; header3\")]\n    [InlineData(\"header1; header2; header2; header3\", \"value1, value2-1, value2-2, value3\", 200, ResponseCondition.Always, \"header2\", \"header1; header3\")]\n    [InlineData(\"header1; header2; header2; header3\", \"value1, value2-1, value2-2, value3\", 404, ResponseCondition.Always, \"header2\", \"header1; header3\")]\n    public async Task RemoveHeader_Success(string names, string values, int status, ResponseCondition condition, string removedHeader, string expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Response.StatusCode = status;\n        var proxyResponse = new HttpResponseMessage();\n        foreach (var (name, subvalues) in TestResources.ParseNameAndValues(names, values))\n        {\n            httpContext.Response.Headers[name] = subvalues;\n        }\n\n        var transform = new ResponseHeaderRemoveTransform(removedHeader, condition);\n        await transform.ApplyAsync(new ResponseTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = true,\n        });\n\n        var expectedHeaders = expected.Split(\"; \", StringSplitOptions.RemoveEmptyEntries);\n        Assert.Equal(expectedHeaders, httpContext.Response.Headers.Select(h => h.Key));\n    }\n\n    [Theory]\n    [InlineData(ResponseCondition.Always)]\n    [InlineData(ResponseCondition.Success)]\n    [InlineData(ResponseCondition.Failure)]\n    public async Task RemoveHeader_ResponseNull_DoNothing(ResponseCondition condition)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Response.StatusCode = 502;\n\n        var transform = new ResponseHeaderRemoveTransform(\"header1\", condition);\n        await transform.ApplyAsync(new ResponseTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = null,\n            HeadersCopied = false,\n        });\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/ResponseHeaderValueTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class ResponseHeaderValueTransformTests\n{\n    [Theory]\n    // Using \";\" to represent multi-line headers\n    [InlineData(\"\", 400, \"new\", false, ResponseCondition.Success, \"\", false)]\n    [InlineData(\"\", 502, \"new\", false, ResponseCondition.Success, \"\", true)]\n    [InlineData(\"\", 200, \"new\", false, ResponseCondition.Success, \"new\", false)]\n    [InlineData(\"\", 400, \"new\", false, ResponseCondition.Always, \"new\", false)]\n    [InlineData(\"\", 200, \"new\", false, ResponseCondition.Always, \"new\", false)]\n    [InlineData(\"\", 502, \"new\", false, ResponseCondition.Always, \"new\", true)]\n    [InlineData(\"\", 502, \"new\", false, ResponseCondition.Failure, \"new\", false)]\n    [InlineData(\"\", 502, \"new\", false, ResponseCondition.Failure, \"new\", true)]\n    [InlineData(\"\", 200, \"new\", false, ResponseCondition.Failure, \"\", false)]\n    [InlineData(\"start\", 400, \"new\", false, ResponseCondition.Success, \"start\", false)]\n    [InlineData(\"start\", 200, \"new\", false, ResponseCondition.Success, \"new\", false)]\n    [InlineData(\"start\", 502, \"new\", false, ResponseCondition.Success, \"start\", true)]\n    [InlineData(\"start\", 400, \"new\", false, ResponseCondition.Always, \"new\", false)]\n    [InlineData(\"start\", 200, \"new\", false, ResponseCondition.Always, \"new\", false)]\n    [InlineData(\"start\", 400, \"new\", true, ResponseCondition.Success, \"start\", false)]\n    [InlineData(\"start\", 200, \"new\", true, ResponseCondition.Success, \"start;new\", false)]\n    [InlineData(\"start\", 400, \"new\", true, ResponseCondition.Always, \"start;new\", false)]\n    [InlineData(\"start\", 200, \"new\", true, ResponseCondition.Always, \"start;new\", false)]\n    [InlineData(\"start,value\", 400, \"new\", true, ResponseCondition.Success, \"start,value\", false)]\n    [InlineData(\"start,value\", 200, \"new\", true, ResponseCondition.Success, \"start,value;new\", false)]\n    [InlineData(\"start,value\", 400, \"new\", true, ResponseCondition.Always, \"start,value;new\", false)]\n    [InlineData(\"start,value\", 200, \"new\", true, ResponseCondition.Always, \"start,value;new\", false)]\n    [InlineData(\"start;value\", 400, \"new\", true, ResponseCondition.Success, \"start;value\", false)]\n    [InlineData(\"start;value\", 200, \"new\", true, ResponseCondition.Success, \"start;value;new\", false)]\n    [InlineData(\"start;value\", 400, \"new\", true, ResponseCondition.Always, \"start;value;new\", false)]\n    [InlineData(\"start;value\", 200, \"new\", true, ResponseCondition.Always, \"start;value;new\", false)]\n    public async Task AddResponseHeader_Success(string startValue, int status, string value, bool append, ResponseCondition condition, string expected, bool responseNull)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Response.Headers[\"name\"] = startValue.Split(\";\", System.StringSplitOptions.RemoveEmptyEntries);\n        httpContext.Response.StatusCode = status;\n        var transformContext = new ResponseTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = responseNull ? null : new HttpResponseMessage(),\n            HeadersCopied = true,\n        };\n        var transform = new ResponseHeaderValueTransform(\"name\", value, append, condition);\n        await transform.ApplyAsync(transformContext);\n        Assert.Equal(expected.Split(\";\", System.StringSplitOptions.RemoveEmptyEntries), httpContext.Response.Headers[\"name\"]);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/ResponseHeadersAllowedTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Net.Http.Headers;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class ResponseHeadersAllowedTransformTests\n{\n    [Theory]\n    [InlineData(\"\", 0)]\n    [InlineData(\"header1\", 1)]\n    [InlineData(\"header1;header2\", 2)]\n    [InlineData(\"header1;header2;header3\", 3)]\n    [InlineData(\"header1;header2;header2;header3\", 3)]\n    public async Task AllowedHeaders_Copied(string names, int expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyResponse = new HttpResponseMessage();\n        proxyResponse.Headers.TryAddWithoutValidation(\"header1\", \"value1\");\n        proxyResponse.Headers.TryAddWithoutValidation(\"header2\", \"value2\");\n        proxyResponse.Headers.TryAddWithoutValidation(\"header3\", \"value3\");\n        proxyResponse.Headers.TryAddWithoutValidation(\"header4\", \"value4\");\n        proxyResponse.Headers.TryAddWithoutValidation(\"header5\", \"value5\");\n\n        var allowed = names.Split(';');\n        var transform = new ResponseHeadersAllowedTransform(allowed);\n        var transformContext = new ResponseTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = false,\n        };\n        await transform.ApplyAsync(transformContext);\n\n        Assert.True(transformContext.HeadersCopied);\n\n        Assert.Equal(expected, httpContext.Response.Headers.Count());\n        foreach (var header in httpContext.Response.Headers)\n        {\n            Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase);\n        }\n    }\n\n    [Theory]\n    [InlineData(\"\", 0)]\n    [InlineData(\"Allow\", 1)]\n    [InlineData(\"content-disposition;header0\", 2)]\n    [InlineData(\"content-length;content-Location;Content-Type\", 3)]\n    [InlineData(\"Allow;Content-Disposition;Content-Encoding;Content-Language;Content-Location;Content-MD5;Content-Range;Content-Type;Expires;Last-Modified;Content-Length\", 11)]\n    public async Task ContentHeaders_CopiedIfAllowed(string names, int expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyResponse = new HttpResponseMessage();\n        proxyResponse.Content = new StringContent(\"\");\n        proxyResponse.Content.Headers.TryAddWithoutValidation(\"header0\", \"value0\");\n        proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.Allow, \"value1\");\n        proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentDisposition, \"value2\");\n        proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentEncoding, \"value3\");\n        proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentLanguage, \"value4\");\n        proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentLocation, \"value5\");\n        proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentMD5, \"value6\");\n        proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentRange, \"value7\");\n        proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentType, \"value8\");\n        proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.Expires, \"value9\");\n        proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.LastModified, \"value10\");\n        proxyResponse.Content.Headers.TryAddWithoutValidation(HeaderNames.ContentLength, \"0\");\n\n        var allowed = names.Split(';');\n        var transform = new ResponseHeadersAllowedTransform(allowed);\n        var transformContext = new ResponseTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = false,\n        };\n        await transform.ApplyAsync(transformContext);\n\n        Assert.True(transformContext.HeadersCopied);\n\n        Assert.Equal(expected, httpContext.Response.Headers.Count());\n        foreach (var header in httpContext.Response.Headers)\n        {\n            Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase);\n        }\n    }\n\n    [Theory]\n    [InlineData(\"\", 0)]\n    [InlineData(\"connection\", 1)]\n    [InlineData(\"Transfer-Encoding;Keep-Alive\", 2)]\n    // See https://github.com/dotnet/yarp/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/RequestUtilities.cs#L61-L83\n    public async Task RestrictedHeaders_CopiedIfAllowed(string names, int expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyResponse = new HttpResponseMessage();\n        proxyResponse.Headers.TryAddWithoutValidation(HeaderNames.Connection, \"value1\");\n        proxyResponse.Headers.TryAddWithoutValidation(HeaderNames.TransferEncoding, \"value2\");\n        proxyResponse.Headers.TryAddWithoutValidation(HeaderNames.KeepAlive, \"value3\");\n\n        var allowed = names.Split(';');\n        var transform = new ResponseHeadersAllowedTransform(allowed);\n        var transformContext = new ResponseTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = false,\n        };\n        await transform.ApplyAsync(transformContext);\n\n        Assert.True(transformContext.HeadersCopied);\n\n        Assert.Equal(expected, httpContext.Response.Headers.Count());\n        foreach (var header in httpContext.Response.Headers)\n        {\n            Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase);\n        }\n    }\n\n    [Fact]\n    public async Task ProxyResponseNull_DoNothing()\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Response.StatusCode = 502;\n\n        var transform = new ResponseHeadersAllowedTransform(new[] { \"header1\" });\n        var transformContext = new ResponseTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = null,\n            HeadersCopied = false,\n        };\n        await transform.ApplyAsync(transformContext);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/ResponseTrailerRemoveTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Xunit;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class ResponseTrailerRemoveTransformTests\n{\n    [Theory]\n    [InlineData(\"header1\", \"value1\", 200, ResponseCondition.Success, \"header1\", \"\")]\n    [InlineData(\"header1\", \"value1\", 404, ResponseCondition.Success, \"header1\", \"header1\")]\n    [InlineData(\"header1\", \"value1\", 200, ResponseCondition.Failure, \"header1\", \"header1\")]\n    [InlineData(\"header1\", \"value1\", 404, ResponseCondition.Failure, \"header1\", \"\")]\n    [InlineData(\"header1\", \"value1\", 200, ResponseCondition.Always, \"header1\", \"\")]\n    [InlineData(\"header1\", \"value1\", 404, ResponseCondition.Always, \"header1\", \"\")]\n    [InlineData(\"header1\", \"value1\", 200, ResponseCondition.Success, \"headerX\", \"header1\")]\n    [InlineData(\"header1\", \"value1\", 404, ResponseCondition.Success, \"headerX\", \"header1\")]\n    [InlineData(\"header1\", \"value1\", 200, ResponseCondition.Always, \"headerX\", \"header1\")]\n    [InlineData(\"header1\", \"value1\", 404, ResponseCondition.Always, \"headerX\", \"header1\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 200, ResponseCondition.Success, \"header2\", \"header1; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 404, ResponseCondition.Success, \"header2\", \"header1; header2; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 200, ResponseCondition.Always, \"header2\", \"header1; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 404, ResponseCondition.Always, \"header2\", \"header1; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 200, ResponseCondition.Success, \"headerX\", \"header1; header2; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 404, ResponseCondition.Success, \"headerX\", \"header1; header2; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 200, ResponseCondition.Always, \"headerX\", \"header1; header2; header3\")]\n    [InlineData(\"header1; header2; header3\", \"value1, value2, value3\", 404, ResponseCondition.Always, \"headerX\", \"header1; header2; header3\")]\n    [InlineData(\"header1; header2; header2; header3\", \"value1, value2-1, value2-2, value3\", 200, ResponseCondition.Success, \"header2\", \"header1; header3\")]\n    [InlineData(\"header1; header2; header2; header3\", \"value1, value2-1, value2-2, value3\", 404, ResponseCondition.Success, \"header2\", \"header1; header2; header3\")]\n    [InlineData(\"header1; header2; header2; header3\", \"value1, value2-1, value2-2, value3\", 200, ResponseCondition.Always, \"header2\", \"header1; header3\")]\n    [InlineData(\"header1; header2; header2; header3\", \"value1, value2-1, value2-2, value3\", 404, ResponseCondition.Always, \"header2\", \"header1; header3\")]\n    public async Task RemoveTrailerFromFeature_Success(string names, string values, int status, ResponseCondition condition, string removedHeader, string expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Response.StatusCode = status;\n        var trailerFeature = new TestTrailersFeature();\n        httpContext.Features.Set<IHttpResponseTrailersFeature>(trailerFeature);\n        var proxyResponse = new HttpResponseMessage();\n        foreach (var (name, subvalues) in TestResources.ParseNameAndValues(names, values))\n        {\n            trailerFeature.Trailers[name] = subvalues;\n        }\n\n        var transform = new ResponseTrailerRemoveTransform(removedHeader, condition);\n        await transform.ApplyAsync(new ResponseTrailersTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = true,\n        });\n\n        var expectedHeaders = expected.Split(\"; \", StringSplitOptions.RemoveEmptyEntries);\n        Assert.Equal(expectedHeaders, trailerFeature.Trailers.Select(h => h.Key));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/ResponseTrailerValueTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Xunit;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class ResponseTrailerValueTransformTests\n{\n    [Theory]\n    // Using \";\" to represent multi-line headers\n    [InlineData(\"\", 400, \"new\", false, ResponseCondition.Success, \"\")]\n    [InlineData(\"\", 200, \"new\", false, ResponseCondition.Success, \"new\")]\n    [InlineData(\"\", 400, \"new\", false, ResponseCondition.Failure, \"new\")]\n    [InlineData(\"\", 200, \"new\", false, ResponseCondition.Failure, \"\")]\n    [InlineData(\"\", 400, \"new\", false, ResponseCondition.Always, \"new\")]\n    [InlineData(\"\", 200, \"new\", false, ResponseCondition.Always, \"new\")]\n    [InlineData(\"start\", 400, \"new\", false, ResponseCondition.Success, \"start\")]\n    [InlineData(\"start\", 200, \"new\", false, ResponseCondition.Success, \"new\")]\n    [InlineData(\"start\", 400, \"new\", false, ResponseCondition.Always, \"new\")]\n    [InlineData(\"start\", 200, \"new\", false, ResponseCondition.Always, \"new\")]\n    [InlineData(\"start\", 400, \"new\", true, ResponseCondition.Success, \"start\")]\n    [InlineData(\"start\", 200, \"new\", true, ResponseCondition.Success, \"start;new\")]\n    [InlineData(\"start\", 400, \"new\", true, ResponseCondition.Always, \"start;new\")]\n    [InlineData(\"start\", 200, \"new\", true, ResponseCondition.Always, \"start;new\")]\n    [InlineData(\"start,value\", 400, \"new\", true, ResponseCondition.Success, \"start,value\")]\n    [InlineData(\"start,value\", 200, \"new\", true, ResponseCondition.Success, \"start,value;new\")]\n    [InlineData(\"start,value\", 400, \"new\", true, ResponseCondition.Always, \"start,value;new\")]\n    [InlineData(\"start,value\", 200, \"new\", true, ResponseCondition.Always, \"start,value;new\")]\n    [InlineData(\"start;value\", 400, \"new\", true, ResponseCondition.Success, \"start;value\")]\n    [InlineData(\"start;value\", 200, \"new\", true, ResponseCondition.Success, \"start;value;new\")]\n    [InlineData(\"start;value\", 400, \"new\", true, ResponseCondition.Always, \"start;value;new\")]\n    [InlineData(\"start;value\", 200, \"new\", true, ResponseCondition.Always, \"start;value;new\")]\n    public async Task AddResponseTrailer_Success(string startValue, int status, string value, bool append, ResponseCondition condition, string expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        var trailerFeature = new TestTrailersFeature();\n        httpContext.Features.Set<IHttpResponseTrailersFeature>(trailerFeature);\n        trailerFeature.Trailers[\"name\"] = startValue.Split(\";\", System.StringSplitOptions.RemoveEmptyEntries);\n        httpContext.Response.StatusCode = status;\n        var transformContext = new ResponseTrailersTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = new HttpResponseMessage(),\n            HeadersCopied = true,\n        };\n        var transform = new ResponseTrailerValueTransform(\"name\", value, append, condition);\n        await transform.ApplyAsync(transformContext);\n        Assert.Equal(expected.Split(\";\", System.StringSplitOptions.RemoveEmptyEntries), trailerFeature.Trailers[\"name\"]);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/ResponseTrailersAllowedTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.Net.Http.Headers;\nusing Xunit;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class ResponseTrailersAllowedTransformTests\n{\n    [Theory]\n    [InlineData(\"\", 0)]\n    [InlineData(\"header1\", 1)]\n    [InlineData(\"header1;header2\", 2)]\n    [InlineData(\"header1;header2;header3\", 3)]\n    [InlineData(\"header1;header2;header2;header3\", 3)]\n    public async Task AllowedHeaders_Copied(string names, int expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        var trailerFeature = new TestTrailersFeature();\n        httpContext.Features.Set<IHttpResponseTrailersFeature>(trailerFeature);\n        var proxyResponse = new HttpResponseMessage();\n        proxyResponse.TrailingHeaders.TryAddWithoutValidation(\"header1\", \"value1\");\n        proxyResponse.TrailingHeaders.TryAddWithoutValidation(\"header2\", \"value2\");\n        proxyResponse.TrailingHeaders.TryAddWithoutValidation(\"header3\", \"value3\");\n        proxyResponse.TrailingHeaders.TryAddWithoutValidation(\"header4\", \"value4\");\n        proxyResponse.TrailingHeaders.TryAddWithoutValidation(\"header5\", \"value5\");\n\n        var allowed = names.Split(';');\n        var transform = new ResponseTrailersAllowedTransform(allowed);\n        var transformContext = new ResponseTrailersTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = false,\n        };\n        await transform.ApplyAsync(transformContext);\n\n        Assert.True(transformContext.HeadersCopied);\n\n        Assert.Equal(expected, trailerFeature.Trailers.Count());\n        foreach (var header in trailerFeature.Trailers)\n        {\n            Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase);\n        }\n    }\n\n    [Theory]\n    [InlineData(\"\", 0)]\n    [InlineData(\"connection\", 1)]\n    [InlineData(\"Transfer-Encoding;Keep-Alive\", 2)]\n    // See https://github.com/dotnet/yarp/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/RequestUtilities.cs#L61-L83\n    public async Task RestrictedHeaders_CopiedIfAllowed(string names, int expected)\n    {\n        var httpContext = new DefaultHttpContext();\n        var trailerFeature = new TestTrailersFeature();\n        httpContext.Features.Set<IHttpResponseTrailersFeature>(trailerFeature);\n        var proxyResponse = new HttpResponseMessage();\n        proxyResponse.TrailingHeaders.TryAddWithoutValidation(HeaderNames.Connection, \"value1\");\n        proxyResponse.TrailingHeaders.TryAddWithoutValidation(HeaderNames.TransferEncoding, \"value2\");\n        proxyResponse.TrailingHeaders.TryAddWithoutValidation(HeaderNames.KeepAlive, \"value3\");\n\n        var allowed = names.Split(';');\n        var transform = new ResponseTrailersAllowedTransform(allowed);\n        var transformContext = new ResponseTrailersTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = false,\n        };\n        await transform.ApplyAsync(transformContext);\n\n        Assert.True(transformContext.HeadersCopied);\n\n        Assert.Equal(expected, trailerFeature.Trailers.Count());\n        foreach (var header in trailerFeature.Trailers)\n        {\n            Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase);\n        }\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/ResponseTrailersTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.Extensions.Primitives;\nusing Xunit;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class ResponseTrailersTransformTests\n{\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void TakeHeader_RemovesAndReturnsHttpResponseTrailer(bool copiedHeaders)\n    {\n        var httpContext = new DefaultHttpContext();\n        var trailerFeature = new TestTrailersFeature();\n        httpContext.Features.Set<IHttpResponseTrailersFeature>(trailerFeature);\n        trailerFeature.Trailers[\"name\"] = \"value0\";\n        var proxyResponse = new HttpResponseMessage();\n        proxyResponse.TrailingHeaders.Add(\"Name\", \"value1\");\n        var result = ResponseTrailersTransform.TakeHeader(new ResponseTrailersTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = copiedHeaders,\n        }, \"name\");\n        Assert.Equal(\"value0\", result);\n        Assert.False(trailerFeature.Trailers.TryGetValue(\"name\", out var _));\n        Assert.Equal(new[] { \"value1\" }, proxyResponse.TrailingHeaders.GetValues(\"name\"));\n    }\n\n    [Fact]\n    public void TakeHeader_HeadersNotCopied_ReturnsHttpResponseMessageTrailer()\n    {\n        var httpContext = new DefaultHttpContext();\n        var trailerFeature = new TestTrailersFeature();\n        httpContext.Features.Set<IHttpResponseTrailersFeature>(trailerFeature);\n        var proxyResponse = new HttpResponseMessage();\n        proxyResponse.TrailingHeaders.Add(\"Name\", \"value1\");\n        var result = ResponseTrailersTransform.TakeHeader(new ResponseTrailersTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = false,\n        }, \"name\");\n        Assert.Equal(\"value1\", result);\n        Assert.False(trailerFeature.Trailers.TryGetValue(\"name\", out var _));\n        Assert.Equal(new[] { \"value1\" }, proxyResponse.TrailingHeaders.GetValues(\"name\"));\n    }\n\n    [Fact]\n    public void TakeHeader_HeadersCopied_ReturnsNothing()\n    {\n        var httpContext = new DefaultHttpContext();\n        var trailerFeature = new TestTrailersFeature();\n        httpContext.Features.Set<IHttpResponseTrailersFeature>(trailerFeature);\n        var proxyResponse = new HttpResponseMessage();\n        proxyResponse.TrailingHeaders.Add(\"Name\", \"value1\");\n        var result = ResponseTrailersTransform.TakeHeader(new ResponseTrailersTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = true,\n        }, \"name\");\n        Assert.Equal(StringValues.Empty, result);\n        Assert.False(trailerFeature.Trailers.TryGetValue(\"name\", out var _));\n        Assert.Equal(new[] { \"value1\" }, proxyResponse.TrailingHeaders.GetValues(\"name\"));\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/ResponseTransformExtensionsTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class ResponseTransformExtensionsTests : TransformExtensionsTestsBase\n{\n    private readonly ResponseTransformFactory _factory = new();\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void WithTransformCopyResponseHeaders(bool copy)\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformCopyResponseHeaders(copy);\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        Assert.Equal(copy, builderContext.CopyResponseHeaders);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void WithTransformCopyResponseTrailers(bool copy)\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformCopyResponseTrailers(copy);\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        Assert.Equal(copy, builderContext.CopyResponseTrailers);\n    }\n\n    [Theory]\n    [InlineData(false, ResponseCondition.Success)]\n    [InlineData(false, ResponseCondition.Always)]\n    [InlineData(false, ResponseCondition.Failure)]\n    [InlineData(true, ResponseCondition.Success)]\n    [InlineData(true, ResponseCondition.Always)]\n    [InlineData(true, ResponseCondition.Failure)]\n    public void WithTransformResponseHeader(bool append, ResponseCondition condition)\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformResponseHeader(\"name\", \"value\", append, condition);\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        ValidateResponseHeader(builderContext, append, condition);\n    }\n\n    [Theory]\n    [InlineData(false, ResponseCondition.Success)]\n    [InlineData(false, ResponseCondition.Always)]\n    [InlineData(false, ResponseCondition.Failure)]\n    [InlineData(true, ResponseCondition.Success)]\n    [InlineData(true, ResponseCondition.Always)]\n    [InlineData(true, ResponseCondition.Failure)]\n    public void AddResponseHeader(bool append, ResponseCondition condition)\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddResponseHeader(\"name\", \"value\", append, condition);\n\n        ValidateResponseHeader(builderContext, append, condition);\n    }\n\n    private static void ValidateResponseHeader(TransformBuilderContext builderContext, bool append, ResponseCondition condition)\n    {\n        var responseTransform = Assert.Single(builderContext.ResponseTransforms);\n        var responseHeaderValueTransform = Assert.IsType<ResponseHeaderValueTransform>(responseTransform);\n        Assert.Equal(\"name\", responseHeaderValueTransform.HeaderName);\n        Assert.Equal(\"value\", responseHeaderValueTransform.Value);\n        Assert.Equal(append, responseHeaderValueTransform.Append);\n        Assert.Equal(condition, responseHeaderValueTransform.Condition);\n    }\n\n    [Fact]\n    public void WithTransformResponseHeaderRemove()\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformResponseHeaderRemove(\"MyHeader\");\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n        var transform = Assert.Single(builderContext.ResponseTransforms) as ResponseHeaderRemoveTransform;\n        Assert.Equal(\"MyHeader\", transform.HeaderName);\n    }\n\n    [Fact]\n    public void AddResponseHeaderRemove()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddResponseHeaderRemove(\"MyHeader\");\n\n        var transform = Assert.Single(builderContext.ResponseTransforms) as ResponseHeaderRemoveTransform;\n        Assert.Equal(\"MyHeader\", transform.HeaderName);\n    }\n\n    [Fact]\n    public void WithTransformResponseHeadersAllowed()\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformResponseHeadersAllowed(\"header1\", \"Header2\");\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n        var transform = Assert.Single(builderContext.ResponseTransforms) as ResponseHeadersAllowedTransform;\n        Assert.Equal(new[] { \"header1\", \"Header2\" }, transform.AllowedHeaders);\n        Assert.False(builderContext.CopyResponseHeaders);\n    }\n\n    [Fact]\n    public void AddResponseHeadersAllowed()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddResponseHeadersAllowed(\"header1\", \"Header2\");\n\n        var transform = Assert.Single(builderContext.ResponseTransforms) as ResponseHeadersAllowedTransform;\n        Assert.Equal(new[] { \"header1\", \"Header2\" }, transform.AllowedHeaders);\n        Assert.False(builderContext.CopyResponseHeaders);\n    }\n\n    [Theory]\n    [InlineData(false, ResponseCondition.Success)]\n    [InlineData(false, ResponseCondition.Always)]\n    [InlineData(false, ResponseCondition.Failure)]\n    [InlineData(true, ResponseCondition.Success)]\n    [InlineData(true, ResponseCondition.Always)]\n    [InlineData(true, ResponseCondition.Failure)]\n    public void WithTransformResponseTrailer(bool append, ResponseCondition condition)\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformResponseTrailer(\"name\", \"value\", append, condition);\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n\n        ValidateResponseTrailer(builderContext, append, condition);\n    }\n\n    [Theory]\n    [InlineData(false, ResponseCondition.Success)]\n    [InlineData(false, ResponseCondition.Always)]\n    [InlineData(false, ResponseCondition.Failure)]\n    [InlineData(true, ResponseCondition.Success)]\n    [InlineData(true, ResponseCondition.Always)]\n    [InlineData(true, ResponseCondition.Failure)]\n    public void AddResponseTrailer(bool append, ResponseCondition condition)\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddResponseTrailer(\"name\", \"value\", append, condition);\n\n        ValidateResponseTrailer(builderContext, append, condition);\n    }\n\n    private static void ValidateResponseTrailer(TransformBuilderContext builderContext, bool append, ResponseCondition condition)\n    {\n        var responseTransform = Assert.Single(builderContext.ResponseTrailersTransforms);\n        var responseHeaderValueTransform = Assert.IsType<ResponseTrailerValueTransform>(responseTransform);\n        Assert.Equal(\"name\", responseHeaderValueTransform.HeaderName);\n        Assert.Equal(\"value\", responseHeaderValueTransform.Value);\n        Assert.Equal(append, responseHeaderValueTransform.Append);\n        Assert.Equal(condition, responseHeaderValueTransform.Condition);\n    }\n\n    [Fact]\n    public void WithTransformResponseTrailerRemove()\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformResponseTrailerRemove(\"MyHeader\");\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n        var transform = Assert.Single(builderContext.ResponseTrailersTransforms) as ResponseTrailerRemoveTransform;\n        Assert.Equal(\"MyHeader\", transform.HeaderName);\n    }\n\n    [Fact]\n    public void AddResponseTrailerRemove()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddResponseTrailerRemove(\"MyHeader\");\n\n        var transform = Assert.Single(builderContext.ResponseTrailersTransforms) as ResponseTrailerRemoveTransform;\n        Assert.Equal(\"MyHeader\", transform.HeaderName);\n    }\n\n    [Fact]\n    public void WithTransformResponseTrailersAllowed()\n    {\n        var routeConfig = new RouteConfig();\n        routeConfig = routeConfig.WithTransformResponseTrailersAllowed(\"header1\", \"Header2\");\n\n        var builderContext = ValidateAndBuild(routeConfig, _factory);\n        var transform = Assert.Single(builderContext.ResponseTrailersTransforms) as ResponseTrailersAllowedTransform;\n        Assert.Equal(new[] { \"header1\", \"Header2\" }, transform.AllowedHeaders);\n        Assert.False(builderContext.CopyResponseTrailers);\n    }\n\n    [Fact]\n    public void AddResponseTrailersAllowed()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddResponseTrailersAllowed(\"header1\", \"Header2\");\n\n        var transform = Assert.Single(builderContext.ResponseTrailersTransforms) as ResponseTrailersAllowedTransform;\n        Assert.Equal(new[] { \"header1\", \"Header2\" }, transform.AllowedHeaders);\n        Assert.False(builderContext.CopyResponseTrailers);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/ResponseTransformTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Net.Http;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class ResponseTransformTests\n{\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public void TakeHeader_RemovesAndReturnsHttpResponseHeader(bool copiedHeaders)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Response.Headers[\"name\"] = \"value0\";\n        var proxyResponse = new HttpResponseMessage();\n        proxyResponse.Headers.Add(\"Name\", \"value1\");\n        proxyResponse.Content = new StringContent(\"hello world\");\n        proxyResponse.Content.Headers.Add(\"Name\", \"value2\");\n        var result = ResponseTransform.TakeHeader(new ResponseTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = copiedHeaders,\n        }, \"name\");\n        Assert.Equal(\"value0\", result);\n        Assert.False(httpContext.Response.Headers.TryGetValue(\"name\", out var _));\n        Assert.Equal(new[] { \"value1\" }, proxyResponse.Headers.GetValues(\"name\"));\n        Assert.Equal(new[] { \"value2\" }, proxyResponse.Content.Headers.GetValues(\"name\"));\n    }\n\n    [Fact]\n    public void TakeHeader_HeadersNotCopied_ReturnsHttpResponseMessageHeader()\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyResponse = new HttpResponseMessage();\n        proxyResponse.Headers.Add(\"Name\", \"value1\");\n        proxyResponse.Content = new StringContent(\"hello world\");\n        proxyResponse.Content.Headers.Add(\"Name\", \"value2\");\n        var result = ResponseTransform.TakeHeader(new ResponseTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = false,\n        }, \"name\");\n        Assert.Equal(\"value1\", result);\n    }\n\n    [Fact]\n    public void TakeHeader_HeadersNotCopied_ReturnsHttpContentHeader()\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyResponse = new HttpResponseMessage();\n        proxyResponse.Content = new StringContent(\"hello world\");\n        proxyResponse.Content.Headers.Add(\"Name\", \"value2\");\n        var result = ResponseTransform.TakeHeader(new ResponseTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = false,\n        }, \"name\");\n        Assert.Equal(\"value2\", result);\n    }\n\n    [Fact]\n    public void TakeHeader_HeadersCopied_ReturnsNothing()\n    {\n        var httpContext = new DefaultHttpContext();\n        var proxyResponse = new HttpResponseMessage();\n        proxyResponse.Headers.Add(\"Name\", \"value1\");\n        proxyResponse.Content = new StringContent(\"hello world\");\n        proxyResponse.Content.Headers.Add(\"Name\", \"value2\");\n        var result = ResponseTransform.TakeHeader(new ResponseTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = proxyResponse,\n            HeadersCopied = true,\n        }, \"name\");\n        Assert.Equal(StringValues.Empty, result);\n    }\n\n    [Fact]\n    public void TakeHeader_ResponseNull_ReturnsNothing()\n    {\n        var httpContext = new DefaultHttpContext();\n        var result = ResponseTransform.TakeHeader(new ResponseTransformContext()\n        {\n            HttpContext = httpContext,\n            ProxyResponse = null,\n            HeadersCopied = false,\n        }, \"name\");\n        Assert.Equal(StringValues.Empty, result);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/TransformBuilderContextFuncExtensionsTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic class TransformBuilderContextFuncExtensionsTests : TransformExtensionsTestsBase\n{\n    [Fact]\n    public void AddRequestTransform()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddRequestTransform(context =>\n        {\n            return default;\n        });\n\n        var requestTransform = Assert.Single(builderContext.RequestTransforms);\n        Assert.IsType<RequestFuncTransform>(requestTransform);\n    }\n\n    [Fact]\n    public void AddResponseTransform()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddResponseTransform(context =>\n        {\n            return default;\n        });\n\n        var responseTransform = Assert.Single(builderContext.ResponseTransforms);\n        Assert.IsType<ResponseFuncTransform>(responseTransform);\n    }\n\n    [Fact]\n    public void AddResponseTrailersTransform()\n    {\n        var builderContext = CreateBuilderContext();\n        builderContext.AddResponseTrailersTransform(context =>\n        {\n            return default;\n        });\n\n        var responseTrailersTransform = Assert.Single(builderContext.ResponseTrailersTransforms);\n        Assert.IsType<ResponseTrailersFuncTransform>(responseTrailersTransform);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Transforms/TransformExtensionsTestsBase.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Xunit;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Transforms.Tests;\n\npublic abstract class TransformExtensionsTestsBase\n{\n    protected static TransformBuilderContext CreateBuilderContext(IServiceProvider services = null) => new()\n    {\n        Route = new RouteConfig(),\n        Services = services,\n    };\n\n    protected static TransformBuilderContext ValidateAndBuild(RouteConfig routeConfig, ITransformFactory factory, IServiceProvider serviceProvider = null)\n    {\n        var transformValues = Assert.Single(routeConfig.Transforms);\n\n        var validationContext = new TransformRouteValidationContext { Route = routeConfig };\n        Assert.True(factory.Validate(validationContext, transformValues));\n        Assert.Empty(validationContext.Errors);\n\n        var builderContext = CreateBuilderContext(serviceProvider);\n        Assert.True(factory.Build(builderContext, transformValues));\n\n        return builderContext;\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Utilities/ActivityCancellationTokenSourceTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Utilities.Tests;\n\npublic class ActivityCancellationTokenSourceTests\n{\n    [Fact]\n    public void ActivityCancellationTokenSource_PoolsSources()\n    {\n        HashSet<ActivityCancellationTokenSource> sources = [];\n\n        for (var i = 0; i < 1_000; i++)\n        {\n            var source = ActivityCancellationTokenSource.Rent(TimeSpan.FromMinutes(10), CancellationToken.None);\n            source.Return();\n            sources.Add(source);\n        }\n\n        Assert.True(sources.Count < 1000);\n    }\n\n    [Fact]\n    public void ActivityCancellationTokenSource_DoesNotPoolsCanceledSources()\n    {\n        var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);\n        cts.Cancel();\n\n        var cts2 = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);\n\n        Assert.NotSame(cts, cts2);\n    }\n\n    [Fact]\n    public void ActivityCancellationTokenSource_RespectsLinkedToken1()\n    {\n        var linkedCts = new CancellationTokenSource();\n\n        var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), linkedCts.Token);\n        linkedCts.Cancel();\n\n        Assert.True(cts.CancelledByLinkedToken);\n        Assert.True(cts.IsCancellationRequested);\n    }\n\n    [Fact]\n    public void ActivityCancellationTokenSource_RespectsLinkedToken2()\n    {\n        var linkedCts = new CancellationTokenSource();\n\n        var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), default, linkedCts.Token);\n        linkedCts.Cancel();\n\n        Assert.True(cts.CancelledByLinkedToken);\n        Assert.True(cts.IsCancellationRequested);\n    }\n\n    [Fact]\n    public void ActivityCancellationTokenSource_RespectsBothLinkedTokens()\n    {\n        var linkedCts1 = new CancellationTokenSource();\n        var linkedCts2 = new CancellationTokenSource();\n\n        var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), linkedCts1.Token, linkedCts2.Token);\n        linkedCts1.Cancel();\n        linkedCts2.Cancel();\n\n        Assert.True(cts.CancelledByLinkedToken);\n        Assert.True(cts.IsCancellationRequested);\n    }\n\n    [Fact]\n    public void ActivityCancellationTokenSource_ClearsRegistrations()\n    {\n        var linkedCts1 = new CancellationTokenSource();\n        var linkedCts2 = new CancellationTokenSource();\n\n        var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), linkedCts1.Token, linkedCts2.Token);\n        cts.Return();\n\n        linkedCts1.Cancel();\n        linkedCts2.Cancel();\n\n        Assert.False(cts.IsCancellationRequested);\n    }\n\n    [Fact]\n    public async Task ActivityCancellationTokenSource_RespectsTimeout()\n    {\n        var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromMilliseconds(1), CancellationToken.None);\n\n        for (var i = 0; i < 1000; i++)\n        {\n            if (cts.IsCancellationRequested)\n            {\n                Assert.False(cts.CancelledByLinkedToken);\n                return;\n            }\n\n            await Task.Delay(1);\n        }\n\n        Assert.Fail(\"Cts was not canceled\");\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Utilities/AtomicCounterTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Utilities.Tests;\n\npublic class AtomicCounterTests\n{\n    [Fact]\n    public void Constructor_Works()\n    {\n        new AtomicCounter();\n    }\n\n    [Fact]\n    public void Increment_ThreadSafety()\n    {\n        const int Iterations = 100_000;\n\n        var counter = new AtomicCounter();\n\n        Parallel.For(0, Iterations, i =>\n        {\n            counter.Increment();\n        });\n\n        Assert.Equal(Iterations, counter.Value);\n    }\n\n    [Fact]\n    public void Decrement_ThreadSafety()\n    {\n        const int Iterations = 100_000;\n\n        var counter = new AtomicCounter();\n\n        Parallel.For(0, Iterations, i =>\n        {\n            counter.Decrement();\n        });\n\n        Assert.Equal(-Iterations, counter.Value);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Utilities/CaseInsensitiveEqualHelperTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Utilities.Tests;\n\npublic class CaseInsensitiveEqualHelperTests\n{\n    [Fact]\n    public void Equals_Same_Instance_Returns_True()\n    {\n        var list1 = new string[] { \"item1\", \"item2\" };\n\n        var equals = CaseInsensitiveEqualHelper.Equals(list1, list1);\n\n        Assert.True(equals);\n    }\n\n    [Fact]\n    public void Equals_Empty_List_Returns_True()\n    {\n        var list1 = System.Array.Empty<string>();\n\n        var list2 = System.Array.Empty<string>();\n\n        var equals = CaseInsensitiveEqualHelper.Equals(list1, list2);\n\n        Assert.True(equals);\n    }\n\n    [Fact]\n    public void Equals_List_Same_Value_Returns_True()\n    {\n        var list1 = new string[] { \"item1\", \"item2\" };\n\n        var list2 = new string[] { \"item1\", \"item2\" };\n\n        var equals = CaseInsensitiveEqualHelper.Equals(list1, list2);\n\n        Assert.True(equals);\n    }\n\n    [Fact]\n    public void Equals_List_Different_Value_Returns_False()\n    {\n        var list1 = new string[] { \"item1\", \"item2\" };\n\n        var list2 = new string[] { \"item3\", \"item4\" };\n\n        var equals = CaseInsensitiveEqualHelper.Equals(list1, list2);\n\n        Assert.False(equals);\n    }\n\n    [Fact]\n    public void Equals_First_List_Null_Returns_False()\n    {\n        var list2 = new string[] { \"item1\", \"item2\" };\n\n        var equals = CaseInsensitiveEqualHelper.Equals(null, list2);\n\n        Assert.False(equals);\n    }\n\n    [Fact]\n    public void Equals_Second_List_Null_Returns_False()\n    {\n        var list1 = new string[] { \"item1\", \"item2\" };\n\n        var equals = CaseInsensitiveEqualHelper.Equals(list1, null);\n\n        Assert.False(equals);\n    }\n\n    [Fact]\n    public void Equals_Null_List_Returns_True()\n    {\n        var equals = CaseInsensitiveEqualHelper.Equals(list1: null, list2: null);\n\n        Assert.True(equals);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Utilities/RandomFactoryTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Utilities.Tests;\n\npublic class RandomFactoryTests\n{\n    [Fact]\n    public void RandomFactory_Work()\n    {\n        // Set up the factory.\n        var factory = new RandomFactory();\n\n        // Create random class object.\n        var random = factory.CreateRandomInstance();\n\n        // Validate.\n        Assert.NotNull(random);\n\n        // Validate functionality\n        var num = random.Next(5);\n        Assert.InRange(num, 0, 5);\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Utilities/TlsFrameHelperTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n// See the LICENSE file in the project root for more information.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Security;\nusing System.Security.Authentication;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Utilities.Tls.Tests;\n\npublic class TlsFrameHelperTests\n{\n    [Fact]\n    public void SniHelper_ValidData_Ok()\n    {\n        InvalidClientHello(s_validClientHello, -1, shouldPass: true);\n    }\n\n    [Theory]\n    [MemberData(nameof(InvalidClientHelloData))]\n    public void SniHelper_InvalidData_Fails(int id, byte[] clientHello)\n    {\n        InvalidClientHello(clientHello, id, shouldPass: false);\n    }\n\n    [Fact]\n    public void SniHelper_TruncatedData_Fails()\n    {\n        // moving inside one test because there are more than 3000 cases and they overflow subresults\n        foreach ((int id, byte[] clientHello) in InvalidClientHelloDataTruncatedBytes())\n        {\n            InvalidClientHello(clientHello, id, shouldPass: false);\n        }\n    }\n\n    private void InvalidClientHello(byte[] clientHello, int id, bool shouldPass)\n    {\n        var ret = TlsFrameHelper.GetServerName(clientHello);\n        if (shouldPass)\n        {\n            Assert.NotNull(ret);\n        }\n        else\n        {\n            Assert.Null(ret);\n        }\n    }\n\n    [Fact]\n    public void TlsFrameHelper_ValidData_Ok()\n    {\n        TlsFrameHelper.TlsFrameInfo info = default;\n        Assert.True(TlsFrameHelper.TryGetFrameInfo(s_validClientHello, ref info));\n\n        Assert.Equal(SslProtocols.Tls12, info.Header.Version);\n        Assert.Equal(203, info.Header.Length);\n        Assert.Equal(SslProtocols.Tls12, info.SupportedVersions);\n        Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.None, info.ApplicationProtocols);\n    }\n\n    [Fact]\n    public void TlsFrameHelper_Tls12ClientHello_Ok()\n    {\n        TlsFrameHelper.TlsFrameInfo info = default;\n        Assert.True(TlsFrameHelper.TryGetFrameInfo(s_Tls12ClientHello, ref info));\n\n        Assert.Equal(SslProtocols.Tls, info.Header.Version);\n        Assert.Equal(SslProtocols.Tls | SslProtocols.Tls12, info.SupportedVersions);\n        Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.Http11 | TlsFrameHelper.ApplicationProtocolInfo.Http2, info.ApplicationProtocols);\n\n        Assert.Equal(46, info.TlsCipherSuites.Length);\n        int expectedCiphersCount = 0;\n        for (int i = 0; i < info.TlsCipherSuites.Length; i++)\n        {\n            // spotcheck on ciphers\n            if (info.TlsCipherSuites.Span[i] == TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 ||\n                info.TlsCipherSuites.Span[i] == TlsCipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256 ||\n                info.TlsCipherSuites.Span[i] == TlsCipherSuite.TLS_RSA_WITH_RC4_128_SHA)\n            {\n                expectedCiphersCount++;\n            }\n        }\n        Assert.Equal(3, expectedCiphersCount);\n    }\n\n    [Fact]\n    public void TlsFrameHelper_Tls13ClientHello_Ok()\n    {\n        TlsFrameHelper.TlsFrameInfo info = default;\n        Assert.True(TlsFrameHelper.TryGetFrameInfo(s_Tls13ClientHello, ref info));\n\n        Assert.Equal(SslProtocols.Tls, info.Header.Version);\n        Assert.Equal(SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13, info.SupportedVersions);\n        Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.Other, info.ApplicationProtocols);\n\n        Assert.Equal(6, info.TlsCipherSuites.Length);\n        int expectedCiphersCount = 0;\n        for (int i = 0; i < info.TlsCipherSuites.Length; i++)\n        {\n            if (info.TlsCipherSuites.Span[i] == TlsCipherSuite.TLS_AES_256_GCM_SHA384 ||\n                info.TlsCipherSuites.Span[i] == TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256 ||\n                info.TlsCipherSuites.Span[i] == TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384)\n            {\n                expectedCiphersCount++;\n            }\n        }\n        Assert.Equal(3, expectedCiphersCount);\n    }\n\n    [Fact]\n    public void TlsFrameHelper_UnifiedClientHello_Ok()\n    {\n        TlsFrameHelper.TlsFrameInfo info = default;\n        Assert.True(TlsFrameHelper.TryGetFrameInfo(s_UnifiedHello, ref info));\n#pragma warning disable CS0618 // Ssl2 and Ssl3 are obsolete\n        Assert.Equal(SslProtocols.Ssl2, info.Header.Version);\n        Assert.Equal(SslProtocols.Ssl2 | SslProtocols.Tls, info.SupportedVersions);\n#pragma warning restore CS0618\n        Assert.Equal(TlsContentType.Handshake, info.Header.Type);\n        Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.None, info.ApplicationProtocols);\n        Assert.Equal(TlsHandshakeType.ClientHello, info.HandshakeType);\n    }\n\n    [Fact]\n    public void TlsFrameHelper_TlsClientHelloNoExtensions_Ok()\n    {\n        TlsFrameHelper.TlsFrameInfo info = default;\n        Assert.True(TlsFrameHelper.TryGetFrameInfo(s_TlsClientHelloNoExtensions, ref info));\n        Assert.Equal(TlsFrameHelper.ParsingStatus.Ok, info.ParsingStatus);\n        Assert.Equal(SslProtocols.Tls12, info.Header.Version);\n        Assert.Equal(SslProtocols.Tls12, info.SupportedVersions);\n        Assert.Equal(TlsContentType.Handshake, info.Header.Type);\n        Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.None, info.ApplicationProtocols);\n        Assert.Equal(TlsHandshakeType.ClientHello, info.HandshakeType);\n    }\n\n    [Fact]\n    public void TlsFrameHelper_Tls12ServerHello_Ok()\n    {\n        TlsFrameHelper.TlsFrameInfo info = default;\n        Assert.True(TlsFrameHelper.TryGetFrameInfo(s_Tls12ServerHello, ref info));\n        Assert.Equal(TlsFrameHelper.ParsingStatus.Ok, info.ParsingStatus);\n        Assert.Equal(SslProtocols.Tls12, info.Header.Version);\n        Assert.Equal(SslProtocols.Tls12, info.SupportedVersions);\n        Assert.Equal(TlsFrameHelper.ApplicationProtocolInfo.Http2, info.ApplicationProtocols);\n    }\n\n    [Fact]\n    public void TlsFrameHelper_FragmentedClientHello_Fails()\n    {\n        TlsFrameHelper.TlsFrameInfo info = default;\n        Assert.False(TlsFrameHelper.TryGetFrameInfo(s_Tls13FragmentedClientHello, ref info));\n        Assert.Equal(TlsFrameHelper.ParsingStatus.InvalidFrame, info.ParsingStatus);\n    }\n\n    public static IEnumerable<object[]> InvalidClientHelloData()\n    {\n        int id = 0;\n        foreach (byte[] invalidClientHello in InvalidClientHello())\n        {\n            id++;\n            yield return new object[] { id, invalidClientHello };\n        }\n    }\n\n    public static IEnumerable<Tuple<int, byte[]>> InvalidClientHelloDataTruncatedBytes()\n    {\n        // converting to base64 first to remove duplicated test cases\n        var uniqueInvalidHellos = new HashSet<string>();\n        foreach (byte[] invalidClientHello in InvalidClientHello())\n        {\n            for (int i = 0; i < invalidClientHello.Length; i++)\n            {\n                uniqueInvalidHellos.Add(Convert.ToBase64String(invalidClientHello.Take(i).ToArray()));\n            }\n        }\n\n        for (int i = 0; i < s_validClientHello.Length; i++)\n        {\n            uniqueInvalidHellos.Add(Convert.ToBase64String(s_validClientHello.Take(i).ToArray()));\n        }\n\n        int id = 0;\n        foreach (string invalidClientHello in uniqueInvalidHellos)\n        {\n            id++;\n            yield return new Tuple<int, byte[]>(id, Convert.FromBase64String(invalidClientHello));\n        }\n    }\n\n    private static readonly byte[] s_validClientHello = new byte[] {\n        // SslPlainText.(ContentType+ProtocolVersion)\n        0x16, 0x03, 0x03,\n        // SslPlainText.length\n        0x00, 0xCB,\n        // Handshake.msg_type (client hello)\n        0x01,\n        // Handshake.length\n        0x00, 0x00, 0xC7,\n        // ClientHello.client_version\n        0x03, 0x03,\n        // ClientHello.random\n        0x0C, 0x3C, 0x85, 0x78, 0xCA,\n        0x67, 0x70, 0xAA, 0x38, 0xCB,\n        0x28, 0xBC, 0xDC, 0x3E, 0x30,\n        0xBF, 0x11, 0x96, 0x95, 0x1A,\n        0xB9, 0xF0, 0x99, 0xA4, 0x91,\n        0x09, 0x13, 0xB4, 0x89, 0x94,\n        0x27, 0x2E,\n        // ClientHello.SessionId\n        0x00,\n        // ClientHello.cipher_suites\n        0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n        0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n        0x00, 0x9F, 0x00, 0x9E, 0xC0,\n        0x24, 0xC0, 0x23, 0xC0, 0x28,\n        0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n        0x09, 0xC0, 0x14, 0xC0, 0x13,\n        0x00, 0x9D, 0x00, 0x9C, 0x00,\n        0x3D, 0x00, 0x3C, 0x00, 0x35,\n        0x00, 0x2F, 0x00, 0x0A,\n        // ClientHello.compression_methods\n        0x01, 0x01,\n        // ClientHello.extension_list_length\n        0x00, 0x74,\n        // Extension.extension_type (server_name)\n        0x00, 0x00,\n        // ServerNameListExtension.length\n        0x00, 0x39,\n        // ServerName.length\n        0x00, 0x37,\n        // ServerName.type\n        0x00,\n        // HostName.length\n        0x00, 0x34,\n        // HostName.bytes\n        0x61, 0x61, 0x61, 0x61, 0x61,\n        0x61, 0x61, 0x61, 0x61, 0x61,\n        0x61, 0x61, 0x61, 0x61, 0x61,\n        0x61, 0x61, 0x61, 0x61, 0x61,\n        0x61, 0x61, 0x61, 0x61, 0x61,\n        0x61, 0x61, 0x61, 0x61, 0x61,\n        0x61, 0x61, 0x61, 0x61, 0x61,\n        0x61, 0x61, 0x61, 0x61, 0x61,\n        0x61, 0x61, 0x61, 0x61, 0x61,\n        0x61, 0x61, 0x61, 0x61, 0x61,\n        0x61, 0x61,\n        // Extension.extension_type (00 0A)\n        0x00, 0x0A,\n        // Extension 0A\n        0x00, 0x08, 0x00, 0x06, 0x00,\n        0x1D, 0x00, 0x17, 0x00, 0x18,\n        // Extension.extension_type (00 0B)\n        0x00, 0x0B,\n        // Extension 0B\n        0x00, 0x02, 0x01, 0x00,\n        // Extension.extension_type (00 0D)\n        0x00, 0x0D,\n        // Extension 0D\n        0x00, 0x14, 0x00, 0x12, 0x04,\n        0x01, 0x05, 0x01, 0x02, 0x01,\n        0x04, 0x03, 0x05, 0x03, 0x02,\n        0x03, 0x02, 0x02, 0x06, 0x01,\n        0x06, 0x03,\n        // Extension.extension_type (00 23)\n        0x00, 0x23,\n        // Extension 00 23\n        0x00, 0x00,\n        // Extension.extension_type (00 17)\n        0x00, 0x17,\n        // Extension 17\n        0x00, 0x00,\n        // Extension.extension_type (FF 01)\n        0xFF, 0x01,\n        // Extension FF01\n        0x00, 0x01, 0x00\n    };\n\n    private static readonly byte[] s_Tls12ClientHello = new byte[] {\n        // SslPlainText.(ContentType+ProtocolVersion)\n        0x16, 0x03, 0x01,\n        // SslPlainText.length\n        0x00, 0xD1,\n        // Handshake.msg_type (client hello)\n        0x01,\n        // Handshake.length\n        0x00, 0x00, 0xCD,\n        // ClientHello.client_version\n        0x03, 0x03,\n        // ClientHello.random\n        0x0C, 0x3C, 0x85, 0x78, 0xCA,\n        0x67, 0x70, 0xAA, 0x38, 0xCB,\n        0x28, 0xBC, 0xDC, 0x3E, 0x30,\n        0xBF, 0x11, 0x96, 0x95, 0x1A,\n        0xB9, 0xF0, 0x99, 0xA4, 0x91,\n        0x09, 0x13, 0xB4, 0x89, 0x94,\n        0x27, 0x2E,\n        // ClientHello.SessionId\n        0x00,\n        // ClientHello.cipher_suites_length\n        0x00, 0x5C,\n        // ClientHello.cipher_suites\n        0xC0, 0x30, 0xC0, 0x2C, 0xC0, 0x28, 0xC0, 0x24,\n        0xC0, 0x14, 0xC0, 0x0A, 0x00, 0x9f, 0x00, 0x6B,\n        0x00, 0x39, 0xCC, 0xA9, 0xCC, 0xA8, 0xCC, 0xAA,\n        0xFF, 0x85, 0x00, 0xC4, 0x00, 0x88, 0x00, 0x81,\n        0x00, 0x9D, 0x00, 0x3D, 0x00, 0x35, 0x00, 0xC0,\n        0x00, 0x84, 0xC0, 0x2f, 0xC0, 0x2B, 0xC0, 0x27,\n        0xC0, 0x23, 0xC0, 0x13, 0xC0, 0x09, 0x00, 0x9E,\n        0x00, 0x67, 0x00, 0x33, 0x00, 0xBE, 0x00, 0x45,\n        0x00, 0x9C, 0x00, 0x3C, 0x00, 0x2F, 0x00, 0xBA,\n        0x00, 0x41, 0xC0, 0x11, 0xC0, 0x07, 0x00, 0x05,\n        0x00, 0x04, 0xC0, 0x12, 0xC0, 0x08, 0x00, 0x16,\n        0x00, 0x0a, 0x00, 0xff,\n        // ClientHello.compression_methods\n        0x01, 0x01,\n        // ClientHello.extension_list_length\n        0x00, 0x48,\n        // Extension.extension_type (ec_point_formats)\n        0x00, 0x0b, 0x00, 0x02, 0x01, 0x00,\n        // Extension.extension_type (supported_groups)\n        0x00, 0x0A, 0x00, 0x08, 0x00, 0x06, 0x00, 0x1D,\n        0x00, 0x17, 0x00, 0x18,\n        // Extension.extension_type (session_ticket)\n        0x00, 0x23, 0x00, 0x00,\n        // Extension.extension_type (signature_algorithms)\n        0x00, 0x0D, 0x00, 0x1C, 0x00, 0x1A, 0x06, 0x01,\n        0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03,\n        0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED,\n        0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03,\n        // Extension.extension_type (application_level_Protocol)\n        0x00, 0x10, 0x00, 0x0e, 0x00, 0x0C, 0x02, 0x68,\n        0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2F, 0x31,\n        0x2E, 0x31\n    };\n\n    private static readonly byte[] s_Tls13ClientHello = new byte[] {\n        // SslPlainText.(ContentType+ProtocolVersion)\n        0x16, 0x03, 0x01,\n        // SslPlainText.length\n        0x01, 0x08,\n        // Handshake.msg_type (client hello)\n        0x01,\n        // Handshake.length\n        0x00, 0x01, 0x04,\n        // ClientHello.client_version\n        0x03, 0x03,\n        // ClientHello.random\n        0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA,\n        0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF,\n        0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4,\n        0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E,\n        // ClientHello.SessionId_Length\n        0x20,\n        // ClientHello.SessionId\n        0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA,\n        0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF,\n        0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4,\n        0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E,\n        // ClientHello.cipher_suites_length\n        0x00, 0x0C,\n        // ClientHello.cipher_suites\n        0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0xC0, 0x14,\n        0xc0, 0x30, 0x00, 0xFF,\n        // ClientHello.compression_methods\n        0x01, 0x00,\n        // ClientHello.extension_list_length\n        0x00, 0xAF,\n        // Extension.extension_type (server_name) (10.211.55.2)\n        0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x00,\n        0x0B, 0x31, 0x30, 0x2E, 0x32, 0x31, 0x31, 0x2E,\n        0x35, 0x35, 0x2E, 0x32,\n        // Extension.extension_type (ec_point_formats)\n        0x00, 0x0B, 0x00, 0x04, 0x03, 0x00, 0x01, 0x02,\n        // Extension.extension_type (supported_groups)\n        0x00, 0x0A, 0x00, 0x0C, 0x00, 0x0A, 0x00, 0x1D,\n        0x00, 0x17, 0x00, 0x1E, 0x00, 0x19, 0x00, 0x18,\n        // Extension.extension_type (application_level_Protocol) (boo)\n        0x00, 0x10, 0x00, 0x06, 0x00, 0x04, 0x03, 0x62,\n        0x6f, 0x6f,\n        // Extension.extension_type (encrypt_then_mac)\n        0x00, 0x16, 0x00, 0x00,\n        // Extension.extension_type (extended_master_key_secret)\n        0x00, 0x17, 0x00, 0x00,\n        // Extension.extension_type (signature_algorithms)\n        0x00, 0x0D, 0x00, 0x30, 0x00, 0x2E,\n        0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03,\n        0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03,\n        0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03,\n        0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED,\n        0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03,\n        0x03, 0x01, 0x03, 0x03, 0x02, 0x01,\n        // Extension.extension_type (supported_versions)\n        0x00, 0x2B, 0x00, 0x09, 0x08, 0x03, 0x04, 0x03,\n        0x03, 0x03, 0x02, 0x03, 0x01,\n        // Extension.extension_type (psk_key_exchange_modes)\n        0x00, 0x2D, 0x00, 0x02, 0x01, 0x01,\n        // Extension.extension_type (key_share)\n        0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1D,\n        0x00, 0x20,\n        0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED,\n        0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03,\n        0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED,\n        0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03\n    };\n\n    private static readonly byte[] s_Tls12ServerHello = new byte[] {\n        // SslPlainText.(ContentType+ProtocolVersion)\n        0x16, 0x03, 0x03,\n        // SslPlainText.length\n        0x00, 0x64,\n        // Handshake.msg_type (srever hello)\n        0x02,\n        // Handshake.length\n        0x00, 0x00, 0x60,\n        // ServerHello.client_version\n        0x03, 0x03,\n        // ServerHello.random\n        0x0C, 0x3C, 0x85, 0x78, 0xCA,\n        0x67, 0x70, 0xAA, 0x38, 0xCB,\n        0x28, 0xBC, 0xDC, 0x3E, 0x30,\n        0xBF, 0x11, 0x96, 0x95, 0x1A,\n        0xB9, 0xF0, 0x99, 0xA4, 0x91,\n        0x09, 0x13, 0xB4, 0x89, 0x94,\n        0x27, 0x2E,\n        // ServerHello.SessionId_Length\n        0x20,\n        // ServerHello.SessionId\n        0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA,\n        0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF,\n        0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4,\n        0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E,\n        // ServerHello.cipher_suite\n        0xC0, 0x2B,\n        // ServerHello.compression_method\n        0x00,\n        // ClientHello.extension_list_length\n        0x00, 0x18,\n        // Extension.extension_type (extended_master_secreet)\n        0x00, 0x17, 0x00, 0x00,\n        // Extension.extension_type (renegotiation_info)\n        0xFF, 0x01, 0x00, 0x01, 0x00,\n        // Extension.extension_type (ec_point_formats)\n        0x00, 0x0B, 0x00, 0x02, 0x01, 0x00,\n        // Extension.extension_type (application_level_Protocol)\n        0x00, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, 0x68, 0x32,\n    };\n\n    private static readonly byte[] s_UnifiedHello = new byte[]\n    {\n        // Length\n        0x80, 0x49,\n        // ClientHello\n        0x01,\n        // Version\n        0x03, 0x01,\n        0x00, 0x30, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00,\n        0x2F, 0x00, 0x00, 0x35, 0x00, 0x00, 0x04, 0x00,\n        0x00, 0x05, 0x00, 0x00, 0x0A, 0x01, 0x00, 0x80,\n        0x07, 0x00, 0xC0, 0x03, 0x00, 0x80, 0x00, 0x00,\n        0x09, 0x06, 0x00, 0x40, 0x00, 0x00, 0x64, 0x00,\n        0x00, 0x62, 0x00, 0x00, 0x03, 0x00, 0x00, 0x06,\n        0x02, 0x00, 0x80, 0x04, 0x00, 0x80, 0x5B, 0x0B,\n        0xA1, 0xEB, 0xBF, 0x2D, 0x57, 0xF5, 0xD1, 0x0F,\n        0x52, 0x3B, 0x12, 0x9C, 0xF8, 0xD4,\n    };\n\n    private static readonly byte[] s_TlsClientHelloNoExtensions = new byte[] {\n        0x16, 0x03, 0x03, 0x00, 0x39, 0x01, 0x00, 0x00,\n        0x35, 0x03, 0x03, 0x62, 0x5d, 0x50, 0x2a, 0x41,\n        0x2f, 0xd8, 0xc3, 0x65, 0x35, 0xea, 0x01, 0x70,\n        0x03, 0x7e, 0x7e, 0x2d, 0xd4, 0xfe, 0x93, 0x39,\n        0xa4, 0x04, 0x66, 0xbb, 0x46, 0x91, 0x41, 0xc3,\n        0x48, 0x87, 0x3d, 0x00, 0x00, 0x0e, 0x00, 0x3d,\n        0x00, 0x3c, 0x00, 0x0a, 0x00, 0x35, 0x00, 0x2f,\n        0x00, 0x05, 0x00, 0x04, 0x01, 0x00\n    };\n\n    private static readonly byte[] s_Tls13FragmentedClientHello = new byte[] {\n        // SslPlainText.(ContentType+ProtocolVersion)\n        0x16, 0x03, 0x01,\n        // SslPlainText.length\n        0x00, 0x04, // Fragmented\n        // Handshake.msg_type (client hello)\n        0x01,\n        // Handshake.length\n        0x00, 0x01, 0x04,\n        // Extra fragment header\n        // SslPlainText.(ContentType+ProtocolVersion)\n        0x16, 0x03, 0x01,\n        // SslPlainText.length\n        0x01, 0x04,\n        // ClientHello.client_version\n        0x03, 0x03,\n\n        // ClientHello.random\n        0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA,\n        0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF,\n        0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4,\n        0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E,\n        // ClientHello.SessionId_Length\n        0x20,\n        // ClientHello.SessionId\n        0x0C, 0x3C, 0x85, 0x78, 0xCA, 0x67, 0x70, 0xAA,\n        0x38, 0xCB, 0x28, 0xBC, 0xDC, 0x3E, 0x30, 0xBF,\n        0x11, 0x96, 0x95, 0x1A, 0xB9, 0xF0, 0x99, 0xA4,\n        0x91, 0x09, 0x13, 0xB4, 0x89, 0x94, 0x27, 0x2E,\n        // ClientHello.cipher_suites_length\n        0x00, 0x0C,\n        // ClientHello.cipher_suites\n        0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0xC0, 0x14,\n        0xc0, 0x30, 0x00, 0xFF,\n        // ClientHello.compression_methods\n        0x01, 0x00,\n        // ClientHello.extension_list_length\n        0x00, 0xAF,\n        // Extension.extension_type (server_name) (10.211.55.2)\n        0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x00,\n        0x0B, 0x31, 0x30, 0x2E, 0x32, 0x31, 0x31, 0x2E,\n        0x35, 0x35, 0x2E, 0x32,\n        // Extension.extension_type (ec_point_formats)\n        0x00, 0x0B, 0x00, 0x04, 0x03, 0x00, 0x01, 0x02,\n        // Extension.extension_type (supported_groups)\n        0x00, 0x0A, 0x00, 0x0C, 0x00, 0x0A, 0x00, 0x1D,\n        0x00, 0x17, 0x00, 0x1E, 0x00, 0x19, 0x00, 0x18,\n        // Extension.extension_type (application_level_Protocol) (boo)\n        0x00, 0x10, 0x00, 0x06, 0x00, 0x04, 0x03, 0x62,\n        0x6f, 0x6f,\n        // Extension.extension_type (encrypt_then_mac)\n        0x00, 0x16, 0x00, 0x00,\n        // Extension.extension_type (extended_master_key_secret)\n        0x00, 0x17, 0x00, 0x00,\n        // Extension.extension_type (signature_algorithms)\n        0x00, 0x0D, 0x00, 0x30, 0x00, 0x2E,\n        0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03,\n        0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03,\n        0x06, 0x03, 0xEF, 0xEF, 0x05, 0x01, 0x05, 0x03,\n        0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED,\n        0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03,\n        0x03, 0x01, 0x03, 0x03, 0x02, 0x01,\n        // Extension.extension_type (supported_versions)\n        0x00, 0x2B, 0x00, 0x09, 0x08, 0x03, 0x04, 0x03,\n        0x03, 0x03, 0x02, 0x03, 0x01,\n        // Extension.extension_type (psk_key_exchange_modes)\n        0x00, 0x2D, 0x00, 0x02, 0x01, 0x01,\n        // Extension.extension_type (key_share)\n        0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1D,\n        0x00, 0x20,\n        0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED,\n        0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03,\n        0x04, 0x01, 0x04, 0x03, 0xEE, 0xEE, 0xED, 0xED,\n        0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03\n    };\n\n    private static IEnumerable<byte[]> InvalidClientHello()\n    {\n        // This test covers following test cases:\n        // - Length of structure off by 1 (search for \"length off by 1\")\n        // - Length of structure is max length (search for \"max length\")\n        // - Type is invalid or unknown (i.e. SslPlainText.ClientType is not 0x16 - search for \"unknown\")\n        // - Invalid utf-8 characters\n        // in each case sni will be null or will cause parsing error - we only expect some parsing errors,\n        // anything else is considered a bug\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01 - length off by 1\n            0x00, 0x02, 0x00\n        };\n\n        // #2\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01 - max length\n            0xFF, 0xFF, 0x00\n        };\n\n        // #3\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17 - length off by 1\n            0x00, 0x01,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #4\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17 - max length\n            0xFF, 0xFF,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #5\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23 - length off by 1\n            0x00, 0x01,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #6\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23 - max length\n            0xFF, 0xFF,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #7\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D - length off by 1\n            0x00, 0x15, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #8\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D - max length\n            0xFF, 0xFF, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #9\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B - length off by 1\n            0x00, 0x03, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #10\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B - max length\n            0xFF, 0xFF, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #11\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A - length off by 1\n            0x00, 0x09, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #10\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A - max length\n            0xFF, 0xFF, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #13\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length - length off by 1\n            0x00, 0x35,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #14\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length - max length\n            0xFF, 0xFF,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #15\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type - unknown\n            0x01,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #16\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length - length off by 1\n            0x00, 0x38,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #17\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length - max length\n            0xFF, 0xFF,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #18\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length - length off by 1\n            0x00, 0x3A,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #19\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length - max length\n            0xFF, 0xFF,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #20\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name) - unknown\n            0x01, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #21\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length - length off by 1\n            0x00, 0x75,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #22\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length - max length\n            0xFF, 0xFF,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #23\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods - length off by 1\n            0x02, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #24\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods - max length\n            0xFF, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #25\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites - length off by 1\n            0x00, 0x2B, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #26\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites - max length\n            0xFF, 0xFF, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #27\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId - length off by 1\n            0x01,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #28\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId - max length\n            0xFF,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #29\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length - length off by 1\n            0x00, 0x00, 0xC8,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #30\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length - max length\n            0xFF, 0xFF, 0xFF,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #31\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello) - unknown\n            0x00,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #32\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length - length off by 1\n            0x00, 0xCC,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #33\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length - max length\n            0xFF, 0xFF,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #34\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion) - unknown\n            0x01, 0x03, 0x04,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x58, 0xAA, 0x5F, 0xE7, 0x22,\n            0xCF, 0x9F, 0x59, 0x8A, 0xC5,\n            0x8B, 0x87, 0xC7, 0x62, 0x32,\n            0x98, 0xD4, 0xD8, 0xA2, 0xBE,\n            0x77, 0xCE, 0xA9, 0xCE, 0x42,\n            0x25, 0x5A, 0x8B, 0xEE, 0x16,\n            0x80, 0xF1,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n\n        // #35\n        yield return new byte[] {\n            // SslPlainText.(ContentType+ProtocolVersion)\n            0x16, 0x03, 0x03,\n            // SslPlainText.length\n            0x00, 0xCB,\n            // Handshake.msg_type (client hello)\n            0x01,\n            // Handshake.length\n            0x00, 0x00, 0xC7,\n            // ClientHello.client_version\n            0x03, 0x03,\n            // ClientHello.random\n            0x0C, 0x3C, 0x85, 0x78, 0xCA,\n            0x67, 0x70, 0xAA, 0x38, 0xCB,\n            0x28, 0xBC, 0xDC, 0x3E, 0x30,\n            0xBF, 0x11, 0x96, 0x95, 0x1A,\n            0xB9, 0xF0, 0x99, 0xA4, 0x91,\n            0x09, 0x13, 0xB4, 0x89, 0x94,\n            0x27, 0x2E,\n            // ClientHello.SessionId\n            0x00,\n            // ClientHello.cipher_suites\n            0x00, 0x2A, 0xC0, 0x2C, 0xC0,\n            0x2B, 0xC0, 0x30, 0xC0, 0x2F,\n            0x00, 0x9F, 0x00, 0x9E, 0xC0,\n            0x24, 0xC0, 0x23, 0xC0, 0x28,\n            0xC0, 0x27, 0xC0, 0x0A, 0xC0,\n            0x09, 0xC0, 0x14, 0xC0, 0x13,\n            0x00, 0x9D, 0x00, 0x9C, 0x00,\n            0x3D, 0x00, 0x3C, 0x00, 0x35,\n            0x00, 0x2F, 0x00, 0x0A,\n            // ClientHello.compression_methods\n            0x01, 0x01,\n            // ClientHello.extension_list_length\n            0x00, 0x74,\n            // Extension.extension_type (server_name)\n            0x00, 0x00,\n            // ServerNameListExtension.length\n            0x00, 0x39,\n            // ServerName.length\n            0x00, 0x37,\n            // ServerName.type\n            0x00,\n            // HostName.length\n            0x00, 0x34,\n            // HostName.bytes\n            0x80, 0x80, 0x80, 0x80, 0x61, // 0x80 0x80 0x80 0x80 is a forbidden utf-8 sequence\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61, 0x61, 0x61, 0x61,\n            0x61, 0x61,\n            // Extension.extension_type (00 0A)\n            0x00, 0x0A,\n            // Extension 0A\n            0x00, 0x08, 0x00, 0x06, 0x00,\n            0x1D, 0x00, 0x17, 0x00, 0x18,\n            // Extension.extension_type (00 0B)\n            0x00, 0x0B,\n            // Extension 0B\n            0x00, 0x02, 0x01, 0x00,\n            // Extension.extension_type (00 0D)\n            0x00, 0x0D,\n            // Extension 0D\n            0x00, 0x14, 0x00, 0x12, 0x04,\n            0x01, 0x05, 0x01, 0x02, 0x01,\n            0x04, 0x03, 0x05, 0x03, 0x02,\n            0x03, 0x02, 0x02, 0x06, 0x01,\n            0x06, 0x03,\n            // Extension.extension_type (00 23)\n            0x00, 0x23,\n            // Extension 00 23\n            0x00, 0x00,\n            // Extension.extension_type (00 17)\n            0x00, 0x17,\n            // Extension 17\n            0x00, 0x00,\n            // Extension.extension_type (FF 01)\n            0xFF, 0x01,\n            // Extension FF01\n            0x00, 0x01, 0x00\n        };\n    }\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/WebSocketsTelemetry/WebSocketsParserTests.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.IO;\nusing System.Text;\nusing Xunit;\nusing Yarp.Tests.Common;\n\nnamespace Yarp.ReverseProxy.WebSocketsTelemetry.Tests;\n\npublic abstract class WebSocketsParserTests\n{\n    protected abstract bool IsServer { get; }\n\n    private int MaskSize => IsServer ? 4 : 0;\n\n    private WebSocketsParser CreateParser(TimeProvider timeProvider = null) => new(timeProvider ?? TimeProvider.System, IsServer);\n\n    private ReadOnlySpan<byte> GetHeader(int opcode, int length, bool endOfMessage = true)\n    {\n        var header = new byte[2 + MaskSize + (length < 126 ? 0 : (length < 65536 ? 2 : 8))];\n\n        Assert.InRange(opcode, 0, 15);\n        header[0] = (byte)opcode;\n\n        if (endOfMessage)\n        {\n            header[0] |= 0x80;\n        }\n\n        if (length < 126)\n        {\n            header[1] = (byte)length;\n        }\n        else\n        {\n            header[1] = (byte)(length < 65536 ? 126 : 127);\n            var i = header.Length - MaskSize - 1;\n            while (length != 0)\n            {\n                header[i--] = (byte)(length % 256);\n                length /= 256;\n            }\n        }\n\n        if (IsServer)\n        {\n            header[1] |= 0x80;\n        }\n\n        return header;\n    }\n\n    private ReadOnlySpan<byte> GetCloseFrame(int length = 0) => GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('a', length)), opcode: 8);\n\n    private ReadOnlySpan<byte> GetPingFrame(int length = 0) => GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('a', length)), opcode: 9);\n\n    private ReadOnlySpan<byte> GetPongFrame(int length = 0) => GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('a', length)), opcode: 10);\n\n    private ReadOnlySpan<byte> GetTextMessageFrame(string message, bool continuation = false, bool endOfMessage = true)\n    {\n        var messageBytes = Encoding.UTF8.GetBytes(message);\n        var header = GetHeader(opcode: continuation ? 0 : 1, length: messageBytes.Length, endOfMessage);\n\n        var frame = new byte[header.Length + messageBytes.Length];\n        header.CopyTo(frame);\n        messageBytes.CopyTo(frame, header.Length);\n\n        return frame;\n    }\n\n    private ReadOnlySpan<byte> GetBinaryMessageFrame(ReadOnlySpan<byte> message, bool continuation = false, bool endOfMessage = true, int opcode = 2)\n    {\n        var header = GetHeader(opcode: continuation ? 0 : opcode, length: message.Length, endOfMessage);\n\n        var frame = new byte[header.Length + message.Length];\n        header.CopyTo(frame);\n        message.CopyTo(frame.AsSpan(header.Length));\n\n        return frame;\n    }\n\n    [Fact]\n    public void CustomClockIsUsedForCloseTime()\n    {\n        var timeProvider = new TestTimeProvider(new TimeSpan(42));\n        var parser = CreateParser(timeProvider);\n\n        Assert.Null(parser.CloseTime);\n\n        parser.Consume(GetCloseFrame());\n\n        Assert.NotNull(parser.CloseTime);\n        Assert.Equal(timeProvider.GetUtcNow(), parser.CloseTime.Value);\n    }\n\n    [Fact]\n    public void MessagesAreCountedCorrectly()\n    {\n        var parser = CreateParser();\n\n        // Whole messages\n        parser.Consume(GetTextMessageFrame(\"Foo\"));\n        Assert.Equal(1, parser.MessageCount);\n\n        parser.Consume(GetBinaryMessageFrame(new byte[] { 4, 2 }));\n        Assert.Equal(2, parser.MessageCount);\n\n\n        // Continuations\n        parser.Consume(GetTextMessageFrame(\"Hello, \", endOfMessage: false));\n        Assert.Equal(2, parser.MessageCount);\n\n        parser.Consume(GetTextMessageFrame(\"world\", continuation: true, endOfMessage: false));\n        Assert.Equal(2, parser.MessageCount);\n\n        parser.Consume(GetTextMessageFrame(\"!\", continuation: true, endOfMessage: true));\n        Assert.Equal(3, parser.MessageCount);\n\n        parser.Consume(GetBinaryMessageFrame(new byte[] { 4 }, endOfMessage: false));\n        Assert.Equal(3, parser.MessageCount);\n\n        parser.Consume(GetBinaryMessageFrame(new byte[] { 2 }, continuation: true, endOfMessage: true));\n        Assert.Equal(4, parser.MessageCount);\n\n\n        // Large messages\n        parser.Consume(GetTextMessageFrame(new string('a', 1_000)));\n        Assert.Equal(5, parser.MessageCount);\n\n        parser.Consume(GetTextMessageFrame(new string('b', 100_000)));\n        Assert.Equal(6, parser.MessageCount);\n\n        parser.Consume(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('c', 1_000))));\n        Assert.Equal(7, parser.MessageCount);\n\n        parser.Consume(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('d', 100_000))));\n        Assert.Equal(8, parser.MessageCount);\n\n\n        // Large messages with continuations\n        parser.Consume(GetTextMessageFrame(new string('a', 1_000), endOfMessage: false));\n        Assert.Equal(8, parser.MessageCount);\n\n        parser.Consume(GetTextMessageFrame(new string('b', 1_000), continuation: true, endOfMessage: true));\n        Assert.Equal(9, parser.MessageCount);\n\n        parser.Consume(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('c', 1_000)), endOfMessage: false));\n        Assert.Equal(9, parser.MessageCount);\n\n        parser.Consume(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('d', 1_000)), continuation: true, endOfMessage: true));\n        Assert.Equal(10, parser.MessageCount);\n\n\n        // Fragmented frames\n        parser.Consume(Array.Empty<byte>());\n        Assert.Equal(10, parser.MessageCount);\n\n        ConsumeInFragments(ref parser, GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('a', 1_000))));\n        Assert.Equal(11, parser.MessageCount);\n\n        var ms = new MemoryStream();\n        for (var i = (int)parser.MessageCount; i < 500; i++)\n        {\n            // Control frames are not counted\n            if (i % 7 == 0)\n            {\n                ms.Write(GetPingFrame());\n            }\n            if (i % 13 == 0)\n            {\n                ms.Write(GetPongFrame());\n            }\n\n            switch (i % 4)\n            {\n                case 0:\n                    ms.Write(GetTextMessageFrame(new string('a', i)));\n                    break;\n\n                case 1:\n                    ms.Write(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('b', i))));\n                    break;\n\n                case 2:\n                    ms.Write(GetTextMessageFrame(new string('a', i), endOfMessage: false));\n                    ms.Write(GetTextMessageFrame(new string('b', i), continuation: true, endOfMessage: false));\n                    ms.Write(GetTextMessageFrame(new string('c', i), continuation: true, endOfMessage: true));\n                    break;\n\n                case 3:\n                    ms.Write(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('a', i)), endOfMessage: false));\n                    ms.Write(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('b', i)), continuation: true, endOfMessage: false));\n                    ms.Write(GetBinaryMessageFrame(Encoding.UTF8.GetBytes(new string('c', i)), continuation: true, endOfMessage: true));\n                    break;\n            }\n        }\n        ConsumeInFragments(ref parser, ms.ToArray());\n        Assert.Equal(500, parser.MessageCount);\n\n\n        // Control frames are not counted\n        parser.Consume(GetPingFrame());\n        parser.Consume(GetPingFrame(length: 10));\n        parser.Consume(GetPongFrame());\n        parser.Consume(GetPongFrame(length: 10));\n        parser.Consume(GetCloseFrame());\n        parser.Consume(GetCloseFrame(length: 10));\n        Assert.Equal(500, parser.MessageCount);\n\n\n        // Messages are still counted after a close frame\n        parser.Consume(GetTextMessageFrame(\"Foo\"));\n        Assert.Equal(501, parser.MessageCount);\n\n        static void ConsumeInFragments(ref WebSocketsParser parser, ReadOnlySpan<byte> message)\n        {\n            var rng = new Random(42);\n            while (message.Length != 0)\n            {\n                var fragmentLength = Math.Min(message.Length, rng.Next(0, 150));\n                parser.Consume(message[..fragmentLength]);\n                message = message[fragmentLength..];\n            }\n        }\n    }\n}\n\npublic sealed class WebSocketsParserTests_Client : WebSocketsParserTests\n{\n    protected override bool IsServer => false;\n}\n\npublic sealed class WebSocketsParserTests_Server : WebSocketsParserTests\n{\n    protected override bool IsServer => true;\n}\n"
  },
  {
    "path": "test/ReverseProxy.Tests/Yarp.ReverseProxy.Tests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(TestTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>Yarp.ReverseProxy</RootNamespace>\n    <NoWarn>$(NoWarn);SYSLIB0039;SYSLIB0057</NoWarn>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Content Include=\"..\\..\\src\\ReverseProxy\\ConfigurationSchema.json\" CopyToOutputDirectory=\"PreserveNewest\" />\n    <Content Include=\"..\\TestCertificates\\*.*\" LinkBase=\"TestCertificates\" CopyToOutputDirectory=\"PreserveNewest\" />\n    <Content Include=\"validSelfSignedClientEkuCertificate.cer\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </Content>\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Autofac\" Version=\"$(AutofacVersion)\" />\n    <PackageReference Include=\"Autofac.Extras.Moq\" Version=\"$(AutofacExtrasMoqVersion)\" />\n    <PackageReference Include=\"coverlet.collector\" Version=\"$(CoverletCollectorVersion)\" />\n    <PackageReference Include=\"Moq\" Version=\"$(MoqVersion)\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.TestHost\" Version=\"$(MicrosoftAspNetCoreTestHostVersion)\" />\n    <PackageReference Include=\"JsonSchema.Net\" Version=\"$(JsonSchemaNetVersion)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n    <ProjectReference Include=\"..\\Tests.Common\\Yarp.Tests.Common.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "test/Tests.Common/TestAutoMockBase.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Autofac.Core;\nusing Autofac.Extras.Moq;\nusing Moq;\n\nnamespace Yarp.Tests.Common;\n\n/// <summary>\n/// Automatically generates mocks for interfaces on the Class under test.\n/// </summary>\npublic class TestAutoMockBase : IDisposable\n{\n    private bool _isDisposed;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"TestAutoMockBase\" /> class.\n    /// </summary>\n    public TestAutoMockBase()\n    {\n        AutoMock = AutoMock.GetLoose();\n    }\n\n    /// <summary>\n    /// Gets the mocks.\n    /// </summary>\n    protected AutoMock AutoMock { get; private set; }\n\n    /// <summary>\n    /// Resets the mocks.\n    /// </summary>\n    public void ResetMocks()\n    {\n        AutoMock.Dispose();\n        AutoMock = AutoMock.GetLoose();\n    }\n\n    /// <summary>\n    /// Creates an object of <typeparamref name=\"TService\"/> using the dependency container.\n    /// </summary>\n    /// <typeparam name=\"TService\">The type of the object to create.</typeparam>\n    /// <param name=\"parameters\">The parameters.</param>\n    /// <returns>\n    /// Instance of <typeparamref name=\"TService\"/>.\n    /// </returns>\n    public virtual TService Create<TService>(params Parameter[] parameters)\n        where TService : class\n    {\n        return AutoMock.Create<TService>(parameters);\n    }\n\n    /// <summary>\n    /// Creates a mock of the specified abstraction.\n    /// </summary>\n    /// <typeparam name=\"TDependencyToMock\">The type of the dependency to mock.</typeparam>\n    /// <returns>A mock of the type.</returns>\n    public Mock<TDependencyToMock> Mock<TDependencyToMock>()\n        where TDependencyToMock : class\n    {\n        return AutoMock.Mock<TDependencyToMock>();\n    }\n\n    /// <summary>\n    /// Provide the specified abstraction to the dependency injection container.\n    /// </summary>\n    /// <typeparam name=\"TService\">The type of the service.</typeparam>\n    /// <param name=\"instance\">The instance.</param>\n    public void Provide<TService>(TService instance)\n        where TService : class\n    {\n        ArgumentNullException.ThrowIfNull(instance);\n        AutoMock.Provide(instance);\n    }\n\n    /// <summary>\n    /// Provide the specified concrete type to the dependency injection container.\n    /// </summary>\n    /// <typeparam name=\"TService\">The type of the service.</typeparam>\n    /// <typeparam name=\"TImplementation\">The type that implements the service.</typeparam>\n    /// <returns>An instance of <typeparamref name=\"TImplementation\"/> that implements <typeparamref name=\"TService\"/>.</returns>\n    public TImplementation Provide<TService, TImplementation>()\n        where TService : class\n        where TImplementation : TService\n    {\n        return (TImplementation)AutoMock.Provide<TService, TImplementation>();\n    }\n\n    /// <inheritdoc/>\n    public virtual void Dispose()\n    {\n        if (!_isDisposed)\n        {\n            AutoMock.Dispose();\n            _isDisposed = true;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Tests.Common/TestLogger.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing Microsoft.Extensions.Logging;\n\nnamespace Yarp.ReverseProxy.Common;\n\npublic sealed class TestLogger(ILogger xunitLogger, string categoryName) : ILogger\n{\n    public record LogEntry(string CategoryName, LogLevel LogLevel, EventId EventId, string Message, Exception Exception);\n\n    private static readonly AsyncLocal<List<LogEntry>> _logsAsyncLocal = new();\n\n    public static List<LogEntry> Collect() => _logsAsyncLocal.Value ??= [];\n\n    public IDisposable BeginScope<TState>(TState state) where TState : notnull => xunitLogger.BeginScope(state);\n\n    public bool IsEnabled(LogLevel logLevel) => true;\n\n    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)\n    {\n        _logsAsyncLocal.Value?.Add(new LogEntry(categoryName, logLevel, eventId, formatter(state, exception), exception));\n\n        xunitLogger.Log(logLevel, eventId, state, exception, formatter);\n    }\n}\n"
  },
  {
    "path": "test/Tests.Common/TestLoggerProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Logging.Testing;\nusing Xunit;\n\nnamespace Yarp.ReverseProxy.Common;\n\npublic sealed class TestLoggerProvider(ITestOutputHelper output) : ILoggerProvider\n{\n    private readonly XunitLoggerProvider _xunitLoggerProvider = new(output);\n\n    public ILogger CreateLogger(string categoryName) => new TestLogger(_xunitLoggerProvider.CreateLogger(categoryName), categoryName);\n\n    public void Dispose() => _xunitLoggerProvider.Dispose();\n}\n"
  },
  {
    "path": "test/Tests.Common/TestRandom.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace Yarp.Tests.Common;\n\npublic class TestRandom : Random\n{\n    public int[] Sequence { get; set; }\n    public int Offset { get; set; }\n\n    public override int Next(int maxValue)\n    {\n        return Sequence[Offset++];\n    }\n}\n"
  },
  {
    "path": "test/Tests.Common/TestRandomFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Yarp.ReverseProxy.Utilities;\n\nnamespace Yarp.Tests.Common;\n\npublic class TestRandomFactory : IRandomFactory\n{\n    public TestRandom Instance { get; set; }\n\n    public Random CreateRandomInstance()\n    {\n        return Instance;\n    }\n}\n"
  },
  {
    "path": "test/Tests.Common/TestTimeProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Xunit;\n\nnamespace Yarp.Tests.Common;\n\n/// <summary>\n/// Simulates passage of time, used for testing.\n/// </summary>\n/// <remarks>\n/// This timer doesn't track real time, but instead tracks virtual time.\n/// Time only advances when any of the following methods are called:\n/// <list type=\"bullet\">\n/// <item><see cref=\"Advance\"/></item>\n/// <item><see cref=\"AdvanceTo(TimeSpan)\"/></item>\n/// </list>\n/// </remarks>\npublic class TestTimeProvider : TimeProvider\n{\n    private readonly List<TestTimer> _timers = new();\n\n    private TimeSpan _currentTime;\n\n    public int TimerCount => _timers.Count;\n\n    // Mess with the frequency to check for bad assumptions in the code.\n    public override long TimestampFrequency => TimeSpan.TicksPerSecond * 7;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"TestTimeProvider\" /> class.\n    /// </summary>\n    /// <param name=\"initialTime\">Initial value for current time. Zero if not specified.</param>\n    public TestTimeProvider(TimeSpan? initialTime = null)\n    {\n        _currentTime = initialTime ?? TimeSpan.Zero;\n    }\n\n    public TestTimeProvider(DateTimeOffset initialTime)\n    {\n        _currentTime = initialTime - DateTimeOffset.UnixEpoch;\n    }\n\n    /// <summary>\n    /// Advances time by the specified amount.\n    /// </summary>\n    /// <param name=\"howMuch\">How much to advance <see cref=\"CurrentTime\"/> by.</param>\n    public void Advance(TimeSpan howMuch)\n    {\n        AdvanceTo(_currentTime + howMuch);\n    }\n\n    /// <summary>\n    /// Advances time to the specified point.\n    /// </summary>\n    /// <param name=\"targetTime\">Advances <see cref=\"CurrentTime\"/> until it equals <paramref name=\"targetTime\"/>.</param>\n    public void AdvanceTo(TimeSpan targetTime)\n    {\n        if (targetTime < _currentTime)\n        {\n            throw new InvalidOperationException(\"Time should not flow backwards\");\n        }\n\n        // We could use this to fire timers, but timers are currently fired manually by tests.\n\n        _currentTime = targetTime;\n    }\n\n    public override DateTimeOffset GetUtcNow() => new DateTime(_currentTime.Ticks, DateTimeKind.Utc);\n\n    // Mess with the frequency to check for bad assumptions in the code.\n    public override long GetTimestamp() => _currentTime.Ticks * 7;\n\n    public override ITimer CreateTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period)\n    {\n        Assert.Equal(Timeout.InfiniteTimeSpan, period);\n        var timer = new TestTimer(callback, state, dueTime, period);\n        _timers.Add(timer);\n        return timer;\n    }\n\n    public void FireTimer(int idx)\n    {\n        _timers[idx].Fire();\n    }\n\n    public void FireAllTimers()\n    {\n        for (var i = 0; i < _timers.Count; i++)\n        {\n            FireTimer(i);\n        }\n    }\n\n    public void VerifyTimer(int idx, TimeSpan dueTime)\n    {\n        Assert.Equal(dueTime, _timers[idx].DueTime);\n    }\n\n    public void AssertTimerDisposed(int idx)\n    {\n        Assert.True(_timers[idx].IsDisposed);\n    }\n\n    private sealed class TestTimer : ITimer\n    {\n        public TestTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period)\n        {\n            Callback = callback;\n            State = state;\n            DueTime = dueTime;\n            Period = period;\n        }\n\n        public TimeSpan DueTime { get; private set; }\n\n        public TimeSpan Period { get; private set; }\n\n        public TimerCallback Callback { get; private set; }\n\n        public object State { get; private set; }\n\n        public bool IsDisposed { get; private set; }\n\n        public bool Change(TimeSpan dueTime, TimeSpan period)\n        {\n            DueTime = dueTime;\n            Period = period;\n            return true;\n        }\n\n        public void Fire()\n        {\n            Callback(State);\n        }\n\n        public void Dispose()\n        {\n            IsDisposed = true;\n        }\n\n        public ValueTask DisposeAsync()\n        {\n            IsDisposed = true;\n            return default;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Tests.Common/XunitLoggerFactoryExtensions.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging.Testing;\nusing Xunit;\n\nnamespace Microsoft.Extensions.Logging;\n\npublic static class XunitLoggerFactoryExtensions\n{\n    public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output)\n    {\n        builder.Services.AddSingleton<ILoggerProvider>(new XunitLoggerProvider(output));\n        return builder;\n    }\n\n    public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output, LogLevel minLevel)\n    {\n        builder.Services.AddSingleton<ILoggerProvider>(new XunitLoggerProvider(output, minLevel));\n        return builder;\n    }\n\n    public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart)\n    {\n        builder.Services.AddSingleton<ILoggerProvider>(new XunitLoggerProvider(output, minLevel, logStart));\n        return builder;\n    }\n\n    public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output)\n    {\n        loggerFactory.AddProvider(new XunitLoggerProvider(output));\n        return loggerFactory;\n    }\n\n    public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output, LogLevel minLevel)\n    {\n        loggerFactory.AddProvider(new XunitLoggerProvider(output, minLevel));\n        return loggerFactory;\n    }\n\n    public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart)\n    {\n        loggerFactory.AddProvider(new XunitLoggerProvider(output, minLevel, logStart));\n        return loggerFactory;\n    }\n}\n"
  },
  {
    "path": "test/Tests.Common/XunitLoggerProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Globalization;\nusing System.Linq;\nusing System.Text;\nusing System.Threading;\nusing Xunit;\n\nnamespace Microsoft.Extensions.Logging.Testing;\n\npublic class XunitLoggerProvider : ILoggerProvider\n{\n    // Used to distinguish when multiple apps are running as part of the same test.\n    private static int InstanceCount;\n    private readonly int _providerInstanceId = Interlocked.Increment(ref InstanceCount);\n    private readonly ITestOutputHelper _output;\n    private readonly LogLevel _minLevel;\n    private readonly DateTimeOffset? _logStart;\n\n    public XunitLoggerProvider(ITestOutputHelper output)\n        : this(output, LogLevel.Trace)\n    {\n    }\n\n    public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel)\n        : this(output, minLevel, null)\n    {\n    }\n\n    public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart)\n    {\n        _output = output;\n        _minLevel = minLevel;\n        _logStart = logStart;\n    }\n\n    public ILogger CreateLogger(string categoryName)\n    {\n        return new XunitLogger(_output, categoryName, _minLevel, _logStart, _providerInstanceId);\n    }\n\n    public void Dispose()\n    {\n    }\n}\n\npublic class XunitLogger : ILogger\n{\n    private static readonly string[] NewLineChars = new[] { Environment.NewLine };\n    private readonly string _category;\n    private readonly LogLevel _minLogLevel;\n    private readonly ITestOutputHelper _output;\n    private readonly DateTimeOffset? _logStart;\n    private readonly int _providerInstanceId;\n\n    public XunitLogger(ITestOutputHelper output, string category, LogLevel minLogLevel, DateTimeOffset? logStart, int providerInstanceId)\n    {\n        _minLogLevel = minLogLevel;\n        _category = category;\n        _output = output;\n        _logStart = logStart;\n        _providerInstanceId = providerInstanceId;\n    }\n\n    public void Log<TState>(\n        LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)\n    {\n        if (!IsEnabled(logLevel))\n        {\n            return;\n        }\n\n        // Buffer the message into a single string in order to avoid shearing the message when running across multiple threads.\n        var messageBuilder = new StringBuilder();\n\n        var timestamp = _logStart.HasValue ?\n            $\"{(DateTimeOffset.UtcNow - _logStart.Value).TotalSeconds.ToString(\"N3\", CultureInfo.InvariantCulture)}s\" :\n            DateTimeOffset.UtcNow.ToString(\"s\", CultureInfo.InvariantCulture);\n\n        var firstLinePrefix = $\"| [{timestamp}] I:{_providerInstanceId} {_category} {logLevel}: \";\n        var lines = formatter(state, exception).Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries);\n        messageBuilder.AppendLine(firstLinePrefix + lines.FirstOrDefault() ?? string.Empty);\n\n        var additionalLinePrefix = \"|\" + new string(' ', firstLinePrefix.Length - 1);\n        foreach (var line in lines.Skip(1))\n        {\n            messageBuilder.AppendLine(additionalLinePrefix + line);\n        }\n\n        if (exception != null)\n        {\n            lines = exception.ToString().Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries);\n            additionalLinePrefix = \"| \";\n            foreach (var line in lines)\n            {\n                messageBuilder.AppendLine(additionalLinePrefix + line);\n            }\n        }\n\n        // Remove the last line-break, because ITestOutputHelper only has WriteLine.\n        var message = messageBuilder.ToString();\n        if (message.EndsWith(Environment.NewLine, StringComparison.Ordinal))\n        {\n            message = message.Substring(0, message.Length - Environment.NewLine.Length);\n        }\n\n        try\n        {\n            _output.WriteLine(message);\n        }\n        catch (Exception)\n        {\n            // We could fail because we're on a background thread and our captured ITestOutputHelper is\n            // busted (if the test \"completed\" before the background thread fired).\n            // So, ignore this. There isn't really anything we can do but hope the\n            // caller has additional loggers registered\n        }\n    }\n\n    public bool IsEnabled(LogLevel logLevel)\n        => logLevel >= _minLogLevel;\n\n    public IDisposable BeginScope<TState>(TState state)\n        => new NullScope();\n\n    private sealed class NullScope : IDisposable\n    {\n        public void Dispose()\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "test/Tests.Common/Yarp.Tests.Common.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(TestTFMs)</TargetFrameworks>\n    <OutputType>Library</OutputType>\n    <RootNamespace>Yarp.Common.Tests</RootNamespace>\n    <!-- This is a test utility project. -->\n    <IsTestProject>false</IsTestProject>\n    <IsTestUtilityProject>true</IsTestUtilityProject>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Autofac\" Version=\"$(AutofacVersion)\" />\n    <PackageReference Include=\"Autofac.Extras.Moq\" Version=\"$(AutofacExtrasMoqVersion)\" />\n    <PackageReference Include=\"coverlet.collector\" Version=\"$(CoverletCollectorVersion)\" />\n    <PackageReference Include=\"Moq\" Version=\"$(MoqVersion)\" />\n    <PackageReference Include=\"xunit.v3.extensibility.core\" Version=\"$(XUnitV3Version)\" />\n    <PackageReference Include=\"xunit.v3.assert\" Version=\"$(XUnitV3Version)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "test.cmd",
    "content": "@echo off\npowershell -ExecutionPolicy ByPass -NoProfile -command \"& \"\"\"%~dp0eng\\common\\Build.ps1\"\"\" -test %*\""
  },
  {
    "path": "test.sh",
    "content": "#!/usr/bin/env bash\n\nsource=\"${BASH_SOURCE[0]}\"\n\n# resolve $SOURCE until the file is no longer a symlink\nwhile [[ -h $source ]]; do\n  scriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n  source=\"$(readlink \"$source\")\"\n\n  # if $source was a relative symlink, we need to resolve it relative to the path where the\n  # symlink file was located\n  [[ $source != /* ]] && source=\"$scriptroot/$source\"\ndone\n\nscriptroot=\"$( cd -P \"$( dirname \"$source\" )\" && pwd )\"\n\"$scriptroot/eng/common/build.sh\" --test $@"
  },
  {
    "path": "testassets/BenchmarkApp/BenchmarkApp.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(TestTFMs)</TargetFrameworks>\n    <NoWarn>$(NoWarn);SYSLIB0057</NoWarn>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Crank.EventSources\" Version=\"$(MicrosoftCrankEventSourcesVersion)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"testCert.pfx\">\n      <CopyToOutputDirectory>Always</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "testassets/BenchmarkApp/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Net.Http;\nusing System.Net;\nusing System.Security.Cryptography.X509Certificates;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Crank.EventSources;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Forwarder;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nBenchmarksEventSource.MeasureAspNetVersion();\nBenchmarksEventSource.MeasureNetCoreAppVersion();\n\nvar config = new ConfigurationBuilder()\n    .AddEnvironmentVariables(prefix: \"ASPNETCORE_\")\n    .AddCommandLine(args)\n    .AddJsonFile(\"appsettings.json\", optional: true)\n    .Build();\n\nvar builder = new WebHostBuilder()\n    .ConfigureLogging(loggerFactory =>\n    {\n        if (Enum.TryParse(config[\"LogLevel\"], out LogLevel logLevel))\n        {\n            Console.WriteLine($\"Console Logging enabled with level '{logLevel}'\");\n            loggerFactory.AddConsole().SetMinimumLevel(logLevel);\n        }\n    })\n    .UseKestrel((context, kestrelOptions) =>\n    {\n        kestrelOptions.ConfigureHttpsDefaults(httpsOptions =>\n        {\n            httpsOptions.ServerCertificate = new X509Certificate2(Path.Combine(context.HostingEnvironment.ContentRootPath, \"testCert.pfx\"), \"testPassword\");\n        });\n    })\n    .UseContentRoot(Directory.GetCurrentDirectory())\n    .UseConfiguration(config)\n    .ConfigureServices(services =>\n    {\n        services.AddHttpForwarder();\n    })\n    ;\n\nbuilder.Configure(app =>\n{\n    var forwarder = app.ApplicationServices.GetRequiredService<IHttpForwarder>();\n    var clusterUrl = GetClusterUrl();\n    var httpClient = new HttpMessageInvoker(CreateHandler());\n    var transformer = CreateHttpTransformer(app);\n\n    app.Run(async context =>\n    {\n        await forwarder.SendAsync(context, clusterUrl, httpClient, ForwarderRequestConfig.Empty, transformer);\n    });\n});\n\nbuilder.Build().Run();\n\nstring GetClusterUrl()\n{\n    var clusterUrls = config[\"clusterUrls\"];\n\n    if (string.IsNullOrWhiteSpace(clusterUrls))\n    {\n        throw new ArgumentException(\"--clusterUrls is required\");\n    }\n\n    var clusterUrl = clusterUrls.Split(';')[0];\n\n    Console.WriteLine($\"ClusterUrl: {clusterUrl}\");\n\n    return clusterUrl;\n}\n\nstatic SocketsHttpHandler CreateHandler()\n{\n    var handler = new SocketsHttpHandler\n    {\n        UseProxy = false,\n        AllowAutoRedirect = false,\n        AutomaticDecompression = DecompressionMethods.None,\n        UseCookies = false,\n        EnableMultipleHttp2Connections = true,\n        ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current),\n        ConnectTimeout = TimeSpan.FromSeconds(15),\n    };\n\n    handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };\n\n    return handler;\n}\n\nstatic HttpTransformer CreateHttpTransformer(IApplicationBuilder app)\n{\n    var transformBuilder = app.ApplicationServices.GetRequiredService<ITransformBuilder>();\n\n    return transformBuilder.Create(context =>\n    {\n        context.UseDefaultForwarders = false;\n    });\n}\n"
  },
  {
    "path": "testassets/BenchmarkApp/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"BenchmarkApp\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"--urls http://localhost:5000 --clusterUrls http://httpbin.org\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "testassets/BenchmarkApp/README.md",
    "content": "### Crank command to test against a local BenchmarkServer\n\n1. Follow the [Crank Getting Started Guide](https://github.com/dotnet/crank/blob/master/docs/getting_started.md) to install Microsoft.Crank.Controller and Microsoft.Crank.Agent globally.\n2. In one shell, run `crank-agent`\n3. In another shell, run `crank` as follows:\n\n```bash\ncrank `\n     --config https://raw.githubusercontent.com/aspnet/Benchmarks/master/scenarios/proxy.benchmarks.yml `\n     --scenario proxy-yarp `\n     --profile local `\n     --load.variables.duration 5 `\n     --variable path=/?s=1024 `\n     --variable serverScheme=https `\n     --variable downstreamScheme=https `\n     --load.variables.transport http2 `\n     --downstream.variables.httpProtocol http2\n```\n"
  },
  {
    "path": "testassets/BenchmarkApp/appsettings.json",
    "content": "{\n  \"AllowedHosts\": \"*\",\n  \"LogLevel\": \"\"\n}\n"
  },
  {
    "path": "testassets/Directory.Build.props",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project>\n  <!-- Recurse up. -->\n  <Import Project=\"$(MSBuildThisFileDirectory)..\\Directory.Build.props\" />\n\n  <PropertyGroup>\n    <IsSampleProject>true</IsSampleProject>\n    <WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "testassets/ReverseProxy.Code/Controllers/HealthController.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Mvc;\nusing Yarp.ReverseProxy.Health;\n\nnamespace Yarp.ReverseProxy.Sample.Controllers;\n\n/// <summary>\n/// Controller for health check api.\n/// </summary>\n[ApiController]\npublic class HealthController : ControllerBase\n{\n    private readonly IActiveHealthCheckMonitor _healthCheckMonitor;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"HealthController\" /> class.\n    /// </summary>\n    public HealthController(IActiveHealthCheckMonitor healthCheckMonitor)\n    {\n        _healthCheckMonitor = healthCheckMonitor;\n    }\n\n    /// <summary>\n    /// Returns 200 if Proxy is healthy.\n    /// </summary>\n    [HttpGet]\n    [Route(\"/api/health\")]\n    public IActionResult CheckHealth()\n    {\n        // TODO: Implement health controller, use guid in route.\n        return _healthCheckMonitor.InitialProbeCompleted ? Ok() : StatusCode(503);\n    }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Code/ForwarderMetricsConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing Yarp.Telemetry.Consumption;\n\nnamespace Yarp.ReverseProxy.Sample;\n\npublic sealed class ForwarderMetricsConsumer : IMetricsConsumer<ForwarderMetrics>\n{\n    public void OnMetrics(ForwarderMetrics previous, ForwarderMetrics current)\n    {\n        var elapsed = current.Timestamp - previous.Timestamp;\n        var newRequests = current.RequestsStarted - previous.RequestsStarted;\n        Console.Title = $\"Proxied {current.RequestsStarted} requests ({newRequests} in the last {(int)elapsed.TotalMilliseconds} ms)\";\n    }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Code/ForwarderTelemetryConsumer.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.Telemetry.Consumption;\n\nnamespace Yarp.ReverseProxy.Sample;\n\npublic sealed class ForwarderTelemetryConsumer : IForwarderTelemetryConsumer\n{\n    private readonly IHttpContextAccessor _httpContextAccessor;\n    private readonly AsyncLocal<DateTime?> _startTime = new();\n\n    public ForwarderTelemetryConsumer(IHttpContextAccessor httpContextAccessor)\n    {\n        _httpContextAccessor = httpContextAccessor;\n    }\n\n    public void OnForwarderStart(DateTime timestamp, string destinationPrefix)\n    {\n        _startTime.Value = timestamp;\n    }\n\n    public void OnForwarderStop(DateTime timestamp, int statusCode)\n    {\n        if (_startTime.Value is DateTime startTime)\n        {\n            var elapsed = timestamp - startTime;\n            var path = _httpContextAccessor.HttpContext.Request.Path;\n            Console.WriteLine($\"Spent {elapsed.TotalMilliseconds:N2} ms proxying {path}\");\n        }\n    }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Code/MyTransformFactory.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net.Http;\nusing Yarp.ReverseProxy.Transforms;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Sample;\n\ninternal sealed class MyTransformFactory : ITransformFactory\n{\n    public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(\"CustomTransform\", out var value))\n        {\n            if (string.IsNullOrEmpty(value))\n            {\n                context.Errors.Add(new ArgumentException(\"A non-empty CustomTransform value is required\"));\n            }\n\n            return true; // Matched\n        }\n        return false;\n    }\n\n    public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)\n    {\n        if (transformValues.TryGetValue(\"CustomTransform\", out var value))\n        {\n            if (string.IsNullOrEmpty(value))\n            {\n                throw new ArgumentException(\"A non-empty CustomTransform value is required\");\n            }\n\n            context.AddRequestTransform(transformContext =>\n            {\n                transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey<string>(\"CustomTransform\"), value);\n                return default;\n            });\n\n            return true;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Code/MyTransformProvider.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.Http;\nusing Yarp.ReverseProxy.Transforms;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nnamespace Yarp.ReverseProxy.Sample;\n\ninternal sealed class MyTransformProvider : ITransformProvider\n{\n    public void ValidateRoute(TransformRouteValidationContext context)\n    {\n        // Check all routes for a custom property and validate the associated transform data.\n        if (context.Route.Metadata?.TryGetValue(\"CustomMetadata\", out var value) ?? false)\n        {\n            if (string.IsNullOrEmpty(value))\n            {\n                context.Errors.Add(new ArgumentException(\"A non-empty CustomMetadata value is required\"));\n            }\n        }\n    }\n\n    public void ValidateCluster(TransformClusterValidationContext context)\n    {\n        // Check all clusters for a custom property and validate the associated transform data.\n        if (context.Cluster.Metadata?.TryGetValue(\"CustomMetadata\", out var value) ?? false)\n        {\n            if (string.IsNullOrEmpty(value))\n            {\n                context.Errors.Add(new ArgumentException(\"A non-empty CustomMetadata value is required\"));\n            }\n        }\n    }\n\n    public void Apply(TransformBuilderContext transformBuildContext)\n    {\n        // Check all routes for a custom property and add the associated transform.\n        if ((transformBuildContext.Route.Metadata?.TryGetValue(\"CustomMetadata\", out var value) ?? false)\n            || (transformBuildContext.Cluster?.Metadata?.TryGetValue(\"CustomMetadata\", out value) ?? false))\n        {\n            if (string.IsNullOrEmpty(value))\n            {\n                throw new ArgumentException(\"A non-empty CustomMetadata value is required\");\n            }\n\n            transformBuildContext.AddRequestTransform(transformContext =>\n            {\n                transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey<string>(\"CustomMetadata\"), value);\n                return default;\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Code/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Net.Http.Headers;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.ReverseProxy;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Sample;\nusing Yarp.ReverseProxy.Transforms;\nusing Yarp.Telemetry.Consumption;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nvar services = builder.Services;\nservices.AddControllers();\nvar routes = new[]\n{\n    new RouteConfig()\n    {\n        RouteId = \"route1\",\n        ClusterId = \"cluster1\",\n        Match = new RouteMatch\n        {\n            Path = \"{**catch-all}\"\n        },\n        Timeout = TimeSpan.FromSeconds(5),\n    }\n};\nvar clusters = new[]\n{\n    new ClusterConfig()\n    {\n        ClusterId = \"cluster1\",\n        SessionAffinity = new SessionAffinityConfig { Enabled = true, Policy = \"Cookie\", AffinityKeyName = \".Yarp.ReverseProxy.Affinity\" },\n        Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n        {\n            { \"destination1\", new DestinationConfig() { Address = \"https://localhost:10000\" } }\n        }\n    },\n    new ClusterConfig()\n    {\n        ClusterId = \"cluster2\",\n        Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)\n        {\n            { \"destination2\", new DestinationConfig() { Address = \"https://localhost:10001\" } }\n        }\n    }\n};\n\nservices.AddReverseProxy()\n    .LoadFromMemory(routes, clusters)\n    .ConfigureHttpClient((context, handler) =>\n    {\n        handler.Expect100ContinueTimeout = TimeSpan.FromMilliseconds(300);\n    })\n    .AddTransformFactory<MyTransformFactory>()\n    .AddTransforms<MyTransformProvider>()\n    .AddTransforms(transformBuilderContext =>\n    {\n        // For each route+cluster pair decide if we want to add transforms, and if so, which?\n        // This logic is re-run each time a route is rebuilt.\n\n        // transformBuilderContext.AddPathPrefix(\"/prefix\");\n\n        // Only do this for routes that require auth.\n        if (string.Equals(\"token\", transformBuilderContext.Route.AuthorizationPolicy))\n        {\n            transformBuilderContext.AddRequestTransform(async transformContext =>\n            {\n                // AuthN and AuthZ will have already been completed after request routing.\n                var ticket = await transformContext.HttpContext.AuthenticateAsync(\"token\");\n                var tokenService = transformContext.HttpContext.RequestServices.GetRequiredService<TokenService>();\n                var token = await tokenService.GetAuthTokenAsync(ticket.Principal);\n                transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", token);\n            });\n        }\n\n        transformBuilderContext.AddResponseTransform(context =>\n        {\n            // Suppress the response body from errors.\n            // The status code was already copied.\n            if (context.ProxyResponse?.IsSuccessStatusCode == false)\n            {\n                context.SuppressResponseBody = true;\n            }\n\n            return default;\n        });\n    });\n\nservices.AddHttpContextAccessor();\nservices.AddSingleton<IMetricsConsumer<ForwarderMetrics>, ForwarderMetricsConsumer>();\nservices.AddTelemetryConsumer<ForwarderTelemetryConsumer>();\nservices.AddTelemetryListeners();\nservices.AddRequestTimeouts(o =>\n{\n    o.DefaultPolicy = new Microsoft.AspNetCore.Http.Timeouts.RequestTimeoutPolicy()\n    {\n        Timeout = TimeSpan.FromSeconds(1),\n        TimeoutStatusCode = StatusCodes.Status418ImATeapot,\n    };\n});\n\nvar app = builder.Build();\n\napp.UseAuthorization();\napp.UseRequestTimeouts();\n\napp.MapControllers();\napp.MapReverseProxy(proxyPipeline =>\n{\n    // Custom endpoint selection\n    proxyPipeline.Use((context, next) =>\n    {\n        var lookup = context.RequestServices.GetRequiredService<IProxyStateLookup>();\n        if (lookup.TryGetCluster(\"cluster2\", out var cluster))\n        {\n            context.ReassignProxyRequest(cluster);\n        }\n\n        var someCriteria = false; // MeetsCriteria(context);\n        if (someCriteria)\n        {\n            var availableDestinationsFeature = context.Features.Get<IReverseProxyFeature>();\n            var destination = availableDestinationsFeature.AvailableDestinations[0]; // PickDestination(availableDestinationsFeature.Destinations);\n                                                                                     // Load balancing will no-op if we've already reduced the list of available destinations to 1.\n            availableDestinationsFeature.AvailableDestinations = destination;\n        }\n\n        return next();\n    });\n    proxyPipeline.UseSessionAffinity();\n    proxyPipeline.UseLoadBalancing();\n});\n\napp.Run();\n"
  },
  {
    "path": "testassets/ReverseProxy.Code/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"https://localhost:44356/\",\n      \"sslPort\": 44356\n    }\n  },\n  \"profiles\": {\n    \"ReverseProxy.Code\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Code/ReverseProxy.Code.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(TestTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>Yarp.ReverseProxy.Sample</RootNamespace>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\TelemetryConsumption\\Yarp.Telemetry.Consumption.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "testassets/ReverseProxy.Code/TokenService.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Security.Claims;\nusing System.Threading.Tasks;\n\nnamespace Yarp.ReverseProxy.Sample;\n\ninternal sealed class TokenService\n{\n    internal Task<string> GetAuthTokenAsync(ClaimsPrincipal user)\n    {\n        return Task.FromResult(user.Identity.Name);\n    }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Code/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Debug\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Code/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Config/Controllers/HealthController.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Yarp.ReverseProxy.Sample.Controllers;\n\n/// <summary>\n/// Controller for health check api.\n/// </summary>\n[ApiController]\npublic class HealthController : ControllerBase\n{\n    /// <summary>\n    /// Returns 200 if Proxy is healthy.\n    /// </summary>\n    [HttpGet]\n    [Route(\"/api/health\")]\n    public IActionResult CheckHealth()\n    {\n        return Ok();\n    }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Config/CustomConfigFilter.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Configuration;\nusing Yarp.ReverseProxy.Configuration;\nusing Yarp.ReverseProxy.Health;\n\nnamespace Yarp.ReverseProxy.Sample;\n\npublic sealed class CustomConfigFilter : IProxyConfigFilter\n{\n    public ValueTask<ClusterConfig> ConfigureClusterAsync(ClusterConfig cluster, CancellationToken cancel)\n    {\n        // How to use custom metadata to configure clusters\n        if (cluster.Metadata?.TryGetValue(\"CustomHealth\", out var customHealth) ?? false\n            && string.Equals(customHealth, \"true\", StringComparison.OrdinalIgnoreCase))\n        {\n            cluster = cluster with\n            {\n                HealthCheck = new HealthCheckConfig\n                {\n                    Active = new ActiveHealthCheckConfig\n                    {\n                        Enabled = true,\n                        Policy = HealthCheckConstants.ActivePolicy.ConsecutiveFailures,\n                    },\n                    Passive = cluster.HealthCheck?.Passive,\n                }\n            };\n        }\n\n        // Or wrap the metadata in config sugar\n        var config = new ConfigurationBuilder().AddInMemoryCollection(cluster.Metadata).Build();\n        if (config.GetValue<bool>(\"CustomHealth\"))\n        {\n            cluster = cluster with\n            {\n                HealthCheck = new HealthCheckConfig\n                {\n                    Active = new ActiveHealthCheckConfig\n                    {\n                        Enabled = true,\n                        Policy = HealthCheckConstants.ActivePolicy.ConsecutiveFailures,\n                    },\n                    Passive = cluster.HealthCheck?.Passive,\n                }\n            };\n        }\n\n        return new ValueTask<ClusterConfig>(cluster);\n    }\n\n    public ValueTask<RouteConfig> ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel)\n    {\n        // Do not let config based routes take priority over code based routes.\n        // Lower numbers are higher priority. Code routes default to 0.\n        if (route.Order.HasValue && route.Order.Value < 1)\n        {\n            return new ValueTask<RouteConfig>(route with { Order = 1 });\n        }\n\n        return new ValueTask<RouteConfig>(route);\n    }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Config/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\nusing Yarp.ReverseProxy.Model;\nusing Yarp.ReverseProxy.Sample;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddControllers();\nbuilder.Services.AddReverseProxy()\n    .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy\"))\n    .LoadFromConfig(builder.Configuration.GetSection(\"ReverseProxy2\"))\n    .AddConfigFilter<CustomConfigFilter>();\n\nvar app = builder.Build();\n\napp.MapControllers();\napp.MapReverseProxy(proxyPipeline =>\n{\n    // Custom endpoint selection\n    proxyPipeline.Use((context, next) =>\n    {\n        var someCriteria = false; // MeetsCriteria(context);\n        if (someCriteria)\n        {\n            var availableDestinationsFeature = context.Features.Get<IReverseProxyFeature>();\n            var destination = availableDestinationsFeature.AvailableDestinations[0]; // PickDestination(availableDestinationsFeature.Destinations);\n                                                                                     // Load balancing will no-op if we've already reduced the list of available destinations to 1.\n            availableDestinationsFeature.AvailableDestinations = destination;\n        }\n\n        return next();\n    });\n    proxyPipeline.UseSessionAffinity();\n    proxyPipeline.UseLoadBalancing();\n    proxyPipeline.UsePassiveHealthChecks();\n}).ConfigureEndpoints((builder, route) => builder.WithDisplayName($\"ReverseProxy {route.RouteId}-{route.ClusterId}\"));\n\napp.Run();\n"
  },
  {
    "path": "testassets/ReverseProxy.Config/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"https://localhost:44356/\",\n      \"sslPort\": 44356\n    }\n  },\n  \"profiles\": {\n    \"ReverseProxy.Config\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Config/ReverseProxy.Config.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(TestTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>Yarp.ReverseProxy.Sample</RootNamespace>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "testassets/ReverseProxy.Config/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Debug\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Config/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"Kestrel\": {\n    \"Endpoints\": {\n      \"https\": {\n        \"Url\": \"https://localhost:5001\"\n      },\n      \"http\": {\n        \"Url\": \"http://localhost:5000\"\n      }\n    }\n  },\n  \"ReverseProxy\": {\n    \"Clusters\": {\n      \"cluster1\": {\n        \"LoadBalancingPolicy\": \"Random\",\n        \"SessionAffinity\": {\n          \"Enabled\": true,\n          \"Policy\": \"Cookie\",\n          \"AffinityKeyName\": \".Yarp.Affinity\"\n        },\n        \"HealthCheck\": {\n          \"Active\": {\n            \"Enabled\": true,\n            \"Interval\": \"00:00:10\",\n            \"Timeout\": \"00:00:10\",\n            \"Policy\": \"ConsecutiveFailures\",\n            \"Path\": \"/api/health\"\n          },\n          \"Passive\": {\n            \"Enabled\": true,\n            \"Policy\": \"TransportFailureRate\",\n            \"ReactivationPeriod\": \"00:05:00\"\n          }\n        },\n        \"Metadata\": {\n          \"ConsecutiveFailuresHealthPolicy.Threshold\": \"3\",\n          \"TransportFailureRateHealthPolicy.RateLimit\": \"0.5\"\n        },\n        \"Destinations\": {\n          \"cluster1/destination1\": {\n            \"Address\": \"https://localhost:10000/\"\n          },\n          \"cluster1/destination2\": {\n            \"Address\": \"http://localhost:10010/\"\n          }\n        }\n      },\n      \"cluster2\": {\n        \"Metadata\": {\n          \"CustomHealth\": \"true\"\n        },\n        \"Destinations\": {\n          \"cluster2/destination1\": {\n            \"Address\": \"https://localhost:10001/\",\n            \"Health\": \"https://localhost:10001/api/health\"\n          }\n        }\n      }\n    },\n    \"Routes\": {\n      \"route1\": {\n        \"ClusterId\": \"cluster1\",\n        \"Match\": {\n          \"Methods\": [ \"GET\", \"POST\" ],\n          \"Hosts\": [ \"localhost\" ],\n          \"Path\": \"/api/{action}\"\n        }\n      },\n      \"route2\": {\n        \"ClusterId\": \"cluster2\",\n        \"Match\": {\n          \"Hosts\": [ \"localhost\" ],\n          \"Path\": \"/api/{plugin}/stuff/{**remainder}\"\n        },\n        \"Transforms\": [\n          { \"PathPattern\": \"/foo/{plugin}/bar/{**remainder}\" },\n          {\n            \"X-Forwarded\": \"Append\",\n            \"HeaderPrefix\": \"X-Forwarded-\"\n          },\n          {\n            \"Forwarded\": \"by,host,for,proto\",\n            \"ByFormat\": \"Random\",\n            \"ForFormat\": \"IpAndPort\"\n          },\n          { \"ClientCert\": \"X-Client-Cert\" },\n\n          { \"RequestHeadersCopy\": true },\n          { \"RequestHeaderOriginalHost\": true },\n          {\n            \"RequestHeader\": \"foo0\",\n            \"Append\": \"bar\"\n          },\n          {\n            \"RequestHeader\": \"foo1\",\n            \"Set\": \"bar, baz\"\n          },\n          {\n            \"RequestHeader\": \"clearMe\",\n            \"Set\": \"\"\n          },\n          {\n            \"ResponseHeader\": \"foo\",\n            \"Append\": \"bar\",\n            \"When\": \"Always\"\n          },\n          {\n            \"ResponseTrailer\": \"foo\",\n            \"Append\": \"trailer\",\n            \"When\": \"Always\"\n          }\n        ]\n      }\n    }\n  },\n  \"ReverseProxy2\": {\n    \"Clusters\": {\n      \"cluster3\": {\n        \"LoadBalancingPolicy\": \"Random\",\n        \"SessionAffinity\": {\n          \"Enabled\": \"true\",\n          \"Policy\": \"Cookie\",\n          \"AffinityKeyName\": \".Yarp.Affinity\"\n        },\n        \"HealthCheck\": {\n          \"Active\": {\n            \"Enabled\": \"true\",\n            \"Interval\": \"00:00:10\",\n            \"Timeout\": \"00:00:10\",\n            \"Policy\": \"ConsecutiveFailures\",\n            \"Path\": \"/api/health\"\n          },\n          \"Passive\": {\n            \"Enabled\": \"true\",\n            \"Policy\": \"TransportFailureRate\",\n            \"ReactivationPeriod\": \"00:05:00\"\n          }\n        },\n        \"Metadata\": {\n          \"ConsecutiveFailuresHealthPolicy.Threshold\": \"3\",\n          \"TransportFailureRateHealthPolicy.RateLimit\": \"0.5\"\n        },\n        \"Destinations\": {\n          \"cluster1/destination1\": {\n            \"Address\": \"https://localhost:10000/\"\n          },\n          \"cluster1/destination2\": {\n            \"Address\": \"http://localhost:10010/\"\n          }\n        }\n      }\n    },\n    \"Routes\": {\n      \"route3\": {\n        \"ClusterId\": \"cluster3\",\n        \"Match\": {\n          \"Methods\": [ \"GET\", \"POST\" ],\n          \"Hosts\": [ \"localhost\" ],\n          \"Path\": \"/api2/{action}\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Direct/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.AspNetCore.Connections;\nusing Yarp.ReverseProxy.Sample;\nusing Yarp.ReverseProxy.Forwarder;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Yarp.ReverseProxy.Transforms;\nusing System.Net.Http;\nusing System.Threading;\nusing System;\nusing System.Net;\nusing System.Diagnostics;\nusing Yarp.ReverseProxy.Transforms.Builder;\n\nvar builder = WebApplication.CreateBuilder(args);\nbuilder.WebHost.ConfigureKestrel(kestrel =>\n{\n    var logger = kestrel.ApplicationServices.GetRequiredService<ILogger<Program>>();\n    kestrel.ListenAnyIP(5001, portOptions =>\n    {\n        portOptions.Use(async (connectionContext, next) =>\n        {\n            await TlsFilter.ProcessAsync(connectionContext, next, logger);\n        });\n        portOptions.UseHttps();\n    });\n});\n\nbuilder.Services.AddHttpForwarder();\n\nvar app = builder.Build();\n\nvar httpClient = new HttpMessageInvoker(new SocketsHttpHandler\n{\n    UseProxy = false,\n    AllowAutoRedirect = false,\n    AutomaticDecompression = DecompressionMethods.None,\n    UseCookies = false,\n    EnableMultipleHttp2Connections = true,\n    ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current),\n    ConnectTimeout = TimeSpan.FromSeconds(15),\n});\n\nvar transformBuilder = app.Services.GetRequiredService<ITransformBuilder>();\nvar transformer = transformBuilder.Create(context =>\n{\n    context.AddQueryRemoveKey(\"param1\");\n    context.AddQueryValue(\"area\", \"xx2\", false);\n    context.AddOriginalHost(false);\n});\n\n// or var transformer = new CustomTransformer();\n// or var transformer = HttpTransformer.Default;\n\nvar requestConfig = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(100) };\n\napp.MapForwarder(\"/{**catch-all}\", \"https://example.com\", requestConfig, transformer, httpClient);\n\napp.Run();\n\ninternal sealed class CustomTransformer : HttpTransformer\n{\n    public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken)\n    {\n        // Copy all request headers\n        await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken);\n\n        // Customize the query string:\n        var queryContext = new QueryTransformContext(httpContext.Request);\n        queryContext.Collection.Remove(\"param1\");\n        queryContext.Collection[\"area\"] = \"xx2\";\n\n        // Assign the custom uri. Be careful about extra slashes when concatenating here.\n        proxyRequest.RequestUri = new Uri(destinationPrefix + httpContext.Request.Path + queryContext.QueryString);\n\n        // Suppress the original request header, use the one from the destination Uri.\n        proxyRequest.Headers.Host = null;\n    }\n\n    public override ValueTask<bool> TransformResponseAsync(HttpContext httpContext, HttpResponseMessage proxyResponse, CancellationToken cancellationToken)\n    {\n        // Suppress the response body from errors.\n        // The status code was already copied.\n        if (!proxyResponse.IsSuccessStatusCode)\n        {\n            return new ValueTask<bool>(false);\n        }\n\n        return base.TransformResponseAsync(httpContext, proxyResponse, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Direct/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"https://localhost:44356/\",\n      \"sslPort\": 44356\n    }\n  },\n  \"profiles\": {\n    \"ReverseProxy.Direct\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Direct/ReverseProxy.Direct.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFrameworks>$(TestTFMs)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>Yarp.ReverseProxy.Sample</RootNamespace>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ReverseProxy\\Yarp.ReverseProxy.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "testassets/ReverseProxy.Direct/TlsFilter.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Buffers;\nusing System.IO.Pipelines;\nusing System.Security.Authentication;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Connections;\nusing Microsoft.Extensions.Logging;\nusing Yarp.ReverseProxy.Utilities.Tls;\n\nnamespace Yarp.ReverseProxy.Sample;\n\npublic static class TlsFilter\n{\n    // Use reasonable limits. Parsing across multiple segments has an O(N^2) worst case, so limit the N.\n    private const int ClientHelloTimeoutMs = 10_000;\n    private const int MaxClientHelloSize = 10 * 1024; // 10 KB\n\n    // This sniffs the TLS handshake and rejects requests that meat specific criteria.\n    internal static async Task ProcessAsync(ConnectionContext connectionContext, Func<Task> next, ILogger logger)\n    {\n        using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(connectionContext.ConnectionClosed))\n        {\n            timeoutCts.CancelAfter(ClientHelloTimeoutMs);\n\n            var input = connectionContext.Transport.Input;\n\n            // Count how many bytes we've examined so we never go backwards, Pipes don't allow that.\n            var minBytesExamined = 0L;\n\n            while (true)\n            {\n                var result = await input.ReadAsync(timeoutCts.Token);\n                var buffer = result.Buffer;\n\n                if (result.IsCompleted || result.IsCanceled)\n                {\n                    return;\n                }\n\n                if (buffer.Length == 0)\n                {\n                    continue;\n                }\n\n                if (!TryReadTlsFrame(buffer, logger, out var frameInfo) && frameInfo.ParsingStatus == TlsFrameHelper.ParsingStatus.IncompleteFrame)\n                {\n                    // We didn't find a TLS frame, we need to read more data.\n                    minBytesExamined = buffer.Length;\n\n                    if (minBytesExamined >= MaxClientHelloSize)\n                    {\n                        logger.LogInformation(\"Client Hello too large. Aborting.\");\n                        return;\n                    }\n\n                    input.AdvanceTo(buffer.Start, buffer.End);\n                    continue;\n                }\n\n                // We're done. We either have a frame we can analyze, or we're giving up.\n                var examined = buffer.Slice(buffer.Start, minBytesExamined).End;\n                input.AdvanceTo(buffer.Start, examined);\n\n                if (frameInfo.ParsingStatus != TlsFrameHelper.ParsingStatus.Ok || frameInfo.HandshakeType != TlsHandshakeType.ClientHello)\n                {\n                    logger.LogInformation(\"Invalid or unexpected TLS frame. Aborting.\");\n                    return;\n                }\n\n                // Perform any additional validation on the Client Hello here.\n                // Rate limiting, throttling checks, J4A fingerprinting, logging, etc. can be performed here as well.\n\n                if (!TryProcessClientHello(frameInfo, logger))\n                {\n                    // Abort the connection.\n                    return;\n                }\n\n                // All checks passed, we can continue processing the request.\n\n#if !NET10_0_OR_GREATER\n                // Workaround for https://github.com/dotnet/runtime/issues/107213, which was fixed in .NET 10.\n                if (minBytesExamined > 0)\n                {\n                    connectionContext.Transport = new DuplexPipe(\n                        PipeReader.Create(input.AsStream(), new StreamPipeReaderOptions(bufferSize: Math.Max(4096, (int)minBytesExamined))),\n                        connectionContext.Transport.Output);\n                }\n#endif\n\n                break;\n            }\n        }\n\n        await next();\n    }\n\n    /// <summary>Process the Client Hello and returns whether it passed validation.</summary>\n    private static bool TryProcessClientHello(TlsFrameHelper.TlsFrameInfo clientHello, ILogger logger)\n    {\n        // This is a sample demonstrating several checks you can perform on the Client Hello.\n        // Replace the logic in this method with your own validation logic.\n\n        string sni = clientHello.TargetName;\n\n        if (string.IsNullOrEmpty(sni))\n        {\n            logger.LogInformation(\"Expected SNI to be specified.\");\n            return false;\n        }\n\n        if (!AllowHost(sni))\n        {\n            logger.LogInformation(\"Unexpected SNI: {sni}.\", sni);\n            return false;\n        }\n\n        if (!clientHello.SupportedVersions.HasFlag(SslProtocols.Tls12) && !clientHello.SupportedVersions.HasFlag(SslProtocols.Tls13))\n        {\n            logger.LogInformation(\"Client for '{sni}' does not support TLS 1.2 or 1.3.\", sni);\n            return false;\n        }\n\n        if (!clientHello.ApplicationProtocols.HasFlag(TlsFrameHelper.ApplicationProtocolInfo.Http2))\n        {\n            logger.LogInformation(\"Client for '{sni}' does not support HTTP/2.\", sni);\n            return false;\n        }\n\n        // All checks passed, we can continue processing the request.\n        return true;\n    }\n\n    private static bool AllowHost(string targetName)\n    {\n        return\n            targetName.Equals(\"localhost\", StringComparison.OrdinalIgnoreCase) ||\n            targetName.Equals(\"contoso.com\", StringComparison.OrdinalIgnoreCase);\n    }\n\n    /// <summary>Attempt to parse the first TLS frame from the <paramref name=\"buffer\"/> and indicate whether more data is needed.</summary>\n    private static bool TryReadTlsFrame(ReadOnlySequence<byte> buffer, ILogger logger, out TlsFrameHelper.TlsFrameInfo frame)\n    {\n        frame = default;\n\n        // Try to process the first segment first.\n        var data = buffer.First.Span;\n\n        if (TlsFrameHelper.TryGetFrameInfo(data, ref frame))\n        {\n            // This is the common fast path.\n            return true;\n        }\n\n        if (frame.ParsingStatus != TlsFrameHelper.ParsingStatus.IncompleteFrame)\n        {\n            // The input is invalid, reading more data won't help.\n            return false;\n        }\n\n        if (buffer.IsSingleSegment)\n        {\n            // We only have one segment and it didn't contain a valid TLS frame. We'll have to read more data.\n            return false;\n        }\n\n        // We have multiple segments. TlsFrameHelper only works with a single span, so we need to combine them.\n        // This may happen on every new read, which is why we limit how much data we're willing to process.\n\n        var pooledBuffer = ArrayPool<byte>.Shared.Rent((int)buffer.Length);\n        buffer.CopyTo(pooledBuffer);\n        data = pooledBuffer.AsSpan(0, (int)buffer.Length);\n\n        bool success = TlsFrameHelper.TryGetFrameInfo(data, ref frame);\n\n        ArrayPool<byte>.Shared.Return(pooledBuffer);\n\n        if (success)\n        {\n            logger.LogDebug(\"Parsed multi-segment TLS frame after {length} bytes\", buffer.Length);\n        }\n\n        return success;\n    }\n\n    private sealed class DuplexPipe(PipeReader input, PipeWriter output) : IDuplexPipe\n    {\n        public PipeReader Input { get; } = input;\n        public PipeWriter Output { get; } = output;\n    }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Direct/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"Microsoft\": \"Debug\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "testassets/ReverseProxy.Direct/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "testassets/TestClient/CommandLineArgs.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\n\nnamespace SampleClient;\n\ninternal sealed class CommandLineArgs\n{\n    private CommandLineArgs()\n    {\n    }\n\n    public bool Help { get; private set; }\n\n    public string Scenario { get; private set; }\n\n    public string Target { get; private set; } = \"https://localhost:1443/\";\n\n    public static CommandLineArgs Parse(string[] args)\n    {\n        var result = new CommandLineArgs();\n\n        var i = 0;\n        for (i = 0; i < args.Length; i++)\n        {\n            switch (args[i])\n            {\n                case \"--help\":\n                case \"-h\":\n                case \"-?\":\n                case \"/?\":\n                    result.Help = true;\n                    break;\n                case \"--scenario\":\n                case \"-s\":\n                    result.Scenario = args[++i];\n                    break;\n                case \"--target\":\n                case \"-t\":\n                    result.Target = args[++i];\n                    break;\n            }\n        }\n\n        if (i < args.Length)\n        {\n            return ParseRemainder(result, args.AsSpan().Slice(i));\n        }\n\n        return result;\n\n        static CommandLineArgs ParseRemainder(CommandLineArgs result, Span<string> remainder)\n        {\n            if (remainder.Length == 0)\n            {\n                throw new ArgumentException(\"Expected additional args.\");\n            }\n\n            if (remainder.Length > 1)\n            {\n                throw new ArgumentException($\"Unexpected arg '{remainder[1]}'.\");\n            }\n\n            result.Scenario = remainder[0];\n            return result;\n        }\n    }\n\n    public static void ShowHelp()\n    {\n        Console.WriteLine(\"ReverseProxy SampleClient.\\n\");\n        Console.WriteLine(\"--scenario <name>, -s <name>: Runs only the specified scenario.\");\n        Console.WriteLine(\n            \"--target <uri>, -t <uri>: Sets the target uri. By default, 'https://localhost:1443/' is used.\");\n        Console.WriteLine(\"--help, -h, -?, /?: Shows this help information.\");\n    }\n}\n"
  },
  {
    "path": "testassets/TestClient/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing SampleClient;\nusing SampleClient.Scenarios;\n\nCommandLineArgs parsedArgs = null;\ntry\n{\n    parsedArgs = CommandLineArgs.Parse(args);\n}\ncatch (ArgumentException)\n{\n    // Do nothing, we will show help right after.\n}\n\nif (parsedArgs is null || parsedArgs.Help)\n{\n    CommandLineArgs.ShowHelp();\n    return 1;\n}\n\nvar scenarioFactories = new Dictionary<string, Func<IScenario>>(StringComparer.OrdinalIgnoreCase) {\n    {\"Http1\", () => new Http1Scenario()},\n    {\"Http2\", () => new Http2Scenario()},\n    {\"Http2PostExpectContinue\", () => new Http2PostExpectContinueScenario()},\n    {\"RawUpgrade\", () => new RawUpgradeScenario()},\n    {\"WebSockets\", () => new WebSocketsScenario()},\n    {\"SessionAffinity\", () => new SessionAffinityScenario()}\n};\n\nif (string.IsNullOrEmpty(parsedArgs.Scenario))\n{\n    // Execute all scenarios\n    var success = true;\n    foreach (var kvp in scenarioFactories.OrderBy(kvp => kvp.Key))\n    {\n        Console.WriteLine();\n        Console.WriteLine($\"Executing scenario '{kvp.Key}'...\");\n        try\n        {\n            var scenario = kvp.Value();\n            await scenario.ExecuteAsync(parsedArgs, CancellationToken.None);\n        }\n        catch (Exception ex)\n        {\n            Console.WriteLine($\"Unexpected exception: {ex}\");\n            success = false;\n        }\n    }\n\n    Console.WriteLine();\n    Console.ForegroundColor = success ? ConsoleColor.Green : ConsoleColor.Red;\n    Console.WriteLine($\"All scenarios completed {(success ? \"successfully\" : \"with errors\")}.\");\n    Console.ResetColor();\n    Console.WriteLine(\"Press any key to exit.\");\n    Console.ReadKey();\n    return success ? 0 : 1;\n}\n\nif (!scenarioFactories.TryGetValue(parsedArgs.Scenario, out var scenarioFactory))\n{\n    Console.WriteLine($\"Unknown scenario '{parsedArgs.Scenario}'. Supported values: \");\n    foreach (var scenarioName in scenarioFactories.Keys.OrderBy(k => k))\n    {\n        Console.WriteLine($\"   {scenarioName}\");\n    }\n\n    Console.WriteLine();\n    return 1;\n}\n\nConsole.WriteLine($\"Executing scenario '{parsedArgs.Scenario}'.\");\ntry\n{\n    var scenario = scenarioFactory();\n    await scenario.ExecuteAsync(parsedArgs, CancellationToken.None);\n}\ncatch (Exception ex)\n{\n    Console.WriteLine($\"Unexpected exception: {ex}\");\n    return 1;\n}\n\nConsole.ForegroundColor = ConsoleColor.Green;\nConsole.WriteLine(\"All scenarios completed successfully!\");\nConsole.ResetColor();\nConsole.WriteLine(\"Press any key to exit.\");\nConsole.ReadKey();\nreturn 0;\n"
  },
  {
    "path": "testassets/TestClient/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"TestClient\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"-t https://localhost:5001\"\n    }\n  }\n}\n"
  },
  {
    "path": "testassets/TestClient/Scenarios/Http1Scenario.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Net;\nusing System.Net.Http;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace SampleClient.Scenarios;\n\ninternal sealed class Http1Scenario : IScenario\n{\n    public async Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation)\n    {\n        using var handler = new HttpClientHandler\n        {\n            AllowAutoRedirect = false,\n            AutomaticDecompression = DecompressionMethods.None,\n            UseCookies = false,\n            UseProxy = false\n        };\n        using var client = new HttpMessageInvoker(handler);\n        var targetUri = new Uri(new Uri(args.Target, UriKind.Absolute), \"api/dump\");\n        var stopwatch = Stopwatch.StartNew();\n        var request = new HttpRequestMessage(HttpMethod.Get, targetUri) { Version = new Version(1, 1) };\n        Console.WriteLine($\"Calling {targetUri} with HTTP/1.1\");\n\n        var response = await client.SendAsync(request, cancellation);\n        Console.WriteLine($\"Received response: {(int)response.StatusCode} in {stopwatch.ElapsedMilliseconds} ms\");\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(cancellation);\n        var json = JsonDocument.Parse(body);\n        Console.WriteLine(\n            \"Received response:\" +\n            $\"{Environment.NewLine}\" +\n            $\"{JsonSerializer.Serialize(json.RootElement, new JsonSerializerOptions { WriteIndented = true })}\");\n\n        response.EnsureSuccessStatusCode();\n    }\n}\n"
  },
  {
    "path": "testassets/TestClient/Scenarios/Http2PostExpectContinueScenario.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace SampleClient.Scenarios;\n\n/// <summary>\n/// Verifies that YARP correctly handles the case where the client specifies\n/// <c>Expect: 100-continue</c> and the destination fails early without accepting the request body.\n/// This scenario can be encountered in real world scenarios, usually when authentication fails on the destination.\n/// The <c>Expect: 100-continue</c> behavior causes the request body copy to not even start on YARP in this case.\n/// </summary>\ninternal sealed class Http2PostExpectContinueScenario : IScenario\n{\n    public async Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation)\n    {\n        using (var handler = new HttpClientHandler\n        {\n            AllowAutoRedirect = false,\n            AutomaticDecompression = DecompressionMethods.None,\n            UseCookies = false,\n            UseProxy = false,\n        })\n        using (var client = new HttpMessageInvoker(handler))\n        {\n            var targetUri = new Uri(new Uri(args.Target, UriKind.Absolute), \"api/skipbody\");\n            var stopwatch = Stopwatch.StartNew();\n            var request = new HttpRequestMessage(HttpMethod.Post, targetUri);\n            request.Version = new Version(2, 0);\n            request.Headers.ExpectContinue = true;\n            request.Content = new StringContent(new string('a', 1024 * 1024 * 10));\n            Console.WriteLine($\"Calling {targetUri} with HTTP/2\");\n\n            var response = await client.SendAsync(request, cancellation);\n            Console.WriteLine($\"Received response: {(int)response.StatusCode} in {stopwatch.ElapsedMilliseconds} ms\");\n            if (response.StatusCode != HttpStatusCode.Conflict)\n            {\n                throw new InvalidOperationException($\"Expected status 409 Conflict!\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "testassets/TestClient/Scenarios/Http2Scenario.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Net;\nusing System.Net.Http;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace SampleClient.Scenarios;\n\ninternal sealed class Http2Scenario : IScenario\n{\n    public async Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation)\n    {\n        using var handler = new HttpClientHandler\n        {\n            AllowAutoRedirect = false,\n            AutomaticDecompression = DecompressionMethods.None,\n            UseCookies = false,\n            UseProxy = false\n        };\n        using var client = new HttpMessageInvoker(handler);\n        var targetUri = new Uri(new Uri(args.Target, UriKind.Absolute), \"api/dump\");\n        var stopwatch = Stopwatch.StartNew();\n        var request = new HttpRequestMessage(HttpMethod.Get, targetUri) { Version = new Version(2, 0) };\n        Console.WriteLine($\"Calling {targetUri} with HTTP/2\");\n\n        var response = await client.SendAsync(request, cancellation);\n        Console.WriteLine($\"Received response: {(int)response.StatusCode} in {stopwatch.ElapsedMilliseconds} ms\");\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(cancellation);\n        var json = JsonDocument.Parse(body);\n        Console.WriteLine(\n            $\"Received response:\" +\n            $\"{Environment.NewLine}\" +\n            $\"{JsonSerializer.Serialize(json.RootElement, new JsonSerializerOptions { WriteIndented = true })}\");\n\n        response.EnsureSuccessStatusCode();\n    }\n}\n"
  },
  {
    "path": "testassets/TestClient/Scenarios/IScenario.cs",
    "content": "﻿// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace SampleClient.Scenarios;\n\n/// <summary>\n/// Interface for the implementation of a scenario that can be executed asynchronously.\n/// </summary>\ninternal interface IScenario\n{\n    /// <summary>\n    /// Executes the scenario asynchronously.\n    /// </summary>\n    Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation);\n}\n"
  },
  {
    "path": "testassets/TestClient/Scenarios/RawUpgradeScenario.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace SampleClient.Scenarios;\n\ninternal sealed class RawUpgradeScenario : IScenario\n{\n    public async Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation)\n    {\n        using var handler = new HttpClientHandler\n        {\n            AllowAutoRedirect = false,\n            AutomaticDecompression = DecompressionMethods.None,\n            UseCookies = false,\n            UseProxy = false\n        };\n        using var client = new HttpMessageInvoker(handler);\n        var targetUri = new Uri(new Uri(args.Target, UriKind.Absolute), \"api/rawupgrade\");\n        var stopwatch = Stopwatch.StartNew();\n        var request = new HttpRequestMessage(HttpMethod.Get, targetUri);\n        request.Headers.TryAddWithoutValidation(\"Connection\", \"upgrade\");\n        request.Version = new Version(1, 1);\n        Console.WriteLine($\"Calling {targetUri} with upgradable HTTP/1.1\");\n\n        var response = await client.SendAsync(request, cancellation);\n        Console.WriteLine($\"Received response: {(int)response.StatusCode} in {stopwatch.ElapsedMilliseconds} ms\");\n        if (response.StatusCode != HttpStatusCode.SwitchingProtocols)\n        {\n            throw new InvalidOperationException(\"Expected status 101 Switching Protocols!\");\n        }\n\n        var rawStream = await response.Content.ReadAsStreamAsync(cancellation);\n        Console.WriteLine(\"Acquired upgraded stream. Testing bidirectional echo...\");\n        stopwatch.Restart();\n        var buffer = new byte[1];\n        for (var i = 0; i <= 255; i++)\n        {\n            buffer[0] = (byte)i;\n            await rawStream.WriteAsync(buffer, cancellation);\n            var read = await rawStream.ReadAsync(buffer, cancellation);\n            if (i == 255)\n            {\n                if (read != 0)\n                {\n                    throw new Exception($\"Read {read} bytes, expected 0 after sending Goodbye.\");\n                }\n\n                Console.WriteLine();\n            }\n            else\n            {\n                if (read != 1)\n                {\n                    throw new Exception($\"Read {read} bytes, expected 1.\");\n                }\n\n                if (buffer[0] != i)\n                {\n                    throw new Exception($\"Received {buffer[0]}, expected {i}.\");\n                }\n\n                Console.Write(\".\");\n            }\n        }\n\n        Console.WriteLine($\"256 ping/pong's completed in {stopwatch.ElapsedMilliseconds} ms.\");\n    }\n}\n"
  },
  {
    "path": "testassets/TestClient/Scenarios/SessionAffinityScenario.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace SampleClient.Scenarios;\n\ninternal sealed class SessionAffinityScenario : IScenario\n{\n    public async Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation)\n    {\n        using var handler = new HttpClientHandler\n        {\n            AllowAutoRedirect = false,\n            AutomaticDecompression = DecompressionMethods.None,\n            // Session affinity key will be stored in a cookie\n            UseCookies = true,\n            UseProxy = false\n        };\n        using var client = new HttpMessageInvoker(handler);\n        var targetUri = new Uri(new Uri(args.Target, UriKind.Absolute), \"api/dump\");\n        var stopwatch = Stopwatch.StartNew();\n\n        var request0 = new HttpRequestMessage(HttpMethod.Get, targetUri) { Version = new Version(1, 1) };\n        Console.WriteLine($\"Sending first request to {targetUri} with HTTP/1.1\");\n        var response0 = await client.SendAsync(request0, cancellation);\n\n        PrintDuration(stopwatch, response0);\n        PrintAffinityCookie(handler, targetUri, response0);\n        await ReadAndPrintBody(response0, cancellation);\n\n        stopwatch.Reset();\n\n        var request1 = new HttpRequestMessage(HttpMethod.Get, targetUri) { Version = new Version(1, 1) };\n        Console.WriteLine($\"Sending second request to {targetUri} with HTTP/1.1\");\n        var response1 = await client.SendAsync(request1, cancellation);\n\n        PrintDuration(stopwatch, response1);\n        PrintAffinityCookie(handler, targetUri, response1);\n        await ReadAndPrintBody(response1, cancellation);\n    }\n\n    private static void PrintDuration(Stopwatch stopwatch, HttpResponseMessage response)\n    {\n        Console.WriteLine($\"Received response: {(int)response.StatusCode} in {stopwatch.ElapsedMilliseconds} ms\");\n        response.EnsureSuccessStatusCode();\n    }\n\n    private static void PrintAffinityCookie(HttpClientHandler handler, Uri targetUri, HttpResponseMessage response)\n    {\n        if (response.Headers.TryGetValues(\"Set-Cookie\", out var setCookieValue))\n        {\n            Console.WriteLine($\"Received header Set-Cookie: {setCookieValue.ToArray()[0]}\");\n        }\n        else\n        {\n            Console.WriteLine($\"Response doesn't have Set-Cookie header.\");\n        }\n\n        var affinityCookie = handler.CookieContainer.GetCookies(targetUri)[\".Yarp.Affinity\"];\n        Console.WriteLine($\"Affinity key stored on a cookie {affinityCookie.Value}\");\n    }\n\n    private static async Task ReadAndPrintBody(HttpResponseMessage response, CancellationToken cancellation)\n    {\n        var body = await response.Content.ReadAsStringAsync(cancellation);\n        var json = JsonDocument.Parse(body);\n        Console.WriteLine(\n            \"Received response:\" +\n            $\"{Environment.NewLine}\" +\n            $\"{JsonSerializer.Serialize(json.RootElement, new JsonSerializerOptions { WriteIndented = true })}\");\n        response.EnsureSuccessStatusCode();\n    }\n}\n"
  },
  {
    "path": "testassets/TestClient/Scenarios/WebSocketsScenario.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Diagnostics;\nusing System.Net.WebSockets;\nusing System.Text;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace SampleClient.Scenarios;\n\ninternal sealed class WebSocketsScenario : IScenario\n{\n    public async Task ExecuteAsync(CommandLineArgs args, CancellationToken cancellation)\n    {\n        var client = new ClientWebSocket();\n        client.Options.AddSubProtocol(\"chat\");\n\n        var webSocketsTarget = args.Target.Replace(\"https://\", \"wss://\").Replace(\"http://\", \"ws://\");\n        var targetUri = new Uri(new Uri(webSocketsTarget, UriKind.Absolute), \"api/websockets\");\n        Console.WriteLine($\"Establishing WebSockets channel with {targetUri}...\");\n\n        var stopwatch = Stopwatch.StartNew();\n        await client.ConnectAsync(targetUri, cancellation);\n        Console.WriteLine($\"Channel established in {stopwatch.ElapsedMilliseconds} ms.\");\n\n        Console.WriteLine(\"Sending text messages...\");\n        var buffer = new byte[1024];\n        stopwatch.Restart();\n        for (var i = 0; i < 256; i++)\n        {\n            var textToSend = $\"Hello {i}\";\n            var numBytes = Encoding.UTF8.GetBytes(textToSend, buffer.AsSpan());\n            await client.SendAsync(new ArraySegment<byte>(buffer, 0, numBytes),\n                WebSocketMessageType.Text,\n                endOfMessage: true,\n                cancellation);\n\n            var message = await client.ReceiveAsync(buffer, cancellation);\n            if (message.MessageType != WebSocketMessageType.Text)\n            {\n                throw new Exception($\"Expected to receive a text message, got '{message.MessageType}' instead.\");\n            }\n\n            if (!message.EndOfMessage)\n            {\n                throw new Exception(\"Expected to receive EndOfMessage = true.\");\n            }\n\n            var text = Encoding.UTF8.GetString(buffer.AsSpan(0, message.Count));\n            if (text != textToSend)\n            {\n                throw new Exception($\"Expected to receive '{textToSend}', but got '{text}'.\");\n            }\n\n            Console.Write(\".\");\n        }\n\n        Console.WriteLine();\n        Console.WriteLine($\"Completed 256 text messages in {stopwatch.ElapsedMilliseconds} ms.\");\n\n        Console.WriteLine(\"Sending binary messages...\");\n        stopwatch.Restart();\n        for (var i = 0; i < 256; i++)\n        {\n            var textToSend = $\"Hello {i}\";\n            var numBytes = Encoding.UTF8.GetBytes(textToSend, buffer.AsSpan());\n            await client.SendAsync(new ArraySegment<byte>(buffer, 0, numBytes),\n                WebSocketMessageType.Binary,\n                endOfMessage: true,\n                cancellation);\n\n            var message = await client.ReceiveAsync(buffer, cancellation);\n            if (message.MessageType != WebSocketMessageType.Binary)\n            {\n                throw new Exception($\"Expected to receive a text message, got '{message.MessageType}' instead.\");\n            }\n\n            if (!message.EndOfMessage)\n            {\n                throw new Exception(\"Expected to receive EndOfMessage = true.\");\n            }\n\n            var text = Encoding.UTF8.GetString(buffer.AsSpan(0, message.Count));\n            if (text != textToSend)\n            {\n                throw new Exception($\"Expected to receive '{textToSend}', but got '{text}'.\");\n            }\n\n            Console.Write(\".\");\n        }\n\n        Console.WriteLine();\n        Console.WriteLine($\"Completed 256 binary messages in {stopwatch.ElapsedMilliseconds} ms.\");\n\n        await client.CloseAsync(WebSocketCloseStatus.NormalClosure, \"Bye\", cancellation);\n    }\n}\n"
  },
  {
    "path": "testassets/TestClient/TestClient.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>$(LatestDevTFM)</TargetFramework>\n    <OutputType>Exe</OutputType>\n  </PropertyGroup>\n\n</Project>\n"
  },
  {
    "path": "testassets/TestServer/AssemblyInfo.cs",
    "content": "﻿// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n"
  },
  {
    "path": "testassets/TestServer/Controllers/HealthController.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Yarp.ReverseProxy.Sample.Controllers;\n\n/// <summary>\n/// Controller for active health check probes.\n/// </summary>\n[ApiController]\npublic class HealthController : ControllerBase\n{\n    private static volatile int _count;\n    /// <summary>\n    /// Returns 200 if server is healthy.\n    /// </summary>\n    [HttpGet]\n    [Route(\"/api/health\")]\n    public IActionResult CheckHealth()\n    {\n        _count++;\n        // Simulate temporary health degradation.\n        return _count % 10 < 4 ? Ok() : StatusCode(500);\n    }\n}\n"
  },
  {
    "path": "testassets/TestServer/Controllers/HttpController.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace SampleServer.Controllers;\n\n/// <summary>\n/// Sample controller.\n/// </summary>\n[ApiController]\npublic class HttpController : ControllerBase\n{\n    /// <summary>\n    /// Returns a 200 response.\n    /// </summary>\n    [HttpGet]\n    [Route(\"/api/noop\")]\n    public void NoOp()\n    {\n    }\n\n    /// <summary>\n    /// Returns a 409 response without consuming the request body.\n    /// This is used to exercise <c>Expect:100-continue</c> behavior.\n    /// </summary>\n    [HttpPost]\n    [Route(\"/api/skipbody\")]\n    public IActionResult SkipBody()\n    {\n        return StatusCode(StatusCodes.Status409Conflict);\n    }\n\n    /// <summary>\n    /// Returns a 409 response without consuming the request body.\n    /// This is used to exercise <c>Expect:100-continue</c> behavior.\n    /// </summary>\n    [HttpGet]\n    [Route(\"/api/slow\")]\n    public async Task<IActionResult> Slow()\n    {\n        await Task.Delay(TimeSpan.FromSeconds(3));\n        return StatusCode(StatusCodes.Status200OK);\n    }\n\n    /// <summary>\n    /// Returns a 200 response dumping all info from the incoming request.\n    /// </summary>\n    [HttpGet, HttpPost]\n    [Route(\"/api/dump\")]\n    [Route(\"/{**catchall}\", Order = int.MaxValue)] // Make this the default route if nothing matches\n    public async Task<IActionResult> Dump()\n    {\n        var result = new {\n            Request.Protocol,\n            Request.Method,\n            Request.Scheme,\n            Host = Request.Host.Value,\n            PathBase = Request.PathBase.Value,\n            Path = Request.Path.Value,\n            Query = Request.QueryString.Value,\n            Headers = Request.Headers.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()),\n            Time = DateTimeOffset.UtcNow,\n            Body = await new StreamReader(Request.Body).ReadToEndAsync(),\n        };\n\n        return Ok(result);\n    }\n\n    /// <summary>\n    /// Returns a 200 response dumping all info from the incoming request.\n    /// </summary>\n    [HttpGet]\n    [Route(\"/api/statuscode\")]\n    public void Status(int statusCode)\n    {\n        Response.StatusCode = statusCode;\n    }\n\n    /// <summary>\n    /// Returns a 200 response dumping all info from the incoming request.\n    /// </summary>\n    [HttpGet]\n    [Route(\"/api/headers\")]\n    public void Headers([FromBody] Dictionary<string, string> headers)\n    {\n        foreach (var (key, value) in headers)\n        {\n            Response.Headers.Append(key, value);\n        }\n    }\n\n    /// <summary>\n    /// Returns a 200 response after <paramref name=\"delay\" /> milliseconds\n    /// and containing with <paramref name=\"responseSize\" /> bytes in the response body.\n    /// </summary>\n    [HttpGet]\n    [HttpPut]\n    [HttpPost]\n    [HttpPatch]\n    [Route(\"/api/stress\")]\n    public async Task Stress([FromQuery] int delay, [FromQuery] int responseSize)\n    {\n        var bodyReader = Request.BodyReader;\n        if (bodyReader is not null)\n        {\n            while (true)\n            {\n                var a = await Request.BodyReader.ReadAsync();\n                if (a.IsCompleted)\n                {\n                    break;\n                }\n            }\n        }\n\n        if (delay > 0)\n        {\n            await Task.Delay(delay);\n        }\n\n        var bodyWriter = Response.BodyWriter;\n        if (bodyWriter is not null && responseSize > 0)\n        {\n            const int WriteBufferSize = 4096;\n\n            var remaining = responseSize;\n            var buffer = new byte[WriteBufferSize];\n\n            while (remaining > 0)\n            {\n                buffer[0] = (byte)(remaining * 17); // Make the output not all zeros\n                var toWrite = Math.Min(buffer.Length, remaining);\n                await bodyWriter.WriteAsync(new ReadOnlyMemory<byte>(buffer, 0, toWrite),\n                    HttpContext.RequestAborted);\n                remaining -= toWrite;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "testassets/TestServer/Controllers/UpgradeController.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System.IO;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Extensions.Logging;\n\nnamespace SampleServer.Controllers;\n\n/// <summary>\n/// Sample controller.\n/// </summary>\n[ApiController]\npublic class UpgradeController : ControllerBase\n{\n    private readonly ILogger<UpgradeController> _logger;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"UpgradeController\" /> class.\n    /// </summary>\n    public UpgradeController(ILogger<UpgradeController> logger)\n    {\n        _logger = logger;\n    }\n\n    /// <summary>\n    /// Upgrades the connection to a raw socket stream, then implements a simple byte ping/pong server.\n    /// Note that this does not use WebSockets, and relies solely on HTTP/1.1 connection upgrade mechanism.\n    /// </summary>\n    [HttpGet]\n    [Route(\"/api/rawupgrade\")]\n    public async Task RawUpgrade()\n    {\n        var upgradeFeature = HttpContext.Features.Get<IHttpUpgradeFeature>();\n        if (upgradeFeature is null || !upgradeFeature.IsUpgradableRequest)\n        {\n            HttpContext.Response.StatusCode = StatusCodes.Status426UpgradeRequired;\n            return;\n        }\n\n        await using var stream = await upgradeFeature.UpgradeAsync();\n        _logger.LogInformation(\"Upgraded connection.\");\n        await RunPingPongAsync(stream);\n        _logger.LogInformation(\"Finished.\");\n    }\n\n    /// <summary>\n    /// Simple echo protocol that echo's each received byte.\n    /// <c>255</c> is treated as a special \"goodbye\" message, which causes us to drop the connection.\n    /// </summary>\n    private async Task RunPingPongAsync(Stream stream)\n    {\n        var buffer = new byte[1];\n        while (await stream.ReadAsync(buffer, HttpContext.RequestAborted) != 0)\n        {\n            if (buffer[0] == 255)\n            {\n                // Goodbye\n                break;\n            }\n\n            await stream.WriteAsync(buffer);\n        }\n    }\n}\n"
  },
  {
    "path": "testassets/TestServer/Controllers/WebSocketsController.cs",
    "content": "﻿// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing System;\nusing System.Net.WebSockets;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Extensions.Logging;\n\nnamespace SampleServer.Controllers;\n\n/// <summary>\n/// Sample controller.\n/// </summary>\n[ApiController]\npublic class WebSocketsController : ControllerBase\n{\n    private readonly ILogger<WebSocketsController> _logger;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"WebSocketsController\" /> class.\n    /// </summary>\n    public WebSocketsController(ILogger<WebSocketsController> logger)\n    {\n        _logger = logger;\n    }\n\n    /// <summary>\n    /// Returns a 200 response.\n    /// </summary>\n    [HttpGet]\n    [Route(\"/api/websockets\")]\n    public async Task WebSockets()\n    {\n        if (!HttpContext.WebSockets.IsWebSocketRequest)\n        {\n            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;\n        }\n\n        using (var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync())\n        {\n            _logger.LogInformation(\"WebSockets established.\");\n            await RunPingPongAsync(webSocket, HttpContext.RequestAborted);\n        }\n\n        _logger.LogInformation(\"WebSockets finished.\");\n    }\n\n    private static async Task RunPingPongAsync(WebSocket webSocket, CancellationToken cancellation)\n    {\n        var buffer = new byte[1024];\n        while (true)\n        {\n            var message = await webSocket.ReceiveAsync(buffer, cancellation);\n            if (message.MessageType == WebSocketMessageType.Close)\n            {\n                await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, \"Bye\", cancellation);\n                return;\n            }\n\n            await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, message.Count),\n                message.MessageType,\n                message.EndOfMessage,\n                cancellation);\n        }\n    }\n}\n"
  },
  {
    "path": "testassets/TestServer/Program.cs",
    "content": "// Licensed to the .NET Foundation under one or more agreements.\n// The .NET Foundation licenses this file to you under the MIT license.\n\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddControllers()\n    .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);\n\nvar app = builder.Build();\n\napp.UseWebSockets();\napp.MapControllers();\n\napp.Run();\n"
  },
  {
    "path": "testassets/TestServer/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"TestServer\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"applicationUrl\": \"https://localhost:10000;https://localhost:10001;http://localhost:10010;http://localhost:10011\"\n    }\n  }\n}\n"
  },
  {
    "path": "testassets/TestServer/TestServer.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFramework>$(LatestDevTFM)</TargetFramework>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>SampleServer</RootNamespace>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "testassets/TestServer/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Information\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "testassets/TestServer/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}"
  }
]