Buscar este blog

viernes, 25 de marzo de 2016

Java servlet download log file

This is a basic servlet to download the log files of  a web app.
The servlet has the following config params:
  • logsConfigLocation. The base dir in which the log files are stored. It is not allowed to read files outside this directory.
  • logsMaxSize. The maximum size of the log file allowed to download
The config params can be configured usen Spring Expression Language, so a valid value could be ${my.config.property:/home/jboss/logs}
If none is specified, the default values are jboss.server.log.dir and 10MB respectively.


In order to prevent public access to this files, the servlet can be configured to use basic authentication. In this case, you need to configure the appropriate login mechanism in the app, form example, in web.xml:
<login-config>
 <auth-method>BASIC</auth-method>
 <realm-name>default</realm-name>
</login-config>

The servlet receives a request param called "path" which contains the relative route of the requested log file.

LoggerServlet:
package es.sisifo.jee6.servlet;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.HttpConstraint;
import javax.servlet.annotation.ServletSecurity;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.util.ServletContextPropertyUtils;


@WebServlet(urlPatterns = { "/loggerServlet" })
@ServletSecurity(@HttpConstraint(rolesAllowed = { "admin" }))
public class LoggerServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    public static final String CONFIG_LOCATION_PARAM = "logsConfigLocation";
    public static final String CONFIG_MAXSIXE_PARAM = "logsMaxSize";

    private String logsLocation = System.getProperty("jboss.server.log.dir");
    private int maxFileSize = 10485760; // 10MB

    @Override
    public void init(final ServletConfig servletConfig) throws ServletException {
        super.init(servletConfig);

        if (servletConfig.getInitParameter(CONFIG_LOCATION_PARAM) != null) {
            logsLocation = ServletContextPropertyUtils.resolvePlaceholders(
                    servletConfig.getInitParameter(CONFIG_LOCATION_PARAM), servletConfig.getServletContext());
        }

        if (servletConfig.getInitParameter(CONFIG_MAXSIXE_PARAM) != null) {
            maxFileSize = Integer.valueOf(ServletContextPropertyUtils.resolvePlaceholders(
                    servletConfig.getInitParameter(CONFIG_MAXSIXE_PARAM), servletConfig.getServletContext()));
        }
    }


    @Override
    protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException,
            IOException {

        final String path = request.getParameter("path");
        if (!isValidPath(path)) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid path");
            return;
        }

        final File logFile = new File(logsLocation + File.separator + path);
        if (!isValidFile(logFile)) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid file");
            return;
        }

        downloadFile(logFile, response);
    }


    private void downloadFile(final File logFile, final HttpServletResponse response) throws IOException {
        final FileInputStream inStream = new FileInputStream(logFile);

        response.setContentType(getServletContext().getMimeType(logFile.getAbsolutePath()));
        response.setContentLength((int) logFile.length());
        response.setHeader("Content-Disposition", String.format("attachment; filename=\"%s\"", logFile.getName()));


        final OutputStream outStream = response.getOutputStream();

        final byte[] buffer = new byte[4096];
        int bytesRead = -1;
        while ((bytesRead = inStream.read(buffer)) != -1) {
            outStream.write(buffer, 0, bytesRead);
        }

        inStream.close();
        outStream.close();
    }


    private boolean isValidPath(final String path) {
        return path != null && !path.contains("..");
    }


    private boolean isValidFile(final File logFile) {
        return logFile.exists() && logFile.isFile() && logFile.length() <= maxFileSize;
    }
}

domingo, 13 de marzo de 2016

Tomcat - Custom request dump filter - Log request and response body

