blob: 98a1b57463f3dd70012c496426d0ef1d800b7453 [file] [log] [blame]
// Copyright (C) 2012 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.httpd;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import com.google.common.base.Strings;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
import com.google.gerrit.httpd.RestTokenVerifier.InvalidTokenException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.OutputFormat;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Enumeration;
import java.util.Map;
import javax.annotation.Nullable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
public abstract class TokenVerifiedRestApiServlet extends RestApiServlet {
private static final long serialVersionUID = 1L;
private static final String FORM_ENCODED = "application/x-www-form-urlencoded";
private static final String UTF_8 = "UTF-8";
private static final String AUTHKEY_NAME = "_authkey";
private static final String AUTHKEY_HEADER = "X-authkey";
private final Gson gson;
private final Provider<CurrentUser> userProvider;
private final RestTokenVerifier verifier;
@Inject
protected TokenVerifiedRestApiServlet(Provider<CurrentUser> userProvider,
RestTokenVerifier verifier) {
super(userProvider);
this.gson = OutputFormat.JSON_COMPACT.newGson();
this.userProvider = userProvider;
this.verifier = verifier;
}
/**
* Process the (possibly state changing) request.
*
* @param req incoming HTTP request.
* @param res outgoing response.
* @param requestData JSON object representing the HTTP request parameters.
* Null if the request body was not supplied in JSON format.
* @throws IOException
* @throws ServletException
*/
protected abstract void doRequest(HttpServletRequest req,
HttpServletResponse res,
@Nullable JsonObject requestData) throws IOException, ServletException;
@Override
protected final void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
CurrentUser user = userProvider.get();
if (!(user instanceof IdentifiedUser)) {
sendError(res, SC_UNAUTHORIZED, "API requires authentication");
return;
}
TokenInfo info = new TokenInfo();
info._authkey = verifier.sign(
((IdentifiedUser) user).getAccountId(),
computeUrl(req));
ByteArrayOutputStream buf = new ByteArrayOutputStream();
String type;
buf.write(JSON_MAGIC);
if (acceptsJson(req)) {
type = JSON_TYPE;
buf.write(gson.toJson(info).getBytes(UTF_8));
} else {
type = FORM_ENCODED;
buf.write(String.format("%s=%s",
AUTHKEY_NAME,
URLEncoder.encode(info._authkey, UTF_8)).getBytes(UTF_8));
}
res.setContentType(type);
res.setCharacterEncoding(UTF_8);
res.setHeader("Content-Disposition", "attachment");
send(req, res, buf.toByteArray());
}
@Override
protected final void doPost(HttpServletRequest req, HttpServletResponse res)
throws IOException, ServletException {
CurrentUser user = userProvider.get();
if (!(user instanceof IdentifiedUser)) {
sendError(res, SC_UNAUTHORIZED, "API requires authentication");
return;
}
ParsedBody body;
if (JSON_TYPE.equals(req.getContentType())) {
body = parseJson(req, res);
} else if (FORM_ENCODED.equals(req.getContentType())) {
body = parseForm(req, res);
} else {
sendError(res, SC_BAD_REQUEST, String.format(
"Expected Content-Type: %s or %s",
JSON_TYPE, FORM_ENCODED));
return;
}
if (body == null) {
return;
}
if (Strings.isNullOrEmpty(body._authkey)) {
String h = req.getHeader(AUTHKEY_HEADER);
if (Strings.isNullOrEmpty(h)) {
sendError(res, SC_BAD_REQUEST, String.format(
"Expected %s in request body or %s in HTTP headers",
AUTHKEY_NAME, AUTHKEY_HEADER));
return;
}
body._authkey = URLDecoder.decode(h, UTF_8);
}
try {
verifier.verify(
((IdentifiedUser) user).getAccountId(),
computeUrl(req),
body._authkey);
} catch (InvalidTokenException err) {
sendError(res, SC_BAD_REQUEST,
String.format("Invalid or expired %s", AUTHKEY_NAME));
return;
}
doRequest(body.req, res, body.json);
}
private static ParsedBody parseJson(HttpServletRequest req,
HttpServletResponse res) throws IOException {
try {
JsonElement element = new JsonParser().parse(req.getReader());
if (!element.isJsonObject()) {
sendError(res, SC_BAD_REQUEST, "Expected JSON object in request body");
return null;
}
ParsedBody body = new ParsedBody();
body.req = req;
body.json = (JsonObject) element;
JsonElement authKey = body.json.remove(AUTHKEY_NAME);
if (authKey != null
&& authKey.isJsonPrimitive()
&& authKey.getAsJsonPrimitive().isString()) {
body._authkey = authKey.getAsString();
}
return body;
} catch (JsonParseException e) {
sendError(res, SC_BAD_REQUEST, "Invalid JSON object in request body");
return null;
}
}
private static ParsedBody parseForm(HttpServletRequest req,
HttpServletResponse res) throws IOException {
ParsedBody body = new ParsedBody();
body.req = new WrappedRequest(req);
body._authkey = req.getParameter(AUTHKEY_NAME);
return body;
}
private static String computeUrl(HttpServletRequest req) {
StringBuffer url = req.getRequestURL();
String qs = req.getQueryString();
if (!Strings.isNullOrEmpty(qs)) {
url.append('?').append(qs);
}
return url.toString();
}
private static class TokenInfo {
String _authkey;
}
private static class ParsedBody {
HttpServletRequest req;
String _authkey;
JsonObject json;
}
private static class WrappedRequest extends HttpServletRequestWrapper {
@SuppressWarnings("rawtypes")
private Map parameters;
WrappedRequest(HttpServletRequest req) {
super(req);
}
@Override
public String getParameter(String name) {
if (AUTHKEY_NAME.equals(name)) {
return null;
}
return super.getParameter(name);
}
@Override
public String[] getParameterValues(String name) {
if (AUTHKEY_NAME.equals(name)) {
return null;
}
return super.getParameterValues(name);
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public Map getParameterMap() {
Map m = parameters;
if (m == null) {
m = super.getParameterMap();
if (m.containsKey(AUTHKEY_NAME)) {
m = Maps.newHashMap(m);
m.remove(AUTHKEY_NAME);
}
parameters = m;
}
return m;
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public Enumeration getParameterNames() {
return Iterators.asEnumeration(getParameterMap().keySet().iterator());
}
}
}