class URLRepository extends BlobStoreRepository {

    private static final Logger LOGGER = LogManager.getLogger(URLRepository.class);

    public static final String TYPE = "url";

    public static final Setting<List<String>> SUPPORTED_PROTOCOLS_SETTING =
        Setting.listSetting("repositories.url.supported_protocols", Arrays.asList("http", "https", "ftp", "file", "jar"),
            Function.identity(), DataTypes.STRING_ARRAY, Property.NodeScope);

    public static final Setting<List<URIPattern>> ALLOWED_URLS_SETTING =
        Setting.listSetting("repositories.url.allowed_urls", Collections.emptyList(), URIPattern::new, DataTypes.STRING_ARRAY, Property.NodeScope);

    public static final Setting<URL> URL_SETTING = new Setting<>("url", "http:///", URLRepository::parseURL, DataTypes.STRING, Property.NodeScope);

    public static final Setting<URL> REPOSITORIES_URL_SETTING = new Setting<>(
        "repositories.url.url",
        (s) -> s.get("repositories.uri.url", "http:///"),
        URLRepository::parseURL,
        DataTypes.STRING,
        Property.NodeScope
        );

    public static List<Setting<?>> mandatorySettings() {
        return List.of(URL_SETTING);
    }

    private final List<String> supportedProtocols;

    private final URIPattern[] urlWhiteList;

    private final Environment environment;

    private final BlobPath basePath;

    private final URL url;

    /**
     * Constructs a read-only URL-based repository
     */
    public URLRepository(RepositoryMetadata metadata,
                         Environment environment,
                         NamedXContentRegistry namedXContentRegistry,
                         ClusterService clusterService,
                         RecoverySettings recoverySettings) {
        super(metadata, namedXContentRegistry, clusterService, recoverySettings, BlobPath.cleanPath());

        if (URL_SETTING.exists(metadata.settings()) == false && REPOSITORIES_URL_SETTING.exists(environment.settings()) == false) {
            throw new RepositoryException(metadata.name(), "missing url");
        }
        this.environment = environment;
        supportedProtocols = SUPPORTED_PROTOCOLS_SETTING.get(environment.settings());
        urlWhiteList = ALLOWED_URLS_SETTING.get(environment.settings()).toArray(new URIPattern[]{});
        basePath = BlobPath.cleanPath();
        url = URL_SETTING.exists(metadata.settings())
            ? URL_SETTING.get(metadata.settings()) : REPOSITORIES_URL_SETTING.get(environment.settings());
    }

    @Override
    protected BlobStore createBlobStore() {
        URL normalizedURL = checkURL(url);
        return new URLBlobStore(environment.settings(), normalizedURL);
    }

    // only use for testing
    @Override
    protected BlobContainer blobContainer() {
        return super.blobContainer();
    }

    // only use for testing
    @Override
    protected BlobStore getBlobStore() {
        return super.getBlobStore();
    }

    @Override
    public BlobPath basePath() {
        return basePath;
    }

    /**
     * Makes sure that the url is white listed or if it points to the local file system it matches one on of the root path in path.repo
     */
    private URL checkURL(URL url) {<FILL_FUNCTION_BODY>}

    @Override
    public boolean isReadOnly() {
        return true;
    }

