| /* | |
| * Copyright (C) 2026 Michael Brown <mbrown@fensystems.co.uk>. | |
| * | |
| * This program is free software; you can redistribute it and/or | |
| * modify it under the terms of the GNU General Public License as | |
| * published by the Free Software Foundation; either version 2 of the | |
| * License, or (at your option) any later version. | |
| * | |
| * This program is distributed in the hope that it will be useful, but | |
| * WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| * General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU General Public License | |
| * along with this program; if not, write to the Free Software | |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | |
| * 02110-1301, USA. | |
| * | |
| * You can also choose to distribute this program under the terms of | |
| * the Unmodified Binary Distribution Licence (as given in the file | |
| * COPYING.UBDL), provided that you have satisfied its requirements. | |
| */ | |
| FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); | |
| #include <stdint.h> | |
| #include <string.h> | |
| #include <strings.h> | |
| #include <ctype.h> | |
| #include <errno.h> | |
| #include <ipxe/image.h> | |
| #include <ipxe/base64.h> | |
| #include <ipxe/mime.h> | |
| /** @file | |
| * | |
| * MIME image format | |
| * | |
| * The MIME format is defined in RFC 2045 and RFC 2046 (with reference | |
| * to RFC 822). We treat it firstly as a simple single-member archive | |
| * format where the content is extracted using the specified encoding. | |
| * | |
| * We additionally support multipart MIME as an archive format from | |
| * which we attempt to extract the first body part that has the MIME | |
| * type "text/x-ipxe". This makes it possible to store both | |
| * cloud-init configuration and an iPXE boot script in the same user | |
| * metadata blob for a cloud instance. | |
| * | |
| * Due to its historical origins, MIME is an irritatingly flexible | |
| * format. We do not attempt to support all possible MIME files: only | |
| * those that we might reasonably expect to encounter as cloud | |
| * instance metadata. | |
| */ | |
| static const struct mime_type * mime_type ( const char *name ); | |
| /** | |
| * Parse MIME attribute | |
| * | |
| * @v image MIME image | |
| * @v value Header value | |
| * @v eol End of header line | |
| * @v name Attribute name (including terminating equals sign) | |
| * @v attr MIME attribute to update | |
| */ | |
| static void mime_parse_attribute ( struct image *image, const char *value, | |
| const char *eol, const char *name, | |
| struct mime_attribute *attr ) { | |
| const char *match; | |
| /* Locate attribute */ | |
| match = strcasestr ( value, name ); | |
| if ( ( ! match ) || ( match >= eol ) ) | |
| return; | |
| attr->value = ( match + strlen ( name ) ); | |
| DBGC2 ( image, "MIME %s found %s\"", image->name, name ); | |
| /* Determine attribute length */ | |
| if ( attr->value[0] == '"' ) | |
| attr->value++; | |
| attr->len = 0; | |
| while ( attr->value[attr->len] && ( attr->value[attr->len] != '"' ) && | |
| ( ! isspace ( attr->value[attr->len] ) ) ) { | |
| DBGC2 ( image, "%c", attr->value[attr->len] ); | |
| attr->len++; | |
| } | |
| DBGC2 ( image, "\"\n" ); | |
| assert ( &attr->value[attr->len] <= eol ); | |
| } | |
| /** | |
| * Parse Content-Type header | |
| * | |
| * @v image MIME image | |
| * @v value Header value | |
| * @v eol End of header line | |
| * @v headers MIME headers to update | |
| */ | |
| static void mime_parse_type ( struct image *image, const char *value, | |
| const char *eol, struct mime_headers *headers ) { | |
| /* Record content type */ | |
| headers->type = value; | |
| DBGC2 ( image, "MIME %s found content type\n", image->name ); | |
| /* Check for boundary separator */ | |
| mime_parse_attribute ( image, value, eol, "boundary=", | |
| &headers->boundary ); | |
| } | |
| /** | |
| * Parse Content-Transfer-Encoding header | |
| * | |
| * @v image MIME image | |
| * @v value Header value | |
| * @v eol End of header line | |
| * @v headers MIME headers to update | |
| */ | |
| static void mime_parse_encoding ( struct image *image, const char *value, | |
| const char *eol __unused, | |
| struct mime_headers *headers ) { | |
| /* Record content transfer encoding */ | |
| headers->encoding = value; | |
| DBGC2 ( image, "MIME %s found content transfer encoding\n", | |
| image->name ); | |
| } | |
| /** Recognised MIME headers */ | |
| const struct mime_header mime_headers[] = { | |
| { | |
| .name = "Content-Type:", | |
| .parse = mime_parse_type, | |
| }, | |
| { | |
| .name = "Content-Transfer-Encoding:", | |
| .parse = mime_parse_encoding, | |
| }, | |
| }; | |
| /** | |
| * Identify MIME header | |
| * | |
| * @v line Header line | |
| * @ret header MIME header, or NULL if not recognised | |
| */ | |
| static const struct mime_header * mime_header ( const char *line ) { | |
| const struct mime_header *header; | |
| unsigned int i; | |
| /* Identify MIME header */ | |
| for ( i = 0 ; i < ( sizeof ( mime_headers ) / | |
| sizeof ( mime_headers[0] ) ) ; i++ ) { | |
| header = &mime_headers[i]; | |
| if ( strncasecmp ( line, header->name, | |
| strlen ( header->name ) ) == 0 ) { | |
| return header; | |
| } | |
| } | |
| return NULL; | |
| } | |
| /** | |
| * Parse MIME headers | |
| * | |
| * @v image MIME image | |
| * @v text Start of MIME headers within image | |
| * @v headers MIME headers to fill in | |
| * @ret rc Return status code | |
| */ | |
| static int mime_parse ( struct image *image, const char *text, | |
| struct mime_headers *headers ) { | |
| const struct mime_header *header; | |
| const char *value; | |
| const char *line; | |
| const char *next; | |
| const char *eol; | |
| /* Initialise headers */ | |
| memset ( headers, 0, sizeof ( *headers ) ); | |
| /* Parse headers until reaching empty-line separator */ | |
| for ( line = text ; ; line = next ) { | |
| /* Locate end of line */ | |
| eol = strchr ( line, '\n' ); | |
| if ( ! eol ) { | |
| DBGC ( image, "MIME %s premature end of file\n", | |
| image->name ); | |
| return -EINVAL; | |
| } | |
| next = ( eol + 1 ); | |
| /* Check for empty line */ | |
| if ( eol == line ) | |
| break; | |
| if ( eol[-1] == '\r' ) | |
| eol--; | |
| if ( eol == line ) | |
| break; | |
| /* Identify header (if recognised) */ | |
| header = mime_header ( line ); | |
| if ( ! header ) | |
| continue; | |
| /* Locate start of value */ | |
| value = ( line + strlen ( header->name ) ); | |
| while ( ( *value != '\n' ) && isspace ( *value ) ) | |
| value++; | |
| /* Parse header */ | |
| header->parse ( image, value, eol, headers ); | |
| } | |
| /* Check for mandatory headers */ | |
| if ( ! headers->type ) { | |
| DBGC ( image, "MIME %s missing content type\n", image->name ); | |
| return -EINVAL; | |
| } | |
| /* Record length of headers */ | |
| headers->len = ( next - text ); | |
| return 0; | |
| } | |
| /** | |
| * Decode MIME entity with identity encoding | |
| * | |
| * @v image MIME image | |
| * @v headers MIME headers | |
| * @v decoded Decoded image | |
| * @ret rc Return status code | |
| */ | |
| static int mime_decode_identity ( struct image *image, | |
| const struct mime_headers *headers, | |
| struct image *decoded ) { | |
| size_t len; | |
| int rc; | |
| /* Allocate space */ | |
| len = ( image->len - headers->len ); | |
| if ( ( rc = image_set_len ( decoded, len ) ) != 0 ) | |
| return rc; | |
| /* Decode data */ | |
| memcpy ( decoded->rwdata, ( image->data + headers->len ), len ); | |
| return 0; | |
| } | |
| /** | |
| * Decode MIME entity with Base64 encoding | |
| * | |
| * @v image MIME image | |
| * @v headers MIME headers | |
| * @v decoded Decoded image | |
| * @ret rc Return status code | |
| */ | |
| static int mime_decode_base64 ( struct image *image, | |
| const struct mime_headers *headers, | |
| struct image *decoded ) { | |
| const char *encoded; | |
| size_t max_len; | |
| int len; | |
| int rc; | |
| /* Allocate space for decoded data */ | |
| encoded = ( image->text + headers->len ); | |
| max_len = base64_decoded_max_len ( encoded ); | |
| if ( ( rc = image_set_len ( decoded, max_len ) ) != 0 ) | |
| return rc; | |
| /* Decode data */ | |
| len = base64_decode ( encoded, decoded->rwdata, decoded->len ); | |
| if ( len < 0 ) { | |
| rc = len; | |
| DBGC ( image, "MIME %s could not decode base64: %s\n", | |
| image->name, strerror ( rc ) ); | |
| return rc; | |
| } | |
| assert ( ( ( size_t ) len ) <= max_len ); | |
| /* Set decoded length */ | |
| if ( ( rc = image_set_len ( decoded, len ) ) != 0 ) | |
| return rc; | |
| return 0; | |
| } | |
| /** MIME encodings */ | |
| static const struct mime_encoding mime_encodings[] = { | |
| { | |
| /* Default encoding */ | |
| .name = "7bit", | |
| .decode = mime_decode_identity, | |
| }, | |
| { | |
| .name = "8bit", | |
| .decode = mime_decode_identity, | |
| }, | |
| { | |
| .name = "binary", | |
| .decode = mime_decode_identity, | |
| }, | |
| { | |
| .name = "base64", | |
| .decode = mime_decode_base64, | |
| }, | |
| }; | |
| /** | |
| * Identify MIME encoding | |
| * | |
| * @v name Encoding name, or NULL to use default | |
| * @ret encoding MIME encoding, or NULL if not recognised | |
| */ | |
| static const struct mime_encoding * mime_encoding ( const char *name ) { | |
| const struct mime_encoding *encoding; | |
| size_t len; | |
| unsigned int i; | |
| /* Use default encoding if no name specified */ | |
| if ( ! name ) | |
| return &mime_encodings[0]; | |
| /* Identify MIME encoding */ | |
| for ( i = 0 ; i < ( sizeof ( mime_encodings ) / | |
| sizeof ( mime_encodings[0] ) ) ; i++ ) { | |
| encoding = &mime_encodings[i]; | |
| len = strlen ( encoding->name ); | |
| if ( ( strncasecmp ( name, encoding->name, len ) == 0 ) && | |
| ( ( name[len] == ';' ) || isspace ( name[len] ) ) ) { | |
| return encoding; | |
| } | |
| } | |
| return NULL; | |
| } | |
| /** | |
| * Extract single MIME entity | |
| * | |
| * @v image MIME image | |
| * @v headers MIME headers | |
| * @v extracted Extracted image | |
| * @ret rc Return status code | |
| */ | |
| static int mime_extract_entity ( struct image *image, | |
| const struct mime_headers *headers, | |
| struct image *extracted ) { | |
| const struct mime_encoding *encoding; | |
| int rc; | |
| /* Identify encoding */ | |
| encoding = mime_encoding ( headers->encoding ); | |
| if ( ! encoding ) { | |
| DBGC ( image, "MIME %s has unrecognised encoding\n", | |
| image->name ); | |
| return -ENOTSUP; | |
| } | |
| DBGC ( image, "MIME %s has encoding %s\n", | |
| image->name, encoding->name ); | |
| /* Decode via applicable encoding */ | |
| if ( ( rc = encoding->decode ( image, headers, extracted ) ) != 0 ) | |
| return rc; | |
| return 0; | |
| } | |
| /** | |
| * Find end of current part in multipart MIME image | |
| * | |
| * @v image MIME image | |
| * @v boundary Boundary separator | |
| * @v pos Current position within image | |
| * @ret end End of current part, or NULL if not found | |
| */ | |
| static const char * mime_part_end ( struct image *image, | |
| const struct mime_attribute *boundary, | |
| const char *pos ) { | |
| /* Locate boundary marker */ | |
| while ( 1 ) { | |
| pos = strstr ( pos, "\n--" ); | |
| if ( ! pos ) | |
| break; | |
| pos += ( 1 /* newline */ ); | |
| if ( strncmp ( ( pos + 2 /* -- */ ), boundary->value, | |
| boundary->len ) != 0 ) { | |
| continue; | |
| } | |
| return pos; | |
| } | |
| DBGC ( image, "MIME %s missing boundary marker\n", image->name ); | |
| return NULL; | |
| } | |
| /** | |
| * Find start of next part in multipart MIME image | |
| * | |
| * @v image MIME image | |
| * @v boundary Boundary separator | |
| * @v pos End of current part within image | |
| * @v next Start of next part, or NULL if not found | |
| */ | |
| static const char * mime_part_next ( struct image *image, | |
| const struct mime_attribute *boundary, | |
| const char *pos ) { | |
| /* Skip boundary marker */ | |
| pos += ( 2 /* -- */ + boundary->len ); | |
| /* Check for end boundary */ | |
| if ( strcmp ( pos, "--" ) == 0 ) | |
| return NULL; | |
| /* Skip trailing whitespace */ | |
| while ( ( *pos != '\n' ) && isspace ( *pos ) ) | |
| pos++; | |
| /* Check for and skip terminating newline */ | |
| if ( *pos != '\n' ) { | |
| DBGC ( image, "MIME %s malformed boundary marker\n", | |
| image->name ); | |
| return NULL; | |
| } | |
| pos++; | |
| return pos; | |
| } | |
| /** | |
| * Extract part from multipart MIME image | |
| * | |
| * @v image MIME image | |
| * @v headers MIME headers | |
| * @v extracted Extracted image | |
| * @ret rc Return status code | |
| * | |
| * Extraction will produce a MIME image containing only the selected | |
| * part. (We rely on the recursive behaviour of image_extract_exec() | |
| * to extract the content within the extracted part.) | |
| */ | |
| static int mime_extract_part ( struct image *image, | |
| const struct mime_headers *headers, | |
| struct image *extracted ) { | |
| const struct mime_attribute *boundary = &headers->boundary; | |
| const struct mime_type *type; | |
| struct mime_headers subheaders; | |
| const char *part; | |
| const char *end; | |
| size_t len; | |
| int rc; | |
| /* Find end of preamble */ | |
| end = mime_part_end ( image, boundary, image->text ); | |
| if ( ! end ) | |
| return -ENOENT; | |
| /* Look for a usable part */ | |
| while ( ( part = mime_part_next ( image, boundary, end ) ) && | |
| ( end = mime_part_end ( image, boundary, part ) ) ) { | |
| /* Parse part headers */ | |
| DBGC ( image, "MIME %s found part at [%#zx,%#zx)\n", | |
| image->name, ( part - image->text ), | |
| ( end - image->text ) ); | |
| if ( ( rc = mime_parse ( image, part, &subheaders ) ) != 0 ) | |
| return rc; | |
| /* Check for a recognised MIME type */ | |
| type = mime_type ( subheaders.type ); | |
| if ( ! type ) | |
| continue; | |
| DBGC ( image, "MIME %s found part with type %s\n", | |
| image->name, type->name ); | |
| /* Extract part */ | |
| len = ( end - part ); | |
| if ( ( rc = image_set_len ( extracted, len ) ) != 0 ) | |
| return rc; | |
| memcpy ( extracted->rwdata, part, len ); | |
| return 0; | |
| } | |
| DBGC ( image, "MIME %s found no usable part\n", image->name ); | |
| return -ENOENT; | |
| } | |
| /** | |
| * Probe MIME image | |
| * | |
| * @v image Compressed kernel image | |
| * @ret rc Return status code | |
| */ | |
| static int mime_probe ( struct image *image ) { | |
| struct mime_headers headers; | |
| int rc; | |
| /* Check for MIME headers */ | |
| if ( ( rc = mime_parse ( image, image->text, &headers ) ) != 0 ) | |
| return rc; | |
| return 0; | |
| } | |
| /** MIME types */ | |
| static const struct mime_type mime_types[] = { | |
| { | |
| .name = "multipart/mixed", | |
| .extract = mime_extract_part, | |
| }, | |
| { | |
| .name = "text/x-ipxe", | |
| .extract = mime_extract_entity, | |
| }, | |
| }; | |
| /** | |
| * Identify MIME type | |
| * | |
| * @v name Encoding | |
| * @ret type MIME type, or NULL if not recognised | |
| */ | |
| static const struct mime_type * mime_type ( const char *name ) { | |
| const struct mime_type *type; | |
| size_t len; | |
| unsigned int i; | |
| /* Identify MIME type */ | |
| for ( i = 0 ; i < ( sizeof ( mime_types ) / | |
| sizeof ( mime_types[0] ) ) ; i++ ) { | |
| type = &mime_types[i]; | |
| len = strlen ( type->name ); | |
| if ( ( strncasecmp ( name, type->name, len ) == 0 ) && | |
| ( ( name[len] == ';' ) || isspace ( name[len] ) ) ) { | |
| return type; | |
| } | |
| } | |
| return NULL; | |
| } | |
| /** | |
| * Extract MIME image | |
| * | |
| * @v image Compressed kernel image | |
| * @v extracted Extracted image | |
| * @ret rc Return status code | |
| */ | |
| static int mime_extract ( struct image *image, struct image *extracted ) { | |
| const struct mime_type *type; | |
| struct mime_headers headers; | |
| int rc; | |
| /* Parse headers */ | |
| if ( ( rc = mime_parse ( image, image->text, &headers ) ) != 0 ) | |
| return rc; | |
| /* Identify MIME type */ | |
| type = mime_type ( headers.type ); | |
| if ( ! type ) { | |
| DBGC ( image, "MIME %s has no type\n", image->name ); | |
| return -ENOTSUP; | |
| } | |
| DBGC ( image, "MIME %s has type %s\n", image->name, type->name ); | |
| /* Extract image */ | |
| if ( ( rc = type->extract ( image, &headers, extracted ) ) != 0 ) | |
| return rc; | |
| return 0; | |
| } | |
| /** MIME image type */ | |
| struct image_type mime_image_type __image_type ( PROBE_NORMAL ) = { | |
| .name = "MIME", | |
| .probe = mime_probe, | |
| .extract = mime_extract, | |
| .exec = image_extract_exec, | |
| }; |