6 Apr 2011

How to develop SCWS Servlet?

I'd like to continue the subject of Smart Card Web Server (SCWS) and explain in this post how to develop servlets for SCWS. Servlet for SCWS from one point is regular JavaCard Applet but at the same it has some differences. Let me explain.

Servlet has the following properties:
  • Triggers upon HTTP request reception
  • Has to be developed using dedicated ETSI 102.588 API
  • Mapped on one or several URIs (e.g.“/operator/app”)
There are two kind of servlets:
  • Dynamic content generation
  • Interception (usage tracking)
Servlet supports the following HTTP Request methods:

GETRequests the specified resource
HEADRequests meta-information on a resource (no body)
PUTUploads (add or replace) a resource on the server
POSTSubmits data to be processed to the identified resource
DELETEDeletes the specified resource on the server
OPTIONSReturns the HTTP methods supported for the specified URI (debug purpose)
TRACEEchos back the received requests (debug purposes)

CONNECT method isn't supported by ETSI TS 102.588 standard.

SCWS API has defined in uicc.scws hence you have to import it:

import uicc.scws.*;

Class has to be declared as follows:

class ServletHelloWorld extends Applet
      implements AppletEvent, ScwsExtension
  • Class should extend Applet as it is JavaCard standard applet
  • It has to implement ScwsExtension interface to be able to trigger by SCWS
  • It has to implement AppletEvent interface to define uninstall() method
  • It could implement ToolkitInterface interface if the servlet should be triggered as STK applet
Methods which have to be implemented:
  • JavaCard mandatory methods: process(), install()
  • ScwsExtension mandatory methods: doDelete(), doGet(), doHead(), doOptions(), doPost(), doPut(), doTrace()
Ok, let's have a look to real source code:

package firstservletpackage;

/*
 * Imported packages
 */
import javacard.framework.*;
import uicc.scws.HttpRequest;
import uicc.scws.HttpResponse;
import uicc.scws.ScwsConstants;
import uicc.scws.ScwsException;
import uicc.scws.ScwsExtension;
import uicc.scws.ScwsExtensionRegistry;

