Skip to content
Snippets Groups Projects
Commit 9654eabe authored by feruzm's avatar feruzm
Browse files

upload image with hivesigner access token

parent 869c0543
No related branches found
No related tags found
1 merge request!1Upload images with Hivesigner access token
......@@ -146,12 +146,12 @@ Creating a signature (psuedocode):
signature = secp256k1_sign(sha256('ImageSigningChallenge'+image_data), account_private_posting_key)
```
Creating a signature (node.js & [dsteem](https://github.com/jnordberg/dsteem))
Creating a signature (node.js & [dhive](https://github.com/openhive-network/dhive))
```js
#!/usr/bin/env node
const dsteem = require('dsteem')
const dhive = require('@hiveio/dhive')
const crypto = require('crypto')
const fs = require('fs')
......@@ -163,7 +163,7 @@ if (!wif || !file) {
}
const data = fs.readFileSync(file)
const key = dsteem.PrivateKey.fromString(wif)
const key = dhive.PrivateKey.fromString(wif)
const imageHash = crypto.createHash('sha256')
.update('ImageSigningChallenge')
.update(data)
......
......@@ -38,6 +38,7 @@ max_image_size = 10000000 # 10mb
duration = 604800000 # in ms (=1 week)
max = 300 # max requests within duration
reputation = 10 # minimum reputation needed for upload
app_account = 'hive.blog'
# blob stores, valid types are: memory, s3
# the s3 type additionally needs the s3_bucket key to be set and expects
......
......@@ -15,7 +15,6 @@
"bunyan": "^2.0.2",
"busboy": "^0.2.14",
"config": "^1.29.4",
"dsteem": "^0.9.0",
"koa": "^2.3.0",
"koa-router": "^7.4.0",
"mmmagic": "^0.5.0",
......
......@@ -3,7 +3,7 @@
import * as config from 'config'
import { base58Enc } from './utils'
import { Account } from 'dsteem'
import { Account } from '@hiveio/dhive'
import {KoaContext, rpcClient} from './common'
import {APIError} from './error'
......
......@@ -2,7 +2,7 @@
import {AbstractBlobStore} from 'abstract-blob-store'
import * as config from 'config'
import {Client} from 'dsteem'
import {Client} from '@hiveio/dhive'
import {IRouterContext} from 'koa-router'
import * as Redis from 'redis'
......
......@@ -7,7 +7,7 @@ import {KoaContext} from './common'
import {legacyProxyHandler} from './legacy-proxy'
import {proxyHandler} from './proxy'
import {serveHandler} from './serve'
import {uploadHandler} from './upload'
import {uploadHandler, uploadHsHandler} from './upload'
const version = require('./version')
const router = new Router()
......@@ -22,6 +22,7 @@ async function healthcheck(ctx: KoaContext) {
router.get('/', healthcheck as any)
router.get('/.well-known/healthcheck.json', healthcheck as any)
router.get('/u/:username/avatar/:size?', avatarHandler as any)
router.post('/hs/:accesstoken', uploadHsHandler as any)
router.post('/:username/:signature', uploadHandler as any)
router.get('/:width(\\d+)x:height(\\d+)/:url(.*)', legacyProxyHandler as any)
router.get('/p/:url', proxyHandler as any)
......
......@@ -3,11 +3,12 @@
import * as Busboy from 'busboy'
import * as config from 'config'
import {createHash} from 'crypto'
import {Client, Signature} from 'dsteem'
import {Client, Signature} from '@hiveio/dhive'
import * as http from 'http'
import * as multihash from 'multihashes'
import * as RateLimiter from 'ratelimiter'
import {URL} from 'url'
import * as hivesigner from 'hivesigner'
import {accountBlacklist} from './blacklist'
import {KoaContext, redisClient, rpcClient, uploadStore} from './common'
......@@ -77,7 +78,122 @@ async function getRatelimit(account: string) {
})
})
}
const b64uLookup = {
'/': '_', _: '/', '+': '-', '-': '+', '=': '.', '.': '=',
}
function b64uToB64 (str: string) {
const tt = str.replace(/(-|_|\.)/g, function(m) { return b64uLookup[m]})
return tt
}
export async function uploadHsHandler(ctx: KoaContext) {
ctx.tag({handler: 'hsupload'})
let validSignature = false
APIError.assert(ctx.method === 'POST', {code: APIError.Code.InvalidMethod})
APIError.assertParams(ctx.params, ['accesstoken'])
APIError.assert(ctx.get('content-type').includes('multipart/form-data'),
{message: 'Only multipart uploads are supported'})
const contentLength = Number.parseInt(ctx.get('content-length'))
APIError.assert(Number.isFinite(contentLength),
APIError.Code.LengthRequired)
APIError.assert(contentLength <= MAX_IMAGE_SIZE,
APIError.Code.PayloadTooLarge)
const file = await parseMultipart(ctx.req)
const data = await readStream(file.stream)
// extra check if client manges to lie about the content-length
APIError.assert((file.stream as any).truncated !== true,
APIError.Code.PayloadTooLarge)
const imageHash = createHash('sha256')
.update('ImageSigningChallenge')
.update(data)
.digest()
const token = ctx.params['accesstoken']
const decoded = Buffer.from(b64uToB64(token), 'base64').toString()
const tokenObj = JSON.parse(decoded)
const signedMessage = tokenObj.signed_message
if (
tokenObj.authors
&& tokenObj.authors[0]
&& tokenObj.signatures
&& tokenObj.signatures[0]
&& signedMessage
&& signedMessage.type
&& ['login', 'posting', 'offline', 'code', 'refresh']
.includes(signedMessage.type)
&& signedMessage.app
) {
const username = tokenObj.authors[0]
let account = {
name: '',
reputation: 0,
}
const cl = new hivesigner.Client({
app: UPLOAD_LIMITS.app_account,
accessToken: token,
})
await cl.me(function (err: any, res: any) {
if (!err && res) {
account = res.account
APIError.assert(account, APIError.Code.NoSuchAccount)
ctx.log.warn('uploading app %s', signedMessage.app)
APIError.assert(username === account.name, APIError.Code.InvalidSignature)
APIError.assert(signedMessage.app === UPLOAD_LIMITS.app_account, APIError.Code.InvalidSignature)
APIError.assert(res.scope.includes('comment'), APIError.Code.InvalidSignature)
if (account && account.name) {
['posting', 'active', 'owner'].forEach((type) => {
account[type].account_auths.forEach((key: string[]) => {
if (
!validSignature
&& key[0] === UPLOAD_LIMITS.app_account
) {
validSignature = true;
}
});
});
}
}
});
APIError.assert(validSignature, APIError.Code.InvalidSignature)
APIError.assert(!accountBlacklist.includes(account.name), APIError.Code.Blacklisted)
let limit: RateLimit = {total: 0, remaining: Infinity, reset: 0}
try {
limit = await getRatelimit(account.name)
} catch (error) {
ctx.log.warn(error, 'unable to enforce upload rate limits')
}
APIError.assert(limit.remaining > 0, APIError.Code.QoutaExceeded)
APIError.assert(repLog10(account.reputation) >= UPLOAD_LIMITS.reputation, APIError.Code.Deplorable)
const key = 'D' + multihash.toB58String(multihash.encode(imageHash, 'sha2-256'))
const url = new URL(`${ key }/${ file.name }`, SERVICE_URL)
if (!(await storeExists(uploadStore, key))) {
await storeWrite(uploadStore, key, data)
} else {
ctx.log.debug('key %s already exists in store', key)
}
ctx.log.info({uploader: account.name, size: data.byteLength}, 'image uploaded')
ctx.status = 200
ctx.body = {url}
}
}
export async function uploadHandler(ctx: KoaContext) {
ctx.tag({handler: 'upload'})
......
import 'mocha'
import * as assert from 'assert'
import {PrivateKey} from 'dsteem'
import {PrivateKey} from '@hiveio/dhive'
import {rpcClient} from './../src/common'
......@@ -41,7 +41,7 @@ export const mockAccounts: any = {
}
before(() => {
// mock out dsteem rpc calls
// mock out dhive rpc calls
const _client = rpcClient as any
_client.call = async (api: string, method: string, params = []) => {
const apiMethod = `${ api }-${ method }`
......
......@@ -5,7 +5,7 @@ import * as needle from 'needle'
import * as path from 'path'
import * as fs from 'fs'
import * as crypto from 'crypto'
import {PrivateKey} from 'dsteem'
import {PrivateKey} from '@hiveio/dhive'
import {app} from './../src/app'
import {rpcClient} from './../src/common'
......
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment