view src/de/mpiwg/itgroup/annotationManager/restlet/AnnotatorResourceImpl.java @ 20:6629e8422760

half baked version for new JWT auth :-(
author casties
date Fri, 23 Mar 2012 21:41:53 +0100
parents b0ef5c860464
children 0cd1e7608d25
line wrap: on
line source

/**
 * Base class for Annotator resource classes.
 */
package de.mpiwg.itgroup.annotationManager.restlet;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.bind.DatatypeConverter;

import net.oauth.jsontoken.JsonToken;
import net.oauth.jsontoken.JsonTokenParser;

import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.restlet.data.ClientInfo;
import org.restlet.data.Form;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.resource.Options;
import org.restlet.resource.ServerResource;
import org.restlet.security.User;

import com.google.gson.JsonPrimitive;

import de.mpiwg.itgroup.annotationManager.Constants.NS;
import de.mpiwg.itgroup.annotationManager.RDFHandling.Annotation;

/**
 * Base class for Annotator resource classes.
 * 
 * @author dwinter, casties
 * 
 */
public abstract class AnnotatorResourceImpl extends ServerResource {

    protected Logger logger = Logger.getRootLogger();

    protected String getAllowedMethodsForHeader() {
        return "OPTIONS,GET,POST";
    }

    /**
     * returns a hex String of a SHA256 digest of text.
     * 
     * @param text
     * @return
     */
    public String getSha256Digest(String text) {
        String digest = null;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.update(text.getBytes("UTF-8"));
            byte[] dg = md.digest();
            digest = DatatypeConverter.printHexBinary(dg);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return digest;
    }
    
    public String encodeJsonId(String id) {
        try {
            return DatatypeConverter.printBase64Binary(id.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }
    
    public String decodeJsonId(String id) {
        try {
            return new String(DatatypeConverter.parseBase64Binary(id), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }

    /**
     * Handle options request to allow CORS for AJAX.
     * 
     * @param entity
     */
    @Options
    public void doOptions(Representation entity) {
        logger.debug("AnnotatorResourceImpl doOptions!");
        setCorsHeaders();
    }

    /**
     * set headers to allow CORS for AJAX.
     */
    protected void setCorsHeaders() {
        Form responseHeaders = (Form) getResponse().getAttributes().get("org.restlet.http.headers");
        if (responseHeaders == null) {
            responseHeaders = new Form();
            getResponse().getAttributes().put("org.restlet.http.headers", responseHeaders);
        }
        responseHeaders.add("Access-Control-Allow-Methods", getAllowedMethodsForHeader());
        // echo back Origin and Request-Headers
        Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
        String origin = requestHeaders.getFirstValue("Origin", true);
        if (origin == null) {
            responseHeaders.add("Access-Control-Allow-Origin", "*");
        } else {
            responseHeaders.add("Access-Control-Allow-Origin", origin);
        }
        String allowHeaders = requestHeaders.getFirstValue("Access-Control-Request-Headers", true);
        if (allowHeaders != null) {
            responseHeaders.add("Access-Control-Allow-Headers", allowHeaders);
        }
        responseHeaders.add("Access-Control-Allow-Credentials", "true");
        responseHeaders.add("Access-Control-Max-Age", "60");
    }

    /**
     * returns if authentication information from headers is valid.
     * 
     * @param entity
     * @return
     */
    public boolean isAuthenticated(Representation entity) {
        return (checkAuthToken(entity) != null);
    }

    /**
     * checks Annotator Auth plugin authentication information from headers. returns userId if successful.
     * 
     * @param entity
     * @return
     */
    public String checkAuthToken(Representation entity) {
        Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
        String authToken = requestHeaders.getFirstValue("x-annotator-auth-token", true);
        String userId = null;
        String tokenString;
        JsonToken token = new JsonTokenParser(null, null).deserialize(authToken);
        String consumerKey = token.getParamAsPrimitive("consumerKey").getAsString();
        // get stored consumer secret for key
        RestServer restServer = (RestServer) getApplication();
        String consumerSecret = restServer.getConsumerSecret(consumerKey);
        logger.debug("requested consumer key=" + consumerKey + " secret=" + consumerSecret);
        if (consumerSecret == null) {
            return null;
        }
        logger.debug("token="+token);
        /* try {
            logger.debug(String.format("authToken=%s", authToken));
            String[] tokenParts = authToken.split("\\.");
            logger.debug(String.format("tokenParts=%s", tokenParts.toString()));
            String payloadEnc = tokenParts[1];
            if (payloadEnc.length() % 4 > 0) {
                // add padding for parseBase64Binary
                payloadEnc += "===".substring(0, payloadEnc.length() % 4);
            }
            String payloadString = new String(DatatypeConverter.parseBase64Binary(payloadEnc), "UTF-8");
            logger.debug(String.format("payloadString=%s", payloadString));
            JSONObject to = new JSONObject(payloadString);
            logger.debug(String.format("jsonToken=%s", to));
            String consumerKey = to.getString("consumerKey");
            // get stored consumer secret for key
            RestServer restServer = (RestServer) getApplication();
            String consumerSecret = restServer.getConsumerSecret(consumerKey);
            logger.debug("requested consumer key=" + consumerKey + " secret=" + consumerSecret);
            if (consumerSecret == null) {
                return null;
            }
            String decrypted = WebToken.decrypt(authToken, consumerSecret);
            logger.debug("decrypted="+decrypted);
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (JSONException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ArrayIndexOutOfBoundsException e) {
            e.printStackTrace();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } */
        //WebToken.decrypt(encrypted, password)
        /*
        String consumerKey = requestHeaders.getFirstValue("x-annotator-consumer-key", true);
        if (consumerKey == null) {
            return null;
        }
        // get stored consumer secret for key
        RestServer restServer = (RestServer) getApplication();
        String consumerSecret = restServer.getConsumerSecret(consumerKey);
        logger.debug("requested consumer key=" + consumerKey + " secret=" + consumerSecret);
        if (consumerSecret == null) {
            return null;
        }
        String userId = requestHeaders.getFirstValue("x-annotator-user-id", true);
        String issueTime = requestHeaders.getFirstValue("x-annotator-auth-token-issue-time", true);
        if (userId == null || issueTime == null) {
            return null;
        }
        // compute hashed token based on the values we know
        // computed_token = hashlib.sha256(consumer.secret + user_id + issue_time).hexdigest()
        String computedToken = getSha256Digest(consumerSecret + userId + issueTime);
        // compare to the token we got
        String authToken = requestHeaders.getFirstValue("x-annotator-auth-token", true);
        logger.debug(String.format("got: authToken=%s consumerSecret=%s userId=%s issueTime=%s computedToken=%s", 
                authToken, consumerSecret, userId, issueTime, computedToken));
        if (!computedToken.equalsIgnoreCase(authToken)) {
            logger.warn("authToken differ!");
            return null;
        }
        // check token lifetime
        // validity = iso8601.parse_date(issue_time)
        // expiry = validity + datetime.timedelta(seconds=consumer.ttl)
        int tokenTtl = 86400;
        DateTime tokenValidity = null;
        DateTime tokenExpiry = null;
        try {
            DateTimeFormatter parser = ISODateTimeFormat.dateTime();
            tokenValidity = parser.parseDateTime(issueTime);
            String tokenTtlString = requestHeaders.getFirstValue("x-annotator-auth-token-ttl", true);
            tokenTtl = Integer.parseInt(tokenTtlString);
            tokenExpiry = tokenValidity.plusSeconds(tokenTtl);
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
        if (tokenValidity == null || tokenValidity.isAfterNow() || tokenExpiry == null || tokenExpiry.isBeforeNow()) {
            logger.warn(String.format("authToken invalid! tokenValidity=%s tokenExpiry=%s now=%s", tokenValidity, tokenExpiry, DateTime.now()));
            // we dont care about validity right now
            //return null;
        }
        */
        // must be ok then
        logger.debug("auth OK! user="+userId);
        return userId;
    }

    /**
     * creates Annotator-JSON from an Annotation object.
     * 
     * @param annot
     * @return
     */
    public JSONObject createAnnotatorJson(Annotation annot) {
        boolean makeUserObject = true;
        JSONObject jo = new JSONObject();
        try {
            jo.put("text", annot.text);
            jo.put("uri", annot.url);

            if (makeUserObject) {
                // create user object
                JSONObject userObject = new JSONObject();
                // save creator as uri
                userObject.put("uri", annot.creator);
                // make short user id
                String userID = annot.creator;
                if (userID.startsWith(NS.MPIWG_PERSONS)) {
                    userID = userID.replace(NS.MPIWG_PERSONS, ""); // entferne NAMESPACE
                }
                // save as id
                userObject.put("id", userID);
                // get full name
                RestServer restServer = (RestServer) getApplication();
                String userName = restServer.getUserNameFromLdap(userID);
                userObject.put("name", userName);
                // save user object
                jo.put("user", userObject);
            } else {
                // save user as string
                jo.put("user", annot.creator);
            }

            List<String> xpointers = new ArrayList<String>();
            if (annot.xpointers == null || annot.xpointers.size() == 0)
                xpointers.add(annot.xpointer);
            else {
                for (String xpointerString : annot.xpointers) {
                    xpointers.add(xpointerString);
                }
            }
            jo.put("ranges", transformToRanges(xpointers));
            // encode Annotation URL (=id) in base64
            String annotUrl = annot.getAnnotationUri();
            String annotId = encodeJsonId(annotUrl);
            jo.put("id", annotId);
            return jo;
        } catch (JSONException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }

    private JSONArray transformToRanges(List<String> xpointers) {

        JSONArray ja = new JSONArray();

        Pattern rg = Pattern
                .compile("#xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)/range-to\\(end-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)\\)");
        Pattern rg1 = Pattern.compile("#xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)");

        try {
            for (String xpointer : xpointers) {
                String decoded = URLDecoder.decode(xpointer, "utf-8");
                Matcher m = rg.matcher(decoded);

                if (m.find()) {
                    {
                        JSONObject jo = new JSONObject();
                        jo.put("start", m.group(1));
                        jo.put("startOffset", m.group(2));
                        jo.put("end", m.group(3));
                        jo.put("endOffset", m.group(4));
                        ja.put(jo);
                    }
                }
                m = rg1.matcher(xpointer);
                if (m.find()) {
                    JSONObject jo = new JSONObject();
                    jo.put("start", m.group(1));
                    jo.put("startOffset", m.group(2));

                    ja.put(jo);
                }
            }
        } catch (JSONException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return ja;
    }

    /**
     * creates an Annotation object with data from JSON. 
     * 
     * uses the specification from the annotator project: {@link https://github.com/okfn/annotator/wiki/Annotation-format}
     * 
     * The username will be transformed to an URI if not given already as URI, if not it will set to the MPIWG namespace defined in
     * de.mpiwg.itgroup.annotationManager.Constants.NS
     * 
     * @param jo
     * @return
     * @throws JSONException
     */
    public Annotation createAnnotation(JSONObject jo, Representation entity) throws JSONException {
        return updateAnnotation(new Annotation(), jo, entity);
    }
        
    public Annotation updateAnnotation(Annotation annot, JSONObject jo, Representation entity) throws JSONException {
        // annotated uri
        String url = annot.url;
        if (jo.has("uri")) {
            url = jo.getString("uri");
        }
        // annotation text
        String text = annot.text;
        if (jo.has("text")) {
            text = jo.getString("text");
        }
        // check authentication
        String authUser = checkAuthToken(entity);
        if (authUser == null) {
            // try http auth
            User httpUser = getHttpAuthUser(entity);
            if (httpUser == null) {
                setStatus(Status.CLIENT_ERROR_FORBIDDEN);
                return null;
            }
            authUser = httpUser.getIdentifier();
        }
        // username not required, if no username given authuser will be used
        String username = null;
        String userUri = annot.creator;
        if (jo.has("user")) {
            if (jo.get("user") instanceof String) {
                // user is just a String
                username = jo.getString("user");
                // TODO: what if username and authUser are different?
            } else {
                // user is an object
                JSONObject user = jo.getJSONObject("user");
                if (user.has("id")) {
                    username = user.getString("id");
                }
                if (user.has("uri")) {
                    userUri = user.getString("uri");
                }
            }
        }
        if (username == null) {
            username = authUser;
        }
        // username should be a URI, if not it will set to the MPIWG namespace defined in
        // de.mpiwg.itgroup.annotationManager.Constants.NS
        if (userUri == null) {
            if (username.startsWith("http")) {
                userUri = username;
            } else {
                userUri = NS.MPIWG_PERSONS + username;
            }
        }
        // TODO: should we overwrite the creator?
        
        // create xpointer
        String xpointer = annot.xpointer;
        if (jo.has("ranges")) {
            JSONObject ranges = jo.getJSONArray("ranges").getJSONObject(0);
            String start = ranges.getString("start");
            String end = ranges.getString("end");
            String startOffset = ranges.getString("startOffset");
            String endOffset = ranges.getString("endOffset");

            try {
                xpointer = url
                        + "#"
                        + URLEncoder.encode(String.format(
                                "xpointer(start-point(string-range(\"%s\",%s,1))/range-to(end-point(string-range(\"%s\",%s,1))))",
                                start, startOffset, end, endOffset), "utf-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
                setStatus(Status.SERVER_ERROR_INTERNAL);
                return null;
            }
        }
        return new Annotation(xpointer, userUri, annot.time, text, annot.type);
    }

    /**
     * returns the logged in User.
     * 
     * @param entity
     * @return
     */
    protected User getHttpAuthUser(Representation entity) {
        RestServer restServer = (RestServer) getApplication();
        if (!restServer.authenticate(getRequest(), getResponse())) {
            // Not authenticated
            return null;
        }

        ClientInfo ci = getRequest().getClientInfo();
        logger.debug(ci);
        return getRequest().getClientInfo().getUser();

    }

}