    private static URL parseURL(String s) {
        try {
            return new URI(s).toURL();
        } catch (URISyntaxException | MalformedURLException e) {
            throw new IllegalArgumentException("Unable to parse URL repository setting", e);
        }
    }
}

        String protocol = url.getProtocol();
        if (protocol == null) {
            throw new RepositoryException(getMetadata().name(), "unknown url protocol from URL [" + url + "]");
        }
        for (String supportedProtocol : supportedProtocols) {
            if (supportedProtocol.equals(protocol)) {
                try {
                    if (URIPattern.match(urlWhiteList, url.toURI())) {
                        // URL matches white list - no additional processing is needed
                        return url;
                    }
                } catch (URISyntaxException ex) {
                    LOGGER.warn("cannot parse the specified url [{}]", url);
                    throw new RepositoryException(getMetadata().name(), "cannot parse the specified url [" + url + "]");
                }
                // We didn't match white list - try to resolve against path.repo
                URL normalizedUrl = environment.resolveRepoURL(url);
                if (normalizedUrl == null) {
                    String logMessage = "The specified url [{}] doesn't start with any repository paths specified by the " +
                        "path.repo setting or by {} setting: [{}] ";
                    LOGGER.warn(logMessage, url, ALLOWED_URLS_SETTING.getKey(), environment.repoFiles());
                    String exceptionMessage = "file url [" + url + "] doesn't match any of the locations specified by path.repo or "
                        + ALLOWED_URLS_SETTING.getKey();
                    throw new RepositoryException(getMetadata().name(), exceptionMessage);
                }
                return normalizedUrl;
            }
        }
        throw new RepositoryException(getMetadata().name(), "unsupported url protocol [" + protocol + "] from URL [" + url + "]");
    
