diff --git a/CHANGELOG.md b/CHANGELOG.md index a234c3db..46258264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ _This release is scheduled to be released on 2022-04-01._ - Added test for new weather forecast absoluteDates porperty. - The modules get a class hidden added/removed if they get hidden/shown - Added new config option `showTitleAsUrl` to newsfeed module. If set, the diplayed title is a link to the article which is useful when running in a browser and you want to read this article. +- Added internal cors proxy to get weather providers working without public proxies (fixes #2714). The new url `http(s)://address:port/cors?url=https://whatever-to-proxy` can be used in other modules too. ### Updated diff --git a/js/server.js b/js/server.js index de07bfcc..7a298cdb 100644 --- a/js/server.js +++ b/js/server.js @@ -10,6 +10,7 @@ const path = require("path"); const ipfilter = require("express-ipfilter").IpFilter; const fs = require("fs"); const helmet = require("helmet"); +const fetch = require("node-fetch"); const Log = require("logger"); const Utils = require("./utils.js"); @@ -61,6 +62,7 @@ function Server(config, callback) { app.use(function (req, res, next) { ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) { if (err === undefined) { + res.header("Access-Control-Allow-Origin", "*"); return next(); } Log.log(err.message); @@ -76,6 +78,33 @@ function Server(config, callback) { app.use(directory, express.static(path.resolve(global.root_path + directory))); } + app.get("/cors", async function (req, res) { + // example: http://localhost:8080/cors?url=https://google.de + + try { + const reg = "^/cors.+url=(.*)"; + let url = ""; + + let match = new RegExp(reg, "g").exec(req.url); + if (!match) { + url = "invalid url: " + req.url; + Log.error(url); + res.send(url); + } else { + url = match[1]; + Log.log("cors url: " + url); + const response = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0 MagicMirror/" + global.version } }); + const header = response.headers.get("Content-Type"); + const data = await response.text(); + if (header) res.set("Content-Type", header); + res.send(data); + } + } catch (error) { + Log.error(error); + res.send(error); + } + }); + app.get("/version", function (req, res) { res.send(global.version); }); diff --git a/modules/default/weather/providers/darksky.js b/modules/default/weather/providers/darksky.js index 16355dd0..a248bc54 100755 --- a/modules/default/weather/providers/darksky.js +++ b/modules/default/weather/providers/darksky.js @@ -18,7 +18,8 @@ WeatherProvider.register("darksky", { // Set the default config properties that is specific to this provider defaults: { - apiBase: "https://cors-anywhere.herokuapp.com/https://api.darksky.net", + useCorsProxy: true, + apiBase: "https://api.darksky.net", weatherEndpoint: "/forecast", apiKey: "", lat: 0, diff --git a/modules/default/weather/providers/envcanada.js b/modules/default/weather/providers/envcanada.js index 5208e005..589cec6e 100644 --- a/modules/default/weather/providers/envcanada.js +++ b/modules/default/weather/providers/envcanada.js @@ -40,6 +40,7 @@ WeatherProvider.register("envcanada", { // Set the default config properties that is specific to this provider defaults: { + useCorsProxy: true, siteCode: "s1234567", provCode: "ON" }, @@ -73,7 +74,7 @@ WeatherProvider.register("envcanada", { // Override the fetchCurrentWeather method to query EC and construct a Current weather object // fetchCurrentWeather() { - this.fetchData(this.getUrl(), "GET") + this.fetchData(this.getUrl(), "GET", "xml") .then((data) => { if (!data) { // Did not receive usable new data. @@ -93,7 +94,7 @@ WeatherProvider.register("envcanada", { // Override the fetchWeatherForecast method to query EC and construct Forecast weather objects // fetchWeatherForecast() { - this.fetchData(this.getUrl(), "GET") + this.fetchData(this.getUrl(), "GET", "xml") .then((data) => { if (!data) { // Did not receive usable new data. @@ -113,7 +114,7 @@ WeatherProvider.register("envcanada", { // Override the fetchWeatherHourly method to query EC and construct Forecast weather objects // fetchWeatherHourly() { - this.fetchData(this.getUrl(), "GET") + this.fetchData(this.getUrl(), "GET", "xml") .then((data) => { if (!data) { // Did not receive usable new data. @@ -129,26 +130,6 @@ WeatherProvider.register("envcanada", { .finally(() => this.updateAvailable()); }, - // - // Override fetchData function to handle XML document (base function assumes JSON) - // - fetchData: function (url, method = "GET", data = null) { - return new Promise(function (resolve, reject) { - const request = new XMLHttpRequest(); - request.open(method, url, true); - request.onreadystatechange = function () { - if (this.readyState === 4) { - if (this.status === 200) { - resolve(this.responseXML); - } else { - reject(request); - } - } - }; - request.send(); - }); - }, - ////////////////////////////////////////////////////////////////////////////////// // // Environment Canada methods - not part of the standard Provider methods @@ -160,11 +141,8 @@ WeatherProvider.register("envcanada", { // URL defaults to the Englsih version simply because there is no language dependancy in the data // being accessed. This is only pertinent when using the EC data elements that contain a textual forecast. // - // Also note that access is supported through a proxy service (thingproxy.freeboard.io) to mitigate - // CORS errors when accessing EC - // getUrl() { - return "https://thingproxy.freeboard.io/fetch/https://dd.weather.gc.ca/citypage_weather/xml/" + this.config.provCode + "/" + this.config.siteCode + "_e.xml"; + return "https://dd.weather.gc.ca/citypage_weather/xml/" + this.config.provCode + "/" + this.config.siteCode + "_e.xml"; }, // diff --git a/modules/default/weather/weatherprovider.js b/modules/default/weather/weatherprovider.js index d08dc30f..d24623b8 100644 --- a/modules/default/weather/weatherprovider.js +++ b/modules/default/weather/weatherprovider.js @@ -111,8 +111,17 @@ const WeatherProvider = Class.extend({ this.delegate.updateAvailable(this); }, + getCorsUrl: function () { + if (this.config.mockData || typeof this.config.useCorsProxy === "undefined" || !this.config.useCorsProxy) { + return ""; + } else { + return location.protocol + "//" + location.host + "/cors?url="; + } + }, + // A convenience function to make requests. It returns a promise. - fetchData: function (url, method = "GET", data = null) { + fetchData: function (url, method = "GET", type = "json") { + url = this.getCorsUrl() + url; const getData = function (mockData) { return new Promise(function (resolve, reject) { if (mockData) { @@ -125,7 +134,11 @@ const WeatherProvider = Class.extend({ request.onreadystatechange = function () { if (this.readyState === 4) { if (this.status === 200) { - resolve(JSON.parse(this.response)); + if (type === "xml") { + resolve(this.responseXML); + } else { + resolve(JSON.parse(this.response)); + } } else { reject(request); } diff --git a/package.json b/package.json index 44a8d610..2dba7181 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ }, "jest": { "verbose": true, - "testTimeout": 15000, + "testTimeout": 20000, "testSequencer": "/tests/configs/test_sequencer.js", "projects": [ {