diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java b/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java index c1ae7dc4431775ae7a9a499b8a816f1c8424bab7..4972ca632764105d12ca5bf5bc0ea6b9cdf49fe0 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java +++ b/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java @@ -37,7 +37,7 @@ public class MongoQuery<T> { private static final Logger LOGGER = LoggerFactory.getLogger(MongoQuery.class); private CachingService<List<T>> cache; - private RequestWrapper qps; + private RequestWrapper wrapper; private DtoFilter<T> dtoFilter; private Bson filter; @@ -45,8 +45,8 @@ public class MongoQuery<T> { private SortOrder order; private List<Bson> aggregates; - public MongoQuery(RequestWrapper qps, DtoFilter<T> dtoFilter, CachingService<List<T>> cache) { - this.qps = qps; + public MongoQuery(RequestWrapper wrapper, DtoFilter<T> dtoFilter, CachingService<List<T>> cache) { + this.wrapper = wrapper; this.dtoFilter = dtoFilter; this.cache = cache; this.aggregates = new ArrayList<>(); @@ -68,10 +68,10 @@ public class MongoQuery<T> { // get the filters for the current DTO List<Bson> filters = new ArrayList<>(); - filters.addAll(dtoFilter.getFilters(qps)); + filters.addAll(dtoFilter.getFilters(wrapper)); // get fields that make up the required fields to enable pagination and check - Optional<String> sortOpt = qps.getFirstParam(UrlParameterNames.SORT); + Optional<String> sortOpt = wrapper.getFirstParam(UrlParameterNames.SORT); if (sortOpt.isPresent()) { String sortVal = sortOpt.get(); // split sort string of `<fieldName> <SortOrder>` @@ -86,7 +86,7 @@ public class MongoQuery<T> { if (!filters.isEmpty()) { this.filter = Filters.and(filters); } - this.aggregates = dtoFilter.getAggregates(qps); + this.aggregates = dtoFilter.getAggregates(wrapper); if (LOGGER.isDebugEnabled()) { LOGGER.debug("MongoDB query initialized with filter: {}", this.filter); @@ -129,7 +129,7 @@ public class MongoQuery<T> { * present and numeric, otherwise returns -1. */ public int getLimit() { - Optional<String> limitVal = qps.getFirstParam(UrlParameterNames.LIMIT); + Optional<String> limitVal = wrapper.getFirstParam(UrlParameterNames.LIMIT); if (limitVal.isPresent() && StringUtils.isNumeric(limitVal.get())) { return Integer.parseInt(limitVal.get()); } @@ -137,7 +137,7 @@ public class MongoQuery<T> { } private void setSort(String sortField, String sortOrder, List<Bson> filters) { - Optional<String> lastOpt = qps.getFirstParam(UrlParameterNames.LAST_SEEN); + Optional<String> lastOpt = wrapper.getFirstParam(UrlParameterNames.LAST_SEEN); List<Sortable<?>> fields = SortableHelper.getSortableFields(getDocType()); Optional<Sortable<?>> fieldContainer = SortableHelper.getSortableFieldByName(fields, sortField); @@ -186,21 +186,21 @@ public class MongoQuery<T> { * @return the docType */ public Class<T> getDocType() { - return (Class<T>) qps.getAttribute(AnnotationClassInjectionFilter.ATTRIBUTE_NAME).get(); + return (Class<T>) wrapper.getAttribute(AnnotationClassInjectionFilter.ATTRIBUTE_NAME).get(); } /** - * @return the qps + * @return the wrapper */ - public RequestWrapper getQps() { - return qps; + public RequestWrapper getWrapper() { + return wrapper; } /** - * @param qps the qps to set + * @param wrapper the wrapper to set */ - public void setQps(RequestWrapper qps) { - this.qps = qps; + public void setWrapper(RequestWrapper wrapper) { + this.wrapper = wrapper; } @Override diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java b/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java index b6f103cbdf45f955bef4dbe16f053e97b554b392..9169035502b91b3fc9e2a0bb5f13efaf125f24e4 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java +++ b/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java @@ -53,7 +53,7 @@ public class RequestWrapper { /** * Retrieves the first value set in a list from the map for a given key. * - * @param params the parameter map containing the value + * @param wrapper the parameter map containing the value * @param key the key to retrieve the value for * @return the first value set in the parameter map for the given key, or null * if absent. @@ -73,7 +73,7 @@ public class RequestWrapper { /** * Retrieves the value list from the map for a given key. * - * @param params the parameter map containing the values + * @param wrapper the parameter map containing the values * @param key the key to retrieve the values for * @return the value list for the given key if it exists, or an empty collection * if none exists. @@ -94,7 +94,7 @@ public class RequestWrapper { * Adds the given value for the given key, preserving previous values if they * exist. * - * @param params map containing parameters to update + * @param wrapper map containing parameters to update * @param key string key to add the value to, must not be null * @param value the value to add to the key */ @@ -166,4 +166,14 @@ public class RequestWrapper { } return this.userAgent; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("RequestWrapper ["); + sb.append("ip=").append(request.getRemoteAddr()); + sb.append(", uri=").append(request.getRequestURI()); + sb.append(", params=").append(getParams()); + return sb.toString(); + } } diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/UserAgent.java b/src/main/java/org/eclipsefoundation/marketplace/model/UserAgent.java index c5221f559ad3c70ea1fcaba8c04658791bfc6238..31700ed0f8b148c15504f8723fbc0407cf07f653 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/model/UserAgent.java +++ b/src/main/java/org/eclipsefoundation/marketplace/model/UserAgent.java @@ -6,12 +6,14 @@ */ package org.eclipsefoundation.marketplace.model; -import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; +import org.eclipsefoundation.marketplace.dto.Install; + import com.google.common.base.Splitter; /** @@ -21,7 +23,6 @@ import com.google.common.base.Splitter; * @author Martin Lowe */ public class UserAgent { - /** * Matches a generic form of User-Agent strings with the following sections * @@ -32,16 +33,18 @@ public class UserAgent { * and version, separated by a slash. */ private static final Pattern USER_AGENT_PATTERN = Pattern - .compile("^(\\S+\\/\\S+)\\s?(?:\\(([^\\)]*?)\\)([^\\(]+(?:\\(([^\\)]*?)\\)([^\\(]+)?)?)?)?$"); + .compile("^(\\S+\\/\\S+)\\s?(?:\\(([^\\)]*?)\\)(?:([^\\(]+)(?:\\(([^\\)]*+)\\))?)?)?$"); private static final String MPC_CLIENT_AGENT_NAME = "mpc"; - private final String name; - private final String version; + private final String base; + + private final String agentDeclaration; private final String systemProperties; - private final String platform; private final String platformDetails; - private final String enhancements; + private final String application; + private String name; + private String version; private String javaVersion; private String javaVendor; @@ -49,8 +52,12 @@ public class UserAgent { private String osVersion; private String locale; + private String product; + private String productVersion; private String eclipseVersion; + private boolean valid = true; + public UserAgent(String userAgent) { Objects.requireNonNull(userAgent); @@ -60,49 +67,52 @@ public class UserAgent { throw new IllegalArgumentException("Passed string does not match an expected user-agent"); } + this.base = userAgent; // get the name and version of the user agent - String agentDeclaration = m.group(0); - Iterator<String> it = Splitter.on('/').trimResults().split(agentDeclaration).iterator(); - this.name = it.next(); - this.version = it.next(); + this.agentDeclaration = m.group(1); + List<String> agentProperties = Splitter.on('/').trimResults().limit(2).splitToList(agentDeclaration); + if (agentProperties.size() != 2) { + // should never throw as format is promised in regex + throw new IllegalArgumentException("Cannot read User-Agent name and version"); + } + this.name = agentProperties.get(0); + this.version = agentProperties.get(1); - this.systemProperties = m.group(1); - this.platform = m.group(2); + this.systemProperties = m.group(2); this.platformDetails = m.group(3); - this.enhancements = m.group(4); - if (MPC_CLIENT_AGENT_NAME.equalsIgnoreCase(name)) { - handleMpc(); + this.application = m.group(4); + if (isFromMPC()) { + consumeSystemProps(); + consumePlatformDetails(); } } - /** - * Breaks down the different MPC properties into explicit properties that can be - * retrieved via getters built into the class. The expected format is defined - * below: - * - * <p> - * {@code mpc/<mpc version> (Java <java version> <java vendor>; <os name> <os version> <os arch>; <locale>) <eclipse product>/<product version> (<eclipse application>)} - * </p> - */ - private void handleMpc() { + private void consumeSystemProps() { + if (this.systemProperties == null) { + this.valid = false; + return; + } // expected form: (Java <java version> <java vendor>; <os name> <os version> <os // arch>; <locale>) - List<String> systemPropList = Splitter.on(';').splitToList(systemProperties); + List<String> systemPropList = Splitter.on(';').trimResults().splitToList(systemProperties); if (systemPropList.size() != 3) { - // TODO throw exception? + this.valid = false; + return; } // expected form example: Java <java version> <vendor> - List<String> javaProps = Splitter.on(' ').limit(3).splitToList(systemPropList.get(0)); + List<String> javaProps = Splitter.on(' ').trimResults().limit(3).splitToList(systemPropList.get(0)); if (javaProps.size() != 3) { - // TODO throw exception? + this.valid = false; + return; } this.javaVersion = javaProps.get(1); this.javaVendor = javaProps.get(2); // expected form: <OS name> <OS version> <OS arch> - List<String> systemProps = Splitter.on(' ').limit(3).splitToList(systemPropList.get(1)); + List<String> systemProps = Splitter.on(' ').trimResults().limit(3).splitToList(systemPropList.get(1)); if (systemProps.size() != 3) { - // TODO throw exception? + this.valid = false; + return; } this.os = systemProps.get(0); this.osVersion = systemProps.get(1); @@ -110,36 +120,75 @@ public class UserAgent { // get the current locale this.locale = systemPropList.get(2); - // expected form: <eclipse product>/<product version> - List<String> platformProps = Splitter.on('/').limit(3).splitToList(platform); + // check if any fields are invalid + if (StringUtils.isBlank(javaVersion) || StringUtils.isBlank(javaVendor) || StringUtils.isBlank(os) + || StringUtils.isBlank(locale)) { + this.valid = false; + } + } + + private void consumePlatformDetails() { + if (this.platformDetails == null) { + this.valid = false; + return; + } + // expected form: <eclipse product>/<product version>/<platform version> + List<String> platformProps = Splitter.on('/').trimResults().limit(3).splitToList(platformDetails); + if (platformProps.size() != 3) { + this.valid = false; + return; + } + // get the properties and check if any fields are invalid + this.product = platformProps.get(0); + this.productVersion = platformProps.get(1); + this.eclipseVersion = platformProps.get(2); + if (StringUtils.isBlank(product) || StringUtils.isBlank(productVersion) + || StringUtils.isBlank(eclipseVersion)) { + this.valid = false; + } } /** - * @return the name + * Generates a basic install record based on information based on the user agent + * properties. This can only be used when the agent is detected as an MPC call + * and the object is valid. + * + * @return a basic populated install record without listing information, or null + * if the call doesn't originate from MPC or is missing information. */ - public String getName() { - return name; + public Install generateInstallRecord() { + // check that agent comes from MPC and is valid before generating + if (!isValid()) { + return null; + } + // generate install record from fields + Install install = new Install(); + install.setJavaVersion(javaVersion); + install.setLocale(locale); + install.setOs(os); + install.setEclipseVersion(eclipseVersion); + return install; } /** - * @return the version + * @return the base user agent string */ - public String getVersion() { - return version; + public String getBase() { + return base; } /** - * @return the version + * @return the systemProperties */ public String getSystemProperties() { return systemProperties; } /** - * @return the platform + * @return the agentDeclaration */ - public String getPlatform() { - return platform; + public String getAgentDeclaration() { + return agentDeclaration; } /** @@ -152,8 +201,22 @@ public class UserAgent { /** * @return the enhancements */ - public String getEnhancements() { - return enhancements; + public String getApplication() { + return application; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @return the version + */ + public String getVersion() { + return version; } /** @@ -191,4 +254,64 @@ public class UserAgent { return locale; } + /** + * @return the product + */ + public String getProduct() { + return product; + } + + /** + * @return the productVersion + */ + public String getProductVersion() { + return productVersion; + } + + /** + * Checks whether the clients agent name matches expected value for the + * marketplace client {@link MPC_CLIENT_AGENT_NAME}. + * + * @return true if client agent name matches expected value, false otherwise. + */ + public boolean isFromMPC() { + return MPC_CLIENT_AGENT_NAME.equalsIgnoreCase(name); + } + + /** + * @return whether the current user agent is a valid MPC user agent + */ + public boolean isValid() { + return valid && isFromMPC(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("UserAgent [name="); + builder.append(name); + builder.append(", version="); + builder.append(version); + builder.append(", javaVersion="); + builder.append(javaVersion); + builder.append(", javaVendor="); + builder.append(javaVendor); + builder.append(", os="); + builder.append(os); + builder.append(", osVersion="); + builder.append(osVersion); + builder.append(", locale="); + builder.append(locale); + builder.append(", eclipseVersion="); + builder.append(eclipseVersion); + builder.append(", product="); + builder.append(product); + builder.append(", productVersion="); + builder.append(productVersion); + builder.append(", application="); + builder.append(application); + builder.append("]"); + return builder.toString(); + } + } diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java index 9b84085298c5f3f6940c28d61d5aa8f33e49e75d..d5882ed583adfbe0a0cae50fbf7e081859db3a5f 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java +++ b/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java @@ -20,11 +20,13 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; import org.eclipsefoundation.marketplace.dao.MongoDao; import org.eclipsefoundation.marketplace.dto.Install; import org.eclipsefoundation.marketplace.dto.filter.DtoFilter; import org.eclipsefoundation.marketplace.helper.StreamHelper; +import org.eclipsefoundation.marketplace.model.Error; import org.eclipsefoundation.marketplace.model.MongoQuery; import org.eclipsefoundation.marketplace.model.RequestWrapper; import org.eclipsefoundation.marketplace.model.ResourceDataType; @@ -50,7 +52,7 @@ public class InstallResource { @Inject MongoDao dao; @Inject - RequestWrapper params; + RequestWrapper wrapper; @Inject DtoFilter<Install> dtoFilter; @@ -59,7 +61,7 @@ public class InstallResource { CachingService<Long> countCache; @Inject CachingService<List<Install>> installCache; - + /** * Endpoint for /listing/\<listingId\>/installs to retrieve install metrics for * a specific listing from the database. @@ -70,9 +72,9 @@ public class InstallResource { @GET @Path("/{listingId}") public Response selectInstallMetrics(@PathParam("listingId") String listingId) { - params.addParam(UrlParameterNames.ID, listingId); - MongoQuery<Install> q = new MongoQuery<>(params, dtoFilter, installCache); - Optional<Long> cachedResults = countCache.get(listingId, params, + wrapper.addParam(UrlParameterNames.ID, listingId); + MongoQuery<Install> q = new MongoQuery<>(wrapper, dtoFilter, installCache); + Optional<Long> cachedResults = countCache.get(listingId, wrapper, () -> StreamHelper.awaitCompletionStage(dao.count(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached listing for ID {}", listingId); @@ -94,11 +96,12 @@ public class InstallResource { */ @GET @Path("/{listingId}/{version}") - public Response selectInstallMetrics(@PathParam("listingId") String listingId, @PathParam("version") String version) { - params.addParam(UrlParameterNames.ID, listingId); - params.addParam(UrlParameterNames.VERSION, version); - MongoQuery<Install> q = new MongoQuery<>(params, dtoFilter, installCache); - Optional<Long> cachedResults = countCache.get(getCompositeKey(listingId, version), params, + public Response selectInstallMetrics(@PathParam("listingId") String listingId, + @PathParam("version") String version) { + wrapper.addParam(UrlParameterNames.ID, listingId); + wrapper.addParam(UrlParameterNames.VERSION, version); + MongoQuery<Install> q = new MongoQuery<>(wrapper, dtoFilter, installCache); + Optional<Long> cachedResults = countCache.get(getCompositeKey(listingId, version), wrapper, () -> StreamHelper.awaitCompletionStage(dao.count(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached listing for ID {}", listingId); @@ -121,21 +124,39 @@ public class InstallResource { @Path("/{listingId}/{version}") public Response postInstallMetrics(@PathParam("listingId") String listingId, @PathParam("version") String version, Install installDetails) { - // update the install details to reflect the current request - installDetails.setInstallDate(new Date(System.currentTimeMillis())); - installDetails.setListingId(listingId); - installDetails.setVersion(version); + Install record = null; + // check that connection was opened by MPC, and check for install information + // from user agent + if (wrapper.getUserAgent().isValid()) { + record = wrapper.getUserAgent().generateInstallRecord(); + } else if (wrapper.getUserAgent().isFromMPC()) { + if (installDetails == null) { + return new Error(Status.BAD_REQUEST, "Install data could not be read from request body") + .asResponse(); + } + record = installDetails; + } else { + LOGGER.warn("Rebuffed request to post install from request: {}", wrapper); + return new Error(Status.FORBIDDEN, "Installs cannot be posted directly from consumer applications") + .asResponse(); + } + + // update the install details to reflect the current request + record.setInstallDate(new Date(System.currentTimeMillis())); + record.setListingId(listingId); + record.setVersion(version); + // create the query wrapper to pass to DB dao - MongoQuery<Install> q = new MongoQuery<>(params, dtoFilter, installCache); + MongoQuery<Install> q = new MongoQuery<>(wrapper, dtoFilter, installCache); // add the object, and await the result - StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(installDetails))); + StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(record))); // return the results as a response return Response.ok().build(); } - + private String getCompositeKey(String listingId, String version) { return listingId + ':' + version; } diff --git a/src/main/node/index.js b/src/main/node/index.js index e0899796e7fce1255aac9fc2eb3dae077981821e..5bbf210026abd738c85dab42ab9d7d7e96a94763 100644 --- a/src/main/node/index.js +++ b/src/main/node/index.js @@ -1,4 +1,8 @@ const axios = require('axios'); +const instance = axios.create({ + timeout: 1000, + headers: {'User-Agent': 'mpc/0.0.0'} +}); const randomWords = require('random-words'); const uuid = require('uuid'); const argv = require('yargs') @@ -68,7 +72,7 @@ function createListing(count) { console.log(`Generating listing ${count} of ${max}`); var json = generateJSON(uuid.v4()); - axios.put(argv.s+"/listings/", json) + axios.post(argv.s+"/listings/", json) .then(() => { var installs = Math.floor(Math.random()*argv.i); console.log(`Generating ${installs} install records for listing '${json.id}'`); @@ -82,7 +86,7 @@ function createCategory(count) { return; } - axios.put(argv.s+"/categories/", generateCategoryJSON(categoryIds[count++])) + instance.put(argv.s+"/categories/", generateCategoryJSON(categoryIds[count++])) .then(() => createCategory(count)) .catch(err => console.log(err)); } @@ -92,7 +96,7 @@ function createMarket(count) { return; } - axios.put(argv.s+"/markets/", generateMarketJSON(marketIds[count++])) + instance.put(argv.s+"/markets/", generateMarketJSON(marketIds[count++])) .then(() => createMarket(count)) .catch(err => console.log(err)); } @@ -102,7 +106,7 @@ function createInstall(curr, max, listing, callback) { return callback(); } var json = generateInstallJSON(listing); - axios.post(`${argv.s}/installs/${json['listing_id']}/${json.version}`, json) + instance.post(`${argv.s}/installs/${json['listing_id']}/${json.version}`, json) .then(createInstall(curr+1,max,listing,callback)) .catch(err => console.log(err)); } @@ -170,8 +174,8 @@ function generateMarketJSON(id) { function generateInstallJSON(listing) { var version = listing.versions[Math.floor(Math.random()*listing.versions.length)]; - var javaVersions = javaVs.splice(javaVs.indexOf(version["min_java_version"])); - var eclipseVersions = eclipseVs.splice(eclipseVs.indexOf(version["eclipse_version"])); + var javaVersions = Array.from(javaVs).splice(javaVs.indexOf(version["min_java_version"])); + var eclipseVersions = Array.from(eclipseVs).splice(eclipseVs.indexOf(version["eclipse_version"])); return { "listing_id": listing.id,