rzo1 opened a new issue, #2085:
URL: https://github.com/apache/stormcrawler/issues/2085

   
   ## What happens
   `AbstractQueryingSpout.nextTuple()` takes the next entry out of the URL 
buffer and emits it unchanged. The per-backend spouts fill that buffer straight 
from the query results. No scheme check and no filtering happens between the 
store and the fetcher: shipped topologies wire the spout to 
`URLPartitionerBolt` and then to `FetcherBolt`, and URL filtering only runs on 
the discovery path, in `StatusEmitterBolt.filterOutlink` and `URLFilterBolt`. A 
row whose URL uses a scheme the operator never intended to crawl is fetched all 
the same, as long as a protocol implementation is registered for it.
   
   ## Where
   
`core/src/main/java/org/apache/stormcrawler/persistence/AbstractQueryingSpout.java:202`:
   
   ```java
               List<Object> fields = buffer.next();
               String url = fields.get(0).toString();
               this.collector.emit(fields, url);
   ```
   
   The same applies to the backend spouts that feed the buffer, for example 
`SQLSpout.processRow`, the OpenSearch `AbstractSpout`, 
`SolrSpout.handleSuccess` and the urlfrontier `Spout.onNext`.
   
   ## Why it matters
   The frontier is the crawl instruction set, so write access to it already 
means a lot of control, and this is not a way in by itself. It does mean there 
is no second line of defence at the point where URLs re-enter the topology: 
whatever ends up in the store is fetched, including schemes the operator's 
filter chain would reject on the discovery path. Combined with the file scheme 
being registered by default, a single bad row is a local file read rather than 
a wasted fetch. A scheme check in the spout is cheap and would keep the two 
paths consistent.
   
   ## Reproduction
   
   Save as 
`core/src/test/java/org/apache/stormcrawler/persistence/AbstractQueryingSpoutSchemeTest.java`.
   
   ```java
   /*
    * Licensed to the Apache Software Foundation (ASF) under one or more
    * contributor license agreements.  See the NOTICE file distributed with
    * this work for additional information regarding copyright ownership.
    * The ASF licenses this file to you 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.apache.stormcrawler.persistence;
   
   import java.util.HashMap;
   import java.util.Map;
   import org.apache.storm.topology.OutputFieldsDeclarer;
   import org.apache.storm.tuple.Fields;
   import org.apache.stormcrawler.Metadata;
   import org.apache.stormcrawler.TestUtil;
   import org.apache.stormcrawler.spout.mocks.FileSpoutOutputCollectorMock;
   import org.junit.jupiter.api.Assertions;
   import org.junit.jupiter.api.Test;
   
   class AbstractQueryingSpoutSchemeTest {
   
       /** Minimal spout which returns whatever a backend row would contain. */
       private static class StoredRowSpout extends AbstractQueryingSpout {
   
           private final String url;
   
           StoredRowSpout(String url) {
               this.url = url;
           }
   
           @Override
           protected void populateBuffer() {
               buffer.add(url, new Metadata());
               markQueryReceivedNow();
           }
   
           @Override
           public void declareOutputFields(OutputFieldsDeclarer declarer) {
               declarer.declare(new Fields("url", "metadata"));
           }
       }
   
       private static Map<String, Object> conf() {
           Map<String, Object> conf = new HashMap<>();
           conf.put(
                   "urlbuffer.class",
                   
"org.apache.stormcrawler.persistence.urlbuffer.SimpleURLBuffer");
           return conf;
       }
   
       @Test
       void httpUrlsFromTheBackendAreEmitted() {
           StoredRowSpout spout = new 
StoredRowSpout("https://example.com/page.html";);
           FileSpoutOutputCollectorMock collector = new 
FileSpoutOutputCollectorMock();
           spout.open(conf(), TestUtil.getMockedTopologyContext(), collector);
           spout.activate();
           // first call fills the buffer, second one emits from it
           spout.nextTuple();
           spout.nextTuple();
           Assertions.assertNotNull(collector.getTuple());
           Assertions.assertEquals("https://example.com/page.html";, 
collector.getTuple().get(0));
       }
   
       @Test
       void urlsWithAnUnexpectedSchemeAreNotEmitted() {
           StoredRowSpout spout = new StoredRowSpout("file:///etc/hosts");
           FileSpoutOutputCollectorMock collector = new 
FileSpoutOutputCollectorMock();
           spout.open(conf(), TestUtil.getMockedTopologyContext(), collector);
           spout.activate();
           // first call fills the buffer, second one emits from it
           spout.nextTuple();
           spout.nextTuple();
           Assertions.assertNull(
                   collector.getTuple(),
                   "the spout emitted a stored URL whose scheme is not in the 
configured list: "
                           + collector.getTuple());
       }
   }
   ```
   
   Run it:
   
   ```
   mvn -pl core test -Dtest=AbstractQueryingSpoutSchemeTest
   ```
   
   It drives a minimal spout with a mocked context and the existing 
`FileSpoutOutputCollectorMock`. The control case with an https URL passes; the 
case with a `file://` row asserts the wanted behaviour and fails on main.
   
   ```
   AbstractQueryingSpoutSchemeTest.urlsWithAnUnexpectedSchemeAreNotEmitted -- 
Time elapsed: 0.010 s <<< FAILURE!
   org.opentest4j.AssertionFailedError: the spout emitted a stored URL whose 
scheme is not in the configured list: [file:///etc/hosts, ] ==> expected: 
<null> but was: <[file:///etc/hosts, ]>
   
   Tests run: 2, Failures: 1, Errors: 0, Skipped: 0
   ```
   
   ## Suggested fix
   In `AbstractQueryingSpout.nextTuple()`, check the scheme of the URL against 
the configured `protocols` list (or a dedicated key) before emitting, and count 
the rejects on the existing event counter so operators can see them. Do not run 
the full URL filter chain there: that is hot path cost on every tuple and would 
silently drop rows an operator deliberately put in the store. Log rejected rows 
at WARN with the URL, since a rejected row otherwise stays in the store and is 
retried on every query.
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to