Spring cleanup: remove unused/unneded code (#761)
* cleanup: remove the archival package See https://github.com/ooni/probe/issues/2116 * cleanup: remove websteps fall 2021 edition See https://github.com/ooni/probe/issues/2116 * cleanup: remove JavaScript based testing framework https://github.com/ooni/probe/issues/2116 * cleanup: remove the unused ooapi package See https://github.com/ooni/probe/issues/2116
This commit is contained in:
parent
8b0815efab
commit
7a0a156aec
106
QA/index.mjs
106
QA/index.mjs
|
@ -1,106 +0,0 @@
|
||||||
// This script performs QA checks.
|
|
||||||
|
|
||||||
import { runTestCase } from "./lib/runner.mjs"
|
|
||||||
import { testCases as webTestCases } from "./lib/web.mjs"
|
|
||||||
|
|
||||||
// testCases lists all the test cases.
|
|
||||||
//
|
|
||||||
// A test case is an object with the structure described
|
|
||||||
// by the documentation of runTestCase in ./lib/runner.mjs.
|
|
||||||
const testCases = [
|
|
||||||
...webTestCases,
|
|
||||||
]
|
|
||||||
|
|
||||||
// checkForDuplicateTestCases ensures there are no duplicate names.
|
|
||||||
function checkForDuplicateTestCases() {
|
|
||||||
let dups = {}
|
|
||||||
for (let i = 0; i < testCases.length; i++) {
|
|
||||||
const testCase = testCases[i]
|
|
||||||
if (dups[testCase.name] !== undefined) {
|
|
||||||
console.log(`fatal: duplicate test case name: ${testCase.name}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
dups[testCase.name] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// runAllTestCases runs all the available test cases.
|
|
||||||
function runAllTestCases() {
|
|
||||||
let result = true
|
|
||||||
for (let i = 0; i < testCases.length; i++) {
|
|
||||||
result = result && runTestCase(testCases[i])
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeTestCasesMap creates a map from the test case name
|
|
||||||
// to the test case definition.
|
|
||||||
function makeTestCasesMap() {
|
|
||||||
var map = {}
|
|
||||||
for (let i = 0; i < testCases.length; i++) {
|
|
||||||
const testCase = testCases[i]
|
|
||||||
map[testCase.name] = testCase
|
|
||||||
}
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
|
|
||||||
// commandRun implements the run command.
|
|
||||||
function commandRun(args) {
|
|
||||||
const bailOnFailure = (result) => {
|
|
||||||
if (!result) {
|
|
||||||
console.log("some checks failed (see above logs)")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (args.length < 1) {
|
|
||||||
bailOnFailure(runAllTestCases())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let result = true
|
|
||||||
const map = makeTestCasesMap()
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
|
||||||
const arg = args[i]
|
|
||||||
const testCase = map[arg]
|
|
||||||
if (testCase === undefined) {
|
|
||||||
console.log(`unknown test case: ${arg}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
result = result && runTestCase(testCase)
|
|
||||||
}
|
|
||||||
bailOnFailure(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// commandList implements the list command.
|
|
||||||
function commandList() {
|
|
||||||
for (let i = 0; i < testCases.length; i++) {
|
|
||||||
const testCase = testCases[i]
|
|
||||||
console.log(`${testCase.name}: ${testCase.description}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// main is the main function.
|
|
||||||
function main() {
|
|
||||||
const usageAndExit = (exitcode) => {
|
|
||||||
console.log("usage: node ./QA/index.mjs list")
|
|
||||||
console.log("usage: node ./QA/index.mjs run [test_case_name...]")
|
|
||||||
process.exit(exitcode)
|
|
||||||
}
|
|
||||||
if (process.argv.length < 3) {
|
|
||||||
usageAndExit(0)
|
|
||||||
}
|
|
||||||
checkForDuplicateTestCases()
|
|
||||||
const command = process.argv[2]
|
|
||||||
switch (command) {
|
|
||||||
case "list":
|
|
||||||
commandList()
|
|
||||||
break
|
|
||||||
case "run":
|
|
||||||
commandRun(process.argv.slice(3))
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
console.log(`unknown command: ${command}`)
|
|
||||||
usageAndExit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
|
@ -1,196 +0,0 @@
|
||||||
// This file contains code for analysing results.
|
|
||||||
|
|
||||||
import { type } from "os"
|
|
||||||
|
|
||||||
// checkAnnotations checks whether we have annotations.
|
|
||||||
function checkAnnotations(report) {
|
|
||||||
let result = true
|
|
||||||
|
|
||||||
const isObject = typeof (report.annotations) === "object"
|
|
||||||
console.log(`checking whether annotations is an object... ${isObject}`)
|
|
||||||
result = result && isObject
|
|
||||||
|
|
||||||
const hasArchitecture = typeof (report.annotations.architecture) === "string"
|
|
||||||
console.log(`checking whether annotations contains architecture... ${hasArchitecture}`)
|
|
||||||
result = result && hasArchitecture
|
|
||||||
|
|
||||||
const hasEngineName = typeof (report.annotations.engine_name) === "string"
|
|
||||||
console.log(`checking whether annotations contains engine_name... ${hasEngineName}`)
|
|
||||||
result = result && hasEngineName
|
|
||||||
|
|
||||||
const hasEngineVersion = typeof (report.annotations.engine_version) === "string"
|
|
||||||
console.log(`checking whether annotations contains engine_version... ${hasEngineVersion}`)
|
|
||||||
result = result && hasEngineVersion
|
|
||||||
|
|
||||||
const hasPlatform = typeof (report.annotations.platform) === "string"
|
|
||||||
console.log(`checking whether annotations contains platform... ${hasPlatform}`)
|
|
||||||
result = result && hasPlatform
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkDataFormatVersion checks whether we have data_format_version.
|
|
||||||
function checkDataFormatVersion(report) {
|
|
||||||
const result = report.data_format_version === "0.2.0"
|
|
||||||
console.log(`checking whether we have the right data format version... ${result}`)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkExtensions ensures that extensions exists and has the right type.
|
|
||||||
function checkExtensions(report) {
|
|
||||||
const result = typeof (report.extensions) === "object"
|
|
||||||
console.log(`checking whether report.extensions is an object... ${result}`)
|
|
||||||
// Quirk: some experiments (e.g. web_connectivity) don't include
|
|
||||||
// an extensions object, and we don't want to fail here.
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkInput ensures that the input is correct.
|
|
||||||
function checkInput(testCase, report) {
|
|
||||||
const result = testCase.input === report.input
|
|
||||||
console.log(`checking whether input is correct... ${result}`)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkExperimentName ensures that the experimentName is correctly
|
|
||||||
// set in the output report file.
|
|
||||||
function checkExperimentName(name, report) {
|
|
||||||
const result = report.test_name === name
|
|
||||||
console.log(`checking whether the experiment name is correct... ${result}`)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkStartTime ensures that a given start time is in the correct format.
|
|
||||||
function checkStartTime(report, key) {
|
|
||||||
const value = report[key]
|
|
||||||
let result = typeof (value) === "string"
|
|
||||||
console.log(`checking whether ${key} is a string... ${result}`)
|
|
||||||
result = result && value.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/) !== null
|
|
||||||
console.log(`checking whether ${key} matches the date regexp... ${result}`)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkASN ensures that an ASN field is correct.
|
|
||||||
function checkASN(report, key) {
|
|
||||||
const value = report[key]
|
|
||||||
let result = typeof (value) === "string"
|
|
||||||
console.log(`checking whether ${key} is a string... ${result}`)
|
|
||||||
result = result && value.match(/^AS[0-9]+$/) !== null
|
|
||||||
console.log(`checking whether ${key} matches the ASN regexp... ${result}`)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkProbeCC ensures that the probe_cc field is correct.
|
|
||||||
function checkProbeCC(report) {
|
|
||||||
const value = report["probe_cc"]
|
|
||||||
let result = typeof (value) === "string"
|
|
||||||
console.log(`checking whether probe_cc is a string... ${result}`)
|
|
||||||
result = result && value.match(/^[A-Z]{2}$/) !== null
|
|
||||||
console.log(`checking whether probe_cc matches the CC regexp... ${result}`)
|
|
||||||
return result
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkProbeIP ensures that the probe_ip field is correct.
|
|
||||||
function checkProbeIP(report) {
|
|
||||||
const result = report.probe_ip === "127.0.0.1"
|
|
||||||
console.log(`checking whether probe_ip is correct... ${result}`)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkReportID ensures that the report_id field is correct.
|
|
||||||
function checkReportID(report) {
|
|
||||||
const result = report.report_id === "" // note: we don't submit
|
|
||||||
console.log(`checking whether report_id is correct... ${result}`)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkString ensures that a field is a string.
|
|
||||||
function checkString(report, key) {
|
|
||||||
const value = report[key]
|
|
||||||
const result = typeof(value) === "string"
|
|
||||||
console.log(`checking whether ${key} is a string... ${result}`)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkNetworkName ensures that an xxx_network_name field is correct.
|
|
||||||
function checkNetworkName(report, key) {
|
|
||||||
return checkString(report, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkResolverIP ensures that the resolver_ip field is correct.
|
|
||||||
function checkResolverIP(report) {
|
|
||||||
return checkString(report, "resolver_ip")
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkSoftwareName ensures that the software_name field is correct.
|
|
||||||
function checkSoftwareName(report) {
|
|
||||||
return checkString(report, "software_name")
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkSoftwareVersion ensures that the software_version field is correct.
|
|
||||||
function checkSoftwareVersion(report) {
|
|
||||||
return checkString(report, "software_version")
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkTestRuntime ensures that the test_runtime field is correct.
|
|
||||||
function checkTestRuntime(report) {
|
|
||||||
const result = typeof(report.test_runtime) === "number"
|
|
||||||
console.log(`checking whether test_runtime is a number... ${result}`)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkTestVersion ensures that the test_version field is correct.
|
|
||||||
function checkTestVersion(report) {
|
|
||||||
return checkString(report, "test_version")
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkTestKeys ensures that test_keys is an object.
|
|
||||||
function checkTestKeys(report) {
|
|
||||||
const result = typeof(report.test_keys) === "object"
|
|
||||||
console.log(`checking whether test_keys is an object... ${result}`)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkMeasurement is a function that invokes a standard set of checks
|
|
||||||
// to validate the top-level test keys of a result.
|
|
||||||
//
|
|
||||||
// This function helps to implement per-experiment checkers.
|
|
||||||
//
|
|
||||||
// Arguments:
|
|
||||||
//
|
|
||||||
// - testCase is the current test case
|
|
||||||
//
|
|
||||||
// - name is the name of the current experiment
|
|
||||||
//
|
|
||||||
// - report is the JSON measurement
|
|
||||||
//
|
|
||||||
// - extraChecks is the optional callback to perform extra checks
|
|
||||||
// that takes in input the report's test keys.
|
|
||||||
export function checkMeasurement(testCase, name, report, extraChecks) {
|
|
||||||
let result = true
|
|
||||||
result = result && checkAnnotations(report)
|
|
||||||
result = result && checkDataFormatVersion(report)
|
|
||||||
result = result && checkExtensions(report)
|
|
||||||
result = result && checkInput(testCase, report)
|
|
||||||
result = result && checkStartTime(report, "measurement_start_time")
|
|
||||||
result = result && checkASN(report, "probe_asn")
|
|
||||||
result = result && checkProbeCC(report)
|
|
||||||
result = result && checkProbeIP(report)
|
|
||||||
result = result && checkNetworkName(report, "probe_network_name")
|
|
||||||
result = result && checkReportID(report)
|
|
||||||
result = result && checkASN(report, "resolver_asn")
|
|
||||||
result = result && checkResolverIP(report)
|
|
||||||
result = result && checkNetworkName(report, "resolver_network_name")
|
|
||||||
result = result && checkSoftwareName(report)
|
|
||||||
result = result && checkSoftwareVersion(report)
|
|
||||||
result = result && checkExperimentName(name, report)
|
|
||||||
result = result && checkTestRuntime(report)
|
|
||||||
result = result && checkStartTime(report, "test_start_time")
|
|
||||||
result = result && checkTestVersion(report)
|
|
||||||
result = result && checkTestKeys(report)
|
|
||||||
if (typeof extraChecks === "function") {
|
|
||||||
result = result && extraChecks(report.test_keys)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
// This file contains helpers for describing blocking rules.
|
|
||||||
|
|
||||||
// hijackPopularDNSServers returns an object containing the rules
|
|
||||||
// for hijacking popular DNS servers with `miniooni --censor`.
|
|
||||||
export function hijackPopularDNSServers() {
|
|
||||||
return {
|
|
||||||
// cloudflare
|
|
||||||
"1.1.1.1:53/udp": "hijack-dns",
|
|
||||||
"1.0.0.1:53/udp": "hijack-dns",
|
|
||||||
// google
|
|
||||||
"8.8.8.8:53/udp": "hijack-dns",
|
|
||||||
"8.8.4.4:53/udp": "hijack-dns",
|
|
||||||
// quad9
|
|
||||||
"9.9.9.9:53/udp": "hijack-dns",
|
|
||||||
"9.9.9.10:53/udp": "hijack-dns",
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
// This file contains code for running test cases.
|
|
||||||
|
|
||||||
import child_process from "child_process"
|
|
||||||
import crypto from "crypto"
|
|
||||||
import fs from "fs"
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
// tempFile returns the name of a temporary file. This function is
|
|
||||||
// not as secure as using mktemp but it does not matter in this
|
|
||||||
// context. We just need to create file names in the local directory
|
|
||||||
// with enough entropy that every run has a different name.
|
|
||||||
//
|
|
||||||
// See https://stackoverflow.com/questions/7055061 for an insightful
|
|
||||||
// discussion about creating temporary files in node.
|
|
||||||
function tempFile(suffix) {
|
|
||||||
return path.join(`tmp-${crypto.randomBytes(16).toString('hex')}.${suffix}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// exec executes a command. This function throws on failure. The stdout
|
|
||||||
// and stderr should be console.log-ed once the command returns.
|
|
||||||
function exec(command) {
|
|
||||||
console.log(`+ ${command}`)
|
|
||||||
child_process.execSync(command)
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeCensorJsonFile writes a censor.json file using a file name
|
|
||||||
// containing random characters and returns the file name.
|
|
||||||
function writeCensorJsonFile(testCase) {
|
|
||||||
const fileName = tempFile("json")
|
|
||||||
fs.writeFileSync(fileName, JSON.stringify(testCase.blocking))
|
|
||||||
return fileName
|
|
||||||
}
|
|
||||||
|
|
||||||
// readReportFile reads and parses the report file, thus returning
|
|
||||||
// the JSON object contained inside the report file.
|
|
||||||
function readReportFile(reportFile) {
|
|
||||||
const data = fs.readFileSync(reportFile, { "encoding": "utf-8" })
|
|
||||||
return JSON.parse(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// runExperiment runs the given test case with the given experiment.
|
|
||||||
function runExperiment(testCase, experiment, checker) {
|
|
||||||
console.log(`## running: ${testCase.name}.${experiment}`)
|
|
||||||
console.log("")
|
|
||||||
const censorJson = writeCensorJsonFile(testCase)
|
|
||||||
const reportJson = tempFile("json")
|
|
||||||
// Note: using -n because we don't want to submit QA checks.
|
|
||||||
exec(`./miniooni -n --censor ${censorJson} -o ${reportJson} -i ${testCase.input} ${experiment}`)
|
|
||||||
console.log("")
|
|
||||||
// TODO(bassosimone): support multiple entries per file
|
|
||||||
const report = readReportFile(reportJson)
|
|
||||||
const analysisResult = checker(testCase, experiment, report)
|
|
||||||
console.log("")
|
|
||||||
console.log("")
|
|
||||||
if (analysisResult !== true && analysisResult !== false) {
|
|
||||||
console.log("the analysis function returned neither true nor false")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
return analysisResult
|
|
||||||
}
|
|
||||||
|
|
||||||
// recompileMiniooni recompiles miniooni if needed.
|
|
||||||
function recompileMiniooni() {
|
|
||||||
exec("go build -v ./internal/cmd/miniooni")
|
|
||||||
}
|
|
||||||
|
|
||||||
// runTestCase runs the given test case.
|
|
||||||
//
|
|
||||||
// A test case is an object with the following fields:
|
|
||||||
//
|
|
||||||
// - name (string): the name of the test case;
|
|
||||||
//
|
|
||||||
// - description (string): a description of the test case;
|
|
||||||
//
|
|
||||||
// - input (string): the input to pass to the experiment;
|
|
||||||
//
|
|
||||||
// - blocking (object): a blocking specification (i.e., the
|
|
||||||
// serialization of a filtering.TProxyConfig struct);
|
|
||||||
//
|
|
||||||
// - experiments (object): the keys are names of nettests
|
|
||||||
// to run and the values are functions taking as their
|
|
||||||
// unique argument the experiment's test_keys.
|
|
||||||
export function runTestCase(testCase) {
|
|
||||||
recompileMiniooni()
|
|
||||||
console.log("")
|
|
||||||
console.log(`# running: ${testCase.name}`)
|
|
||||||
let result = true
|
|
||||||
for (const [name, checker] of Object.entries(testCase.experiments)) {
|
|
||||||
result = result && runExperiment(testCase, name, checker)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
641
QA/lib/web.mjs
641
QA/lib/web.mjs
|
@ -1,641 +0,0 @@
|
||||||
// This file contains test cases for web nettests.
|
|
||||||
|
|
||||||
import { checkMeasurement } from "./analysis.mjs"
|
|
||||||
|
|
||||||
// webConnectivityCheckTopLevel checks the top-level keys
|
|
||||||
// of the web connectivity experiment agains a template
|
|
||||||
// object. Returns true if they match, false on mismatch.
|
|
||||||
function webConnectivityCheckTopLevel(tk, template) {
|
|
||||||
let result = true
|
|
||||||
for (const [key, value] of Object.entries(template)) {
|
|
||||||
const check = tk[key] === value
|
|
||||||
console.log(`checking whether ${key}'s value is ${value}... ${check}`)
|
|
||||||
result = result && check
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here we export all the test cases. Please, see the documentation
|
|
||||||
// of runner.runTestCase for a description of what is a test case.
|
|
||||||
export const testCases = [
|
|
||||||
|
|
||||||
//
|
|
||||||
// DNS checks
|
|
||||||
//
|
|
||||||
// We start with checks where _only_ the system resolver fails.
|
|
||||||
//
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_dns_system_nxdomain",
|
|
||||||
description: "the system resolver returns NXDOMAIN",
|
|
||||||
input: "https://nexa.polito.it/",
|
|
||||||
blocking: {
|
|
||||||
Domains: {
|
|
||||||
"nexa.polito.it": "nxdomain"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": "dns_nxdomain_error",
|
|
||||||
"dns_consistency": "inconsistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"body_length_match": null,
|
|
||||||
"body_proportion": 0,
|
|
||||||
"status_code_match": null,
|
|
||||||
"headers_match": null,
|
|
||||||
"title_match": null,
|
|
||||||
"accessible": false,
|
|
||||||
"blocking": "dns",
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_dns_system_refused",
|
|
||||||
description: "the system resolver returns REFUSED",
|
|
||||||
input: "https://nexa.polito.it/",
|
|
||||||
blocking: {
|
|
||||||
Domains: {
|
|
||||||
"nexa.polito.it": "refused"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": "dns_refused_error",
|
|
||||||
"dns_consistency": "inconsistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"body_length_match": null,
|
|
||||||
"body_proportion": 0,
|
|
||||||
"status_code_match": null,
|
|
||||||
"headers_match": null,
|
|
||||||
"title_match": null,
|
|
||||||
"accessible": null,
|
|
||||||
"blocking": null, // TODO(bassosimone): this is clearly a bug
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_dns_system_localhost",
|
|
||||||
description: "the system resolver returns localhost",
|
|
||||||
input: "https://nexa.polito.it/",
|
|
||||||
blocking: {
|
|
||||||
Domains: {
|
|
||||||
"nexa.polito.it": "localhost",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
// TODO(bassosimone): Web Connectivity does not correctly handle this case
|
|
||||||
// but, still, correctly sets blocking as "dns"
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": null,
|
|
||||||
"dns_consistency": "inconsistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"http_experiment_failure": "connection_refused",
|
|
||||||
"body_length_match": null,
|
|
||||||
"body_proportion": 0,
|
|
||||||
"status_code_match": null,
|
|
||||||
"headers_match": null,
|
|
||||||
"title_match": null,
|
|
||||||
"accessible": false,
|
|
||||||
"blocking": "dns",
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_dns_system_bogon_not_localhost",
|
|
||||||
description: "the system resolver returns a bogon that is not localhost",
|
|
||||||
input: "https://nexa.polito.it/",
|
|
||||||
blocking: {
|
|
||||||
DNSCache: {
|
|
||||||
"nexa.polito.it": ["10.0.0.1"],
|
|
||||||
},
|
|
||||||
Domains: {
|
|
||||||
"nexa.polito.it": "cache",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
// TODO(bassosimone): Web Connectivity does not correctly handle this case
|
|
||||||
// but, still, correctly sets blocking as "dns"
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": null,
|
|
||||||
"dns_consistency": "inconsistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"http_experiment_failure": "generic_timeout_error",
|
|
||||||
"body_length_match": null,
|
|
||||||
"body_proportion": 0,
|
|
||||||
"status_code_match": null,
|
|
||||||
"headers_match": null,
|
|
||||||
"title_match": null,
|
|
||||||
"accessible": false,
|
|
||||||
"blocking": "dns",
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_dns_system_no_answer",
|
|
||||||
description: "the system resolver returns an empty answer",
|
|
||||||
input: "https://nexa.polito.it/",
|
|
||||||
blocking: {
|
|
||||||
Domains: {
|
|
||||||
"nexa.polito.it": "no-answer",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": "dns_no_answer",
|
|
||||||
"dns_consistency": "inconsistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"http_experiment_failure": null,
|
|
||||||
"body_length_match": null,
|
|
||||||
"body_proportion": 0,
|
|
||||||
"status_code_match": null,
|
|
||||||
"headers_match": null,
|
|
||||||
"title_match": null,
|
|
||||||
"accessible": null,
|
|
||||||
"blocking": null, // TODO(bassosimone): this is clearly a bug
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_dns_system_timeout",
|
|
||||||
description: "the system resolver times out",
|
|
||||||
input: "https://nexa.polito.it/",
|
|
||||||
blocking: {
|
|
||||||
Domains: {
|
|
||||||
"nexa.polito.it": "timeout",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": "generic_timeout_error",
|
|
||||||
"dns_consistency": "inconsistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"http_experiment_failure": null,
|
|
||||||
"body_length_match": null,
|
|
||||||
"body_proportion": 0,
|
|
||||||
"status_code_match": null,
|
|
||||||
"headers_match": null,
|
|
||||||
"title_match": null,
|
|
||||||
"accessible": null,
|
|
||||||
"blocking": null, // TODO(bassosimone): this is clearly a bug
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO(bassosimone): here we should insert more checks where not only the system
|
|
||||||
// resolver is blocked but also other resolvers are.
|
|
||||||
|
|
||||||
//
|
|
||||||
// TCP connect
|
|
||||||
//
|
|
||||||
// This section contains TCP connect failures.
|
|
||||||
//
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_tcp_connect_timeout",
|
|
||||||
description: "timeout when connecting to the IP address",
|
|
||||||
input: "https://nexa.polito.it/",
|
|
||||||
blocking: {
|
|
||||||
Endpoints: {
|
|
||||||
"130.192.16.171:443/tcp": "tcp-drop-syn",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": null,
|
|
||||||
"dns_consistency": "consistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"http_experiment_failure": "generic_timeout_error",
|
|
||||||
"body_length_match": null,
|
|
||||||
"body_proportion": 0,
|
|
||||||
"status_code_match": null,
|
|
||||||
"headers_match": null,
|
|
||||||
"title_match": null,
|
|
||||||
"accessible": false,
|
|
||||||
"blocking": "tcp_ip",
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_tcp_connect_refused",
|
|
||||||
description: "connection refused when connecting to the IP address",
|
|
||||||
input: "https://nexa.polito.it/",
|
|
||||||
blocking: {
|
|
||||||
Endpoints: {
|
|
||||||
"130.192.16.171:443/tcp": "tcp-reject-syn",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": null,
|
|
||||||
"dns_consistency": "consistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"http_experiment_failure": "connection_refused",
|
|
||||||
"body_length_match": null,
|
|
||||||
"body_proportion": 0,
|
|
||||||
"status_code_match": null,
|
|
||||||
"headers_match": null,
|
|
||||||
"title_match": null,
|
|
||||||
"accessible": false,
|
|
||||||
"blocking": "tcp_ip",
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// TLS handshake
|
|
||||||
//
|
|
||||||
// This section contains TLS handshake failures.
|
|
||||||
//
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_tls_handshake_timeout",
|
|
||||||
description: "timeout when performing the TLS handshake",
|
|
||||||
input: "https://nexa.polito.it/",
|
|
||||||
blocking: {
|
|
||||||
Endpoints: {
|
|
||||||
"130.192.16.171:443/tcp": "drop-data",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": null,
|
|
||||||
"dns_consistency": "consistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"http_experiment_failure": "generic_timeout_error",
|
|
||||||
"body_length_match": null,
|
|
||||||
"body_proportion": 0,
|
|
||||||
"status_code_match": null,
|
|
||||||
"headers_match": null,
|
|
||||||
"title_match": null,
|
|
||||||
"accessible": false,
|
|
||||||
"blocking": "http-failure",
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_tls_handshake_reset",
|
|
||||||
description: "reset when performing the TLS handshake",
|
|
||||||
input: "https://nexa.polito.it/",
|
|
||||||
blocking: {
|
|
||||||
Endpoints: {
|
|
||||||
"130.192.16.171:443/tcp": "hijack-tls",
|
|
||||||
},
|
|
||||||
SNIs: {
|
|
||||||
"nexa.polito.it": "reset",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": null,
|
|
||||||
"dns_consistency": "consistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"http_experiment_failure": "connection_reset",
|
|
||||||
"body_length_match": null,
|
|
||||||
"body_proportion": 0,
|
|
||||||
"status_code_match": null,
|
|
||||||
"headers_match": null,
|
|
||||||
"title_match": null,
|
|
||||||
"accessible": false,
|
|
||||||
"blocking": "http-failure",
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// QUIC
|
|
||||||
//
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_quic_handshake_timeout",
|
|
||||||
description: "timeout when performing the QUIC handshake",
|
|
||||||
input: "https://dns.google/",
|
|
||||||
blocking: {
|
|
||||||
Endpoints: {
|
|
||||||
"8.8.8.8:443/udp": "drop-data",
|
|
||||||
"8.8.4.4:443/udp": "drop-data",
|
|
||||||
"[2001:4860:4860::8888]:443/udp": "drop-data",
|
|
||||||
"[2001:4860:4860::8844]:443/udp": "drop-data",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// Cleartext HTTP
|
|
||||||
//
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_http_reset",
|
|
||||||
description: "reset when performing the HTTP round trip",
|
|
||||||
input: "http://nexa.polito.it/",
|
|
||||||
blocking: {
|
|
||||||
Endpoints: {
|
|
||||||
"130.192.16.171:80/tcp": "hijack-http",
|
|
||||||
},
|
|
||||||
Hosts: {
|
|
||||||
"nexa.polito.it": "reset",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": null,
|
|
||||||
"dns_consistency": "consistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"http_experiment_failure": "connection_reset",
|
|
||||||
"body_length_match": null,
|
|
||||||
"body_proportion": 0,
|
|
||||||
"status_code_match": null,
|
|
||||||
"headers_match": null,
|
|
||||||
"title_match": null,
|
|
||||||
"accessible": false,
|
|
||||||
"blocking": "http-failure",
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_http_timeout",
|
|
||||||
description: "timeout when performing the HTTP round trip",
|
|
||||||
input: "http://nexa.polito.it/",
|
|
||||||
blocking: {
|
|
||||||
Endpoints: {
|
|
||||||
"130.192.16.171:80/tcp": "hijack-http",
|
|
||||||
},
|
|
||||||
Hosts: {
|
|
||||||
"nexa.polito.it": "timeout",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": null,
|
|
||||||
"dns_consistency": "consistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"http_experiment_failure": "generic_timeout_error",
|
|
||||||
"body_length_match": null,
|
|
||||||
"body_proportion": 0,
|
|
||||||
"status_code_match": null,
|
|
||||||
"headers_match": null,
|
|
||||||
"title_match": null,
|
|
||||||
"accessible": false,
|
|
||||||
"blocking": "http-failure",
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "web_http_451",
|
|
||||||
description: "451 when performing the HTTP round trip",
|
|
||||||
input: "http://nexa.polito.it/",
|
|
||||||
blocking: {
|
|
||||||
Endpoints: {
|
|
||||||
"130.192.16.171:80/tcp": "hijack-http",
|
|
||||||
},
|
|
||||||
Hosts: {
|
|
||||||
"nexa.polito.it": "451",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
// TODO(bassosimone): there is no easy way to check for the body
|
|
||||||
// proportion robustly because it's a float.
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": null,
|
|
||||||
"dns_consistency": "consistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"http_experiment_failure": null,
|
|
||||||
"body_length_match": false,
|
|
||||||
"status_code_match": false,
|
|
||||||
"headers_match": false,
|
|
||||||
"title_match": false,
|
|
||||||
"accessible": false,
|
|
||||||
"blocking": "http-diff",
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// More complex scenarios
|
|
||||||
//
|
|
||||||
|
|
||||||
// In this scenario the second IP address for the domain fails
|
|
||||||
// with reset. Web Connectivity sees that but overall says it's
|
|
||||||
// all good because the good IP happens to be the first. We'll
|
|
||||||
// see what changes if we swap the IPs in the next scenario.
|
|
||||||
{
|
|
||||||
name: "web_tcp_second_ip_connection_reset",
|
|
||||||
description: "the second IP returned by DNS fails with connection reset",
|
|
||||||
input: "https://dns.google/",
|
|
||||||
blocking: {
|
|
||||||
DNSCache: {
|
|
||||||
"dns.google": ["8.8.4.4", "8.8.8.8"],
|
|
||||||
},
|
|
||||||
Domains: {
|
|
||||||
"dns.google": "cache",
|
|
||||||
},
|
|
||||||
Endpoints: {
|
|
||||||
"8.8.8.8:443/tcp": "hijack-tls",
|
|
||||||
},
|
|
||||||
SNIs: {
|
|
||||||
"dns.google": "reset",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": null,
|
|
||||||
"dns_consistency": "consistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"http_experiment_failure": null,
|
|
||||||
"body_length_match": true,
|
|
||||||
"body_proportion": 1,
|
|
||||||
"status_code_match": true,
|
|
||||||
"headers_match": true,
|
|
||||||
"title_match": true,
|
|
||||||
"accessible": true,
|
|
||||||
"blocking": false,
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// This scenario is like the previous one except that we swap
|
|
||||||
// the IP addresses and now Web Connectivity says failure.
|
|
||||||
{
|
|
||||||
name: "web_tcp_first_ip_connection_reset",
|
|
||||||
description: "the first IP returned by DNS fails with connection reset",
|
|
||||||
input: "https://dns.google/",
|
|
||||||
blocking: {
|
|
||||||
DNSCache: {
|
|
||||||
"dns.google": ["8.8.4.4", "8.8.8.8"],
|
|
||||||
},
|
|
||||||
Domains: {
|
|
||||||
"dns.google": "cache",
|
|
||||||
},
|
|
||||||
Endpoints: {
|
|
||||||
"8.8.4.4:443/tcp": "hijack-tls",
|
|
||||||
},
|
|
||||||
SNIs: {
|
|
||||||
"dns.google": "reset",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
websteps: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report)
|
|
||||||
},
|
|
||||||
web_connectivity: (testCase, name, report) => {
|
|
||||||
return checkMeasurement(testCase, name, report, (tk) => {
|
|
||||||
let result = true
|
|
||||||
result = result && webConnectivityCheckTopLevel(tk, {
|
|
||||||
"dns_experiment_failure": null,
|
|
||||||
"dns_consistency": "consistent",
|
|
||||||
"control_failure": null,
|
|
||||||
"http_experiment_failure": "connection_reset",
|
|
||||||
"body_length_match": null,
|
|
||||||
"body_proportion": 0,
|
|
||||||
"status_code_match": null,
|
|
||||||
"headers_match": null,
|
|
||||||
"title_match": null,
|
|
||||||
"accessible": false,
|
|
||||||
"blocking": "http-failure",
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
2
go.mod
2
go.mod
|
@ -20,7 +20,6 @@ require (
|
||||||
github.com/hexops/gotextdiff v1.0.3
|
github.com/hexops/gotextdiff v1.0.3
|
||||||
github.com/iancoleman/strcase v0.2.0
|
github.com/iancoleman/strcase v0.2.0
|
||||||
github.com/lucas-clemente/quic-go v0.27.0
|
github.com/lucas-clemente/quic-go v0.27.0
|
||||||
github.com/marten-seemann/qtls-go1-18 v0.1.1
|
|
||||||
github.com/mattn/go-colorable v0.1.12
|
github.com/mattn/go-colorable v0.1.12
|
||||||
github.com/miekg/dns v1.1.49
|
github.com/miekg/dns v1.1.49
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1
|
github.com/mitchellh/go-wordwrap v1.0.1
|
||||||
|
@ -73,6 +72,7 @@ require (
|
||||||
github.com/marten-seemann/qpack v0.2.1 // indirect
|
github.com/marten-seemann/qpack v0.2.1 // indirect
|
||||||
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
|
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
|
||||||
github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect
|
github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect
|
||||||
|
github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.13 // indirect
|
github.com/mattn/go-sqlite3 v1.14.13 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
//
|
|
||||||
// Saves dial and net.Conn events
|
|
||||||
//
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NetworkEvent contains a network event. This kind of events
|
|
||||||
// are generated by Dialer, QUICDialer, Conn, QUICConn.
|
|
||||||
type NetworkEvent struct {
|
|
||||||
Count int
|
|
||||||
Failure error
|
|
||||||
Finished time.Time
|
|
||||||
Network string
|
|
||||||
Operation string
|
|
||||||
RemoteAddr string
|
|
||||||
Started time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialContext dials with the given dialer with the given arguments
|
|
||||||
// and stores the dial result inside of this saver.
|
|
||||||
func (s *Saver) DialContext(ctx context.Context,
|
|
||||||
dialer model.Dialer, network, address string) (net.Conn, error) {
|
|
||||||
started := time.Now()
|
|
||||||
conn, err := dialer.DialContext(ctx, network, address)
|
|
||||||
s.appendNetworkEvent(&NetworkEvent{
|
|
||||||
Count: 0,
|
|
||||||
Failure: err,
|
|
||||||
Finished: time.Now(),
|
|
||||||
Network: network,
|
|
||||||
Operation: netxlite.ConnectOperation,
|
|
||||||
RemoteAddr: address,
|
|
||||||
Started: started,
|
|
||||||
})
|
|
||||||
return conn, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads from the given conn and stores the results in the saver.
|
|
||||||
func (s *Saver) Read(conn net.Conn, buf []byte) (int, error) {
|
|
||||||
network := conn.RemoteAddr().Network()
|
|
||||||
remoteAddr := conn.RemoteAddr().String()
|
|
||||||
started := time.Now()
|
|
||||||
count, err := conn.Read(buf)
|
|
||||||
s.appendNetworkEvent(&NetworkEvent{
|
|
||||||
Count: count,
|
|
||||||
Failure: err,
|
|
||||||
Finished: time.Now(),
|
|
||||||
Network: network,
|
|
||||||
Operation: netxlite.ReadOperation,
|
|
||||||
RemoteAddr: remoteAddr,
|
|
||||||
Started: started,
|
|
||||||
})
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes to the given conn and stores the results into the saver.
|
|
||||||
func (s *Saver) Write(conn net.Conn, buf []byte) (int, error) {
|
|
||||||
network := conn.RemoteAddr().Network()
|
|
||||||
remoteAddr := conn.RemoteAddr().String()
|
|
||||||
started := time.Now()
|
|
||||||
count, err := conn.Write(buf)
|
|
||||||
s.appendNetworkEvent(&NetworkEvent{
|
|
||||||
Count: count,
|
|
||||||
Failure: err,
|
|
||||||
Finished: time.Now(),
|
|
||||||
Network: network,
|
|
||||||
Operation: netxlite.WriteOperation,
|
|
||||||
RemoteAddr: remoteAddr,
|
|
||||||
Started: started,
|
|
||||||
})
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Saver) appendNetworkEvent(ev *NetworkEvent) {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.trace.Network = append(s.trace.Network, ev)
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
|
@ -1,281 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSaverDialContext(t *testing.T) {
|
|
||||||
// newConn creates a new connection with the desired properties.
|
|
||||||
newConn := func(address string) net.Conn {
|
|
||||||
return &mocks.Conn{
|
|
||||||
MockRemoteAddr: func() net.Addr {
|
|
||||||
return &mocks.Addr{
|
|
||||||
MockString: func() string {
|
|
||||||
return address
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MockClose: func() error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// newDialer creates a dialer for testing.
|
|
||||||
newDialer := func(conn net.Conn, err error) model.Dialer {
|
|
||||||
return &mocks.Dialer{
|
|
||||||
MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
time.Sleep(1 * time.Microsecond)
|
|
||||||
return conn, err
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("on success", func(t *testing.T) {
|
|
||||||
const mockedEndpoint = "8.8.4.4:443"
|
|
||||||
dialer := newDialer(newConn(mockedEndpoint), nil)
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleNetworkEventValidator{
|
|
||||||
ExpectedCount: 0,
|
|
||||||
ExpectedErr: nil,
|
|
||||||
ExpectedNetwork: "tcp",
|
|
||||||
ExpectedOp: netxlite.ConnectOperation,
|
|
||||||
ExpectedEpnt: mockedEndpoint,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
conn, err := saver.DialContext(ctx, dialer, "tcp", mockedEndpoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if conn == nil {
|
|
||||||
t.Fatal("expected non-nil conn")
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on failure", func(t *testing.T) {
|
|
||||||
const mockedEndpoint = "8.8.4.4:443"
|
|
||||||
mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF)
|
|
||||||
dialer := newDialer(nil, mockedError)
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleNetworkEventValidator{
|
|
||||||
ExpectedCount: 0,
|
|
||||||
ExpectedErr: mockedError,
|
|
||||||
ExpectedNetwork: "tcp",
|
|
||||||
ExpectedOp: netxlite.ConnectOperation,
|
|
||||||
ExpectedEpnt: mockedEndpoint,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
conn, err := saver.DialContext(ctx, dialer, "tcp", mockedEndpoint)
|
|
||||||
if !errors.Is(err, mockedError) {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if conn != nil {
|
|
||||||
t.Fatal("expected nil conn")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaverRead(t *testing.T) {
|
|
||||||
// newConn is a helper function for creating a new connection.
|
|
||||||
newConn := func(endpoint string, numBytes int, err error) net.Conn {
|
|
||||||
return &mocks.Conn{
|
|
||||||
MockRead: func(b []byte) (int, error) {
|
|
||||||
time.Sleep(time.Microsecond)
|
|
||||||
return numBytes, err
|
|
||||||
},
|
|
||||||
MockRemoteAddr: func() net.Addr {
|
|
||||||
return &mocks.Addr{
|
|
||||||
MockString: func() string {
|
|
||||||
return endpoint
|
|
||||||
},
|
|
||||||
MockNetwork: func() string {
|
|
||||||
return "tcp"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("on success", func(t *testing.T) {
|
|
||||||
const mockedEndpoint = "8.8.4.4:443"
|
|
||||||
const mockedNumBytes = 128
|
|
||||||
conn := newConn(mockedEndpoint, mockedNumBytes, nil)
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleNetworkEventValidator{
|
|
||||||
ExpectedCount: mockedNumBytes,
|
|
||||||
ExpectedErr: nil,
|
|
||||||
ExpectedNetwork: "tcp",
|
|
||||||
ExpectedOp: netxlite.ReadOperation,
|
|
||||||
ExpectedEpnt: mockedEndpoint,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
count, err := saver.Read(conn, buf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if count != mockedNumBytes {
|
|
||||||
t.Fatal("unexpected count")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on failure", func(t *testing.T) {
|
|
||||||
const mockedEndpoint = "8.8.4.4:443"
|
|
||||||
mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF)
|
|
||||||
conn := newConn(mockedEndpoint, 0, mockedError)
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleNetworkEventValidator{
|
|
||||||
ExpectedCount: 0,
|
|
||||||
ExpectedErr: mockedError,
|
|
||||||
ExpectedNetwork: "tcp",
|
|
||||||
ExpectedOp: netxlite.ReadOperation,
|
|
||||||
ExpectedEpnt: mockedEndpoint,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
count, err := saver.Read(conn, buf)
|
|
||||||
if !errors.Is(err, mockedError) {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if count != 0 {
|
|
||||||
t.Fatal("unexpected count")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaverWrite(t *testing.T) {
|
|
||||||
// newConn is a helper function for creating a new connection.
|
|
||||||
newConn := func(endpoint string, numBytes int, err error) net.Conn {
|
|
||||||
return &mocks.Conn{
|
|
||||||
MockWrite: func(b []byte) (int, error) {
|
|
||||||
time.Sleep(time.Microsecond)
|
|
||||||
return numBytes, err
|
|
||||||
},
|
|
||||||
MockRemoteAddr: func() net.Addr {
|
|
||||||
return &mocks.Addr{
|
|
||||||
MockString: func() string {
|
|
||||||
return endpoint
|
|
||||||
},
|
|
||||||
MockNetwork: func() string {
|
|
||||||
return "tcp"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("on success", func(t *testing.T) {
|
|
||||||
const mockedEndpoint = "8.8.4.4:443"
|
|
||||||
const mockedNumBytes = 128
|
|
||||||
conn := newConn(mockedEndpoint, mockedNumBytes, nil)
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleNetworkEventValidator{
|
|
||||||
ExpectedCount: mockedNumBytes,
|
|
||||||
ExpectedErr: nil,
|
|
||||||
ExpectedNetwork: "tcp",
|
|
||||||
ExpectedOp: netxlite.WriteOperation,
|
|
||||||
ExpectedEpnt: mockedEndpoint,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
count, err := saver.Write(conn, buf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if count != mockedNumBytes {
|
|
||||||
t.Fatal("unexpected count")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on failure", func(t *testing.T) {
|
|
||||||
const mockedEndpoint = "8.8.4.4:443"
|
|
||||||
mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF)
|
|
||||||
conn := newConn(mockedEndpoint, 0, mockedError)
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleNetworkEventValidator{
|
|
||||||
ExpectedCount: 0,
|
|
||||||
ExpectedErr: mockedError,
|
|
||||||
ExpectedNetwork: "tcp",
|
|
||||||
ExpectedOp: netxlite.WriteOperation,
|
|
||||||
ExpectedEpnt: mockedEndpoint,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
count, err := saver.Write(conn, buf)
|
|
||||||
if !errors.Is(err, mockedError) {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if count != 0 {
|
|
||||||
t.Fatal("unexpected count")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SingleNetworkEventValidator expects to find a single
|
|
||||||
// network event inside of the saver and ensures that such
|
|
||||||
// an event contains the required field values.
|
|
||||||
type SingleNetworkEventValidator struct {
|
|
||||||
ExpectedCount int
|
|
||||||
ExpectedErr error
|
|
||||||
ExpectedNetwork string
|
|
||||||
ExpectedOp string
|
|
||||||
ExpectedEpnt string
|
|
||||||
Saver *Saver
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *SingleNetworkEventValidator) Validate() error {
|
|
||||||
trace := v.Saver.MoveOutTrace()
|
|
||||||
if len(trace.Network) != 1 {
|
|
||||||
return errors.New("expected to see a single .Network event")
|
|
||||||
}
|
|
||||||
entry := trace.Network[0]
|
|
||||||
if entry.Count != v.ExpectedCount {
|
|
||||||
return errors.New("expected to see a different .Count")
|
|
||||||
}
|
|
||||||
if !errors.Is(entry.Failure, v.ExpectedErr) {
|
|
||||||
return errors.New("unexpected .Failure")
|
|
||||||
}
|
|
||||||
if !entry.Finished.After(entry.Started) {
|
|
||||||
return errors.New(".Finished should be after .Started")
|
|
||||||
}
|
|
||||||
if entry.Network != v.ExpectedNetwork {
|
|
||||||
return errors.New("invalid value for .Network")
|
|
||||||
}
|
|
||||||
if entry.Operation != v.ExpectedOp {
|
|
||||||
return errors.New("invalid value for .Operation")
|
|
||||||
}
|
|
||||||
if entry.RemoteAddr != v.ExpectedEpnt {
|
|
||||||
return errors.New("unexpected value for .RemoteAddr")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
// Package implements a Saver type that saves network, TCP, DNS,
|
|
||||||
// and TLS events. Given a Saver, you can export a Trace. Given a
|
|
||||||
// Trace you can obtain data in the OONI archival data format.
|
|
||||||
package archival
|
|
|
@ -1,107 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
//
|
|
||||||
// Saves HTTP events
|
|
||||||
//
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTTPRoundTripEvent contains an HTTP round trip.
|
|
||||||
type HTTPRoundTripEvent struct {
|
|
||||||
Failure error
|
|
||||||
Finished time.Time
|
|
||||||
Method string
|
|
||||||
RequestHeaders http.Header
|
|
||||||
ResponseBody []byte
|
|
||||||
ResponseBodyIsTruncated bool
|
|
||||||
ResponseBodyLength int64
|
|
||||||
ResponseHeaders http.Header
|
|
||||||
Started time.Time
|
|
||||||
StatusCode int64
|
|
||||||
Transport string
|
|
||||||
URL string
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPRoundTrip performs the round trip with the given transport and
|
|
||||||
// the given arguments and saves the results into the saver.
|
|
||||||
//
|
|
||||||
// The maxBodySnapshotSize argument controls the maximum size of the
|
|
||||||
// body snapshot that we collect along with the HTTP round trip.
|
|
||||||
func (s *Saver) HTTPRoundTrip(
|
|
||||||
txp model.HTTPTransport, maxBodySnapshotSize int64,
|
|
||||||
req *http.Request) (*http.Response, error) {
|
|
||||||
started := time.Now()
|
|
||||||
resp, err := txp.RoundTrip(req)
|
|
||||||
rt := &HTTPRoundTripEvent{
|
|
||||||
Failure: nil, // set later
|
|
||||||
Finished: time.Time{}, // set later
|
|
||||||
Method: req.Method,
|
|
||||||
RequestHeaders: s.cloneRequestHeaders(req),
|
|
||||||
ResponseBody: nil, // set later
|
|
||||||
ResponseBodyIsTruncated: false,
|
|
||||||
ResponseBodyLength: 0,
|
|
||||||
ResponseHeaders: nil, // set later
|
|
||||||
Started: started,
|
|
||||||
StatusCode: 0, // set later
|
|
||||||
Transport: txp.Network(),
|
|
||||||
URL: req.URL.String(),
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
rt.Finished = time.Now()
|
|
||||||
rt.Failure = err
|
|
||||||
s.appendHTTPRoundTripEvent(rt)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rt.StatusCode = int64(resp.StatusCode)
|
|
||||||
rt.ResponseHeaders = resp.Header.Clone()
|
|
||||||
r := io.LimitReader(resp.Body, maxBodySnapshotSize)
|
|
||||||
body, err := netxlite.ReadAllContext(req.Context(), r)
|
|
||||||
if err != nil {
|
|
||||||
rt.Finished = time.Now()
|
|
||||||
rt.Failure = err
|
|
||||||
s.appendHTTPRoundTripEvent(rt)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Body = &archivalHTTPTransportBody{ // allow for reading again the whole body
|
|
||||||
Reader: io.MultiReader(bytes.NewReader(body), resp.Body),
|
|
||||||
Closer: resp.Body,
|
|
||||||
}
|
|
||||||
rt.ResponseBody = body
|
|
||||||
rt.ResponseBodyLength = int64(len(body))
|
|
||||||
rt.ResponseBodyIsTruncated = int64(len(body)) >= maxBodySnapshotSize
|
|
||||||
rt.Finished = time.Now()
|
|
||||||
s.appendHTTPRoundTripEvent(rt)
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// cloneRequestHeaders ensure we include the Host header among the saved
|
|
||||||
// headers, which is what OONI should do, even though the Go transport is
|
|
||||||
// such that this header is added later when we're sending the request.
|
|
||||||
func (s *Saver) cloneRequestHeaders(req *http.Request) http.Header {
|
|
||||||
header := req.Header.Clone()
|
|
||||||
if req.Host != "" {
|
|
||||||
header.Set("Host", req.Host)
|
|
||||||
} else {
|
|
||||||
header.Set("Host", req.URL.Host)
|
|
||||||
}
|
|
||||||
return header
|
|
||||||
}
|
|
||||||
|
|
||||||
type archivalHTTPTransportBody struct {
|
|
||||||
io.Reader
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Saver) appendHTTPRoundTripEvent(ev *HTTPRoundTripEvent) {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.trace.HTTPRoundTrip = append(s.trace.HTTPRoundTrip, ev)
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
|
@ -1,354 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSaverHTTPRoundTrip(t *testing.T) {
|
|
||||||
// newHTTPTransport creates a new HTTP transport for testing.
|
|
||||||
newHTTPTransport := func(resp *http.Response, err error) model.HTTPTransport {
|
|
||||||
return &mocks.HTTPTransport{
|
|
||||||
MockRoundTrip: func(req *http.Request) (*http.Response, error) {
|
|
||||||
return resp, err
|
|
||||||
},
|
|
||||||
MockNetwork: func() string {
|
|
||||||
return "tcp"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// successFlowWithBody is a successful test case with possible body truncation.
|
|
||||||
successFlowWithBody := func(realBody []byte, maxBodySize int64) error {
|
|
||||||
// truncate the expected body if required
|
|
||||||
expectedBody := realBody
|
|
||||||
truncated := false
|
|
||||||
if int64(len(realBody)) > maxBodySize {
|
|
||||||
expectedBody = realBody[:maxBodySize]
|
|
||||||
truncated = true
|
|
||||||
}
|
|
||||||
// construct the saver and the validator
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleHTTPRoundTripValidator{
|
|
||||||
ExpectFailure: nil,
|
|
||||||
ExpectMethod: "GET",
|
|
||||||
ExpectRequestHeaders: map[string][]string{
|
|
||||||
"Host": {"127.0.0.1:8080"},
|
|
||||||
"User-Agent": {"antani/1.0"},
|
|
||||||
"X-Client-IP": {"130.192.91.211"},
|
|
||||||
},
|
|
||||||
ExpectResponseBody: expectedBody,
|
|
||||||
ExpectResponseBodyIsTruncated: truncated,
|
|
||||||
ExpectResponseBodyLength: int64(len(expectedBody)),
|
|
||||||
ExpectResponseHeaders: map[string][]string{
|
|
||||||
"Server": {"antani/1.0"},
|
|
||||||
"Content-Type": {"text/plain"},
|
|
||||||
},
|
|
||||||
ExpectStatusCode: 200,
|
|
||||||
ExpectTransport: "tcp",
|
|
||||||
ExpectURL: "http://127.0.0.1:8080/antani",
|
|
||||||
RealResponseBody: realBody,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
// construct transport and perform the HTTP round trip
|
|
||||||
txp := newHTTPTransport(v.NewHTTPResponse(), nil)
|
|
||||||
resp, err := saver.HTTPRoundTrip(txp, maxBodySize, v.NewHTTPRequest())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
return errors.New("expected non-nil resp")
|
|
||||||
}
|
|
||||||
// ensure that we can still read the _full_ response body
|
|
||||||
ctx := context.Background()
|
|
||||||
data, err := netxlite.ReadAllContext(ctx, resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(realBody, data); diff != "" {
|
|
||||||
return errors.New(diff)
|
|
||||||
}
|
|
||||||
// validate the content of the trace
|
|
||||||
return v.Validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("on success without truncation", func(t *testing.T) {
|
|
||||||
realBody := []byte("0xdeadbeef")
|
|
||||||
const maxBodySize = 1 << 20
|
|
||||||
err := successFlowWithBody(realBody, maxBodySize)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on success with truncation", func(t *testing.T) {
|
|
||||||
realBody := []byte("0xdeadbeef")
|
|
||||||
const maxBodySize = 4
|
|
||||||
err := successFlowWithBody(realBody, maxBodySize)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on failure during round trip", func(t *testing.T) {
|
|
||||||
expectedError := netxlite.NewTopLevelGenericErrWrapper(netxlite.ECONNRESET)
|
|
||||||
const maxBodySize = 1 << 20
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleHTTPRoundTripValidator{
|
|
||||||
ExpectFailure: expectedError,
|
|
||||||
ExpectMethod: "GET",
|
|
||||||
ExpectRequestHeaders: map[string][]string{
|
|
||||||
"Host": {"127.0.0.1:8080"},
|
|
||||||
"User-Agent": {"antani/1.0"},
|
|
||||||
"X-Client-IP": {"130.192.91.211"},
|
|
||||||
},
|
|
||||||
ExpectResponseBody: nil,
|
|
||||||
ExpectResponseBodyIsTruncated: false,
|
|
||||||
ExpectResponseBodyLength: 0,
|
|
||||||
ExpectResponseHeaders: nil,
|
|
||||||
ExpectStatusCode: 0,
|
|
||||||
ExpectTransport: "tcp",
|
|
||||||
ExpectURL: "http://127.0.0.1:8080/antani",
|
|
||||||
RealResponseBody: nil,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
txp := newHTTPTransport(nil, expectedError)
|
|
||||||
resp, err := saver.HTTPRoundTrip(txp, maxBodySize, v.NewHTTPRequest())
|
|
||||||
if !errors.Is(err, expectedError) {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
t.Fatal("expected nil resp")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on failure reading body", func(t *testing.T) {
|
|
||||||
expectedError := netxlite.NewTopLevelGenericErrWrapper(netxlite.ECONNRESET)
|
|
||||||
const maxBodySize = 1 << 20
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleHTTPRoundTripValidator{
|
|
||||||
ExpectFailure: expectedError,
|
|
||||||
ExpectMethod: "GET",
|
|
||||||
ExpectRequestHeaders: map[string][]string{
|
|
||||||
"Host": {"127.0.0.1:8080"},
|
|
||||||
"User-Agent": {"antani/1.0"},
|
|
||||||
"X-Client-IP": {"130.192.91.211"},
|
|
||||||
},
|
|
||||||
ExpectResponseBody: nil,
|
|
||||||
ExpectResponseBodyIsTruncated: false,
|
|
||||||
ExpectResponseBodyLength: 0,
|
|
||||||
ExpectResponseHeaders: map[string][]string{
|
|
||||||
"Server": {"antani/1.0"},
|
|
||||||
"Content-Type": {"text/plain"},
|
|
||||||
},
|
|
||||||
ExpectStatusCode: 200,
|
|
||||||
ExpectTransport: "tcp",
|
|
||||||
ExpectURL: "http://127.0.0.1:8080/antani",
|
|
||||||
RealResponseBody: nil,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
resp := v.NewHTTPResponse()
|
|
||||||
// Hack the body so it returns a connection reset error
|
|
||||||
// after some useful piece of data. We do not see any
|
|
||||||
// body in the response or in the trace. We may possibly
|
|
||||||
// want to include all the body we could read into the
|
|
||||||
// trace in the future, but for now it seems fine to do
|
|
||||||
// exactly what the previous code was doing.
|
|
||||||
resp.Body = io.NopCloser(io.MultiReader(
|
|
||||||
bytes.NewReader([]byte("0xdeadbeef")),
|
|
||||||
&mocks.Reader{
|
|
||||||
MockRead: func(b []byte) (int, error) {
|
|
||||||
return 0, expectedError
|
|
||||||
},
|
|
||||||
},
|
|
||||||
))
|
|
||||||
txp := newHTTPTransport(resp, nil)
|
|
||||||
resp, err := saver.HTTPRoundTrip(txp, maxBodySize, v.NewHTTPRequest())
|
|
||||||
if !errors.Is(err, expectedError) {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
t.Fatal("expected nil resp")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("cloneRequestHeaders", func(t *testing.T) {
|
|
||||||
// doWithRequest is an helper function that creates a suitable
|
|
||||||
// round tripper and returns trace.HTTPRoundTrip[0] for inspection
|
|
||||||
doWithRequest := func(req *http.Request) (*HTTPRoundTripEvent, error) {
|
|
||||||
expect := errors.New("mocked err")
|
|
||||||
txp := newHTTPTransport(nil, expect)
|
|
||||||
saver := NewSaver()
|
|
||||||
const maxBodySize = 1 << 20 // irrelevant
|
|
||||||
resp, err := saver.HTTPRoundTrip(txp, maxBodySize, req)
|
|
||||||
if !errors.Is(err, expect) {
|
|
||||||
return nil, fmt.Errorf("unexpected error: %w", err)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
return nil, errors.New("expected nil resp")
|
|
||||||
}
|
|
||||||
trace := saver.MoveOutTrace()
|
|
||||||
if len(trace.HTTPRoundTrip) != 1 {
|
|
||||||
return nil, errors.New("expected exactly one HTTPRoundTrip")
|
|
||||||
}
|
|
||||||
return trace.HTTPRoundTrip[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("with req.URL.Host", func(t *testing.T) {
|
|
||||||
req, err := http.NewRequest("GET", "https://x.org/", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
ev, err := doWithRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if ev.RequestHeaders.Get("Host") != "x.org" {
|
|
||||||
t.Fatal("unexpected request host")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with req.Host", func(t *testing.T) {
|
|
||||||
req, err := http.NewRequest("GET", "https://x.org/", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
req.Host = "google.com"
|
|
||||||
ev, err := doWithRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if ev.RequestHeaders.Get("Host") != "google.com" {
|
|
||||||
t.Fatal("unexpected request host")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type SingleHTTPRoundTripValidator struct {
|
|
||||||
ExpectFailure error
|
|
||||||
ExpectMethod string
|
|
||||||
ExpectRequestHeaders http.Header
|
|
||||||
ExpectResponseBody []byte
|
|
||||||
ExpectResponseBodyIsTruncated bool
|
|
||||||
ExpectResponseBodyLength int64
|
|
||||||
ExpectResponseHeaders http.Header
|
|
||||||
ExpectStatusCode int64
|
|
||||||
ExpectTransport string
|
|
||||||
ExpectURL string
|
|
||||||
RealResponseBody []byte
|
|
||||||
Saver *Saver
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *SingleHTTPRoundTripValidator) NewHTTPRequest() *http.Request {
|
|
||||||
parsedURL, err := url.Parse(v.ExpectURL)
|
|
||||||
runtimex.PanicOnError(err, "url.Parse should not fail here")
|
|
||||||
// The saving code clones the headers and adds the host header, which
|
|
||||||
// Go would instead add later. So, a realistic mock should not include
|
|
||||||
// such an header inside of the http.Request.
|
|
||||||
clonedHeaders := v.ExpectRequestHeaders.Clone()
|
|
||||||
clonedHeaders.Del("Host")
|
|
||||||
return &http.Request{
|
|
||||||
Method: v.ExpectMethod,
|
|
||||||
URL: parsedURL,
|
|
||||||
Proto: "",
|
|
||||||
ProtoMajor: 0,
|
|
||||||
ProtoMinor: 0,
|
|
||||||
Header: clonedHeaders,
|
|
||||||
Body: nil,
|
|
||||||
GetBody: nil,
|
|
||||||
ContentLength: 0,
|
|
||||||
TransferEncoding: nil,
|
|
||||||
Close: false,
|
|
||||||
Host: "",
|
|
||||||
Form: nil,
|
|
||||||
PostForm: nil,
|
|
||||||
MultipartForm: nil,
|
|
||||||
Trailer: nil,
|
|
||||||
RemoteAddr: "",
|
|
||||||
RequestURI: "",
|
|
||||||
TLS: nil,
|
|
||||||
Cancel: nil,
|
|
||||||
Response: nil,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *SingleHTTPRoundTripValidator) NewHTTPResponse() *http.Response {
|
|
||||||
body := io.NopCloser(bytes.NewReader(v.RealResponseBody))
|
|
||||||
return &http.Response{
|
|
||||||
Status: http.StatusText(int(v.ExpectStatusCode)),
|
|
||||||
StatusCode: int(v.ExpectStatusCode),
|
|
||||||
Proto: "",
|
|
||||||
ProtoMajor: 0,
|
|
||||||
ProtoMinor: 0,
|
|
||||||
Header: v.ExpectResponseHeaders,
|
|
||||||
Body: body,
|
|
||||||
ContentLength: 0,
|
|
||||||
TransferEncoding: nil,
|
|
||||||
Close: false,
|
|
||||||
Uncompressed: false,
|
|
||||||
Trailer: nil,
|
|
||||||
Request: nil,
|
|
||||||
TLS: nil,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *SingleHTTPRoundTripValidator) Validate() error {
|
|
||||||
trace := v.Saver.MoveOutTrace()
|
|
||||||
if len(trace.HTTPRoundTrip) != 1 {
|
|
||||||
return errors.New("expected to see one event")
|
|
||||||
}
|
|
||||||
entry := trace.HTTPRoundTrip[0]
|
|
||||||
if !errors.Is(entry.Failure, v.ExpectFailure) {
|
|
||||||
return errors.New("unexpected .Failure")
|
|
||||||
}
|
|
||||||
if !entry.Finished.After(entry.Started) {
|
|
||||||
return errors.New(".Finished is not after .Started")
|
|
||||||
}
|
|
||||||
if entry.Method != v.ExpectMethod {
|
|
||||||
return errors.New("unexpected .Method")
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(v.ExpectRequestHeaders, entry.RequestHeaders); diff != "" {
|
|
||||||
return errors.New(diff)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(v.ExpectResponseBody, entry.ResponseBody); diff != "" {
|
|
||||||
return errors.New(diff)
|
|
||||||
}
|
|
||||||
if entry.ResponseBodyIsTruncated != v.ExpectResponseBodyIsTruncated {
|
|
||||||
return errors.New("unexpected .ResponseBodyIsTruncated")
|
|
||||||
}
|
|
||||||
if entry.ResponseBodyLength != v.ExpectResponseBodyLength {
|
|
||||||
return errors.New("unexpected .ResponseBodyLength")
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(v.ExpectResponseHeaders, entry.ResponseHeaders); diff != "" {
|
|
||||||
return errors.New(diff)
|
|
||||||
}
|
|
||||||
if entry.StatusCode != v.ExpectStatusCode {
|
|
||||||
return errors.New("unexpected .StatusCode")
|
|
||||||
}
|
|
||||||
if entry.Transport != v.ExpectTransport {
|
|
||||||
return errors.New("unexpected .Transport")
|
|
||||||
}
|
|
||||||
if entry.URL != v.ExpectURL {
|
|
||||||
return errors.New("unexpected .URL")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
//
|
|
||||||
// Saves QUIC events.
|
|
||||||
//
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WriteTo performs WriteTo with the given pconn and saves the
|
|
||||||
// operation's results inside the saver.
|
|
||||||
func (s *Saver) WriteTo(pconn model.UDPLikeConn, buf []byte, addr net.Addr) (int, error) {
|
|
||||||
started := time.Now()
|
|
||||||
count, err := pconn.WriteTo(buf, addr)
|
|
||||||
s.appendNetworkEvent(&NetworkEvent{
|
|
||||||
Count: count,
|
|
||||||
Failure: err,
|
|
||||||
Finished: time.Now(),
|
|
||||||
Network: addr.Network(),
|
|
||||||
Operation: netxlite.WriteToOperation,
|
|
||||||
RemoteAddr: addr.String(),
|
|
||||||
Started: started,
|
|
||||||
})
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFrom performs ReadFrom with the given pconn and saves the
|
|
||||||
// operation's results inside the saver.
|
|
||||||
func (s *Saver) ReadFrom(pconn model.UDPLikeConn, buf []byte) (int, net.Addr, error) {
|
|
||||||
started := time.Now()
|
|
||||||
count, addr, err := pconn.ReadFrom(buf)
|
|
||||||
s.appendNetworkEvent(&NetworkEvent{
|
|
||||||
Count: count,
|
|
||||||
Failure: err,
|
|
||||||
Finished: time.Now(),
|
|
||||||
Network: "udp", // must be always set even on failure
|
|
||||||
Operation: netxlite.ReadFromOperation,
|
|
||||||
RemoteAddr: s.safeAddrString(addr),
|
|
||||||
Started: started,
|
|
||||||
})
|
|
||||||
return count, addr, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Saver) safeAddrString(addr net.Addr) (out string) {
|
|
||||||
if addr != nil {
|
|
||||||
out = addr.String()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// QUICDialContext dials a QUIC session using the given dialer
|
|
||||||
// and saves the results inside of the saver.
|
|
||||||
func (s *Saver) QUICDialContext(ctx context.Context, dialer model.QUICDialer,
|
|
||||||
network, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
|
|
||||||
started := time.Now()
|
|
||||||
var state tls.ConnectionState
|
|
||||||
sess, err := dialer.DialContext(ctx, network, address, tlsConfig, quicConfig)
|
|
||||||
if err == nil {
|
|
||||||
<-sess.HandshakeComplete().Done() // robustness (the dialer already does that)
|
|
||||||
state = sess.ConnectionState().TLS.ConnectionState
|
|
||||||
}
|
|
||||||
s.appendQUICHandshake(&QUICTLSHandshakeEvent{
|
|
||||||
ALPN: tlsConfig.NextProtos,
|
|
||||||
CipherSuite: netxlite.TLSCipherSuiteString(state.CipherSuite),
|
|
||||||
Failure: err,
|
|
||||||
Finished: time.Now(),
|
|
||||||
NegotiatedProto: state.NegotiatedProtocol,
|
|
||||||
Network: "quic",
|
|
||||||
PeerCerts: s.tlsPeerCerts(err, &state),
|
|
||||||
RemoteAddr: address,
|
|
||||||
SNI: tlsConfig.ServerName,
|
|
||||||
SkipVerify: tlsConfig.InsecureSkipVerify,
|
|
||||||
Started: started,
|
|
||||||
TLSVersion: netxlite.TLSVersionString(state.Version),
|
|
||||||
})
|
|
||||||
return sess, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Saver) appendQUICHandshake(ev *QUICTLSHandshakeEvent) {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.trace.QUICHandshake = append(s.trace.QUICHandshake, ev)
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
|
@ -1,420 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/marten-seemann/qtls-go1-18" // it's annoying to depend on that
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/fakefill"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSaverWriteTo(t *testing.T) {
|
|
||||||
// newAddr creates an new net.Addr for testing.
|
|
||||||
newAddr := func(endpoint string) net.Addr {
|
|
||||||
return &mocks.Addr{
|
|
||||||
MockString: func() string {
|
|
||||||
return endpoint
|
|
||||||
},
|
|
||||||
MockNetwork: func() string {
|
|
||||||
return "udp"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// newConn is a helper function for creating a new connection.
|
|
||||||
newConn := func(numBytes int, err error) model.UDPLikeConn {
|
|
||||||
return &mocks.UDPLikeConn{
|
|
||||||
MockWriteTo: func(p []byte, addr net.Addr) (int, error) {
|
|
||||||
time.Sleep(time.Microsecond)
|
|
||||||
return numBytes, err
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("on success", func(t *testing.T) {
|
|
||||||
const mockedEndpoint = "8.8.4.4:443"
|
|
||||||
const mockedNumBytes = 128
|
|
||||||
addr := newAddr(mockedEndpoint)
|
|
||||||
conn := newConn(mockedNumBytes, nil)
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleNetworkEventValidator{
|
|
||||||
ExpectedCount: mockedNumBytes,
|
|
||||||
ExpectedErr: nil,
|
|
||||||
ExpectedNetwork: "udp",
|
|
||||||
ExpectedOp: netxlite.WriteToOperation,
|
|
||||||
ExpectedEpnt: mockedEndpoint,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
count, err := saver.WriteTo(conn, buf, addr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if count != mockedNumBytes {
|
|
||||||
t.Fatal("invalid count")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on failure", func(t *testing.T) {
|
|
||||||
const mockedEndpoint = "8.8.4.4:443"
|
|
||||||
mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF)
|
|
||||||
addr := newAddr(mockedEndpoint)
|
|
||||||
conn := newConn(0, mockedError)
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleNetworkEventValidator{
|
|
||||||
ExpectedCount: 0,
|
|
||||||
ExpectedErr: mockedError,
|
|
||||||
ExpectedNetwork: "udp",
|
|
||||||
ExpectedOp: netxlite.WriteToOperation,
|
|
||||||
ExpectedEpnt: mockedEndpoint,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
count, err := saver.WriteTo(conn, buf, addr)
|
|
||||||
if !errors.Is(err, mockedError) {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if count != 0 {
|
|
||||||
t.Fatal("invalid count")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaverReadFrom(t *testing.T) {
|
|
||||||
// newAddr creates an new net.Addr for testing.
|
|
||||||
newAddr := func(endpoint string) net.Addr {
|
|
||||||
return &mocks.Addr{
|
|
||||||
MockString: func() string {
|
|
||||||
return endpoint
|
|
||||||
},
|
|
||||||
MockNetwork: func() string {
|
|
||||||
return "udp"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// newConn is a helper function for creating a new connection.
|
|
||||||
newConn := func(numBytes int, addr net.Addr, err error) model.UDPLikeConn {
|
|
||||||
return &mocks.UDPLikeConn{
|
|
||||||
MockReadFrom: func(p []byte) (int, net.Addr, error) {
|
|
||||||
time.Sleep(time.Microsecond)
|
|
||||||
return numBytes, addr, err
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("on success", func(t *testing.T) {
|
|
||||||
const mockedEndpoint = "8.8.4.4:443"
|
|
||||||
const mockedNumBytes = 128
|
|
||||||
expectedAddr := newAddr(mockedEndpoint)
|
|
||||||
conn := newConn(mockedNumBytes, expectedAddr, nil)
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleNetworkEventValidator{
|
|
||||||
ExpectedCount: mockedNumBytes,
|
|
||||||
ExpectedErr: nil,
|
|
||||||
ExpectedNetwork: "udp",
|
|
||||||
ExpectedOp: netxlite.ReadFromOperation,
|
|
||||||
ExpectedEpnt: mockedEndpoint,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
count, addr, err := saver.ReadFrom(conn, buf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if expectedAddr.Network() != addr.Network() {
|
|
||||||
t.Fatal("invalid addr.Network")
|
|
||||||
}
|
|
||||||
if expectedAddr.String() != addr.String() {
|
|
||||||
t.Fatal("invalid addr.String")
|
|
||||||
}
|
|
||||||
if count != mockedNumBytes {
|
|
||||||
t.Fatal("invalid count")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on failure", func(t *testing.T) {
|
|
||||||
mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF)
|
|
||||||
conn := newConn(0, nil, mockedError)
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleNetworkEventValidator{
|
|
||||||
ExpectedCount: 0,
|
|
||||||
ExpectedErr: mockedError,
|
|
||||||
ExpectedNetwork: "udp",
|
|
||||||
ExpectedOp: netxlite.ReadFromOperation,
|
|
||||||
ExpectedEpnt: "",
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
count, addr, err := saver.ReadFrom(conn, buf)
|
|
||||||
if !errors.Is(err, mockedError) {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if addr != nil {
|
|
||||||
t.Fatal("invalid addr")
|
|
||||||
}
|
|
||||||
if count != 0 {
|
|
||||||
t.Fatal("invalid count")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaverQUICDialContext(t *testing.T) {
|
|
||||||
// newQUICDialer creates a new QUICDialer for testing.
|
|
||||||
newQUICDialer := func(qconn quic.EarlyConnection, err error) model.QUICDialer {
|
|
||||||
return &mocks.QUICDialer{
|
|
||||||
MockDialContext: func(
|
|
||||||
ctx context.Context, network, address string, tlsConfig *tls.Config,
|
|
||||||
quicConfig *quic.Config) (quic.EarlyConnection, error) {
|
|
||||||
time.Sleep(time.Microsecond)
|
|
||||||
return qconn, err
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// newQUICConnection creates a new quic.EarlyConnection for testing.
|
|
||||||
newQUICConnection := func(handshakeComplete context.Context, state tls.ConnectionState) quic.EarlyConnection {
|
|
||||||
return &mocks.QUICEarlyConnection{
|
|
||||||
MockHandshakeComplete: func() context.Context {
|
|
||||||
return handshakeComplete
|
|
||||||
},
|
|
||||||
MockConnectionState: func() quic.ConnectionState {
|
|
||||||
return quic.ConnectionState{
|
|
||||||
TLS: qtls.ConnectionStateWith0RTT{
|
|
||||||
ConnectionState: state,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MockCloseWithError: func(code quic.ApplicationErrorCode, reason string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("on success", func(t *testing.T) {
|
|
||||||
handshakeCtx := context.Background()
|
|
||||||
handshakeCtx, handshakeCancel := context.WithCancel(handshakeCtx)
|
|
||||||
handshakeCancel() // simulate a completed handshake
|
|
||||||
const expectedNetwork = "udp"
|
|
||||||
const mockedEndpoint = "8.8.4.4:443"
|
|
||||||
saver := NewSaver()
|
|
||||||
var peerCerts [][]byte
|
|
||||||
ff := &fakefill.Filler{}
|
|
||||||
ff.Fill(&peerCerts)
|
|
||||||
if len(peerCerts) < 1 {
|
|
||||||
t.Fatal("did not fill peerCerts")
|
|
||||||
}
|
|
||||||
v := &SingleQUICTLSHandshakeValidator{
|
|
||||||
ExpectedALPN: []string{"h3"},
|
|
||||||
ExpectedSNI: "dns.google",
|
|
||||||
ExpectedSkipVerify: true,
|
|
||||||
//
|
|
||||||
ExpectedCipherSuite: tls.TLS_AES_128_GCM_SHA256,
|
|
||||||
ExpectedNegotiatedProtocol: "h3",
|
|
||||||
ExpectedPeerCerts: peerCerts,
|
|
||||||
ExpectedVersion: tls.VersionTLS13,
|
|
||||||
//
|
|
||||||
ExpectedNetwork: "quic",
|
|
||||||
ExpectedRemoteAddr: mockedEndpoint,
|
|
||||||
//
|
|
||||||
QUICConfig: &quic.Config{},
|
|
||||||
//
|
|
||||||
ExpectedFailure: nil,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
qconn := newQUICConnection(handshakeCtx, v.NewTLSConnectionState())
|
|
||||||
dialer := newQUICDialer(qconn, nil)
|
|
||||||
ctx := context.Background()
|
|
||||||
qconn, err := saver.QUICDialContext(ctx, dialer, expectedNetwork,
|
|
||||||
mockedEndpoint, v.NewTLSConfig(), v.QUICConfig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if qconn == nil {
|
|
||||||
t.Fatal("expected nil qconn")
|
|
||||||
}
|
|
||||||
qconn.CloseWithError(0, "")
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on other error", func(t *testing.T) {
|
|
||||||
mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF)
|
|
||||||
const expectedNetwork = "udp"
|
|
||||||
const mockedEndpoint = "8.8.4.4:443"
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleQUICTLSHandshakeValidator{
|
|
||||||
ExpectedALPN: []string{"h3"},
|
|
||||||
ExpectedSNI: "dns.google",
|
|
||||||
ExpectedSkipVerify: true,
|
|
||||||
//
|
|
||||||
ExpectedCipherSuite: 0,
|
|
||||||
ExpectedNegotiatedProtocol: "",
|
|
||||||
ExpectedPeerCerts: nil,
|
|
||||||
ExpectedVersion: 0,
|
|
||||||
//
|
|
||||||
ExpectedNetwork: "quic",
|
|
||||||
ExpectedRemoteAddr: mockedEndpoint,
|
|
||||||
//
|
|
||||||
QUICConfig: &quic.Config{},
|
|
||||||
//
|
|
||||||
ExpectedFailure: mockedError,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
dialer := newQUICDialer(nil, mockedError)
|
|
||||||
ctx := context.Background()
|
|
||||||
qconn, err := saver.QUICDialContext(ctx, dialer, expectedNetwork,
|
|
||||||
mockedEndpoint, v.NewTLSConfig(), v.QUICConfig)
|
|
||||||
if !errors.Is(err, mockedError) {
|
|
||||||
t.Fatal("unexpected error")
|
|
||||||
}
|
|
||||||
if qconn != nil {
|
|
||||||
t.Fatal("expected nil connection")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO(bassosimone): here we're not testing the case in which
|
|
||||||
// the certificate is invalid for the required SNI.
|
|
||||||
//
|
|
||||||
// We need first to figure out whether this is what happens
|
|
||||||
// when we validate for QUIC in such cases. If that's the case
|
|
||||||
// indeed, then we can write the tests.
|
|
||||||
|
|
||||||
t.Run("on x509.HostnameError", func(t *testing.T) {
|
|
||||||
t.Skip("test not implemented")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on x509.UnknownAuthorityError", func(t *testing.T) {
|
|
||||||
t.Skip("test not implemented")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on x509.CertificateInvalidError", func(t *testing.T) {
|
|
||||||
t.Skip("test not implemented")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type SingleQUICTLSHandshakeValidator struct {
|
|
||||||
// related to the tls.Config
|
|
||||||
ExpectedALPN []string
|
|
||||||
ExpectedSNI string
|
|
||||||
ExpectedSkipVerify bool
|
|
||||||
|
|
||||||
// related to the tls.ConnectionState
|
|
||||||
ExpectedCipherSuite uint16
|
|
||||||
ExpectedNegotiatedProtocol string
|
|
||||||
ExpectedPeerCerts [][]byte
|
|
||||||
ExpectedVersion uint16
|
|
||||||
|
|
||||||
// related to the mocked conn (TLS) / dial params (QUIC)
|
|
||||||
ExpectedNetwork string
|
|
||||||
ExpectedRemoteAddr string
|
|
||||||
|
|
||||||
// tells us whether we're using QUIC
|
|
||||||
QUICConfig *quic.Config
|
|
||||||
|
|
||||||
// other fields
|
|
||||||
ExpectedFailure error
|
|
||||||
Saver *Saver
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *SingleQUICTLSHandshakeValidator) NewTLSConfig() *tls.Config {
|
|
||||||
return &tls.Config{
|
|
||||||
NextProtos: v.ExpectedALPN,
|
|
||||||
ServerName: v.ExpectedSNI,
|
|
||||||
InsecureSkipVerify: v.ExpectedSkipVerify,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *SingleQUICTLSHandshakeValidator) NewTLSConnectionState() tls.ConnectionState {
|
|
||||||
var state tls.ConnectionState
|
|
||||||
if v.ExpectedCipherSuite != 0 {
|
|
||||||
state.CipherSuite = v.ExpectedCipherSuite
|
|
||||||
}
|
|
||||||
if v.ExpectedNegotiatedProtocol != "" {
|
|
||||||
state.NegotiatedProtocol = v.ExpectedNegotiatedProtocol
|
|
||||||
}
|
|
||||||
for _, cert := range v.ExpectedPeerCerts {
|
|
||||||
state.PeerCertificates = append(state.PeerCertificates, &x509.Certificate{
|
|
||||||
Raw: cert,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if v.ExpectedVersion != 0 {
|
|
||||||
state.Version = v.ExpectedVersion
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *SingleQUICTLSHandshakeValidator) Validate() error {
|
|
||||||
trace := v.Saver.MoveOutTrace()
|
|
||||||
var entries []*QUICTLSHandshakeEvent
|
|
||||||
if v.QUICConfig != nil {
|
|
||||||
entries = trace.QUICHandshake
|
|
||||||
} else {
|
|
||||||
entries = trace.TLSHandshake
|
|
||||||
}
|
|
||||||
if len(entries) != 1 {
|
|
||||||
return errors.New("expected to see a single entry")
|
|
||||||
}
|
|
||||||
entry := entries[0]
|
|
||||||
if diff := cmp.Diff(entry.ALPN, v.ExpectedALPN); diff != "" {
|
|
||||||
return errors.New(diff)
|
|
||||||
}
|
|
||||||
if entry.CipherSuite != netxlite.TLSCipherSuiteString(v.ExpectedCipherSuite) {
|
|
||||||
return errors.New("unexpected .CipherSuite")
|
|
||||||
}
|
|
||||||
if !errors.Is(entry.Failure, v.ExpectedFailure) {
|
|
||||||
return errors.New("unexpected .Failure")
|
|
||||||
}
|
|
||||||
if !entry.Finished.After(entry.Started) {
|
|
||||||
return errors.New(".Finished is not after .Started")
|
|
||||||
}
|
|
||||||
if entry.NegotiatedProto != v.ExpectedNegotiatedProtocol {
|
|
||||||
return errors.New("unexpected .NegotiatedProto")
|
|
||||||
}
|
|
||||||
if entry.Network != v.ExpectedNetwork {
|
|
||||||
return errors.New("unexpected .Network")
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(entry.PeerCerts, v.ExpectedPeerCerts); diff != "" {
|
|
||||||
return errors.New("unexpected .PeerCerts")
|
|
||||||
}
|
|
||||||
if entry.RemoteAddr != v.ExpectedRemoteAddr {
|
|
||||||
return errors.New("unexpected .RemoteAddr")
|
|
||||||
}
|
|
||||||
if entry.SNI != v.ExpectedSNI {
|
|
||||||
return errors.New("unexpected .ServerName")
|
|
||||||
}
|
|
||||||
if entry.SkipVerify != v.ExpectedSkipVerify {
|
|
||||||
return errors.New("unexpected .SkipVerify")
|
|
||||||
}
|
|
||||||
if entry.TLSVersion != netxlite.TLSVersionString(v.ExpectedVersion) {
|
|
||||||
return errors.New("unexpected .Version")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
//
|
|
||||||
// Saves DNS lookup events
|
|
||||||
//
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DNSLookupEvent contains the results of a DNS lookup.
|
|
||||||
type DNSLookupEvent struct {
|
|
||||||
ALPNs []string
|
|
||||||
Addresses []string
|
|
||||||
Domain string
|
|
||||||
Failure error
|
|
||||||
Finished time.Time
|
|
||||||
LookupType string
|
|
||||||
ResolverAddress string
|
|
||||||
ResolverNetwork string
|
|
||||||
Started time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupHost performs a host lookup with the given resolver
|
|
||||||
// and saves the results into the saver.
|
|
||||||
func (s *Saver) LookupHost(ctx context.Context, reso model.Resolver, domain string) ([]string, error) {
|
|
||||||
started := time.Now()
|
|
||||||
addrs, err := reso.LookupHost(ctx, domain)
|
|
||||||
s.appendLookupHostEvent(&DNSLookupEvent{
|
|
||||||
ALPNs: nil,
|
|
||||||
Addresses: addrs,
|
|
||||||
Domain: domain,
|
|
||||||
Failure: err,
|
|
||||||
Finished: time.Now(),
|
|
||||||
LookupType: "getaddrinfo",
|
|
||||||
ResolverAddress: reso.Address(),
|
|
||||||
ResolverNetwork: reso.Network(),
|
|
||||||
Started: started,
|
|
||||||
})
|
|
||||||
return addrs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Saver) appendLookupHostEvent(ev *DNSLookupEvent) {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.trace.DNSLookupHost = append(s.trace.DNSLookupHost, ev)
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupHTTPS performs an HTTPSSvc-record lookup using the given
|
|
||||||
// resolver and saves the results into the saver.
|
|
||||||
func (s *Saver) LookupHTTPS(ctx context.Context, reso model.Resolver, domain string) (*model.HTTPSSvc, error) {
|
|
||||||
started := time.Now()
|
|
||||||
https, err := reso.LookupHTTPS(ctx, domain)
|
|
||||||
s.appendLookupHTTPSEvent(&DNSLookupEvent{
|
|
||||||
ALPNs: s.safeALPNs(https),
|
|
||||||
Addresses: s.safeAddresses(https),
|
|
||||||
Domain: domain,
|
|
||||||
Failure: err,
|
|
||||||
Finished: time.Now(),
|
|
||||||
LookupType: "https",
|
|
||||||
ResolverAddress: reso.Address(),
|
|
||||||
ResolverNetwork: reso.Network(),
|
|
||||||
Started: started,
|
|
||||||
})
|
|
||||||
return https, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Saver) appendLookupHTTPSEvent(ev *DNSLookupEvent) {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.trace.DNSLookupHTTPS = append(s.trace.DNSLookupHTTPS, ev)
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Saver) safeALPNs(https *model.HTTPSSvc) (out []string) {
|
|
||||||
if https != nil {
|
|
||||||
out = https.ALPN
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Saver) safeAddresses(https *model.HTTPSSvc) (out []string) {
|
|
||||||
if https != nil {
|
|
||||||
out = append(out, https.IPv4...)
|
|
||||||
out = append(out, https.IPv6...)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNSRoundTripEvent contains the result of a DNS round trip.
|
|
||||||
type DNSRoundTripEvent struct {
|
|
||||||
Address string
|
|
||||||
Failure error
|
|
||||||
Finished time.Time
|
|
||||||
Network string
|
|
||||||
Query []byte
|
|
||||||
Reply []byte
|
|
||||||
Started time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNSRoundTrip implements ArchivalSaver.DNSRoundTrip.
|
|
||||||
func (s *Saver) DNSRoundTrip(ctx context.Context, txp model.DNSTransport, query []byte) ([]byte, error) {
|
|
||||||
started := time.Now()
|
|
||||||
reply, err := txp.RoundTrip(ctx, query)
|
|
||||||
s.appendDNSRoundTripEvent(&DNSRoundTripEvent{
|
|
||||||
Address: txp.Address(),
|
|
||||||
Failure: err,
|
|
||||||
Finished: time.Now(),
|
|
||||||
Network: txp.Network(),
|
|
||||||
Query: query,
|
|
||||||
Reply: reply,
|
|
||||||
Started: started,
|
|
||||||
})
|
|
||||||
return reply, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Saver) appendDNSRoundTripEvent(ev *DNSRoundTripEvent) {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.trace.DNSRoundTrip = append(s.trace.DNSRoundTrip, ev)
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
|
@ -1,347 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/fakefill"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSaverLookupHost(t *testing.T) {
|
|
||||||
// newResolver helps to create a new resolver.
|
|
||||||
newResolver := func(addrs []string, err error) model.Resolver {
|
|
||||||
return &mocks.Resolver{
|
|
||||||
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
|
|
||||||
return addrs, err
|
|
||||||
},
|
|
||||||
MockAddress: func() string {
|
|
||||||
return "8.8.8.8:53"
|
|
||||||
},
|
|
||||||
MockNetwork: func() string {
|
|
||||||
return "udp"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("on success", func(t *testing.T) {
|
|
||||||
const domain = "dns.google"
|
|
||||||
expectAddrs := []string{"8.8.8.8", "8.8.4.4"}
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleDNSLookupValidator{
|
|
||||||
ExpectALPNs: nil,
|
|
||||||
ExpectAddrs: expectAddrs,
|
|
||||||
ExpectDomain: domain,
|
|
||||||
ExpectLookupType: "getaddrinfo",
|
|
||||||
ExpectFailure: nil,
|
|
||||||
ExpectResolverAddress: "8.8.8.8:53",
|
|
||||||
ExpectResolverNetwork: "udp",
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
reso := newResolver(expectAddrs, nil)
|
|
||||||
ctx := context.Background()
|
|
||||||
addrs, err := saver.LookupHost(ctx, reso, domain)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(expectAddrs, addrs); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on failure", func(t *testing.T) {
|
|
||||||
mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF)
|
|
||||||
const domain = "dns.google"
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleDNSLookupValidator{
|
|
||||||
ExpectALPNs: nil,
|
|
||||||
ExpectAddrs: nil,
|
|
||||||
ExpectDomain: domain,
|
|
||||||
ExpectLookupType: "getaddrinfo",
|
|
||||||
ExpectFailure: mockedError,
|
|
||||||
ExpectResolverAddress: "8.8.8.8:53",
|
|
||||||
ExpectResolverNetwork: "udp",
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
reso := newResolver(nil, mockedError)
|
|
||||||
ctx := context.Background()
|
|
||||||
addrs, err := saver.LookupHost(ctx, reso, domain)
|
|
||||||
if !errors.Is(err, mockedError) {
|
|
||||||
t.Fatal("invalid err", err)
|
|
||||||
}
|
|
||||||
if len(addrs) != 0 {
|
|
||||||
t.Fatal("invalid addrs")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaverLookupHTTPS(t *testing.T) {
|
|
||||||
// newResolver helps to create a new resolver.
|
|
||||||
newResolver := func(alpns, ipv4, ipv6 []string, err error) model.Resolver {
|
|
||||||
return &mocks.Resolver{
|
|
||||||
MockLookupHTTPS: func(ctx context.Context, domain string) (*model.HTTPSSvc, error) {
|
|
||||||
if alpns == nil && ipv4 == nil && ipv6 == nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &model.HTTPSSvc{
|
|
||||||
ALPN: alpns,
|
|
||||||
IPv4: ipv4,
|
|
||||||
IPv6: ipv6,
|
|
||||||
}, err
|
|
||||||
},
|
|
||||||
MockAddress: func() string {
|
|
||||||
return "8.8.8.8:53"
|
|
||||||
},
|
|
||||||
MockNetwork: func() string {
|
|
||||||
return "udp"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("on success", func(t *testing.T) {
|
|
||||||
const domain = "dns.google"
|
|
||||||
expectALPN := []string{"h3", "h2", "http/1.1"}
|
|
||||||
expectA := []string{"8.8.8.8", "8.8.4.4"}
|
|
||||||
expectAAAA := []string{"2001:4860:4860::8844"}
|
|
||||||
expectAddrs := append(expectA, expectAAAA...)
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleDNSLookupValidator{
|
|
||||||
ExpectALPNs: expectALPN,
|
|
||||||
ExpectAddrs: expectAddrs,
|
|
||||||
ExpectDomain: domain,
|
|
||||||
ExpectLookupType: "https",
|
|
||||||
ExpectFailure: nil,
|
|
||||||
ExpectResolverAddress: "8.8.8.8:53",
|
|
||||||
ExpectResolverNetwork: "udp",
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
reso := newResolver(expectALPN, expectA, expectAAAA, nil)
|
|
||||||
ctx := context.Background()
|
|
||||||
https, err := saver.LookupHTTPS(ctx, reso, domain)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(expectALPN, https.ALPN); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(expectA, https.IPv4); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(expectAAAA, https.IPv6); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on failure", func(t *testing.T) {
|
|
||||||
mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF)
|
|
||||||
const domain = "dns.google"
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleDNSLookupValidator{
|
|
||||||
ExpectALPNs: nil,
|
|
||||||
ExpectAddrs: nil,
|
|
||||||
ExpectDomain: domain,
|
|
||||||
ExpectLookupType: "https",
|
|
||||||
ExpectFailure: mockedError,
|
|
||||||
ExpectResolverAddress: "8.8.8.8:53",
|
|
||||||
ExpectResolverNetwork: "udp",
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
reso := newResolver(nil, nil, nil, mockedError)
|
|
||||||
ctx := context.Background()
|
|
||||||
https, err := saver.LookupHTTPS(ctx, reso, domain)
|
|
||||||
if !errors.Is(err, mockedError) {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if https != nil {
|
|
||||||
t.Fatal("expected nil https")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type SingleDNSLookupValidator struct {
|
|
||||||
ExpectALPNs []string
|
|
||||||
ExpectAddrs []string
|
|
||||||
ExpectDomain string
|
|
||||||
ExpectLookupType string
|
|
||||||
ExpectFailure error
|
|
||||||
ExpectResolverAddress string
|
|
||||||
ExpectResolverNetwork string
|
|
||||||
Saver *Saver
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *SingleDNSLookupValidator) Validate() error {
|
|
||||||
trace := v.Saver.MoveOutTrace()
|
|
||||||
var entries []*DNSLookupEvent
|
|
||||||
switch v.ExpectLookupType {
|
|
||||||
case "getaddrinfo":
|
|
||||||
entries = trace.DNSLookupHost
|
|
||||||
case "https":
|
|
||||||
entries = trace.DNSLookupHTTPS
|
|
||||||
default:
|
|
||||||
return errors.New("invalid v.ExpectLookupType")
|
|
||||||
}
|
|
||||||
if len(entries) != 1 {
|
|
||||||
return errors.New("expected a single entry")
|
|
||||||
}
|
|
||||||
entry := entries[0]
|
|
||||||
if diff := cmp.Diff(v.ExpectALPNs, entry.ALPNs); diff != "" {
|
|
||||||
return errors.New(diff)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(v.ExpectAddrs, entry.Addresses); diff != "" {
|
|
||||||
return errors.New(diff)
|
|
||||||
}
|
|
||||||
if v.ExpectDomain != entry.Domain {
|
|
||||||
return errors.New("invalid .Domain value")
|
|
||||||
}
|
|
||||||
if !errors.Is(entry.Failure, v.ExpectFailure) {
|
|
||||||
return errors.New("invalid .Failure value")
|
|
||||||
}
|
|
||||||
if !entry.Finished.After(entry.Started) {
|
|
||||||
return errors.New(".Finished is not after .Started")
|
|
||||||
}
|
|
||||||
if entry.ResolverAddress != v.ExpectResolverAddress {
|
|
||||||
return errors.New("invalid .ResolverAddress value")
|
|
||||||
}
|
|
||||||
if entry.ResolverNetwork != v.ExpectResolverNetwork {
|
|
||||||
return errors.New("invalid .ResolverNetwork value")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaverDNSRoundTrip(t *testing.T) {
|
|
||||||
// generateQueryAndReply generates a fake query and reply.
|
|
||||||
generateQueryAndReply := func() (query, reply []byte, err error) {
|
|
||||||
ff := &fakefill.Filler{}
|
|
||||||
ff.Fill(&query)
|
|
||||||
ff.Fill(&reply)
|
|
||||||
if len(query) < 1 || len(reply) < 1 {
|
|
||||||
return nil, nil, errors.New("did not generate query or reply")
|
|
||||||
}
|
|
||||||
return query, reply, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newDNSTransport creates a suitable DNSTransport.
|
|
||||||
newDNSTransport := func(reply []byte, err error) model.DNSTransport {
|
|
||||||
return &mocks.DNSTransport{
|
|
||||||
MockRoundTrip: func(ctx context.Context, query []byte) ([]byte, error) {
|
|
||||||
return reply, err
|
|
||||||
},
|
|
||||||
MockNetwork: func() string {
|
|
||||||
return "udp"
|
|
||||||
},
|
|
||||||
MockAddress: func() string {
|
|
||||||
return "8.8.8.8:53"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("on success", func(t *testing.T) {
|
|
||||||
query, expectedReply, err := generateQueryAndReply()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleDNSRoundTripValidator{
|
|
||||||
ExpectAddress: "8.8.8.8:53",
|
|
||||||
ExpectFailure: nil,
|
|
||||||
ExpectNetwork: "udp",
|
|
||||||
ExpectQuery: query,
|
|
||||||
ExpectReply: expectedReply,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
txp := newDNSTransport(expectedReply, nil)
|
|
||||||
reply, err := saver.DNSRoundTrip(ctx, txp, query)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(expectedReply, reply); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on failure", func(t *testing.T) {
|
|
||||||
mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF)
|
|
||||||
query, _, err := generateQueryAndReply()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleDNSRoundTripValidator{
|
|
||||||
ExpectAddress: "8.8.8.8:53",
|
|
||||||
ExpectFailure: mockedError,
|
|
||||||
ExpectNetwork: "udp",
|
|
||||||
ExpectQuery: query,
|
|
||||||
ExpectReply: nil,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
txp := newDNSTransport(nil, mockedError)
|
|
||||||
reply, err := saver.DNSRoundTrip(ctx, txp, query)
|
|
||||||
if !errors.Is(err, mockedError) {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(reply) != 0 {
|
|
||||||
t.Fatal("unexpected reply")
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type SingleDNSRoundTripValidator struct {
|
|
||||||
ExpectAddress string
|
|
||||||
ExpectFailure error
|
|
||||||
ExpectNetwork string
|
|
||||||
ExpectQuery []byte
|
|
||||||
ExpectReply []byte
|
|
||||||
Saver *Saver
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *SingleDNSRoundTripValidator) Validate() error {
|
|
||||||
trace := v.Saver.MoveOutTrace()
|
|
||||||
if len(trace.DNSRoundTrip) != 1 {
|
|
||||||
return errors.New("expected a single entry")
|
|
||||||
}
|
|
||||||
entry := trace.DNSRoundTrip[0]
|
|
||||||
if v.ExpectAddress != entry.Address {
|
|
||||||
return errors.New("invalid .Address")
|
|
||||||
}
|
|
||||||
if !errors.Is(entry.Failure, v.ExpectFailure) {
|
|
||||||
return errors.New("invalid .Failure value")
|
|
||||||
}
|
|
||||||
if !entry.Finished.After(entry.Started) {
|
|
||||||
return errors.New(".Finished is not after .Started")
|
|
||||||
}
|
|
||||||
if v.ExpectNetwork != entry.Network {
|
|
||||||
return errors.New("invalid .Network value")
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(v.ExpectQuery, entry.Query); diff != "" {
|
|
||||||
return errors.New(diff)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(v.ExpectReply, entry.Reply); diff != "" {
|
|
||||||
return errors.New(diff)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
//
|
|
||||||
// Saver implementation
|
|
||||||
//
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Saver allows to save network, DNS, QUIC, TLS, HTTP events.
|
|
||||||
//
|
|
||||||
// You MUST use NewSaver to create a new instance.
|
|
||||||
type Saver struct {
|
|
||||||
// mu provides mutual exclusion.
|
|
||||||
mu sync.Mutex
|
|
||||||
|
|
||||||
// trace is the current trace.
|
|
||||||
trace *Trace
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSaver creates a new Saver instance.
|
|
||||||
//
|
|
||||||
// You MUST use this function to create a Saver.
|
|
||||||
func NewSaver() *Saver {
|
|
||||||
return &Saver{
|
|
||||||
mu: sync.Mutex{},
|
|
||||||
trace: &Trace{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveOutTrace moves the current trace out of the saver and
|
|
||||||
// creates a new empty trace inside it.
|
|
||||||
func (as *Saver) MoveOutTrace() *Trace {
|
|
||||||
as.mu.Lock()
|
|
||||||
t := as.trace
|
|
||||||
as.trace = &Trace{}
|
|
||||||
as.mu.Unlock()
|
|
||||||
return t
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/fakefill"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSaverNewSaver(t *testing.T) {
|
|
||||||
saver := NewSaver()
|
|
||||||
if saver.trace == nil {
|
|
||||||
t.Fatal("expected non-nil trace here")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaverMoveOutTrace(t *testing.T) {
|
|
||||||
saver := NewSaver()
|
|
||||||
var ev DNSRoundTripEvent
|
|
||||||
ff := &fakefill.Filler{}
|
|
||||||
ff.Fill(&ev)
|
|
||||||
if len(ev.Query) < 1 {
|
|
||||||
t.Fatal("did not fill") // be sure
|
|
||||||
}
|
|
||||||
saver.appendDNSRoundTripEvent(&ev)
|
|
||||||
trace := saver.MoveOutTrace()
|
|
||||||
if len(saver.trace.DNSRoundTrip) != 0 {
|
|
||||||
t.Fatal("expected zero length")
|
|
||||||
}
|
|
||||||
if len(trace.DNSRoundTrip) != 1 {
|
|
||||||
t.Fatal("expected one entry")
|
|
||||||
}
|
|
||||||
entry := trace.DNSRoundTrip[0]
|
|
||||||
if diff := cmp.Diff(&ev, entry); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
//
|
|
||||||
// Saves TLS events
|
|
||||||
//
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
// QUICTLSHandshakeEvent contains a QUIC or TLS handshake event.
|
|
||||||
type QUICTLSHandshakeEvent struct {
|
|
||||||
ALPN []string
|
|
||||||
CipherSuite string
|
|
||||||
Failure error
|
|
||||||
Finished time.Time
|
|
||||||
NegotiatedProto string
|
|
||||||
Network string
|
|
||||||
PeerCerts [][]byte
|
|
||||||
RemoteAddr string
|
|
||||||
SNI string
|
|
||||||
SkipVerify bool
|
|
||||||
Started time.Time
|
|
||||||
TLSVersion string
|
|
||||||
}
|
|
||||||
|
|
||||||
// TLSHandshake performs a TLS handshake with the given handshaker
|
|
||||||
// and saves the results into the saver.
|
|
||||||
func (s *Saver) TLSHandshake(ctx context.Context, thx model.TLSHandshaker,
|
|
||||||
conn net.Conn, config *tls.Config) (net.Conn, tls.ConnectionState, error) {
|
|
||||||
network := conn.RemoteAddr().Network()
|
|
||||||
remoteAddr := conn.RemoteAddr().String()
|
|
||||||
started := time.Now()
|
|
||||||
tconn, state, err := thx.Handshake(ctx, conn, config)
|
|
||||||
// Implementation note: state is an empty ConnectionState on failure
|
|
||||||
// so it's safe to access its fields also in that case
|
|
||||||
s.appendTLSHandshake(&QUICTLSHandshakeEvent{
|
|
||||||
ALPN: config.NextProtos,
|
|
||||||
CipherSuite: netxlite.TLSCipherSuiteString(state.CipherSuite),
|
|
||||||
Failure: err,
|
|
||||||
Finished: time.Now(),
|
|
||||||
NegotiatedProto: state.NegotiatedProtocol,
|
|
||||||
Network: network,
|
|
||||||
PeerCerts: s.tlsPeerCerts(err, &state),
|
|
||||||
RemoteAddr: remoteAddr,
|
|
||||||
SNI: config.ServerName,
|
|
||||||
SkipVerify: config.InsecureSkipVerify,
|
|
||||||
Started: started,
|
|
||||||
TLSVersion: netxlite.TLSVersionString(state.Version),
|
|
||||||
})
|
|
||||||
return tconn, state, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Saver) appendTLSHandshake(ev *QUICTLSHandshakeEvent) {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.trace.TLSHandshake = append(s.trace.TLSHandshake, ev)
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Saver) tlsPeerCerts(err error, state *tls.ConnectionState) (out [][]byte) {
|
|
||||||
var x509HostnameError x509.HostnameError
|
|
||||||
if errors.As(err, &x509HostnameError) {
|
|
||||||
// Test case: https://wrong.host.badssl.com/
|
|
||||||
return [][]byte{x509HostnameError.Certificate.Raw}
|
|
||||||
}
|
|
||||||
var x509UnknownAuthorityError x509.UnknownAuthorityError
|
|
||||||
if errors.As(err, &x509UnknownAuthorityError) {
|
|
||||||
// Test case: https://self-signed.badssl.com/. This error has
|
|
||||||
// never been among the ones returned by MK.
|
|
||||||
return [][]byte{x509UnknownAuthorityError.Cert.Raw}
|
|
||||||
}
|
|
||||||
var x509CertificateInvalidError x509.CertificateInvalidError
|
|
||||||
if errors.As(err, &x509CertificateInvalidError) {
|
|
||||||
// Test case: https://expired.badssl.com/
|
|
||||||
return [][]byte{x509CertificateInvalidError.Cert.Raw}
|
|
||||||
}
|
|
||||||
for _, cert := range state.PeerCertificates {
|
|
||||||
out = append(out, cert.Raw)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,178 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/fakefill"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSaverTLSHandshake(t *testing.T) {
|
|
||||||
// newTLSHandshaker helps with building a TLS handshaker
|
|
||||||
newTLSHandshaker := func(tlsConn net.Conn, state tls.ConnectionState, err error) model.TLSHandshaker {
|
|
||||||
return &mocks.TLSHandshaker{
|
|
||||||
MockHandshake: func(ctx context.Context, tcpConn net.Conn, config *tls.Config) (net.Conn, tls.ConnectionState, error) {
|
|
||||||
time.Sleep(1 * time.Microsecond)
|
|
||||||
return tlsConn, state, err
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// newTCPConn creates a suitable net.Conn
|
|
||||||
newTCPConn := func(address string) net.Conn {
|
|
||||||
return &mocks.Conn{
|
|
||||||
MockRemoteAddr: func() net.Addr {
|
|
||||||
return &mocks.Addr{
|
|
||||||
MockString: func() string {
|
|
||||||
return address
|
|
||||||
},
|
|
||||||
MockNetwork: func() string {
|
|
||||||
return "tcp"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MockClose: func() error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("on success", func(t *testing.T) {
|
|
||||||
const mockedEndpoint = "8.8.4.4:443"
|
|
||||||
var certs [][]byte
|
|
||||||
ff := &fakefill.Filler{}
|
|
||||||
ff.Fill(&certs)
|
|
||||||
if len(certs) < 1 {
|
|
||||||
t.Fatal("did not fill certs")
|
|
||||||
}
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleQUICTLSHandshakeValidator{
|
|
||||||
ExpectedALPN: []string{"h2", "http/1.1"},
|
|
||||||
ExpectedSNI: "dns.google",
|
|
||||||
ExpectedSkipVerify: true,
|
|
||||||
//
|
|
||||||
ExpectedCipherSuite: tls.TLS_AES_128_GCM_SHA256,
|
|
||||||
ExpectedNegotiatedProtocol: "h2",
|
|
||||||
ExpectedPeerCerts: certs,
|
|
||||||
ExpectedVersion: tls.VersionTLS12,
|
|
||||||
//
|
|
||||||
ExpectedNetwork: "tcp",
|
|
||||||
ExpectedRemoteAddr: mockedEndpoint,
|
|
||||||
//
|
|
||||||
QUICConfig: nil, // this is not QUIC
|
|
||||||
ExpectedFailure: nil,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
expectedState := v.NewTLSConnectionState()
|
|
||||||
thx := newTLSHandshaker(newTCPConn(mockedEndpoint), expectedState, nil)
|
|
||||||
ctx := context.Background()
|
|
||||||
tcpConn := newTCPConn(mockedEndpoint)
|
|
||||||
conn, state, err := saver.TLSHandshake(ctx, thx, tcpConn, v.NewTLSConfig())
|
|
||||||
if conn == nil {
|
|
||||||
t.Fatal("expected non-nil conn")
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
if diff := cmp.Diff(expectedState, state, cmpopts.IgnoreUnexported(tls.ConnectionState{})); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// failureFlow is the flow we run on failure.
|
|
||||||
failureFlow := func(mockedError error, peerCerts [][]byte) error {
|
|
||||||
const mockedEndpoint = "8.8.4.4:443"
|
|
||||||
saver := NewSaver()
|
|
||||||
v := &SingleQUICTLSHandshakeValidator{
|
|
||||||
ExpectedALPN: []string{"h2", "http/1.1"},
|
|
||||||
ExpectedSNI: "dns.google",
|
|
||||||
ExpectedSkipVerify: true,
|
|
||||||
//
|
|
||||||
ExpectedCipherSuite: 0,
|
|
||||||
ExpectedNegotiatedProtocol: "",
|
|
||||||
ExpectedPeerCerts: peerCerts,
|
|
||||||
ExpectedVersion: 0,
|
|
||||||
//
|
|
||||||
ExpectedNetwork: "tcp",
|
|
||||||
ExpectedRemoteAddr: mockedEndpoint,
|
|
||||||
//
|
|
||||||
QUICConfig: nil, // this is not QUIC
|
|
||||||
ExpectedFailure: mockedError,
|
|
||||||
Saver: saver,
|
|
||||||
}
|
|
||||||
expectedState := v.NewTLSConnectionState()
|
|
||||||
thx := newTLSHandshaker(nil, expectedState, mockedError)
|
|
||||||
ctx := context.Background()
|
|
||||||
tcpConn := newTCPConn(mockedEndpoint)
|
|
||||||
conn, state, err := saver.TLSHandshake(ctx, thx, tcpConn, v.NewTLSConfig())
|
|
||||||
if conn != nil {
|
|
||||||
return errors.New("expected nil conn")
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(expectedState, state, cmpopts.IgnoreUnexported(tls.ConnectionState{})); diff != "" {
|
|
||||||
return errors.New(diff)
|
|
||||||
}
|
|
||||||
if !errors.Is(err, mockedError) {
|
|
||||||
return fmt.Errorf("unexpected err: %w", err)
|
|
||||||
}
|
|
||||||
return v.Validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("on generic failure", func(t *testing.T) {
|
|
||||||
mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF)
|
|
||||||
if err := failureFlow(mockedError, nil); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on x509.HostnameError", func(t *testing.T) {
|
|
||||||
var certificate []byte
|
|
||||||
ff := &fakefill.Filler{}
|
|
||||||
ff.Fill(&certificate)
|
|
||||||
mockedError := x509.HostnameError{
|
|
||||||
Certificate: &x509.Certificate{Raw: certificate},
|
|
||||||
}
|
|
||||||
if err := failureFlow(mockedError, [][]byte{certificate}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on x509.UnknownAuthorityError", func(t *testing.T) {
|
|
||||||
var certificate []byte
|
|
||||||
ff := &fakefill.Filler{}
|
|
||||||
ff.Fill(&certificate)
|
|
||||||
mockedError := x509.UnknownAuthorityError{
|
|
||||||
Cert: &x509.Certificate{Raw: certificate},
|
|
||||||
}
|
|
||||||
if err := failureFlow(mockedError, [][]byte{certificate}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("on x509.CertificateInvalidError", func(t *testing.T) {
|
|
||||||
var certificate []byte
|
|
||||||
ff := &fakefill.Filler{}
|
|
||||||
ff.Fill(&certificate)
|
|
||||||
mockedError := x509.CertificateInvalidError{
|
|
||||||
Cert: &x509.Certificate{Raw: certificate},
|
|
||||||
}
|
|
||||||
if err := failureFlow(mockedError, [][]byte{certificate}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,283 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/geolocate"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
//
|
|
||||||
// Trace implementation
|
|
||||||
//
|
|
||||||
|
|
||||||
// Trace contains the events.
|
|
||||||
type Trace struct {
|
|
||||||
// DNSLookupHTTPS contains DNSLookupHTTPS events.
|
|
||||||
DNSLookupHTTPS []*DNSLookupEvent
|
|
||||||
|
|
||||||
// DNSLookupHost contains DNSLookupHost events.
|
|
||||||
DNSLookupHost []*DNSLookupEvent
|
|
||||||
|
|
||||||
// DNSRoundTrip contains DNSRoundTrip events.
|
|
||||||
DNSRoundTrip []*DNSRoundTripEvent
|
|
||||||
|
|
||||||
// HTTPRoundTrip contains HTTPRoundTrip round trip events.
|
|
||||||
HTTPRoundTrip []*HTTPRoundTripEvent
|
|
||||||
|
|
||||||
// Network contains network events.
|
|
||||||
Network []*NetworkEvent
|
|
||||||
|
|
||||||
// QUICHandshake contains QUICHandshake handshake events.
|
|
||||||
QUICHandshake []*QUICTLSHandshakeEvent
|
|
||||||
|
|
||||||
// TLSHandshake contains TLSHandshake handshake events.
|
|
||||||
TLSHandshake []*QUICTLSHandshakeEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Trace) newFailure(err error) (out *string) {
|
|
||||||
if err != nil {
|
|
||||||
s := err.Error()
|
|
||||||
out = &s
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// TCP connect
|
|
||||||
//
|
|
||||||
|
|
||||||
// NewArchivalTCPConnectResultList builds a TCP connect list in the OONI archival
|
|
||||||
// data format out of the results saved inside the trace.
|
|
||||||
func (t *Trace) NewArchivalTCPConnectResultList(begin time.Time) (out []model.ArchivalTCPConnectResult) {
|
|
||||||
for _, ev := range t.Network {
|
|
||||||
if ev.Operation != netxlite.ConnectOperation || ev.Network != "tcp" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// We assume Go is passing us legit data structures
|
|
||||||
ip, sport, _ := net.SplitHostPort(ev.RemoteAddr)
|
|
||||||
iport, _ := strconv.Atoi(sport)
|
|
||||||
out = append(out, model.ArchivalTCPConnectResult{
|
|
||||||
IP: ip,
|
|
||||||
Port: iport,
|
|
||||||
Status: model.ArchivalTCPConnectStatus{
|
|
||||||
Blocked: nil, // Web Connectivity only, depends on the control
|
|
||||||
Failure: t.newFailure(ev.Failure),
|
|
||||||
Success: ev.Failure == nil,
|
|
||||||
},
|
|
||||||
T: ev.Finished.Sub(begin).Seconds(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// HTTP
|
|
||||||
//
|
|
||||||
|
|
||||||
// NewArchivalHTTPRequestResultList builds an HTTP requests list in the OONI
|
|
||||||
// archival data format out of the results saved inside the trace.
|
|
||||||
//
|
|
||||||
// This function will sort the emitted list of requests such that the last
|
|
||||||
// request that happened in time is the first one to be emitted. If the
|
|
||||||
// measurement code performs related requests sequentially (which is a kinda a
|
|
||||||
// given because you cannot follow a redirect before reading the previous request),
|
|
||||||
// then the result is sorted how the OONI pipeline expects it to be.
|
|
||||||
func (t *Trace) NewArchivalHTTPRequestResultList(begin time.Time) (out []model.ArchivalHTTPRequestResult) {
|
|
||||||
for _, ev := range t.HTTPRoundTrip {
|
|
||||||
out = append(out, model.ArchivalHTTPRequestResult{
|
|
||||||
Failure: t.newFailure(ev.Failure),
|
|
||||||
Request: model.ArchivalHTTPRequest{
|
|
||||||
Body: model.ArchivalMaybeBinaryData{},
|
|
||||||
BodyIsTruncated: false,
|
|
||||||
HeadersList: t.newHTTPHeadersList(ev.RequestHeaders),
|
|
||||||
Headers: t.newHTTPHeadersMap(ev.RequestHeaders),
|
|
||||||
Method: ev.Method,
|
|
||||||
Tor: model.ArchivalHTTPTor{},
|
|
||||||
Transport: ev.Transport,
|
|
||||||
URL: ev.URL,
|
|
||||||
},
|
|
||||||
Response: model.ArchivalHTTPResponse{
|
|
||||||
Body: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: string(ev.ResponseBody),
|
|
||||||
},
|
|
||||||
BodyIsTruncated: ev.ResponseBodyIsTruncated,
|
|
||||||
Code: ev.StatusCode,
|
|
||||||
HeadersList: t.newHTTPHeadersList(ev.ResponseHeaders),
|
|
||||||
Headers: t.newHTTPHeadersMap(ev.ResponseHeaders),
|
|
||||||
Locations: ev.ResponseHeaders.Values("Location"), // safe with nil headers
|
|
||||||
},
|
|
||||||
T: ev.Finished.Sub(begin).Seconds(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Implementation note: historically OONI has always added
|
|
||||||
// the _last_ measurement in _first_ position. This has only
|
|
||||||
// been relevant for sequentially performed requests. For
|
|
||||||
// this purpose it feels okay to use T as the sorting key,
|
|
||||||
// since it's the time when we exited RoundTrip().
|
|
||||||
sort.Slice(out, func(i, j int) bool {
|
|
||||||
return out[i].T > out[j].T
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Trace) newHTTPHeadersList(source http.Header) (out []model.ArchivalHTTPHeader) {
|
|
||||||
for key, values := range source {
|
|
||||||
for _, value := range values {
|
|
||||||
out = append(out, model.ArchivalHTTPHeader{
|
|
||||||
Key: key,
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: value,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Implementation note: we need to sort the keys to have
|
|
||||||
// stable testing since map iteration is random.
|
|
||||||
sort.Slice(out, func(i, j int) bool {
|
|
||||||
return out[i].Key < out[j].Key
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Trace) newHTTPHeadersMap(source http.Header) (out map[string]model.ArchivalMaybeBinaryData) {
|
|
||||||
for key, values := range source {
|
|
||||||
for index, value := range values {
|
|
||||||
if index > 0 {
|
|
||||||
break // only the first entry
|
|
||||||
}
|
|
||||||
if out == nil {
|
|
||||||
out = make(map[string]model.ArchivalMaybeBinaryData)
|
|
||||||
}
|
|
||||||
out[key] = model.ArchivalMaybeBinaryData{Value: value}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// DNS
|
|
||||||
//
|
|
||||||
|
|
||||||
// NewArchivalDNSLookupResultList builds a DNS lookups list in the OONI
|
|
||||||
// archival data format out of the results saved inside the trace.
|
|
||||||
func (t *Trace) NewArchivalDNSLookupResultList(begin time.Time) (out []model.ArchivalDNSLookupResult) {
|
|
||||||
for _, ev := range t.DNSLookupHost {
|
|
||||||
out = append(out, model.ArchivalDNSLookupResult{
|
|
||||||
Answers: t.gatherA(ev.Addresses),
|
|
||||||
Engine: ev.ResolverNetwork,
|
|
||||||
Failure: t.newFailure(ev.Failure),
|
|
||||||
Hostname: ev.Domain,
|
|
||||||
QueryType: "A",
|
|
||||||
ResolverHostname: nil, // legacy
|
|
||||||
ResolverPort: nil, // legacy
|
|
||||||
ResolverAddress: ev.ResolverAddress,
|
|
||||||
T: ev.Finished.Sub(begin).Seconds(),
|
|
||||||
})
|
|
||||||
aaaa := t.gatherAAAA(ev.Addresses)
|
|
||||||
if len(aaaa) <= 0 && ev.Failure == nil {
|
|
||||||
// We don't have any AAAA results. Historically we do not
|
|
||||||
// create a record for AAAA with no results when A succeeded
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, model.ArchivalDNSLookupResult{
|
|
||||||
Answers: aaaa,
|
|
||||||
Engine: ev.ResolverNetwork,
|
|
||||||
Failure: t.newFailure(ev.Failure),
|
|
||||||
Hostname: ev.Domain,
|
|
||||||
QueryType: "AAAA",
|
|
||||||
ResolverHostname: nil, // legacy
|
|
||||||
ResolverPort: nil, // legacy
|
|
||||||
ResolverAddress: ev.ResolverAddress,
|
|
||||||
T: ev.Finished.Sub(begin).Seconds(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Trace) gatherA(addrs []string) (out []model.ArchivalDNSAnswer) {
|
|
||||||
for _, addr := range addrs {
|
|
||||||
if strings.Contains(addr, ":") {
|
|
||||||
continue // it's AAAA so we need to skip it
|
|
||||||
}
|
|
||||||
answer := model.ArchivalDNSAnswer{AnswerType: "A"}
|
|
||||||
asn, org, _ := geolocate.LookupASN(addr)
|
|
||||||
answer.ASN = int64(asn)
|
|
||||||
answer.ASOrgName = org
|
|
||||||
answer.IPv4 = addr
|
|
||||||
out = append(out, answer)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Trace) gatherAAAA(addrs []string) (out []model.ArchivalDNSAnswer) {
|
|
||||||
for _, addr := range addrs {
|
|
||||||
if !strings.Contains(addr, ":") {
|
|
||||||
continue // it's A so we need to skip it
|
|
||||||
}
|
|
||||||
answer := model.ArchivalDNSAnswer{AnswerType: "AAAA"}
|
|
||||||
asn, org, _ := geolocate.LookupASN(addr)
|
|
||||||
answer.ASN = int64(asn)
|
|
||||||
answer.ASOrgName = org
|
|
||||||
answer.IPv6 = addr
|
|
||||||
out = append(out, answer)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// NetworkEvents
|
|
||||||
//
|
|
||||||
|
|
||||||
// NewArchivalNetworkEventList builds a network events list in the OONI
|
|
||||||
// archival data format out of the results saved inside the trace.
|
|
||||||
func (t *Trace) NewArchivalNetworkEventList(begin time.Time) (out []model.ArchivalNetworkEvent) {
|
|
||||||
for _, ev := range t.Network {
|
|
||||||
out = append(out, model.ArchivalNetworkEvent{
|
|
||||||
Address: ev.RemoteAddr,
|
|
||||||
Failure: t.newFailure(ev.Failure),
|
|
||||||
NumBytes: int64(ev.Count),
|
|
||||||
Operation: ev.Operation,
|
|
||||||
Proto: ev.Network,
|
|
||||||
T: ev.Finished.Sub(begin).Seconds(),
|
|
||||||
Tags: nil,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// TLS handshake
|
|
||||||
//
|
|
||||||
|
|
||||||
// NewArchivalTLSHandshakeResultList builds a TLS handshakes list in the OONI
|
|
||||||
// archival data format out of the results saved inside the trace.
|
|
||||||
func (t *Trace) NewArchivalTLSHandshakeResultList(begin time.Time) (out []model.ArchivalTLSOrQUICHandshakeResult) {
|
|
||||||
for _, ev := range t.TLSHandshake {
|
|
||||||
out = append(out, model.ArchivalTLSOrQUICHandshakeResult{
|
|
||||||
CipherSuite: ev.CipherSuite,
|
|
||||||
Failure: t.newFailure(ev.Failure),
|
|
||||||
NegotiatedProtocol: ev.NegotiatedProto,
|
|
||||||
NoTLSVerify: ev.SkipVerify,
|
|
||||||
PeerCertificates: t.makePeerCerts(ev.PeerCerts),
|
|
||||||
ServerName: ev.SNI,
|
|
||||||
T: ev.Finished.Sub(begin).Seconds(),
|
|
||||||
Tags: nil,
|
|
||||||
TLSVersion: ev.TLSVersion,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Trace) makePeerCerts(in [][]byte) (out []model.ArchivalMaybeBinaryData) {
|
|
||||||
for _, v := range in {
|
|
||||||
out = append(out, model.ArchivalMaybeBinaryData{Value: string(v)})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,921 +0,0 @@
|
||||||
package archival
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
// traceTime generates a time that is based off a fixed beginning
|
|
||||||
// in time, so we can easily compare times.
|
|
||||||
func traceTime(d int64) time.Time {
|
|
||||||
t := time.Date(2021, 01, 13, 14, 21, 59, 0, time.UTC)
|
|
||||||
return t.Add(time.Duration(d) * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
// deltaSinceTraceTime computes the delta since the original
|
|
||||||
// trace time expressed in floating point seconds.
|
|
||||||
func deltaSinceTraceTime(d int64) float64 {
|
|
||||||
return (time.Duration(d) * time.Millisecond).Seconds()
|
|
||||||
}
|
|
||||||
|
|
||||||
// failureFromString converts a string to a failure.
|
|
||||||
func failureFromString(failure string) *string {
|
|
||||||
return &failure
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTraceNewArchivalTCPConnectResultList(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
DNSLookupHTTPS []*DNSLookupEvent
|
|
||||||
DNSLookupHost []*DNSLookupEvent
|
|
||||||
DNSRoundTrip []*DNSRoundTripEvent
|
|
||||||
HTTPRoundTrip []*HTTPRoundTripEvent
|
|
||||||
Network []*NetworkEvent
|
|
||||||
QUICHandshake []*QUICTLSHandshakeEvent
|
|
||||||
TLSHandshake []*QUICTLSHandshakeEvent
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
begin time.Time
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
wantOut []model.ArchivalTCPConnectResult
|
|
||||||
}{{
|
|
||||||
name: "with empty trace",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: nil,
|
|
||||||
}, {
|
|
||||||
name: "we ignore I/O operations",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{{
|
|
||||||
Count: 1024,
|
|
||||||
Failure: nil,
|
|
||||||
Finished: traceTime(2),
|
|
||||||
Network: "tcp",
|
|
||||||
Operation: netxlite.WriteOperation,
|
|
||||||
RemoteAddr: "8.8.8.8:443",
|
|
||||||
Started: traceTime(1),
|
|
||||||
}, {
|
|
||||||
Count: 4096,
|
|
||||||
Failure: nil,
|
|
||||||
Finished: traceTime(4),
|
|
||||||
Network: "tcp",
|
|
||||||
Operation: netxlite.ReadOperation,
|
|
||||||
RemoteAddr: "8.8.8.8:443",
|
|
||||||
Started: traceTime(3),
|
|
||||||
}},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: nil,
|
|
||||||
}, {
|
|
||||||
name: "we ignore UDP connect",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{{
|
|
||||||
Count: 0,
|
|
||||||
Failure: nil,
|
|
||||||
Finished: traceTime(2),
|
|
||||||
Network: "udp",
|
|
||||||
Operation: netxlite.ConnectOperation,
|
|
||||||
RemoteAddr: "8.8.8.8:53",
|
|
||||||
Started: traceTime(1),
|
|
||||||
}},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: nil,
|
|
||||||
}, {
|
|
||||||
name: "with TCP connect success",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{{
|
|
||||||
Count: 0,
|
|
||||||
Failure: nil,
|
|
||||||
Finished: traceTime(2),
|
|
||||||
Network: "tcp",
|
|
||||||
Operation: netxlite.ConnectOperation,
|
|
||||||
RemoteAddr: "8.8.8.8:443",
|
|
||||||
Started: traceTime(1),
|
|
||||||
}},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: []model.ArchivalTCPConnectResult{{
|
|
||||||
IP: "8.8.8.8",
|
|
||||||
Port: 443,
|
|
||||||
Status: model.ArchivalTCPConnectStatus{
|
|
||||||
Blocked: nil,
|
|
||||||
Failure: nil,
|
|
||||||
Success: true,
|
|
||||||
},
|
|
||||||
T: deltaSinceTraceTime(2),
|
|
||||||
}},
|
|
||||||
}, {
|
|
||||||
name: "with TCP connect failure",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{{
|
|
||||||
Count: 0,
|
|
||||||
Failure: netxlite.NewTopLevelGenericErrWrapper(netxlite.ECONNREFUSED),
|
|
||||||
Finished: traceTime(2),
|
|
||||||
Network: "tcp",
|
|
||||||
Operation: netxlite.ConnectOperation,
|
|
||||||
RemoteAddr: "8.8.8.8:443",
|
|
||||||
Started: traceTime(1),
|
|
||||||
}},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: []model.ArchivalTCPConnectResult{{
|
|
||||||
IP: "8.8.8.8",
|
|
||||||
Port: 443,
|
|
||||||
Status: model.ArchivalTCPConnectStatus{
|
|
||||||
Blocked: nil,
|
|
||||||
Failure: failureFromString(netxlite.FailureConnectionRefused),
|
|
||||||
Success: false,
|
|
||||||
},
|
|
||||||
T: deltaSinceTraceTime(2),
|
|
||||||
}},
|
|
||||||
}}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
tr := &Trace{
|
|
||||||
DNSLookupHTTPS: tt.fields.DNSLookupHTTPS,
|
|
||||||
DNSLookupHost: tt.fields.DNSLookupHost,
|
|
||||||
DNSRoundTrip: tt.fields.DNSRoundTrip,
|
|
||||||
HTTPRoundTrip: tt.fields.HTTPRoundTrip,
|
|
||||||
Network: tt.fields.Network,
|
|
||||||
QUICHandshake: tt.fields.QUICHandshake,
|
|
||||||
TLSHandshake: tt.fields.TLSHandshake,
|
|
||||||
}
|
|
||||||
gotOut := tr.NewArchivalTCPConnectResultList(tt.args.begin)
|
|
||||||
if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTraceNewArchivalHTTPRequestResultList(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
DNSLookupHTTPS []*DNSLookupEvent
|
|
||||||
DNSLookupHost []*DNSLookupEvent
|
|
||||||
DNSRoundTrip []*DNSRoundTripEvent
|
|
||||||
HTTPRoundTrip []*HTTPRoundTripEvent
|
|
||||||
Network []*NetworkEvent
|
|
||||||
QUICHandshake []*QUICTLSHandshakeEvent
|
|
||||||
TLSHandshake []*QUICTLSHandshakeEvent
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
begin time.Time
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
wantOut []model.ArchivalHTTPRequestResult
|
|
||||||
}{{
|
|
||||||
name: "with empty trace",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: nil,
|
|
||||||
}, {
|
|
||||||
name: "with failure",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{{
|
|
||||||
Failure: netxlite.NewTopLevelGenericErrWrapper(netxlite.ECONNRESET),
|
|
||||||
Finished: traceTime(2),
|
|
||||||
Method: "GET",
|
|
||||||
RequestHeaders: http.Header{
|
|
||||||
"Accept": {"*/*"},
|
|
||||||
"X-Cookie": {"A", "B", "C"},
|
|
||||||
},
|
|
||||||
ResponseBody: nil,
|
|
||||||
ResponseBodyIsTruncated: false,
|
|
||||||
ResponseBodyLength: 0,
|
|
||||||
ResponseHeaders: nil,
|
|
||||||
Started: traceTime(1),
|
|
||||||
StatusCode: 0,
|
|
||||||
Transport: "tcp",
|
|
||||||
URL: "http://x.org/",
|
|
||||||
}},
|
|
||||||
Network: []*NetworkEvent{},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: []model.ArchivalHTTPRequestResult{{
|
|
||||||
Failure: failureFromString(netxlite.FailureConnectionReset),
|
|
||||||
Request: model.ArchivalHTTPRequest{
|
|
||||||
Body: model.ArchivalMaybeBinaryData{},
|
|
||||||
BodyIsTruncated: false,
|
|
||||||
HeadersList: []model.ArchivalHTTPHeader{{
|
|
||||||
Key: "Accept",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "*/*",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Key: "X-Cookie",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "A",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Key: "X-Cookie",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "B",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Key: "X-Cookie",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "C",
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
Headers: map[string]model.ArchivalMaybeBinaryData{
|
|
||||||
"Accept": {Value: "*/*"},
|
|
||||||
"X-Cookie": {Value: "A"},
|
|
||||||
},
|
|
||||||
Method: "GET",
|
|
||||||
Tor: model.ArchivalHTTPTor{},
|
|
||||||
Transport: "tcp",
|
|
||||||
URL: "http://x.org/",
|
|
||||||
},
|
|
||||||
Response: model.ArchivalHTTPResponse{
|
|
||||||
Body: model.ArchivalMaybeBinaryData{},
|
|
||||||
BodyIsTruncated: false,
|
|
||||||
Code: 0,
|
|
||||||
HeadersList: nil,
|
|
||||||
Headers: nil,
|
|
||||||
Locations: nil,
|
|
||||||
},
|
|
||||||
T: deltaSinceTraceTime(2),
|
|
||||||
}},
|
|
||||||
}, {
|
|
||||||
name: "with success",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{{
|
|
||||||
Failure: nil,
|
|
||||||
Finished: traceTime(2),
|
|
||||||
Method: "GET",
|
|
||||||
RequestHeaders: http.Header{
|
|
||||||
"Accept": {"*/*"},
|
|
||||||
"X-Cookie": {"A", "B", "C"},
|
|
||||||
},
|
|
||||||
ResponseBody: []byte("0xdeadbeef"),
|
|
||||||
ResponseBodyIsTruncated: true,
|
|
||||||
ResponseBodyLength: 10,
|
|
||||||
ResponseHeaders: http.Header{
|
|
||||||
"Server": {"antani/1.0"},
|
|
||||||
"X-Cookie-Reply": {"C", "D", "F"},
|
|
||||||
"Location": {"https://x.org/", "https://x.org/robots.txt"},
|
|
||||||
},
|
|
||||||
Started: traceTime(1),
|
|
||||||
StatusCode: 302,
|
|
||||||
Transport: "tcp",
|
|
||||||
URL: "http://x.org/",
|
|
||||||
}},
|
|
||||||
Network: []*NetworkEvent{},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: []model.ArchivalHTTPRequestResult{{
|
|
||||||
Failure: nil,
|
|
||||||
Request: model.ArchivalHTTPRequest{
|
|
||||||
Body: model.ArchivalMaybeBinaryData{},
|
|
||||||
BodyIsTruncated: false,
|
|
||||||
HeadersList: []model.ArchivalHTTPHeader{{
|
|
||||||
Key: "Accept",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "*/*",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Key: "X-Cookie",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "A",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Key: "X-Cookie",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "B",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Key: "X-Cookie",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "C",
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
Headers: map[string]model.ArchivalMaybeBinaryData{
|
|
||||||
"Accept": {Value: "*/*"},
|
|
||||||
"X-Cookie": {Value: "A"},
|
|
||||||
},
|
|
||||||
Method: "GET",
|
|
||||||
Tor: model.ArchivalHTTPTor{},
|
|
||||||
Transport: "tcp",
|
|
||||||
URL: "http://x.org/",
|
|
||||||
},
|
|
||||||
Response: model.ArchivalHTTPResponse{
|
|
||||||
Body: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "0xdeadbeef",
|
|
||||||
},
|
|
||||||
BodyIsTruncated: true,
|
|
||||||
Code: 302,
|
|
||||||
HeadersList: []model.ArchivalHTTPHeader{{
|
|
||||||
Key: "Location",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "https://x.org/",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Key: "Location",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "https://x.org/robots.txt",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Key: "Server",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "antani/1.0",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Key: "X-Cookie-Reply",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "C",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Key: "X-Cookie-Reply",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "D",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Key: "X-Cookie-Reply",
|
|
||||||
Value: model.ArchivalMaybeBinaryData{
|
|
||||||
Value: "F",
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
Headers: map[string]model.ArchivalMaybeBinaryData{
|
|
||||||
"Server": {Value: "antani/1.0"},
|
|
||||||
"X-Cookie-Reply": {Value: "C"},
|
|
||||||
"Location": {Value: "https://x.org/"},
|
|
||||||
},
|
|
||||||
Locations: []string{
|
|
||||||
"https://x.org/",
|
|
||||||
"https://x.org/robots.txt",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
T: deltaSinceTraceTime(2),
|
|
||||||
}},
|
|
||||||
}, {
|
|
||||||
name: "The result is sorted by the value of T",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{{
|
|
||||||
Failure: nil,
|
|
||||||
Finished: traceTime(3),
|
|
||||||
Method: "",
|
|
||||||
RequestHeaders: map[string][]string{},
|
|
||||||
ResponseBody: []byte{},
|
|
||||||
ResponseBodyIsTruncated: false,
|
|
||||||
ResponseBodyLength: 0,
|
|
||||||
ResponseHeaders: map[string][]string{},
|
|
||||||
Started: time.Time{},
|
|
||||||
StatusCode: 0,
|
|
||||||
Transport: "",
|
|
||||||
URL: "",
|
|
||||||
}, {
|
|
||||||
Failure: nil,
|
|
||||||
Finished: traceTime(2),
|
|
||||||
Method: "",
|
|
||||||
RequestHeaders: map[string][]string{},
|
|
||||||
ResponseBody: []byte{},
|
|
||||||
ResponseBodyIsTruncated: false,
|
|
||||||
ResponseBodyLength: 0,
|
|
||||||
ResponseHeaders: map[string][]string{},
|
|
||||||
Started: time.Time{},
|
|
||||||
StatusCode: 0,
|
|
||||||
Transport: "",
|
|
||||||
URL: "",
|
|
||||||
}, {
|
|
||||||
Failure: nil,
|
|
||||||
Finished: traceTime(5),
|
|
||||||
Method: "",
|
|
||||||
RequestHeaders: map[string][]string{},
|
|
||||||
ResponseBody: []byte{},
|
|
||||||
ResponseBodyIsTruncated: false,
|
|
||||||
ResponseBodyLength: 0,
|
|
||||||
ResponseHeaders: map[string][]string{},
|
|
||||||
Started: time.Time{},
|
|
||||||
StatusCode: 0,
|
|
||||||
Transport: "",
|
|
||||||
URL: "",
|
|
||||||
}},
|
|
||||||
Network: []*NetworkEvent{},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: []model.ArchivalHTTPRequestResult{{
|
|
||||||
Failure: nil,
|
|
||||||
Request: model.ArchivalHTTPRequest{},
|
|
||||||
Response: model.ArchivalHTTPResponse{},
|
|
||||||
T: deltaSinceTraceTime(5),
|
|
||||||
}, {
|
|
||||||
Failure: nil,
|
|
||||||
Request: model.ArchivalHTTPRequest{},
|
|
||||||
Response: model.ArchivalHTTPResponse{},
|
|
||||||
T: deltaSinceTraceTime(3),
|
|
||||||
}, {
|
|
||||||
Failure: nil,
|
|
||||||
Request: model.ArchivalHTTPRequest{},
|
|
||||||
Response: model.ArchivalHTTPResponse{},
|
|
||||||
T: deltaSinceTraceTime(2),
|
|
||||||
}},
|
|
||||||
}}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
tr := &Trace{
|
|
||||||
DNSLookupHTTPS: tt.fields.DNSLookupHTTPS,
|
|
||||||
DNSLookupHost: tt.fields.DNSLookupHost,
|
|
||||||
DNSRoundTrip: tt.fields.DNSRoundTrip,
|
|
||||||
HTTPRoundTrip: tt.fields.HTTPRoundTrip,
|
|
||||||
Network: tt.fields.Network,
|
|
||||||
QUICHandshake: tt.fields.QUICHandshake,
|
|
||||||
TLSHandshake: tt.fields.TLSHandshake,
|
|
||||||
}
|
|
||||||
gotOut := tr.NewArchivalHTTPRequestResultList(tt.args.begin)
|
|
||||||
if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTraceNewArchivalDNSLookupResultList(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
DNSLookupHTTPS []*DNSLookupEvent
|
|
||||||
DNSLookupHost []*DNSLookupEvent
|
|
||||||
DNSRoundTrip []*DNSRoundTripEvent
|
|
||||||
HTTPRoundTrip []*HTTPRoundTripEvent
|
|
||||||
Network []*NetworkEvent
|
|
||||||
QUICHandshake []*QUICTLSHandshakeEvent
|
|
||||||
TLSHandshake []*QUICTLSHandshakeEvent
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
begin time.Time
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
wantOut []model.ArchivalDNSLookupResult
|
|
||||||
}{{
|
|
||||||
name: "with empty trace",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: nil,
|
|
||||||
}, {
|
|
||||||
name: "with NXDOMAIN failure",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{{
|
|
||||||
ALPNs: nil,
|
|
||||||
Addresses: nil,
|
|
||||||
Domain: "example.com",
|
|
||||||
Failure: netxlite.NewTopLevelGenericErrWrapper(errors.New(netxlite.DNSNoSuchHostSuffix)),
|
|
||||||
Finished: traceTime(2),
|
|
||||||
LookupType: "", // not processed
|
|
||||||
ResolverAddress: "8.8.8.8:53",
|
|
||||||
ResolverNetwork: "udp",
|
|
||||||
Started: traceTime(1),
|
|
||||||
}},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: []model.ArchivalDNSLookupResult{{
|
|
||||||
Answers: nil,
|
|
||||||
Engine: "udp",
|
|
||||||
Failure: failureFromString(netxlite.FailureDNSNXDOMAINError),
|
|
||||||
Hostname: "example.com",
|
|
||||||
QueryType: "A",
|
|
||||||
ResolverHostname: nil,
|
|
||||||
ResolverPort: nil,
|
|
||||||
ResolverAddress: "8.8.8.8:53",
|
|
||||||
T: deltaSinceTraceTime(2),
|
|
||||||
}, {
|
|
||||||
Answers: nil,
|
|
||||||
Engine: "udp",
|
|
||||||
Failure: failureFromString(netxlite.FailureDNSNXDOMAINError),
|
|
||||||
Hostname: "example.com",
|
|
||||||
QueryType: "AAAA",
|
|
||||||
ResolverHostname: nil,
|
|
||||||
ResolverPort: nil,
|
|
||||||
ResolverAddress: "8.8.8.8:53",
|
|
||||||
T: deltaSinceTraceTime(2),
|
|
||||||
}},
|
|
||||||
}, {
|
|
||||||
name: "with success for A and AAAA",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{{
|
|
||||||
ALPNs: nil,
|
|
||||||
Addresses: []string{
|
|
||||||
"8.8.8.8", "8.8.4.4", "2001:4860:4860::8844",
|
|
||||||
},
|
|
||||||
Domain: "dns.google",
|
|
||||||
Failure: nil,
|
|
||||||
Finished: traceTime(2),
|
|
||||||
LookupType: "", // not processed
|
|
||||||
ResolverAddress: "8.8.8.8:53",
|
|
||||||
ResolverNetwork: "udp",
|
|
||||||
Started: traceTime(1),
|
|
||||||
}},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: []model.ArchivalDNSLookupResult{{
|
|
||||||
Answers: []model.ArchivalDNSAnswer{{
|
|
||||||
ASN: 15169,
|
|
||||||
ASOrgName: "Google LLC",
|
|
||||||
AnswerType: "A",
|
|
||||||
Hostname: "",
|
|
||||||
IPv4: "8.8.8.8",
|
|
||||||
IPv6: "",
|
|
||||||
TTL: nil,
|
|
||||||
}, {
|
|
||||||
ASN: 15169,
|
|
||||||
ASOrgName: "Google LLC",
|
|
||||||
AnswerType: "A",
|
|
||||||
Hostname: "",
|
|
||||||
IPv4: "8.8.4.4",
|
|
||||||
IPv6: "",
|
|
||||||
TTL: nil,
|
|
||||||
}},
|
|
||||||
Engine: "udp",
|
|
||||||
Failure: nil,
|
|
||||||
Hostname: "dns.google",
|
|
||||||
QueryType: "A",
|
|
||||||
ResolverHostname: nil,
|
|
||||||
ResolverPort: nil,
|
|
||||||
ResolverAddress: "8.8.8.8:53",
|
|
||||||
T: deltaSinceTraceTime(2),
|
|
||||||
}, {
|
|
||||||
Answers: []model.ArchivalDNSAnswer{{
|
|
||||||
ASN: 15169,
|
|
||||||
ASOrgName: "Google LLC",
|
|
||||||
AnswerType: "AAAA",
|
|
||||||
Hostname: "",
|
|
||||||
IPv4: "",
|
|
||||||
IPv6: "2001:4860:4860::8844",
|
|
||||||
TTL: nil,
|
|
||||||
}},
|
|
||||||
Engine: "udp",
|
|
||||||
Failure: nil,
|
|
||||||
Hostname: "dns.google",
|
|
||||||
QueryType: "AAAA",
|
|
||||||
ResolverHostname: nil,
|
|
||||||
ResolverPort: nil,
|
|
||||||
ResolverAddress: "8.8.8.8:53",
|
|
||||||
T: deltaSinceTraceTime(2),
|
|
||||||
}},
|
|
||||||
}, {
|
|
||||||
name: "when a domain has no AAAA addresses",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{{
|
|
||||||
ALPNs: nil,
|
|
||||||
Addresses: []string{
|
|
||||||
"8.8.8.8", "8.8.4.4",
|
|
||||||
},
|
|
||||||
Domain: "dns.google",
|
|
||||||
Failure: nil,
|
|
||||||
Finished: traceTime(2),
|
|
||||||
LookupType: "", // not processed
|
|
||||||
ResolverAddress: "8.8.8.8:53",
|
|
||||||
ResolverNetwork: "udp",
|
|
||||||
Started: traceTime(1),
|
|
||||||
}},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: []model.ArchivalDNSLookupResult{{
|
|
||||||
Answers: []model.ArchivalDNSAnswer{{
|
|
||||||
ASN: 15169,
|
|
||||||
ASOrgName: "Google LLC",
|
|
||||||
AnswerType: "A",
|
|
||||||
Hostname: "",
|
|
||||||
IPv4: "8.8.8.8",
|
|
||||||
IPv6: "",
|
|
||||||
TTL: nil,
|
|
||||||
}, {
|
|
||||||
ASN: 15169,
|
|
||||||
ASOrgName: "Google LLC",
|
|
||||||
AnswerType: "A",
|
|
||||||
Hostname: "",
|
|
||||||
IPv4: "8.8.4.4",
|
|
||||||
IPv6: "",
|
|
||||||
TTL: nil,
|
|
||||||
}},
|
|
||||||
Engine: "udp",
|
|
||||||
Failure: nil,
|
|
||||||
Hostname: "dns.google",
|
|
||||||
QueryType: "A",
|
|
||||||
ResolverHostname: nil,
|
|
||||||
ResolverPort: nil,
|
|
||||||
ResolverAddress: "8.8.8.8:53",
|
|
||||||
T: deltaSinceTraceTime(2),
|
|
||||||
}},
|
|
||||||
}}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
tr := &Trace{
|
|
||||||
DNSLookupHTTPS: tt.fields.DNSLookupHTTPS,
|
|
||||||
DNSLookupHost: tt.fields.DNSLookupHost,
|
|
||||||
DNSRoundTrip: tt.fields.DNSRoundTrip,
|
|
||||||
HTTPRoundTrip: tt.fields.HTTPRoundTrip,
|
|
||||||
Network: tt.fields.Network,
|
|
||||||
QUICHandshake: tt.fields.QUICHandshake,
|
|
||||||
TLSHandshake: tt.fields.TLSHandshake,
|
|
||||||
}
|
|
||||||
gotOut := tr.NewArchivalDNSLookupResultList(tt.args.begin)
|
|
||||||
if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTraceNewArchivalNetworkEventList(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
DNSLookupHTTPS []*DNSLookupEvent
|
|
||||||
DNSLookupHost []*DNSLookupEvent
|
|
||||||
DNSRoundTrip []*DNSRoundTripEvent
|
|
||||||
HTTPRoundTrip []*HTTPRoundTripEvent
|
|
||||||
Network []*NetworkEvent
|
|
||||||
QUICHandshake []*QUICTLSHandshakeEvent
|
|
||||||
TLSHandshake []*QUICTLSHandshakeEvent
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
begin time.Time
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
wantOut []model.ArchivalNetworkEvent
|
|
||||||
}{{
|
|
||||||
name: "with empty trace",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: nil,
|
|
||||||
}, {
|
|
||||||
name: "we fill all the fields we should be filling",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{{
|
|
||||||
Count: 1234,
|
|
||||||
Failure: netxlite.NewTopLevelGenericErrWrapper(io.EOF),
|
|
||||||
Finished: traceTime(2),
|
|
||||||
Network: "tcp",
|
|
||||||
Operation: netxlite.ReadOperation,
|
|
||||||
RemoteAddr: "8.8.8.8:443",
|
|
||||||
Started: traceTime(1),
|
|
||||||
}},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: []model.ArchivalNetworkEvent{{
|
|
||||||
Address: "8.8.8.8:443",
|
|
||||||
Failure: failureFromString(netxlite.FailureEOFError),
|
|
||||||
NumBytes: 1234,
|
|
||||||
Operation: netxlite.ReadOperation,
|
|
||||||
Proto: "tcp",
|
|
||||||
T: deltaSinceTraceTime(2),
|
|
||||||
Tags: nil,
|
|
||||||
}},
|
|
||||||
}}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
tr := &Trace{
|
|
||||||
DNSLookupHTTPS: tt.fields.DNSLookupHTTPS,
|
|
||||||
DNSLookupHost: tt.fields.DNSLookupHost,
|
|
||||||
DNSRoundTrip: tt.fields.DNSRoundTrip,
|
|
||||||
HTTPRoundTrip: tt.fields.HTTPRoundTrip,
|
|
||||||
Network: tt.fields.Network,
|
|
||||||
QUICHandshake: tt.fields.QUICHandshake,
|
|
||||||
TLSHandshake: tt.fields.TLSHandshake,
|
|
||||||
}
|
|
||||||
gotOut := tr.NewArchivalNetworkEventList(tt.args.begin)
|
|
||||||
if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTraceNewArchivalTLSHandshakeResultList(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
DNSLookupHTTPS []*DNSLookupEvent
|
|
||||||
DNSLookupHost []*DNSLookupEvent
|
|
||||||
DNSRoundTrip []*DNSRoundTripEvent
|
|
||||||
HTTPRoundTrip []*HTTPRoundTripEvent
|
|
||||||
Network []*NetworkEvent
|
|
||||||
QUICHandshake []*QUICTLSHandshakeEvent
|
|
||||||
TLSHandshake []*QUICTLSHandshakeEvent
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
begin time.Time
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
wantOut []model.ArchivalTLSOrQUICHandshakeResult
|
|
||||||
}{{
|
|
||||||
name: "with empty trace",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: nil,
|
|
||||||
}, {
|
|
||||||
name: "we fill all the fields we should be filling",
|
|
||||||
fields: fields{
|
|
||||||
DNSLookupHTTPS: []*DNSLookupEvent{},
|
|
||||||
DNSLookupHost: []*DNSLookupEvent{},
|
|
||||||
DNSRoundTrip: []*DNSRoundTripEvent{},
|
|
||||||
HTTPRoundTrip: []*HTTPRoundTripEvent{},
|
|
||||||
Network: []*NetworkEvent{},
|
|
||||||
QUICHandshake: []*QUICTLSHandshakeEvent{},
|
|
||||||
TLSHandshake: []*QUICTLSHandshakeEvent{{
|
|
||||||
ALPN: []string{"h2", "http/1.1"},
|
|
||||||
CipherSuite: "TLS_AES_128_GCM_SHA256",
|
|
||||||
Failure: netxlite.NewTopLevelGenericErrWrapper(io.EOF),
|
|
||||||
Finished: traceTime(2),
|
|
||||||
NegotiatedProto: "h2",
|
|
||||||
Network: "tcp",
|
|
||||||
PeerCerts: [][]byte{
|
|
||||||
[]byte("deadbeef"),
|
|
||||||
[]byte("xox"),
|
|
||||||
},
|
|
||||||
RemoteAddr: "8.8.8.8:443",
|
|
||||||
SNI: "dns.google",
|
|
||||||
SkipVerify: true,
|
|
||||||
Started: traceTime(1),
|
|
||||||
TLSVersion: "TLSv1.3",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
begin: traceTime(0),
|
|
||||||
},
|
|
||||||
wantOut: []model.ArchivalTLSOrQUICHandshakeResult{{
|
|
||||||
CipherSuite: "TLS_AES_128_GCM_SHA256",
|
|
||||||
Failure: failureFromString(netxlite.FailureEOFError),
|
|
||||||
NegotiatedProtocol: "h2",
|
|
||||||
NoTLSVerify: true,
|
|
||||||
PeerCertificates: []model.ArchivalMaybeBinaryData{{
|
|
||||||
Value: "deadbeef",
|
|
||||||
}, {
|
|
||||||
Value: "xox",
|
|
||||||
}},
|
|
||||||
ServerName: "dns.google",
|
|
||||||
T: deltaSinceTraceTime(2),
|
|
||||||
Tags: nil,
|
|
||||||
TLSVersion: "TLSv1.3",
|
|
||||||
}},
|
|
||||||
}}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
tr := &Trace{
|
|
||||||
DNSLookupHTTPS: tt.fields.DNSLookupHTTPS,
|
|
||||||
DNSLookupHost: tt.fields.DNSLookupHost,
|
|
||||||
DNSRoundTrip: tt.fields.DNSRoundTrip,
|
|
||||||
HTTPRoundTrip: tt.fields.HTTPRoundTrip,
|
|
||||||
Network: tt.fields.Network,
|
|
||||||
QUICHandshake: tt.fields.QUICHandshake,
|
|
||||||
TLSHandshake: tt.fields.TLSHandshake,
|
|
||||||
}
|
|
||||||
gotOut := tr.NewArchivalTLSHandshakeResultList(tt.args.begin)
|
|
||||||
if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,9 +20,6 @@ import (
|
||||||
"github.com/ooni/probe-cli/v3/internal/kvstore"
|
"github.com/ooni/probe-cli/v3/internal/kvstore"
|
||||||
"github.com/ooni/probe-cli/v3/internal/legacy/assetsdir"
|
"github.com/ooni/probe-cli/v3/internal/legacy/assetsdir"
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite/filtering"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/version"
|
"github.com/ooni/probe-cli/v3/internal/version"
|
||||||
"github.com/pborman/getopt/v2"
|
"github.com/pborman/getopt/v2"
|
||||||
)
|
)
|
||||||
|
@ -30,7 +27,6 @@ import (
|
||||||
// Options contains the options you can set from the CLI.
|
// Options contains the options you can set from the CLI.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Annotations []string
|
Annotations []string
|
||||||
Censor string
|
|
||||||
ExtraOptions []string
|
ExtraOptions []string
|
||||||
HomeDir string
|
HomeDir string
|
||||||
Inputs []string
|
Inputs []string
|
||||||
|
@ -65,10 +61,6 @@ func init() {
|
||||||
getopt.FlagLong(
|
getopt.FlagLong(
|
||||||
&globalOptions.Annotations, "annotation", 'A', "Add annotaton", "KEY=VALUE",
|
&globalOptions.Annotations, "annotation", 'A', "Add annotaton", "KEY=VALUE",
|
||||||
)
|
)
|
||||||
getopt.FlagLong(
|
|
||||||
&globalOptions.Censor, "censor", 0,
|
|
||||||
"Specifies censorship rules to apply for QA purposes", "FILE",
|
|
||||||
)
|
|
||||||
getopt.FlagLong(
|
getopt.FlagLong(
|
||||||
&globalOptions.ExtraOptions, "option", 'O',
|
&globalOptions.ExtraOptions, "option", 'O',
|
||||||
"Pass an option to the experiment", "KEY=VALUE",
|
"Pass an option to the experiment", "KEY=VALUE",
|
||||||
|
@ -316,17 +308,6 @@ func MainWithConfiguration(experimentName string, currentOptions Options) {
|
||||||
}
|
}
|
||||||
log.Log = logger
|
log.Log = logger
|
||||||
|
|
||||||
if currentOptions.Censor != "" {
|
|
||||||
config, err := filtering.NewTProxyConfig(currentOptions.Censor)
|
|
||||||
runtimex.PanicOnError(err, "cannot parse --censor file as JSON")
|
|
||||||
tproxy, err := filtering.NewTProxy(config, log.Log)
|
|
||||||
runtimex.PanicOnError(err, "cannot create tproxy instance")
|
|
||||||
defer tproxy.Close()
|
|
||||||
netxlite.TProxy = tproxy
|
|
||||||
log.Infof("miniooni: disabling submission with --censor to avoid pulluting OONI data")
|
|
||||||
currentOptions.NoCollector = true
|
|
||||||
}
|
|
||||||
|
|
||||||
//Mon Jan 2 15:04:05 -0700 MST 2006
|
//Mon Jan 2 15:04:05 -0700 MST 2006
|
||||||
log.Infof("Current time: %s", time.Now().Format("2006-01-02 15:04:05 MST"))
|
log.Infof("Current time: %s", time.Now().Format("2006-01-02 15:04:05 MST"))
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/ooni/probe-cli/v3/internal/cmd/oohelper/internal"
|
"github.com/ooni/probe-cli/v3/internal/cmd/oohelper/internal"
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webstepsx"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||||
"github.com/ooni/probe-cli/v3/internal/measurex"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
)
|
)
|
||||||
|
@ -25,7 +23,6 @@ var (
|
||||||
resolver model.Resolver
|
resolver model.Resolver
|
||||||
server = flag.String("server", "", "URL of the test helper")
|
server = flag.String("server", "", "URL of the test helper")
|
||||||
target = flag.String("target", "", "Target URL for the test helper")
|
target = flag.String("target", "", "Target URL for the test helper")
|
||||||
fwebsteps = flag.Bool("websteps", false, "Use the websteps TH")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func newhttpclient() *http.Client {
|
func newhttpclient() *http.Client {
|
||||||
|
@ -54,34 +51,12 @@ func main() {
|
||||||
}
|
}
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
log.SetLevel(logmap[*debug])
|
log.SetLevel(logmap[*debug])
|
||||||
apimap := map[bool]func() interface{}{
|
cresp := wcth()
|
||||||
false: wcth,
|
|
||||||
true: webstepsth,
|
|
||||||
}
|
|
||||||
cresp := apimap[*fwebsteps]()
|
|
||||||
data, err := json.MarshalIndent(cresp, "", " ")
|
data, err := json.MarshalIndent(cresp, "", " ")
|
||||||
runtimex.PanicOnError(err, "json.MarshalIndent failed")
|
runtimex.PanicOnError(err, "json.MarshalIndent failed")
|
||||||
fmt.Printf("%s\n", string(data))
|
fmt.Printf("%s\n", string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func webstepsth() interface{} {
|
|
||||||
serverURL := *server
|
|
||||||
if serverURL == "" {
|
|
||||||
serverURL = "https://1.th.ooni.org/api/v1/websteps"
|
|
||||||
}
|
|
||||||
clnt := &webstepsx.THClient{
|
|
||||||
DNServers: []*measurex.ResolverInfo{{
|
|
||||||
Network: "udp",
|
|
||||||
Address: "8.8.4.4:53",
|
|
||||||
}},
|
|
||||||
HTTPClient: httpClient,
|
|
||||||
ServerURL: serverURL,
|
|
||||||
}
|
|
||||||
cresp, err := clnt.Run(ctx, *target)
|
|
||||||
runtimex.PanicOnError(err, "client.Run failed")
|
|
||||||
return cresp
|
|
||||||
}
|
|
||||||
|
|
||||||
func wcth() interface{} {
|
func wcth() interface{} {
|
||||||
serverURL := *server
|
serverURL := *server
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/ooni/probe-cli/v3/internal/cmd/oohelperd/internal/webconnectivity"
|
"github.com/ooni/probe-cli/v3/internal/cmd/oohelperd/internal/webconnectivity"
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webstepsx"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
@ -58,7 +57,6 @@ func main() {
|
||||||
|
|
||||||
func testableMain() {
|
func testableMain() {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("/api/v1/websteps", &webstepsx.THHandler{})
|
|
||||||
mux.Handle("/", webconnectivity.Handler{
|
mux.Handle("/", webconnectivity.Handler{
|
||||||
Client: httpx,
|
Client: httpx,
|
||||||
Dialer: dialer,
|
Dialer: dialer,
|
||||||
|
|
|
@ -29,7 +29,6 @@ import (
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/vanillator"
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/vanillator"
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity"
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity"
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webstepsx"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/whatsapp"
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/whatsapp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -352,18 +351,6 @@ var experimentsByName = map[string]func(*Session) *ExperimentBuilder{
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"websteps": func(session *Session) *ExperimentBuilder {
|
|
||||||
return &ExperimentBuilder{
|
|
||||||
build: func(config interface{}) *Experiment {
|
|
||||||
return NewExperiment(session, webstepsx.NewExperimentMeasurer(
|
|
||||||
*config.(*webstepsx.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &webstepsx.Config{},
|
|
||||||
inputPolicy: InputOrQueryBackend,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"whatsapp": func(session *Session) *ExperimentBuilder {
|
"whatsapp": func(session *Session) *ExperimentBuilder {
|
||||||
return &ExperimentBuilder{
|
return &ExperimentBuilder{
|
||||||
build: func(config interface{}) *Experiment {
|
build: func(config interface{}) *Experiment {
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
// Package webstepsx contains a websteps implementation
|
|
||||||
// based on the internal/measurex package.
|
|
||||||
//
|
|
||||||
// This implementation does not follow any existing spec
|
|
||||||
// rather we are modeling the spec on this one.
|
|
||||||
package webstepsx
|
|
|
@ -1,223 +0,0 @@
|
||||||
package webstepsx
|
|
||||||
|
|
||||||
//
|
|
||||||
// Measurer
|
|
||||||
//
|
|
||||||
// This file contains the client implementation.
|
|
||||||
//
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/measurex"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
testName = "websteps"
|
|
||||||
testVersion = "0.0.3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config contains the experiment config.
|
|
||||||
type Config struct{}
|
|
||||||
|
|
||||||
// TestKeys contains the experiment's test keys.
|
|
||||||
type TestKeys struct {
|
|
||||||
*measurex.ArchivalURLMeasurement
|
|
||||||
}
|
|
||||||
|
|
||||||
// Measurer performs the measurement.
|
|
||||||
type Measurer struct {
|
|
||||||
Config Config
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ model.ExperimentMeasurer = &Measurer{}
|
|
||||||
_ model.ExperimentMeasurerAsync = &Measurer{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewExperimentMeasurer creates a new ExperimentMeasurer.
|
|
||||||
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
|
|
||||||
return &Measurer{Config: config}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExperimentName implements ExperimentMeasurer.ExperExperimentName.
|
|
||||||
func (mx *Measurer) ExperimentName() string {
|
|
||||||
return testName
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExperimentVersion implements ExperimentMeasurer.ExperExperimentVersion.
|
|
||||||
func (mx *Measurer) ExperimentVersion() string {
|
|
||||||
return testVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrNoAvailableTestHelpers is emitted when there are no available test helpers.
|
|
||||||
ErrNoAvailableTestHelpers = errors.New("no available helpers")
|
|
||||||
|
|
||||||
// ErrNoInput indicates that no input was provided.
|
|
||||||
ErrNoInput = errors.New("no input provided")
|
|
||||||
|
|
||||||
// ErrInputIsNotAnURL indicates that the input is not an URL.
|
|
||||||
ErrInputIsNotAnURL = errors.New("input is not an URL")
|
|
||||||
|
|
||||||
// ErrUnsupportedInput indicates that the input URL scheme is unsupported.
|
|
||||||
ErrUnsupportedInput = errors.New("unsupported input scheme")
|
|
||||||
)
|
|
||||||
|
|
||||||
// RunAsync implements ExperimentMeasurerAsync.RunAsync.
|
|
||||||
func (mx *Measurer) RunAsync(
|
|
||||||
ctx context.Context, sess model.ExperimentSession, input string,
|
|
||||||
callbacks model.ExperimentCallbacks) (<-chan *model.ExperimentAsyncTestKeys, error) {
|
|
||||||
// 1. Parse and verify URL
|
|
||||||
URL, err := url.Parse(input)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrInputIsNotAnURL
|
|
||||||
}
|
|
||||||
if URL.Scheme != "http" && URL.Scheme != "https" {
|
|
||||||
return nil, ErrUnsupportedInput
|
|
||||||
}
|
|
||||||
// 2. Find the testhelper
|
|
||||||
testhelpers, _ := sess.GetTestHelpersByName("web-connectivity")
|
|
||||||
var testhelper *model.OOAPIService
|
|
||||||
for _, th := range testhelpers {
|
|
||||||
if th.Type == "https" {
|
|
||||||
testhelper = &th
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if testhelper == nil {
|
|
||||||
return nil, ErrNoAvailableTestHelpers
|
|
||||||
}
|
|
||||||
testhelper.Address = "https://1.th.ooni.org/api/v1/websteps" // TODO(bassosimone): remove!
|
|
||||||
out := make(chan *model.ExperimentAsyncTestKeys)
|
|
||||||
go mx.runAsync(ctx, sess, input, testhelper, out)
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var measurerResolvers = []*measurex.ResolverInfo{{
|
|
||||||
Network: "system",
|
|
||||||
Address: "",
|
|
||||||
}, {
|
|
||||||
Network: "udp",
|
|
||||||
Address: "8.8.4.4:53",
|
|
||||||
}}
|
|
||||||
|
|
||||||
func (mx *Measurer) runAsync(ctx context.Context, sess model.ExperimentSession,
|
|
||||||
URL string, th *model.OOAPIService, out chan<- *model.ExperimentAsyncTestKeys) {
|
|
||||||
defer close(out)
|
|
||||||
helper := &measurerMeasureURLHelper{
|
|
||||||
Clnt: sess.DefaultHTTPClient(),
|
|
||||||
Logger: sess.Logger(),
|
|
||||||
THURL: th.Address,
|
|
||||||
UserAgent: sess.UserAgent(),
|
|
||||||
}
|
|
||||||
mmx := &measurex.Measurer{
|
|
||||||
Begin: time.Now(),
|
|
||||||
HTTPClient: sess.DefaultHTTPClient(),
|
|
||||||
MeasureURLHelper: helper,
|
|
||||||
Logger: sess.Logger(),
|
|
||||||
Resolvers: measurerResolvers,
|
|
||||||
TLSHandshaker: netxlite.NewTLSHandshakerStdlib(sess.Logger()),
|
|
||||||
}
|
|
||||||
cookies := measurex.NewCookieJar()
|
|
||||||
const parallelism = 3
|
|
||||||
in := mmx.MeasureURLAndFollowRedirections(
|
|
||||||
ctx, parallelism, URL, measurex.NewHTTPRequestHeaderForMeasuring(), cookies)
|
|
||||||
for m := range in {
|
|
||||||
out <- &model.ExperimentAsyncTestKeys{
|
|
||||||
Extensions: map[string]int64{
|
|
||||||
archival.ExtHTTP.Name: archival.ExtHTTP.V,
|
|
||||||
archival.ExtDNS.Name: archival.ExtDNS.V,
|
|
||||||
archival.ExtNetevents.Name: archival.ExtNetevents.V,
|
|
||||||
archival.ExtTCPConnect.Name: archival.ExtTCPConnect.V,
|
|
||||||
archival.ExtTLSHandshake.Name: archival.ExtTLSHandshake.V,
|
|
||||||
},
|
|
||||||
Input: model.MeasurementTarget(m.URL),
|
|
||||||
MeasurementRuntime: m.TotalRuntime.Seconds(),
|
|
||||||
TestKeys: &TestKeys{
|
|
||||||
ArchivalURLMeasurement: measurex.NewArchivalURLMeasurement(m),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// measurerMeasureURLHelper injects the TH into the normal
|
|
||||||
// URL measurement flow implemented by measurex.
|
|
||||||
type measurerMeasureURLHelper struct {
|
|
||||||
// Clnt is the MANDATORY client to use
|
|
||||||
Clnt model.HTTPClient
|
|
||||||
|
|
||||||
// Logger is the MANDATORY Logger to use
|
|
||||||
Logger model.Logger
|
|
||||||
|
|
||||||
// THURL is the MANDATORY TH URL.
|
|
||||||
THURL string
|
|
||||||
|
|
||||||
// UserAgent is the OPTIONAL user-agent to use.
|
|
||||||
UserAgent string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mth *measurerMeasureURLHelper) LookupExtraHTTPEndpoints(
|
|
||||||
ctx context.Context, URL *url.URL, headers http.Header,
|
|
||||||
curEndpoints ...*measurex.HTTPEndpoint) (
|
|
||||||
[]*measurex.HTTPEndpoint, *measurex.THMeasurement, error) {
|
|
||||||
cc := &THClientCall{
|
|
||||||
Endpoints: measurex.HTTPEndpointsToEndpoints(curEndpoints),
|
|
||||||
HTTPClient: mth.Clnt,
|
|
||||||
Header: headers,
|
|
||||||
THURL: mth.THURL,
|
|
||||||
TargetURL: URL.String(),
|
|
||||||
UserAgent: mth.UserAgent,
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
ol := measurex.NewOperationLogger(
|
|
||||||
mth.Logger, "THClientCall %s", URL.String())
|
|
||||||
resp, err := cc.Call(ctx)
|
|
||||||
ol.Stop(err)
|
|
||||||
if err != nil {
|
|
||||||
return nil, resp, err
|
|
||||||
}
|
|
||||||
var out []*measurex.HTTPEndpoint
|
|
||||||
for _, epnt := range resp.Endpoints {
|
|
||||||
out = append(out, &measurex.HTTPEndpoint{
|
|
||||||
Domain: URL.Hostname(),
|
|
||||||
Network: epnt.Network,
|
|
||||||
Address: epnt.Address,
|
|
||||||
SNI: URL.Hostname(),
|
|
||||||
ALPN: measurex.ALPNForHTTPEndpoint(epnt.Network),
|
|
||||||
URL: URL,
|
|
||||||
Header: headers,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return out, resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run implements ExperimentMeasurer.Run.
|
|
||||||
func (mx *Measurer) Run(ctx context.Context, sess model.ExperimentSession,
|
|
||||||
measurement *model.Measurement, callbacks model.ExperimentCallbacks) error {
|
|
||||||
return errors.New("sync run is not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SummaryKeys contains summary keys for this experiment.
|
|
||||||
//
|
|
||||||
// Note that this structure is part of the ABI contract with ooniprobe
|
|
||||||
// therefore we should be careful when changing it.
|
|
||||||
type SummaryKeys struct {
|
|
||||||
Accessible bool `json:"accessible"`
|
|
||||||
Blocking string `json:"blocking"`
|
|
||||||
IsAnomaly bool `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
|
|
||||||
func (mx *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
|
||||||
sk := SummaryKeys{}
|
|
||||||
return sk, nil
|
|
||||||
}
|
|
|
@ -1,372 +0,0 @@
|
||||||
package webstepsx
|
|
||||||
|
|
||||||
//
|
|
||||||
// TH (Test Helper)
|
|
||||||
//
|
|
||||||
// This file contains an implementation of the
|
|
||||||
// (proposed) websteps test helper spec.
|
|
||||||
//
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/apex/log"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/measurex"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
//
|
|
||||||
// Messages exchanged by the TH client and server
|
|
||||||
//
|
|
||||||
|
|
||||||
// THClientRequest is the request received by the test helper.
|
|
||||||
type THClientRequest struct {
|
|
||||||
// Endpoints is a list of endpoints to measure.
|
|
||||||
Endpoints []*measurex.Endpoint
|
|
||||||
|
|
||||||
// URL is the URL we want to measure.
|
|
||||||
URL string
|
|
||||||
|
|
||||||
// HTTPRequestHeaders contains the request headers.
|
|
||||||
HTTPRequestHeaders http.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
// THServerResponse is the response from the test helper.
|
|
||||||
type THServerResponse = measurex.THMeasurement
|
|
||||||
|
|
||||||
// thMaxAcceptableBodySize is the maximum acceptable body size by TH code.
|
|
||||||
const thMaxAcceptableBodySize = 1 << 20
|
|
||||||
|
|
||||||
//
|
|
||||||
// TH client implementation
|
|
||||||
//
|
|
||||||
|
|
||||||
// THClient is the high-level API to invoke the TH. This API
|
|
||||||
// should be used by command line clients.
|
|
||||||
type THClient struct {
|
|
||||||
// DNSServers is the MANDATORY list of DNS-over-UDP
|
|
||||||
// servers to use to discover endpoints locally.
|
|
||||||
DNServers []*measurex.ResolverInfo
|
|
||||||
|
|
||||||
// HTTPClient is the MANDATORY HTTP client to
|
|
||||||
// use for contacting the TH.
|
|
||||||
HTTPClient model.HTTPClient
|
|
||||||
|
|
||||||
// ServerURL is the MANDATORY URL of the TH HTTP endpoint.
|
|
||||||
ServerURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run calls the TH and returns the response or an error.
|
|
||||||
//
|
|
||||||
// Arguments:
|
|
||||||
//
|
|
||||||
// - ctx is the context with timeout/deadline/cancellation
|
|
||||||
//
|
|
||||||
// - URL is the URL the TH server should measure for us
|
|
||||||
//
|
|
||||||
// Algorithm:
|
|
||||||
//
|
|
||||||
// - use DNSServers to discover extra endpoints for the target URL
|
|
||||||
//
|
|
||||||
// - call the TH using the HTTPClient and the ServerURL
|
|
||||||
//
|
|
||||||
// - return response or error.
|
|
||||||
func (c *THClient) Run(ctx context.Context, URL string) (*THServerResponse, error) {
|
|
||||||
parsed, err := url.Parse(URL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mx := measurex.NewMeasurerWithDefaultSettings()
|
|
||||||
var dns []*measurex.DNSMeasurement
|
|
||||||
const parallelism = 3
|
|
||||||
for m := range mx.LookupURLHostParallel(ctx, parallelism, parsed, c.DNServers...) {
|
|
||||||
dns = append(dns, m)
|
|
||||||
}
|
|
||||||
endpoints, err := measurex.AllEndpointsForURL(parsed, dns...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return (&THClientCall{
|
|
||||||
Endpoints: endpoints,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
Header: measurex.NewHTTPRequestHeaderForMeasuring(),
|
|
||||||
THURL: c.ServerURL,
|
|
||||||
TargetURL: URL,
|
|
||||||
}).Call(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// THClientCall allows to perform a single TH client call. Make sure
|
|
||||||
// you fill all the fields marked as MANDATORY before use.
|
|
||||||
type THClientCall struct {
|
|
||||||
// Endpoints contains the MANDATORY endpoints we discovered.
|
|
||||||
Endpoints []*measurex.Endpoint
|
|
||||||
|
|
||||||
// HTTPClient is the MANDATORY HTTP client to
|
|
||||||
// use for contacting the TH.
|
|
||||||
HTTPClient model.HTTPClient
|
|
||||||
|
|
||||||
// Header contains the MANDATORY request headers.
|
|
||||||
Header http.Header
|
|
||||||
|
|
||||||
// THURL is the MANDATORY test helper URL.
|
|
||||||
THURL string
|
|
||||||
|
|
||||||
// TargetURL is the MANDATORY URL to measure.
|
|
||||||
TargetURL string
|
|
||||||
|
|
||||||
// UserAgent is the OPTIONAL user-agent to use.
|
|
||||||
UserAgent string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call performs the specified TH call and returns either a response or an error.
|
|
||||||
func (c *THClientCall) Call(ctx context.Context) (*THServerResponse, error) {
|
|
||||||
creq := &THClientRequest{
|
|
||||||
Endpoints: c.Endpoints,
|
|
||||||
URL: c.TargetURL,
|
|
||||||
HTTPRequestHeaders: c.Header,
|
|
||||||
}
|
|
||||||
reqBody, err := json.Marshal(creq)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req, err := http.NewRequestWithContext(
|
|
||||||
ctx, "POST", c.THURL, bytes.NewReader(reqBody))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("User-Agent", c.UserAgent)
|
|
||||||
return c.httpClientDo(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// errTHRequestFailed is the error returned if the TH response is not 200 Ok.
|
|
||||||
var errTHRequestFailed = errors.New("th: request failed")
|
|
||||||
|
|
||||||
func (c *THClientCall) httpClientDo(req *http.Request) (*THServerResponse, error) {
|
|
||||||
resp, err := c.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != 200 { // THHandler returns either 400 or 200
|
|
||||||
return nil, errTHRequestFailed
|
|
||||||
}
|
|
||||||
r := io.LimitReader(resp.Body, thMaxAcceptableBodySize)
|
|
||||||
respBody, err := netxlite.ReadAllContext(req.Context(), r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var sresp THServerResponse
|
|
||||||
if err := json.Unmarshal(respBody, &sresp); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &sresp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// TH server implementation
|
|
||||||
//
|
|
||||||
|
|
||||||
// THHandler implements the test helper API.
|
|
||||||
//
|
|
||||||
// This handler exposes a unique HTTP endpoint that you need to
|
|
||||||
// mount to the desired path when creating the server.
|
|
||||||
//
|
|
||||||
// The canonical mount point for the HTTP endpoint is /api/v1/websteps.
|
|
||||||
//
|
|
||||||
// Accepted methods and request body:
|
|
||||||
//
|
|
||||||
// - we only accept POST;
|
|
||||||
//
|
|
||||||
// - we expect a THClientRequest as the body.
|
|
||||||
//
|
|
||||||
// Status code and response body:
|
|
||||||
//
|
|
||||||
// - on success, status is 200 and THServerResponse is the body;
|
|
||||||
//
|
|
||||||
// - on failure, status is 400 and there is no body.
|
|
||||||
//
|
|
||||||
type THHandler struct{}
|
|
||||||
|
|
||||||
// ServerHTTP implements http.Handler.ServeHTTP.
|
|
||||||
func (h *THHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
||||||
w.Header().Add("Server", fmt.Sprintf("oohelperd/%s", version.Version))
|
|
||||||
if req.Method != "POST" {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reader := io.LimitReader(req.Body, thMaxAcceptableBodySize)
|
|
||||||
data, err := netxlite.ReadAllContext(req.Context(), reader)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var creq THClientRequest
|
|
||||||
if err := json.Unmarshal(data, &creq); err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cresp, err := h.singleStep(req.Context(), &creq)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// We assume that the following call cannot fail because it's a
|
|
||||||
// clearly serializable data structure.
|
|
||||||
data, err = json.Marshal(cresp)
|
|
||||||
runtimex.PanicOnError(err, "json.Marshal failed")
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// singleStep performs a singleStep measurement.
|
|
||||||
//
|
|
||||||
// The function name derives from the definition (we invented)
|
|
||||||
// of "web steps". Each redirection is a step. For each step you
|
|
||||||
// need to figure out the endpoints to use with the DNS. After
|
|
||||||
// that, you need to check all endpoints. Because here we do not
|
|
||||||
// perform redirection, this is just a single "step".
|
|
||||||
//
|
|
||||||
// The algorithm is the following:
|
|
||||||
//
|
|
||||||
// 1. parse the URL and return error if it does not parse or
|
|
||||||
// the scheme is neither HTTP nor HTTPS;
|
|
||||||
//
|
|
||||||
// 2. discover additional endpoints using a suitable DoH
|
|
||||||
// resolver and the URL's hostname as the domain;
|
|
||||||
//
|
|
||||||
// 3. measure each discovered endpoint.
|
|
||||||
//
|
|
||||||
// The return value is either a THServerResponse or an error.
|
|
||||||
func (h *THHandler) singleStep(
|
|
||||||
ctx context.Context, req *THClientRequest) (*THServerResponse, error) {
|
|
||||||
mx := measurex.NewMeasurerWithDefaultSettings()
|
|
||||||
mx.MeasureURLHelper = &thMeasureURLHelper{req.Endpoints}
|
|
||||||
mx.Resolvers = []*measurex.ResolverInfo{{
|
|
||||||
Network: measurex.ResolverForeign,
|
|
||||||
ForeignResolver: thResolver,
|
|
||||||
}}
|
|
||||||
jar := measurex.NewCookieJar()
|
|
||||||
const parallelism = 3
|
|
||||||
meas, err := mx.MeasureURL(ctx, parallelism, req.URL, req.HTTPRequestHeaders, jar)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &THServerResponse{
|
|
||||||
DNS: meas.DNS,
|
|
||||||
Endpoints: h.simplifyEndpoints(meas.Endpoints),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *THHandler) simplifyEndpoints(
|
|
||||||
in []*measurex.HTTPEndpointMeasurement) (out []*measurex.HTTPEndpointMeasurement) {
|
|
||||||
for _, epnt := range in {
|
|
||||||
out = append(out, &measurex.HTTPEndpointMeasurement{
|
|
||||||
URL: epnt.URL,
|
|
||||||
Network: epnt.Network,
|
|
||||||
Address: epnt.Address,
|
|
||||||
Measurement: h.simplifyMeasurement(epnt.Measurement),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *THHandler) simplifyMeasurement(in *measurex.Measurement) (out *measurex.Measurement) {
|
|
||||||
out = &measurex.Measurement{
|
|
||||||
Connect: in.Connect,
|
|
||||||
TLSHandshake: h.simplifyHandshake(in.TLSHandshake),
|
|
||||||
QUICHandshake: h.simplifyHandshake(in.QUICHandshake),
|
|
||||||
LookupHost: in.LookupHost,
|
|
||||||
LookupHTTPSSvc: in.LookupHTTPSSvc,
|
|
||||||
HTTPRoundTrip: h.simplifyHTTPRoundTrip(in.HTTPRoundTrip),
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *THHandler) simplifyHandshake(
|
|
||||||
in []*measurex.QUICTLSHandshakeEvent) (out []*measurex.QUICTLSHandshakeEvent) {
|
|
||||||
for _, ev := range in {
|
|
||||||
out = append(out, &measurex.QUICTLSHandshakeEvent{
|
|
||||||
CipherSuite: ev.CipherSuite,
|
|
||||||
Failure: ev.Failure,
|
|
||||||
NegotiatedProto: ev.NegotiatedProto,
|
|
||||||
TLSVersion: ev.TLSVersion,
|
|
||||||
PeerCerts: nil,
|
|
||||||
Finished: 0,
|
|
||||||
RemoteAddr: ev.RemoteAddr,
|
|
||||||
SNI: ev.SNI,
|
|
||||||
ALPN: ev.ALPN,
|
|
||||||
SkipVerify: ev.SkipVerify,
|
|
||||||
Oddity: ev.Oddity,
|
|
||||||
Network: ev.Network,
|
|
||||||
Started: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *THHandler) simplifyHTTPRoundTrip(
|
|
||||||
in []*measurex.HTTPRoundTripEvent) (out []*measurex.HTTPRoundTripEvent) {
|
|
||||||
for _, ev := range in {
|
|
||||||
out = append(out, &measurex.HTTPRoundTripEvent{
|
|
||||||
Failure: ev.Failure,
|
|
||||||
Method: ev.Method,
|
|
||||||
URL: ev.URL,
|
|
||||||
RequestHeaders: ev.RequestHeaders,
|
|
||||||
StatusCode: ev.StatusCode,
|
|
||||||
ResponseHeaders: ev.ResponseHeaders,
|
|
||||||
ResponseBody: nil, // we don't transfer the body
|
|
||||||
ResponseBodyLength: ev.ResponseBodyLength,
|
|
||||||
ResponseBodyIsTruncated: ev.ResponseBodyIsTruncated,
|
|
||||||
ResponseBodyIsUTF8: ev.ResponseBodyIsUTF8,
|
|
||||||
Finished: ev.Finished,
|
|
||||||
Started: ev.Started,
|
|
||||||
Oddity: ev.Oddity,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type thMeasureURLHelper struct {
|
|
||||||
epnts []*measurex.Endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
func (thh *thMeasureURLHelper) LookupExtraHTTPEndpoints(
|
|
||||||
ctx context.Context, URL *url.URL, headers http.Header,
|
|
||||||
serverEpnts ...*measurex.HTTPEndpoint) (
|
|
||||||
epnts []*measurex.HTTPEndpoint, thMeaurement *measurex.THMeasurement, err error) {
|
|
||||||
for _, epnt := range thh.epnts {
|
|
||||||
epnts = append(epnts, &measurex.HTTPEndpoint{
|
|
||||||
Domain: URL.Hostname(),
|
|
||||||
Network: epnt.Network,
|
|
||||||
Address: epnt.Address,
|
|
||||||
SNI: URL.Hostname(),
|
|
||||||
ALPN: measurex.ALPNForHTTPEndpoint(epnt.Network),
|
|
||||||
URL: URL,
|
|
||||||
Header: headers, // but overriden later anyway
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// thResolverURL is the DNS resolver URL used by the TH. We use an
|
|
||||||
// encrypted resolver to reduce the risk that there is DNS-over-UDP
|
|
||||||
// censorship in the place where we deploy the TH.
|
|
||||||
const thResolverURL = "https://dns.google/dns-query"
|
|
||||||
|
|
||||||
// thResolver is the DNS resolver used by the TH.
|
|
||||||
//
|
|
||||||
// Here we're using github.com/apex/log as the logger, which
|
|
||||||
// is fine because this is backend only code.
|
|
||||||
var thResolver = netxlite.WrapResolver(log.Log, netxlite.NewSerialResolver(
|
|
||||||
netxlite.NewDNSOverHTTPSTransport(http.DefaultClient, thResolverURL),
|
|
||||||
))
|
|
|
@ -1,357 +0,0 @@
|
||||||
package filtering
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TProxyPolicy is a policy for TPRoxy.
|
|
||||||
type TProxyPolicy string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// TProxyPolicyTCPDropSYN simulates a SYN segment being dropped.
|
|
||||||
TProxyPolicyTCPDropSYN = TProxyPolicy("tcp-drop-syn")
|
|
||||||
|
|
||||||
// TProxyPolicyTCPRejectSYN simulates a closed TCP port.
|
|
||||||
TProxyPolicyTCPRejectSYN = TProxyPolicy("tcp-reject-syn")
|
|
||||||
|
|
||||||
// TProxyPolicyDropData drops outgoing data of an
|
|
||||||
// established TCP/UDP connection.
|
|
||||||
TProxyPolicyDropData = TProxyPolicy("drop-data")
|
|
||||||
|
|
||||||
// TProxyPolicyHijackDNS causes the dialer to replace the target
|
|
||||||
// address with the address of the local censored resolver.
|
|
||||||
TProxyPolicyHijackDNS = TProxyPolicy("hijack-dns")
|
|
||||||
|
|
||||||
// TProxyPolicyHijackTLS causes the dialer to replace the target
|
|
||||||
// address with the address of the local censored TLS server.
|
|
||||||
TProxyPolicyHijackTLS = TProxyPolicy("hijack-tls")
|
|
||||||
|
|
||||||
// TProxyPolicyHijackHTTP causes the dialer to replace the target
|
|
||||||
// address with the address of the local censored HTTP server.
|
|
||||||
TProxyPolicyHijackHTTP = TProxyPolicy("hijack-http")
|
|
||||||
)
|
|
||||||
|
|
||||||
// TProxyConfig contains configuration for TProxy.
|
|
||||||
type TProxyConfig struct {
|
|
||||||
// DNSCache is the cached used when the domains policy is "cache". Note
|
|
||||||
// that the map MUST contain FQDNs. That is, you need to append
|
|
||||||
// a final dot to the domain name (e.g., `example.com.`). If you
|
|
||||||
// use the NewTProxyConfig factory, you don't need to worry about this
|
|
||||||
// issue, because the factory will canonicalize non-canonical
|
|
||||||
// entries. Otherwise, you can explicitly call the CanonicalizeDNS
|
|
||||||
// method _before_ using the TProxy.
|
|
||||||
DNSCache map[string][]string
|
|
||||||
|
|
||||||
// Domains contains rules for filtering the lookup of domains. Note
|
|
||||||
// that the map MUST contain FQDNs. That is, you need to append
|
|
||||||
// a final dot to the domain name (e.g., `example.com.`). If you
|
|
||||||
// use the NewTProxyConfig factory, you don't need to worry about this
|
|
||||||
// issue, because the factory will canonicalize non-canonical
|
|
||||||
// entries. Otherwise, you can explicitly call the CanonicalizeDNS
|
|
||||||
// method _before_ using the TProxy.
|
|
||||||
Domains map[string]DNSAction
|
|
||||||
|
|
||||||
// Endpoints contains rules for filtering TCP/UDP endpoints.
|
|
||||||
Endpoints map[string]TProxyPolicy
|
|
||||||
|
|
||||||
// SNIs contains rules for filtering TLS SNIs.
|
|
||||||
SNIs map[string]TLSAction
|
|
||||||
|
|
||||||
// Hosts contains rules for filtering by HTTP host.
|
|
||||||
Hosts map[string]HTTPAction
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTProxyConfig reads the TProxyConfig from the given file.
|
|
||||||
func NewTProxyConfig(file string) (*TProxyConfig, error) {
|
|
||||||
data, err := os.ReadFile(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var config TProxyConfig
|
|
||||||
if err := json.Unmarshal(data, &config); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.CanonicalizeDNS()
|
|
||||||
return &config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanonicalizeDNS ensures all DNS names are canonicalized. This method
|
|
||||||
// modifies the TProxyConfig structure in place.
|
|
||||||
func (c *TProxyConfig) CanonicalizeDNS() {
|
|
||||||
domains := make(map[string]DNSAction)
|
|
||||||
for domain, policy := range c.Domains {
|
|
||||||
domains[dns.CanonicalName(domain)] = policy
|
|
||||||
}
|
|
||||||
c.Domains = domains
|
|
||||||
cache := make(map[string][]string)
|
|
||||||
for domain, addrs := range c.DNSCache {
|
|
||||||
cache[dns.CanonicalName(domain)] = addrs
|
|
||||||
}
|
|
||||||
c.DNSCache = cache
|
|
||||||
}
|
|
||||||
|
|
||||||
// TProxy is a model.UnderlyingNetworkLibrary that implements self censorship.
|
|
||||||
type TProxy struct {
|
|
||||||
// config contains settings for TProxy.
|
|
||||||
config *TProxyConfig
|
|
||||||
|
|
||||||
// dnsClient is the DNS client we'll internally use.
|
|
||||||
dnsClient model.Resolver
|
|
||||||
|
|
||||||
// dnsListener is the DNS listener.
|
|
||||||
dnsListener DNSListener
|
|
||||||
|
|
||||||
// httpListener is the HTTP listener.
|
|
||||||
httpListener net.Listener
|
|
||||||
|
|
||||||
// listenUDP allows overriding net.ListenUDP calls in tests
|
|
||||||
listenUDP func(network string, laddr *net.UDPAddr) (model.UDPLikeConn, error)
|
|
||||||
|
|
||||||
// logger is the underlying logger to use.
|
|
||||||
logger model.InfoLogger
|
|
||||||
|
|
||||||
// tlsListener is the TLS listener.
|
|
||||||
tlsListener net.Listener
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Constructor and destructor
|
|
||||||
//
|
|
||||||
|
|
||||||
// NewTProxy creates a new TProxy instance.
|
|
||||||
func NewTProxy(config *TProxyConfig, logger model.InfoLogger) (*TProxy, error) {
|
|
||||||
return newTProxy(config, logger, "127.0.0.1:0", "127.0.0.1:0", "127.0.0.1:0")
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTProxy(config *TProxyConfig, logger model.InfoLogger, dnsListenerAddr,
|
|
||||||
tlsListenerAddr, httpListenerAddr string) (*TProxy, error) {
|
|
||||||
p := &TProxy{
|
|
||||||
config: config,
|
|
||||||
listenUDP: func(network string, laddr *net.UDPAddr) (model.UDPLikeConn, error) {
|
|
||||||
return net.ListenUDP(network, laddr)
|
|
||||||
},
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
if err := p.newDNSListener(dnsListenerAddr); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p.newDNSClient(logger)
|
|
||||||
if err := p.newTLSListener(tlsListenerAddr, logger); err != nil {
|
|
||||||
p.dnsListener.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := p.newHTTPListener(httpListenerAddr); err != nil {
|
|
||||||
p.dnsListener.Close()
|
|
||||||
p.tlsListener.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *TProxy) newDNSListener(listenAddr string) error {
|
|
||||||
var err error
|
|
||||||
dnsProxy := &DNSProxy{Cache: p.config.DNSCache, OnQuery: p.onQuery}
|
|
||||||
p.dnsListener, err = dnsProxy.Start(listenAddr)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *TProxy) newDNSClient(logger model.DebugLogger) {
|
|
||||||
dialer := netxlite.NewDialerWithoutResolver(logger)
|
|
||||||
p.dnsClient = netxlite.NewResolverUDP(logger, dialer, p.dnsListener.LocalAddr().String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *TProxy) newTLSListener(listenAddr string, logger model.DebugLogger) error {
|
|
||||||
var err error
|
|
||||||
tlsProxy := &TLSProxy{OnIncomingSNI: p.onIncomingSNI}
|
|
||||||
p.tlsListener, err = tlsProxy.Start(listenAddr)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *TProxy) newHTTPListener(listenAddr string) error {
|
|
||||||
var err error
|
|
||||||
httpProxy := &HTTPProxy{OnIncomingHost: p.onIncomingHost}
|
|
||||||
p.httpListener, err = httpProxy.Start(listenAddr)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the resources used by a TProxy.
|
|
||||||
func (p *TProxy) Close() error {
|
|
||||||
p.dnsClient.CloseIdleConnections()
|
|
||||||
p.dnsListener.Close()
|
|
||||||
p.httpListener.Close()
|
|
||||||
p.tlsListener.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// QUIC
|
|
||||||
//
|
|
||||||
|
|
||||||
// ListenUDP implements netxlite.TProxy.ListenUDP.
|
|
||||||
func (p *TProxy) ListenUDP(network string, laddr *net.UDPAddr) (model.UDPLikeConn, error) {
|
|
||||||
pconn, err := p.listenUDP(network, laddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &tProxyUDPLikeConn{UDPLikeConn: pconn, proxy: p}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// tProxyUDPLikeConn is a TProxy-aware UDPLikeConn.
|
|
||||||
type tProxyUDPLikeConn struct {
|
|
||||||
// UDPLikeConn is the underlying conn type.
|
|
||||||
model.UDPLikeConn
|
|
||||||
|
|
||||||
// proxy refers to the TProxy.
|
|
||||||
proxy *TProxy
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTo implements UDPLikeConn.WriteTo. This function will
|
|
||||||
// apply the proper tproxy policies, if required.
|
|
||||||
func (c *tProxyUDPLikeConn) WriteTo(pkt []byte, addr net.Addr) (int, error) {
|
|
||||||
endpoint := fmt.Sprintf("%s/%s", addr.String(), addr.Network())
|
|
||||||
policy := c.proxy.config.Endpoints[endpoint]
|
|
||||||
switch policy {
|
|
||||||
case TProxyPolicyDropData:
|
|
||||||
c.proxy.logger.Infof("tproxy: WriteTo: %s => %s", endpoint, policy)
|
|
||||||
return len(pkt), nil
|
|
||||||
default:
|
|
||||||
return c.UDPLikeConn.WriteTo(pkt, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// System resolver
|
|
||||||
//
|
|
||||||
|
|
||||||
// LookupHost implements netxlite.TProxy.LookupHost.
|
|
||||||
func (p *TProxy) LookupHost(ctx context.Context, domain string) ([]string, error) {
|
|
||||||
return p.dnsClient.LookupHost(ctx, domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Dialer
|
|
||||||
//
|
|
||||||
|
|
||||||
// NewSimpleDialer implements netxlite.TProxy.NewTProxyDialer.
|
|
||||||
func (p *TProxy) NewSimpleDialer(timeout time.Duration) model.SimpleDialer {
|
|
||||||
return &tProxyDialer{
|
|
||||||
dialer: &net.Dialer{Timeout: timeout},
|
|
||||||
proxy: p,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tProxyDialer is a TProxy-aware Dialer.
|
|
||||||
type tProxyDialer struct {
|
|
||||||
// dialer is the underlying network dialer.
|
|
||||||
dialer *net.Dialer
|
|
||||||
|
|
||||||
// proxy refers to the TProxy.
|
|
||||||
proxy *TProxy
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialContext behaves like net.Dialer.DialContext. This function will
|
|
||||||
// apply the proper tproxy policies, if required.
|
|
||||||
func (d *tProxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
endpoint := fmt.Sprintf("%s/%s", address, network)
|
|
||||||
policy := d.proxy.config.Endpoints[endpoint]
|
|
||||||
switch policy {
|
|
||||||
case TProxyPolicyTCPDropSYN:
|
|
||||||
d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy)
|
|
||||||
var cancel context.CancelFunc
|
|
||||||
const timeout = 70 * time.Second
|
|
||||||
ctx, cancel = context.WithTimeout(ctx, timeout)
|
|
||||||
defer cancel()
|
|
||||||
<-ctx.Done()
|
|
||||||
return nil, errors.New("i/o timeout")
|
|
||||||
case TProxyPolicyTCPRejectSYN:
|
|
||||||
d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy)
|
|
||||||
return nil, netxlite.ECONNREFUSED
|
|
||||||
case TProxyPolicyHijackDNS:
|
|
||||||
d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy)
|
|
||||||
address = d.proxy.dnsListener.LocalAddr().String()
|
|
||||||
case TProxyPolicyHijackTLS:
|
|
||||||
d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy)
|
|
||||||
address = d.proxy.tlsListener.Addr().String()
|
|
||||||
case TProxyPolicyHijackHTTP:
|
|
||||||
d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy)
|
|
||||||
address = d.proxy.httpListener.Addr().String()
|
|
||||||
default:
|
|
||||||
// nothing
|
|
||||||
}
|
|
||||||
conn, err := d.dialer.DialContext(ctx, network, address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &tProxyConn{Conn: conn, proxy: d.proxy}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// tProxyConn is a TProxy-aware net.Conn.
|
|
||||||
type tProxyConn struct {
|
|
||||||
// Conn is the underlying conn.
|
|
||||||
net.Conn
|
|
||||||
|
|
||||||
// proxy refers to the TProxy.
|
|
||||||
proxy *TProxy
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write implements Conn.Write. This function will apply
|
|
||||||
// the proper tproxy policies, if required.
|
|
||||||
func (c *tProxyConn) Write(b []byte) (int, error) {
|
|
||||||
addr := c.Conn.RemoteAddr()
|
|
||||||
endpoint := fmt.Sprintf("%s/%s", addr.String(), addr.Network())
|
|
||||||
policy := c.proxy.config.Endpoints[endpoint]
|
|
||||||
switch policy {
|
|
||||||
case TProxyPolicyDropData:
|
|
||||||
c.proxy.logger.Infof("tproxy: Write: %s => %s", endpoint, policy)
|
|
||||||
return len(b), nil
|
|
||||||
default:
|
|
||||||
return c.Conn.Write(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Filtering policies implementation
|
|
||||||
//
|
|
||||||
|
|
||||||
// onQuery is called for filtering outgoing DNS queries.
|
|
||||||
func (p *TProxy) onQuery(domain string) DNSAction {
|
|
||||||
policy := p.config.Domains[domain]
|
|
||||||
if policy == "" {
|
|
||||||
policy = DNSActionPass
|
|
||||||
} else {
|
|
||||||
p.logger.Infof("tproxy: DNS: %s => %s", domain, policy)
|
|
||||||
}
|
|
||||||
return policy
|
|
||||||
}
|
|
||||||
|
|
||||||
// onIncomingSNI is called for filtering SNI values.
|
|
||||||
func (p *TProxy) onIncomingSNI(sni string) TLSAction {
|
|
||||||
policy := p.config.SNIs[sni]
|
|
||||||
if policy == "" {
|
|
||||||
policy = TLSActionPass
|
|
||||||
} else {
|
|
||||||
p.logger.Infof("tproxy: TLS: %s => %s", sni, policy)
|
|
||||||
}
|
|
||||||
return policy
|
|
||||||
}
|
|
||||||
|
|
||||||
// onIncomingHost is called for filtering HTTP hosts.
|
|
||||||
func (p *TProxy) onIncomingHost(host string) HTTPAction {
|
|
||||||
policy := p.config.Hosts[host]
|
|
||||||
if policy == "" {
|
|
||||||
policy = HTTPActionPass
|
|
||||||
} else {
|
|
||||||
p.logger.Infof("tproxy: HTTP: %s => %s", host, policy)
|
|
||||||
}
|
|
||||||
return policy
|
|
||||||
}
|
|
|
@ -1,578 +0,0 @@
|
||||||
package filtering
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/apex/log"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
// tProxyDialerAdapter adapts a netxlite.TProxyDialer to be a netxlite.Dialer.
|
|
||||||
type tProxyDialerAdapter struct {
|
|
||||||
model.SimpleDialer
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseIdleConnections implements Dialer.CloseIdleConnections.
|
|
||||||
func (*tProxyDialerAdapter) CloseIdleConnections() {
|
|
||||||
// nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewTProxyConfig(t *testing.T) {
|
|
||||||
t.Run("with nonexistent file", func(t *testing.T) {
|
|
||||||
config, err := NewTProxyConfig(filepath.Join("testdata", "nonexistent"))
|
|
||||||
if !errors.Is(err, syscall.ENOENT) {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if config != nil {
|
|
||||||
t.Fatal("expected nil config here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with file containing invalid JSON", func(t *testing.T) {
|
|
||||||
config, err := NewTProxyConfig(filepath.Join("testdata", "invalid.json"))
|
|
||||||
if err == nil || !strings.HasSuffix(err.Error(), "unexpected end of JSON input") {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if config != nil {
|
|
||||||
t.Fatal("expected nil config here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with file containing valid JSON", func(t *testing.T) {
|
|
||||||
config, err := NewTProxyConfig(filepath.Join("testdata", "valid.json"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if config == nil {
|
|
||||||
t.Fatal("expected non-nil config here")
|
|
||||||
}
|
|
||||||
if config.Domains["x.org."] != "pass" {
|
|
||||||
t.Fatal("did not auto-canonicalize config.Domains")
|
|
||||||
}
|
|
||||||
if len(config.DNSCache["dns.google."]) != 2 {
|
|
||||||
t.Fatal("did not auto-canonicalize config.DNSCache")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewTProxy(t *testing.T) {
|
|
||||||
t.Run("successful creation and destruction", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := proxy.Close(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("cannot create DNS listener", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{}
|
|
||||||
proxy, err := newTProxy(config, log.Log, "127.0.0.1", "", "")
|
|
||||||
if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if proxy != nil {
|
|
||||||
t.Fatal("expected nil proxy here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("cannot create TLS listener", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{}
|
|
||||||
proxy, err := newTProxy(config, log.Log, "127.0.0.1:0", "127.0.0.1", "")
|
|
||||||
if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if proxy != nil {
|
|
||||||
t.Fatal("expected nil proxy here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("cannot create HTTP listener", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{}
|
|
||||||
proxy, err := newTProxy(config, log.Log, "127.0.0.1:0", "127.0.0.1:0", "127.0.0.1")
|
|
||||||
if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if proxy != nil {
|
|
||||||
t.Fatal("expected nil proxy here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTProxyQUIC(t *testing.T) {
|
|
||||||
t.Run("ListenUDP", func(t *testing.T) {
|
|
||||||
t.Run("failure", func(t *testing.T) {
|
|
||||||
proxy, err := NewTProxy(&TProxyConfig{}, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
pconn, err := proxy.ListenUDP("tcp", &net.UDPAddr{})
|
|
||||||
if err == nil || !strings.HasSuffix(err.Error(), "unknown network tcp") {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if pconn != nil {
|
|
||||||
t.Fatal("expected nil pconn here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("success", func(t *testing.T) {
|
|
||||||
proxy, err := NewTProxy(&TProxyConfig{}, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
pconn, err := proxy.ListenUDP("udp", &net.UDPAddr{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
uconn := pconn.(*tProxyUDPLikeConn)
|
|
||||||
if uconn.proxy != proxy {
|
|
||||||
t.Fatal("proxy not correctly set")
|
|
||||||
}
|
|
||||||
if _, okay := uconn.UDPLikeConn.(*net.UDPConn); !okay {
|
|
||||||
t.Fatal("underlying connection should be an UDPConn")
|
|
||||||
}
|
|
||||||
uconn.Close()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("WriteTo", func(t *testing.T) {
|
|
||||||
t.Run("without the drop policy", func(t *testing.T) {
|
|
||||||
proxy, err := NewTProxy(&TProxyConfig{}, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
var called bool
|
|
||||||
proxy.listenUDP = func(network string, laddr *net.UDPAddr) (model.UDPLikeConn, error) {
|
|
||||||
return &mocks.UDPLikeConn{
|
|
||||||
MockWriteTo: func(p []byte, addr net.Addr) (int, error) {
|
|
||||||
called = true
|
|
||||||
return len(p), nil
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
pconn, err := proxy.ListenUDP("udp", &net.UDPAddr{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
data := make([]byte, 128)
|
|
||||||
count, err := pconn.WriteTo(data, &net.UDPAddr{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if count != len(data) {
|
|
||||||
t.Fatal("unexpected number of bytes written")
|
|
||||||
}
|
|
||||||
if !called {
|
|
||||||
t.Fatal("not called")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with the drop policy", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{
|
|
||||||
Endpoints: map[string]TProxyPolicy{
|
|
||||||
"127.0.0.1:1234/udp": TProxyPolicyDropData,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
var called bool
|
|
||||||
proxy.listenUDP = func(network string, laddr *net.UDPAddr) (model.UDPLikeConn, error) {
|
|
||||||
return &mocks.UDPLikeConn{
|
|
||||||
MockWriteTo: func(p []byte, addr net.Addr) (int, error) {
|
|
||||||
called = true
|
|
||||||
return len(p), nil
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
pconn, err := proxy.ListenUDP("udp", &net.UDPAddr{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
data := make([]byte, 128)
|
|
||||||
destAddr := &net.UDPAddr{
|
|
||||||
IP: net.IPv4(127, 0, 0, 1),
|
|
||||||
Port: 1234,
|
|
||||||
Zone: "",
|
|
||||||
}
|
|
||||||
count, err := pconn.WriteTo(data, destAddr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if count != len(data) {
|
|
||||||
t.Fatal("unexpected number of bytes written")
|
|
||||||
}
|
|
||||||
if called {
|
|
||||||
t.Fatal("called")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTProxyLookupHost(t *testing.T) {
|
|
||||||
t.Run("without filtering", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
ctx := context.Background()
|
|
||||||
addrs, err := proxy.LookupHost(ctx, "dns.google")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(addrs) < 2 {
|
|
||||||
t.Fatal("too few addrs")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with filtering", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{
|
|
||||||
Domains: map[string]DNSAction{
|
|
||||||
"dns.google.": DNSActionNXDOMAIN,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
ctx := context.Background()
|
|
||||||
addrs, err := proxy.LookupHost(ctx, "dns.google")
|
|
||||||
if err == nil || err.Error() != "dns_nxdomain_error" {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if len(addrs) != 0 {
|
|
||||||
t.Fatal("too many addrs")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTProxyOnIncomingSNI(t *testing.T) {
|
|
||||||
t.Run("without filtering", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{
|
|
||||||
Endpoints: map[string]TProxyPolicy{
|
|
||||||
"8.8.8.8:443/tcp": TProxyPolicyHijackTLS,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
ctx := context.Background()
|
|
||||||
dialer := proxy.NewSimpleDialer(10 * time.Second)
|
|
||||||
conn, err := dialer.DialContext(ctx, "tcp", "8.8.8.8:443")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
tconn := tls.Client(conn, &tls.Config{ServerName: "dns.google"})
|
|
||||||
err = tconn.HandshakeContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
tconn.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with filtering", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{
|
|
||||||
Endpoints: map[string]TProxyPolicy{
|
|
||||||
"8.8.8.8:443/tcp": TProxyPolicyHijackTLS,
|
|
||||||
},
|
|
||||||
SNIs: map[string]TLSAction{
|
|
||||||
"dns.google": TLSActionReset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
ctx := context.Background()
|
|
||||||
dialer := proxy.NewSimpleDialer(10 * time.Second)
|
|
||||||
conn, err := dialer.DialContext(ctx, "tcp", "8.8.8.8:443")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
tlsh := netxlite.NewTLSHandshakerStdlib(log.Log)
|
|
||||||
tconn, _, err := tlsh.Handshake(ctx, conn, &tls.Config{ServerName: "dns.google"})
|
|
||||||
if err == nil || err.Error() != netxlite.FailureConnectionReset {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if tconn != nil {
|
|
||||||
t.Fatal("expected nil tconn")
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTProxyOnIncomingHost(t *testing.T) {
|
|
||||||
t.Run("without filtering", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{
|
|
||||||
Endpoints: map[string]TProxyPolicy{
|
|
||||||
"130.192.16.171:80/tcp": TProxyPolicyHijackHTTP,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
dialer := proxy.NewSimpleDialer(10 * time.Second)
|
|
||||||
req, err := http.NewRequest("GET", "http://130.192.16.171:80", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
req.Host = "nexa.polito.it"
|
|
||||||
txp := &http.Transport{DialContext: dialer.DialContext}
|
|
||||||
resp, err := txp.RoundTrip(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with filtering", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{
|
|
||||||
Endpoints: map[string]TProxyPolicy{
|
|
||||||
"130.192.16.171:80/tcp": TProxyPolicyHijackHTTP,
|
|
||||||
},
|
|
||||||
Hosts: map[string]HTTPAction{
|
|
||||||
"nexa.polito.it": HTTPActionReset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
dialer := netxlite.WrapDialer(
|
|
||||||
log.Log,
|
|
||||||
netxlite.NewResolverStdlib(log.Log),
|
|
||||||
&tProxyDialerAdapter{
|
|
||||||
proxy.NewSimpleDialer(10 * time.Second),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
req, err := http.NewRequest("GET", "http://130.192.16.171:80", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
req.Host = "nexa.polito.it"
|
|
||||||
txp := &http.Transport{DialContext: dialer.DialContext}
|
|
||||||
resp, err := txp.RoundTrip(req)
|
|
||||||
if err == nil || !strings.HasSuffix(err.Error(), netxlite.FailureConnectionReset) {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
t.Fatal("expected nil resp here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTProxyDial(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skip test in short mode")
|
|
||||||
}
|
|
||||||
t.Run("with drop SYN", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{
|
|
||||||
Endpoints: map[string]TProxyPolicy{
|
|
||||||
"130.192.16.171:80/tcp": TProxyPolicyTCPDropSYN,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
dialer := proxy.NewSimpleDialer(10 * time.Second)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://130.192.16.171:80", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
req.Host = "nexa.polito.it"
|
|
||||||
txp := &http.Transport{DialContext: dialer.DialContext}
|
|
||||||
resp, err := txp.RoundTrip(req)
|
|
||||||
if !errors.Is(err, context.DeadlineExceeded) {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
t.Fatal("expected nil resp here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with reject SYN", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{
|
|
||||||
Endpoints: map[string]TProxyPolicy{
|
|
||||||
"130.192.16.171:80/tcp": TProxyPolicyTCPRejectSYN,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
dialer := netxlite.WrapDialer(log.Log,
|
|
||||||
netxlite.NewResolverStdlib(log.Log),
|
|
||||||
&tProxyDialerAdapter{
|
|
||||||
proxy.NewSimpleDialer(10 * time.Second)})
|
|
||||||
req, err := http.NewRequest("GET", "http://130.192.16.171:80", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
req.Host = "nexa.polito.it"
|
|
||||||
txp := &http.Transport{DialContext: dialer.DialContext}
|
|
||||||
resp, err := txp.RoundTrip(req)
|
|
||||||
if err == nil || !strings.HasSuffix(err.Error(), netxlite.FailureConnectionRefused) {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
t.Fatal("expected nil resp here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with drop data", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{
|
|
||||||
Endpoints: map[string]TProxyPolicy{
|
|
||||||
"130.192.16.171:80/tcp": TProxyPolicyDropData,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
dialer := proxy.NewSimpleDialer(10 * time.Second)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
req, err := http.NewRequestWithContext(
|
|
||||||
ctx, "GET", "http://130.192.16.171:80", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
req.Host = "nexa.polito.it"
|
|
||||||
txp := &http.Transport{DialContext: dialer.DialContext}
|
|
||||||
resp, err := txp.RoundTrip(req)
|
|
||||||
if !errors.Is(err, context.DeadlineExceeded) {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
t.Fatal("expected nil resp here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with hijack DNS", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{
|
|
||||||
Endpoints: map[string]TProxyPolicy{
|
|
||||||
"8.8.8.8:53/udp": TProxyPolicyHijackDNS,
|
|
||||||
},
|
|
||||||
Domains: map[string]DNSAction{
|
|
||||||
"example.com.": DNSActionNXDOMAIN,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
dialer := proxy.NewSimpleDialer(10 * time.Second)
|
|
||||||
resolver := netxlite.NewResolverUDP(
|
|
||||||
log.Log, &tProxyDialerAdapter{dialer}, "8.8.8.8:53")
|
|
||||||
addrs, err := resolver.LookupHost(context.Background(), "example.com")
|
|
||||||
if err == nil || err.Error() != netxlite.FailureDNSNXDOMAINError {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if len(addrs) != 0 {
|
|
||||||
t.Fatal("expected no addrs here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with invalid destination address", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer proxy.Close()
|
|
||||||
dialer := proxy.NewSimpleDialer(10 * time.Second)
|
|
||||||
ctx := context.Background()
|
|
||||||
conn, err := dialer.DialContext(ctx, "tcp", "127.0.0.1")
|
|
||||||
if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if conn != nil {
|
|
||||||
t.Fatal("expected nil conn here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTProxyDNSCache(t *testing.T) {
|
|
||||||
t.Run("without cache but with the cache rule", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{
|
|
||||||
Domains: map[string]DNSAction{
|
|
||||||
"dns.google.": DNSActionCache,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
addrs, err := proxy.LookupHost(ctx, "dns.google")
|
|
||||||
if err == nil || err.Error() != netxlite.FailureDNSNXDOMAINError {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
if addrs != nil {
|
|
||||||
t.Fatal("expected nil addrs")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with cache", func(t *testing.T) {
|
|
||||||
config := &TProxyConfig{
|
|
||||||
DNSCache: map[string][]string{
|
|
||||||
"dns.google.": {"8.8.8.8", "8.8.4.4"},
|
|
||||||
},
|
|
||||||
Domains: map[string]DNSAction{
|
|
||||||
"dns.google.": DNSActionCache,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
proxy, err := NewTProxy(config, log.Log)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
addrs, err := proxy.LookupHost(ctx, "dns.google")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(addrs) != 2 {
|
|
||||||
t.Fatal("expected two addrs")
|
|
||||||
}
|
|
||||||
if addrs[0] != "8.8.8.8" {
|
|
||||||
t.Fatal("invalid first address")
|
|
||||||
}
|
|
||||||
if addrs[1] != "8.8.4.4" {
|
|
||||||
t.Fatal("invalid second address")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
package apimodel
|
|
||||||
|
|
||||||
import "github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
|
|
||||||
// CheckInRequestWebConnectivity contains WebConnectivity
|
|
||||||
// specific parameters to include into CheckInRequest
|
|
||||||
type CheckInRequestWebConnectivity struct {
|
|
||||||
CategoryCodes []string `json:"category_codes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckInRequest is the check-in API request
|
|
||||||
type CheckInRequest struct {
|
|
||||||
Charging bool `json:"charging"`
|
|
||||||
OnWiFi bool `json:"on_wifi"`
|
|
||||||
Platform string `json:"platform"`
|
|
||||||
ProbeASN string `json:"probe_asn"`
|
|
||||||
ProbeCC string `json:"probe_cc"`
|
|
||||||
RunType model.RunType `json:"run_type"`
|
|
||||||
SoftwareName string `json:"software_name"`
|
|
||||||
SoftwareVersion string `json:"software_version"`
|
|
||||||
WebConnectivity CheckInRequestWebConnectivity `json:"web_connectivity"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckInResponseURLInfo contains information about an URL.
|
|
||||||
type CheckInResponseURLInfo struct {
|
|
||||||
CategoryCode string `json:"category_code"`
|
|
||||||
CountryCode string `json:"country_code"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckInResponseWebConnectivity contains WebConnectivity
|
|
||||||
// specific information of a CheckInResponse
|
|
||||||
type CheckInResponseWebConnectivity struct {
|
|
||||||
ReportID string `json:"report_id"`
|
|
||||||
URLs []CheckInResponseURLInfo `json:"urls"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckInResponse is the check-in API response
|
|
||||||
type CheckInResponse struct {
|
|
||||||
ProbeASN string `json:"probe_asn"`
|
|
||||||
ProbeCC string `json:"probe_cc"`
|
|
||||||
Tests CheckInResponseTests `json:"tests"`
|
|
||||||
V int64 `json:"v"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckInResponseTests contains configuration for tests
|
|
||||||
type CheckInResponseTests struct {
|
|
||||||
WebConnectivity CheckInResponseWebConnectivity `json:"web_connectivity"`
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
package apimodel
|
|
||||||
|
|
||||||
// CheckReportIDRequest is the CheckReportID request.
|
|
||||||
type CheckReportIDRequest struct {
|
|
||||||
ReportID string `query:"report_id" required:"true"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckReportIDResponse is the CheckReportID response.
|
|
||||||
type CheckReportIDResponse struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
Found bool `json:"found"`
|
|
||||||
V int64 `json:"v"`
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
// Package apimodel describes the data types used by OONI's API.
|
|
||||||
//
|
|
||||||
// If you edit this package to integrate the data model, remember to
|
|
||||||
// run `go generate ./...`.
|
|
||||||
//
|
|
||||||
// We annotate fields with tagging. When a field should be sent
|
|
||||||
// over as JSON, use the usual `json` tag.
|
|
||||||
//
|
|
||||||
// When a field needs to be sent using the query string, use
|
|
||||||
// the `query` tag instead. We limit what can be sent using the
|
|
||||||
// query string to int64, string, and bool.
|
|
||||||
//
|
|
||||||
// The `path` tag indicates that the URL path contains a
|
|
||||||
// template. We will replace the value of this field with
|
|
||||||
// the template. Note that the template should use the
|
|
||||||
// Go name of the field (e.g. `{{ .ReportID }}`) as opposed
|
|
||||||
// to the name in the tag, which is only used when we
|
|
||||||
// generate the API Swagger.
|
|
||||||
//
|
|
||||||
// The `required` tag indicates required fields. A required
|
|
||||||
// field cannot be empty (for the Go definition of empty).
|
|
||||||
package apimodel
|
|
|
@ -1,15 +0,0 @@
|
||||||
package apimodel
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// LoginRequest is the login API request
|
|
||||||
type LoginRequest struct {
|
|
||||||
ClientID string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginResponse is the login API response
|
|
||||||
type LoginResponse struct {
|
|
||||||
Expire time.Time `json:"expire"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
package apimodel
|
|
||||||
|
|
||||||
// MeasurementMetaRequest is the MeasurementMeta Request.
|
|
||||||
type MeasurementMetaRequest struct {
|
|
||||||
ReportID string `query:"report_id" required:"true"`
|
|
||||||
Full bool `query:"full"`
|
|
||||||
Input string `query:"input"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MeasurementMetaResponse is the MeasurementMeta Response.
|
|
||||||
type MeasurementMetaResponse struct {
|
|
||||||
Anomaly bool `json:"anomaly"`
|
|
||||||
CategoryCode string `json:"category_code"`
|
|
||||||
Confirmed bool `json:"confirmed"`
|
|
||||||
Failure bool `json:"failure"`
|
|
||||||
Input string `json:"input"`
|
|
||||||
MeasurementStartTime string `json:"measurement_start_time"`
|
|
||||||
ProbeASN int64 `json:"probe_asn"`
|
|
||||||
ProbeCC string `json:"probe_cc"`
|
|
||||||
RawMeasurement string `json:"raw_measurement"`
|
|
||||||
ReportID string `json:"report_id"`
|
|
||||||
Scores string `json:"scores"`
|
|
||||||
TestName string `json:"test_name"`
|
|
||||||
TestStartTime string `json:"test_start_time"`
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
package apimodel
|
|
||||||
|
|
||||||
// OpenReportRequest is the OpenReport request.
|
|
||||||
type OpenReportRequest struct {
|
|
||||||
DataFormatVersion string `json:"data_format_version"`
|
|
||||||
Format string `json:"format"`
|
|
||||||
ProbeASN string `json:"probe_asn"`
|
|
||||||
ProbeCC string `json:"probe_cc"`
|
|
||||||
SoftwareName string `json:"software_name"`
|
|
||||||
SoftwareVersion string `json:"software_version"`
|
|
||||||
TestName string `json:"test_name"`
|
|
||||||
TestStartTime string `json:"test_start_time"`
|
|
||||||
TestVersion string `json:"test_version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenReportResponse is the OpenReport response.
|
|
||||||
type OpenReportResponse struct {
|
|
||||||
BackendVersion string `json:"backend_version"`
|
|
||||||
ReportID string `json:"report_id"`
|
|
||||||
SupportedFormats []string `json:"supported_formats"`
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package apimodel
|
|
||||||
|
|
||||||
// PsiphonConfigRequest is the request for the PsiphonConfig API
|
|
||||||
type PsiphonConfigRequest struct{}
|
|
||||||
|
|
||||||
// PsiphonConfigResponse is the response from the PsiphonConfig API
|
|
||||||
type PsiphonConfigResponse map[string]interface{}
|
|
|
@ -1,26 +0,0 @@
|
||||||
package apimodel
|
|
||||||
|
|
||||||
// RegisterRequest is the request for the Register API.
|
|
||||||
type RegisterRequest struct {
|
|
||||||
// just password
|
|
||||||
Password string `json:"password"`
|
|
||||||
|
|
||||||
// metadata
|
|
||||||
AvailableBandwidth string `json:"available_bandwidth,omitempty"`
|
|
||||||
DeviceToken string `json:"device_token,omitempty"`
|
|
||||||
Language string `json:"language,omitempty"`
|
|
||||||
NetworkType string `json:"network_type,omitempty"`
|
|
||||||
Platform string `json:"platform"`
|
|
||||||
ProbeASN string `json:"probe_asn"`
|
|
||||||
ProbeCC string `json:"probe_cc"`
|
|
||||||
ProbeFamily string `json:"probe_family,omitempty"`
|
|
||||||
ProbeTimezone string `json:"probe_timezone,omitempty"`
|
|
||||||
SoftwareName string `json:"software_name"`
|
|
||||||
SoftwareVersion string `json:"software_version"`
|
|
||||||
SupportedTests []string `json:"supported_tests"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterResponse is the response from the Register API.
|
|
||||||
type RegisterResponse struct {
|
|
||||||
ClientID string `json:"client_id"`
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
package apimodel
|
|
||||||
|
|
||||||
// SubmitMeasurementRequest is the SubmitMeasurement request.
|
|
||||||
type SubmitMeasurementRequest struct {
|
|
||||||
ReportID string `path:"report_id"`
|
|
||||||
Format string `json:"format"`
|
|
||||||
Content interface{} `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubmitMeasurementResponse is the SubmitMeasurement response.
|
|
||||||
type SubmitMeasurementResponse struct {
|
|
||||||
MeasurementUID string `json:"measurement_uid"`
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
package apimodel
|
|
||||||
|
|
||||||
// TestHelpersRequest is the TestHelpers request.
|
|
||||||
type TestHelpersRequest struct{}
|
|
||||||
|
|
||||||
// TestHelpersResponse is the TestHelpers response.
|
|
||||||
type TestHelpersResponse map[string][]TestHelpersHelperInfo
|
|
||||||
|
|
||||||
// TestHelpersHelperInfo is a single helper within the
|
|
||||||
// response returned by the TestHelpers API.
|
|
||||||
type TestHelpersHelperInfo struct {
|
|
||||||
Address string `json:"address"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Front string `json:"front,omitempty"`
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package apimodel
|
|
||||||
|
|
||||||
// TorTargetsRequest is a request for the TorTargets API.
|
|
||||||
type TorTargetsRequest struct{}
|
|
||||||
|
|
||||||
// TorTargetsResponse is the response from the TorTargets API.
|
|
||||||
type TorTargetsResponse map[string]TorTargetsTarget
|
|
||||||
|
|
||||||
// TorTargetsTarget is a target for the tor experiment.
|
|
||||||
type TorTargetsTarget struct {
|
|
||||||
Address string `json:"address"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Params map[string][]string `json:"params"`
|
|
||||||
Protocol string `json:"protocol"`
|
|
||||||
Source string `json:"source"`
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
package apimodel
|
|
||||||
|
|
||||||
// URLsRequest is the URLs request.
|
|
||||||
type URLsRequest struct {
|
|
||||||
CategoryCodes string `query:"category_codes"`
|
|
||||||
CountryCode string `query:"country_code"`
|
|
||||||
Limit int64 `query:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// URLsResponse is the URLs response.
|
|
||||||
type URLsResponse struct {
|
|
||||||
Metadata URLsMetadata `json:"metadata"`
|
|
||||||
Results []URLsResponseURL `json:"results"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// URLsMetadata contains metadata in the URLs response.
|
|
||||||
type URLsMetadata struct {
|
|
||||||
Count int64 `json:"count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// URLsResponseURL is a single URL in the URLs response.
|
|
||||||
type URLsResponseURL struct {
|
|
||||||
CategoryCode string `json:"category_code"`
|
|
||||||
CountryCode string `json:"country_code"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
|
@ -1,618 +0,0 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
|
||||||
// 2022-05-19 20:30:46.371368 +0200 CEST m=+0.001320792
|
|
||||||
|
|
||||||
package ooapi
|
|
||||||
|
|
||||||
//go:generate go run ./internal/generator -file apis.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
// simpleCheckReportIDAPI implements the CheckReportID API.
|
|
||||||
type simpleCheckReportIDAPI struct {
|
|
||||||
BaseURL string // optional
|
|
||||||
HTTPClient HTTPClient // optional
|
|
||||||
JSONCodec JSONCodec // optional
|
|
||||||
RequestMaker RequestMaker // optional
|
|
||||||
UserAgent string // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleCheckReportIDAPI) baseURL() string {
|
|
||||||
if api.BaseURL != "" {
|
|
||||||
return api.BaseURL
|
|
||||||
}
|
|
||||||
return "https://ps1.ooni.io"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleCheckReportIDAPI) requestMaker() RequestMaker {
|
|
||||||
if api.RequestMaker != nil {
|
|
||||||
return api.RequestMaker
|
|
||||||
}
|
|
||||||
return &defaultRequestMaker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleCheckReportIDAPI) jsonCodec() JSONCodec {
|
|
||||||
if api.JSONCodec != nil {
|
|
||||||
return api.JSONCodec
|
|
||||||
}
|
|
||||||
return &defaultJSONCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleCheckReportIDAPI) httpClient() HTTPClient {
|
|
||||||
if api.HTTPClient != nil {
|
|
||||||
return api.HTTPClient
|
|
||||||
}
|
|
||||||
return http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls the CheckReportID API.
|
|
||||||
func (api *simpleCheckReportIDAPI) Call(ctx context.Context, req *apimodel.CheckReportIDRequest) (*apimodel.CheckReportIDResponse, error) {
|
|
||||||
httpReq, err := api.newRequest(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
httpReq.Header.Add("Accept", "application/json")
|
|
||||||
if api.UserAgent != "" {
|
|
||||||
httpReq.Header.Add("User-Agent", api.UserAgent)
|
|
||||||
}
|
|
||||||
httpResp, err := api.httpClient().Do(httpReq)
|
|
||||||
return api.newResponse(ctx, httpResp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleCheckInAPI implements the CheckIn API.
|
|
||||||
type simpleCheckInAPI struct {
|
|
||||||
BaseURL string // optional
|
|
||||||
HTTPClient HTTPClient // optional
|
|
||||||
JSONCodec JSONCodec // optional
|
|
||||||
RequestMaker RequestMaker // optional
|
|
||||||
UserAgent string // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleCheckInAPI) baseURL() string {
|
|
||||||
if api.BaseURL != "" {
|
|
||||||
return api.BaseURL
|
|
||||||
}
|
|
||||||
return "https://ps1.ooni.io"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleCheckInAPI) requestMaker() RequestMaker {
|
|
||||||
if api.RequestMaker != nil {
|
|
||||||
return api.RequestMaker
|
|
||||||
}
|
|
||||||
return &defaultRequestMaker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleCheckInAPI) jsonCodec() JSONCodec {
|
|
||||||
if api.JSONCodec != nil {
|
|
||||||
return api.JSONCodec
|
|
||||||
}
|
|
||||||
return &defaultJSONCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleCheckInAPI) httpClient() HTTPClient {
|
|
||||||
if api.HTTPClient != nil {
|
|
||||||
return api.HTTPClient
|
|
||||||
}
|
|
||||||
return http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls the CheckIn API.
|
|
||||||
func (api *simpleCheckInAPI) Call(ctx context.Context, req *apimodel.CheckInRequest) (*apimodel.CheckInResponse, error) {
|
|
||||||
httpReq, err := api.newRequest(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
httpReq.Header.Add("Accept", "application/json")
|
|
||||||
if api.UserAgent != "" {
|
|
||||||
httpReq.Header.Add("User-Agent", api.UserAgent)
|
|
||||||
}
|
|
||||||
httpResp, err := api.httpClient().Do(httpReq)
|
|
||||||
return api.newResponse(ctx, httpResp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleLoginAPI implements the Login API.
|
|
||||||
type simpleLoginAPI struct {
|
|
||||||
BaseURL string // optional
|
|
||||||
HTTPClient HTTPClient // optional
|
|
||||||
JSONCodec JSONCodec // optional
|
|
||||||
RequestMaker RequestMaker // optional
|
|
||||||
UserAgent string // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleLoginAPI) baseURL() string {
|
|
||||||
if api.BaseURL != "" {
|
|
||||||
return api.BaseURL
|
|
||||||
}
|
|
||||||
return "https://ps1.ooni.io"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleLoginAPI) requestMaker() RequestMaker {
|
|
||||||
if api.RequestMaker != nil {
|
|
||||||
return api.RequestMaker
|
|
||||||
}
|
|
||||||
return &defaultRequestMaker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleLoginAPI) jsonCodec() JSONCodec {
|
|
||||||
if api.JSONCodec != nil {
|
|
||||||
return api.JSONCodec
|
|
||||||
}
|
|
||||||
return &defaultJSONCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleLoginAPI) httpClient() HTTPClient {
|
|
||||||
if api.HTTPClient != nil {
|
|
||||||
return api.HTTPClient
|
|
||||||
}
|
|
||||||
return http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls the Login API.
|
|
||||||
func (api *simpleLoginAPI) Call(ctx context.Context, req *apimodel.LoginRequest) (*apimodel.LoginResponse, error) {
|
|
||||||
httpReq, err := api.newRequest(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
httpReq.Header.Add("Accept", "application/json")
|
|
||||||
if api.UserAgent != "" {
|
|
||||||
httpReq.Header.Add("User-Agent", api.UserAgent)
|
|
||||||
}
|
|
||||||
httpResp, err := api.httpClient().Do(httpReq)
|
|
||||||
return api.newResponse(ctx, httpResp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleMeasurementMetaAPI implements the MeasurementMeta API.
|
|
||||||
type simpleMeasurementMetaAPI struct {
|
|
||||||
BaseURL string // optional
|
|
||||||
HTTPClient HTTPClient // optional
|
|
||||||
JSONCodec JSONCodec // optional
|
|
||||||
RequestMaker RequestMaker // optional
|
|
||||||
UserAgent string // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleMeasurementMetaAPI) baseURL() string {
|
|
||||||
if api.BaseURL != "" {
|
|
||||||
return api.BaseURL
|
|
||||||
}
|
|
||||||
return "https://ps1.ooni.io"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleMeasurementMetaAPI) requestMaker() RequestMaker {
|
|
||||||
if api.RequestMaker != nil {
|
|
||||||
return api.RequestMaker
|
|
||||||
}
|
|
||||||
return &defaultRequestMaker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleMeasurementMetaAPI) jsonCodec() JSONCodec {
|
|
||||||
if api.JSONCodec != nil {
|
|
||||||
return api.JSONCodec
|
|
||||||
}
|
|
||||||
return &defaultJSONCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleMeasurementMetaAPI) httpClient() HTTPClient {
|
|
||||||
if api.HTTPClient != nil {
|
|
||||||
return api.HTTPClient
|
|
||||||
}
|
|
||||||
return http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls the MeasurementMeta API.
|
|
||||||
func (api *simpleMeasurementMetaAPI) Call(ctx context.Context, req *apimodel.MeasurementMetaRequest) (*apimodel.MeasurementMetaResponse, error) {
|
|
||||||
httpReq, err := api.newRequest(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
httpReq.Header.Add("Accept", "application/json")
|
|
||||||
if api.UserAgent != "" {
|
|
||||||
httpReq.Header.Add("User-Agent", api.UserAgent)
|
|
||||||
}
|
|
||||||
httpResp, err := api.httpClient().Do(httpReq)
|
|
||||||
return api.newResponse(ctx, httpResp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleRegisterAPI implements the Register API.
|
|
||||||
type simpleRegisterAPI struct {
|
|
||||||
BaseURL string // optional
|
|
||||||
HTTPClient HTTPClient // optional
|
|
||||||
JSONCodec JSONCodec // optional
|
|
||||||
RequestMaker RequestMaker // optional
|
|
||||||
UserAgent string // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleRegisterAPI) baseURL() string {
|
|
||||||
if api.BaseURL != "" {
|
|
||||||
return api.BaseURL
|
|
||||||
}
|
|
||||||
return "https://ps1.ooni.io"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleRegisterAPI) requestMaker() RequestMaker {
|
|
||||||
if api.RequestMaker != nil {
|
|
||||||
return api.RequestMaker
|
|
||||||
}
|
|
||||||
return &defaultRequestMaker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleRegisterAPI) jsonCodec() JSONCodec {
|
|
||||||
if api.JSONCodec != nil {
|
|
||||||
return api.JSONCodec
|
|
||||||
}
|
|
||||||
return &defaultJSONCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleRegisterAPI) httpClient() HTTPClient {
|
|
||||||
if api.HTTPClient != nil {
|
|
||||||
return api.HTTPClient
|
|
||||||
}
|
|
||||||
return http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls the Register API.
|
|
||||||
func (api *simpleRegisterAPI) Call(ctx context.Context, req *apimodel.RegisterRequest) (*apimodel.RegisterResponse, error) {
|
|
||||||
httpReq, err := api.newRequest(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
httpReq.Header.Add("Accept", "application/json")
|
|
||||||
if api.UserAgent != "" {
|
|
||||||
httpReq.Header.Add("User-Agent", api.UserAgent)
|
|
||||||
}
|
|
||||||
httpResp, err := api.httpClient().Do(httpReq)
|
|
||||||
return api.newResponse(ctx, httpResp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleTestHelpersAPI implements the TestHelpers API.
|
|
||||||
type simpleTestHelpersAPI struct {
|
|
||||||
BaseURL string // optional
|
|
||||||
HTTPClient HTTPClient // optional
|
|
||||||
JSONCodec JSONCodec // optional
|
|
||||||
RequestMaker RequestMaker // optional
|
|
||||||
UserAgent string // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleTestHelpersAPI) baseURL() string {
|
|
||||||
if api.BaseURL != "" {
|
|
||||||
return api.BaseURL
|
|
||||||
}
|
|
||||||
return "https://ps1.ooni.io"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleTestHelpersAPI) requestMaker() RequestMaker {
|
|
||||||
if api.RequestMaker != nil {
|
|
||||||
return api.RequestMaker
|
|
||||||
}
|
|
||||||
return &defaultRequestMaker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleTestHelpersAPI) jsonCodec() JSONCodec {
|
|
||||||
if api.JSONCodec != nil {
|
|
||||||
return api.JSONCodec
|
|
||||||
}
|
|
||||||
return &defaultJSONCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleTestHelpersAPI) httpClient() HTTPClient {
|
|
||||||
if api.HTTPClient != nil {
|
|
||||||
return api.HTTPClient
|
|
||||||
}
|
|
||||||
return http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls the TestHelpers API.
|
|
||||||
func (api *simpleTestHelpersAPI) Call(ctx context.Context, req *apimodel.TestHelpersRequest) (apimodel.TestHelpersResponse, error) {
|
|
||||||
httpReq, err := api.newRequest(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
httpReq.Header.Add("Accept", "application/json")
|
|
||||||
if api.UserAgent != "" {
|
|
||||||
httpReq.Header.Add("User-Agent", api.UserAgent)
|
|
||||||
}
|
|
||||||
httpResp, err := api.httpClient().Do(httpReq)
|
|
||||||
return api.newResponse(ctx, httpResp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// simplePsiphonConfigAPI implements the PsiphonConfig API.
|
|
||||||
type simplePsiphonConfigAPI struct {
|
|
||||||
BaseURL string // optional
|
|
||||||
HTTPClient HTTPClient // optional
|
|
||||||
JSONCodec JSONCodec // optional
|
|
||||||
Token string // mandatory
|
|
||||||
RequestMaker RequestMaker // optional
|
|
||||||
UserAgent string // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithToken returns a copy of the API where the
|
|
||||||
// value of the Token field is replaced with token.
|
|
||||||
func (api *simplePsiphonConfigAPI) WithToken(token string) callerForPsiphonConfigAPI {
|
|
||||||
out := &simplePsiphonConfigAPI{}
|
|
||||||
out.BaseURL = api.BaseURL
|
|
||||||
out.HTTPClient = api.HTTPClient
|
|
||||||
out.JSONCodec = api.JSONCodec
|
|
||||||
out.RequestMaker = api.RequestMaker
|
|
||||||
out.UserAgent = api.UserAgent
|
|
||||||
out.Token = token
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simplePsiphonConfigAPI) baseURL() string {
|
|
||||||
if api.BaseURL != "" {
|
|
||||||
return api.BaseURL
|
|
||||||
}
|
|
||||||
return "https://ps1.ooni.io"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simplePsiphonConfigAPI) requestMaker() RequestMaker {
|
|
||||||
if api.RequestMaker != nil {
|
|
||||||
return api.RequestMaker
|
|
||||||
}
|
|
||||||
return &defaultRequestMaker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simplePsiphonConfigAPI) jsonCodec() JSONCodec {
|
|
||||||
if api.JSONCodec != nil {
|
|
||||||
return api.JSONCodec
|
|
||||||
}
|
|
||||||
return &defaultJSONCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simplePsiphonConfigAPI) httpClient() HTTPClient {
|
|
||||||
if api.HTTPClient != nil {
|
|
||||||
return api.HTTPClient
|
|
||||||
}
|
|
||||||
return http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls the PsiphonConfig API.
|
|
||||||
func (api *simplePsiphonConfigAPI) Call(ctx context.Context, req *apimodel.PsiphonConfigRequest) (apimodel.PsiphonConfigResponse, error) {
|
|
||||||
httpReq, err := api.newRequest(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
httpReq.Header.Add("Accept", "application/json")
|
|
||||||
if api.Token == "" {
|
|
||||||
return nil, ErrMissingToken
|
|
||||||
}
|
|
||||||
httpReq.Header.Add("Authorization", newAuthorizationHeader(api.Token))
|
|
||||||
if api.UserAgent != "" {
|
|
||||||
httpReq.Header.Add("User-Agent", api.UserAgent)
|
|
||||||
}
|
|
||||||
httpResp, err := api.httpClient().Do(httpReq)
|
|
||||||
return api.newResponse(ctx, httpResp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleTorTargetsAPI implements the TorTargets API.
|
|
||||||
type simpleTorTargetsAPI struct {
|
|
||||||
BaseURL string // optional
|
|
||||||
HTTPClient HTTPClient // optional
|
|
||||||
JSONCodec JSONCodec // optional
|
|
||||||
Token string // mandatory
|
|
||||||
RequestMaker RequestMaker // optional
|
|
||||||
UserAgent string // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithToken returns a copy of the API where the
|
|
||||||
// value of the Token field is replaced with token.
|
|
||||||
func (api *simpleTorTargetsAPI) WithToken(token string) callerForTorTargetsAPI {
|
|
||||||
out := &simpleTorTargetsAPI{}
|
|
||||||
out.BaseURL = api.BaseURL
|
|
||||||
out.HTTPClient = api.HTTPClient
|
|
||||||
out.JSONCodec = api.JSONCodec
|
|
||||||
out.RequestMaker = api.RequestMaker
|
|
||||||
out.UserAgent = api.UserAgent
|
|
||||||
out.Token = token
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleTorTargetsAPI) baseURL() string {
|
|
||||||
if api.BaseURL != "" {
|
|
||||||
return api.BaseURL
|
|
||||||
}
|
|
||||||
return "https://ps1.ooni.io"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleTorTargetsAPI) requestMaker() RequestMaker {
|
|
||||||
if api.RequestMaker != nil {
|
|
||||||
return api.RequestMaker
|
|
||||||
}
|
|
||||||
return &defaultRequestMaker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleTorTargetsAPI) jsonCodec() JSONCodec {
|
|
||||||
if api.JSONCodec != nil {
|
|
||||||
return api.JSONCodec
|
|
||||||
}
|
|
||||||
return &defaultJSONCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleTorTargetsAPI) httpClient() HTTPClient {
|
|
||||||
if api.HTTPClient != nil {
|
|
||||||
return api.HTTPClient
|
|
||||||
}
|
|
||||||
return http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls the TorTargets API.
|
|
||||||
func (api *simpleTorTargetsAPI) Call(ctx context.Context, req *apimodel.TorTargetsRequest) (apimodel.TorTargetsResponse, error) {
|
|
||||||
httpReq, err := api.newRequest(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
httpReq.Header.Add("Accept", "application/json")
|
|
||||||
if api.Token == "" {
|
|
||||||
return nil, ErrMissingToken
|
|
||||||
}
|
|
||||||
httpReq.Header.Add("Authorization", newAuthorizationHeader(api.Token))
|
|
||||||
if api.UserAgent != "" {
|
|
||||||
httpReq.Header.Add("User-Agent", api.UserAgent)
|
|
||||||
}
|
|
||||||
httpResp, err := api.httpClient().Do(httpReq)
|
|
||||||
return api.newResponse(ctx, httpResp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleURLsAPI implements the URLs API.
|
|
||||||
type simpleURLsAPI struct {
|
|
||||||
BaseURL string // optional
|
|
||||||
HTTPClient HTTPClient // optional
|
|
||||||
JSONCodec JSONCodec // optional
|
|
||||||
RequestMaker RequestMaker // optional
|
|
||||||
UserAgent string // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleURLsAPI) baseURL() string {
|
|
||||||
if api.BaseURL != "" {
|
|
||||||
return api.BaseURL
|
|
||||||
}
|
|
||||||
return "https://ps1.ooni.io"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleURLsAPI) requestMaker() RequestMaker {
|
|
||||||
if api.RequestMaker != nil {
|
|
||||||
return api.RequestMaker
|
|
||||||
}
|
|
||||||
return &defaultRequestMaker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleURLsAPI) jsonCodec() JSONCodec {
|
|
||||||
if api.JSONCodec != nil {
|
|
||||||
return api.JSONCodec
|
|
||||||
}
|
|
||||||
return &defaultJSONCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleURLsAPI) httpClient() HTTPClient {
|
|
||||||
if api.HTTPClient != nil {
|
|
||||||
return api.HTTPClient
|
|
||||||
}
|
|
||||||
return http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls the URLs API.
|
|
||||||
func (api *simpleURLsAPI) Call(ctx context.Context, req *apimodel.URLsRequest) (*apimodel.URLsResponse, error) {
|
|
||||||
httpReq, err := api.newRequest(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
httpReq.Header.Add("Accept", "application/json")
|
|
||||||
if api.UserAgent != "" {
|
|
||||||
httpReq.Header.Add("User-Agent", api.UserAgent)
|
|
||||||
}
|
|
||||||
httpResp, err := api.httpClient().Do(httpReq)
|
|
||||||
return api.newResponse(ctx, httpResp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleOpenReportAPI implements the OpenReport API.
|
|
||||||
type simpleOpenReportAPI struct {
|
|
||||||
BaseURL string // optional
|
|
||||||
HTTPClient HTTPClient // optional
|
|
||||||
JSONCodec JSONCodec // optional
|
|
||||||
RequestMaker RequestMaker // optional
|
|
||||||
UserAgent string // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleOpenReportAPI) baseURL() string {
|
|
||||||
if api.BaseURL != "" {
|
|
||||||
return api.BaseURL
|
|
||||||
}
|
|
||||||
return "https://ps1.ooni.io"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleOpenReportAPI) requestMaker() RequestMaker {
|
|
||||||
if api.RequestMaker != nil {
|
|
||||||
return api.RequestMaker
|
|
||||||
}
|
|
||||||
return &defaultRequestMaker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleOpenReportAPI) jsonCodec() JSONCodec {
|
|
||||||
if api.JSONCodec != nil {
|
|
||||||
return api.JSONCodec
|
|
||||||
}
|
|
||||||
return &defaultJSONCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleOpenReportAPI) httpClient() HTTPClient {
|
|
||||||
if api.HTTPClient != nil {
|
|
||||||
return api.HTTPClient
|
|
||||||
}
|
|
||||||
return http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls the OpenReport API.
|
|
||||||
func (api *simpleOpenReportAPI) Call(ctx context.Context, req *apimodel.OpenReportRequest) (*apimodel.OpenReportResponse, error) {
|
|
||||||
httpReq, err := api.newRequest(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
httpReq.Header.Add("Accept", "application/json")
|
|
||||||
if api.UserAgent != "" {
|
|
||||||
httpReq.Header.Add("User-Agent", api.UserAgent)
|
|
||||||
}
|
|
||||||
httpResp, err := api.httpClient().Do(httpReq)
|
|
||||||
return api.newResponse(ctx, httpResp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleSubmitMeasurementAPI implements the SubmitMeasurement API.
|
|
||||||
type simpleSubmitMeasurementAPI struct {
|
|
||||||
BaseURL string // optional
|
|
||||||
HTTPClient HTTPClient // optional
|
|
||||||
JSONCodec JSONCodec // optional
|
|
||||||
RequestMaker RequestMaker // optional
|
|
||||||
TemplateExecutor templateExecutor // optional
|
|
||||||
UserAgent string // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleSubmitMeasurementAPI) baseURL() string {
|
|
||||||
if api.BaseURL != "" {
|
|
||||||
return api.BaseURL
|
|
||||||
}
|
|
||||||
return "https://ps1.ooni.io"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleSubmitMeasurementAPI) requestMaker() RequestMaker {
|
|
||||||
if api.RequestMaker != nil {
|
|
||||||
return api.RequestMaker
|
|
||||||
}
|
|
||||||
return &defaultRequestMaker{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleSubmitMeasurementAPI) jsonCodec() JSONCodec {
|
|
||||||
if api.JSONCodec != nil {
|
|
||||||
return api.JSONCodec
|
|
||||||
}
|
|
||||||
return &defaultJSONCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleSubmitMeasurementAPI) templateExecutor() templateExecutor {
|
|
||||||
if api.TemplateExecutor != nil {
|
|
||||||
return api.TemplateExecutor
|
|
||||||
}
|
|
||||||
return &defaultTemplateExecutor{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleSubmitMeasurementAPI) httpClient() HTTPClient {
|
|
||||||
if api.HTTPClient != nil {
|
|
||||||
return api.HTTPClient
|
|
||||||
}
|
|
||||||
return http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls the SubmitMeasurement API.
|
|
||||||
func (api *simpleSubmitMeasurementAPI) Call(ctx context.Context, req *apimodel.SubmitMeasurementRequest) (*apimodel.SubmitMeasurementResponse, error) {
|
|
||||||
httpReq, err := api.newRequest(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
httpReq.Header.Add("Accept", "application/json")
|
|
||||||
if api.UserAgent != "" {
|
|
||||||
httpReq.Header.Add("User-Agent", api.UserAgent)
|
|
||||||
}
|
|
||||||
httpResp, err := api.httpClient().Do(httpReq)
|
|
||||||
return api.newResponse(ctx, httpResp, err)
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,98 +0,0 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
|
||||||
// 2022-05-19 20:30:47.186361 +0200 CEST m=+0.000721751
|
|
||||||
|
|
||||||
package ooapi
|
|
||||||
|
|
||||||
//go:generate go run ./internal/generator -file caching.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
// withCacheMeasurementMetaAPI implements caching for simpleMeasurementMetaAPI.
|
|
||||||
type withCacheMeasurementMetaAPI struct {
|
|
||||||
API callerForMeasurementMetaAPI // mandatory
|
|
||||||
GobCodec GobCodec // optional
|
|
||||||
KVStore KVStore // mandatory
|
|
||||||
}
|
|
||||||
|
|
||||||
type cacheEntryForMeasurementMetaAPI struct {
|
|
||||||
Req *apimodel.MeasurementMetaRequest
|
|
||||||
Resp *apimodel.MeasurementMetaResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls the API and implements caching.
|
|
||||||
func (c *withCacheMeasurementMetaAPI) Call(ctx context.Context, req *apimodel.MeasurementMetaRequest) (*apimodel.MeasurementMetaResponse, error) {
|
|
||||||
if resp, _ := c.readcache(req); resp != nil {
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
resp, err := c.API.Call(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := c.writecache(req, resp); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *withCacheMeasurementMetaAPI) gobCodec() GobCodec {
|
|
||||||
if c.GobCodec != nil {
|
|
||||||
return c.GobCodec
|
|
||||||
}
|
|
||||||
return &defaultGobCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *withCacheMeasurementMetaAPI) getcache() ([]cacheEntryForMeasurementMetaAPI, error) {
|
|
||||||
data, err := c.KVStore.Get("MeasurementMeta.cache")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var out []cacheEntryForMeasurementMetaAPI
|
|
||||||
if err := c.gobCodec().Decode(data, &out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *withCacheMeasurementMetaAPI) setcache(in []cacheEntryForMeasurementMetaAPI) error {
|
|
||||||
data, err := c.gobCodec().Encode(in)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.KVStore.Set("MeasurementMeta.cache", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *withCacheMeasurementMetaAPI) readcache(req *apimodel.MeasurementMetaRequest) (*apimodel.MeasurementMetaResponse, error) {
|
|
||||||
cache, err := c.getcache()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, cur := range cache {
|
|
||||||
if reflect.DeepEqual(req, cur.Req) {
|
|
||||||
return cur.Resp, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, errCacheNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *withCacheMeasurementMetaAPI) writecache(req *apimodel.MeasurementMetaRequest, resp *apimodel.MeasurementMetaResponse) error {
|
|
||||||
cache, _ := c.getcache()
|
|
||||||
out := []cacheEntryForMeasurementMetaAPI{{Req: req, Resp: resp}}
|
|
||||||
const toomany = 64
|
|
||||||
for idx, cur := range cache {
|
|
||||||
if reflect.DeepEqual(req, cur.Req) {
|
|
||||||
continue // we already updated the cache
|
|
||||||
}
|
|
||||||
if idx > toomany {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
out = append(out, cur)
|
|
||||||
}
|
|
||||||
return c.setcache(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ callerForMeasurementMetaAPI = &withCacheMeasurementMetaAPI{}
|
|
|
@ -1,223 +0,0 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
|
||||||
// 2022-05-19 20:30:47.604277 +0200 CEST m=+0.000796918
|
|
||||||
|
|
||||||
package ooapi
|
|
||||||
|
|
||||||
//go:generate go run ./internal/generator -file caching_test.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/kvstore"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCachesimpleMeasurementMetaAPISuccess(t *testing.T) {
|
|
||||||
ff := &fakeFill{}
|
|
||||||
var expect *apimodel.MeasurementMetaResponse
|
|
||||||
ff.Fill(&expect)
|
|
||||||
cache := &withCacheMeasurementMetaAPI{
|
|
||||||
API: &FakeMeasurementMetaAPI{
|
|
||||||
Response: expect,
|
|
||||||
},
|
|
||||||
KVStore: &kvstore.Memory{},
|
|
||||||
}
|
|
||||||
var req *apimodel.MeasurementMetaRequest
|
|
||||||
ff.Fill(&req)
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := cache.Call(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non-nil response")
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(expect, resp); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCachesimpleMeasurementMetaAPIWriteCacheError(t *testing.T) {
|
|
||||||
errMocked := errors.New("mocked error")
|
|
||||||
ff := &fakeFill{}
|
|
||||||
var expect *apimodel.MeasurementMetaResponse
|
|
||||||
ff.Fill(&expect)
|
|
||||||
cache := &withCacheMeasurementMetaAPI{
|
|
||||||
API: &FakeMeasurementMetaAPI{
|
|
||||||
Response: expect,
|
|
||||||
},
|
|
||||||
KVStore: &FakeKVStore{SetError: errMocked},
|
|
||||||
}
|
|
||||||
var req *apimodel.MeasurementMetaRequest
|
|
||||||
ff.Fill(&req)
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := cache.Call(ctx, req)
|
|
||||||
if !errors.Is(err, errMocked) {
|
|
||||||
t.Fatal("not the error we expected", err)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
t.Fatal("expected nil response")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCachesimpleMeasurementMetaAPIFailureWithNoCache(t *testing.T) {
|
|
||||||
errMocked := errors.New("mocked error")
|
|
||||||
ff := &fakeFill{}
|
|
||||||
cache := &withCacheMeasurementMetaAPI{
|
|
||||||
API: &FakeMeasurementMetaAPI{
|
|
||||||
Err: errMocked,
|
|
||||||
},
|
|
||||||
KVStore: &kvstore.Memory{},
|
|
||||||
}
|
|
||||||
var req *apimodel.MeasurementMetaRequest
|
|
||||||
ff.Fill(&req)
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := cache.Call(ctx, req)
|
|
||||||
if !errors.Is(err, errMocked) {
|
|
||||||
t.Fatal("not the error we expected", err)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
t.Fatal("expected nil response")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCachesimpleMeasurementMetaAPIFailureWithPreviousCache(t *testing.T) {
|
|
||||||
ff := &fakeFill{}
|
|
||||||
var expect *apimodel.MeasurementMetaResponse
|
|
||||||
ff.Fill(&expect)
|
|
||||||
fakeapi := &FakeMeasurementMetaAPI{
|
|
||||||
Response: expect,
|
|
||||||
}
|
|
||||||
cache := &withCacheMeasurementMetaAPI{
|
|
||||||
API: fakeapi,
|
|
||||||
KVStore: &kvstore.Memory{},
|
|
||||||
}
|
|
||||||
var req *apimodel.MeasurementMetaRequest
|
|
||||||
ff.Fill(&req)
|
|
||||||
ctx := context.Background()
|
|
||||||
// first pass with no error at all
|
|
||||||
// use a separate scope to be sure we avoid mistakes
|
|
||||||
{
|
|
||||||
resp, err := cache.Call(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non-nil response")
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(expect, resp); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// second pass with failure
|
|
||||||
errMocked := errors.New("mocked error")
|
|
||||||
fakeapi.Err = errMocked
|
|
||||||
fakeapi.Response = nil
|
|
||||||
resp2, err := cache.Call(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp2 == nil {
|
|
||||||
t.Fatal("expected non-nil response")
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(expect, resp2); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCachesimpleMeasurementMetaAPISetcacheWithEncodeError(t *testing.T) {
|
|
||||||
ff := &fakeFill{}
|
|
||||||
errMocked := errors.New("mocked error")
|
|
||||||
var in []cacheEntryForMeasurementMetaAPI
|
|
||||||
ff.Fill(&in)
|
|
||||||
cache := &withCacheMeasurementMetaAPI{
|
|
||||||
GobCodec: &FakeCodec{EncodeErr: errMocked},
|
|
||||||
}
|
|
||||||
err := cache.setcache(in)
|
|
||||||
if !errors.Is(err, errMocked) {
|
|
||||||
t.Fatal("not the error we expected", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCachesimpleMeasurementMetaAPIReadCacheNotFound(t *testing.T) {
|
|
||||||
ff := &fakeFill{}
|
|
||||||
var incache []cacheEntryForMeasurementMetaAPI
|
|
||||||
ff.Fill(&incache)
|
|
||||||
cache := &withCacheMeasurementMetaAPI{
|
|
||||||
KVStore: &kvstore.Memory{},
|
|
||||||
}
|
|
||||||
err := cache.setcache(incache)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
var req *apimodel.MeasurementMetaRequest
|
|
||||||
ff.Fill(&req)
|
|
||||||
out, err := cache.readcache(req)
|
|
||||||
if !errors.Is(err, errCacheNotFound) {
|
|
||||||
t.Fatal("not the error we expected", err)
|
|
||||||
}
|
|
||||||
if out != nil {
|
|
||||||
t.Fatal("expected nil here")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCachesimpleMeasurementMetaAPIWriteCacheDuplicate(t *testing.T) {
|
|
||||||
ff := &fakeFill{}
|
|
||||||
var req *apimodel.MeasurementMetaRequest
|
|
||||||
ff.Fill(&req)
|
|
||||||
var resp1 *apimodel.MeasurementMetaResponse
|
|
||||||
ff.Fill(&resp1)
|
|
||||||
var resp2 *apimodel.MeasurementMetaResponse
|
|
||||||
ff.Fill(&resp2)
|
|
||||||
cache := &withCacheMeasurementMetaAPI{
|
|
||||||
KVStore: &kvstore.Memory{},
|
|
||||||
}
|
|
||||||
err := cache.writecache(req, resp1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
err = cache.writecache(req, resp2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
out, err := cache.readcache(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if out == nil {
|
|
||||||
t.Fatal("expected non-nil here")
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(resp2, out); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCachesimpleMeasurementMetaAPICacheSizeLimited(t *testing.T) {
|
|
||||||
ff := &fakeFill{}
|
|
||||||
cache := &withCacheMeasurementMetaAPI{
|
|
||||||
KVStore: &kvstore.Memory{},
|
|
||||||
}
|
|
||||||
var prev int
|
|
||||||
for {
|
|
||||||
var req *apimodel.MeasurementMetaRequest
|
|
||||||
ff.Fill(&req)
|
|
||||||
var resp *apimodel.MeasurementMetaResponse
|
|
||||||
ff.Fill(&resp)
|
|
||||||
err := cache.writecache(req, resp)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
out, err := cache.getcache()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(out) > prev {
|
|
||||||
prev = len(out)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
|
||||||
// 2022-05-19 20:30:48.00142 +0200 CEST m=+0.000791126
|
|
||||||
|
|
||||||
package ooapi
|
|
||||||
|
|
||||||
//go:generate go run ./internal/generator -file callers.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
// callerForCheckReportIDAPI represents any type exposing a method
|
|
||||||
// like simpleCheckReportIDAPI.Call.
|
|
||||||
type callerForCheckReportIDAPI interface {
|
|
||||||
Call(ctx context.Context, req *apimodel.CheckReportIDRequest) (*apimodel.CheckReportIDResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// callerForCheckInAPI represents any type exposing a method
|
|
||||||
// like simpleCheckInAPI.Call.
|
|
||||||
type callerForCheckInAPI interface {
|
|
||||||
Call(ctx context.Context, req *apimodel.CheckInRequest) (*apimodel.CheckInResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// callerForLoginAPI represents any type exposing a method
|
|
||||||
// like simpleLoginAPI.Call.
|
|
||||||
type callerForLoginAPI interface {
|
|
||||||
Call(ctx context.Context, req *apimodel.LoginRequest) (*apimodel.LoginResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// callerForMeasurementMetaAPI represents any type exposing a method
|
|
||||||
// like simpleMeasurementMetaAPI.Call.
|
|
||||||
type callerForMeasurementMetaAPI interface {
|
|
||||||
Call(ctx context.Context, req *apimodel.MeasurementMetaRequest) (*apimodel.MeasurementMetaResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// callerForRegisterAPI represents any type exposing a method
|
|
||||||
// like simpleRegisterAPI.Call.
|
|
||||||
type callerForRegisterAPI interface {
|
|
||||||
Call(ctx context.Context, req *apimodel.RegisterRequest) (*apimodel.RegisterResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// callerForTestHelpersAPI represents any type exposing a method
|
|
||||||
// like simpleTestHelpersAPI.Call.
|
|
||||||
type callerForTestHelpersAPI interface {
|
|
||||||
Call(ctx context.Context, req *apimodel.TestHelpersRequest) (apimodel.TestHelpersResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// callerForPsiphonConfigAPI represents any type exposing a method
|
|
||||||
// like simplePsiphonConfigAPI.Call.
|
|
||||||
type callerForPsiphonConfigAPI interface {
|
|
||||||
Call(ctx context.Context, req *apimodel.PsiphonConfigRequest) (apimodel.PsiphonConfigResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// callerForTorTargetsAPI represents any type exposing a method
|
|
||||||
// like simpleTorTargetsAPI.Call.
|
|
||||||
type callerForTorTargetsAPI interface {
|
|
||||||
Call(ctx context.Context, req *apimodel.TorTargetsRequest) (apimodel.TorTargetsResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// callerForURLsAPI represents any type exposing a method
|
|
||||||
// like simpleURLsAPI.Call.
|
|
||||||
type callerForURLsAPI interface {
|
|
||||||
Call(ctx context.Context, req *apimodel.URLsRequest) (*apimodel.URLsResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// callerForOpenReportAPI represents any type exposing a method
|
|
||||||
// like simpleOpenReportAPI.Call.
|
|
||||||
type callerForOpenReportAPI interface {
|
|
||||||
Call(ctx context.Context, req *apimodel.OpenReportRequest) (*apimodel.OpenReportResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// callerForSubmitMeasurementAPI represents any type exposing a method
|
|
||||||
// like simpleSubmitMeasurementAPI.Call.
|
|
||||||
type callerForSubmitMeasurementAPI interface {
|
|
||||||
Call(ctx context.Context, req *apimodel.SubmitMeasurementRequest) (*apimodel.SubmitMeasurementResponse, error)
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
package ooapi
|
|
||||||
|
|
||||||
// Client is a client for speaking with the OONI API. Make sure you
|
|
||||||
// fill in the mandatory fields.
|
|
||||||
type Client struct {
|
|
||||||
// KVStore is the MANDATORY key-value store. You can use
|
|
||||||
// the kvstore.Memory{} struct for an in-memory store.
|
|
||||||
KVStore KVStore
|
|
||||||
|
|
||||||
// The following fields are optional. When they are empty
|
|
||||||
// we will fallback to sensible defaults.
|
|
||||||
BaseURL string
|
|
||||||
GobCodec GobCodec
|
|
||||||
HTTPClient HTTPClient
|
|
||||||
JSONCodec JSONCodec
|
|
||||||
RequestMaker RequestMaker
|
|
||||||
UserAgent string
|
|
||||||
}
|
|
|
@ -1,214 +0,0 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
|
||||||
// 2022-05-19 20:30:48.430212 +0200 CEST m=+0.000888918
|
|
||||||
|
|
||||||
package ooapi
|
|
||||||
|
|
||||||
//go:generate go run ./internal/generator -file clientcall.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Client) newCheckReportIDCaller() callerForCheckReportIDAPI {
|
|
||||||
return &simpleCheckReportIDAPI{
|
|
||||||
BaseURL: c.BaseURL,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
RequestMaker: c.RequestMaker,
|
|
||||||
UserAgent: c.UserAgent,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckReportID calls the CheckReportID API.
|
|
||||||
func (c *Client) CheckReportID(
|
|
||||||
ctx context.Context, req *apimodel.CheckReportIDRequest,
|
|
||||||
) (*apimodel.CheckReportIDResponse, error) {
|
|
||||||
api := c.newCheckReportIDCaller()
|
|
||||||
return api.Call(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newCheckInCaller() callerForCheckInAPI {
|
|
||||||
return &simpleCheckInAPI{
|
|
||||||
BaseURL: c.BaseURL,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
RequestMaker: c.RequestMaker,
|
|
||||||
UserAgent: c.UserAgent,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckIn calls the CheckIn API.
|
|
||||||
func (c *Client) CheckIn(
|
|
||||||
ctx context.Context, req *apimodel.CheckInRequest,
|
|
||||||
) (*apimodel.CheckInResponse, error) {
|
|
||||||
api := c.newCheckInCaller()
|
|
||||||
return api.Call(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newMeasurementMetaCaller() callerForMeasurementMetaAPI {
|
|
||||||
return &withCacheMeasurementMetaAPI{
|
|
||||||
API: &simpleMeasurementMetaAPI{
|
|
||||||
BaseURL: c.BaseURL,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
RequestMaker: c.RequestMaker,
|
|
||||||
UserAgent: c.UserAgent,
|
|
||||||
},
|
|
||||||
GobCodec: c.GobCodec,
|
|
||||||
KVStore: c.KVStore,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MeasurementMeta calls the MeasurementMeta API.
|
|
||||||
func (c *Client) MeasurementMeta(
|
|
||||||
ctx context.Context, req *apimodel.MeasurementMetaRequest,
|
|
||||||
) (*apimodel.MeasurementMetaResponse, error) {
|
|
||||||
api := c.newMeasurementMetaCaller()
|
|
||||||
return api.Call(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newTestHelpersCaller() callerForTestHelpersAPI {
|
|
||||||
return &simpleTestHelpersAPI{
|
|
||||||
BaseURL: c.BaseURL,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
RequestMaker: c.RequestMaker,
|
|
||||||
UserAgent: c.UserAgent,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHelpers calls the TestHelpers API.
|
|
||||||
func (c *Client) TestHelpers(
|
|
||||||
ctx context.Context, req *apimodel.TestHelpersRequest,
|
|
||||||
) (apimodel.TestHelpersResponse, error) {
|
|
||||||
api := c.newTestHelpersCaller()
|
|
||||||
return api.Call(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newPsiphonConfigCaller() callerForPsiphonConfigAPI {
|
|
||||||
return &withLoginPsiphonConfigAPI{
|
|
||||||
API: &simplePsiphonConfigAPI{
|
|
||||||
BaseURL: c.BaseURL,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
RequestMaker: c.RequestMaker,
|
|
||||||
UserAgent: c.UserAgent,
|
|
||||||
},
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
KVStore: c.KVStore,
|
|
||||||
RegisterAPI: &simpleRegisterAPI{
|
|
||||||
BaseURL: c.BaseURL,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
RequestMaker: c.RequestMaker,
|
|
||||||
UserAgent: c.UserAgent,
|
|
||||||
},
|
|
||||||
LoginAPI: &simpleLoginAPI{
|
|
||||||
BaseURL: c.BaseURL,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
RequestMaker: c.RequestMaker,
|
|
||||||
UserAgent: c.UserAgent,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PsiphonConfig calls the PsiphonConfig API.
|
|
||||||
func (c *Client) PsiphonConfig(
|
|
||||||
ctx context.Context, req *apimodel.PsiphonConfigRequest,
|
|
||||||
) (apimodel.PsiphonConfigResponse, error) {
|
|
||||||
api := c.newPsiphonConfigCaller()
|
|
||||||
return api.Call(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newTorTargetsCaller() callerForTorTargetsAPI {
|
|
||||||
return &withLoginTorTargetsAPI{
|
|
||||||
API: &simpleTorTargetsAPI{
|
|
||||||
BaseURL: c.BaseURL,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
RequestMaker: c.RequestMaker,
|
|
||||||
UserAgent: c.UserAgent,
|
|
||||||
},
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
KVStore: c.KVStore,
|
|
||||||
RegisterAPI: &simpleRegisterAPI{
|
|
||||||
BaseURL: c.BaseURL,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
RequestMaker: c.RequestMaker,
|
|
||||||
UserAgent: c.UserAgent,
|
|
||||||
},
|
|
||||||
LoginAPI: &simpleLoginAPI{
|
|
||||||
BaseURL: c.BaseURL,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
RequestMaker: c.RequestMaker,
|
|
||||||
UserAgent: c.UserAgent,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TorTargets calls the TorTargets API.
|
|
||||||
func (c *Client) TorTargets(
|
|
||||||
ctx context.Context, req *apimodel.TorTargetsRequest,
|
|
||||||
) (apimodel.TorTargetsResponse, error) {
|
|
||||||
api := c.newTorTargetsCaller()
|
|
||||||
return api.Call(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newURLsCaller() callerForURLsAPI {
|
|
||||||
return &simpleURLsAPI{
|
|
||||||
BaseURL: c.BaseURL,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
RequestMaker: c.RequestMaker,
|
|
||||||
UserAgent: c.UserAgent,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// URLs calls the URLs API.
|
|
||||||
func (c *Client) URLs(
|
|
||||||
ctx context.Context, req *apimodel.URLsRequest,
|
|
||||||
) (*apimodel.URLsResponse, error) {
|
|
||||||
api := c.newURLsCaller()
|
|
||||||
return api.Call(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newOpenReportCaller() callerForOpenReportAPI {
|
|
||||||
return &simpleOpenReportAPI{
|
|
||||||
BaseURL: c.BaseURL,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
RequestMaker: c.RequestMaker,
|
|
||||||
UserAgent: c.UserAgent,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenReport calls the OpenReport API.
|
|
||||||
func (c *Client) OpenReport(
|
|
||||||
ctx context.Context, req *apimodel.OpenReportRequest,
|
|
||||||
) (*apimodel.OpenReportResponse, error) {
|
|
||||||
api := c.newOpenReportCaller()
|
|
||||||
return api.Call(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newSubmitMeasurementCaller() callerForSubmitMeasurementAPI {
|
|
||||||
return &simpleSubmitMeasurementAPI{
|
|
||||||
BaseURL: c.BaseURL,
|
|
||||||
HTTPClient: c.HTTPClient,
|
|
||||||
JSONCodec: c.JSONCodec,
|
|
||||||
RequestMaker: c.RequestMaker,
|
|
||||||
UserAgent: c.UserAgent,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubmitMeasurement calls the SubmitMeasurement API.
|
|
||||||
func (c *Client) SubmitMeasurement(
|
|
||||||
ctx context.Context, req *apimodel.SubmitMeasurementRequest,
|
|
||||||
) (*apimodel.SubmitMeasurementResponse, error) {
|
|
||||||
api := c.newSubmitMeasurementCaller()
|
|
||||||
return api.Call(ctx, req)
|
|
||||||
}
|
|
|
@ -1,899 +0,0 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
|
||||||
// 2022-05-19 20:30:48.846389 +0200 CEST m=+0.000695543
|
|
||||||
|
|
||||||
package ooapi
|
|
||||||
|
|
||||||
//go:generate go run ./internal/generator -file clientcall_test.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/kvstore"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
type handleClientCallCheckReportID struct {
|
|
||||||
accept string
|
|
||||||
body []byte
|
|
||||||
contentType string
|
|
||||||
count int32
|
|
||||||
method string
|
|
||||||
mu sync.Mutex
|
|
||||||
resp *apimodel.CheckReportIDResponse
|
|
||||||
url *url.URL
|
|
||||||
userAgent string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handleClientCallCheckReportID) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ff := fakeFill{}
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
h.mu.Lock()
|
|
||||||
if h.count > 0 {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.count++
|
|
||||||
if r.Body != nil {
|
|
||||||
data, err := netxlite.ReadAllContext(r.Context(), r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.body = data
|
|
||||||
}
|
|
||||||
h.method = r.Method
|
|
||||||
h.url = r.URL
|
|
||||||
h.accept = r.Header.Get("Accept")
|
|
||||||
h.contentType = r.Header.Get("Content-Type")
|
|
||||||
h.userAgent = r.Header.Get("User-Agent")
|
|
||||||
var out *apimodel.CheckReportIDResponse
|
|
||||||
ff.Fill(&out)
|
|
||||||
h.resp = out
|
|
||||||
data, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckReportIDClientCallRoundTrip(t *testing.T) {
|
|
||||||
// setup
|
|
||||||
handler := &handleClientCallCheckReportID{}
|
|
||||||
srvr := httptest.NewServer(handler)
|
|
||||||
defer srvr.Close()
|
|
||||||
req := &apimodel.CheckReportIDRequest{}
|
|
||||||
ff := &fakeFill{}
|
|
||||||
ff.Fill(&req)
|
|
||||||
clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL}
|
|
||||||
ff.Fill(&clnt.UserAgent)
|
|
||||||
// issue request
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.CheckReportID(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non-nil response here")
|
|
||||||
}
|
|
||||||
// compare our response and server's one
|
|
||||||
if diff := cmp.Diff(handler.resp, resp); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
// check whether headers are OK
|
|
||||||
if handler.accept != "application/json" {
|
|
||||||
t.Fatal("invalid accept header")
|
|
||||||
}
|
|
||||||
if handler.userAgent != clnt.UserAgent {
|
|
||||||
t.Fatal("invalid user-agent header")
|
|
||||||
}
|
|
||||||
// check whether the method is OK
|
|
||||||
if handler.method != "GET" {
|
|
||||||
t.Fatal("invalid method")
|
|
||||||
}
|
|
||||||
// check the query
|
|
||||||
api := &simpleCheckReportIDAPI{BaseURL: srvr.URL}
|
|
||||||
httpReq, err := api.newRequest(context.Background(), req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type handleClientCallCheckIn struct {
|
|
||||||
accept string
|
|
||||||
body []byte
|
|
||||||
contentType string
|
|
||||||
count int32
|
|
||||||
method string
|
|
||||||
mu sync.Mutex
|
|
||||||
resp *apimodel.CheckInResponse
|
|
||||||
url *url.URL
|
|
||||||
userAgent string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handleClientCallCheckIn) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ff := fakeFill{}
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
h.mu.Lock()
|
|
||||||
if h.count > 0 {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.count++
|
|
||||||
if r.Body != nil {
|
|
||||||
data, err := netxlite.ReadAllContext(r.Context(), r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.body = data
|
|
||||||
}
|
|
||||||
h.method = r.Method
|
|
||||||
h.url = r.URL
|
|
||||||
h.accept = r.Header.Get("Accept")
|
|
||||||
h.contentType = r.Header.Get("Content-Type")
|
|
||||||
h.userAgent = r.Header.Get("User-Agent")
|
|
||||||
var out *apimodel.CheckInResponse
|
|
||||||
ff.Fill(&out)
|
|
||||||
h.resp = out
|
|
||||||
data, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckInClientCallRoundTrip(t *testing.T) {
|
|
||||||
// setup
|
|
||||||
handler := &handleClientCallCheckIn{}
|
|
||||||
srvr := httptest.NewServer(handler)
|
|
||||||
defer srvr.Close()
|
|
||||||
req := &apimodel.CheckInRequest{}
|
|
||||||
ff := &fakeFill{}
|
|
||||||
ff.Fill(&req)
|
|
||||||
clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL}
|
|
||||||
ff.Fill(&clnt.UserAgent)
|
|
||||||
// issue request
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.CheckIn(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non-nil response here")
|
|
||||||
}
|
|
||||||
// compare our response and server's one
|
|
||||||
if diff := cmp.Diff(handler.resp, resp); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
// check whether headers are OK
|
|
||||||
if handler.accept != "application/json" {
|
|
||||||
t.Fatal("invalid accept header")
|
|
||||||
}
|
|
||||||
if handler.userAgent != clnt.UserAgent {
|
|
||||||
t.Fatal("invalid user-agent header")
|
|
||||||
}
|
|
||||||
// check whether the method is OK
|
|
||||||
if handler.method != "POST" {
|
|
||||||
t.Fatal("invalid method")
|
|
||||||
}
|
|
||||||
// check the body
|
|
||||||
if handler.contentType != "application/json" {
|
|
||||||
t.Fatal("invalid content-type header")
|
|
||||||
}
|
|
||||||
got := &apimodel.CheckInRequest{}
|
|
||||||
if err := json.Unmarshal(handler.body, &got); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(req, got); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type handleClientCallMeasurementMeta struct {
|
|
||||||
accept string
|
|
||||||
body []byte
|
|
||||||
contentType string
|
|
||||||
count int32
|
|
||||||
method string
|
|
||||||
mu sync.Mutex
|
|
||||||
resp *apimodel.MeasurementMetaResponse
|
|
||||||
url *url.URL
|
|
||||||
userAgent string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handleClientCallMeasurementMeta) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ff := fakeFill{}
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
h.mu.Lock()
|
|
||||||
if h.count > 0 {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.count++
|
|
||||||
if r.Body != nil {
|
|
||||||
data, err := netxlite.ReadAllContext(r.Context(), r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.body = data
|
|
||||||
}
|
|
||||||
h.method = r.Method
|
|
||||||
h.url = r.URL
|
|
||||||
h.accept = r.Header.Get("Accept")
|
|
||||||
h.contentType = r.Header.Get("Content-Type")
|
|
||||||
h.userAgent = r.Header.Get("User-Agent")
|
|
||||||
var out *apimodel.MeasurementMetaResponse
|
|
||||||
ff.Fill(&out)
|
|
||||||
h.resp = out
|
|
||||||
data, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMeasurementMetaClientCallRoundTrip(t *testing.T) {
|
|
||||||
// setup
|
|
||||||
handler := &handleClientCallMeasurementMeta{}
|
|
||||||
srvr := httptest.NewServer(handler)
|
|
||||||
defer srvr.Close()
|
|
||||||
req := &apimodel.MeasurementMetaRequest{}
|
|
||||||
ff := &fakeFill{}
|
|
||||||
ff.Fill(&req)
|
|
||||||
clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL}
|
|
||||||
ff.Fill(&clnt.UserAgent)
|
|
||||||
// issue request
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.MeasurementMeta(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non-nil response here")
|
|
||||||
}
|
|
||||||
// compare our response and server's one
|
|
||||||
if diff := cmp.Diff(handler.resp, resp); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
// check whether headers are OK
|
|
||||||
if handler.accept != "application/json" {
|
|
||||||
t.Fatal("invalid accept header")
|
|
||||||
}
|
|
||||||
if handler.userAgent != clnt.UserAgent {
|
|
||||||
t.Fatal("invalid user-agent header")
|
|
||||||
}
|
|
||||||
// check whether the method is OK
|
|
||||||
if handler.method != "GET" {
|
|
||||||
t.Fatal("invalid method")
|
|
||||||
}
|
|
||||||
// check the query
|
|
||||||
api := &simpleMeasurementMetaAPI{BaseURL: srvr.URL}
|
|
||||||
httpReq, err := api.newRequest(context.Background(), req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type handleClientCallTestHelpers struct {
|
|
||||||
accept string
|
|
||||||
body []byte
|
|
||||||
contentType string
|
|
||||||
count int32
|
|
||||||
method string
|
|
||||||
mu sync.Mutex
|
|
||||||
resp apimodel.TestHelpersResponse
|
|
||||||
url *url.URL
|
|
||||||
userAgent string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handleClientCallTestHelpers) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ff := fakeFill{}
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
h.mu.Lock()
|
|
||||||
if h.count > 0 {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.count++
|
|
||||||
if r.Body != nil {
|
|
||||||
data, err := netxlite.ReadAllContext(r.Context(), r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.body = data
|
|
||||||
}
|
|
||||||
h.method = r.Method
|
|
||||||
h.url = r.URL
|
|
||||||
h.accept = r.Header.Get("Accept")
|
|
||||||
h.contentType = r.Header.Get("Content-Type")
|
|
||||||
h.userAgent = r.Header.Get("User-Agent")
|
|
||||||
var out apimodel.TestHelpersResponse
|
|
||||||
ff.Fill(&out)
|
|
||||||
h.resp = out
|
|
||||||
data, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTestHelpersClientCallRoundTrip(t *testing.T) {
|
|
||||||
// setup
|
|
||||||
handler := &handleClientCallTestHelpers{}
|
|
||||||
srvr := httptest.NewServer(handler)
|
|
||||||
defer srvr.Close()
|
|
||||||
req := &apimodel.TestHelpersRequest{}
|
|
||||||
ff := &fakeFill{}
|
|
||||||
ff.Fill(&req)
|
|
||||||
clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL}
|
|
||||||
ff.Fill(&clnt.UserAgent)
|
|
||||||
// issue request
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.TestHelpers(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non-nil response here")
|
|
||||||
}
|
|
||||||
// compare our response and server's one
|
|
||||||
if diff := cmp.Diff(handler.resp, resp); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
// check whether headers are OK
|
|
||||||
if handler.accept != "application/json" {
|
|
||||||
t.Fatal("invalid accept header")
|
|
||||||
}
|
|
||||||
if handler.userAgent != clnt.UserAgent {
|
|
||||||
t.Fatal("invalid user-agent header")
|
|
||||||
}
|
|
||||||
// check whether the method is OK
|
|
||||||
if handler.method != "GET" {
|
|
||||||
t.Fatal("invalid method")
|
|
||||||
}
|
|
||||||
// check the query
|
|
||||||
api := &simpleTestHelpersAPI{BaseURL: srvr.URL}
|
|
||||||
httpReq, err := api.newRequest(context.Background(), req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type handleClientCallPsiphonConfig struct {
|
|
||||||
accept string
|
|
||||||
body []byte
|
|
||||||
contentType string
|
|
||||||
count int32
|
|
||||||
method string
|
|
||||||
mu sync.Mutex
|
|
||||||
resp apimodel.PsiphonConfigResponse
|
|
||||||
url *url.URL
|
|
||||||
userAgent string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handleClientCallPsiphonConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ff := fakeFill{}
|
|
||||||
if r.URL.Path == "/api/v1/register" {
|
|
||||||
var out apimodel.RegisterResponse
|
|
||||||
ff.Fill(&out)
|
|
||||||
data, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.URL.Path == "/api/v1/login" {
|
|
||||||
var out apimodel.LoginResponse
|
|
||||||
ff.Fill(&out)
|
|
||||||
data, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
h.mu.Lock()
|
|
||||||
if h.count > 0 {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.count++
|
|
||||||
if r.Body != nil {
|
|
||||||
data, err := netxlite.ReadAllContext(r.Context(), r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.body = data
|
|
||||||
}
|
|
||||||
h.method = r.Method
|
|
||||||
h.url = r.URL
|
|
||||||
h.accept = r.Header.Get("Accept")
|
|
||||||
h.contentType = r.Header.Get("Content-Type")
|
|
||||||
h.userAgent = r.Header.Get("User-Agent")
|
|
||||||
var out apimodel.PsiphonConfigResponse
|
|
||||||
ff.Fill(&out)
|
|
||||||
h.resp = out
|
|
||||||
data, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPsiphonConfigClientCallRoundTrip(t *testing.T) {
|
|
||||||
// setup
|
|
||||||
handler := &handleClientCallPsiphonConfig{}
|
|
||||||
srvr := httptest.NewServer(handler)
|
|
||||||
defer srvr.Close()
|
|
||||||
req := &apimodel.PsiphonConfigRequest{}
|
|
||||||
ff := &fakeFill{}
|
|
||||||
ff.Fill(&req)
|
|
||||||
clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL}
|
|
||||||
ff.Fill(&clnt.UserAgent)
|
|
||||||
// issue request
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.PsiphonConfig(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non-nil response here")
|
|
||||||
}
|
|
||||||
// compare our response and server's one
|
|
||||||
if diff := cmp.Diff(handler.resp, resp); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
// check whether headers are OK
|
|
||||||
if handler.accept != "application/json" {
|
|
||||||
t.Fatal("invalid accept header")
|
|
||||||
}
|
|
||||||
if handler.userAgent != clnt.UserAgent {
|
|
||||||
t.Fatal("invalid user-agent header")
|
|
||||||
}
|
|
||||||
// check whether the method is OK
|
|
||||||
if handler.method != "GET" {
|
|
||||||
t.Fatal("invalid method")
|
|
||||||
}
|
|
||||||
// check the query
|
|
||||||
api := &simplePsiphonConfigAPI{BaseURL: srvr.URL}
|
|
||||||
httpReq, err := api.newRequest(context.Background(), req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type handleClientCallTorTargets struct {
|
|
||||||
accept string
|
|
||||||
body []byte
|
|
||||||
contentType string
|
|
||||||
count int32
|
|
||||||
method string
|
|
||||||
mu sync.Mutex
|
|
||||||
resp apimodel.TorTargetsResponse
|
|
||||||
url *url.URL
|
|
||||||
userAgent string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handleClientCallTorTargets) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ff := fakeFill{}
|
|
||||||
if r.URL.Path == "/api/v1/register" {
|
|
||||||
var out apimodel.RegisterResponse
|
|
||||||
ff.Fill(&out)
|
|
||||||
data, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.URL.Path == "/api/v1/login" {
|
|
||||||
var out apimodel.LoginResponse
|
|
||||||
ff.Fill(&out)
|
|
||||||
data, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
h.mu.Lock()
|
|
||||||
if h.count > 0 {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.count++
|
|
||||||
if r.Body != nil {
|
|
||||||
data, err := netxlite.ReadAllContext(r.Context(), r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.body = data
|
|
||||||
}
|
|
||||||
h.method = r.Method
|
|
||||||
h.url = r.URL
|
|
||||||
h.accept = r.Header.Get("Accept")
|
|
||||||
h.contentType = r.Header.Get("Content-Type")
|
|
||||||
h.userAgent = r.Header.Get("User-Agent")
|
|
||||||
var out apimodel.TorTargetsResponse
|
|
||||||
ff.Fill(&out)
|
|
||||||
h.resp = out
|
|
||||||
data, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTorTargetsClientCallRoundTrip(t *testing.T) {
|
|
||||||
// setup
|
|
||||||
handler := &handleClientCallTorTargets{}
|
|
||||||
srvr := httptest.NewServer(handler)
|
|
||||||
defer srvr.Close()
|
|
||||||
req := &apimodel.TorTargetsRequest{}
|
|
||||||
ff := &fakeFill{}
|
|
||||||
ff.Fill(&req)
|
|
||||||
clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL}
|
|
||||||
ff.Fill(&clnt.UserAgent)
|
|
||||||
// issue request
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.TorTargets(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non-nil response here")
|
|
||||||
}
|
|
||||||
// compare our response and server's one
|
|
||||||
if diff := cmp.Diff(handler.resp, resp); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
// check whether headers are OK
|
|
||||||
if handler.accept != "application/json" {
|
|
||||||
t.Fatal("invalid accept header")
|
|
||||||
}
|
|
||||||
if handler.userAgent != clnt.UserAgent {
|
|
||||||
t.Fatal("invalid user-agent header")
|
|
||||||
}
|
|
||||||
// check whether the method is OK
|
|
||||||
if handler.method != "GET" {
|
|
||||||
t.Fatal("invalid method")
|
|
||||||
}
|
|
||||||
// check the query
|
|
||||||
api := &simpleTorTargetsAPI{BaseURL: srvr.URL}
|
|
||||||
httpReq, err := api.newRequest(context.Background(), req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type handleClientCallURLs struct {
|
|
||||||
accept string
|
|
||||||
body []byte
|
|
||||||
contentType string
|
|
||||||
count int32
|
|
||||||
method string
|
|
||||||
mu sync.Mutex
|
|
||||||
resp *apimodel.URLsResponse
|
|
||||||
url *url.URL
|
|
||||||
userAgent string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handleClientCallURLs) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ff := fakeFill{}
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
h.mu.Lock()
|
|
||||||
if h.count > 0 {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.count++
|
|
||||||
if r.Body != nil {
|
|
||||||
data, err := netxlite.ReadAllContext(r.Context(), r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.body = data
|
|
||||||
}
|
|
||||||
h.method = r.Method
|
|
||||||
h.url = r.URL
|
|
||||||
h.accept = r.Header.Get("Accept")
|
|
||||||
h.contentType = r.Header.Get("Content-Type")
|
|
||||||
h.userAgent = r.Header.Get("User-Agent")
|
|
||||||
var out *apimodel.URLsResponse
|
|
||||||
ff.Fill(&out)
|
|
||||||
h.resp = out
|
|
||||||
data, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestURLsClientCallRoundTrip(t *testing.T) {
|
|
||||||
// setup
|
|
||||||
handler := &handleClientCallURLs{}
|
|
||||||
srvr := httptest.NewServer(handler)
|
|
||||||
defer srvr.Close()
|
|
||||||
req := &apimodel.URLsRequest{}
|
|
||||||
ff := &fakeFill{}
|
|
||||||
ff.Fill(&req)
|
|
||||||
clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL}
|
|
||||||
ff.Fill(&clnt.UserAgent)
|
|
||||||
// issue request
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.URLs(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non-nil response here")
|
|
||||||
}
|
|
||||||
// compare our response and server's one
|
|
||||||
if diff := cmp.Diff(handler.resp, resp); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
// check whether headers are OK
|
|
||||||
if handler.accept != "application/json" {
|
|
||||||
t.Fatal("invalid accept header")
|
|
||||||
}
|
|
||||||
if handler.userAgent != clnt.UserAgent {
|
|
||||||
t.Fatal("invalid user-agent header")
|
|
||||||
}
|
|
||||||
// check whether the method is OK
|
|
||||||
if handler.method != "GET" {
|
|
||||||
t.Fatal("invalid method")
|
|
||||||
}
|
|
||||||
// check the query
|
|
||||||
api := &simpleURLsAPI{BaseURL: srvr.URL}
|
|
||||||
httpReq, err := api.newRequest(context.Background(), req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type handleClientCallOpenReport struct {
|
|
||||||
accept string
|
|
||||||
body []byte
|
|
||||||
contentType string
|
|
||||||
count int32
|
|
||||||
method string
|
|
||||||
mu sync.Mutex
|
|
||||||
resp *apimodel.OpenReportResponse
|
|
||||||
url *url.URL
|
|
||||||
userAgent string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handleClientCallOpenReport) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ff := fakeFill{}
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
h.mu.Lock()
|
|
||||||
if h.count > 0 {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.count++
|
|
||||||
if r.Body != nil {
|
|
||||||
data, err := netxlite.ReadAllContext(r.Context(), r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.body = data
|
|
||||||
}
|
|
||||||
h.method = r.Method
|
|
||||||
h.url = r.URL
|
|
||||||
h.accept = r.Header.Get("Accept")
|
|
||||||
h.contentType = r.Header.Get("Content-Type")
|
|
||||||
h.userAgent = r.Header.Get("User-Agent")
|
|
||||||
var out *apimodel.OpenReportResponse
|
|
||||||
ff.Fill(&out)
|
|
||||||
h.resp = out
|
|
||||||
data, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOpenReportClientCallRoundTrip(t *testing.T) {
|
|
||||||
// setup
|
|
||||||
handler := &handleClientCallOpenReport{}
|
|
||||||
srvr := httptest.NewServer(handler)
|
|
||||||
defer srvr.Close()
|
|
||||||
req := &apimodel.OpenReportRequest{}
|
|
||||||
ff := &fakeFill{}
|
|
||||||
ff.Fill(&req)
|
|
||||||
clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL}
|
|
||||||
ff.Fill(&clnt.UserAgent)
|
|
||||||
// issue request
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.OpenReport(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non-nil response here")
|
|
||||||
}
|
|
||||||
// compare our response and server's one
|
|
||||||
if diff := cmp.Diff(handler.resp, resp); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
// check whether headers are OK
|
|
||||||
if handler.accept != "application/json" {
|
|
||||||
t.Fatal("invalid accept header")
|
|
||||||
}
|
|
||||||
if handler.userAgent != clnt.UserAgent {
|
|
||||||
t.Fatal("invalid user-agent header")
|
|
||||||
}
|
|
||||||
// check whether the method is OK
|
|
||||||
if handler.method != "POST" {
|
|
||||||
t.Fatal("invalid method")
|
|
||||||
}
|
|
||||||
// check the body
|
|
||||||
if handler.contentType != "application/json" {
|
|
||||||
t.Fatal("invalid content-type header")
|
|
||||||
}
|
|
||||||
got := &apimodel.OpenReportRequest{}
|
|
||||||
if err := json.Unmarshal(handler.body, &got); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(req, got); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type handleClientCallSubmitMeasurement struct {
|
|
||||||
accept string
|
|
||||||
body []byte
|
|
||||||
contentType string
|
|
||||||
count int32
|
|
||||||
method string
|
|
||||||
mu sync.Mutex
|
|
||||||
resp *apimodel.SubmitMeasurementResponse
|
|
||||||
url *url.URL
|
|
||||||
userAgent string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handleClientCallSubmitMeasurement) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ff := fakeFill{}
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
h.mu.Lock()
|
|
||||||
if h.count > 0 {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.count++
|
|
||||||
if r.Body != nil {
|
|
||||||
data, err := netxlite.ReadAllContext(r.Context(), r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.body = data
|
|
||||||
}
|
|
||||||
h.method = r.Method
|
|
||||||
h.url = r.URL
|
|
||||||
h.accept = r.Header.Get("Accept")
|
|
||||||
h.contentType = r.Header.Get("Content-Type")
|
|
||||||
h.userAgent = r.Header.Get("User-Agent")
|
|
||||||
var out *apimodel.SubmitMeasurementResponse
|
|
||||||
ff.Fill(&out)
|
|
||||||
h.resp = out
|
|
||||||
data, err := json.Marshal(out)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubmitMeasurementClientCallRoundTrip(t *testing.T) {
|
|
||||||
// setup
|
|
||||||
handler := &handleClientCallSubmitMeasurement{}
|
|
||||||
srvr := httptest.NewServer(handler)
|
|
||||||
defer srvr.Close()
|
|
||||||
req := &apimodel.SubmitMeasurementRequest{}
|
|
||||||
ff := &fakeFill{}
|
|
||||||
ff.Fill(&req)
|
|
||||||
clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL}
|
|
||||||
ff.Fill(&clnt.UserAgent)
|
|
||||||
// issue request
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.SubmitMeasurement(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non-nil response here")
|
|
||||||
}
|
|
||||||
// compare our response and server's one
|
|
||||||
if diff := cmp.Diff(handler.resp, resp); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
// check whether headers are OK
|
|
||||||
if handler.accept != "application/json" {
|
|
||||||
t.Fatal("invalid accept header")
|
|
||||||
}
|
|
||||||
if handler.userAgent != clnt.UserAgent {
|
|
||||||
t.Fatal("invalid user-agent header")
|
|
||||||
}
|
|
||||||
// check whether the method is OK
|
|
||||||
if handler.method != "POST" {
|
|
||||||
t.Fatal("invalid method")
|
|
||||||
}
|
|
||||||
// check the body
|
|
||||||
if handler.contentType != "application/json" {
|
|
||||||
t.Fatal("invalid content-type header")
|
|
||||||
}
|
|
||||||
got := &apimodel.SubmitMeasurementRequest{}
|
|
||||||
if err := json.Unmarshal(handler.body, &got); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(req, got); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
|
||||||
// 2022-05-19 20:30:49.209515 +0200 CEST m=+0.000689210
|
|
||||||
|
|
||||||
package ooapi
|
|
||||||
|
|
||||||
//go:generate go run ./internal/generator -file cloners.go
|
|
||||||
|
|
||||||
// clonerForPsiphonConfigAPI represents any type exposing a method
|
|
||||||
// like simplePsiphonConfigAPI.WithToken.
|
|
||||||
type clonerForPsiphonConfigAPI interface {
|
|
||||||
WithToken(token string) callerForPsiphonConfigAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
// clonerForTorTargetsAPI represents any type exposing a method
|
|
||||||
// like simpleTorTargetsAPI.WithToken.
|
|
||||||
type clonerForTorTargetsAPI interface {
|
|
||||||
WithToken(token string) callerForTorTargetsAPI
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
package ooapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/gob"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
)
|
|
||||||
|
|
||||||
type defaultRequestMaker struct{}
|
|
||||||
|
|
||||||
func (*defaultRequestMaker) NewRequest(
|
|
||||||
ctx context.Context, method, URL string, body io.Reader) (*http.Request, error) {
|
|
||||||
return http.NewRequestWithContext(ctx, method, URL, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
type defaultJSONCodec struct{}
|
|
||||||
|
|
||||||
func (*defaultJSONCodec) Encode(v interface{}) ([]byte, error) {
|
|
||||||
return json.Marshal(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*defaultJSONCodec) Decode(b []byte, v interface{}) error {
|
|
||||||
return json.Unmarshal(b, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
type defaultTemplateExecutor struct{}
|
|
||||||
|
|
||||||
func (*defaultTemplateExecutor) Execute(tmpl string, v interface{}) (string, error) {
|
|
||||||
to, err := template.New("t").Parse(tmpl)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
var sb strings.Builder
|
|
||||||
if err := to.Execute(&sb, v); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return sb.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type defaultGobCodec struct{}
|
|
||||||
|
|
||||||
func (*defaultGobCodec) Encode(v interface{}) ([]byte, error) {
|
|
||||||
var bb bytes.Buffer
|
|
||||||
if err := gob.NewEncoder(&bb).Encode(v); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return bb.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*defaultGobCodec) Decode(b []byte, v interface{}) error {
|
|
||||||
return gob.NewDecoder(bytes.NewReader(b)).Decode(v)
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package ooapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDefaultTemplateExecutorParseError(t *testing.T) {
|
|
||||||
te := &defaultTemplateExecutor{}
|
|
||||||
out, err := te.Execute("{{ .Foo", nil)
|
|
||||||
if err == nil || !strings.HasSuffix(err.Error(), "unclosed action") {
|
|
||||||
t.Fatal("not the error we expected", err)
|
|
||||||
}
|
|
||||||
if out != "" {
|
|
||||||
t.Fatal("expected empty string")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultTemplateExecutorExecError(t *testing.T) {
|
|
||||||
te := &defaultTemplateExecutor{}
|
|
||||||
arg := make(chan interface{})
|
|
||||||
out, err := te.Execute("{{ .Foo }}", arg)
|
|
||||||
if err == nil || !strings.Contains(err.Error(), `can't evaluate field Foo`) {
|
|
||||||
t.Fatal("not the error we expected", err)
|
|
||||||
}
|
|
||||||
if out != "" {
|
|
||||||
t.Fatal("expected empty string")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultGobCodecEncodeError(t *testing.T) {
|
|
||||||
codec := &defaultGobCodec{}
|
|
||||||
arg := make(chan interface{})
|
|
||||||
data, err := codec.Encode(arg)
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "can't handle type") {
|
|
||||||
t.Fatal("not the error we expected", err)
|
|
||||||
}
|
|
||||||
if data != nil {
|
|
||||||
t.Fatal("expected nil data")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
package ooapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
// JSONCodec is a JSON encoder and decoder. Generally, we use a
|
|
||||||
// default JSONCodec in Client. This is the interface to implement
|
|
||||||
// if you want to override such a default.
|
|
||||||
type JSONCodec interface {
|
|
||||||
// Encode encodes v as a serialized JSON byte slice.
|
|
||||||
Encode(v interface{}) ([]byte, error)
|
|
||||||
|
|
||||||
// Decode decodes the serialized JSON byte slice into v.
|
|
||||||
Decode(b []byte, v interface{}) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestMaker makes an HTTP request. Generally, we use a
|
|
||||||
// default RequestMaker in Client. This is the interface to implement
|
|
||||||
// if you want to override such a default.
|
|
||||||
type RequestMaker interface {
|
|
||||||
// NewRequest creates a new HTTP request.
|
|
||||||
NewRequest(ctx context.Context, method, URL string, body io.Reader) (*http.Request, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// templateExecutor parses and executes a text template.
|
|
||||||
type templateExecutor interface {
|
|
||||||
// Execute takes in input a template string and some piece of data. It
|
|
||||||
// returns either a string where template parameters have been replaced,
|
|
||||||
// on success, or an error, on failure.
|
|
||||||
Execute(tmpl string, v interface{}) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPClient is the interface of a generic HTTP client. The
|
|
||||||
// stdlib's http.Client implements this interface. We use
|
|
||||||
// http.DefaultClient as the default HTTPClient used by Client.
|
|
||||||
// Consumers of this package typically provide a custom HTTPClient
|
|
||||||
// with additional functionality (e.g., DoH, circumvention).
|
|
||||||
type HTTPClient = model.HTTPClient
|
|
||||||
|
|
||||||
// GobCodec is a Gob encoder and decoder. Generally, we use a
|
|
||||||
// default GobCodec in Client. This is the interface to implement
|
|
||||||
// if you want to override such a default.
|
|
||||||
type GobCodec interface {
|
|
||||||
// Encode encodes v as a serialized gob byte slice.
|
|
||||||
Encode(v interface{}) ([]byte, error)
|
|
||||||
|
|
||||||
// Decode decodes the serialized gob byte slice into v.
|
|
||||||
Decode(b []byte, v interface{}) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// KVStore is a key-value store. This is the interface the
|
|
||||||
// client expect for the key-value store used to save persistent
|
|
||||||
// state (typically on the file system).
|
|
||||||
type KVStore = model.KeyValueStore
|
|
|
@ -1,58 +0,0 @@
|
||||||
// Package ooapi contains a client for the OONI API. We
|
|
||||||
// automatically generate the code in this package from the
|
|
||||||
// apimodel and internal/generator packages.
|
|
||||||
//
|
|
||||||
// Note
|
|
||||||
//
|
|
||||||
// This package is currrently unused. We plan on replacing
|
|
||||||
// existing code to speak with the OONI API with it.
|
|
||||||
//
|
|
||||||
// Usage
|
|
||||||
//
|
|
||||||
// You need to create a Client. Make sure you set all
|
|
||||||
// the mandatory fields. You will then have a function
|
|
||||||
// for every supported OONI API. This function will
|
|
||||||
// take in input a context and a request. You need to
|
|
||||||
// fill the request, of course. The return value is
|
|
||||||
// either a response or an error.
|
|
||||||
//
|
|
||||||
// If an API requires login, we will automatically
|
|
||||||
// perform the login. If an API uses caching, we will
|
|
||||||
// automatically use the cache.
|
|
||||||
//
|
|
||||||
// Design
|
|
||||||
//
|
|
||||||
// Most of the code in this package is auto-generated from the
|
|
||||||
// data model in ./apimodel and the definition of APIs provided
|
|
||||||
// by ./internal/generator/spec.go.
|
|
||||||
//
|
|
||||||
// We keep the generated files up-to-date by running
|
|
||||||
//
|
|
||||||
// go generate ./...
|
|
||||||
//
|
|
||||||
// We have tests that ensure that the definition of the API
|
|
||||||
// used here is reasonably close to the server's one.
|
|
||||||
//
|
|
||||||
// Testing
|
|
||||||
//
|
|
||||||
// The following command
|
|
||||||
//
|
|
||||||
// go test ./...
|
|
||||||
//
|
|
||||||
// will, among other things, ensure that the our API spec
|
|
||||||
// is consistent with the server's one. Running
|
|
||||||
//
|
|
||||||
// go test -short ./...
|
|
||||||
//
|
|
||||||
// will exclude most (slow) integration tests.
|
|
||||||
//
|
|
||||||
// Architecture
|
|
||||||
//
|
|
||||||
// The ./apimodel sub-package contains the definition of request
|
|
||||||
// and response messages. We rely on tagging to specify how
|
|
||||||
// we should encode and decode messages.
|
|
||||||
//
|
|
||||||
// The ./internal/generator sub-package contains code to generate most
|
|
||||||
// code in this package. In particular, the spec.go file is
|
|
||||||
// the specification of the APIs.
|
|
||||||
package ooapi
|
|
|
@ -1,14 +0,0 @@
|
||||||
package ooapi
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
// Errors defined by this package.
|
|
||||||
var (
|
|
||||||
ErrAPICallFailed = errors.New("ooapi: API call failed")
|
|
||||||
ErrEmptyField = errors.New("ooapi: empty field")
|
|
||||||
ErrHTTPFailure = errors.New("ooapi: http request failed")
|
|
||||||
ErrJSONLiteralNull = errors.New("ooapi: server returned us a literal null")
|
|
||||||
ErrMissingToken = errors.New("ooapi: missing auth token")
|
|
||||||
ErrUnauthorized = errors.New("ooapi: not authorized")
|
|
||||||
errCacheNotFound = errors.New("ooapi: not found in cache")
|
|
||||||
)
|
|
|
@ -1,103 +0,0 @@
|
||||||
package ooapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/fakefill"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
// fakeFill forwards the fakefill.Filler type
|
|
||||||
type fakeFill = fakefill.Filler
|
|
||||||
|
|
||||||
type FakeCodec struct {
|
|
||||||
DecodeErr error
|
|
||||||
EncodeData []byte
|
|
||||||
EncodeErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *FakeCodec) Encode(v interface{}) ([]byte, error) {
|
|
||||||
return mc.EncodeData, mc.EncodeErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *FakeCodec) Decode(b []byte, v interface{}) error {
|
|
||||||
return mc.DecodeErr
|
|
||||||
}
|
|
||||||
|
|
||||||
type FakeHTTPClient struct {
|
|
||||||
Err error
|
|
||||||
Resp *http.Response
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *FakeHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
|
||||||
time.Sleep(10 * time.Microsecond)
|
|
||||||
if req.Body != nil {
|
|
||||||
_, _ = netxlite.ReadAllContext(req.Context(), req.Body)
|
|
||||||
req.Body.Close()
|
|
||||||
}
|
|
||||||
if c.Err != nil {
|
|
||||||
return nil, c.Err
|
|
||||||
}
|
|
||||||
c.Resp.Request = req // non thread safe but it doesn't matter
|
|
||||||
return c.Resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *FakeHTTPClient) CloseIdleConnections() {}
|
|
||||||
|
|
||||||
type FakeBody struct {
|
|
||||||
Data []byte
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fb *FakeBody) Read(p []byte) (int, error) {
|
|
||||||
time.Sleep(10 * time.Microsecond)
|
|
||||||
if fb.Err != nil {
|
|
||||||
return 0, fb.Err
|
|
||||||
}
|
|
||||||
if len(fb.Data) <= 0 {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
n := copy(p, fb.Data)
|
|
||||||
fb.Data = fb.Data[n:]
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fb *FakeBody) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type FakeRequestMaker struct {
|
|
||||||
Req *http.Request
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (frm *FakeRequestMaker) NewRequest(
|
|
||||||
ctx context.Context, method, URL string, body io.Reader) (*http.Request, error) {
|
|
||||||
return frm.Req, frm.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
type FakeTemplateExecutor struct {
|
|
||||||
Out string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fte *FakeTemplateExecutor) Execute(tmpl string, v interface{}) (string, error) {
|
|
||||||
return fte.Out, fte.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
type FakeKVStore struct {
|
|
||||||
SetError error
|
|
||||||
GetData []byte
|
|
||||||
GetError error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *FakeKVStore) Get(key string) ([]byte, error) {
|
|
||||||
return fs.GetData, fs.GetError
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *FakeKVStore) Set(key string, value []byte) error {
|
|
||||||
return fs.SetError
|
|
||||||
}
|
|
|
@ -1,212 +0,0 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
|
||||||
// 2022-05-19 20:30:49.546037 +0200 CEST m=+0.000695501
|
|
||||||
|
|
||||||
package ooapi
|
|
||||||
|
|
||||||
//go:generate go run ./internal/generator -file fakeapi_test.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/atomicx"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FakeCheckReportIDAPI struct {
|
|
||||||
Err error
|
|
||||||
Response *apimodel.CheckReportIDResponse
|
|
||||||
CountCall *atomicx.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *FakeCheckReportIDAPI) Call(ctx context.Context, req *apimodel.CheckReportIDRequest) (*apimodel.CheckReportIDResponse, error) {
|
|
||||||
if fapi.CountCall != nil {
|
|
||||||
fapi.CountCall.Add(1)
|
|
||||||
}
|
|
||||||
return fapi.Response, fapi.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ callerForCheckReportIDAPI = &FakeCheckReportIDAPI{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type FakeCheckInAPI struct {
|
|
||||||
Err error
|
|
||||||
Response *apimodel.CheckInResponse
|
|
||||||
CountCall *atomicx.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *FakeCheckInAPI) Call(ctx context.Context, req *apimodel.CheckInRequest) (*apimodel.CheckInResponse, error) {
|
|
||||||
if fapi.CountCall != nil {
|
|
||||||
fapi.CountCall.Add(1)
|
|
||||||
}
|
|
||||||
return fapi.Response, fapi.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ callerForCheckInAPI = &FakeCheckInAPI{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type FakeLoginAPI struct {
|
|
||||||
Err error
|
|
||||||
Response *apimodel.LoginResponse
|
|
||||||
CountCall *atomicx.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *FakeLoginAPI) Call(ctx context.Context, req *apimodel.LoginRequest) (*apimodel.LoginResponse, error) {
|
|
||||||
if fapi.CountCall != nil {
|
|
||||||
fapi.CountCall.Add(1)
|
|
||||||
}
|
|
||||||
return fapi.Response, fapi.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ callerForLoginAPI = &FakeLoginAPI{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type FakeMeasurementMetaAPI struct {
|
|
||||||
Err error
|
|
||||||
Response *apimodel.MeasurementMetaResponse
|
|
||||||
CountCall *atomicx.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *FakeMeasurementMetaAPI) Call(ctx context.Context, req *apimodel.MeasurementMetaRequest) (*apimodel.MeasurementMetaResponse, error) {
|
|
||||||
if fapi.CountCall != nil {
|
|
||||||
fapi.CountCall.Add(1)
|
|
||||||
}
|
|
||||||
return fapi.Response, fapi.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ callerForMeasurementMetaAPI = &FakeMeasurementMetaAPI{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type FakeRegisterAPI struct {
|
|
||||||
Err error
|
|
||||||
Response *apimodel.RegisterResponse
|
|
||||||
CountCall *atomicx.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *FakeRegisterAPI) Call(ctx context.Context, req *apimodel.RegisterRequest) (*apimodel.RegisterResponse, error) {
|
|
||||||
if fapi.CountCall != nil {
|
|
||||||
fapi.CountCall.Add(1)
|
|
||||||
}
|
|
||||||
return fapi.Response, fapi.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ callerForRegisterAPI = &FakeRegisterAPI{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type FakeTestHelpersAPI struct {
|
|
||||||
Err error
|
|
||||||
Response apimodel.TestHelpersResponse
|
|
||||||
CountCall *atomicx.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *FakeTestHelpersAPI) Call(ctx context.Context, req *apimodel.TestHelpersRequest) (apimodel.TestHelpersResponse, error) {
|
|
||||||
if fapi.CountCall != nil {
|
|
||||||
fapi.CountCall.Add(1)
|
|
||||||
}
|
|
||||||
return fapi.Response, fapi.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ callerForTestHelpersAPI = &FakeTestHelpersAPI{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type FakePsiphonConfigAPI struct {
|
|
||||||
WithResult callerForPsiphonConfigAPI
|
|
||||||
Err error
|
|
||||||
Response apimodel.PsiphonConfigResponse
|
|
||||||
CountCall *atomicx.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *FakePsiphonConfigAPI) Call(ctx context.Context, req *apimodel.PsiphonConfigRequest) (apimodel.PsiphonConfigResponse, error) {
|
|
||||||
if fapi.CountCall != nil {
|
|
||||||
fapi.CountCall.Add(1)
|
|
||||||
}
|
|
||||||
return fapi.Response, fapi.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *FakePsiphonConfigAPI) WithToken(token string) callerForPsiphonConfigAPI {
|
|
||||||
return fapi.WithResult
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ callerForPsiphonConfigAPI = &FakePsiphonConfigAPI{}
|
|
||||||
_ clonerForPsiphonConfigAPI = &FakePsiphonConfigAPI{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type FakeTorTargetsAPI struct {
|
|
||||||
WithResult callerForTorTargetsAPI
|
|
||||||
Err error
|
|
||||||
Response apimodel.TorTargetsResponse
|
|
||||||
CountCall *atomicx.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *FakeTorTargetsAPI) Call(ctx context.Context, req *apimodel.TorTargetsRequest) (apimodel.TorTargetsResponse, error) {
|
|
||||||
if fapi.CountCall != nil {
|
|
||||||
fapi.CountCall.Add(1)
|
|
||||||
}
|
|
||||||
return fapi.Response, fapi.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *FakeTorTargetsAPI) WithToken(token string) callerForTorTargetsAPI {
|
|
||||||
return fapi.WithResult
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ callerForTorTargetsAPI = &FakeTorTargetsAPI{}
|
|
||||||
_ clonerForTorTargetsAPI = &FakeTorTargetsAPI{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type FakeURLsAPI struct {
|
|
||||||
Err error
|
|
||||||
Response *apimodel.URLsResponse
|
|
||||||
CountCall *atomicx.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *FakeURLsAPI) Call(ctx context.Context, req *apimodel.URLsRequest) (*apimodel.URLsResponse, error) {
|
|
||||||
if fapi.CountCall != nil {
|
|
||||||
fapi.CountCall.Add(1)
|
|
||||||
}
|
|
||||||
return fapi.Response, fapi.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ callerForURLsAPI = &FakeURLsAPI{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type FakeOpenReportAPI struct {
|
|
||||||
Err error
|
|
||||||
Response *apimodel.OpenReportResponse
|
|
||||||
CountCall *atomicx.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *FakeOpenReportAPI) Call(ctx context.Context, req *apimodel.OpenReportRequest) (*apimodel.OpenReportResponse, error) {
|
|
||||||
if fapi.CountCall != nil {
|
|
||||||
fapi.CountCall.Add(1)
|
|
||||||
}
|
|
||||||
return fapi.Response, fapi.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ callerForOpenReportAPI = &FakeOpenReportAPI{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type FakeSubmitMeasurementAPI struct {
|
|
||||||
Err error
|
|
||||||
Response *apimodel.SubmitMeasurementResponse
|
|
||||||
CountCall *atomicx.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fapi *FakeSubmitMeasurementAPI) Call(ctx context.Context, req *apimodel.SubmitMeasurementRequest) (*apimodel.SubmitMeasurementResponse, error) {
|
|
||||||
if fapi.CountCall != nil {
|
|
||||||
fapi.CountCall.Add(1)
|
|
||||||
}
|
|
||||||
return fapi.Response, fapi.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ callerForSubmitMeasurementAPI = &FakeSubmitMeasurementAPI{}
|
|
||||||
)
|
|
|
@ -1,23 +0,0 @@
|
||||||
package ooapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type VerboseHTTPClient struct {
|
|
||||||
T *testing.T
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *VerboseHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
|
||||||
c.T.Logf("> %s %s", req.Method, req.URL.String())
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
c.T.Logf("< %s", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.T.Logf("< %d", resp.StatusCode)
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *VerboseHTTPClient) CloseIdleConnections() {}
|
|
|
@ -1,167 +0,0 @@
|
||||||
package ooapi_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/kvstore"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestWithRealServerDoCheckIn(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skip test in short mode")
|
|
||||||
}
|
|
||||||
req := &apimodel.CheckInRequest{
|
|
||||||
Charging: true,
|
|
||||||
OnWiFi: true,
|
|
||||||
Platform: "android",
|
|
||||||
ProbeASN: "AS12353",
|
|
||||||
ProbeCC: "IT",
|
|
||||||
RunType: model.RunTypeTimed,
|
|
||||||
SoftwareName: "ooniprobe-android",
|
|
||||||
SoftwareVersion: "2.7.1",
|
|
||||||
WebConnectivity: apimodel.CheckInRequestWebConnectivity{
|
|
||||||
CategoryCodes: []string{"NEWS", "CULTR"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
httpClnt := &ooapi.VerboseHTTPClient{T: t}
|
|
||||||
clnt := &ooapi.Client{HTTPClient: httpClnt, KVStore: &kvstore.Memory{}}
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.CheckIn(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non nil pointer here")
|
|
||||||
}
|
|
||||||
for idx, url := range resp.Tests.WebConnectivity.URLs {
|
|
||||||
if idx >= 3 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
t.Logf("- %+v", url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithRealServerDoCheckReportID(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skip test in short mode")
|
|
||||||
}
|
|
||||||
req := &apimodel.CheckReportIDRequest{
|
|
||||||
ReportID: "20210223T093606Z_ndt_JO_8376_n1_kDYToqrugDY54Soy",
|
|
||||||
}
|
|
||||||
clnt := &ooapi.Client{KVStore: &kvstore.Memory{}}
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.CheckReportID(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non nil pointer here")
|
|
||||||
}
|
|
||||||
t.Logf("%+v", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithRealServerDoMeasurementMeta(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skip test in short mode")
|
|
||||||
}
|
|
||||||
req := &apimodel.MeasurementMetaRequest{
|
|
||||||
ReportID: "20210223T093606Z_ndt_JO_8376_n1_kDYToqrugDY54Soy",
|
|
||||||
}
|
|
||||||
clnt := &ooapi.Client{KVStore: &kvstore.Memory{}}
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.MeasurementMeta(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non nil pointer here")
|
|
||||||
}
|
|
||||||
t.Logf("%+v", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithRealServerDoOpenReport(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skip test in short mode")
|
|
||||||
}
|
|
||||||
req := &apimodel.OpenReportRequest{
|
|
||||||
DataFormatVersion: "0.2.0",
|
|
||||||
Format: "json",
|
|
||||||
ProbeASN: "AS137",
|
|
||||||
ProbeCC: "IT",
|
|
||||||
SoftwareName: "miniooni",
|
|
||||||
SoftwareVersion: "0.1.0-dev",
|
|
||||||
TestName: "example",
|
|
||||||
TestStartTime: "2018-11-01 15:33:20",
|
|
||||||
TestVersion: "0.1.0",
|
|
||||||
}
|
|
||||||
clnt := &ooapi.Client{KVStore: &kvstore.Memory{}}
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.OpenReport(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non nil pointer here")
|
|
||||||
}
|
|
||||||
t.Logf("%+v", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithRealServerDoPsiphonConfig(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skip test in short mode")
|
|
||||||
}
|
|
||||||
req := &apimodel.PsiphonConfigRequest{}
|
|
||||||
httpClnt := &ooapi.VerboseHTTPClient{T: t}
|
|
||||||
clnt := &ooapi.Client{HTTPClient: httpClnt, KVStore: &kvstore.Memory{}}
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.PsiphonConfig(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non nil pointer here")
|
|
||||||
}
|
|
||||||
t.Logf("%+v", resp != nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithRealServerDoTorTargets(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skip test in short mode")
|
|
||||||
}
|
|
||||||
req := &apimodel.TorTargetsRequest{}
|
|
||||||
httpClnt := &ooapi.VerboseHTTPClient{T: t}
|
|
||||||
clnt := &ooapi.Client{HTTPClient: httpClnt, KVStore: &kvstore.Memory{}}
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.TorTargets(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non nil pointer here")
|
|
||||||
}
|
|
||||||
t.Logf("%+v", resp != nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithRealServerDoURLs(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skip test in short mode")
|
|
||||||
}
|
|
||||||
req := &apimodel.URLsRequest{
|
|
||||||
CountryCode: "IT",
|
|
||||||
Limit: 3,
|
|
||||||
}
|
|
||||||
clnt := &ooapi.Client{KVStore: &kvstore.Memory{}}
|
|
||||||
ctx := context.Background()
|
|
||||||
resp, err := clnt.URLs(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
t.Fatal("expected non nil pointer here")
|
|
||||||
}
|
|
||||||
t.Logf("%+v", resp)
|
|
||||||
}
|
|
|
@ -1,181 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// apiField contains the fields of an API data structure
|
|
||||||
type apiField struct {
|
|
||||||
// name is the field name
|
|
||||||
name string
|
|
||||||
|
|
||||||
// kind is the filed type
|
|
||||||
kind string
|
|
||||||
|
|
||||||
// comment is a brief comment to document the field
|
|
||||||
comment string
|
|
||||||
|
|
||||||
// ifLogin indicates whether this field should only be
|
|
||||||
// emitted when the API requires login
|
|
||||||
ifLogin bool
|
|
||||||
|
|
||||||
// ifTemplate indicates whether this field should only be
|
|
||||||
// emitted when the URL path is a template
|
|
||||||
ifTemplate bool
|
|
||||||
|
|
||||||
// noClone is true when this field should not be copied
|
|
||||||
// from the parent data structure when cloning
|
|
||||||
noClone bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var apiFields = []apiField{{
|
|
||||||
name: "BaseURL",
|
|
||||||
kind: "string",
|
|
||||||
comment: "optional",
|
|
||||||
}, {
|
|
||||||
name: "HTTPClient",
|
|
||||||
kind: "HTTPClient",
|
|
||||||
comment: "optional",
|
|
||||||
}, {
|
|
||||||
name: "JSONCodec",
|
|
||||||
kind: "JSONCodec",
|
|
||||||
comment: "optional",
|
|
||||||
}, {
|
|
||||||
name: "Token",
|
|
||||||
kind: "string",
|
|
||||||
comment: "mandatory",
|
|
||||||
ifLogin: true,
|
|
||||||
noClone: true,
|
|
||||||
}, {
|
|
||||||
name: "RequestMaker",
|
|
||||||
kind: "RequestMaker",
|
|
||||||
comment: "optional",
|
|
||||||
}, {
|
|
||||||
name: "TemplateExecutor",
|
|
||||||
kind: "templateExecutor",
|
|
||||||
comment: "optional",
|
|
||||||
ifTemplate: true,
|
|
||||||
}, {
|
|
||||||
name: "UserAgent",
|
|
||||||
kind: "string",
|
|
||||||
comment: "optional",
|
|
||||||
}}
|
|
||||||
|
|
||||||
func (d *Descriptor) genNewAPI(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "// %s implements the %s API.\n", d.APIStructName(), d.Name)
|
|
||||||
fmt.Fprintf(sb, "type %s struct {\n", d.APIStructName())
|
|
||||||
for _, f := range apiFields {
|
|
||||||
if !d.RequiresLogin && f.ifLogin {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !d.URLPath.IsTemplate && f.ifTemplate {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "\t%s %s // %s\n", f.name, f.kind, f.comment)
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
if d.RequiresLogin {
|
|
||||||
fmt.Fprintf(sb, "// WithToken returns a copy of the API where the\n")
|
|
||||||
fmt.Fprintf(sb, "// value of the Token field is replaced with token.\n")
|
|
||||||
fmt.Fprintf(sb, "func (api *%s) WithToken(token string) %s {\n",
|
|
||||||
d.APIStructName(), d.CallerInterfaceName())
|
|
||||||
fmt.Fprintf(sb, "out := &%s{}\n", d.APIStructName())
|
|
||||||
for _, f := range apiFields {
|
|
||||||
if !d.URLPath.IsTemplate && f.ifTemplate {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if f.noClone == true {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "out.%s = api.%s\n", f.name, f.name)
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "out.Token = token\n")
|
|
||||||
fmt.Fprint(sb, "return out\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (api *%s) baseURL() string {\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\tif api.BaseURL != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn api.BaseURL\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn \"https://ps1.ooni.io\"\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (api *%s) requestMaker() RequestMaker {\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\tif api.RequestMaker != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn api.RequestMaker\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn &defaultRequestMaker{}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (api *%s) jsonCodec() JSONCodec {\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\tif api.JSONCodec != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn api.JSONCodec\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn &defaultJSONCodec{}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
if d.URLPath.IsTemplate {
|
|
||||||
fmt.Fprintf(
|
|
||||||
sb, "func (api *%s) templateExecutor() templateExecutor {\n",
|
|
||||||
d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\tif api.TemplateExecutor != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn api.TemplateExecutor\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn &defaultTemplateExecutor{}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(
|
|
||||||
sb, "func (api *%s) httpClient() HTTPClient {\n",
|
|
||||||
d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\tif api.HTTPClient != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn api.HTTPClient\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn http.DefaultClient\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "// Call calls the %s API.\n", d.Name)
|
|
||||||
fmt.Fprintf(
|
|
||||||
sb, "func (api *%s) Call(ctx context.Context, req %s) (%s, error) {\n",
|
|
||||||
d.APIStructName(), d.RequestTypeName(), d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\thttpReq, err := api.newRequest(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\thttpReq.Header.Add(\"Accept\", \"application/json\")\n")
|
|
||||||
if d.RequiresLogin {
|
|
||||||
fmt.Fprint(sb, "\tif api.Token == \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, ErrMissingToken\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\thttpReq.Header.Add(\"Authorization\", newAuthorizationHeader(api.Token))\n")
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\tif api.UserAgent != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\thttpReq.Header.Add(\"User-Agent\", api.UserAgent)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\thttpResp, err := api.httpClient().Do(httpReq)\n")
|
|
||||||
fmt.Fprint(sb, "\treturn api.newResponse(ctx, httpResp, err)\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenAPIsGo generates apis.go.
|
|
||||||
func GenAPIsGo(file string) {
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
fmt.Fprint(&sb, "import (\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"context\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"net/http\"\n")
|
|
||||||
fmt.Fprint(&sb, "\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
|
|
||||||
fmt.Fprint(&sb, ")\n")
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
desc.genNewAPI(&sb)
|
|
||||||
}
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,461 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestNewRequest(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "\treq := &%s{}\n", d.RequestTypeNameAsStruct())
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(req)\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestInvalidURL(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sInvalidURL(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: \"\\t\", // invalid\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
d.genTestNewRequest(sb)
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err == nil || !strings.HasSuffix(err.Error(), \"invalid control character in URL\") {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestWithMissingToken(sb *strings.Builder) {
|
|
||||||
if d.RequiresLogin == false {
|
|
||||||
return // does not make sense when login isn't required
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "func Test%sWithMissingToken(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{} // no token\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
d.genTestNewRequest(sb)
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, ErrMissingToken) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestWithHTTPErr(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sWithHTTPErr(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Err: errMocked}\n")
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
|
|
||||||
if d.RequiresLogin == true {
|
|
||||||
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
d.genTestNewRequest(sb)
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestMarshalErr(sb *strings.Builder) {
|
|
||||||
if d.Method != "POST" {
|
|
||||||
return // does not make sense when we don't send a request body
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "func Test%sMarshalErr(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tJSONCodec: &FakeCodec{EncodeErr: errMocked},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
d.genTestNewRequest(sb)
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestWithNewRequestErr(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sWithNewRequestErr(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tRequestMaker: &FakeRequestMaker{Err: errMocked},\n")
|
|
||||||
if d.RequiresLogin == true {
|
|
||||||
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
d.genTestNewRequest(sb)
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestWith401(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sWith401(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}}\n")
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
|
|
||||||
if d.RequiresLogin == true {
|
|
||||||
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
d.genTestNewRequest(sb)
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, ErrUnauthorized) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestWith400(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sWith400(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}}\n")
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
|
|
||||||
if d.RequiresLogin == true {
|
|
||||||
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
d.genTestNewRequest(sb)
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, ErrHTTPFailure) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestWithResponseBodyReadErr(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sWithResponseBodyReadErr(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tStatusCode: 200,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBody: &FakeBody{Err: errMocked},\n")
|
|
||||||
fmt.Fprint(sb, "\t}}\n")
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
|
|
||||||
if d.RequiresLogin == true {
|
|
||||||
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
d.genTestNewRequest(sb)
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestWithUnmarshalFailure(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sWithUnmarshalFailure(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tStatusCode: 200,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBody: &FakeBody{Data: []byte(`{}`)},\n")
|
|
||||||
fmt.Fprint(sb, "\t}}\n")
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tJSONCodec: &FakeCodec{DecodeErr: errMocked},\n")
|
|
||||||
if d.RequiresLogin == true {
|
|
||||||
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
d.genTestNewRequest(sb)
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestRoundTrip(sb *strings.Builder) {
|
|
||||||
// generate the type of the handler
|
|
||||||
fmt.Fprintf(sb, "type handle%s struct {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\taccept string\n")
|
|
||||||
fmt.Fprint(sb, "\tbody []byte\n")
|
|
||||||
fmt.Fprint(sb, "\tcontentType string\n")
|
|
||||||
fmt.Fprint(sb, "\tcount int32\n")
|
|
||||||
fmt.Fprint(sb, "\tmethod string\n")
|
|
||||||
fmt.Fprint(sb, "\tmu sync.Mutex\n")
|
|
||||||
fmt.Fprintf(sb, "\tresp %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\turl *url.URL\n")
|
|
||||||
fmt.Fprint(sb, "\tuserAgent string\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
// generate the handling function
|
|
||||||
fmt.Fprintf(sb,
|
|
||||||
"func (h *handle%s) ServeHTTP(w http.ResponseWriter, r *http.Request) {",
|
|
||||||
d.Name)
|
|
||||||
fmt.Fprint(sb, "\tdefer h.mu.Unlock()\n")
|
|
||||||
fmt.Fprint(sb, "\th.mu.Lock()\n")
|
|
||||||
fmt.Fprint(sb, "\tif h.count > 0 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tw.WriteHeader(400)\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\th.count++\n")
|
|
||||||
fmt.Fprint(sb, "\tif r.Body != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tdata, err := netxlite.ReadAllContext(r.Context(), r.Body)\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\t\tw.WriteHeader(400)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\t\treturn\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\th.body = data\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\th.method = r.Method\n")
|
|
||||||
fmt.Fprint(sb, "\th.url = r.URL\n")
|
|
||||||
fmt.Fprint(sb, "\th.accept = r.Header.Get(\"Accept\")\n")
|
|
||||||
fmt.Fprint(sb, "\th.contentType = r.Header.Get(\"Content-Type\")\n")
|
|
||||||
fmt.Fprint(sb, "\th.userAgent = r.Header.Get(\"User-Agent\")\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar out %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff := fakeFill{}\n")
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&out)\n")
|
|
||||||
fmt.Fprintf(sb, "\th.resp = out\n")
|
|
||||||
fmt.Fprintf(sb, "\tdata, err := json.Marshal(out)\n")
|
|
||||||
fmt.Fprintf(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tw.WriteHeader(400)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\treturn\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tw.Write(data)\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n\n")
|
|
||||||
|
|
||||||
// generate the test itself
|
|
||||||
fmt.Fprintf(sb, "func Test%sRoundTrip(t *testing.T) {\n", d.Name)
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// setup\n")
|
|
||||||
fmt.Fprintf(sb, "\thandler := &handle%s{}\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
|
|
||||||
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
|
|
||||||
fmt.Fprintf(sb, "\treq := &%s{}\n", d.RequestTypeNameAsStruct())
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{BaseURL: srvr.URL}\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&api.UserAgent)\n")
|
|
||||||
if d.RequiresLogin {
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&api.Token)\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// issue request\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response here\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// compare our response and server's one\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.resp, resp); diff != \"\" {")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// check whether headers are OK\n")
|
|
||||||
fmt.Fprint(sb, "\tif handler.accept != \"application/json\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid accept header\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif handler.userAgent != api.UserAgent {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid user-agent header\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// check whether the method is OK\n")
|
|
||||||
fmt.Fprintf(sb, "\tif handler.method != \"%s\" {\n", d.Method)
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid method\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
if d.Method == "POST" {
|
|
||||||
fmt.Fprint(sb, "\t// check the body\n")
|
|
||||||
fmt.Fprint(sb, "\tif handler.contentType != \"application/json\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid content-type header\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tgot := &%s{}\n", d.RequestTypeNameAsStruct())
|
|
||||||
fmt.Fprintf(sb, "\tif err := json.Unmarshal(handler.body, &got); err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(req, got); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
} else {
|
|
||||||
fmt.Fprint(sb, "\t// check the query\n")
|
|
||||||
fmt.Fprint(sb, "\thttpReq, err := api.newRequest(context.Background(), req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestResponseLiteralNull(sb *strings.Builder) {
|
|
||||||
switch d.ResponseTypeKind() {
|
|
||||||
case reflect.Map:
|
|
||||||
// fallthrough
|
|
||||||
case reflect.Struct:
|
|
||||||
return // test not applicable
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "func Test%sResponseLiteralNull(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tStatusCode: 200,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBody: &FakeBody{Data: []byte(`null`)},\n")
|
|
||||||
fmt.Fprint(sb, "\t}}\n")
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
|
|
||||||
if d.RequiresLogin == true {
|
|
||||||
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
d.genTestNewRequest(sb)
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, ErrJSONLiteralNull) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestMandatoryFields(sb *strings.Builder) {
|
|
||||||
fields := d.StructFieldsWithTag(d.Request, tagForRequired)
|
|
||||||
if len(fields) < 1 {
|
|
||||||
return // nothing to test
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "func Test%sMandatoryFields(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tStatusCode: 500,\n")
|
|
||||||
fmt.Fprint(sb, "\t}}\n")
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
|
|
||||||
if d.RequiresLogin == true {
|
|
||||||
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
fmt.Fprintf(sb, "\treq := &%s{} // deliberately empty\n", d.RequestTypeNameAsStruct())
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, ErrEmptyField) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestTemplateErr(sb *strings.Builder) {
|
|
||||||
if !d.URLPath.IsTemplate {
|
|
||||||
return // nothing to test
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "func Test%sTemplateErr(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tStatusCode: 500,\n")
|
|
||||||
fmt.Fprint(sb, "\t}}\n")
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
|
|
||||||
if d.RequiresLogin == true {
|
|
||||||
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\t\tTemplateExecutor: &FakeTemplateExecutor{Err: errMocked},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
d.genTestNewRequest(sb)
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(bassosimone): we should add a panic for every switch for
|
|
||||||
// the type of a request or a response for robustness.
|
|
||||||
|
|
||||||
func (d *Descriptor) genAPITests(sb *strings.Builder) {
|
|
||||||
d.genTestInvalidURL(sb)
|
|
||||||
d.genTestWithMissingToken(sb)
|
|
||||||
d.genTestWithHTTPErr(sb)
|
|
||||||
d.genTestMarshalErr(sb)
|
|
||||||
d.genTestWithNewRequestErr(sb)
|
|
||||||
d.genTestWith401(sb)
|
|
||||||
d.genTestWith400(sb)
|
|
||||||
d.genTestWithResponseBodyReadErr(sb)
|
|
||||||
d.genTestWithUnmarshalFailure(sb)
|
|
||||||
d.genTestRoundTrip(sb)
|
|
||||||
d.genTestResponseLiteralNull(sb)
|
|
||||||
d.genTestMandatoryFields(sb)
|
|
||||||
d.genTestTemplateErr(sb)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenAPIsTestGo generates apis_test.go.
|
|
||||||
func GenAPIsTestGo(file string) {
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
fmt.Fprint(&sb, "import (\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"context\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"encoding/json\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"errors\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"net/http/httptest\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"net/http\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"net/url\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"strings\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"testing\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"sync\"\n")
|
|
||||||
fmt.Fprint(&sb, "\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/google/go-cmp/cmp\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/netxlite\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
|
|
||||||
fmt.Fprint(&sb, ")\n")
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
desc.genAPITests(&sb)
|
|
||||||
}
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Descriptor) genNewCache(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "// %s implements caching for %s.\n",
|
|
||||||
d.WithCacheAPIStructName(), d.APIStructName())
|
|
||||||
fmt.Fprintf(sb, "type %s struct {\n", d.WithCacheAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\tAPI %s // mandatory\n", d.CallerInterfaceName())
|
|
||||||
fmt.Fprint(sb, "\tGobCodec GobCodec // optional\n")
|
|
||||||
fmt.Fprint(sb, "\tKVStore KVStore // mandatory\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "type %s struct {\n", d.CacheEntryName())
|
|
||||||
fmt.Fprintf(sb, "\tReq %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprintf(sb, "\tResp %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "// Call calls the API and implements caching.\n")
|
|
||||||
fmt.Fprintf(sb, "func (c *%s) Call(ctx context.Context, req %s) (%s, error) {\n",
|
|
||||||
d.WithCacheAPIStructName(), d.RequestTypeName(), d.ResponseTypeName())
|
|
||||||
if d.CachePolicy == CacheAlways {
|
|
||||||
fmt.Fprint(sb, "\tif resp, _ := c.readcache(req); resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn resp, nil\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\tresp, err := c.API.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
if d.CachePolicy == CacheFallback {
|
|
||||||
fmt.Fprint(sb, "\t\tif resp, _ := c.readcache(req); resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\treturn resp, nil\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif err := c.writecache(req, resp); err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn resp, nil\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (c *%s) gobCodec() GobCodec {\n", d.WithCacheAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\tif c.GobCodec != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn c.GobCodec\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn &defaultGobCodec{}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (c *%s) getcache() ([]%s, error) {\n",
|
|
||||||
d.WithCacheAPIStructName(), d.CacheEntryName())
|
|
||||||
fmt.Fprintf(sb, "\tdata, err := c.KVStore.Get(\"%s\")\n", d.CacheKey())
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar out []%s\n", d.CacheEntryName())
|
|
||||||
fmt.Fprint(sb, "\tif err := c.gobCodec().Decode(data, &out); err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn out, nil\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (c *%s) setcache(in []%s) error {\n",
|
|
||||||
d.WithCacheAPIStructName(), d.CacheEntryName())
|
|
||||||
fmt.Fprint(sb, "\tdata, err := c.gobCodec().Encode(in)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\treturn c.KVStore.Set(\"%s\", data)\n", d.CacheKey())
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (c *%s) readcache(req %s) (%s, error) {\n",
|
|
||||||
d.WithCacheAPIStructName(), d.RequestTypeName(), d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tcache, err := c.getcache()\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tfor _, cur := range cache {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif reflect.DeepEqual(req, cur.Req) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\treturn cur.Resp, nil\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn nil, errCacheNotFound\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (c *%s) writecache(req %s, resp %s) error {\n",
|
|
||||||
d.WithCacheAPIStructName(), d.RequestTypeName(), d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tcache, _ := c.getcache()\n")
|
|
||||||
fmt.Fprintf(sb, "\tout := []%s{{Req: req, Resp: resp}}\n", d.CacheEntryName())
|
|
||||||
fmt.Fprint(sb, "\tconst toomany = 64\n")
|
|
||||||
fmt.Fprint(sb, "\tfor idx, cur := range cache {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif reflect.DeepEqual(req, cur.Req) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tcontinue // we already updated the cache\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif idx > toomany {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tbreak\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tout = append(out, cur)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn c.setcache(out)\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "var _ %s = &%s{}\n\n", d.CallerInterfaceName(),
|
|
||||||
d.WithCacheAPIStructName())
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenCachingGo generates caching.go.
|
|
||||||
func GenCachingGo(file string) {
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
fmt.Fprint(&sb, "import (\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"context\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"reflect\"\n")
|
|
||||||
fmt.Fprint(&sb, "\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
|
|
||||||
fmt.Fprint(&sb, ")\n")
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
if desc.CachePolicy == CacheNone {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
desc.genNewCache(&sb)
|
|
||||||
}
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,275 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestCacheSuccess(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func TestCache%sSuccess(t *testing.T) {\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&expect)\n")
|
|
||||||
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\t\tResponse: expect,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
fmt.Fprint(sb, "\tresp, err := cache.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestWriteCacheError(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func TestCache%sWriteCacheError(t *testing.T) {\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&expect)\n")
|
|
||||||
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\t\tResponse: expect,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &FakeKVStore{SetError: errMocked},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
fmt.Fprint(sb, "\tresp, err := cache.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestFailureWithNoCache(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func TestCache%sFailureWithNoCache(t *testing.T) {\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\t\tErr: errMocked,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
fmt.Fprint(sb, "\tresp, err := cache.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestFailureWithPreviousCache(sb *strings.Builder) {
|
|
||||||
// This works for both caching policies.
|
|
||||||
fmt.Fprintf(sb, "func TestCache%sFailureWithPreviousCache(t *testing.T) {\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&expect)\n")
|
|
||||||
fmt.Fprintf(sb, "\tfakeapi := &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tResponse: expect,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tAPI: fakeapi,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
fmt.Fprint(sb, "\t// first pass with no error at all\n")
|
|
||||||
fmt.Fprint(sb, "\t// use a separate scope to be sure we avoid mistakes\n")
|
|
||||||
fmt.Fprint(sb, "\t{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tresp, err := cache.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t// second pass with failure\n")
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprint(sb, "\tfakeapi.Err = errMocked\n")
|
|
||||||
fmt.Fprint(sb, "\tfakeapi.Response = nil\n")
|
|
||||||
fmt.Fprint(sb, "\tresp2, err := cache.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp2 == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp2); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestSetcacheWithEncodeError(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func TestCache%sSetcacheWithEncodeError(t *testing.T) {\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar in []%s\n", d.CacheEntryName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&in)\n")
|
|
||||||
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tGobCodec: &FakeCodec{EncodeErr: errMocked},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\terr := cache.setcache(in)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestReadCacheNotFound(sb *strings.Builder) {
|
|
||||||
if fields := d.StructFields(d.Request); len(fields) <= 0 {
|
|
||||||
// this test cannot work when there are no fields in the
|
|
||||||
// request because we will always find a match.
|
|
||||||
// TODO(bassosimone): how to avoid having uncovered code?
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "func TestCache%sReadCacheNotFound(t *testing.T) {\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar incache []%s\n", d.CacheEntryName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&incache)\n")
|
|
||||||
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\terr := cache.setcache(incache)\n")
|
|
||||||
fmt.Fprintf(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprintf(sb, "\tout, err := cache.readcache(req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errCacheNotFound) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif out != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil here\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestWriteCacheDuplicate(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func TestCache%sWriteCacheDuplicate(t *testing.T) {\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar resp1 %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&resp1)\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar resp2 %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&resp2)\n")
|
|
||||||
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\terr := cache.writecache(req, resp1)\n")
|
|
||||||
fmt.Fprintf(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\terr = cache.writecache(req, resp2)\n")
|
|
||||||
fmt.Fprintf(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tout, err := cache.readcache(req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif out == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil here\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(resp2, out); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestCachSizeLimited(sb *strings.Builder) {
|
|
||||||
if fields := d.StructFields(d.Request); len(fields) <= 0 {
|
|
||||||
// this test cannot work when there are no fields in the
|
|
||||||
// request because we will always find a match.
|
|
||||||
// TODO(bassosimone): how to avoid having uncovered code?
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "func TestCache%sCacheSizeLimited(t *testing.T) {\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar prev int\n")
|
|
||||||
fmt.Fprintf(sb, "\tfor {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\t\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tvar resp %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\t\tff.Fill(&resp)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\terr := cache.writecache(req, resp)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tout, err := cache.getcache()\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif len(out) > prev {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tprev = len(out)\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tcontinue\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tbreak\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenCachingTestGo generates caching_test.go.
|
|
||||||
func GenCachingTestGo(file string) {
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
fmt.Fprint(&sb, "import (\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"context\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"errors\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"testing\"\n")
|
|
||||||
fmt.Fprint(&sb, "\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/google/go-cmp/cmp\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/kvstore\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
|
|
||||||
fmt.Fprint(&sb, ")\n")
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
if desc.CachePolicy == CacheNone {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
desc.genTestCacheSuccess(&sb)
|
|
||||||
desc.genTestWriteCacheError(&sb)
|
|
||||||
desc.genTestFailureWithNoCache(&sb)
|
|
||||||
desc.genTestFailureWithPreviousCache(&sb)
|
|
||||||
desc.genTestSetcacheWithEncodeError(&sb)
|
|
||||||
desc.genTestReadCacheNotFound(&sb)
|
|
||||||
desc.genTestWriteCacheDuplicate(&sb)
|
|
||||||
desc.genTestCachSizeLimited(&sb)
|
|
||||||
}
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Descriptor) genNewCaller(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "// %s represents any type exposing a method\n",
|
|
||||||
d.CallerInterfaceName())
|
|
||||||
fmt.Fprintf(sb, "// like %s.Call.\n", d.APIStructName())
|
|
||||||
fmt.Fprintf(sb, "type %s interface {\n", d.CallerInterfaceName())
|
|
||||||
fmt.Fprintf(sb, "\tCall(ctx context.Context, req %s) (%s, error)\n",
|
|
||||||
d.RequestTypeName(), d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenCallersGo generates callers.go.
|
|
||||||
func GenCallersGo(file string) {
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
fmt.Fprint(&sb, "import (\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"context\"\n")
|
|
||||||
fmt.Fprint(&sb, "\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
|
|
||||||
fmt.Fprint(&sb, ")\n")
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
desc.genNewCaller(&sb)
|
|
||||||
}
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Descriptor) clientMakeAPIBase(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "&%s{\n", d.APIStructName())
|
|
||||||
for _, field := range apiFields {
|
|
||||||
if field.ifLogin || field.ifTemplate {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "\t%s: c.%s,\n", field.name, field.name)
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "}")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) clientMakeAPI(sb *strings.Builder) {
|
|
||||||
if d.RequiresLogin && d.CachePolicy != CacheNone {
|
|
||||||
panic("we don't support requiresLogin with caching")
|
|
||||||
}
|
|
||||||
if d.RequiresLogin {
|
|
||||||
fmt.Fprintf(sb, "&%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\tAPI:")
|
|
||||||
d.clientMakeAPIBase(sb)
|
|
||||||
fmt.Fprint(sb, ",\n")
|
|
||||||
fmt.Fprint(sb, "\tJSONCodec: c.JSONCodec,\n")
|
|
||||||
fmt.Fprint(sb, "\tKVStore: c.KVStore,\n")
|
|
||||||
fmt.Fprint(sb, "\tRegisterAPI: &simpleRegisterAPI{\n")
|
|
||||||
for _, field := range apiFields {
|
|
||||||
if field.ifLogin || field.ifTemplate {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "\t%s: c.%s,\n", field.name, field.name)
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\t},\n")
|
|
||||||
fmt.Fprint(sb, "\tLoginAPI: &simpleLoginAPI{\n")
|
|
||||||
for _, field := range apiFields {
|
|
||||||
if field.ifLogin || field.ifTemplate {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "\t%s: c.%s,\n", field.name, field.name)
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\t},\n")
|
|
||||||
fmt.Fprint(sb, "}\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if d.CachePolicy != CacheNone {
|
|
||||||
fmt.Fprintf(sb, "&%s{\n", d.WithCacheAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\tAPI:")
|
|
||||||
d.clientMakeAPIBase(sb)
|
|
||||||
fmt.Fprint(sb, ",\n")
|
|
||||||
fmt.Fprint(sb, "\tGobCodec: c.GobCodec,\n")
|
|
||||||
fmt.Fprint(sb, "\tKVStore: c.KVStore,\n")
|
|
||||||
fmt.Fprint(sb, "}\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.clientMakeAPIBase(sb)
|
|
||||||
fmt.Fprint(sb, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genClientNewCaller(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func (c *Client) new%sCaller() ", d.Name)
|
|
||||||
fmt.Fprintf(sb, "%s {\n", d.CallerInterfaceName())
|
|
||||||
fmt.Fprint(sb, "\treturn ")
|
|
||||||
d.clientMakeAPI(sb)
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genClientCall(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "// %s calls the %s API.\n", d.Name, d.Name)
|
|
||||||
fmt.Fprintf(sb, "func (c *Client) %s(\n", d.Name)
|
|
||||||
fmt.Fprintf(sb, "ctx context.Context, req %s,\n) ", d.RequestTypeName())
|
|
||||||
fmt.Fprintf(sb, "(%s, error) {\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprintf(sb, "\tapi := c.new%sCaller()\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\treturn api.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenClientCallGo generates clientcall.go.
|
|
||||||
func GenClientCallGo(file string) {
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
fmt.Fprint(&sb, "import (\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"context\"\n")
|
|
||||||
fmt.Fprint(&sb, "\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
|
|
||||||
fmt.Fprint(&sb, ")\n")
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
switch desc.Name {
|
|
||||||
case "Register", "Login":
|
|
||||||
// We don't want to generate these APIs as toplevel.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
desc.genClientNewCaller(&sb)
|
|
||||||
desc.genClientCall(&sb)
|
|
||||||
}
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,182 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestClientCallRoundTrip(sb *strings.Builder) {
|
|
||||||
// generate the type of the handler
|
|
||||||
fmt.Fprintf(sb, "type handleClientCall%s struct {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\taccept string\n")
|
|
||||||
fmt.Fprint(sb, "\tbody []byte\n")
|
|
||||||
fmt.Fprint(sb, "\tcontentType string\n")
|
|
||||||
fmt.Fprint(sb, "\tcount int32\n")
|
|
||||||
fmt.Fprint(sb, "\tmethod string\n")
|
|
||||||
fmt.Fprint(sb, "\tmu sync.Mutex\n")
|
|
||||||
fmt.Fprintf(sb, "\tresp %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\turl *url.URL\n")
|
|
||||||
fmt.Fprint(sb, "\tuserAgent string\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
// generate the handling function
|
|
||||||
fmt.Fprintf(sb,
|
|
||||||
"func (h *handleClientCall%s) ServeHTTP(w http.ResponseWriter, r *http.Request) {",
|
|
||||||
d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := fakeFill{}\n")
|
|
||||||
if d.RequiresLogin {
|
|
||||||
fmt.Fprintf(sb, "\tif r.URL.Path == \"/api/v1/register\" {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tvar out apimodel.RegisterResponse\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tff.Fill(&out)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tdata, err := json.Marshal(out)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\t\tw.WriteHeader(400)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\t\treturn\n")
|
|
||||||
fmt.Fprintf(sb, "\t\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tw.Write(data)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\treturn\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tif r.URL.Path == \"/api/v1/login\" {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tvar out apimodel.LoginResponse\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tff.Fill(&out)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tdata, err := json.Marshal(out)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\t\tw.WriteHeader(400)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\t\treturn\n")
|
|
||||||
fmt.Fprintf(sb, "\t\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tw.Write(data)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\treturn\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n")
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\tdefer h.mu.Unlock()\n")
|
|
||||||
fmt.Fprint(sb, "\th.mu.Lock()\n")
|
|
||||||
fmt.Fprint(sb, "\tif h.count > 0 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tw.WriteHeader(400)\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\th.count++\n")
|
|
||||||
fmt.Fprint(sb, "\tif r.Body != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tdata, err := netxlite.ReadAllContext(r.Context(), r.Body)\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\t\tw.WriteHeader(400)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\t\treturn\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\th.body = data\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\th.method = r.Method\n")
|
|
||||||
fmt.Fprint(sb, "\th.url = r.URL\n")
|
|
||||||
fmt.Fprint(sb, "\th.accept = r.Header.Get(\"Accept\")\n")
|
|
||||||
fmt.Fprint(sb, "\th.contentType = r.Header.Get(\"Content-Type\")\n")
|
|
||||||
fmt.Fprint(sb, "\th.userAgent = r.Header.Get(\"User-Agent\")\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar out %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&out)\n")
|
|
||||||
fmt.Fprintf(sb, "\th.resp = out\n")
|
|
||||||
fmt.Fprintf(sb, "\tdata, err := json.Marshal(out)\n")
|
|
||||||
fmt.Fprintf(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tw.WriteHeader(400)\n")
|
|
||||||
fmt.Fprintf(sb, "\t\treturn\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tw.Write(data)\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n\n")
|
|
||||||
|
|
||||||
// generate the test itself
|
|
||||||
fmt.Fprintf(sb, "func Test%sClientCallRoundTrip(t *testing.T) {\n", d.Name)
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// setup\n")
|
|
||||||
fmt.Fprintf(sb, "\thandler := &handleClientCall%s{}\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
|
|
||||||
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
|
|
||||||
fmt.Fprintf(sb, "\treq := &%s{}\n", d.RequestTypeNameAsStruct())
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tclnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL}\n")
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&clnt.UserAgent)\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// issue request\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
fmt.Fprintf(sb, "\tresp, err := clnt.%s(ctx, req)\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response here\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// compare our response and server's one\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.resp, resp); diff != \"\" {")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// check whether headers are OK\n")
|
|
||||||
fmt.Fprint(sb, "\tif handler.accept != \"application/json\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid accept header\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif handler.userAgent != clnt.UserAgent {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid user-agent header\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// check whether the method is OK\n")
|
|
||||||
fmt.Fprintf(sb, "\tif handler.method != \"%s\" {\n", d.Method)
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid method\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
if d.Method == "POST" {
|
|
||||||
fmt.Fprint(sb, "\t// check the body\n")
|
|
||||||
fmt.Fprint(sb, "\tif handler.contentType != \"application/json\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid content-type header\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tgot := &%s{}\n", d.RequestTypeNameAsStruct())
|
|
||||||
fmt.Fprintf(sb, "\tif err := json.Unmarshal(handler.body, &got); err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(req, got); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
} else {
|
|
||||||
fmt.Fprint(sb, "\t// check the query\n")
|
|
||||||
fmt.Fprintf(sb, "\tapi := &%s{BaseURL: srvr.URL}\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\thttpReq, err := api.newRequest(context.Background(), req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenClientCallTestGo generates clientcall_test.go.
|
|
||||||
func GenClientCallTestGo(file string) {
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
fmt.Fprint(&sb, "import (\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"context\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"encoding/json\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"net/http/httptest\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"net/http\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"net/url\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"testing\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"sync\"\n")
|
|
||||||
fmt.Fprint(&sb, "\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/google/go-cmp/cmp\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/netxlite\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/kvstore\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
|
|
||||||
fmt.Fprint(&sb, ")\n")
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
if desc.Name == "Login" || desc.Name == "Register" {
|
|
||||||
continue // they cannot be called directly
|
|
||||||
}
|
|
||||||
desc.genTestClientCallRoundTrip(&sb)
|
|
||||||
}
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Descriptor) genNewCloner(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "// %s represents any type exposing a method\n",
|
|
||||||
d.ClonerInterfaceName())
|
|
||||||
fmt.Fprintf(sb, "// like %s.WithToken.\n", d.APIStructName())
|
|
||||||
fmt.Fprintf(sb, "type %s interface {\n", d.ClonerInterfaceName())
|
|
||||||
fmt.Fprintf(sb, "\tWithToken(token string) %s\n", d.CallerInterfaceName())
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenClonersGo generates cloners.go.
|
|
||||||
func GenClonersGo(file string) {
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
if !desc.RequiresLogin {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
desc.genNewCloner(&sb)
|
|
||||||
}
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Descriptor) genNewFakeAPI(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "type %s struct {\n", d.FakeAPIStructName())
|
|
||||||
if d.RequiresLogin {
|
|
||||||
fmt.Fprintf(sb, "\tWithResult %s\n", d.CallerInterfaceName())
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\tErr error\n")
|
|
||||||
fmt.Fprintf(sb, "\tResponse %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tCountCall *atomicx.Int64\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (fapi *%s) Call(ctx context.Context, req %s) (%s, error) {\n",
|
|
||||||
d.FakeAPIStructName(), d.RequestTypeName(), d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tif fapi.CountCall != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tfapi.CountCall.Add(1)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn fapi.Response, fapi.Err\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
if d.RequiresLogin {
|
|
||||||
fmt.Fprintf(sb, "func (fapi *%s) WithToken(token string) %s {\n",
|
|
||||||
d.FakeAPIStructName(), d.CallerInterfaceName())
|
|
||||||
fmt.Fprint(sb, "\treturn fapi.WithResult\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "var (\n")
|
|
||||||
fmt.Fprintf(sb, "\t_ %s = &%s{}\n", d.CallerInterfaceName(),
|
|
||||||
d.FakeAPIStructName())
|
|
||||||
if d.RequiresLogin {
|
|
||||||
fmt.Fprintf(sb, "\t_ %s = &%s{}\n", d.ClonerInterfaceName(),
|
|
||||||
d.FakeAPIStructName())
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, ")\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenFakeAPITestGo generates fakeapi_test.go.
|
|
||||||
func GenFakeAPITestGo(file string) {
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
fmt.Fprint(&sb, "import (\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"context\"\n")
|
|
||||||
fmt.Fprint(&sb, "\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/atomicx\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
|
|
||||||
fmt.Fprint(&sb, ")\n")
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
desc.genNewFakeAPI(&sb)
|
|
||||||
}
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
// Command generator generates code in the ooapi package.
|
|
||||||
//
|
|
||||||
// To this end, it uses the content of the apimodel package as
|
|
||||||
// well as the content of the spec.go file.
|
|
||||||
//
|
|
||||||
// The apimodel package defines the model, i.e., the structure
|
|
||||||
// of requests and responses and how messages should be sent
|
|
||||||
// and received.
|
|
||||||
//
|
|
||||||
// The spec.go file describes all the implemented APIs.
|
|
||||||
//
|
|
||||||
// If you change apimodel or spec.go, remember to run the
|
|
||||||
// `go generate ./...` command to regenerate all files.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var flagFile = flag.String("file", "", "Indicate which file to regenerate")
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
switch file := *flagFile; file {
|
|
||||||
case "apis.go":
|
|
||||||
GenAPIsGo(file)
|
|
||||||
case "responses.go":
|
|
||||||
GenResponsesGo(file)
|
|
||||||
case "requests.go":
|
|
||||||
GenRequestsGo(file)
|
|
||||||
case "swagger_test.go":
|
|
||||||
GenSwaggerTestGo(file)
|
|
||||||
case "apis_test.go":
|
|
||||||
GenAPIsTestGo(file)
|
|
||||||
case "callers.go":
|
|
||||||
GenCallersGo(file)
|
|
||||||
case "caching.go":
|
|
||||||
GenCachingGo(file)
|
|
||||||
case "login.go":
|
|
||||||
GenLoginGo(file)
|
|
||||||
case "cloners.go":
|
|
||||||
GenClonersGo(file)
|
|
||||||
case "fakeapi_test.go":
|
|
||||||
GenFakeAPITestGo(file)
|
|
||||||
case "caching_test.go":
|
|
||||||
GenCachingTestGo(file)
|
|
||||||
case "login_test.go":
|
|
||||||
GenLoginTestGo(file)
|
|
||||||
case "clientcall.go":
|
|
||||||
GenClientCallGo(file)
|
|
||||||
case "clientcall_test.go":
|
|
||||||
GenClientCallTestGo(file)
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("don't know how to create this file: %s", file))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,182 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Descriptor) genNewLogin(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "// %s implements login for %s.\n",
|
|
||||||
d.WithLoginAPIStructName(), d.APIStructName())
|
|
||||||
fmt.Fprintf(sb, "type %s struct {\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\tAPI %s // mandatory\n", d.ClonerInterfaceName())
|
|
||||||
fmt.Fprint(sb, "\tJSONCodec JSONCodec // optional\n")
|
|
||||||
fmt.Fprint(sb, "\tKVStore KVStore // mandatory\n")
|
|
||||||
fmt.Fprint(sb, "\tRegisterAPI callerForRegisterAPI // mandatory\n")
|
|
||||||
fmt.Fprint(sb, "\tLoginAPI callerForLoginAPI // mandatory\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "// Call logins, if needed, then calls the API.\n")
|
|
||||||
fmt.Fprintf(sb, "func (api *%s) Call(ctx context.Context, req %s) (%s, error) {\n",
|
|
||||||
d.WithLoginAPIStructName(), d.RequestTypeName(), d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\ttoken, err := api.maybeLogin(ctx)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.API.WithToken(token).Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif errors.Is(err, ErrUnauthorized) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t// Maybe the clock is just off? Let's try to obtain\n")
|
|
||||||
fmt.Fprint(sb, "\t\t// a token again and see if this fixes it.\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif token, err = api.forceLogin(ctx); err == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tswitch resp, err = api.API.WithToken(token).Call(ctx, req); err {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tcase nil:\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t\treturn resp, nil\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tcase ErrUnauthorized:\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t\t// fallthrough\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tdefault:\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\t// Okay, this seems a broader problem. How about we try\n")
|
|
||||||
fmt.Fprint(sb, "\t\t// and re-register ourselves again instead?\n")
|
|
||||||
fmt.Fprint(sb, "\t\ttoken, err = api.forceRegister(ctx)\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tresp, err = api.API.WithToken(token).Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\t\t// fallthrough\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn resp, nil\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (api *%s) jsonCodec() JSONCodec {\n",
|
|
||||||
d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\tif api.JSONCodec != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn api.JSONCodec\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn &defaultJSONCodec{}\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (api *%s) readstate() (*loginState, error) {\n",
|
|
||||||
d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\tdata, err := api.KVStore.Get(loginKey)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tvar ls loginState\n")
|
|
||||||
fmt.Fprint(sb, "\tif err := api.jsonCodec().Decode(data, &ls); err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn &ls, nil\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (api *%s) writestate(ls *loginState) error {\n",
|
|
||||||
d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\tdata, err := api.jsonCodec().Encode(*ls)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn api.KVStore.Set(loginKey, data)\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (api *%s) doRegister(ctx context.Context, password string) (string, error) {\n",
|
|
||||||
d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\treq := newRegisterRequest(password)\n")
|
|
||||||
fmt.Fprint(sb, "\tls := &loginState{}\n")
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.RegisterAPI.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn \"\", err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tls.ClientID = resp.ClientID\n")
|
|
||||||
fmt.Fprint(sb, "\tls.Password = req.Password\n")
|
|
||||||
fmt.Fprint(sb, "\treturn api.doLogin(ctx, ls)\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (api *%s) forceRegister(ctx context.Context) (string, error) {\n",
|
|
||||||
d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\tvar password string\n")
|
|
||||||
fmt.Fprint(sb, "\t// If we already have a previous password, let us keep\n")
|
|
||||||
fmt.Fprint(sb, "\t// using it. This will allow a new version of the API to\n")
|
|
||||||
fmt.Fprint(sb, "\t// be able to continue to identify this probe. (This\n")
|
|
||||||
fmt.Fprint(sb, "\t// assumes that we have a stateless API that generates\n")
|
|
||||||
fmt.Fprint(sb, "\t// the user ID as a signature of the password plus a\n")
|
|
||||||
fmt.Fprint(sb, "\t// timestamp and that the key to generate the signature\n")
|
|
||||||
fmt.Fprint(sb, "\t// is not lost. If all these conditions are met, we\n")
|
|
||||||
fmt.Fprint(sb, "\t// can then serve better test targets to more long running\n")
|
|
||||||
fmt.Fprint(sb, "\t// (and therefore trusted) probes.)\n")
|
|
||||||
fmt.Fprint(sb, "\tif ls, err := api.readstate(); err == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tpassword = ls.Password\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif password == \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tpassword = newRandomPassword()\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn api.doRegister(ctx, password)\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (api *%s) forceLogin(ctx context.Context) (string, error) {\n",
|
|
||||||
d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\tls, err := api.readstate()\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn \"\", err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn api.doLogin(ctx, ls)\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (api *%s) maybeLogin(ctx context.Context) (string, error) {\n",
|
|
||||||
d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\tls, _ := api.readstate()\n")
|
|
||||||
fmt.Fprint(sb, "\tif ls == nil || !ls.credentialsValid() {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn api.forceRegister(ctx)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif !ls.tokenValid() {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn api.doLogin(ctx, ls)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn ls.Token, nil\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "func (api *%s) doLogin(ctx context.Context, ls *loginState) (string, error) {\n",
|
|
||||||
d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\treq := &apimodel.LoginRequest{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tClientID: ls.ClientID,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tPassword: ls.Password,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tresp, err := api.LoginAPI.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn \"\", err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tls.Token = resp.Token\n")
|
|
||||||
fmt.Fprint(sb, "\tls.Expire = resp.Expire\n")
|
|
||||||
fmt.Fprint(sb, "\tif err := api.writestate(ls); err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn \"\", err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\treturn ls.Token, nil\n")
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
fmt.Fprintf(sb, "var _ %s = &%s{}\n\n", d.CallerInterfaceName(),
|
|
||||||
d.WithLoginAPIStructName())
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenLoginGo generates login.go.
|
|
||||||
func GenLoginGo(file string) {
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
fmt.Fprint(&sb, "import (\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"context\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"errors\"\n")
|
|
||||||
fmt.Fprint(&sb, "\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
|
|
||||||
fmt.Fprint(&sb, ")\n")
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
if !desc.RequiresLogin {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
desc.genNewLogin(&sb)
|
|
||||||
}
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,899 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestRegisterAndLoginSuccess(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func TestRegisterAndLogin%sSuccess(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&expect)\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestContinueUsingToken(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sContinueUsingToken(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&expect)\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n")
|
|
||||||
fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tif loginAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tif registerAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// step 2: we disable register and login but we\n")
|
|
||||||
fmt.Fprint(sb, "\t// should be okay because of the token\n")
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI.Err = errMocked\n")
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI.Response = nil\n")
|
|
||||||
fmt.Fprint(sb, "\tloginAPI.Err = errMocked\n")
|
|
||||||
fmt.Fprint(sb, "\tloginAPI.Response = nil\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestWithValidButExpiredToken(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sWithValidButExpiredToken(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&expect)\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tErr: errMocked,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tls := &loginState{\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tClientID: \"antani-antani\",\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tExpire: time.Now().Add(-5 * time.Second),\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tToken: \"antani-antani-token\",\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tPassword: \"antani-antani-password\",\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tif err := login.writestate(ls); err != nil {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 0 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestWithRegisterAPIError(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sWithRegisterAPIError(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&expect)\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tErr: errMocked,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestWithLoginFailure(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sWithLoginFailure(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&expect)\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tErr: errMocked,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestRegisterAndLoginThenFail(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func TestRegisterAndLogin%sThenFail(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&expect)\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tErr: errMocked,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestTheDatabaseIsReplaced(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sTheDatabaseIsReplaced(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprint(sb, "\thandler := &LoginHandler{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt: t,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
|
|
||||||
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\tAPI : baseAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n")
|
|
||||||
fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// step 2: we forget accounts and try again.\n")
|
|
||||||
fmt.Fprint(sb, "\thandler.forgetLogins()\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif handler.logins.Load() != 3 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif handler.registers.Load() != 2 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestTheDatabaseIsReplacedThenFailure(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sTheDatabaseIsReplacedThenFailure(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprint(sb, "\thandler := &LoginHandler{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt: t,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
|
|
||||||
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\tAPI : baseAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n")
|
|
||||||
fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// step 2: we forget accounts and try again.\n")
|
|
||||||
fmt.Fprint(sb, "\t// but registrations are also failing.\n")
|
|
||||||
fmt.Fprint(sb, "\thandler.forgetLogins()\n")
|
|
||||||
fmt.Fprint(sb, "\thandler.noRegister = true\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, ErrHTTPFailure) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif handler.logins.Load() != 2 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif handler.registers.Load() != 2 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestRegisterAndLoginCannotWriteState(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func TestRegisterAndLogin%sCannotWriteState(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&expect)\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tJSONCodec: &FakeCodec{\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tEncodeErr: errMocked,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestReadStateDecodeFailure(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sReadStateDecodeFailure(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&expect)\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tJSONCodec: &FakeCodec{DecodeErr: errMocked},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tls := &loginState{\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tClientID: \"antani-antani\",\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tExpire: time.Now().Add(-5 * time.Second),\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tToken: \"antani-antani-token\",\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tPassword: \"antani-antani-password\",\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tif err := login.writestate(ls); err != nil {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tout, err := login.forceLogin(context.Background())\n")
|
|
||||||
fmt.Fprintf(sb, "if !errors.Is(err, errMocked) {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "if out != \"\" {\n")
|
|
||||||
fmt.Fprintf(sb, "\t\tt.Fatal(\"expected empty string here\")\n")
|
|
||||||
fmt.Fprintf(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestClockIsOffThenSuccess(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sClockIsOffThenSuccess(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprint(sb, "\thandler := &LoginHandler{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt: t,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
|
|
||||||
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\tAPI : baseAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n")
|
|
||||||
fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// step 2: we forget tokens and try again.\n")
|
|
||||||
fmt.Fprint(sb, "\t// this should simulate the client clock\n")
|
|
||||||
fmt.Fprint(sb, "\t// being off and considering a token still valid\n")
|
|
||||||
fmt.Fprint(sb, "\thandler.forgetTokens()\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif handler.logins.Load() != 2 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif handler.registers.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestClockIsOffThen401(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sClockIsOffThen401(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprint(sb, "\thandler := &LoginHandler{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt: t,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
|
|
||||||
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\tAPI : baseAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n")
|
|
||||||
fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// step 2: we forget tokens and try again.\n")
|
|
||||||
fmt.Fprint(sb, "\t// this should simulate the client clock\n")
|
|
||||||
fmt.Fprint(sb, "\t// being off and considering a token still valid\n")
|
|
||||||
fmt.Fprint(sb, "\thandler.forgetTokens()\n")
|
|
||||||
fmt.Fprint(sb, "\thandler.failCallWith = []int{401, 401}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif handler.logins.Load() != 3 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif handler.registers.Load() != 2 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genTestClockIsOffThen500(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb, "func Test%sClockIsOffThen500(t *testing.T) {\n", d.Name)
|
|
||||||
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
|
|
||||||
fmt.Fprint(sb, "\thandler := &LoginHandler{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt: t,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
|
|
||||||
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName())
|
|
||||||
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
|
|
||||||
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
|
|
||||||
fmt.Fprintf(sb, "\tAPI : baseAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n")
|
|
||||||
fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
|
|
||||||
fmt.Fprint(sb, "\tff.Fill(&req)\n")
|
|
||||||
fmt.Fprint(sb, "\tctx := context.Background()\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n")
|
|
||||||
fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t{\n")
|
|
||||||
fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t\tif resp == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n")
|
|
||||||
fmt.Fprint(sb, "\t\t}\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t// step 2: we forget tokens and try again.\n")
|
|
||||||
fmt.Fprint(sb, "\t// this should simulate the client clock\n")
|
|
||||||
fmt.Fprint(sb, "\t// being off and considering a token still valid\n")
|
|
||||||
fmt.Fprint(sb, "\thandler.forgetTokens()\n")
|
|
||||||
fmt.Fprint(sb, "\thandler.failCallWith = []int{401, 500}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif !errors.Is(err, ErrHTTPFailure) {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif handler.logins.Load() != 2 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif handler.registers.Load() != 1 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenLoginTestGo generates login_test.go.
|
|
||||||
func GenLoginTestGo(file string) {
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
fmt.Fprint(&sb, "import (\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"context\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"errors\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"net/http/httptest\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"testing\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"time\"\n")
|
|
||||||
fmt.Fprint(&sb, "\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/google/go-cmp/cmp\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/atomicx\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/kvstore\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
|
|
||||||
fmt.Fprint(&sb, ")\n")
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
if !desc.RequiresLogin {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
desc.genTestRegisterAndLoginSuccess(&sb)
|
|
||||||
desc.genTestContinueUsingToken(&sb)
|
|
||||||
desc.genTestWithValidButExpiredToken(&sb)
|
|
||||||
desc.genTestWithRegisterAPIError(&sb)
|
|
||||||
desc.genTestWithLoginFailure(&sb)
|
|
||||||
desc.genTestRegisterAndLoginThenFail(&sb)
|
|
||||||
desc.genTestTheDatabaseIsReplaced(&sb)
|
|
||||||
desc.genTestRegisterAndLoginCannotWriteState(&sb)
|
|
||||||
desc.genTestReadStateDecodeFailure(&sb)
|
|
||||||
desc.genTestTheDatabaseIsReplacedThenFailure(&sb)
|
|
||||||
desc.genTestClockIsOffThenSuccess(&sb)
|
|
||||||
desc.genTestClockIsOffThen401(&sb)
|
|
||||||
desc.genTestClockIsOffThen500(&sb)
|
|
||||||
}
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,153 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TypeName returns v's package-qualified type name.
|
|
||||||
func (d *Descriptor) TypeName(v interface{}) string {
|
|
||||||
return reflect.TypeOf(v).String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestTypeName calls d.TypeName(d.Request).
|
|
||||||
func (d *Descriptor) RequestTypeName() string {
|
|
||||||
return d.TypeName(d.Request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseTypeName calls d.TypeName(d.Response).
|
|
||||||
func (d *Descriptor) ResponseTypeName() string {
|
|
||||||
return d.TypeName(d.Response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIStructName returns the correct struct type name
|
|
||||||
// for the API we're currently processing.
|
|
||||||
func (d *Descriptor) APIStructName() string {
|
|
||||||
return fmt.Sprintf("simple%sAPI", d.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FakeAPIStructName returns the correct struct type name
|
|
||||||
// for the fake for the API we're currently processing.
|
|
||||||
func (d *Descriptor) FakeAPIStructName() string {
|
|
||||||
return fmt.Sprintf("Fake%sAPI", d.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLoginAPIStructName returns the correct struct type name
|
|
||||||
// for the WithLoginAPI we're currently processing.
|
|
||||||
func (d *Descriptor) WithLoginAPIStructName() string {
|
|
||||||
return fmt.Sprintf("withLogin%sAPI", d.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CallerInterfaceName returns the correct caller interface name
|
|
||||||
// for the API we're currently processing.
|
|
||||||
func (d *Descriptor) CallerInterfaceName() string {
|
|
||||||
return fmt.Sprintf("callerFor%sAPI", d.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClonerInterfaceName returns the correct cloner interface name
|
|
||||||
// for the API we're currently processing.
|
|
||||||
func (d *Descriptor) ClonerInterfaceName() string {
|
|
||||||
return fmt.Sprintf("clonerFor%sAPI", d.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithCacheAPIStructName returns the correct struct type name for
|
|
||||||
// the cache for the API we're currently processing.
|
|
||||||
func (d *Descriptor) WithCacheAPIStructName() string {
|
|
||||||
return fmt.Sprintf("withCache%sAPI", d.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CacheEntryName returns the correct struct type name for the
|
|
||||||
// cache entry for the API we're currently processing.
|
|
||||||
func (d *Descriptor) CacheEntryName() string {
|
|
||||||
return fmt.Sprintf("cacheEntryFor%sAPI", d.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CacheKey returns the correct cache key for the API
|
|
||||||
// we're currently processing.
|
|
||||||
func (d *Descriptor) CacheKey() string {
|
|
||||||
return fmt.Sprintf("%s.cache", d.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StructFields returns all the struct fields of in. This function
|
|
||||||
// assumes that in is a pointer to struct, and will otherwise panic.
|
|
||||||
func (d *Descriptor) StructFields(in interface{}) []*reflect.StructField {
|
|
||||||
t := reflect.TypeOf(in)
|
|
||||||
if t.Kind() != reflect.Ptr {
|
|
||||||
panic("not a pointer")
|
|
||||||
}
|
|
||||||
t = t.Elem()
|
|
||||||
if t.Kind() != reflect.Struct {
|
|
||||||
panic("not a struct")
|
|
||||||
}
|
|
||||||
var out []*reflect.StructField
|
|
||||||
for idx := 0; idx < t.NumField(); idx++ {
|
|
||||||
f := t.Field(idx)
|
|
||||||
out = append(out, &f)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// StructFieldsWithTag returns all the struct fields of
|
|
||||||
// in that have the specified tag.
|
|
||||||
func (d *Descriptor) StructFieldsWithTag(in interface{}, tag string) []*reflect.StructField {
|
|
||||||
var out []*reflect.StructField
|
|
||||||
for _, f := range d.StructFields(in) {
|
|
||||||
if f.Tag.Get(tag) != "" {
|
|
||||||
out = append(out, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestOrResponseTypeKind returns the type kind of in, which should
|
|
||||||
// be a request or a response. This function assumes that in is either a
|
|
||||||
// pointer to struct or a map and will panic otherwise.
|
|
||||||
func (d *Descriptor) RequestOrResponseTypeKind(in interface{}) reflect.Kind {
|
|
||||||
t := reflect.TypeOf(in)
|
|
||||||
if t.Kind() == reflect.Ptr {
|
|
||||||
t = t.Elem()
|
|
||||||
if t.Kind() != reflect.Struct {
|
|
||||||
panic("not a struct")
|
|
||||||
}
|
|
||||||
return reflect.Struct
|
|
||||||
}
|
|
||||||
if t.Kind() != reflect.Map {
|
|
||||||
panic("not a map")
|
|
||||||
}
|
|
||||||
return reflect.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestTypeKind calls d.RequestOrResponseTypeKind(d.Request).
|
|
||||||
func (d *Descriptor) RequestTypeKind() reflect.Kind {
|
|
||||||
return d.RequestOrResponseTypeKind(d.Request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseTypeKind calls d.RequestOrResponseTypeKind(d.Response).
|
|
||||||
func (d *Descriptor) ResponseTypeKind() reflect.Kind {
|
|
||||||
return d.RequestOrResponseTypeKind(d.Response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypeNameAsStruct assumes that in is a pointer to struct and
|
|
||||||
// returns the type of the corresponding struct. The returned
|
|
||||||
// type is package qualified.
|
|
||||||
func (d *Descriptor) TypeNameAsStruct(in interface{}) string {
|
|
||||||
t := reflect.TypeOf(in)
|
|
||||||
if t.Kind() != reflect.Ptr {
|
|
||||||
panic("not a pointer")
|
|
||||||
}
|
|
||||||
t = t.Elem()
|
|
||||||
if t.Kind() != reflect.Struct {
|
|
||||||
panic("not a struct")
|
|
||||||
}
|
|
||||||
return t.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestTypeNameAsStruct calls d.TypeNameAsStruct(d.Request)
|
|
||||||
func (d *Descriptor) RequestTypeNameAsStruct() string {
|
|
||||||
return d.TypeNameAsStruct(d.Request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseTypeNameAsStruct calls d.TypeNameAsStruct(d.Response)
|
|
||||||
func (d *Descriptor) ResponseTypeNameAsStruct() string {
|
|
||||||
return d.TypeNameAsStruct(d.Response)
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
tagForQuery = "query"
|
|
||||||
tagForRequired = "required"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Descriptor) genNewRequestQueryElemString(sb *strings.Builder, f *reflect.StructField) {
|
|
||||||
name := f.Name
|
|
||||||
query := f.Tag.Get(tagForQuery)
|
|
||||||
if f.Tag.Get(tagForRequired) == "true" {
|
|
||||||
fmt.Fprintf(sb, "\tif req.%s == \"\" {\n", name)
|
|
||||||
fmt.Fprintf(sb, "\t\treturn nil, newErrEmptyField(\"%s\")\n", name)
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprintf(sb, "\tq.Add(\"%s\", req.%s)\n", query, name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "\tif req.%s != \"\" {\n", name)
|
|
||||||
fmt.Fprintf(sb, "\t\tq.Add(\"%s\", req.%s)\n", query, name)
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genNewRequestQueryElemBool(sb *strings.Builder, f *reflect.StructField) {
|
|
||||||
// required does not make much sense for a boolean field
|
|
||||||
name := f.Name
|
|
||||||
query := f.Tag.Get(tagForQuery)
|
|
||||||
fmt.Fprintf(sb, "\tif req.%s {\n", name)
|
|
||||||
fmt.Fprintf(sb, "\t\tq.Add(\"%s\", \"true\")\n", query)
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genNewRequestQueryElemInt64(sb *strings.Builder, f *reflect.StructField) {
|
|
||||||
// required does not make much sense for an integer field
|
|
||||||
name := f.Name
|
|
||||||
query := f.Tag.Get(tagForQuery)
|
|
||||||
fmt.Fprintf(sb, "\tif req.%s != 0 {\n", name)
|
|
||||||
fmt.Fprintf(sb, "\t\tq.Add(\"%s\", newQueryFieldInt64(req.%s))\n", query, name)
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genNewRequestQuery(sb *strings.Builder) {
|
|
||||||
if d.Method != "GET" {
|
|
||||||
return // we only generate query for GET
|
|
||||||
}
|
|
||||||
fields := d.StructFieldsWithTag(d.Request, tagForQuery)
|
|
||||||
if len(fields) <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\tq := url.Values{}\n")
|
|
||||||
for idx, f := range fields {
|
|
||||||
switch f.Type.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
d.genNewRequestQueryElemString(sb, f)
|
|
||||||
case reflect.Bool:
|
|
||||||
d.genNewRequestQueryElemBool(sb, f)
|
|
||||||
case reflect.Int64:
|
|
||||||
d.genNewRequestQueryElemInt64(sb, f)
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unexpected query type at index %d", idx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\tURL.RawQuery = q.Encode()\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genNewRequestCallNewRequest(sb *strings.Builder) {
|
|
||||||
if d.Method == "POST" {
|
|
||||||
fmt.Fprint(sb, "\tbody, err := api.jsonCodec().Encode(req)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tout, err := api.requestMaker().NewRequest(")
|
|
||||||
fmt.Fprintf(sb, "ctx, \"%s\", URL.String(), ", d.Method)
|
|
||||||
fmt.Fprint(sb, "bytes.NewReader(body))\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tout.Header.Set(\"Content-Type\", \"application/json\")\n")
|
|
||||||
fmt.Fprint(sb, "\treturn out, nil\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprint(sb, "\treturn api.requestMaker().NewRequest(")
|
|
||||||
fmt.Fprintf(sb, "ctx, \"%s\", URL.String(), ", d.Method)
|
|
||||||
fmt.Fprint(sb, "nil)\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genNewRequest(sb *strings.Builder) {
|
|
||||||
|
|
||||||
fmt.Fprintf(
|
|
||||||
sb, "func (api *%s) newRequest(ctx context.Context, req %s) %s {\n",
|
|
||||||
d.APIStructName(), d.RequestTypeName(), "(*http.Request, error)")
|
|
||||||
fmt.Fprint(sb, "\tURL, err := url.Parse(api.baseURL())\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
switch d.URLPath.IsTemplate {
|
|
||||||
case false:
|
|
||||||
fmt.Fprintf(sb, "\tURL.Path = \"%s\"\n", d.URLPath.Value)
|
|
||||||
case true:
|
|
||||||
fmt.Fprintf(
|
|
||||||
sb, "\tup, err := api.templateExecutor().Execute(\"%s\", req)\n",
|
|
||||||
d.URLPath.Value)
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tURL.Path = up\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
d.genNewRequestQuery(sb)
|
|
||||||
d.genNewRequestCallNewRequest(sb)
|
|
||||||
|
|
||||||
fmt.Fprintf(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenRequestsGo generates requests.go.
|
|
||||||
func GenRequestsGo(file string) {
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
fmt.Fprint(&sb, "import (\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"bytes\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"context\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"net/http\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"net/url\"\n")
|
|
||||||
fmt.Fprint(&sb, "\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
|
|
||||||
fmt.Fprint(&sb, ")\n\n")
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
desc.genNewRequest(&sb)
|
|
||||||
}
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Descriptor) genNewResponse(sb *strings.Builder) {
|
|
||||||
fmt.Fprintf(sb,
|
|
||||||
"func (api *%s) newResponse(ctx context.Context, resp *http.Response, err error) (%s, error) {\n",
|
|
||||||
d.APIStructName(), d.ResponseTypeName())
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp.StatusCode == 401 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, ErrUnauthorized\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tif resp.StatusCode != 200 {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, newHTTPFailure(resp.StatusCode)\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
fmt.Fprint(sb, "\tdefer resp.Body.Close()\n")
|
|
||||||
fmt.Fprint(sb, "\treader := io.LimitReader(resp.Body, 4<<20)\n")
|
|
||||||
fmt.Fprint(sb, "\tdata, err := netxlite.ReadAllContext(ctx, reader)\n")
|
|
||||||
fmt.Fprint(sb, "\tif err != nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
switch d.ResponseTypeKind() {
|
|
||||||
case reflect.Map:
|
|
||||||
fmt.Fprintf(sb, "\tout := %s{}\n", d.ResponseTypeName())
|
|
||||||
case reflect.Struct:
|
|
||||||
fmt.Fprintf(sb, "\tout := &%s{}\n", d.ResponseTypeNameAsStruct())
|
|
||||||
}
|
|
||||||
|
|
||||||
switch d.ResponseTypeKind() {
|
|
||||||
case reflect.Map:
|
|
||||||
fmt.Fprint(sb, "\tif err := api.jsonCodec().Decode(data, &out); err != nil {\n")
|
|
||||||
case reflect.Struct:
|
|
||||||
fmt.Fprint(sb, "\tif err := api.jsonCodec().Decode(data, out); err != nil {\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, err\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
|
|
||||||
switch d.ResponseTypeKind() {
|
|
||||||
case reflect.Map:
|
|
||||||
// For rationale, see https://play.golang.org/p/m9-MsTaQ5wt and
|
|
||||||
// https://play.golang.org/p/6h-v-PShMk9.
|
|
||||||
fmt.Fprint(sb, "\tif out == nil {\n")
|
|
||||||
fmt.Fprint(sb, "\t\treturn nil, ErrJSONLiteralNull\n")
|
|
||||||
fmt.Fprint(sb, "\t}\n")
|
|
||||||
case reflect.Struct:
|
|
||||||
// nothing
|
|
||||||
}
|
|
||||||
fmt.Fprintf(sb, "\treturn out, nil\n")
|
|
||||||
fmt.Fprintf(sb, "}\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenResponsesGo generates responses.go.
|
|
||||||
func GenResponsesGo(file string) {
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
fmt.Fprint(&sb, "import (\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"context\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"io\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"net/http\"\n")
|
|
||||||
fmt.Fprint(&sb, "\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/netxlite\"\n")
|
|
||||||
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
|
|
||||||
fmt.Fprint(&sb, ")\n\n")
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
desc.genNewResponse(&sb)
|
|
||||||
}
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
|
|
||||||
// URLPath describes a URLPath.
|
|
||||||
type URLPath struct {
|
|
||||||
// IsTemplate indicates whether Value contains a template. A future
|
|
||||||
// version of this implementation will automatically deduce that.
|
|
||||||
IsTemplate bool
|
|
||||||
|
|
||||||
// Value is the value of the URL path.
|
|
||||||
Value string
|
|
||||||
|
|
||||||
// InSwagger indicates the corresponding name to be used in
|
|
||||||
// the Swagger specification.
|
|
||||||
InSwagger string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Descriptor is an API descriptor. It tells the generator
|
|
||||||
// what code it should emit for a given API.
|
|
||||||
type Descriptor struct {
|
|
||||||
// Name is the name of the API.
|
|
||||||
Name string
|
|
||||||
|
|
||||||
// CachePolicy indicates the caching policy to use.
|
|
||||||
CachePolicy int
|
|
||||||
|
|
||||||
// RequiresLogin indicates whether the API requires login.
|
|
||||||
RequiresLogin bool
|
|
||||||
|
|
||||||
// Method is the method to use ("GET" or "POST").
|
|
||||||
Method string
|
|
||||||
|
|
||||||
// URLPath is the URL path.
|
|
||||||
URLPath URLPath
|
|
||||||
|
|
||||||
// Request is an instance of the request type.
|
|
||||||
Request interface{}
|
|
||||||
|
|
||||||
// Response is an instance of the response type.
|
|
||||||
Response interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// These are the caching policies.
|
|
||||||
const (
|
|
||||||
// CacheNone indicates we don't use a cache.
|
|
||||||
CacheNone = iota
|
|
||||||
|
|
||||||
// CacheFallback indicates we fallback to the cache
|
|
||||||
// when there is a failure.
|
|
||||||
CacheFallback
|
|
||||||
|
|
||||||
// CacheAlways indicates that we always check the
|
|
||||||
// cache before sending a request.
|
|
||||||
CacheAlways
|
|
||||||
)
|
|
||||||
|
|
||||||
// Descriptors describes all the APIs.
|
|
||||||
//
|
|
||||||
// Note that it matters whether the requests and responses
|
|
||||||
// are pointers. Generally speaking, if the message is a
|
|
||||||
// struct, use a pointer. If it's a map, don't.
|
|
||||||
var Descriptors = []Descriptor{{
|
|
||||||
Name: "CheckReportID",
|
|
||||||
Method: "GET",
|
|
||||||
URLPath: URLPath{Value: "/api/_/check_report_id"},
|
|
||||||
Request: &apimodel.CheckReportIDRequest{},
|
|
||||||
Response: &apimodel.CheckReportIDResponse{},
|
|
||||||
}, {
|
|
||||||
Name: "CheckIn",
|
|
||||||
Method: "POST",
|
|
||||||
URLPath: URLPath{Value: "/api/v1/check-in"},
|
|
||||||
Request: &apimodel.CheckInRequest{},
|
|
||||||
Response: &apimodel.CheckInResponse{},
|
|
||||||
}, {
|
|
||||||
Name: "Login",
|
|
||||||
Method: "POST",
|
|
||||||
URLPath: URLPath{Value: "/api/v1/login"},
|
|
||||||
Request: &apimodel.LoginRequest{},
|
|
||||||
Response: &apimodel.LoginResponse{},
|
|
||||||
}, {
|
|
||||||
Name: "MeasurementMeta",
|
|
||||||
Method: "GET",
|
|
||||||
URLPath: URLPath{Value: "/api/v1/measurement_meta"},
|
|
||||||
Request: &apimodel.MeasurementMetaRequest{},
|
|
||||||
Response: &apimodel.MeasurementMetaResponse{},
|
|
||||||
CachePolicy: CacheAlways,
|
|
||||||
}, {
|
|
||||||
Name: "Register",
|
|
||||||
Method: "POST",
|
|
||||||
URLPath: URLPath{Value: "/api/v1/register"},
|
|
||||||
Request: &apimodel.RegisterRequest{},
|
|
||||||
Response: &apimodel.RegisterResponse{},
|
|
||||||
}, {
|
|
||||||
Name: "TestHelpers",
|
|
||||||
Method: "GET",
|
|
||||||
URLPath: URLPath{Value: "/api/v1/test-helpers"},
|
|
||||||
Request: &apimodel.TestHelpersRequest{},
|
|
||||||
Response: apimodel.TestHelpersResponse{},
|
|
||||||
}, {
|
|
||||||
Name: "PsiphonConfig",
|
|
||||||
RequiresLogin: true,
|
|
||||||
Method: "GET",
|
|
||||||
URLPath: URLPath{Value: "/api/v1/test-list/psiphon-config"},
|
|
||||||
Request: &apimodel.PsiphonConfigRequest{},
|
|
||||||
Response: apimodel.PsiphonConfigResponse{},
|
|
||||||
}, {
|
|
||||||
Name: "TorTargets",
|
|
||||||
RequiresLogin: true,
|
|
||||||
Method: "GET",
|
|
||||||
URLPath: URLPath{Value: "/api/v1/test-list/tor-targets"},
|
|
||||||
Request: &apimodel.TorTargetsRequest{},
|
|
||||||
Response: apimodel.TorTargetsResponse{},
|
|
||||||
}, {
|
|
||||||
Name: "URLs",
|
|
||||||
Method: "GET",
|
|
||||||
URLPath: URLPath{Value: "/api/v1/test-list/urls"},
|
|
||||||
Request: &apimodel.URLsRequest{},
|
|
||||||
Response: &apimodel.URLsResponse{},
|
|
||||||
}, {
|
|
||||||
Name: "OpenReport",
|
|
||||||
Method: "POST",
|
|
||||||
URLPath: URLPath{Value: "/report"},
|
|
||||||
Request: &apimodel.OpenReportRequest{},
|
|
||||||
Response: &apimodel.OpenReportResponse{},
|
|
||||||
}, {
|
|
||||||
Name: "SubmitMeasurement",
|
|
||||||
Method: "POST",
|
|
||||||
URLPath: URLPath{
|
|
||||||
InSwagger: "/report/{report_id}",
|
|
||||||
IsTemplate: true,
|
|
||||||
Value: "/report/{{ .ReportID }}",
|
|
||||||
},
|
|
||||||
Request: &apimodel.SubmitMeasurementRequest{},
|
|
||||||
Response: &apimodel.SubmitMeasurementResponse{},
|
|
||||||
}}
|
|
|
@ -1,194 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/internal/openapi"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
tagForJSON = "json"
|
|
||||||
tagForPath = "path"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *Descriptor) genSwaggerURLPath() string {
|
|
||||||
up := d.URLPath
|
|
||||||
if up.InSwagger != "" {
|
|
||||||
return up.InSwagger
|
|
||||||
}
|
|
||||||
if up.IsTemplate {
|
|
||||||
panic("we should always use InSwapper and IsTemplate together")
|
|
||||||
}
|
|
||||||
return up.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genSwaggerSchema(cur reflect.Type) *openapi.Schema {
|
|
||||||
switch cur.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
return &openapi.Schema{Type: "string"}
|
|
||||||
case reflect.Bool:
|
|
||||||
return &openapi.Schema{Type: "boolean"}
|
|
||||||
case reflect.Int64:
|
|
||||||
return &openapi.Schema{Type: "integer"}
|
|
||||||
case reflect.Slice:
|
|
||||||
return &openapi.Schema{Type: "array", Items: d.genSwaggerSchema(cur.Elem())}
|
|
||||||
case reflect.Map:
|
|
||||||
return &openapi.Schema{Type: "object"}
|
|
||||||
case reflect.Ptr:
|
|
||||||
return d.genSwaggerSchema(cur.Elem())
|
|
||||||
case reflect.Struct:
|
|
||||||
if cur.String() == "time.Time" {
|
|
||||||
// Implementation note: we don't want to dive into time.Time but
|
|
||||||
// rather we want to pretend it's a string. The JSON parser for
|
|
||||||
// time.Time can indeed reconstruct a time.Time from a string, and
|
|
||||||
// it's much easier for us to let it do the parsing.
|
|
||||||
return &openapi.Schema{Type: "string"}
|
|
||||||
}
|
|
||||||
sinfo := &openapi.Schema{Type: "object"}
|
|
||||||
var once sync.Once
|
|
||||||
initmap := func() {
|
|
||||||
sinfo.Properties = make(map[string]*openapi.Schema)
|
|
||||||
}
|
|
||||||
for idx := 0; idx < cur.NumField(); idx++ {
|
|
||||||
field := cur.Field(idx)
|
|
||||||
if field.Tag.Get(tagForPath) != "" {
|
|
||||||
continue // skipping because this is a path param
|
|
||||||
}
|
|
||||||
if field.Tag.Get(tagForQuery) != "" {
|
|
||||||
continue // skipping because this is a query param
|
|
||||||
}
|
|
||||||
v := field.Name
|
|
||||||
if j := field.Tag.Get(tagForJSON); j != "" {
|
|
||||||
j = strings.Replace(j, ",omitempty", "", 1) // remove options
|
|
||||||
if j == "-" {
|
|
||||||
continue // not exported via JSON
|
|
||||||
}
|
|
||||||
v = j
|
|
||||||
}
|
|
||||||
once.Do(initmap)
|
|
||||||
sinfo.Properties[v] = d.genSwaggerSchema(field.Type)
|
|
||||||
}
|
|
||||||
return sinfo
|
|
||||||
case reflect.Interface:
|
|
||||||
return &openapi.Schema{Type: "object"}
|
|
||||||
default:
|
|
||||||
panic("unsupported type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) swaggerParamForType(t reflect.Type) string {
|
|
||||||
switch t.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
return "string"
|
|
||||||
case reflect.Bool:
|
|
||||||
return "boolean"
|
|
||||||
case reflect.Int64:
|
|
||||||
return "integer"
|
|
||||||
default:
|
|
||||||
panic("unsupported type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genSwaggerParams(cur reflect.Type) []*openapi.Parameter {
|
|
||||||
// when we have params the input must be a pointer to struct
|
|
||||||
if cur.Kind() != reflect.Ptr {
|
|
||||||
panic("not a pointer")
|
|
||||||
}
|
|
||||||
cur = cur.Elem()
|
|
||||||
if cur.Kind() != reflect.Struct {
|
|
||||||
panic("not a pointer to struct")
|
|
||||||
}
|
|
||||||
// now that we're sure of the type, inspect the fields
|
|
||||||
var out []*openapi.Parameter
|
|
||||||
for idx := 0; idx < cur.NumField(); idx++ {
|
|
||||||
f := cur.Field(idx)
|
|
||||||
if q := f.Tag.Get(tagForQuery); q != "" {
|
|
||||||
out = append(
|
|
||||||
out, &openapi.Parameter{
|
|
||||||
Name: q,
|
|
||||||
In: "query",
|
|
||||||
Required: f.Tag.Get(tagForRequired) == "true",
|
|
||||||
Type: d.swaggerParamForType(f.Type),
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if p := f.Tag.Get(tagForPath); p != "" {
|
|
||||||
out = append(out, &openapi.Parameter{
|
|
||||||
Name: p,
|
|
||||||
In: "path",
|
|
||||||
Required: true,
|
|
||||||
Type: d.swaggerParamForType(f.Type),
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Descriptor) genSwaggerPath() (string, *openapi.Path) {
|
|
||||||
pathStr, pathInfo := d.genSwaggerURLPath(), &openapi.Path{}
|
|
||||||
rtinfo := &openapi.RoundTrip{Produces: []string{"application/json"}}
|
|
||||||
switch d.Method {
|
|
||||||
case "GET":
|
|
||||||
pathInfo.Get = rtinfo
|
|
||||||
case "POST":
|
|
||||||
rtinfo.Consumes = append(rtinfo.Consumes, "application/json")
|
|
||||||
pathInfo.Post = rtinfo
|
|
||||||
default:
|
|
||||||
panic("unsupported method")
|
|
||||||
}
|
|
||||||
rtinfo.Parameters = d.genSwaggerParams(reflect.TypeOf(d.Request))
|
|
||||||
if d.Method != "GET" {
|
|
||||||
rtinfo.Parameters = append(rtinfo.Parameters, &openapi.Parameter{
|
|
||||||
Name: "body",
|
|
||||||
In: "body",
|
|
||||||
Required: true,
|
|
||||||
Schema: d.genSwaggerSchema(reflect.TypeOf(d.Request)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
rtinfo.Responses = &openapi.Responses{Successful: openapi.Body{
|
|
||||||
Description: "all good",
|
|
||||||
Schema: d.genSwaggerSchema(reflect.TypeOf(d.Response)),
|
|
||||||
}}
|
|
||||||
return pathStr, pathInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func genSwaggerVersion() string {
|
|
||||||
return time.Now().UTC().Format("0.20060102.1150405")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenSwaggerTestGo generates swagger_test.go
|
|
||||||
func GenSwaggerTestGo(file string) {
|
|
||||||
swagger := openapi.Swagger{
|
|
||||||
Swagger: "2.0",
|
|
||||||
Info: openapi.API{
|
|
||||||
Title: "OONI API specification",
|
|
||||||
Version: genSwaggerVersion(),
|
|
||||||
},
|
|
||||||
Host: "api.ooni.io",
|
|
||||||
BasePath: "/",
|
|
||||||
Schemes: []string{"https"},
|
|
||||||
Paths: make(map[string]*openapi.Path),
|
|
||||||
}
|
|
||||||
for _, desc := range Descriptors {
|
|
||||||
pathStr, pathInfo := desc.genSwaggerPath()
|
|
||||||
swagger.Paths[pathStr] = pathInfo
|
|
||||||
}
|
|
||||||
data, err := json.MarshalIndent(swagger, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
var sb strings.Builder
|
|
||||||
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
|
|
||||||
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
|
|
||||||
fmt.Fprint(&sb, "package ooapi\n\n")
|
|
||||||
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
|
|
||||||
fmt.Fprintf(&sb, "const swagger = `%s`\n", string(data))
|
|
||||||
writefile(file, &sb)
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/sys/execabs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func writefile(name string, sb *strings.Builder) {
|
|
||||||
filep, err := os.Create(name)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := fmt.Fprint(filep, sb.String()); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := filep.Close(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
cmd := execabs.Command("go", "fmt", name)
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
// Package openapi contains data structures for Swagger v2.0.
|
|
||||||
//
|
|
||||||
// We use these data structures to compare the API specification we
|
|
||||||
// have here with the one of the server.
|
|
||||||
package openapi
|
|
||||||
|
|
||||||
// Schema is the schema of a specific parameter or
|
|
||||||
// or the schema used by the response body
|
|
||||||
type Schema struct {
|
|
||||||
Properties map[string]*Schema `json:"properties,omitempty"`
|
|
||||||
Items *Schema `json:"items,omitempty"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parameter describes an input parameter, which could be in the
|
|
||||||
// URL path, in the query string, or in the request body
|
|
||||||
type Parameter struct {
|
|
||||||
In string `json:"in"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Required bool `json:"required,omitempty"`
|
|
||||||
Schema *Schema `json:"schema,omitempty"`
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Body describes a response body
|
|
||||||
type Body struct {
|
|
||||||
Description interface{} `json:"description,omitempty"`
|
|
||||||
Schema *Schema `json:"schema"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Responses describes the possible responses
|
|
||||||
type Responses struct {
|
|
||||||
Successful Body `json:"200"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoundTrip describes an HTTP round trip with a given method and path
|
|
||||||
type RoundTrip struct {
|
|
||||||
Consumes []string `json:"consumes,omitempty"`
|
|
||||||
Produces []string `json:"produces,omitempty"`
|
|
||||||
Parameters []*Parameter `json:"parameters,omitempty"`
|
|
||||||
Responses *Responses `json:"responses,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path describes a path served by the API
|
|
||||||
type Path struct {
|
|
||||||
Get *RoundTrip `json:"get,omitempty"`
|
|
||||||
Post *RoundTrip `json:"post,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// API contains info about the API
|
|
||||||
type API struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swagger is the toplevel structure
|
|
||||||
type Swagger struct {
|
|
||||||
Swagger string `json:"swagger"`
|
|
||||||
Info API `json:"info"`
|
|
||||||
Host string `json:"host"`
|
|
||||||
BasePath string `json:"basePath"`
|
|
||||||
Schemes []string `json:"schemes"`
|
|
||||||
Paths map[string]*Path `json:"paths"`
|
|
||||||
}
|
|
|
@ -1,295 +0,0 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
|
||||||
// 2022-05-19 20:30:49.900013 +0200 CEST m=+0.000719959
|
|
||||||
|
|
||||||
package ooapi
|
|
||||||
|
|
||||||
//go:generate go run ./internal/generator -file login.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
// withLoginPsiphonConfigAPI implements login for simplePsiphonConfigAPI.
|
|
||||||
type withLoginPsiphonConfigAPI struct {
|
|
||||||
API clonerForPsiphonConfigAPI // mandatory
|
|
||||||
JSONCodec JSONCodec // optional
|
|
||||||
KVStore KVStore // mandatory
|
|
||||||
RegisterAPI callerForRegisterAPI // mandatory
|
|
||||||
LoginAPI callerForLoginAPI // mandatory
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call logins, if needed, then calls the API.
|
|
||||||
func (api *withLoginPsiphonConfigAPI) Call(ctx context.Context, req *apimodel.PsiphonConfigRequest) (apimodel.PsiphonConfigResponse, error) {
|
|
||||||
token, err := api.maybeLogin(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp, err := api.API.WithToken(token).Call(ctx, req)
|
|
||||||
if errors.Is(err, ErrUnauthorized) {
|
|
||||||
// Maybe the clock is just off? Let's try to obtain
|
|
||||||
// a token again and see if this fixes it.
|
|
||||||
if token, err = api.forceLogin(ctx); err == nil {
|
|
||||||
switch resp, err = api.API.WithToken(token).Call(ctx, req); err {
|
|
||||||
case nil:
|
|
||||||
return resp, nil
|
|
||||||
case ErrUnauthorized:
|
|
||||||
// fallthrough
|
|
||||||
default:
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Okay, this seems a broader problem. How about we try
|
|
||||||
// and re-register ourselves again instead?
|
|
||||||
token, err = api.forceRegister(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp, err = api.API.WithToken(token).Call(ctx, req)
|
|
||||||
// fallthrough
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginPsiphonConfigAPI) jsonCodec() JSONCodec {
|
|
||||||
if api.JSONCodec != nil {
|
|
||||||
return api.JSONCodec
|
|
||||||
}
|
|
||||||
return &defaultJSONCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginPsiphonConfigAPI) readstate() (*loginState, error) {
|
|
||||||
data, err := api.KVStore.Get(loginKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var ls loginState
|
|
||||||
if err := api.jsonCodec().Decode(data, &ls); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ls, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginPsiphonConfigAPI) writestate(ls *loginState) error {
|
|
||||||
data, err := api.jsonCodec().Encode(*ls)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return api.KVStore.Set(loginKey, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginPsiphonConfigAPI) doRegister(ctx context.Context, password string) (string, error) {
|
|
||||||
req := newRegisterRequest(password)
|
|
||||||
ls := &loginState{}
|
|
||||||
resp, err := api.RegisterAPI.Call(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
ls.ClientID = resp.ClientID
|
|
||||||
ls.Password = req.Password
|
|
||||||
return api.doLogin(ctx, ls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginPsiphonConfigAPI) forceRegister(ctx context.Context) (string, error) {
|
|
||||||
var password string
|
|
||||||
// If we already have a previous password, let us keep
|
|
||||||
// using it. This will allow a new version of the API to
|
|
||||||
// be able to continue to identify this probe. (This
|
|
||||||
// assumes that we have a stateless API that generates
|
|
||||||
// the user ID as a signature of the password plus a
|
|
||||||
// timestamp and that the key to generate the signature
|
|
||||||
// is not lost. If all these conditions are met, we
|
|
||||||
// can then serve better test targets to more long running
|
|
||||||
// (and therefore trusted) probes.)
|
|
||||||
if ls, err := api.readstate(); err == nil {
|
|
||||||
password = ls.Password
|
|
||||||
}
|
|
||||||
if password == "" {
|
|
||||||
password = newRandomPassword()
|
|
||||||
}
|
|
||||||
return api.doRegister(ctx, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginPsiphonConfigAPI) forceLogin(ctx context.Context) (string, error) {
|
|
||||||
ls, err := api.readstate()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return api.doLogin(ctx, ls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginPsiphonConfigAPI) maybeLogin(ctx context.Context) (string, error) {
|
|
||||||
ls, _ := api.readstate()
|
|
||||||
if ls == nil || !ls.credentialsValid() {
|
|
||||||
return api.forceRegister(ctx)
|
|
||||||
}
|
|
||||||
if !ls.tokenValid() {
|
|
||||||
return api.doLogin(ctx, ls)
|
|
||||||
}
|
|
||||||
return ls.Token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginPsiphonConfigAPI) doLogin(ctx context.Context, ls *loginState) (string, error) {
|
|
||||||
req := &apimodel.LoginRequest{
|
|
||||||
ClientID: ls.ClientID,
|
|
||||||
Password: ls.Password,
|
|
||||||
}
|
|
||||||
resp, err := api.LoginAPI.Call(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
ls.Token = resp.Token
|
|
||||||
ls.Expire = resp.Expire
|
|
||||||
if err := api.writestate(ls); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return ls.Token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ callerForPsiphonConfigAPI = &withLoginPsiphonConfigAPI{}
|
|
||||||
|
|
||||||
// withLoginTorTargetsAPI implements login for simpleTorTargetsAPI.
|
|
||||||
type withLoginTorTargetsAPI struct {
|
|
||||||
API clonerForTorTargetsAPI // mandatory
|
|
||||||
JSONCodec JSONCodec // optional
|
|
||||||
KVStore KVStore // mandatory
|
|
||||||
RegisterAPI callerForRegisterAPI // mandatory
|
|
||||||
LoginAPI callerForLoginAPI // mandatory
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call logins, if needed, then calls the API.
|
|
||||||
func (api *withLoginTorTargetsAPI) Call(ctx context.Context, req *apimodel.TorTargetsRequest) (apimodel.TorTargetsResponse, error) {
|
|
||||||
token, err := api.maybeLogin(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp, err := api.API.WithToken(token).Call(ctx, req)
|
|
||||||
if errors.Is(err, ErrUnauthorized) {
|
|
||||||
// Maybe the clock is just off? Let's try to obtain
|
|
||||||
// a token again and see if this fixes it.
|
|
||||||
if token, err = api.forceLogin(ctx); err == nil {
|
|
||||||
switch resp, err = api.API.WithToken(token).Call(ctx, req); err {
|
|
||||||
case nil:
|
|
||||||
return resp, nil
|
|
||||||
case ErrUnauthorized:
|
|
||||||
// fallthrough
|
|
||||||
default:
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Okay, this seems a broader problem. How about we try
|
|
||||||
// and re-register ourselves again instead?
|
|
||||||
token, err = api.forceRegister(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp, err = api.API.WithToken(token).Call(ctx, req)
|
|
||||||
// fallthrough
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginTorTargetsAPI) jsonCodec() JSONCodec {
|
|
||||||
if api.JSONCodec != nil {
|
|
||||||
return api.JSONCodec
|
|
||||||
}
|
|
||||||
return &defaultJSONCodec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginTorTargetsAPI) readstate() (*loginState, error) {
|
|
||||||
data, err := api.KVStore.Get(loginKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var ls loginState
|
|
||||||
if err := api.jsonCodec().Decode(data, &ls); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ls, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginTorTargetsAPI) writestate(ls *loginState) error {
|
|
||||||
data, err := api.jsonCodec().Encode(*ls)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return api.KVStore.Set(loginKey, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginTorTargetsAPI) doRegister(ctx context.Context, password string) (string, error) {
|
|
||||||
req := newRegisterRequest(password)
|
|
||||||
ls := &loginState{}
|
|
||||||
resp, err := api.RegisterAPI.Call(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
ls.ClientID = resp.ClientID
|
|
||||||
ls.Password = req.Password
|
|
||||||
return api.doLogin(ctx, ls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginTorTargetsAPI) forceRegister(ctx context.Context) (string, error) {
|
|
||||||
var password string
|
|
||||||
// If we already have a previous password, let us keep
|
|
||||||
// using it. This will allow a new version of the API to
|
|
||||||
// be able to continue to identify this probe. (This
|
|
||||||
// assumes that we have a stateless API that generates
|
|
||||||
// the user ID as a signature of the password plus a
|
|
||||||
// timestamp and that the key to generate the signature
|
|
||||||
// is not lost. If all these conditions are met, we
|
|
||||||
// can then serve better test targets to more long running
|
|
||||||
// (and therefore trusted) probes.)
|
|
||||||
if ls, err := api.readstate(); err == nil {
|
|
||||||
password = ls.Password
|
|
||||||
}
|
|
||||||
if password == "" {
|
|
||||||
password = newRandomPassword()
|
|
||||||
}
|
|
||||||
return api.doRegister(ctx, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginTorTargetsAPI) forceLogin(ctx context.Context) (string, error) {
|
|
||||||
ls, err := api.readstate()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return api.doLogin(ctx, ls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginTorTargetsAPI) maybeLogin(ctx context.Context) (string, error) {
|
|
||||||
ls, _ := api.readstate()
|
|
||||||
if ls == nil || !ls.credentialsValid() {
|
|
||||||
return api.forceRegister(ctx)
|
|
||||||
}
|
|
||||||
if !ls.tokenValid() {
|
|
||||||
return api.doLogin(ctx, ls)
|
|
||||||
}
|
|
||||||
return ls.Token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *withLoginTorTargetsAPI) doLogin(ctx context.Context, ls *loginState) (string, error) {
|
|
||||||
req := &apimodel.LoginRequest{
|
|
||||||
ClientID: ls.ClientID,
|
|
||||||
Password: ls.Password,
|
|
||||||
}
|
|
||||||
resp, err := api.LoginAPI.Call(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
ls.Token = resp.Token
|
|
||||||
ls.Expire = resp.Expire
|
|
||||||
if err := api.writestate(ls); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return ls.Token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ callerForTorTargetsAPI = &withLoginTorTargetsAPI{}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,208 +0,0 @@
|
||||||
package ooapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/atomicx"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoginHandler is an http.Handler to test login
|
|
||||||
type LoginHandler struct {
|
|
||||||
failCallWith []int // ignored by login and register
|
|
||||||
mu sync.Mutex
|
|
||||||
noRegister bool
|
|
||||||
state []*loginState
|
|
||||||
t *testing.T
|
|
||||||
logins *atomicx.Int64
|
|
||||||
registers *atomicx.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lh *LoginHandler) forgetLogins() {
|
|
||||||
defer lh.mu.Unlock()
|
|
||||||
lh.mu.Lock()
|
|
||||||
lh.state = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lh *LoginHandler) forgetTokens() {
|
|
||||||
defer lh.mu.Unlock()
|
|
||||||
lh.mu.Lock()
|
|
||||||
for _, entry := range lh.state {
|
|
||||||
// This should be enough to cause all tokens to
|
|
||||||
// be expired and force clients to relogin.
|
|
||||||
//
|
|
||||||
// (It does not matter much whether the client
|
|
||||||
// clock is off, or the server clock is off,
|
|
||||||
// thanks Galileo for explaining this to us <3.)
|
|
||||||
entry.Expire = time.Now().Add(-3600 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lh *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Implementation note: we don't check for the method
|
|
||||||
// for simplicity since it's already tested.
|
|
||||||
switch r.URL.Path {
|
|
||||||
case "/api/v1/register":
|
|
||||||
if lh.registers != nil {
|
|
||||||
lh.registers.Add(1)
|
|
||||||
}
|
|
||||||
lh.register(w, r)
|
|
||||||
case "/api/v1/login":
|
|
||||||
if lh.logins != nil {
|
|
||||||
lh.logins.Add(1)
|
|
||||||
}
|
|
||||||
lh.login(w, r)
|
|
||||||
case "/api/v1/test-list/psiphon-config":
|
|
||||||
lh.psiphon(w, r)
|
|
||||||
case "/api/v1/test-list/tor-targets":
|
|
||||||
lh.tor(w, r)
|
|
||||||
default:
|
|
||||||
w.WriteHeader(500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lh *LoginHandler) register(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Body == nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := netxlite.ReadAllContext(r.Context(), r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req apimodel.RegisterRequest
|
|
||||||
if err := json.Unmarshal(data, &req); err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if req.Password == "" {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer lh.mu.Unlock()
|
|
||||||
lh.mu.Lock()
|
|
||||||
if lh.noRegister {
|
|
||||||
// We have been asked to stop registering clients so
|
|
||||||
// we're going to make a boo boo.
|
|
||||||
w.WriteHeader(500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var resp apimodel.RegisterResponse
|
|
||||||
ff := &fakeFill{}
|
|
||||||
ff.Fill(&resp)
|
|
||||||
lh.state = append(lh.state, &loginState{
|
|
||||||
ClientID: resp.ClientID, Password: req.Password})
|
|
||||||
data, err = json.Marshal(&resp)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lh.t.Logf("register: %+v", string(data))
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lh *LoginHandler) login(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Body == nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := netxlite.ReadAllContext(r.Context(), r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req apimodel.LoginRequest
|
|
||||||
if err := json.Unmarshal(data, &req); err != nil {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer lh.mu.Unlock()
|
|
||||||
lh.mu.Lock()
|
|
||||||
for _, s := range lh.state {
|
|
||||||
if req.ClientID == s.ClientID && req.Password == s.Password {
|
|
||||||
var resp apimodel.LoginResponse
|
|
||||||
ff := &fakeFill{}
|
|
||||||
ff.Fill(&resp)
|
|
||||||
// We want the token to be many seconds in the future while
|
|
||||||
// ff.fill only sets the tokent to now plus a small delta.
|
|
||||||
resp.Expire = time.Now().Add(3600 * time.Second)
|
|
||||||
s.Expire = resp.Expire
|
|
||||||
s.Token = resp.Token
|
|
||||||
data, err = json.Marshal(&resp)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lh.t.Logf("login: %+v", string(data))
|
|
||||||
w.Write(data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lh.t.Log("login: 401")
|
|
||||||
w.WriteHeader(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lh *LoginHandler) psiphon(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer lh.mu.Unlock()
|
|
||||||
lh.mu.Lock()
|
|
||||||
if len(lh.failCallWith) > 0 {
|
|
||||||
code := lh.failCallWith[0]
|
|
||||||
lh.failCallWith = lh.failCallWith[1:]
|
|
||||||
w.WriteHeader(code)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
token := strings.Replace(r.Header.Get("Authorization"), "Bearer ", "", 1)
|
|
||||||
for _, s := range lh.state {
|
|
||||||
if token == s.Token && time.Now().Before(s.Expire) {
|
|
||||||
var resp apimodel.PsiphonConfigResponse
|
|
||||||
ff := &fakeFill{}
|
|
||||||
ff.Fill(&resp)
|
|
||||||
data, err := json.Marshal(&resp)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lh.t.Logf("psiphon: %+v", string(data))
|
|
||||||
w.Write(data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lh.t.Log("psiphon: 401")
|
|
||||||
w.WriteHeader(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lh *LoginHandler) tor(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer lh.mu.Unlock()
|
|
||||||
lh.mu.Lock()
|
|
||||||
if len(lh.failCallWith) > 0 {
|
|
||||||
code := lh.failCallWith[0]
|
|
||||||
lh.failCallWith = lh.failCallWith[1:]
|
|
||||||
w.WriteHeader(code)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
token := strings.Replace(r.Header.Get("Authorization"), "Bearer ", "", 1)
|
|
||||||
for _, s := range lh.state {
|
|
||||||
if token == s.Token && time.Now().Before(s.Expire) {
|
|
||||||
var resp apimodel.TorTargetsResponse
|
|
||||||
ff := &fakeFill{}
|
|
||||||
ff.Fill(&resp)
|
|
||||||
data, err := json.Marshal(&resp)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lh.t.Logf("tor: %+v", string(data))
|
|
||||||
w.Write(data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lh.t.Log("tor: 401")
|
|
||||||
w.WriteHeader(401)
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
package ooapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
|
||||||
)
|
|
||||||
|
|
||||||
// loginState is the struct saved in the kvstore
|
|
||||||
// to keep track of the login state.
|
|
||||||
type loginState struct {
|
|
||||||
ClientID string
|
|
||||||
Expire time.Time
|
|
||||||
Password string
|
|
||||||
Token string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ls *loginState) credentialsValid() bool {
|
|
||||||
return ls.ClientID != "" && ls.Password != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ls *loginState) tokenValid() bool {
|
|
||||||
return ls.Token != "" && time.Now().Add(60*time.Second).Before(ls.Expire)
|
|
||||||
}
|
|
||||||
|
|
||||||
// loginKey is the key with which loginState is saved
|
|
||||||
// into the key-value store used by Client.
|
|
||||||
const loginKey = "orchestra.state"
|
|
||||||
|
|
||||||
// newRandomPassword generates a new random password.
|
|
||||||
func newRandomPassword() string {
|
|
||||||
b := make([]byte, 48)
|
|
||||||
_, err := rand.Read(b)
|
|
||||||
runtimex.PanicOnError(err, "rand.Read failed")
|
|
||||||
return base64.StdEncoding.EncodeToString(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newRegisterRequest creates a new RegisterRequest.
|
|
||||||
func newRegisterRequest(password string) *apimodel.RegisterRequest {
|
|
||||||
return &apimodel.RegisterRequest{
|
|
||||||
// The original implementation has as its only use case that we
|
|
||||||
// were registering and logging in for sending an update regarding
|
|
||||||
// the probe whereabouts. Yet here in probe-engine, the orchestra
|
|
||||||
// is currently only used to fetch inputs. For this purpose, we don't
|
|
||||||
// need to communicate any specific information. The code that will
|
|
||||||
// perform an update used to be responsible of doing that. Now, we
|
|
||||||
// are not using orchestra for this purpose anymore.
|
|
||||||
Platform: "miniooni",
|
|
||||||
ProbeASN: "AS0",
|
|
||||||
ProbeCC: "ZZ",
|
|
||||||
SoftwareName: "miniooni",
|
|
||||||
SoftwareVersion: "0.1.0-dev",
|
|
||||||
SupportedTests: []string{"web_connectivity"},
|
|
||||||
Password: password,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,192 +0,0 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
|
||||||
// 2022-05-19 20:30:50.891059 +0200 CEST m=+0.000799292
|
|
||||||
|
|
||||||
package ooapi
|
|
||||||
|
|
||||||
//go:generate go run ./internal/generator -file requests.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (api *simpleCheckReportIDAPI) newRequest(ctx context.Context, req *apimodel.CheckReportIDRequest) (*http.Request, error) {
|
|
||||||
URL, err := url.Parse(api.baseURL())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
URL.Path = "/api/_/check_report_id"
|
|
||||||
q := url.Values{}
|
|
||||||
if req.ReportID == "" {
|
|
||||||
return nil, newErrEmptyField("ReportID")
|
|
||||||
}
|
|
||||||
q.Add("report_id", req.ReportID)
|
|
||||||
URL.RawQuery = q.Encode()
|
|
||||||
return api.requestMaker().NewRequest(ctx, "GET", URL.String(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleCheckInAPI) newRequest(ctx context.Context, req *apimodel.CheckInRequest) (*http.Request, error) {
|
|
||||||
URL, err := url.Parse(api.baseURL())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
URL.Path = "/api/v1/check-in"
|
|
||||||
body, err := api.jsonCodec().Encode(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out, err := api.requestMaker().NewRequest(ctx, "POST", URL.String(), bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out.Header.Set("Content-Type", "application/json")
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleLoginAPI) newRequest(ctx context.Context, req *apimodel.LoginRequest) (*http.Request, error) {
|
|
||||||
URL, err := url.Parse(api.baseURL())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
URL.Path = "/api/v1/login"
|
|
||||||
body, err := api.jsonCodec().Encode(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out, err := api.requestMaker().NewRequest(ctx, "POST", URL.String(), bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out.Header.Set("Content-Type", "application/json")
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleMeasurementMetaAPI) newRequest(ctx context.Context, req *apimodel.MeasurementMetaRequest) (*http.Request, error) {
|
|
||||||
URL, err := url.Parse(api.baseURL())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
URL.Path = "/api/v1/measurement_meta"
|
|
||||||
q := url.Values{}
|
|
||||||
if req.ReportID == "" {
|
|
||||||
return nil, newErrEmptyField("ReportID")
|
|
||||||
}
|
|
||||||
q.Add("report_id", req.ReportID)
|
|
||||||
if req.Full {
|
|
||||||
q.Add("full", "true")
|
|
||||||
}
|
|
||||||
if req.Input != "" {
|
|
||||||
q.Add("input", req.Input)
|
|
||||||
}
|
|
||||||
URL.RawQuery = q.Encode()
|
|
||||||
return api.requestMaker().NewRequest(ctx, "GET", URL.String(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleRegisterAPI) newRequest(ctx context.Context, req *apimodel.RegisterRequest) (*http.Request, error) {
|
|
||||||
URL, err := url.Parse(api.baseURL())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
URL.Path = "/api/v1/register"
|
|
||||||
body, err := api.jsonCodec().Encode(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out, err := api.requestMaker().NewRequest(ctx, "POST", URL.String(), bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out.Header.Set("Content-Type", "application/json")
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleTestHelpersAPI) newRequest(ctx context.Context, req *apimodel.TestHelpersRequest) (*http.Request, error) {
|
|
||||||
URL, err := url.Parse(api.baseURL())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
URL.Path = "/api/v1/test-helpers"
|
|
||||||
return api.requestMaker().NewRequest(ctx, "GET", URL.String(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simplePsiphonConfigAPI) newRequest(ctx context.Context, req *apimodel.PsiphonConfigRequest) (*http.Request, error) {
|
|
||||||
URL, err := url.Parse(api.baseURL())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
URL.Path = "/api/v1/test-list/psiphon-config"
|
|
||||||
return api.requestMaker().NewRequest(ctx, "GET", URL.String(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleTorTargetsAPI) newRequest(ctx context.Context, req *apimodel.TorTargetsRequest) (*http.Request, error) {
|
|
||||||
URL, err := url.Parse(api.baseURL())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
URL.Path = "/api/v1/test-list/tor-targets"
|
|
||||||
return api.requestMaker().NewRequest(ctx, "GET", URL.String(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleURLsAPI) newRequest(ctx context.Context, req *apimodel.URLsRequest) (*http.Request, error) {
|
|
||||||
URL, err := url.Parse(api.baseURL())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
URL.Path = "/api/v1/test-list/urls"
|
|
||||||
q := url.Values{}
|
|
||||||
if req.CategoryCodes != "" {
|
|
||||||
q.Add("category_codes", req.CategoryCodes)
|
|
||||||
}
|
|
||||||
if req.CountryCode != "" {
|
|
||||||
q.Add("country_code", req.CountryCode)
|
|
||||||
}
|
|
||||||
if req.Limit != 0 {
|
|
||||||
q.Add("limit", newQueryFieldInt64(req.Limit))
|
|
||||||
}
|
|
||||||
URL.RawQuery = q.Encode()
|
|
||||||
return api.requestMaker().NewRequest(ctx, "GET", URL.String(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleOpenReportAPI) newRequest(ctx context.Context, req *apimodel.OpenReportRequest) (*http.Request, error) {
|
|
||||||
URL, err := url.Parse(api.baseURL())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
URL.Path = "/report"
|
|
||||||
body, err := api.jsonCodec().Encode(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out, err := api.requestMaker().NewRequest(ctx, "POST", URL.String(), bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out.Header.Set("Content-Type", "application/json")
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleSubmitMeasurementAPI) newRequest(ctx context.Context, req *apimodel.SubmitMeasurementRequest) (*http.Request, error) {
|
|
||||||
URL, err := url.Parse(api.baseURL())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
up, err := api.templateExecutor().Execute("/report/{{ .ReportID }}", req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
URL.Path = up
|
|
||||||
body, err := api.jsonCodec().Encode(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out, err := api.requestMaker().NewRequest(ctx, "POST", URL.String(), bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out.Header.Set("Content-Type", "application/json")
|
|
||||||
return out, nil
|
|
||||||
}
|
|
|
@ -1,277 +0,0 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
|
||||||
// 2022-05-19 20:30:51.316851 +0200 CEST m=+0.000816793
|
|
||||||
|
|
||||||
package ooapi
|
|
||||||
|
|
||||||
//go:generate go run ./internal/generator -file responses.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (api *simpleCheckReportIDAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.CheckReportIDResponse, error) {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, newHTTPFailure(resp.StatusCode)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
reader := io.LimitReader(resp.Body, 4<<20)
|
|
||||||
data, err := netxlite.ReadAllContext(ctx, reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := &apimodel.CheckReportIDResponse{}
|
|
||||||
if err := api.jsonCodec().Decode(data, out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleCheckInAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.CheckInResponse, error) {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, newHTTPFailure(resp.StatusCode)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
reader := io.LimitReader(resp.Body, 4<<20)
|
|
||||||
data, err := netxlite.ReadAllContext(ctx, reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := &apimodel.CheckInResponse{}
|
|
||||||
if err := api.jsonCodec().Decode(data, out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleLoginAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.LoginResponse, error) {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, newHTTPFailure(resp.StatusCode)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
reader := io.LimitReader(resp.Body, 4<<20)
|
|
||||||
data, err := netxlite.ReadAllContext(ctx, reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := &apimodel.LoginResponse{}
|
|
||||||
if err := api.jsonCodec().Decode(data, out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleMeasurementMetaAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.MeasurementMetaResponse, error) {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, newHTTPFailure(resp.StatusCode)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
reader := io.LimitReader(resp.Body, 4<<20)
|
|
||||||
data, err := netxlite.ReadAllContext(ctx, reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := &apimodel.MeasurementMetaResponse{}
|
|
||||||
if err := api.jsonCodec().Decode(data, out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleRegisterAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.RegisterResponse, error) {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, newHTTPFailure(resp.StatusCode)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
reader := io.LimitReader(resp.Body, 4<<20)
|
|
||||||
data, err := netxlite.ReadAllContext(ctx, reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := &apimodel.RegisterResponse{}
|
|
||||||
if err := api.jsonCodec().Decode(data, out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleTestHelpersAPI) newResponse(ctx context.Context, resp *http.Response, err error) (apimodel.TestHelpersResponse, error) {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, newHTTPFailure(resp.StatusCode)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
reader := io.LimitReader(resp.Body, 4<<20)
|
|
||||||
data, err := netxlite.ReadAllContext(ctx, reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := apimodel.TestHelpersResponse{}
|
|
||||||
if err := api.jsonCodec().Decode(data, &out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if out == nil {
|
|
||||||
return nil, ErrJSONLiteralNull
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simplePsiphonConfigAPI) newResponse(ctx context.Context, resp *http.Response, err error) (apimodel.PsiphonConfigResponse, error) {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, newHTTPFailure(resp.StatusCode)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
reader := io.LimitReader(resp.Body, 4<<20)
|
|
||||||
data, err := netxlite.ReadAllContext(ctx, reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := apimodel.PsiphonConfigResponse{}
|
|
||||||
if err := api.jsonCodec().Decode(data, &out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if out == nil {
|
|
||||||
return nil, ErrJSONLiteralNull
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleTorTargetsAPI) newResponse(ctx context.Context, resp *http.Response, err error) (apimodel.TorTargetsResponse, error) {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, newHTTPFailure(resp.StatusCode)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
reader := io.LimitReader(resp.Body, 4<<20)
|
|
||||||
data, err := netxlite.ReadAllContext(ctx, reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := apimodel.TorTargetsResponse{}
|
|
||||||
if err := api.jsonCodec().Decode(data, &out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if out == nil {
|
|
||||||
return nil, ErrJSONLiteralNull
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleURLsAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.URLsResponse, error) {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, newHTTPFailure(resp.StatusCode)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
reader := io.LimitReader(resp.Body, 4<<20)
|
|
||||||
data, err := netxlite.ReadAllContext(ctx, reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := &apimodel.URLsResponse{}
|
|
||||||
if err := api.jsonCodec().Decode(data, out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleOpenReportAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.OpenReportResponse, error) {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, newHTTPFailure(resp.StatusCode)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
reader := io.LimitReader(resp.Body, 4<<20)
|
|
||||||
data, err := netxlite.ReadAllContext(ctx, reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := &apimodel.OpenReportResponse{}
|
|
||||||
if err := api.jsonCodec().Decode(data, out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *simpleSubmitMeasurementAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.SubmitMeasurementResponse, error) {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 401 {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, newHTTPFailure(resp.StatusCode)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
reader := io.LimitReader(resp.Body, 4<<20)
|
|
||||||
data, err := netxlite.ReadAllContext(ctx, reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := &apimodel.SubmitMeasurementResponse{}
|
|
||||||
if err := api.jsonCodec().Decode(data, out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
|
@ -1,578 +0,0 @@
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
|
||||||
// 2022-05-19 20:30:51.768205 +0200 CEST m=+0.001003793
|
|
||||||
|
|
||||||
package ooapi
|
|
||||||
|
|
||||||
//go:generate go run ./internal/generator -file swagger_test.go
|
|
||||||
|
|
||||||
const swagger = `{
|
|
||||||
"swagger": "2.0",
|
|
||||||
"info": {
|
|
||||||
"title": "OONI API specification",
|
|
||||||
"version": "0.20220519.5183051"
|
|
||||||
},
|
|
||||||
"host": "api.ooni.io",
|
|
||||||
"basePath": "/",
|
|
||||||
"schemes": [
|
|
||||||
"https"
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
"/api/_/check_report_id": {
|
|
||||||
"get": {
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "report_id",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "all good",
|
|
||||||
"schema": {
|
|
||||||
"properties": {
|
|
||||||
"error": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"found": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"v": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/check-in": {
|
|
||||||
"post": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "body",
|
|
||||||
"name": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"properties": {
|
|
||||||
"charging": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"on_wifi": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"platform": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"probe_asn": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"probe_cc": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"run_type": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"software_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"software_version": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"web_connectivity": {
|
|
||||||
"properties": {
|
|
||||||
"category_codes": {
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "all good",
|
|
||||||
"schema": {
|
|
||||||
"properties": {
|
|
||||||
"probe_asn": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"probe_cc": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"tests": {
|
|
||||||
"properties": {
|
|
||||||
"web_connectivity": {
|
|
||||||
"properties": {
|
|
||||||
"report_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"urls": {
|
|
||||||
"items": {
|
|
||||||
"properties": {
|
|
||||||
"category_code": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"country_code": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"v": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/login": {
|
|
||||||
"post": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "body",
|
|
||||||
"name": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"properties": {
|
|
||||||
"password": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "all good",
|
|
||||||
"schema": {
|
|
||||||
"properties": {
|
|
||||||
"expire": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/measurement_meta": {
|
|
||||||
"get": {
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "report_id",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "full",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "input",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "all good",
|
|
||||||
"schema": {
|
|
||||||
"properties": {
|
|
||||||
"anomaly": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"category_code": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"confirmed": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"failure": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"input": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"measurement_start_time": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"probe_asn": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"probe_cc": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"raw_measurement": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"report_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"scores": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"test_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"test_start_time": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/register": {
|
|
||||||
"post": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "body",
|
|
||||||
"name": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"properties": {
|
|
||||||
"available_bandwidth": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"device_token": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"language": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"network_type": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"platform": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"probe_asn": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"probe_cc": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"probe_family": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"probe_timezone": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"software_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"software_version": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"supported_tests": {
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "all good",
|
|
||||||
"schema": {
|
|
||||||
"properties": {
|
|
||||||
"client_id": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/test-helpers": {
|
|
||||||
"get": {
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "all good",
|
|
||||||
"schema": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/test-list/psiphon-config": {
|
|
||||||
"get": {
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "all good",
|
|
||||||
"schema": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/test-list/tor-targets": {
|
|
||||||
"get": {
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "all good",
|
|
||||||
"schema": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/test-list/urls": {
|
|
||||||
"get": {
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "category_codes",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "country_code",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "limit",
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "all good",
|
|
||||||
"schema": {
|
|
||||||
"properties": {
|
|
||||||
"metadata": {
|
|
||||||
"properties": {
|
|
||||||
"count": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"results": {
|
|
||||||
"items": {
|
|
||||||
"properties": {
|
|
||||||
"category_code": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"country_code": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/report": {
|
|
||||||
"post": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "body",
|
|
||||||
"name": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"properties": {
|
|
||||||
"data_format_version": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"format": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"probe_asn": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"probe_cc": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"software_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"software_version": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"test_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"test_start_time": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"test_version": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "all good",
|
|
||||||
"schema": {
|
|
||||||
"properties": {
|
|
||||||
"backend_version": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"report_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"supported_formats": {
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/report/{report_id}": {
|
|
||||||
"post": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "path",
|
|
||||||
"name": "report_id",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "body",
|
|
||||||
"name": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"properties": {
|
|
||||||
"content": {
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"format": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "all good",
|
|
||||||
"schema": {
|
|
||||||
"properties": {
|
|
||||||
"measurement_uid": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`
|
|
|
@ -1,161 +0,0 @@
|
||||||
package ooapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hexops/gotextdiff"
|
|
||||||
"github.com/hexops/gotextdiff/myers"
|
|
||||||
"github.com/hexops/gotextdiff/span"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/ooapi/internal/openapi"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
productionURL = "https://api.ooni.io/apispec_1.json"
|
|
||||||
testingURL = "https://ams-pg-test.ooni.org/apispec_1.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeModel(data []byte) *openapi.Swagger {
|
|
||||||
var out openapi.Swagger
|
|
||||||
if err := json.Unmarshal(data, &out); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
// We reduce irrelevant differences by producing a common header
|
|
||||||
return &openapi.Swagger{Paths: out.Paths}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getServerModel(serverURL string) *openapi.Swagger {
|
|
||||||
resp, err := http.Get(serverURL)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
data, err := netxlite.ReadAllContext(context.Background(), resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
return makeModel(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getClientModel() *openapi.Swagger {
|
|
||||||
return makeModel([]byte(swagger))
|
|
||||||
}
|
|
||||||
|
|
||||||
func simplifyRoundTrip(rt *openapi.RoundTrip) {
|
|
||||||
// Normalize the used name when a parameter is in body. This
|
|
||||||
// should only have a cosmetic impact on the spec.
|
|
||||||
for _, param := range rt.Parameters {
|
|
||||||
if param.In == "body" {
|
|
||||||
param.Name = "body"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort parameters so the comparison does not depend on order.
|
|
||||||
sort.SliceStable(rt.Parameters, func(i, j int) bool {
|
|
||||||
left, right := rt.Parameters[i].Name, rt.Parameters[j].Name
|
|
||||||
return strings.Compare(left, right) < 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// Normalize description of 200 response
|
|
||||||
rt.Responses.Successful.Description = "all good"
|
|
||||||
}
|
|
||||||
|
|
||||||
func simplifyInPlace(path *openapi.Path) *openapi.Path {
|
|
||||||
if path.Get != nil && path.Post != nil {
|
|
||||||
log.Fatal("unsupported configuration")
|
|
||||||
}
|
|
||||||
if path.Get != nil {
|
|
||||||
simplifyRoundTrip(path.Get)
|
|
||||||
}
|
|
||||||
if path.Post != nil {
|
|
||||||
simplifyRoundTrip(path.Post)
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
func jsonify(model interface{}) string {
|
|
||||||
data, err := json.MarshalIndent(model, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
return string(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
type diffable struct {
|
|
||||||
name string
|
|
||||||
value string
|
|
||||||
}
|
|
||||||
|
|
||||||
func computediff(server, client *diffable) string {
|
|
||||||
d := gotextdiff.ToUnified(server.name, client.name, server.value, myers.ComputeEdits(
|
|
||||||
span.URIFromPath(server.name), server.value, client.value,
|
|
||||||
))
|
|
||||||
return fmt.Sprint(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
// maybediff emits the diff between the server and the client and
|
|
||||||
// returns the length of the diff itself in bytes.
|
|
||||||
func maybediff(key string, server, client *openapi.Path) int {
|
|
||||||
diff := computediff(&diffable{
|
|
||||||
name: fmt.Sprintf("server%s.json", key),
|
|
||||||
value: jsonify(simplifyInPlace(server)),
|
|
||||||
}, &diffable{
|
|
||||||
name: fmt.Sprintf("client%s.json", key),
|
|
||||||
value: jsonify(simplifyInPlace(client)),
|
|
||||||
})
|
|
||||||
if diff != "" {
|
|
||||||
fmt.Printf("%s", diff)
|
|
||||||
}
|
|
||||||
return len(diff)
|
|
||||||
}
|
|
||||||
|
|
||||||
func compare(serverURL string) bool {
|
|
||||||
good := true
|
|
||||||
serverModel, clientModel := getServerModel(serverURL), getClientModel()
|
|
||||||
// Implementation note: the server model is richer than the client
|
|
||||||
// model, so we ignore everything not defined by the client.
|
|
||||||
var count int
|
|
||||||
for key := range serverModel.Paths {
|
|
||||||
if _, found := clientModel.Paths[key]; !found {
|
|
||||||
delete(serverModel.Paths, key)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
if maybediff(key, serverModel.Paths[key], clientModel.Paths[key]) > 0 {
|
|
||||||
good = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if count <= 0 {
|
|
||||||
panic("no element found")
|
|
||||||
}
|
|
||||||
return good
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithProductionAPI(t *testing.T) {
|
|
||||||
t.Skip("skip until we use this part of the codebase")
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skip test in short mode")
|
|
||||||
}
|
|
||||||
t.Log("using ", productionURL)
|
|
||||||
if !compare(productionURL) {
|
|
||||||
t.Fatal("model mismatch (see above)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithTestingAPI(t *testing.T) {
|
|
||||||
t.Skip("skip until we use this part of the codebase")
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skip test in short mode")
|
|
||||||
}
|
|
||||||
t.Log("using ", testingURL)
|
|
||||||
if !compare(testingURL) {
|
|
||||||
t.Fatal("model mismatch (see above)")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
package ooapi
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func newErrEmptyField(field string) error {
|
|
||||||
return fmt.Errorf("%w: %s", ErrEmptyField, field)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTPFailure(status int) error {
|
|
||||||
return fmt.Errorf("%w: %d", ErrHTTPFailure, status)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newQueryFieldInt64(v int64) string {
|
|
||||||
return fmt.Sprintf("%d", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newQueryFieldBool(v bool) string {
|
|
||||||
return fmt.Sprintf("%v", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAuthorizationHeader(token string) string {
|
|
||||||
return fmt.Sprintf("Bearer %s", token)
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package ooapi
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestNewQueryFieldBoolWorks(t *testing.T) {
|
|
||||||
if s := newQueryFieldBool(true); s != "true" {
|
|
||||||
t.Fatal("invalid encoding of true")
|
|
||||||
}
|
|
||||||
if s := newQueryFieldBool(false); s != "false" {
|
|
||||||
t.Fatal("invalid encoding of false")
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user