public class FirstServlet extends javacard.framework.Applet
                          implements AppletEvent, ScwsExtension {
    /** the servlet url */
    public final static byte[] url = { '/', 'F', 'S' };
    public final static byte[] appId = { 'F', 'i', 'r', 's', 't', 'S', 'e'
                                        ,'r', 'v', 'l', 'e', 't' };
    public final static byte[] CRLF = { '\r', '\n' };
    public static final byte[] SPACE = { ' ' };
    /** error status word when install parameter are invalid */
    public final static short SW_INVALID_INSTALL_PARAMETER = (short) 0x6F06;
    /** install tag: should find the http server aid inside */
    public final static byte INSTALL_TAG = (byte) 0x71;

    public final static byte[] METHOD_GET = { 'G', 'E', 'T' };
    public final static byte[] TEXT_PLAIN = { 't', 'e', 'x', 't', '/', 'p'
                                             ,'l', 'a', 'i', 'n' };

    // Temporary operation buffer
    public byte[] temporaryBuffer;
    public final static short TEMPORARY_BUFFER_LENGTH = (short) 100;

    public final static byte[] REQUEST_BODY = { 'R', 'e', 'q', 'u', 'e', 's'
                                             ,'t', ' ', 'B', 'o', 'd', 'y', ':' };
    public final static byte[] PROTOCOL = { 'P', 'r', 'o', 't', 'o', 'c', 'o'
                                           ,'l', ':' };
    public final static byte[] CONTENT_TYPE = { 'C', 'o', 'n', 't', 'e', 'n'
                                             ,'t', ' ', 'T', 'y', 'p', 'e', ':' };
    public final static byte[] HEADER_UA = { 'H', 'e', 'a', 'd', 'e', 'r', ' '
                                            ,'U', 's', 'e', 'r', ' '
                                            , 'A', 'g', 'e', 'n', 't', ':' };
    public final static byte[] METHOD = { 'M', 'e', 't', 'h', 'o', 'd', ':' };
    public final static byte[] QUERY_STRING = { 'Q', 'u', 'e', 'r', 'y', ' '
                                             ,'S', 't', 'r', 'i', 'n', 'g', ':' };
    public final static byte[] REQUEST_URI = { 'R', 'e', 'q', 'u', 'e', 's'
                                              ,'t', ' ', 'U', 'R', 'I', ':' };
    public final static byte[] TRACE = { 'T', 'R', 'A', 'C', 'E' };
    public final static byte[] ACCEPT = { 'A', 'c', 'c', 'e', 'p', 't', ':' };
    public final static byte[] ACCEPT_LANG = { 'A', 'c', 'c', 'e', 'p', 't'
                              ,'-', 'L', 'a', 'n', 'g', 'u', 'a', 'g', 'e', ':' };
    public final static byte[] CONNECTION = { 'C', 'o', 'n', 'n', 'e', 'c'
                                             ,'t', 'i', 'o', 'n', ':' };
    public final static byte[] HOST = { 'H', 'o', 's', 't', ':' };
    public final static byte[] REFERER = { 'R', 'e', 'f', 'e', 'r', 'e', 'r', ':' };
    public final static byte[] USER_AGENT = { 'U', 's', 'e', 'r', '-', 'A'
                                             ,'g', 'e', 'n', 't', ':' };
    public final static byte[] ACCEPT_ENCODING = { 'A', 'c', 'c', 'e', 'p', 't'
                                                  , '-', 'E', 'n', 'c', 'o', 'd'
                                                  , 'i', 'n', 'g', ':' };
    public final static byte[] ALLOW = { 'A', 'l', 'l', 'o', 'w' };
    public final static byte[] ALLOW_OPTIONS = { 'G', 'E', 'T', ','
                                       , 'T', 'R', 'A', 'C', 'E'
                                       , ',', 'O', 'P', 'T', 'I', 'O', 'N', 'S' };
    public final static byte[] HTTP_10 = { 'H', 'T', 'T', 'P', ':'
                                          , '/', '1', '.', '0' };
    public final static byte[] HTTP_11 = { 'H', 'T', 'T', 'P', ':'
                                          , '/', '1', '.', '1' };

    public final static byte[] HELLO = { 'H', 'e', 'l', 'l', 'o' };

    /**
     * Constructor of servlet
     */
    public FirstServlet(byte[] buffer, short offset, byte length) {
        // First LV is instance AID
        short aid = offset;
        offset += buffer[offset] + (byte) 1;
        // Second LV is Privilege
        offset += buffer[offset] + (byte) 1;
        // Third LV is specific install parameter (extract from TAG C9)
        offset++; // skip C9 Length
        // Register the new applet instance to the JCRE
        register(buffer, (short) (aid + (short) 1), buffer[aid]);
        // Register application id,there is corresponding appId in the Run/Debug
        // configuration for URL Mapping
        // if you update this appId in current code,you may need update
        // corresponding value there for URL Mapping
        ScwsExtensionRegistry.register(this, appId, (short) 0,
                (short) appId.length);
        try {
            // Create a temporary buffer for read/write
            temporaryBuffer = JCSystem.makeTransientByteArray(
                    TEMPORARY_BUFFER_LENGTH, JCSystem.CLEAR_ON_RESET);
        } catch (SystemException se) {
            // create buffer in persistent memory as not enough transient is
            // available
            temporaryBuffer = new byte[TEMPORARY_BUFFER_LENGTH];
        }

    }

    /**
     * Method called by the JCRE at the installation of the applet
     * 
     * @param bArray
     *            the byte array containing the AID bytes
     * @param bOffset
     *            the start of AID bytes in bArray
     * @param bLength
     *            the length of the AID bytes in bArray
     */
    public static void install(byte[] bArray, short sOffset, byte bLength)
            throws ISOException {
        new FirstServlet(bArray, sOffset, bLength);
    }

    /**
     * Method called by the JCRE, uninstall the applet
     */
    public void uninstall() {
        ScwsExtensionRegistry.deregister(this);
    }

    /**
     * Method called by the JCRE, once selected
     * 
     * @param apdu
     *            the incoming APDU object
     */

    public void process(APDU apdu) throws ISOException {
        // ignore the applet select command dispached to the process
        if (selectingApplet()) {
            return;
        }
        // actually no IO methods needed for the server.
        ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
    }

    /**
     * Called by the SCWS to allow a servlet to handle a DELETE request.
     * <p>
     * If the HTTP DELETE request is incorrectly formatted, doDelete returns an
     * HTTP "Bad Request" message.
     * </p>
     * 
     * @param req
     *            - the HttpRequest object that contains the request the client
     *            made of the servlet
     * @param resp
     *            - the HttpResponse object that contains the response the
     *            servlet returns to the client
     * @throws ScwsException
     *             - if the request for the DELETE cannot be handled
     */
    public void doDelete(HttpRequest req, HttpResponse resp)
            throws ScwsException {
    }

    /**
     * Called by SCWS to allow a servlet to handle a GET request.
     * <p>
     * When using HTTP 1.1 chunked encoding (which means that the response has a
     * Transfer-Encoding header), do not set the Content-Length header.
     * </p>
     * If the HTTP POST request is incorrectly formatted, doPost returns an HTTP
     * "Bad Request" message.
     * 
     * @param req
     *            - an HttpRequest object that contains the request the client
     *            has made of the servlet
     * @param resp
     *            - an HttpResponse object that contains the response the
     *            servlet sends to the client
     * @throws ScwsException
     *             - if the request for the GET could not be handled
     */
    public void doGet(HttpRequest req, HttpResponse resp) throws ScwsException {
        // TODO Replace your doGet method below
        // ----------------------------------------------------------------------
        // return on response body:
        // status: 200 (OK)
        // Content Type: text/html
        // Protocol: HTTP/1.1
        // Method: GET
        // Header User Agent: Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0)
        // Content Type: text/html
        // ...
        try {
            short httpVersion = req.getRequestHttpVersion();

            // set header
            resp.writeStatusCode(ScwsConstants.SC_OK);
            resp.setContentType(ScwsConstants.CONTENT_TYPE_TEXT_HTML);
            resp.enableChunkMode();

            // write body
            resp.appendContent(HELLO, (short) 0, (short) HELLO.length);
            
        } catch (Exception e) {
            resp.writeStatusCode(ScwsConstants.SC_BAD_REQUEST);
        }

        resp.flush();

    }

    /**
     * Receives an HTTP HEAD request from the protected service method and
     * handles the request.
     * <p>
     * The client sends a HEAD request when it wants to see only the headers of
     * a response, such as <tt>Content-Type</tt> or <tt>Content-Length</tt>.<br>
     * <p>
     * The HTTP HEAD method counts the output bytes in the response to set the
     * Content-Length header accurately.
     * <p>
     * If the HTTP HEAD request is incorrectly formatted, doHead returns an HTTP
     * "Bad Request" message.
     * 
     * @param req
     *            - the request object that is passed to the servlet
     * @param resp
     *            - the response object that the servlet uses to return the
     *            headers to the clien
     * @throws ScwsException
     *             - if the request for the HEAD could not be handled
     */
    public void doHead(HttpRequest req, HttpResponse resp) throws ScwsException {

    }

    /**
     * The OPTIONS method represents a request for information about the
     * communication options available on the request/response chain identified
     * by the Servlet.
     * <p>
     * Called by the SCWS to allow a servlet to handle a OPTIONS request. <br>
     * The OPTIONS request determines which HTTP methods the server supports and
     * returns an appropriate header.<br>
     * For example, if a servlet overrides doGet, this method returns the
     * following header:
     * 
     * <pre>
     * Allow: <tt>GET</tt>, <tt>HEAD</tt>, <tt>OPTIONS</tt>
     * </pre>
     * 
     * </p>
     * 
     * @param req
     *            - the HttpRequest object that contains the request the client
     *            made of the servlet
     * @param resp
     *            - the HttpResponse object that contains the response the
     *            servlet returns to the client
     * @throws ScwsException
     *             - if the request for the OPTIONS cannot be handled
     */
    public void doOptions(HttpRequest req, HttpResponse resp)
            throws ScwsException {
        resp.appendHeaderVariable(ALLOW, (short) 0, (short) ALLOW.length,
                ALLOW_OPTIONS, (short) 0, (short) ALLOW_OPTIONS.length);
        resp.flush();

    }

    /**
     * Called by the SCWS to allow the servlet to handle a POST request.
     * <p>
     * When using HTTP 1.1 chunked encoding (which means that the response has a
     * Transfer-Encoding header), do not set the Content-Length header.
     * </p>
     * If the HTTP POST request is incorrectly formatted, doPost returns an HTTP
     * "Bad Request" message.
     * 
     * @param req
     *            - an HttpRequest object that contains the request the client
     *            has made of the servlet
     * @param resp
     *            - an HttpResponse object that contains the response the
     *            servlet sends to the client
     * @throws ScwsException
     *             - if the request for the POST could not be handled
     */
    public void doPost(HttpRequest req, HttpResponse resp) throws ScwsException {
    }

    /**
     * Called by the SCWS (via the service method) to allow a servlet to handle
     * a PUT request.
     * <p>
     * If the HTTP PUT request is incorrectly formatted, doPut returns an HTTP
     * "Bad Request" message.
     * 
     * @param req
     *            - the HttpRequest object that contains the request the client
     *            made of the servlet
     * @param resp
     *            - the HttpResponse object that contains the response the
     *            servlet returns to the client
     * @throws HttpCardServletException
     *             - if the request for the PUT cannot be handled
     * @throws java.lang.Exception
     */
    public void doPut(HttpRequest req, HttpResponse resp) throws ScwsException {
    }

    /**
     * Called by the server (via the service method) to allow a servlet to
     * handle a TRACE request.
     * <p>
     * The TRACE method is used to invoke a remote, application-layer loop-back
     * of the request message.
     * </p>
     * 
     * @param req
     *            - the HttpRequest object that contains the request the client
     *            made of the servlet
     * @param resp
     *            - the HttpResponse object that contains the response the
     *            servlet returns to the client
     * @throws ScwsException
     *             - if the request for the TRACE cannot be handled
     */
    public void doTrace(HttpRequest req, HttpResponse resp)
            throws ScwsException {
        // TODO Auto-generated method stub
        // --------------------------
        /*
         * A TRACE returns the headers sent with the TRACE request to the
         * client, so that they can be used in debugging.
         */
        try {

            // set header
            resp.writeStatusCode(ScwsConstants.SC_OK);
            resp.setContentType(ScwsConstants.CONTENT_TYPE_TEXT_PLAIN);
            resp.enableChunkMode();

            // TRACE /MyServlet1 HTTP/1.1
            resp.appendContent(TRACE, (short) 0, (short) TRACE.length);
            resp.appendContent(SPACE, (short) 0, (short) SPACE.length);
            getAndOutputContent(req, resp, ScwsConstants.URI_PATH_TAG);

            resp.appendContent(SPACE, (short) 0, (short) SPACE.length);
            short httpVersion = req.getRequestHttpVersion();
            if (httpVersion == ScwsConstants.HTTP_PROTOCOL_VERSION_10)
                resp.appendContent(HTTP_10, (short) 0, (short) HTTP_10.length);
            else
                resp.appendContent(HTTP_11, (short) 0, (short) HTTP_11.length);
            resp.appendContent(CRLF, (short) 0, (short) CRLF.length);

            // ex: Accept: */*
            resp.appendContent(ACCEPT, (short) 0, (short) ACCEPT.length);
            getAndOutputContent(req, resp, ScwsConstants.HEADER_ACCEPT);
            resp.appendContent(CRLF, (short) 0, (short) CRLF.length);

            // ex: Accept-Language: en-us
            resp.appendContent(ACCEPT_LANG, (short) 0,
                    (short) ACCEPT_LANG.length);
            getAndOutputContent(req, resp, ScwsConstants.HEADER_ACCEPT_LANGUAGE);
            resp.appendContent(CRLF, (short) 0, (short) CRLF.length);

            // ex: Host: localhost
            resp.appendContent(HOST, (short) 0, (short) HOST.length);
            getAndOutputContent(req, resp, ScwsConstants.HEADER_HOST);
            resp.appendContent(CRLF, (short) 0, (short) CRLF.length);

            // ex: User-Agent: Mozilla/4.0 (compatible; MSIE 5.5; Windows NT
            // 5.0)
            resp
                    .appendContent(USER_AGENT, (short) 0,
                            (short) USER_AGENT.length);
            getAndOutputContent(req, resp, ScwsConstants.HEADER_USER_AGENT);
            resp.appendContent(CRLF, (short) 0, (short) CRLF.length);

            // ex: Accept-Encoding: gzip, deflate
            resp.appendContent(ACCEPT_ENCODING, (short) 0,
                    (short) ACCEPT_ENCODING.length);
            getAndOutputContent(req, resp, ScwsConstants.HEADER_ACCEPT_ENCODING);
            resp.appendContent(CRLF, (short) 0, (short) CRLF.length);

        } catch (Exception e) {
            resp.writeStatusCode(ScwsConstants.SC_NOT_IMPLEMENTED);
        }

        resp.flush();

    }

    private void getAndOutputContent(HttpRequest req, HttpResponse resp,
            short keyword) {
        short contentLgth = req.findAndCopyKeywordValue(keyword,
                temporaryBuffer, (short) 1,
                (short) (temporaryBuffer.length - 1));
        if (contentLgth == ScwsConstants.KEYWORD_NOT_FOUND)
            contentLgth++; // Query string may not exist, so set it 0 if did
        temporaryBuffer[0] = (byte) (contentLgth);
        resp.appendContent(temporaryBuffer, (short) 1,
                (short) temporaryBuffer[0]);
        clearBuffer(); // Clear buffer for reuse
    }

    private void clearBuffer() {
        for (short i = 0; i < temporaryBuffer.length; i++) {
            temporaryBuffer[i] = 0;
        }
    }

    private short getLength(byte[] data) {
        for (short i = 0; i < data.length; i++) {
            if (data[i] == 0) {
                return i;
            }
        }
        return 0;
    }
}

No comments: