cxf-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From cohei...@apache.org
Subject svn commit: r1180086 - in /cxf/trunk/services/sts/sts-core/src: main/java/org/apache/cxf/sts/ main/java/org/apache/cxf/sts/token/provider/ main/java/org/apache/cxf/sts/token/validator/ test/java/org/apache/cxf/sts/token/validator/
Date Fri, 07 Oct 2011 16:07:44 GMT
Author: coheigea
Date: Fri Oct  7 16:07:43 2011
New Revision: 1180086

URL: http://svn.apache.org/viewvc?rev=1180086&view=rev
Log:
[CXF-2850] - SAML and UsernameToken validation should validate the realm of the incoming token
against cached tokens
 - Patch applied, thanks

Added:
    cxf/trunk/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/CacheSAMLRealmCodec.java
    cxf/trunk/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/SAMLTokenValidatorCachedRealmTest.java
Modified:
    cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/STSConstants.java
    cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SAMLTokenProvider.java
    cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SCTProvider.java
    cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/SAMLTokenValidator.java
    cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/UsernameTokenValidator.java

Modified: cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/STSConstants.java
URL: http://svn.apache.org/viewvc/cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/STSConstants.java?rev=1180086&r1=1180085&r2=1180086&view=diff
==============================================================================
--- cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/STSConstants.java (original)
+++ cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/STSConstants.java Fri
Oct  7 16:07:43 2011
@@ -124,6 +124,11 @@ public final class STSConstants {
     public static final String INVALID_REASON = 
         "The Trust service did not successfully validate the input";
     
+    /**
+     * Constant to store the realms in cached Security Token properties.
+     */
+    public static final String TOKEN_REALM = "org.apache.cxf.sts.token.realm";
+    
     private STSConstants() {
         // complete
     }

Modified: cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SAMLTokenProvider.java
URL: http://svn.apache.org/viewvc/cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SAMLTokenProvider.java?rev=1180086&r1=1180085&r2=1180086&view=diff
==============================================================================
--- cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SAMLTokenProvider.java
(original)
+++ cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SAMLTokenProvider.java
Fri Oct  7 16:07:43 2011
@@ -24,6 +24,7 @@ import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Properties;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -127,6 +128,14 @@ public class SAMLTokenProvider implement
                     hash = Arrays.hashCode(signatureValue);
                     securityToken.setAssociatedHash(hash);
                 }
+                if (tokenParameters.getRealm() != null) {
+                    Properties props = securityToken.getProperties();
+                    if (props == null) {
+                        props = new Properties();
+                    }
+                    props.setProperty(STSConstants.TOKEN_REALM, tokenParameters.getRealm());
+                    securityToken.setProperties(props);
+                }
                 Integer timeToLive = (int)(conditionsProvider.getLifetime() * 1000);
                 tokenParameters.getTokenStore().add(securityToken, timeToLive);
             }

Modified: cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SCTProvider.java
URL: http://svn.apache.org/viewvc/cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SCTProvider.java?rev=1180086&r1=1180085&r2=1180086&view=diff
==============================================================================
--- cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SCTProvider.java
(original)
+++ cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SCTProvider.java
Fri Oct  7 16:07:43 2011
@@ -19,6 +19,7 @@
 
 package org.apache.cxf.sts.token.provider;
 
+import java.util.Properties;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -26,6 +27,7 @@ import org.w3c.dom.Document;
 
 import org.apache.cxf.common.logging.LogUtils;
 import org.apache.cxf.helpers.DOMUtils;
+import org.apache.cxf.sts.STSConstants;
 import org.apache.cxf.sts.request.TokenRequirements;
 import org.apache.cxf.ws.security.sts.provider.STSException;
 import org.apache.cxf.ws.security.tokenstore.SecurityToken;
@@ -131,6 +133,14 @@ public class SCTProvider implements Toke
             SecurityToken token = new SecurityToken(sct.getIdentifier());
             token.setSecret(keyHandler.getSecret());
             token.setPrincipal(tokenParameters.getPrincipal());
