Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds user agent header for requests to Green Web Foundation APIs #184

Merged
merged 19 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,17 @@
"bug",
"code"
]
},
{
"login": "sfishel18",
"name": "Simon Fishel",
"avatar_url": "https://avatars.githubusercontent.com/u/294695?v=4",
"profile": "https://github.com/sfishel18",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"linkToUsage": false
}
}
3 changes: 3 additions & 0 deletions .esbuild.browser.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const esbuildCommon = require("./.esbuild.common");

require('esbuild').buildSync({
...esbuildCommon,
entryPoints: ['src/index.js'],
outdir: 'dist/iife',
globalName: 'co2',
Expand Down
7 changes: 7 additions & 0 deletions .esbuild.common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const CO2JS_VERSION = require("./package.json").version;

module.exports = {
define: {
"process.env.CO2JS_VERSION": JSON.stringify(CO2JS_VERSION),
},
};
4 changes: 3 additions & 1 deletion .esbuild.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ const esbuild = require('esbuild')
// For this build however we need to filter out some extra files
// that are used for nodejs, but not in browsers, so we use the
// library directly instead of using `esbuild-plugin-glob` as a plugin
const glob = require('tiny-glob');
const glob = require('tiny-glob')
const esbuildCommon = require('./.esbuild.common')

async function main() {
const results = await glob('src/**/!(*.test.js|test-constants.js|!(*.js))')
Expand All @@ -12,6 +13,7 @@ async function main() {
const justBrowserCompatibleFiles = results.filter(filepath => !filepath.endsWith('node.js'))

esbuild.build({
...esbuildCommon,
entryPoints: justBrowserCompatibleFiles,
bundle: false,
minify: false,
Expand Down
2 changes: 2 additions & 0 deletions .esbuild.node.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const { globPlugin } = require('esbuild-plugin-glob');
const esbuildCommon = require('./.esbuild.common');

function main() {
require('esbuild').build({
...esbuildCommon,
entryPoints: ['src/**/!(*.test.js|test-constants.js|!(*.js))'],
bundle: false,
minify: false,
Expand Down
28 changes: 28 additions & 0 deletions __mocks__/https.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getApiRequestHeaders } from "../src/helpers/index.js";
const https = jest.createMockFromModule("https");
import { Stream } from "stream";

const stream = new Stream();

https.get.mockImplementation((url, options, callback) => {
url, { headers: getApiRequestHeaders("TestRunner") }, callback(stream);
if (url.includes("greencheckmulti")) {
stream.emit(
"data",
Buffer.from(
`{"google.com": {"url":"google.com","hosted_by":"Google Inc.","hosted_by_website":"https://www.google.com","partner":null,"green":true}}`
)
);
} else {
stream.emit(
"data",
Buffer.from(
`{"url":"google.com","hosted_by":"Google Inc.","hosted_by_website":"https://www.google.com","partner":null,"green":true}`
)
);
}

stream.emit("end");
});

module.exports = https;
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"intensity-data:average": "node data/functions/generate_average_co2.js",
"intensity-data:marginal": "node data/functions/generate_marginal_co2.js",
"intensity-data": "npm run intensity-data:average && npm run intensity-data:marginal && npm run format-data",
"format-data": "cd data && prettier --write '**/*.{js,json}'"
"format-data": "cd data && prettier --write '**/*.{js,json}'",
"version": "npm run build"
},
"keywords": [
"sustainability",
Expand Down Expand Up @@ -75,4 +76,4 @@
"type": "git",
"url": "https://github.com/thegreenwebfoundation/co2.js.git"
}
}
}
12 changes: 11 additions & 1 deletion src/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,14 @@ function parseOptions(options) {
return adjustments;
}

export { formatNumber, parseOptions };
/**
* Returns an object containing all the HTTP headers to use when making a request to the Green Web Foundation API.
* @param {string} comment - Optional. The app, site, or organisation that is making the request.
*
* @returns {import('http').OutgoingHttpHeaders}
*/
function getApiRequestHeaders(comment = "") {
return { "User-Agent": `co2js/${process.env.CO2JS_VERSION} ${comment}` };
}

export { formatNumber, parseOptions, getApiRequestHeaders };
24 changes: 17 additions & 7 deletions src/hosting-api.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
"use strict";

import { getApiRequestHeaders } from "./helpers/index.js";

/**
* Check if a string or array of domains has been provided
* @param {string|array} domain - The domain to check, or an array of domains to be checked.
* @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request.
*/

function check(domain) {
function check(domain, userAgentIdentifier) {
// is it a single domain or an array of them?
if (typeof domain === "string") {
return checkAgainstAPI(domain);
return checkAgainstAPI(domain, userAgentIdentifier);
} else {
return checkDomainsAgainstAPI(domain);
return checkDomainsAgainstAPI(domain, userAgentIdentifier);
}
}

/**
* Check if a domain is hosted by a green web host by querying the Green Web Foundation API.
* @param {string} domain - The domain to check.
* @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request.
* @returns {boolean} - A boolean indicating whether the domain is hosted by a green web host.
*/
async function checkAgainstAPI(domain) {
async function checkAgainstAPI(domain, userAgentIdentifier) {
const req = await fetch(
`https://api.thegreenwebfoundation.org/greencheck/${domain}`
`https://api.thegreenwebfoundation.org/greencheck/${domain}`,
{
headers: getApiRequestHeaders(userAgentIdentifier),
}
);
const res = await req.json();
return res.green;
Expand All @@ -30,15 +37,18 @@ async function checkAgainstAPI(domain) {
/**
* Check if an array of domains is hosted by a green web host by querying the Green Web Foundation API.
* @param {array} domains - An array of domains to check.
* @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request.
* @returns {array} - An array of domains that are hosted by a green web host.
*/

async function checkDomainsAgainstAPI(domains) {
async function checkDomainsAgainstAPI(domains, userAgentIdentifier) {
try {
const apiPath = "https://api.thegreenwebfoundation.org/v2/greencheckmulti";
const domainsString = JSON.stringify(domains);

const req = await fetch(`${apiPath}/${domainsString}`);
const req = await fetch(`${apiPath}/${domainsString}`, {
headers: getApiRequestHeaders(userAgentIdentifier),
});

const allGreenCheckResults = await req.json();

Expand Down
80 changes: 58 additions & 22 deletions src/hosting-api.test.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,72 @@
"use strict";

import hosting from "./hosting-node.js";
import nock from "nock";
import hosting from "./hosting-api.js";
/* eslint-disable jest/no-disabled-tests */

process.env.CO2JS_VERSION = "1.2.34";
const requestHeaderComment = "TestRunner";

global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ green: true }),
})
);

describe("hostingAPI", () => {
beforeEach(() => {
fetch.mockClear();
});
describe("checking a single domain with #check", () => {
it.skip("using the API", async () => {
const scope = nock("https://api.thegreenwebfoundation.org/")
.get("/greencheck/google.com")
.reply(200, {
url: "google.com",
green: true,
});
it("using the API", async () => {
const res = await hosting.check("google.com");
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenLastCalledWith(
expect.any(String),
expect.objectContaining({
headers: { "User-Agent": "co2js/1.2.34 " },
})
);
expect(res).toEqual(true);
});
it("sets the correct user agent header", async () => {
const res = await hosting.check("google.com", requestHeaderComment);

expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenLastCalledWith(
expect.any(String),
expect.objectContaining({
headers: { "User-Agent": "co2js/1.2.34 TestRunner" },
})
);
expect(res).toEqual(true);
});
});
describe("implicitly checking multiple domains with #check", () => {
it.skip("using the API", async () => {
const scope = nock("https://api.thegreenwebfoundation.org/")
.get("/v2/greencheckmulti/[%22google.com%22,%22kochindustries.com%22]")
.reply(200, {
"google.com": {
url: "google.com",
green: true,
},
"kochindustries.com": {
url: "kochindustries.com",
green: null,
},
});
it("using the API", async () => {
fetch.mockImplementation(() =>
Promise.resolve({
json: () =>
Promise.resolve({
"google.com": { url: "google.com", green: true },
}),
})
);
const res = await hosting.check(["google.com", "kochindustries.com"]);
expect(fetch).toHaveBeenCalledTimes(1);
expect(res).toContain("google.com");
});
it("sets the correct user agent header", async () => {
const res = await hosting.check(
["google.com", "kochindustries.com"],
requestHeaderComment
);
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenLastCalledWith(
expect.any(String),
expect.objectContaining({
headers: { "User-Agent": "co2js/1.2.34 TestRunner" },
})
);
expect(res).toContain("google.com");
});
});
Expand Down
57 changes: 35 additions & 22 deletions src/hosting-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,39 @@ This lets us keep the total library small, and dependencies minimal.
import https from "https";

import hostingJSON from "./hosting-json.node.js";
import { getApiRequestHeaders } from "./helpers/index.js";

/**
* Accept a url and perform an http request, returning the body
* for parsing as JSON.
*
* @param {string} url
* @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request.
* @return {string}
*/
async function getBody(url) {
async function getBody(url, userAgentIdentifier) {
return new Promise(function (resolve, reject) {
// Do async job
const req = https.get(url, function (res) {
if (res.statusCode < 200 || res.statusCode >= 300) {
return reject(
new Error(
`Could not get info from: ${url}. Status Code: ${res.statusCode}`
)
);
}
const data = [];
const req = https.get(
url,
{ headers: getApiRequestHeaders(userAgentIdentifier) },
function (res) {
if (res.statusCode < 200 || res.statusCode >= 300) {
return reject(
new Error(
`Could not get info from: ${url}. Status Code: ${res.statusCode}`
)
);
}
const data = [];

res.on("data", (chunk) => {
data.push(chunk);
});
res.on("data", (chunk) => {
data.push(chunk);
});

res.on("end", () => resolve(Buffer.concat(data).toString()));
});
res.on("end", () => resolve(Buffer.concat(data).toString()));
}
);
req.end();
});
}
Expand All @@ -46,46 +52,53 @@ async function getBody(url) {
* Check if a domain is hosted by a green web host.
* @param {string|array} domain - The domain to check, or an array of domains to be checked.
* @param {object} db - Optional. A database object to use for lookups.
* @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request.
* @returns {boolean|array} - A boolean if a string was provided, or an array of booleans if an array of domains was provided.
*/

function check(domain, db) {
function check(domain, db, userAgentIdentifier) {
if (db) {
return hostingJSON.check(domain, db);
}

// is it a single domain or an array of them?
if (typeof domain === "string") {
return checkAgainstAPI(domain);
return checkAgainstAPI(domain, userAgentIdentifier);
} else {
return checkDomainsAgainstAPI(domain);
return checkDomainsAgainstAPI(domain, userAgentIdentifier);
}
}

/**
* Check if a domain is hosted by a green web host by querying the Green Web Foundation API.
* @param {string} domain - The domain to check.
* @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request.
* @returns {boolean} - A boolean indicating whether the domain is hosted by a green web host.
*/
async function checkAgainstAPI(domain) {
async function checkAgainstAPI(domain, userAgentIdentifier) {
const res = JSON.parse(
await getBody(`https://api.thegreenwebfoundation.org/greencheck/${domain}`)
await getBody(
`https://api.thegreenwebfoundation.org/greencheck/${domain}`,
userAgentIdentifier
)
);
return res.green;
}

/**
* Check if an array of domains is hosted by a green web host by querying the Green Web Foundation API.
* @param {array} domains - An array of domains to check.
* @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request.
* @returns {array} - An array of domains that are hosted by a green web host.
*/
async function checkDomainsAgainstAPI(domains) {
async function checkDomainsAgainstAPI(domains, userAgentIdentifier) {
try {
const allGreenCheckResults = JSON.parse(
await getBody(
`https://api.thegreenwebfoundation.org/v2/greencheckmulti/${JSON.stringify(
domains
)}`
)}`,
userAgentIdentifier
)
);
return hostingJSON.greenDomainsFromResults(allGreenCheckResults);
Expand Down
Loading
Loading