Final set of CSRFP -> CSRF renames, use headers instead of POST fields and build CSRF handling into the XHR wrapper.
This commit is contained in:
parent
2e9adf0d06
commit
bc2d7a08d0
12 changed files with 157 additions and 191 deletions
24
assets/misuzu.js/csrf.js
Normal file
24
assets/misuzu.js/csrf.js
Normal file
|
@ -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;
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -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'));
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -1,4 +1,4 @@
|
|||
#include utility.js
|
||||
#include xhr.js
|
||||
|
||||
const MszUiharu = function(apiUrl) {
|
||||
const maxBatchSize = 4;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include msgbox.jsx
|
||||
#include parsing.js
|
||||
#include utility.js
|
||||
#include xhr.js
|
||||
#include ext/eeprom.js
|
||||
|
||||
let MszForumEditorAllowClose = false;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include utility.js
|
||||
#include xhr.js
|
||||
#include embed/embed.js
|
||||
#include events/christmas2019.js
|
||||
#include events/events.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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 || '';
|
||||
|
|
116
assets/misuzu.js/xhr.js
Normal file
116
assets/misuzu.js/xhr.js
Normal file
|
@ -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),
|
||||
};
|
||||
})();
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
{% include '_layout/meta.twig' %}
|
||||
<meta name="csrfp-token" content="{{ csrf_token() }}">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<link href="/vendor/fontawesome/css/all.min.css" type="text/css" rel="stylesheet">
|
||||
<link href="{{ asset('misuzu.css') }}" type="text/css" rel="stylesheet">
|
||||
{% if site_background is defined %}
|
||||
|
|
Reference in a new issue