/** @file RedfishHttpOperation handles HTTP operations. Copyright (c) 2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved. SPDX-License-Identifier: BSD-2-Clause-Patent **/ #include "RedfishHttpOperation.h" #include "RedfishHttpData.h" /** This function copies all headers in SrcHeaders to DstHeaders. It's call responsibility to release returned DstHeaders. @param[in] SrcHeaders Source headers. @param[in] SrcHeaderCount Number of header in source headers. @param[out] DstHeaders Destination headers. @param[out] DstHeaderCount Number of header in designation headers. @retval EFI_SUCCESS Headers are copied successfully. @retval Others Errors occur. **/ EFI_STATUS CopyHttpHeaders ( IN EFI_HTTP_HEADER *SrcHeaders, IN UINTN SrcHeaderCount, OUT EFI_HTTP_HEADER **DstHeaders, OUT UINTN *DstHeaderCount ) { UINTN Index; if ((SrcHeaders == NULL) || (SrcHeaderCount == 0) || (DstHeaders == NULL) || (DstHeaderCount == NULL)) { return EFI_INVALID_PARAMETER; } *DstHeaderCount = 0; *DstHeaders = AllocateZeroPool (sizeof (EFI_HTTP_HEADER) * SrcHeaderCount); if (*DstHeaders == NULL) { return EFI_OUT_OF_RESOURCES; } *DstHeaderCount = SrcHeaderCount; for (Index = 0; Index < SrcHeaderCount; Index++) { (*DstHeaders)[Index].FieldName = ASCII_STR_DUPLICATE (SrcHeaders[Index].FieldName); if ((*DstHeaders)[Index].FieldName == NULL) { return EFI_OUT_OF_RESOURCES; } (*DstHeaders)[Index].FieldValue = ASCII_STR_DUPLICATE (SrcHeaders[Index].FieldValue); if ((*DstHeaders)[Index].FieldValue == NULL) { return EFI_OUT_OF_RESOURCES; } } return EFI_SUCCESS; } /** This function free resources in Request. Request is no longer available after this function returns successfully. @param[in] Request HTTP request to be released. @retval EFI_SUCCESS Resource is released successfully. @retval Others Errors occur. **/ EFI_STATUS ReleaseRedfishRequest ( IN REDFISH_REQUEST *Request ) { if (Request == NULL) { return EFI_INVALID_PARAMETER; } if ((Request->Headers != NULL) && (Request->HeaderCount > 0)) { HttpFreeHeaderFields (Request->Headers, Request->HeaderCount); Request->Headers = NULL; Request->HeaderCount = 0; } if (Request->Content != NULL) { FreePool (Request->Content); Request->Content = NULL; } if (Request->ContentType != NULL) { FreePool (Request->ContentType); Request->ContentType = NULL; } Request->ContentLength = 0; return EFI_SUCCESS; } /** This function free resources in given Response. @param[in] Response HTTP response to be released. @retval EFI_SUCCESS Resource is released successfully. @retval Others Errors occur. **/ EFI_STATUS ReleaseRedfishResponse ( IN REDFISH_RESPONSE *Response ) { if (Response == NULL) { return EFI_INVALID_PARAMETER; } if ((Response->Headers != NULL) && (Response->HeaderCount > 0)) { HttpFreeHeaderFields (Response->Headers, Response->HeaderCount); Response->Headers = NULL; Response->HeaderCount = 0; } if (Response->Payload != NULL) { ReleaseRedfishPayload (Response->Payload); Response->Payload = NULL; } if (Response->StatusCode != NULL) { FreePool (Response->StatusCode); Response->StatusCode = NULL; } return EFI_SUCCESS; } /** This function free resources in given HTTP message. @param[in] HttpMessage HTTP message to be released. @param[in] IsRequest TRUE if this is request type of HTTP message. FALSE if this is response type of HTTP message. @retval EFI_SUCCESS Resource is released successfully. @retval Others Errors occur. **/ EFI_STATUS ReleaseHttpMessage ( IN EFI_HTTP_MESSAGE *HttpMessage, IN BOOLEAN IsRequest ) { if (HttpMessage == NULL) { return EFI_INVALID_PARAMETER; } if (IsRequest) { if (HttpMessage->Data.Request != NULL) { if (HttpMessage->Data.Request->Url != NULL) { FreePool (HttpMessage->Data.Request->Url); } FreePool (HttpMessage->Data.Request); HttpMessage->Data.Request = NULL; } } else { if (HttpMessage->Data.Response != NULL) { FreePool (HttpMessage->Data.Response); HttpMessage->Data.Response = NULL; } } if (HttpMessage->Body != NULL) { FreePool (HttpMessage->Body); HttpMessage->Body = NULL; } if (HttpMessage->Headers != NULL) { HttpFreeHeaderFields (HttpMessage->Headers, HttpMessage->HeaderCount); HttpMessage->Headers = NULL; HttpMessage->HeaderCount = 0; } return EFI_SUCCESS; } /** This function build Redfish message for sending data to Redfish service. It's call responsibility to properly release returned HTTP message by calling ReleaseHttpMessage. @param[in] ServicePrivate Pointer to Redfish service private data. @param[in] Uri Redfish service URI. @param[in] Method HTTP method. @param[in] Request Additional data to send to Redfish service. This is optional. @param[in] ContentEncoding Content encoding method to compress HTTP context. This is optional. When ContentEncoding is NULL, No compress method will be performed. @retval EFI_HTTP_MESSAGE * Pointer to newly created HTTP message. @retval NULL Error occurred. **/ EFI_HTTP_MESSAGE * BuildRequestMessage ( IN REDFISH_SERVICE_PRIVATE *ServicePrivate, IN EFI_STRING Uri, IN EFI_HTTP_METHOD Method, IN REDFISH_REQUEST *Request OPTIONAL, IN CHAR8 *ContentEncoding OPTIONAL ) { EFI_STATUS Status; EFI_STRING Url; UINTN UrlSize; UINTN Index; EFI_HTTP_MESSAGE *RequestMsg; EFI_HTTP_REQUEST_DATA *RequestData; UINTN HeaderCount; UINTN HeaderIndex; EFI_HTTP_HEADER *Headers; CHAR8 ContentLengthStr[REDFISH_CONTENT_LENGTH_SIZE]; VOID *Content; UINTN ContentLength; BOOLEAN HasContent; BOOLEAN DoContentEncoding; RequestMsg = NULL; RequestData = NULL; Url = NULL; UrlSize = 0; Content = NULL; ContentLength = 0; HeaderCount = REDFISH_COMMON_HEADER_SIZE; HeaderIndex = 0; Headers = NULL; HasContent = FALSE; DoContentEncoding = FALSE; if ((ServicePrivate == NULL) || (IS_EMPTY_STRING (Uri))) { return NULL; } if (Method >= HttpMethodMax) { return NULL; } DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a: %s\n", __func__, Uri)); // // Build full URL for HTTP query. // UrlSize = (AsciiStrLen (ServicePrivate->Host) + StrLen (Uri) + 1) * sizeof (CHAR16); Url = AllocateZeroPool (UrlSize); if (Url == NULL) { return NULL; } UnicodeSPrint (Url, UrlSize, L"%a%s", ServicePrivate->Host, Uri); DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a: Url: %s\n", __func__, Url)); // // Step 1: build the HTTP headers. // if (!IS_EMPTY_STRING (ServicePrivate->SessionToken) || !IS_EMPTY_STRING (ServicePrivate->BasicAuth)) { HeaderCount++; } if ((Request != NULL) && (Request->HeaderCount > 0)) { HeaderCount += Request->HeaderCount; } // // Check and see if we will do content encoding or not // if (!IS_EMPTY_STRING (ContentEncoding)) { if (AsciiStrCmp (ContentEncoding, REDFISH_HTTP_CONTENT_ENCODING_NONE) != 0) { DoContentEncoding = TRUE; } } if ((Request != NULL) && !IS_EMPTY_STRING (Request->Content)) { HeaderCount += 2; HasContent = TRUE; if (DoContentEncoding) { HeaderCount += 1; } } Headers = AllocateZeroPool (HeaderCount * sizeof (EFI_HTTP_HEADER)); if (Headers == NULL) { goto ON_ERROR; } if (!IS_EMPTY_STRING (ServicePrivate->SessionToken)) { Status = HttpSetFieldNameAndValue (&Headers[HeaderIndex++], HTTP_HEADER_X_AUTH_TOKEN, ServicePrivate->SessionToken); if (EFI_ERROR (Status)) { goto ON_ERROR; } } else if (!IS_EMPTY_STRING (ServicePrivate->BasicAuth)) { Status = HttpSetFieldNameAndValue (&Headers[HeaderIndex++], HTTP_HEADER_AUTHORIZATION, ServicePrivate->BasicAuth); if (EFI_ERROR (Status)) { goto ON_ERROR; } } if (Request != NULL) { for (Index = 0; Index < Request->HeaderCount; Index++) { Status = HttpSetFieldNameAndValue (&Headers[HeaderIndex++], Request->Headers[Index].FieldName, Request->Headers[Index].FieldValue); if (EFI_ERROR (Status)) { goto ON_ERROR; } } } Status = HttpSetFieldNameAndValue (&Headers[HeaderIndex++], HTTP_HEADER_HOST, ServicePrivate->HostName); if (EFI_ERROR (Status)) { goto ON_ERROR; } Status = HttpSetFieldNameAndValue (&Headers[HeaderIndex++], REDFISH_HTTP_HEADER_ODATA_VERSION_STR, REDFISH_HTTP_HEADER_ODATA_VERSION_VALUE); if (EFI_ERROR (Status)) { goto ON_ERROR; } Status = HttpSetFieldNameAndValue (&Headers[HeaderIndex++], HTTP_HEADER_ACCEPT, HTTP_CONTENT_TYPE_APP_JSON); if (EFI_ERROR (Status)) { goto ON_ERROR; } Status = HttpSetFieldNameAndValue (&Headers[HeaderIndex++], HTTP_HEADER_USER_AGENT, REDFISH_HTTP_HEADER_USER_AGENT_VALUE); if (EFI_ERROR (Status)) { goto ON_ERROR; } Status = HttpSetFieldNameAndValue (&Headers[HeaderIndex++], REDFISH_HTTP_HEADER_CONNECTION_STR, REDFISH_HTTP_HEADER_CONNECTION_VALUE); if (EFI_ERROR (Status)) { goto ON_ERROR; } // // Handle content header // if (HasContent) { if (Request->ContentType == NULL) { Status = HttpSetFieldNameAndValue (&Headers[HeaderIndex++], HTTP_HEADER_CONTENT_TYPE, HTTP_CONTENT_TYPE_APP_JSON); if (EFI_ERROR (Status)) { goto ON_ERROR; } } else { Status = HttpSetFieldNameAndValue (&Headers[HeaderIndex++], HTTP_HEADER_CONTENT_TYPE, Request->ContentType); if (EFI_ERROR (Status)) { goto ON_ERROR; } } if (Request->ContentLength == 0) { Request->ContentLength = AsciiStrLen (Request->Content); } AsciiSPrint ( ContentLengthStr, sizeof (ContentLengthStr), "%lu", (UINT64)Request->ContentLength ); Status = HttpSetFieldNameAndValue (&Headers[HeaderIndex++], HTTP_HEADER_CONTENT_LENGTH, ContentLengthStr); if (EFI_ERROR (Status)) { goto ON_ERROR; } // // Encoding // if (DoContentEncoding) { // // We currently only support gzip Content-Encoding. // Status = RedfishContentEncode ( ContentEncoding, Request->Content, Request->ContentLength, &Content, &ContentLength ); if (Status == EFI_INVALID_PARAMETER) { DEBUG ((DEBUG_ERROR, "%a: Error to encode content.\n", __func__)); goto ON_ERROR; } else if (Status == EFI_UNSUPPORTED) { DoContentEncoding = FALSE; DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a: No content coding for %a! Use raw data instead.\n", __func__, ContentEncoding)); Status = HttpSetFieldNameAndValue (&Headers[HeaderIndex++], HTTP_HEADER_CONTENT_ENCODING, HTTP_CONTENT_ENCODING_IDENTITY); if (EFI_ERROR (Status)) { goto ON_ERROR; } } else { Status = HttpSetFieldNameAndValue (&Headers[HeaderIndex++], HTTP_HEADER_CONTENT_ENCODING, HTTP_CONTENT_ENCODING_GZIP); if (EFI_ERROR (Status)) { goto ON_ERROR; } } } // // When the content is from caller, we use our own copy so that we properly release it later. // if (!DoContentEncoding) { Content = AllocateCopyPool (Request->ContentLength, Request->Content); if (Content == NULL) { goto ON_ERROR; } ContentLength = Request->ContentLength; } } // // Step 2: build the rest of HTTP request info. // RequestData = AllocateZeroPool (sizeof (EFI_HTTP_REQUEST_DATA)); if (RequestData == NULL) { goto ON_ERROR; } RequestData->Method = Method; RequestData->Url = Url; // // Step 3: fill in EFI_HTTP_MESSAGE // RequestMsg = AllocateZeroPool (sizeof (EFI_HTTP_MESSAGE)); if (RequestMsg == NULL) { goto ON_ERROR; } ASSERT (HeaderIndex == HeaderCount); RequestMsg->Data.Request = RequestData; RequestMsg->HeaderCount = HeaderIndex; RequestMsg->Headers = Headers; if (HasContent) { RequestMsg->BodyLength = ContentLength; RequestMsg->Body = Content; } return RequestMsg; ON_ERROR: if (Headers != NULL) { HttpFreeHeaderFields (Headers, HeaderIndex); } if (RequestData != NULL) { FreePool (RequestData); } if (RequestMsg != NULL) { FreePool (RequestMsg); } if (Url != NULL) { FreePool (Url); } return NULL; } /** This function parse response message from Redfish service, and build Redfish response for caller. It's call responsibility to properly release Redfish response by calling ReleaseRedfishResponse. @param[in] ServicePrivate Pointer to Redfish service private data. @param[in] ResponseMsg Response message from Redfish service. @param[out] RedfishResponse Redfish response data. @retval EFI_SUCCESS Redfish response is returned successfully. @retval Others Errors occur. **/ EFI_STATUS ParseResponseMessage ( IN REDFISH_SERVICE_PRIVATE *ServicePrivate, IN EFI_HTTP_MESSAGE *ResponseMsg, OUT REDFISH_RESPONSE *RedfishResponse ) { EFI_STATUS Status; EDKII_JSON_VALUE JsonData; EFI_HTTP_HEADER *ContentEncodedHeader; VOID *DecodedBody; UINTN DecodedLength; if ((ServicePrivate == NULL) || (ResponseMsg == NULL) || (RedfishResponse == NULL)) { return EFI_INVALID_PARAMETER; } DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a\n", __func__)); // // Initialization // JsonData = NULL; RedfishResponse->HeaderCount = 0; RedfishResponse->Headers = NULL; RedfishResponse->Payload = NULL; RedfishResponse->StatusCode = NULL; DecodedBody = NULL; DecodedLength = 0; // // Return the HTTP StatusCode. // if (ResponseMsg->Data.Response != NULL) { DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a: status: %d\n", __func__, ResponseMsg->Data.Response->StatusCode)); RedfishResponse->StatusCode = AllocateCopyPool (sizeof (EFI_HTTP_STATUS_CODE), &ResponseMsg->Data.Response->StatusCode); if (RedfishResponse->StatusCode == NULL) { DEBUG ((DEBUG_ERROR, "%a: Failed to create status code.\n", __func__)); } } // // Return the HTTP headers. // if (ResponseMsg->Headers != NULL) { DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a: header count: %d\n", __func__, ResponseMsg->HeaderCount)); Status = CopyHttpHeaders ( ResponseMsg->Headers, ResponseMsg->HeaderCount, &RedfishResponse->Headers, &RedfishResponse->HeaderCount ); if (EFI_ERROR (Status)) { DEBUG ((DEBUG_ERROR, "%a: Failed to copy HTTP headers: %r\n", __func__, Status)); } } // // Return the HTTP body. // if ((ResponseMsg->BodyLength != 0) && (ResponseMsg->Body != NULL)) { DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a: body length: %d\n", __func__, ResponseMsg->BodyLength)); // // Check if data is encoded. // ContentEncodedHeader = HttpFindHeader (RedfishResponse->HeaderCount, RedfishResponse->Headers, HTTP_HEADER_CONTENT_ENCODING); if (ContentEncodedHeader != NULL) { // // The content is encoded. // Status = RedfishContentDecode ( ContentEncodedHeader->FieldValue, ResponseMsg->Body, ResponseMsg->BodyLength, &DecodedBody, &DecodedLength ); if (EFI_ERROR (Status)) { DEBUG ((DEBUG_ERROR, "%a: Failed to decompress the response content: %r decoding method: %a\n.", __func__, Status, ContentEncodedHeader->FieldValue)); goto ON_ERROR; } JsonData = JsonLoadBuffer (DecodedBody, DecodedLength, 0, NULL); FreePool (DecodedBody); } else { JsonData = JsonLoadBuffer (ResponseMsg->Body, ResponseMsg->BodyLength, 0, NULL); } if (!JsonValueIsNull (JsonData)) { RedfishResponse->Payload = CreateRedfishPayload (ServicePrivate, JsonData); if (RedfishResponse->Payload == NULL) { DEBUG ((DEBUG_ERROR, "%a: Failed to create payload\n.", __func__)); } JsonValueFree (JsonData); } else { DEBUG ((DEBUG_ERROR, "%a: No payload available\n", __func__)); } } return EFI_SUCCESS; ON_ERROR: if (RedfishResponse != NULL) { ReleaseRedfishResponse (RedfishResponse); } return Status; } /** This function send Redfish request to Redfish service by calling Rest Ex protocol. @param[in] Service Pointer to Redfish service. @param[in] Uri Uri of Redfish service. @param[in] Method HTTP method. @param[in] Request Request data. This is optional. @param[out] Response Redfish response data. @retval EFI_SUCCESS Request is sent and received successfully. @retval Others Errors occur. **/ EFI_STATUS HttpSendReceive ( IN REDFISH_SERVICE Service, IN EFI_STRING Uri, IN EFI_HTTP_METHOD Method, IN REDFISH_REQUEST *Request OPTIONAL, OUT REDFISH_RESPONSE *Response ) { EFI_STATUS Status; EFI_STATUS RestExStatus; EFI_HTTP_MESSAGE *RequestMsg; EFI_HTTP_MESSAGE ResponseMsg; REDFISH_SERVICE_PRIVATE *ServicePrivate; EFI_HTTP_HEADER *XAuthTokenHeader; CHAR8 *HttpContentEncoding; if ((Service == NULL) || IS_EMPTY_STRING (Uri) || (Response == NULL)) { return EFI_INVALID_PARAMETER; } DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a: Method: 0x%x %s\n", __func__, Method, Uri)); ServicePrivate = (REDFISH_SERVICE_PRIVATE *)Service; if (ServicePrivate->Signature != REDFISH_HTTP_SERVICE_SIGNATURE) { DEBUG ((DEBUG_ERROR, "%a: signature check failure\n", __func__)); return EFI_INVALID_PARAMETER; } ZeroMem (&ResponseMsg, sizeof (ResponseMsg)); HttpContentEncoding = (CHAR8 *)PcdGetPtr (PcdRedfishServiceContentEncoding); RequestMsg = BuildRequestMessage (Service, Uri, Method, Request, HttpContentEncoding); if (RequestMsg == NULL) { DEBUG ((DEBUG_ERROR, "%a: cannot build request message for %s\n", __func__, Uri)); return EFI_PROTOCOL_ERROR; } // // call RESTEx to get response from REST service. // RestExStatus = ServicePrivate->RestEx->SendReceive (ServicePrivate->RestEx, RequestMsg, &ResponseMsg); if (EFI_ERROR (RestExStatus)) { DEBUG ((DEBUG_ERROR, "%a: %s SendReceive failure: %r\n", __func__, Uri, RestExStatus)); } // // Return status code, headers and payload to caller as much as possible even when RestEx returns failure. // Status = ParseResponseMessage (ServicePrivate, &ResponseMsg, Response); if (EFI_ERROR (Status)) { DEBUG ((DEBUG_ERROR, "%a: %s parse response failure: %r\n", __func__, Uri, Status)); } else { // // Capture session token in header // if ((Method == HttpMethodPost) && (Response->StatusCode != NULL) && ((*Response->StatusCode == HTTP_STATUS_200_OK) || (*Response->StatusCode == HTTP_STATUS_204_NO_CONTENT))) { XAuthTokenHeader = HttpFindHeader (ResponseMsg.HeaderCount, ResponseMsg.Headers, HTTP_HEADER_X_AUTH_TOKEN); if (XAuthTokenHeader != NULL) { Status = UpdateSessionToken (ServicePrivate, XAuthTokenHeader->FieldValue); if (EFI_ERROR (Status)) { DEBUG ((DEBUG_ERROR, "%a: update session token failure: %r\n", __func__, Status)); } } } } // // Release resources // if (RequestMsg != NULL) { ReleaseHttpMessage (RequestMsg, TRUE); FreePool (RequestMsg); } ReleaseHttpMessage (&ResponseMsg, FALSE); return RestExStatus; }