/* * Zed Attack Proxy (ZAP) and its related class files. * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * * 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 org.zaproxy.zap.extension.api; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import net.sf.json.JSONException; import net.sf.json.JSONObject; import org.parosproxy.paros.Constant; import org.parosproxy.paros.common.AbstractParam; import org.parosproxy.paros.network.HttpMessage; import org.zaproxy.zap.extension.api.API.RequestType; public abstract class ApiImplementor { private static final String GET_OPTION_PREFIX = "option"; private static final String SET_OPTION_PREFIX = "setOption"; private static final String ADD_OPTION_PREFIX = "addOption"; private static final String REMOVE_OPTION_PREFIX = "removeOption"; private static final Comparator<Method> METHOD_NAME_COMPARATOR; static { METHOD_NAME_COMPARATOR = new Comparator<Method>() { @Override public int compare(Method method, Method otherMethod) { if (method == null) { if (otherMethod == null) { return 0; } return -1; } else if (otherMethod == null) { return 1; } return method.getName().compareTo(otherMethod.getName()); } }; } private List<ApiAction> apiActions = new ArrayList<>(); private List<ApiView> apiViews = new ArrayList<>(); private List<ApiOther> apiOthers = new ArrayList<>(); private List<String> apiShortcuts = new ArrayList<>(); private AbstractParam param = null; public List<ApiView> getApiViews() { return this.apiViews; } public List<ApiAction> getApiActions() { return this.apiActions; } public List<ApiOther> getApiOthers() { return this.apiOthers; } public void addApiView (ApiView view) { this.apiViews.add(view); } public void addApiOthers (ApiOther other) { this.apiOthers.add(other); } public void addApiAction (ApiAction action) { this.apiActions.add(action); } public void addApiShortcut (String shortcut) { this.apiShortcuts.add(shortcut); } /** * Adds the given options to the API implementor. * * @param param the options for the API * @see ZapApiIgnore */ public void addApiOptions(AbstractParam param) { // Add option parameter getters and setters via reflection this.param = param; Method[] methods = param.getClass().getDeclaredMethods(); Arrays.sort(methods, METHOD_NAME_COMPARATOR); List<String> addedActions = new ArrayList<>(); // Check for string setters (which take precedence) for (Method method : methods) { if (isIgnored(method)) { continue; } boolean deprecated = method.getAnnotation(Deprecated.class) != null; if (method.getName().startsWith("get") && method.getParameterTypes().length == 0) { ApiView view = new ApiView(GET_OPTION_PREFIX + method.getName().substring(3)); setApiOptionDeprecated(view, deprecated); addApiView(view); } if (method.getName().startsWith("is") && method.getParameterTypes().length == 0) { ApiView view = new ApiView(GET_OPTION_PREFIX + method.getName().substring(2)); setApiOptionDeprecated(view, deprecated); addApiView(view); } if (method.getName().startsWith("set") && method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(String.class)) { ApiAction action = new ApiAction(SET_OPTION_PREFIX + method.getName().substring(3), new String[]{"String"}); setApiOptionDeprecated(action, deprecated); this.addApiAction(action); addedActions.add(method.getName()); } if (method.getName().startsWith("add") && method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(String.class)) { ApiAction action = new ApiAction(ADD_OPTION_PREFIX + method.getName().substring(3), new String[]{"String"}); setApiOptionDeprecated(action, deprecated); this.addApiAction(action); addedActions.add(method.getName()); } if (method.getName().startsWith("remove") && method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(String.class)) { ApiAction action = new ApiAction(REMOVE_OPTION_PREFIX + method.getName().substring(6), new String[]{"String"}); setApiOptionDeprecated(action, deprecated); this.addApiAction(action); addedActions.add(method.getName()); } } // Now check for non string setters for (Method method : methods) { if (isIgnored(method)) { continue; } boolean deprecated = method.getAnnotation(Deprecated.class) != null; if (method.getName().startsWith("set") && method.getParameterTypes().length == 1 && ! addedActions.contains(method.getName())) { // Non String setter if (method.getParameterTypes()[0].equals(Integer.class) || method.getParameterTypes()[0].equals(int.class)) { ApiAction action = new ApiAction(SET_OPTION_PREFIX + method.getName().substring(3), new String[]{"Integer"}); setApiOptionDeprecated(action, deprecated); this.addApiAction(action); addedActions.add(method.getName()); // Just in case there are more overloads } else if (method.getParameterTypes()[0].equals(Boolean.class) || method.getParameterTypes()[0].equals(boolean.class)) { ApiAction action = new ApiAction(SET_OPTION_PREFIX + method.getName().substring(3), new String[]{"Boolean"}); setApiOptionDeprecated(action, deprecated); this.addApiAction(action); addedActions.add(method.getName()); // Just in case there are more overloads } } } } /** * Tells whether or not the given {@code method} should be ignored, thus not included in the ZAP API. * <p> * Checks if the given {@code method} has been annotated with {@code ZapApiIgnore} or if it's not public, if any of the * conditions is {@code true} the {@code method} is ignored. * * @param method the method that will be checked * @return {@code true} if the method should be ignored, {@code false} otherwise. * @see ZapApiIgnore */ private static boolean isIgnored(Method method) { return method.getAnnotation(ZapApiIgnore.class) != null || !Modifier.isPublic(method.getModifiers()); } private void setApiOptionDeprecated(ApiElement apiOption, boolean deprecated) { if (deprecated) { apiOption.setDeprecated(deprecated); if (Constant.messages != null) { // Add a custom message when running from ZAP. apiOption.setDeprecatedDescription(Constant.messages.getString("api.deprecated.option.endpoint")); } } } public ApiResponse handleApiOptionView(String name, JSONObject params) throws ApiException { if (this.param == null) { return null; } if (name.startsWith(GET_OPTION_PREFIX)) { name = name.substring(GET_OPTION_PREFIX.length()); Method[] methods = param.getClass().getDeclaredMethods(); for (Method method : methods) { if (isIgnored(method)) { continue; } if ((method.getName().equals("get" + name) || method.getName().equals("is" + name)) && method.getParameterTypes().length == 0) { try { return new ApiResponseElement(name, method.invoke(this.param).toString()); } catch (Exception e) { throw new ApiException(ApiException.Type.INTERNAL_ERROR, e.getMessage()); } } } } return null; } public ApiResponse handleApiOptionAction(String name, JSONObject params) throws ApiException { if (this.param == null) { return null; } boolean isApiOption = false; if (name.startsWith(SET_OPTION_PREFIX)) { name = "set" + name.substring(SET_OPTION_PREFIX.length()); isApiOption = true; } else if (name.startsWith(ADD_OPTION_PREFIX)) { name = "add" + name.substring(ADD_OPTION_PREFIX.length()); isApiOption = true; } else if (name.startsWith(REMOVE_OPTION_PREFIX)) { name = "remove" + name.substring(REMOVE_OPTION_PREFIX.length()); isApiOption = true; } if (isApiOption) { try { Method[] methods = param.getClass().getDeclaredMethods(); for (Method method : methods) { if (isIgnored(method)) { continue; } if (method.getName().equals(name) && method.getParameterTypes().length == 1) { Object val = null; if (method.getParameterTypes()[0].equals(String.class)) { val = params.getString("String"); } else if (method.getParameterTypes()[0].equals(Integer.class) || method.getParameterTypes()[0].equals(int.class)) { try { val = params.getInt("Integer"); } catch (JSONException e) { throw new ApiException(ApiException.Type.ILLEGAL_PARAMETER, "Integer"); } } else if (method.getParameterTypes()[0].equals(Boolean.class) || method.getParameterTypes()[0].equals(boolean.class)) { try { val = params.getBoolean("Boolean"); } catch (JSONException e) { throw new ApiException(ApiException.Type.ILLEGAL_PARAMETER, "Boolean"); } } if (val == null) { // Value supplied doesnt match the type - try the next one continue; } method.invoke(this.param, val); return ApiResponseElement.OK; } } } catch (ApiException e) { throw e; } catch (Exception e) { throw new ApiException(ApiException.Type.INTERNAL_ERROR, e.getMessage()); } } return null; } /** * Override if implementing one or more views * @param name the name of the requested view * @param params the API request parameters * @return the API response * @throws ApiException if an error occurred while handling the API view endpoint */ public ApiResponse handleApiView(String name, JSONObject params) throws ApiException { throw new ApiException(ApiException.Type.BAD_VIEW, name); } /** * Override if implementing one or more actions * @param name the name of the requested action * @param params the API request parameters * @return the API response * @throws ApiException if an error occurred while handling the API action endpoint */ public ApiResponse handleApiAction(String name, JSONObject params) throws ApiException { throw new ApiException(ApiException.Type.BAD_ACTION, name); } /** * Override if implementing one or more 'other' operations - these are operations that _dont_ return structured data * @param msg the HTTP message containing the API request * @param name the name of the requested other endpoint * @param params the API request parameters * @return the HTTP message with the API response * @throws ApiException if an error occurred while handling the API other endpoint */ public HttpMessage handleApiOther(HttpMessage msg, String name, JSONObject params) throws ApiException { throw new ApiException(ApiException.Type.BAD_OTHER, name); } /** * Override if handling callbacks * @param msg the HTTP message containing the API request and response * @return the API response (set in the HTTP response body) * @throws ApiException if an error occurred while handling the API callback */ public String handleCallBack(HttpMessage msg) throws ApiException { throw new ApiException (ApiException.Type.URL_NOT_FOUND, msg.getRequestHeader().getURI().toString()); } public HttpMessage handleShortcut(HttpMessage msg) throws ApiException { throw new ApiException (ApiException.Type.URL_NOT_FOUND, msg.getRequestHeader().getURI().toString()); } public abstract String getPrefix(); public ApiAction getApiAction(String name) { for (ApiAction action :this.apiActions) { if (action.getName().equals(name)) { return action; } } return null; } public ApiView getApiView(String name) { for (ApiView view :this.apiViews) { if (view.getName().equals(name)) { return view; } } return null; } public ApiOther getApiOther(String name) { for (ApiOther other : this.apiOthers) { if (other.getName().equals(name)) { return other; } } return null; } protected List<String> getApiShortcuts() { return this.apiShortcuts; } protected int getParam(JSONObject params, String name, int defaultValue) { try { return params.getInt(name); } catch (Exception e) { return defaultValue; } } protected String getParam(JSONObject params, String name, String defaultValue) { try { return params.getString(name); } catch (Exception e) { return defaultValue; } } protected boolean getParam(JSONObject params, String name, boolean defaultValue) { try { return params.getBoolean(name); } catch (Exception e) { return defaultValue; } } /** * Validates that a parameter with the given {@code name} exists (and it has a value) in the given {@code parameters}. * * @param parameters the parameters * @param name the name of the parameter that must exist * @throws ApiException if the parameter with the given name does not exist or it has no value. * @since 2.6.0 */ protected void validateParamExists(JSONObject parameters, String name) throws ApiException { if (!parameters.has(name) || parameters.getString(name).length() == 0) { throw new ApiException(ApiException.Type.MISSING_PARAMETER, name); } } /** * Override to add custom headers for specific API operations * @param name the name of the operation * @param type the type of the operation * @param msg the HTTP response message to the API request */ public void addCustomHeaders(String name, RequestType type, HttpMessage msg) { // Do nothing in the default implementation } }