1919import java .io .ByteArrayInputStream ;
2020import java .io .ByteArrayOutputStream ;
2121import java .io .IOException ;
22- import java .nio . charset . StandardCharsets ;
22+ import java .io . InputStream ;
2323import java .util .Arrays ;
2424import java .util .List ;
2525import java .util .Map ;
3535import javax .mail .internet .MimeMessage ;
3636import javax .mail .internet .MimeMultipart ;
3737
38+ import org .apache .commons .codec .binary .Base64 ;
3839import org .apache .commons .collections .CollectionUtils ;
3940import org .apache .commons .lang3 .StringUtils ;
4041import org .apache .log4j .Logger ;
4142
4243import com .cloud .utils .component .AdapterBase ;
4344import com .cloud .utils .exception .CloudRuntimeException ;
45+ import com .sun .mail .util .BASE64DecoderStream ;
4446
4547public class CloudInitUserDataProvider extends AdapterBase implements UserDataProvider {
4648
@@ -69,11 +71,11 @@ public String getName() {
6971 return "cloud-init" ;
7072 }
7173
72- protected boolean isGZipped (String userdata ) {
73- if (StringUtils .isEmpty (userdata )) {
74+ protected boolean isGZipped (String encodedUserdata ) {
75+ if (StringUtils .isEmpty (encodedUserdata )) {
7476 return false ;
7577 }
76- byte [] data = userdata . getBytes ( StandardCharsets . ISO_8859_1 );
78+ byte [] data = Base64 . decodeBase64 ( encodedUserdata );
7779 if (data .length < 2 ) {
7880 return false ;
7981 }
@@ -82,9 +84,6 @@ protected boolean isGZipped(String userdata) {
8284 }
8385
8486 protected String extractUserDataHeader (String userdata ) {
85- if (isGZipped (userdata )) {
86- throw new CloudRuntimeException ("Gzipped user data can not be used together with other user data formats" );
87- }
8887 List <String > lines = Arrays .stream (userdata .split ("\n " ))
8988 .filter (x -> (x .startsWith ("#" ) && !x .startsWith ("##" )) || (x .startsWith ("Content-Type:" )))
9089 .collect (Collectors .toList ());
@@ -131,7 +130,7 @@ protected FormatType getUserDataFormatType(String userdata) {
131130
132131 private String getContentType (String userData , FormatType formatType ) throws MessagingException {
133132 if (formatType == FormatType .MIME ) {
134- MimeMessage msg = new MimeMessage (session , new ByteArrayInputStream (userData .getBytes ()));
133+ NoIdMimeMessage msg = new NoIdMimeMessage (session , new ByteArrayInputStream (userData .getBytes ()));
135134 return msg .getContentType ();
136135 }
137136 if (!formatContentTypeMap .containsKey (formatType )) {
@@ -141,15 +140,35 @@ private String getContentType(String userData, FormatType formatType) throws Mes
141140 return formatContentTypeMap .get (formatType );
142141 }
143142
144- protected MimeBodyPart generateBodyPartMIMEMessage (String userData , FormatType formatType ) throws MessagingException {
143+ protected String getBodyPartContentAsString (BodyPart bodyPart ) throws MessagingException , IOException {
144+ Object content = bodyPart .getContent ();
145+ if (content instanceof BASE64DecoderStream ) {
146+ return new String (((BASE64DecoderStream )bodyPart .getContent ()).readAllBytes ());
147+ } else if (content instanceof ByteArrayInputStream ) {
148+ return new String (((ByteArrayInputStream )bodyPart .getContent ()).readAllBytes ());
149+ } else if (content instanceof String ) {
150+ return (String )bodyPart .getContent ();
151+ }
152+ throw new CloudRuntimeException (String .format ("Failed to get content for multipart data with content type: %s" , getBodyPartContentType (bodyPart )));
153+ }
154+
155+ private String getBodyPartContentType (BodyPart bodyPart ) throws MessagingException {
156+ String contentType = StringUtils .defaultString (bodyPart .getDataHandler ().getContentType (), bodyPart .getContentType ());
157+ return contentType .contains (";" ) ? contentType .substring (0 , contentType .indexOf (';' )) : contentType ;
158+ }
159+
160+ protected MimeBodyPart generateBodyPartMimeMessage (String userData , String contentType ) throws MessagingException {
145161 MimeBodyPart bodyPart = new MimeBodyPart ();
146- String contentType = getContentType (userData , formatType );
147162 bodyPart .setContent (userData , contentType );
148163 bodyPart .addHeader ("Content-Transfer-Encoding" , "base64" );
149164 return bodyPart ;
150165 }
151166
152- private Multipart getMessageContent (MimeMessage message ) {
167+ protected MimeBodyPart generateBodyPartMimeMessage (String userData , FormatType formatType ) throws MessagingException {
168+ return generateBodyPartMimeMessage (userData , getContentType (userData , formatType ));
169+ }
170+
171+ private Multipart getMessageContent (NoIdMimeMessage message ) {
153172 Multipart messageContent ;
154173 try {
155174 messageContent = (MimeMultipart ) message .getContent ();
@@ -159,40 +178,83 @@ private Multipart getMessageContent(MimeMessage message) {
159178 return messageContent ;
160179 }
161180
162- private void addBodyPartsToMessageContentFromUserDataContent (Multipart messageContent ,
163- MimeMessage msgFromUserdata ) throws MessagingException , IOException {
164- Multipart msgFromUserdataParts = (MimeMultipart ) msgFromUserdata .getContent ();
165- int count = msgFromUserdataParts .getCount ();
166- int i = 0 ;
167- while (i < count ) {
168- BodyPart bodyPart = msgFromUserdataParts .getBodyPart (0 );
169- messageContent .addBodyPart (bodyPart );
170- i ++;
181+ private void addBodyPartToMultipart (Multipart existingMultipart , MimeBodyPart bodyPart ) throws MessagingException , IOException {
182+ boolean added = false ;
183+ final int existingCount = existingMultipart .getCount ();
184+ for (int j = 0 ; j < existingCount ; ++j ) {
185+ MimeBodyPart existingBodyPart = (MimeBodyPart )existingMultipart .getBodyPart (j );
186+ String existingContentType = getBodyPartContentType (existingBodyPart );
187+ String newContentType = getBodyPartContentType (bodyPart );
188+ if (existingContentType .equals (newContentType )) {
189+ String existingContent = getBodyPartContentAsString (existingBodyPart );
190+ String newContent = getBodyPartContentAsString (bodyPart );
191+ // generating a combined content MimeBodyPart to replace
192+ MimeBodyPart combinedBodyPart = generateBodyPartMimeMessage (
193+ simpleAppendSameFormatTypeUserData (existingContent , newContent ), existingContentType );
194+ existingMultipart .removeBodyPart (j );
195+ existingMultipart .addBodyPart (combinedBodyPart , j );
196+ added = true ;
197+ break ;
198+ }
199+ }
200+ if (!added ) {
201+ existingMultipart .addBodyPart (bodyPart );
202+ }
203+ }
204+
205+ private void addBodyPartsToMessageContentFromUserDataContent (Multipart existingMultipart ,
206+ NoIdMimeMessage msgFromUserdata ) throws MessagingException , IOException {
207+ MimeMultipart newMultipart = (MimeMultipart )msgFromUserdata .getContent ();
208+ final int existingCount = existingMultipart .getCount ();
209+ final int newCount = newMultipart .getCount ();
210+ for (int i = 0 ; i < newCount ; ++i ) {
211+ BodyPart bodyPart = newMultipart .getBodyPart (i );
212+ if (existingCount == 0 ) {
213+ existingMultipart .addBodyPart (bodyPart );
214+ continue ;
215+ }
216+ addBodyPartToMultipart (existingMultipart , (MimeBodyPart )bodyPart );
171217 }
172218 }
173219
174- private MimeMessage createMultipartMessageAddingUserdata (String userData , FormatType formatType ,
175- MimeMessage message ) throws MessagingException , IOException {
176- MimeMessage newMessage = new MimeMessage (session );
220+ private NoIdMimeMessage createMultipartMessageAddingUserdata (String userData , FormatType formatType ,
221+ NoIdMimeMessage message ) throws MessagingException , IOException {
222+ NoIdMimeMessage newMessage = new NoIdMimeMessage (session );
177223 Multipart messageContent = getMessageContent (message );
178224
179225 if (formatType == FormatType .MIME ) {
180- MimeMessage msgFromUserdata = new MimeMessage (session , new ByteArrayInputStream (userData .getBytes ()));
226+ NoIdMimeMessage msgFromUserdata = new NoIdMimeMessage (session , new ByteArrayInputStream (userData .getBytes ()));
181227 addBodyPartsToMessageContentFromUserDataContent (messageContent , msgFromUserdata );
182228 } else {
183- MimeBodyPart part = generateBodyPartMIMEMessage (userData , formatType );
184- messageContent . addBodyPart ( part );
229+ MimeBodyPart part = generateBodyPartMimeMessage (userData , formatType );
230+ addBodyPartToMultipart ( messageContent , part );
185231 }
186232 newMessage .setContent (messageContent );
187233 return newMessage ;
188234 }
189235
236+ private String simpleAppendSameFormatTypeUserData (String userData1 , String userData2 ) {
237+ return String .format ("%s\n \n %s" , userData1 , userData2 .substring (userData2 .indexOf ('\n' )+1 ));
238+ }
239+
240+ private void checkGzipAppend (String encodedUserData1 , String encodedUserData2 ) {
241+ if (isGZipped (encodedUserData1 ) || isGZipped (encodedUserData2 )) {
242+ throw new CloudRuntimeException ("Gzipped user data can not be used together with other user data formats" );
243+ }
244+ }
245+
190246 @ Override
191- public String appendUserData (String userData1 , String userData2 ) {
247+ public String appendUserData (String encodedUserData1 , String encodedUserData2 ) {
192248 try {
249+ checkGzipAppend (encodedUserData1 , encodedUserData2 );
250+ String userData1 = new String (Base64 .decodeBase64 (encodedUserData1 ));
251+ String userData2 = new String (Base64 .decodeBase64 (encodedUserData2 ));
193252 FormatType formatType1 = getUserDataFormatType (userData1 );
194253 FormatType formatType2 = getUserDataFormatType (userData2 );
195- MimeMessage message = new MimeMessage (session );
254+ if (formatType1 .equals (formatType2 ) && List .of (FormatType .CLOUD_CONFIG , FormatType .BASH_SCRIPT ).contains (formatType1 )) {
255+ return simpleAppendSameFormatTypeUserData (userData1 , userData2 );
256+ }
257+ NoIdMimeMessage message = new NoIdMimeMessage (session );
196258 message = createMultipartMessageAddingUserdata (userData1 , formatType1 , message );
197259 message = createMultipartMessageAddingUserdata (userData2 , formatType2 , message );
198260 ByteArrayOutputStream output = new ByteArrayOutputStream ();
@@ -205,4 +267,20 @@ public String appendUserData(String userData1, String userData2) {
205267 throw new CloudRuntimeException (msg , e );
206268 }
207269 }
270+
271+ /* This is a wrapper class just to remove Message-ID header from the resultant
272+ multipart data which may contain server details.
273+ */
274+ private class NoIdMimeMessage extends MimeMessage {
275+ NoIdMimeMessage (Session session ) {
276+ super (session );
277+ }
278+ NoIdMimeMessage (Session session , InputStream is ) throws MessagingException {
279+ super (session , is );
280+ }
281+ @ Override
282+ protected void updateMessageID () throws MessagingException {
283+ removeHeader ("Message-ID" );
284+ }
285+ }
208286}
0 commit comments