Merge pull request #1112 from cosmos/allow-sending-headers

Create HttpEndpoint interface to allow sending custom headers
This commit is contained in:
Simon Warta 2022-04-13 08:02:16 +02:00 committed by GitHub
commit ae06012a15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 285 additions and 27 deletions

View File

@ -145,9 +145,13 @@ jobs:
- run:
name: Start socket server
command: ./scripts/socketserver/start.sh
- run:
name: Start http server
command: ./scripts/httpserver/start.sh
- run:
name: Run tests
environment:
HTTPSERVER_ENABLED: 1
TENDERMINT_ENABLED: 1
SOCKETSERVER_ENABLED: 1
SKIP_BUILD: 1
@ -166,6 +170,7 @@ jobs:
name: Run CLI examples
working_directory: packages/cli
environment:
HTTPSERVER_ENABLED: 1
TENDERMINT_ENABLED: 1
SOCKETSERVER_ENABLED: 1
SKIP_BUILD: 1
@ -177,6 +182,7 @@ jobs:
- run:
name: Stop chains
command: |
./scripts/httpserver/stop.sh
./scripts/socketserver/stop.sh
./scripts/tendermint/all_stop.sh
./scripts/<< parameters.simapp >>/stop.sh
@ -265,8 +271,12 @@ jobs:
- run:
name: Start socket server
command: ./scripts/socketserver/start.sh
- run:
name: Start http server
command: ./scripts/httpserver/start.sh
- run:
environment:
HTTPSERVER_ENABLED: 1
SIMAPP42_ENABLED: 1
SLOW_SIMAPP42_ENABLED: 1
TENDERMINT_ENABLED: 1
@ -285,6 +295,7 @@ jobs:
name: Run CLI examples
working_directory: packages/cli
environment:
HTTPSERVER_ENABLED: 1
SIMAPP42_ENABLED: 1
SLOW_SIMAPP42_ENABLED: 1
TENDERMINT_ENABLED: 1
@ -295,6 +306,7 @@ jobs:
- run:
name: Stop chains
command: |
./scripts/httpserver/stop.sh
./scripts/socketserver/stop.sh
./scripts/tendermint/all_stop.sh
./scripts/simapp42/stop.sh
@ -376,8 +388,12 @@ jobs:
- run:
name: Start socket server
command: ./scripts/socketserver/start.sh
- run:
name: Start http server
command: ./scripts/httpserver/start.sh
- run:
environment:
HTTPSERVER_ENABLED: 1
SIMAPP42_ENABLED: 1
SLOW_SIMAPP42_ENABLED: 1
TENDERMINT_ENABLED: 1
@ -388,6 +404,7 @@ jobs:
- run:
name: Stop chains
command: |
./scripts/httpserver/stop.sh
./scripts/socketserver/stop.sh
./scripts/tendermint/all_stop.sh
./scripts/simapp42/stop.sh
@ -468,8 +485,12 @@ jobs:
- run:
name: Start socket server
command: ./scripts/socketserver/start.sh
- run:
name: Start http server
command: ./scripts/httpserver/start.sh
- run:
environment:
HTTPSERVER_ENABLED: 1
SIMAPP42_ENABLED: 1
SLOW_SIMAPP42_ENABLED: 1
TENDERMINT_ENABLED: 1
@ -483,6 +504,7 @@ jobs:
- run:
name: Stop chains
command: |
./scripts/httpserver/stop.sh
./scripts/socketserver/stop.sh
./scripts/tendermint/all_stop.sh
./scripts/simapp42/stop.sh

View File

