/* * RESTHeart - the Web API for MongoDB * Copyright (C) SoftInstigate Srl * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.restheart.handlers.injectors; import com.mongodb.util.JSON; import io.undertow.server.HttpServerExchange; import java.util.Arrays; import java.util.Deque; import java.util.Optional; import org.bson.BSONObject; import org.bson.BsonDocument; import org.bson.json.JsonParseException; import org.restheart.Bootstrapper; import org.restheart.db.CursorPool.EAGER_CURSOR_ALLOCATION_POLICY; import org.restheart.hal.UnsupportedDocumentIdException; import org.restheart.handlers.aggregation.AggregationPipeline; import org.restheart.handlers.PipedHttpHandler; import org.restheart.handlers.RequestContext; import org.restheart.handlers.RequestContext.DOC_ID_TYPE; import static org.restheart.handlers.RequestContext.EAGER_CURSOR_ALLOCATION_POLICY_QPARAM_KEY; import static org.restheart.handlers.RequestContext.FILTER_QPARAM_KEY; import static org.restheart.handlers.RequestContext.KEYS_QPARAM_KEY; import static org.restheart.handlers.RequestContext.PAGESIZE_QPARAM_KEY; import static org.restheart.handlers.RequestContext.PAGE_QPARAM_KEY; import static org.restheart.handlers.RequestContext.SORT_BY_QPARAM_KEY; import static org.restheart.handlers.RequestContext.HAL_QPARAM_KEY; import static org.restheart.handlers.RequestContext.HAL_MODE; import org.restheart.utils.HttpStatus; import org.restheart.utils.ResponseHelper; import org.restheart.utils.URLUtils; import static org.restheart.handlers.RequestContext.DOC_ID_TYPE_QPARAM_KEY; import org.restheart.handlers.RequestContext.METHOD; import org.restheart.handlers.RequestContext.REPRESENTATION_FORMAT; import org.restheart.handlers.RequestContext.TYPE; import static org.restheart.handlers.RequestContext.SHARDKEY_QPARAM_KEY; import static org.restheart.handlers.RequestContext.SORT_QPARAM_KEY; /** * * @author Andrea Di Cesare {@literal <andrea@softinstigate.com>} */ public class RequestContextInjectorHandler extends PipedHttpHandler { private final String whereUri; private final String whatUri; /** * * @param whereUri * @param whatUri * @param next */ public RequestContextInjectorHandler(String whereUri, String whatUri, PipedHttpHandler next) { super(next); if (whereUri == null) { throw new IllegalArgumentException("whereUri cannot be null. check your mongo-mounts."); } if (!whereUri.startsWith("/")) { throw new IllegalArgumentException("whereUri must start with \"/\". check your mongo-mounts"); } if (!whatUri.startsWith("/") && !whatUri.equals("*")) { throw new IllegalArgumentException("whatUri must start with \"/\". check your mongo-mounts"); } this.whereUri = URLUtils.removeTrailingSlashes(whereUri); this.whatUri = whatUri; } /** * * @param exchange * @param context * @throws Exception */ @Override public void handleRequest(HttpServerExchange exchange, RequestContext context) throws Exception { RequestContext rcontext = new RequestContext(exchange, whereUri, whatUri); // skip parameters injection if method is OPTIONS // this makes sure OPTIONS works even on wrong paramenter // e.g. OPTIONS 127.0.0.1:8080?page=a if (rcontext.getMethod() == METHOD.OPTIONS) { next(exchange, rcontext); return; } // get and check rep parameter (representation format) Deque<String> __rep = exchange .getQueryParameters() .get(RequestContext.REPRESENTATION_FORMAT_KEY); // default value REPRESENTATION_FORMAT rep = Bootstrapper .getConfiguration() .getDefaultRepresentationFormat(); if (__rep != null && !__rep.isEmpty()) { String _rep = __rep.getFirst(); if (_rep != null && !_rep.isEmpty()) { try { rep = REPRESENTATION_FORMAT.valueOf(_rep.trim().toUpperCase()); } catch (IllegalArgumentException iae) { rcontext.addWarning("illegal rep paramenter " + _rep + " (must be PLAIN_JSON, PJ or HAL)"); } } } rcontext.setRepresentationFormat(rep); // check database name to be a valid mongodb name if (rcontext.getDBName() != null && rcontext.isDbNameInvalid()) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal database name, see https://docs.mongodb.org/v3.2/reference/limits/#naming-restrictions"); next(exchange, rcontext); return; } // check collection name to be a valid mongodb name if (rcontext.getCollectionName() != null && rcontext.isCollectionNameInvalid()) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal collection name, " + "see https://docs.mongodb.org/v3.2/reference/limits/#naming-restrictions"); next(exchange, rcontext); return; } // check collection name to be a valid mongodb name if (rcontext.isReservedResource()) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_FORBIDDEN, "reserved resource"); next(exchange, rcontext); return; } Deque<String> __pagesize = exchange.getQueryParameters() .get(PAGESIZE_QPARAM_KEY); int page = 1; // default page int pagesize = 100; // default pagesize if (__pagesize != null && !(__pagesize.isEmpty())) { try { pagesize = Integer.parseInt(__pagesize.getFirst()); } catch (NumberFormatException ex) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal pagesize paramenter, it is not a number", ex); next(exchange, rcontext); return; } } if (pagesize < 0 || pagesize > 1_000) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal page parameter, pagesize must be >= 0 and <= 1000"); next(exchange, rcontext); return; } else { rcontext.setPagesize(pagesize); } Deque<String> __page = exchange.getQueryParameters() .get(PAGE_QPARAM_KEY); if (__page != null && !(__page.isEmpty())) { try { page = Integer.parseInt(__page.getFirst()); } catch (NumberFormatException ex) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal page paramenter, it is not a number", ex); next(exchange, rcontext); return; } } if (page < 1) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal page paramenter, it is < 1"); next(exchange, rcontext); return; } else { rcontext.setPage(page); } Deque<String> __count = exchange.getQueryParameters().get("count"); if (__count != null) { rcontext.setCount(true); } // get and check sort_by parameter Deque<String> sort_by = null; if (exchange.getQueryParameters().containsKey(SORT_BY_QPARAM_KEY)) { sort_by = exchange.getQueryParameters().get(SORT_BY_QPARAM_KEY); } else if (exchange.getQueryParameters().containsKey(SORT_QPARAM_KEY)) { sort_by = exchange.getQueryParameters().get(SORT_QPARAM_KEY); } if (sort_by != null) { if (sort_by.stream().anyMatch(s -> s == null || s.isEmpty())) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal sort_by paramenter"); next(exchange, rcontext); return; } if (sort_by.stream() .anyMatch(s -> s.trim().equals("_last_updated_on") || s.trim().equals("+_last_updated_on") || s.trim().equals("-_last_updated_on"))) { rcontext.addWarning("unexepecting sorting; " + "the _last_updated_on timestamp is generated " + "from the _etag property if present"); } if (sort_by.stream() .anyMatch(s -> s.trim().equals("_created_on") || s.trim().equals("_created_on") || s.trim().equals("_created_on"))) { rcontext.addWarning("unexepecting sorting; " + "the _created_on timestamp is generated " + "from the _id property if it is an ObjectId"); } rcontext.setSortBy(sort_by); } Deque<String> keys = exchange.getQueryParameters().get(KEYS_QPARAM_KEY); if (keys != null) { if (keys.stream().anyMatch(f -> { if (f == null || f.isEmpty()) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal keys paramenter (empty)"); return true; } try { Object _keys = JSON.parse(f); if (!(_keys instanceof BSONObject)) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal keys paramenter, it is not a json object: " + f + " => " + f.getClass().getSimpleName()); return true; } } catch (Throwable t) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal keys paramenter: " + f, t); return true; } return false; })) { next(exchange, rcontext); return; // an error occurred } rcontext.setKeys(exchange.getQueryParameters().get(KEYS_QPARAM_KEY)); } // get and check filter parameter Deque<String> filters = exchange.getQueryParameters().get(FILTER_QPARAM_KEY); if (filters != null) { if (filters.stream().anyMatch(f -> { if (f == null || f.isEmpty()) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal filter paramenter (empty)"); return true; } try { Object _filter = JSON.parse(f); if (!(_filter instanceof BSONObject)) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal filter paramenter, it is not a json object: " + f + " => " + f.getClass().getSimpleName()); return true; } else if (((BSONObject) _filter).keySet().isEmpty()) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal filter paramenter (empty json object)"); return true; } } catch (Throwable t) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal filter paramenter: " + f, t); return true; } return false; })) { next(exchange, rcontext); return; // an error occurred } rcontext.setFilter(exchange.getQueryParameters().get(FILTER_QPARAM_KEY)); } // filter qparam is mandatory for bulk DELETE and PATCH if (rcontext.getType() == TYPE.BULK_DOCUMENTS && (rcontext.getMethod() == METHOD.DELETE || rcontext.getMethod() == METHOD.PATCH) && (filters == null || filters.isEmpty())) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "filter paramenter is mandatory for bulk write requests"); next(exchange, rcontext); return; } // get and check avars parameter Deque<String> avars = exchange.getQueryParameters().get(RequestContext.AGGREGATION_VARIABLES_QPARAM_KEY); if (avars != null) { Optional<String> _qvars = avars.stream().findFirst(); if (!_qvars.isPresent()) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal avars paramenter (empty)"); try { next(exchange, rcontext); } catch (Exception e) { // nothing to do } return; } try { BsonDocument qvars; try { qvars = BsonDocument.parse(_qvars.get()); } catch (JsonParseException jpe) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal avars paramenter, it is not a json object: " + _qvars.get()); try { next(exchange, rcontext); } catch (Exception e) { // nothing to do } return; } // throws SecurityException if aVars contains operators AggregationPipeline.checkAggregationVariables(qvars); rcontext.setAggregationVars(qvars); } catch (Throwable t) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal avars paramenter: " + _qvars.get(), t); try { next(exchange, rcontext); } catch (Exception e) { // nothing to do } return; } } // get and check eager parameter Deque<String> __eager = exchange.getQueryParameters().get(EAGER_CURSOR_ALLOCATION_POLICY_QPARAM_KEY); // default value EAGER_CURSOR_ALLOCATION_POLICY eager = EAGER_CURSOR_ALLOCATION_POLICY.LINEAR; if (__eager != null && !__eager.isEmpty()) { String _eager = __eager.getFirst(); if (_eager != null && !_eager.isEmpty()) { try { eager = EAGER_CURSOR_ALLOCATION_POLICY.valueOf(_eager.trim().toUpperCase()); } catch (IllegalArgumentException iae) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal eager paramenter (must be LINEAR, RANDOM or NONE)"); try { next(exchange, rcontext); } catch (Exception e) { // nothing to do } return; } } } rcontext.setCursorAllocationPolicy(eager); // get and check the doc id type parameter Deque<String> __docIdType = exchange.getQueryParameters().get(DOC_ID_TYPE_QPARAM_KEY); // default value DOC_ID_TYPE docIdType = DOC_ID_TYPE.STRING_OID; if (__docIdType != null && !__docIdType.isEmpty()) { String _docIdType = __docIdType.getFirst(); if (_docIdType != null && !_docIdType.isEmpty()) { try { docIdType = DOC_ID_TYPE.valueOf(_docIdType.trim().toUpperCase()); } catch (IllegalArgumentException iae) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal " + DOC_ID_TYPE_QPARAM_KEY + " paramenter; must be " + Arrays.toString(DOC_ID_TYPE.values())); try { next(exchange, rcontext); } catch (Exception e) { // nothing to do } return; } } } rcontext.setDocIdType(docIdType); // for POST the doc _id is set by BodyjectorHandler if (rcontext.getMethod() != METHOD.POST) { // get and check the document id String _docId = rcontext.getDocumentIdRaw(); try { rcontext.setDocumentId(URLUtils.getDocumentIdFromURI(_docId, docIdType)); } catch (UnsupportedDocumentIdException idide) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "wrong document id format: not a valid " + docIdType.name(), idide); try { next(exchange, rcontext); } catch (Exception e) { // nothing to do } return; } } // get the HAL query parameter Deque<String> __halMode = exchange.getQueryParameters().get(HAL_QPARAM_KEY); if (__halMode == null || __halMode.isEmpty()) { // default is compact mode rcontext.setHalMode(HAL_MODE.COMPACT); } else { String _halMode = __halMode.getFirst(); try { rcontext.setHalMode(HAL_MODE.valueOf(_halMode.trim().toUpperCase())); } catch (IllegalArgumentException iae) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal " + HAL_QPARAM_KEY + " paramenter; valid values are " + Arrays.toString(HAL_MODE.values())); try { next(exchange, rcontext); } catch (Exception e) { // nothing to do } return; } // if representation has not been set explicitly, set it to HAL if (exchange .getQueryParameters() .get(RequestContext.REPRESENTATION_FORMAT_KEY) == null) { rcontext.setRepresentationFormat(REPRESENTATION_FORMAT.HAL); } } // get the shardkey query parameter // get and check shardKeys parameter Deque<String> shardKeys = exchange.getQueryParameters().get(SHARDKEY_QPARAM_KEY); if (shardKeys != null) { if (shardKeys.stream().anyMatch(f -> { if (f == null || f.isEmpty()) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal shardkey paramenter (empty)"); try { next(exchange, rcontext); } catch (Exception e) { // nothing to do } return true; } try { BsonDocument _shardKeys = BsonDocument.parse(f); if (_shardKeys.keySet().isEmpty()) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal shardkey paramenter (empty json object)"); try { next(exchange, rcontext); } catch (Exception e) { // nothing to do } return true; } rcontext.setShardKey(_shardKeys); } catch (Throwable t) { ResponseHelper.endExchangeWithMessage( exchange, rcontext, HttpStatus.SC_BAD_REQUEST, "illegal shardkey paramenter: " + f, t); try { next(exchange, rcontext); } catch (Exception e) { // nothing to do } return true; } return false; })) { return; // an error occurred } rcontext.setFilter(exchange.getQueryParameters().get(FILTER_QPARAM_KEY)); } next(exchange, rcontext); } @Override public void handleRequest(HttpServerExchange exchange) throws Exception { handleRequest(exchange, new RequestContext(exchange, whereUri, whatUri)); } }