├── .gitattributes ├── apiconfig.example.json ├── src ├── dreamsThing.ts ├── test.ts ├── config.ts ├── server.ts ├── dreamsId.ts └── dreamsClient.ts ├── tsconfig.json ├── package.json ├── LICENSE ├── readme.md └── .gitignore /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /apiconfig.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "Cookie": { 3 | "AuthToken": "", 4 | "Issuer": "256", 5 | "Locale": "en_GB", 6 | "SilentSignin": "true", 7 | "Path": "/", 8 | "HttpOnly": true, 9 | "Secure": true 10 | } 11 | } -------------------------------------------------------------------------------- /src/dreamsThing.ts: -------------------------------------------------------------------------------- 1 | export enum ThingType { 2 | COLLECTION, 3 | COMMENT, 4 | DREAM, 5 | ELEMENT, 6 | GREIF, 7 | IDENTITY, 8 | NEWS, 9 | PHOTO, 10 | REVISION, 11 | SCENE, 12 | TAG, 13 | USER, 14 | VERSION, 15 | WEBPAGE 16 | }; -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import { toDreamsId, toIndreamsId } from './dreamsId'; 2 | 3 | console.log(toDreamsId('uAAAAAAAAAB')) 4 | console.log(toIndreamsId('u00000000000001')) 5 | 6 | console.log(toDreamsId('uCApMyVUyho')) 7 | console.log(toIndreamsId('u0b154326a95f68')) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "paths": { 12 | "*": [ 13 | "node_modules/*" 14 | ] 15 | } 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ] 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dreamsapi", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/server.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "prestart": "npm run build", 9 | "pretest": "npm run build", 10 | "start": "node .", 11 | "test": "node ./dist/test.js" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "axios": "^0.19.2", 18 | "body-parser": "^1.19.0", 19 | "express": "^4.17.1" 20 | }, 21 | "devDependencies": { 22 | "@types/express": "^4.17.6", 23 | "@types/node": "^14.0.13", 24 | "typescript": "^3.9.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | 4 | const configPath = path.resolve(__dirname, '../apiconfig.json'); 5 | 6 | let config = JSON.parse(fs.readFileSync(configPath, 'utf8')); 7 | 8 | export function getConfig() { 9 | return config; 10 | } 11 | 12 | export function setConfig(newConfig: any) { 13 | config = newConfig; 14 | fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2)); 15 | } 16 | 17 | export function getConfigValue(key: string) { 18 | return config[key]; 19 | } 20 | 21 | export function setConfigValue(key: string, value: any) { 22 | setConfig({ 23 | ...config, 24 | [key]: value 25 | }); 26 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 jaames 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | > ⚠️ 2 | > This does NOT provide a way to download Dreams level data, nor will it ever. Unfortunately I no longer have the time/motivation to investivate Dreams or develop this tool further. 3 | > ⚠️ 4 | 5 | ## Dreams API 6 | 7 | API proxy & reverse-engineering notes for Media Molecule's [Dreams](https://www.playstation.com/en-gb/games/dreams-ps4/); a rad user-generated-content game on the PS4. 8 | 9 | Currently consists of a NodeJS [indreams.me](https://indreams.me/) API proxy that handles auth headers for you, which can be used for automatically gathering level stats, user profiles, etc. There's also some [documentation](https://github.com/jaames/dreams-api/wiki/Indreams-API) that covers some basic file format structures and so on. 10 | 11 | ## Proxy Setup 12 | 13 | Requires a NodeJS install (tested on v12.17.0) with NPM 14 | 15 | Clone the repo from Github: 16 | 17 | ```bash 18 | git clone https://github.com/jaames/dreams-api 19 | ``` 20 | 21 | Then inside the repo directory, install dependencies: 22 | 23 | ```bash 24 | npm install 25 | ``` 26 | 27 | Copy `apiconfig.example.json` to `apiconfig.json` 28 | 29 | ```bash 30 | cp apiconfig.example.json apiconfig.json 31 | ``` 32 | 33 | Then start the server: 34 | 35 | ```bash 36 | npm run start 37 | ``` 38 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import { 4 | requestGet, 5 | requestPost, 6 | requestPostAudio, 7 | QueryStringObject, 8 | } from './dreamsClient'; 9 | 10 | const server = express(); 11 | const port = 3000; 12 | 13 | server.use(express.json()); 14 | server.use(bodyParser.raw()); 15 | 16 | // requires /cm/dev/admin perms sadly :( 17 | server.post('/api/audio/import', async (req, res) => { 18 | const filename = req.header('x-filename'); 19 | const dreamsResponse = await requestPostAudio(req.path, filename, req.body); 20 | res.status(dreamsResponse.status); 21 | res.set(dreamsResponse.headers); 22 | res.send(dreamsResponse.data); 23 | }); 24 | 25 | server.get('/*', async (req, res) => { 26 | const dreamsResponse = await requestGet(req.path, req.query as QueryStringObject); 27 | res.status(dreamsResponse.status); 28 | res.set(dreamsResponse.headers); 29 | res.send(dreamsResponse.data); 30 | }); 31 | 32 | server.post('/*', async (req, res) => { 33 | const dreamsResponse = await requestPost(req.path, req.body); 34 | res.status(dreamsResponse.status); 35 | res.set(dreamsResponse.headers); 36 | res.send(dreamsResponse.data); 37 | }); 38 | 39 | server.listen(port, () => console.log(`Dreams API server running on http://localhost:${port}`)); -------------------------------------------------------------------------------- /src/dreamsId.ts: -------------------------------------------------------------------------------- 1 | import { ThingType } from './dreamsThing'; 2 | 3 | export const indreamsIdRegex = /^[c|k|m|o|g|j|n|p|r|d|t|u|v|w]{1}[a-zA-Z0-9]{10}$/; 4 | export const dreamsIdRegex = /^[c|k|m|o|g|j|n|p|r|d|t|u|v|w]{1}[a-f0-9]{14}$/; 5 | 6 | const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; 7 | const alphabetSize = alphabet.length; 8 | 9 | export function toIndreamsId(dreamsId: string) { 10 | let result = dreamsId[0]; 11 | for (let chunkOffset = 1; chunkOffset < 15; chunkOffset += 7) { 12 | const chunkInt = parseInt(dreamsId.substr(chunkOffset, 7), 16); 13 | for (let chunkDiv = Math.pow(alphabetSize, 4); chunkDiv >= 1; chunkDiv /= alphabetSize) { 14 | result += alphabet[(chunkInt / chunkDiv | 0) % alphabetSize]; 15 | } 16 | } 17 | return result; 18 | } 19 | 20 | export function isIndreamsId(indreamsId: string) { 21 | indreamsIdRegex.test(indreamsId); 22 | } 23 | 24 | export function toDreamsId(indreamsId: string) { 25 | const id = indreamsId.replace('O', 'o').replace('0', 'o'); 26 | let result = id[0]; 27 | for (var chunkOffset = 1; chunkOffset < 11; chunkOffset += 5) { 28 | const chunk = id.substr(chunkOffset, 5); 29 | let chunkInt = 0; 30 | for (let charOffset = 0; charOffset < 5; charOffset++) { 31 | const char = chunk[charOffset] 32 | chunkInt = chunkInt * alphabetSize + alphabet.indexOf(char); 33 | } 34 | result += chunkInt.toString(16).padStart(7, '0'); 35 | } 36 | return result; 37 | } 38 | 39 | export function isDreamsId(dreamsId: string) { 40 | dreamsIdRegex.test(dreamsId); 41 | } 42 | 43 | export const IdTypeMap: Record = { 44 | 'c': ThingType.COLLECTION, 45 | 'd': ThingType.SCENE, 46 | 'j': ThingType.IDENTITY, 47 | 'k': ThingType.COMMENT, 48 | 'm': ThingType.DREAM, 49 | 'o': ThingType.ELEMENT, 50 | 'p': ThingType.PHOTO, 51 | 'r': ThingType.REVISION, 52 | 'u': ThingType.USER, 53 | 'v': ThingType.VERSION, 54 | 'w': ThingType.WEBPAGE 55 | }; 56 | 57 | export function toDreamsThingType(id: string) { 58 | return IdTypeMap[id.charAt(0)]; 59 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | debug 3 | apiconfig.json 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | -------------------------------------------------------------------------------- /src/dreamsClient.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse } from 'axios'; 2 | import crypto from 'crypto'; 3 | import { getConfigValue, setConfigValue } from './config'; 4 | 5 | const baseUrl = 'https://indreams.me'; 6 | const hmacSecret = 'gUkXp2s5v8y/B?E(H+MbQeThWmYq3t6w'; 7 | 8 | export interface CookieStringObject { [key: string]: undefined | string | number | boolean}; 9 | export interface QueryStringObject { [key: string]: undefined | string | number | string[] | number[] }; 10 | 11 | function stringifyCookie(cookie: CookieStringObject) { 12 | if (!cookie) 13 | return ''; 14 | return Object.entries(cookie).map(([key, value]) => { 15 | if (value === true) 16 | return key; 17 | return `${ key }=${ value }`; 18 | }).join('; '); 19 | } 20 | 21 | function parseCookieString(cookieString: string) { 22 | return cookieString.split('; ').reduce((cookieObject, item) => { 23 | const [key, value] = item.split(/\=(.+)/); 24 | cookieObject[key] = value !== undefined ? value : true; 25 | return cookieObject; 26 | }, >{}); 27 | } 28 | 29 | function parseSetCookie(setCookie: string[]) { 30 | const cookieParts = setCookie.map((part: string) => parseCookieString(part)); 31 | return Object.assign.apply(null, [{}, ...cookieParts]); 32 | } 33 | 34 | function stringifyQuery(query: QueryStringObject) { 35 | if (!query) 36 | return ''; 37 | const queryArray = Object.entries(query).map(([key, value]) => { 38 | return `${ key }=${ encodeURIComponent(Array.isArray(value) ? value.join('+') : value) }`; 39 | }); 40 | return queryArray.length > 0 ? `?${queryArray.join('&')}` : ''; 41 | } 42 | 43 | function parseJsonWebToken(token: string) { 44 | const parts = token.split('.').map(part => Buffer.from(part, 'base64').toString('ascii')); 45 | return { 46 | header: JSON.parse(parts[0]), 47 | payload: JSON.parse(parts[1]), 48 | signature: parts[2] 49 | } 50 | } 51 | 52 | function updateCookieFromResponse(response: AxiosResponse) { 53 | const headers = response.headers; 54 | if (headers['set-cookie']) { 55 | const newCookie = parseSetCookie(headers['set-cookie']); 56 | const oldCookie = getConfigValue('Cookie'); 57 | setConfigValue('Cookie', { 58 | ...oldCookie, 59 | ...newCookie, 60 | }); 61 | } 62 | } 63 | 64 | function authHeaders(path: string, body: string) { 65 | const timestamp = Math.round((new Date).getTime() / 1000); 66 | const hmac = crypto.createHmac('sha256', hmacSecret); 67 | if (body) { 68 | hmac.update(body); 69 | } 70 | hmac.update(path); 71 | hmac.update(timestamp.toString()); 72 | return { 73 | 'X-Auth': hmac.digest('hex'), 74 | 'X-Ts': timestamp 75 | } 76 | } 77 | 78 | function jwtHeaders() { 79 | const cookie = getConfigValue('Cookie'); 80 | return { 81 | 'Cookie': stringifyCookie(cookie) 82 | } 83 | } 84 | 85 | export async function requestGet(endpoint: string, queryObject: QueryStringObject) { 86 | const path = endpoint + stringifyQuery(queryObject); 87 | return axios.get(`${ baseUrl }${ path }`, { 88 | headers: { 89 | ...jwtHeaders(), 90 | ...authHeaders(path, null) 91 | } 92 | }) 93 | .then(response => { 94 | updateCookieFromResponse(response); 95 | return response; 96 | }) 97 | .catch(error => error.response); 98 | } 99 | 100 | export async function requestPost(path: string, body: any) { 101 | return axios.post(`${ baseUrl }${ path }`, body, { 102 | headers: { 103 | ...jwtHeaders(), 104 | ...authHeaders(path, JSON.stringify(body)) 105 | } 106 | }) 107 | .then(response => { 108 | updateCookieFromResponse(response); 109 | return response; 110 | }) 111 | .catch(error => error.response); 112 | } 113 | 114 | export async function requestPostAudio(path: string, filename: string, body: any) { 115 | const safeFilename = encodeURIComponent(filename); 116 | return axios.post(`${ baseUrl }${ path }`, body, { 117 | headers: { 118 | ...jwtHeaders(), 119 | ...authHeaders(path, safeFilename), 120 | 'X-Filename': safeFilename 121 | } 122 | }) 123 | .then(response => { 124 | updateCookieFromResponse(response); 125 | return response; 126 | }) 127 | .catch(error => error.response) 128 | } --------------------------------------------------------------------------------