Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Establish /.well-known/pay #1546

Merged
merged 5 commits into from Jan 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/create-app.js
Expand Up @@ -15,6 +15,7 @@ const vhost = require('vhost')
const EmailService = require('./services/email-service')
const TokenService = require('./services/token-service')
const capabilityDiscovery = require('./capability-discovery')
const paymentPointerDiscovery = require('./payment-pointer-discovery')
const API = require('./api')
const errorPages = require('./handlers/error-pages')
const config = require('./server-config')
Expand Down Expand Up @@ -166,6 +167,7 @@ function initHeaders (app) {
})

app.use('/', capabilityDiscovery())
app.use('/', paymentPointerDiscovery())
}

/**
Expand Down
83 changes: 83 additions & 0 deletions lib/payment-pointer-discovery.js
@@ -0,0 +1,83 @@
'use strict'
/**
* @module payment-pointer-discovery
*/
const express = require('express')
const { promisify } = require('util')
const fs = require('fs')
const rdf = require('rdflib')

module.exports = paymentPointerDiscovery

const PROFILE_PATH = '/profile/card'

/**
* Returns a set of routes to deal with server payment pointer discovery
* @method paymentPointerDiscovery
* @return {Router} Express router
*/
function paymentPointerDiscovery () {
const router = express.Router('/')

// Advertise the server payment pointer discover endpoint
router.get('/.well-known/pay', paymentPointerDocument())
return router
}

/**
* Serves the service payment pointer document (containing server root URL, including
* any base path the user specified in config, server API endpoints, etc).
* @method paymentPointerDocument
* @param req
* @param res
* @param next
*/
function paymentPointerDocument () {
return async (req, res) => {
try {
const ldp = req.app.locals.ldp
const url = ldp.resourceMapper.resolveUrl(req.hostname, PROFILE_PATH)
const contentType = 'text/turtle'
const createIfNotExists = false
const { path } = await ldp.resourceMapper.mapUrlToFile({ url, contentType, createIfNotExists })
let body
try {
// Read the file from disk
body = await promisify(fs.readFile)(path, { encoding: 'utf8' })
} catch (e) {
if (e.message.startsWith('ENOENT: no such file or directory,')) {
res.json({
error: `Please create ${PROFILE_PATH} on your pod`
})
}
}
const webid = rdf.Namespace(`${url}#`)('me')
const pp = rdf.Namespace('http://paymentpointers.org/ns#')('PaymentPointer')
let paymentPointer
try {
const graph = rdf.graph()
// Parse the file as Turtle
rdf.parse(body, graph, url, contentType)
paymentPointer = graph.any(webid, pp)
} catch (e) {
console.error(e)
res.json({
error: `Please make sure ${PROFILE_PATH} contains valid Turtle`
})
}
if (paymentPointer === null) {
res.json({ fail: 'Add triple', subject: `<${webid.value}>`, predicate: `<${pp.value}>`, object: '$alice.example' })
}
if (paymentPointer.value.startsWith('$')) {
let suffix = ''
if (paymentPointer.value.indexOf('/') === -1) {
suffix = '/.well-known/pay'
}
paymentPointer.value = `https://${paymentPointer.value.substring(1)}${suffix}`
}
res.redirect(paymentPointer.value)
} catch (e) {
res.json({ fail: e.message })
}
}
}
3 changes: 1 addition & 2 deletions test/integration/capability-discovery-test.js
Expand Up @@ -7,8 +7,7 @@ const supertest = require('supertest')
const expect = require('chai').expect
// In this test we always assume that we are Alice