+            if (tokenParameters.getRealm() != null) {
+                Properties props = token.getProperties();
+                if (props == null) {
+                    props = new Properties();
+                }
+                props.setProperty(STSConstants.TOKEN_REALM, tokenParameters.getRealm());
+                token.setProperties(props);
+            }
             if (lifetime > 0) {
                 Integer lifetimeInteger = new Integer(Long.valueOf(lifetime).intValue());
                 tokenParameters.getTokenStore().add(token, lifetimeInteger);

Modified: cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/SAMLTokenValidator.java
URL: http://svn.apache.org/viewvc/cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/SAMLTokenValidator.java?rev=1180086&r1=1180085&r2=1180086&view=diff
==============================================================================
--- cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/SAMLTokenValidator.java
(original)
+++ cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/SAMLTokenValidator.java
Fri Oct  7 16:07:43 2011
@@ -21,6 +21,7 @@ package org.apache.cxf.sts.token.validat
 import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Properties;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -29,6 +30,7 @@ import javax.security.auth.callback.Call
 import org.w3c.dom.Element;
 
 import org.apache.cxf.common.logging.LogUtils;
+import org.apache.cxf.sts.STSConstants;
 import org.apache.cxf.sts.STSPropertiesMBean;
 import org.apache.cxf.sts.request.ReceivedToken;
 import org.apache.cxf.sts.request.TokenRequirements;
@@ -182,6 +184,16 @@ public class SAMLTokenValidator implemen
             String tokenRealm = null;
             if (samlRealmCodec != null) {
                 tokenRealm = samlRealmCodec.getRealmFromToken(assertion);
+                // verify the realm against the cached token
+                if (secToken != null) {
+                    Properties props = secToken.getProperties();
+                    if (props != null) {
+                        String cachedRealm = props.getProperty(STSConstants.TOKEN_REALM);
+                        if (!tokenRealm.equals(cachedRealm)) {
+                            return response;
+                        }
+                    }
+                }
             }
             
             response.setValid(true);

Modified: cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/UsernameTokenValidator.java
URL: http://svn.apache.org/viewvc/cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/UsernameTokenValidator.java?rev=1180086&r1=1180085&r2=1180086&view=diff
==============================================================================
--- cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/UsernameTokenValidator.java
(original)
+++ cxf/trunk/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/UsernameTokenValidator.java
Fri Oct  7 16:07:43 2011
@@ -19,6 +19,7 @@
 package org.apache.cxf.sts.token.validator;
 
 import java.security.Principal;
+import java.util.Properties;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -34,6 +35,7 @@ import org.w3c.dom.Element;
 import org.apache.cxf.common.logging.LogUtils;
 import org.apache.cxf.helpers.DOMUtils;
 import org.apache.cxf.sts.QNameConstants;
+import org.apache.cxf.sts.STSConstants;
 import org.apache.cxf.sts.STSPropertiesMBean;
 import org.apache.cxf.sts.request.ReceivedToken;
 import org.apache.cxf.sts.request.TokenRequirements;
@@ -182,6 +184,16 @@ public class UsernameTokenValidator impl
             String tokenRealm = null;
             if (usernameTokenRealmCodec != null) {
                 tokenRealm = usernameTokenRealmCodec.getRealmFromToken(ut);
+                // verify the realm against the cached token
+                if (secToken != null) {
+                    Properties props = secToken.getProperties();
+                    if (props != null) {
+                        String cachedRealm = props.getProperty(STSConstants.TOKEN_REALM);
+                        if (!tokenRealm.equals(cachedRealm)) {
+                            return response;
+                        }
+                    }
+                }
             }
             
             response.setPrincipal(principal);

Added: cxf/trunk/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/CacheSAMLRealmCodec.java
URL: http://svn.apache.org/viewvc/cxf/trunk/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/CacheSAMLRealmCodec.java?rev=1180086&view=auto
==============================================================================
--- cxf/trunk/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/CacheSAMLRealmCodec.java
(added)
+++ cxf/trunk/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/CacheSAMLRealmCodec.java
Fri Oct  7 16:07:43 2011
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.sts.token.validator;
+
+import org.apache.cxf.sts.token.realm.SAMLRealmCodec;
+import org.apache.ws.security.saml.ext.AssertionWrapper;
+
+
+/**
+ * This class returns a realm associated with a SAML Assertion depending on the issuer.
+ */
+public class CacheSAMLRealmCodec implements SAMLRealmCodec {
+    
+    /**
+     * Get the realm associated with the AssertionWrapper parameter
+     * @param assertion a SAML Assertion wrapper object
+     * @return the realm associated with the AssertionWrapper parameter
+     */
+    public String getRealmFromToken(AssertionWrapper assertion) {
+        if ("A-Issuer".equals(assertion.getIssuerString())) {
+            return "C";
+        } else if ("B-Issuer".equals(assertion.getIssuerString())) {
+            return "D";
+        }
+        return null;
+    }
+}

Added: cxf/trunk/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/SAMLTokenValidatorCachedRealmTest.java
URL: http://svn.apache.org/viewvc/cxf/trunk/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/SAMLTokenValidatorCachedRealmTest.java?rev=1180086&view=auto
==============================================================================
--- cxf/trunk/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/SAMLTokenValidatorCachedRealmTest.java
(added)
+++ cxf/trunk/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/SAMLTokenValidatorCachedRealmTest.java
Fri Oct  7 16:07:43 2011
@@ -0,0 +1,247 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.sts.token.validator;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.security.auth.callback.CallbackHandler;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import org.apache.cxf.jaxws.context.WebServiceContextImpl;
+import org.apache.cxf.jaxws.context.WrappedMessageContext;
+import org.apache.cxf.message.MessageImpl;
+import org.apache.cxf.sts.STSConstants;
+import org.apache.cxf.sts.StaticSTSProperties;
+import org.apache.cxf.sts.cache.DefaultInMemoryTokenStore;
+import org.apache.cxf.sts.cache.STSTokenStore;
+import org.apache.cxf.sts.common.PasswordCallbackHandler;
+import org.apache.cxf.sts.request.KeyRequirements;
+import org.apache.cxf.sts.request.ReceivedToken;
+import org.apache.cxf.sts.request.TokenRequirements;
+import org.apache.cxf.sts.service.EncryptionProperties;
+import org.apache.cxf.sts.token.provider.SAMLTokenProvider;
+import org.apache.cxf.sts.token.provider.TokenProvider;
+import org.apache.cxf.sts.token.provider.TokenProviderParameters;
+import org.apache.cxf.sts.token.provider.TokenProviderResponse;
+import org.apache.cxf.sts.token.realm.SAMLRealm;
+import org.apache.cxf.sts.token.realm.SAMLRealmCodec;
+import org.apache.ws.security.CustomTokenPrincipal;
+import org.apache.ws.security.WSConstants;
+import org.apache.ws.security.WSSecurityException;
+import org.apache.ws.security.components.crypto.Crypto;
+import org.apache.ws.security.components.crypto.CryptoFactory;
+import org.junit.BeforeClass;
+
+
+/**
+ * Some unit tests for validating a SAML token with a realm against the cached tokens realm.
+ */
+public class SAMLTokenValidatorCachedRealmTest extends org.junit.Assert {
+    
+    private static STSTokenStore tokenStore;
+    
+    @BeforeClass
+    public static void init() {
+        tokenStore = new DefaultInMemoryTokenStore();
+    }
+    
+    /**
+     * Test a SAML Assertion with valid cached realm.
+     */
+    @org.junit.Test
+    public void testValidCachedRealm() throws Exception {
+        TokenValidator samlTokenValidator = new SAMLTokenValidator();
+        TokenValidatorParameters validatorParameters = createValidatorParameters();
+        TokenRequirements tokenRequirements = validatorParameters.getTokenRequirements();
+        
+        // Create a ValidateTarget consisting of a SAML Assertion
+        Crypto crypto = CryptoFactory.getInstance(getEncryptionProperties());
+        CallbackHandler callbackHandler = new PasswordCallbackHandler();
+        Element samlToken = 
+            createSAMLAssertion(WSConstants.WSS_SAML_TOKEN_TYPE, crypto, "mystskey", callbackHandler,
"A");
+        Document doc = samlToken.getOwnerDocument();
+        samlToken = (Element)doc.appendChild(samlToken);
+        
+        ReceivedToken validateTarget = new ReceivedToken(samlToken);
+        tokenRequirements.setValidateTarget(validateTarget);
+        
+        // Now set the SAMLRealmCodec implementation on the Validator
+        SAMLRealmCodec samlRealmCodec = new IssuerSAMLRealmCodec();
+        ((SAMLTokenValidator)samlTokenValidator).setSamlRealmCodec(samlRealmCodec);
+        
+        TokenValidatorResponse validatorResponse = samlTokenValidator.validateToken(validatorParameters);
+        assertTrue(validatorResponse != null);
+        assertTrue(validatorResponse.isValid());
+        assertTrue(validatorResponse.getTokenRealm().equals("A"));
+        
+    }
+    
+    /**
+     * Test a SAML Assertion with invalid cached realm.
+     */
+    @org.junit.Test
+    public void testInValidCachedRealm() throws Exception {
+        TokenValidator samlTokenValidator = new SAMLTokenValidator();
+        TokenValidatorParameters validatorParameters = createValidatorParameters();
+        TokenRequirements tokenRequirements = validatorParameters.getTokenRequirements();
+        
+        // Create a ValidateTarget consisting of a SAML Assertion
+        Crypto crypto = CryptoFactory.getInstance(getEncryptionProperties());
+        CallbackHandler callbackHandler = new PasswordCallbackHandler();
+        Element samlToken = 
+            createSAMLAssertion(WSConstants.WSS_SAML_TOKEN_TYPE, crypto, "mystskey", callbackHandler,
"A");
+        Document doc = samlToken.getOwnerDocument();
+        samlToken = (Element)doc.appendChild(samlToken);
+        
+        ReceivedToken validateTarget = new ReceivedToken(samlToken);
+        tokenRequirements.setValidateTarget(validateTarget);
+        
+        // Now set the SAMLRealmCodec implementation on the Validator
+        SAMLRealmCodec samlRealmCodec = new CacheSAMLRealmCodec();
+        ((SAMLTokenValidator)samlTokenValidator).setSamlRealmCodec(samlRealmCodec);
+        
+        TokenValidatorResponse validatorResponse = samlTokenValidator.validateToken(validatorParameters);
+        assertTrue(validatorResponse != null);
+        assertFalse(validatorResponse.isValid());
+        assertNull(validatorResponse.getTokenRealm());
+        
+    }
+    
+    private TokenValidatorParameters createValidatorParameters() throws WSSecurityException
{
+        TokenValidatorParameters parameters = new TokenValidatorParameters();
+        
+        TokenRequirements tokenRequirements = new TokenRequirements();
+        tokenRequirements.setTokenType(STSConstants.STATUS);
+        parameters.setTokenRequirements(tokenRequirements);
+        
+        KeyRequirements keyRequirements = new KeyRequirements();
+        parameters.setKeyRequirements(keyRequirements);
+        
+        parameters.setPrincipal(new CustomTokenPrincipal("alice"));
+        // Mock up message context
+        MessageImpl msg = new MessageImpl();
+        WrappedMessageContext msgCtx = new WrappedMessageContext(msg);
+        WebServiceContextImpl webServiceContext = new WebServiceContextImpl(msgCtx);
+        parameters.setWebServiceContext(webServiceContext);
+        
+        // Add STSProperties object
+        StaticSTSProperties stsProperties = new StaticSTSProperties();
+        Crypto crypto = CryptoFactory.getInstance(getEncryptionProperties());
+        stsProperties.setEncryptionCrypto(crypto);
+        stsProperties.setSignatureCrypto(crypto);
+        stsProperties.setEncryptionUsername("myservicekey");
+        stsProperties.setSignatureUsername("mystskey");
+        stsProperties.setCallbackHandler(new PasswordCallbackHandler());
+        stsProperties.setIssuer("STS-2");
+        parameters.setStsProperties(stsProperties);
+        parameters.setTokenStore(tokenStore);
+        return parameters;
+    }
+    
+    private Element createSAMLAssertion(
+        String tokenType, 
+        Crypto crypto, 
+        String signatureUsername, 
+        CallbackHandler callbackHandler,
+        String realm
+    ) throws WSSecurityException {
+        TokenProvider samlTokenProvider = new SAMLTokenProvider();
+        TokenProviderParameters providerParameters = 
+            createProviderParameters(
+                tokenType, STSConstants.BEARER_KEY_KEYTYPE, crypto, signatureUsername, callbackHandler
+            );
+        providerParameters.setRealm(realm);
+        
+        // Create Realms
+        Map<String, SAMLRealm> samlRealms = getSamlRealms();
+        ((SAMLTokenProvider)samlTokenProvider).setRealmMap(samlRealms);
+        
+        TokenProviderResponse providerResponse = samlTokenProvider.createToken(providerParameters);
+        assertTrue(providerResponse != null);
+        assertTrue(providerResponse.getToken() != null && providerResponse.getTokenId()
!= null);
+        
+        return providerResponse.getToken();
+    }
+    
+    private Map<String, SAMLRealm> getSamlRealms() {
+        // Create Realms
+        Map<String, SAMLRealm> samlRealms = new HashMap<String, SAMLRealm>();
+        SAMLRealm samlRealm = new SAMLRealm();
+        samlRealm.setIssuer("A-Issuer");
+        samlRealms.put("A", samlRealm);
+        samlRealm = new SAMLRealm();
+        samlRealm.setIssuer("B-Issuer");
+        samlRealms.put("B", samlRealm);
+        return samlRealms;
+    }
+    
+    private TokenProviderParameters createProviderParameters(
+        String tokenType, String keyType, Crypto crypto, 
+        String signatureUsername, CallbackHandler callbackHandler
+    ) throws WSSecurityException {
+        TokenProviderParameters parameters = new TokenProviderParameters();
+
+        TokenRequirements tokenRequirements = new TokenRequirements();
+        tokenRequirements.setTokenType(tokenType);
+        parameters.setTokenRequirements(tokenRequirements);
+
+        KeyRequirements keyRequirements = new KeyRequirements();
+        keyRequirements.setKeyType(keyType);
+        parameters.setKeyRequirements(keyRequirements);
+
+        parameters.setPrincipal(new CustomTokenPrincipal("alice"));
+        // Mock up message context
+        MessageImpl msg = new MessageImpl();
+        WrappedMessageContext msgCtx = new WrappedMessageContext(msg);
+        WebServiceContextImpl webServiceContext = new WebServiceContextImpl(msgCtx);
+        parameters.setWebServiceContext(webServiceContext);
+
+        parameters.setAppliesToAddress("http://dummy-service.com/dummy");
+
+        // Add STSProperties object
+        StaticSTSProperties stsProperties = new StaticSTSProperties();
+        stsProperties.setSignatureCrypto(crypto);
+        stsProperties.setSignatureUsername(signatureUsername);
+        stsProperties.setCallbackHandler(callbackHandler);
+        stsProperties.setIssuer("STS");
+        parameters.setStsProperties(stsProperties);
+
+        parameters.setEncryptionProperties(new EncryptionProperties());
+        parameters.setTokenStore(tokenStore);
+        return parameters;
+    }
+    
+    private Properties getEncryptionProperties() {
+        Properties properties = new Properties();
+        properties.put(
+            "org.apache.ws.security.crypto.provider", "org.apache.ws.security.components.crypto.Merlin"
+        );
+        properties.put("org.apache.ws.security.crypto.merlin.keystore.password", "stsspass");
+        properties.put("org.apache.ws.security.crypto.merlin.keystore.file", "stsstore.jks");
+        
+        return properties;
+    }
+    
+    
+}
\ No newline at end of file



Mime
View raw message