@ -14,7 +14,16 @@ and this project adheres to
- @cosmjs/faucet: Docker build image is 90 % smaller now (from 500 MB to 50 MB)
due to build system optimizations ([#1120], [#1121]).
- @cosmjs/cosmwasm-stargate: `CosmWasmClient.connect` and
`SigningCosmWasmClient.connectWithSigner` now accept custom HTTP headers
([#1007])
- @cosmjs/stargate: `StargateClient.connect` and
`SigningStargateClient.connectWithSigner` now accept custom HTTP headers
([#1007])
- @cosmjs/tendermint-rpc: `Tendermint34Client.connect` now accepts custom HTTP
headers ([#1007]).
[#1007]: https://github.com/cosmos/cosmjs/issues/1007
[#1110]: https://github.com/cosmos/cosmjs/issues/1110
[#1120]: https://github.com/cosmos/cosmjs/pull/1120
[#1121]: https://github.com/cosmos/cosmjs/pull/1121

View File

@ -64,13 +64,19 @@ export TENDERMINT_ENABLED=1
./scripts/socketserver/start.sh
export SOCKETSERVER_ENABLED=1
# Start Http server
./scripts/httpserver/start.sh
export HTTPSERVER_ENABLED=1
# now more tests are running that were marked as "pending" before
yarn test
# And at the end of the day
unset HTTPSERVER_ENABLED
unset SOCKETSERVER_ENABLED
unset TENDERMINT_ENABLED
unset LAUNCHPAD_ENABLED
./scripts/httpserver/stop.sh
./scripts/socketserver/stop.sh
./scripts/tendermint/all_stop.sh
./scripts/launchpad/stop.sh
@ -100,6 +106,7 @@ order to avoid conflicts. Here is an overview of the ports used:
| 1319 | wasmd LCD API | Manual Stargate debugging |
| 4444 | socketserver | @cosmjs/sockets tests |
| 4445 | socketserver slow | @cosmjs/sockets tests |
| 5555 | httpserver | @cosmjs/tendermint-rpc tests |
| 9090 | simapp gRPC | Manual Stargate debugging |
| 11134 | Tendermint 0.34 RPC | @cosmjs/tendermint-rpc tests |
| 26658 | simapp Tendermint RPC | Stargate client tests |

View File

@ -0,0 +1,21 @@
import { StargateClient } from "@cosmjs/stargate";
// Network config
const rpcEndpoint = {
// Note: we removed the /status patch from the examples because we use the HTTP POST API
url: "https://cosmoshub-4--rpc--full.datahub.figment.io/",
headers: {
"Authorization": "5195ebb0bfb7f0fe5c43409240c8b2c4",
}
};
// Setup client
const client = await StargateClient.connect(rpcEndpoint);
// Get some data
const chainId = await client.getChainId();
console.log("Chain ID:", chainId);
const balance = await client.getAllBalances("cosmos1ey69r37gfxvxg62sh4r0ktpuc46pzjrmz29g45");
console.log("Balances:", balance);
client.disconnect();

View File

@ -17,3 +17,6 @@ if [ -n "${SIMAPP42_ENABLED:-}" ]; then
yarn node ./bin/cosmjs-cli --init examples/stargate.ts --code "process.exit(0)"
yarn node ./bin/cosmjs-cli --init examples/simulate.ts --code "process.exit(0)"
fi
# Disabled as this requires internet access
# yarn node ./bin/cosmjs-cli --init examples/figment.ts --code "process.exit(0)"

View File

@ -23,7 +23,7 @@ import {
TimeoutError,
TxExtension,
} from "@cosmjs/stargate";
import { Tendermint34Client, toRfc3339WithNanoseconds } from "@cosmjs/tendermint-rpc";
import { HttpEndpoint, Tendermint34Client, toRfc3339WithNanoseconds } from "@cosmjs/tendermint-rpc";
import { assert, sleep } from "@cosmjs/utils";
import {
CodeInfoResponse,
@ -89,7 +89,7 @@ export class CosmWasmClient {
private readonly codesCache = new Map<number, CodeDetails>();
private chainId: string | undefined;
public static async connect(endpoint: string): Promise<CosmWasmClient> {
public static async connect(endpoint: string | HttpEndpoint): Promise<CosmWasmClient> {
const tmClient = await Tendermint34Client.connect(endpoint);
return new CosmWasmClient(tmClient);
}

View File

@ -29,3 +29,6 @@ export {
SigningCosmWasmClientOptions,
UploadResult,
} from "./signingcosmwasmclient";
// Re-exported because this is part of the CosmWasmClient/SigningCosmWasmClient APIs
export { HttpEndpoint } from "@cosmjs/tendermint-rpc";

View File

@ -30,7 +30,7 @@ import {
SignerData,
StdFee,
} from "@cosmjs/stargate";
import { Tendermint34Client } from "@cosmjs/tendermint-rpc";
import { HttpEndpoint, Tendermint34Client } from "@cosmjs/tendermint-rpc";
import { assert, assertDefined } from "@cosmjs/utils";
import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx";
import { MsgDelegate, MsgUndelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx";
@ -172,7 +172,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
private readonly gasPrice: GasPrice | undefined;
public static async connectWithSigner(
endpoint: string,
endpoint: string | HttpEndpoint,
signer: OfflineSigner,
options: SigningCosmWasmClientOptions = {},
): Promise<SigningCosmWasmClient> {

View File

@ -122,3 +122,6 @@ export {
} from "./stargateclient";
export { StdFee } from "@cosmjs/amino";
export { Coin, coin, coins, makeCosmoshubPath, parseCoins } from "@cosmjs/proto-signing";
// Re-exported because this is part of the StargateClient/SigningStargateClient APIs
export { HttpEndpoint } from "@cosmjs/tendermint-rpc";

View File

@ -12,7 +12,7 @@ import {
Registry,
TxBodyEncodeObject,
} from "@cosmjs/proto-signing";
import { Tendermint34Client } from "@cosmjs/tendermint-rpc";
import { HttpEndpoint, Tendermint34Client } from "@cosmjs/tendermint-rpc";
import { assert, assertDefined } from "@cosmjs/utils";
import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin";
import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx";
@ -116,7 +116,7 @@ export class SigningStargateClient extends StargateClient {
private readonly gasPrice: GasPrice | undefined;
public static async connectWithSigner(
endpoint: string,
endpoint: string | HttpEndpoint,
signer: OfflineSigner,
options: SigningStargateClientOptions = {},
): Promise<SigningStargateClient> {

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { toHex } from "@cosmjs/encoding";
import { Uint53 } from "@cosmjs/math";
import { Tendermint34Client, toRfc3339WithNanoseconds } from "@cosmjs/tendermint-rpc";
import { HttpEndpoint, Tendermint34Client, toRfc3339WithNanoseconds } from "@cosmjs/tendermint-rpc";
import { sleep } from "@cosmjs/utils";
import { MsgData } from "cosmjs-types/cosmos/base/abci/v1beta1/abci";
import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin";
@ -149,7 +149,7 @@ export class StargateClient {
private readonly accountParser: AccountParser;
public static async connect(
endpoint: string,
endpoint: string | HttpEndpoint,
options: StargateClientOptions = {},
): Promise<StargateClient> {
const tmClient = await Tendermint34Client.connect(endpoint);

View File

@ -12,6 +12,10 @@ export {
toRfc3339WithNanoseconds,
toSeconds,
} from "./dates";
export {
// This type is part of the Tendermint34Client.connect API
HttpEndpoint,
} from "./rpcclients";
export { HttpClient, WebsocketClient } from "./rpcclients"; // TODO: Why do we export those outside of this package?
export {
AbciInfoRequest,

View File

@ -1,27 +1,71 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { createJsonRpcRequest } from "../jsonrpc";
import { defaultInstance } from "../testutil.spec";
import { http, HttpClient } from "./httpclient";
function pendingWithoutTendermint(): void {
if (!process.env.TENDERMINT_ENABLED) {
pending("Set TENDERMINT_ENABLED to enable tendermint rpc tests");
pending("Set TENDERMINT_ENABLED to enable Tendermint RPC tests");
}
}
function pendingWithoutHttpServer(): void {
if (!process.env.HTTPSERVER_ENABLED) {
pending("Set HTTPSERVER_ENABLED to enable HTTP tests");
}
}
const tendermintUrl = defaultInstance.url;
const echoUrl = "http://localhost:5555/echo_headers";
describe("http", () => {
it("can send a health request", async () => {
pendingWithoutTendermint();
const response = await http("POST", `http://${tendermintUrl}`, createJsonRpcRequest("health"));
const response = await http("POST", `http://${tendermintUrl}`, undefined, createJsonRpcRequest("health"));
expect(response).toEqual(jasmine.objectContaining({ jsonrpc: "2.0" }));
});
it("errors for non-open port", async () => {
await expectAsync(
http("POST", `http://localhost:56745`, createJsonRpcRequest("health")),
http("POST", `http://localhost:56745`, undefined, createJsonRpcRequest("health")),
).toBeRejectedWithError(/(ECONNREFUSED|Failed to fetch)/i);
});
it("can send custom headers", async () => {
pendingWithoutHttpServer();
// Without custom headers
const response1 = await http("POST", echoUrl, undefined, createJsonRpcRequest("health"));
expect(response1).toEqual({
request_headers: jasmine.objectContaining({
// Basic headers from http client
Accept: jasmine.any(String),
"Content-Length": jasmine.any(String),
"Content-Type": "application/json",
Host: jasmine.any(String),
"User-Agent": jasmine.any(String),
}),
});
// With custom headers
const response2 = await http(
"POST",
echoUrl,
{ foo: "bar123", Authorization: "Basic Z3Vlc3Q6bm9QYXNzMTIz" },
createJsonRpcRequest("health"),
);
expect(response2).toEqual({
request_headers: jasmine.objectContaining({
// Basic headers from http client
"Content-Length": jasmine.any(String),
"Content-Type": "application/json",
Host: jasmine.any(String),
"User-Agent": jasmine.any(String),
// Custom headers
foo: "bar123",
Authorization: "Basic Z3Vlc3Q6bm9QYXNzMTIz",
}),
});
});
});
describe("HttpClient", () => {

View File

@ -25,23 +25,58 @@ function filterBadStatus(res: any): any {
* For some reason, fetch does not complain about missing server-side CORS support.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function http(method: "POST", url: string, request?: any): Promise<any> {
export async function http(
method: "POST",
url: string,
headers: Record<string, string> | undefined,
request?: any,
): Promise<any> {
if (typeof fetch !== "undefined") {
const body = request ? JSON.stringify(request) : undefined;
return fetch(url, { method: method, body: body })
const settings = {
method: method,
body: request ? JSON.stringify(request) : undefined,
headers: {
// eslint-disable-next-line @typescript-eslint/naming-convention
"Content-Type": "application/json",
...headers,
},
};
return fetch(url, settings)
.then(filterBadStatus)
.then((res: any) => res.json());
} else {
return axios.request({ url: url, method: method, data: request }).then((res) => res.data);
return axios
.request({ url: url, method: method, data: request, headers: headers })
.then((res) => res.data);
}
}
export interface HttpEndpoint {
/**
* The URL of the HTTP endpoint.
*
* For POST APIs like Tendermint RPC in CosmJS,
* this is without the method specific paths (e.g. https://cosmoshub-4--rpc--full.datahub.figment.io/)
*/
readonly url: string;
/**
* HTTP headers that are sent with every request, such as authorization information.
*/
readonly headers: Record<string, string>;
}
export class HttpClient implements RpcClient {
protected readonly url: string;
protected readonly headers: Record<string, string> | undefined;
public constructor(url: string) {
// accept host.name:port and assume http protocol
this.url = hasProtocol(url) ? url : "http://" + url;
public constructor(endpoint: string | HttpEndpoint) {
if (typeof endpoint === "string") {
// accept host.name:port and assume http protocol
this.url = hasProtocol(endpoint) ? endpoint : "http://" + endpoint;
} else {
this.url = endpoint.url;
this.headers = endpoint.headers;
}
}
public disconnect(): void {
@ -49,7 +84,7 @@ export class HttpClient implements RpcClient {
}
public async execute(request: JsonRpcRequest): Promise<JsonRpcSuccessResponse> {
const response = parseJsonRpcResponse(await http("POST", this.url, request));
const response = parseJsonRpcResponse(await http("POST", this.url, this.headers, request));
if (isJsonRpcErrorResponse(response)) {
throw new Error(JSON.stringify(response.error));
}

View File

@ -1,5 +1,5 @@
// This folder contains Tendermint-specific RPC clients
export { HttpClient } from "./httpclient";
export { HttpClient, HttpEndpoint } from "./httpclient";
export { instanceOfRpcStreamingClient, RpcClient, RpcStreamingClient, SubscriptionEvent } from "./rpcclient";
export { WebsocketClient } from "./websocketclient";

View File

@ -6,7 +6,7 @@ import { WebsocketClient } from "./websocketclient";
function pendingWithoutTendermint(): void {
if (!process.env.TENDERMINT_ENABLED) {
pending("Set TENDERMINT_ENABLED to enable tendermint rpc tests");
pending("Set TENDERMINT_ENABLED to enable Tendermint RPC tests");
}
}

View File

@ -9,7 +9,7 @@ import { WebsocketClient } from "./websocketclient";
function pendingWithoutTendermint(): void {
if (!process.env.TENDERMINT_ENABLED) {
pending("Set TENDERMINT_ENABLED to enable tendermint rpc tests");
pending("Set TENDERMINT_ENABLED to enable Tendermint RPC tests");
}
}

View File

@ -4,6 +4,7 @@ import { Stream } from "xstream";
import { createJsonRpcRequest } from "../jsonrpc";
import {
HttpClient,
HttpEndpoint,
instanceOfRpcStreamingClient,
RpcClient,
SubscriptionEvent,
@ -19,10 +20,14 @@ export class Tendermint34Client {
*
* Uses HTTP when the URL schema is http or https. Uses WebSockets otherwise.
*/
public static async connect(url: string): Promise<Tendermint34Client> {
const useHttp = url.startsWith("http://") || url.startsWith("https://");
const rpcClient = useHttp ? new HttpClient(url) : new WebsocketClient(url);
return Tendermint34Client.create(rpcClient);
public static async connect(endpoint: string | HttpEndpoint): Promise<Tendermint34Client> {
if (typeof endpoint === "object") {
return Tendermint34Client.create(new HttpClient(endpoint));
} else {
const useHttp = endpoint.startsWith("http://") || endpoint.startsWith("https://");
const rpcClient = useHttp ? new HttpClient(endpoint) : new WebsocketClient(endpoint);
return Tendermint34Client.create(rpcClient);
}
}
/**

View File

@ -16,7 +16,10 @@ module.exports = [
filename: "tests.js",
},
plugins: [
new webpack.EnvironmentPlugin({ TENDERMINT_ENABLED: "" }),
new webpack.EnvironmentPlugin({
HTTPSERVER_ENABLED: "",
TENDERMINT_ENABLED: "",
}),
new webpack.ProvidePlugin({
Buffer: ["buffer", "Buffer"],
}),

View File

@ -0,0 +1,7 @@
FROM python:3.9-alpine
WORKDIR /usr/src/app
COPY echo.py ./
ENTRYPOINT ["python", "./echo.py"]

53
scripts/httpserver/echo.py Executable file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env python3
#pylint:disable=missing-docstring,invalid-name
import argparse
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import sys
HOST = "0.0.0.0"
def log(data):
print(data, flush=True)
class CORSRequestHandler(BaseHTTPRequestHandler):
def end_headers(self):
self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Headers", "*")
BaseHTTPRequestHandler.end_headers(self)
def do_OPTIONS(self):
self.send_response(200)
self.end_headers()
def do_GET(self):
"""Respond to a GET request."""
if self.path == "/echo_headers":
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.send_header('Content-type', 'application/json')
self.end_headers()
body = {
"request_headers": dict(self.headers)
}
self.wfile.write(json.dumps(body, sort_keys=True).encode())
else:
self.send_response(404)
self.wfile.write("404. Try /echo_headers".encode())
def do_POST(self):
self.do_GET()
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("--port",
help="Port to listen on",
type=int,
default=5555)
args = parser.parse_args()
httpd = HTTPServer((HOST, args.port), CORSRequestHandler)
log("Starting server at {}:{}".format(HOST, args.port))
httpd.serve_forever()
log("Running now.")

31
scripts/httpserver/start.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
set -o errexit -o nounset -o pipefail
command -v shellcheck >/dev/null && shellcheck "$0"
# Please keep this in sync with the Ports overview in HACKING.md
DEFAULT_PORT_GUEST="5555"
DEFAULT_PORT_HOST="5555"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HTTPSERVER_DIR=$(mktemp -d "${TMPDIR:-/tmp}/httpserver.XXXXXXXXX")
export HTTPSERVER_DIR
echo "HTTPSERVER_DIR = $HTTPSERVER_DIR"
IMAGE_NAME="httpserver:local"
CONTAINER_NAME="httpserver"
LOGFILE_DEFAULT="${HTTPSERVER_DIR}/httpserver_$DEFAULT_PORT_HOST.log"
docker build -t "$IMAGE_NAME" "$SCRIPT_DIR"
docker run --rm \
--user="$UID" \
--name "$CONTAINER_NAME" \
-p "$DEFAULT_PORT_HOST:$DEFAULT_PORT_GUEST" \
"$IMAGE_NAME" \
>"$LOGFILE_DEFAULT" &
# Debug start
sleep 3
cat "$LOGFILE_DEFAULT"

8
scripts/httpserver/stop.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
set -o errexit -o nounset -o pipefail
command -v shellcheck >/dev/null && shellcheck "$0"
CONTAINER_NAME="httpserver"
echo "Killing socketserver containers ..."
docker container kill "$CONTAINER_NAME"