From aa78c1105e7b6b74d6185cc33daa42f93ccbea58 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 2 Nov 2021 16:20:39 +0100 Subject: anastasis-core: provide reducer CLI, refactor state machine --- packages/taler-util/src/clk.ts | 620 ++++++++++++++++++++++++++++++++++ packages/taler-util/src/index.node.ts | 1 + 2 files changed, 621 insertions(+) create mode 100644 packages/taler-util/src/clk.ts (limited to 'packages/taler-util/src') diff --git a/packages/taler-util/src/clk.ts b/packages/taler-util/src/clk.ts new file mode 100644 index 000000000..d172eed48 --- /dev/null +++ b/packages/taler-util/src/clk.ts @@ -0,0 +1,620 @@ +/* + This file is part of GNU Taler + (C) 2019 GNUnet e.V. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import process from "process"; +import path from "path"; +import readline from "readline"; + +export namespace clk { + class Converter {} + + export const INT = new Converter(); + export const STRING: Converter = new Converter(); + + export interface OptionArgs { + help?: string; + default?: T; + onPresentHandler?: (v: T) => void; + } + + export interface ArgumentArgs { + metavar?: string; + help?: string; + default?: T; + } + + export interface SubcommandArgs { + help?: string; + } + + export interface FlagArgs { + help?: string; + } + + export interface ProgramArgs { + help?: string; + } + + interface ArgumentDef { + name: string; + conv: Converter; + args: ArgumentArgs; + required: boolean; + } + + interface SubcommandDef { + commandGroup: CommandGroup; + name: string; + args: SubcommandArgs; + } + + type ActionFn = (x: TG) => void; + + type SubRecord = { + [Y in S]: { [X in N]: V }; + }; + + interface OptionDef { + name: string; + flagspec: string[]; + /** + * Converter, only present for options, not for flags. + */ + conv?: Converter; + args: OptionArgs; + isFlag: boolean; + required: boolean; + } + + function splitOpt(opt: string): { key: string; value?: string } { + const idx = opt.indexOf("="); + if (idx == -1) { + return { key: opt }; + } + return { key: opt.substring(0, idx), value: opt.substring(idx + 1) }; + } + + function formatListing(key: string, value?: string): string { + const res = " " + key; + if (!value) { + return res; + } + if (res.length >= 25) { + return res + "\n" + " " + value; + } else { + return res.padEnd(24) + " " + value; + } + } + + export class CommandGroup { + private shortOptions: { [name: string]: OptionDef } = {}; + private longOptions: { [name: string]: OptionDef } = {}; + private subcommandMap: { [name: string]: SubcommandDef } = {}; + private subcommands: SubcommandDef[] = []; + private options: OptionDef[] = []; + private arguments: ArgumentDef[] = []; + + private myAction?: ActionFn; + + constructor( + private argKey: string, + private name: string | null, + private scArgs: SubcommandArgs, + ) {} + + action(f: ActionFn): void { + if (this.myAction) { + throw Error("only one action supported per command"); + } + this.myAction = f; + } + + requiredOption( + name: N, + flagspec: string[], + conv: Converter, + args: OptionArgs = {}, + ): CommandGroup> { + const def: OptionDef = { + args: args, + conv: conv, + flagspec: flagspec, + isFlag: false, + required: true, + name: name as string, + }; + this.options.push(def); + for (const flag of flagspec) { + if (flag.startsWith("--")) { + const flagname = flag.substring(2); + this.longOptions[flagname] = def; + } else if (flag.startsWith("-")) { + const flagname = flag.substring(1); + this.shortOptions[flagname] = def; + } else { + throw Error("option must start with '-' or '--'"); + } + } + return this as any; + } + + maybeOption( + name: N, + flagspec: string[], + conv: Converter, + args: OptionArgs = {}, + ): CommandGroup> { + const def: OptionDef = { + args: args, + conv: conv, + flagspec: flagspec, + isFlag: false, + required: false, + name: name as string, + }; + this.options.push(def); + for (const flag of flagspec) { + if (flag.startsWith("--")) { + const flagname = flag.substring(2); + this.longOptions[flagname] = def; + } else if (flag.startsWith("-")) { + const flagname = flag.substring(1); + this.shortOptions[flagname] = def; + } else { + throw Error("option must start with '-' or '--'"); + } + } + return this as any; + } + + requiredArgument( + name: N, + conv: Converter, + args: ArgumentArgs = {}, + ): CommandGroup> { + const argDef: ArgumentDef = { + args: args, + conv: conv, + name: name as string, + required: true, + }; + this.arguments.push(argDef); + return this as any; + } + + maybeArgument( + name: N, + conv: Converter, + args: ArgumentArgs = {}, + ): CommandGroup> { + const argDef: ArgumentDef = { + args: args, + conv: conv, + name: name as string, + required: false, + }; + this.arguments.push(argDef); + return this as any; + } + + flag( + name: N, + flagspec: string[], + args: OptionArgs = {}, + ): CommandGroup> { + const def: OptionDef = { + args: args, + flagspec: flagspec, + isFlag: true, + required: false, + name: name as string, + }; + this.options.push(def); + for (const flag of flagspec) { + if (flag.startsWith("--")) { + const flagname = flag.substring(2); + this.longOptions[flagname] = def; + } else if (flag.startsWith("-")) { + const flagname = flag.substring(1); + this.shortOptions[flagname] = def; + } else { + throw Error("option must start with '-' or '--'"); + } + } + return this as any; + } + + subcommand( + argKey: GN, + name: string, + args: SubcommandArgs = {}, + ): CommandGroup { + const cg = new CommandGroup(argKey as string, name, args); + const def: SubcommandDef = { + commandGroup: cg, + name: name as string, + args: args, + }; + cg.flag("help", ["-h", "--help"], { + help: "Show this message and exit.", + }); + this.subcommandMap[name as string] = def; + this.subcommands.push(def); + this.subcommands = this.subcommands.sort((x1, x2) => { + const a = x1.name; + const b = x2.name; + if (a === b) { + return 0; + } else if (a < b) { + return -1; + } else { + return 1; + } + }); + return cg as any; + } + + printHelp(progName: string, parents: CommandGroup[]): void { + let usageSpec = ""; + for (const p of parents) { + usageSpec += (p.name ?? progName) + " "; + if (p.arguments.length >= 1) { + usageSpec += " "; + } + } + usageSpec += (this.name ?? progName) + " "; + if (this.subcommands.length != 0) { + usageSpec += "COMMAND "; + } + for (const a of this.arguments) { + const argName = a.args.metavar ?? a.name; + usageSpec += `<${argName}> `; + } + usageSpec = usageSpec.trimRight(); + console.log(`Usage: ${usageSpec}`); + if (this.scArgs.help) { + console.log(); + console.log(this.scArgs.help); + } + if (this.options.length != 0) { + console.log(); + console.log("Options:"); + for (const opt of this.options) { + let optSpec = opt.flagspec.join(", "); + if (!opt.isFlag) { + optSpec = optSpec + "=VALUE"; + } + console.log(formatListing(optSpec, opt.args.help)); + } + } + + if (this.subcommands.length != 0) { + console.log(); + console.log("Commands:"); + for (const subcmd of this.subcommands) { + console.log(formatListing(subcmd.name, subcmd.args.help)); + } + } + } + + /** + * Run the (sub-)command with the given command line parameters. + */ + run( + progname: string, + parents: CommandGroup[], + unparsedArgs: string[], + parsedArgs: any, + ): void { + let posArgIndex = 0; + let argsTerminated = false; + let i; + let foundSubcommand: CommandGroup | undefined = undefined; + const myArgs: any = (parsedArgs[this.argKey] = {}); + const foundOptions: { [name: string]: boolean } = {}; + const currentName = this.name ?? progname; + for (i = 0; i < unparsedArgs.length; i++) { + const argVal = unparsedArgs[i]; + if (argsTerminated == false) { + if (argVal === "--") { + argsTerminated = true; + continue; + } + if (argVal.startsWith("--")) { + const opt = argVal.substring(2); + const r = splitOpt(opt); + const d = this.longOptions[r.key]; + if (!d) { + console.error( + `error: unknown option '--${r.key}' for ${currentName}`, + ); + process.exit(-1); + throw Error("not reached"); + } + if (d.isFlag) { + if (r.value !== undefined) { + console.error(`error: flag '--${r.key}' does not take a value`); + process.exit(-1); + throw Error("not reached"); + } + foundOptions[d.name] = true; + myArgs[d.name] = true; + } else { + if (r.value === undefined) { + if (i === unparsedArgs.length - 1) { + console.error(`error: option '--${r.key}' needs an argument`); + process.exit(-1); + throw Error("not reached"); + } + myArgs[d.name] = unparsedArgs[i + 1]; + i++; + } else { + myArgs[d.name] = r.value; + } + foundOptions[d.name] = true; + } + continue; + } + if (argVal.startsWith("-") && argVal != "-") { + const optShort = argVal.substring(1); + for (let si = 0; si < optShort.length; si++) { + const chr = optShort[si]; + const opt = this.shortOptions[chr]; + if (!opt) { + console.error(`error: option '-${chr}' not known`); + process.exit(-1); + } + if (opt.isFlag) { + myArgs[opt.name] = true; + foundOptions[opt.name] = true; + } else { + if (si == optShort.length - 1) { + if (i === unparsedArgs.length - 1) { + console.error(`error: option '-${chr}' needs an argument`); + process.exit(-1); + throw Error("not reached"); + } else { + myArgs[opt.name] = unparsedArgs[i + 1]; + i++; + } + } else { + myArgs[opt.name] = optShort.substring(si + 1); + } + foundOptions[opt.name] = true; + break; + } + } + continue; + } + } + if (this.subcommands.length != 0) { + const subcmd = this.subcommandMap[argVal]; + if (!subcmd) { + console.error(`error: unknown command '${argVal}'`); + process.exit(-1); + throw Error("not reached"); + } + foundSubcommand = subcmd.commandGroup; + break; + } else { + const d = this.arguments[posArgIndex]; + if (!d) { + console.error(`error: too many arguments for ${currentName}`); + process.exit(-1); + throw Error("not reached"); + } + myArgs[d.name] = unparsedArgs[i]; + posArgIndex++; + } + } + + if (parsedArgs[this.argKey].help) { + this.printHelp(progname, parents); + process.exit(0); + throw Error("not reached"); + } + + for (let i = posArgIndex; i < this.arguments.length; i++) { + const d = this.arguments[i]; + if (d.required) { + if (d.args.default !== undefined) { + myArgs[d.name] = d.args.default; + } else { + console.error( + `error: missing positional argument '${d.name}' for ${currentName}`, + ); + process.exit(-1); + throw Error("not reached"); + } + } + } + + for (const option of this.options) { + if (option.isFlag == false && option.required == true) { + if (!foundOptions[option.name]) { + if (option.args.default !== undefined) { + myArgs[option.name] = option.args.default; + } else { + const name = option.flagspec.join(","); + console.error(`error: missing option '${name}'`); + process.exit(-1); + throw Error("not reached"); + } + } + } + } + + for (const option of this.options) { + const ph = option.args.onPresentHandler; + if (ph && foundOptions[option.name]) { + ph(myArgs[option.name]); + } + } + + if (foundSubcommand) { + foundSubcommand.run( + progname, + Array.prototype.concat(parents, [this]), + unparsedArgs.slice(i + 1), + parsedArgs, + ); + } else if (this.myAction) { + let r; + try { + r = this.myAction(parsedArgs); + } catch (e) { + console.error(`An error occurred while running ${currentName}`); + console.error(e); + process.exit(1); + } + Promise.resolve(r).catch((e) => { + console.error(`An error occurred while running ${currentName}`); + console.error(e); + process.exit(1); + }); + } else { + this.printHelp(progname, parents); + process.exit(-1); + throw Error("not reached"); + } + } + } + + export class Program { + private mainCommand: CommandGroup; + + constructor(argKey: string, args: ProgramArgs = {}) { + this.mainCommand = new CommandGroup(argKey, null, { + help: args.help, + }); + this.mainCommand.flag("help", ["-h", "--help"], { + help: "Show this message and exit.", + }); + } + + run(): void { + const args = process.argv; + if (args.length < 2) { + console.error( + "Error while parsing command line arguments: not enough arguments", + ); + process.exit(-1); + } + const progname = path.basename(args[1]); + const rest = args.slice(2); + + this.mainCommand.run(progname, [], rest, {}); + } + + subcommand( + argKey: GN, + name: string, + args: SubcommandArgs = {}, + ): CommandGroup { + const cmd = this.mainCommand.subcommand(argKey, name as string, args); + return cmd as any; + } + + requiredOption( + name: N, + flagspec: string[], + conv: Converter, + args: OptionArgs = {}, + ): Program> { + this.mainCommand.requiredOption(name, flagspec, conv, args); + return this as any; + } + + maybeOption( + name: N, + flagspec: string[], + conv: Converter, + args: OptionArgs = {}, + ): Program> { + this.mainCommand.maybeOption(name, flagspec, conv, args); + return this as any; + } + + /** + * Add a flag (option without value) to the program. + */ + flag( + name: N, + flagspec: string[], + args: OptionArgs = {}, + ): Program> { + this.mainCommand.flag(name, flagspec, args); + return this as any; + } + + /** + * Add a required positional argument to the program. + */ + requiredArgument( + name: N, + conv: Converter, + args: ArgumentArgs = {}, + ): Program> { + this.mainCommand.requiredArgument(name, conv, args); + return this as any; + } + + /** + * Add an optional argument to the program. + */ + maybeArgument( + name: N, + conv: Converter, + args: ArgumentArgs = {}, + ): Program> { + this.mainCommand.maybeArgument(name, conv, args); + return this as any; + } + + action(f: ActionFn): void { + this.mainCommand.action(f); + } + } + + export type GetArgType = T extends Program + ? AT + : T extends CommandGroup + ? AT + : any; + + export function program( + argKey: PN, + args: ProgramArgs = {}, + ): Program { + return new Program(argKey as string, args); + } + + export function prompt(question: string): Promise { + const stdinReadline = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + return new Promise((resolve, reject) => { + stdinReadline.question(question, (res) => { + resolve(res); + stdinReadline.close(); + }); + }); + } +} diff --git a/packages/taler-util/src/index.node.ts b/packages/taler-util/src/index.node.ts index 018b4767f..bd59f320a 100644 --- a/packages/taler-util/src/index.node.ts +++ b/packages/taler-util/src/index.node.ts @@ -21,3 +21,4 @@ initNodePrng(); export * from "./index.js"; export * from "./talerconfig.js"; export * from "./globbing/minimatch.js"; +export { clk } from "./clk.js"; -- cgit v1.2.3 From fdc36b4fb75201e23023a583b8eebd05b1f24f77 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 2 Nov 2021 17:02:04 +0100 Subject: anastasis-core: compute upload fees --- packages/anastasis-core/src/index.ts | 60 +++++++++++++++++++++++----- packages/anastasis-core/src/reducer-types.ts | 3 +- packages/taler-util/src/amounts.ts | 3 +- packages/taler-util/src/time.ts | 14 +++++++ 4 files changed, 68 insertions(+), 12 deletions(-) (limited to 'packages/taler-util/src') diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts index 07f8122e3..9bb4e347a 100644 --- a/packages/anastasis-core/src/index.ts +++ b/packages/anastasis-core/src/index.ts @@ -1,13 +1,19 @@ import { + AmountJson, + AmountLike, + Amounts, AmountString, buildSigPS, bytesToString, Codec, codecForAny, decodeCrock, + Duration, eddsaSign, encodeCrock, + getDurationRemaining, getRandomBytes, + getTimestampNow, hash, j2s, Logger, @@ -1051,27 +1057,62 @@ async function nextFromAuthenticationsEditing( async function updateUploadFees( state: ReducerStateBackup, ): Promise { - for (const prov of state.policy_providers ?? []) { - const info = state.authentication_providers![prov.provider_url]; - if (!("currency" in info)) { - continue; + const expiration = state.expiration; + if (!expiration) { + return { ...state }; + } + logger.info("updating upload fees"); + const feePerCurrency: Record = {}; + const coveredProviders = new Set(); + const addFee = (x: AmountLike) => { + x = Amounts.jsonifyAmount(x); + feePerCurrency[x.currency] = Amounts.add( + feePerCurrency[x.currency] ?? Amounts.getZero(x.currency), + x, + ).amount; + }; + const years = Duration.toIntegerYears(Duration.getRemaining(expiration)); + logger.info(`computing fees for ${years} years`); + for (const x of state.policies ?? []) { + for (const m of x.methods) { + const prov = state.authentication_providers![ + m.provider + ] as AuthenticationProviderStatusOk; + const authMethod = state.authentication_methods![m.authentication_method]; + if (!coveredProviders.has(m.provider)) { + const annualFee = Amounts.mult(prov.annual_fee, years).amount; + logger.info(`adding annual fee ${Amounts.stringify(annualFee)}`); + addFee(annualFee); + coveredProviders.add(m.provider); + } + for (const pm of prov.methods) { + if (pm.type === authMethod.type) { + addFee(pm.usage_fee); + break; + } + } } } - return { ...state, upload_fees: [] }; + return { + ...state, + upload_fees: Object.values(feePerCurrency).map((x) => ({ + fee: Amounts.stringify(x), + })), + }; } async function enterSecret( state: ReducerStateBackup, args: ActionArgEnterSecret, ): Promise { - return { + return updateUploadFees({ ...state, expiration: args.expiration, core_secret: { mime: args.secret.mime ?? "text/plain", value: args.secret.value, }, - }; + }); } async function nextFromChallengeSelecting( @@ -1102,11 +1143,10 @@ async function updateSecretExpiration( state: ReducerStateBackup, args: ActionArgsUpdateExpiration, ): Promise { - // FIXME: implement! - return { + return updateUploadFees({ ...state, expiration: args.expiration, - }; + }); } const backupTransitions: Record< diff --git a/packages/anastasis-core/src/reducer-types.ts b/packages/anastasis-core/src/reducer-types.ts index 03883ce17..2f0d324ae 100644 --- a/packages/anastasis-core/src/reducer-types.ts +++ b/packages/anastasis-core/src/reducer-types.ts @@ -66,6 +66,7 @@ export interface ReducerStateBackup { selected_country?: string; secret_name?: string; policies?: Policy[]; + /** * Policy providers are providers that we checked to be functional * and that are actually used in policies. @@ -82,7 +83,7 @@ export interface ReducerStateBackup { expiration?: Timestamp; - upload_fees?: AmountString[]; + upload_fees?: { fee: AmountString }[]; } export interface AuthMethod { diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts index 5a8c7f06f..41fd14bee 100644 --- a/packages/taler-util/src/amounts.ts +++ b/packages/taler-util/src/amounts.ts @@ -349,7 +349,8 @@ export class Amounts { } } - static mult(a: AmountJson, n: number): Result { + static mult(a: AmountLike, n: number): Result { + a = this.jsonifyAmount(a); if (!Number.isInteger(n)) { throw Error("amount can only be multipied by an integer"); } diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts index c0858ada6..856db8a57 100644 --- a/packages/taler-util/src/time.ts +++ b/packages/taler-util/src/time.ts @@ -69,6 +69,20 @@ export function getDurationRemaining( return { d_ms: deadline.t_ms - now.t_ms }; } +export namespace Duration { + export const getRemaining = getDurationRemaining; + export function toIntegerYears(d: Duration): number { + if (typeof d.d_ms !== "number") { + throw Error("infinite duration"); + } + return Math.ceil(d.d_ms / 1000 / 60 / 60 / 24 / 365); + } +} + +export namespace Timestamp { + export const min = timestampMin; +} + export function timestampMin(t1: Timestamp, t2: Timestamp): Timestamp { if (t1.t_ms === "never") { return { t_ms: t2.t_ms }; -- cgit v1.2.3 From ab6fd6c8c72ac674648ef66d7bcec01f7a232410 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 3 Nov 2021 13:17:57 +0100 Subject: move HTTP status codes to taler-util --- packages/taler-util/src/http-status-codes.ts | 379 +++++++++++++++++++++ packages/taler-util/src/index.ts | 1 + .../src/operations/backup/index.ts | 10 +- packages/taler-wallet-core/src/operations/pay.ts | 6 +- .../taler-wallet-core/src/operations/refresh.ts | 5 +- packages/taler-wallet-core/src/util/http.ts | 11 - 6 files changed, 390 insertions(+), 22 deletions(-) create mode 100644 packages/taler-util/src/http-status-codes.ts (limited to 'packages/taler-util/src') diff --git a/packages/taler-util/src/http-status-codes.ts b/packages/taler-util/src/http-status-codes.ts new file mode 100644 index 000000000..848839990 --- /dev/null +++ b/packages/taler-util/src/http-status-codes.ts @@ -0,0 +1,379 @@ +/** + * Hypertext Transfer Protocol (HTTP) response status codes. + * + * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes} + */ +export enum HttpStatusCode { + /** + * The server has received the request headers and the client should proceed to send the request body + * (in the case of a request for which a body needs to be sent; for example, a POST request). + * Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient. + * To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request + * and receive a 100 Continue status code in response before sending the body. The response 417 Expectation Failed indicates the request should not be continued. + */ + Continue = 100, + + /** + * The requester has asked the server to switch protocols and the server has agreed to do so. + */ + SwitchingProtocols = 101, + + /** + * A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request. + * This code indicates that the server has received and is processing the request, but no response is available yet. + * This prevents the client from timing out and assuming the request was lost. + */ + Processing = 102, + + /** + * Standard response for successful HTTP requests. + * The actual response will depend on the request method used. + * In a GET request, the response will contain an entity corresponding to the requested resource. + * In a POST request, the response will contain an entity describing or containing the result of the action. + */ + Ok = 200, + + /** + * The request has been fulfilled, resulting in the creation of a new resource. + */ + Created = 201, + + /** + * The request has been accepted for processing, but the processing has not been completed. + * The request might or might not be eventually acted upon, and may be disallowed when processing occurs. + */ + Accepted = 202, + + /** + * SINCE HTTP/1.1 + * The server is a transforming proxy that received a 200 OK from its origin, + * but is returning a modified version of the origin's response. + */ + NonAuthoritativeInformation = 203, + + /** + * The server successfully processed the request and is not returning any content. + */ + NoContent = 204, + + /** + * The server successfully processed the request, but is not returning any content. + * Unlike a 204 response, this response requires that the requester reset the document view. + */ + ResetContent = 205, + + /** + * The server is delivering only part of the resource (byte serving) due to a range header sent by the client. + * The range header is used by HTTP clients to enable resuming of interrupted downloads, + * or split a download into multiple simultaneous streams. + */ + PartialContent = 206, + + /** + * The message body that follows is an XML message and can contain a number of separate response codes, + * depending on how many sub-requests were made. + */ + MultiStatus = 207, + + /** + * The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response, + * and are not being included again. + */ + AlreadyReported = 208, + + /** + * The server has fulfilled a request for the resource, + * and the response is a representation of the result of one or more instance-manipulations applied to the current instance. + */ + ImUsed = 226, + + /** + * Indicates multiple options for the resource from which the client may choose (via agent-driven content negotiation). + * For example, this code could be used to present multiple video format options, + * to list files with different filename extensions, or to suggest word-sense disambiguation. + */ + MultipleChoices = 300, + + /** + * This and all future requests should be directed to the given URI. + */ + MovedPermanently = 301, + + /** + * This is an example of industry practice contradicting the standard. + * The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect + * (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302 + * with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 + * to distinguish between the two behaviours. However, some Web applications and frameworks + * use the 302 status code as if it were the 303. + */ + Found = 302, + + /** + * SINCE HTTP/1.1 + * The response to the request can be found under another URI using a GET method. + * When received in response to a POST (or PUT/DELETE), the client should presume that + * the server has received the data and should issue a redirect with a separate GET message. + */ + SeeOther = 303, + + /** + * Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match. + * In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy. + */ + NotModified = 304, + + /** + * SINCE HTTP/1.1 + * The requested resource is available only through a proxy, the address for which is provided in the response. + * Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle responses with this status code, primarily for security reasons. + */ + UseProxy = 305, + + /** + * No longer used. Originally meant "Subsequent requests should use the specified proxy." + */ + SwitchProxy = 306, + + /** + * SINCE HTTP/1.1 + * In this case, the request should be repeated with another URI; however, future requests should still use the original URI. + * In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. + * For example, a POST request should be repeated using another POST request. + */ + TemporaryRedirect = 307, + + /** + * The request and all future requests should be repeated using another URI. + * 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change. + * So, for example, submitting a form to a permanently redirected resource may continue smoothly. + */ + PermanentRedirect = 308, + + /** + * The server cannot or will not process the request due to an apparent client error + * (e.g., malformed request syntax, too large size, invalid request message framing, or deceptive request routing). + */ + BadRequest = 400, + + /** + * Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet + * been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the + * requested resource. See Basic access authentication and Digest access authentication. 401 semantically means + * "unauthenticated",i.e. the user does not have the necessary credentials. + */ + Unauthorized = 401, + + /** + * Reserved for future use. The original intention was that this code might be used as part of some form of digital + * cash or micro payment scheme, but that has not happened, and this code is not usually used. + * Google Developers API uses this status if a particular developer has exceeded the daily limit on requests. + */ + PaymentRequired = 402, + + /** + * The request was valid, but the server is refusing action. + * The user might not have the necessary permissions for a resource. + */ + Forbidden = 403, + + /** + * The requested resource could not be found but may be available in the future. + * Subsequent requests by the client are permissible. + */ + NotFound = 404, + + /** + * A request method is not supported for the requested resource; + * for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource. + */ + MethodNotAllowed = 405, + + /** + * The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request. + */ + NotAcceptable = 406, + + /** + * The client must first authenticate itself with the proxy. + */ + ProxyAuthenticationRequired = 407, + + /** + * The server timed out waiting for the request. + * According to HTTP specifications: + * "The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time." + */ + RequestTimeout = 408, + + /** + * Indicates that the request could not be processed because of conflict in the request, + * such as an edit conflict between multiple simultaneous updates. + */ + Conflict = 409, + + /** + * Indicates that the resource requested is no longer available and will not be available again. + * This should be used when a resource has been intentionally removed and the resource should be purged. + * Upon receiving a 410 status code, the client should not request the resource in the future. + * Clients such as search engines should remove the resource from their indices. + * Most use cases do not require clients and search engines to purge the resource, and a "404 Not Found" may be used instead. + */ + Gone = 410, + + /** + * The request did not specify the length of its content, which is required by the requested resource. + */ + LengthRequired = 411, + + /** + * The server does not meet one of the preconditions that the requester put on the request. + */ + PreconditionFailed = 412, + + /** + * The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large". + */ + PayloadTooLarge = 413, + + /** + * The URI provided was too long for the server to process. Often the result of too much data being encoded as a query-string of a GET request, + * in which case it should be converted to a POST request. + * Called "Request-URI Too Long" previously. + */ + UriTooLong = 414, + + /** + * The request entity has a media type which the server or resource does not support. + * For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format. + */ + UnsupportedMediaType = 415, + + /** + * The client has asked for a portion of the file (byte serving), but the server cannot supply that portion. + * For example, if the client asked for a part of the file that lies beyond the end of the file. + * Called "Requested Range Not Satisfiable" previously. + */ + RangeNotSatisfiable = 416, + + /** + * The server cannot meet the requirements of the Expect request-header field. + */ + ExpectationFailed = 417, + + /** + * This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol, + * and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by + * teapots requested to brew coffee. This HTTP status is used as an Easter egg in some websites, including Google.com. + */ + IAmATeapot = 418, + + /** + * The request was directed at a server that is not able to produce a response (for example because a connection reuse). + */ + MisdirectedRequest = 421, + + /** + * The request was well-formed but was unable to be followed due to semantic errors. + */ + UnprocessableEntity = 422, + + /** + * The resource that is being accessed is locked. + */ + Locked = 423, + + /** + * The request failed due to failure of a previous request (e.g., a PROPPATCH). + */ + FailedDependency = 424, + + /** + * The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. + */ + UpgradeRequired = 426, + + /** + * The origin server requires the request to be conditional. + * Intended to prevent "the 'lost update' problem, where a client + * GETs a resource's state, modifies it, and PUTs it back to the server, + * when meanwhile a third party has modified the state on the server, leading to a conflict." + */ + PreconditionRequired = 428, + + /** + * The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes. + */ + TooManyRequests = 429, + + /** + * The server is unwilling to process the request because either an individual header field, + * or all the header fields collectively, are too large. + */ + RequestHeaderFieldsTooLarge = 431, + + /** + * A server operator has received a legal demand to deny access to a resource or to a set of resources + * that includes the requested resource. The code 451 was chosen as a reference to the novel Fahrenheit 451. + */ + UnavailableForLegalReasons = 451, + + /** + * A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. + */ + InternalServerError = 500, + + /** + * The server either does not recognize the request method, or it lacks the ability to fulfill the request. + * Usually this implies future availability (e.g., a new feature of a web-service API). + */ + NotImplemented = 501, + + /** + * The server was acting as a gateway or proxy and received an invalid response from the upstream server. + */ + BadGateway = 502, + + /** + * The server is currently unavailable (because it is overloaded or down for maintenance). + * Generally, this is a temporary state. + */ + ServiceUnavailable = 503, + + /** + * The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. + */ + GatewayTimeout = 504, + + /** + * The server does not support the HTTP protocol version used in the request + */ + HttpVersionNotSupported = 505, + + /** + * Transparent content negotiation for the request results in a circular reference. + */ + VariantAlsoNegotiates = 506, + + /** + * The server is unable to store the representation needed to complete the request. + */ + InsufficientStorage = 507, + + /** + * The server detected an infinite loop while processing the request. + */ + LoopDetected = 508, + + /** + * Further extensions to the request are required for the server to fulfill it. + */ + NotExtended = 510, + + /** + * The client needs to authenticate to gain network access. + * Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used + * to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot). + */ + NetworkAuthenticationRequired = 511, +} diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts index 4ad752954..c42e5e66a 100644 --- a/packages/taler-util/src/index.ts +++ b/packages/taler-util/src/index.ts @@ -22,6 +22,7 @@ export * from "./url.js"; export { fnutil } from "./fnutils.js"; export * from "./kdf.js"; export * from "./talerCrypto.js"; +export * from "./http-status-codes.js"; export { randomBytes, secretbox, diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index 913ffcb2e..3f4c02274 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -40,6 +40,7 @@ import { ConfirmPayResultType, durationFromSpec, getTimestampNow, + HttpStatusCode, j2s, Logger, notEmpty, @@ -84,7 +85,6 @@ import { } from "../../db.js"; import { guardOperationException } from "../../errors.js"; import { - HttpResponseStatus, readSuccessResponseJsonOrThrow, readTalerErrorResponse, } from "../../util/http.js"; @@ -317,7 +317,7 @@ async function runBackupCycleForProvider( logger.trace(`sync response status: ${resp.status}`); - if (resp.status === HttpResponseStatus.NotModified) { + if (resp.status === HttpStatusCode.NotModified) { await ws.db .mktx((x) => ({ backupProvider: x.backupProviders })) .runReadWrite(async (tx) => { @@ -335,7 +335,7 @@ async function runBackupCycleForProvider( return; } - if (resp.status === HttpResponseStatus.PaymentRequired) { + if (resp.status === HttpStatusCode.PaymentRequired) { logger.trace("payment required for backup"); logger.trace(`headers: ${j2s(resp.headers)}`); const talerUri = resp.headers.get("taler"); @@ -396,7 +396,7 @@ async function runBackupCycleForProvider( return; } - if (resp.status === HttpResponseStatus.NoContent) { + if (resp.status === HttpStatusCode.NoContent) { await ws.db .mktx((x) => ({ backupProviders: x.backupProviders })) .runReadWrite(async (tx) => { @@ -415,7 +415,7 @@ async function runBackupCycleForProvider( return; } - if (resp.status === HttpResponseStatus.Conflict) { + if (resp.status === HttpStatusCode.Conflict) { logger.info("conflicting backup found"); const backupEnc = new Uint8Array(await resp.bytes()); const backupConfig = await provideBackupState(ws); diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index 8fad55994..a42480f40 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -53,6 +53,7 @@ import { Logger, URL, getDurationRemaining, + HttpStatusCode, } from "@gnu-taler/taler-util"; import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util"; import { @@ -89,7 +90,6 @@ import { } from "../db.js"; import { getHttpResponseErrorDetails, - HttpResponseStatus, readSuccessResponseJsonOrErrorCode, readSuccessResponseJsonOrThrow, readTalerErrorResponse, @@ -1222,7 +1222,7 @@ async function submitPay( }; } - if (resp.status === HttpResponseStatus.BadRequest) { + if (resp.status === HttpStatusCode.BadRequest) { const errDetails = await readUnexpectedResponseDetails(resp); logger.warn("unexpected 400 response for /pay"); logger.warn(j2s(errDetails)); @@ -1242,7 +1242,7 @@ async function submitPay( throw new OperationFailedAndReportedError(errDetails); } - if (resp.status === HttpResponseStatus.Conflict) { + if (resp.status === HttpStatusCode.Conflict) { const err = await readTalerErrorResponse(resp); if ( err.code === diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 144514e1c..d727bd06f 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see */ -import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util"; +import { encodeCrock, getRandomBytes, HttpStatusCode } from "@gnu-taler/taler-util"; import { CoinRecord, CoinSourceType, @@ -40,7 +40,6 @@ import { import { AmountJson, Amounts } from "@gnu-taler/taler-util"; import { amountToPretty } from "@gnu-taler/taler-util"; import { - HttpResponseStatus, readSuccessResponseJsonOrThrow, readUnexpectedResponseDetails, } from "../util/http.js"; @@ -377,7 +376,7 @@ async function refreshMelt( }); }); - if (resp.status === HttpResponseStatus.NotFound) { + if (resp.status === HttpStatusCode.NotFound) { const errDetails = await readUnexpectedResponseDetails(resp); await ws.db .mktx((x) => ({ diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-wallet-core/src/util/http.ts index d01f2ee42..0556d2274 100644 --- a/packages/taler-wallet-core/src/util/http.ts +++ b/packages/taler-wallet-core/src/util/http.ts @@ -59,17 +59,6 @@ export interface HttpRequestOptions { body?: string | ArrayBuffer | ArrayBufferView; } -export enum HttpResponseStatus { - Ok = 200, - NoContent = 204, - Gone = 210, - NotModified = 304, - BadRequest = 400, - PaymentRequired = 402, - NotFound = 404, - Conflict = 409, -} - /** * Headers, roughly modeled after the fetch API's headers object. */ -- cgit v1.2.3 From 6fc3aa0b31021f02d10e5a2efb12879aa64774fd Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 5 Nov 2021 13:10:15 +0100 Subject: taler-wallet: implement log level, use new wallet for every benchmark iteration --- packages/taler-util/src/logging.ts | 101 ++++++++++++++++++++++++++++++-- packages/taler-wallet-cli/src/bench1.ts | 17 +++--- packages/taler-wallet-cli/src/index.ts | 8 +++ 3 files changed, 112 insertions(+), 14 deletions(-) (limited to 'packages/taler-util/src') diff --git a/packages/taler-util/src/logging.ts b/packages/taler-util/src/logging.ts index 0037d95a3..8b9de1ab0 100644 --- a/packages/taler-util/src/logging.ts +++ b/packages/taler-util/src/logging.ts @@ -23,6 +23,47 @@ const isNode = typeof process.release !== "undefined" && process.release.name === "node"; +export enum LogLevel { + Trace = "trace", + Message = "message", + Info = "info", + Warn = "warn", + Error = "error", + None = "none", +} + +export let globalLogLevel = LogLevel.Info; + +export function setGlobalLogLevelFromString(logLevelStr: string) { + let level: LogLevel; + switch (logLevelStr.toLowerCase()) { + case "trace": + level = LogLevel.Trace; + break; + case "info": + level = LogLevel.Info; + break; + case "warn": + case "warning": + level = LogLevel.Warn; + break; + case "error": + level = LogLevel.Error; + break; + case "none": + level = LogLevel.None; + break; + default: + if (isNode) { + process.stderr.write(`Invalid log level, defaulting to WARNING`); + } else { + console.warn(`Invalid log level, defaulting to WARNING`); + } + level = LogLevel.Warn; + } + globalLogLevel = level; +} + function writeNodeLog( message: any, tag: string, @@ -57,21 +98,60 @@ export class Logger { constructor(private tag: string) {} shouldLogTrace() { - // FIXME: Implement logic to check loglevel - return true; + switch (globalLogLevel) { + case LogLevel.Trace: + return true; + case LogLevel.Message: + case LogLevel.Info: + case LogLevel.Warn: + case LogLevel.Error: + case LogLevel.None: + return false; + } } shouldLogInfo() { - // FIXME: Implement logic to check loglevel - return true; + switch (globalLogLevel) { + case LogLevel.Trace: + case LogLevel.Message: + case LogLevel.Info: + return true; + case LogLevel.Warn: + case LogLevel.Error: + case LogLevel.None: + return false; + } } shouldLogWarn() { - // FIXME: Implement logic to check loglevel - return true; + switch (globalLogLevel) { + case LogLevel.Trace: + case LogLevel.Message: + case LogLevel.Info: + case LogLevel.Warn: + return true; + case LogLevel.Error: + case LogLevel.None: + return false; + } + } + + shouldLogError() { + switch (globalLogLevel) { + case LogLevel.Trace: + case LogLevel.Message: + case LogLevel.Info: + case LogLevel.Warn: + case LogLevel.Error: + case LogLevel.None: + return false; + } } info(message: string, ...args: any[]): void { + if (!this.shouldLogInfo()) { + return; + } if (isNode) { writeNodeLog(message, this.tag, "INFO", args); } else { @@ -83,6 +163,9 @@ export class Logger { } warn(message: string, ...args: any[]): void { + if (!this.shouldLogWarn()) { + return; + } if (isNode) { writeNodeLog(message, this.tag, "WARN", args); } else { @@ -94,6 +177,9 @@ export class Logger { } error(message: string, ...args: any[]): void { + if (!this.shouldLogError()) { + return; + } if (isNode) { writeNodeLog(message, this.tag, "ERROR", args); } else { @@ -105,6 +191,9 @@ export class Logger { } trace(message: any, ...args: any[]): void { + if (!this.shouldLogTrace()) { + return; + } if (isNode) { writeNodeLog(message, this.tag, "TRACE", args); } else { diff --git a/packages/taler-wallet-cli/src/bench1.ts b/packages/taler-wallet-cli/src/bench1.ts index 4a2651f36..ec0430d8d 100644 --- a/packages/taler-wallet-cli/src/bench1.ts +++ b/packages/taler-wallet-cli/src/bench1.ts @@ -40,16 +40,17 @@ export async function runBench1(configJson: any): Promise { const b1conf = codecForBench1Config().decode(configJson); const myHttpLib = new NodeHttpLib(); - const wallet = await getDefaultNodeWallet({ - // No persistent DB storage. - persistentStoragePath: undefined, - httpLib: myHttpLib, - }); - await wallet.client.call(WalletApiOperation.InitWallet, {}); const numIter = b1conf.iterations ?? 1; for (let i = 0; i < numIter; i++) { + const wallet = await getDefaultNodeWallet({ + // No persistent DB storage. + persistentStoragePath: undefined, + httpLib: myHttpLib, + }); + await wallet.client.call(WalletApiOperation.InitWallet, {}); + await wallet.client.call(WalletApiOperation.WithdrawFakebank, { amount: "TESTKUDOS:10", bank: b1conf.bank, @@ -68,9 +69,9 @@ export async function runBench1(configJson: any): Promise { await wallet.runTaskLoop({ stopWhenDone: true, }); - } - wallet.stop(); + wallet.stop(); + } } /** diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 142e98e7c..71431b5eb 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -43,6 +43,8 @@ import { Configuration, decodeCrock, rsaBlind, + LogLevel, + setGlobalLogLevelFromString, } from "@gnu-taler/taler-util"; import { NodeHttpLib, @@ -161,6 +163,12 @@ export const walletCli = clk setDangerousTimetravel(x / 1000); }, }) + .maybeOption("log", ["-L", "--log"], clk.STRING, { + help: "configure log level (NONE, ..., TRACE)", + onPresentHandler: (x) => { + setGlobalLogLevelFromString(x); + }, + }) .maybeOption("inhibit", ["--inhibit"], clk.STRING, { help: "Inhibit running certain operations, useful for debugging and testing.", -- cgit v1.2.3