diff --git a/assets/misuzu.js/csrf.js b/assets/misuzu.js/csrf.js new file mode 100644 index 0000000..7df0610 --- /dev/null +++ b/assets/misuzu.js/csrf.js @@ -0,0 +1,24 @@ +#include utility.js + +const MszCSRF = (() => { + let elem; + const getElement = () => { + if(elem === undefined) + elem = $q('meta[name="csrf-token"]'); + return elem; + }; + + return { + get token() { + return getElement()?.content ?? ''; + }, + set token(token) { + if(typeof token !== 'string') + throw 'token must be a string'; + + const elem = getElement(); + if(elem instanceof HTMLMetaElement) + elem.content = token; + }, + }; +})(); diff --git a/assets/misuzu.js/csrfp.js b/assets/misuzu.js/csrfp.js deleted file mode 100644 index 86b0991..0000000 --- a/assets/misuzu.js/csrfp.js +++ /dev/null @@ -1,40 +0,0 @@ -#include utility.js - -const MszCSRFP = (() => { - let elem; - const getElement = () => { - if(elem === undefined) - elem = $q('meta[name="csrfp-token"]'); - return elem; - }; - - const getToken = () => { - const elem = getElement(); - return typeof elem.content === 'string' ? elem.content : ''; - }; - - const setToken = token => { - if(typeof token !== 'string') - throw 'token must be a string'; - - const elem = getElement(); - if(typeof elem.content === 'string') - elem.content = token; - }; - - return { - getToken: getToken, - setToken: setToken, - setFromHeaders: result => { - if(typeof result.headers !== 'function') - throw 'result.headers is not a function'; - - const headers = result.headers(); - if(!(headers instanceof Map)) - throw 'result of result.headers does not return a map'; - - if(headers.has('x-csrfp-token')) - setToken(headers.get('x-csrfp-token')); - }, - }; -})(); diff --git a/assets/misuzu.js/ext/uiharu.js b/assets/misuzu.js/ext/uiharu.js index 6c0299d..b288a8d 100644 --- a/assets/misuzu.js/ext/uiharu.js +++ b/assets/misuzu.js/ext/uiharu.js @@ -1,4 +1,4 @@ -#include utility.js +#include xhr.js const MszUiharu = function(apiUrl) { const maxBatchSize = 4; diff --git a/assets/misuzu.js/forum/editor.jsx b/assets/misuzu.js/forum/editor.jsx index 1e8432b..c54c5a2 100644 --- a/assets/misuzu.js/forum/editor.jsx +++ b/assets/misuzu.js/forum/editor.jsx @@ -1,6 +1,7 @@ #include msgbox.jsx #include parsing.js #include utility.js +#include xhr.js #include ext/eeprom.js let MszForumEditorAllowClose = false; diff --git a/assets/misuzu.js/main.js b/assets/misuzu.js/main.js index ba1774b..883b976 100644 --- a/assets/misuzu.js/main.js +++ b/assets/misuzu.js/main.js @@ -1,4 +1,5 @@ #include utility.js +#include xhr.js #include embed/embed.js #include events/christmas2019.js #include events/events.js diff --git a/assets/misuzu.js/messages/messages.js b/assets/misuzu.js/messages/messages.js index c43084a..74ff985 100644 --- a/assets/misuzu.js/messages/messages.js +++ b/assets/misuzu.js/messages/messages.js @@ -1,6 +1,6 @@ -#include csrfp.js #include msgbox.jsx #include utility.js +#include xhr.js #include messages/actbtn.js #include messages/list.js #include messages/recipient.js @@ -33,7 +33,6 @@ const MszMessages = () => { const msgsCreate = async (title, text, parser, draft, recipient, replyTo) => { const formData = new FormData; - formData.append('_csrfp', MszCSRFP.getToken()); formData.append('title', title); formData.append('body', text); formData.append('parser', parser); @@ -41,10 +40,7 @@ const MszMessages = () => { formData.append('recipient', recipient); formData.append('reply', replyTo); - const result = await $x.post('/messages/create', { type: 'json' }, formData); - - MszCSRFP.setFromHeaders(result); - + const result = await $x.post('/messages/create', { type: 'json', csrf: true }, formData); const body = result.body(); if(body.error !== undefined) throw body.error; @@ -54,16 +50,12 @@ const MszMessages = () => { const msgsUpdate = async (messageId, title, text, parser, draft) => { const formData = new FormData; - formData.append('_csrfp', MszCSRFP.getToken()); formData.append('title', title); formData.append('body', text); formData.append('parser', parser); formData.append('draft', draft); - const result = await $x.post(`/messages/${encodeURIComponent(messageId)}`, { type: 'json' }, formData); - - MszCSRFP.setFromHeaders(result); - + const result = await $x.post(`/messages/${encodeURIComponent(messageId)}`, { type: 'json', csrf: true }, formData); const body = result.body(); if(body.error !== undefined) throw body.error; @@ -72,14 +64,10 @@ const MszMessages = () => { }; const msgsMark = async (msgs, state) => { - const result = await $x.post('/messages/mark', { type: 'json' }, { - _csrfp: MszCSRFP.getToken(), + const result = await $x.post('/messages/mark', { type: 'json', csrf: true }, { type: state, messages: msgs.map(extractMsgIds).join(','), }); - - MszCSRFP.setFromHeaders(result); - const body = result.body(); if(body.error !== undefined) throw body.error; @@ -88,13 +76,9 @@ const MszMessages = () => { }; const msgsDelete = async msgs => { - const result = await $x.post('/messages/delete', { type: 'json' }, { - _csrfp: MszCSRFP.getToken(), + const result = await $x.post('/messages/delete', { type: 'json', csrf: true }, { messages: msgs.map(extractMsgIds).join(','), }); - - MszCSRFP.setFromHeaders(result); - const body = result.body(); if(body.error !== undefined) throw body.error; @@ -103,13 +87,9 @@ const MszMessages = () => { }; const msgsRestore = async msgs => { - const result = await $x.post('/messages/restore', { type: 'json' }, { - _csrfp: MszCSRFP.getToken(), + const result = await $x.post('/messages/restore', { type: 'json', csrf: true }, { messages: msgs.map(extractMsgIds).join(','), }); - - MszCSRFP.setFromHeaders(result); - const body = result.body(); if(body.error !== undefined) throw body.error; @@ -118,13 +98,10 @@ const MszMessages = () => { }; const msgsNuke = async msgs => { - const result = await $x.post('/messages/nuke', { type: 'json' }, { - _csrfp: MszCSRFP.getToken(), + const result = await $x.post('/messages/nuke', { type: 'json', csrf: true }, { messages: msgs.map(extractMsgIds).join(','), }); - MszCSRFP.setFromHeaders(result); - const body = result.body(); if(body.error !== undefined) throw body.error; diff --git a/assets/misuzu.js/messages/recipient.js b/assets/misuzu.js/messages/recipient.js index 22374ad..ea0ca20 100644 --- a/assets/misuzu.js/messages/recipient.js +++ b/assets/misuzu.js/messages/recipient.js @@ -1,5 +1,4 @@ -#include csrfp.js -#include utility.js +#include xhr.js const MszMessagesRecipient = function(element) { if(!(element instanceof Element)) @@ -10,13 +9,9 @@ const MszMessagesRecipient = function(element) { let updateHandler = undefined; const update = async () => { - const result = await $x.post(element.dataset.msgLookup, { type: 'json' }, { - _csrfp: MszCSRFP.getToken(), + const result = await $x.post(element.dataset.msgLookup, { type: 'json', csrf: true }, { name: nameInput.value, }); - - MszCSRFP.setFromHeaders(result); - const body = result.body(); if(updateHandler !== undefined) diff --git a/assets/misuzu.js/utility.js b/assets/misuzu.js/utility.js index 71c0037..cb845f2 100644 --- a/assets/misuzu.js/utility.js +++ b/assets/misuzu.js/utility.js @@ -162,114 +162,6 @@ const $as = function(array) { } }; -const $x = (function() { - const send = function(method, url, options, body) { - if(options === undefined) - options = {}; - else if(typeof options !== 'object') - throw 'options must be undefined or an object'; - - const xhr = new XMLHttpRequest; - const requestHeaders = new Map; - - if('headers' in options && typeof options.headers === 'object') - for(const name in options.headers) - if(options.headers.hasOwnProperty(name)) - requestHeaders.set(name.toLowerCase(), options.headers[name]); - - if(typeof options.download === 'function') { - xhr.onloadstart = ev => options.download(ev); - xhr.onprogress = ev => options.download(ev); - xhr.onloadend = ev => options.download(ev); - } - - if(typeof options.upload === 'function') { - xhr.upload.onloadstart = ev => options.upload(ev); - xhr.upload.onprogress = ev => options.upload(ev); - xhr.upload.onloadend = ev => options.upload(ev); - } - - if(options.authed) - xhr.withCredentials = true; - - if(typeof options.timeout === 'number') - xhr.timeout = options.timeout; - - if(typeof options.type === 'string') - xhr.responseType = options.type; - - if(typeof options.abort === 'function') - options.abort(() => xhr.abort()); - - if(typeof options.xhr === 'function') - options.xhr(() => xhr); - - if(typeof body === 'object') { - if(body instanceof URLSearchParams) { - requestHeaders.set('content-type', 'application/x-www-form-urlencoded'); - } else if(body instanceof FormData) { - // content-type is implicitly set - } else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof DataView) { - if(!requestHeaders.has('content-type')) - requestHeaders.set('content-type', 'application/octet-stream'); - } else if(!requestHeaders.has('content-type')) { - const bodyParts = []; - for(const name in body) - if(body.hasOwnProperty(name)) - bodyParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(body[name])); - body = bodyParts.join('&'); - requestHeaders.set('content-type', 'application/x-www-form-urlencoded'); - } - } - - return new Promise((resolve, reject) => { - let responseHeaders = undefined; - - xhr.onload = ev => resolve({ - status: xhr.status, - body: () => xhr.response, - text: () => xhr.responseText, - headers: () => { - if(responseHeaders !== undefined) - return responseHeaders; - - responseHeaders = new Map; - - const raw = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/); - for(const name in raw) - if(raw.hasOwnProperty(name)) { - const parts = raw[name].split(': '); - responseHeaders.set(parts.shift(), parts.join(': ')); - } - - return responseHeaders; - }, - xhr: xhr, - ev: ev, - }); - - xhr.onerror = ev => reject({ - xhr: xhr, - ev: ev, - }); - - xhr.open(method, url); - for(const [name, value] of requestHeaders) - xhr.setRequestHeader(name, value); - xhr.send(body); - }); - }; - - return { - send: send, - get: (url, options, body) => send('GET', url, options, body), - post: (url, options, body) => send('POST', url, options, body), - delete: (url, options, body) => send('DELETE', url, options, body), - patch: (url, options, body) => send('PATCH', url, options, body), - put: (url, options, body) => send('PUT', url, options, body), - }; -})(); - const $insertTags = function(target, tagOpen, tagClose) { tagOpen = tagOpen || ''; tagClose = tagClose || ''; diff --git a/assets/misuzu.js/xhr.js b/assets/misuzu.js/xhr.js new file mode 100644 index 0000000..f59d020 --- /dev/null +++ b/assets/misuzu.js/xhr.js @@ -0,0 +1,116 @@ +#include csrf.js + +const $x = (function() { + const send = function(method, url, options, body) { + if(options === undefined) + options = {}; + else if(typeof options !== 'object') + throw 'options must be undefined or an object'; + + Object.freeze(options); + + const xhr = new XMLHttpRequest; + const requestHeaders = new Map; + + if('headers' in options && typeof options.headers === 'object') + for(const name in options.headers) + if(options.headers.hasOwnProperty(name)) + requestHeaders.set(name.toLowerCase(), options.headers[name]); + + if(options.csrf) + requestHeaders.set('x-csrf-token', MszCSRF.token); + + if(typeof options.download === 'function') { + xhr.onloadstart = ev => options.download(ev); + xhr.onprogress = ev => options.download(ev); + xhr.onloadend = ev => options.download(ev); + } + + if(typeof options.upload === 'function') { + xhr.upload.onloadstart = ev => options.upload(ev); + xhr.upload.onprogress = ev => options.upload(ev); + xhr.upload.onloadend = ev => options.upload(ev); + } + + if(options.authed) + xhr.withCredentials = true; + + if(typeof options.timeout === 'number') + xhr.timeout = options.timeout; + + if(typeof options.type === 'string') + xhr.responseType = options.type; + + if(typeof options.abort === 'function') + options.abort(() => xhr.abort()); + + if(typeof options.xhr === 'function') + options.xhr(() => xhr); + + if(typeof body === 'object') { + if(body instanceof URLSearchParams) { + requestHeaders.set('content-type', 'application/x-www-form-urlencoded'); + } else if(body instanceof FormData) { + // content-type is implicitly set + } else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof DataView) { + if(!requestHeaders.has('content-type')) + requestHeaders.set('content-type', 'application/octet-stream'); + } else if(!requestHeaders.has('content-type')) { + const bodyParts = []; + for(const name in body) + if(body.hasOwnProperty(name)) + bodyParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(body[name])); + body = bodyParts.join('&'); + requestHeaders.set('content-type', 'application/x-www-form-urlencoded'); + } + } + + return new Promise((resolve, reject) => { + xhr.onload = ev => { + const headers = (headersString => { + const headers = new Map; + + const raw = headersString.trim().split(/[\r\n]+/); + for(const name in raw) + if(raw.hasOwnProperty(name)) { + const parts = raw[name].split(': '); + headers.set(parts.shift(), parts.join(': ')); + } + + return headers; + })(xhr.getAllResponseHeaders()); + + if(options.csrf && headers.has('x-csrf-token')) + MszCSRF.token = headers.get('x-csrf-token'); + + resolve({ + status: xhr.status, + body: () => xhr.response, + text: () => xhr.responseText, + headers: () => headers, + xhr: xhr, + ev: ev, + }); + }; + + xhr.onerror = ev => reject({ + xhr: xhr, + ev: ev, + }); + + xhr.open(method, url); + for(const [name, value] of requestHeaders) + xhr.setRequestHeader(name, value); + xhr.send(body); + }); + }; + + return { + send: send, + get: (url, options, body) => send('GET', url, options, body), + post: (url, options, body) => send('POST', url, options, body), + delete: (url, options, body) => send('DELETE', url, options, body), + patch: (url, options, body) => send('PATCH', url, options, body), + put: (url, options, body) => send('PUT', url, options, body), + }; +})(); diff --git a/src/Hanyuu/HanyuuRpcHandler.php b/src/Hanyuu/HanyuuRpcHandler.php index 7942bc5..e735732 100644 --- a/src/Hanyuu/HanyuuRpcHandler.php +++ b/src/Hanyuu/HanyuuRpcHandler.php @@ -160,8 +160,8 @@ final class HanyuuRpcHandler implements RpcHandler { if($userInfo !== $userInfoReal) { $response['guise'] = $extractUserInfo($userInfoReal); - $csrfp = CSRF::create($sessionInfo->token); - $response['guise']['revert_url'] = $baseUrl . $this->urls->format('auth-revert', ['csrf' => $csrfp->createToken()]); + $csrf = CSRF::create($sessionInfo->token); + $response['guise']['revert_url'] = $baseUrl . $this->urls->format('auth-revert', ['csrf' => $csrf->createToken()]); } return self::createPayload('auth:check:success', $response); diff --git a/src/Messages/MessagesRoutes.php b/src/Messages/MessagesRoutes.php index e90630b..8cb1d80 100644 --- a/src/Messages/MessagesRoutes.php +++ b/src/Messages/MessagesRoutes.php @@ -60,7 +60,7 @@ class MessagesRoutes implements RouteHandler, UrlSource { if(!($content instanceof FormHttpContent)) return 400; - if(!$content->hasParam('_csrfp') || !CSRF::validate((string)$content->getParam('_csrfp'))) + if(!CSRF::validate($request->getHeaderLine('x-csrf-token'))) return [ 'error' => [ 'name' => 'msgs:verify', @@ -68,7 +68,7 @@ class MessagesRoutes implements RouteHandler, UrlSource { ], ]; - $response->setHeader('X-CSRFP-Token', CSRF::token()); + $response->setHeader('X-CSRF-Token', CSRF::token()); } } diff --git a/templates/master.twig b/templates/master.twig index 72ffa4b..f55d129 100644 --- a/templates/master.twig +++ b/templates/master.twig @@ -4,7 +4,7 @@ {% include '_layout/meta.twig' %} - + {% if site_background is defined %}