chore: 🔧 enable tor proxy config (#197)

This commit is contained in:
Anthony Potdevin 2020-12-25 21:42:19 +01:00 • committed by GitHub
parent f1c9e4cf41
commit 5bcfed1b24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 208 additions and 75 deletions

1
.env
View file

@ -10,6 +10,7 @@
# Server Configs
# -----------
# LOG_LEVEL='info'
# TOR_PROXY_SERVER=socks://127.0.0.1:9050
# -----------
# Interface Configs

View file

@ -114,6 +114,7 @@ You can define some environment variables that ThunderHub can start with. To do
# Server Configs
# -----------
LOG_LEVEL = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly' # Default: 'info'
TOR_PROXY_SERVER='socks://127.0.0.1:9050' # Default: ''
# -----------
# Interface Configs
@ -131,6 +132,16 @@ DISABLE_LNMARKETS = true | false # Default: false
NO_VERSION_CHECK = true | false # Default: false
```
### TOR Requests
ThunderHub connects to different external services for example to fetch BOS scores, BTC/fiat prices and BTC blockchain fees. Normally they are done through clearnet but you can configure a TOR proxy server so that they are all proxied through TOR.
You need to add the following parameter into your `.env` file with your TOR endpoint:
```bash
TOR_PROXY_SERVER='socks://your.tor.endpoint' # i.e. 'socks://127.0.0.1:9050'
```
### SSO Account
You can define an account to work with SSO cookie authentication by adding the following parameters in the `.env` file:

View file

@ -35,6 +35,7 @@ module.exports = withBundleAnalyzer({
serverRuntimeConfig: {
nodeEnv: process.env.NODE_ENV || 'development',
logLevel: process.env.LOG_LEVEL || 'info',
torProxy: process.env.TOR_PROXY_SERVER || '',
...ssoEnv,
...accountConfig,
...sslEnv,

43
package-lock.json generated
View file

@ -16439,7 +16439,8 @@
"extract-files": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz",
"integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ=="
"integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==",
"dev": true
},
"extsprintf": {
"version": "1.3.0",
@ -17552,6 +17553,7 @@
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-3.4.0.tgz",
"integrity": "sha512-acrTzidSlwAj8wBNO7Q/UQHS8T+z5qRGquCQRv9J1InwR01BBWV9ObnoE+JS5nCCEj8wSGS0yrDXVDoRiKZuOg==",
"dev": true,
"requires": {
"cross-fetch": "^3.0.6",
"extract-files": "^9.0.0",
@ -18303,6 +18305,11 @@
}
}
},
"ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo="
},
"ip-regex": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
@ -26674,6 +26681,11 @@
"integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
"dev": true
},
"smart-buffer": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz",
"integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw=="
},
"snapdragon": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@ -26776,6 +26788,35 @@
}
}
},
"socks": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.5.1.tgz",
"integrity": "sha512-oZCsJJxapULAYJaEYBSzMcz8m3jqgGrHaGhkmU/o/PQfFWYWxkAaA0UMGImb6s6tEXfKi959X6VJjMMQ3P6TTQ==",
"requires": {
"ip": "^1.1.5",
"smart-buffer": "^4.1.0"
}
},
"socks-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz",
"integrity": "sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA==",
"requires": {
"agent-base": "6",
"debug": "4",
"socks": "^2.3.3"
},
"dependencies": {
"debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"requires": {
"ms": "2.1.2"
}
}
}
},
"source-list-map": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",

View file

@ -59,7 +59,6 @@
"graphql": "^15.4.0",
"graphql-iso-date": "^3.6.1",
"graphql-rate-limit": "^2.0.1",
"graphql-request": "^3.4.0",
"intersection-observer": "^0.12.0",
"js-cookie": "^2.2.1",
"js-yaml": "^3.14.1",
@ -70,6 +69,7 @@
"lodash.merge": "^4.6.2",
"lodash.omit": "^4.5.0",
"next": "^10.0.3",
"node-fetch": "^2.6.1",
"numeral": "^2.0.6",
"qrcode.react": "^1.0.0",
"react": "^17.0.1",
@ -87,6 +87,7 @@
"react-toastify": "^6.2.0",
"react-tooltip": "^4.2.11",
"secp256k1": "^4.0.2",
"socks-proxy-agent": "^5.0.0",
"styled-components": "^5.2.1",
"styled-react-modal": "^2.0.1",
"styled-theming": "^2.2.0",

View file

@ -1,10 +1,11 @@
import { logger } from 'server/helpers/logger';
import { appUrls } from 'server/utils/appUrls';
import { fetchWithProxy } from 'server/utils/fetch';
export const BoltzApi = {
getPairs: async () => {
try {
const response = await fetch(`${appUrls.boltz}/getpairs`);
const response = await fetchWithProxy(`${appUrls.boltz}/getpairs`);
return await response.json();
} catch (error) {
logger.error('Error getting pairs from Boltz: %o', error);
@ -13,7 +14,9 @@ export const BoltzApi = {
},
getFeeEstimations: async () => {
try {
const response = await fetch(`${appUrls.boltz}/getfeeestimation`);
const response = await fetchWithProxy(
`${appUrls.boltz}/getfeeestimation`
);
return await response.json();
} catch (error) {
logger.error('Error getting fee estimations from Boltz: %o', error);
@ -23,7 +26,7 @@ export const BoltzApi = {
getSwapStatus: async (id: string) => {
try {
const body = { id };
const response = await fetch(`${appUrls.boltz}/swapstatus`, {
const response = await fetchWithProxy(`${appUrls.boltz}/swapstatus`, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
@ -48,7 +51,7 @@ export const BoltzApi = {
preimageHash,
claimPublicKey,
};
const response = await fetch(`${appUrls.boltz}/createswap`, {
const response = await fetchWithProxy(`${appUrls.boltz}/createswap`, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
@ -65,11 +68,14 @@ export const BoltzApi = {
currency: 'BTC',
transactionHex,
};
const response = await fetch(`${appUrls.boltz}/broadcasttransaction`, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
});
const response = await fetchWithProxy(
`${appUrls.boltz}/broadcasttransaction`,
{
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
}
);
return await response.json();
} catch (error) {
logger.error('Error broadcasting transaction from Boltz: %o', error);

View file

@ -1,10 +1,11 @@
import { logger } from 'server/helpers/logger';
import { appUrls } from 'server/utils/appUrls';
import { fetchWithProxy } from 'server/utils/fetch';
export const LnMarketsApi = {
getUser: async (token: string) => {
try {
const response = await fetch(`${appUrls.lnMarkets}/user`, {
const response = await fetchWithProxy(`${appUrls.lnMarkets}/user`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
@ -21,15 +22,18 @@ export const LnMarketsApi = {
},
getDepositInvoice: async (token: string, amount: number) => {
try {
const response = await fetch(`${appUrls.lnMarkets}/user/deposit`, {
method: 'post',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'content-type': 'application/json',
},
body: JSON.stringify({ amount, unit: 'sat' }),
});
const response = await fetchWithProxy(
`${appUrls.lnMarkets}/user/deposit`,
{
method: 'post',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'content-type': 'application/json',
},
body: JSON.stringify({ amount, unit: 'sat' }),
}
);
return await response.json();
} catch (error) {
logger.error(
@ -41,15 +45,18 @@ export const LnMarketsApi = {
},
withdraw: async (token: string, amount: number, invoice: string) => {
try {
const response = await fetch(`${appUrls.lnMarkets}/user/withdraw`, {
method: 'post',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'content-type': 'application/json',
},
body: JSON.stringify({ amount, unit: 'sat', invoice }),
});
const response = await fetchWithProxy(
`${appUrls.lnMarkets}/user/withdraw`,
{
method: 'post',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'content-type': 'application/json',
},
body: JSON.stringify({ amount, unit: 'sat', invoice }),
}
);
return await response.json();
} catch (error) {
logger.error(`Error withdrawing from LnMarkets. Error: %o`, error);

View file

@ -11,6 +11,7 @@ import * as bip32 from 'bip32';
import * as secp256k1 from 'secp256k1';
import { appUrls } from 'server/utils/appUrls';
import { decodeLnUrl } from 'src/utils/url';
import { fetchWithProxy } from 'server/utils/fetch';
import { to } from './async';
import { logger } from './logger';
@ -108,7 +109,7 @@ export const getLnMarketsAuth = async (
// Get a new lnUrl from LnMarkets
try {
const response = await fetch(`${appUrls.lnMarkets}/lnurl/a/c`);
const response = await fetchWithProxy(`${appUrls.lnMarkets}/lnurl/a/c`);
const json = await response.json();
logger.debug('Get lnUrl from LnMarkets response: %o', json);
@ -128,7 +129,7 @@ export const getLnMarketsAuth = async (
// Try to authenticate with lnMarkets
try {
const response = await fetch(`${finalUrl}&jwt=true&expiry=3600`);
const response = await fetchWithProxy(`${finalUrl}&jwt=true&expiry=3600`);
const json = await response.json();
logger.debug('LnUrlAuth response: %o', json);

View file

@ -19,7 +19,10 @@ describe('Bitcoin Resolvers', () => {
expect(res.errors).toBe(undefined);
expect(fetchMock).toBeCalledWith('https://blockchain.info/ticker');
expect(fetchMock).toBeCalledWith(
'https://blockchain.info/ticker',
undefined
);
expect(res).toMatchSnapshot();
});
test('failure', async () => {
@ -54,7 +57,8 @@ describe('Bitcoin Resolvers', () => {
expect(res.errors).toBe(undefined);
expect(fetchMock).toBeCalledWith(
'https://mempool.space/api/v1/fees/recommended'
'https://mempool.space/api/v1/fees/recommended',
undefined
);
expect(res).toMatchSnapshot();
});

View file

@ -2,6 +2,7 @@ import { ContextType } from 'server/types/apiTypes';
import { logger } from 'server/helpers/logger';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { appUrls } from 'server/utils/appUrls';
import { fetchWithProxy } from 'server/utils/fetch';
export const bitcoinResolvers = {
Query: {
@ -13,7 +14,7 @@ export const bitcoinResolvers = {
await requestLimiter(context.ip, 'bitcoinPrice');
try {
const response = await fetch(appUrls.ticker);
const response = await fetchWithProxy(appUrls.ticker);
const json = await response.json();
return JSON.stringify(json);
@ -30,7 +31,7 @@ export const bitcoinResolvers = {
await requestLimiter(context.ip, 'bitcoinFee');
try {
const response = await fetch(appUrls.fees);
const response = await fetchWithProxy(appUrls.fees);
const json = await response.json();
if (json) {

View file

@ -3,6 +3,7 @@ import { ContextType } from 'server/types/apiTypes';
import { toWithError } from 'server/helpers/async';
import { appUrls } from 'server/utils/appUrls';
import { logger } from 'server/helpers/logger';
import { fetchWithProxy } from 'server/utils/fetch';
export const githubResolvers = {
Query: {
@ -13,7 +14,9 @@ export const githubResolvers = {
) => {
await requestLimiter(context.ip, 'getLnPay');
const [response, error] = await toWithError(fetch(appUrls.github));
const [response, error] = await toWithError(
fetchWithProxy(appUrls.github)
);
if (error || !response) {
logger.debug('Unable to get latest github version');

View file

@ -10,6 +10,7 @@ import {
PayInvoiceType,
} from 'server/types/ln-service.types';
import { lnAuthUrlGenerator } from 'server/helpers/lnAuth';
import { fetchWithProxy } from 'server/utils/fetch';
type LnUrlPayResponseType = {
pr?: string;
@ -74,7 +75,7 @@ export const lnUrlResolvers = {
const finalUrl = await lnAuthUrlGenerator(url, lnd);
try {
const response = await fetch(finalUrl);
const response = await fetchWithProxy(finalUrl);
const json = await response.json();
logger.debug('LnUrlAuth response: %o', json);
@ -97,7 +98,7 @@ export const lnUrlResolvers = {
await requestLimiter(context.ip, 'fetchLnUrl');
try {
const response = await fetch(url);
const response = await fetchWithProxy(url);
const json = await response.json();
if (json.status === 'ERROR') {
@ -136,7 +137,7 @@ export const lnUrlResolvers = {
};
try {
const response = await fetch(finalUrl);
const response = await fetchWithProxy(finalUrl);
lnServiceResponse = await response.json();
if (lnServiceResponse.status === 'ERROR') {
@ -215,7 +216,7 @@ export const lnUrlResolvers = {
const finalUrl = `${callback}?k1=${k1}&pr=${info.request}`;
try {
const response = await fetch(finalUrl);
const response = await fetchWithProxy(finalUrl);
const json = await response.json();
logger.debug('LnUrlWithdraw response: %o', json);

View file

@ -1,20 +1,19 @@
import { ContextType } from 'server/types/apiTypes';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { toWithError } from 'server/helpers/async';
import { appUrls } from 'server/utils/appUrls';
import { request, gql } from 'graphql-request';
import { logger } from 'server/helpers/logger';
import { GraphQLError } from 'graphql';
import { appConstants } from 'server/utils/appConstants';
import cookieLib from 'cookie';
import { graphqlFetchWithProxy } from 'server/utils/fetch';
const getBaseCanConnectQuery = gql`
const getBaseCanConnectQuery = `
{
hello
}
`;
const getBaseInfoQuery = gql`
const getBaseInfoQuery = `
{
getInfo {
lastBosUpdate
@ -24,7 +23,7 @@ const getBaseInfoQuery = gql`
}
`;
const getBaseNodesQuery = gql`
const getBaseNodesQuery = `
{
getNodes {
_id
@ -35,7 +34,7 @@ const getBaseNodesQuery = gql`
}
`;
const getBasePointsQuery = gql`
const getBasePointsQuery = `
{
getPoints {
alias
@ -44,7 +43,7 @@ const getBasePointsQuery = gql`
}
`;
const createBaseInvoiceQuery = gql`
const createBaseInvoiceQuery = `
mutation CreateInvoice($amount: Int!) {
createInvoice(amount: $amount) {
request
@ -53,7 +52,7 @@ const createBaseInvoiceQuery = gql`
}
`;
const createBaseTokenInvoiceQuery = gql`
const createBaseTokenInvoiceQuery = `
mutation CreateTokenInvoice($days: Int) {
createTokenInvoice(days: $days) {
request
@ -62,7 +61,7 @@ const createBaseTokenInvoiceQuery = gql`
}
`;
const createThunderPointsQuery = gql`
const createThunderPointsQuery = `
mutation CreatePoints(
$id: String!
$alias: String!
@ -73,13 +72,13 @@ const createThunderPointsQuery = gql`
}
`;
const createBaseTokenQuery = gql`
const createBaseTokenQuery = `
mutation CreateBaseToken($id: String!) {
createBaseToken(id: $id)
}
`;
const getBosScoresQuery = gql`
const getBosScoresQuery = `
{
getBosScores {
updated
@ -94,7 +93,7 @@ const getBosScoresQuery = gql`
}
`;
const getBosNodeScoresQuery = gql`
const getBosNodeScoresQuery = `
query GetNodeScores($publicKey: String!, $token: String!) {
getNodeScores(publicKey: $publicKey, token: $token) {
alias
@ -111,8 +110,9 @@ export const tbaseResolvers = {
getBaseInfo: async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'getBaseInfo');
const [data, error] = await toWithError(
request(appUrls.tbase, getBaseInfoQuery)
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
getBaseInfoQuery
);
if (error || !data?.getInfo) {
@ -129,8 +129,9 @@ export const tbaseResolvers = {
): Promise<boolean> => {
await requestLimiter(context.ip, 'getBaseCanConnect');
const [data, error] = await toWithError(
request(appUrls.tbase, getBaseCanConnectQuery)
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
getBaseCanConnectQuery
);
if (error || !data?.hello) return false;
@ -149,11 +150,13 @@ export const tbaseResolvers = {
await requestLimiter(ip, 'getBosNodeScores');
const [data, error] = await toWithError(
request(appUrls.tbase, getBosNodeScoresQuery, {
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
getBosNodeScoresQuery,
{
publicKey,
token: tokenAuth,
})
}
);
if (error) {
@ -166,12 +169,14 @@ export const tbaseResolvers = {
getBosScores: async (_: undefined, __: any, context: ContextType) => {
await requestLimiter(context.ip, 'getBosScores');
const [data, error] = await toWithError(
request(appUrls.tbase, getBosScoresQuery)
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
getBosScoresQuery
);
if (error || !data?.getBosScores) {
logger.error('Error getting BOS scores');
logger.error(error);
throw new GraphQLError('ErrorGettingBosScores');
}
@ -180,8 +185,9 @@ export const tbaseResolvers = {
getBaseNodes: async (_: undefined, __: any, context: ContextType) => {
await requestLimiter(context.ip, 'getBaseNodes');
const [data, error] = await toWithError(
request(appUrls.tbase, getBaseNodesQuery)
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
getBaseNodesQuery
);
if (error || !data?.getNodes) return [];
@ -193,8 +199,9 @@ export const tbaseResolvers = {
getBasePoints: async (_: undefined, __: any, context: ContextType) => {
await requestLimiter(context.ip, 'getBasePoints');
const [data, error] = await toWithError(
request(appUrls.tbase, getBasePointsQuery)
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
getBasePointsQuery
);
if (error || !data?.getPoints) return [];
@ -212,8 +219,10 @@ export const tbaseResolvers = {
if (!params?.amount) return '';
const [data, error] = await toWithError(
request(appUrls.tbase, createBaseInvoiceQuery, params)
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
createBaseInvoiceQuery,
params
);
if (error) return null;
@ -228,8 +237,10 @@ export const tbaseResolvers = {
) => {
await requestLimiter(ip, 'createBaseInvoice');
const [data, error] = await toWithError(
request(appUrls.tbase, createBaseTokenQuery, { id })
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
createBaseTokenQuery,
{ id }
);
if (error || !data?.createBaseToken) {
@ -278,8 +289,9 @@ export const tbaseResolvers = {
) => {
await requestLimiter(context.ip, 'createBaseTokenInvoice');
const [data, error] = await toWithError(
request(appUrls.tbase, createBaseTokenInvoiceQuery)
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
createBaseTokenInvoiceQuery
);
if (error || !data?.createTokenInvoice) {
@ -295,11 +307,13 @@ export const tbaseResolvers = {
): Promise<boolean> => {
await requestLimiter(context.ip, 'createThunderPoints');
const [info, error] = await toWithError(
request(appUrls.tbase, createThunderPointsQuery, params)
const { data, error } = await graphqlFetchWithProxy(
appUrls.tbase,
createThunderPointsQuery,
params
);
if (error || !info?.createPoints) return false;
if (error || !data?.createPoints) return false;
return true;
},

41
server/utils/fetch.ts Normal file
View file

@ -0,0 +1,41 @@
import { Agent } from 'https';
import getConfig from 'next/config';
import { SocksProxyAgent } from 'socks-proxy-agent';
import fetch from 'node-fetch';
import { GraphQLError } from 'graphql';
import { logger } from 'server/helpers/logger';
const { serverRuntimeConfig } = getConfig() || { serverRuntimeConfig: {} };
const { torProxy } = serverRuntimeConfig;
let agent: Agent | null = null;
if (torProxy) {
logger.info(`Using tor proxy for external requests: ${torProxy}`);
agent = new SocksProxyAgent(torProxy) as any;
}
export const fetchWithProxy = (url: string, options?: {}) => {
return agent ? fetch(url, { agent, ...options }) : fetch(url, options);
};
export const graphqlFetchWithProxy = async (
url: string,
query: string,
variables?: { [key: string]: string | number | string[] }
): Promise<{
data: any;
error: undefined | GraphQLError;
}> => {
return fetchWithProxy(url, {
method: 'post',
headers: { Accept: 'application/json' },
body: JSON.stringify({ query, variables }),
})
.then(res => res.json())
.then(data => data)
.catch(error => {
logger.error('Error doing graphql fetch: %o', error);
return { data: undefined, error };
});
};