Tomcat 7 has two components to intercept server requests:
Both are almost the same, with the difference that valves are tomcat specific.
In previous versions, tomcat had a valve to log http traffic, but in the last releases it was eliminated and substituted by a filter called Request_Dumper_Filter (https://tomcat.apache.org/tomcat-7.0-doc/config/filter.html#Request_Dumper_Filter).
You can configure this filter in tomcat server.xml, so it will intercept all requests for all the applications hosted by the server (well, you can specify a URL pattern). In this way, if you want to monitor a concrete app, you dont need to change it, just reconfigure tomcat.

The problem with Request_Dumper_Filter is that it does not work properly with application/x-www-form-urlencoded POST requests. So it may make the back-end servlets to fail, because when you read the request parameters, you are consuming its input stream.

The solution is to create a custom Dumper Filter to solve this. I found two interesting partial implementations here and here, based in request and response wrappers, and unify them in a single filter.

The code is in https://github.com/evazquezma/tomcat/tree/master/custom-tomcat-utils.

In order to use this filter you need to do as follows:
1) Copy the jar to the ${CATALINA_HOME}/lib

2) Edit ${CATALINA_HOME}/conf/server.xml and declare the filter:
<filter>
 <filter-name>requestdumper</filter-name>
 <filter-class>es.sisifo.tomcatutil.filters.SimpleRequestDumperFilter</filter-class>
</filter>
<filter-mapping>
 <filter-name>requestdumper</filter-name>
 <url-pattern>*</url-pattern>
</filter-mapping>

3) Edit ${CATALINA_HOME}/conf/loggin.properties and configure the traces:
handlers = (...), 5request-dumper.org.apache.juli.FileHandler
(....)
5request-dumper.org.apache.juli.FileHandler.level = INFO
5request-dumper.org.apache.juli.FileHandler.directory = ${catalina.base}/logs
5request-dumper.org.apache.juli.FileHandler.prefix = request-dumper.
5request-dumper.org.apache.juli.FileHandler.formatter = org.apache.juli.VerbatimFormatter
es.sisifo.tomcatutil.filters.SimpleRequestDumperFilter.level = INFO
es.sisifo.tomcatutil.filters.SimpleRequestDumperFilter.handlers = \
5request-dumper.org.apache.juli.FileHandler

