Обмен сообщениями перекрестного документа
Как правило сценарии, загруженные веб-контентом, подаваемым от одного источника (узел и домен), не могут получить доступ к веб-контенту, подаваемому различным источником. Это - важное средство защиты, предотвращающее множество различных векторов атаки безопасности. Однако это также мешает сценариям взаимодействовать друг с другом через эти границы.
Для создания коммуникации между документами от различных источников проще спецификация HTML 5 добавляет обмен сообщениями перекрестного документа. Эта функция поддерживается в Safari 4.0 и позже.
Добавление сообщения к окну
Для добавления сообщения необходимо сначала получить Window
объект документа Вы хотите обмениваться сообщениями. В действительности это означает, что можно добавить сообщения только к:
другие кадры или встроенные рамки в Вашем окне документа (или их потомки, если все промежуточные кадры или встроенные рамки подавались от того же источника).
var iFrameObj = document.getElementById('myId');
var windowObj = iFrameObj.contentWindow;
окна, которые Ваш документ явно открыл через вызовы JavaScript.
var windowObj = window.open(...);
окно, содержащее Ваше окно документа, окно, содержащее то окно, и т.д. до корневого окна.
var windowObj = window.parent;
окно, открывшее Ваш документ.
var windowObj = window.opener;
Как только Вы получили Window
объект для целевого документа, можно отправить ему сообщение со следующим кодом:
windowObj.postMessage('test message', 'http://example.com'); |
Первый параметр является произвольным сообщением.
Второй параметр является целевым значением источника. Значением источника является просто URL с удаленной частью пути. Например, источник локального файла file://
. Путем указания целевого источника Вы говорите, что то Ваше сообщение должно только быть передано, если бы текущее содержание целевого окна прибыло из того источника.
Несмотря на то, что можно указать звездочку (*
) подстановочный знак для целевого источника (чтобы позволить сообщению, которое будет отправлено независимо от того, куда содержание целевого окна прибыло из), необходимо сделать так, только если Вы уверены, что не было бы вредно, если бы Ваше сообщение было получено довольным возникновение из различного веб-сайта.
Получение сообщения, добавленного к окну
Для получения сообщений необходимо добавить слушателя события для message
тип события к Вашему документу window
объект. Чтобы сделать это, используйте следующий код:
function messageReceive(evt) { |
if (evt.origin == 'http://example.com') { |
// The message came from an origin that |
// your code trusts. Work with the message. |
alert('Received data '+evt.data); |
// Send a message back to the source: |
evt.source.postMessage('response', evt.origin); |
} else { |
alert('unexpected message from origin '+evt.origin); |
} |
} |
window.addEventListener('message', messageReceive, false); |
message
событие, которое Вы получаете, имеет три свойства интереса:
data
— содержимое сообщения.origin
— домен, от которого было отправлено сообщение (http://example.com
в этом случае).source
— окно, от которого было отправлено сообщение.
Пример открытия службы: окна сообщения
Этот раздел содержит два листинга кода: index.html
и msg_contents.html
это, когда объединено, реализует открытие базовой услуги и передачу данных поверх перекрестного документа, передающего архитектуру.
Для использования этого кода необходимо сначала сделать следующие вещи:
Safari 4.0 установки или позже, или установка недавний WebKit ночью.
Сохраните содержание этих двух листингов кода в отдельных файлах.
В файле
msg_contents.html
, измените переменнуюallowed_origins
содержать список источников, от которых Вы планируете служитьindex.html
илиmsg_contents.html
файл.Например, если Вы намереваетесь поместить этот файл в
http://www.example.org/message_test/msg_contents.html
, необходимо удостовериться это'http://www.example.org'
(включенный в кавычки), ключallowed_origins
объект.Если Вы только имеете одну машину, включаете веб-совместное использование, то используйте
http://localhost
как один источник иfile://
как другой.В файле
msg_contents.html
, дополнительно замените переменнуюroot_origin
к фактическому ожидаемому источнику высокоуровневой страницы HTML.Поместите копию измененного
msg_contents.html
файл на желаемом сервере или серверах.В файле
index.html
, заменитеallowed_origins
объявление с тем от Вашегоmsg_contents.html
файл. Затем обновитеboxes
объект обеспечить URLs дляmsg_contents.html
регистрирует Вы просто ставите свои серверы.Если у Вас только есть одна машина, используйте a
file://
URL, указывающий на путьmsg_contents.html
файл. Например, если Вы поместили файл в/Library/WebServer/Documents/message_test/msg_contents.html
, локальный URL был быfile:///Library/WebServer/Documents/message_test/msg_contents.html
.Поместите это изменило
index.html
файл на одном из серверов и перешел к URL или открывает его как локальный файл на диске.
Как только Вы завершили эти шаги, необходимо видеть несколько полей, один на запись в boxes
объект. Каждое из этих полей должно содержать маленькую форму с полем ввода текста, серией флажков (один для каждого из внешних полей), и кнопка отправки.
При вводе чего-то в текстовое поле проверьте один из флажков, и щелчок подчиняется, текст должен появиться у основания окна, имя которого соответствует флажку.
Перекрестный документ перечисления 1, передающий пример: index.html
<html><head> |
<script language='javascript' type='text/javascript'><!-- |
/*global alert, navigator, document, window */ |
var window_list = []; |
var origin_list = []; |
var boxes = { |
local: "http://host1.domain1.top/messages2/msg_contents.html", |
remote: "http://host2.domain2.top/messages2/msg_contents.html", |
third: "http://host3.domain3.top/messages2/msg_contents.html" |
}; |
var allowed_origins = { |
'http://host1.domain1.top': 1, |
'http://host2.domain2.top': 1, |
'http://host3.domain3.top': 1 |
}; |
function smartsplit(string, pattern, count) |
{ |
// alert('string '+string); |
// alert('pattern "'+pattern+'"'); |
// alert('count '+count); |
var lastpos = count - 1; |
var arr = string.split(/ /); |
// alert('AC: '+arr.length+" "+string); |
if (arr.length > lastpos) { |
var temparr = []; |
for (var i=lastpos; i<arr.length; i++) { |
temparr[i-lastpos] = arr[i]; |
arr[i] = undefined; |
} |
arr[lastpos] = temparr.join(pattern); |
} |
return arr; |
} |
function listWindows() |
{ |
var retstring = ""; |
for (var i in origin_list) { |
if (origin_list.hasOwnProperty(i)) { |
// alert('UUID: '+i+' Origin: '+origin_list[i]); |
retstring += i+' '+origin_list[i]+'\n'; |
} |
} |
return retstring; |
} |
function messageReceive(evt) { |
var windowlist; |
if (evt.origin === null || evt.origin in allowed_origins) { |
var arr = smartsplit(evt.data, " ", 2); |
if (arr[0] == 'sendto') { |
// usage: sendto UUID remote_origin message |
arr = smartsplit(evt.data, " ", 4); |
var remote_window = window_list[arr[1]]; |
remote_window.postMessage('sendto_output '+arr[3], arr[2]); |
} else if (arr[0] == 'register') { |
// usage register UUID |
var name = arr[1]; |
if (window_list[name]) { |
// name conflict. |
var add=1; |
while (window_list[name+'_'+add]) { |
add++; |
} |
name = name+'_'+add; |
evt.source.postMessage("register_newid "+name, evt.origin); |
} |
window_list[name] = evt.source; |
origin_list[name] = evt.origin; |
windowlist = listWindows(); |
for (var windowid in window_list) { |
if (window_list.hasOwnProperty(windowid)) { |
// alert('windowid: '+windowid); |
window_list[windowid].postMessage("list_output "+windowlist, origin_list[windowid]); |
} |
} |
} else if (arr[0] == 'list') { |
// usage list |
windowlist = listWindows(); |
evt.source.postMessage("list_output "+windowlist, evt.origin); |
} else { |
alert('unknown command '+arr[0]); |
} |
} else { |
alert('unexpected message from origin '+evt.origin); |
} |
} |
function dosetup() |
{ |
if (navigator.userAgent.match(/Safari/)) { |
var version = parseFloat(navigator.userAgent.replace(/.*AppleWebKit\//, "").replace(/[^0-9.].*$/, "")); |
if (version < 528) { |
alert('WebKit version '+version+' does not support this application.'); |
} |
} |
// for (var i=0; i<nchannels; i++) { |
// alert('setup channel '+i); |
// channel[i] = new MessageChannel(); |
// } |
window.addEventListener('message', messageReceive, false); |
var boxstr = ""; |
for (var i in boxes) { |
if (boxes.hasOwnProperty(i)) { |
boxstr += "<iframe height='600' id='"+i+"' src='"+boxes[i]+"'></iframe>\n"; |
} |
} |
var boxlistdiv = document.getElementById('boxlist'); |
boxlistdiv.innerHTML = boxstr; |
} |
dosetup(); |
--></script> |
</head> |
<body onload='dosetup();'> |
<div id='boxlist'> |
</div> |
</body> |
</html> |
Перекрестный документ перечисления 2, передающий пример: message_contents.html
<html><head> |
<script language='javascript' type='text/javascript'><!-- |
/*global alert, document, window, navigator */ |
var allowed_origins = { |
'http://stavromula-beta.apple.com': 1, |
'http://holst.apple.com': 1 |
}; |
var received_root_origin = ''; |
var root_origin = '*'; |
function smartsplit(string, pattern, count) |
{ |
// alert('string '+string); |
// alert('pattern "'+pattern+'"'); |
// alert('count '+count); |
var lastpos = count - 1; |
var arr = string.split(/ /); |
// alert('AC: '+arr.length+" "+string); |
if (arr.length > lastpos) { |
var temparr = []; |
for (var i=lastpos; i<arr.length; i++) { |
temparr[i-lastpos] = arr[i]; |
arr[i] = undefined; |
} |
arr[lastpos] = temparr.join(pattern); } |
return arr; |
} |
function mkcheckbox(inpstr) |
{ |
var str = ""; |
var arr = inpstr.split("\n"); |
for (var entid in arr) { |
if (arr.hasOwnProperty(entid)) { |
var ent = arr[entid]; |
if (ent !== "") { |
// alert('ent: '+ent); |
var bits = smartsplit(ent, " ", 2); |
str += "<input type=checkbox name='"+ent+"'>"+bits[0]+"</input>\n"; |
} |
} |
} |
return str; |
} |
function messageReceive(evt) { |
if (evt.origin === null || evt.origin in allowed_origins) { |
// The message came from an origin that |
// your code trusts. Work with the message. |
// alert('Received data: '+evt.data); |
// var tmp = 'Test this, please'; |
// var x = smartsplit(tmp, " ", 2); |
// alert('x[0] = '+x[0]); |
// alert('x[1] = '+x[1]); |
// alert('x[2] = '+x[2]); |
var arr = smartsplit(evt.data, " ", 2); |
if (arr[0] == 'list_output') { |
if (evt.origin == root_origin || root_origin == '*') { |
var div2 = document.getElementById('temp2'); |
div2.innerHTML = mkcheckbox(arr[1]); |
received_root_origin = evt.origin; |
// alert('arr[1] = '+arr[1]); |
} else { |
alert('received list_output message from unexpected origin: '+evt.origin); |
} |
} else if (arr[0] == 'sendto_output') { |
// alert('Received data: '+evt.data); |
var div3 = document.getElementById('temp3'); |
div3.innerHTML += arr[1]+'<br />\n'; |
} else if (arr[0] == 'register_newid') { |
var mydiv = document.getElementById('temp'); |
mydiv.innerHTML = arr[1]+" box"; |
} |
// Send a message back to the source: |
// evt.source.postMessage('response', evt.origin); |
} else { |
alert('unexpected message from origin '+evt.origin); |
} |
} |
function setup_listener() |
{ |
window.addEventListener('message', messageReceive, false); |
} |
function setup() |
{ |
var mydiv = document.getElementById('temp'); |
// mydiv.innerHTML = 'Test'; |
// mydiv.innerHTML = ' '+bigdoc; |
// If we can access the document object, we are locally loaded and should |
// talk to the remotely-loaded window. Otherwise, the reverse |
// is true. |
var myid = ''; |
if (window.parent.document) { |
myid = 'local'; |
} else { |
myid = 'remote'; |
} |
mydiv.innerHTML = myid+" box"; |
// window.addEventListener('message', messageReceive, false); |
setup_listener(); |
var topwindow = window; |
while (topwindow.parent && (topwindow.parent != topwindow)) { topwindow = topwindow.parent; } |
topwindow.postMessage('register '+myid, root_origin); |
// window.parent.postMessage('list', '*'); |
} |
function sendmsg() |
{ |
var message = document.getElementById('sendtext').value; |
var mydiv2 = document.getElementById('temp2'); |
var checkboxes = mydiv2.children; |
// alert('checkboxes: '+checkboxes); |
var topwindow = window; |
while (topwindow.parent && (topwindow.parent != topwindow)) { topwindow = topwindow.parent; } |
for (var boxid in checkboxes) { |
if (checkboxes.hasOwnProperty(boxid)) { |
var checkbox = checkboxes[boxid]; |
// alert('checkbox: '+checkbox); |
if (checkbox.tagName == "INPUT") { |
// alert('input'); |
if (checkbox.checked) { |
// alert('checked'); |
var arr = smartsplit(checkbox.name, " ", 2); |
var uuid = arr[0]; |
var origin = arr[1]; |
// alert('send to: '+uuid+' origin '+origin); |
// alert('topwindow is '+topwindow); |
topwindow.postMessage('sendto '+uuid+' '+origin+' '+message, received_root_origin); |
} |
} |
} |
} |
return false; |
} |
--></script> |
</head><body onload='setup();'> |
<div id='temp'></div> |
<form onsubmit='return false;'> |
<input type='text' id='sendtext'></input> |
<input type='submit' value='submit' onclick='sendmsg();'></input> |
<div id='temp2'></div> |
</form> |
<div id='temp3'></div> |
</body></html> |
Можно создать многочисленные интересные расширения поверх этого вида проекта. Например, Вы могли бы добавить сообщение команды, спрашивающее окно в другом конце, какие команды это поддерживает, затем свяжитесь с ним при совместном использовании единого набора команд. Возможности безграничны.
Соображения безопасности
Существует несколько ключевых вещей, о которых необходимо знать при использовании обмена сообщениями перекрестного документа:
Получение
Window
объекты для других окон не всегда просты. Сценарии, работающие в окне, кадре, илиiframe
элемент, подаваемый от одного источника, не может получить доступ к дереву DOM документов, врученных от различного источника, и таким образом не может получить доступ кWindow
объекты другогоiframe
элементы в таком окне.Среди прочего это означает это два
iframe
элементы от различных доменов не могут непосредственно получить друг другаWindow
объекты, потому что один или другой (по определению) от различного источника, чем документ, содержащийiframe
элементы. В этой ситуации существует два возможных решения.Самое простое решение состоит в том, чтобы иметь оба окна, обнаруживают друг друга использующего родительское окно в качестве коммуникационного концентратора. Этим решением является также самое общее решение, потому что это работает, даже если никакое окно не видит, что элемент содержит другое окно.
Также окно с доступом к дереву DOM родительского окна могло обнаружить другое и инициировать коммуникацию. Путем выполнения так, второе окно получает первое окно
Window
объект посредствомsource
поле в конечном счете возражает.Если та информация содержит информацию о входе в систему или другие уязвимые данные, отправка данных к другим окнам может быть опасной, особенно. Необходимо почти всегда использовать в своих интересах целевое поле источника при отправке сообщений для предотвращения перехвата содержанием, подаваемым другими сайтами. Необходимо только использовать подстановочную звездочку (
*
) целевое значение источника, если Вы абсолютно уверены, что данные безопасны.Получение данных из других окон может также быть опасным. Необходимо обычно проверять поле источника при получении сообщений, чтобы удостовериться, что данные были отправлены от веб-сайта, которому Вы доверяете до некоторой степени.
Где возможно, необходимо также проверить любые полученные данные на законность перед использованием его. В частности необходимо обычно избегать выполнять код JavaScript, полученный от другого окна (за возможным исключением объектов JSON после тщательной проверки законности).
Как с любым программным обеспечением, для максимальной надежности и безопасности, необходимо записать выходной код тщательно для минимизации риска порождения проблем для другого кода, и необходимо записать код под предположением, что другой код злонамеренно пытается атаковать код, и таким образом необходимо выполнить тип, границы и другие проверки работоспособности соответственно.