<methods>public org.elasticsearch.common.blobstore.BlobPath basePath() ,public org.elasticsearch.common.blobstore.BlobStore blobStore() ,public void deleteSnapshots(Collection<org.elasticsearch.snapshots.SnapshotId>, long, org.elasticsearch.Version, ActionListener<org.elasticsearch.repositories.RepositoryData>) ,public void endVerification(java.lang.String) ,public void executeConsistentStateUpdate(Function<org.elasticsearch.repositories.RepositoryData,org.elasticsearch.cluster.ClusterStateUpdateTask>, java.lang.String, Consumer<java.lang.Exception>) ,public void finalizeSnapshot(org.elasticsearch.repositories.ShardGenerations, long, org.elasticsearch.cluster.metadata.Metadata, org.elasticsearch.snapshots.SnapshotInfo, org.elasticsearch.Version, UnaryOperator<org.elasticsearch.cluster.ClusterState>, ActionListener<org.elasticsearch.repositories.RepositoryData>) ,public org.elasticsearch.cluster.metadata.RepositoryMetadata getMetadata() ,public CompletableFuture<org.elasticsearch.repositories.RepositoryData> getRepositoryData() ,public CompletableFuture<org.elasticsearch.index.snapshots.IndexShardSnapshotStatus> getShardSnapshotStatus(org.elasticsearch.snapshots.SnapshotId, org.elasticsearch.repositories.IndexId, org.elasticsearch.index.shard.ShardId) ,public CompletableFuture<org.elasticsearch.cluster.metadata.Metadata> getSnapshotGlobalMetadata(org.elasticsearch.snapshots.SnapshotId) ,public CompletableFuture<org.elasticsearch.cluster.metadata.IndexMetadata> getSnapshotIndexMetadata(org.elasticsearch.repositories.RepositoryData, org.elasticsearch.snapshots.SnapshotId, org.elasticsearch.repositories.IndexId) ,public CompletableFuture<Collection<org.elasticsearch.cluster.metadata.IndexMetadata>> getSnapshotIndexMetadata(org.elasticsearch.repositories.RepositoryData, org.elasticsearch.snapshots.SnapshotId, Collection<org.elasticsearch.repositories.IndexId>) ,public CompletableFuture<org.elasticsearch.snapshots.SnapshotInfo> getSnapshotInfo(org.elasticsearch.snapshots.SnapshotId) ,public boolean isReadOnly() ,public org.elasticsearch.index.snapshots.blobstore.BlobStoreIndexShardSnapshot loadShardSnapshot(org.elasticsearch.common.blobstore.BlobContainer, org.elasticsearch.snapshots.SnapshotId) ,public java.io.InputStream maybeRateLimitRestores(java.io.InputStream) ,public java.io.InputStream maybeRateLimitSnapshots(java.io.InputStream) ,public void restoreShard(org.elasticsearch.index.store.Store, org.elasticsearch.snapshots.SnapshotId, org.elasticsearch.repositories.IndexId, org.elasticsearch.index.shard.ShardId, org.elasticsearch.indices.recovery.RecoveryState, ActionListener<java.lang.Void>) ,public org.elasticsearch.common.blobstore.BlobContainer shardContainer(org.elasticsearch.repositories.IndexId, int) ,public void snapshotShard(org.elasticsearch.index.store.Store, org.elasticsearch.snapshots.SnapshotId, org.elasticsearch.repositories.IndexId, IndexCommit, java.lang.String, org.elasticsearch.index.snapshots.IndexShardSnapshotStatus, org.elasticsearch.Version, ActionListener<java.lang.String>) ,public java.lang.String startVerification() ,public static java.lang.String testBlobPrefix(java.lang.String) ,public org.elasticsearch.threadpool.ThreadPool threadPool() ,public java.lang.String toString() ,public void updateState(org.elasticsearch.cluster.ClusterState) ,public void verify(java.lang.String, org.elasticsearch.cluster.node.DiscoveryNode) <variables>public static final Setting<java.lang.Boolean> COMPRESS_SETTING,public static final ChecksumBlobStoreFormat<org.elasticsearch.cluster.metadata.Metadata> GLOBAL_METADATA_FORMAT,public static final java.lang.String INDEX_FILE_PREFIX,public static final java.lang.String INDEX_LATEST_BLOB,public static final ChecksumBlobStoreFormat<org.elasticsearch.cluster.metadata.IndexMetadata> INDEX_METADATA_FORMAT,public static final ChecksumBlobStoreFormat<org.elasticsearch.index.snapshots.blobstore.BlobStoreIndexShardSnapshots> INDEX_SHARD_SNAPSHOTS_FORMAT,public static final ChecksumBlobStoreFormat<org.elasticsearch.index.snapshots.blobstore.BlobStoreIndexShardSnapshot> INDEX_SHARD_SNAPSHOT_FORMAT,private static final Setting<org.elasticsearch.common.unit.ByteSizeValue> IO_BUFFER_SIZE_SETTING,private static final Logger LOGGER,public static final java.lang.String METADATA_NAME_FORMAT,public static final java.lang.String METADATA_PREFIX,private static final java.lang.String SNAPSHOT_CODEC,public static final ChecksumBlobStoreFormat<org.elasticsearch.snapshots.SnapshotInfo> SNAPSHOT_FORMAT,private static final java.lang.String SNAPSHOT_INDEX_NAME_FORMAT,private static final java.lang.String SNAPSHOT_INDEX_PREFIX,public static final java.lang.String SNAPSHOT_NAME_FORMAT,public static final java.lang.String SNAPSHOT_PREFIX,private static final java.lang.String TESTS_FILE,private static final java.lang.String UPLOADED_DATA_BLOB_PREFIX,private static final java.lang.String VIRTUAL_DATA_BLOB_PREFIX,private final non-sealed org.elasticsearch.common.blobstore.BlobPath basePath,private volatile boolean bestEffortConsistency,private final SetOnce<org.elasticsearch.common.blobstore.BlobContainer> blobContainer,private final SetOnce<org.elasticsearch.common.blobstore.BlobStore> blobStore,protected final non-sealed int bufferSize,private final non-sealed org.elasticsearch.cluster.service.ClusterService clusterService,private final non-sealed boolean compress,private final java.util.concurrent.atomic.AtomicLong latestKnownRepoGen,private final java.lang.Object lock,protected volatile org.elasticsearch.cluster.metadata.RepositoryMetadata metadata,private final non-sealed org.elasticsearch.common.xcontent.NamedXContentRegistry namedXContentRegistry,private final non-sealed boolean readOnly,private final non-sealed org.elasticsearch.indices.recovery.RecoverySettings recoverySettings,private final non-sealed RateLimiter restoreRateLimiter,private final org.elasticsearch.common.metrics.CounterMetric restoreRateLimitingTimeInNanos,private final non-sealed RateLimiter snapshotRateLimiter,private final org.elasticsearch.common.metrics.CounterMetric snapshotRateLimitingTimeInNanos,protected final non-sealed org.elasticsearch.threadpool.ThreadPool threadPool,private boolean uncleanStart