// FIXME #1502
describe.skip('API', () => {
describe('API', () => {
let alice

const aliceServerUri = 'https://localhost:5000'
Expand Down
149 changes: 149 additions & 0 deletions test/integration/payment-pointer-test.js
@@ -0,0 +1,149 @@
/* eslint-disable no-unused-expressions */

const Solid = require('../../index')
const path = require('path')
const { cleanDir } = require('../utils')
const supertest = require('supertest')
const expect = require('chai').expect

describe('API', () => {
const configPath = path.join(__dirname, '../resources/config')

const serverConfig = {
sslKey: path.join(__dirname, '../keys/key.pem'),
sslCert: path.join(__dirname, '../keys/cert.pem'),
auth: 'oidc',
dataBrowser: false,
webid: true,
multiuser: false,
configPath
}

function startServer (pod, port) {
return new Promise((resolve) => {
pod.listen(port, () => { resolve() })
})
}

describe('Payment Pointer Alice', () => {
let alice
const aliceServerUri = 'https://localhost:5000'
const aliceDbPath = path.join(__dirname,
'../resources/accounts-scenario/alice/db')
const aliceRootPath = path.join(__dirname, '../resources/accounts-scenario/alice')

const alicePod = Solid.createServer(
Object.assign({
root: aliceRootPath,
serverUri: aliceServerUri,
dbPath: aliceDbPath
}, serverConfig)
)

before(() => {
return Promise.all([
startServer(alicePod, 5000)
]).then(() => {
alice = supertest(aliceServerUri)
})
})

after(() => {
alicePod.close()
cleanDir(aliceRootPath)
})

describe('GET Payment Pointer document', () => {
it('should show instructions to add a triple', (done) => {
alice.get('/.well-known/pay')
.expect(200)
.expect('content-type', /application\/json/)
.end(function (err, req) {
if (err) {
done(err)
} else {
expect(req.body).deep.equal({
fail: 'Add triple',
subject: '<https://localhost:5000/profile/card#me>',
predicate: '<http://paymentpointers.org/ns#PaymentPointer>',
object: '$alice.example'
})
done()
}
})
})
})
})

describe('Payment Pointer Bob', () => {
let bob
const bobServerUri = 'https://localhost:5001'
const bobDbPath = path.join(__dirname,
'../resources/accounts-scenario/bob/db')
const bobRootPath = path.join(__dirname, '../resources/accounts-scenario/bob')
const bobPod = Solid.createServer(
Object.assign({
root: bobRootPath,
serverUri: bobServerUri,
dbPath: bobDbPath
}, serverConfig)
)

before(() => {
return Promise.all([
startServer(bobPod, 5001)
]).then(() => {
bob = supertest(bobServerUri)
})
})

after(() => {
bobPod.close()
cleanDir(bobRootPath)
})

describe('GET Payment Pointer document', () => {
it('should redirect to example.com', (done) => {
bob.get('/.well-known/pay')
.expect('location', 'https://bob.com/.well-known/pay')
.expect(302, done)
})
})
})

describe('Payment Pointer Charlie', () => {
let charlie
const charlieServerUri = 'https://localhost:5002'
const charlieDbPath = path.join(__dirname,
'../resources/accounts-scenario/charlie/db')
const charlieRootPath = path.join(__dirname, '../resources/accounts-scenario/charlie')
const charliePod = Solid.createServer(
Object.assign({
root: charlieRootPath,
serverUri: charlieServerUri,
dbPath: charlieDbPath
}, serverConfig)
)

before(() => {
return Promise.all([
startServer(charliePod, 5002)
]).then(() => {
charlie = supertest(charlieServerUri)
})
})

after(() => {
charliePod.close()
cleanDir(charlieRootPath)
})

describe('GET Payment Pointer document', () => {
it('should redirect to example.com/charlie', (done) => {
charlie.get('/.well-known/pay')
.expect('location', 'https://service.com/charlie')
.expect(302, done)
})
})
})
})
5 changes: 5 additions & 0 deletions test/resources/accounts-scenario/bob/profile/card$.ttl
@@ -0,0 +1,5 @@
@prefix : <#>.
@prefix pp: <http://paymentpointers.org/ns#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

:me pp:PaymentPointer "$bob.com"^^xsd:string .
5 changes: 5 additions & 0 deletions test/resources/accounts-scenario/charlie/profile/card$.ttl
@@ -0,0 +1,5 @@
@prefix : <#>.
@prefix pp: <http://paymentpointers.org/ns#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

:me pp:PaymentPointer "$service.com/charlie"^^xsd:string .