2021-07-19 10:16:43 +02:00
|
|
|
import Axios from "axios";
|
2021-07-19 15:57:50 +02:00
|
|
|
import ipaddr from "ipaddr.js";
|
|
|
|
import { Resolver } from "dns";
|
|
|
|
import { promisify } from "util";
|
|
|
|
import { LocalUrlError } from "./LocalUrlError";
|
|
|
|
import { ITiledMap } from "@workadventure/tiled-map-type-guard";
|
|
|
|
import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
2021-07-19 10:16:43 +02:00
|
|
|
|
|
|
|
class MapFetcher {
|
|
|
|
async fetchMap(mapUrl: string): Promise<ITiledMap> {
|
|
|
|
// Before trying to make the query, let's verify the map is actually on the open internet (and not a local test map)
|
|
|
|
|
|
|
|
if (await this.isLocalUrl(mapUrl)) {
|
2021-07-19 15:57:50 +02:00
|
|
|
throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map');
|
2021-07-19 10:16:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Note: mapUrl is provided by the client. A possible attack vector would be to use a rogue DNS server that
|
|
|
|
// returns local URLs. Alas, Axios cannot pin a URL to a given IP. So "isLocalUrl" and Axios.get could potentially
|
|
|
|
// target to different servers (and one could trick Axios.get into loading resources on the internal network
|
|
|
|
// despite isLocalUrl checking that.
|
|
|
|
// We can deem this problem not that important because:
|
|
|
|
// - We make sure we are only passing "GET" requests
|
|
|
|
// - The result of the query is never displayed to the end user
|
|
|
|
const res = await Axios.get(mapUrl, {
|
2021-07-19 15:57:50 +02:00
|
|
|
maxContentLength: 50 * 1024 * 1024, // Max content length: 50MB. Maps should not be bigger
|
2021-07-19 10:32:31 +02:00
|
|
|
timeout: 10000, // Timeout after 10 seconds
|
2021-07-19 10:16:43 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
if (!isTiledMap(res.data)) {
|
2021-07-19 15:57:50 +02:00
|
|
|
throw new Error("Invalid map format for map " + mapUrl);
|
2021-07-19 10:16:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return res.data;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the domain name is localhost of *.localhost
|
|
|
|
* Returns true if the domain name resolves to an IP address that is "private" (like 10.x.x.x or 192.168.x.x)
|
2021-07-21 18:42:20 +02:00
|
|
|
*
|
|
|
|
* @private
|
2021-07-19 10:16:43 +02:00
|
|
|
*/
|
2021-07-21 18:42:20 +02:00
|
|
|
async isLocalUrl(url: string): Promise<boolean> {
|
2021-07-19 10:16:43 +02:00
|
|
|
const urlObj = new URL(url);
|
2021-07-19 15:57:50 +02:00
|
|
|
if (urlObj.hostname === "localhost" || urlObj.hostname.endsWith(".localhost")) {
|
2021-07-19 10:16:43 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
let addresses = [];
|
|
|
|
if (!ipaddr.isValid(urlObj.hostname)) {
|
|
|
|
const resolver = new Resolver();
|
2021-07-21 18:42:20 +02:00
|
|
|
addresses = await promisify(resolver.resolve).bind(resolver)(urlObj.hostname);
|
2021-07-19 10:16:43 +02:00
|
|
|
} else {
|
|
|
|
addresses = [urlObj.hostname];
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const address of addresses) {
|
|
|
|
const addr = ipaddr.parse(address);
|
2021-07-19 15:57:50 +02:00
|
|
|
if (addr.range() !== "unicast") {
|
2021-07-19 10:16:43 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const mapFetcher = new MapFetcher();
|