1import * as crypto from 'crypto'; 2import express from 'express'; 3import FormData from 'form-data'; 4import * as fs from 'fs'; 5import * as path from 'path'; 6import { serializeDictionary } from 'structured-headers'; 7import { setTimeout } from 'timers/promises'; 8 9const app = express(); 10 11let server: { close: () => void } | null; 12 13let messages: any[] = []; 14let responsesToServe: any[] = []; 15 16let updateRequest: null = null; 17 18let manifestToServe: null = null; 19let manifestHeadersToServe: { [x: string]: any } | null = null; 20 21let multipartResponseToServe: any = null; 22let requestedStaticFiles: string[] = []; 23 24let protocolVersion: number = 1; 25 26function start(port: any, protocol: number = 1) { 27 if (!server) { 28 server = app.listen(port); 29 protocolVersion = protocol; 30 } 31} 32 33function stop() { 34 if (server) { 35 server.close(); 36 server = null; 37 } 38 messages = []; 39 responsesToServe = []; 40 updateRequest = null; 41 manifestToServe = null; 42 manifestHeadersToServe = null; 43 multipartResponseToServe = null; 44 requestedStaticFiles = []; 45} 46 47function consumeRequestedStaticFiles() { 48 const returnArray = requestedStaticFiles; 49 requestedStaticFiles = []; 50 return returnArray; 51} 52 53app.use(express.json()); 54app.use('/static', (req: { url: string }, res: any, next: () => void) => { 55 requestedStaticFiles.push(path.basename(req.url)); 56 next(); 57}); 58app.use('/static', express.static(path.resolve(__dirname, '..', '.static'))); 59 60app.get( 61 '/notify/:string', 62 ( 63 req: { params: { string: any } }, 64 res: { 65 set: (arg0: string, arg1: string) => void; 66 json: (arg0: any) => void; 67 send: (arg0: string) => void; 68 } 69 ) => { 70 messages.push(req.params.string); 71 res.set('Cache-Control', 'no-store'); 72 if (responsesToServe[0]) { 73 res.json(responsesToServe.shift()); 74 } else { 75 res.send('Received request'); 76 } 77 } 78); 79 80app.post( 81 '/post', 82 ( 83 req: { body: any }, 84 res: { 85 set: (arg0: string, arg1: string) => void; 86 json: (arg0: any) => void; 87 send: (arg0: string) => void; 88 } 89 ) => { 90 messages.push(req.body); 91 res.set('Cache-Control', 'no-store'); 92 if (responsesToServe[0]) { 93 res.json(responsesToServe.shift()); 94 } else { 95 res.send('Received request'); 96 } 97 } 98); 99 100app.get('/update', (req: any, res: any) => { 101 updateRequest = req; 102 if (multipartResponseToServe) { 103 // Protocol 1: multipart and rollbacks supported 104 const form = new FormData(); 105 106 if (multipartResponseToServe.manifest) { 107 form.append('manifest', JSON.stringify(multipartResponseToServe.manifest), { 108 contentType: 'application/json', 109 header: { 110 'content-type': 'application/json; charset=utf-8', 111 'expo-signature': multipartResponseToServe.manifestSignature, 112 }, 113 }); 114 } 115 116 if (multipartResponseToServe.directive) { 117 form.append('directive', JSON.stringify(multipartResponseToServe.directive), { 118 contentType: 'application/json', 119 header: { 120 'content-type': 'application/json; charset=utf-8', 121 'expo-signature': multipartResponseToServe.directiveSignature, 122 }, 123 }); 124 } 125 126 res.statusCode = 200; 127 res.setHeader('expo-protocol-version', 1); 128 res.setHeader('expo-sfv-version', 0); 129 res.setHeader('cache-control', 'private, max-age=0'); 130 res.setHeader('content-type', `multipart/mixed; boundary=${form.getBoundary()}`); 131 res.write(form.getBuffer()); 132 res.end(); 133 } else { 134 // Protocol 0 135 if (manifestToServe) { 136 if (manifestHeadersToServe) { 137 Object.keys(manifestHeadersToServe).forEach((headerName) => { 138 res.set(headerName, manifestHeadersToServe ? manifestHeadersToServe[headerName] : ''); 139 }); 140 } 141 res.json(manifestToServe); 142 } else { 143 res.status(404).send('No update available'); 144 } 145 res.status(404).send('No update available'); 146 } 147}); 148 149async function waitForUpdateRequest(timeout: number): Promise<{ headers: any }> { 150 const finishTime = new Date().getTime() + timeout; 151 while (!updateRequest && server) { 152 const currentTime = new Date().getTime(); 153 if (currentTime >= finishTime) { 154 throw new Error('Timed out waiting for update request'); 155 } 156 if (!server) { 157 throw new Error('Server killed while waiting for update'); 158 } 159 await setTimeout(50); 160 } 161 162 const request: { headers: any } = updateRequest || { headers: {} }; 163 updateRequest = null; 164 return request; 165} 166 167async function getPrivateKeyAsync(projectRoot: string) { 168 const codeSigningPrivateKeyPath = path.join(projectRoot, 'keys', 'private-key.pem'); 169 const pemBuffer = fs.readFileSync(path.resolve(codeSigningPrivateKeyPath)); 170 return pemBuffer.toString('utf8'); 171} 172 173function signRSASHA256( 174 data: string, 175 privateKey: crypto.KeyLike | crypto.SignKeyObjectInput | crypto.SignPrivateKeyInput 176) { 177 const sign = crypto.createSign('RSA-SHA256'); 178 sign.update(data, 'utf8'); 179 sign.end(); 180 return sign.sign(privateKey, 'base64'); 181} 182 183function convertToDictionaryItemsRepresentation( 184 obj: { [s: string]: unknown } | ArrayLike<unknown> 185): any { 186 return new Map( 187 Object.entries(obj).map(([k, v]) => { 188 return [k, [v, new Map()]]; 189 }) 190 ); 191} 192 193function serveManifest(manifest: any, headers: any = null) { 194 manifestToServe = manifest; 195 manifestHeadersToServe = headers; 196} 197 198async function serveSignedManifest(manifest: any, projectRoot: any) { 199 if (protocolVersion === 0) { 200 serveSignedManifest0(manifest, projectRoot); 201 } else { 202 serveSignedManifest1(manifest, projectRoot); 203 } 204} 205 206// Protocol 0 207async function serveSignedManifest0(manifest: any, projectRoot: any) { 208 const privateKey = await getPrivateKeyAsync(projectRoot); 209 const manifestString = JSON.stringify(manifest); 210 const hashSignature = signRSASHA256(manifestString, privateKey); 211 const dictionary = convertToDictionaryItemsRepresentation({ 212 sig: hashSignature, 213 keyid: 'main', 214 }); 215 const signature = serializeDictionary(dictionary); 216 serveManifest(manifest, { 'expo-protocol-version': '0', 'expo-signature': signature }); 217} 218 219// Protocol 1 multipart response 220async function serveSignedManifest1(manifest: any, projectRoot: any) { 221 const privateKey = await getPrivateKeyAsync(projectRoot); 222 const manifestString = JSON.stringify(manifest); 223 const hashSignature = signRSASHA256(manifestString, privateKey); 224 const dictionary = convertToDictionaryItemsRepresentation({ 225 sig: hashSignature, 226 keyid: 'main', 227 }); 228 const signature = serializeDictionary(dictionary); 229 multipartResponseToServe = { 230 manifest, 231 manifestSignature: signature, 232 }; 233} 234 235// Protocol 1 directive response (including rollback) 236async function serveSignedDirective(directive: any, projectRoot: string) { 237 const privateKey = await getPrivateKeyAsync(projectRoot); 238 const directiveString = JSON.stringify(directive); 239 const hashSignature = signRSASHA256(directiveString, privateKey); 240 const dictionary = convertToDictionaryItemsRepresentation({ 241 sig: hashSignature, 242 keyid: 'main', 243 }); 244 const signature = serializeDictionary(dictionary); 245 multipartResponseToServe = { 246 directive, 247 directiveSignature: signature, 248 }; 249} 250 251const Server = { 252 start, 253 stop, 254 waitForUpdateRequest, 255 serveManifest, 256 serveSignedManifest, 257 serveSignedDirective, 258 consumeRequestedStaticFiles, 259}; 260 261export default Server; 262