The result will be something like this (this is a log from this app):
http-bio-8080-exec-4 ******************=********************************************
http-bio-8080-exec-4 START TIME       a =13-mar-2016 12:07:02
http-bio-8080-exec-4         requestURI=/afirma-server-triphase-signer/SignatureService
http-bio-8080-exec-4           authType=null
http-bio-8080-exec-4  characterEncoding=null
http-bio-8080-exec-4      contentLength=2259
http-bio-8080-exec-4        contentType=application/x-www-form-urlencoded
http-bio-8080-exec-4        contextPath=/afirma-server-triphase-signer
http-bio-8080-exec-4             header=accept=*/*
http-bio-8080-exec-4             header=content-type=application/x-www-form-urlencoded
http-bio-8080-exec-4             header=user-agent=Java/1.8.0_60
http-bio-8080-exec-4             header=host=localhost:8080
http-bio-8080-exec-4             header=connection=keep-alive
http-bio-8080-exec-4             header=content-length=2259
http-bio-8080-exec-4             locale=es_ES
http-bio-8080-exec-4             method=POST
http-bio-8080-exec-4          parameter=op=pre
http-bio-8080-exec-4          parameter=cop=sign
http-bio-8080-exec-4          parameter=format=pades
http-bio-8080-exec-4          parameter=algo=SHA512withRSA
http-bio-8080-exec-4          parameter=cert=MIIC6jCCAdKgAwIBAgIEVtGLlDANBgkqhkiG9w0BAQsFADA9MRswGQYDVQQLDBJKYm9zcyBBcnF1aXRlY3R1cmExHjAcBgNVBAMMFUNBLUpCT1NTIEFycXVpdGVjdHVyYTAgFw0xNjAyMjcxMTQyMzNaGA8yMTE1MDIyNzExNDIzM1owLzENMAsGA1UECgwEdGVzdDENMAsGA1UECwwEdGVzdDEPMA0GA1UEAwwGc2lzaWZvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6GJwUNgmPFoE48U-8CU2jdTPRTl-GthiGyk7ggo-V3MYMgQ_7imW7WzAm7rGVncwtJ_-JYiHSG-Kry1Fb558ux2_9yLY6cfOvQroid9kpH2lRoZ-pBBl1Ww7GO3Z0FZKVf0TJIIDSv__NuPFIV0IxEYezWWDCW9NfzKqjrkbdNgJB07T8M7cWUuojW8LcbV_i3Z-m0pLif51cbdyLjowtbqKN_b04tP74PjQt5hI9tmBRIL51bRebAn6xc2C34_fiIf9AEETpRV_YLh80eClueeb4qiiaSWkYwo11FCswZE0eHSBTpwYrHX1IWMdZ64-IUYAwSvz4Ypx0Ma_JEiFJQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBtI5LSsXlCK4u3PRjYXdD-lruCEIpw1YNow9SYIe_zyg_W060R0Ir-Xr3wEt6Buf1vjfOSg5J1hF3xxoRt-Dvj1La6xWNnYiF8g2mozNFs-FeL8m1Tv7Ll8LKShmOyJXDzkEpBcK4eZp92mgBH2JuOF8emhrP6BJqb2C4fR6KL3um4OlgIynGWYcdZvXcUt3k5rU6QShrCNxpfraPzeGzDifG2zmSI4GraoB7FwTiMLDjg8_20kmxot_w5Tm31D79F-XL2_qfP9TQL_qhV7F-5WR-bMSU1oa3hI0rJYvRuNF8KsPEaRUoLvRpEpW8dJ-XEgUL9etVAuSEk3obPFzJF,MIIC-DCCAeCgAwIBAgIEVMp9_TANBgkqhkiG9w0BAQsFADA9MRswGQYDVQQLDBJKYm9zcyBBcnF1aXRlY3R1cmExHjAcBgNVBAMMFUNBLUpCT1NTIEFycXVpdGVjdHVyYTAgFw0xNTAxMjkxODM4MTFaGA8yMTE0MDEyOTE4MzgxMVowPTEbMBkGA1UECwwSSmJvc3MgQXJxdWl0ZWN0dXJhMR4wHAYDVQQDDBVDQS1KQk9TUyBBcnF1aXRlY3R1cmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCBnpiP835iOWmqrDdgBWyp8yaG6pe_PVI944YqU_AKlrsa_WZFxMRy6Uri0TGuT5hlmUJsw6e7fDR9bf7F4K5wE4ZSEVG_njlDShjlsJ4CgvVfneK-kOva9KzlpUM3do-Bm-3WbfvHl0d8eujc_sHD4uSyH1yI04r9YrHN0OeY1qFfsowS3EzPdTsw9oeoBAyW4nVZQG-VqXwBSnkwxQOEERN_-3l9tSxEV12gRDieUJj5y3USdfxHruqNPbmAu6yc2rXIR7yeeq0CCrbnvTlW81lCVWvSVV2o9fFt-irdP4P20e-ecR-24VzIV7Brm4LE3snmExDb-aLF6XmqiokTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAcsXzdYsrAnUwd269PMUNa4cSBnDk7g9YY36KHIDLo72XTJOgbeb-pQXWEwr5A5vuH1MeT_CHYMJkPvqM4kaFVN4zQVOhi3XKzTSAsLCERprvwU0bg2Nk65TeE6SIELWfWkcdko8x3Hn0uwEniJXA275G_kT15aBQ1tiyEGhYwPpO3pdKeMRmtFZfyvuernVAko9-FCBauH7KFZGIpne-VzA2rIFa0kTta1HesVeoAmetltgvxGdNrwnQJZfg8PNTF3ceQSyK0quCJv4nxcy_pwV-tQRXXLScvIFRuXdu6782PSDFa17TYY9FOPmyMKiwsz-yjCu8JB9xuPCrzRnuE=
http-bio-8080-exec-4          parameter=doc=cHJ1ZWJhLnBkZg==
http-bio-8080-exec-4          parameter=params=Iw0KI1N1biBNYXIgMTMgMTI6MDc6MDIgQ0VUIDIwMTYNCnNlcnZlclVybD1odHRwXDovL2xvY2FsaG9zdFw6ODA4MC9hZmlybWEtc2VydmVyLXRyaXBoYXNlLXNpZ25lci9TaWduYXR1cmVTZXJ2aWNlDQo=
http-bio-8080-exec-4           pathInfo=null
http-bio-8080-exec-4           protocol=HTTP/1.1
http-bio-8080-exec-4        queryString=null
http-bio-8080-exec-4         remoteAddr=127.0.0.1
http-bio-8080-exec-4         remoteHost=127.0.0.1
http-bio-8080-exec-4         remoteUser=null
http-bio-8080-exec-4 requestedSessionId=null
http-bio-8080-exec-4             scheme=http
http-bio-8080-exec-4         serverName=localhost
http-bio-8080-exec-4         serverPort=8080
http-bio-8080-exec-4        servletPath=/SignatureService
http-bio-8080-exec-4           isSecure=false
http-bio-8080-exec-4           authType=null
http-bio-8080-exec-4         remoteUser=null
http-bio-8080-exec-4 ------------------=--------------------------------------------
http-bio-8080-exec-4 ------------------=--------------------------------------------
http-bio-8080-exec-4        contentType=text/plain;charset=utf-8
http-bio-8080-exec-4             header=Access-Control-Allow-Origin=*
http-bio-8080-exec-4             status=200
http-bio-8080-exec-4 Response body=PHhtbD4KIDxmaXJtYXM-CiAgPGZpcm1hIElkPSI5MzQ2YTllYi00NDNlLTRhYjAtODVhZC03ODE1YjViMjFhM2IiPgogICA8cGFyYW0gbj0iTkVFRF9QUkUiPnRydWU8L3BhcmFtPgogICA8cGFyYW0gbj0iVElNRSI-MTQ1Nzg2NzIyMjY3ODwvcGFyYW0-CiAgIDxwYXJhbSBuPSJQUkUiPk1ZSUJJekFZQmdrcWhraUc5dzBCQ1FNeEN3WUpLb1pJaHZjTkFRY0JNRThHQ1NxR1NJYjNEUUVKQkRGQ0JFQk1NdHpSTTNIV3F0R1NEYWlBQjBHalFKNTd0dXh3bGVIV2pBcmhTV2ViRVltd0dFcFpyYjFndGIwSUZKbGhmMmVITmkzYnpKVUlzMHBTSFg0OHhFVU5NSUcxQmdzcWhraUc5dzBCQ1JBQ0x6R0JwVENCb2pDQm56Q0JuREFOQmdsZ2hrZ0JaUU1FQWdNRkFBUkEvRWNydW1KSFhzaSt5Y2xZa05WODNlQ0RUWWZLM3VuTkhHeEpTdmRaanBwR1RIVW5lUkpmLyszVW9yV3FZdlZGelJzMkNORGlsQTFTVnVFOHN3dEtWREJKTUVHa1B6QTlNUnN3R1FZRFZRUUxEQkpLWW05emN5QkJjbkYxYVhSbFkzUjFjbUV4SGpBY0JnTlZCQU1NRlVOQkxVcENUMU5USUVGeWNYVnBkR1ZqZEhWeVlRSUVWdEdMbEE9PTwvcGFyYW0-CiAgIDxwYXJhbSBuPSJQSUQiPld6eGhPREpqWVdVMU1qZGtaR015WW1SalpURmpNR1JpTXpneU5EUTROakkxTXo0OE4yTTRNemxqT0RobE9EaGhZakU0TURKallqTm1PRE14TWpneE5XTmtOR0krWFE9PTwvcGFyYW0-CiAgPC9maXJtYT4KIDwvZmlybWFzPgo8L3htbD4=
http-bio-8080-exec-4 END TIME          =13-mar-2016 12:07:02
http-bio-8080-exec-4 ===============================================================

sábado, 27 de febrero de 2016

Spring Boot - Spring Security - Certificate authentication behind Apache Web Server

In this post I describe how to configure a Spring Boot application, using Spring Security, to authenticate the user with his personal certificate. The certificate will be requested by Apache Web server and sent to the app via AJP.

This project is based (a.k.a copied) from the Spring getting started guides:  https://spring.io/guides/gs/securing-web/. In this post I´ll only talk about the classes I changed.

In order to complete the modification, we need to make several things:
  1. Configure the Spring Boot app to use the user certificate
  2. Configure the embedded tomcat container of the spring boot app to expose an AJP port
  3. Configure apache to serve as the front-end

Configure the Spring Boot app to use the user certificate

This is very easy. You only need to add the x509 config to the HttpSecurity element.
In this example the authentication is doing with an in-memory repository, so you have to add the user certificate as a valid application user.

WebSecurityConfig.jave file:
package hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {             
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
     
   
     http.x509().subjectPrincipalRegex("CN=(.*?),");         
    }          

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {    
        auth
            .inMemoryAuthentication()
                .withUser("user").password("password").roles("USER")
                .and()
                .withUser("sisifo").password("not_used").roles("USER");
    } 
}

The x509 filter will search the user certificate in the request (in the 'javax.servlet.request.X509Certificate' attribute). When it exists, the filter will extract the "user name" and try to authenticate it against the userDetailsService, in this case the in-memory repository.

But the user will be able to loggin by using an username/password, so in the loggin HTML page you need to put a link to force him to send his certificate.

Login.html page:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>              
    </head>
    <body>
      <script type="text/javascript">         
         var copyFields = function() {
          document.getElementById('username').value = document.getElementById('usernameFake').value;
          document.getElementById('password').value = document.getElementById('passwordFake').value; 
         }
         
         var submitRealForm = function() {
          copyFields();
          document.getElementById('realLoginForm').submit();
          return false;
         }
         
         var submitFormOnEnter = function(event) {
          if (event.keyCode == 13) {
           submitRealForm();
          }
         }
        </script>
        
        <div th:if="${param.error}">
            Invalid username and password.
        </div>
        
        <div th:if="${param.logout}">
            You have been logged out.
        </div>
        
         
        <form id="realLoginForm" th:action="@{/login}" method="post" autocomplete="off" style="display:none">
            <input id="username" type="hidden" name="username"  readonly="readonly"/>
            <input id="password" type="hidden" name="password" readonly="readonly"/>       
        </form>
      
        
        <div class="simulateForm">
            <div><label> User Name : <input id="usernameFake" type="text" onkeydown="submitFormOnEnter(event)"/> </label></div>
            <div><label> Password: <input id="passwordFake" type="password" onkeydown="submitFormOnEnter(event)"/> </label></div>
            <div> <button type="button" onclick="submitRealForm()">Login user/pass</button></div>
            <div> <a th:href="@{/logincert}">Login certificate</a></div>
        </div>
    </body>    
</html>

When the user clicks in the Login certificate link he will be sent to https://serverhost/logincert. Apache will take care that in this URL the user will be asked for a valid certificate, and then Apache will sent this certificate to the app.

Configure the embedded tomcat container of the Spring Boot app to expose an AJP port

Apache will talk with the Spring Boot app by using the AJP protocol. The app is configured to use an embedded tomcat server, so you have to configure it.
This part is copied from this post http://www.appsdev.is.ed.ac.uk/blog/?p=525.

Basically, you create your own Connector based on three config properties located in application.properties file.

Application.java file:
package hello;

import org.apache.catalina.connector.Connector;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {
 @Value("${tomcat.ajp.port}")
 private int ajpPort;

 @Value("${tomcat.ajp.remoteauthentication}")
 private String remoteAuthentication;

 @Value("${tomcat.ajp.enabled}")
 private boolean tomcatAjpEnabled;
 

    @Bean
    public EmbeddedServletContainerFactory servletContainer() {
        TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory();
        if (tomcatAjpEnabled)  {
            Connector ajpConnector = new Connector("AJP/1.3");
            ajpConnector.setProtocol("AJP/1.3");
            ajpConnector.setPort(ajpPort);
            ajpConnector.setSecure(false);
            ajpConnector.setAllowTrace(false);
            ajpConnector.setScheme("http");
            tomcat.addAdditionalTomcatConnectors(ajpConnector);
        }
        return tomcat;
    }
    
    
    public static void main(String[] args) throws Throwable {
        SpringApplication.run(Application.class, args);
    }

}

And this is the properties file. Note that the server port was changed to 9080 instead 8080.
server.port = 9080

tomcat.ajp.port=9090
tomcat.ajp.remoteauthentication=false
tomcat.ajp.enabled=true

Configure apache to serve as the front-end

The final step is to configure the web server. I created two virtual host:
  • The  http virtual host will only redirect to the https virtual host.
  • The https virtual host will configure the SSL and serve as proxy to the Spring Boot app.
In the https virtual host you need two location, one of them will ask for the user certificate. The URL location match the link placed in the login page.

<VirtualHost springBoot.local:80>
   ServerName springBoot.local:80
   serverAlias springBoot.local.80

   Redirect permanent / https://springBoot.local/
</VirtualHost>



<VirtualHost springBoot.local:443>
 DocumentRoot "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/htdocs"
 ServerName springBoot.local:443
 serverAlias springBoot.local.433


 ErrorLog "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/logs/error_SpringBoot.log"
 TransferLog "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/logs/access_StpringBoot.log"

 SSLEngine on
 SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL

 SSLCertificateFile conf/keystores/desarr.local.cer
 SSLCertificateKeyFile "conf/keystores/desarr.local.key
 SSLCACertificateFile conf/keystores/desarr.local-cas.pem


 <FilesMatch "\.(cgi|shtml|phtml|php)$">
  SSLOptions +StdEnvVars
 </FilesMatch>

 <Directory "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/cgi-bin">
  SSLOptions +StdEnvVars
 </Directory>

 BrowserMatch ".*MSIE.*" \
    nokeepalive ssl-unclean-shutdown \
    downgrade-1.0 force-response-1.0


 CustomLog "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/logs/ssl_request_springBoot.log"  "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
     
    
 ProxyRequests     Off
 ProxyPreserveHost On
 SSLProxyEngine on

 <Location /logincert>
  SSLRequireSSL
  
  SSLVerifyClient require
  SSLVerifyDepth  2
  SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate
  
  ProxyPass ajp://localhost:9090/
  ProxyPassReverse ajp://localhost:9090/
 </Location>


 <Location />
    SSLRequireSSL
  ProxyPass http://localhost:9080/
  ProxyPassReverse http://localhost:9080/
 </Location>

</VirtualHost>


The source code is in my github: https://github.com/evazquezma/JEE6/tree/master/gs-security-web

viernes, 26 de febrero de 2016

HTML Login Form - Disable autocomplete and password remember

Most users use the autocomplete a password remember options of their browsers. In this way, if you try to login in a site where you previously were logged, the browser autocompletes the login, and then suggests the password.
But recently, in one critical web app, we have a requirement to disable this behaviour. What seemed a simple task, resulted in a small head ache.

We start with this basic login page.
<body>
 <div th:if="${param.error}">
  Invalid username and password.
 </div>
 
 <div th:if="${param.logout}">
  You have been logged out.
 </div>
 
 <form th:action="@{/login}" method="post">
  <div><label> User Name : <input type="text" name="username"/> </label></div>
  <div><label> Password: <input type="password" name="password"/> </label></div>
  <div><input type="submit" value="Sign In"/></div>
 </form>
</body>

In this page, the browser will ask you if you want to remember the password. Any way, the next time you visit this page, the browser will autocomplete the login field.

Step 1, disable autocomplete

If you only need to disable autocomplete (but you can afford the browser ask for password remember) there is a HTML attribute which should work... autocomplete="off"
<body>
 <div th:if="${param.error}">
  Invalid username and password.
 </div>
 
 <div th:if="${param.logout}">
  You have been logged out.
 </div>
 
 <form th:action="@{/login}" method="post" autocomplete="off">
  <div><label> User Name : <input type="text" name="username"/> </label></div>
  <div><label> Password: <input type="password" name="password"/> </label></div>
  <div><input type="submit" value="Sign In"/></div>
 </form>
</body>

Step 2, disable password remember

If you want to force the browser to not ask for password remember, this is a bit more complex. After a lot of  test, the solution I found was to create a hidden form with hidden fields. The user fills a fake form, and when she presses the submit button, these fields and the submit event are transferred to the true login form.
<body>
 <script type="text/javascript">         
  var copyFields = function() {
   document.getElementById('username').value = document.getElementById('usernameFake').value;
   document.getElementById('password').value = document.getElementById('passwordFake').value; 
  }
  
  var submitRealForm = function() {
   copyFields();
   document.getElementById('realLoginForm').submit();
   return false;
  }
 </script>
 
 <div th:if="${param.error}">
  Invalid username and password.
 </div>
 
 <div th:if="${param.logout}">
  You have been logged out.
 </div>
 
  
 <form id="realLoginForm" th:action="@{/login}" method="post" autocomplete="off" style="display:none">
  <input id="username" type="hidden" name="username"/>
  <input id="password" type="hidden" name="password"/>       
 </form>
  
 
 <div class="simulateForm">
  <div><label> User Name : <input id="usernameFake" type="text" /> </label></div>
  <div><label> Password: <input id="passwordFake" type="password"/> </label></div>
  <div> <button type="button" onclick="submitRealForm()">Click Me!</button></div>
 </div>
</body>


Step 3, caputre enter in password

When the user are inside an HTML form and she presses enter key, the browser send a click event to the first button in this form.
With the solution I made, when user press enter, nothing happens. To solve this problem, with javascript you can listen this event and trigger the submit manually.
<body>
 <script type="text/javascript">         
  var copyFields = function() {
   document.getElementById('username').value = document.getElementById('usernameFake').value;
   document.getElementById('password').value = document.getElementById('passwordFake').value; 
  }
  
  var submitRealForm = function() {
   copyFields();
   document.getElementById('realLoginForm').submit();
   return false;
  }
  
  var submitFormOnEnter = function(event) {
   if (event.keyCode == 13) {
    submitRealForm();
   }
  }
 </script>
 
 <div th:if="${param.error}">
  Invalid username and password.
 </div>
 
 <div th:if="${param.logout}">
  You have been logged out.
 </div>
 
  
 <form id="realLoginForm" th:action="@{/login}" method="post" autocomplete="off" style="display:none">
  <input id="username" type="hidden" name="username"/>
  <input id="password" type="hidden" name="password"/>       
 </form>
  
 
 <div class="simulateForm">
  <div><label> User Name : <input id="usernameFake" type="text" onkeydown="submitFormOnEnter(event)"/> </label></div>
  <div><label> Password: <input id="passwordFake" type="password" onkeydown="submitFormOnEnter(event)"/> </label></div>
  <div> <button type="button" onclick="submitRealForm()">Click Me!</button></div>
 </div>
</body>

Note.
This example is based on the Spring Security getting started (https://spring.io/guides/gs/securing-web/). Their source code is in https://github.com/spring-guides/gs-securing-web, I just played with de login.html page.

domingo, 21 de febrero de 2016

XMLHttpRequest - Download PDF and show it in iframe

These snippets show how to download a PDF from a Servlet and how to show it inside a iframe.
The immediate solution would be to invoke the servlet by GET, and to put this URL in de src attribute of the iframe, but in this case I want to invoke the servlet by POST.

Servlet code:
@WebServlet(urlPatterns="/sello")
public class SelloRegistroServlet extends HttpServlet {
 private static final long serialVersionUID = 1L;

 private final SelloService selloService = new SelloServiceImpl();

 @Override
 protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
     final DatosSello datos = recuperarDatosSello(request);
     final ConfiguracionSello config = recuperarDatosConfig(request);

     final ByteArrayOutputStream baos = selloService.generarSello(datos, config);
  
            response.setContentType("application/pdf");
            response.setContentLength(baos.size());
            response.setHeader("Content-Disposition", "inline;filename=\"selo.pdf\"");
            response.setHeader("filename", "selo.pdf");

            final OutputStream outStream = response.getOutputStream();
            outStream.write(baos.toByteArray());
            outStream.flush();
            outStream.close();
 }
 
 private DatosSello recuperarDatosSello(final HttpServletRequest request) {
     (...)
 }


 private ConfiguracionSello recuperarDatosConfig(final HttpServletRequest request) {
     (...)
 }
}

HTML code:
<html>
<head>          
    <script type="text/javascript">
        var urlSello = 'http://localhost:8080/registro-itext/sello';
        
        var generarSello = function() {          
            var data = {
                    numeroRegistro:             document.getElementsByName("numReg")[0].value,
                    fechaRegistro:              document.getElementsByName("fechaReg")[0].value,
                    codigoOficina:              document.getElementsByName("codigoOfi")[0].value,
                    nombreOficina:              document.getElementsByName("nombreOfi")[0].value,
                    codigoUnidad:               document.getElementsByName("codigoUnidad")[0].value,
                    nombreOficina:              document.getElementsByName("nombreOfi")[0].value
            };
            
            var params = Object.keys(data).map(function(key) {
                return key + '=' +  encodeURI(data[key]);
            }).join('&');
            
            var xhr = new XMLHttpRequest();           
            xhr.onreadystatechange = function(){                
                if (this.readyState == 4 && this.status == 200){
                    console.log(this.response, typeof this.response);               
                    var urlBuilder = window.URL || window.webkitURL;
                    document.getElementById("iFramePDF").src = urlBuilder.createObjectURL(this.response);
                    document.getElementById("iFramePDF").style.display = 'block';               
                }
            };    
            xhr.open('POST', urlSello, true);
            xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            xhr.setRequestHeader("Content-length", params.length);
            xhr.responseType = 'blob';
            xhr.send(params);                       
        };                
    </script>
</head>

<body>
    <div id="contenido">     
        <button id="btnSello" type="button" onclick="generarSello();">Generar Sello</button>
                  
        <table>
            <tr><td>Numero de registro: </td> <td> <input name="numReg"  type="text" value="899998"></td></tr>
            <tr><td>Fecha de registro</td> <td> <input name="fechaReg"   type="text" value="2015/01/01 10:10:00"></td></tr>
            <tr><td>Codigo de oficina</td> <td> <input name="codigoOfi"  type="text" value="000"></td></tr>
            <tr><td>Nombre de oficina</td> <td> <input name="nombreOfi"  type="text" value="Rexistro Xeral \nProbas"></td></tr>
            <tr><td>Codigo de unidad</td> <td> <input name="codigoUnidad" type="text" value="V0"></td></tr>           
        </table>
    </div>
    

    <iframe id="iFramePDF" name="iFramePDF" src=""  width="100%" height="100%" frameBorder="0" styele="display:none"></iframe>

</body>
</html>

The important parts are:
  1. You create an object with all the params to serialize it in the way "name=value&" and to send the outcome string to the servlet.
  2. You invoke the servlet by using XMLHttpRequest. In the callback function, you convert the whole response to a new URL and pass it to the iframe