diff --git a/example/typescript/proxy_instance.ts b/example/typescript/proxy_instance.ts new file mode 100644 index 0000000..60a669e --- /dev/null +++ b/example/typescript/proxy_instance.ts @@ -0,0 +1,21 @@ +import Mastodon, { Instance, ProxyConfig } from 'megalodon' + +declare var process: { + env: { + PROXY_HOST: string + PROXY_PORT: number + PROXY_PROTOCOL: string + } +} + +const BASE_URL: string = 'http://mastodon.social' + +const proxy: ProxyConfig = { + host: process.env.PROXY_HOST, + port: process.env.PROXY_PORT, + protocol: process.env.PROXY_PROTOCOL +} + +Mastodon.get('/api/v1/instance', {}, BASE_URL, proxy).then(res => { + console.log(res) +}) diff --git a/example/typescript/proxy_timeline.ts b/example/typescript/proxy_timeline.ts new file mode 100644 index 0000000..cdfd514 --- /dev/null +++ b/example/typescript/proxy_timeline.ts @@ -0,0 +1,26 @@ +import Mastodon, { Status, Response, ProxyConfig } from 'megalodon' + +declare var process: { + env: { + MASTODON_ACCESS_TOKEN: string + PROXY_HOST: string + PROXY_PORT: number + PROXY_PROTOCOL: string + } +} + +const BASE_URL: string = 'https://mastodon.social' + +const access_token: string = process.env.MASTODON_ACCESS_TOKEN + +const proxy: ProxyConfig = { + host: process.env.PROXY_HOST, + port: process.env.PROXY_PORT, + protocol: process.env.PROXY_PROTOCOL +} + +const client = new Mastodon(access_token, BASE_URL + '/api/v1', 'megalodon', proxy) + +client.get>('/timelines/public').then((resp: Response>) => { + console.log(resp.data) +}) diff --git a/example/typescript/timeline.ts b/example/typescript/timeline.ts index aa0a83a..026933f 100644 --- a/example/typescript/timeline.ts +++ b/example/typescript/timeline.ts @@ -12,6 +12,6 @@ const access_token: string = process.env.MASTODON_ACCESS_TOKEN const client = new Mastodon(access_token, BASE_URL + '/api/v1') -client.get>('/timelines/home').then((resp: Response>) => { +client.get>('/timelines/public').then((resp: Response>) => { console.log(resp.data) }) diff --git a/example/typescript/yarn.lock b/example/typescript/yarn.lock index f76cbfa..6bd7cf5 100644 --- a/example/typescript/yarn.lock +++ b/example/typescript/yarn.lock @@ -48,6 +48,13 @@ dependencies: "@types/node" "*" +agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + ajv@^5.3.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" @@ -153,6 +160,13 @@ debug@=3.1.0: dependencies: ms "2.0.0" +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" @@ -173,6 +187,18 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -267,6 +293,14 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +https-proxy-agent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz#0106efa5d63d6d6f3ab87c999fa4877a3fd1ff97" + integrity sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + is-buffer@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" @@ -337,6 +371,7 @@ log4js@^5.2.2: "@types/request" "^2.47.0" "@types/ws" "^6.0.1" axios "^0.18.1" + https-proxy-agent "^3.0.0" moment "^2.24.0" oauth "^0.9.15" request "^2.87.0" diff --git a/package.json b/package.json index be73e66..0a83c55 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@types/request": "^2.47.0", "@types/ws": "^6.0.1", "axios": "^0.18.1", + "https-proxy-agent": "^3.0.0", "moment": "^2.24.0", "oauth": "^0.9.15", "request": "^2.87.0", diff --git a/src/index.ts b/src/index.ts index ddcd3bb..2d24b96 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import Mastodon, { MegalodonInstance } from './mastodon' +import Mastodon, { MegalodonInstance, ProxyConfig } from './mastodon' import StreamListener from './stream_listener' import WebSocket from './web_socket' import Response from './response' @@ -43,6 +43,7 @@ export { MegalodonInstance, RequestCanceledError, isCancel, + ProxyConfig, /** * Entities */ diff --git a/src/mastodon.ts b/src/mastodon.ts index 34e8079..04de19d 100644 --- a/src/mastodon.ts +++ b/src/mastodon.ts @@ -1,5 +1,6 @@ import { OAuth2 } from 'oauth' import axios, { AxiosResponse, CancelTokenSource } from 'axios' +import HttpsProxyAgent from 'https-proxy-agent' import StreamListener from './stream_listener' import WebSocket from './web_socket' @@ -26,6 +27,16 @@ export interface MegalodonInstance { socket(path: string, strea: string): WebSocket } +export type ProxyConfig = { + host: string + port: number + auth?: { + username: string + password: string + } + protocol: string +} + /** * Mastodon API client. * @@ -40,16 +51,23 @@ export default class Mastodon implements MegalodonInstance { private baseUrl: string private userAgent: string private cancelTokenSource: CancelTokenSource + private proxyConfig: ProxyConfig | false = false /** * @param accessToken access token from OAuth2 authorization * @param baseUrl hostname or base URL */ - constructor(accessToken: string, baseUrl = DEFAULT_URL, userAgent = DEFAULT_UA) { + constructor( + accessToken: string, + baseUrl: string = DEFAULT_URL, + userAgent: string = DEFAULT_UA, + proxyConfig: ProxyConfig | false = false + ) { this.accessToken = accessToken this.baseUrl = baseUrl this.userAgent = userAgent this.cancelTokenSource = axios.CancelToken.source() + this.proxyConfig = proxyConfig } /** @@ -211,26 +229,46 @@ export default class Mastodon implements MegalodonInstance { * @param params Query parameters * @param baseUrl base URL of the target */ - public static async get(path: string, params = {}, baseUrl = DEFAULT_URL): Promise> { + public static async get( + path: string, + params = {}, + baseUrl = DEFAULT_URL, + proxyConfig: ProxyConfig | false = false + ): Promise> { const apiUrl = baseUrl - return axios - .get(apiUrl + path, { - params - }) - .then((resp: AxiosResponse) => { - const res: Response = { - data: resp.data, - status: resp.status, - statusText: resp.statusText, - headers: resp.headers - } - return res + let options = { + params: params + } + if (proxyConfig) { + options = Object.assign(options, { + httpsAgent: this._proxyAgent(proxyConfig) }) + } + return axios.get(apiUrl + path, options).then((resp: AxiosResponse) => { + const res: Response = { + data: resp.data, + status: resp.status, + statusText: resp.statusText, + headers: resp.headers + } + return res + }) } - private static async _post(path: string, params = {}, baseUrl = DEFAULT_URL): Promise> { + private static async _post( + path: string, + params = {}, + baseUrl = DEFAULT_URL, + proxyConfig: ProxyConfig | false = false + ): Promise> { + let options = {} + if (proxyConfig) { + options = Object.assign(options, { + httpsAgent: this._proxyAgent(proxyConfig) + }) + } const apiUrl = baseUrl - return axios.post(apiUrl + path, params).then((resp: AxiosResponse) => { + return axios.post(apiUrl + path, params, options).then((resp: AxiosResponse) => { const res: Response = { data: resp.data, status: resp.status, @@ -247,14 +285,20 @@ export default class Mastodon implements MegalodonInstance { * @param params Query parameters */ public async get(path: string, params = {}): Promise> { - return axios - .get(this.baseUrl + path, { - cancelToken: this.cancelTokenSource.token, - headers: { - Authorization: `Bearer ${this.accessToken}` - }, - params + let options = { + cancelToken: this.cancelTokenSource.token, + headers: { + Authorization: `Bearer ${this.accessToken}` + }, + params: params + } + if (this.proxyConfig) { + options = Object.assign(options, { + httpsAgent: Mastodon._proxyAgent(this.proxyConfig) }) + } + return axios + .get(this.baseUrl + path, options) .catch((err: Error) => { if (axios.isCancel(err)) { throw new RequestCanceledError(err.message) @@ -279,13 +323,19 @@ export default class Mastodon implements MegalodonInstance { * @param params Form data. If you want to post file, please use FormData() */ public async put(path: string, params = {}): Promise> { - return axios - .put(this.baseUrl + path, params, { - cancelToken: this.cancelTokenSource.token, - headers: { - Authorization: `Bearer ${this.accessToken}` - } + let options = { + cancelToken: this.cancelTokenSource.token, + headers: { + Authorization: `Bearer ${this.accessToken}` + } + } + if (this.proxyConfig) { + options = Object.assign(options, { + httpsAgent: Mastodon._proxyAgent(this.proxyConfig) }) + } + return axios + .put(this.baseUrl + path, params, options) .catch((err: Error) => { if (axios.isCancel(err)) { throw new RequestCanceledError(err.message) @@ -310,13 +360,19 @@ export default class Mastodon implements MegalodonInstance { * @param params Form data. If you want to post file, please use FormData() */ public async patch(path: string, params = {}): Promise> { - return axios - .patch(this.baseUrl + path, params, { - cancelToken: this.cancelTokenSource.token, - headers: { - Authorization: `Bearer ${this.accessToken}` - } + let options = { + cancelToken: this.cancelTokenSource.token, + headers: { + Authorization: `Bearer ${this.accessToken}` + } + } + if (this.proxyConfig) { + options = Object.assign(options, { + httpsAgent: Mastodon._proxyAgent(this.proxyConfig) }) + } + return axios + .patch(this.baseUrl + path, params, options) .catch((err: Error) => { if (axios.isCancel(err)) { throw new RequestCanceledError(err.message) @@ -341,22 +397,26 @@ export default class Mastodon implements MegalodonInstance { * @param params Form data */ public async post(path: string, params = {}): Promise> { - return axios - .post(this.baseUrl + path, params, { - cancelToken: this.cancelTokenSource.token, - headers: { - Authorization: `Bearer ${this.accessToken}` - } - }) - .then((resp: AxiosResponse) => { - const res: Response = { - data: resp.data, - status: resp.status, - statusText: resp.statusText, - headers: resp.headers - } - return res + let options = { + cancelToken: this.cancelTokenSource.token, + headers: { + Authorization: `Bearer ${this.accessToken}` + } + } + if (this.proxyConfig) { + options = Object.assign(options, { + httpsAgent: Mastodon._proxyAgent(this.proxyConfig) }) + } + return axios.post(this.baseUrl + path, params, options).then((resp: AxiosResponse) => { + const res: Response = { + data: resp.data, + status: resp.status, + statusText: resp.statusText, + headers: resp.headers + } + return res + }) } /** @@ -365,14 +425,20 @@ export default class Mastodon implements MegalodonInstance { * @param params Form data */ public async del(path: string, params = {}): Promise> { - return axios - .delete(this.baseUrl + path, { - cancelToken: this.cancelTokenSource.token, - data: params, - headers: { - Authorization: `Bearer ${this.accessToken}` - } + let options = { + cancelToken: this.cancelTokenSource.token, + data: params, + headers: { + Authorization: `Bearer ${this.accessToken}` + } + } + if (this.proxyConfig) { + options = Object.assign(options, { + httpsAgent: Mastodon._proxyAgent(this.proxyConfig) }) + } + return axios + .delete(this.baseUrl + path, options) .catch((err: Error) => { if (axios.isCancel(err)) { throw new RequestCanceledError(err.message) @@ -437,4 +503,13 @@ export default class Mastodon implements MegalodonInstance { }) return streaming } + + private static _proxyAgent(proxyConfig: ProxyConfig): HttpsProxyAgent { + let auth = '' + if (proxyConfig.auth) { + auth = `${proxyConfig.auth.username}:${proxyConfig.auth.password}@` + } + const agent = new HttpsProxyAgent(`${proxyConfig.protocol}://${auth}${proxyConfig.host}:${proxyConfig.port}`) + return agent + } } diff --git a/yarn.lock b/yarn.lock index 5dac188..d2b9226 100644 --- a/yarn.lock +++ b/yarn.lock @@ -496,6 +496,13 @@ acorn@^6.0.1, acorn@^6.0.7: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.1.tgz#3ed8422d6dec09e6121cc7a843ca86a330a86b51" integrity sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q== +agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: version "6.10.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" @@ -1001,7 +1008,7 @@ debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: dependencies: ms "2.0.0" -debug@^3.2.6: +debug@^3.1.0, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -1172,6 +1179,18 @@ es-to-primitive@^1.2.0: is-date-object "^1.0.1" is-symbol "^1.0.2" +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -1817,6 +1836,14 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +https-proxy-agent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz#0106efa5d63d6d6f3ab87c999fa4877a3fd1ff97" + integrity sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"