diff --git a/.gitignore b/.gitignore index 3dfafe60..23387198 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ proto/autogenerated/debug.txt metrics_cache/ metric_cache/ metrics_events/ -bundler_events/ \ No newline at end of file +bundler_events/ +metric_events/ \ No newline at end of file diff --git a/env.example b/env.example index 6a03789b..319585fb 100644 --- a/env.example +++ b/env.example @@ -104,6 +104,7 @@ LSP_MAX_FEE_BPS=100 # Disable outbound payments aka honeypot mode #DISABLE_EXTERNAL_PAYMENTS=false #ALLOW_RESET_METRICS_STORAGES=false +ALLOW_HTTP_UPGRADE=false #WATCHDOG SECURITY # A last line of defense against 0-day drainage attacks diff --git a/proto/autogenerated/go/http_client.go b/proto/autogenerated/go/http_client.go index 970a39d0..97f6ff04 100644 --- a/proto/autogenerated/go/http_client.go +++ b/proto/autogenerated/go/http_client.go @@ -821,7 +821,32 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, - // server streaming method: GetHttpCreds not implemented + GetHttpCreds: func() (*HttpCreds, error) { + auth, err := params.RetrieveUserAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/user/http_creds" + body := []byte{} + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return nil, err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := HttpCreds{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, GetInviteLinkState: func(req GetInviteTokenStateRequest) (*GetInviteTokenStateResponse, error) { auth, err := params.RetrieveAdminAuth() if err != nil { diff --git a/proto/autogenerated/ts/express_server.ts b/proto/autogenerated/ts/express_server.ts index 92f54ba2..9be3037d 100644 --- a/proto/autogenerated/ts/express_server.ts +++ b/proto/autogenerated/ts/express_server.ts @@ -403,6 +403,16 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'GetHttpCreds': + if (!methods.GetHttpCreds) { + throw new Error('method GetHttpCreds not found' ) + } else { + opStats.validate = opStats.guard + const res = await methods.GetHttpCreds({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'GetLNURLChannelLink': if (!methods.GetLNURLChannelLink) { throw new Error('method GetLNURLChannelLink not found' ) @@ -926,6 +936,25 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.GetHttpCreds) throw new Error('method: GetHttpCreds is not implemented') + app.post('/api/user/http_creds', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'GetHttpCreds', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.GetHttpCreds) throw new Error('method: GetHttpCreds is not implemented') + const authContext = await opts.UserAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + stats.validate = stats.guard + const query = req.query + const params = req.params + const response = await methods.GetHttpCreds({rpcName:'GetHttpCreds', ctx:authContext }) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.GetInviteLinkState) throw new Error('method: GetInviteLinkState is not implemented') app.post('/api/admin/app/invite/get', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'GetInviteLinkState', batch: false, nostr: false, batchSize: 0} diff --git a/proto/autogenerated/ts/http_client.ts b/proto/autogenerated/ts/http_client.ts index 8ba73a0f..9fa96276 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -360,7 +360,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, - GetHttpCreds: async (cb: (v:ResultError | ({ status: 'OK' }& Types.HttpCreds)) => void): Promise => { throw new Error('http streams are not supported')}, + GetHttpCreds: async (): Promise => { + const auth = await params.retrieveUserAuth() + if (auth === null) throw new Error('retrieveUserAuth() returned null') + let finalRoute = '/api/user/http_creds' + const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.HttpCredsValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, GetInviteLinkState: async (request: Types.GetInviteTokenStateRequest): Promise => { const auth = await params.retrieveAdminAuth() if (auth === null) throw new Error('retrieveAdminAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_client.ts b/proto/autogenerated/ts/nostr_client.ts index d7dc1d06..f6dcf1dd 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -276,20 +276,19 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, - GetHttpCreds: async (cb: (res:ResultError | ({ status: 'OK' }& Types.HttpCreds)) => void): Promise => { + GetHttpCreds: async (): Promise => { const auth = await params.retrieveNostrUserAuth() if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') const nostrRequest: NostrRequest = {} - subscribe(params.pubDestination, {rpcName:'GetHttpCreds',authIdentifier:auth, ...nostrRequest }, (data) => { - if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data) - if (data.status === 'OK') { - const result = data - if(!params.checkResult) return cb({ status: 'OK', ...result }) - const error = Types.HttpCredsValidate(result) - if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message }) - } - return cb({ status: 'ERROR', reason: 'invalid response' }) - }) + const data = await send(params.pubDestination, {rpcName:'GetHttpCreds',authIdentifier:auth, ...nostrRequest }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.HttpCredsValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } }, GetInviteLinkState: async (request: Types.GetInviteTokenStateRequest): Promise => { const auth = await params.retrieveNostrAdminAuth() diff --git a/proto/autogenerated/ts/nostr_transport.ts b/proto/autogenerated/ts/nostr_transport.ts index 99d7ec86..4f48a701 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -285,6 +285,16 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'GetHttpCreds': + if (!methods.GetHttpCreds) { + throw new Error('method not defined: GetHttpCreds') + } else { + opStats.validate = opStats.guard + const res = await methods.GetHttpCreds({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'GetLNURLChannelLink': if (!methods.GetLNURLChannelLink) { throw new Error('method not defined: GetLNURLChannelLink') @@ -670,10 +680,10 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { stats.guard = process.hrtime.bigint() authCtx = authContext stats.validate = stats.guard - methods.GetHttpCreds({rpcName:'GetHttpCreds', ctx:authContext ,cb: (response, err) => { + const response = await methods.GetHttpCreds({rpcName:'GetHttpCreds', ctx:authContext }) stats.handle = process.hrtime.bigint() - if (err) { logErrorAndReturnResponse(err, err.message, res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)} else { res({status: 'OK', ...response});opts.metricsCallback([{ ...info, ...stats, ...authContext }])} - }}) + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break case 'GetInviteLinkState': diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index 6bdaa118..ae494f22 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -35,8 +35,8 @@ export type UserContext = { app_user_id: string user_id: string } -export type UserMethodInputs = AddProduct_Input | AddUserOffer_Input | AuthorizeDebit_Input | BanDebit_Input | DecodeInvoice_Input | DeleteUserOffer_Input | EditDebit_Input | EnrollAdminToken_Input | GetDebitAuthorizations_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetPaymentState_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOfferInvoices_Input | GetUserOffers_Input | GetUserOperations_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UpdateUserOffer_Input | UserHealth_Input -export type UserMethodOutputs = AddProduct_Output | AddUserOffer_Output | AuthorizeDebit_Output | BanDebit_Output | DecodeInvoice_Output | DeleteUserOffer_Output | EditDebit_Output | EnrollAdminToken_Output | GetDebitAuthorizations_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetPaymentState_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOfferInvoices_Output | GetUserOffers_Output | GetUserOperations_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UpdateUserOffer_Output | UserHealth_Output +export type UserMethodInputs = AddProduct_Input | AddUserOffer_Input | AuthorizeDebit_Input | BanDebit_Input | DecodeInvoice_Input | DeleteUserOffer_Input | EditDebit_Input | EnrollAdminToken_Input | GetDebitAuthorizations_Input | GetHttpCreds_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetPaymentState_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOfferInvoices_Input | GetUserOffers_Input | GetUserOperations_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UpdateUserOffer_Input | UserHealth_Input +export type UserMethodOutputs = AddProduct_Output | AddUserOffer_Output | AuthorizeDebit_Output | BanDebit_Output | DecodeInvoice_Output | DeleteUserOffer_Output | EditDebit_Output | EnrollAdminToken_Output | GetDebitAuthorizations_Output | GetHttpCreds_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetPaymentState_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOfferInvoices_Output | GetUserOffers_Output | GetUserOperations_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UpdateUserOffer_Output | UserHealth_Output export type AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest} @@ -117,8 +117,8 @@ export type GetDebitAuthorizations_Output = ResultError | ({ status: 'OK' } & De export type GetErrorStats_Input = {rpcName:'GetErrorStats'} export type GetErrorStats_Output = ResultError | ({ status: 'OK' } & ErrorStats) -export type GetHttpCreds_Input = {rpcName:'GetHttpCreds', cb:(res: HttpCreds, err:Error|null)=> void} -export type GetHttpCreds_Output = ResultError | { status: 'OK' } +export type GetHttpCreds_Input = {rpcName:'GetHttpCreds'} +export type GetHttpCreds_Output = ResultError | ({ status: 'OK' } & HttpCreds) export type GetInviteLinkState_Input = {rpcName:'GetInviteLinkState', req: GetInviteTokenStateRequest} export type GetInviteLinkState_Output = ResultError | ({ status: 'OK' } & GetInviteTokenStateResponse) @@ -327,7 +327,7 @@ export type ServerMethods = { GetBundleMetrics?: (req: GetBundleMetrics_Input & {ctx: MetricsContext }) => Promise GetDebitAuthorizations?: (req: GetDebitAuthorizations_Input & {ctx: UserContext }) => Promise GetErrorStats?: (req: GetErrorStats_Input & {ctx: MetricsContext }) => Promise - GetHttpCreds?: (req: GetHttpCreds_Input & {ctx: UserContext }) => Promise + GetHttpCreds?: (req: GetHttpCreds_Input & {ctx: UserContext }) => Promise GetInviteLinkState?: (req: GetInviteLinkState_Input & {ctx: AdminContext }) => Promise GetLNURLChannelLink?: (req: GetLNURLChannelLink_Input & {ctx: UserContext }) => Promise GetLiveDebitRequests?: (req: GetLiveDebitRequests_Input & {ctx: UserContext }) => Promise diff --git a/proto/service/methods.proto b/proto/service/methods.proto index 0d129d29..81a9165e 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -618,7 +618,7 @@ service LightningPub { option (http_route) = "/api/user/migrations/sub"; option (nostr) = true; } - rpc GetHttpCreds(structs.Empty) returns (stream structs.HttpCreds){ + rpc GetHttpCreds(structs.Empty) returns (structs.HttpCreds){ option (auth_type) = "User"; option (http_method) = "post"; option (http_route) = "/api/user/http_creds"; diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index de130262..502860c9 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -35,6 +35,16 @@ export default class { return decoded } + GetHttpCreds(ctx: Types.UserContext): Types.HttpCreds { + if (!this.settings.allowHttpUpgrade) { + throw new Error("http upgrade not allowed") + } + return { + url: this.settings.serviceUrl, + token: this.SignUserToken(ctx.user_id, ctx.app_id, ctx.app_user_id) + } + } + async BanUser(userId: string): Promise { const banned = await this.storage.userStorage.BanUser(userId) const appUsers = await this.storage.applicationStorage.GetAllAppUsersFromUser(userId) diff --git a/src/services/main/settings.ts b/src/services/main/settings.ts index d412e658..fe1d6b7d 100644 --- a/src/services/main/settings.ts +++ b/src/services/main/settings.ts @@ -36,6 +36,7 @@ export type MainSettings = { lnurlMetaText: string, bridgeUrl: string, allowResetMetricsStorages: boolean + allowHttpUpgrade: boolean } export type BitcoinCoreSettings = { @@ -76,7 +77,8 @@ export const LoadMainSettingsFromEnv = (): MainSettings => { pushBackupsToNostr: process.env.PUSH_BACKUPS_TO_NOSTR === 'true' || false, lnurlMetaText: process.env.LNURL_META_TEXT || "LNURL via Lightning.pub", bridgeUrl: process.env.BRIDGE_URL || "https://shockwallet.app", - allowResetMetricsStorages: process.env.ALLOW_RESET_METRICS_STORAGES === 'true' || false + allowResetMetricsStorages: process.env.ALLOW_RESET_METRICS_STORAGES === 'true' || false, + allowHttpUpgrade: process.env.ALLOW_HTTP_UPGRADE === 'true' || false } } diff --git a/src/services/main/unlocker.ts b/src/services/main/unlocker.ts index 1a5cd361..084fb533 100644 --- a/src/services/main/unlocker.ts +++ b/src/services/main/unlocker.ts @@ -81,9 +81,14 @@ export class Unlocker { const unlocker = this.GetUnlockerClient(lndCert) const walletPassword = this.GetWalletPassword() await unlocker.unlockWallet({ walletPassword, recoveryWindow: 0, statelessInit: false, channelBackups: undefined }, DeadLineMetadata()) - const infoAfter = await this.GetLndInfo(ln) + let infoAfter = await this.GetLndInfo(ln) if (!infoAfter.ok) { - throw new Error("failed to unlock lnd wallet " + infoAfter.failure) + this.log("failed to unlock lnd wallet, retrying in 5 seconds...") + await new Promise(resolve => setTimeout(resolve, 5000)) + infoAfter = await this.GetLndInfo(ln) + if (!infoAfter.ok) { + throw new Error("failed to unlock lnd wallet " + infoAfter.failure) + } } this.log("unlocked wallet with pub:", infoAfter.pub) this.nodePub = infoAfter.pub @@ -152,7 +157,7 @@ export class Unlocker { const info = await ln.getInfo({}, DeadLineMetadata()) return { ok: true, pub: info.response.identityPubkey } } catch (err: any) { - if (err.message === '2 UNKNOWN: wallet locked, unlock it to enable full RPC access') { + if (err.message === 'wallet locked, unlock it to enable full RPC access') { this.log("wallet is locked") return { ok: false, failure: 'locked' } } else if (err.message === '2 UNKNOWN: the RPC server is in the process of starting up, but not yet ready to accept calls') { diff --git a/src/services/nostr/handler.ts b/src/services/nostr/handler.ts index 6174deb1..46b10145 100644 --- a/src/services/nostr/handler.ts +++ b/src/services/nostr/handler.ts @@ -252,6 +252,8 @@ export default class Handler { })) if (!sent) { log("failed to send event") + } else { + log("sent event") } } diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index d9f767df..015e66e6 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -400,6 +400,9 @@ export default (mainHandler: Main): Types.ServerMethods => { }) if (err != null) throw new Error(err.message) return mainHandler.offerManager.GetUserOfferInvoices(ctx, req) - } + }, + GetHttpCreds: async ({ ctx }) => { + return mainHandler.appUserManager.GetHttpCreds(ctx) + }, } } \ No newline at end of file